mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
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
595 lines
19 KiB
TypeScript
595 lines
19 KiB
TypeScript
/**
|
|
* L0-L2 tests for Native Session Discovery - Resume Mechanism Fixes
|
|
*
|
|
* Test coverage:
|
|
* - L0: Transaction ID parsing from session files
|
|
* - L0: Session discoverer factory methods
|
|
* - L1: Transaction ID matching logic
|
|
* - L1: Prompt-based fallback matching
|
|
* - L2: Concurrent session disambiguation
|
|
*
|
|
* Test layers:
|
|
* - L0 (Unit): Isolated method tests with mocks
|
|
* - L1 (Integration): Session matching with mock files
|
|
* - L2 (System): Concurrent execution scenarios
|
|
*/
|
|
|
|
import { after, afterEach, before, beforeEach, describe, it, mock } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { existsSync, mkdtempSync, mkdirSync, rmSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
const TEST_CCW_HOME = mkdtempSync(join(tmpdir(), 'ccw-session-discovery-home-'));
|
|
const TEST_PROJECT_ROOT = mkdtempSync(join(tmpdir(), 'ccw-session-discovery-project-'));
|
|
|
|
const sessionDiscoveryUrl = new URL('../dist/tools/native-session-discovery.js', import.meta.url);
|
|
const cliExecutorUrl = new URL('../dist/tools/cli-executor.js', import.meta.url);
|
|
|
|
sessionDiscoveryUrl.searchParams.set('t', String(Date.now()));
|
|
cliExecutorUrl.searchParams.set('t', String(Date.now()));
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
let mod: any;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
let cliExecutorMod: 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 Gemini session file
|
|
*/
|
|
function createMockGeminiSession(filePath: string, options: {
|
|
sessionId: string;
|
|
startTime: string;
|
|
transactionId?: string;
|
|
firstPrompt?: string;
|
|
}): void {
|
|
const sessionData = {
|
|
sessionId: options.sessionId,
|
|
startTime: options.startTime,
|
|
lastUpdated: new Date().toISOString(),
|
|
messages: [
|
|
{
|
|
type: 'user',
|
|
content: options.transactionId
|
|
? `[CCW-TX-ID: ${options.transactionId}]\n\n${options.firstPrompt || 'Test prompt'}`
|
|
: (options.firstPrompt || 'Test prompt')
|
|
},
|
|
{
|
|
type: 'model',
|
|
content: 'Test response'
|
|
}
|
|
]
|
|
};
|
|
|
|
mkdirSync(join(filePath, '..'), { recursive: true });
|
|
writeFileSync(filePath, JSON.stringify(sessionData), 'utf8');
|
|
}
|
|
|
|
/**
|
|
* Helper: Create a mock Qwen session file (JSONL format)
|
|
*/
|
|
function createMockQwenSession(filePath: string, options: {
|
|
sessionId: string;
|
|
startTime: string;
|
|
transactionId?: string;
|
|
firstPrompt?: string;
|
|
}): void {
|
|
const userContent = options.transactionId
|
|
? `[CCW-TX-ID: ${options.transactionId}]\n\n${options.firstPrompt || 'Test prompt'}`
|
|
: (options.firstPrompt || 'Test prompt');
|
|
|
|
const lines = [
|
|
JSON.stringify({ type: 'user', content: userContent, timestamp: options.startTime }),
|
|
JSON.stringify({ type: 'assistant', content: 'Test response', timestamp: new Date().toISOString() })
|
|
];
|
|
|
|
mkdirSync(join(filePath, '..'), { recursive: true });
|
|
writeFileSync(filePath, lines.join('\n'), 'utf8');
|
|
}
|
|
|
|
/**
|
|
* Helper: Create a mock Codex session file (JSONL format)
|
|
*/
|
|
function createMockCodexSession(filePath: string, options: {
|
|
sessionId: string;
|
|
startTime: string;
|
|
transactionId?: string;
|
|
firstPrompt?: string;
|
|
}): void {
|
|
const userContent = options.transactionId
|
|
? `[CCW-TX-ID: ${options.transactionId}]\n\n${options.firstPrompt || 'Test prompt'}`
|
|
: (options.firstPrompt || 'Test prompt');
|
|
|
|
const lines = [
|
|
JSON.stringify({ role: 'user', message: { role: 'user', content: userContent }, isMeta: false }),
|
|
JSON.stringify({ role: 'model', message: { role: 'assistant', content: 'Test response' }, isMeta: false })
|
|
];
|
|
|
|
mkdirSync(join(filePath, '..', '..'), { recursive: true });
|
|
writeFileSync(filePath, lines.join('\n'), 'utf8');
|
|
}
|
|
|
|
describe('Native Session Discovery - Resume Mechanism Fixes (L0-L2)', async () => {
|
|
before(async () => {
|
|
process.env.CCW_DATA_DIR = TEST_CCW_HOME;
|
|
mod = await import(sessionDiscoveryUrl.href);
|
|
cliExecutorMod = await import(cliExecutorUrl.href);
|
|
});
|
|
|
|
beforeEach(() => {
|
|
process.env.CCW_DATA_DIR = TEST_CCW_HOME;
|
|
mock.method(console, 'warn', () => {});
|
|
mock.method(console, 'error', () => {});
|
|
mock.method(console, 'log', () => {});
|
|
|
|
resetDir(TEST_CCW_HOME);
|
|
});
|
|
|
|
afterEach(() => {
|
|
mock.restoreAll();
|
|
});
|
|
|
|
after(() => {
|
|
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: Transaction ID parsing', () => {
|
|
it('extracts transaction ID from prompt with CCW-TX-ID format', () => {
|
|
const prompt = '[CCW-TX-ID: ccw-tx-12345-abc]\n\nActual prompt content here';
|
|
const txId = cliExecutorMod.extractTransactionId?.(prompt);
|
|
|
|
assert.ok(txId);
|
|
assert.equal(txId, 'ccw-tx-12345-abc');
|
|
});
|
|
|
|
it('returns null for prompt without transaction ID', () => {
|
|
const prompt = 'Just a regular prompt without any transaction ID';
|
|
const txId = cliExecutorMod.extractTransactionId?.(prompt);
|
|
|
|
assert.equal(txId, null);
|
|
});
|
|
|
|
it('handles malformed transaction ID gracefully', () => {
|
|
const prompt = '[CCW-TX-ID: \n\nMalformed ID without closing bracket';
|
|
const txId = cliExecutorMod.extractTransactionId?.(prompt);
|
|
|
|
// Should either return null or the malformed content
|
|
assert.ok(txId === null || typeof txId === 'string');
|
|
});
|
|
|
|
it('generates valid transaction ID format', () => {
|
|
const conversationId = '1702123456789-gemini';
|
|
const txId = cliExecutorMod.generateTransactionId?.(conversationId);
|
|
|
|
assert.ok(txId);
|
|
assert.ok(txId.startsWith('ccw-tx-'));
|
|
assert.ok(txId.includes(conversationId));
|
|
});
|
|
|
|
it('injects transaction ID into prompt', () => {
|
|
const originalPrompt = 'Original user prompt';
|
|
const txId = 'ccw-tx-test-123';
|
|
const injected = cliExecutorMod.injectTransactionId?.(originalPrompt, txId);
|
|
|
|
assert.ok(injected.startsWith('[CCW-TX-ID:'));
|
|
assert.ok(injected.includes(txId));
|
|
assert.ok(injected.includes(originalPrompt));
|
|
});
|
|
});
|
|
|
|
describe('L0: Session discoverer factory', () => {
|
|
it('returns discoverer for supported tools', () => {
|
|
const geminiDiscoverer = mod.getDiscoverer('gemini');
|
|
assert.ok(geminiDiscoverer);
|
|
|
|
const qwenDiscoverer = mod.getDiscoverer('qwen');
|
|
assert.ok(qwenDiscoverer);
|
|
|
|
const codexDiscoverer = mod.getDiscoverer('codex');
|
|
assert.ok(codexDiscoverer);
|
|
|
|
const claudeDiscoverer = mod.getDiscoverer('claude');
|
|
assert.ok(claudeDiscoverer);
|
|
});
|
|
|
|
it('returns null for unsupported tool', () => {
|
|
const discoverer = mod.getDiscoverer('unsupported-tool');
|
|
assert.equal(discoverer, null);
|
|
});
|
|
|
|
it('checkNativeResume returns true for gemini', () => {
|
|
assert.equal(mod.supportsNativeResume('gemini'), true);
|
|
});
|
|
|
|
it('checkNativeResume returns true for qwen', () => {
|
|
assert.equal(mod.supportsNativeResume('qwen'), true);
|
|
});
|
|
|
|
it('checkNativeResume returns false for codex (TTY limitation)', () => {
|
|
assert.equal(mod.supportsNativeResume('codex'), false);
|
|
});
|
|
});
|
|
|
|
describe('L0: Transaction ID matching - mock sessions', () => {
|
|
it('matches session by exact transaction ID', () => {
|
|
const txId = 'ccw-tx-test-abc-123';
|
|
const sessions = [
|
|
{ filePath: '', sessionId: 'session-1', tool: 'gemini' },
|
|
{ filePath: '', sessionId: 'session-2', tool: 'gemini' }
|
|
];
|
|
|
|
// Mock extractFirstUserMessage to return transaction ID
|
|
const mockDiscoverer = {
|
|
extractFirstUserMessage: (path: string) => {
|
|
if (path === 'match') {
|
|
return `[CCW-TX-ID: ${txId}]\n\nPrompt`;
|
|
}
|
|
return '[CCW-TX-ID: different-id]\n\nPrompt';
|
|
}
|
|
};
|
|
|
|
// Add matchSessionByTransactionId method to mock
|
|
Object.assign(mockDiscoverer, {
|
|
matchSessionByTransactionId: (txId: string, sessions: any[]) => {
|
|
for (const session of sessions) {
|
|
try {
|
|
const userMessage = mockDiscoverer.extractFirstUserMessage(session.filePath);
|
|
if (userMessage) {
|
|
const match = userMessage.match(/\[CCW-TX-ID:\s+([^\]]+)\]/);
|
|
if (match && match[1] === txId) {
|
|
return session;
|
|
}
|
|
}
|
|
} catch {
|
|
// Skip
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
});
|
|
|
|
sessions[0].filePath = 'match';
|
|
sessions[1].filePath = 'no-match';
|
|
|
|
const matched = mockDiscoverer.matchSessionByTransactionId(txId, sessions);
|
|
assert.equal(matched, sessions[0]);
|
|
});
|
|
|
|
it('returns null when transaction ID not found', () => {
|
|
const txId = 'ccw-tx-not-found';
|
|
const sessions = [
|
|
{ filePath: 'session1', sessionId: 's1', tool: 'gemini' }
|
|
];
|
|
|
|
const mockDiscoverer = {
|
|
extractFirstUserMessage: () => '[CCW-TX-ID: different-id]\n\nPrompt'
|
|
};
|
|
|
|
Object.assign(mockDiscoverer, {
|
|
matchSessionByTransactionId: (txId: string, sessions: any[]) => {
|
|
for (const session of sessions) {
|
|
try {
|
|
const userMessage = mockDiscoverer.extractFirstUserMessage(session.filePath);
|
|
if (userMessage) {
|
|
const match = userMessage.match(/\[CCW-TX-ID:\s+([^\]]+)\]/);
|
|
if (match && match[1] === txId) {
|
|
return session;
|
|
}
|
|
}
|
|
} catch {
|
|
// Skip
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
});
|
|
|
|
const matched = mockDiscoverer.matchSessionByTransactionId(txId, sessions);
|
|
assert.equal(matched, null);
|
|
});
|
|
});
|
|
|
|
describe('L1: Session file parsing with transaction IDs', () => {
|
|
it('extracts transaction ID from Gemini session file', () => {
|
|
const tempDir = join(TEST_CCW_HOME, 'gemini-sessions');
|
|
const projectHash = 'abc123';
|
|
const sessionPath = join(tempDir, projectHash, 'chats', `session-test-${Date.now()}.json`);
|
|
|
|
createMockGeminiSession(sessionPath, {
|
|
sessionId: `uuid-${Date.now()}`,
|
|
startTime: new Date().toISOString(),
|
|
transactionId: 'ccw-tx-gemini-test-123',
|
|
firstPrompt: 'Test Gemini prompt'
|
|
});
|
|
|
|
// Verify file was created
|
|
assert.ok(existsSync(sessionPath));
|
|
|
|
// Read and verify content
|
|
const content = JSON.parse(readFileSync(sessionPath, 'utf8'));
|
|
assert.ok(content.messages[0].content.includes('[CCW-TX-ID:'));
|
|
assert.ok(content.messages[0].content.includes('ccw-tx-gemini-test-123'));
|
|
});
|
|
|
|
it('extracts transaction ID from Qwen session file', () => {
|
|
const tempDir = join(TEST_CCW_HOME, 'qwen-sessions');
|
|
const encodedPath = encodeURIComponent(TEST_PROJECT_ROOT);
|
|
const sessionPath = join(tempDir, encodedPath, 'chats', `test-${Date.now()}.jsonl`);
|
|
|
|
createMockQwenSession(sessionPath, {
|
|
sessionId: `uuid-${Date.now()}`,
|
|
startTime: new Date().toISOString(),
|
|
transactionId: 'ccw-tx-qwen-test-456',
|
|
firstPrompt: 'Test Qwen prompt'
|
|
});
|
|
|
|
assert.ok(existsSync(sessionPath));
|
|
|
|
const content = readFileSync(sessionPath, 'utf8');
|
|
assert.ok(content.includes('[CCW-TX-ID:'));
|
|
assert.ok(content.includes('ccw-tx-qwen-test-456'));
|
|
});
|
|
|
|
it('extracts transaction ID from Codex session file', () => {
|
|
const tempDir = join(TEST_CCW_HOME, 'codex-sessions');
|
|
mkdirSync(tempDir, { recursive: true });
|
|
const sessionPath = join(tempDir, `rollout-test-${Date.now()}.jsonl`);
|
|
|
|
createMockCodexSession(sessionPath, {
|
|
sessionId: `uuid-${Date.now()}`,
|
|
startTime: new Date().toISOString(),
|
|
transactionId: 'ccw-tx-codex-test-789',
|
|
firstPrompt: 'Test Codex prompt'
|
|
});
|
|
|
|
assert.ok(existsSync(sessionPath));
|
|
|
|
const content = readFileSync(sessionPath, 'utf8');
|
|
assert.ok(content.includes('[CCW-TX-ID:'));
|
|
assert.ok(content.includes('ccw-tx-codex-test-789'));
|
|
});
|
|
});
|
|
|
|
describe('L1: Prompt-based fallback matching', () => {
|
|
it('matches sessions by prompt prefix when transaction ID not available', () => {
|
|
const prompt = 'Implement authentication feature with JWT tokens';
|
|
const sessions = [
|
|
{
|
|
filePath: 'match1',
|
|
sessionId: 's1',
|
|
tool: 'gemini',
|
|
firstPrompt: 'Implement authentication feature with JWT tokens and refresh'
|
|
},
|
|
{
|
|
filePath: 'nomatch',
|
|
sessionId: 's2',
|
|
tool: 'gemini',
|
|
firstPrompt: 'Completely different prompt about database'
|
|
}
|
|
];
|
|
|
|
// Mock extractFirstUserMessage
|
|
const mockSessions = sessions.map(s => ({
|
|
...s,
|
|
extractFirstUserMessage: () => s.firstPrompt
|
|
}));
|
|
|
|
// Simple prefix match logic
|
|
const matched = mockSessions.find(s => s.firstPrompt.startsWith(prompt.substring(0, 50)));
|
|
|
|
assert.ok(matched);
|
|
assert.equal(matched.sessionId, 's1');
|
|
});
|
|
});
|
|
|
|
describe('L2: Concurrent session disambiguation', () => {
|
|
it('distinguishes between concurrent sessions with same timestamp', () => {
|
|
const baseTime = new Date().toISOString();
|
|
const txId1 = 'ccw-tx-concurrent-session1-abc';
|
|
const txId2 = 'ccw-tx-concurrent-session2-def';
|
|
|
|
const sessions = [
|
|
{
|
|
filePath: 'session1',
|
|
sessionId: 'uuid-1',
|
|
tool: 'gemini',
|
|
createdAt: new Date(baseTime)
|
|
},
|
|
{
|
|
filePath: 'session2',
|
|
sessionId: 'uuid-2',
|
|
tool: 'gemini',
|
|
createdAt: new Date(baseTime)
|
|
}
|
|
];
|
|
|
|
// Mock extractor that returns different transaction IDs
|
|
const mockDiscoverer = {
|
|
extractFirstUserMessage: (path: string) => {
|
|
if (path === 'session1') {
|
|
return `[CCW-TX-ID: ${txId1}]\n\nPrompt 1`;
|
|
}
|
|
return `[CCW-TX-ID: ${txId2}]\n\nPrompt 2`;
|
|
},
|
|
matchSessionByTransactionId: (txId: string, sessions: any[]) => {
|
|
for (const session of sessions) {
|
|
try {
|
|
const userMessage = mockDiscoverer.extractFirstUserMessage(session.filePath);
|
|
if (userMessage) {
|
|
const match = userMessage.match(/\[CCW-TX-ID:\s+([^\]]+)\]/);
|
|
if (match && match[1] === txId) {
|
|
return session;
|
|
}
|
|
}
|
|
} catch {
|
|
// Skip
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const matched1 = mockDiscoverer.matchSessionByTransactionId(txId1, sessions);
|
|
const matched2 = mockDiscoverer.matchSessionByTransactionId(txId2, sessions);
|
|
|
|
assert.equal(matched1.sessionId, 'uuid-1');
|
|
assert.equal(matched2.sessionId, 'uuid-2');
|
|
assert.notEqual(matched1, matched2);
|
|
});
|
|
|
|
it('handles missing transaction ID gracefully with timestamp fallback', () => {
|
|
const baseTime = Date.now();
|
|
|
|
const sessions = [
|
|
{
|
|
filePath: 'newer',
|
|
sessionId: 's1',
|
|
tool: 'gemini',
|
|
createdAt: new Date(baseTime + 1000),
|
|
firstPrompt: 'Newer session prompt'
|
|
},
|
|
{
|
|
filePath: 'older',
|
|
sessionId: 's2',
|
|
tool: 'gemini',
|
|
createdAt: new Date(baseTime),
|
|
firstPrompt: 'Older session prompt'
|
|
}
|
|
];
|
|
|
|
// Without transaction ID, should fallback to latest by timestamp
|
|
const latest = sessions.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0];
|
|
|
|
assert.equal(latest.sessionId, 's1');
|
|
});
|
|
});
|
|
|
|
describe('L0: Edge cases and error handling', () => {
|
|
it('handles empty session files gracefully', () => {
|
|
const tempDir = join(TEST_CCW_HOME, 'empty-session');
|
|
const sessionPath = join(tempDir, 'session-empty.json');
|
|
mkdirSync(join(sessionPath, '..'), { recursive: true });
|
|
writeFileSync(sessionPath, '', 'utf8');
|
|
|
|
assert.ok(existsSync(sessionPath));
|
|
|
|
// Should not throw when reading empty file
|
|
try {
|
|
const content = readFileSync(sessionPath, 'utf8');
|
|
assert.equal(content, '');
|
|
} catch (e) {
|
|
// File read might fail, which is acceptable
|
|
assert.ok((e as Error).message);
|
|
}
|
|
});
|
|
|
|
it('handles malformed JSON in session files', () => {
|
|
const tempDir = join(TEST_CCW_HOME, 'malformed-session');
|
|
const sessionPath = join(tempDir, 'session-bad.json');
|
|
mkdirSync(join(sessionPath, '..'), { recursive: true });
|
|
writeFileSync(sessionPath, '{ invalid json }', 'utf8');
|
|
|
|
assert.ok(existsSync(sessionPath));
|
|
|
|
// Should not throw when parsing malformed JSON
|
|
try {
|
|
JSON.parse(readFileSync(sessionPath, 'utf8'));
|
|
assert.fail('Expected JSON.parse to throw');
|
|
} catch (e) {
|
|
assert.ok((e as Error).message.includes('JSON'));
|
|
}
|
|
});
|
|
|
|
it('handles special characters in transaction ID', () => {
|
|
const specialTxId = 'ccw-tx-test-with_underscores-and-123';
|
|
const prompt = `[CCW-TX-ID: ${specialTxId}]\n\nTest`;
|
|
|
|
const match = prompt.match(/\[CCW-TX-ID:\s+([^\]]+)\]/);
|
|
assert.ok(match);
|
|
assert.equal(match[1], specialTxId);
|
|
});
|
|
|
|
it('handles unicode characters in prompt with transaction ID', () => {
|
|
const txId = 'ccw-tx-unicode-test';
|
|
const prompt = `[CCW-TX-ID: ${txId}]\n\nTest with unicode: emoji characters`;
|
|
|
|
const match = prompt.match(/\[CCW-TX-ID:\s+([^\]]+)\]/);
|
|
assert.ok(match);
|
|
assert.equal(match[1], txId);
|
|
});
|
|
});
|
|
|
|
describe('L1: Performance assertions', () => {
|
|
it('transaction ID extraction completes quickly', () => {
|
|
const longPrompt = 'x'.repeat(10000) + '\n\n[CCW-TX-ID: test-id]\n\n' + 'y'.repeat(10000);
|
|
|
|
const start = Date.now();
|
|
const match = longPrompt.match(/\[CCW-TX-ID:\s+([^\]]+)\]/);
|
|
const duration = Date.now() - start;
|
|
|
|
assert.ok(match);
|
|
assert.ok(duration < 10, `Extraction took ${duration}ms, expected < 10ms`);
|
|
});
|
|
|
|
it('handles rapid transaction ID generations', () => {
|
|
const start = Date.now();
|
|
const ids = new Set();
|
|
|
|
for (let i = 0; i < 100; i++) {
|
|
const id = cliExecutorMod.generateTransactionId?.(`conv-${i}`);
|
|
ids.add(id);
|
|
}
|
|
|
|
const duration = Date.now() - start;
|
|
|
|
// All IDs should be unique
|
|
assert.equal(ids.size, 100);
|
|
|
|
// Should complete in reasonable time
|
|
assert.ok(duration < 100, `Generation took ${duration}ms, expected < 100ms`);
|
|
});
|
|
});
|
|
|
|
describe('L1: Integration with session discovery', () => {
|
|
it('creates valid session object structure', () => {
|
|
const session = {
|
|
sessionId: `uuid-${Date.now()}`,
|
|
tool: 'gemini',
|
|
filePath: '/fake/path/session.json',
|
|
projectHash: 'abc123',
|
|
createdAt: new Date(),
|
|
updatedAt: new Date()
|
|
};
|
|
|
|
assert.ok(session.sessionId);
|
|
assert.ok(session.tool);
|
|
assert.ok(session.filePath);
|
|
assert.equal(typeof session.createdAt.getTime(), 'number');
|
|
assert.equal(typeof session.updatedAt.getTime(), 'number');
|
|
});
|
|
|
|
it('handles session discovery options', () => {
|
|
const options = {
|
|
workingDir: TEST_PROJECT_ROOT,
|
|
limit: 10,
|
|
afterTimestamp: new Date(Date.now() - 3600000)
|
|
};
|
|
|
|
assert.ok(options.workingDir);
|
|
assert.equal(typeof options.limit, 'number');
|
|
assert.ok(options.afterTimestamp instanceof Date);
|
|
});
|
|
});
|
|
});
|