import assert from "node:assert/strict"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import test from "node:test"; import { withApp } from "../lib/app.mjs"; import { extensionZipBase64, LOCALIZED_EXTENSION_MESSAGES, localizedExtensionZipBase64, OVERSIZED_EXTENSION_NAME, oversizedExtensionZipBase64, writeUnpackedExtension, } from "../lib/fixtures.mjs"; const VLESS_URI = "vless://6d6e21a1-4829-4d2b-bc7f-1b25707b61e4@127.0.0.1:443?encryption=none&flow=xtls-rprx-vision&security=reality&sni=www.example.com&fp=chrome&pbk=BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc&sid=0123456789abcdef&spx=%2F&type=tcp&headerType=none#MCP"; async function jsonRequest( url, { method = "GET", token, body, headers = {} } = {}, ) { const response = await fetch(url, { method, headers: { ...(token ? { authorization: `Bearer ${token}` } : {}), ...(body === undefined ? {} : { "content-type": "application/json" }), ...headers, }, body: body === undefined ? undefined : JSON.stringify(body), }); const text = await response.text(); let value = null; if (text) { try { value = JSON.parse(text); } catch { value = text; } } return { response, value }; } async function seedTerms(app) { const home = path.join(app.root, "home"); const directory = process.platform === "darwin" ? path.join(home, "Library", "Application Support", "Wayfern") : process.platform === "win32" ? path.join(app.root, "windows", "roaming", "Wayfern") : path.join(app.root, "xdg", "config", "Wayfern"); await mkdir(directory, { recursive: true }); await writeFile( path.join(directory, "license-accepted"), String(Math.floor(Date.now() / 1000)), ); } async function invokeContract(app, command, args = {}) { try { return { ok: true, value: await app.invoke(command, args) }; } catch (error) { return { ok: false, error: String(error) }; } } /** * Evidence that a command's BODY ran, not merely that it was invoked. * * `assert.ok(await invokeContract(...))` was the pattern here, and it cannot * fail: `invokeContract` always resolves to an object, and every object is * truthy. It passed whether the command succeeded, refused, or did not exist at * all, so eight commands whose only coverage-map evidence was that line could * have had their entire bodies deleted with every suite still green. * * Each caller now states which of the two outcomes it expects and pins it, so * the assertion fails if the command stops reaching its real logic. The two * outcomes are both legitimate here: these are cloud and update commands, and a * hermetic E2E run has no session, so a refusal FOR THE RIGHT REASON is exactly * as much proof that the body ran as a success is. */ async function assertContract(app, command, expected, args = {}) { const result = await invokeContract(app, command, args); if (expected.refusedWith) { assert.equal( result.ok, false, `${command} was expected to refuse, but returned ${JSON.stringify(result.value)}`, ); assert.match( result.error, expected.refusedWith, `${command} refused for a different reason than the one that proves its body ran`, ); return result; } assert.equal(result.ok, true, `${command} failed: ${result.error}`); expected.answers(result.value); return result; } async function assertCommandErrorCode(app, command, code, args = {}) { const error = await app.invokeError(command, args); assert.match(error, new RegExp(`"code":"${code}"`)); } test("authenticated REST API serves its complete OpenAPI contract and CRUD lifecycle", async () => { await withApp("integrations-rest", async (app) => { await seedTerms(app); const settings = await app.invoke("get_app_settings"); const saved = await app.invoke("save_app_settings", { settings: { ...settings, api_enabled: true, api_port: 0, api_token: null, onboarding_completed: true, }, }); assert.ok(saved.api_token?.length >= 32); const port = await app.invoke("start_api_server", { port: 0 }); assert.equal(await app.invoke("get_api_server_status"), port); const diagnostic = await app.invoke("check_integration_connection", { target: "api", }); assert.equal(diagnostic.configured, true); assert.equal(diagnostic.reachable, true); assert.equal(diagnostic.authorized, true); assert.equal(diagnostic.http_status, 200); assert.ok(diagnostic.checked_at > 0); assert.deepEqual(Object.keys(diagnostic).sort(), [ "authorized", "checked_at", "configured", "http_status", "reachable", ]); const unconfigured = await app.invoke("check_integration_connection", { target: "remote", }); assert.equal(unconfigured.configured, false); assert.equal(unconfigured.authorized, null); const base = `http://127.0.0.1:${port}`; const openapi = await jsonRequest(`${base}/openapi.json`); assert.equal(openapi.response.status, 200); assert.equal(openapi.value.openapi.startsWith("3."), true); const paths = Object.keys(openapi.value.paths); for (const required of [ "/v1/profiles", "/v1/profiles/{id}/run", "/v1/groups", "/v1/proxies", "/v1/vpns/{id}/export", "/v1/extensions", "/v1/browsers/{browser}/versions", ]) { assert.ok(paths.includes(required), `OpenAPI is missing ${required}`); } // The served spec comes from the hand-maintained ApiDoc derive, not from // the router, so an extension route can answer requests while being absent // from the contract an agent generates its client from. for (const [route, methods] of [ ["/v1/extensions", ["get", "post"]], ["/v1/extensions/{id}", ["get", "put", "delete"]], ["/v1/extension-groups", ["get", "post"]], ["/v1/extension-groups/{id}", ["get", "put", "delete"]], [ "/v1/extension-groups/{id}/extensions/{extension_id}", ["post", "delete"], ], ]) { assert.ok(paths.includes(route), `OpenAPI is missing ${route}`); for (const method of methods) { assert.ok( openapi.value.paths[route][method], `OpenAPI is missing ${method.toUpperCase()} ${route}`, ); } } const unauthorized = await jsonRequest(`${base}/v1/profiles`); assert.equal(unauthorized.response.status, 401); const wrongToken = await jsonRequest(`${base}/v1/profiles`, { token: "wrong", }); assert.equal(wrongToken.response.status, 401); const groupsInitially = await jsonRequest(`${base}/v1/groups`, { token: saved.api_token, }); assert.equal(groupsInitially.response.status, 200); assert.deepEqual(groupsInitially.value, []); const createdGroup = await jsonRequest(`${base}/v1/groups`, { method: "POST", token: saved.api_token, body: { name: "REST Group" }, }); assert.equal(createdGroup.response.status, 200); assert.equal(createdGroup.value.name, "REST Group"); const groupId = createdGroup.value.id; const updatedGroup = await jsonRequest(`${base}/v1/groups/${groupId}`, { method: "PUT", token: saved.api_token, body: { name: "REST Group Updated" }, }); assert.equal(updatedGroup.value.name, "REST Group Updated"); const createdProxy = await jsonRequest(`${base}/v1/proxies`, { method: "POST", token: saved.api_token, body: { name: "REST Proxy", proxy_settings: { proxy_type: "http", host: "127.0.0.1", port: 8080, username: null, password: null, }, }, }); assert.equal(createdProxy.response.status, 200); assert.equal(createdProxy.value.proxy_settings.port, 8080); const proxyId = createdProxy.value.id; const fetchedProxy = await jsonRequest(`${base}/v1/proxies/${proxyId}`, { token: saved.api_token, }); assert.equal(fetchedProxy.value.name, "REST Proxy"); const createdVless = await jsonRequest(`${base}/v1/proxies`, { method: "POST", token: saved.api_token, body: { name: "REST VLESS Reality", proxy_settings: { proxy_type: "vless", host: "127.0.0.1", port: 443, username: null, password: null, vless_uri: VLESS_URI, }, }, }); assert.equal(createdVless.response.status, 200); assert.equal(createdVless.value.proxy_settings.vless_uri, VLESS_URI); assert.equal(createdVless.value.proxy_settings.host, "127.0.0.1"); assert.equal(createdVless.value.proxy_settings.port, 443); const vlessProxyId = createdVless.value.id; const invalidVless = await jsonRequest( `${base}/v1/proxies/${vlessProxyId}`, { method: "PUT", token: saved.api_token, body: { proxy_settings: { ...createdVless.value.proxy_settings, vless_uri: VLESS_URI.replace("security=reality", "security=tls"), }, }, }, ); assert.equal(invalidVless.response.status, 400); assert.match(JSON.stringify(invalidVless.value), /VLESS_CONFIG_INVALID/); assert.equal( ( await jsonRequest(`${base}/v1/proxies/${vlessProxyId}`, { token: saved.api_token, }) ).value.proxy_settings.vless_uri, VLESS_URI, ); const imported = await jsonRequest(`${base}/v1/proxies/import`, { method: "POST", token: saved.api_token, body: { format: "txt", content: "http://127.0.0.1:8081", name_prefix: "API", }, }); assert.equal(imported.response.status, 200); assert.equal(imported.value.imported_count, 1); // Extensions arrive either as an inline payload or as a path the app can // read, and the folder form is the whole point: it is how an agent reaches // the "load unpacked" flow that the desktop offers through a file picker. const archiveExtension = await jsonRequest(`${base}/v1/extensions`, { method: "POST", token: saved.api_token, body: { name: "REST Archive Extension", file_name: "fixture.zip", file_data_base64: extensionZipBase64(), }, }); assert.equal( archiveExtension.response.status, 201, JSON.stringify(archiveExtension.value), ); assert.equal(archiveExtension.value.name, "Donut E2E Fixture"); assert.equal(archiveExtension.value.source_kind, "archive"); assert.equal(archiveExtension.value.linked_path, null); const unpackedDir = await writeUnpackedExtension( path.join(app.root, "fixtures", "rest-unpacked-extension"), { name: "Donut REST Unpacked", version: "1.2.0" }, ); const folderExtension = await jsonRequest(`${base}/v1/extensions`, { method: "POST", token: saved.api_token, body: { name: "REST Folder Extension", source_path: unpackedDir }, }); assert.equal( folderExtension.response.status, 201, JSON.stringify(folderExtension.value), ); assert.equal(folderExtension.value.name, "Donut REST Unpacked"); assert.equal(folderExtension.value.version, "1.2.0"); assert.equal(folderExtension.value.source_kind, "unpacked"); assert.equal(folderExtension.value.linked_path, null); // Two sources in one request have no defined winner, so the request is // refused rather than silently resolved. const ambiguousSource = await jsonRequest(`${base}/v1/extensions`, { method: "POST", token: saved.api_token, body: { name: "REST Ambiguous Extension", file_name: "fixture.zip", file_data_base64: extensionZipBase64(), source_path: unpackedDir, }, }); assert.equal(ambiguousSource.response.status, 400); assert.match( JSON.stringify(ambiguousSource.value), /EXTENSION_SOURCE_REQUIRED/, ); const sourcelessExtension = await jsonRequest(`${base}/v1/extensions`, { method: "POST", token: saved.api_token, body: { name: "REST Sourceless Extension" }, }); assert.equal(sourcelessExtension.response.status, 400); assert.match( JSON.stringify(sourcelessExtension.value), /EXTENSION_SOURCE_REQUIRED/, ); // An archive has no folder to keep loading from, so linking one is refused // rather than quietly stored as a copy. const linkedArchive = await jsonRequest(`${base}/v1/extensions`, { method: "POST", token: saved.api_token, body: { name: "REST Linked Archive", file_name: "fixture.zip", file_data_base64: extensionZipBase64(), link: true, }, }); assert.equal(linkedArchive.response.status, 400); assert.match( JSON.stringify(linkedArchive.value), /EXTENSION_LINK_REQUIRES_DIRECTORY/, ); const extensionId = folderExtension.value.id; assert.equal( ( await jsonRequest(`${base}/v1/extensions/${extensionId}`, { token: saved.api_token, }) ).value.id, extensionId, ); const renamedExtension = await jsonRequest( `${base}/v1/extensions/${extensionId}`, { method: "PUT", token: saved.api_token, body: { name: "REST Renamed Extension" }, }, ); assert.equal( renamedExtension.response.status, 200, JSON.stringify(renamedExtension.value), ); assert.equal(renamedExtension.value.name, "REST Renamed Extension"); assert.equal( (await jsonRequest(`${base}/v1/extensions`, { token: saved.api_token })) .value.length, 2, ); // Axum's default body limit is 2 MiB, which plenty of real `.crx` files // exceed: every one of them was refused before the handler ran until the // extension payload routes got a limit of their own. The fixture below is // stored rather than deflated, so the body genuinely stays over the // default and a 201 can only come from the raised limit. const oversizedBody = { name: "REST Oversized Extension", file_name: "oversized.zip", file_data_base64: oversizedExtensionZipBase64(), }; assert.ok( Buffer.byteLength(JSON.stringify(oversizedBody)) > 2 * 1024 * 1024, "the oversized fixture must exceed the default body limit it tests", ); const oversized = await jsonRequest(`${base}/v1/extensions`, { method: "POST", token: saved.api_token, body: oversizedBody, }); assert.equal( oversized.response.status, 201, JSON.stringify(oversized.value), ); // Read out of the archive that arrived, so the payload landed whole rather // than merely being accepted. assert.equal(oversized.value.name, OVERSIZED_EXTENSION_NAME); assert.equal(oversized.value.version, "1.0.0"); assert.equal(oversized.value.file_type, "zip"); // The raised limit is scoped to the two paths that carry a payload. A // group name is never megabytes long, so a route that accepted one would // mean the layer had been attached to the whole router. const oversizedGroupBody = { name: "G".repeat(3 * 1024 * 1024) }; assert.ok( Buffer.byteLength(JSON.stringify(oversizedGroupBody)) > 2 * 1024 * 1024, ); const oversizedGroup = await jsonRequest(`${base}/v1/extension-groups`, { method: "POST", token: saved.api_token, body: oversizedGroupBody, }); assert.equal( oversizedGroup.response.status, 413, JSON.stringify(oversizedGroup.value), ); assert.deepEqual( ( await jsonRequest(`${base}/v1/extension-groups`, { token: saved.api_token, }) ).value, [], "the refused group request must not have stored anything", ); // Chrome Web Store extensions overwhelmingly localize their manifest: the // name a user recognizes sits in `_locales//messages.json` // and the manifest holds `__MSG_extName__`. Storing the manifest verbatim // is what puts a raw placeholder in the extension list. const localized = await jsonRequest(`${base}/v1/extensions`, { method: "POST", token: saved.api_token, body: { name: "REST Localized Extension", file_name: "localized.zip", file_data_base64: localizedExtensionZipBase64(), }, }); assert.equal( localized.response.status, 201, JSON.stringify(localized.value), ); assert.equal(localized.value.name, LOCALIZED_EXTENSION_MESSAGES.extName); assert.equal( localized.value.description, LOCALIZED_EXTENSION_MESSAGES.extDescription, ); assert.equal( localized.value.author, LOCALIZED_EXTENSION_MESSAGES.extAuthor, ); assert.doesNotMatch(JSON.stringify(localized.value), /__MSG_/); // The resolved strings have to be what was persisted, not something the // create response computed on its way out. assert.equal( ( await jsonRequest(`${base}/v1/extensions/${localized.value.id}`, { token: saved.api_token, }) ).value.name, LOCALIZED_EXTENSION_MESSAGES.extName, ); // A placeholder the locale file cannot resolve falls back to the name the // caller sent. What it must never do is store `__MSG_extName__` itself. const unresolved = await jsonRequest(`${base}/v1/extensions`, { method: "POST", token: saved.api_token, body: { name: "REST Unresolved Placeholder", file_name: "unresolved.zip", file_data_base64: localizedExtensionZipBase64({ messages: {} }), }, }); assert.equal( unresolved.response.status, 201, JSON.stringify(unresolved.value), ); assert.equal(unresolved.value.name, "REST Unresolved Placeholder"); assert.equal(unresolved.value.description, null); assert.equal(unresolved.value.author, null); assert.doesNotMatch(JSON.stringify(unresolved.value), /__MSG_/); const extensionGroup = await jsonRequest(`${base}/v1/extension-groups`, { method: "POST", token: saved.api_token, body: { name: "REST Extension Group" }, }); assert.equal( extensionGroup.response.status, 201, JSON.stringify(extensionGroup.value), ); assert.equal(extensionGroup.value.name, "REST Extension Group"); assert.deepEqual(extensionGroup.value.extension_ids, []); const extensionGroupId = extensionGroup.value.id; const renamedExtensionGroup = await jsonRequest( `${base}/v1/extension-groups/${extensionGroupId}`, { method: "PUT", token: saved.api_token, body: { name: "REST Extension Group Updated" }, }, ); assert.equal( renamedExtensionGroup.response.status, 200, JSON.stringify(renamedExtensionGroup.value), ); assert.equal( renamedExtensionGroup.value.name, "REST Extension Group Updated", ); const membershipUrl = `${base}/v1/extension-groups/${extensionGroupId}/extensions/${extensionId}`; const joined = await jsonRequest(membershipUrl, { method: "POST", token: saved.api_token, }); assert.equal(joined.response.status, 200, JSON.stringify(joined.value)); assert.deepEqual(joined.value.extension_ids, [extensionId]); assert.deepEqual( ( await jsonRequest(`${base}/v1/extension-groups/${extensionGroupId}`, { token: saved.api_token, }) ).value.extension_ids, [extensionId], ); const left = await jsonRequest(membershipUrl, { method: "DELETE", token: saved.api_token, }); assert.equal(left.response.status, 200, JSON.stringify(left.value)); assert.deepEqual(left.value.extension_ids, []); assert.deepEqual( ( await jsonRequest(`${base}/v1/extension-groups/${extensionGroupId}`, { token: saved.api_token, }) ).value.extension_ids, [], ); // The whole path an automation client takes: an extension, a group holding // it, and a profile that will load that group the next time it launches. // Each piece already had coverage; the sequence did not, and it is the // sequence that has to work for extensions to be usable over REST at all. const launchProfile = await app.invoke("create_browser_profile_new", { name: "REST Extension Profile", browserStr: "wayfern", version: "150.0.7871.100", releaseType: "stable", proxyId: null, vpnId: null, // A stored fingerprint keeps this suite off the real browser; the // browser suite covers generation. wayfernConfig: { fingerprint: "{}" }, groupId: null, ephemeral: false, dnsBlocklist: null, launchHook: null, }); const launchGroup = await jsonRequest(`${base}/v1/extension-groups`, { method: "POST", token: saved.api_token, body: { name: "REST Launch Extension Group" }, }); assert.equal( launchGroup.response.status, 201, JSON.stringify(launchGroup.value), ); const launchGroupId = launchGroup.value.id; assert.deepEqual( ( await jsonRequest( `${base}/v1/extension-groups/${launchGroupId}/extensions/${archiveExtension.value.id}`, { method: "POST", token: saved.api_token }, ) ).value.extension_ids, [archiveExtension.value.id], ); const assigned = await jsonRequest( `${base}/v1/profiles/${launchProfile.id}`, { method: "PUT", token: saved.api_token, body: { extension_group_id: launchGroupId }, }, ); assert.equal(assigned.response.status, 200, JSON.stringify(assigned.value)); assert.equal(assigned.value.profile.id, launchProfile.id); // A caller that can set the assignment must be able to read it back without // dropping to the desktop app. assert.equal( assigned.value.profile.extension_group_id, launchGroupId, "the update response must echo the assignment it just made", ); assert.equal( ( await jsonRequest(`${base}/v1/profiles/${launchProfile.id}`, { token: saved.api_token, }) ).value.profile.extension_group_id, launchGroupId, "a fresh GET must report the assignment", ); // Also read it back through the surface the launcher itself resolves, so // the REST field and the launch path cannot drift apart. const assignedGroup = () => app.invoke("get_extension_group_for_profile", { profileId: launchProfile.id, }); assert.equal((await assignedGroup()).id, launchGroupId); assert.deepEqual((await assignedGroup()).extension_ids, [ archiveExtension.value.id, ]); // A group that does not exist used to be stored anyway and fail at launch, // far from the request that caused it. const missingExtensionGroup = await jsonRequest( `${base}/v1/profiles/${launchProfile.id}`, { method: "PUT", token: saved.api_token, body: { extension_group_id: "00000000-0000-0000-0000-0000000000ee" }, }, ); assert.equal( missingExtensionGroup.response.status, 404, JSON.stringify(missingExtensionGroup.value), ); assert.equal( (await assignedGroup()).id, launchGroupId, "a refused assignment must leave the previous one in place", ); assert.equal( ( await jsonRequest(`${base}/v1/profiles/${launchProfile.id}`, { method: "PUT", token: saved.api_token, body: { extension_group_id: "" }, }) ).response.status, 200, ); assert.equal(await assignedGroup(), null); assert.equal( ( await jsonRequest(`${base}/v1/profiles/${launchProfile.id}`, { token: saved.api_token, }) ).value.profile.extension_group_id, null, "clearing the assignment must be visible over REST too", ); assert.equal( ( await jsonRequest(`${base}/v1/extension-groups/${launchGroupId}`, { method: "DELETE", token: saved.api_token, }) ).response.status, 204, ); await app.invoke("delete_profile", { profileId: launchProfile.id }); for (const id of [ extensionId, archiveExtension.value.id, oversized.value.id, localized.value.id, unresolved.value.id, ]) { assert.equal( ( await jsonRequest(`${base}/v1/extensions/${id}`, { method: "DELETE", token: saved.api_token, }) ).response.status, 204, ); } assert.equal( ( await jsonRequest(`${base}/v1/extension-groups/${extensionGroupId}`, { method: "DELETE", token: saved.api_token, }) ).response.status, 204, ); const missing = await jsonRequest(`${base}/v1/groups/missing`, { token: saved.api_token, }); assert.equal(missing.response.status, 404); const invalidProfile = await jsonRequest(`${base}/v1/profiles`, { method: "POST", token: saved.api_token, body: { name: "Bad", browser: "unsupported", version: "latest" }, }); assert.equal(invalidProfile.response.status, 400); assert.equal( ( await jsonRequest(`${base}/v1/proxies/${proxyId}`, { method: "DELETE", token: saved.api_token, }) ).response.status, 204, ); assert.equal( ( await jsonRequest(`${base}/v1/proxies/${vlessProxyId}`, { method: "DELETE", token: saved.api_token, }) ).response.status, 204, ); for (const importedProxy of imported.value.proxies) { await jsonRequest(`${base}/v1/proxies/${importedProxy.id}`, { method: "DELETE", token: saved.api_token, }); } assert.equal( ( await jsonRequest(`${base}/v1/groups/${groupId}`, { method: "DELETE", token: saved.api_token, }) ).response.status, 204, ); await app.invoke("stop_api_server"); assert.equal(await app.invoke("get_api_server_status"), null); const stoppedDiagnostic = await app.invoke("check_integration_connection", { target: "api", }); assert.equal(stoppedDiagnostic.configured, true); assert.equal(stoppedDiagnostic.reachable, false); assert.equal(stoppedDiagnostic.authorized, null); }); }); test("local MCP is removed: enabling it and installing a local client are refused", async () => { await withApp("integrations-mcp", async (app) => { await seedTerms(app); // Enabling the local server is refused with the removal code (it used to // start a loopback MCP server), so nothing is left listening as a result. await assertCommandErrorCode(app, "start_mcp_server", "MCP_LOCAL_REMOVED"); assert.equal(await app.invoke("get_mcp_server_status"), false); // Nothing is configured for a server that no longer exists. assert.equal(await app.invoke("get_mcp_config"), null); // stop is a no-op when nothing is running. await assertCommandErrorCode( app, "stop_mcp_server", "MCP_SERVER_NOT_RUNNING", ); // The client roster still resolves, so the Integrations page can offer the // remote endpoint and show which clients are still on the removed local one. const agents = await app.invoke("list_mcp_agents"); assert.ok(agents.some((agent) => agent.id === "cursor")); // fx cannot take the bearer from its config file, so the page tells the // user which variable to export; that name travels with the row. assert.equal( agents.find((agent) => agent.id === "fx").token_env, "DONUT_MCP_TOKEN", ); assert.equal(agents.find((agent) => agent.id === "cursor").token_env, null); // An unknown agent and an unknown target keep their own distinct errors. await assertCommandErrorCode(app, "add_mcp_to_agent", "MCP_AGENT_UNKNOWN", { agentId: "missing-e2e-agent", target: "remote", }); await assertCommandErrorCode( app, "remove_mcp_from_agent", "MCP_AGENT_UNKNOWN", { agentId: "missing-e2e-agent" }, ); await assertCommandErrorCode(app, "add_mcp_to_agent", "INTERNAL_ERROR", { agentId: "cursor", target: "bogus", }); // Installing a client to the LOCAL endpoint is refused now (it used to // write a local config), and nothing is written. await assertCommandErrorCode(app, "add_mcp_to_agent", "MCP_LOCAL_REMOVED", { agentId: "cursor", target: "local", }); assert.equal( (await app.invoke("list_mcp_agents")).find( (agent) => agent.id === "cursor", ).connected, false, "a refused local install must not have written an entry", ); // Remote MCP with no stored `dmk_` credential is refused with a code the UI // can explain, and writes nothing into the client's config. await assertCommandErrorCode( app, "add_mcp_to_agent", "MCP_REMOTE_KEY_MISSING", { agentId: "cursor", target: "remote" }, ); assert.equal( (await app.invoke("list_mcp_agents")).find( (agent) => agent.id === "cursor", ).connected, false, "a refused remote install must not have written an entry", ); }); }); test("the remote-control bridge refuses a signed-out desktop and stays off", async () => { await withApp("integrations-mcp-remote", async (app) => { await seedTerms(app); // Off by default, and it must stay that way without an explicit opt-in: the // bridge hands Donut cloud the ability to drive this browser, which is not // something to switch on for somebody because their plan allows it. const settings = await app.invoke("get_app_settings"); assert.equal(settings.mcp_remote_enabled, false); const initial = await app.invoke("get_mcp_remote_status"); assert.equal(initial.enabled, false); assert.equal(initial.connected, false); assert.equal(initial.lastError, null); assert.ok(initial.instanceId.length >= 8); // Stable across calls, and persisted: the id is how this desktop reclaims // its own connection after a blip, so a desktop that renamed itself on // every read could never do so. assert.equal( (await app.invoke("get_mcp_remote_status")).instanceId, initial.instanceId, ); // A signed-out desktop has no credential to authenticate the socket with, // so it is refused here rather than allowed to open one and be dropped. assert.match( await app.invokeError("start_mcp_remote_bridge"), /"code":"MCP_REMOTE_REQUIRES_SIGN_IN"/, ); // The bridge must not merely be un-enabled: it must not have been STARTED. // The sign-in gate sits above `mcp_remote::start`, and `enabled` is the only // half that says so: it is `is_running()`, which `start` flips synchronously // before it spawns the reconnect loop. `connected` is not a substitute: // it only goes true once the socket authenticates, which a signed-out // desktop never manages, so it reads false whether or not the bridge was // started and is dialling donutbrowser.com in the background. const afterRefusedStart = await app.invoke("get_mcp_remote_status"); assert.equal( afterRefusedStart.enabled, false, "a refused start must not have started the bridge task", ); assert.equal( afterRefusedStart.connected, false, "a refused start must not have opened a socket", ); assert.equal( (await app.invoke("get_app_settings")).mcp_remote_enabled, false, "a refused start must not leave the setting on, or the next launch dials a socket the user never enabled", ); // Stopping something that is not running is a no-op, not an error: the // desktop calls this on sign-out and on quit, and both must be safe. const stoppedWhileOff = await app.invoke("stop_mcp_remote_bridge"); assert.equal(stoppedWhileOff.enabled, false); assert.equal(stoppedWhileOff.connected, false); assert.equal(stoppedWhileOff.instanceId, initial.instanceId); // The no-op case says nothing about the PERSISTED effect, and that is the // half that matters: `ensure_remote_bridge` re-opens the bridge on the next // launch (and on the next sign-in, and on the ten-minute reconnect tick) // from `mcp_remote_enabled` alone. So put the flag on disk the way a // previously opted-in session would have left it, and stop again. // // Every field of the returned status is computed from in-memory bridge // state (`enabled` is `is_running()`, `connected` is `is_connected()`), so // dropping the settings write inside `stop_mcp_remote_bridge` leaves all // three assertions above green while the internet-facing bridge comes back // by itself. Only the on-disk flag tells a real stop from `Ok(status())`. // // `save_app_settings` is NOT how the flag gets there. It belongs to the // start and stop commands alone: a settings save that carried it would // switch the internet-facing bridge on for the next launch without ever // passing the sign-in and terms gates those commands enforce. Prove the // save cannot flip it, then seed the file directly. const beforeSeed = await app.invoke("get_app_settings"); const saved = await app.invoke("save_app_settings", { settings: { ...beforeSeed, mcp_remote_enabled: true }, }); assert.equal( saved.mcp_remote_enabled, false, "a settings save must not be able to switch remote control on", ); assert.equal( (await app.invoke("get_app_settings")).mcp_remote_enabled, false, "nor may it reach disk through the save", ); const settingsFile = path.join( app.dataRoot, "data", "settings", "app_settings.json", ); const onDisk = JSON.parse(await readFile(settingsFile, "utf8")); await writeFile( settingsFile, `${JSON.stringify({ ...onDisk, mcp_remote_enabled: true }, null, 2)}\n`, ); assert.equal( (await app.invoke("get_app_settings")).mcp_remote_enabled, true, "the seeded opt-in must reach disk, or the stop below proves nothing", ); const stopped = await app.invoke("stop_mcp_remote_bridge"); assert.equal(stopped.enabled, false); assert.equal(stopped.connected, false); assert.equal(stopped.instanceId, initial.instanceId); assert.equal( (await app.invoke("get_app_settings")).mcp_remote_enabled, false, "stopping must clear the persisted opt-in, or the next launch re-opens the bridge the user just switched off", ); // Entitlement is asked of the SERVER, because neither of the local answers // is right: the cached entitlement is per-account, so an entitled // enterprise team MEMBER reads as unentitled, and an open socket only // proves the plan is active, not that remote control is allowed. // // Signed out there is no credential to ask with, and the honest outcome is // a clean error the dialog swallows to leave the local cache in charge: // never a crash, and never a fabricated `true`. const entitlementError = await app.invokeError( "get_remote_control_entitlement", ); assert.match(entitlementError, /INTERNAL_ERROR|Not logged in/); // The remote MCP credential, the `dmk_` key agents present to the remote // endpoint. A fresh desktop holds none, and reports exactly that: the // shape is `{ present, token_prefix }` with no plaintext anywhere in it. assert.deepEqual(await app.invoke("get_mcp_remote_credential"), { present: false, token_prefix: null, }); // Minting needs the session, so a signed-out desktop is refused with the // same code as the bridge, and refused BEFORE anything is stored. // // Signed in, the answer is `{ token_prefix, failed_clients }`: the key is // stored and its predecessor revoked before any client is rewritten, so a // client that could not be rewritten is named there rather than turning // the rotation into an error the dialog would answer by minting again. assert.match( await app.invokeError("rotate_mcp_remote_credential"), /"code":"MCP_REMOTE_REQUIRES_SIGN_IN"/, ); assert.equal( (await app.invoke("get_mcp_remote_credential")).present, false, "a refused rotation must not have stored a credential", ); // Forgetting nothing is a no-op, not an error: sign-out and the // Integrations page both reach it without checking first. await app.invoke("forget_mcp_remote_credential"); assert.deepEqual(await app.invoke("get_mcp_remote_credential"), { present: false, token_prefix: null, }); // The local MCP server is a separate transport and is unaffected either way. assert.equal(await app.invoke("get_mcp_server_status"), false); }); }); test("the remote-control bridge refuses before the terms are accepted", async () => { // `start_mcp_remote_bridge` has TWO gates (the Wayfern terms, then the // signed-in check), and every other test seeds the terms first, so only the // second one was ever reached. The first could have been deleted with the // whole suite green, which would let a desktop that never accepted the terms // open an internet-facing hook into itself. // // `wayfernTermsAccepted: false` is what makes this session different: the // harness seeds the acceptance file by DEFAULT, so merely omitting the // explicit `seedTerms` call leaves the terms accepted and this test proves // nothing (it first ran that way and hit the sign-in gate instead). await withApp( "integrations-mcp-remote-terms", async (app) => { assert.match( await app.invokeError("start_mcp_remote_bridge"), /"code":"WAYFERN_TERMS_REQUIRED"/, ); const status = await app.invoke("get_mcp_remote_status"); // `enabled` first, for the same reason as the sign-in gate: it is // `is_running()` and flips inside `mcp_remote::start`, so it is what // catches a gate that stopped sitting above the start. `connected` alone // would stay false on a bridge that was started and merely never got a // socket up. assert.equal( status.enabled, false, "the bridge may not have been started", ); assert.equal(status.connected, false, "no socket may have been opened"); assert.equal( (await app.invoke("get_app_settings")).mcp_remote_enabled, false, "a terms refusal must not leave the setting on either", ); }, { wayfernTermsAccepted: false }, ); }); test("REST browser automation requests hit the shared automation rate limit", async () => { await withApp( "integrations-rate-limit", async (app) => { await seedTerms(app); const settings = await app.invoke("get_app_settings"); const saved = await app.invoke("save_app_settings", { settings: { ...settings, api_enabled: true, api_port: 0, api_token: null, onboarding_completed: true, }, }); const apiPort = await app.invoke("start_api_server", { port: 0 }); const apiBase = `http://127.0.0.1:${apiPort}`; const missingProfileId = "00000000-0000-0000-0000-000000000000"; // The shared automation limiter sits innermost, past auth, so an // authenticated automation call consumes a token even when the profile // is missing (404). With the window set to 2/hour below the contract is // exact: 404, 404, then 429 with a Retry-After. The limiter is shared // with the MCP tool engine, whose branch has no automated coverage now // that the loopback endpoint is gone: it needs the e2e-only override // that `cargo test --lib` does not compile. const run = () => jsonRequest(`${apiBase}/v1/profiles/${missingProfileId}/run`, { method: "POST", token: saved.api_token, body: {}, }); assert.equal((await run()).response.status, 404); assert.equal((await run()).response.status, 404); const limited = await run(); assert.equal(limited.response.status, 429); assert.ok(Number(limited.response.headers.get("retry-after")) > 0); // Only automation calls spend the budget: a plain read still answers. const listed = await jsonRequest(`${apiBase}/v1/profiles`, { method: "GET", token: saved.api_token, }); assert.equal(listed.response.status, 200); await app.invoke("stop_api_server"); }, { extraEnv: { DONUT_E2E_REQUESTS_PER_HOUR: "2", WAYFERN_TEST_TOKEN: "donut-e2e-rate-limit", }, }, ); }); test("offline cloud, update, team-lock, trial, and synchronizer contracts are deterministic", async () => { await withApp( "integrations-contracts", async (app) => { // Local MCP is removed: the enable command refuses uniformly with the // removal code, regardless of whether the terms have been accepted. await assertCommandErrorCode( app, "start_mcp_server", "MCP_LOCAL_REMOVED", ); assert.equal(await app.invoke("cloud_get_user"), null); assert.equal(await app.invoke("cloud_get_proxy_usage"), null); assert.ok(await app.invoke("cloud_get_wayfern_token")); assert.deepEqual(await app.invoke("get_team_locks"), []); assert.equal( await app.invoke("get_team_lock_status", { profileId: "00000000-0000-0000-0000-000000000000", }), null, ); assert.deepEqual(await app.invoke("get_sync_sessions"), []); const startResult = await invokeContract(app, "start_sync_session", { leaderProfileId: "00000000-0000-0000-0000-000000000001", followerProfileIds: ["00000000-0000-0000-0000-000000000002"], }); assert.equal(startResult.ok, false); const stopError = await app.invokeError("stop_sync_session", { sessionId: "missing", }); assert.match(stopError, /not found|session/i); const removeError = await app.invokeError("remove_sync_follower", { sessionId: "missing", followerProfileId: "missing", }); assert.match(removeError, /not found|session/i); // The controls a person uses on a live session. Without one to act on, // each has to refuse by its own code rather than pretend it worked: the // panel reads these back, and a silent success would leave a button // claiming a state the backend never entered. assert.match( await app.invokeError("set_sync_session_paused", { sessionId: "missing", paused: true, }), /SYNC_SESSION_NOT_FOUND/, ); assert.match( await app.invokeError("set_sync_follower_held", { sessionId: "missing", followerProfileId: "missing", held: true, }), /SYNC_SESSION_NOT_FOUND/, ); assert.match( await app.invokeError("arrange_sync_windows", { sessionId: "missing", layout: "grid", }), /SYNC_SESSION_NOT_FOUND/, ); // An unknown layout never reaches the display: it fails to deserialise. assert.ok( await app.invokeError("arrange_sync_windows", { sessionId: "missing", layout: "diagonal", }), ); assert.equal(await app.invoke("check_for_app_updates"), null); assert.equal(await app.invoke("check_for_app_updates_manual"), null); await assertContract( app, "cloud_exchange_device_code", { // Any of these proves the command's BODY ran and reached its network // layer, which is what the evidence is for. Pinning only the server's // "invalid or expired login code" sentence made a test named // "offline ... deterministic" depend on a live round-trip to // api.donutbrowser.com: red offline, behind a proxy, when the // unauthenticated challenge is rate-limited, or the day the backend // rewords it, with no signal that the desktop is fine. refusedWith: /invalid or expired login code|failed to fetch challenge|challenge request failed/i, }, { code: "DONUT-E2E-INVALID-CODE" }, ); await assertContract(app, "cloud_refresh_profile", { refusedWith: /not logged in/i, }); await assertContract(app, "cloud_get_countries", { refusedWith: /not logged in/i, }); await assertContract( app, "create_cloud_location_proxy", { refusedWith: /no cloud proxy available/i }, { name: "E2E unavailable cloud proxy", country: "ZZ", region: null, city: null, isp: null, }, ); await assertContract(app, "cloud_refresh_wayfern_token", { // Compared against the value the harness injected, NOT matched against // a hex shape. Under the `e2e` feature this command returns // WAYFERN_TEST_TOKEN, so a /^[0-9a-f]{32,}$/ assertion only proved the // harness's own env var looks like a token: it validated the fixture // and would have passed with the command's body deleted. Equality // proves the command actually reached the token and returned it. answers: (token) => assert.equal( String(token), process.env.WAYFERN_TEST_TOKEN, "the command must return the token it was given, not a different value", ), }); await assertContract(app, "trigger_manual_version_update", { answers: (report) => { assert.ok( Array.isArray(report), "the update run must report per browser", ); // Non-empty, or the per-entry loop below asserts nothing at all: an // `[]` satisfied every check while the command did no work. assert.ok( report.length > 0, `the update run must report at least one browser, got ${JSON.stringify(report)}`, ); for (const entry of report) { assert.ok( typeof entry.browser === "string" && entry.browser.length > 0, `every entry names its browser: ${JSON.stringify(entry)}`, ); assert.equal(typeof entry.updated_successfully, "boolean"); } }, }); await assertContract(app, "clear_all_version_cache_and_refetch", { answers: (value) => assert.equal(value, null), }); await assertContract(app, "check_for_browser_updates", { answers: (updates) => assert.ok( Array.isArray(updates), `the update check must answer with a list, got ${JSON.stringify(updates)}`, ), }); await app.invoke("dismiss_update_notification", { notificationId: "missing-e2e-notification", }); assert.deepEqual( await app.invoke("complete_browser_update_with_auto_update", { browser: "wayfern", newVersion: "150.0.7871.100", }), [], ); const prepareError = await app.invokeError( "download_and_prepare_app_update", { updateInfo: { current_version: "0.0.0", new_version: "0.0.1-e2e", release_notes: "E2E invalid update contract", download_url: `${process.env.DONUT_E2E_FIXTURE_URL}/invalid-update.zip`, is_nightly: false, published_at: "2026-01-01T00:00:00Z", manual_update_required: false, release_page_url: null, repo_update: false, checksums_url: null, asset_digest: null, }, }, ); assert.match(prepareError, /checksum|verif|Failed to download/i); const versionStatus = await app.invoke("get_version_update_status"); assert.ok(versionStatus && typeof versionStatus === "object"); assert.equal(typeof (await app.invoke("is_default_browser")), "boolean"); // Remote sessions and the cookie bot are brokered by the cloud backend. // Signed out, every one of them must fail as a code the UI can // translate — a raw English string from the transport would reach the // user untranslated, which is what the {"code":…} convention prevents. const notSignedIn = /"code":"CLOUD_NOT_SIGNED_IN"/; const missingProfileId = "00000000-0000-0000-0000-0000000000ff"; assert.match(await app.invokeError("list_remote_sessions"), notSignedIn); assert.match( await app.invokeError("get_remote_session", { sessionId: "missing-e2e-session", }), notSignedIn, ); assert.match( await app.invokeError("stop_remote_session", { sessionId: "missing-e2e-session", }), notSignedIn, ); // The local-launch gate. Nothing has run remotely in this session, so it // is empty — but it must answer, because a UI that cannot read it shows // an enabled Run button over a profile the backend will refuse. const handoff = await app.invoke("get_remote_handoff_states"); assert.ok( handoff && typeof handoff === "object" && !Array.isArray(handoff), "the handoff gate must answer with a profile-keyed object", ); assert.equal(Object.keys(handoff).length, 0); // The transition stream is what the desktop uses instead of polling, so // its subscriber has to start, report itself, and stop on demand. Both // calls are repeated: a second start must not open a second socket, and // a second stop must not fail. assert.equal(await app.invoke("get_remote_session_events_status"), false); await app.invoke("start_remote_session_events"); assert.equal(await app.invoke("get_remote_session_events_status"), true); await app.invoke("start_remote_session_events"); assert.equal(await app.invoke("get_remote_session_events_status"), true); await app.invoke("stop_remote_session_events"); assert.equal(await app.invoke("get_remote_session_events_status"), false); await app.invoke("stop_remote_session_events"); assert.equal(await app.invoke("get_remote_session_events_status"), false); assert.match( await app.invokeError("get_cookie_bot_schedules", { scope: "mine" }), notSignedIn, ); assert.match( await app.invokeError("get_cookie_bot_schedule", { profileId: missingProfileId, }), notSignedIn, ); assert.match( await app.invokeError("delete_cookie_bot_schedule", { profileId: missingProfileId, }), notSignedIn, ); // Saved site lists are cloud-backed like the schedules above, so they // must refuse the same way rather than appearing to work offline. assert.match( await app.invokeError("get_cookie_bot_user_templates", {}), notSignedIn, ); assert.match( await app.invokeError("create_cookie_bot_user_template", { name: "e2e list", sites: ["example.com"], }), notSignedIn, ); assert.match( await app.invokeError("update_cookie_bot_user_template", { id: "00000000-0000-0000-0000-000000000000", name: "renamed", sites: null, }), notSignedIn, ); assert.match( await app.invokeError("delete_cookie_bot_user_template", { id: "00000000-0000-0000-0000-000000000000", }), notSignedIn, ); assert.match( await app.invokeError("check_cookie_bot_conflicts", { profileId: missingProfileId, runAtMinute: 120, daysMask: 127, }), notSignedIn, ); assert.match( await app.invokeError("get_cookie_bot_runs", { limit: 10 }), notSignedIn, ); assert.match( await app.invokeError("cancel_cookie_bot_run", { runId: "missing-e2e-run", }), notSignedIn, ); assert.match( await app.invokeError("get_cookie_bot_presets"), notSignedIn, ); assert.match( await app.invokeError("get_remote_hours_quota"), notSignedIn, ); assert.match( await app.invokeError("get_cookie_bot_usage", { period: "2026-01" }), notSignedIn, ); // Enrolling and running act on a profile this machine holds: both are // refused before any network call when it does not exist, so a bad id // can never reach a leased host or an hour of the pooled budget. assert.match( await app.invokeError("save_cookie_bot_schedule", { profileId: missingProfileId, schedule: { profile_name: "E2E missing profile", platform: "windows", enabled: true, run_at_minute: 120, days_mask: 127, timezone: "UTC", preset: "balanced", max_minutes: 60, sites: ["https://example.com"], }, acknowledgeConflict: false, }), /"code":"PROFILE_NOT_FOUND"/, ); assert.match( await app.invokeError("run_cookie_bot_now", { profileId: missingProfileId, maxMinutes: 30, }), /"code":"PROFILE_NOT_FOUND"/, ); // The agent plane is brokered by the same cloud. Signed out, every read // and write must refuse as a translatable code, and the two things the // desktop can judge for itself — is this profile here, does the goal say // anything — must be judged BEFORE any of that, so a bad request never // becomes a model bill. assert.match( await app.invokeError("get_agent_runs", { limit: 5 }), notSignedIn, ); assert.match( await app.invokeError("get_agent_run", { runId: "missing-e2e-run" }), notSignedIn, ); assert.match( await app.invokeError("cancel_agent_run", { runId: "missing-e2e-run" }), notSignedIn, ); assert.match(await app.invokeError("get_agent_recipes"), notSignedIn); assert.match( await app.invokeError("create_agent_recipe", { name: "E2E recipe", steps: [{ type: "navigate", url: "https://example.com" }], }), notSignedIn, ); assert.match( await app.invokeError("update_agent_recipe", { id: "00000000-0000-0000-0000-000000000000", name: "E2E recipe", steps: [{ type: "navigate", url: "https://example.com" }], }), notSignedIn, ); assert.match( await app.invokeError("delete_agent_recipe", { id: "00000000-0000-0000-0000-000000000000", }), notSignedIn, ); // A goal is judged before the profile is even looked up: an empty goal is // the one refusal that costs nothing to make locally, and it must not // depend on being signed in. assert.match( await app.invokeError("start_agent_run", { input: { profileId: missingProfileId, target: "desktop", goal: " ", }, }), /"code":"AGENT_GOAL_INVALID"/, ); assert.match( await app.invokeError("start_agent_run", { input: { profileId: missingProfileId, target: "desktop", goal: "Open the dashboard and export last week's report", }, }), /"code":"PROFILE_NOT_FOUND"/, ); // Recipes are validated the same way, and with the code the rest of the // app already uses for a blank name rather than an agent-specific one. assert.match( await app.invokeError("create_agent_recipe", { name: " ", steps: [{ type: "navigate", url: "https://example.com" }], }), /"code":"NAME_CANNOT_BE_EMPTY"/, ); // A step is an object the API validates, never a line of prose: the old // string shape is refused here rather than sent and rejected by the // server. So is a step that names an element without saying which. for (const steps of [ [], ["open the dashboard"], [{ url: "https://example.com" }], [{ type: "teleport" }], [{ type: "click" }], [{ type: "click", selector: "#buy", locator: { role: "button" } }], ]) { assert.match( await app.invokeError("create_agent_recipe", { name: "E2E recipe", steps, }), /"code":"AGENT_RECIPE_INVALID"/, `${JSON.stringify(steps)} must be refused before any network call`, ); } // The step stream is how the run panel fills; without it a page opened // during a run shows a goal and nothing else. It has to start, name the // run it is watching, follow a switch to another run, and stop on demand. assert.equal(await app.invoke("get_agent_run_events_status"), null); await app.invoke("start_agent_run_events", { runId: "e2e-agent-run-1" }); assert.equal( await app.invoke("get_agent_run_events_status"), "e2e-agent-run-1", ); // A second start for the same run is a no-op, not a second socket. await app.invoke("start_agent_run_events", { runId: "e2e-agent-run-1" }); assert.equal( await app.invoke("get_agent_run_events_status"), "e2e-agent-run-1", ); // Opening a different run replaces the stream: the panel shows one run. await app.invoke("start_agent_run_events", { runId: "e2e-agent-run-2" }); assert.equal( await app.invoke("get_agent_run_events_status"), "e2e-agent-run-2", ); await app.invoke("stop_agent_run_events"); assert.equal(await app.invoke("get_agent_run_events_status"), null); // A second stop must not fail. await app.invoke("stop_agent_run_events"); assert.equal(await app.invoke("get_agent_run_events_status"), null); const trial = await app.invoke("get_commercial_trial_status"); assert.ok(trial && typeof trial === "object"); await app.invoke("acknowledge_trial_expiration"); assert.equal(await app.invoke("has_acknowledged_trial_expiration"), true); await app.invoke("cloud_logout"); assert.equal(await app.invoke("cloud_get_user"), null); }, { wayfernTermsAccepted: false }, ); });