package librarydiff import ( "strings" "testing" ) func rows(t *testing.T, body string) []map[string]any { t.Helper() parsed, err := ReadDocuments(strings.NewReader(body)) if err != nil { t.Fatalf("ReadDocuments: %v", err) } return parsed } func TestCompareUsesStableIdentityAndIgnoresVolatileColumns(t *testing.T) { base := rows(t, `[ {"id":"doc-a","title":"机械臂报警","content":"正文一","updated_at":"2026-10-01T00:00:00Z"}, {"id":"doc-b","title":"示教器","content":"正文二","updated_at":"2026-10-01T00:00:00Z"}, {"id":"doc-c","title":"将删除","content":"正文三"} ]`) target := rows(t, `[ {"id":"doc-a","title":"机械臂报警","content":"正文一","updated_at":"2026-10-28T00:00:00Z"}, {"id":"doc-b","title":"示教器","content":"正文二(已更新)","updated_at":"2026-10-01T00:00:00Z"}, {"id":"doc-d","title":"新增文档","content":"正文四"} ]`) result, err := Compare(base, target) if err != nil { t.Fatalf("Compare: %v", err) } // doc-a 只有 updated_at 变了:不算更新。 if len(result.Added) != 1 || result.Added[0].ID != "doc-d" { t.Fatalf("added = %+v, want doc-d", result.Added) } if len(result.Updated) != 1 || result.Updated[0].ID != "doc-b" { t.Fatalf("updated = %+v, want doc-b", result.Updated) } if len(result.Removed) != 1 || result.Removed[0].ID != "doc-c" { t.Fatalf("removed = %+v, want doc-c", result.Removed) } if result.BaseCount != 3 || result.TargetCount != 3 { t.Fatalf("counts = %d/%d, want 3/3", result.BaseCount, result.TargetCount) } } func TestCompareTreatsTitleChangeAsUpdate(t *testing.T) { base := rows(t, `[{"id":"doc-a","title":"旧标题","content":"正文"}]`) target := rows(t, `[{"id":"doc-a","title":"新标题","content":"正文"}]`) result, err := Compare(base, target) if err != nil { t.Fatalf("Compare: %v", err) } if len(result.Updated) != 1 || result.Updated[0].Title != "新标题" { t.Fatalf("title change must count as update: %+v", result.Updated) } } func TestCompareRejectsDuplicateIDs(t *testing.T) { base := rows(t, `[{"id":"doc-a","title":"x"}]`) dup := rows(t, `[{"id":"doc-a","title":"x"},{"id":"doc-a","title":"y"}]`) if _, err := Compare(base, dup); err == nil { t.Fatal("duplicate knowledge IDs must be rejected, not silently collapsed") } } func TestCompareEmptyBaseMarksEverythingAdded(t *testing.T) { result, err := Compare(nil, rows(t, `[{"id":"doc-a","title":"x"}]`)) if err != nil { t.Fatalf("Compare: %v", err) } if len(result.Added) != 1 || len(result.Removed) != 0 { t.Fatalf("empty base must be all added: %+v", result) } } func TestReadDocumentsRejectsNonArray(t *testing.T) { if _, err := ReadDocuments(strings.NewReader(`{"id":"x"}`)); err == nil { t.Fatal("object payload must be rejected") } }