test: cover session adapter edge cases

This commit is contained in:
Affaan Mustafa
2026-04-29 18:21:31 -04:00
parent 33edfd3bb3
commit aaaf52fb1e
2 changed files with 393 additions and 12 deletions

View File

@@ -6,8 +6,13 @@ const os = require('os');
const path = require('path');
const {
SESSION_SCHEMA_VERSION,
buildAggregates,
getFallbackSessionRecordingPath,
persistCanonicalSnapshot
normalizeClaudeHistorySession,
normalizeDmuxSnapshot,
persistCanonicalSnapshot,
validateCanonicalSnapshot
} = 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');
@@ -55,6 +60,75 @@ function withHome(homeDir, fn) {
}
}
function canonicalSnapshot(overrides = {}) {
const snapshot = {
schemaVersion: SESSION_SCHEMA_VERSION,
adapterId: 'test-adapter',
session: {
id: 'session-1',
kind: 'test',
state: 'active',
repoRoot: null,
sourceTarget: {
type: 'session',
value: 'session-1'
}
},
workers: [{
id: 'worker-1',
label: 'Worker 1',
state: 'running',
health: 'healthy',
branch: null,
worktree: null,
runtime: {
kind: 'test-runtime',
command: null,
pid: null,
active: true,
dead: false
},
intent: {
objective: 'Test objective',
seedPaths: []
},
outputs: {
summary: [],
validation: [],
remainingRisks: []
},
artifacts: {}
}]
};
snapshot.aggregates = buildAggregates(snapshot.workers);
if (overrides.session) {
snapshot.session = { ...snapshot.session, ...overrides.session };
}
if (overrides.sourceTarget) {
snapshot.session.sourceTarget = {
...snapshot.session.sourceTarget,
...overrides.sourceTarget
};
}
if (Object.prototype.hasOwnProperty.call(overrides, 'workers')) {
snapshot.workers = overrides.workers;
snapshot.aggregates = buildAggregates(Array.isArray(overrides.workers) ? overrides.workers : []);
}
if (overrides.aggregates) {
snapshot.aggregates = { ...snapshot.aggregates, ...overrides.aggregates };
}
for (const [key, value] of Object.entries(overrides)) {
if (!['session', 'sourceTarget', 'workers', 'aggregates'].includes(key)) {
snapshot[key] = value;
}
}
return snapshot;
}
test('dmux adapter normalizes orchestration snapshots into canonical form', () => {
const recordingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-recordings-'));
@@ -509,6 +583,324 @@ test('adapter registry lists adapter metadata and target types', () => {
);
});
test('canonical snapshot validation rejects malformed required fields', () => {
const invalidCases = [
[null, /must be an object/],
[canonicalSnapshot({ schemaVersion: 'ecc.session.v0' }), /Unsupported canonical session schema version/],
[canonicalSnapshot({ adapterId: '' }), /adapterId/],
[canonicalSnapshot({ session: { id: '' } }), /session.id/],
[canonicalSnapshot({ session: { repoRoot: 42 } }), /session.repoRoot/],
[canonicalSnapshot({ sourceTarget: { type: '' } }), /session.sourceTarget.type/],
[(() => {
const snapshot = canonicalSnapshot();
snapshot.workers = [null];
snapshot.aggregates = { workerCount: 1, states: { unknown: 1 }, healths: { unknown: 1 } };
return snapshot;
})(), /workers\[0\] to be an object/],
[canonicalSnapshot({
workers: [{
...canonicalSnapshot().workers[0],
branch: 7
}]
}), /workers\[0\].branch/],
[canonicalSnapshot({
workers: [{
...canonicalSnapshot().workers[0],
runtime: {
...canonicalSnapshot().workers[0].runtime,
command: 123
}
}]
}), /workers\[0\].runtime.command/],
[canonicalSnapshot({
workers: [{
...canonicalSnapshot().workers[0],
runtime: {
...canonicalSnapshot().workers[0].runtime,
active: 'yes'
}
}]
}), /workers\[0\].runtime.active/],
[canonicalSnapshot({
workers: [{
...canonicalSnapshot().workers[0],
intent: {
objective: 'ok',
seedPaths: ['README.md', 123]
}
}]
}), /workers\[0\].intent.seedPaths/],
[canonicalSnapshot({
workers: [{
...canonicalSnapshot().workers[0],
outputs: {
summary: [],
validation: 'nope',
remainingRisks: []
}
}]
}), /workers\[0\].outputs.validation/],
[canonicalSnapshot({ aggregates: { workerCount: 99 } }), /aggregates.workerCount to match/],
[canonicalSnapshot({ aggregates: { states: [] } }), /aggregates.states to be an object/],
[canonicalSnapshot({ aggregates: { states: { running: -1 } } }), /aggregates.states.running/],
[canonicalSnapshot({ aggregates: { healths: null } }), /aggregates.healths to be an object/]
];
for (const [snapshot, pattern] of invalidCases) {
assert.throws(() => validateCanonicalSnapshot(snapshot), pattern);
}
});
function dmuxWorker(workerSlug, status = {}, overrides = {}) {
return {
workerSlug,
workerDir: `/tmp/${workerSlug}`,
status: {
state: 'running',
updated: new Date().toISOString(),
branch: null,
worktree: null,
...status
},
task: {
objective: `${workerSlug} objective`,
seedPaths: ['README.md'],
...(overrides.task || {})
},
handoff: {
summary: ['summary'],
validation: ['validation'],
remainingRisks: ['risk'],
...(overrides.handoff || {})
},
files: {
status: `/tmp/${workerSlug}/status.md`,
task: `/tmp/${workerSlug}/task.md`,
handoff: `/tmp/${workerSlug}/handoff.md`,
...(overrides.files || {})
},
pane: Object.prototype.hasOwnProperty.call(overrides, 'pane')
? overrides.pane
: {
currentCommand: 'codex',
pid: 123,
active: true,
dead: false
}
};
}
function dmuxSnapshot(overrides = {}) {
return {
sessionName: 'edge-session',
repoRoot: '/tmp/repo',
sessionActive: false,
workerStates: {},
workerCount: 0,
workers: [],
...overrides
};
}
test('dmux normalization covers missing failed idle and stale worker states', () => {
const sourceTarget = { type: 'session', value: 'edge-session' };
const missing = normalizeDmuxSnapshot(dmuxSnapshot(), sourceTarget);
assert.strictEqual(missing.session.state, 'missing');
assert.strictEqual(missing.aggregates.workerCount, 0);
const failed = normalizeDmuxSnapshot(dmuxSnapshot({
workerStates: { failed: 1 },
workerCount: 1,
workers: [
dmuxWorker('failure', { state: 'failed' }, { pane: null })
]
}), sourceTarget);
assert.strictEqual(failed.session.state, 'failed');
assert.strictEqual(failed.workers[0].health, 'degraded');
assert.strictEqual(failed.workers[0].runtime.active, false);
assert.strictEqual(failed.workers[0].runtime.dead, false);
const idle = normalizeDmuxSnapshot(dmuxSnapshot({
workerStates: { running: 1, queued: 1 },
workerCount: 2,
workers: [
dmuxWorker('missing-update', { state: 'running', updated: undefined }),
dmuxWorker('stale-update', { state: 'active', updated: '2001-01-01T00:00:00Z' }),
dmuxWorker('dead-pane', { state: 'running' }, { pane: { dead: true, active: false } }),
dmuxWorker('mystery', { state: 'queued' }, {
task: { seedPaths: 'not-array' },
handoff: { summary: 'not-array', validation: null, remainingRisks: undefined },
pane: null
})
]
}), sourceTarget);
assert.strictEqual(idle.session.state, 'idle');
assert.deepStrictEqual(
idle.workers.map(worker => worker.health),
['stale', 'stale', 'degraded', 'unknown']
);
assert.deepStrictEqual(idle.workers[3].intent.seedPaths, []);
assert.deepStrictEqual(idle.workers[3].outputs.summary, []);
const completed = normalizeDmuxSnapshot(dmuxSnapshot({
workerStates: null,
workerCount: 2,
workers: [
dmuxWorker('done-a', { state: 'done' }),
dmuxWorker('done-b', { state: 'success' })
]
}), sourceTarget);
assert.strictEqual(completed.session.state, 'completed');
assert.deepStrictEqual(completed.workers.map(worker => worker.health), ['healthy', 'healthy']);
});
test('claude history normalization falls back to filename ids and empty metadata defaults', () => {
const snapshot = normalizeClaudeHistorySession({
shortId: 'no-id',
filename: '2026-03-13-no-id-session.tmp',
sessionPath: '/tmp/2026-03-13-no-id-session.tmp',
metadata: {
title: '',
completed: 'not-array',
inProgress: ['Resume from filename fallback'],
context: '',
notes: ''
}
}, {
type: 'claude-history',
value: 'latest'
});
assert.strictEqual(snapshot.session.id, '2026-03-13-no-id-session');
assert.strictEqual(snapshot.workers[0].id, '2026-03-13-no-id-session');
assert.strictEqual(snapshot.workers[0].label, '2026-03-13-no-id-session.tmp');
assert.strictEqual(snapshot.workers[0].intent.objective, 'Resume from filename fallback');
assert.deepStrictEqual(snapshot.workers[0].intent.seedPaths, []);
assert.deepStrictEqual(snapshot.workers[0].outputs.summary, []);
assert.deepStrictEqual(snapshot.workers[0].outputs.remainingRisks, []);
const pathOnly = normalizeClaudeHistorySession({
sessionPath: '/tmp/path-only-session.tmp',
metadata: {
title: 'Path Only',
inProgress: ['Continue work'],
context: ' README.md \n\n scripts/ecc.js ',
notes: 'No risks'
}
}, {
type: 'claude-history',
value: '/tmp/path-only-session.tmp'
});
assert.strictEqual(pathOnly.session.id, 'path-only-session');
assert.strictEqual(pathOnly.workers[0].intent.objective, 'Continue work');
assert.deepStrictEqual(pathOnly.workers[0].intent.seedPaths, ['README.md', 'scripts/ecc.js']);
assert.deepStrictEqual(pathOnly.workers[0].outputs.remainingRisks, ['No risks']);
});
test('fallback recordings sanitize paths, use env dirs, and preserve changed history', () => {
const recordingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-recordings-env-'));
const previousRecordingDir = process.env.ECC_SESSION_RECORDING_DIR;
try {
process.env.ECC_SESSION_RECORDING_DIR = recordingDir;
const first = canonicalSnapshot({
adapterId: 'adapter with spaces',
session: { id: 'session id/with:chars' }
});
const recordingPath = getFallbackSessionRecordingPath(first);
assert.ok(recordingPath.includes(`${path.sep}adapter_with_spaces${path.sep}`));
assert.ok(recordingPath.endsWith(`${path.sep}session_id_with_chars.json`));
fs.mkdirSync(path.dirname(recordingPath), { recursive: true });
fs.writeFileSync(recordingPath, '{not json', 'utf8');
const firstPersistence = persistCanonicalSnapshot(first, {
loadStateStoreImpl: () => null
});
const changed = canonicalSnapshot({
adapterId: 'adapter with spaces',
session: { id: 'session id/with:chars', state: 'idle' }
});
persistCanonicalSnapshot(changed, { loadStateStoreImpl: () => null });
persistCanonicalSnapshot(changed, { loadStateStoreImpl: () => null });
const persisted = JSON.parse(fs.readFileSync(recordingPath, 'utf8'));
assert.strictEqual(firstPersistence.backend, 'json-file');
assert.strictEqual(firstPersistence.path, recordingPath);
assert.strictEqual(persisted.schemaVersion, 'ecc.session.recording.v1');
assert.strictEqual(persisted.latest.session.state, 'idle');
assert.strictEqual(persisted.history.length, 2);
assert.strictEqual(persisted.history[0].snapshot.session.state, 'active');
assert.strictEqual(persisted.history[1].snapshot.session.state, 'idle');
assert.strictEqual(persisted.createdAt, persisted.history[0].recordedAt);
} finally {
if (typeof previousRecordingDir === 'string') {
process.env.ECC_SESSION_RECORDING_DIR = previousRecordingDir;
} else {
delete process.env.ECC_SESSION_RECORDING_DIR;
}
fs.rmSync(recordingDir, { recursive: true, force: true });
}
});
test('persistence supports skip mode, writer variants, and missing state-store fallback', () => {
const snapshot = canonicalSnapshot();
const skipped = persistCanonicalSnapshot(snapshot, { persist: false });
assert.deepStrictEqual(skipped, {
backend: 'skipped',
path: null,
recordedAt: null
});
const topLevelStore = {
calls: [],
recordCanonicalSessionSnapshot(snapshotArg, metadata) {
this.calls.push({ snapshot: snapshotArg, metadata });
}
};
const stateStoreResult = persistCanonicalSnapshot(snapshot, { stateStore: topLevelStore });
assert.strictEqual(stateStoreResult.backend, 'state-store');
assert.strictEqual(topLevelStore.calls.length, 1);
assert.strictEqual(topLevelStore.calls[0].metadata.sessionId, 'session-1');
const nestedStore = {
sessions: {
calls: [],
recordSessionSnapshot(snapshotArg, metadata) {
this.calls.push({ snapshot: snapshotArg, metadata });
}
}
};
persistCanonicalSnapshot(snapshot, { stateStore: nestedStore });
assert.strictEqual(nestedStore.sessions.calls.length, 1);
const noWriterDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-no-writer-'));
const missingModuleDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-session-missing-module-'));
try {
const noWriter = persistCanonicalSnapshot(snapshot, {
recordingDir: noWriterDir,
stateStore: { createStateStore() {} }
});
assert.strictEqual(noWriter.backend, 'json-file');
const missingModule = new Error("Cannot find module '../state-store'");
missingModule.code = 'MODULE_NOT_FOUND';
const fallback = persistCanonicalSnapshot(snapshot, {
recordingDir: missingModuleDir,
loadStateStoreImpl() {
throw missingModule;
}
});
assert.strictEqual(fallback.backend, 'json-file');
} finally {
fs.rmSync(noWriterDir, { recursive: true, force: true });
fs.rmSync(missingModuleDir, { recursive: true, force: true });
}
});
test('persistence only falls back when the state-store module is missing', () => {
const snapshot = {
schemaVersion: 'ecc.session.v1',

View File

@@ -19,7 +19,6 @@ const fs = require('fs');
const path = require('path');
const repoRoot = path.resolve(__dirname, '..');
const repoRootWithSep = `${repoRoot}${path.sep}`;
const packageJsonPath = path.join(repoRoot, 'package.json');
const packageLockPath = path.join(repoRoot, 'package-lock.json');
const rootAgentsPath = path.join(repoRoot, 'AGENTS.md');
@@ -70,16 +69,6 @@ function loadJsonObject(filePath, label) {
return parsed;
}
function assertSafeRepoRelativePath(relativePath, label) {
const normalized = path.posix.normalize(relativePath.replace(/\\/g, '/'));
assert.ok(!path.isAbsolute(relativePath), `${label} must not be absolute: ${relativePath}`);
assert.ok(
!normalized.startsWith('../') && !normalized.includes('/../'),
`${label} must not traverse directories: ${relativePath}`,
);
}
function collectMarkdownFiles(rootPath) {
if (!fs.existsSync(rootPath)) {
return [];