mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-07 17:53:32 +08:00
feat: record canonical session snapshots via adapters (#511)
This commit is contained in:
@@ -5,9 +5,16 @@ const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const {
|
||||
getFallbackSessionRecordingPath,
|
||||
persistCanonicalSnapshot
|
||||
} = require('../../scripts/lib/session-adapters/canonical-session');
|
||||
const { createClaudeHistoryAdapter } = require('../../scripts/lib/session-adapters/claude-history');
|
||||
const { createDmuxTmuxAdapter } = require('../../scripts/lib/session-adapters/dmux-tmux');
|
||||
const { createAdapterRegistry } = require('../../scripts/lib/session-adapters/registry');
|
||||
const {
|
||||
createAdapterRegistry,
|
||||
inspectSessionTarget
|
||||
} = require('../../scripts/lib/session-adapters/registry');
|
||||
|
||||
console.log('=== Testing session-adapters ===\n');
|
||||
|
||||
@@ -41,74 +48,233 @@ function withHome(homeDir, fn) {
|
||||
}
|
||||
|
||||
test('dmux adapter normalizes orchestration snapshots into canonical form', () => {
|
||||
const adapter = createDmuxTmuxAdapter({
|
||||
collectSessionSnapshotImpl: () => ({
|
||||
sessionName: 'workflow-visual-proof',
|
||||
coordinationDir: '/tmp/.claude/orchestration/workflow-visual-proof',
|
||||
repoRoot: '/tmp/repo',
|
||||
targetType: 'plan',
|
||||
sessionActive: true,
|
||||
paneCount: 1,
|
||||
workerCount: 1,
|
||||
workerStates: { running: 1 },
|
||||
panes: [{
|
||||
paneId: '%95',
|
||||
windowIndex: 1,
|
||||
paneIndex: 0,
|
||||
title: 'seed-check',
|
||||
currentCommand: 'codex',
|
||||
currentPath: '/tmp/worktree',
|
||||
active: false,
|
||||
dead: false,
|
||||
pid: 1234
|
||||
}],
|
||||
workers: [{
|
||||
workerSlug: 'seed-check',
|
||||
workerDir: '/tmp/.claude/orchestration/workflow-visual-proof/seed-check',
|
||||
status: {
|
||||
state: 'running',
|
||||
updated: '2026-03-13T00:00:00Z',
|
||||
branch: 'feature/seed-check',
|
||||
worktree: '/tmp/worktree',
|
||||
taskFile: '/tmp/task.md',
|
||||
handoffFile: '/tmp/handoff.md'
|
||||
},
|
||||
task: {
|
||||
objective: 'Inspect seeded files.',
|
||||
seedPaths: ['scripts/orchestrate-worktrees.js']
|
||||
},
|
||||
handoff: {
|
||||
summary: ['Pending'],
|
||||
validation: [],
|
||||
remainingRisks: ['No screenshot yet']
|
||||
},
|
||||
files: {
|
||||
status: '/tmp/status.md',
|
||||
task: '/tmp/task.md',
|
||||
handoff: '/tmp/handoff.md'
|
||||
},
|
||||
pane: {
|
||||
const recordingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-recordings-'));
|
||||
|
||||
try {
|
||||
const adapter = createDmuxTmuxAdapter({
|
||||
collectSessionSnapshotImpl: () => ({
|
||||
sessionName: 'workflow-visual-proof',
|
||||
coordinationDir: '/tmp/.claude/orchestration/workflow-visual-proof',
|
||||
repoRoot: '/tmp/repo',
|
||||
targetType: 'plan',
|
||||
sessionActive: true,
|
||||
paneCount: 1,
|
||||
workerCount: 1,
|
||||
workerStates: { running: 1 },
|
||||
panes: [{
|
||||
paneId: '%95',
|
||||
title: 'seed-check'
|
||||
}
|
||||
}]
|
||||
})
|
||||
});
|
||||
windowIndex: 1,
|
||||
paneIndex: 0,
|
||||
title: 'seed-check',
|
||||
currentCommand: 'codex',
|
||||
currentPath: '/tmp/worktree',
|
||||
active: false,
|
||||
dead: false,
|
||||
pid: 1234
|
||||
}],
|
||||
workers: [{
|
||||
workerSlug: 'seed-check',
|
||||
workerDir: '/tmp/.claude/orchestration/workflow-visual-proof/seed-check',
|
||||
status: {
|
||||
state: 'running',
|
||||
updated: '2026-03-13T00:00:00Z',
|
||||
branch: 'feature/seed-check',
|
||||
worktree: '/tmp/worktree',
|
||||
taskFile: '/tmp/task.md',
|
||||
handoffFile: '/tmp/handoff.md'
|
||||
},
|
||||
task: {
|
||||
objective: 'Inspect seeded files.',
|
||||
seedPaths: ['scripts/orchestrate-worktrees.js']
|
||||
},
|
||||
handoff: {
|
||||
summary: ['Pending'],
|
||||
validation: [],
|
||||
remainingRisks: ['No screenshot yet']
|
||||
},
|
||||
files: {
|
||||
status: '/tmp/status.md',
|
||||
task: '/tmp/task.md',
|
||||
handoff: '/tmp/handoff.md'
|
||||
},
|
||||
pane: {
|
||||
paneId: '%95',
|
||||
title: 'seed-check'
|
||||
}
|
||||
}]
|
||||
}),
|
||||
recordingDir
|
||||
});
|
||||
|
||||
const snapshot = adapter.open('workflow-visual-proof').getSnapshot();
|
||||
const snapshot = adapter.open('workflow-visual-proof').getSnapshot();
|
||||
const recordingPath = getFallbackSessionRecordingPath(snapshot, { recordingDir });
|
||||
const persisted = JSON.parse(fs.readFileSync(recordingPath, 'utf8'));
|
||||
|
||||
assert.strictEqual(snapshot.schemaVersion, 'ecc.session.v1');
|
||||
assert.strictEqual(snapshot.adapterId, 'dmux-tmux');
|
||||
assert.strictEqual(snapshot.session.id, 'workflow-visual-proof');
|
||||
assert.strictEqual(snapshot.session.kind, 'orchestrated');
|
||||
assert.strictEqual(snapshot.session.sourceTarget.type, 'session');
|
||||
assert.strictEqual(snapshot.aggregates.workerCount, 1);
|
||||
assert.strictEqual(snapshot.workers[0].runtime.kind, 'tmux-pane');
|
||||
assert.strictEqual(snapshot.workers[0].outputs.remainingRisks[0], 'No screenshot yet');
|
||||
assert.strictEqual(snapshot.schemaVersion, 'ecc.session.v1');
|
||||
assert.strictEqual(snapshot.adapterId, 'dmux-tmux');
|
||||
assert.strictEqual(snapshot.session.id, 'workflow-visual-proof');
|
||||
assert.strictEqual(snapshot.session.kind, 'orchestrated');
|
||||
assert.strictEqual(snapshot.session.state, 'active');
|
||||
assert.strictEqual(snapshot.session.sourceTarget.type, 'session');
|
||||
assert.strictEqual(snapshot.aggregates.workerCount, 1);
|
||||
assert.strictEqual(snapshot.workers[0].runtime.kind, 'tmux-pane');
|
||||
assert.strictEqual(snapshot.workers[0].outputs.remainingRisks[0], 'No screenshot yet');
|
||||
assert.strictEqual(persisted.latest.session.state, 'active');
|
||||
assert.strictEqual(persisted.latest.adapterId, 'dmux-tmux');
|
||||
assert.strictEqual(persisted.history.length, 1);
|
||||
} finally {
|
||||
fs.rmSync(recordingDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('dmux adapter marks finished sessions as completed and records history', () => {
|
||||
const recordingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-recordings-'));
|
||||
|
||||
try {
|
||||
const adapter = createDmuxTmuxAdapter({
|
||||
collectSessionSnapshotImpl: () => ({
|
||||
sessionName: 'workflow-visual-proof',
|
||||
coordinationDir: '/tmp/.claude/orchestration/workflow-visual-proof',
|
||||
repoRoot: '/tmp/repo',
|
||||
targetType: 'session',
|
||||
sessionActive: false,
|
||||
paneCount: 0,
|
||||
workerCount: 2,
|
||||
workerStates: { completed: 2 },
|
||||
panes: [],
|
||||
workers: [{
|
||||
workerSlug: 'seed-check',
|
||||
workerDir: '/tmp/.claude/orchestration/workflow-visual-proof/seed-check',
|
||||
status: {
|
||||
state: 'completed',
|
||||
updated: '2026-03-13T00:00:00Z',
|
||||
branch: 'feature/seed-check',
|
||||
worktree: '/tmp/worktree-a',
|
||||
taskFile: '/tmp/task-a.md',
|
||||
handoffFile: '/tmp/handoff-a.md'
|
||||
},
|
||||
task: {
|
||||
objective: 'Inspect seeded files.',
|
||||
seedPaths: ['scripts/orchestrate-worktrees.js']
|
||||
},
|
||||
handoff: {
|
||||
summary: ['Finished'],
|
||||
validation: ['Reviewed outputs'],
|
||||
remainingRisks: []
|
||||
},
|
||||
files: {
|
||||
status: '/tmp/status-a.md',
|
||||
task: '/tmp/task-a.md',
|
||||
handoff: '/tmp/handoff-a.md'
|
||||
},
|
||||
pane: null
|
||||
}, {
|
||||
workerSlug: 'proof',
|
||||
workerDir: '/tmp/.claude/orchestration/workflow-visual-proof/proof',
|
||||
status: {
|
||||
state: 'completed',
|
||||
updated: '2026-03-13T00:10:00Z',
|
||||
branch: 'feature/proof',
|
||||
worktree: '/tmp/worktree-b',
|
||||
taskFile: '/tmp/task-b.md',
|
||||
handoffFile: '/tmp/handoff-b.md'
|
||||
},
|
||||
task: {
|
||||
objective: 'Capture proof.',
|
||||
seedPaths: ['README.md']
|
||||
},
|
||||
handoff: {
|
||||
summary: ['Delivered proof'],
|
||||
validation: ['Checked screenshots'],
|
||||
remainingRisks: []
|
||||
},
|
||||
files: {
|
||||
status: '/tmp/status-b.md',
|
||||
task: '/tmp/task-b.md',
|
||||
handoff: '/tmp/handoff-b.md'
|
||||
},
|
||||
pane: null
|
||||
}]
|
||||
}),
|
||||
recordingDir
|
||||
});
|
||||
|
||||
const snapshot = adapter.open('workflow-visual-proof').getSnapshot();
|
||||
const recordingPath = getFallbackSessionRecordingPath(snapshot, { recordingDir });
|
||||
const persisted = JSON.parse(fs.readFileSync(recordingPath, 'utf8'));
|
||||
|
||||
assert.strictEqual(snapshot.session.state, 'completed');
|
||||
assert.strictEqual(snapshot.aggregates.states.completed, 2);
|
||||
assert.strictEqual(persisted.latest.session.state, 'completed');
|
||||
assert.strictEqual(persisted.history.length, 1);
|
||||
} finally {
|
||||
fs.rmSync(recordingDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('fallback recording does not append duplicate history entries for unchanged snapshots', () => {
|
||||
const recordingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-recordings-'));
|
||||
|
||||
try {
|
||||
const adapter = createDmuxTmuxAdapter({
|
||||
collectSessionSnapshotImpl: () => ({
|
||||
sessionName: 'workflow-visual-proof',
|
||||
coordinationDir: '/tmp/.claude/orchestration/workflow-visual-proof',
|
||||
repoRoot: '/tmp/repo',
|
||||
targetType: 'session',
|
||||
sessionActive: true,
|
||||
paneCount: 1,
|
||||
workerCount: 1,
|
||||
workerStates: { running: 1 },
|
||||
panes: [],
|
||||
workers: [{
|
||||
workerSlug: 'seed-check',
|
||||
workerDir: '/tmp/.claude/orchestration/workflow-visual-proof/seed-check',
|
||||
status: {
|
||||
state: 'running',
|
||||
updated: '2026-03-13T00:00:00Z',
|
||||
branch: 'feature/seed-check',
|
||||
worktree: '/tmp/worktree',
|
||||
taskFile: '/tmp/task.md',
|
||||
handoffFile: '/tmp/handoff.md'
|
||||
},
|
||||
task: {
|
||||
objective: 'Inspect seeded files.',
|
||||
seedPaths: ['scripts/orchestrate-worktrees.js']
|
||||
},
|
||||
handoff: {
|
||||
summary: ['Pending'],
|
||||
validation: [],
|
||||
remainingRisks: []
|
||||
},
|
||||
files: {
|
||||
status: '/tmp/status.md',
|
||||
task: '/tmp/task.md',
|
||||
handoff: '/tmp/handoff.md'
|
||||
},
|
||||
pane: null
|
||||
}]
|
||||
}),
|
||||
recordingDir
|
||||
});
|
||||
|
||||
const handle = adapter.open('workflow-visual-proof');
|
||||
const firstSnapshot = handle.getSnapshot();
|
||||
const secondSnapshot = handle.getSnapshot();
|
||||
const recordingPath = getFallbackSessionRecordingPath(firstSnapshot, { recordingDir });
|
||||
const persisted = JSON.parse(fs.readFileSync(recordingPath, 'utf8'));
|
||||
|
||||
assert.deepStrictEqual(secondSnapshot, firstSnapshot);
|
||||
assert.strictEqual(persisted.history.length, 1);
|
||||
assert.deepStrictEqual(persisted.latest, secondSnapshot);
|
||||
} finally {
|
||||
fs.rmSync(recordingDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('claude-history adapter loads the latest recorded session', () => {
|
||||
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-adapter-home-'));
|
||||
const recordingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-recordings-'));
|
||||
const sessionsDir = path.join(homeDir, '.claude', 'sessions');
|
||||
fs.mkdirSync(sessionsDir, { recursive: true });
|
||||
|
||||
@@ -140,8 +306,10 @@ test('claude-history adapter loads the latest recorded session', () => {
|
||||
|
||||
try {
|
||||
withHome(homeDir, () => {
|
||||
const adapter = createClaudeHistoryAdapter();
|
||||
const adapter = createClaudeHistoryAdapter({ recordingDir });
|
||||
const snapshot = adapter.open('claude:latest').getSnapshot();
|
||||
const recordingPath = getFallbackSessionRecordingPath(snapshot, { recordingDir });
|
||||
const persisted = JSON.parse(fs.readFileSync(recordingPath, 'utf8'));
|
||||
|
||||
assert.strictEqual(snapshot.schemaVersion, 'ecc.session.v1');
|
||||
assert.strictEqual(snapshot.adapterId, 'claude-history');
|
||||
@@ -151,11 +319,15 @@ test('claude-history adapter loads the latest recorded session', () => {
|
||||
assert.strictEqual(snapshot.workers[0].branch, 'feat/session-adapter');
|
||||
assert.strictEqual(snapshot.workers[0].worktree, '/tmp/ecc-worktree');
|
||||
assert.strictEqual(snapshot.workers[0].runtime.kind, 'claude-session');
|
||||
assert.deepStrictEqual(snapshot.workers[0].intent.seedPaths, ['scripts/lib/orchestration-session.js']);
|
||||
assert.strictEqual(snapshot.workers[0].artifacts.sessionFile, sessionPath);
|
||||
assert.ok(snapshot.workers[0].outputs.summary.includes('Build snapshot prototype'));
|
||||
assert.strictEqual(persisted.latest.adapterId, 'claude-history');
|
||||
assert.strictEqual(persisted.history.length, 1);
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
fs.rmSync(recordingDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -264,6 +436,41 @@ test('adapter registry resolves structured target types into the correct adapter
|
||||
}
|
||||
});
|
||||
|
||||
test('default registry forwards a nested state-store writer to adapters', () => {
|
||||
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-registry-home-'));
|
||||
const sessionsDir = path.join(homeDir, '.claude', 'sessions');
|
||||
fs.mkdirSync(sessionsDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(sessionsDir, '2026-03-13-z9y8x7w6-session.tmp'),
|
||||
'# History Session\n\n**Branch:** feat/history\n'
|
||||
);
|
||||
|
||||
const stateStore = {
|
||||
sessions: {
|
||||
persisted: [],
|
||||
persistCanonicalSessionSnapshot(snapshot, metadata) {
|
||||
this.persisted.push({ snapshot, metadata });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
withHome(homeDir, () => {
|
||||
const snapshot = inspectSessionTarget('claude:latest', {
|
||||
cwd: process.cwd(),
|
||||
stateStore
|
||||
});
|
||||
|
||||
assert.strictEqual(snapshot.adapterId, 'claude-history');
|
||||
assert.strictEqual(stateStore.sessions.persisted.length, 1);
|
||||
assert.strictEqual(stateStore.sessions.persisted[0].snapshot.adapterId, 'claude-history');
|
||||
assert.strictEqual(stateStore.sessions.persisted[0].metadata.sessionId, snapshot.session.id);
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('adapter registry lists adapter metadata and target types', () => {
|
||||
const registry = createAdapterRegistry();
|
||||
const adapters = registry.listAdapters();
|
||||
@@ -281,5 +488,66 @@ test('adapter registry lists adapter metadata and target types', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('persistence only falls back when the state-store module is missing', () => {
|
||||
const snapshot = {
|
||||
schemaVersion: 'ecc.session.v1',
|
||||
adapterId: 'claude-history',
|
||||
session: {
|
||||
id: 'a1b2c3d4',
|
||||
kind: 'history',
|
||||
state: 'recorded',
|
||||
repoRoot: null,
|
||||
sourceTarget: {
|
||||
type: 'claude-history',
|
||||
value: 'latest'
|
||||
}
|
||||
},
|
||||
workers: [{
|
||||
id: 'a1b2c3d4',
|
||||
label: 'Session Review',
|
||||
state: 'recorded',
|
||||
branch: null,
|
||||
worktree: null,
|
||||
runtime: {
|
||||
kind: 'claude-session',
|
||||
command: 'claude',
|
||||
pid: null,
|
||||
active: false,
|
||||
dead: true
|
||||
},
|
||||
intent: {
|
||||
objective: 'Session Review',
|
||||
seedPaths: []
|
||||
},
|
||||
outputs: {
|
||||
summary: [],
|
||||
validation: [],
|
||||
remainingRisks: []
|
||||
},
|
||||
artifacts: {
|
||||
sessionFile: '/tmp/session.tmp',
|
||||
context: null
|
||||
}
|
||||
}],
|
||||
aggregates: {
|
||||
workerCount: 1,
|
||||
states: {
|
||||
recorded: 1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loadError = new Error('state-store bootstrap failed');
|
||||
loadError.code = 'ERR_STATE_STORE_BOOT';
|
||||
|
||||
assert.throws(() => {
|
||||
persistCanonicalSnapshot(snapshot, {
|
||||
loadStateStoreImpl() {
|
||||
throw loadError;
|
||||
}
|
||||
});
|
||||
}, /state-store bootstrap failed/);
|
||||
});
|
||||
|
||||
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
|
||||
if (failed > 0) process.exit(1);
|
||||
|
||||
@@ -8,6 +8,8 @@ const os = require('os');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
const { getFallbackSessionRecordingPath } = require('../../scripts/lib/session-adapters/canonical-session');
|
||||
|
||||
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'session-inspect.js');
|
||||
|
||||
function run(args = [], options = {}) {
|
||||
@@ -67,6 +69,7 @@ function runTests() {
|
||||
|
||||
if (test('prints canonical JSON for claude history targets', () => {
|
||||
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-inspect-home-'));
|
||||
const recordingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-inspect-recordings-'));
|
||||
const sessionsDir = path.join(homeDir, '.claude', 'sessions');
|
||||
fs.mkdirSync(sessionsDir, { recursive: true });
|
||||
|
||||
@@ -77,16 +80,24 @@ function runTests() {
|
||||
);
|
||||
|
||||
const result = run(['claude:latest'], {
|
||||
env: { HOME: homeDir }
|
||||
env: {
|
||||
HOME: homeDir,
|
||||
ECC_SESSION_RECORDING_DIR: recordingDir
|
||||
}
|
||||
});
|
||||
|
||||
assert.strictEqual(result.code, 0, result.stderr);
|
||||
const payload = JSON.parse(result.stdout);
|
||||
const recordingPath = getFallbackSessionRecordingPath(payload, { recordingDir });
|
||||
const persisted = JSON.parse(fs.readFileSync(recordingPath, 'utf8'));
|
||||
assert.strictEqual(payload.adapterId, 'claude-history');
|
||||
assert.strictEqual(payload.session.kind, 'history');
|
||||
assert.strictEqual(payload.workers[0].branch, 'feat/session-inspect');
|
||||
assert.strictEqual(persisted.latest.adapterId, 'claude-history');
|
||||
assert.strictEqual(persisted.history.length, 1);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
fs.rmSync(recordingDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user