9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
119 lines
4.8 KiB
JavaScript
119 lines
4.8 KiB
JavaScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { ArgumentError, AuthRequiredError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
|
|
const {
|
|
mockEnsureOnClaude,
|
|
mockEnsureClaudeComposer,
|
|
mockEnsureClaudeLogin,
|
|
mockSendMessage,
|
|
mockParseBoolFlag,
|
|
mockRequireNonEmptyPrompt,
|
|
mockGetVisibleMessages,
|
|
mockGetConversationList,
|
|
mockRequirePositiveInt,
|
|
mockRequireConversationId,
|
|
mockWithRetry,
|
|
} = vi.hoisted(() => ({
|
|
mockEnsureOnClaude: vi.fn(),
|
|
mockEnsureClaudeComposer: vi.fn(),
|
|
mockEnsureClaudeLogin: vi.fn(),
|
|
mockSendMessage: vi.fn(),
|
|
mockParseBoolFlag: vi.fn((v) => v === true || v === 'true'),
|
|
mockRequireNonEmptyPrompt: vi.fn((v) => String(v ?? '')),
|
|
mockGetVisibleMessages: vi.fn(),
|
|
mockGetConversationList: vi.fn(),
|
|
mockRequirePositiveInt: vi.fn((v) => Number(v)),
|
|
mockRequireConversationId: vi.fn((v) => String(v ?? '').trim()),
|
|
mockWithRetry: vi.fn(async (fn) => fn()),
|
|
}));
|
|
|
|
vi.mock('./utils.js', () => ({
|
|
CLAUDE_DOMAIN: 'claude.ai',
|
|
CLAUDE_URL: 'https://claude.ai/new',
|
|
ensureOnClaude: mockEnsureOnClaude,
|
|
ensureClaudeComposer: mockEnsureClaudeComposer,
|
|
ensureClaudeLogin: mockEnsureClaudeLogin,
|
|
sendMessage: mockSendMessage,
|
|
parseBoolFlag: mockParseBoolFlag,
|
|
requireNonEmptyPrompt: mockRequireNonEmptyPrompt,
|
|
getVisibleMessages: mockGetVisibleMessages,
|
|
getConversationList: mockGetConversationList,
|
|
requirePositiveInt: mockRequirePositiveInt,
|
|
requireConversationId: mockRequireConversationId,
|
|
withRetry: mockWithRetry,
|
|
}));
|
|
|
|
import { sendCommand } from './send.js';
|
|
import { newCommand } from './new.js';
|
|
import { readCommand } from './read.js';
|
|
import { historyCommand } from './history.js';
|
|
import { detailCommand } from './detail.js';
|
|
|
|
describe('claude command-level fail-fast contracts', () => {
|
|
const page = {
|
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
wait: vi.fn().mockResolvedValue(undefined),
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockEnsureOnClaude.mockResolvedValue(false);
|
|
mockEnsureClaudeComposer.mockResolvedValue({ isLoggedIn: true, hasComposer: true });
|
|
mockEnsureClaudeLogin.mockResolvedValue({ isLoggedIn: true });
|
|
mockSendMessage.mockResolvedValue({ ok: true });
|
|
mockRequireNonEmptyPrompt.mockImplementation((v) => String(v ?? ''));
|
|
mockGetVisibleMessages.mockResolvedValue([{ Index: 0, Role: 'assistant', Text: 'hi' }]);
|
|
mockGetConversationList.mockResolvedValue([{ Index: 1, Id: 'abc', Title: 'Hi', Url: 'https://claude.ai/chat/abc' }]);
|
|
mockRequirePositiveInt.mockImplementation((v) => Number(v));
|
|
mockRequireConversationId.mockImplementation((v) => String(v ?? '').trim());
|
|
});
|
|
|
|
it('send rejects empty prompt via ArgumentError', async () => {
|
|
mockRequireNonEmptyPrompt.mockImplementation(() => {
|
|
throw new ArgumentError('claude send prompt cannot be empty');
|
|
});
|
|
|
|
await expect(sendCommand.func(page, { prompt: '', new: false })).rejects.toThrow(ArgumentError);
|
|
});
|
|
|
|
it('send surfaces auth failure from composer readiness', async () => {
|
|
mockEnsureClaudeComposer.mockRejectedValue(new AuthRequiredError('claude.ai', 'Claude send requires a logged-in Claude session.'));
|
|
|
|
await expect(sendCommand.func(page, { prompt: 'hi', new: false })).rejects.toThrow(AuthRequiredError);
|
|
});
|
|
|
|
it('new no longer false-succeeds on login wall', async () => {
|
|
mockEnsureClaudeComposer.mockRejectedValue(new AuthRequiredError('claude.ai', 'Claude new requires a logged-in Claude session with a visible composer.'));
|
|
|
|
await expect(newCommand.func(page)).rejects.toThrow(AuthRequiredError);
|
|
});
|
|
|
|
it('read throws EmptyResultError instead of a placeholder row', async () => {
|
|
mockGetVisibleMessages.mockResolvedValue([]);
|
|
|
|
await expect(readCommand.func(page)).rejects.toThrow(EmptyResultError);
|
|
});
|
|
|
|
it('history rejects invalid --limit values instead of silently coercing them', async () => {
|
|
mockRequirePositiveInt.mockImplementation(() => {
|
|
throw new ArgumentError('claude history --limit must be a positive integer');
|
|
});
|
|
|
|
await expect(historyCommand.func(page, { limit: 0 })).rejects.toThrow(ArgumentError);
|
|
});
|
|
|
|
it('history throws EmptyResultError on an empty /recents page', async () => {
|
|
mockGetConversationList.mockResolvedValue([]);
|
|
|
|
await expect(historyCommand.func(page, { limit: 20 })).rejects.toThrow(EmptyResultError);
|
|
});
|
|
|
|
it('detail rejects a missing conversation id', async () => {
|
|
mockRequireConversationId.mockImplementation(() => {
|
|
throw new ArgumentError('claude detail requires a conversation id');
|
|
});
|
|
|
|
await expect(detailCommand.func(page, { id: '' })).rejects.toThrow(ArgumentError);
|
|
});
|
|
});
|