mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-16 22:59:04 +02:00
* fix: bind quick-create attachments to created issues Co-authored-by: multica-agent <github@multica.ai> * test: use real image markdown in quick-create attachment test Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
535 lines
21 KiB
Go
535 lines
21 KiB
Go
package daemon
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestBuildQuickCreatePromptRules locks in the rules that govern how the
|
|
// quick-create agent is allowed to translate raw user input into the issue
|
|
// description body. Each substring corresponds to a concrete failure mode
|
|
// observed in production output:
|
|
// - meta-instructions ("create an issue", "cc @X") leaking into the body
|
|
// - the Context section being misused as an apology log when no external
|
|
// references were actually fetched
|
|
// - hard-line rules being silently dropped on prompt rewrites
|
|
func TestBuildQuickCreatePromptRules(t *testing.T) {
|
|
out := buildQuickCreatePrompt(Task{QuickCreatePrompt: "fix the login button color"})
|
|
|
|
mustContain := []string{
|
|
// high-fidelity invariant
|
|
"Faithfully restate what the user wants",
|
|
"Preserve specific names, identifiers, file paths",
|
|
// strip non-spec material: verbal routing wrappers + conversational fillers
|
|
"verbal routing wrappers about creating the issue",
|
|
"pure conversational fillers",
|
|
// cc routing must survive: mention link stays in description so the
|
|
// auto-subscribe path fires (multica issue create has no --subscriber flag)
|
|
"CC exception",
|
|
"auto-subscribes members",
|
|
// context section is conditional and must not be an apology log
|
|
"include ONLY when the input cited external resources",
|
|
"never use it as an apology log",
|
|
// output/reporting must be workspace-prefix agnostic. Workspaces can
|
|
// use custom issue prefixes, so a successful issue creation should
|
|
// not look failed merely because the identifier does not match one
|
|
// fixed prefix.
|
|
"multica issue create --output json",
|
|
"JSON response",
|
|
"identifier",
|
|
"Do not scrape human output",
|
|
"do not assume any workspace issue prefix",
|
|
"Created <identifier-or-id>: <title>",
|
|
// hard rules
|
|
"never invent requirements",
|
|
"never reduce multi-sentence input",
|
|
}
|
|
for _, s := range mustContain {
|
|
if !strings.Contains(out, s) {
|
|
t.Errorf("buildQuickCreatePrompt output missing required rule: %q", s)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestBuildQuickCreatePromptAssigneeIncludesSquads locks in the MUL-2165
|
|
// fix: the assignee-resolution rules must tell the agent to consult the
|
|
// squad list alongside members and agents. Before this, a quick-create
|
|
// input like "assign to <SquadName>" silently fell through to
|
|
// "Unrecognized assignee" because squads were never queried.
|
|
func TestBuildQuickCreatePromptAssigneeIncludesSquads(t *testing.T) {
|
|
out := buildQuickCreatePrompt(Task{QuickCreatePrompt: "fix the login button color"})
|
|
mustContain := []string{
|
|
"multica squad list",
|
|
"Squads are first-class assignees",
|
|
"Treat bare @-routing as an assignee directive",
|
|
"让 @独立团 review 这个 PR",
|
|
"pass the squad's `id` as `--assignee-id`",
|
|
}
|
|
for _, s := range mustContain {
|
|
if !strings.Contains(out, s) {
|
|
t.Errorf("buildQuickCreatePrompt assignee block missing %q\n--- output ---\n%s", s, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestBuildQuickCreatePromptSquadDefaultsToSquad locks in the MUL-2203
|
|
// fix: when the picker was a squad, the task runs on the squad's leader
|
|
// agent, but the default assignee for issues created by this run must
|
|
// point at the SQUAD's UUID — not the leader agent's UUID. The previous
|
|
// "default to YOURSELF" instruction made squad-created issues land under
|
|
// the leader, hiding them from the squad's delegation flow.
|
|
func TestBuildQuickCreatePromptSquadDefaultsToSquad(t *testing.T) {
|
|
const (
|
|
squadID = "aaaa1111-2222-3333-4444-555555555555"
|
|
squadName = "独立团"
|
|
leaderID = "bbbb1111-2222-3333-4444-666666666666"
|
|
)
|
|
out := buildQuickCreatePrompt(Task{
|
|
QuickCreatePrompt: "fix the login button color",
|
|
Agent: &AgentData{ID: leaderID, Name: "leader-agent"},
|
|
SquadID: squadID,
|
|
SquadName: squadName,
|
|
})
|
|
|
|
// The default-assignee instruction must point at the squad UUID.
|
|
if !strings.Contains(out, "--assignee-id \""+squadID+"\"") {
|
|
t.Errorf("buildQuickCreatePrompt with SquadID must default to the squad's UUID, got:\n%s", out)
|
|
}
|
|
// And it must NOT tell the agent to default to itself (the leader).
|
|
if strings.Contains(out, "--assignee-id \""+leaderID+"\"") {
|
|
t.Errorf("buildQuickCreatePrompt with SquadID must NOT default to the leader agent's UUID, got:\n%s", out)
|
|
}
|
|
// The squad name should appear in the instruction so the agent has
|
|
// human-readable context for the routing decision.
|
|
if !strings.Contains(out, squadName) {
|
|
t.Errorf("buildQuickCreatePrompt with SquadID should mention the squad name %q, got:\n%s", squadName, out)
|
|
}
|
|
// And the prompt must explicitly call out the squad-vs-leader rule
|
|
// so the agent does not silently regress to "default to YOURSELF".
|
|
mustContain := []string{
|
|
"picker SQUAD",
|
|
"running on the squad's behalf",
|
|
"do not assign it to your own agent UUID",
|
|
}
|
|
for _, s := range mustContain {
|
|
if !strings.Contains(out, s) {
|
|
t.Errorf("buildQuickCreatePrompt with SquadID missing %q\n--- output ---\n%s", s, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestBuildQuickCreatePromptProjectPinning verifies that when the user
|
|
// pins a project in the quick-create modal, the prompt instructs the agent
|
|
// to pass `--project <uuid>` exactly. Without this, the agent would re-read
|
|
// the workspace default and silently drop the user's selection — the same
|
|
// "I have to retype 'in project X' every time" failure mode the modal
|
|
// addition was meant to fix.
|
|
func TestBuildQuickCreatePromptProjectPinning(t *testing.T) {
|
|
const projectID = "11111111-2222-3333-4444-555555555555"
|
|
out := buildQuickCreatePrompt(Task{
|
|
QuickCreatePrompt: "fix the login button color",
|
|
ProjectID: projectID,
|
|
ProjectTitle: "Web App",
|
|
})
|
|
mustContain := []string{
|
|
"--project \"" + projectID + "\"",
|
|
"Web App",
|
|
"modal selection is authoritative",
|
|
}
|
|
for _, s := range mustContain {
|
|
if !strings.Contains(out, s) {
|
|
t.Errorf("buildQuickCreatePrompt with project missing %q\n--- output ---\n%s", s, out)
|
|
}
|
|
}
|
|
|
|
// Without a project, the prompt must keep the legacy "omit" instruction
|
|
// so the agent doesn't accidentally start passing --project on plain
|
|
// quick-create runs.
|
|
plain := buildQuickCreatePrompt(Task{QuickCreatePrompt: "fix the login button color"})
|
|
if !strings.Contains(plain, "**project**: omit") {
|
|
t.Errorf("buildQuickCreatePrompt without project must keep the omit instruction, got:\n%s", plain)
|
|
}
|
|
if strings.Contains(plain, "--project") {
|
|
t.Errorf("buildQuickCreatePrompt without project must NOT mention --project, got:\n%s", plain)
|
|
}
|
|
}
|
|
|
|
// TestBuildQuickCreatePromptParentPinning verifies that when the user
|
|
// opened quick-create from "Add sub issue" on an existing issue, the prompt
|
|
// instructs the agent to pass `--parent <uuid>` so the new issue is filed
|
|
// as a sub-issue. The frontend already seeds parent_issue_id silently
|
|
// through the manual→agent switch, so this is the last hop that has to
|
|
// hold up — without the prompt instruction the agent would create a
|
|
// standalone issue and the sub-issue relationship would be silently
|
|
// dropped.
|
|
func TestBuildQuickCreatePromptParentPinning(t *testing.T) {
|
|
const (
|
|
parentID = "33333333-2222-1111-4444-555555555555"
|
|
parentIdentifier = "MUL-2534"
|
|
)
|
|
out := buildQuickCreatePrompt(Task{
|
|
QuickCreatePrompt: "fix the login button color",
|
|
ParentIssueID: parentID,
|
|
ParentIssueIdentifier: parentIdentifier,
|
|
})
|
|
mustContain := []string{
|
|
"--parent \"" + parentID + "\"",
|
|
parentIdentifier,
|
|
"modal entry point is authoritative",
|
|
"filed as a sub-issue",
|
|
}
|
|
for _, s := range mustContain {
|
|
if !strings.Contains(out, s) {
|
|
t.Errorf("buildQuickCreatePrompt with parent missing %q\n--- output ---\n%s", s, out)
|
|
}
|
|
}
|
|
|
|
// When only the UUID is available (identifier lookup failed on claim),
|
|
// the agent must still get the --parent instruction so the sub-issue
|
|
// intent isn't silently dropped.
|
|
uuidOnly := buildQuickCreatePrompt(Task{
|
|
QuickCreatePrompt: "fix the login button color",
|
|
ParentIssueID: parentID,
|
|
})
|
|
if !strings.Contains(uuidOnly, "--parent \""+parentID+"\"") {
|
|
t.Errorf("buildQuickCreatePrompt with parent UUID only must still pin --parent, got:\n%s", uuidOnly)
|
|
}
|
|
|
|
// Without a parent, the prompt must NOT mention --parent at all — a
|
|
// plain quick-create run should not start filing sub-issues.
|
|
plain := buildQuickCreatePrompt(Task{QuickCreatePrompt: "fix the login button color"})
|
|
if strings.Contains(plain, "--parent") {
|
|
t.Errorf("buildQuickCreatePrompt without parent must NOT mention --parent, got:\n%s", plain)
|
|
}
|
|
}
|
|
|
|
// TestBuildPromptSquadLeaderNoActionForMemberTrigger verifies that the
|
|
// squad leader no_action prohibition is injected in the per-turn prompt
|
|
// regardless of whether the triggering comment was posted by an agent or
|
|
// a member. This was the root cause of the "LGTM is a pure acknowledgment
|
|
// — no reply needed. Exiting silently." noise comment: the prohibition
|
|
// only fired for agent-triggered comments, so member-triggered ones
|
|
// (like "LGTM") bypassed it.
|
|
func TestBuildPromptSquadLeaderNoActionForMemberTrigger(t *testing.T) {
|
|
task := Task{
|
|
IssueID: "issue-123",
|
|
TriggerCommentID: "comment-456",
|
|
TriggerCommentContent: "LGTM",
|
|
TriggerAuthorType: "member",
|
|
TriggerAuthorName: "Bohan",
|
|
Agent: &AgentData{
|
|
Instructions: "Some instructions\n\n## Squad Operating Protocol\n\nYou are the LEADER...",
|
|
},
|
|
}
|
|
out := BuildPrompt(task, "claude")
|
|
if !strings.Contains(out, "Squad leader no_action rule") {
|
|
t.Errorf("buildCommentPrompt must inject squad leader no_action rule for member-triggered comments, got:\n%s", out)
|
|
}
|
|
if !strings.Contains(out, "DO NOT post any comment") {
|
|
t.Errorf("buildCommentPrompt must contain DO NOT post prohibition for member-triggered squad leader, got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
// TestBuildPromptSquadLeaderNoActionForAgentTrigger verifies the rule also
|
|
// fires for agent-triggered comments (the original path that already worked).
|
|
func TestBuildPromptSquadLeaderNoActionForAgentTrigger(t *testing.T) {
|
|
task := Task{
|
|
IssueID: "issue-123",
|
|
TriggerCommentID: "comment-456",
|
|
TriggerCommentContent: "Deploy complete.",
|
|
TriggerAuthorType: "agent",
|
|
TriggerAuthorName: "deploy-boy",
|
|
Agent: &AgentData{
|
|
Instructions: "Some instructions\n\n## Squad Operating Protocol\n\nYou are the LEADER...",
|
|
},
|
|
}
|
|
out := BuildPrompt(task, "claude")
|
|
if !strings.Contains(out, "Squad leader no_action rule") {
|
|
t.Errorf("buildCommentPrompt must inject squad leader no_action rule for agent-triggered comments, got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestBuildChatPromptAttachmentIDsCanBeBoundToCreatedIssues(t *testing.T) {
|
|
task := Task{
|
|
ChatSessionID: "sess-1",
|
|
ChatMessage: "please create an issue with this screenshot",
|
|
ChatMessageAttachments: []ChatAttachmentMeta{
|
|
{ID: "019ec09d-6222-722b-bdfa-427b105d80be", Filename: "shot.png", ContentType: "image/png"},
|
|
},
|
|
}
|
|
out := BuildPrompt(task, "claude")
|
|
for _, want := range []string{
|
|
"Attachments on this message:",
|
|
"id=019ec09d-6222-722b-bdfa-427b105d80be",
|
|
"multica attachment download <id>",
|
|
"--attachment-id <id>",
|
|
} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("chat prompt missing %q\n--- output ---\n%s", want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildChatPromptSlashSkills(t *testing.T) {
|
|
t.Run("injects selected skills block", func(t *testing.T) {
|
|
task := Task{
|
|
ChatSessionID: "sess-1",
|
|
ChatMessage: "please [/deploy](slash://skill/abc-123) this",
|
|
Agent: &AgentData{
|
|
Skills: []SkillData{{ID: "abc-123", Name: "deploy"}},
|
|
},
|
|
}
|
|
out := buildChatPrompt(task)
|
|
if !strings.Contains(out, "Explicitly selected skills:\n- deploy\n") {
|
|
t.Fatalf("expected selected skills block, got:\n%s", out)
|
|
}
|
|
if !strings.Contains(out, "User message:\nplease [/deploy](slash://skill/abc-123) this") {
|
|
t.Fatalf("expected raw user message preserved, got:\n%s", out)
|
|
}
|
|
})
|
|
|
|
t.Run("ignores skills not belonging to agent", func(t *testing.T) {
|
|
task := Task{
|
|
ChatSessionID: "sess-1",
|
|
ChatMessage: "[/hacker-skill](slash://skill/evil-id)",
|
|
Agent: &AgentData{
|
|
Skills: []SkillData{{ID: "good-id", Name: "deploy"}},
|
|
},
|
|
}
|
|
out := buildChatPrompt(task)
|
|
if strings.Contains(out, "Explicitly selected skills") {
|
|
t.Fatalf("should not inject block for unknown skill ID, got:\n%s", out)
|
|
}
|
|
})
|
|
|
|
t.Run("validates by ID not label", func(t *testing.T) {
|
|
task := Task{
|
|
ChatSessionID: "sess-1",
|
|
ChatMessage: "[/deploy](slash://skill/wrong-id)",
|
|
Agent: &AgentData{
|
|
Skills: []SkillData{{ID: "real-id", Name: "deploy"}},
|
|
},
|
|
}
|
|
out := buildChatPrompt(task)
|
|
if strings.Contains(out, "Explicitly selected skills") {
|
|
t.Fatalf("matching label with wrong ID must not pass, got:\n%s", out)
|
|
}
|
|
})
|
|
|
|
t.Run("uses canonical name not label", func(t *testing.T) {
|
|
task := Task{
|
|
ChatSessionID: "sess-1",
|
|
ChatMessage: "[/spoofed-name](slash://skill/real-id)",
|
|
Agent: &AgentData{
|
|
Skills: []SkillData{{ID: "real-id", Name: "deploy"}},
|
|
},
|
|
}
|
|
out := buildChatPrompt(task)
|
|
if !strings.Contains(out, "- deploy\n") {
|
|
t.Fatalf("expected canonical name 'deploy', got:\n%s", out)
|
|
}
|
|
if strings.Contains(out, "- spoofed-name\n") {
|
|
t.Fatalf("selected skills block must not use spoofed label, got:\n%s", out)
|
|
}
|
|
if !strings.Contains(out, "User message:\n[/spoofed-name](slash://skill/real-id)") {
|
|
t.Fatalf("expected raw user message with spoofed label preserved, got:\n%s", out)
|
|
}
|
|
})
|
|
|
|
t.Run("deduplicates skills", func(t *testing.T) {
|
|
task := Task{
|
|
ChatSessionID: "sess-1",
|
|
ChatMessage: "[/deploy](slash://skill/a) and [/deploy](slash://skill/a) again",
|
|
Agent: &AgentData{
|
|
Skills: []SkillData{{ID: "a", Name: "deploy"}},
|
|
},
|
|
}
|
|
out := buildChatPrompt(task)
|
|
if strings.Count(out, "- deploy") != 1 {
|
|
t.Fatalf("expected exactly 1 '- deploy', got:\n%s", out)
|
|
}
|
|
})
|
|
|
|
t.Run("omits block when no valid skills", func(t *testing.T) {
|
|
task := Task{
|
|
ChatSessionID: "sess-1",
|
|
ChatMessage: "just a normal message",
|
|
Agent: &AgentData{Skills: []SkillData{{ID: "a", Name: "deploy"}}},
|
|
}
|
|
out := buildChatPrompt(task)
|
|
if strings.Contains(out, "Explicitly selected skills") {
|
|
t.Fatalf("should not inject block when no slash links, got:\n%s", out)
|
|
}
|
|
})
|
|
|
|
t.Run("omits block when agent has no skills", func(t *testing.T) {
|
|
task := Task{
|
|
ChatSessionID: "sess-1",
|
|
ChatMessage: "[/deploy](slash://skill/abc-123)",
|
|
Agent: &AgentData{},
|
|
}
|
|
out := buildChatPrompt(task)
|
|
if strings.Contains(out, "Explicitly selected skills") {
|
|
t.Fatalf("should not inject block for agent with no skills, got:\n%s", out)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestBuildPromptDefaultMentionsRecent pins that the catch-all fallback
|
|
// prompt (no trigger comment, no chat, no autopilot, no quick-create) also
|
|
// teaches the agent about --recent as the long-issue-friendly alternative
|
|
// to the flat dump, even though it cannot anchor a --thread without a
|
|
// trigger comment id.
|
|
func TestBuildPromptDefaultMentionsRecent(t *testing.T) {
|
|
out := BuildPrompt(Task{IssueID: "issue-default-1"}, "claude")
|
|
for _, s := range []string{
|
|
"--recent 20 --output json",
|
|
"Next thread cursor:",
|
|
"--since",
|
|
} {
|
|
if !strings.Contains(out, s) {
|
|
t.Errorf("default BuildPrompt missing %q\n--- output ---\n%s", s, out)
|
|
}
|
|
}
|
|
// And the default path must NOT inject a --thread example, because there
|
|
// is no trigger comment id to anchor on.
|
|
if strings.Contains(out, "--thread") {
|
|
t.Errorf("default BuildPrompt should NOT mention --thread (no trigger comment to anchor on)\n--- output ---\n%s", out)
|
|
}
|
|
// The legacy "If you need comment history" soft phrasing conflicts with
|
|
// the assignment-trigger runtime workflow, which treats reading comments
|
|
// as mandatory. Guard against it sneaking back in.
|
|
if strings.Contains(out, "If you need comment history") {
|
|
t.Errorf("default BuildPrompt still carries the legacy 'If you need' soft phrasing that conflicts with the mandatory workflow\n--- output ---\n%s", out)
|
|
}
|
|
}
|
|
|
|
// TestBuildPromptNonSquadLeaderNoRule verifies that non-squad-leader agents
|
|
// do NOT get the squad leader no_action rule injected.
|
|
func TestBuildPromptNonSquadLeaderNoRule(t *testing.T) {
|
|
task := Task{
|
|
IssueID: "issue-123",
|
|
TriggerCommentID: "comment-456",
|
|
TriggerCommentContent: "LGTM",
|
|
TriggerAuthorType: "member",
|
|
TriggerAuthorName: "Bohan",
|
|
Agent: &AgentData{
|
|
Instructions: "Some instructions without the squad marker",
|
|
},
|
|
}
|
|
out := BuildPrompt(task, "claude")
|
|
if strings.Contains(out, "Squad leader no_action rule") {
|
|
t.Errorf("buildCommentPrompt must NOT inject squad leader no_action rule for non-squad-leader agents, got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
// TestBuildPromptNewCommentsHint pins that a comment-triggered task whose agent
|
|
// ran before on this issue (NewCommentsSince set, NewCommentCount > 0) gets the
|
|
// since-delta hint with the ISSUE-WIDE new-comment count, but is steered to read
|
|
// the triggering (parent) thread first rather than blindly pulling every new
|
|
// comment.
|
|
func TestBuildPromptNewCommentsHint(t *testing.T) {
|
|
const (
|
|
issueID = "issue-new-1"
|
|
since = "2026-05-28T11:00:00Z"
|
|
)
|
|
task := Task{
|
|
IssueID: issueID,
|
|
TriggerCommentID: "trigger-1",
|
|
TriggerThreadID: "thread-root-1",
|
|
TriggerCommentContent: "please look",
|
|
TriggerAuthorType: "member",
|
|
NewCommentCount: 3,
|
|
NewCommentsSince: since,
|
|
}
|
|
out := BuildPrompt(task, "claude")
|
|
|
|
// Issue-wide count (reverted from the thread-scoped wording).
|
|
if !strings.Contains(out, "3 new comment(s) on this issue since your last run") {
|
|
t.Errorf("hint must report the issue-wide new-comment count, got:\n%s", out)
|
|
}
|
|
// Don't-blindly-read-all guidance.
|
|
if !strings.Contains(out, "blindly") {
|
|
t.Errorf("hint must discourage blindly reading every new comment, got:\n%s", out)
|
|
}
|
|
// Parent thread first: the --thread <trigger> read is the prioritized action.
|
|
if !strings.Contains(out, "multica issue comment list "+issueID+" --thread thread-root-1 --since "+since+" --output json") {
|
|
t.Errorf("hint must point at the triggering (parent) thread --since read first, got:\n%s", out)
|
|
}
|
|
if !strings.Contains(out, "--tail 30") {
|
|
t.Errorf("hint must offer the full-thread (--tail 30) option, got:\n%s", out)
|
|
}
|
|
// Issue-wide catch-up is demoted to an only-if-needed fallback.
|
|
if !strings.Contains(out, "multica issue comment list "+issueID+" --since "+since+" --output json") {
|
|
t.Errorf("hint must keep the issue-wide --since catch-up as a fallback, got:\n%s", out)
|
|
}
|
|
// The old cursor-heavy paragraph must be gone.
|
|
if strings.Contains(out, "Next reply cursor") || strings.Contains(out, "--before-id") {
|
|
t.Errorf("the old cursor-pagination paragraph must not render, got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
// TestBuildPromptColdStartThreadRead pins the cold-start case: no prior run means
|
|
// no since anchor (NewCommentsSince empty), so we suppress the delta hint and
|
|
// instead point the agent at the triggering CONVERSATION (--thread <trigger>
|
|
// --tail 30) rather than dumping the flat timeline.
|
|
func TestBuildPromptColdStartThreadRead(t *testing.T) {
|
|
const issueID = "issue-cold-1"
|
|
task := Task{
|
|
IssueID: issueID,
|
|
TriggerCommentID: "trigger-1",
|
|
TriggerThreadID: "thread-root-1",
|
|
TriggerCommentContent: "hi",
|
|
TriggerAuthorType: "member",
|
|
NewCommentCount: 0,
|
|
NewCommentsSince: "",
|
|
}
|
|
out := BuildPrompt(task, "claude")
|
|
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 thread-root-1 --tail 30 --output json") {
|
|
t.Errorf("cold start must point at the triggering thread read, got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
// TestBuildPromptResumedNoDeltaDoesNotForceThreadRead pins the warm/no-delta
|
|
// path: when a prior provider session is actually being resumed, the triggering
|
|
// comment is already embedded in the per-turn prompt, so the agent should not
|
|
// be told to re-read the triggering thread's latest 30 replies by default.
|
|
func TestBuildPromptResumedNoDeltaDoesNotForceThreadRead(t *testing.T) {
|
|
const issueID = "issue-resumed-1"
|
|
task := Task{
|
|
IssueID: issueID,
|
|
TriggerCommentID: "trigger-1",
|
|
TriggerThreadID: "thread-root-1",
|
|
TriggerCommentContent: "hi again",
|
|
TriggerAuthorType: "member",
|
|
PriorSessionID: "session-123",
|
|
NewCommentCount: 0,
|
|
NewCommentsSince: "",
|
|
}
|
|
out := BuildPrompt(task, "claude")
|
|
|
|
for _, want := range []string{
|
|
"triggering comment is already included above",
|
|
"No other new comments on this issue since your last run",
|
|
"active thread anchor `thread-root-1` and triggering comment ID `trigger-1`",
|
|
"If your reply depends on thread context",
|
|
"do not rely only on resumed session memory",
|
|
"multica issue comment list " + issueID + " --thread thread-root-1 --tail 30 --output json",
|
|
} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("resumed/no-delta prompt missing %q\n--- output ---\n%s", want, out)
|
|
}
|
|
}
|
|
// The stale thread-scoped wording (since-delta used to be thread-scoped)
|
|
// must not reappear.
|
|
if strings.Contains(out, "scoped to the triggering thread") {
|
|
t.Errorf("resumed/no-delta prompt 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 prompt must not use the cold-start forced-read wording, got:\n%s", out)
|
|
}
|
|
}
|