mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
fix: fifth review round (durable mount failures, {#key} hydration slots)
cursor[bot]:
- variant_mount_failed now sets the session's pendingEvent (without
clobbering a still-pending generate), so a helper restart replays it
onto /poll and a repair --reply resolves instead of returning
unknown_poll_reply_id. live-resume's next action names the real event
id instead of a literal EVENT_ID placeholder.
- Contract v2 text hydration strips {#key} DELIMITERS from the zip
source (content stays; it always renders), so key blocks can no longer
shift expression slots against the live DOM.
This work was produced with AI assistance (Claude Code).
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Code
parent
39df25ee5a
commit
e5f6d27a9c
@@ -5501,9 +5501,13 @@
|
||||
}
|
||||
|
||||
// Remove balanced {#each}...{/each} and {#if}...{/if} regions (including
|
||||
// the delimiters) from a markup string. Nesting-aware.
|
||||
// the delimiters) from a markup string. Nesting-aware. {#key} blocks keep
|
||||
// their CONTENT (it always renders) but lose their delimiter tokens, which
|
||||
// would otherwise consume live text slots in the zip and shift every
|
||||
// following expression.
|
||||
function stripSvelteBlockRegions(markup) {
|
||||
let out = String(markup || '');
|
||||
out = stripSvelteKeyDelimiters(out);
|
||||
for (const kind of ['each', 'if']) {
|
||||
const open = '{#' + kind;
|
||||
const close = '{/' + kind + '}';
|
||||
@@ -5530,6 +5534,30 @@
|
||||
return out;
|
||||
}
|
||||
|
||||
function stripSvelteKeyDelimiters(markup) {
|
||||
let out = String(markup || '');
|
||||
for (;;) {
|
||||
const start = out.indexOf('{#key');
|
||||
if (start === -1) break;
|
||||
// The opening tag runs to its matching close brace (expressions inside
|
||||
// may nest braces).
|
||||
let depth = 0;
|
||||
let i = start;
|
||||
let openEnd = -1;
|
||||
while (i < out.length) {
|
||||
if (out[i] === '{') depth++;
|
||||
else if (out[i] === '}') {
|
||||
depth--;
|
||||
if (depth === 0) { openEnd = i + 1; break; }
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (openEnd === -1) break;
|
||||
out = out.slice(0, start) + out.slice(openEnd);
|
||||
}
|
||||
return out.split('{/key}').join('');
|
||||
}
|
||||
|
||||
function cloneWithoutElements(rootEl, excludedEls) {
|
||||
if (!excludedEls || excludedEls.length === 0) return rootEl;
|
||||
const excludedSet = new Set(excludedEls);
|
||||
|
||||
@@ -69,7 +69,7 @@ export function mountFailureAction(snapshot = {}) {
|
||||
if (!latest) return null;
|
||||
const where = latest.url ? ` from ${latest.url}` : '';
|
||||
const why = latest.error ? ` (${latest.error})` : '';
|
||||
return `The browser failed to mount variant ${latest.variant}${where}${why}; nothing is on screen. Fix the variant files, then reply with live-poll.mjs --reply EVENT_ID done --file <manifest or source path> for the queued variant_mount_failed event (or republish) so the browser retries.`;
|
||||
return `The browser failed to mount variant ${latest.variant}${where}${why}; nothing is on screen. Fix the variant files, then reply with live-poll.mjs --reply ${snapshot?.pendingEvent?.id || snapshot?.id || 'SESSION_ID'} done --file <manifest or source path> for the queued variant_mount_failed event (or republish) so the browser retries.`;
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
|
||||
@@ -410,6 +410,13 @@ function applyEvent(snapshot, entry) {
|
||||
},
|
||||
].slice(-MOUNT_FAILURE_HISTORY);
|
||||
next.renderState = deriveRenderState(next);
|
||||
// The failure needs an agent reply, so it must survive a helper
|
||||
// restart the same way a generate does. Never clobber a still-pending
|
||||
// generate: a progressive publish can fail an early mount while the
|
||||
// generate event itself is still leased.
|
||||
if (!next.pendingEvent) {
|
||||
next.pendingEvent = toPendingEvent(event);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'checkpoint':
|
||||
|
||||
@@ -163,7 +163,7 @@ describe('live recovery CLI commands', () => {
|
||||
'event=live_resume.mount_failure_action actor=agent operation=recover_session risk=agent_thinks_variants_are_on_screen expected=named failing variant and url actual=' + resume.nextAction,
|
||||
);
|
||||
assert.match(resume.nextAction, /variant_mount_failed/);
|
||||
assert.match(resume.nextAction, /--reply EVENT_ID done --file/);
|
||||
assert.match(resume.nextAction, /--reply cli-render-2 done --file/);
|
||||
|
||||
const status = runJson(STATUS_SCRIPT, [], cwd);
|
||||
assert.match(status.recoveryHint, /failed to mount variant 2/);
|
||||
|
||||
@@ -730,3 +730,39 @@ describe('live-session-store', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('review regressions: durable mount failures', () => {
|
||||
it('variant_mount_failed survives a helper restart as the pending event', () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'impeccable-store-mountfail-'));
|
||||
try {
|
||||
const store = createLiveSessionStore({ cwd: tmp });
|
||||
store.appendEvent({ type: 'generate', id: 'mf123456', count: 3, pageUrl: '/', element: { tagName: 'h1' } });
|
||||
store.appendEvent({ type: 'agent_done', id: 'mf123456' });
|
||||
store.appendEvent({ type: 'variant_mount_failed', id: 'mf123456', variant: 2, url: 'http://x/v2.svelte', error: 'boom' });
|
||||
|
||||
// A second store instance = the restarted helper.
|
||||
const restarted = createLiveSessionStore({ cwd: tmp });
|
||||
const snapshot = restarted.getSnapshot('mf123456');
|
||||
assert.equal(snapshot.pendingEvent?.type, 'variant_mount_failed');
|
||||
assert.equal(snapshot.pendingEvent?.variant, 2);
|
||||
|
||||
// The repair reply retires it.
|
||||
restarted.appendEvent({ type: 'agent_done', id: 'mf123456', sourceEventType: 'variant_mount_failed' });
|
||||
assert.equal(restarted.getSnapshot('mf123456').pendingEvent, null);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('a mount failure does not clobber a still-pending generate', () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'impeccable-store-mountfail2-'));
|
||||
try {
|
||||
const store = createLiveSessionStore({ cwd: tmp });
|
||||
store.appendEvent({ type: 'generate', id: 'mf223456', count: 3, pageUrl: '/', element: { tagName: 'h1' } });
|
||||
store.appendEvent({ type: 'variant_mount_failed', id: 'mf223456', variant: 1, url: 'http://x/v1.svelte', error: 'early' });
|
||||
assert.equal(store.getSnapshot('mf223456').pendingEvent?.type, 'generate');
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user