package memory import ( "strings" "testing" "time" ) func TestParseFact_HappyPath(t *testing.T) { src := strings.Join([]string{ "---", "name: build-gate", "description: gate is `go vet ./...`", "type: reference", "created: 2026-05-19", "last_used: 2026-05-19", "pin: false", "---", "", "body line one", "body line two", "", }, "\n") f, err := ParseFact([]byte(src)) if err != nil { t.Fatalf("ParseFact: %v", err) } if f.Name != "build-gate" { t.Errorf("name = %q", f.Name) } if f.Type != TypeReference { t.Errorf("type = %q", f.Type) } if f.Pin { t.Error("pin should be false") } if f.Body != "body line one\nbody line two\n" { t.Errorf("body = %q", f.Body) } } func TestParseFact_OptionalFields(t *testing.T) { src := strings.Join([]string{ "---", "name: bug-x", "description: fix bug X", "type: finding", "created: 2026-05-19", "last_used: 2026-05-19", "ref: internal/config/config.go", "expires: 2026-12-31", "status: open", "pin: true", "---", "detail", "", }, "\n") f, err := ParseFact([]byte(src)) if err != nil { t.Fatalf("ParseFact: %v", err) } if f.Ref != "internal/config/config.go" { t.Errorf("ref = %q", f.Ref) } if f.Expires == nil || !f.Expires.Equal(time.Date(2026, 12, 31, 0, 0, 0, 0, time.UTC)) { t.Errorf("expires = %v", f.Expires) } if f.Status != "open" { t.Errorf("status = %q", f.Status) } if !f.Pin { t.Error("pin should be true") } } func TestParseFact_QuotedAndComments(t *testing.T) { src := strings.Join([]string{ "---", `name: "quoted-name"`, `description: 'single quoted'`, "# comment line", "", "type: user", "created: 2026-05-19", "last_used: 2026-05-19", "pin: false", "unknown_key: tolerated", "---", }, "\n") f, err := ParseFact([]byte(src)) if err != nil { t.Fatalf("ParseFact: %v", err) } if f.Name != "quoted-name" { t.Errorf("name = %q", f.Name) } if f.Description != "single quoted" { t.Errorf("description = %q", f.Description) } } func TestParseFact_ErrorCases(t *testing.T) { cases := map[string]string{ "missing opening delim": "name: x\n", "missing closing delim": "---\nname: x\n", "missing colon": "---\nthis has no colon\n---\n", "bad date": "---\nname: a\ndescription: b\ntype: user\ncreated: not-a-date\nlast_used: 2026-05-19\npin: false\n---\n", "bad type": "---\nname: a\ndescription: b\ntype: not-a-type\ncreated: 2026-05-19\nlast_used: 2026-05-19\npin: false\n---\n", "bad pin": "---\nname: a\ndescription: b\ntype: user\ncreated: 2026-05-19\nlast_used: 2026-05-19\npin: maybe\n---\n", "bad ref-traversal": "---\nname: a\ndescription: b\ntype: reference\ncreated: 2026-05-19\nlast_used: 2026-05-19\nref: ../escape\npin: false\n---\n", "bad ref-absolute": "---\nname: a\ndescription: b\ntype: reference\ncreated: 2026-05-19\nlast_used: 2026-05-19\nref: /etc/passwd\npin: false\n---\n", "missing name": "---\ndescription: b\ntype: user\ncreated: 2026-05-19\nlast_used: 2026-05-19\npin: false\n---\n", "bad name": "---\nname: NotKebab\ndescription: b\ntype: user\ncreated: 2026-05-19\nlast_used: 2026-05-19\npin: false\n---\n", "missing description": "---\nname: a\ntype: user\ncreated: 2026-05-19\nlast_used: 2026-05-19\npin: false\n---\n", } for label, src := range cases { t.Run(label, func(t *testing.T) { if _, err := ParseFact([]byte(src)); err == nil { t.Fatalf("ParseFact(%q) succeeded; expected error", label) } }) } } func TestSerialize_RoundTrip(t *testing.T) { exp := time.Date(2026, 12, 31, 0, 0, 0, 0, time.UTC) in := &Fact{ Name: "round-trip", Description: "round trip", Type: TypeFinding, Created: time.Date(2026, 5, 19, 0, 0, 0, 0, time.UTC), LastUsed: time.Date(2026, 5, 20, 0, 0, 0, 0, time.UTC), Ref: "internal/config/config.go", Expires: &exp, Status: "open", Pin: true, Body: "body text\nwith two lines", } raw, err := Serialize(in) if err != nil { t.Fatal(err) } got, err := ParseFact(raw) if err != nil { t.Fatalf("re-parse:\n%s\nerr: %v", raw, err) } if got.Name != in.Name || got.Description != in.Description || got.Type != in.Type || !got.Created.Equal(in.Created) || !got.LastUsed.Equal(in.LastUsed) || got.Ref != in.Ref || got.Status != in.Status || got.Pin != in.Pin { t.Errorf("round-trip diverged.\nwant: %+v\ngot: %+v", *in, *got) } if got.Expires == nil || !got.Expires.Equal(*in.Expires) { t.Errorf("expires diverged: %v", got.Expires) } if strings.TrimRight(got.Body, "\n") != strings.TrimRight(in.Body, "\n") { t.Errorf("body diverged:\nwant: %q\ngot: %q", in.Body, got.Body) } } func TestSerialize_OmitsEmptyOptionals(t *testing.T) { in := &Fact{ Name: "minimal", Description: "minimal fact", Type: TypeUser, Created: time.Date(2026, 5, 19, 0, 0, 0, 0, time.UTC), LastUsed: time.Date(2026, 5, 19, 0, 0, 0, 0, time.UTC), } raw, err := Serialize(in) if err != nil { t.Fatal(err) } s := string(raw) for _, banned := range []string{"ref:", "expires:", "status:", "source:", "agent:", "disabled:"} { if strings.Contains(s, banned) { t.Errorf("output should omit %q for empty values:\n%s", banned, s) } } if !strings.Contains(s, "pin: false") { t.Errorf("pin: false should always be emitted:\n%s", s) } } func TestParseFact_AdaptationFields(t *testing.T) { src := strings.Join([]string{ "---", "name: terse-feedback", "description: user prefers terse responses", "type: feedback", "created: 2026-05-22", "last_used: 2026-05-22", "pin: false", "source: stop summarizing what you just did", "agent: claude-opus-4-7", "disabled: true", "---", "reasoning", "", }, "\n") f, err := ParseFact([]byte(src)) if err != nil { t.Fatalf("ParseFact: %v", err) } if f.Source != "stop summarizing what you just did" { t.Errorf("source = %q", f.Source) } if f.Agent != "claude-opus-4-7" { t.Errorf("agent = %q", f.Agent) } if !f.Disabled { t.Error("disabled should be true") } } func TestParseFact_DisabledBadValue(t *testing.T) { src := strings.Join([]string{ "---", "name: bad", "description: bad", "type: user", "created: 2026-05-22", "last_used: 2026-05-22", "pin: false", "disabled: maybe", "---", }, "\n") if _, err := ParseFact([]byte(src)); err == nil { t.Fatal("expected error on disabled: maybe") } } func TestParseFact_OldFactStillLoads(t *testing.T) { // A fact written before the source/agent/disabled fields existed // must still parse cleanly: the new fields are optional on the wire. src := strings.Join([]string{ "---", "name: legacy", "description: legacy fact", "type: feedback", "created: 2026-01-01", "last_used: 2026-01-01", "pin: false", "---", }, "\n") f, err := ParseFact([]byte(src)) if err != nil { t.Fatalf("ParseFact: %v", err) } if f.Source != "" || f.Agent != "" || f.Disabled { t.Errorf("new fields should be zero for legacy fact: %+v", f) } } func TestSerialize_RoundTripAdaptationFields(t *testing.T) { in := &Fact{ Name: "adapt", Description: "adapt to operator", Type: TypeUser, Created: time.Date(2026, 5, 22, 0, 0, 0, 0, time.UTC), LastUsed: time.Date(2026, 5, 22, 0, 0, 0, 0, time.UTC), Source: "stop using emojis", Agent: "claude-opus-4-7", Disabled: true, Body: "body", } raw, err := Serialize(in) if err != nil { t.Fatal(err) } got, err := ParseFact(raw) if err != nil { t.Fatalf("re-parse: %v", err) } if got.Source != in.Source || got.Agent != in.Agent || got.Disabled != in.Disabled { t.Errorf("adaptation fields diverged: %+v", got) } } func TestValidate_RejectsOversizeSource(t *testing.T) { long := strings.Repeat("x", MaxSourceLen+1) in := &Fact{ Name: "oversize", Description: "oversize source", Type: TypeFeedback, Created: time.Date(2026, 5, 22, 0, 0, 0, 0, time.UTC), LastUsed: time.Date(2026, 5, 22, 0, 0, 0, 0, time.UTC), Source: long, } if err := in.Validate(); err == nil { t.Fatal("Validate: expected error on oversize source") } } func TestSerialize_RejectsInvalid(t *testing.T) { in := &Fact{Name: "x"} // missing required fields if _, err := Serialize(in); err == nil { t.Fatal("Serialize: expected validation error") } } // --- setField residual branches --- func TestSetField_ExpiresEmpty_ClearsField(t *testing.T) { src := strings.Join([]string{ "---", "name: exp-empty", "description: empty expires clears the field", "type: user", "created: 2026-05-19", "last_used: 2026-05-19", "expires:", "pin: false", "---", }, "\n") f, err := ParseFact([]byte(src)) if err != nil { t.Fatalf("ParseFact: %v", err) } if f.Expires != nil { t.Errorf("expires = %v, want nil", f.Expires) } } func TestSetField_ExpiresBadDate(t *testing.T) { src := strings.Join([]string{ "---", "name: exp-bad", "description: bad expires date", "type: user", "created: 2026-05-19", "last_used: 2026-05-19", "expires: not-a-date", "pin: false", "---", }, "\n") _, err := ParseFact([]byte(src)) if err == nil { t.Fatal("expected bad expires date to error") } if !strings.Contains(err.Error(), "expires:") { t.Errorf("err = %v, want substring expires:", err) } } func TestSetField_DisabledFalseExplicit(t *testing.T) { src := strings.Join([]string{ "---", "name: dis-false", "description: explicit disabled false", "type: user", "created: 2026-05-19", "last_used: 2026-05-19", "pin: false", "disabled: false", "---", }, "\n") f, err := ParseFact([]byte(src)) if err != nil { t.Fatalf("ParseFact: %v", err) } if f.Disabled { t.Error("disabled should be false") } }