fix: tour -- mock settingsCard, guard applyGlobalPromptQuiet at its root
Round 6's sweep of raw API.* calls that bypass Store entirely (a layer none of the prior five rounds checked) found the most easily-triggered gap yet: models.js's "Export settings..." button calls API.settingsCard directly, a real unmocked POST /api/settings-card -- and it's on the FIRST screen the tour's own "models-modal" step shows, no sidebar navigation needed (unlike round 5's Combo Sweep panel, which needed a click into Settings first). Fixed by mocking API.settingsCard with a real Blob (the function resolves a Blob and feeds it straight to URL.createObjectURL, so a JSON mock like every other entry uses would break the button instead of blocking it) -- a 1x1 transparent PNG is enough for the card to render. Also closed the secondary finding: applyGlobalPromptQuiet has TWO internal closure-scoped callers (setEditorSetting's anchorEnabled toggle, applyPromptTemplate) besides its exported Store property -- the same "block the export, miss the internal call" shape as commit()'s original vulnerability, so a patchStore() block-list entry alone couldn't have closed it. It reaches a real, unmocked API.parsePrompt call. Fixed with the same root-cause pattern as commit()'s guard: a window.__FUNPACK_TOUR__ check directly inside the function, which also covers scheduleGlobalPromptApply transitively since that just debounces into a call to this same function. Verified live: API.settingsCard resolves a real 68-byte PNG Blob (not an error); toggling anchorEnabled and calling scheduleGlobalPromptApply directly, waited past both debounce windows, zero prompt/expand or settings-card requests in the network log; full tour sequence still completes with zero thrown errors. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
37574ac44c
commit
97e1029ca0
@@ -636,6 +636,14 @@
|
||||
}
|
||||
|
||||
async function applyGlobalPromptQuiet(text) {
|
||||
// Same shape as commit()'s guard above, and needed for the same reason:
|
||||
// this has TWO internal closure-scoped callers (setEditorSetting's
|
||||
// anchorEnabled toggle, applyPromptTemplate) besides its exported
|
||||
// property, so blocking Store.applyGlobalPromptQuiet from tour.js alone
|
||||
// would miss both. It reaches a real, unmocked API.parsePrompt network
|
||||
// call, so it needs its own gate here rather than relying on
|
||||
// scheduleSaveSilent()/commit() downstream to cover it.
|
||||
if (window.__FUNPACK_TOUR__) return false;
|
||||
if (!state.project) return false;
|
||||
const trimmed = String(text || "").trim();
|
||||
if (!trimmed) return false;
|
||||
|
||||
@@ -337,6 +337,20 @@
|
||||
API.exportClip = ok({});
|
||||
API.exportClipsCombined = ok({ job_id: "tour-demo" });
|
||||
API.interrupt = ok({});
|
||||
// models.js's "Export settings..." button calls API.settingsCard
|
||||
// directly -- it never goes through window.Store, so it was never a
|
||||
// candidate for patchStore()'s blocklist, and it's a real, unmocked
|
||||
// POST /api/settings-card one click away on the models-modal tour step
|
||||
// (the FIRST screen that step shows, no sidebar navigation needed).
|
||||
// The real function resolves a Blob (models.js feeds it straight to
|
||||
// URL.createObjectURL), so the mock needs to be a real Blob too, not
|
||||
// JSON -- a 1x1 transparent PNG is enough for the card to render.
|
||||
API.settingsCard = () => Promise.resolve(new Blob(
|
||||
[Uint8Array.from(atob(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
|
||||
), (c) => c.charCodeAt(0))],
|
||||
{ type: "image/png" }
|
||||
));
|
||||
}
|
||||
|
||||
function patchStore(Store) {
|
||||
|
||||
Reference in New Issue
Block a user