mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-11 02:46:45 +08:00
Agents could not see custom issue statuses (MUL-6243): the brief's status command line hardcoded the seven built-in keys, so a workspace's Human Review / Rework statuses were invisible even when instructions asked for them, and the CLI offers no catalog read an agent could fall back on. Ship the catalog on the claim payload, the same path WorkspaceContext already rides: buildClaimedTaskResponse reads the workspace's ACTIVE CUSTOM statuses (built-ins stay off the wire — their keys, names and semantics are locked) and the daemon copies them into TaskContextForEnv. The brief's status bullet then renders the workspace's statuses grouped by category, with each custom status's name and description — category anchors the inherited platform behavior, name is what users call the status in instructions, and description is the admin's disambiguator when a category holds more than one status. A bridge bullet in the issue workflow connects the existing category-level status rules (which stay untouched) to the concrete key choice. With no custom statuses — every old-server pairing included — both renders are byte-identical to before, keeping the prompt-cache prefix stable for existing deployments (MUL-5377). The catalog is durable workspace configuration like WorkspaceContext, not per-turn state, so it belongs in the brief rather than the per-turn prompt. The claim read is not gated on the custom_issue_statuses flag: the flag only guards creation, and a catalog written before a flag flip must stay visible as long as issues can sit on it. Name/description are user-authored and pass through sanitizeNameForBriefMarkdown; a key failing the code-token guard drops its entry. A 30-entry cap with an explicit omitted count keeps a scripted catalog from flooding the brief while disclosing the truncation. Also update the built-in skills that document status semantics (multica-working-on-issues, multica-squads) and their source maps: statuses beyond the built-ins inherit their category's behavior in full, with the two literal-key exceptions (failed-task rollback writes todo, merged close-intent PR writes done) called out. Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]>
95 lines
4.4 KiB
Go
95 lines
4.4 KiB
Go
package execenv
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// legacyStatusLine is the pre-MUL-6460 status bullet. Workspaces without
|
|
// custom statuses — including every deployment behind an old server — must
|
|
// keep rendering it byte-identical: it is part of the prompt-cache prefix and
|
|
// the no-custom-statuses path is the compatibility contract of MUL-6460.
|
|
const legacyStatusLine = "- `multica issue status <id> <status> [--no-start]` — flip status (todo / in_progress / in_review / done / blocked / backlog / cancelled).\n"
|
|
|
|
// catalogBridgeBullet is the workflow-section bridge from category rules to a
|
|
// concrete key choice; it must appear exactly when a catalog is present.
|
|
const catalogBridgeBullet = "- The status rules above are category rules — every status in this workspace's catalog (`## Available Commands`) inherits them from its category. When a category holds more than one status, pick the specific one by its name/description or your instructions.\n"
|
|
|
|
func TestBriefStatusCatalogAbsentKeepsLegacyLine(t *testing.T) {
|
|
t.Parallel()
|
|
base := TaskContextForEnv{IssueID: "issue-1", AgentID: "a-1", AgentName: "Eve"}
|
|
out := buildMetaSkillContent("claude", base)
|
|
if !strings.Contains(out, legacyStatusLine) {
|
|
t.Fatalf("brief without a catalog must keep the legacy status line\n---\n%s", out)
|
|
}
|
|
if strings.Contains(out, catalogBridgeBullet) {
|
|
t.Errorf("brief without a catalog must not carry the catalog bridge bullet")
|
|
}
|
|
|
|
withEmpty := base
|
|
withEmpty.IssueStatuses = []IssueStatusForEnv{}
|
|
if got := buildMetaSkillContent("claude", withEmpty); got != out {
|
|
t.Errorf("empty catalog must render byte-identical to absent catalog:\n%s", firstBriefDiff(out, got))
|
|
}
|
|
}
|
|
|
|
func TestBriefStatusCatalogRendered(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := TaskContextForEnv{
|
|
IssueID: "issue-1", AgentID: "a-1", AgentName: "Eve",
|
|
IssueStatuses: []IssueStatusForEnv{
|
|
{Key: "later", Name: "Later", Category: "backlog", Description: "Deferred on purpose"},
|
|
{Key: "rework", Name: "Rework", Category: "todo"},
|
|
{Key: "human_review", Name: "Human Review", Category: "in_review", Description: "Awaiting human acceptance"},
|
|
},
|
|
}
|
|
out := buildMetaSkillContent("claude", ctx)
|
|
if strings.Contains(out, legacyStatusLine) {
|
|
t.Errorf("catalog brief must replace the legacy seven-value enumeration")
|
|
}
|
|
for _, want := range []string{
|
|
"- `multica issue status <id> <status> [--no-start]` — flip status. This workspace's statuses by category — a custom status inherits its category's platform behavior in full:\n",
|
|
" - `backlog`: `backlog` (built-in), `later` (Later — Deferred on purpose)\n",
|
|
" - `todo`: `todo` (built-in), `rework` (Rework)\n",
|
|
" - `in_review`: `in_review` (built-in), `human_review` (Human Review — Awaiting human acceptance)\n",
|
|
" - Built-in key only: `in_progress`, `done`, `blocked`, `cancelled`.\n",
|
|
catalogBridgeBullet,
|
|
} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("catalog brief missing %q\n---\n%s", want, out)
|
|
}
|
|
}
|
|
if strings.Contains(out, "more custom statuses not listed") {
|
|
t.Errorf("no truncation disclosure may appear when nothing was omitted")
|
|
}
|
|
}
|
|
|
|
// TestBriefStatusCatalogSanitizesAndDiscloses pins the two safety properties:
|
|
// user-authored name/description cannot inject markdown structure into the
|
|
// trusted brief, and a key that fails the code-token guard drops its entry
|
|
// entirely instead of rendering mangled.
|
|
func TestBriefStatusCatalogSanitizesAndDiscloses(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := TaskContextForEnv{
|
|
IssueID: "issue-1", AgentID: "a-1", AgentName: "Eve",
|
|
IssueStatuses: []IssueStatusForEnv{
|
|
{Key: "qa", Name: "QA *bold*\n# Heading", Category: "in_review", Description: "line1\nline2 [x]"},
|
|
{Key: "bad key!", Name: "Evil", Category: "todo", Description: "must be dropped"},
|
|
},
|
|
IssueStatusesOmitted: 4,
|
|
}
|
|
out := buildMetaSkillContent("claude", ctx)
|
|
for _, want := range []string{
|
|
` - ` + "`in_review`: `in_review`" + ` (built-in), ` + "`qa`" + ` (QA \*bold\* # Heading — line1 line2 \[x\])` + "\n",
|
|
" - Built-in key only: `backlog`, `todo`, `in_progress`, `done`, `blocked`, `cancelled`.\n",
|
|
" - …and 4 more custom statuses not listed; an invalid status errors with the full valid list.\n",
|
|
} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("catalog brief missing %q\n---\n%s", want, out)
|
|
}
|
|
}
|
|
if strings.Contains(out, "Evil") || strings.Contains(out, "bad key") {
|
|
t.Errorf("entry with an invalid key must be dropped entirely\n---\n%s", out)
|
|
}
|
|
}
|