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:
Clawdie AI 2026-04-15 05:14:52 +00:00
parent abe3be71fd
commit aba4886ed8
2 changed files with 367 additions and 0 deletions

View 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);
});
});

View 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');
});
});