Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
- run: node --test tests/javascript/experience.test.cjs
- uses: actions/setup-python@v5
with:
python-version: "3.11"
Expand Down
26 changes: 22 additions & 4 deletions docs/assets/experience.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@
let activeHost = 'claude';
let activeKind = 'skill';
let phase = 0;
// A restart invalidates every delayed continuation from the previous demo.
let conversation = 0;
let plannedKind;
const approve = $('#demo-approve');
const start = $('#demo-run');
const status = $('#demo-status');
Expand Down Expand Up @@ -190,31 +193,46 @@
}));
start.addEventListener('click', async () => {
if (start.disabled) return;
const run = ++conversation;
start.disabled = true; phase = 0; approve.hidden = true; demoBody.replaceChildren();
const path = $('#demo-path').value.trim() || kinds[activeKind].path;
const kind = kinds[activeKind];
plannedKind = kind; // Preset edits configure the next conversation only.
status.textContent = 'PLANNING'; toolLine.textContent = 'Calling doctor and plan…';
addMessage('user', 'YOU', `Improve the ${kind.noun} at ${path}. Show the plan and call count before starting.`);
await pause(460);
if (run !== conversation) return;
addMessage('tool', 'AGENTDESCENT · DOCTOR', 'Checked the agent, provider configuration and run store.', 'No files changed.');
await pause(580);
if (run !== conversation) return;
addMessage('tool', 'AGENTDESCENT · PLAN', 'Sample plan: 12 cases · 4 workers · up to 48 agent calls.', 'Please review the plan and cost before starting.');
status.textContent = 'AWAITING APPROVAL'; toolLine.textContent = 'Plan ready · no run started';
approve.textContent = 'Approve sample run ↗'; approve.hidden = false; phase = 1; start.disabled = false;
});
approve.addEventListener('click', async () => {
if (phase !== 1 && phase !== 2) return;
const run = conversation;
const kind = plannedKind;
approve.hidden = true;
if (phase === 1) {
phase = 0; status.textContent = 'RUNNING'; toolLine.textContent = 'start → status → show';
addMessage('user', 'YOU', 'The plan looks good. Start the sample run.');
await pause(380); addMessage('tool', 'AGENTDESCENT · START', 'Started a background evolution run.');
await pause(550); addMessage('tool', 'AGENTDESCENT · STATUS', 'Workers proposed edits; the aggregator evaluated the candidate.');
await pause(550); addMessage('tool', 'AGENTDESCENT · SHOW', `Proposed diff: + ${kinds[activeKind].example}.`, 'The real artifact remains untouched until you approve apply.');
await pause(380);
if (run !== conversation) return;
addMessage('tool', 'AGENTDESCENT · START', 'Started a background evolution run.');
await pause(550);
if (run !== conversation) return;
addMessage('tool', 'AGENTDESCENT · STATUS', 'Workers proposed edits; the aggregator evaluated the candidate.');
await pause(550);
if (run !== conversation) return;
addMessage('tool', 'AGENTDESCENT · SHOW', `Proposed diff: + ${kind.example}.`, 'The real artifact remains untouched until you approve apply.');
status.textContent = 'DIFF READY'; toolLine.textContent = 'Diff ready · waiting for apply approval';
approve.textContent = 'Approve sample apply ↗'; approve.hidden = false; phase = 2;
} else if (phase === 2) {
phase = 0; addMessage('user', 'YOU', 'Apply the sample result.');
await pause(350); addMessage('tool', 'AGENTDESCENT · APPLY', 'Sample change applied; the original is backed up.', 'This page is only a simulation and changed no files.');
await pause(350);
if (run !== conversation) return;
addMessage('tool', 'AGENTDESCENT · APPLY', 'Sample change applied; the original is backed up.', 'This page is only a simulation and changed no files.');
status.textContent = 'COMPLETE'; toolLine.textContent = 'Sample workflow complete · no real files changed';
}
});
Expand Down
114 changes: 114 additions & 0 deletions tests/javascript/experience.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Execute the shipped script with a minimal DOM and manually advanced timers.
// This is deterministic interaction coverage, not a rendered-browser test.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const {test} = require('node:test');
const source = fs.readFileSync(process.env.EXPERIENCE_JS || 'docs/assets/experience.js', 'utf8');

function demo() {
class Element {
constructor(dataset = {}) {
this.dataset = dataset; this.children = []; this.listeners = {};
this.textContent = ''; this.value = ''; this.hidden = false; this.disabled = false;
this.classList = {toggle() {}};
}
addEventListener(name, callback) { this.listeners[name] = callback; }
click() { if (!this.disabled) this.listeners.click?.({currentTarget: this}); }
append(...children) { this.children.push(...children); }
replaceChildren() { this.children = []; }
getAttribute() { return null; }
setAttribute() {}
}
const elements = Object.fromEntries(['demo-chat-body', 'demo-approve', 'demo-run',
'demo-status', 'demo-tool-line', 'demo-path'].map(id => [id, new Element()]));
const presets = ['skill', 'prompt', 'code'].map(demoKind => new Element({demoKind}));
let now = 0;
const timers = [];
vm.runInNewContext(source, {
document: {
querySelector: selector => elements[selector.slice(1)] || null,
querySelectorAll: selector => selector === '.demo-presets button' ? presets : [],
createElement: () => new Element(),
},
setTimeout: (callback, delay) => timers.push({callback, at: now + delay}),
});
async function advance(ms) {
const end = now + ms;
while (true) {
timers.sort((a, b) => a.at - b.at);
if (!timers.length || timers[0].at > end) break;
const timer = timers.shift(); now = timer.at; timer.callback();
await Promise.resolve();
}
now = end;
await Promise.resolve();
}
return {
start: elements['demo-run'], approve: elements['demo-approve'],
status: elements['demo-status'], path: elements['demo-path'], advance,
select: kind => presets.find(button => button.dataset.demoKind === kind).click(),
messages: () => elements['demo-chat-body'].children.map(article => {
const body = article.children[1];
return {label: body.children[0].textContent, text: body.children[1].textContent};
}),
};
}

test('approval gates and repeated clicks preserve the complete sample workflow', async () => {
const d = demo();
d.approve.click(); assert.deepEqual(d.messages(), []);
d.start.click(); d.start.click(); d.approve.click();
await d.advance(1040);
assert.equal(d.messages().length, 3);
assert.equal(d.status.textContent, 'AWAITING APPROVAL');
await d.advance(5000); assert.equal(d.messages().length, 3);
d.approve.click(); d.approve.click();
await d.advance(1480);
assert.equal(d.status.textContent, 'DIFF READY');
assert.equal(d.messages().length, 7);
await d.advance(5000); assert.equal(d.messages().length, 7);
d.approve.click(); d.approve.click(); await d.advance(350);
assert.equal(d.status.textContent, 'COMPLETE');
assert.equal(d.messages().length, 9);
d.approve.click(); assert.equal(d.messages().length, 9);
});

for (const delay of [0, 380, 930, 1480, 1481]) {
test(`restart cancels stale run/apply callbacks after ${delay}ms`, async () => {
const d = demo(); d.start.click(); await d.advance(1040); d.approve.click();
await d.advance(delay);
if (delay === 1481) d.approve.click(); // Interrupt the apply animation too.
d.select('code'); d.start.click(); await d.advance(5000);
assert.equal(d.status.textContent, 'AWAITING APPROVAL');
assert.deepEqual(d.messages().map(m => m.label), ['YOU', 'AGENTDESCENT · DOCTOR', 'AGENTDESCENT · PLAN']);
assert.match(d.messages()[0].text, /agent code/);
assert.equal(d.approve.hidden, false);
d.approve.click(); await d.advance(1480);
assert.equal(d.status.textContent, 'DIFF READY');
assert.match(d.messages().at(-1).text, /Rank retrieved evidence/);
});
}

for (const initial of ['skill', 'prompt', 'code']) {
test(`preset edits cannot change the ${initial} plan or approved diff`, async () => {
const examples = {skill: 'missing appendix pages', prompt: 'cite the source', code: 'Rank retrieved evidence'};
const d = demo(); d.select(initial); d.path.value = './custom-artifact'; d.start.click();
d.select('prompt'); await d.advance(1040);
d.select('code'); d.approve.click(); await d.advance(380);
d.select('skill'); await d.advance(1100);
assert.match(d.messages()[0].text, /custom-artifact/);
assert.ok(d.messages().at(-1).text.includes(examples[initial]));
d.approve.click(); await d.advance(350);
d.select('prompt'); d.start.click(); await d.advance(1040); d.approve.click(); await d.advance(1480);
assert.ok(d.messages().at(-1).text.includes(examples.prompt));
});
}

test('restart while awaiting approval replaces the old plan', async () => {
const d = demo(); d.start.click(); await d.advance(1040);
d.select('prompt'); d.start.click(); d.approve.click(); await d.advance(1040);
assert.equal(d.messages().length, 3);
d.approve.click(); await d.advance(1480);
assert.match(d.messages().at(-1).text, /cite the source/);
});
Loading