test(web): prove the dedicated app origin end to end

`frame-ancestors` is checked against every ancestor, not just the parent.
A published app document is framed by the sandbox proxy, which is framed
by the Inspector page — so admitting only the proxy's origin blocked the
frame outright (it rendered as a chrome-error frame that never reached
the bridge). `appDocumentEmbedders` now admits both.

That failure was invisible to every unit test, so `smoke:web:app` grows a
second phase: the same demo app declaring `_meta.ui.domain`, driven
through the same connect → open → ready chain, then asserting the two
facts that only hold on the dedicated path — the inner frame was
navigated to a published document, and its own `location.origin` is a
real http origin rather than "null". Both phases end at
`data-app-status="ready"`, so nothing short of reading the frame's origin
can tell them apart.

Adds `test-servers/configs/mcp-app-domain-http.json` (and a `domain`
param on the `mcp_app_demo_widget` preset) as the showcase server for
both the smoke and hand testing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-23 23:21:41 -04:00 • committed by GitHub
commit 8e8c1bbad9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -19,7 +19,7 @@ import {
DEFAULT_APP_ORIGIN_PORT,
parseDocumentId,
resolveAppOriginPort,
sandboxOriginList,
appDocumentEmbedders,
} from "../../../../server/app-origin-controller.js";
describe("resolveAppOriginPort", () => {
@ -85,20 +85,37 @@ describe("parseDocumentId", () => {
});
});
describe("sandboxOriginList", () => {
it("reduces the sandbox URL to its origin", () => {
expect(sandboxOriginList("http://127.0.0.1:6275/sandbox")).toEqual([
describe("appDocumentEmbedders", () => {
it("admits the sandbox proxy AND the Inspector page", () => {
// `frame-ancestors` is checked against EVERY ancestor, not just the parent:
// the document is framed by the proxy, which is framed by the app page.
// Omitting the app page's origin blocks the frame outright (it renders as a
// chrome-error frame that never reaches the bridge).
expect(
appDocumentEmbedders("http://127.0.0.1:6275/sandbox", [
"http://127.0.0.1:6274",
"http://localhost:6274",
]),
).toEqual([
"http://127.0.0.1:6275",
"http://127.0.0.1:6274",
"http://localhost:6274",
]);
});
it("still returns the allow-list when the sandbox never bound", () => {
expect(appDocumentEmbedders(null, ["http://127.0.0.1:6274"])).toEqual([
"http://127.0.0.1:6274",
]);
});
it.each([[null], [undefined], [""]])(
"returns undefined for %j so frame-ancestors falls back to loopback",
"returns undefined for %j with no allow-list, so frame-ancestors falls back to loopback",
(value) => {
// Not `[]`: an empty list would make the directive `'none'` and block the
// app frame outright, which is worse than a permissive loopback fallback.
expect(
sandboxOriginList(value as string | null | undefined),
appDocumentEmbedders(value as string | null | undefined),
).toBeUndefined();
},
);