| 1 | package memory |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | ) |
| 9 | |
| 10 | func TestAssessRememberWriteAutoAllowsOnlyLowRiskProjectCreates(t *testing.T) { |
| 11 | store := Store{Dir: t.TempDir()} |
| 12 | safe := json.RawMessage(`{"name":"release-target","description":"Release target for this project","type":"project","body":"Release artifacts are published from main-v2."}`) |
| 13 | assessment := AssessRememberWrite(store, safe) |
| 14 | if !assessment.AutoAllow || assessment.Name != "release-target" || assessment.Reason == "" { |
| 15 | t.Fatalf("safe project create assessment = %+v", assessment) |
| 16 | } |
| 17 | |
| 18 | cases := map[string]json.RawMessage{ |
| 19 | "implicit type": json.RawMessage(`{"name":"release-target","description":"Release target","body":"Use main-v2."}`), |
| 20 | "global": json.RawMessage(`{"name":"release-target","description":"Release target","type":"project","scope":"global","body":"Use main-v2."}`), |
| 21 | "global reference": json.RawMessage(`{"name":"global/release-target.md","description":"Release target","type":"project","body":"Use main-v2."}`), |
| 22 | "user preference": json.RawMessage(`{"name":"prefers-go","description":"Preferred language","type":"user","body":"Prefer Go."}`), |
| 23 | "feedback": json.RawMessage(`{"name":"concise","description":"Response style","type":"feedback","body":"Keep answers concise."}`), |
| 24 | "stable id update": json.RawMessage(`{"id":"mem-existing","expected_revision":1,"description":"Update","type":"project","body":"Updated body."}`), |
| 25 | "credential": json.RawMessage(`{"name":"deploy-key","description":"Deploy credential","type":"project","body":"DEPLOY_API_KEY=sk-example-secret-value-123456"}`), |
| 26 | "email": json.RawMessage(`{"name":"release-owner","description":"Release owner","type":"project","body":"Contact release-owner@example.test."}`), |
| 27 | } |
| 28 | for name, args := range cases { |
| 29 | t.Run(name, func(t *testing.T) { |
| 30 | if got := AssessRememberWrite(store, args); got.AutoAllow || got.Reason == "" { |
| 31 | t.Fatalf("assessment = %+v, want approval with reason", got) |
| 32 | } |
| 33 | }) |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | func TestAssessRememberWriteRejectsConflictingReferenceScope(t *testing.T) { |
| 38 | store := Store{Dir: t.TempDir(), GlobalDir: t.TempDir()} |
| 39 | args := json.RawMessage(`{"name":"global/release-target.md","description":"Release target","type":"project","scope":"project","body":"Use main-v2."}`) |
| 40 | got := AssessRememberWrite(store, args) |
| 41 | if got.AutoAllow || !strings.Contains(got.Reason, "conflicts") { |
| 42 | t.Fatalf("conflicting reference assessment = %+v", got) |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | func TestAssessRememberWriteRequiresApprovalForExistingOrSemanticDuplicate(t *testing.T) { |
| 47 | store := Store{Dir: t.TempDir()} |
| 48 | if _, err := store.Save(Memory{ |
| 49 | Name: "release-target", Title: "Release target", Description: "Current release branch", |
| 50 | Type: TypeProject, Scope: FactScopeProject, Body: "Use main-v2.", |
| 51 | }); err != nil { |
| 52 | t.Fatal(err) |
| 53 | } |
| 54 | |
| 55 | for _, args := range []json.RawMessage{ |
| 56 | json.RawMessage(`{"name":"release-target","description":"Changed release branch","type":"project","body":"Use release-v2."}`), |
| 57 | json.RawMessage(`{"name":"another-name","title":"Release target","description":"Current release branch","type":"project","body":"Use main-v2."}`), |
| 58 | } { |
| 59 | if got := AssessRememberWrite(store, args); got.AutoAllow || !strings.Contains(got.Reason, "existing") { |
| 60 | t.Fatalf("duplicate assessment = %+v", got) |
| 61 | } |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | func TestRememberAutoWriteClaimRemainsCreateOnlyAtExecution(t *testing.T) { |
| 66 | store := Store{Dir: t.TempDir()} |
| 67 | args := json.RawMessage(`{"name":"release-target","description":"Release target","type":"project","body":"Use main-v2."}`) |
| 68 | claim := &fakeAutoWriteQueue{claim: true} |
| 69 | ctx := WithQueue(context.Background(), claim) |
| 70 | |
| 71 | // Simulate another writer creating the same name after approval assessment but |
| 72 | // before the remember tool executes. |
| 73 | if _, err := store.Save(Memory{Name: "release-target", Description: "concurrent", Body: "Do not overwrite."}); err != nil { |
| 74 | t.Fatal(err) |
| 75 | } |
| 76 | if _, err := NewRememberTool(store).Execute(ctx, args); err == nil || !strings.Contains(err.Error(), "already exists") { |
| 77 | t.Fatalf("auto-approved create became an overwrite: %v", err) |
| 78 | } |
| 79 | got, ok := store.Read("release-target") |
| 80 | if !ok || got.Body != "Do not overwrite." { |
| 81 | t.Fatalf("concurrent memory was overwritten: %+v, ok=%v", got, ok) |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | type fakeAutoWriteQueue struct { |
| 86 | notes []string |
| 87 | claim bool |
| 88 | } |
| 89 | |
| 90 | func (q *fakeAutoWriteQueue) QueueMemory(note string) { q.notes = append(q.notes, note) } |
| 91 | func (q *fakeAutoWriteQueue) ClaimAutoMemoryWrite(json.RawMessage) bool { |
| 92 | claimed := q.claim |
| 93 | q.claim = false |
| 94 | return claimed |
| 95 | } |
| 96 |