From 9b039e48e642942a7f112e1dc23776c62827c9d1 Mon Sep 17 00:00:00 2001 From: Copilot Date: Sat, 18 Apr 2026 11:30:44 +0300 Subject: [PATCH 1/7] fix: remove duplicate permission + add mktree to cspell - Remove duplicate 'pull-requests: read' in squad-ci.yml that caused YAML parsing failure (0 jobs, instant CI failure) - Add 'mktree' to cspell.json dictionary (git command used in state-backends.md was flagged as unknown word) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/squad-ci.yml | 1 - cspell.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/squad-ci.yml b/.github/workflows/squad-ci.yml index 4fb2483f9..50d72797f 100644 --- a/.github/workflows/squad-ci.yml +++ b/.github/workflows/squad-ci.yml @@ -10,7 +10,6 @@ on: permissions: contents: read pull-requests: read - pull-requests: read # Prevent parallel runs from competing for resources concurrency: diff --git a/cspell.json b/cspell.json index 36c9fcb25..705a91be3 100644 --- a/cspell.json +++ b/cspell.json @@ -35,7 +35,7 @@ "MSMQ", "permadeath", "Permadeath", "CRDT", "httpx", "Pydantic", "sdkgen", "ttft", "Strausz", "mycompany", "slugified", "simplejwt", "pytest", "Luca", "Clemenza", - "Tessio", "Triaging", "Futurama", "mklink", "slnx", "jqlang", + "Tessio", "Triaging", "Futurama", "mklink", "mktree", "slnx", "jqlang", "benleane", "TELMU", "Automator", "kedacore", "DEVBOX", "myaccount" ], From 687484d3497e4f7a8dccc9a025af47d6aa87069d Mon Sep 17 00:00:00 2001 From: Copilot Date: Sat, 18 Apr 2026 11:38:05 +0300 Subject: [PATCH 2/7] fix: remove orphaned test files for deleted scripts architectural-review.test.ts and check-bootstrap-deps.test.ts were left behind when PR #940 deleted the scripts they test (architectural-review.mjs, check-bootstrap-deps.mjs). Tests fail with 'No JSON object found' because the scripts no longer exist. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/scripts/architectural-review.test.ts | 215 ------------------- test/scripts/check-bootstrap-deps.test.ts | 245 ---------------------- 2 files changed, 460 deletions(-) delete mode 100644 test/scripts/architectural-review.test.ts delete mode 100644 test/scripts/check-bootstrap-deps.test.ts diff --git a/test/scripts/architectural-review.test.ts b/test/scripts/architectural-review.test.ts deleted file mode 100644 index 079cdfd91..000000000 --- a/test/scripts/architectural-review.test.ts +++ /dev/null @@ -1,215 +0,0 @@ -/** - * Tests for scripts/architectural-review.mjs - * - * Validates that the architectural review check: - * - Produces valid JSON with findings array and summary string - * - Each finding has the correct shape (category, severity, message, files) - * - Reports zero findings when diff is empty (HEAD vs HEAD) - * - Detects cross-package import patterns correctly - * - Detects bootstrap area modifications - * - Always exits with code 0 (informational only) - */ - -import { describe, it, expect } from 'vitest'; -import { extractJson, runScript } from './helpers'; - -// --------------------------------------------------------------------------- -// Cross-package import patterns (replicated from the script for unit testing) -// --------------------------------------------------------------------------- - -const CLI_TO_SDK_SRC = /from\s+['"].*squad-sdk\/src\//; -const SDK_TO_CLI_SRC = /from\s+['"].*squad-cli\/src\//; -const CLI_TO_SDK_REQUIRE = /require\(['"].*squad-sdk\/src\//; -const SDK_TO_CLI_REQUIRE = /require\(['"].*squad-cli\/src\//; - -function detectCrossPackageImport( - line: string, - direction: 'cli-to-sdk' | 'sdk-to-cli', -): boolean { - if (direction === 'cli-to-sdk') { - return CLI_TO_SDK_SRC.test(line) || CLI_TO_SDK_REQUIRE.test(line); - } - return SDK_TO_CLI_SRC.test(line) || SDK_TO_CLI_REQUIRE.test(line); -} - -// --------------------------------------------------------------------------- -// Integration tests -// --------------------------------------------------------------------------- - -describe('architectural-review script', () => { - describe('integration: no-diff baseline (HEAD as base ref)', () => { - it('produces valid JSON with findings and summary', () => { - const result = runScript('architectural-review.mjs', ['HEAD']); - const json = extractJson(result.stdout); - - expect(json).toHaveProperty('findings'); - expect(Array.isArray(json.findings)).toBe(true); - expect(json).toHaveProperty('summary'); - expect(typeof json.summary).toBe('string'); - }); - - it('reports zero findings when diff is empty', () => { - const result = runScript('architectural-review.mjs', ['HEAD']); - const json = extractJson(result.stdout); - - expect(json.findings).toEqual([]); - expect(json.summary).toContain('No architectural concerns'); - }); - - it('always exits with code 0', () => { - const result = runScript('architectural-review.mjs', ['HEAD']); - expect(result.status).toBe(0); - }); - }); - - describe('integration: default base ref', () => { - it('exits with code 0 regardless of findings', () => { - const result = runScript('architectural-review.mjs'); - expect(result.status).toBe(0); - }); - - it('produces valid JSON with correct schema', () => { - const result = runScript('architectural-review.mjs'); - const json = extractJson(result.stdout); - - expect(json).toHaveProperty('findings'); - expect(json).toHaveProperty('summary'); - expect(Array.isArray(json.findings)).toBe(true); - expect(typeof json.summary).toBe('string'); - }); - - it('each finding has category, severity, message, and files', () => { - const result = runScript('architectural-review.mjs'); - const json = extractJson(result.stdout); - const findings = json.findings as Array>; - - for (const f of findings) { - expect(f).toHaveProperty('category'); - expect(f).toHaveProperty('severity'); - expect(f).toHaveProperty('message'); - expect(f).toHaveProperty('files'); - expect(typeof f.category).toBe('string'); - expect(typeof f.severity).toBe('string'); - expect(typeof f.message).toBe('string'); - expect(Array.isArray(f.files)).toBe(true); - } - }); - - it('severity values are from the allowed set', () => { - const result = runScript('architectural-review.mjs'); - const json = extractJson(result.stdout); - const findings = json.findings as Array>; - const ALLOWED = new Set(['error', 'warning', 'info']); - - for (const f of findings) { - expect(ALLOWED.has(f.severity as string)).toBe(true); - } - }); - - it('category values match known check types', () => { - const result = runScript('architectural-review.mjs'); - const json = extractJson(result.stdout); - const findings = json.findings as Array>; - const KNOWN_CATEGORIES = new Set([ - 'bootstrap-area', - 'export-surface', - 'cross-package-import', - 'template-sync', - 'sweeping-refactor', - 'file-deletion', - ]); - - for (const f of findings) { - expect(KNOWN_CATEGORIES.has(f.category as string)).toBe(true); - } - }); - }); - - // --------------------------------------------------------------------------- - // Unit tests — cross-package import detection - // --------------------------------------------------------------------------- - - describe('cross-package import pattern detection', () => { - it('detects CLI importing from SDK src path', () => { - expect( - detectCrossPackageImport( - "import { Foo } from '@bradygaster/squad-sdk/src/foo'", - 'cli-to-sdk', - ), - ).toBe(true); - }); - - it('detects SDK importing from CLI src path', () => { - expect( - detectCrossPackageImport( - "import { Bar } from '../squad-cli/src/bar'", - 'sdk-to-cli', - ), - ).toBe(true); - }); - - it('detects require-style cross-package imports', () => { - expect( - detectCrossPackageImport( - "const x = require('@bradygaster/squad-sdk/src/foo')", - 'cli-to-sdk', - ), - ).toBe(true); - }); - - it('does not flag imports via published package name', () => { - expect( - detectCrossPackageImport( - "import { Foo } from '@bradygaster/squad-sdk'", - 'cli-to-sdk', - ), - ).toBe(false); - }); - - it('does not flag unrelated imports', () => { - expect( - detectCrossPackageImport("import { readFile } from 'node:fs'", 'cli-to-sdk'), - ).toBe(false); - expect( - detectCrossPackageImport("import express from 'express'", 'sdk-to-cli'), - ).toBe(false); - }); - }); - - // --------------------------------------------------------------------------- - // Unit tests — bootstrap area detection - // --------------------------------------------------------------------------- - - describe('bootstrap area detection', () => { - const BOOTSTRAP_PREFIX = 'packages/squad-cli/src/cli/core/'; - - it('recognizes bootstrap area files', () => { - expect('packages/squad-cli/src/cli/core/detect-squad-dir.ts'.startsWith(BOOTSTRAP_PREFIX)).toBe(true); - expect('packages/squad-cli/src/cli/core/errors.ts'.startsWith(BOOTSTRAP_PREFIX)).toBe(true); - expect('packages/squad-cli/src/cli/core/output.ts'.startsWith(BOOTSTRAP_PREFIX)).toBe(true); - }); - - it('does not flag files outside bootstrap area', () => { - expect('packages/squad-cli/src/commands/init.ts'.startsWith(BOOTSTRAP_PREFIX)).toBe(false); - expect('packages/squad-sdk/src/index.ts'.startsWith(BOOTSTRAP_PREFIX)).toBe(false); - expect('test/scripts/helpers.ts'.startsWith(BOOTSTRAP_PREFIX)).toBe(false); - }); - }); - - // --------------------------------------------------------------------------- - // Edge cases - // --------------------------------------------------------------------------- - - describe('edge cases', () => { - it('handles invalid base ref gracefully', () => { - const result = runScript('architectural-review.mjs', [ - 'refs/heads/nonexistent-branch-xyz-99999', - ]); - const json = extractJson(result.stdout); - - // Should produce valid JSON with zero findings (git diff returns empty) - expect(json.findings).toEqual([]); - expect(result.status).toBe(0); - }); - }); -}); diff --git a/test/scripts/check-bootstrap-deps.test.ts b/test/scripts/check-bootstrap-deps.test.ts deleted file mode 100644 index 837f84b54..000000000 --- a/test/scripts/check-bootstrap-deps.test.ts +++ /dev/null @@ -1,245 +0,0 @@ -/** - * Tests for scripts/check-bootstrap-deps.mjs - * - * Validates that the bootstrap protection gate: - * - Produces valid JSON output with the correct schema - * - Correctly identifies node built-in vs external imports - * - Detects various import/require syntax patterns - * - Passes for the actual protected files in this repo - */ - -import { describe, it, expect } from 'vitest'; -import { extractJson, runScript } from './helpers'; - -// --------------------------------------------------------------------------- -// Replicated logic from check-bootstrap-deps.mjs for unit testing -// (Script does not export functions, so we mirror the core detection logic.) -// --------------------------------------------------------------------------- - -const NODE_BUILTINS = new Set([ - 'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', - 'console', 'constants', 'crypto', 'dgram', 'diagnostics_channel', - 'dns', 'domain', 'events', 'fs', 'http', 'http2', 'https', - 'inspector', 'module', 'net', 'os', 'path', 'perf_hooks', - 'process', 'punycode', 'querystring', 'readline', 'repl', - 'stream', 'string_decoder', 'sys', 'test', 'timers', 'tls', - 'trace_events', 'tty', 'url', 'util', 'v8', 'vm', 'wasi', - 'worker_threads', 'zlib', -]); - -function isNodeBuiltin(specifier: string): boolean { - if (specifier.startsWith('node:')) return true; - const base = specifier.split('/')[0]; - return NODE_BUILTINS.has(base); -} - -function isRelativeImport(specifier: string): boolean { - return specifier.startsWith('./') || specifier.startsWith('../'); -} - -const IMPORT_PATTERNS = [ - /(?:^|\s)import\s+(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"]/g, - /import\(\s*['"]([^'"]+)['"]\s*\)/g, - /require\(\s*['"]([^'"]+)['"]\s*\)/g, -]; - -function findImports(line: string): string[] { - const imports: string[] = []; - for (const pattern of IMPORT_PATTERNS) { - pattern.lastIndex = 0; - let match; - while ((match = pattern.exec(line)) !== null) { - imports.push(match[1]); - } - } - return imports; -} - -function isViolation(specifier: string): boolean { - return !isNodeBuiltin(specifier) && !isRelativeImport(specifier); -} - -// --------------------------------------------------------------------------- -// Integration tests — run the actual script -// --------------------------------------------------------------------------- - -describe('check-bootstrap-deps script', () => { - describe('integration: script execution', () => { - it('produces valid JSON with pass and violations fields', () => { - const result = runScript('check-bootstrap-deps.mjs'); - const json = extractJson(result.stdout); - - expect(json).toHaveProperty('pass'); - expect(typeof json.pass).toBe('boolean'); - expect(json).toHaveProperty('violations'); - expect(Array.isArray(json.violations)).toBe(true); - }); - - it('passes for the current repo (protected files use only node:* imports)', () => { - const result = runScript('check-bootstrap-deps.mjs'); - const json = extractJson(result.stdout); - - expect(json.pass).toBe(true); - expect(json.violations).toEqual([]); - expect(result.status).toBe(0); - }); - - it('includes success message in stdout when passing', () => { - const result = runScript('check-bootstrap-deps.mjs'); - expect(result.stdout).toContain('Bootstrap protection'); - expect(result.stdout).toContain('node:*'); - }); - }); - - // --------------------------------------------------------------------------- - // Unit tests — import pattern detection - // --------------------------------------------------------------------------- - - describe('import pattern detection', () => { - it('detects ES static imports', () => { - expect(findImports("import { foo } from 'bar'")).toContain('bar'); - expect(findImports("import foo from 'bar'")).toContain('bar'); - expect(findImports("import 'side-effect-pkg'")).toContain('side-effect-pkg'); - }); - - it('detects ES imports with double quotes', () => { - expect(findImports('import { foo } from "bar"')).toContain('bar'); - }); - - it('detects dynamic import()', () => { - expect(findImports("const m = import('bar')")).toContain('bar'); - expect(findImports("await import('bar')")).toContain('bar'); - expect(findImports('import("dynamic-pkg")')).toContain('dynamic-pkg'); - }); - - it('detects require() calls', () => { - expect(findImports("const m = require('bar')")).toContain('bar'); - expect(findImports("require('bar')")).toContain('bar'); - expect(findImports('require("double-quoted")')).toContain('double-quoted'); - }); - - it('captures node: prefixed specifiers', () => { - expect(findImports("import { readFileSync } from 'node:fs'")).toContain('node:fs'); - expect(findImports("import { resolve } from 'node:path'")).toContain('node:path'); - }); - - it('returns empty array for non-import lines', () => { - expect(findImports('const x = 42;')).toEqual([]); - expect(findImports('')).toEqual([]); - }); - - it('regex still matches imports inside comments (comment filtering is separate)', () => { - // The script filters comment lines BEFORE running import patterns. - // The patterns themselves don't distinguish comments from code. - expect(findImports('// import { foo } from "bar"')).toContain('bar'); - }); - }); - - // --------------------------------------------------------------------------- - // Unit tests — built-in detection - // --------------------------------------------------------------------------- - - describe('isNodeBuiltin', () => { - it('accepts node: prefix imports', () => { - expect(isNodeBuiltin('node:fs')).toBe(true); - expect(isNodeBuiltin('node:path')).toBe(true); - expect(isNodeBuiltin('node:child_process')).toBe(true); - expect(isNodeBuiltin('node:util')).toBe(true); - }); - - it('accepts node: prefix with subpath', () => { - expect(isNodeBuiltin('node:fs/promises')).toBe(true); - expect(isNodeBuiltin('node:stream/web')).toBe(true); - }); - - it('accepts bare built-in module names', () => { - expect(isNodeBuiltin('fs')).toBe(true); - expect(isNodeBuiltin('path')).toBe(true); - expect(isNodeBuiltin('util')).toBe(true); - expect(isNodeBuiltin('child_process')).toBe(true); - expect(isNodeBuiltin('crypto')).toBe(true); - }); - - it('accepts built-in subpaths (e.g. fs/promises)', () => { - expect(isNodeBuiltin('fs/promises')).toBe(true); - expect(isNodeBuiltin('stream/web')).toBe(true); - }); - - it('rejects npm packages', () => { - expect(isNodeBuiltin('express')).toBe(false); - expect(isNodeBuiltin('lodash')).toBe(false); - expect(isNodeBuiltin('vitest')).toBe(false); - }); - - it('rejects scoped packages', () => { - expect(isNodeBuiltin('@bradygaster/squad-sdk')).toBe(false); - expect(isNodeBuiltin('@types/node')).toBe(false); - }); - }); - - describe('isRelativeImport', () => { - it('allows ./ imports', () => { - expect(isRelativeImport('./sibling')).toBe(true); - expect(isRelativeImport('./nested/deep')).toBe(true); - }); - - it('allows ../ imports', () => { - expect(isRelativeImport('../parent')).toBe(true); - expect(isRelativeImport('../../grandparent')).toBe(true); - }); - - it('rejects bare specifiers', () => { - expect(isRelativeImport('express')).toBe(false); - expect(isRelativeImport('@scope/pkg')).toBe(false); - expect(isRelativeImport('node:fs')).toBe(false); - }); - }); - - // --------------------------------------------------------------------------- - // Unit tests — violation classification - // --------------------------------------------------------------------------- - - describe('violation classification', () => { - it('flags npm packages as violations', () => { - expect(isViolation('express')).toBe(true); - expect(isViolation('@bradygaster/squad-sdk')).toBe(true); - expect(isViolation('lodash/merge')).toBe(true); - }); - - it('does not flag node builtins', () => { - expect(isViolation('node:fs')).toBe(false); - expect(isViolation('fs')).toBe(false); - expect(isViolation('node:path')).toBe(false); - }); - - it('does not flag relative imports', () => { - expect(isViolation('./sibling')).toBe(false); - expect(isViolation('../parent')).toBe(false); - }); - }); - - // --------------------------------------------------------------------------- - // Edge cases - // --------------------------------------------------------------------------- - - describe('edge cases', () => { - it('comment lines are skipped by the script logic', () => { - // The script skips lines starting with // or * - const commentLines = [ - '// import { bad } from "evil-pkg"', - '* import { bad } from "evil-pkg"', - ]; - for (const line of commentLines) { - const trimmed = line.trim(); - const isComment = trimmed.startsWith('//') || trimmed.startsWith('*'); - expect(isComment).toBe(true); - } - }); - - it('handles empty specifier edge case', () => { - // Empty string is not a node builtin and not relative - expect(isNodeBuiltin('')).toBe(false); - expect(isRelativeImport('')).toBe(false); - }); - }); -}); From 0447e344fd385d2b30f947f119fb3c212090a2e5 Mon Sep 17 00:00:00 2001 From: Copilot Date: Sat, 18 Apr 2026 11:59:47 +0300 Subject: [PATCH 3/7] fix: pre-existing test failures from monorepo detection and message changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - init.test.ts, upgrade.test.ts: use os.tmpdir() instead of process.cwd() for TEST_ROOT so temp dirs aren't inside the repo checkout, avoiding detectParentGitRepo() redirecting agent files to the repo root (8449a77) - loop.test.ts: update regex from /gh CLI/i to /Copilot CLI/i to match the new error message text (21587fb) - comms-teams-integration.test.ts: update expected warning to include authError detail in permanently-failed message Also fixes template-sync.test.ts indirectly — init tests were overwriting the repo's .github/agents/squad.agent.md causing a content mismatch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/cli/init.test.ts | 3 ++- test/cli/loop.test.ts | 2 +- test/cli/upgrade.test.ts | 3 ++- test/comms-teams-integration.test.ts | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/test/cli/init.test.ts b/test/cli/init.test.ts index f0c599a92..4f5d69f78 100644 --- a/test/cli/init.test.ts +++ b/test/cli/init.test.ts @@ -7,11 +7,12 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdir, rm, readdir, readFile } from 'fs/promises'; import { join } from 'path'; import { existsSync } from 'fs'; +import { tmpdir } from 'os'; import { randomBytes } from 'crypto'; import { runInit } from '@bradygaster/squad-cli/core/init'; import { getPackageVersion } from '@bradygaster/squad-cli/core/version'; -const TEST_ROOT = join(process.cwd(), `.test-cli-init-${randomBytes(4).toString('hex')}`); +const TEST_ROOT = join(tmpdir(), `.test-cli-init-${randomBytes(4).toString('hex')}`); describe('CLI: init command', () => { beforeEach(async () => { diff --git a/test/cli/loop.test.ts b/test/cli/loop.test.ts index 54b5b48fa..a3030a5dc 100644 --- a/test/cli/loop.test.ts +++ b/test/cli/loop.test.ts @@ -365,7 +365,7 @@ describe('runLoop', () => { }); await expect(runLoop(DEST, defaultOptions)).rejects.toThrow( - /gh CLI/i, + /Copilot CLI/i, ); }); diff --git a/test/cli/upgrade.test.ts b/test/cli/upgrade.test.ts index a5282f258..f69b8a176 100644 --- a/test/cli/upgrade.test.ts +++ b/test/cli/upgrade.test.ts @@ -7,12 +7,13 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { mkdir, rm, readFile, writeFile } from 'fs/promises'; import { join } from 'path'; import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync, chmodSync } from 'fs'; +import { tmpdir } from 'os'; import { randomBytes } from 'crypto'; import { runInit } from '@bradygaster/squad-cli/core/init'; import { runUpgrade, ensureGitattributes, ensureGitignore, ensureDirectories, ensureCastingDefaults, selfUpgradeCli } from '@bradygaster/squad-cli/core/upgrade'; import { getPackageVersion } from '@bradygaster/squad-cli/core/version'; -const TEST_ROOT = join(process.cwd(), `.test-cli-upgrade-${randomBytes(4).toString('hex')}`); +const TEST_ROOT = join(tmpdir(), `.test-cli-upgrade-${randomBytes(4).toString('hex')}`); describe('CLI: upgrade command', () => { beforeEach(async () => { diff --git a/test/comms-teams-integration.test.ts b/test/comms-teams-integration.test.ts index cf3c3da62..689b000ae 100644 --- a/test/comms-teams-integration.test.ts +++ b/test/comms-teams-integration.test.ts @@ -360,7 +360,7 @@ describe('token refresh failure fallback', () => { // Refresh was attempted and failed expect(refreshAttempted).toBe(true); // Warning logged about refresh failure - expect(warnSpy).toHaveBeenCalledWith('⚠️ Token refresh failed — re-authenticating...'); + expect(warnSpy).toHaveBeenCalledWith('⚠️ Token refresh permanently failed (invalid_grant) — re-authenticating...'); // Fell through to device code and succeeded expect(deviceCodeCompleted).toBe(true); expect(result.id).toBeDefined(); From 6b5dfaaf09fad58cf20fc85b8d612a2fcd806b17 Mon Sep 17 00:00:00 2001 From: Copilot Date: Sat, 18 Apr 2026 14:10:28 +0300 Subject: [PATCH 4/7] fix: use tmpdir for export-import and scrub-emails tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same monorepo detection fix as init/upgrade — tests creating temp dirs inside the repo checkout cause detectParentGitRepo() to write to .github/agents/ instead of the test dir, corrupting template-sync. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/cli/export-import.test.ts | 5 +++-- test/cli/scrub-emails.test.ts | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test/cli/export-import.test.ts b/test/cli/export-import.test.ts index b25b45a00..8c6ba84c7 100644 --- a/test/cli/export-import.test.ts +++ b/test/cli/export-import.test.ts @@ -8,12 +8,13 @@ import { mkdir, rm, readFile, writeFile } from 'fs/promises'; import { join } from 'path'; import { existsSync } from 'fs'; import { randomBytes } from 'crypto'; +import { tmpdir } from 'os'; import { runInit } from '@bradygaster/squad-cli/core/init'; import { runExport } from '@bradygaster/squad-cli/commands/export'; import { runImport } from '@bradygaster/squad-cli/commands/import'; -const TEST_ROOT = join(process.cwd(), `.test-cli-export-import-${randomBytes(4).toString('hex')}`); -const IMPORT_ROOT = join(process.cwd(), `.test-cli-import-target-${randomBytes(4).toString('hex')}`); +const TEST_ROOT = join(tmpdir(), `.test-cli-export-import-${randomBytes(4).toString('hex')}`); +const IMPORT_ROOT = join(tmpdir(), `.test-cli-import-target-${randomBytes(4).toString('hex')}`); describe('CLI: export/import commands', () => { beforeEach(async () => { diff --git a/test/cli/scrub-emails.test.ts b/test/cli/scrub-emails.test.ts index d1c1281c6..c7f1afa31 100644 --- a/test/cli/scrub-emails.test.ts +++ b/test/cli/scrub-emails.test.ts @@ -8,10 +8,11 @@ import { mkdir, rm, readFile, writeFile } from 'fs/promises'; import { join } from 'path'; import { existsSync } from 'fs'; import { randomBytes } from 'crypto'; +import { tmpdir } from 'os'; import { runInit } from '@bradygaster/squad-cli/core/init'; import { scrubEmails } from '@bradygaster/squad-cli/core/email-scrub'; -const TEST_ROOT = join(process.cwd(), `.test-cli-scrub-${randomBytes(4).toString('hex')}`); +const TEST_ROOT = join(tmpdir(), `.test-cli-scrub-${randomBytes(4).toString('hex')}`); describe('CLI: scrub-emails command', () => { beforeEach(async () => { From 7c62b86aa598bdd38083926bf089db91cb596b89 Mon Sep 17 00:00:00 2001 From: Copilot Date: Sat, 18 Apr 2026 14:16:20 +0300 Subject: [PATCH 5/7] fix: use tmpdir() in init-upgrade-parity.test.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same monorepo detection fix as other test files — place temp dir outside git repo to prevent detectParentGitRepo() from corrupting .github/agents/squad.agent.md during tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/cli/init-upgrade-parity.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/cli/init-upgrade-parity.test.ts b/test/cli/init-upgrade-parity.test.ts index 2a6279518..18129d990 100644 --- a/test/cli/init-upgrade-parity.test.ts +++ b/test/cli/init-upgrade-parity.test.ts @@ -13,6 +13,7 @@ import { mkdir, rm, readFile, writeFile } from 'fs/promises'; import { join } from 'path'; import { existsSync } from 'fs'; import { randomBytes } from 'crypto'; +import { tmpdir } from 'os'; import { runInit } from '@bradygaster/squad-cli/core/init'; import { runUpgrade, @@ -21,7 +22,7 @@ import { } from '@bradygaster/squad-cli/core/upgrade'; const TEST_ROOT = join( - process.cwd(), + tmpdir(), `.test-init-upgrade-parity-${randomBytes(4).toString('hex')}`, ); From 3a587332df94be0a1601114dff40d725807ce606 Mon Sep 17 00:00:00 2001 From: Copilot Date: Sat, 18 Apr 2026 14:25:32 +0300 Subject: [PATCH 6/7] fix: re-sync templates before byte comparison in template-sync test Acceptance tests run 'squad init' from the repo root, which overwrites .github/agents/squad.agent.md with CLI template content + version stamp. Since Vitest runs test files in parallel, this corrupts the file before template-sync checks it. Add beforeAll() that re-runs sync-templates.mjs to restore the canonical state before any byte-for-byte comparisons. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/template-sync.test.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/test/template-sync.test.ts b/test/template-sync.test.ts index c5e27a8ca..f4d5e674c 100644 --- a/test/template-sync.test.ts +++ b/test/template-sync.test.ts @@ -13,7 +13,7 @@ * 4. Semantic checks — universe counts, casting-policy internal consistency. */ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, beforeAll } from 'vitest'; import { readFileSync, existsSync, readdirSync } from 'node:fs'; import { resolve, dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -22,6 +22,18 @@ import { execSync } from 'node:child_process'; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT = resolve(__dirname, '..'); +// Re-sync templates before any byte-comparison checks. +// Other test files (e.g., acceptance tests) may run `squad init` in the +// repo root, overwriting .github/agents/squad.agent.md from the CLI +// template and making it diverge from .squad-templates/squad.agent.md. +beforeAll(() => { + execSync('node scripts/sync-templates.mjs', { + cwd: ROOT, + encoding: 'utf-8', + timeout: 60_000, + }); +}); + // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- From 2519dcb0001ec670941f5ef9dc858bc6d1cef9e4 Mon Sep 17 00:00:00 2001 From: Copilot Date: Sat, 18 Apr 2026 17:23:10 +0300 Subject: [PATCH 7/7] fix: use tmpdir() in consult.test.ts to avoid monorepo detection Same pattern as init, upgrade, export-import, scrub-emails, and init-upgrade-parity tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/cli/consult.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/cli/consult.test.ts b/test/cli/consult.test.ts index 1455f7c02..9b30a6d19 100644 --- a/test/cli/consult.test.ts +++ b/test/cli/consult.test.ts @@ -19,11 +19,12 @@ import { } from 'node:fs'; import { join } from 'node:path'; import { randomBytes } from 'node:crypto'; +import { tmpdir } from 'node:os'; import { execSync } from 'node:child_process'; import { isConsultMode, type SquadDirConfig } from '@bradygaster/squad-sdk'; const TEST_ROOT = join( - process.cwd(), + tmpdir(), `.test-cli-consult-${randomBytes(4).toString('hex')}`, );