d48cda4081
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
228 lines
8.5 KiB
JavaScript
228 lines
8.5 KiB
JavaScript
/**
|
|
* Tests for scripts/lib/plan-canvas/sessions.js
|
|
*
|
|
* Run with: node tests/lib/plan-canvas-sessions.test.js
|
|
*/
|
|
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
const {
|
|
canonicalizeArtifactPath,
|
|
createSessionStore,
|
|
normalizeFeedbackItem,
|
|
sessionKeyFor
|
|
} = require('../../scripts/lib/plan-canvas/sessions');
|
|
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(` ✓ ${name}`);
|
|
return true;
|
|
} catch (err) {
|
|
console.log(` ✗ ${name}`);
|
|
console.log(` Error: ${err.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function makeFixture() {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'plan-canvas-test-'));
|
|
const artifact = path.join(dir, 'demo.plan.md');
|
|
fs.writeFileSync(artifact, '# Plan\n');
|
|
const store = createSessionStore({ stateDir: path.join(dir, 'state') });
|
|
return { dir, artifact, store };
|
|
}
|
|
|
|
function runTests() {
|
|
console.log('\n=== Testing plan-canvas sessions.js ===\n');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
const fixtures = [];
|
|
|
|
console.log('Keys and normalization:');
|
|
|
|
if (test('sessionKeyFor is a stable 12-char hex key', () => {
|
|
const key = sessionKeyFor('/tmp/x.md');
|
|
assert.match(key, /^[a-f0-9]{12}$/);
|
|
assert.strictEqual(key, sessionKeyFor('/tmp/x.md'));
|
|
assert.notStrictEqual(key, sessionKeyFor('/tmp/y.md'));
|
|
})) passed++; else failed++;
|
|
|
|
if (test('canonicalizeArtifactPath resolves relative paths', () => {
|
|
const abs = canonicalizeArtifactPath('some-file.md');
|
|
assert.ok(path.isAbsolute(abs));
|
|
})) passed++; else failed++;
|
|
|
|
if (test('normalizeFeedbackItem accepts chat, annotation, verdict', () => {
|
|
assert.strictEqual(normalizeFeedbackItem({ kind: 'chat', text: 'hi' }, 1).kind, 'chat');
|
|
const ann = normalizeFeedbackItem(
|
|
{ kind: 'annotation', text: 'fix', anchor: { selector: 'h2', tag: 'h2', snippet: 'Phase 2' } },
|
|
2
|
|
);
|
|
assert.strictEqual(ann.anchor.selector, 'h2');
|
|
const verdict = normalizeFeedbackItem({ kind: 'verdict', verdict: 'approve' }, 3);
|
|
assert.strictEqual(verdict.verdict, 'approve');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('normalizeFeedbackItem rejects malformed input', () => {
|
|
assert.strictEqual(normalizeFeedbackItem(null, 1), null);
|
|
assert.strictEqual(normalizeFeedbackItem({ kind: 'nope', text: 'x' }, 1), null);
|
|
assert.strictEqual(normalizeFeedbackItem({ kind: 'chat', text: '' }, 1), null);
|
|
assert.strictEqual(normalizeFeedbackItem({ kind: 'verdict', verdict: 'maybe' }, 1), null);
|
|
assert.strictEqual(normalizeFeedbackItem({ kind: 'annotation', text: 'x' }, 1), null);
|
|
assert.strictEqual(normalizeFeedbackItem({ kind: 'annotation', text: '', anchor: { selector: 'p' } }, 1), null);
|
|
})) passed++; else failed++;
|
|
|
|
console.log('\nOpen / reopen semantics:');
|
|
|
|
if (test('open creates a session keyed by canonical path', () => {
|
|
const fx = makeFixture();
|
|
fixtures.push(fx);
|
|
const { session, refused } = fx.store.open(fx.artifact);
|
|
assert.strictEqual(refused, false);
|
|
assert.strictEqual(session.status, 'open');
|
|
assert.strictEqual(session.file, canonicalizeArtifactPath(fx.artifact));
|
|
assert.strictEqual(fx.store.findByFile(fx.artifact).key, session.key);
|
|
})) passed++; else failed++;
|
|
|
|
if (test('user-ended sessions refuse a plain reopen but allow --reopen', () => {
|
|
const fx = makeFixture();
|
|
fixtures.push(fx);
|
|
const { session } = fx.store.open(fx.artifact);
|
|
fx.store.end(session.key, 'user');
|
|
assert.strictEqual(fx.store.open(fx.artifact).refused, true);
|
|
const forced = fx.store.open(fx.artifact, { reopen: true });
|
|
assert.strictEqual(forced.refused, false);
|
|
assert.strictEqual(forced.session.status, 'open');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('agent-ended sessions reopen without a flag', () => {
|
|
const fx = makeFixture();
|
|
fixtures.push(fx);
|
|
const { session } = fx.store.open(fx.artifact);
|
|
fx.store.end(session.key, 'agent');
|
|
assert.strictEqual(fx.store.open(fx.artifact).refused, false);
|
|
})) passed++; else failed++;
|
|
|
|
console.log('\nFeedback queue / deliver-and-drain:');
|
|
|
|
if (test('queueFeedback filters bad items and mirrors chat', () => {
|
|
const fx = makeFixture();
|
|
fixtures.push(fx);
|
|
const { session } = fx.store.open(fx.artifact);
|
|
const result = fx.store.queueFeedback(session.key, [
|
|
{ kind: 'chat', text: 'hello agent' },
|
|
{ kind: 'bogus' },
|
|
{ kind: 'verdict', verdict: 'approve' }
|
|
]);
|
|
assert.strictEqual(result.accepted.length, 2);
|
|
assert.strictEqual(result.pending, 2);
|
|
const chat = fx.store.get(session.key).chat;
|
|
assert.strictEqual(chat.length, 2);
|
|
assert.strictEqual(chat[0].role, 'user');
|
|
assert.ok(chat[1].text.includes('Approved the plan'));
|
|
})) passed++; else failed++;
|
|
|
|
if (test('takeFeedback drains once, then returns waiting', () => {
|
|
const fx = makeFixture();
|
|
fixtures.push(fx);
|
|
const { session } = fx.store.open(fx.artifact);
|
|
fx.store.queueFeedback(session.key, [{ kind: 'chat', text: 'one' }]);
|
|
const first = fx.store.takeFeedback(session.key);
|
|
assert.strictEqual(first.status, 'feedback');
|
|
assert.strictEqual(first.items.length, 1);
|
|
assert.strictEqual(fx.store.takeFeedback(session.key).status, 'waiting');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('takeFeedback reports missing for unknown sessions', () => {
|
|
const fx = makeFixture();
|
|
fixtures.push(fx);
|
|
assert.strictEqual(fx.store.takeFeedback('deadbeef0000').status, 'missing');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('send-and-end delivers final batch with attribution', () => {
|
|
const fx = makeFixture();
|
|
fixtures.push(fx);
|
|
const { session } = fx.store.open(fx.artifact);
|
|
fx.store.queueFeedback(session.key, [{ kind: 'chat', text: 'last words' }], { endSession: true });
|
|
const result = fx.store.takeFeedback(session.key);
|
|
assert.strictEqual(result.status, 'feedback');
|
|
assert.strictEqual(result.sessionEnded, true);
|
|
assert.strictEqual(result.endedBy, 'user');
|
|
const after = fx.store.takeFeedback(session.key);
|
|
assert.strictEqual(after.status, 'ended');
|
|
assert.strictEqual(after.endedBy, 'user');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('queueFeedback on an ended session is refused', () => {
|
|
const fx = makeFixture();
|
|
fixtures.push(fx);
|
|
const { session } = fx.store.open(fx.artifact);
|
|
fx.store.end(session.key, 'agent');
|
|
assert.strictEqual(fx.store.queueFeedback(session.key, [{ kind: 'chat', text: 'late' }]), null);
|
|
})) passed++; else failed++;
|
|
|
|
console.log('\nPersistence:');
|
|
|
|
if (test('queued feedback survives a store reload (server restart)', () => {
|
|
const fx = makeFixture();
|
|
fixtures.push(fx);
|
|
const { session } = fx.store.open(fx.artifact);
|
|
fx.store.queueFeedback(session.key, [{ kind: 'chat', text: 'persist me' }]);
|
|
const reloaded = createSessionStore({ stateDir: fx.store.stateDir });
|
|
const result = reloaded.takeFeedback(session.key);
|
|
assert.strictEqual(result.status, 'feedback');
|
|
assert.strictEqual(result.items[0].text, 'persist me');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('corrupt state file starts fresh instead of crashing', () => {
|
|
const fx = makeFixture();
|
|
fixtures.push(fx);
|
|
fs.mkdirSync(fx.store.stateDir, { recursive: true });
|
|
fs.writeFileSync(fx.store.stateFile, '{not json');
|
|
const reloaded = createSessionStore({ stateDir: fx.store.stateDir });
|
|
assert.deepStrictEqual(reloaded.list(), []);
|
|
})) passed++; else failed++;
|
|
|
|
if (test('addAgentReply appends to the transcript', () => {
|
|
const fx = makeFixture();
|
|
fixtures.push(fx);
|
|
const { session } = fx.store.open(fx.artifact);
|
|
fx.store.addAgentReply(session.key, 'done, take a look');
|
|
const chat = fx.store.get(session.key).chat;
|
|
assert.strictEqual(chat[chat.length - 1].role, 'agent');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('list and hasOpenSessions reflect state', () => {
|
|
const fx = makeFixture();
|
|
fixtures.push(fx);
|
|
assert.strictEqual(fx.store.hasOpenSessions(), false);
|
|
const { session } = fx.store.open(fx.artifact);
|
|
assert.strictEqual(fx.store.hasOpenSessions(), true);
|
|
assert.strictEqual(fx.store.list().length, 1);
|
|
fx.store.end(session.key, 'user');
|
|
assert.strictEqual(fx.store.hasOpenSessions(), false);
|
|
})) passed++; else failed++;
|
|
|
|
for (const fx of fixtures) {
|
|
try {
|
|
fs.rmSync(fx.dir, { recursive: true, force: true });
|
|
} catch {
|
|
// best-effort cleanup
|
|
}
|
|
}
|
|
|
|
console.log('\n' + '='.repeat(40));
|
|
console.log(`Passed: ${passed}`);
|
|
console.log(`Failed: ${failed}`);
|
|
console.log('='.repeat(40));
|
|
|
|
process.exit(failed > 0 ? 1 : 0);
|
|
}
|
|
|
|
runTests();
|