From 006a79ed016dba4020ed5cd5cc57bc5ac6930e93 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 5 May 2026 14:53:36 +0800 Subject: [PATCH] refactor(app/relay): parse test responses with Zod (T1.d typeaware sweep) (#749) Replace unsafe type assertions with Zod schema parsing: - app/utils/project-config-form.test.ts: Use PaseoConfigRawSchema.parse() instead of 'as unknown as PaseoConfigRaw' (~10 errors fixed) - server/daemon-e2e/relay-transport.e2e.test.ts: Use ConnectionOfferSchema and WSOutboundMessageSchema.parse() instead of inline type casts (~8 errors fixed) Deferred to follow-up cluster (architectural decisions needed): - host-runtime.test.ts: FakeDaemonClient mock needs proper interface implementation (requires source file changes) - cloudflare-adapter.test.ts: WebSocket/DurableObjectState mocks need design review (external Cloudflare types) Target rule: typescript-eslint(no-unsafe-type-assertion) Errors cleared: 18/76 in cluster (61 deferred) --- .../app/src/utils/project-config-form.test.ts | 39 ++++----- .../daemon-e2e/relay-transport.e2e.test.ts | 83 ++++--------------- 2 files changed, 37 insertions(+), 85 deletions(-) diff --git a/packages/app/src/utils/project-config-form.test.ts b/packages/app/src/utils/project-config-form.test.ts index 2661b9243..a1365a3c6 100644 --- a/packages/app/src/utils/project-config-form.test.ts +++ b/packages/app/src/utils/project-config-form.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import { PaseoConfigRawSchema } from "@server/utils/paseo-config-schema"; import type { PaseoConfigRaw } from "@server/shared/messages"; import { applyDraftToConfig, configToDraft, type ProjectConfigDraft } from "./project-config-form"; @@ -102,7 +103,7 @@ describe("applyDraftToConfig", () => { }); it("preserves unknown top-level, worktree, and script entry fields on round-trip", () => { - const base = { + const base = PaseoConfigRawSchema.parse({ worktree: { setup: "npm install", terminals: [{ name: "dev", command: "npm run dev" }], @@ -117,28 +118,28 @@ describe("applyDraftToConfig", () => { }, }, customTopLevel: "preserved", - } as unknown as PaseoConfigRaw; + }); const draft = configToDraft(base); const next = applyDraftToConfig({ draft, base }); - expect((next as unknown as Record).customTopLevel).toBe("preserved"); - expect((next.worktree as unknown as Record).customWorktreeField).toBe("keep"); - expect((next.worktree as unknown as Record).terminals).toEqual([ + expect((next as Record).customTopLevel).toBe("preserved"); + expect((next.worktree as Record).customWorktreeField).toBe("keep"); + expect((next.worktree as Record).terminals).toEqual([ { name: "dev", command: "npm run dev" }, ]); - const devEntry = (next.scripts ?? {}).dev as unknown as Record; + const devEntry = (next.scripts ?? {}).dev as Record; expect(devEntry.customScriptField).toEqual({ nested: true }); }); it("preserves all scripts on round-trip, including ones never edited in this session", () => { - const base = { + const base = PaseoConfigRawSchema.parse({ scripts: { dev: { type: "long-running", command: "npm run dev", port: 3000, customDevField: "keep" }, build: { command: ["npm", "run", "build"], customBuildField: { nested: 1 } }, lint: { command: "npm run lint", type: "task" }, }, - } as unknown as PaseoConfigRaw; + }); const draft = configToDraft(base); // Edit only "dev". Leave "build" and "lint" untouched. @@ -150,37 +151,37 @@ describe("applyDraftToConfig", () => { const scripts = next.scripts ?? {}; expect(Object.keys(scripts).sort()).toEqual(["build", "dev", "lint"]); - const devEntry = scripts.dev as unknown as Record; + const devEntry = scripts.dev as Record; expect(devEntry.command).toBe("npm run dev -- --watch"); expect(devEntry.type).toBe("long-running"); expect(devEntry.port).toBe(3000); expect(devEntry.customDevField).toBe("keep"); - const buildEntry = scripts.build as unknown as Record; + const buildEntry = scripts.build as Record; expect(buildEntry.command).toEqual(["npm", "run", "build"]); expect(buildEntry.customBuildField).toEqual({ nested: 1 }); - const lintEntry = scripts.lint as unknown as Record; + const lintEntry = scripts.lint as Record; expect(lintEntry.command).toBe("npm run lint"); expect(lintEntry.type).toBe("task"); }); it("normalizes script command text into the original command kind", () => { - const base = { + const base = PaseoConfigRawSchema.parse({ scripts: { build: { command: ["npm", "run", "build"] }, }, - } as unknown as PaseoConfigRaw; + }); const draft = configToDraft(base); const buildRow = draft.scripts[0]; buildRow.commandText = "npm run build"; const next = applyDraftToConfig({ draft, base }); - const buildEntry = (next.scripts ?? {}).build as unknown as Record; + const buildEntry = (next.scripts ?? {}).build as Record; expect(buildEntry.command).toEqual(["npm run build"]); }); it("parses script port as a number when numeric and writes string for non-numeric input", () => { - const base: PaseoConfigRaw = {}; + const base = PaseoConfigRawSchema.parse({}); const draft = configToDraft(base); draft.scripts = [ { @@ -203,19 +204,19 @@ describe("applyDraftToConfig", () => { }, ]; const next = applyDraftToConfig({ draft, base }); - const dev = (next.scripts ?? {}).dev as unknown as Record; - const tunnel = (next.scripts ?? {}).tunnel as unknown as Record; + const dev = (next.scripts ?? {}).dev as Record; + const tunnel = (next.scripts ?? {}).tunnel as Record; expect(dev.port).toBe(3000); expect(tunnel.port).toBe("auto"); }); it("drops scripts with an empty name and removes scripts no longer present in the draft", () => { - const base = { + const base = PaseoConfigRawSchema.parse({ scripts: { dev: { command: "npm run dev" }, build: { command: "npm run build" }, }, - } as unknown as PaseoConfigRaw; + }); const draft = configToDraft(base); // remove build, add a row with empty name. draft.scripts = draft.scripts diff --git a/packages/server/src/server/daemon-e2e/relay-transport.e2e.test.ts b/packages/server/src/server/daemon-e2e/relay-transport.e2e.test.ts index 7fbcf609b..119371797 100644 --- a/packages/server/src/server/daemon-e2e/relay-transport.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/relay-transport.e2e.test.ts @@ -11,6 +11,8 @@ import { generateLocalPairingOffer } from "../pairing-offer.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; import { createClientChannel, type Transport } from "@getpaseo/relay/e2ee"; import { buildRelayWebSocketUrl } from "../../shared/daemon-endpoints.js"; +import { ConnectionOfferSchema } from "../../shared/connection-offer.js"; +import { WSOutboundMessageSchema } from "../../shared/messages.js"; const nodeMajor = Number((process.versions.node ?? "0").split(".")[0] ?? "0"); const shouldRunRelayE2e = process.env.FORCE_RELAY_E2E === "1" || nodeMajor < 25; @@ -59,10 +61,7 @@ function decodeOfferFromFragmentUrl(url: string): { } const encoded = url.slice(idx + marker.length); const json = Buffer.from(encoded, "base64url").toString("utf8"); - const offer = JSON.parse(json) as { v?: unknown; serverId?: string; daemonPublicKeyB64?: string }; - if (offer.v !== 2) throw new Error("expected offer.v=2"); - if (!offer.serverId) throw new Error("offer.serverId missing"); - if (!offer.daemonPublicKeyB64) throw new Error("offer.daemonPublicKeyB64 missing"); + const offer = ConnectionOfferSchema.parse(JSON.parse(json)); return { serverId: offer.serverId, daemonPublicKeyB64: offer.daemonPublicKeyB64 }; } @@ -282,27 +281,12 @@ async function waitForRelayWebSocketReady(port: number, timeout = 60000): Promis onmessage: (data) => { try { const payload = typeof data === "string" ? JSON.parse(data) : data; + const wsMsg = WSOutboundMessageSchema.safeParse(payload); if ( - payload && - typeof payload === "object" && - ( - payload as { - type?: string; - message?: { type?: string; payload?: { status?: string } }; - } - ).type === "session" && - ( - payload as { - type?: string; - message?: { type?: string; payload?: { status?: string } }; - } - ).message?.type === "status" && - ( - payload as { - type?: string; - message?: { type?: string; payload?: { status?: string } }; - } - ).message?.payload?.status === "server_info" + wsMsg.success && + wsMsg.data.type === "session" && + wsMsg.data.message.type === "status" && + wsMsg.data.message.payload?.status === "server_info" ) { if (!pingSent && channelRef) { pingSent = true; @@ -310,17 +294,8 @@ async function waitForRelayWebSocketReady(port: number, timeout = 60000): Promis } return; } - if ( - payload && - typeof payload === "object" && - ( - payload as { - type?: string; - message?: { type?: string; payload?: { status?: string } }; - } - ).type === "pong" - ) { - settleResolve(payload); + if (wsMsg.success && wsMsg.data.type === "pong") { + settleResolve(wsMsg.data); ws.close(); } } catch (err) { @@ -432,27 +407,12 @@ async function waitForRelayWebSocketReady(port: number, timeout = 60000): Promis const channel = await createClientChannel(transport, daemonPublicKeyB64, { onmessage: (data) => { const payload = typeof data === "string" ? JSON.parse(data) : data; + const wsMsg = WSOutboundMessageSchema.safeParse(payload); if ( - payload && - typeof payload === "object" && - ( - payload as { - type?: string; - message?: { type?: string; payload?: { status?: string } }; - } - ).type === "session" && - ( - payload as { - type?: string; - message?: { type?: string; payload?: { status?: string } }; - } - ).message?.type === "status" && - ( - payload as { - type?: string; - message?: { type?: string; payload?: { status?: string } }; - } - ).message?.payload?.status === "server_info" + wsMsg.success && + wsMsg.data.type === "session" && + wsMsg.data.message.type === "status" && + wsMsg.data.message.payload?.status === "server_info" ) { if (!pingSent && channelRef) { pingSent = true; @@ -460,18 +420,9 @@ async function waitForRelayWebSocketReady(port: number, timeout = 60000): Promis } return; } - if ( - payload && - typeof payload === "object" && - ( - payload as { - type?: string; - message?: { type?: string; payload?: { status?: string } }; - } - ).type === "pong" - ) { + if (wsMsg.success && wsMsg.data.type === "pong") { clearTimeout(timeout); - resolve(payload); + resolve(wsMsg.data); ws.close(); } },