Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 11:55:55 +08:00

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();