ktx/packages/cli/src/demo-interaction.test.ts
2026-05-10 23:51:24 +02:00

127 lines
4.1 KiB
TypeScript

import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ensureDemoProject } from './demo-assets.js';
import {
chooseDemoProjectForInteractiveRun,
createTestDemoPromptAdapter,
resolveFullCredentialDecision,
} from './demo-interaction.js';
function io(isTTY: boolean) {
return {
stdin: { isTTY },
stdout: { isTTY, write: vi.fn() },
stderr: { write: vi.fn() },
};
}
describe('demo interaction decisions', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'ktx-demo-interaction-'));
});
afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});
it('reuses a valid project without prompting in no-input mode', async () => {
await ensureDemoProject({ projectDir: tempDir, force: false });
await expect(
chooseDemoProjectForInteractiveRun({
projectDir: tempDir,
inputMode: 'disabled',
io: io(false),
prompts: createTestDemoPromptAdapter({ choices: [] }),
}),
).resolves.toEqual({ action: 'use', projectDir: tempDir, reset: false });
});
it('fails corrupted projects in no-input mode with reset guidance', async () => {
await ensureDemoProject({ projectDir: tempDir, force: false });
await rm(join(tempDir, 'demo.db'), { force: true });
await expect(
chooseDemoProjectForInteractiveRun({
projectDir: tempDir,
inputMode: 'disabled',
io: io(false),
prompts: createTestDemoPromptAdapter({ choices: [] }),
}),
).rejects.toThrow(
`Demo project is not ready at ${tempDir}: missing demo.db. Run ktx setup demo reset --project-dir ${tempDir} --force --no-input`,
);
});
it('lets interactive users reset a corrupted project', async () => {
await ensureDemoProject({ projectDir: tempDir, force: false });
await rm(join(tempDir, 'demo.db'), { force: true });
await expect(
chooseDemoProjectForInteractiveRun({
projectDir: tempDir,
io: io(true),
prompts: createTestDemoPromptAdapter({ choices: ['reset'], confirms: [true] }),
}),
).resolves.toEqual({ action: 'use', projectDir: tempDir, reset: true });
});
it('lets interactive users choose another project directory', async () => {
await ensureDemoProject({ projectDir: tempDir, force: false });
const otherDir = join(tempDir, 'other-demo');
await expect(
chooseDemoProjectForInteractiveRun({
projectDir: tempDir,
io: io(true),
prompts: createTestDemoPromptAdapter({ choices: ['other'], texts: [otherDir] }),
}),
).resolves.toEqual({ action: 'use', projectDir: otherDir, reset: false });
});
it('uses a pasted Anthropic key only for the returned process env', async () => {
// pragma: allowlist secret
const prompts = createTestDemoPromptAdapter({ choices: ['process_key'], passwords: ['sk-ant-process'] });
await expect(
resolveFullCredentialDecision({
needsAnthropicKey: true,
inputMode: 'auto',
io: io(true),
env: {},
prompts,
}),
).resolves.toEqual({
action: 'full',
env: { ANTHROPIC_API_KEY: 'sk-ant-process' }, // pragma: allowlist secret
});
});
it('lets interactive users explicitly choose seeded mode when the key is missing', async () => {
await expect(
resolveFullCredentialDecision({
needsAnthropicKey: true,
inputMode: 'auto',
io: io(true),
env: {},
prompts: createTestDemoPromptAdapter({ choices: ['seeded'] }),
}),
).resolves.toEqual({ action: 'run-mode', mode: 'seeded' });
});
it('does not prompt when input is disabled', async () => {
await expect(
resolveFullCredentialDecision({
needsAnthropicKey: true,
inputMode: 'disabled',
io: io(false),
env: {},
prompts: createTestDemoPromptAdapter({ choices: ['seeded'] }),
}),
).resolves.toEqual({ action: 'full', env: {} });
});
});