mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-06 01:54:11 +08:00
466 lines
14 KiB
TypeScript
466 lines
14 KiB
TypeScript
/**
|
|
* E2E tests for Session Lifecycle (Golden Path)
|
|
*
|
|
* Tests the complete lifecycle of a workflow session:
|
|
* 1. Initialize session
|
|
* 2. Add tasks
|
|
* 3. Update task status
|
|
* 4. Archive session
|
|
*
|
|
* Verifies both dual parameter format support (legacy/new) and
|
|
* boundary conditions (invalid JSON, non-existent session, path traversal).
|
|
*/
|
|
|
|
import { after, afterEach, before, describe, it, mock } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
const sessionManagerUrl = new URL('../../dist/tools/session-manager.js', import.meta.url);
|
|
sessionManagerUrl.searchParams.set('t', String(Date.now()));
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
let sessionManager: any;
|
|
|
|
function readJson(filePath: string): any {
|
|
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
}
|
|
|
|
function workflowPath(projectRoot: string, ...parts: string[]): string {
|
|
return join(projectRoot, '.workflow', ...parts);
|
|
}
|
|
|
|
describe('E2E: Session Lifecycle (Golden Path)', async () => {
|
|
let projectRoot: string;
|
|
const originalCwd = process.cwd();
|
|
|
|
before(async () => {
|
|
projectRoot = mkdtempSync(join(tmpdir(), 'ccw-e2e-session-lifecycle-'));
|
|
process.chdir(projectRoot);
|
|
sessionManager = await import(sessionManagerUrl.href);
|
|
mock.method(console, 'error', () => {});
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(workflowPath(projectRoot), { recursive: true, force: true });
|
|
});
|
|
|
|
after(() => {
|
|
process.chdir(originalCwd);
|
|
rmSync(projectRoot, { recursive: true, force: true });
|
|
mock.restoreAll();
|
|
});
|
|
|
|
it('completes full session lifecycle: init → add tasks → update status → archive', async () => {
|
|
const sessionId = 'WFS-e2e-golden-001';
|
|
|
|
// Step 1: Initialize session
|
|
const initRes = await sessionManager.handler({
|
|
operation: 'init',
|
|
session_id: sessionId,
|
|
metadata: {
|
|
type: 'workflow',
|
|
description: 'E2E golden path test',
|
|
project: 'test-project'
|
|
}
|
|
});
|
|
|
|
assert.equal(initRes.success, true);
|
|
assert.equal(initRes.result.location, 'active');
|
|
assert.equal(initRes.result.session_id, sessionId);
|
|
|
|
const sessionPath = workflowPath(projectRoot, 'active', sessionId);
|
|
assert.equal(existsSync(sessionPath), true);
|
|
assert.equal(existsSync(join(sessionPath, '.task')), true);
|
|
assert.equal(existsSync(join(sessionPath, '.summaries')), true);
|
|
assert.equal(existsSync(join(sessionPath, '.process')), true);
|
|
|
|
const metaFile = join(sessionPath, 'workflow-session.json');
|
|
const meta = readJson(metaFile);
|
|
assert.equal(meta.session_id, sessionId);
|
|
assert.equal(meta.type, 'workflow');
|
|
assert.equal(meta.status, 'initialized');
|
|
|
|
// Step 2: Add tasks
|
|
const task1 = {
|
|
task_id: 'IMPL-001',
|
|
title: 'Implement feature A',
|
|
status: 'pending',
|
|
priority: 'high'
|
|
};
|
|
|
|
const task2 = {
|
|
task_id: 'IMPL-002',
|
|
title: 'Implement feature B',
|
|
status: 'pending',
|
|
priority: 'medium'
|
|
};
|
|
|
|
const writeTask1 = await sessionManager.handler({
|
|
operation: 'write',
|
|
session_id: sessionId,
|
|
content_type: 'task',
|
|
path_params: { task_id: 'IMPL-001' },
|
|
content: task1
|
|
});
|
|
|
|
assert.equal(writeTask1.success, true);
|
|
assert.equal(existsSync(join(sessionPath, '.task', 'IMPL-001.json')), true);
|
|
|
|
const writeTask2 = await sessionManager.handler({
|
|
operation: 'write',
|
|
session_id: sessionId,
|
|
content_type: 'task',
|
|
path_params: { task_id: 'IMPL-002' },
|
|
content: task2
|
|
});
|
|
|
|
assert.equal(writeTask2.success, true);
|
|
assert.equal(existsSync(join(sessionPath, '.task', 'IMPL-002.json')), true);
|
|
|
|
// Step 3: Update task status
|
|
const updateRes = await sessionManager.handler({
|
|
operation: 'update',
|
|
session_id: sessionId,
|
|
content_type: 'task',
|
|
path_params: { task_id: 'IMPL-001' },
|
|
content: {
|
|
status: 'in_progress',
|
|
updated_at: new Date().toISOString()
|
|
}
|
|
});
|
|
|
|
assert.equal(updateRes.success, true);
|
|
const updatedTask = readJson(join(sessionPath, '.task', 'IMPL-001.json'));
|
|
assert.equal(updatedTask.status, 'in_progress');
|
|
assert.equal(updatedTask.title, 'Implement feature A');
|
|
assert.ok(updatedTask.updated_at);
|
|
|
|
// Complete the task
|
|
const completeRes = await sessionManager.handler({
|
|
operation: 'update',
|
|
session_id: sessionId,
|
|
content_type: 'task',
|
|
path_params: { task_id: 'IMPL-001' },
|
|
content: {
|
|
status: 'completed',
|
|
completed_at: new Date().toISOString()
|
|
}
|
|
});
|
|
|
|
assert.equal(completeRes.success, true);
|
|
const completedTask = readJson(join(sessionPath, '.task', 'IMPL-001.json'));
|
|
assert.equal(completedTask.status, 'completed');
|
|
|
|
// Step 4: Update session status to completed
|
|
const updateSessionRes = await sessionManager.handler({
|
|
operation: 'update',
|
|
session_id: sessionId,
|
|
content_type: 'session',
|
|
content: {
|
|
status: 'completed',
|
|
completed_at: new Date().toISOString()
|
|
}
|
|
});
|
|
|
|
assert.equal(updateSessionRes.success, true);
|
|
const updatedMeta = readJson(metaFile);
|
|
assert.equal(updatedMeta.status, 'completed');
|
|
|
|
// Step 5: Archive session
|
|
const archiveRes = await sessionManager.handler({
|
|
operation: 'archive',
|
|
session_id: sessionId,
|
|
update_status: true
|
|
});
|
|
|
|
assert.equal(archiveRes.success, true);
|
|
assert.equal(archiveRes.result.source_location, 'active');
|
|
assert.ok(archiveRes.result.destination.includes('archives'));
|
|
|
|
// Verify session moved to archives
|
|
assert.equal(existsSync(sessionPath), false);
|
|
const archivedPath = workflowPath(projectRoot, 'archives', sessionId);
|
|
assert.equal(existsSync(archivedPath), true);
|
|
assert.equal(existsSync(join(archivedPath, '.task', 'IMPL-001.json')), true);
|
|
assert.equal(existsSync(join(archivedPath, '.task', 'IMPL-002.json')), true);
|
|
|
|
const archivedMeta = readJson(join(archivedPath, 'workflow-session.json'));
|
|
assert.equal(archivedMeta.session_id, sessionId);
|
|
assert.equal(archivedMeta.status, 'completed');
|
|
assert.ok(archivedMeta.archived_at, 'should have archived_at timestamp');
|
|
});
|
|
|
|
it('supports dual parameter format: legacy (operation) and new (explicit params)', async () => {
|
|
const sessionId = 'WFS-e2e-dual-format';
|
|
|
|
// New format: explicit parameters
|
|
const newFormatRes = await sessionManager.handler({
|
|
operation: 'init',
|
|
session_id: sessionId,
|
|
metadata: { type: 'workflow' }
|
|
});
|
|
|
|
assert.equal(newFormatRes.success, true);
|
|
|
|
// Legacy format: operation-based with session_id
|
|
const legacyReadRes = await sessionManager.handler({
|
|
operation: 'read',
|
|
session_id: sessionId,
|
|
content_type: 'session'
|
|
});
|
|
|
|
assert.equal(legacyReadRes.success, true);
|
|
assert.equal(legacyReadRes.result.content.session_id, sessionId);
|
|
});
|
|
|
|
it('handles boundary condition: invalid JSON in task file', async () => {
|
|
const sessionId = 'WFS-e2e-invalid-json';
|
|
|
|
// Initialize session
|
|
await sessionManager.handler({
|
|
operation: 'init',
|
|
session_id: sessionId,
|
|
metadata: { type: 'workflow' }
|
|
});
|
|
|
|
const sessionPath = workflowPath(projectRoot, 'active', sessionId);
|
|
const invalidTaskPath = join(sessionPath, '.task', 'IMPL-BAD.json');
|
|
|
|
// Write invalid JSON manually
|
|
const fs = await import('fs');
|
|
fs.writeFileSync(invalidTaskPath, '{ invalid json', 'utf8');
|
|
|
|
// Attempt to read invalid JSON
|
|
const readRes = await sessionManager.handler({
|
|
operation: 'read',
|
|
session_id: sessionId,
|
|
content_type: 'task',
|
|
path_params: { task_id: 'IMPL-BAD' }
|
|
});
|
|
|
|
assert.equal(readRes.success, false);
|
|
assert.ok(readRes.error.includes('Unexpected') || readRes.error.includes('JSON'));
|
|
});
|
|
|
|
it('handles boundary condition: non-existent session', async () => {
|
|
const readRes = await sessionManager.handler({
|
|
operation: 'read',
|
|
session_id: 'WFS-DOES-NOT-EXIST',
|
|
content_type: 'session'
|
|
});
|
|
|
|
assert.equal(readRes.success, false);
|
|
assert.ok(readRes.error.includes('not found'));
|
|
|
|
const updateRes = await sessionManager.handler({
|
|
operation: 'update',
|
|
session_id: 'WFS-DOES-NOT-EXIST',
|
|
content_type: 'session',
|
|
content: { status: 'active' }
|
|
});
|
|
|
|
assert.equal(updateRes.success, false);
|
|
assert.ok(updateRes.error.includes('not found'));
|
|
});
|
|
|
|
it('handles boundary condition: path traversal attempt', async () => {
|
|
// Attempt to create session with path traversal
|
|
const traversalRes = await sessionManager.handler({
|
|
operation: 'init',
|
|
session_id: '../../../etc/WFS-traversal',
|
|
metadata: { type: 'workflow' }
|
|
});
|
|
|
|
assert.equal(traversalRes.success, false);
|
|
assert.ok(traversalRes.error.includes('Invalid session_id format'));
|
|
|
|
// Attempt to read with path traversal in content_type
|
|
const sessionId = 'WFS-e2e-safe';
|
|
await sessionManager.handler({
|
|
operation: 'init',
|
|
session_id: sessionId,
|
|
metadata: { type: 'workflow' }
|
|
});
|
|
|
|
const readTraversalRes = await sessionManager.handler({
|
|
operation: 'read',
|
|
session_id: sessionId,
|
|
content_type: 'task',
|
|
path_params: { task_id: '../../../etc/passwd' }
|
|
});
|
|
|
|
assert.equal(readTraversalRes.success, false);
|
|
// Should reject path traversal or not find file
|
|
assert.ok(readTraversalRes.error);
|
|
});
|
|
|
|
it('handles concurrent task updates without data loss', async () => {
|
|
const sessionId = 'WFS-e2e-concurrent';
|
|
|
|
await sessionManager.handler({
|
|
operation: 'init',
|
|
session_id: sessionId,
|
|
metadata: { type: 'workflow' }
|
|
});
|
|
|
|
const task = {
|
|
task_id: 'IMPL-RACE',
|
|
title: 'Test concurrent updates',
|
|
status: 'pending',
|
|
counter: 0
|
|
};
|
|
|
|
await sessionManager.handler({
|
|
operation: 'write',
|
|
session_id: sessionId,
|
|
content_type: 'task',
|
|
path_params: { task_id: 'IMPL-RACE' },
|
|
content: task
|
|
});
|
|
|
|
// Perform concurrent updates
|
|
const updates = await Promise.all([
|
|
sessionManager.handler({
|
|
operation: 'update',
|
|
session_id: sessionId,
|
|
content_type: 'task',
|
|
path_params: { task_id: 'IMPL-RACE' },
|
|
content: { field1: 'value1' }
|
|
}),
|
|
sessionManager.handler({
|
|
operation: 'update',
|
|
session_id: sessionId,
|
|
content_type: 'task',
|
|
path_params: { task_id: 'IMPL-RACE' },
|
|
content: { field2: 'value2' }
|
|
}),
|
|
sessionManager.handler({
|
|
operation: 'update',
|
|
session_id: sessionId,
|
|
content_type: 'task',
|
|
path_params: { task_id: 'IMPL-RACE' },
|
|
content: { field3: 'value3' }
|
|
})
|
|
]);
|
|
|
|
// All updates should succeed
|
|
updates.forEach(res => assert.equal(res.success, true));
|
|
|
|
// Verify final state
|
|
const finalRes = await sessionManager.handler({
|
|
operation: 'read',
|
|
session_id: sessionId,
|
|
content_type: 'task',
|
|
path_params: { task_id: 'IMPL-RACE' }
|
|
});
|
|
|
|
assert.equal(finalRes.success, true);
|
|
// Note: Due to shallow merge and race conditions, last write wins
|
|
// At least one field should be present
|
|
const hasField = finalRes.result.content.field1 ||
|
|
finalRes.result.content.field2 ||
|
|
finalRes.result.content.field3;
|
|
assert.ok(hasField);
|
|
});
|
|
|
|
it('preserves task data when archiving session', async () => {
|
|
const sessionId = 'WFS-e2e-archive-preserve';
|
|
|
|
await sessionManager.handler({
|
|
operation: 'init',
|
|
session_id: sessionId,
|
|
metadata: { type: 'workflow', description: 'Archive test' }
|
|
});
|
|
|
|
const complexTask = {
|
|
task_id: 'IMPL-COMPLEX',
|
|
title: 'Complex task with nested data',
|
|
status: 'completed',
|
|
metadata: {
|
|
nested: {
|
|
deep: {
|
|
value: 'preserved'
|
|
}
|
|
},
|
|
array: [1, 2, 3],
|
|
bool: true
|
|
},
|
|
tags: ['tag1', 'tag2']
|
|
};
|
|
|
|
await sessionManager.handler({
|
|
operation: 'write',
|
|
session_id: sessionId,
|
|
content_type: 'task',
|
|
path_params: { task_id: 'IMPL-COMPLEX' },
|
|
content: complexTask
|
|
});
|
|
|
|
await sessionManager.handler({
|
|
operation: 'archive',
|
|
session_id: sessionId
|
|
});
|
|
|
|
const archivedPath = workflowPath(projectRoot, 'archives', sessionId);
|
|
const archivedTask = readJson(join(archivedPath, '.task', 'IMPL-COMPLEX.json'));
|
|
|
|
assert.deepEqual(archivedTask.metadata, complexTask.metadata);
|
|
assert.deepEqual(archivedTask.tags, complexTask.tags);
|
|
assert.equal(archivedTask.title, complexTask.title);
|
|
});
|
|
|
|
it('lists sessions across all locations', async () => {
|
|
// Create sessions in different locations
|
|
await sessionManager.handler({
|
|
operation: 'init',
|
|
session_id: 'WFS-active-1',
|
|
metadata: { type: 'workflow' }
|
|
});
|
|
|
|
await sessionManager.handler({
|
|
operation: 'init',
|
|
session_id: 'lite-plan-1',
|
|
metadata: { type: 'lite-plan' }
|
|
});
|
|
|
|
await sessionManager.handler({
|
|
operation: 'init',
|
|
session_id: 'lite-fix-1',
|
|
metadata: { type: 'lite-fix' }
|
|
});
|
|
|
|
// Archive one
|
|
await sessionManager.handler({
|
|
operation: 'init',
|
|
session_id: 'WFS-to-archive',
|
|
metadata: { type: 'workflow' }
|
|
});
|
|
|
|
await sessionManager.handler({
|
|
operation: 'archive',
|
|
session_id: 'WFS-to-archive'
|
|
});
|
|
|
|
// List all
|
|
const listRes = await sessionManager.handler({
|
|
operation: 'list',
|
|
location: 'all',
|
|
include_metadata: true
|
|
});
|
|
|
|
assert.equal(listRes.success, true);
|
|
assert.equal(listRes.result.active.length, 1);
|
|
assert.equal(listRes.result.archived.length, 1);
|
|
assert.equal(listRes.result.litePlan.length, 1);
|
|
assert.equal(listRes.result.liteFix.length, 1);
|
|
assert.equal(listRes.result.total, 4);
|
|
|
|
// Verify metadata included
|
|
const activeSession = listRes.result.active[0];
|
|
assert.ok(activeSession.metadata);
|
|
assert.equal(activeSession.metadata.session_id, 'WFS-active-1');
|
|
});
|
|
});
|