package execenv import ( "fmt" "os" "path/filepath" "strings" "testing" ) // Sub-issue Creation section — after MUL-2538 the platform posts the // child-done parent notification itself, so the brief no longer carries // any parent-notification rule (per Bohan's call on PR #3055: delete the // guidance entirely, do not replace it with a "do not post one" sentence // — the agent should not be thinking about parent comments at all). All // that remains is the `--status todo` vs `--status backlog` rule for // creating sub-issues, which is unrelated to the notification path. func TestSubIssueCreationSectionPresentForIssueRuns(t *testing.T) { t.Parallel() cases := []struct { name string ctx TaskContextForEnv }{ { name: "assignment-triggered", ctx: TaskContextForEnv{IssueID: "11111111-2222-3333-4444-555555555555"}, }, { name: "comment-triggered", ctx: TaskContextForEnv{ IssueID: "22222222-3333-4444-5555-666666666666", TriggerCommentID: "33333333-4444-5555-6666-777777777777", }, }, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() out := buildMetaSkillContent("claude", tc.ctx) if !strings.Contains(out, "## Sub-issue Creation") { t.Fatalf("expected Sub-issue Creation section in %s brief", tc.name) } for _, want := range []string{ "**Choosing `--status` when creating sub-issues.**", "`--status todo` = **start now**", "`--status backlog` = **wait**", "`multica issue status todo`", "all `--status todo`", "`--status backlog` from the start", } { if !strings.Contains(out, want) { t.Errorf("[%s] section missing %q", tc.name, want) } } }) } } // The brief must no longer carry any parent-notification guidance. PR // #2918 added a "Tell the parent when you finish a child" rule that // turned into noise (self-mention loops, planner ack ping-pong, // hardcoded `MUL-` prefix). PR #3055 first downgraded it to a "do NOT // post one" guardrail, but Bohan's product call was to remove the // guidance entirely rather than substitute a new prohibition. These // canaries lock that in: any wording that re-introduces the // parent-comment concept — positive, negative, or descriptive — must // not come back through future edits. func TestBriefHasNoParentNotificationGuidance(t *testing.T) { t.Parallel() cases := []TaskContextForEnv{ {IssueID: "11111111-2222-3333-4444-555555555555"}, { IssueID: "22222222-3333-4444-5555-666666666666", TriggerCommentID: "33333333-4444-5555-6666-777777777777", }, } for _, ctx := range cases { ctx := ctx out := buildMetaSkillContent("claude", ctx) // The pre-MUL-2538 phrasing instructed the agent to compose a // parent comment by hand — including a hardcoded `MUL-` prefix // and an assignee mention. The intermediate revision (PR #3055 // before Bohan's call) instead told the agent NOT to post one. // Both framings must stay out. for _, banned := range []string{ // Old "do it yourself" framing (PR #2918). "## Parent / Sub-issue Protocol", "**Tell the parent when you finish a child.**", "multica issue comment add ", "with NO `--parent`", "link the child as `[MUL-", "`@mention` the parent's assignee", "`mention://agent/`", "`mention://member/`", "`mention://squad/`", // Intermediate "do NOT do it yourself" framing (PR #3055 // before Bohan's call) — also out per product direction. "**Do NOT post your own parent-notification comment.**", "Do NOT post your own parent-notification comment", "parent-notification comment", "system comment on the parent fires from the status transition", "re-trigger the parent's assignee for nothing", "platform posts a top-level system comment on the parent", // Earlier revisions split rules by trigger type or used // table/subsection layouts. None of those structures should // come back either. "| Parent assignee | Parent status |", "The same agent as yourself", "| Member or squad |", "### A. Notify the parent", "### B. Choose", "When this issue has `parent_issue_id`:", "**Closing out child work** (only if this issue has `parent_issue_id`)", "**Notify the parent** (only if this issue has `parent_issue_id`", "**Creating sub-issues** (applies to any issue-bound run)", "For parent/child work, use these best-effort rules", // The protocol must no longer emit a placeholder // `` status flip — the workflow above owns // that command with the real issue id substituted. "`multica issue status in_review`", // Non-existent CLI form Elon's earlier review flagged. "issue list --parent", } { if strings.Contains(out, banned) { t.Errorf("expected %q to be removed from the brief", banned) } } } } // Comment-triggered briefs must NOT carry any unconditional status-flip // command targeting the current issue. Previous revisions had a // dedicated protocol step that wrote `multica issue status in_review`; // the comment-triggered workflow rule "Do NOT change the issue status // unless the comment explicitly asks for it" must remain the source of // truth (Elon's blocking review on PR #2918). func TestCommentTriggeredProtocolDoesNotForceInReview(t *testing.T) { t.Parallel() ctx := TaskContextForEnv{ IssueID: "55555555-6666-7777-8888-999999999999", TriggerCommentID: "66666666-7777-8888-9999-aaaaaaaaaaaa", } out := buildMetaSkillContent("claude", ctx) if strings.Contains(out, "`multica issue status in_review`") { t.Errorf("comment-triggered brief must not contain a placeholder ` in_review` flip — that conflicts with the comment-triggered \"do not change status unless asked\" rule") } const guardrail = "Do NOT change the issue status unless the comment explicitly asks for it" if !strings.Contains(out, guardrail) { t.Errorf("expected the comment-triggered workflow guardrail %q to be present", guardrail) } } // The CLAUDE.md workflow surface must carry the same issue-wide since-delta // new-comment hint as the per-turn prompt. PR #2816 requires the two surfaces // stay in sync. func TestCommentTriggeredBriefCarriesNewCommentsHint(t *testing.T) { t.Parallel() const ( issueID = "55555555-6666-7777-8888-999999999999" since = "2026-05-28T11:00:00Z" ) ctx := TaskContextForEnv{ IssueID: issueID, TriggerCommentID: "reply-abc", NewCommentCount: 4, NewCommentsSince: since, } out := buildMetaSkillContent("claude", ctx) // Issue-wide count. if !strings.Contains(out, "4 new comment(s) on this issue since your last run") { t.Errorf("comment brief must report the issue-wide new-comment count, got:\n%s", out) } if !strings.Contains(out, "blindly") { t.Errorf("comment brief must discourage blindly reading every new comment, got:\n%s", out) } // Parent thread first. if !strings.Contains(out, "--thread reply-abc --since "+since+" --output json") { t.Errorf("comment brief must point at the triggering (parent) thread --since read first, got:\n%s", out) } if !strings.Contains(out, "--tail 30") { t.Errorf("comment brief must offer the full-thread (--tail 30) option, got:\n%s", out) } // Issue-wide catch-up demoted to an only-if-needed fallback. if !strings.Contains(out, "multica issue comment list "+issueID+" --since "+since+" --output json") { t.Errorf("comment brief must keep the issue-wide --since catch-up fallback, got:\n%s", out) } // The removed resolve step must not reappear. if strings.Contains(out, "multica comment resolve") { t.Errorf("comment brief must not carry the dropped resolve step, got:\n%s", out) } } // Cold start (no prior run → no since anchor) must point the agent at the // triggering CONVERSATION (--thread --tail 30) instead of the flat // timeline dump or the since-delta hint. func TestCommentTriggeredBriefColdStartThreadRead(t *testing.T) { t.Parallel() const issueID = "55555555-6666-7777-8888-999999999999" ctx := TaskContextForEnv{ IssueID: issueID, TriggerCommentID: "trigger-1", NewCommentCount: 0, NewCommentsSince: "", } out := buildMetaSkillContent("claude", ctx) if strings.Contains(out, "new comment(s) since your last run") { t.Errorf("no since-delta hint should render on cold start, got:\n%s", out) } if !strings.Contains(out, "multica issue comment list "+issueID+" --thread trigger-1 --tail 30 --output json") { t.Errorf("cold start must point at the triggering thread read, got:\n%s", out) } } // A resumed comment session with no since-delta should not fall back to the // cold-start "read the triggering conversation first" instruction. The trigger // body is already embedded in the per-turn prompt and the resumed session should // carry prior thread context, so the thread read is only a fallback. func TestCommentTriggeredBriefResumedNoDeltaSkipsDefaultThreadRead(t *testing.T) { t.Parallel() const issueID = "55555555-6666-7777-8888-999999999999" ctx := TaskContextForEnv{ IssueID: issueID, TriggerCommentID: "trigger-1", PriorSessionResumed: true, NewCommentCount: 0, NewCommentsSince: "", } out := buildMetaSkillContent("claude", ctx) for _, want := range []string{ "triggering comment is already included above", "No other new comments on this issue since your last run", "triggering comment ID / thread anchor", "If your reply depends on thread context", "do not rely only on resumed session memory", "multica issue comment list " + issueID + " --thread trigger-1 --tail 30 --output json", } { if !strings.Contains(out, want) { t.Errorf("resumed/no-delta brief missing %q\n--- output ---\n%s", want, out) } } if strings.Contains(out, "scoped to the triggering thread") { t.Errorf("resumed/no-delta brief must not claim the delta is thread-scoped, got:\n%s", out) } if strings.Contains(out, "Read the triggering conversation first") { t.Errorf("resumed/no-delta brief must not use the cold-start forced-read wording, got:\n%s", out) } } // Assignment-triggered briefs are the inverse boundary: when the agent // owns the issue lifecycle, the brief AS A WHOLE must still tell it to // flip to in_review on completion. The flip lives in the // assignment-triggered workflow above (with the real id substituted). func TestAssignmentTriggeredProtocolStillFlipsInReview(t *testing.T) { t.Parallel() const issueID = "77777777-8888-9999-aaaa-bbbbbbbbbbbb" ctx := TaskContextForEnv{IssueID: issueID} out := buildMetaSkillContent("claude", ctx) want := "`multica issue status " + issueID + " in_review`" if !strings.Contains(out, want) { t.Errorf("assignment-triggered brief must still flip to in_review on completion (expected %q in the workflow above)", want) } } // The sub-issue creation rule must reach top-level parents that have no // `parent_issue_id` of their own — that is where the `todo` vs `backlog` // decision matters most. The section must not gate on this issue being // a child, and must not even mention `parent_issue_id`. func TestSubIssueCreationSectionIsUnconditional(t *testing.T) { t.Parallel() ctx := TaskContextForEnv{ IssueID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", } out := buildMetaSkillContent("claude", ctx) const header = "## Sub-issue Creation" start := strings.Index(out, header) if start == -1 { t.Fatalf("sub-issue creation section missing") } rest := out[start:] end := strings.Index(rest[len(header):], "\n## ") var section string if end == -1 { section = rest } else { section = rest[:len(header)+end] } if strings.Contains(section, "parent_issue_id") { t.Errorf("Sub-issue Creation section must not reference `parent_issue_id` — it applies to any issue-bound run, including top-level parents:\n%s", section) } } // Workspace Context block: workspace.context (the per-workspace system prompt // owners set in Settings → General) must reach the brief as `## Workspace // Context` for every task kind so agents see a consistent shared system prompt // regardless of how they were triggered. Empty content must skip the heading // entirely — bare headings would just add noise. func TestWorkspaceContextRenderedAcrossTaskKinds(t *testing.T) { t.Parallel() const wsContext = "All comments must be in English. Prefer concise PR descriptions." cases := []struct { name string ctx TaskContextForEnv }{ { name: "assignment-triggered", ctx: TaskContextForEnv{ IssueID: "11111111-2222-3333-4444-555555555555", WorkspaceContext: wsContext, }, }, { name: "comment-triggered", ctx: TaskContextForEnv{ IssueID: "22222222-3333-4444-5555-666666666666", TriggerCommentID: "33333333-4444-5555-6666-777777777777", WorkspaceContext: wsContext, }, }, { name: "chat", ctx: TaskContextForEnv{ ChatSessionID: "chat-1", WorkspaceContext: wsContext, }, }, { name: "quick-create", ctx: TaskContextForEnv{ QuickCreatePrompt: "create me an issue", WorkspaceContext: wsContext, }, }, { name: "autopilot run-only", ctx: TaskContextForEnv{ AutopilotRunID: "run-1", WorkspaceContext: wsContext, }, }, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() out := buildMetaSkillContent("claude", tc.ctx) if !strings.Contains(out, "## Workspace Context") { t.Fatalf("[%s] expected `## Workspace Context` heading", tc.name) } if !strings.Contains(out, wsContext) { t.Errorf("[%s] brief missing workspace context body %q", tc.name, wsContext) } // The block must precede Available Commands so it acts as // background framing, not a footer hidden below CLI usage. ctxIdx := strings.Index(out, "## Workspace Context") cmdsIdx := strings.Index(out, "## Available Commands") if ctxIdx == -1 || cmdsIdx == -1 || ctxIdx > cmdsIdx { t.Errorf("[%s] `## Workspace Context` must appear above `## Available Commands` (ctx=%d, cmds=%d)", tc.name, ctxIdx, cmdsIdx) } }) } } func TestWorkspaceContextHeadingSkippedWhenEmpty(t *testing.T) { t.Parallel() cases := []struct { name string ctx TaskContextForEnv }{ { name: "empty string", ctx: TaskContextForEnv{ IssueID: "11111111-2222-3333-4444-555555555555", WorkspaceContext: "", }, }, { name: "whitespace only", ctx: TaskContextForEnv{ IssueID: "11111111-2222-3333-4444-555555555555", WorkspaceContext: " \n\t \r\n", }, }, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() out := buildMetaSkillContent("claude", tc.ctx) if strings.Contains(out, "## Workspace Context") { t.Errorf("[%s] empty workspace context must NOT emit the heading", tc.name) } }) } } func TestSubIssueCreationSectionSkippedForNonIssueModes(t *testing.T) { t.Parallel() cases := []struct { name string ctx TaskContextForEnv }{ { name: "chat", ctx: TaskContextForEnv{ChatSessionID: "chat-1"}, }, { name: "quick-create", ctx: TaskContextForEnv{QuickCreatePrompt: "create me an issue"}, }, { name: "autopilot run-only", ctx: TaskContextForEnv{AutopilotRunID: "run-1"}, }, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() out := buildMetaSkillContent("claude", tc.ctx) if strings.Contains(out, "## Sub-issue Creation") { t.Errorf("%s mode must NOT emit the Sub-issue Creation section", tc.name) } }) } } // writeRuntimeConfigFile is the safe replacement for the previous // unconditional os.WriteFile of CLAUDE.md / AGENTS.md / GEMINI.md. The three // states it must handle correctly are: file missing, file present without // markers (user-authored content already there — the regression case from // MUL-2753), and file present with markers (idempotent second-run replace). func TestWriteRuntimeConfigFileCreatesMissingFile(t *testing.T) { t.Parallel() dir := t.TempDir() path := filepath.Join(dir, "CLAUDE.md") const brief = "# Multica Agent Runtime\n\nbrief body line" if err := writeRuntimeConfigFile(path, brief); err != nil { t.Fatalf("writeRuntimeConfigFile returned error: %v", err) } got, err := os.ReadFile(path) if err != nil { t.Fatalf("read back file: %v", err) } s := string(got) if !strings.HasPrefix(s, runtimeMarkerBegin+"\n") { t.Errorf("output should start with begin marker, got:\n%s", s) } if !strings.Contains(s, brief) { t.Errorf("output should contain brief body, got:\n%s", s) } if !strings.Contains(s, "\n"+runtimeMarkerEnd+"\n") { t.Errorf("output should contain end marker followed by newline, got:\n%s", s) } } func TestWriteRuntimeConfigFilePreservesUserContent(t *testing.T) { t.Parallel() dir := t.TempDir() path := filepath.Join(dir, "CLAUDE.md") const userContent = "# User repo CLAUDE.md\n\n- rule one\n- rule two\n" if err := os.WriteFile(path, []byte(userContent), 0o644); err != nil { t.Fatalf("seed user file: %v", err) } const brief = "## Multica brief\n\ninjected body" if err := writeRuntimeConfigFile(path, brief); err != nil { t.Fatalf("writeRuntimeConfigFile returned error: %v", err) } got, err := os.ReadFile(path) if err != nil { t.Fatalf("read back file: %v", err) } s := string(got) // The user's original content must be untouched and appear before the // injected marker block; this is the core regression case from MUL-2753. if !strings.HasPrefix(s, userContent) { t.Errorf("user content must be preserved verbatim at the top of the file, got:\n%s", s) } beginIdx := strings.Index(s, runtimeMarkerBegin) endIdx := strings.Index(s, runtimeMarkerEnd) if beginIdx < 0 || endIdx <= beginIdx { t.Fatalf("expected a well-formed marker block in:\n%s", s) } if beginIdx < len(userContent) { t.Errorf("begin marker must appear after user content, beginIdx=%d userLen=%d", beginIdx, len(userContent)) } if !strings.Contains(s, brief) { t.Errorf("brief body missing from output:\n%s", s) } } func TestWriteRuntimeConfigFileReplacesExistingBlock(t *testing.T) { t.Parallel() dir := t.TempDir() path := filepath.Join(dir, "AGENTS.md") const userBefore = "# User AGENTS.md\n\nuser line above\n" const userAfter = "\nuser line below the block\n" original := userBefore + runtimeMarkerBegin + "\n" + "OLD BRIEF CONTENT THAT MUST GO AWAY\n" + runtimeMarkerEnd + "\n" + userAfter if err := os.WriteFile(path, []byte(original), 0o644); err != nil { t.Fatalf("seed: %v", err) } const newBrief = "## New Multica brief\n\nfresh body" if err := writeRuntimeConfigFile(path, newBrief); err != nil { t.Fatalf("writeRuntimeConfigFile returned error: %v", err) } got, err := os.ReadFile(path) if err != nil { t.Fatalf("read back file: %v", err) } s := string(got) if !strings.HasPrefix(s, userBefore) { t.Errorf("content above the marker block must be preserved, got:\n%s", s) } if !strings.HasSuffix(s, userAfter) { t.Errorf("content below the marker block must be preserved, got:\n%s", s) } if strings.Contains(s, "OLD BRIEF CONTENT THAT MUST GO AWAY") { t.Errorf("previous block body must be replaced, got:\n%s", s) } if !strings.Contains(s, newBrief) { t.Errorf("new brief body missing from output:\n%s", s) } if strings.Count(s, runtimeMarkerBegin) != 1 || strings.Count(s, runtimeMarkerEnd) != 1 { t.Errorf("there must be exactly one begin/end marker pair, got:\n%s", s) } } func TestWriteRuntimeConfigFileIsIdempotent(t *testing.T) { t.Parallel() dir := t.TempDir() path := filepath.Join(dir, "CLAUDE.md") const userContent = "# User CLAUDE.md\n\nimportant rules\n" if err := os.WriteFile(path, []byte(userContent), 0o644); err != nil { t.Fatalf("seed user file: %v", err) } const brief = "## Multica brief\n\nbody" for i := 0; i < 5; i++ { if err := writeRuntimeConfigFile(path, brief); err != nil { t.Fatalf("iteration %d: %v", i, err) } } got, err := os.ReadFile(path) if err != nil { t.Fatalf("read back file: %v", err) } s := string(got) if strings.Count(s, runtimeMarkerBegin) != 1 { t.Errorf("repeated runs must not duplicate the begin marker, count=%d, file:\n%s", strings.Count(s, runtimeMarkerBegin), s) } if strings.Count(s, runtimeMarkerEnd) != 1 { t.Errorf("repeated runs must not duplicate the end marker, count=%d, file:\n%s", strings.Count(s, runtimeMarkerEnd), s) } if strings.Count(s, brief) != 1 { t.Errorf("repeated runs must not duplicate the brief body, count=%d, file:\n%s", strings.Count(s, brief), s) } if !strings.HasPrefix(s, userContent) { t.Errorf("user content must remain intact at the top of the file, got:\n%s", s) } } // InjectRuntimeConfig is the production entry point — verify the marker // semantics propagate through it for each provider's target filename. func TestInjectRuntimeConfigPreservesUserContent(t *testing.T) { t.Parallel() cases := []struct { provider string filename string }{ {"claude", "CLAUDE.md"}, {"codex", "AGENTS.md"}, {"copilot", "AGENTS.md"}, {"opencode", "AGENTS.md"}, {"openclaw", "AGENTS.md"}, {"hermes", "AGENTS.md"}, {"pi", "AGENTS.md"}, {"cursor", "AGENTS.md"}, {"kimi", "AGENTS.md"}, {"kiro", "AGENTS.md"}, {"antigravity", "AGENTS.md"}, {"gemini", "GEMINI.md"}, } for _, tc := range cases { tc := tc t.Run(tc.provider, func(t *testing.T) { t.Parallel() dir := t.TempDir() path := filepath.Join(dir, tc.filename) const userContent = "# User-authored file\n\ndon't touch this\n" if err := os.WriteFile(path, []byte(userContent), 0o644); err != nil { t.Fatalf("seed: %v", err) } content, err := InjectRuntimeConfig(dir, tc.provider, TaskContextForEnv{ IssueID: "11111111-2222-3333-4444-555555555555", }) if err != nil { t.Fatalf("InjectRuntimeConfig: %v", err) } if content == "" { t.Fatalf("returned brief content must be non-empty") } got, err := os.ReadFile(path) if err != nil { t.Fatalf("read back: %v", err) } s := string(got) if !strings.HasPrefix(s, userContent) { t.Errorf("[%s] user content must be preserved verbatim at the top of %s, got:\n%s", tc.provider, tc.filename, s) } if !strings.Contains(s, runtimeMarkerBegin) || !strings.Contains(s, runtimeMarkerEnd) { t.Errorf("[%s] %s must contain the runtime marker block, got:\n%s", tc.provider, tc.filename, s) } }) } } func TestInjectRuntimeConfigUnknownProviderSkipsWrite(t *testing.T) { t.Parallel() dir := t.TempDir() // Seed all three candidate filenames so we can verify none of them get // written when the provider is unknown. for _, name := range []string{"CLAUDE.md", "AGENTS.md", "GEMINI.md"} { if err := os.WriteFile(filepath.Join(dir, name), []byte("untouched\n"), 0o644); err != nil { t.Fatalf("seed %s: %v", name, err) } } if _, err := InjectRuntimeConfig(dir, "totally-unknown-provider", TaskContextForEnv{ IssueID: "11111111-2222-3333-4444-555555555555", }); err != nil { t.Fatalf("InjectRuntimeConfig: %v", err) } for _, name := range []string{"CLAUDE.md", "AGENTS.md", "GEMINI.md"} { got, err := os.ReadFile(filepath.Join(dir, name)) if err != nil { t.Fatalf("read %s: %v", name, err) } if string(got) != "untouched\n" { t.Errorf("unknown provider must not write %s; got:\n%s", name, string(got)) } } } // Parser hardening: the end marker must be found strictly after the begin // marker so a stray end marker that appears earlier in user content (e.g. // a documentation snippet showing what the wire format looks like) doesn't // trick writeRuntimeConfigFile into thinking the file is malformed and // appending another block on every run. func TestWriteRuntimeConfigFileIgnoresStrayEndMarkerBeforeBegin(t *testing.T) { t.Parallel() dir := t.TempDir() path := filepath.Join(dir, "CLAUDE.md") // Seed a file whose user-authored portion documents the marker format // (so the *end* marker appears before any *begin* marker), then has a // real block authored by an earlier Multica run below. const userDoc = "# Repo CLAUDE.md\n\nExample of what Multica writes:\n" + runtimeMarkerEnd + "\n\n# Real config below\n" original := userDoc + runtimeMarkerBegin + "\nFIRST BRIEF\n" + runtimeMarkerEnd + "\n" if err := os.WriteFile(path, []byte(original), 0o644); err != nil { t.Fatalf("seed: %v", err) } const newBrief = "SECOND BRIEF" if err := writeRuntimeConfigFile(path, newBrief); err != nil { t.Fatalf("writeRuntimeConfigFile: %v", err) } got, err := os.ReadFile(path) if err != nil { t.Fatalf("read back: %v", err) } s := string(got) // The user's stray end marker line plus surrounding doc text must still // be present, and the file must contain exactly one begin marker and // one *additional* end marker (so two end markers total — the stray // one and the one closing our block). if !strings.Contains(s, userDoc) { t.Errorf("user doc with stray end marker must be preserved verbatim, got:\n%s", s) } if got, want := strings.Count(s, runtimeMarkerBegin), 1; got != want { t.Errorf("expected exactly %d begin markers, got %d:\n%s", want, got, s) } if got, want := strings.Count(s, runtimeMarkerEnd), 2; got != want { t.Errorf("expected exactly %d end markers (1 user stray + 1 closing our block), got %d:\n%s", want, got, s) } if strings.Contains(s, "FIRST BRIEF") { t.Errorf("previous brief body must be replaced, got:\n%s", s) } if !strings.Contains(s, newBrief) { t.Errorf("new brief body missing from output:\n%s", s) } // Idempotency under the stray-end pattern: a second write must not // stack another block. if err := writeRuntimeConfigFile(path, newBrief); err != nil { t.Fatalf("second writeRuntimeConfigFile: %v", err) } got2, _ := os.ReadFile(path) s2 := string(got2) if got, want := strings.Count(s2, runtimeMarkerBegin), 1; got != want { t.Errorf("repeat write must not grow begin markers, got %d, want %d:\n%s", got, want, s2) } } // Parser hardening: a file containing only a begin marker (e.g. a previous // run that crashed mid-write) must not cause every subsequent run to stack // another block beneath the half-block. func TestWriteRuntimeConfigFileReplacesMalformedHalfBlock(t *testing.T) { t.Parallel() dir := t.TempDir() path := filepath.Join(dir, "AGENTS.md") const userTop = "# Repo AGENTS.md\n\nrules above\n" const halfBlock = "leftover from crashed write\nsecond line\n" original := userTop + runtimeMarkerBegin + "\n" + halfBlock if err := os.WriteFile(path, []byte(original), 0o644); err != nil { t.Fatalf("seed: %v", err) } const newBrief = "recovered brief" if err := writeRuntimeConfigFile(path, newBrief); err != nil { t.Fatalf("writeRuntimeConfigFile: %v", err) } got, err := os.ReadFile(path) if err != nil { t.Fatalf("read back: %v", err) } s := string(got) if !strings.HasPrefix(s, userTop) { t.Errorf("user content above the half-block must be preserved, got:\n%s", s) } if strings.Contains(s, "leftover from crashed write") { t.Errorf("half-block contents must be replaced, got:\n%s", s) } if got, want := strings.Count(s, runtimeMarkerBegin), 1; got != want { t.Errorf("expected exactly %d begin marker, got %d:\n%s", want, got, s) } if got, want := strings.Count(s, runtimeMarkerEnd), 1; got != want { t.Errorf("expected exactly %d end marker after recovery, got %d:\n%s", want, got, s) } if !strings.Contains(s, newBrief) { t.Errorf("new brief body missing from output:\n%s", s) } } // Cleanup excises the marker block, preserving every byte of surrounding // user content. This is the local_directory invariant: a `claude` / // `codex` run started by the user after a Multica task must see the same // file the user wrote. func TestCleanupRuntimeConfigPreservesUserContent(t *testing.T) { t.Parallel() dir := t.TempDir() path := filepath.Join(dir, "CLAUDE.md") const userBefore = "# Repo CLAUDE.md\n\nuser line above\n" const userAfter = "\nuser line below the block\n" const userExpected = "# Repo CLAUDE.md\n\nuser line above\n\nuser line below the block\n" // Inject via the production write path so we exercise the actual // marker block format, not a hand-rolled approximation. if err := os.WriteFile(path, []byte(userBefore+userAfter), 0o644); err != nil { t.Fatalf("seed: %v", err) } if err := writeRuntimeConfigFile(path, "brief body"); err != nil { t.Fatalf("seed brief: %v", err) } if err := CleanupRuntimeConfig(dir, "claude"); err != nil { t.Fatalf("CleanupRuntimeConfig: %v", err) } got, err := os.ReadFile(path) if err != nil { t.Fatalf("read back: %v", err) } s := string(got) if strings.Contains(s, runtimeMarkerBegin) || strings.Contains(s, runtimeMarkerEnd) { t.Errorf("marker block must be removed, got:\n%s", s) } if strings.Contains(s, "brief body") { t.Errorf("brief body must be removed, got:\n%s", s) } if s != userExpected { t.Errorf("user content must be preserved byte-for-byte\n got:\n%q\nwant:\n%q", s, userExpected) } } // Cleanup removes the file entirely when the marker block was the only // content — i.e. we created the file from scratch in a directory that had // no pre-existing CLAUDE.md / AGENTS.md / GEMINI.md. func TestCleanupRuntimeConfigRemovesFileWhenOnlyBlockRemained(t *testing.T) { t.Parallel() dir := t.TempDir() path := filepath.Join(dir, "CLAUDE.md") // No seed — writeRuntimeConfigFile creates the file with only the // marker block inside. if err := writeRuntimeConfigFile(path, "brief body"); err != nil { t.Fatalf("seed brief: %v", err) } if err := CleanupRuntimeConfig(dir, "claude"); err != nil { t.Fatalf("CleanupRuntimeConfig: %v", err) } if _, err := os.Stat(path); !os.IsNotExist(err) { t.Errorf("expected file to be removed, stat err=%v", err) } } // Cleanup is a no-op when no marker block exists or when the file is // missing — Cleanup is safe to call defensively from the daemon's defer. func TestCleanupRuntimeConfigNoOpCases(t *testing.T) { t.Parallel() t.Run("missing file", func(t *testing.T) { t.Parallel() dir := t.TempDir() if err := CleanupRuntimeConfig(dir, "claude"); err != nil { t.Errorf("missing file must be no-op, got: %v", err) } // And the directory must remain untouched. entries, err := os.ReadDir(dir) if err != nil { t.Fatalf("readdir: %v", err) } if len(entries) != 0 { t.Errorf("expected dir to remain empty, got: %v", entries) } }) t.Run("file without marker block", func(t *testing.T) { t.Parallel() dir := t.TempDir() path := filepath.Join(dir, "CLAUDE.md") const userContent = "# Repo CLAUDE.md\n\nrules\n" if err := os.WriteFile(path, []byte(userContent), 0o644); err != nil { t.Fatalf("seed: %v", err) } if err := CleanupRuntimeConfig(dir, "claude"); err != nil { t.Errorf("no-marker-block file must be no-op, got: %v", err) } got, err := os.ReadFile(path) if err != nil { t.Fatalf("read back: %v", err) } if string(got) != userContent { t.Errorf("file must be untouched\n got:\n%q\nwant:\n%q", string(got), userContent) } }) t.Run("unknown provider", func(t *testing.T) { t.Parallel() dir := t.TempDir() // Seed every candidate filename to verify none of them get touched. for _, name := range []string{"CLAUDE.md", "AGENTS.md", "GEMINI.md"} { if err := os.WriteFile(filepath.Join(dir, name), []byte("untouched\n"), 0o644); err != nil { t.Fatalf("seed %s: %v", name, err) } } if err := CleanupRuntimeConfig(dir, "totally-unknown-provider"); err != nil { t.Errorf("unknown provider must be no-op, got: %v", err) } for _, name := range []string{"CLAUDE.md", "AGENTS.md", "GEMINI.md"} { got, err := os.ReadFile(filepath.Join(dir, name)) if err != nil { t.Fatalf("read %s: %v", name, err) } if string(got) != "untouched\n" { t.Errorf("unknown provider must not touch %s; got:\n%s", name, string(got)) } } }) } // Cleanup must handle a half-block left by a previous crashed run: begin // marker present but no end. Otherwise the half-block would survive // cleanup and pollute the next manual CLI invocation in the same dir. func TestCleanupRuntimeConfigRemovesMalformedHalfBlock(t *testing.T) { t.Parallel() dir := t.TempDir() path := filepath.Join(dir, "AGENTS.md") const userTop = "# Repo AGENTS.md\n\nrules\n" original := userTop + runtimeMarkerBegin + "\nhalf-written brief no end\n" if err := os.WriteFile(path, []byte(original), 0o644); err != nil { t.Fatalf("seed: %v", err) } if err := CleanupRuntimeConfig(dir, "codex"); err != nil { t.Fatalf("CleanupRuntimeConfig: %v", err) } got, err := os.ReadFile(path) if err != nil { t.Fatalf("read back: %v", err) } s := string(got) if strings.Contains(s, runtimeMarkerBegin) { t.Errorf("half-block begin marker must be excised, got:\n%s", s) } if strings.Contains(s, "half-written brief no end") { t.Errorf("half-block body must be excised, got:\n%s", s) } if !strings.HasPrefix(s, userTop) { t.Errorf("user content above the half-block must remain, got:\n%s", s) } } // Cleanup must remove the marker block for every provider's target file, // using the same provider→filename mapping as InjectRuntimeConfig — so a // new provider added to one side cannot drift past the other. func TestCleanupRuntimeConfigByProvider(t *testing.T) { t.Parallel() cases := []struct { provider string filename string }{ {"claude", "CLAUDE.md"}, {"codex", "AGENTS.md"}, {"copilot", "AGENTS.md"}, {"opencode", "AGENTS.md"}, {"openclaw", "AGENTS.md"}, {"hermes", "AGENTS.md"}, {"pi", "AGENTS.md"}, {"cursor", "AGENTS.md"}, {"kimi", "AGENTS.md"}, {"kiro", "AGENTS.md"}, {"antigravity", "AGENTS.md"}, {"gemini", "GEMINI.md"}, } for _, tc := range cases { tc := tc t.Run(tc.provider, func(t *testing.T) { t.Parallel() dir := t.TempDir() path := filepath.Join(dir, tc.filename) const userContent = "# User file\n\ndon't touch this\n" if err := os.WriteFile(path, []byte(userContent), 0o644); err != nil { t.Fatalf("seed: %v", err) } // Inject through the production path so cleanup runs against // the same wire format the agent saw. if _, err := InjectRuntimeConfig(dir, tc.provider, TaskContextForEnv{ IssueID: "11111111-2222-3333-4444-555555555555", }); err != nil { t.Fatalf("InjectRuntimeConfig: %v", err) } if err := CleanupRuntimeConfig(dir, tc.provider); err != nil { t.Fatalf("CleanupRuntimeConfig: %v", err) } got, err := os.ReadFile(path) if err != nil { t.Fatalf("read back: %v", err) } s := string(got) if strings.Contains(s, runtimeMarkerBegin) || strings.Contains(s, runtimeMarkerEnd) { t.Errorf("[%s] marker block must be removed from %s, got:\n%s", tc.provider, tc.filename, s) } if s != userContent { t.Errorf("[%s] user content in %s must be preserved byte-for-byte\n got:\n%q\nwant:\n%q", tc.provider, tc.filename, s, userContent) } }) } } // Inject → Cleanup → manual edit → Inject must converge back to the // pre-injection state on the next Cleanup. This is the end-to-end // regression that locks in: the user's repo is byte-identical to what // they had before the task, every task cycle. func TestInjectThenCleanupRoundTrip(t *testing.T) { t.Parallel() dir := t.TempDir() path := filepath.Join(dir, "CLAUDE.md") const userContent = "# User-authored CLAUDE.md\n\n- rule A\n- rule B\n" if err := os.WriteFile(path, []byte(userContent), 0o644); err != nil { t.Fatalf("seed: %v", err) } // Two full inject→cleanup cycles — covers both the "first task on a // fresh user file" path and the "subsequent task hits a clean file // again" path. for i := 0; i < 2; i++ { if _, err := InjectRuntimeConfig(dir, "claude", TaskContextForEnv{ IssueID: "11111111-2222-3333-4444-555555555555", }); err != nil { t.Fatalf("iter %d inject: %v", i, err) } if err := CleanupRuntimeConfig(dir, "claude"); err != nil { t.Fatalf("iter %d cleanup: %v", i, err) } got, err := os.ReadFile(path) if err != nil { t.Fatalf("iter %d read back: %v", i, err) } if string(got) != userContent { t.Errorf("iter %d: user file must be byte-identical to pre-injection state\n got:\n%q\nwant:\n%q", i, string(got), userContent) } } } // Byte-exact boundary coverage flagged in PR #3438 review (Elon): the // previous cleanup used TrimRight + "\n" and TrimSpace-based file removal, // which created a real diff in three boundary cases. The table walks each // one through a full inject→cleanup cycle and asserts the file ends up // byte-identical (or, for missing-file, that it stays missing). func TestInjectThenCleanupRoundTripByteExactBoundaries(t *testing.T) { t.Parallel() cases := []struct { name string // seed describes the pre-inject filesystem state. When seedExists // is false the file is absent; when true the file is created with // seedContent (which may be empty / whitespace-only / arbitrary // bytes). seedExists bool seedContent string }{ { name: "file missing — Inject creates, Cleanup removes", seedExists: false, seedContent: "", }, { name: "pre-existing empty file (zero bytes)", seedExists: true, seedContent: "", }, { name: "pre-existing whitespace-only file", seedExists: true, seedContent: " \n", }, { name: "no trailing newline", seedExists: true, seedContent: "rules", }, { name: "one trailing newline (the common markdown shape)", seedExists: true, seedContent: "# Rules\n\nbody\n", }, { name: "two trailing newlines", seedExists: true, seedContent: "rules\n\n", }, { name: "many trailing newlines", seedExists: true, seedContent: "rules\n\n\n\n", }, { name: "CRLF line endings", seedExists: true, seedContent: "rule A\r\nrule B\r\n", }, { name: "no final newline AND embedded blank lines", seedExists: true, seedContent: "para 1\n\npara 2\n\npara 3", }, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() dir := t.TempDir() path := filepath.Join(dir, "CLAUDE.md") if tc.seedExists { if err := os.WriteFile(path, []byte(tc.seedContent), 0o644); err != nil { t.Fatalf("seed: %v", err) } } // Two cycles to cover both "first inject hits user file" and // "subsequent inject hits a cleaned file" paths. for i := 0; i < 2; i++ { if _, err := InjectRuntimeConfig(dir, "claude", TaskContextForEnv{ IssueID: "11111111-2222-3333-4444-555555555555", }); err != nil { t.Fatalf("iter %d inject: %v", i, err) } if err := CleanupRuntimeConfig(dir, "claude"); err != nil { t.Fatalf("iter %d cleanup: %v", i, err) } if !tc.seedExists { // Missing file must remain missing after the cycle so // the user's directory listing is also byte-identical // (no zero-byte stub left behind). if _, err := os.Stat(path); !os.IsNotExist(err) { t.Errorf("iter %d: file must remain missing, stat err=%v", i, err) } continue } got, err := os.ReadFile(path) if err != nil { t.Fatalf("iter %d read back: %v", i, err) } if string(got) != tc.seedContent { t.Errorf("iter %d: file must be byte-identical to seed\n got: %q\n want: %q", i, string(got), tc.seedContent) } } }) } } // Idempotency across the byte-exact boundaries: when a second Inject runs // against a file that already carries a marker block (the "replace in // place" branch), the surrounding bytes must stay untouched and the // subsequent Cleanup must still restore the user's original file // byte-exactly. This guards against a regression where the replace path // would re-normalise pre/post bytes the way the old cleanup did. func TestInjectReplaceThenCleanupRestoresByteExact(t *testing.T) { t.Parallel() cases := []struct { name string seedContent string }{ {name: "no trailing newline", seedContent: "rules"}, {name: "two trailing newlines", seedContent: "rules\n\n"}, {name: "empty file", seedContent: ""}, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() dir := t.TempDir() path := filepath.Join(dir, "CLAUDE.md") if err := os.WriteFile(path, []byte(tc.seedContent), 0o644); err != nil { t.Fatalf("seed: %v", err) } // First inject — append path. if _, err := InjectRuntimeConfig(dir, "claude", TaskContextForEnv{ IssueID: "11111111-2222-3333-4444-555555555555", }); err != nil { t.Fatalf("first inject: %v", err) } // Second inject — replace-in-place path. if _, err := InjectRuntimeConfig(dir, "claude", TaskContextForEnv{ IssueID: "11111111-2222-3333-4444-555555555555", }); err != nil { t.Fatalf("second inject: %v", err) } if err := CleanupRuntimeConfig(dir, "claude"); err != nil { t.Fatalf("cleanup: %v", err) } got, err := os.ReadFile(path) if err != nil { t.Fatalf("read back: %v", err) } if string(got) != tc.seedContent { t.Errorf("file must be byte-identical to seed after replace+cleanup\n got: %q\n want: %q", string(got), tc.seedContent) } }) } } // The fixed managed separator is the invariant that makes byte-exact // cleanup possible. This test pins it: writeRuntimeConfigFile must // produce exactly `<\n\n>` for ANY non-empty // or empty pre-existing file, with no trailing-newline normalisation. func TestWriteRuntimeConfigFileAlwaysInsertsFixedManagedSeparator(t *testing.T) { t.Parallel() for _, seed := range []string{"", "rules", "rules\n", "rules\n\n", "rules\n\n\n\n"} { seed := seed t.Run(fmt.Sprintf("seed=%q", seed), func(t *testing.T) { t.Parallel() dir := t.TempDir() path := filepath.Join(dir, "CLAUDE.md") if err := os.WriteFile(path, []byte(seed), 0o644); err != nil { t.Fatalf("seed: %v", err) } if err := writeRuntimeConfigFile(path, "brief body"); err != nil { t.Fatalf("write: %v", err) } got, err := os.ReadFile(path) if err != nil { t.Fatalf("read back: %v", err) } s := string(got) // The seed must appear verbatim at the start of the file — // no extra newline appended, no trailing newline trimmed. if !strings.HasPrefix(s, seed) { t.Errorf("seed bytes must survive verbatim at the start of the file\n got: %q\n seed: %q", s, seed) } // Immediately after the seed we must see the fixed managed // separator, then the begin marker. markerStart := len(seed) + len(runtimeManagedSeparator) if len(s) < markerStart+len(runtimeMarkerBegin) { t.Fatalf("file shorter than expected layout\n got: %q", s) } if got, want := s[len(seed):markerStart], runtimeManagedSeparator; got != want { t.Errorf("expected managed separator %q immediately after seed, got %q", want, got) } if got, want := s[markerStart:markerStart+len(runtimeMarkerBegin)], runtimeMarkerBegin; got != want { t.Errorf("expected begin marker after managed separator, got %q", got) } }) } }