mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
- Implement tests for KeywordDetector including keyword detection, sanitization, and priority handling. - Add tests for SessionStateService covering session validation, loading, saving, and state updates. - Create tests for UserAbortDetector to validate user abort detection logic and pattern matching.
269 lines
7.8 KiB
TypeScript
269 lines
7.8 KiB
TypeScript
/**
|
|
* Tests for SessionStateService
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import {
|
|
validateSessionId,
|
|
getSessionStatePath,
|
|
loadSessionState,
|
|
saveSessionState,
|
|
clearSessionState,
|
|
updateSessionState,
|
|
incrementSessionLoad,
|
|
SessionStateService,
|
|
type SessionState
|
|
} from '../src/core/services/session-state-service.js';
|
|
import { existsSync, rmSync, mkdirSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { homedir } from 'os';
|
|
|
|
describe('validateSessionId', () => {
|
|
it('should accept valid session IDs', () => {
|
|
expect(validateSessionId('abc123')).toBe(true);
|
|
expect(validateSessionId('session-123')).toBe(true);
|
|
expect(validateSessionId('test_session')).toBe(true);
|
|
expect(validateSessionId('a')).toBe(true);
|
|
expect(validateSessionId('ABC-123_XYZ')).toBe(true);
|
|
});
|
|
|
|
it('should reject invalid session IDs', () => {
|
|
expect(validateSessionId('')).toBe(false);
|
|
expect(validateSessionId('.hidden')).toBe(false);
|
|
expect(validateSessionId('-starts-with-dash')).toBe(false);
|
|
expect(validateSessionId('../../../etc')).toBe(false);
|
|
expect(validateSessionId('has spaces')).toBe(false);
|
|
expect(validateSessionId('has/slash')).toBe(false);
|
|
expect(validateSessionId('has\\backslash')).toBe(false);
|
|
expect(validateSessionId('has.dot')).toBe(false);
|
|
});
|
|
|
|
it('should reject non-string inputs', () => {
|
|
expect(validateSessionId(null as any)).toBe(false);
|
|
expect(validateSessionId(undefined as any)).toBe(false);
|
|
expect(validateSessionId(123 as any)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('getSessionStatePath', () => {
|
|
const testSessionId = 'test-session-123';
|
|
|
|
describe('global storage (default)', () => {
|
|
it('should return path in global state directory', () => {
|
|
const path = getSessionStatePath(testSessionId);
|
|
expect(path).toContain('.claude');
|
|
expect(path).toContain('.ccw-sessions');
|
|
expect(path).toContain(`session-${testSessionId}.json`);
|
|
});
|
|
|
|
it('should throw error for invalid session ID', () => {
|
|
expect(() => getSessionStatePath('../../../etc')).toThrow('Invalid session ID');
|
|
});
|
|
});
|
|
|
|
describe('session-scoped storage', () => {
|
|
const projectPath = '/tmp/test-project';
|
|
|
|
it('should return path in project session directory', () => {
|
|
const path = getSessionStatePath(testSessionId, {
|
|
storageType: 'session-scoped',
|
|
projectPath
|
|
});
|
|
expect(path).toContain('.workflow');
|
|
expect(path).toContain('sessions');
|
|
expect(path).toContain(testSessionId);
|
|
expect(path).toContain('state.json');
|
|
});
|
|
|
|
it('should throw error when projectPath is missing', () => {
|
|
expect(() => getSessionStatePath(testSessionId, { storageType: 'session-scoped' }))
|
|
.toThrow('projectPath is required');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('loadSessionState / saveSessionState', () => {
|
|
const testSessionId = 'test-load-save-session';
|
|
const testState: SessionState = {
|
|
firstLoad: '2025-01-01T00:00:00.000Z',
|
|
loadCount: 5,
|
|
lastPrompt: 'test prompt'
|
|
};
|
|
|
|
afterEach(() => {
|
|
// Cleanup
|
|
try {
|
|
clearSessionState(testSessionId);
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
});
|
|
|
|
it('should return null for non-existent session', () => {
|
|
const state = loadSessionState('non-existent-session-xyz');
|
|
expect(state).toBeNull();
|
|
});
|
|
|
|
it('should save and load session state', () => {
|
|
saveSessionState(testSessionId, testState);
|
|
const loaded = loadSessionState(testSessionId);
|
|
|
|
expect(loaded).not.toBeNull();
|
|
expect(loaded!.firstLoad).toBe(testState.firstLoad);
|
|
expect(loaded!.loadCount).toBe(testState.loadCount);
|
|
expect(loaded!.lastPrompt).toBe(testState.lastPrompt);
|
|
});
|
|
|
|
it('should return null for invalid session ID', () => {
|
|
expect(loadSessionState('../../../etc')).toBeNull();
|
|
});
|
|
|
|
it('should handle state without optional fields', () => {
|
|
const minimalState: SessionState = {
|
|
firstLoad: '2025-01-01T00:00:00.000Z',
|
|
loadCount: 1
|
|
};
|
|
|
|
saveSessionState(testSessionId, minimalState);
|
|
const loaded = loadSessionState(testSessionId);
|
|
|
|
expect(loaded).not.toBeNull();
|
|
expect(loaded!.lastPrompt).toBeUndefined();
|
|
expect(loaded!.activeMode).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('clearSessionState', () => {
|
|
const testSessionId = 'test-clear-session';
|
|
|
|
it('should clear existing session state', () => {
|
|
saveSessionState(testSessionId, {
|
|
firstLoad: new Date().toISOString(),
|
|
loadCount: 1
|
|
});
|
|
|
|
expect(loadSessionState(testSessionId)).not.toBeNull();
|
|
|
|
const result = clearSessionState(testSessionId);
|
|
expect(result).toBe(true);
|
|
expect(loadSessionState(testSessionId)).toBeNull();
|
|
});
|
|
|
|
it('should return false for non-existent session', () => {
|
|
const result = clearSessionState('non-existent-session-xyz');
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false for invalid session ID', () => {
|
|
expect(clearSessionState('../../../etc')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('updateSessionState', () => {
|
|
const testSessionId = 'test-update-session';
|
|
|
|
afterEach(() => {
|
|
try {
|
|
clearSessionState(testSessionId);
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
});
|
|
|
|
it('should create new state if none exists', () => {
|
|
const state = updateSessionState(testSessionId, { loadCount: 1 });
|
|
|
|
expect(state.firstLoad).toBeDefined();
|
|
expect(state.loadCount).toBe(1);
|
|
});
|
|
|
|
it('should merge updates with existing state', () => {
|
|
saveSessionState(testSessionId, {
|
|
firstLoad: '2025-01-01T00:00:00.000Z',
|
|
loadCount: 5,
|
|
lastPrompt: 'old prompt'
|
|
});
|
|
|
|
const state = updateSessionState(testSessionId, {
|
|
loadCount: 6,
|
|
lastPrompt: 'new prompt'
|
|
});
|
|
|
|
expect(state.firstLoad).toBe('2025-01-01T00:00:00.000Z');
|
|
expect(state.loadCount).toBe(6);
|
|
expect(state.lastPrompt).toBe('new prompt');
|
|
});
|
|
});
|
|
|
|
describe('incrementSessionLoad', () => {
|
|
const testSessionId = 'test-increment-session';
|
|
|
|
afterEach(() => {
|
|
try {
|
|
clearSessionState(testSessionId);
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
});
|
|
|
|
it('should create new state on first load', () => {
|
|
const result = incrementSessionLoad(testSessionId, 'first prompt');
|
|
|
|
expect(result.isFirstPrompt).toBe(true);
|
|
expect(result.state.loadCount).toBe(1);
|
|
expect(result.state.lastPrompt).toBe('first prompt');
|
|
});
|
|
|
|
it('should increment load count on subsequent loads', () => {
|
|
incrementSessionLoad(testSessionId, 'first prompt');
|
|
const result = incrementSessionLoad(testSessionId, 'second prompt');
|
|
|
|
expect(result.isFirstPrompt).toBe(false);
|
|
expect(result.state.loadCount).toBe(2);
|
|
expect(result.state.lastPrompt).toBe('second prompt');
|
|
});
|
|
|
|
it('should preserve prompt when not provided', () => {
|
|
incrementSessionLoad(testSessionId, 'first prompt');
|
|
const result = incrementSessionLoad(testSessionId);
|
|
|
|
expect(result.state.lastPrompt).toBe('first prompt');
|
|
});
|
|
});
|
|
|
|
describe('SessionStateService class', () => {
|
|
const testSessionId = 'test-service-class-session';
|
|
let service: SessionStateService;
|
|
|
|
beforeEach(() => {
|
|
service = new SessionStateService();
|
|
});
|
|
|
|
afterEach(() => {
|
|
try {
|
|
service.clear(testSessionId);
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
});
|
|
|
|
it('should provide object-oriented interface', () => {
|
|
const result = service.incrementLoad(testSessionId, 'test prompt');
|
|
|
|
expect(result.isFirstPrompt).toBe(true);
|
|
expect(service.getLoadCount(testSessionId)).toBe(1);
|
|
expect(service.isFirstLoad(testSessionId)).toBe(false);
|
|
});
|
|
|
|
it('should support update method', () => {
|
|
service.save(testSessionId, {
|
|
firstLoad: new Date().toISOString(),
|
|
loadCount: 1
|
|
});
|
|
|
|
const state = service.update(testSessionId, { activeMode: 'write' });
|
|
expect(state.activeMode).toBe('write');
|
|
expect(state.loadCount).toBe(1);
|
|
});
|
|
});
|