test: add agent-cli-check and identity-restore tests
27 new tests: detectAgentClis/requireAtLeastOneAgentCli/NoAgentCliError with mocked commandExists, plus identity-restore run() covering all skip conditions (missing .env, no Supabase config, fetch errors) and happy path (canonical + legacy paths, no-overwrite guard, blank file overwrite, header verification, quoted .env values). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- Build: FAIL | Tests: pass — Tests 1390 passed (1390)
This commit is contained in:
parent
abe3be71fd
commit
aba4886ed8
2 changed files with 367 additions and 0 deletions
151
setup/agent-cli-check.test.ts
Normal file
151
setup/agent-cli-check.test.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
/**
|
||||
* setup/agent-cli-check.test.ts — agent CLI detection tests.
|
||||
*
|
||||
* Tests AGENT_CLIS, detectAgentClis(), NoAgentCliError, and
|
||||
* requireAtLeastOneAgentCli() with mocked commandExists.
|
||||
*
|
||||
* Run with: npx vitest run setup/agent-cli-check.test.ts
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock commandExists from platform.js
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const platformMock = vi.hoisted(() => ({
|
||||
commandExists: vi.fn<[string], boolean>(() => false),
|
||||
}));
|
||||
|
||||
vi.mock('./platform.js', () => platformMock);
|
||||
|
||||
import {
|
||||
AGENT_CLIS,
|
||||
detectAgentClis,
|
||||
NoAgentCliError,
|
||||
requireAtLeastOneAgentCli,
|
||||
} from './agent-cli-check.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AGENT_CLIS
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('AGENT_CLIS', () => {
|
||||
it('contains pi, aider, claude, codex, gemini', () => {
|
||||
const commands = AGENT_CLIS.map((c) => c.command);
|
||||
expect(commands).toContain('pi');
|
||||
expect(commands).toContain('aider');
|
||||
expect(commands).toContain('claude');
|
||||
expect(commands).toContain('codex');
|
||||
expect(commands).toContain('gemini');
|
||||
});
|
||||
|
||||
it('each entry has name and command fields', () => {
|
||||
for (const cli of AGENT_CLIS) {
|
||||
expect(typeof cli.name).toBe('string');
|
||||
expect(typeof cli.command).toBe('string');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// detectAgentClis
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('detectAgentClis', () => {
|
||||
beforeEach(() => {
|
||||
platformMock.commandExists.mockReset().mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('returns one entry per AGENT_CLIS entry', () => {
|
||||
const result = detectAgentClis();
|
||||
expect(result).toHaveLength(AGENT_CLIS.length);
|
||||
});
|
||||
|
||||
it('marks all as not present when commandExists returns false', () => {
|
||||
const result = detectAgentClis();
|
||||
expect(result.every((c) => !c.present)).toBe(true);
|
||||
});
|
||||
|
||||
it('marks all as present when commandExists returns true', () => {
|
||||
platformMock.commandExists.mockReturnValue(true);
|
||||
const result = detectAgentClis();
|
||||
expect(result.every((c) => c.present)).toBe(true);
|
||||
});
|
||||
|
||||
it('marks only "pi" as present when only pi is available', () => {
|
||||
platformMock.commandExists.mockImplementation((cmd) => cmd === 'pi');
|
||||
const result = detectAgentClis();
|
||||
const piEntry = result.find((c) => c.command === 'pi');
|
||||
const aiderEntry = result.find((c) => c.command === 'aider');
|
||||
expect(piEntry?.present).toBe(true);
|
||||
expect(aiderEntry?.present).toBe(false);
|
||||
});
|
||||
|
||||
it('passes the command string to commandExists', () => {
|
||||
detectAgentClis();
|
||||
const calledCommands = platformMock.commandExists.mock.calls.map((c) => c[0]);
|
||||
expect(calledCommands).toContain('pi');
|
||||
expect(calledCommands).toContain('aider');
|
||||
});
|
||||
|
||||
it('returned entries include name and command from AGENT_CLIS', () => {
|
||||
const result = detectAgentClis();
|
||||
for (let i = 0; i < AGENT_CLIS.length; i++) {
|
||||
expect(result[i].name).toBe(AGENT_CLIS[i].name);
|
||||
expect(result[i].command).toBe(AGENT_CLIS[i].command);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NoAgentCliError
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('NoAgentCliError', () => {
|
||||
it('is an instance of Error', () => {
|
||||
expect(new NoAgentCliError()).toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
it('has name "NoAgentCliError"', () => {
|
||||
expect(new NoAgentCliError().name).toBe('NoAgentCliError');
|
||||
});
|
||||
|
||||
it('message mentions agent CLI and install guidance', () => {
|
||||
const msg = new NoAgentCliError().message;
|
||||
expect(msg).toContain('No agent CLI found');
|
||||
expect(msg.toLowerCase()).toContain('pi');
|
||||
expect(msg.toLowerCase()).toContain('aider');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// requireAtLeastOneAgentCli
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('requireAtLeastOneAgentCli', () => {
|
||||
beforeEach(() => {
|
||||
platformMock.commandExists.mockReset().mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('throws NoAgentCliError when no CLI is present', () => {
|
||||
expect(() => requireAtLeastOneAgentCli()).toThrow(NoAgentCliError);
|
||||
});
|
||||
|
||||
it('does not throw when at least one CLI is present', () => {
|
||||
platformMock.commandExists.mockImplementation((cmd) => cmd === 'claude');
|
||||
expect(() => requireAtLeastOneAgentCli()).not.toThrow();
|
||||
});
|
||||
|
||||
it('returns the full list of CLIs', () => {
|
||||
platformMock.commandExists.mockReturnValue(true);
|
||||
const result = requireAtLeastOneAgentCli();
|
||||
expect(result).toHaveLength(AGENT_CLIS.length);
|
||||
});
|
||||
|
||||
it('returned list includes present=true for available CLIs', () => {
|
||||
platformMock.commandExists.mockImplementation((cmd) => cmd === 'aider');
|
||||
const result = requireAtLeastOneAgentCli();
|
||||
const aider = result.find((c) => c.command === 'aider');
|
||||
expect(aider?.present).toBe(true);
|
||||
});
|
||||
});
|
||||
216
setup/identity-restore.test.ts
Normal file
216
setup/identity-restore.test.ts
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
/**
|
||||
* setup/identity-restore.test.ts — identity-restore run() tests.
|
||||
*
|
||||
* Tests the skip-conditions (no .env, missing Supabase config, fetch error)
|
||||
* and the happy path (resolveFile with multi-path candidates, no-overwrite guard).
|
||||
*
|
||||
* The private parseEnv() and resolveFile() helpers are covered via run().
|
||||
*
|
||||
* Run with: npx vitest run setup/identity-restore.test.ts
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import path from 'path';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const fsMocks = vi.hoisted(() => ({
|
||||
existsSync: vi.fn<[string], boolean>(() => false),
|
||||
readFileSync: vi.fn<[string, BufferEncoding], string>(() => ''),
|
||||
writeFileSync: vi.fn<[string, string, string], void>(),
|
||||
}));
|
||||
|
||||
vi.mock('fs', async () => {
|
||||
const actual = await vi.importActual<typeof import('fs')>('fs');
|
||||
const mocked = { ...actual, ...fsMocks };
|
||||
return { ...mocked, default: mocked };
|
||||
});
|
||||
|
||||
vi.mock('../src/logger.js', () => ({
|
||||
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock('./status.js', () => ({ emitStatus: vi.fn() }));
|
||||
|
||||
// Global fetch mock
|
||||
const fetchMock = vi.fn<[string, RequestInit?], Promise<Response>>();
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
import { run } from './identity-restore.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ENV_WITH_SUPABASE =
|
||||
'SUPABASE_URL=https://abc.supabase.co\nSUPABASE_SERVICE_ROLE_KEY=svc-key\n';
|
||||
|
||||
function makeResponse(data: unknown, ok = true, status = 200): Response {
|
||||
return {
|
||||
ok,
|
||||
status,
|
||||
text: vi.fn().mockResolvedValue(JSON.stringify(data)),
|
||||
json: vi.fn().mockResolvedValue(data),
|
||||
} as unknown as Response;
|
||||
}
|
||||
|
||||
const ENV_FILE = path.join(process.cwd(), '.env');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Skip conditions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('identity-restore.run — skip conditions', () => {
|
||||
beforeEach(() => {
|
||||
fsMocks.existsSync.mockReset().mockReturnValue(false);
|
||||
fsMocks.readFileSync.mockReset().mockReturnValue('');
|
||||
fsMocks.writeFileSync.mockReset();
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
it('skips when .env does not exist', async () => {
|
||||
fsMocks.existsSync.mockReturnValue(false);
|
||||
await run([]);
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(fsMocks.writeFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips when SUPABASE_URL is not set', async () => {
|
||||
fsMocks.existsSync.mockReturnValue(true);
|
||||
fsMocks.readFileSync.mockReturnValue('AGENT_NAME=clawdie\n');
|
||||
await run([]);
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips when SUPABASE_SERVICE_ROLE_KEY is not set', async () => {
|
||||
fsMocks.existsSync.mockReturnValue(true);
|
||||
fsMocks.readFileSync.mockReturnValue('SUPABASE_URL=https://abc.supabase.co\n');
|
||||
await run([]);
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips gracefully when fetch throws', async () => {
|
||||
fsMocks.existsSync.mockReturnValue(true);
|
||||
fsMocks.readFileSync.mockReturnValue(ENV_WITH_SUPABASE);
|
||||
fetchMock.mockRejectedValue(new Error('Network unreachable'));
|
||||
await expect(run([])).resolves.toBeUndefined();
|
||||
expect(fsMocks.writeFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips gracefully when fetch returns non-ok status', async () => {
|
||||
fsMocks.existsSync.mockReturnValue(true);
|
||||
fsMocks.readFileSync.mockReturnValue(ENV_WITH_SUPABASE);
|
||||
fetchMock.mockResolvedValue(makeResponse('Unauthorized', false, 401));
|
||||
await expect(run([])).resolves.toBeUndefined();
|
||||
expect(fsMocks.writeFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Happy path — file writing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('identity-restore.run — happy path', () => {
|
||||
beforeEach(() => {
|
||||
fsMocks.existsSync.mockReset();
|
||||
fsMocks.readFileSync.mockReset();
|
||||
fsMocks.writeFileSync.mockReset();
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
it('writes SOUL.md when found under canonical path', async () => {
|
||||
fsMocks.existsSync.mockImplementation((p: string) => {
|
||||
return p === ENV_FILE; // .env exists; identity files do not
|
||||
});
|
||||
fsMocks.readFileSync.mockReturnValue(ENV_WITH_SUPABASE);
|
||||
const rows = [{ file_path: 'SOUL.md', content: '# Soul content' }];
|
||||
fetchMock.mockResolvedValue(makeResponse(rows));
|
||||
|
||||
await run([]);
|
||||
const written = fsMocks.writeFileSync.mock.calls.find(
|
||||
(c) => (c[0] as string).endsWith('SOUL.md'),
|
||||
);
|
||||
expect(written).toBeDefined();
|
||||
expect(written?.[1]).toBe('# Soul content');
|
||||
});
|
||||
|
||||
it('writes SOUL.md when found under legacy path v2/SOUL.md', async () => {
|
||||
fsMocks.existsSync.mockImplementation((p: string) => p === ENV_FILE);
|
||||
fsMocks.readFileSync.mockReturnValue(ENV_WITH_SUPABASE);
|
||||
const rows = [{ file_path: 'v2/SOUL.md', content: '# Legacy soul' }];
|
||||
fetchMock.mockResolvedValue(makeResponse(rows));
|
||||
|
||||
await run([]);
|
||||
const written = fsMocks.writeFileSync.mock.calls.find(
|
||||
(c) => (c[0] as string).endsWith('SOUL.md'),
|
||||
);
|
||||
expect(written).toBeDefined();
|
||||
expect(written?.[1]).toBe('# Legacy soul');
|
||||
});
|
||||
|
||||
it('does not overwrite existing non-empty SOUL.md', async () => {
|
||||
fsMocks.existsSync.mockImplementation(() => true); // everything exists
|
||||
fsMocks.readFileSync.mockImplementation((p: string) => {
|
||||
if ((p as string) === ENV_FILE) return ENV_WITH_SUPABASE;
|
||||
return '# Existing soul content'; // non-empty existing file
|
||||
});
|
||||
const rows = [{ file_path: 'SOUL.md', content: '# New soul' }];
|
||||
fetchMock.mockResolvedValue(makeResponse(rows));
|
||||
|
||||
await run([]);
|
||||
expect(fsMocks.writeFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('overwrites empty (blank) existing identity file', async () => {
|
||||
fsMocks.existsSync.mockImplementation(() => true);
|
||||
fsMocks.readFileSync.mockImplementation((p: string) => {
|
||||
if ((p as string) === ENV_FILE) return ENV_WITH_SUPABASE;
|
||||
return ' \n'; // blank file
|
||||
});
|
||||
const rows = [{ file_path: 'SOUL.md', content: '# Restored soul' }];
|
||||
fetchMock.mockResolvedValue(makeResponse(rows));
|
||||
|
||||
await run([]);
|
||||
const written = fsMocks.writeFileSync.mock.calls.find(
|
||||
(c) => (c[0] as string).endsWith('SOUL.md'),
|
||||
);
|
||||
expect(written).toBeDefined();
|
||||
expect(written?.[1]).toBe('# Restored soul');
|
||||
});
|
||||
|
||||
it('writes nothing when Supabase returns empty rows', async () => {
|
||||
fsMocks.existsSync.mockImplementation((p: string) => p === ENV_FILE);
|
||||
fsMocks.readFileSync.mockReturnValue(ENV_WITH_SUPABASE);
|
||||
fetchMock.mockResolvedValue(makeResponse([]));
|
||||
|
||||
await run([]);
|
||||
expect(fsMocks.writeFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sends Authorization and apikey headers to Supabase', async () => {
|
||||
fsMocks.existsSync.mockImplementation((p: string) => p === ENV_FILE);
|
||||
fsMocks.readFileSync.mockReturnValue(ENV_WITH_SUPABASE);
|
||||
fetchMock.mockResolvedValue(makeResponse([]));
|
||||
|
||||
await run([]);
|
||||
const [, opts] = fetchMock.mock.calls[0] as [string, RequestInit];
|
||||
const headers = opts.headers as Record<string, string>;
|
||||
expect(headers['Authorization']).toBe('Bearer svc-key');
|
||||
expect(headers['apikey']).toBe('svc-key');
|
||||
});
|
||||
|
||||
it('parses .env quoted values correctly (strips single quotes)', async () => {
|
||||
fsMocks.existsSync.mockImplementation((p: string) => p === ENV_FILE);
|
||||
fsMocks.readFileSync.mockReturnValue(
|
||||
"SUPABASE_URL='https://quoted.supabase.co'\nSUPABASE_SERVICE_ROLE_KEY='quoted-key'\n",
|
||||
);
|
||||
fetchMock.mockResolvedValue(makeResponse([]));
|
||||
|
||||
await run([]);
|
||||
// Fetch should have been called — confirming URL was parsed correctly
|
||||
expect(fetchMock).toHaveBeenCalled();
|
||||
const [url] = fetchMock.mock.calls[0] as [string];
|
||||
expect(url).toContain('https://quoted.supabase.co');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue