package metadata import ( "strings" "testing" "time" "github.com/google/uuid" "git.warky.dev/wdevs/amcs/internal/config" thoughttypes "git.warky.dev/wdevs/amcs/internal/types" ) func testCaptureConfig() config.CaptureConfig { return config.CaptureConfig{ Source: "mcp", MetadataDefaults: config.CaptureMetadataDefault{ Type: "observation", TopicFallback: "uncategorized", }, } } func TestFallbackUsesConfiguredDefaults(t *testing.T) { got := Fallback(testCaptureConfig()) if got.Type != "observation" { t.Fatalf("Fallback type = %q, want observation", got.Type) } if len(got.Topics) != 1 || got.Topics[0] != "uncategorized" { t.Fatalf("Fallback topics = %#v, want [uncategorized]", got.Topics) } if got.Source != "mcp" { t.Fatalf("Fallback source = %q, want mcp", got.Source) } if got.MetadataStatus != MetadataStatusComplete { t.Fatalf("Fallback metadata status = %q, want complete", got.MetadataStatus) } } func TestNormalizeTrimsDedupesAndCapsTopics(t *testing.T) { topics := []string{} for i := 0; i < 12; i++ { topics = append(topics, strings.TrimSpace(" topic ")) topics = append(topics, string(rune('a'+i))) } got := Normalize(thoughttypes.ThoughtMetadata{ People: []string{" Alice ", "alice", "", "Bob"}, Topics: topics, Type: "INVALID", }, testCaptureConfig()) if len(got.People) != 2 { t.Fatalf("People len = %d, want 2", len(got.People)) } if got.Type != "observation" { t.Fatalf("Type = %q, want observation", got.Type) } if len(got.Topics) != maxTopics { t.Fatalf("Topics len = %d, want %d", len(got.Topics), maxTopics) } } func TestMergeAddsPatchAndNormalizes(t *testing.T) { base := thoughttypes.ThoughtMetadata{ People: []string{"Alice"}, Topics: []string{"go"}, Type: "idea", Source: "mcp", } patch := thoughttypes.ThoughtMetadata{ People: []string{" Bob ", "alice"}, Topics: []string{"testing"}, Type: "task", } got := Merge(base, patch, testCaptureConfig()) if got.Type != "task" { t.Fatalf("Type = %q, want task", got.Type) } if len(got.People) != 2 { t.Fatalf("People len = %d, want 2", len(got.People)) } if len(got.Topics) != 2 { t.Fatalf("Topics len = %d, want 2", len(got.Topics)) } } func TestNormalizeDedupesAttachmentsByFileID(t *testing.T) { id := uuid.New() got := Normalize(thoughttypes.ThoughtMetadata{ Attachments: []thoughttypes.ThoughtAttachment{ {FileID: id, Name: " one.png ", MediaType: " image/png ", Kind: " image ", SizeBytes: 12, SHA256: " abc "}, {FileID: id, Name: "two.png", MediaType: "image/png", Kind: "image", SizeBytes: 99, SHA256: "def"}, }, }, testCaptureConfig()) if len(got.Attachments) != 1 { t.Fatalf("Attachments len = %d, want 1", len(got.Attachments)) } if got.Attachments[0].Name != "one.png" { t.Fatalf("Attachment name = %q, want one.png", got.Attachments[0].Name) } if got.Attachments[0].Kind != "image" { t.Fatalf("Attachment kind = %q, want image", got.Attachments[0].Kind) } } func TestSanitizeExtractedDropsAttachmentsAndMetadataControlFields(t *testing.T) { id := uuid.New() got := SanitizeExtracted(thoughttypes.ThoughtMetadata{ Type: "idea", Attachments: []thoughttypes.ThoughtAttachment{{FileID: id, Name: "secret.pdf"}}, MetadataStatus: MetadataStatusFailed, MetadataUpdatedAt: "2026-03-30T10:00:00Z", MetadataLastAttemptedAt: "2026-03-30T10:01:00Z", MetadataError: "boom", }) if len(got.Attachments) != 0 { t.Fatalf("Attachments len = %d, want 0", len(got.Attachments)) } if got.MetadataStatus != "" { t.Fatalf("MetadataStatus = %q, want empty", got.MetadataStatus) } if got.MetadataUpdatedAt != "" { t.Fatalf("MetadataUpdatedAt = %q, want empty", got.MetadataUpdatedAt) } if got.MetadataLastAttemptedAt != "" { t.Fatalf("MetadataLastAttemptedAt = %q, want empty", got.MetadataLastAttemptedAt) } if got.MetadataError != "" { t.Fatalf("MetadataError = %q, want empty", got.MetadataError) } if got.Type != "idea" { t.Fatalf("Type = %q, want idea", got.Type) } } func TestMarkMetadataPendingTracksAttemptWithoutClearingPreviousSuccess(t *testing.T) { attempt := time.Date(2026, 3, 30, 10, 0, 0, 0, time.UTC) base := thoughttypes.ThoughtMetadata{ Topics: []string{"go"}, MetadataUpdatedAt: "2026-03-29T10:00:00Z", } got := MarkMetadataPending(base, testCaptureConfig(), attempt, errTestMetadataFailure) if got.MetadataStatus != MetadataStatusPending { t.Fatalf("MetadataStatus = %q, want pending", got.MetadataStatus) } if got.MetadataUpdatedAt != "2026-03-29T10:00:00Z" { t.Fatalf("MetadataUpdatedAt = %q, want previous success timestamp", got.MetadataUpdatedAt) } if got.MetadataLastAttemptedAt != "2026-03-30T10:00:00Z" { t.Fatalf("MetadataLastAttemptedAt = %q, want attempt timestamp", got.MetadataLastAttemptedAt) } if got.MetadataError == "" { t.Fatal("MetadataError is empty, want failure message") } } func TestMarkMetadataCompleteClearsErrorAndSetsTimestamps(t *testing.T) { attempt := time.Date(2026, 3, 30, 10, 0, 0, 0, time.UTC) got := MarkMetadataComplete(thoughttypes.ThoughtMetadata{ Topics: []string{"go"}, MetadataStatus: MetadataStatusFailed, MetadataError: "timeout", }, testCaptureConfig(), attempt) if got.MetadataStatus != MetadataStatusComplete { t.Fatalf("MetadataStatus = %q, want complete", got.MetadataStatus) } if got.MetadataUpdatedAt != "2026-03-30T10:00:00Z" { t.Fatalf("MetadataUpdatedAt = %q, want completion timestamp", got.MetadataUpdatedAt) } if got.MetadataLastAttemptedAt != "2026-03-30T10:00:00Z" { t.Fatalf("MetadataLastAttemptedAt = %q, want completion timestamp", got.MetadataLastAttemptedAt) } if got.MetadataError != "" { t.Fatalf("MetadataError = %q, want empty", got.MetadataError) } } var errTestMetadataFailure = testError("timeout") type testError string func (e testError) Error() string { return string(e) }