From 59ee1042c592fbd16c7cac4686b07b419d402b35 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Fri, 5 Jun 2026 15:54:48 -0400 Subject: [PATCH] test: cover error/fallback branches for codex-worktree + opencode adapters Lift global branch coverage past the 80% gate (was 79.53%). Adds error and fallback path tests: missing-session/unknown-id throws, findRolloutById/ findSessionInfoById, direct file targets, objective truncation, model fallbacks, corrupt-line skip, mtime activity fallback, and the real resolveGitBranch path outside a repo. codex-worktree.js branch 52.8%->78.3%; global branch 80.04%. --- tests/lib/session-adapters-codex.test.js | 75 ++++++++++++++++++++- tests/lib/session-adapters-opencode.test.js | 73 +++++++++++++++++++- 2 files changed, 146 insertions(+), 2 deletions(-) diff --git a/tests/lib/session-adapters-codex.test.js b/tests/lib/session-adapters-codex.test.js index e5202ddf..584af325 100644 --- a/tests/lib/session-adapters-codex.test.js +++ b/tests/lib/session-adapters-codex.test.js @@ -8,7 +8,10 @@ const path = require('path'); const { createCodexWorktreeAdapter, parseCodexTarget, - findLatestRollout + parseCodexRollout, + isCodexRolloutFileTarget, + findLatestRollout, + findRolloutById } = require('../../scripts/lib/session-adapters/codex-worktree'); const { normalizeCodexWorktreeSession, @@ -131,5 +134,75 @@ test('registry routes structured codex-worktree target and direct rollout path', assert.ok(listed.includes('codex-worktree'), 'registry lists codex-worktree adapter'); }); + +// --- branch/error coverage --- + +function writeRollout(dir, name, lines) { + const fp = require('path').join(dir, name); + require('fs').writeFileSync(fp, lines.map(l => JSON.stringify(l)).join('\n') + '\n', 'utf8'); + return fp; +} + +test('parseCodexTarget handles non-string and unprefixed input', () => { + assert.strictEqual(parseCodexTarget(null), null); + assert.strictEqual(parseCodexTarget(42), null); + assert.strictEqual(parseCodexTarget('/abs/path.jsonl'), null); +}); + +test('adapter throws clear errors for missing sessions and unknown ids', () => { + const sessionsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-codex-empty-')); + const adapter = createCodexWorktreeAdapter({ sessionsDir, loadStateStoreImpl: () => null }); + assert.throws(() => adapter.open('codex:latest', { cwd: os.tmpdir() }).getSnapshot(), /No Codex rollout sessions found/); + assert.throws(() => adapter.open('codex:nope-not-real', { cwd: os.tmpdir() }).getSnapshot(), /not found/); + assert.throws(() => adapter.open('/not/a/rollout.txt', { cwd: os.tmpdir() }).getSnapshot(), /Unsupported Codex session target/); +}); + +test('findRolloutById + direct file target + isCodexRolloutFileTarget', () => { + const sessionsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-codex-byid-')); + const day = path.join(sessionsDir, '2026', '06', '02'); + fs.mkdirSync(day, { recursive: true }); + const now = new Date().toISOString(); + const fp = writeRollout(day, 'rollout-2026-06-02T03-00-00-019eUNIQUEID0001.jsonl', [ + { type: 'session_meta', timestamp: now, payload: { id: '019eUNIQUEID0001', cwd: sessionsDir } } + ]); + + assert.strictEqual(findRolloutById(sessionsDir, '019eUNIQUEID0001'), fp); + assert.ok(isCodexRolloutFileTarget(fp, os.tmpdir())); + assert.ok(!isCodexRolloutFileTarget('not-a-file.jsonl', os.tmpdir())); + + const adapter = createCodexWorktreeAdapter({ sessionsDir, loadStateStoreImpl: () => null, resolveBranchImpl: () => null }); + const byId = adapter.open('codex:019eUNIQUEID0001', { cwd: os.tmpdir() }).getSnapshot(); + assert.strictEqual(byId.session.id, '019eUNIQUEID0001'); + const byFile = adapter.open(fp, { cwd: os.tmpdir() }).getSnapshot(); + assert.strictEqual(byFile.session.id, '019eUNIQUEID0001'); +}); + +test('parseCodexRollout: model fallbacks, objective truncation, corrupt-line skip, mtime fallback', () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-codex-parse-')); + const longObjective = 'x'.repeat(400); + const fp = path.join(dir, 'rollout-2026-06-02T03-00-00-019eMODELFALL0002.jsonl'); + // include a corrupt line, no turn_context (force meta.model_provider fallback), no timestamps (force mtime) + fs.writeFileSync(fp, [ + JSON.stringify({ type: 'session_meta', payload: { id: '019eMODELFALL0002', cwd: dir, model_provider: 'openai' } }), + '{ this is corrupt json', + JSON.stringify({ type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'text', text: longObjective }] } }) + ].join('\n') + '\n', 'utf8'); + + const parsed = parseCodexRollout(fp, { resolveBranchImpl: () => null }); + assert.strictEqual(parsed.model, 'openai', 'falls back to model_provider when no turn_context/model'); + assert.ok(parsed.objective.endsWith('...'), 'long objective is truncated'); + assert.ok(parsed.objective.length <= 280); + assert.strictEqual(parsed.active, true, 'no record timestamps => falls back to (recent) file mtime'); +}); + +test('resolveGitBranch returns null when cwd is not a git repo (real path)', () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-codex-nogit-')); + const fp = path.join(dir, 'rollout-2026-06-02T03-00-00-019eNOGIT00003.jsonl'); + fs.writeFileSync(fp, JSON.stringify({ type: 'session_meta', payload: { id: '019eNOGIT00003', cwd: dir } }) + '\n', 'utf8'); + // no resolveBranchImpl => exercises the real execFileSync + catch path + const parsed = parseCodexRollout(fp, {}); + assert.strictEqual(parsed.branch, null); +}); + console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`); if (failed > 0) process.exit(1); diff --git a/tests/lib/session-adapters-opencode.test.js b/tests/lib/session-adapters-opencode.test.js index f578eef1..edd9bc05 100644 --- a/tests/lib/session-adapters-opencode.test.js +++ b/tests/lib/session-adapters-opencode.test.js @@ -8,7 +8,10 @@ const path = require('path'); const { createOpencodeAdapter, parseOpencodeTarget, - findLatestSessionInfo + parseOpencodeSession, + isOpencodeSessionFileTarget, + findLatestSessionInfo, + findSessionInfoById } = require('../../scripts/lib/session-adapters/opencode'); const { normalizeOpencodeSession, @@ -144,5 +147,73 @@ test('registry routes structured opencode target and lists the adapter', () => { assert.ok(listed.includes('codex-worktree'), 'registry still lists codex-worktree adapter'); }); + +// --- branch/error coverage --- + +function writeSession(storageDir, projectHash, sessionId, info, messages) { + const sdir = path.join(storageDir, 'session', projectHash); + const mdir = path.join(storageDir, 'message', sessionId); + fs.mkdirSync(sdir, { recursive: true }); + fs.mkdirSync(mdir, { recursive: true }); + fs.writeFileSync(path.join(sdir, sessionId + '.json'), JSON.stringify(info), 'utf8'); + (messages || []).forEach((m, i) => fs.writeFileSync(path.join(mdir, 'msg_' + i + '.json'), JSON.stringify(m), 'utf8')); + return path.join(sdir, sessionId + '.json'); +} + +test('parseOpencodeTarget handles non-string and unprefixed input', () => { + assert.strictEqual(parseOpencodeTarget(null), null); + assert.strictEqual(parseOpencodeTarget('/abs/ses_x.json'), null); +}); + +test('adapter throws for empty store and unknown id; findLatest on empty => null', () => { + const storageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-oc-empty-')); + assert.strictEqual(findLatestSessionInfo(storageDir), null); + const adapter = createOpencodeAdapter({ storageDir, loadStateStoreImpl: () => null }); + assert.throws(() => adapter.open('opencode:latest', { cwd: os.tmpdir() }).getSnapshot(), /No OpenCode sessions found/); + assert.throws(() => adapter.open('opencode:ses_missing', { cwd: os.tmpdir() }).getSnapshot(), /not found/); +}); + +test('findSessionInfoById + direct file target + isOpencodeSessionFileTarget', () => { + const storageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-oc-byid-')); + const now = Date.now(); + const fp = writeSession(storageDir, 'projhash', 'ses_UNIQUE001', { + id: 'ses_UNIQUE001', directory: storageDir, title: 'real title', time: { created: now - 5000, updated: now - 5000 } + }, []); + + assert.strictEqual(findSessionInfoById(storageDir, 'ses_UNIQUE001'), fp); + assert.ok(isOpencodeSessionFileTarget(fp, os.tmpdir())); + assert.ok(!isOpencodeSessionFileTarget('/tmp/not-session.json', os.tmpdir())); + + const adapter = createOpencodeAdapter({ storageDir, loadStateStoreImpl: () => null, resolveBranchImpl: () => null }); + const byId = adapter.open('opencode:ses_UNIQUE001', { cwd: os.tmpdir() }).getSnapshot(); + assert.strictEqual(byId.session.id, 'ses_UNIQUE001'); + const byFile = adapter.open(fp, { cwd: os.tmpdir() }).getSnapshot(); + assert.strictEqual(byFile.session.id, 'ses_UNIQUE001'); +}); + +test('parseOpencodeSession: model from later assistant message, missing-time => recorded', () => { + const storageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-oc-parse-')); + const fp = writeSession(storageDir, 'ph', 'ses_MODEL01', { + id: 'ses_MODEL01', directory: storageDir, title: 'do work' + // no time block => updatedMs null => recorded/inactive + }, [ + { id: 'm0', role: 'user' }, + { id: 'm1', role: 'assistant', modelID: 'claude-sonnet-4-5-20250929', providerID: 'anthropic' } + ]); + const parsed = parseOpencodeSession(fp, { storageDir, resolveBranchImpl: () => null }); + assert.strictEqual(parsed.model, 'claude-sonnet-4-5-20250929'); + assert.strictEqual(parsed.provider, 'anthropic'); + assert.strictEqual(parsed.active, false); +}); + +test('resolveGitBranch real path returns null outside a repo', () => { + const storageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-oc-nogit-')); + const fp = writeSession(storageDir, 'ph', 'ses_NOGIT01', { + id: 'ses_NOGIT01', directory: storageDir, title: 't', time: { created: 1, updated: 1 } + }, []); + const parsed = parseOpencodeSession(fp, { storageDir }); // no resolveBranchImpl => real git path + assert.strictEqual(parsed.branch, null); +}); + console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`); if (failed > 0) process.exit(1);