Repository navigation
test(cli): add init→upgrade→doctor parity test suite - #840
Conversation
🟡 Impact Analysis — PR #840Risk tier: 🟡 MEDIUM 📊 Summary
🎯 Risk Factors
📦 Modules Affectedroot (1 file)
tests (1 file)
This report is generated automatically for every PR. See #733 for details. |
🛫 PR Readiness Check
|
| Status | Check | Details |
|---|---|---|
| ❌ | Single commit | 3 commits — consider squashing before review |
| ✅ | Not in draft | Ready for review |
| ✅ | Branch up to date | Up to date with dev |
| ❌ | Copilot review | No Copilot review yet — it may still be processing |
| ✅ | Changeset present | Changeset file found |
| ✅ | Scope clean | No .squad/ or docs/proposals/ files |
| ✅ | No merge conflicts | No merge conflicts |
| ❌ | Copilot threads resolved | 4 unresolved Copilot thread(s) — fix and resolve before merging |
| ❌ | CI passing | 16 check(s) still running |
This check runs automatically on every push. Fix any ❌ items and push again.
See CONTRIBUTING.md and PR Requirements for details.
There was a problem hiding this comment.
Pull request overview
Adds a new Vitest parity test suite to prevent drift between squad init and squad upgrade scaffolding, guarding against regressions that would later surface in squad doctor.
Changes:
- Introduces
init-upgrade-parity.test.tscovering directory scaffolding, casting defaults, non-overwrite behavior, and idempotency of ensure helpers. - Verifies
.gitattributesmerge=union rules and repeated ensure calls don’t duplicate entries.
| // Track which dirs init missed so drift is visible in test output. | ||
| // Ideally created.length === 0 (init scaffolds everything upgrade expects). | ||
| if (created.length > 0) { | ||
| console.warn( | ||
| `[parity drift] init missed ${created.length} dirs that upgrade ensures: ${created.join(', ')}`, | ||
| ); | ||
| } |
There was a problem hiding this comment.
The test logs parity drift via console.warn but never fails when drift is detected (created.length > 0). This means init/upgrade directory drift will not be caught by CI. Convert this into an assertion (e.g., expect created to be empty) so the contract is enforced.
| // Track which dirs init missed so drift is visible in test output. | |
| // Ideally created.length === 0 (init scaffolds everything upgrade expects). | |
| if (created.length > 0) { | |
| console.warn( | |
| `[parity drift] init missed ${created.length} dirs that upgrade ensures: ${created.join(', ')}`, | |
| ); | |
| } | |
| // Enforce parity: init should already scaffold everything upgrade ensures. | |
| expect( | |
| created, | |
| `[parity drift] init missed ${created.length} dirs that upgrade ensures: ${created.join(', ')}`, | |
| ).toEqual([]); |
| '.squad/sessions', | ||
| '.squad/decisions/inbox', | ||
| '.squad/casting', | ||
| '.squad/agents', |
There was a problem hiding this comment.
INIT_INFRASTRUCTURE_DIRS claims to be derived from what runInit scaffolds, but it omits at least .squad/plugins/ (see existing init integration tests). If parity is meant to prevent init/upgrade drift, include the full set of init-created infrastructure dirs here (or derive them programmatically) so missing upgrade coverage is actually detected.
| '.squad/agents', | |
| '.squad/agents', | |
| '.squad/plugins', |
| it('upgrade scaffolds casting directory and init creates casting files', async () => { | ||
| await runInit(TEST_ROOT); | ||
|
|
||
| const castingDir = join(TEST_ROOT, '.squad', 'casting'); | ||
| expect(existsSync(castingDir)).toBe(true); | ||
|
|
||
| // Init should have created the three casting JSON files | ||
| for (const file of CASTING_FILES) { | ||
| const filePath = join(castingDir, file); | ||
| expect(existsSync(filePath)).toBe(true); | ||
|
|
||
| const content = await readFile(filePath, 'utf-8'); | ||
| expect(() => JSON.parse(content)).not.toThrow(); | ||
| } | ||
|
|
||
| // After upgrade on an already-init'd directory, casting files survive | ||
| await runUpgrade(TEST_ROOT); | ||
|
|
||
| for (const file of CASTING_FILES) { | ||
| expect(existsSync(join(castingDir, file))).toBe(true); | ||
| } | ||
| }); |
There was a problem hiding this comment.
This test name/comment says it verifies upgrade scaffolds casting defaults, but it only checks that init created the files and that upgrade preserves them. To validate upgrade scaffolding behavior, delete .squad/casting/ (or the JSON files) after init, run runUpgrade, and assert the directory/files are recreated with valid JSON.
| const sentinel = '<!-- USER_SENTINEL -->\n'; | ||
| if (existsSync(teamPath)) { | ||
| await writeFile(teamPath, sentinel); | ||
| } | ||
|
|
||
| // Write a sentinel casting file | ||
| const registryPath = join(TEST_ROOT, '.squad', 'casting', 'registry.json'); | ||
| const customRegistry = JSON.stringify({ agents: { sentinel: true } }); | ||
| if (existsSync(registryPath)) { | ||
| await writeFile(registryPath, customRegistry); | ||
| } | ||
|
|
||
| await runUpgrade(TEST_ROOT); | ||
|
|
||
| // team.md should still have our sentinel | ||
| if (existsSync(teamPath)) { | ||
| const teamContent = await readFile(teamPath, 'utf-8'); | ||
| expect(teamContent).toContain('USER_SENTINEL'); | ||
| } | ||
|
|
||
| // casting/registry.json should still have our custom content | ||
| if (existsSync(registryPath)) { | ||
| const regContent = await readFile(registryPath, 'utf-8'); | ||
| expect(regContent).toContain('sentinel'); | ||
| } |
There was a problem hiding this comment.
The assertions in this test are conditional on team.md and casting/registry.json already existing, so the test can pass without actually verifying upgrade's non-overwrite behavior. Since runInit is expected to create these, assert they exist after init (and fail if they don't) before writing/validating sentinel content.
| const sentinel = '<!-- USER_SENTINEL -->\n'; | |
| if (existsSync(teamPath)) { | |
| await writeFile(teamPath, sentinel); | |
| } | |
| // Write a sentinel casting file | |
| const registryPath = join(TEST_ROOT, '.squad', 'casting', 'registry.json'); | |
| const customRegistry = JSON.stringify({ agents: { sentinel: true } }); | |
| if (existsSync(registryPath)) { | |
| await writeFile(registryPath, customRegistry); | |
| } | |
| await runUpgrade(TEST_ROOT); | |
| // team.md should still have our sentinel | |
| if (existsSync(teamPath)) { | |
| const teamContent = await readFile(teamPath, 'utf-8'); | |
| expect(teamContent).toContain('USER_SENTINEL'); | |
| } | |
| // casting/registry.json should still have our custom content | |
| if (existsSync(registryPath)) { | |
| const regContent = await readFile(registryPath, 'utf-8'); | |
| expect(regContent).toContain('sentinel'); | |
| } | |
| const registryPath = join(TEST_ROOT, '.squad', 'casting', 'registry.json'); | |
| const sentinel = '<!-- USER_SENTINEL -->\n'; | |
| const customRegistry = JSON.stringify({ agents: { sentinel: true } }); | |
| expect(existsSync(teamPath)).toBe(true); | |
| expect(existsSync(registryPath)).toBe(true); | |
| await writeFile(teamPath, sentinel); | |
| // Write a sentinel casting file | |
| await writeFile(registryPath, customRegistry); | |
| await runUpgrade(TEST_ROOT); | |
| // team.md should still have our sentinel | |
| const teamContent = await readFile(teamPath, 'utf-8'); | |
| expect(teamContent).toContain('USER_SENTINEL'); | |
| // casting/registry.json should still have our custom content | |
| const regContent = await readFile(registryPath, 'utf-8'); | |
| expect(regContent).toContain('sentinel'); |
95cf3ce to
76fce2a
Compare
tamirdresher
left a comment
There was a problem hiding this comment.
✅ LGTM — Smart parity tests guarding init/upgrade drift. Sentinel no-overwrite test is clever.
Summary
Adds a parity test suite that verifies init and upgrade produce equivalent scaffolding. This guards against the class of drift discovered when casting was added to init but upgrade wasn't updated (Brady's insiders install).
Tests (7)
Test-only PR — no product code changes
Closes #822
Related: #817, #823