chore: import upstream snapshot with attribution
CI / Test (ubuntu-latest, Node 18.x, bun) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, npm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, pnpm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, yarn) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 20.x, bun) (push) Failing after 17m13s
CI / Test (ubuntu-latest, Node 20.x, npm) (push) Failing after 18m42s
CI / Test (ubuntu-latest, Node 20.x, pnpm) (push) Failing after 15m0s
CI / Test (ubuntu-latest, Node 20.x, yarn) (push) Failing after 49m44s
CI / Test (ubuntu-latest, Node 22.x, bun) (push) Failing after 51m55s
CI / Test (ubuntu-latest, Node 22.x, pnpm) (push) Failing after 21m57s
CI / Test (ubuntu-latest, Node 22.x, npm) (push) Failing after 37m39s
CI / Test (ubuntu-latest, Node 22.x, yarn) (push) Failing after 34m7s
CI / Validate Components (push) Failing after 37m15s
CI / Python Tests (push) Failing after 10m1s
CI / Security Scan (push) Failing after 10m1s
CI / Lint (push) Failing after 17m12s
CI / Coverage (push) Failing after 20m19s
CI / Test (macos-latest, Node 18.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, yarn) (push) Has been cancelled
CI / Test (ubuntu-latest, Node 18.x, bun) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, npm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, pnpm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, yarn) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 20.x, bun) (push) Failing after 17m13s
CI / Test (ubuntu-latest, Node 20.x, npm) (push) Failing after 18m42s
CI / Test (ubuntu-latest, Node 20.x, pnpm) (push) Failing after 15m0s
CI / Test (ubuntu-latest, Node 20.x, yarn) (push) Failing after 49m44s
CI / Test (ubuntu-latest, Node 22.x, bun) (push) Failing after 51m55s
CI / Test (ubuntu-latest, Node 22.x, pnpm) (push) Failing after 21m57s
CI / Test (ubuntu-latest, Node 22.x, npm) (push) Failing after 37m39s
CI / Test (ubuntu-latest, Node 22.x, yarn) (push) Failing after 34m7s
CI / Validate Components (push) Failing after 37m15s
CI / Python Tests (push) Failing after 10m1s
CI / Security Scan (push) Failing after 10m1s
CI / Lint (push) Failing after 17m12s
CI / Coverage (push) Failing after 20m19s
CI / Test (macos-latest, Node 18.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, yarn) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const {
|
||||
createOpencodeAdapter,
|
||||
parseOpencodeTarget,
|
||||
parseOpencodeSession,
|
||||
isOpencodeSessionFileTarget,
|
||||
findLatestSessionInfo,
|
||||
findSessionInfoById
|
||||
} = require('../../scripts/lib/session-adapters/opencode');
|
||||
const {
|
||||
normalizeOpencodeSession,
|
||||
validateCanonicalSnapshot
|
||||
} = require('../../scripts/lib/session-adapters/canonical-session');
|
||||
const { createAdapterRegistry } = require('../../scripts/lib/session-adapters/registry');
|
||||
|
||||
console.log('=== Testing opencode session adapter ===\n');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
passed += 1;
|
||||
console.log(` ok - ${name}`);
|
||||
} catch (error) {
|
||||
failed += 1;
|
||||
console.log(` FAIL - ${name}`);
|
||||
console.log(` ${error && error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function writeOpencodeFixture({ title = 'rebuild the basket trader rebalancer', updatedAgoMs = 0 } = {}) {
|
||||
const storageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-opencode-store-'));
|
||||
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-opencode-repo-'));
|
||||
const projectHash = 'b43c6d2f5bbf6e71bc3d139c1656bf3afe1935aa';
|
||||
const sessionId = 'ses_66d5468bdffeVlx1Hy2KkdIshB';
|
||||
|
||||
const sessionDir = path.join(storageDir, 'session', projectHash);
|
||||
const messageDir = path.join(storageDir, 'message', sessionId);
|
||||
fs.mkdirSync(sessionDir, { recursive: true });
|
||||
fs.mkdirSync(messageDir, { recursive: true });
|
||||
|
||||
const updated = Date.now() - updatedAgoMs;
|
||||
fs.writeFileSync(path.join(sessionDir, `${sessionId}.json`), JSON.stringify({
|
||||
id: sessionId,
|
||||
version: '0.12.1',
|
||||
projectID: projectHash,
|
||||
directory: repoRoot,
|
||||
title,
|
||||
time: { created: updated - 10000, updated }
|
||||
}), 'utf8');
|
||||
|
||||
// one user message + one assistant message carrying the model
|
||||
fs.writeFileSync(path.join(messageDir, 'msg_user01.json'), JSON.stringify({
|
||||
id: 'msg_user01', sessionID: sessionId, role: 'user', time: { created: updated - 9000 }
|
||||
}), 'utf8');
|
||||
fs.writeFileSync(path.join(messageDir, 'msg_asst01.json'), JSON.stringify({
|
||||
id: 'msg_asst01', sessionID: sessionId, role: 'assistant',
|
||||
time: { created: updated - 8000, completed: updated - 7000 },
|
||||
modelID: 'claude-sonnet-4-5-20250929', providerID: 'anthropic'
|
||||
}), 'utf8');
|
||||
|
||||
return { storageDir, repoRoot, sessionId, sessionInfoPath: path.join(sessionDir, `${sessionId}.json`) };
|
||||
}
|
||||
|
||||
test('normalizeOpencodeSession produces a valid ecc.session.v1 snapshot', () => {
|
||||
const snapshot = normalizeOpencodeSession({
|
||||
sessionId: 'ses_x', sessionPath: '/tmp/s.json', cwd: '/repo', branch: 'main',
|
||||
objective: 'do the thing', title: 'do the thing', model: 'claude-sonnet-4-5-20250929',
|
||||
provider: 'anthropic', version: '0.12.1', projectId: 'proj', createdAt: '2026-01-01T00:00:00Z',
|
||||
updatedAt: '2026-01-01T00:05:00Z', messageCount: 2, active: false
|
||||
}, { type: 'opencode', value: 'ses_x' });
|
||||
|
||||
validateCanonicalSnapshot(snapshot);
|
||||
assert.strictEqual(snapshot.adapterId, 'opencode');
|
||||
assert.strictEqual(snapshot.session.kind, 'opencode');
|
||||
assert.strictEqual(snapshot.workers[0].runtime.kind, 'opencode-session');
|
||||
assert.strictEqual(snapshot.workers[0].artifacts.provider, 'anthropic');
|
||||
});
|
||||
|
||||
test('parseOpencodeTarget strips the opencode prefix', () => {
|
||||
assert.strictEqual(parseOpencodeTarget('opencode:latest'), 'latest');
|
||||
assert.strictEqual(parseOpencodeTarget('opencode:ses_abc'), 'ses_abc');
|
||||
assert.strictEqual(parseOpencodeTarget('codex:latest'), null);
|
||||
});
|
||||
|
||||
test('adapter reads latest session, extracts model from messages, derives objective from title', () => {
|
||||
const { storageDir, repoRoot, sessionInfoPath } = writeOpencodeFixture({ updatedAgoMs: 0 });
|
||||
const recordingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-opencode-rec-'));
|
||||
|
||||
assert.strictEqual(findLatestSessionInfo(storageDir), sessionInfoPath);
|
||||
|
||||
const adapter = createOpencodeAdapter({
|
||||
storageDir, recordingDir, loadStateStoreImpl: () => null, resolveBranchImpl: () => 'main'
|
||||
});
|
||||
const snapshot = adapter.open('opencode:latest', { cwd: repoRoot }).getSnapshot();
|
||||
|
||||
assert.strictEqual(snapshot.adapterId, 'opencode');
|
||||
assert.strictEqual(snapshot.session.state, 'active');
|
||||
assert.strictEqual(snapshot.workers[0].worktree, repoRoot);
|
||||
assert.strictEqual(snapshot.workers[0].branch, 'main');
|
||||
assert.strictEqual(snapshot.workers[0].artifacts.model, 'claude-sonnet-4-5-20250929');
|
||||
assert.strictEqual(snapshot.workers[0].artifacts.provider, 'anthropic');
|
||||
assert.strictEqual(snapshot.workers[0].artifacts.messageCount, 2);
|
||||
assert.strictEqual(snapshot.workers[0].intent.objective, 'rebuild the basket trader rebalancer');
|
||||
});
|
||||
|
||||
test('auto-title "New session - ..." yields empty objective; stale session is recorded', () => {
|
||||
const { storageDir, repoRoot } = writeOpencodeFixture({
|
||||
title: 'New session - 2025-09-28T23:32:22.978Z',
|
||||
updatedAgoMs: 60 * 60 * 1000
|
||||
});
|
||||
const recordingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-opencode-rec2-'));
|
||||
|
||||
const adapter = createOpencodeAdapter({
|
||||
storageDir, recordingDir, loadStateStoreImpl: () => null, resolveBranchImpl: () => null
|
||||
});
|
||||
const snapshot = adapter.open('opencode:latest', { cwd: repoRoot }).getSnapshot();
|
||||
|
||||
assert.strictEqual(snapshot.workers[0].intent.objective, '');
|
||||
assert.strictEqual(snapshot.session.state, 'recorded');
|
||||
assert.strictEqual(snapshot.workers[0].runtime.dead, true);
|
||||
});
|
||||
|
||||
test('registry routes structured opencode target and lists the adapter', () => {
|
||||
const { storageDir, repoRoot } = writeOpencodeFixture();
|
||||
const recordingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-opencode-reg-'));
|
||||
|
||||
const registry = createAdapterRegistry({
|
||||
recordingDir,
|
||||
loadStateStoreImpl: () => null,
|
||||
adapterOptions: { opencode: { storageDir, resolveBranchImpl: () => null } }
|
||||
});
|
||||
|
||||
const typed = registry.open({ type: 'opencode', value: 'latest' }, { cwd: repoRoot }).getSnapshot();
|
||||
assert.strictEqual(typed.adapterId, 'opencode');
|
||||
|
||||
const listed = registry.listAdapters().map(a => a.id);
|
||||
assert.ok(listed.includes('opencode'), 'registry lists opencode 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);
|
||||
Reference in New Issue
Block a user