Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions test/acceptance/acceptance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@

import { afterEach } from 'vitest';
import { join } from 'node:path';
import { rmSync } from 'node:fs';
import { createRegistry, runFeature } from './support/runner.js';
import { registerCLISteps } from './steps/cli-steps.js';
import { cleanupAcceptanceTempDirs, registerCLISteps } from './steps/cli-steps.js';
import { TerminalHarness } from './harness.js';

// Create step definition registry
Expand All @@ -20,6 +19,7 @@ afterEach(async (context) => {
if (harness) {
await harness.close();
}
cleanupAcceptanceTempDirs();
});

// Run all feature files
Expand Down
7 changes: 4 additions & 3 deletions test/acceptance/features/init-command.feature
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
Feature: Init command

Scenario: Init in existing project shows ready message
Given the current directory has a ".squad" directory
When I run "squad init"
Given a temporary project with an existing ".squad" directory
When I run "squad init" in the temp directory
Then the output contains "Squad initialized"
And the output contains "already exists"
And the exit code is 0

Scenario: Init exit code is zero on success
When I run "squad init"
Given a directory without a ".squad" directory
When I run "squad init" in the temp directory
Then the exit code is 0
31 changes: 28 additions & 3 deletions test/acceptance/steps/cli-steps.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import { expect } from 'vitest';
import { existsSync, mkdtempSync } from 'node:fs';
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { TerminalHarness } from '../harness.js';
import { StepDefinitions, registerStep } from '../support/runner.js';

const tempDirs = new Set<string>();

export function cleanupAcceptanceTempDirs(): void {
for (const dir of tempDirs) rmSync(dir, { recursive: true, force: true });
tempDirs.clear();
}

function makeTempDir(): string {
const dir = mkdtempSync(join(tmpdir(), 'squad-e2e-'));
tempDirs.add(dir);
return dir;
}

/**
* CLI step definitions for Gherkin acceptance tests.
*/
Expand Down Expand Up @@ -32,14 +45,26 @@ export function registerCLISteps(registry: StepDefinitions): void {

registerStep(
'Given',
/a directory without a "\.squad" directory/,
/a temporary project with an existing "\.squad" directory/,
async (_stepText, context) => {
const tempDir = mkdtempSync(join(tmpdir(), 'squad-e2e-'));
const tempDir = makeTempDir();
const squadDir = join(tempDir, '.squad');
mkdirSync(squadDir, { recursive: true });
writeFileSync(join(squadDir, 'team.md'), '# Acceptance team\n');
context.tempDir = tempDir;
},
registry
);

registerStep(
'Given',
/a directory without a "\.squad" directory/,
async (_stepText, context) => {
context.tempDir = makeTempDir();
},
registry
);

// --- When steps ---

registerStep(
Expand Down
3 changes: 2 additions & 1 deletion test/casting-legacy-migration.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { afterEach, describe, expect, it } from 'vitest';
import {
_setCastingDurabilityHooksForTesting,
Expand All @@ -25,7 +26,7 @@ function fixture() {
}

function castingDir(): string {
const root = mkdtempSync(join(process.cwd(), '.test-legacy-migration-'));
const root = mkdtempSync(join(tmpdir(), '.test-legacy-migration-'));
roots.push(root);
const dir = join(root, 'casting');
mkdirSync(dir);
Expand Down
3 changes: 2 additions & 1 deletion test/casting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { afterEach, describe, it, expect } from 'vitest';
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import {
CastingEngine,
type CastingConfig,
Expand All @@ -16,7 +17,7 @@ import {
const castingRoots: string[] = [];

function castingDir(): string {
const root = mkdtempSync(join(process.cwd(), '.casting-registry-test-'));
const root = mkdtempSync(join(tmpdir(), 'casting-registry-test-'));
const dir = join(root, 'casting');
mkdirSync(dir);
castingRoots.push(root);
Expand Down
6 changes: 6 additions & 0 deletions test/cli-packaging-smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ const NO_COLOR_ENV = {
FORCE_COLOR: '0',
npm_config_loglevel: process.env['npm_config_loglevel'] ?? 'warn',
};
delete NO_COLOR_ENV['npm_config_local_prefix'];
delete NO_COLOR_ENV['npm_config_workspace'];
delete NO_COLOR_ENV['npm_config_workspaces'];
delete NO_COLOR_ENV['NPM_CONFIG_LOCAL_PREFIX'];
delete NO_COLOR_ENV['NPM_CONFIG_WORKSPACE'];
delete NO_COLOR_ENV['NPM_CONFIG_WORKSPACES'];
const MISSING_MODULE_RE = /MODULE_NOT_FOUND|ERR_MODULE_NOT_FOUND|Cannot find module|Cannot find package/i;
const EXIT_COMMAND_TIMEOUT_MS = process.platform === 'win32' ? 10_000 : 2_000;

Expand Down
10 changes: 4 additions & 6 deletions test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
*/

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdir, rm, writeFile, readFile } from 'fs/promises';
import { mkdir, mkdtemp, rm, writeFile, readFile } from 'fs/promises';
import { join } from 'path';
import { existsSync } from 'fs';
import { tmpdir } from 'os';

import {
parseVersion,
Expand Down Expand Up @@ -310,14 +311,11 @@ describe('getInstallInstructions', () => {
// Copilot Install — installFromCopilot
// ============================================================================

const INSTALL_TEST_ROOT = join(process.cwd(), 'test-fixtures', 'copilot-install-test');
let INSTALL_TEST_ROOT: string;

describe('installFromCopilot', () => {
beforeEach(async () => {
if (existsSync(INSTALL_TEST_ROOT)) {
await rm(INSTALL_TEST_ROOT, { recursive: true, force: true });
}
await mkdir(INSTALL_TEST_ROOT, { recursive: true });
INSTALL_TEST_ROOT = await mkdtemp(join(tmpdir(), 'squad-copilot-install-'));
});

afterEach(async () => {
Expand Down
7 changes: 5 additions & 2 deletions test/cli/cast.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import { mkdir, rm, writeFile, readFile } from 'fs/promises';
import { join } from 'path';
import { existsSync } from 'fs';
import { randomBytes } from 'crypto';
import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os';

const TEST_ROOT = join(process.cwd(), `.test-cast-${randomBytes(4).toString('hex')}`);
const TEST_ROOT = join(tmpdir(), `.test-cast-${randomBytes(4).toString('hex')}`);

/**
* Charter using ## Identity sections, as expected by parseCharterMetadata().
Expand Down Expand Up @@ -49,6 +51,7 @@ describe('squad cast', () => {
await rm(TEST_ROOT, { recursive: true, force: true });
}
await mkdir(TEST_ROOT, { recursive: true });
execFileSync('git', ['init', '--quiet'], { cwd: TEST_ROOT });
});

afterEach(async () => {
Expand All @@ -73,7 +76,7 @@ describe('squad cast', () => {
const output = logSpy.mock.calls.map(c => c.join(' ')).join('\n');
expect(output).toContain('TestAgent');
expect(output).toContain('Session Cast');
});
}, 15_000);

it('does not look in double-nested .squad/.squad/agents/ path', async () => {
await scaffold(TEST_ROOT);
Expand Down
5 changes: 3 additions & 2 deletions test/cli/legacy-casting-upgrade.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { runInit } from '@bradygaster/squad-cli/core/init';
import { runUpgrade } from '@bradygaster/squad-cli/core/upgrade';
Expand Down Expand Up @@ -35,8 +36,8 @@ function version(value: string): void {

describe('upgrade preflights legacy casting before managed writes', () => {
beforeEach(async () => {
root = mkdtempSync(join(process.cwd(), '.test-legacy-upgrade-'));
home = mkdtempSync(join(process.cwd(), '.test-legacy-home-'));
root = mkdtempSync(join(tmpdir(), 'squad-legacy-upgrade-'));
home = mkdtempSync(join(tmpdir(), 'squad-legacy-home-'));
execFileSync('git', ['init', '--quiet'], { cwd: root });
process.env.SQUAD_HOME_DIR_OVERRIDE = home;
await runInit(root);
Expand Down
3 changes: 2 additions & 1 deletion test/cli/state-mcp-lifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { randomBytes } from 'node:crypto';

const ROOT = process.cwd();
const TMP = path.join(ROOT, `.test-state-mcp-lifecycle-${randomBytes(4).toString('hex')}`);
const TMP = path.join(tmpdir(), `.test-state-mcp-lifecycle-${randomBytes(4).toString('hex')}`);
const CLI_ENTRY = path.join(ROOT, 'packages', 'squad-cli', 'dist', 'cli-entry.js');
const activeChildren = new Set<ChildProcessWithoutNullStreams>();

Expand Down
11 changes: 4 additions & 7 deletions test/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,20 @@
*/

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdir, rm, readFile, writeFile } from 'fs/promises';
import { mkdir, mkdtemp, rm, readFile, writeFile } from 'fs/promises';
import { join } from 'path';
import { existsSync } from 'fs';
import { tmpdir } from 'node:os';
import { initSquad, MANIFEST_SKILL_NAMES } from '@bradygaster/squad-sdk/config';
import { onboardAgent, addAgentToConfig } from '@bradygaster/squad-sdk/agents';
import type { InitOptions, InitAgentSpec } from '@bradygaster/squad-sdk/config';
import type { OnboardOptions } from '@bradygaster/squad-sdk/agents';

const TEST_ROOT = join(process.cwd(), 'test-fixtures', 'init-test');
let TEST_ROOT: string;

describe('Squad Initialization', () => {
beforeEach(async () => {
// Clean up test directory
if (existsSync(TEST_ROOT)) {
await rm(TEST_ROOT, { recursive: true, force: true });
}
await mkdir(TEST_ROOT, { recursive: true });
TEST_ROOT = await mkdtemp(join(tmpdir(), 'squad-sdk-init-test-'));
});

afterEach(async () => {
Expand Down
3 changes: 2 additions & 1 deletion test/memory-governance.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { afterEach, describe, expect, it } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { randomUUID } from 'node:crypto';
import { FSStorageProvider } from '../packages/squad-sdk/src/storage/fs-storage-provider.js';
import {
Expand All @@ -15,7 +16,7 @@ import { ensureMemoryGovernanceUpgradeDefaults } from '../packages/squad-cli/src
const roots: string[] = [];

function testRoot(prefix: string): string {
const root = path.join(process.cwd(), `.test-${prefix}-${randomUUID()}`);
const root = path.join(os.tmpdir(), `.test-${prefix}-${randomUUID()}`);
roots.push(root);
fs.mkdirSync(path.join(root, '.squad'), { recursive: true });
return root;
Expand Down
3 changes: 2 additions & 1 deletion test/memory-providers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import { afterEach, describe, expect, it } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { randomUUID } from 'node:crypto';
import { FSStorageProvider } from '../packages/squad-sdk/src/storage/fs-storage-provider.js';
import {
Expand All @@ -28,7 +29,7 @@ import {
const roots: string[] = [];

function testRoot(prefix: string): string {
const root = path.join(process.cwd(), `.test-${prefix}-${randomUUID()}`);
const root = path.join(os.tmpdir(), `.test-${prefix}-${randomUUID()}`);
roots.push(root);
fs.mkdirSync(path.join(root, '.squad'), { recursive: true });
return root;
Expand Down
4 changes: 2 additions & 2 deletions test/presets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import { join } from 'node:path';
import { randomBytes } from 'node:crypto';
import { spawn } from 'node:child_process';
import { hostname } from 'node:os';
import { hostname, tmpdir } from 'node:os';
import { resolveSquadHome, ensureSquadHome, resolvePresetsDir } from '@bradygaster/squad-sdk/resolution';
import {
listPresets,
Expand All @@ -37,7 +37,7 @@ import {
} from '../packages/squad-sdk/src/presets/scaffold.js';
import { applyPreset as applyPresetSource } from '../packages/squad-sdk/src/presets/index.js';

const TMP = join(process.cwd(), `.test-presets-${randomBytes(4).toString('hex')}`);
const TMP = join(tmpdir(), `.test-presets-${randomBytes(4).toString('hex')}`);

function scaffold(...dirs: string[]): void {
for (const d of dirs) {
Expand Down
57 changes: 57 additions & 0 deletions test/setup/tracked-worktree-guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { execFileSync } from 'node:child_process';

export interface TrackedWorktreeSnapshot {
head: string;
diff: Buffer;
files: string[];
}

function git(cwd: string, args: string[]): Buffer {
return execFileSync('git', args, { cwd, maxBuffer: 64 * 1024 * 1024 });
}

export function snapshotTrackedWorktree(cwd: string): TrackedWorktreeSnapshot {
return {
head: git(cwd, ['rev-parse', 'HEAD']).toString('utf8').trim(),
diff: git(cwd, ['diff', '--no-ext-diff', '--binary', '--ignore-cr-at-eol', 'HEAD', '--']),
files: git(cwd, ['diff', '--no-ext-diff', '--name-only', '-z', '--ignore-cr-at-eol', 'HEAD', '--'])
.toString('utf8')
.split('\0')
.filter(Boolean),
};
}

function repositoryRoot(cwd: string): string | undefined {
try {
return git(cwd, ['rev-parse', '--show-toplevel']).toString('utf8').trim();
} catch (error) {
const failure = error as { status?: number; stderr?: Buffer };
if (failure.status === 128 && failure.stderr?.toString('utf8').includes('not a git repository')) {
return undefined;
}
throw error;
}
}

export function assertTrackedWorktreeUnchanged(cwd: string, before: TrackedWorktreeSnapshot): void {
const after = snapshotTrackedWorktree(cwd);
if (before.head === after.head && before.diff.equals(after.diff)) return;

const files = [...new Set([...before.files, ...after.files])].sort();
const headChanged = before.head !== after.head ? '\n HEAD changed during the test run.' : '';
throw new Error(
`Tests changed tracked files in the working tree:${headChanged}\n${files.map(file => ` ${file}`).join('\n')}`,
);
}

export function createTrackedWorktreeGuard(cwd: string): (() => void) | undefined {
const root = repositoryRoot(cwd);
if (!root) return;

const before = snapshotTrackedWorktree(root);
return () => assertTrackedWorktreeUnchanged(root, before);
}

export function setup(): (() => void) | undefined {
return createTrackedWorktreeGuard(process.cwd());
}
3 changes: 2 additions & 1 deletion test/speed-gates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import { describe, it, expect, afterEach } from 'vitest';
import { resolve } from 'node:path';
import { existsSync, mkdirSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { TerminalHarness } from './acceptance/harness.js';
import { parseInput } from '@bradygaster/squad-cli/shell/router';
import { isInitNoColor } from '@bradygaster/squad-cli/core/init';
Expand Down Expand Up @@ -75,7 +76,7 @@ describe('Speed: squad init ceremony', () => {
});

it('init ceremony in non-TTY completes under 5 seconds', async () => {
const tmpDir = resolve(process.cwd(), 'test-fixtures', '_speed-test-init-' + Date.now());
const tmpDir = resolve(tmpdir(), '_speed-test-init-' + Date.now());
mkdirSync(tmpDir, { recursive: true });

let harness: TerminalHarness | null = null;
Expand Down
Loading
Loading