mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
fix(story-mode): files written in Bash, the advisor, and honest checks, from review round 2
- Files a Bash call writes through here-documents (`cat > f <<'EOF'`, `tee`) are whole-file edits on the event: in the diff drawer, on the code page, in "N files changed". Before, a worker that wrote seven files that way drew a page with zero files changed. Labels leave the bodies out (the wire row was a wall of file text), and a relative target resolves against the transcript line's cwd unless the command changed directory first. - A caption names the part of a one-liner that decided it (`node --test`, the commit), not its first line. - The advisor (a server tool, answer encrypted in the transcript) is drawn: "Claude asks the advisor for a second opinion." It is not counted as a helper. Server web search maps to the web panel. - Checks that all pass from the start read "Build and test, N rounds. Every check passes.", not as fixing. - pytest's collection errors are counted apart: "The tests don't even load." - A result remark replaces a step announcement on its check. - A turn still open when the next prompt starts has ended; an interrupted turn is timed by its span. - The mission drawer keeps prompts up to 4000 characters. - Drawer: diff headers, titles and commands read relative to the session folder (the folder itself reads "."); a bare name under an `ls dir:` header links into that folder; the chosen answer is marked, not struck through; boards count errors. - While a question waits, the pencils say "Waiting for your answer…"; balloons start below the LIVE tag; the loop's balloon points at its verdict, above the board. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
831599a6cf
commit
ca035dd41b
7 files changed
+439
-64
No files matched your search
@@ -515,6 +515,70 @@ describe('story-mode.js overlay', () => {
|
||||
expect($('.story-panel[data-kind="investigate"] .story-hit')!.title).toBe('Open rename_by_date.sh');
|
||||
});
|
||||
|
||||
it('files written through a here-document open as a diff from the wire, with short paths', async () => {
|
||||
const lines = [
|
||||
prompt('write'),
|
||||
...bash('b1', "cat > app.js <<'EOF'\nconst a = 1;\nEOF\nnode --test", 'Write and test', '# pass 1\n# fail 0'),
|
||||
end(1000),
|
||||
];
|
||||
(lines[1] as { cwd?: string }).cwd = '/work/demo-shop';
|
||||
boot(storyOf(lines));
|
||||
app.openStoryMode(SID);
|
||||
await settle();
|
||||
app._storySetDirector(true);
|
||||
const link = $$('.story-wire .story-link').find((l) => l.dataset.storyOpen?.startsWith('diff:'))!;
|
||||
link.click();
|
||||
expect($('.story-drawer-kind')!.textContent).toBe('Diff');
|
||||
expect($('.story-drawer-title')!.textContent).toBe('app.js');
|
||||
expect($('.story-df-path')!.textContent).toBe('app.js');
|
||||
expect($('.story-df-path')!.title).toBe('/work/demo-shop/app.js');
|
||||
expect($$('.story-ln--add').map((l) => l.textContent)).toEqual(['1+const a = 1;']);
|
||||
});
|
||||
|
||||
it('while a question waits, the pencils say so; the chosen answer is marked, not struck', async () => {
|
||||
const ask = (answered: boolean) =>
|
||||
storyOf([
|
||||
prompt('pick'),
|
||||
tool('q1', 'AskUserQuestion', {
|
||||
questions: [{ question: 'Which name?', options: [{ label: 'slugify' }, { label: 'kebab' }] }],
|
||||
}),
|
||||
...(answered ? [result('q1', 'kebab', { toolUseResult: { answers: { 'Which name?': 'kebab' } } })] : []),
|
||||
]);
|
||||
boot(ask(false));
|
||||
app.openStoryMode(SID);
|
||||
await settle();
|
||||
expect($('.story-pencils small')!.textContent).toBe('Waiting for your answer…');
|
||||
responses = [{ status: 200, body: { success: true, data: ask(true) } }];
|
||||
await app._storyRefresh();
|
||||
await settle();
|
||||
sizeAndDraw();
|
||||
$('.story-panel[data-kind="ask"] .story-hit')!.click();
|
||||
const chosen = $('.story-drawer-body .is-chosen')!;
|
||||
expect(chosen.textContent).toBe('kebab');
|
||||
expect($('.story-drawer-body .is-done')).toBeNull();
|
||||
expect($('.story-pencils small')!.textContent).toBe('Claude is working…');
|
||||
});
|
||||
|
||||
it('a bare name under an ls header links into that folder; the session folder reads as "."', async () => {
|
||||
boot(
|
||||
storyOf([
|
||||
prompt('look'),
|
||||
...bash('b1', 'ls /work/demo-shop && ls -R /work/demo-shop/clone', 'List the clone', 'clone/src:\nkebab.py\n\nnotes.md'),
|
||||
end(1000),
|
||||
])
|
||||
);
|
||||
app.openStoryMode(SID);
|
||||
await settle();
|
||||
sizeAndDraw();
|
||||
$('.story-panel[data-kind="investigate"] .story-hit')!.click();
|
||||
expect($('.story-out-cmd')!.textContent).toBe('$ ls . && ls -R clone');
|
||||
const links = $$('.story-drawer-body .story-link');
|
||||
links.find((l) => l.textContent === 'kebab.py')!.click();
|
||||
expect(app.openFilePreview).toHaveBeenLastCalledWith('/work/demo-shop/clone/src/kebab.py', SID);
|
||||
links.find((l) => l.textContent === 'notes.md')!.click();
|
||||
expect(app.openFilePreview).toHaveBeenLastCalledWith('/work/demo-shop/notes.md', SID);
|
||||
});
|
||||
|
||||
it('turning the setting off hides the button and closes an open story', async () => {
|
||||
boot(storyOf(demoTurn()));
|
||||
app.openStoryMode(SID);
|
||||
|
||||
+108
-3
@@ -11,6 +11,7 @@ import {
|
||||
createStoryReducer,
|
||||
exactDuration,
|
||||
firstSentence,
|
||||
heredocWrites,
|
||||
parseTestCounts,
|
||||
planFromText,
|
||||
printedFiles,
|
||||
@@ -110,8 +111,8 @@ describe('parseTestCounts beyond "N passed"', () => {
|
||||
['# tests 7\n# suites 0\n# pass 7\n# fail 0', { passed: 7, failed: 0 }],
|
||||
['ℹ tests 3\nℹ pass 2\nℹ fail 1', { passed: 2, failed: 1 }],
|
||||
['FAILED test_a.py::test_x\n===== 3 failed in 0.12s =====', { passed: 0, failed: 3 }],
|
||||
['==== 1 error in 0.05s ====', { passed: 0, failed: 1 }],
|
||||
['=== 1 failed, 18 passed, 2 errors in 0.40s ===', { passed: 18, failed: 3 }],
|
||||
['==== 1 error in 0.05s ====', { passed: 0, failed: 0, errors: 1 }],
|
||||
['=== 1 failed, 18 passed, 2 errors in 0.40s ===', { passed: 18, failed: 1, errors: 2 }],
|
||||
[' Tests 3 failed (3)', { passed: 0, failed: 3 }],
|
||||
])('%j', (output, counts) => {
|
||||
expect(parseTestCounts(output)).toEqual(counts);
|
||||
@@ -614,13 +615,117 @@ describe('buildStoryPage', () => {
|
||||
...bash('b2', 'git push -u origin main && git -C remote.git log', 'Push', 'fatal: bad object', true),
|
||||
end(1000),
|
||||
]);
|
||||
expect(page.events[1]).toMatchObject({ ok: false, tests: { passed: 0, failed: 1 } });
|
||||
expect(page.events[1]).toMatchObject({ ok: false, tests: { passed: 0, failed: 0, errors: 1 } });
|
||||
const check = page.panels.find((p) => p.kind === 'check')!;
|
||||
expect(check.caption.story).toBe("The tests don't even load.");
|
||||
expect(check.caption.director).toContain('0 passed, 1 error');
|
||||
const save = page.panels.find((p) => p.kind === 'save')!;
|
||||
expect(save.ok).toBe(false);
|
||||
expect(save.caption.story).toBe('Claude tries to send the work up, and the command fails.');
|
||||
expect(save.caption.director).toContain('1 error');
|
||||
});
|
||||
|
||||
it('files written through a here-document are edits: on the event, in the counts, out of the label', () => {
|
||||
const command = [
|
||||
"cat > index.js <<'EOF'",
|
||||
'export const add = (a, b) => a + b;',
|
||||
'test(1 > 2)',
|
||||
'EOF',
|
||||
'cat <<EOF > /tmp/scratch.txt',
|
||||
'scratch',
|
||||
'EOF',
|
||||
'node --test && git add -A && git commit -qm "feat: add"',
|
||||
].join('\n');
|
||||
const lines = [prompt('write it'), ...bash('b1', command, 'Write, test, commit', '# pass 3\n# fail 0'), end(1000)];
|
||||
(lines[1] as { cwd?: string }).cwd = '/work/app';
|
||||
const page = pageOf(lines);
|
||||
const ev = page.events[1];
|
||||
expect(ev.intent).toBe('test');
|
||||
expect(ev.edits?.map((d) => [d.file, d.added])).toEqual([['/work/app/index.js', 2]]);
|
||||
expect(ev.edits?.[0].hunks[0].lines).toEqual(['+export const add = (a, b) => a + b;', '+test(1 > 2)']);
|
||||
expect(ev.label).not.toContain('export const add');
|
||||
expect(ev.headline).toBe('node --test');
|
||||
expect(page.filesChanged).toBe(1);
|
||||
const check = page.panels.find((p) => p.kind === 'check')!;
|
||||
expect(check.caption.director).toMatch(/^node --test → ok · 3 passed · writes 1 file$/);
|
||||
});
|
||||
|
||||
it('heredocWrites skips scratch, stdin bodies and relative paths after a cd', () => {
|
||||
expect(heredocWrites("python3 - <<'EOF'\nprint(1)\nEOF", '/w')).toEqual([]);
|
||||
expect(heredocWrites('cat > "$S/x.txt" <<EOF\nx\nEOF', '/w')).toEqual([]);
|
||||
expect(heredocWrites('cd sub && cat > a.txt <<EOF\nx\nEOF', '/w').map((d) => d.file)).toEqual(['a.txt']);
|
||||
expect(heredocWrites('tee -a notes.md <<EOF\nx\nEOF', '/w').map((d) => d.file)).toEqual(['/w/notes.md']);
|
||||
});
|
||||
|
||||
it('checks that all pass from the start read as building, not fixing', () => {
|
||||
const page = pageOf([
|
||||
prompt('build it'),
|
||||
...edit('e1', '/w/a.py', ['+a = 1']),
|
||||
...bash('b1', 'pytest -q', 'Run tests', '3 passed in 0.1s'),
|
||||
...edit('e2', '/w/b.py', ['+b = 1']),
|
||||
...bash('b2', 'pytest -q', 'Run tests', '6 passed in 0.1s'),
|
||||
end(1000),
|
||||
]);
|
||||
expect(page.panels.find((p) => p.kind === 'loop')!.caption.story).toBe(
|
||||
'Build and test, 2 rounds. Every check passes.'
|
||||
);
|
||||
});
|
||||
|
||||
it('the advisor (a server tool) is a helper giving a second opinion', () => {
|
||||
const page = pageOf([
|
||||
prompt('think hard'),
|
||||
{
|
||||
type: 'assistant',
|
||||
timestamp: '2026-10-18T09:41:20.000Z',
|
||||
message: {
|
||||
content: [
|
||||
{ type: 'server_tool_use', id: 'srv1', name: 'advisor', input: {} },
|
||||
{
|
||||
type: 'advisor_tool_result',
|
||||
tool_use_id: 'srv1',
|
||||
content: { type: 'advisor_redacted_result', encrypted_content: 'xx' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
end(1000),
|
||||
]);
|
||||
const backup = page.panels.find((p) => p.kind === 'backup')!;
|
||||
expect(backup.caption.story).toBe('Claude asks the advisor for a second opinion.');
|
||||
expect(page.events[1]).toMatchObject({ tool: 'Advisor', ok: true, helper: { name: 'advisor' } });
|
||||
expect(page.events[1].helper!.result).toMatch(/encrypted/);
|
||||
expect(page.helpers).toBe(0);
|
||||
});
|
||||
|
||||
it('a turn left open when the next prompt starts has ended; an interrupted turn is timed', () => {
|
||||
const turns = buildStoryTurns(
|
||||
entriesOf([
|
||||
prompt('one'),
|
||||
...bash('b', 'ls', 'List', 'a'),
|
||||
prompt('two'),
|
||||
...bash('c', 'ls', 'List', 'a'),
|
||||
{ type: 'user', timestamp: '2026-10-18T09:50:00.000Z', message: { content: '[Request interrupted by user]' } },
|
||||
])
|
||||
);
|
||||
expect(turns[0]).toMatchObject({ ended: true, interrupted: false });
|
||||
expect(turns[0].durationMs).toBeGreaterThan(0);
|
||||
expect(turns[1]).toMatchObject({ ended: true, interrupted: true });
|
||||
expect(turns[1].durationMs).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('a result remark replaces a step announcement on its check', () => {
|
||||
const page = pageOf([
|
||||
prompt('steps'),
|
||||
said('Step 1: split the module.'),
|
||||
...bash('b1', 'node --test', 'Run tests', '# pass 8\n# fail 0'),
|
||||
said('All 8 tests pass.'),
|
||||
...bash('b2', 'git commit -qm x', 'Commit', 'ok'),
|
||||
end(1000),
|
||||
]);
|
||||
expect(page.panels.find((p) => p.kind === 'check')!.balloon?.text).toBe('All 8 tests pass.');
|
||||
expect(page.panels.find((p) => p.kind === 'save')!.balloon).toBeUndefined();
|
||||
});
|
||||
|
||||
it('a loop ends at its last check; writes after it are a panel of their own', () => {
|
||||
const page = pageOf([
|
||||
prompt('tdd'),
|
||||
|
||||
Reference in new issue
Block a user