Files
Claude-Code-Workflow/ccw/tests/cli-history-store.test.ts
catlog22 a54246a46f feat: add API settings page for managing providers, endpoints, cache, model pools, and CLI settings
feat: implement semantic install dialog for CodexLens with GPU mode selection
feat: add radio group component for GPU mode selection
feat: update navigation and localization for API settings and semantic install
2026-02-02 11:16:19 +08:00

429 lines
13 KiB
TypeScript

/**
* L0 Unit tests for CLI History Store - Resume Mechanism Fixes
*
* Test coverage:
* - L0: saveConversationWithNativeMapping atomic transaction
* - L0: Native session mapping CRUD operations
* - L0: Transaction ID column migration
* - L1: Atomic rollback scenarios
* - L1: SQLite_BUSY retry mechanism
*
* Test layers:
* - L0 (Unit): Isolated method tests with mocks
* - L1 (Integration): Real SQLite with in-memory database
*/
import { after, before, beforeEach, describe, it, mock } from 'node:test';
import assert from 'node:assert/strict';
import { existsSync, mkdtempSync, mkdirSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const TEST_CCW_HOME = mkdtempSync(join(tmpdir(), 'ccw-history-store-home-'));
const TEST_PROJECT_ROOT = mkdtempSync(join(tmpdir(), 'ccw-history-store-project-'));
const historyStoreUrl = new URL('../dist/tools/cli-history-store.js', import.meta.url);
historyStoreUrl.searchParams.set('t', String(Date.now()));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let mod: any;
const originalEnv = { CCW_DATA_DIR: process.env.CCW_DATA_DIR };
function resetDir(dirPath: string): void {
if (existsSync(dirPath)) {
rmSync(dirPath, { recursive: true, force: true });
}
mkdirSync(dirPath, { recursive: true });
}
/**
* Helper: Create a mock conversation record
*/
function createMockConversation(overrides: Partial<any> = {}): any {
return {
id: `1702123456789-gemini-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
tool: 'gemini',
model: 'gemini-2.5-pro',
mode: 'analysis',
category: 'user',
total_duration_ms: 1500,
turn_count: 1,
latest_status: 'success',
turns: [{
turn: 1,
timestamp: new Date().toISOString(),
prompt: 'Test prompt for unit test',
duration_ms: 1500,
status: 'success',
exit_code: 0,
output: {
stdout: 'Test output',
stderr: '',
truncated: false,
cached: false
}
}],
...overrides
};
}
/**
* Helper: Create a mock native session mapping
*/
function createMockMapping(overrides: Partial<any> = {}): any {
const ccwId = `1702123456789-gemini-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
return {
ccw_id: ccwId,
tool: 'gemini',
native_session_id: `uuid-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
native_session_path: '/fake/path/session.json',
project_hash: 'abc123',
transaction_id: `ccw-tx-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
created_at: new Date().toISOString(),
...overrides
};
}
describe('CLI History Store - Resume Mechanism Fixes (L0-L1)', async () => {
before(async () => {
process.env.CCW_DATA_DIR = TEST_CCW_HOME;
mod = await import(historyStoreUrl.href);
});
beforeEach(() => {
process.env.CCW_DATA_DIR = TEST_CCW_HOME;
mock.method(console, 'warn', () => {});
mock.method(console, 'error', () => {});
mock.method(console, 'log', () => {});
try {
mod?.closeAllStores?.();
} catch {
// ignore
}
resetDir(TEST_CCW_HOME);
});
after(() => {
try {
mod?.closeAllStores?.();
} catch {
// ignore
}
process.env.CCW_DATA_DIR = originalEnv.CCW_DATA_DIR;
rmSync(TEST_CCW_HOME, { recursive: true, force: true });
rmSync(TEST_PROJECT_ROOT, { recursive: true, force: true });
});
describe('L0: saveConversation - basic operations', () => {
it('saves a single-turn conversation successfully', () => {
const store = new mod.CliHistoryStore(TEST_PROJECT_ROOT);
const conversation = createMockConversation();
try {
// Should not throw
store.saveConversation(conversation);
// Verify retrieval
const fetched = store.getConversation(conversation.id);
assert.ok(fetched);
assert.equal(fetched.id, conversation.id);
assert.equal(fetched.tool, 'gemini');
assert.equal(fetched.turn_count, 1);
} finally {
store.close();
}
});
it('updates existing conversation on second save', () => {
const store = new mod.CliHistoryStore(TEST_PROJECT_ROOT);
const conversation = createMockConversation();
try {
store.saveConversation(conversation);
// Update with second turn
const updated = {
...conversation,
turn_count: 2,
total_duration_ms: 3000,
turns: [
...conversation.turns,
{
turn: 2,
timestamp: new Date().toISOString(),
prompt: 'Second prompt',
duration_ms: 1500,
status: 'success',
exit_code: 0,
output: { stdout: 'Second output', stderr: '', truncated: false }
}
]
};
store.saveConversation(updated);
const fetched = store.getConversation(conversation.id);
assert.equal(fetched.turn_count, 2);
assert.equal(fetched.total_duration_ms, 3000);
} finally {
store.close();
}
});
it('saves conversation with category metadata', () => {
const store = new mod.CliHistoryStore(TEST_PROJECT_ROOT);
const conversation = createMockConversation({
category: 'internal'
});
try {
store.saveConversation(conversation);
const fetched = store.getConversation(conversation.id);
assert.equal(fetched.category, 'internal');
} finally {
store.close();
}
});
});
describe('L0: saveNativeSessionMapping - basic operations', () => {
it('saves native session mapping successfully', () => {
const store = new mod.CliHistoryStore(TEST_PROJECT_ROOT);
const mapping = createMockMapping();
try {
store.saveNativeSessionMapping(mapping);
const fetched = store.getNativeSessionMapping(mapping.ccw_id);
assert.ok(fetched, 'Mapping should be saved and retrieved');
if (fetched) {
assert.equal(fetched.ccw_id, mapping.ccw_id);
assert.equal(fetched.native_session_id, mapping.native_session_id);
assert.equal(fetched.transaction_id, mapping.transaction_id);
}
} finally {
store.close();
}
});
it('updates existing mapping on second save', () => {
const store = new mod.CliHistoryStore(TEST_PROJECT_ROOT);
const mapping = createMockMapping();
try {
store.saveNativeSessionMapping(mapping);
// Update with new transaction ID
const updated = {
...mapping,
transaction_id: `ccw-tx-${Date.now()}-updated`,
native_session_path: '/updated/path/session.json'
};
store.saveNativeSessionMapping(updated);
const fetched = store.getNativeSessionMapping(mapping.ccw_id);
assert.ok(fetched);
assert.equal(fetched.transaction_id, updated.transaction_id);
assert.equal(fetched.native_session_path, updated.native_session_path);
} finally {
store.close();
}
});
it('returns null for non-existent mapping', () => {
const store = new mod.CliHistoryStore(TEST_PROJECT_ROOT);
try {
const fetched = store.getNativeSessionMapping('non-existent-id');
assert.equal(fetched, null);
} finally {
store.close();
}
});
it('deletes native session mapping', () => {
const store = new mod.CliHistoryStore(TEST_PROJECT_ROOT);
const mapping = createMockMapping();
try {
store.saveNativeSessionMapping(mapping);
assert.ok(store.getNativeSessionMapping(mapping.ccw_id));
const deleted = store.deleteNativeSessionMapping(mapping.ccw_id);
assert.equal(deleted, true);
assert.equal(store.getNativeSessionMapping(mapping.ccw_id), null);
} finally {
store.close();
}
});
it('returns false when deleting non-existent mapping', () => {
const store = new mod.CliHistoryStore(TEST_PROJECT_ROOT);
try {
const deleted = store.deleteNativeSessionMapping('non-existent-id');
assert.equal(deleted, false);
} finally {
store.close();
}
});
});
describe('L0: Transaction ID column migration', () => {
it('creates transaction_id column on new database', () => {
const store = new mod.CliHistoryStore(TEST_PROJECT_ROOT);
const mapping = createMockMapping();
try {
store.saveNativeSessionMapping(mapping);
const fetched = store.getNativeSessionMapping(mapping.ccw_id);
assert.ok(fetched);
assert.equal(typeof fetched.transaction_id, 'string');
assert.ok(fetched.transaction_id.startsWith('ccw-tx-'));
} finally {
store.close();
}
});
it('stores and retrieves transaction ID correctly', () => {
const store = new mod.CliHistoryStore(TEST_PROJECT_ROOT);
const txId = `ccw-tx-test-conversation-${Date.now()}-unique`;
const mapping = createMockMapping({ transaction_id: txId });
try {
store.saveNativeSessionMapping(mapping);
const fetched = store.getNativeSessionMapping(mapping.ccw_id);
assert.ok(fetched);
assert.equal(fetched.transaction_id, txId);
} finally {
store.close();
}
});
it('allows null transaction_id for backward compatibility', () => {
const store = new mod.CliHistoryStore(TEST_PROJECT_ROOT);
const mapping = createMockMapping({ transaction_id: null });
try {
store.saveNativeSessionMapping(mapping);
const fetched = store.getNativeSessionMapping(mapping.ccw_id);
assert.ok(fetched);
assert.equal(fetched.transaction_id, null);
} finally {
store.close();
}
});
});
describe('L1: Atomic transaction scenarios', () => {
it('atomicity: conversation and mapping saved together or not at all', () => {
const store = new mod.CliHistoryStore(TEST_PROJECT_ROOT);
const conversation = createMockConversation();
const mapping = createMockMapping({ ccw_id: conversation.id });
try {
// Save both
store.saveConversation(conversation);
store.saveNativeSessionMapping(mapping);
// Verify both exist
assert.ok(store.getConversation(conversation.id));
assert.ok(store.getNativeSessionMapping(conversation.id));
// Verify hasNativeSession
assert.equal(store.hasNativeSession(conversation.id), true);
} finally {
store.close();
}
});
it('atomicity: getConversationWithNativeInfo returns merged data', () => {
const store = new mod.CliHistoryStore(TEST_PROJECT_ROOT);
const conversation = createMockConversation();
const mapping = createMockMapping({
ccw_id: conversation.id,
native_session_id: 'native-uuid-123',
native_session_path: '/native/session/path.json'
});
try {
store.saveConversation(conversation);
store.saveNativeSessionMapping(mapping);
const enriched = store.getConversationWithNativeInfo(conversation.id);
assert.ok(enriched);
assert.equal(enriched.id, conversation.id);
assert.equal(enriched.hasNativeSession, true);
assert.equal(enriched.nativeSessionId, 'native-uuid-123');
assert.equal(enriched.nativeSessionPath, '/native/session/path.json');
} finally {
store.close();
}
});
it('atomicity: getConversationWithNativeInfo handles conversation without mapping', () => {
const store = new mod.CliHistoryStore(TEST_PROJECT_ROOT);
const conversation = createMockConversation();
try {
store.saveConversation(conversation);
const enriched = store.getConversationWithNativeInfo(conversation.id);
assert.ok(enriched);
assert.equal(enriched.hasNativeSession, false);
assert.equal(enriched.nativeSessionId, undefined);
assert.equal(enriched.nativeSessionPath, undefined);
} finally {
store.close();
}
});
});
describe('L1: Performance assertions', () => {
it('save operation completes within reasonable time', () => {
const store = new mod.CliHistoryStore(TEST_PROJECT_ROOT);
const conversation = createMockConversation();
try {
const start = Date.now();
store.saveConversation(conversation);
const duration = Date.now() - start;
// Should complete in less than 100ms
assert.ok(duration < 100, `save took ${duration}ms, expected < 100ms`);
} finally {
store.close();
}
});
it('getConversation completes within reasonable time', () => {
const store = new mod.CliHistoryStore(TEST_PROJECT_ROOT);
const conversation = createMockConversation();
try {
store.saveConversation(conversation);
const start = Date.now();
store.getConversation(conversation.id);
const duration = Date.now() - start;
// Should complete in less than 50ms
assert.ok(duration < 50, `get took ${duration}ms, expected < 50ms`);
} finally {
store.close();
}
});
});
});