Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/fix-cast-base-path.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@bradygaster/squad-cli': patch
---

Fix cast command passing wrong base path to LocalAgentSource — used repo root (cwd) instead of .squad/ dir to prevent double-nested .squad/.squad/agents/ lookup
2 changes: 1 addition & 1 deletion packages/squad-cli/src/cli/commands/cast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export async function runCast(cwd: string): Promise<void> {
}

// Discover project agents
const projectSource = new LocalAgentSource(paths.teamDir);
const projectSource = new LocalAgentSource(cwd);
const projectAgents = await projectSource.listAgents();

// Discover personal agents
Expand Down
97 changes: 97 additions & 0 deletions test/cli/cast.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* squad cast — session cast display tests
*
* Verifies the cast command correctly discovers project agents
* by passing repo root (not .squad/ dir) to LocalAgentSource.
* Regression test for #871 (double-nested .squad/.squad/agents path).
*/

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { mkdir, rm, writeFile } from 'fs/promises';
import { join } from 'path';
import { existsSync } from 'fs';
import { randomBytes } from 'crypto';

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

const SAMPLE_CHARTER = `---
name: TestAgent
role: Core Dev
---
# TestAgent

Test agent for cast command tests.
`;

async function scaffold(root: string): Promise<void> {
const sq = join(root, '.squad');
await mkdir(join(sq, 'agents', 'test-agent'), { recursive: true });
await writeFile(join(sq, 'agents', 'test-agent', 'charter.md'), SAMPLE_CHARTER);
await mkdir(join(sq, 'casting'), { recursive: true });
await writeFile(join(sq, 'team.md'), '# Team\n\n## Members\n\n- TestAgent\n');
await writeFile(join(sq, 'routing.md'), '# Routing\n');
await writeFile(join(sq, 'decisions.md'), '# Decisions\n');
await writeFile(
join(sq, 'casting', 'registry.json'),
JSON.stringify({ agents: [] }, null, 2),
);
}

// Mock personal agents to isolate project agent discovery
vi.mock('@bradygaster/squad-sdk/agents/personal', () => ({
resolvePersonalAgents: vi.fn(async () => [] as unknown[]),
mergeSessionCast: vi.fn((project: unknown[], personal: unknown[]) => [...(project as unknown[]), ...(personal as unknown[])]),
}));

describe('squad cast', () => {
beforeEach(async () => {
if (existsSync(TEST_ROOT)) {
await rm(TEST_ROOT, { recursive: true, force: true });
}
await mkdir(TEST_ROOT, { recursive: true });
});

afterEach(async () => {
vi.restoreAllMocks();
if (existsSync(TEST_ROOT)) {
await rm(TEST_ROOT, { recursive: true, force: true });
}
});

it('discovers project agents using repo root, not .squad/ dir (#871)', async () => {
await scaffold(TEST_ROOT);

// Suppress console output during test
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

const { runCast } = await import('@bradygaster/squad-cli/commands/cast');
await runCast(TEST_ROOT);

// If the bug were present (passing paths.teamDir = .squad/ to LocalAgentSource),
// it would look in .squad/.squad/agents/ — which doesn't exist — and find 0 agents.
// With the fix, it looks in TEST_ROOT/.squad/agents/ and finds our test agent.
const output = logSpy.mock.calls.map(c => c.join(' ')).join('\n');
// Agent discovered from .squad/agents/test-agent/ (name derived from directory)
expect(output).toContain('test-agent');
expect(output).toContain('Session Cast');
});

it('does not look in double-nested .squad/.squad/agents/ path', async () => {
await scaffold(TEST_ROOT);

// Create a decoy agent at the WRONG double-nested path
const wrongPath = join(TEST_ROOT, '.squad', '.squad', 'agents', 'decoy');
await mkdir(wrongPath, { recursive: true });
await writeFile(join(wrongPath, 'charter.md'), `---\nname: Decoy\nrole: Wrong\n---\n# Decoy\n`);

const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

const { runCast } = await import('@bradygaster/squad-cli/commands/cast');
await runCast(TEST_ROOT);

const output = logSpy.mock.calls.map(c => c.join(' ')).join('\n');
// Should find test-agent from correct path, not decoy from wrong path
expect(output).toContain('test-agent');
expect(output).not.toContain('decoy');
});
});
Loading