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:
digital-garbage
2026-09-15 22:59:29 +03:00
co-authored by Claude Sonnet 5
parent 37574ac44c
commit 97e1029ca0
2 changed files with 22 additions and 0 deletions
+8
View File
@@ -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;
+14
View File
@@ -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) {