Skip to content

Commit 52984ea

Browse files
bradygasterclaude
andcommitted
test(claude): pin MCP launch spec in host guards, cover Claude upgrade, mark Claude perSpawnModel
Review feedback on #2445. - Pin the squad_state launch spec to the exact-version npx form in the host projection guards. In a built checkout the real resolver writes the local node binary and checkout path into .mcp.json, so the Copilot snapshot depended on where the checkout lives. Regenerate the snapshot. - Add guard 5: with user content in .mcp.json, .gitignore and CLAUDE.md, `squad upgrade --client claude` (plain and --force) on an existing Claude install changes nothing, user content survives, and removing Claude restores the user's bytes. - Set hosts.claude.runtime.perSpawnModel to true (the Agent tool takes a per-call model) and regenerate the matrix page. The model-selection cell stays unsupported/expected-fail until the coordinator uses it. Refs #2440 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 7a19c26 commit 52984ea

4 files changed

Lines changed: 50 additions & 5 deletions

File tree

‎docs/src/content/docs/reference/host-feature-matrix.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Each matrix cell shows **capability validation** (for example `✅ 🧪` = suppo
5454
| Host | CLI | Coordinator file | MCP config | Subagents | Background | Per-spawn model | Subagents inherit MCP |
5555
|------|-----|------------------|------------|-----------|------------|-----------------|-----------------------|
5656
| **GitHub Copilot (CLI / app / VS Code)** (`copilot`) | `copilot` | .github/agents/squad.agent.md (canonical) | .mcp.json | yes | yes | yes | yes |
57-
| **Claude Code** (`claude`) | `claude` | .claude/agents/squad.md (digest) + CLAUDE.md block | .mcp.json | yes | yes | no | yes |
57+
| **Claude Code** (`claude`) | `claude` | .claude/agents/squad.md (digest) + CLAUDE.md block | .mcp.json | yes | yes | yes | yes |
5858
| **Kiro CLI** (`kiro`) | `kiro-cli` | .kiro/agents/squad.json + .kiro/steering/squad.md (manual) | .kiro/settings/mcp.json | yes | no | no | yes |
5959
| **OpenCode** (`opencode`) | `opencode` | .opencode/agents/squad.md (primary) | opencode.json (mcp.squad_state) | yes | yes | no | yes |
6060
| **Cursor** (`cursor`) | `cursor-agent` (installed; the live smoke run was blocked by a usage limit (see live run log), so cells without a dedicated test are untested) | .cursor/rules/squad.mdc (alwaysApply: false) + .cursor/agents/squad.md | .cursor/mcp.json | yes | no | no | yes |

‎test-fixtures/spec/suite-v0.2/host-feature-matrix.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"runtime": {
5252
"subagents": true,
5353
"backgroundRuns": true,
54-
"perSpawnModel": false,
54+
"perSpawnModel": true,
5555
"sqlTool": false,
5656
"shell": true,
5757
"mcpInheritedBySubagents": true

‎test/__snapshots__/host-projection-regression-guards.test.ts.snap‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,6 @@ exports[`host projection regression guards (#2438) > guard 1: the Copilot projec
3636
".github/workflows/squad-triage.yml": "54f8a35713f9cd88bef9ee54b03da8f7d9cbd0bbdcbb03c73cb0dd37fd9bd1f2",
3737
".github/workflows/sync-squad-labels.yml": "105f9f0b1e004833154c6511c84b7f70c465e69e1e8f3da8676ec1255396c34d",
3838
".gitignore": "37aa6c4e49c873d2603b79e173ba46e216b18c596057964aac1cd6e62ff4e89f",
39-
".mcp.json": "02726247df63fc75f8a5d1eafdcbd43b4e1ee14be34e8ccd7b7bf182947b007c",
39+
".mcp.json": "bfdf4acbf0b686e8cf8450c5416b3983ceda7016718e6b1e5a551d2576d1eb80",
4040
}
4141
`;

‎test/host-projection-regression-guards.test.ts‎

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@
88
* 2. `squad upgrade` on a steady-state repo without `claude` in `clients[]` changes no file.
99
* 3. `squad upgrade --client copilot` changes only `.squad/config.json` and writes no Claude files.
1010
* 4. Adding Claude and then removing it (`uninstallClients`) leaves the Copilot projection untouched.
11+
* 5. `squad upgrade --client claude` on an existing Claude install with user content in the shared
12+
* files changes only Squad-managed content, and removing Claude restores the user's bytes.
1113
*
12-
* Byte-exact rollback of pre-existing user files for every host is covered separately in
13-
* test/clients.test.ts ("Reviewer gate ... byte-exact rollback across all 7 hosts").
14+
* The squad_state MCP launch spec is pinned to the exact-version `npx` form for this suite: the real
15+
* resolver writes machine-specific paths (node binary, checkout location) once the CLI is built, which
16+
* would make the snapshot depend on where and whether the checkout was built.
17+
*
18+
* Byte-exact rollback of pre-existing user files for every host through `installClients` is also
19+
* covered in test/clients.test.ts ("Reviewer gate ... byte-exact rollback across all 7 hosts").
1420
*
1521
* If guard 1 fails in a PR that intentionally changes Copilot output (outside #2438), review the
1622
* listed files and update the snapshot with `npx vitest run test/host-projection-regression-guards.test.ts -u`.
@@ -25,6 +31,16 @@ import { runInit } from '../packages/squad-cli/src/cli/core/init.js';
2531
import { runUpgrade } from '../packages/squad-cli/src/cli/core/upgrade.js';
2632
import { uninstallClients } from '../packages/squad-sdk/src/client/adapters/index.js';
2733

34+
// Deterministic launch spec: the pinned exact-version fallback, never a local binary or checkout path.
35+
vi.mock('../packages/squad-cli/src/cli/core/mcp-spec.js', async (importOriginal) => {
36+
const actual = await importOriginal<typeof import('../packages/squad-cli/src/cli/core/mcp-spec.js')>();
37+
return {
38+
...actual,
39+
resolveSquadStateMcpSpec: (cliVersion: string) =>
40+
actual.resolveSquadStateMcpSpec(cliVersion, { standaloneCheck: () => null, localEntryCheck: () => null }),
41+
};
42+
});
43+
2844
const CLI_VERSION: string = JSON.parse(
2945
fs.readFileSync(path.resolve(__dirname, '../packages/squad-cli/package.json'), 'utf8'),
3046
).version;
@@ -138,4 +154,33 @@ describe('host projection regression guards (#2438)', () => {
138154
uninstallClients(['claude'], { root, mcp: { command: 'squad', args: ['state-mcp'] } }, ['copilot']);
139155
expect(changedPaths(before, hashTree({ excludeSquadDir: true })), 'after uninstalling claude').toEqual([]);
140156
}, 60_000);
157+
158+
it('guard 5: upgrading an existing Claude install keeps user content, and removing Claude restores it', async () => {
159+
await steadyStateCopilotRepo();
160+
// User-owned content in every file Claude shares with the user or with Copilot.
161+
const mcp = JSON.parse(fs.readFileSync(path.join(root, '.mcp.json'), 'utf8'));
162+
mcp.mcpServers.user_srv = { command: 'user-mcp', args: ['--flag'] };
163+
fs.writeFileSync(path.join(root, '.mcp.json'), `${JSON.stringify(mcp, null, 2)}\n`);
164+
fs.appendFileSync(path.join(root, '.gitignore'), 'user-build/\n');
165+
fs.writeFileSync(path.join(root, 'CLAUDE.md'), '# User Claude notes\n\nKeep this paragraph.\n');
166+
const userBytes = (p: string) => fs.readFileSync(path.join(root, p), 'utf8');
167+
const original = Object.fromEntries(['.mcp.json', '.gitignore', 'CLAUDE.md'].map((p) => [p, userBytes(p)]));
168+
const before = hashTree({ excludeSquadDir: true });
169+
170+
await runUpgrade(root, { clients: ['copilot', 'claude'] });
171+
const installed = hashTree({ excludeSquadDir: true });
172+
expect(JSON.parse(userBytes('.mcp.json')).mcpServers.user_srv).toEqual({ command: 'user-mcp', args: ['--flag'] });
173+
expect(userBytes('.gitignore')).toContain('user-build/');
174+
expect(userBytes('CLAUDE.md')).toContain('Keep this paragraph.');
175+
176+
// Upgrading the existing Claude install is a no-op for every file once it is current.
177+
await runUpgrade(root, { clients: ['copilot', 'claude'] });
178+
expect(changedPaths(installed, hashTree({ excludeSquadDir: true })), 'squad upgrade --client claude').toEqual([]);
179+
await runUpgrade(root, { clients: ['copilot', 'claude'], force: true });
180+
expect(changedPaths(installed, hashTree({ excludeSquadDir: true })), 'squad upgrade --client claude --force').toEqual([]);
181+
182+
uninstallClients(['claude'], { root, mcp: { command: 'squad', args: ['state-mcp'] } }, ['copilot']);
183+
for (const [p, bytes] of Object.entries(original)) expect(userBytes(p), `rollback of ${p}`).toBe(bytes);
184+
expect(changedPaths(before, hashTree({ excludeSquadDir: true })), 'after uninstalling claude').toEqual([]);
185+
}, 60_000);
141186
});

0 commit comments

Comments
 (0)