Files
openwork/apps/app/tests/mcp-app-frame.test.ts

167 lines
6.8 KiB
TypeScript

import { describe, expect, test } from "bun:test"
import {
createOpenworkServerClient,
normalizeMcpAppHostOrigin,
OpenworkServerError,
type OpenworkMcpAppResource,
} from "../src/app/lib/openwork-server"
import { formatMcpAppDiagnostic, safeMcpAppDiagnosticMessage } from "../src/components/chat/mcp-app-diagnostics"
import {
buildMcpAppCsp,
gatewayMcpAppLaunch,
isActionableMcpAppResolutionError,
secureMcpAppHtml,
} from "../src/components/chat/mcp-app-frame"
function fixture(overrides: Partial<OpenworkMcpAppResource> = {}): OpenworkMcpAppResource {
return {
serverName: "fixture",
toolName: "render",
resourceUri: "ui://fixture/view.html",
html: "<!doctype html><html><head><title>Fixture</title></head><body>ok</body></html>",
csp: {
connectDomains: [],
resourceDomains: [],
frameDomains: [],
baseUriDomains: [],
},
prefersBorder: true,
...overrides,
}
}
describe("MCP App iframe policy", () => {
test("accepts a namespaced gateway launch reference without exposing credentials", () => {
expect(gatewayMcpAppLaunch({
source: "provider",
"openwork/mcpApp": {
connectionId: "emc_01atlas",
toolName: "open_project_atlas",
resourceUri: "ui://atlas/1/index.html",
arguments: { query: "migration" },
},
})).toEqual({
connectionId: "emc_01atlas",
toolName: "open_project_atlas",
resourceUri: "ui://atlas/1/index.html",
arguments: { query: "migration" },
})
expect(gatewayMcpAppLaunch({
"openwork/mcpApp": {
connectionId: "emc_01atlas",
toolName: "open_project_atlas",
resourceUri: "ui://atlas/1/index.html",
},
})).toBeNull()
})
test("accepts a same-server generated App launch without a connection reference", () => {
expect(gatewayMcpAppLaunch({
"openwork/mcpApp": {
toolName: "render_artifact_view",
resourceUri: "ui://openwork/artifacts/atlas/views/1/index.html",
arguments: { input: { query: "migration" } },
},
})).toEqual({
toolName: "render_artifact_view",
resourceUri: "ui://openwork/artifacts/atlas/views/1/index.html",
arguments: { input: { query: "migration" } },
})
})
test("uses the opaque message origin for packaged file hosts", () => {
expect(normalizeMcpAppHostOrigin("file://")).toBe("null")
expect(normalizeMcpAppHostOrigin("null")).toBe("null")
expect(normalizeMcpAppHostOrigin("https://desktop.example")).toBe("https://desktop.example")
const client = createOpenworkServerClient({ baseUrl: "http://localhost:61856" })
const sandbox = client.mcpAppSandbox(fixture(), "file://")
expect(new URL(sandbox.url).searchParams.get("hostOrigin")).toBe("null")
})
test("keeps ordinary tools silent while surfacing advertised resource failures", () => {
expect(isActionableMcpAppResolutionError(new OpenworkServerError(503, "mcp_unreachable", "offline"))).toBe(false)
expect(isActionableMcpAppResolutionError(new OpenworkServerError(404, "resource_read_failed", "missing"))).toBe(true)
expect(isActionableMcpAppResolutionError(new Error("generic failure"))).toBe(false)
})
test("formats safe, copyable handshake diagnostics", () => {
const details = formatMcpAppDiagnostic({
code: "MCP_APP_INITIALIZE_TIMEOUT",
causeCode: "mcp_unreachable",
stage: "app-initialization",
message: "The HTML document loaded, but initialization did not complete.",
toolName: "artifact_render_card",
resourceUri: "ui://openwork/artifacts/arv_1/views/avr_2/index.html",
sandboxOrigin: "http://127.0.0.1:4321",
elapsedMs: 10_025,
checkpoints: ["resource-resolved+0ms", "resource-document-loaded+24ms"],
sandboxDocument: { readyState: "complete", hasHtmlRoot: true, scriptCount: 1 },
})
expect(details).toContain("Code: MCP_APP_INITIALIZE_TIMEOUT")
expect(details).toContain("Cause code: mcp_unreachable")
expect(details).toContain("Stage: app-initialization")
expect(details).toContain("Resource: ui://openwork/artifacts/arv_1/views/avr_2/index.html")
expect(details).toContain("Document: readyState=complete, htmlRoot=true, scripts=1")
expect(details).toContain("resource-document-loaded+24ms")
})
test("redacts credentials from diagnostic messages", () => {
expect(safeMcpAppDiagnosticMessage(
new Error("request failed: Bearer secret-value https://example.com?access_token=also-secret"),
"fallback",
)).toBe("request failed: Bearer [redacted] https://example.com?access_token=[redacted]")
})
test("defaults every ambient capability closed", () => {
const csp = buildMcpAppCsp(fixture())
expect(csp).toContain("default-src 'none'")
expect(csp).toContain("connect-src 'none'")
expect(csp).toContain("frame-src 'none'")
expect(csp).toContain("base-uri 'none'")
expect(csp).toContain("form-action 'none'")
})
test("injects the host-enforced CSP before resource markup runs", () => {
const html = secureMcpAppHtml(fixture())
const policy = html.indexOf('http-equiv="Content-Security-Policy"')
const title = html.indexOf("<title>")
expect(policy).toBeGreaterThan(-1)
expect(policy).toBeLessThan(title)
})
test("creates a valid policy-bearing head when the resource omits one", () => {
const html = secureMcpAppHtml(fixture({ html: "<html><body>headless resource</body></html>" }))
expect(html).toContain('<html><head><meta http-equiv="Content-Security-Policy"')
expect(html.indexOf("Content-Security-Policy")).toBeLessThan(html.indexOf("<body>"))
const fragment = secureMcpAppHtml(fixture({ html: "<main>fragment resource</main>" }))
expect(fragment).toStartWith('<!doctype html><html><head><meta http-equiv="Content-Security-Policy"')
expect(fragment).toContain("<body><main>fragment resource</main></body>")
})
test("rejects executable markup before an existing document policy", () => {
expect(() => secureMcpAppHtml(fixture({
html: "<script>globalThis.beforePolicy = true</script><html><head></head><body>bad</body></html>",
}))).toThrow("executable markup before its HTML root")
expect(() => secureMcpAppHtml(fixture({
html: "<html><script>globalThis.beforePolicy = true</script><head></head><body>bad</body></html>",
}))).toThrow("markup before its policy-bearing head")
})
test("allows only the server-declared origins in each directive", () => {
const csp = buildMcpAppCsp(fixture({
csp: {
connectDomains: ["https://api.example.com"],
resourceDomains: ["https://static.example.com"],
frameDomains: ["https://embed.example.com"],
baseUriDomains: [],
},
}))
expect(csp).toContain("connect-src https://api.example.com")
expect(csp).toContain("script-src 'unsafe-inline' https://static.example.com")
expect(csp).toContain("frame-src https://embed.example.com")
})
})