diff options
| author | Adam Malczewski <[email protected]> | 2026-09-05 05:20:01 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-09-05 05:20:01 +0900 |
| commit | 535215bfd1fee6087613108f42d28cd2807184d3 (patch) | |
| tree | de34459b2357e722dc08ff3e2e02b6b19fc8e401 | |
| parent | 5e5f02c67d0c165dd93f8277a3815d51a540d996 (diff) | |
| download | dispatch-web-535215bfd1fee6087613108f42d28cd2807184d3.tar.gz dispatch-web-535215bfd1fee6087613108f42d28cd2807184d3.zip | |
fix(layout): guard plugin-host sync against malformed /plugins list; fix App test fake
| -rw-r--r-- | src/app/App.test.ts | 17 | ||||
| -rw-r--r-- | src/app/plugin-host.svelte.ts | 6 |
2 files changed, 23 insertions, 0 deletions
diff --git a/src/app/App.test.ts b/src/app/App.test.ts index 94b9c0c..026c5c7 100644 --- a/src/app/App.test.ts +++ b/src/app/App.test.ts @@ -87,6 +87,23 @@ function fakeFetchImpl(): typeof fetch { status: 200, }); } + if (/\/plugins\/[^/]+\/frontend/.test(url)) { + // plan 03 frontend module (no fixtures declare one) — the plain 404 + // the route returns for unknown/unloaded/frontend-less plugins. + return new Response(JSON.stringify({ error: "No frontend module for this plugin" }), { + status: 404, + }); + } + if (/\/plugins(\?.*)?$/.test(url)) { + // Valid EMPTY list (plan 03): the plugin host's boot + // `extensions-changed`/workspace syncs read this — a shapeless + // fallback body would be cast to PluginListResponse and crash the + // diff (`list.loaded` not iterable). + return new Response( + JSON.stringify({ loaded: [], disabledWithReason: [], discovered: [], missing: [] }), + { status: 200 }, + ); + } return new Response(JSON.stringify({ chunks: [], latestSeq: 0 }), { status: 200 }); }; } diff --git a/src/app/plugin-host.svelte.ts b/src/app/plugin-host.svelte.ts index 9e53bc1..729a8aa 100644 --- a/src/app/plugin-host.svelte.ts +++ b/src/app/plugin-host.svelte.ts @@ -122,6 +122,12 @@ export function createPluginHost(opts: PluginHostOptions): PluginHost { opts.http.workspacePlugins(workspaceId), ]); if (list === null) return; + // Shape guard at the outermost edge (asymmetric testing: the shell + // guards, the pure diff stays trusting): a malformed-but-200 + // `/plugins` response (wrong shape, contract drift, a test fake that + // doesn't model the route) must fail SOFT exactly like `null` — keep + // the current imported set, never throw from the fire-and-forget sync. + if (!Array.isArray(list.loaded)) return; const candidates = frontendCandidates(list, { workspaceId, usableIds: usablePluginIds(workspace?.plugins ?? []), |
