mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
fix(story-mode): live helper steps behind an unchanged page, a clean reopen, no wedged view
From review round 7 (PR review):
- A helper's drawer froze while the helper worked: a background helper
writes only to its own transcript, so the parent page answered 304 and
the refresh returned before anything refreshed the drawer. An unchanged
page now still refreshes an open drawer's working helpers (ETag'd).
- A helper messaged twice read as superseded ("The trail stops here…"):
its one-page run took its ending from the first turn. It takes the last.
- More than 60 turns in a small file listed the newest 60 with no hint;
`older` is now set whenever the list leaves turns out.
- One /story request that never settled wedged the view, even across a
reopen. Requests carry a token and a 30 s timeout, and opening resets
the request state, with what was announced and shown.
- The background canvases (megabytes each at 2x) are released when Story
Mode closes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
91ee063fc2
commit
e7ce3b8e56
6 files changed
+132
-9
No files matched your search
@@ -202,6 +202,16 @@ describe('GET /api/sessions/:id/story', () => {
|
||||
expect(res.json().data).toMatchObject({ supported: true, turns: [], page: null });
|
||||
});
|
||||
|
||||
it('says when the list leaves turns out past the 60 it shows, even in a small file', async () => {
|
||||
const lines: unknown[] = [];
|
||||
for (let i = 0; i < 80; i++) lines.push(prompt(`job ${i}`), said('ok'), end(1000));
|
||||
writeFileSync(transcript, jsonl(lines));
|
||||
const { app } = await setup();
|
||||
const body = (await app.inject({ method: 'GET', url: URL })).json();
|
||||
expect(body.data.turns).toHaveLength(60);
|
||||
expect(body.data.older).toBe(true);
|
||||
});
|
||||
|
||||
it('404s an unknown session', async () => {
|
||||
const { app } = await setup();
|
||||
const res = await app.inject({ method: 'GET', url: '/api/sessions/nope/story' });
|
||||
|
||||
@@ -604,6 +604,68 @@ describe('story-mode.js overlay', () => {
|
||||
expect(app.openFilePreview).toHaveBeenLastCalledWith('/work/demo-shop/a.md', SID);
|
||||
});
|
||||
|
||||
it("a page that holds still (304) still refreshes a working helper's steps in its open drawer", async () => {
|
||||
const data = storyOf([
|
||||
prompt('docs'),
|
||||
tool('a1', 'Agent', { description: 'Write README', subagent_type: 'general-purpose', prompt: 'Write it' }),
|
||||
result('a1', 'Async agent launched successfully. agentId: h9', {
|
||||
toolUseResult: { isAsync: true, status: 'async_launched', agentId: 'h9' },
|
||||
}),
|
||||
end(1000),
|
||||
]);
|
||||
boot(data);
|
||||
const helperPage = storyOf([prompt('Write it'), ...read('r1', '/work/demo-shop/a.md')]).page;
|
||||
let helperCalls = 0;
|
||||
app._api = async (path: string) => {
|
||||
if (path.includes('/story/helpers/h9')) {
|
||||
helperCalls++;
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: { get: () => null },
|
||||
json: async () => ({ success: true, data: { supported: true, helper: { agentId: 'h9' }, page: helperPage } }),
|
||||
};
|
||||
}
|
||||
// The parent page never changes while the helper works: the same tag every time.
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: { get: (h: string) => (h.toLowerCase() === 'etag' ? 'W/"same"' : null) },
|
||||
json: async () => ({ success: true, data }),
|
||||
};
|
||||
};
|
||||
app.openStoryMode(SID);
|
||||
await settle();
|
||||
sizeAndDraw();
|
||||
$('.story-panel[data-kind="backup"] .story-hit')!.click();
|
||||
await settle();
|
||||
const before = helperCalls;
|
||||
expect(before).toBeGreaterThan(0);
|
||||
await app._storyRefresh();
|
||||
await settle();
|
||||
expect(helperCalls).toBe(before + 1);
|
||||
});
|
||||
|
||||
it('a request that never settled does not block the next open: the page draws', async () => {
|
||||
const data = storyOf(demoTurn());
|
||||
boot(data);
|
||||
app._api = () => new Promise(() => {}); // hangs forever
|
||||
app.openStoryMode(SID);
|
||||
await settle();
|
||||
app.closeStoryMode();
|
||||
app._api = async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: { get: () => null },
|
||||
json: async () => ({ success: true, data }),
|
||||
});
|
||||
app.openStoryMode(SID);
|
||||
await settle();
|
||||
expect($$('.story-panel').length).toBeGreaterThan(0);
|
||||
// Reopened, nothing from before is announced as news.
|
||||
expect($('.story-sr')!.textContent).toBe('');
|
||||
});
|
||||
|
||||
it('Tab stays inside the overlay', async () => {
|
||||
boot(storyOf(demoTurn()));
|
||||
app.openStoryMode(SID);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
STORY_LIMITS,
|
||||
buildHelperPage,
|
||||
buildStoryPage,
|
||||
buildStoryTurns,
|
||||
classifyBashCommand,
|
||||
@@ -996,6 +997,17 @@ describe('buildStoryPage', () => {
|
||||
for (const [name, fn] of shapes) expect([name, ms(fn) < 250]).toEqual([name, true]);
|
||||
});
|
||||
|
||||
it('a helper messaged twice reads as one finished run, not as superseded', () => {
|
||||
const r = createStoryReducer({ sidechain: true });
|
||||
const lines = [prompt('write it'), said('Done.'), prompt('also the tests'), said('Done too.')].map((l) => ({
|
||||
...(l as object),
|
||||
isSidechain: true,
|
||||
}));
|
||||
const page = buildHelperPage(buildStoryTurns(lines.flatMap((l) => r.reduce(l))), true)!;
|
||||
expect(page.superseded).toBeUndefined();
|
||||
expect(page.panels.at(-1)!.caption.story).not.toMatch(/trail stops here/);
|
||||
});
|
||||
|
||||
it('a python or node script fed by a here-document: the files it opens for writing, never guessed', () => {
|
||||
const files = (command: string) => heredocWrites(command, '/w').map((d) => d.file);
|
||||
expect(
|
||||
|
||||
Reference in new issue
Block a user