Files
silo/internal/kc/pack_test.go
Forbes cffcf56085 feat(api): item dependency extraction, indexing, and resolve endpoints
- Add Dependency type to internal/kc and extract silo/dependencies.json
  from .kc files on commit
- Create ItemDependencyRepository with ReplaceForRevision, ListByItem,
  and Resolve (LEFT JOIN against items table)
- Add GET /{partNumber}/dependencies and
  GET /{partNumber}/dependencies/resolve endpoints
- Index dependencies in extractKCMetadata with SSE broadcast
- Pack real dependency data into .kc files on checkout
- Update PackInput.Dependencies from []any to []Dependency

Closes #143
2026-02-18 18:53:40 -06:00

230 lines
6.5 KiB
Go

package kc
import (
"archive/zip"
"bytes"
"io"
"testing"
)
func TestHasSiloDir_PlainFCStd(t *testing.T) {
data := buildZip(t, map[string][]byte{
"Document.xml": []byte("<xml/>"),
})
has, err := HasSiloDir(data)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if has {
t.Fatal("expected false for plain .fcstd")
}
}
func TestHasSiloDir_KC(t *testing.T) {
data := buildZip(t, map[string][]byte{
"Document.xml": []byte("<xml/>"),
"silo/manifest.json": []byte("{}"),
})
has, err := HasSiloDir(data)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !has {
t.Fatal("expected true for .kc with silo/ dir")
}
}
func TestHasSiloDir_NotAZip(t *testing.T) {
_, err := HasSiloDir([]byte("not a zip"))
if err == nil {
t.Fatal("expected error for non-ZIP data")
}
}
func TestPack_PlainFCStd_Passthrough(t *testing.T) {
original := buildZip(t, map[string][]byte{
"Document.xml": []byte("<xml/>"),
"thumbnails/a.png": []byte("png-data"),
})
result, err := Pack(original, &PackInput{
Manifest: &Manifest{UUID: "test"},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !bytes.Equal(result, original) {
t.Fatal("expected original bytes returned unchanged for plain .fcstd")
}
}
func TestPack_RoundTrip(t *testing.T) {
// Build a .kc with old silo/ data
oldManifest := Manifest{UUID: "old-uuid", KCVersion: "0.9", RevisionHash: "old-hash"}
oldMetadata := Metadata{SchemaName: "old-schema", Tags: []string{"old"}, LifecycleState: "draft"}
original := buildZip(t, map[string][]byte{
"Document.xml": []byte("<freecad/>"),
"thumbnails/t.png": []byte("thumb-data"),
"silo/manifest.json": mustJSON(t, oldManifest),
"silo/metadata.json": mustJSON(t, oldMetadata),
})
// Pack with new data
newManifest := &Manifest{UUID: "new-uuid", KCVersion: "1.0", RevisionHash: "new-hash", SiloInstance: "https://silo.test"}
newMetadata := &Metadata{SchemaName: "mechanical-part-v2", Tags: []string{"aluminum", "structural"}, LifecycleState: "review", Fields: map[string]any{"material": "7075-T6"}}
comment := "initial commit"
history := []HistoryEntry{
{RevisionNumber: 1, CreatedAt: "2026-01-01T00:00:00Z", Comment: &comment, Status: "draft", Labels: []string{}},
}
packed, err := Pack(original, &PackInput{
Manifest: newManifest,
Metadata: newMetadata,
History: history,
Dependencies: []Dependency{},
})
if err != nil {
t.Fatalf("Pack error: %v", err)
}
// Extract and verify new silo/ data
result, err := Extract(packed)
if err != nil {
t.Fatalf("Extract error: %v", err)
}
if result == nil {
t.Fatal("expected non-nil extract result")
}
if result.Manifest.UUID != "new-uuid" {
t.Errorf("manifest UUID = %q, want %q", result.Manifest.UUID, "new-uuid")
}
if result.Manifest.KCVersion != "1.0" {
t.Errorf("manifest KCVersion = %q, want %q", result.Manifest.KCVersion, "1.0")
}
if result.Manifest.SiloInstance != "https://silo.test" {
t.Errorf("manifest SiloInstance = %q, want %q", result.Manifest.SiloInstance, "https://silo.test")
}
if result.Metadata.SchemaName != "mechanical-part-v2" {
t.Errorf("metadata SchemaName = %q, want %q", result.Metadata.SchemaName, "mechanical-part-v2")
}
if result.Metadata.LifecycleState != "review" {
t.Errorf("metadata LifecycleState = %q, want %q", result.Metadata.LifecycleState, "review")
}
if len(result.Metadata.Tags) != 2 {
t.Errorf("metadata Tags len = %d, want 2", len(result.Metadata.Tags))
}
if result.Metadata.Fields["material"] != "7075-T6" {
t.Errorf("metadata Fields[material] = %v, want 7075-T6", result.Metadata.Fields["material"])
}
// Verify non-silo entries are preserved
r, err := zip.NewReader(bytes.NewReader(packed), int64(len(packed)))
if err != nil {
t.Fatalf("opening packed ZIP: %v", err)
}
entryMap := make(map[string]bool)
for _, f := range r.File {
entryMap[f.Name] = true
}
if !entryMap["Document.xml"] {
t.Error("Document.xml missing from packed ZIP")
}
if !entryMap["thumbnails/t.png"] {
t.Error("thumbnails/t.png missing from packed ZIP")
}
// Verify non-silo content is byte-identical
for _, f := range r.File {
if f.Name == "Document.xml" {
content := readZipEntry(t, f)
if string(content) != "<freecad/>" {
t.Errorf("Document.xml content = %q, want %q", content, "<freecad/>")
}
}
if f.Name == "thumbnails/t.png" {
content := readZipEntry(t, f)
if string(content) != "thumb-data" {
t.Errorf("thumbnails/t.png content = %q, want %q", content, "thumb-data")
}
}
}
}
func TestPack_NilFields(t *testing.T) {
original := buildZip(t, map[string][]byte{
"Document.xml": []byte("<xml/>"),
"silo/manifest.json": []byte(`{"uuid":"x"}`),
})
// Pack with only manifest, nil metadata/history/deps
packed, err := Pack(original, &PackInput{
Manifest: &Manifest{UUID: "updated"},
})
if err != nil {
t.Fatalf("Pack error: %v", err)
}
// Extract — should have manifest but no metadata
result, err := Extract(packed)
if err != nil {
t.Fatalf("Extract error: %v", err)
}
if result.Manifest == nil || result.Manifest.UUID != "updated" {
t.Errorf("manifest UUID = %v, want updated", result.Manifest)
}
if result.Metadata != nil {
t.Errorf("expected nil metadata, got %+v", result.Metadata)
}
// Verify no old silo/ entries leaked through
r, _ := zip.NewReader(bytes.NewReader(packed), int64(len(packed)))
for _, f := range r.File {
if f.Name == "silo/metadata.json" {
t.Error("old silo/metadata.json should have been removed")
}
}
}
func TestPack_EmptyDependencies(t *testing.T) {
original := buildZip(t, map[string][]byte{
"silo/manifest.json": []byte(`{"uuid":"x"}`),
})
packed, err := Pack(original, &PackInput{
Manifest: &Manifest{UUID: "x"},
Dependencies: []Dependency{},
})
if err != nil {
t.Fatalf("Pack error: %v", err)
}
// Verify dependencies.json exists and is []
r, _ := zip.NewReader(bytes.NewReader(packed), int64(len(packed)))
for _, f := range r.File {
if f.Name == "silo/dependencies.json" {
content := readZipEntry(t, f)
if string(content) != "[]" {
t.Errorf("dependencies.json = %q, want %q", content, "[]")
}
return
}
}
t.Error("silo/dependencies.json not found in packed ZIP")
}
// readZipEntry reads the full contents of a zip.File.
func readZipEntry(t *testing.T, f *zip.File) []byte {
t.Helper()
rc, err := f.Open()
if err != nil {
t.Fatalf("opening zip entry %s: %v", f.Name, err)
}
defer rc.Close()
data, err := io.ReadAll(rc)
if err != nil {
t.Fatalf("reading zip entry %s: %v", f.Name, err)
}
return data
}