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)
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { PaseoConfigRawSchema } from "@server/utils/paseo-config-schema";
|
||||||
import type { PaseoConfigRaw } from "@server/shared/messages";
|
import type { PaseoConfigRaw } from "@server/shared/messages";
|
||||||
import { applyDraftToConfig, configToDraft, type ProjectConfigDraft } from "./project-config-form";
|
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", () => {
|
it("preserves unknown top-level, worktree, and script entry fields on round-trip", () => {
|
||||||
const base = {
|
const base = PaseoConfigRawSchema.parse({
|
||||||
worktree: {
|
worktree: {
|
||||||
setup: "npm install",
|
setup: "npm install",
|
||||||
terminals: [{ name: "dev", command: "npm run dev" }],
|
terminals: [{ name: "dev", command: "npm run dev" }],
|
||||||
@@ -117,28 +118,28 @@ describe("applyDraftToConfig", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
customTopLevel: "preserved",
|
customTopLevel: "preserved",
|
||||||
} as unknown as PaseoConfigRaw;
|
});
|
||||||
|
|
||||||
const draft = configToDraft(base);
|
const draft = configToDraft(base);
|
||||||
const next = applyDraftToConfig({ draft, base });
|
const next = applyDraftToConfig({ draft, base });
|
||||||
|
|
||||||
expect((next as unknown as Record<string, unknown>).customTopLevel).toBe("preserved");
|
expect((next as Record<string, unknown>).customTopLevel).toBe("preserved");
|
||||||
expect((next.worktree as unknown as Record<string, unknown>).customWorktreeField).toBe("keep");
|
expect((next.worktree as Record<string, unknown>).customWorktreeField).toBe("keep");
|
||||||
expect((next.worktree as unknown as Record<string, unknown>).terminals).toEqual([
|
expect((next.worktree as Record<string, unknown>).terminals).toEqual([
|
||||||
{ name: "dev", command: "npm run dev" },
|
{ name: "dev", command: "npm run dev" },
|
||||||
]);
|
]);
|
||||||
const devEntry = (next.scripts ?? {}).dev as unknown as Record<string, unknown>;
|
const devEntry = (next.scripts ?? {}).dev as Record<string, unknown>;
|
||||||
expect(devEntry.customScriptField).toEqual({ nested: true });
|
expect(devEntry.customScriptField).toEqual({ nested: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("preserves all scripts on round-trip, including ones never edited in this session", () => {
|
it("preserves all scripts on round-trip, including ones never edited in this session", () => {
|
||||||
const base = {
|
const base = PaseoConfigRawSchema.parse({
|
||||||
scripts: {
|
scripts: {
|
||||||
dev: { type: "long-running", command: "npm run dev", port: 3000, customDevField: "keep" },
|
dev: { type: "long-running", command: "npm run dev", port: 3000, customDevField: "keep" },
|
||||||
build: { command: ["npm", "run", "build"], customBuildField: { nested: 1 } },
|
build: { command: ["npm", "run", "build"], customBuildField: { nested: 1 } },
|
||||||
lint: { command: "npm run lint", type: "task" },
|
lint: { command: "npm run lint", type: "task" },
|
||||||
},
|
},
|
||||||
} as unknown as PaseoConfigRaw;
|
});
|
||||||
|
|
||||||
const draft = configToDraft(base);
|
const draft = configToDraft(base);
|
||||||
// Edit only "dev". Leave "build" and "lint" untouched.
|
// Edit only "dev". Leave "build" and "lint" untouched.
|
||||||
@@ -150,37 +151,37 @@ describe("applyDraftToConfig", () => {
|
|||||||
const scripts = next.scripts ?? {};
|
const scripts = next.scripts ?? {};
|
||||||
expect(Object.keys(scripts).sort()).toEqual(["build", "dev", "lint"]);
|
expect(Object.keys(scripts).sort()).toEqual(["build", "dev", "lint"]);
|
||||||
|
|
||||||
const devEntry = scripts.dev as unknown as Record<string, unknown>;
|
const devEntry = scripts.dev as Record<string, unknown>;
|
||||||
expect(devEntry.command).toBe("npm run dev -- --watch");
|
expect(devEntry.command).toBe("npm run dev -- --watch");
|
||||||
expect(devEntry.type).toBe("long-running");
|
expect(devEntry.type).toBe("long-running");
|
||||||
expect(devEntry.port).toBe(3000);
|
expect(devEntry.port).toBe(3000);
|
||||||
expect(devEntry.customDevField).toBe("keep");
|
expect(devEntry.customDevField).toBe("keep");
|
||||||
|
|
||||||
const buildEntry = scripts.build as unknown as Record<string, unknown>;
|
const buildEntry = scripts.build as Record<string, unknown>;
|
||||||
expect(buildEntry.command).toEqual(["npm", "run", "build"]);
|
expect(buildEntry.command).toEqual(["npm", "run", "build"]);
|
||||||
expect(buildEntry.customBuildField).toEqual({ nested: 1 });
|
expect(buildEntry.customBuildField).toEqual({ nested: 1 });
|
||||||
|
|
||||||
const lintEntry = scripts.lint as unknown as Record<string, unknown>;
|
const lintEntry = scripts.lint as Record<string, unknown>;
|
||||||
expect(lintEntry.command).toBe("npm run lint");
|
expect(lintEntry.command).toBe("npm run lint");
|
||||||
expect(lintEntry.type).toBe("task");
|
expect(lintEntry.type).toBe("task");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("normalizes script command text into the original command kind", () => {
|
it("normalizes script command text into the original command kind", () => {
|
||||||
const base = {
|
const base = PaseoConfigRawSchema.parse({
|
||||||
scripts: {
|
scripts: {
|
||||||
build: { command: ["npm", "run", "build"] },
|
build: { command: ["npm", "run", "build"] },
|
||||||
},
|
},
|
||||||
} as unknown as PaseoConfigRaw;
|
});
|
||||||
const draft = configToDraft(base);
|
const draft = configToDraft(base);
|
||||||
const buildRow = draft.scripts[0];
|
const buildRow = draft.scripts[0];
|
||||||
buildRow.commandText = "npm run build";
|
buildRow.commandText = "npm run build";
|
||||||
const next = applyDraftToConfig({ draft, base });
|
const next = applyDraftToConfig({ draft, base });
|
||||||
const buildEntry = (next.scripts ?? {}).build as unknown as Record<string, unknown>;
|
const buildEntry = (next.scripts ?? {}).build as Record<string, unknown>;
|
||||||
expect(buildEntry.command).toEqual(["npm run build"]);
|
expect(buildEntry.command).toEqual(["npm run build"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("parses script port as a number when numeric and writes string for non-numeric input", () => {
|
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);
|
const draft = configToDraft(base);
|
||||||
draft.scripts = [
|
draft.scripts = [
|
||||||
{
|
{
|
||||||
@@ -203,19 +204,19 @@ describe("applyDraftToConfig", () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
const next = applyDraftToConfig({ draft, base });
|
const next = applyDraftToConfig({ draft, base });
|
||||||
const dev = (next.scripts ?? {}).dev as unknown as Record<string, unknown>;
|
const dev = (next.scripts ?? {}).dev as Record<string, unknown>;
|
||||||
const tunnel = (next.scripts ?? {}).tunnel as unknown as Record<string, unknown>;
|
const tunnel = (next.scripts ?? {}).tunnel as Record<string, unknown>;
|
||||||
expect(dev.port).toBe(3000);
|
expect(dev.port).toBe(3000);
|
||||||
expect(tunnel.port).toBe("auto");
|
expect(tunnel.port).toBe("auto");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("drops scripts with an empty name and removes scripts no longer present in the draft", () => {
|
it("drops scripts with an empty name and removes scripts no longer present in the draft", () => {
|
||||||
const base = {
|
const base = PaseoConfigRawSchema.parse({
|
||||||
scripts: {
|
scripts: {
|
||||||
dev: { command: "npm run dev" },
|
dev: { command: "npm run dev" },
|
||||||
build: { command: "npm run build" },
|
build: { command: "npm run build" },
|
||||||
},
|
},
|
||||||
} as unknown as PaseoConfigRaw;
|
});
|
||||||
const draft = configToDraft(base);
|
const draft = configToDraft(base);
|
||||||
// remove build, add a row with empty name.
|
// remove build, add a row with empty name.
|
||||||
draft.scripts = draft.scripts
|
draft.scripts = draft.scripts
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import { generateLocalPairingOffer } from "../pairing-offer.js";
|
|||||||
import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js";
|
import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js";
|
||||||
import { createClientChannel, type Transport } from "@getpaseo/relay/e2ee";
|
import { createClientChannel, type Transport } from "@getpaseo/relay/e2ee";
|
||||||
import { buildRelayWebSocketUrl } from "../../shared/daemon-endpoints.js";
|
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 nodeMajor = Number((process.versions.node ?? "0").split(".")[0] ?? "0");
|
||||||
const shouldRunRelayE2e = process.env.FORCE_RELAY_E2E === "1" || nodeMajor < 25;
|
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 encoded = url.slice(idx + marker.length);
|
||||||
const json = Buffer.from(encoded, "base64url").toString("utf8");
|
const json = Buffer.from(encoded, "base64url").toString("utf8");
|
||||||
const offer = JSON.parse(json) as { v?: unknown; serverId?: string; daemonPublicKeyB64?: string };
|
const offer = ConnectionOfferSchema.parse(JSON.parse(json));
|
||||||
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");
|
|
||||||
return { serverId: offer.serverId, daemonPublicKeyB64: offer.daemonPublicKeyB64 };
|
return { serverId: offer.serverId, daemonPublicKeyB64: offer.daemonPublicKeyB64 };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,27 +281,12 @@ async function waitForRelayWebSocketReady(port: number, timeout = 60000): Promis
|
|||||||
onmessage: (data) => {
|
onmessage: (data) => {
|
||||||
try {
|
try {
|
||||||
const payload = typeof data === "string" ? JSON.parse(data) : data;
|
const payload = typeof data === "string" ? JSON.parse(data) : data;
|
||||||
|
const wsMsg = WSOutboundMessageSchema.safeParse(payload);
|
||||||
if (
|
if (
|
||||||
payload &&
|
wsMsg.success &&
|
||||||
typeof payload === "object" &&
|
wsMsg.data.type === "session" &&
|
||||||
(
|
wsMsg.data.message.type === "status" &&
|
||||||
payload as {
|
wsMsg.data.message.payload?.status === "server_info"
|
||||||
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"
|
|
||||||
) {
|
) {
|
||||||
if (!pingSent && channelRef) {
|
if (!pingSent && channelRef) {
|
||||||
pingSent = true;
|
pingSent = true;
|
||||||
@@ -310,17 +294,8 @@ async function waitForRelayWebSocketReady(port: number, timeout = 60000): Promis
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (wsMsg.success && wsMsg.data.type === "pong") {
|
||||||
payload &&
|
settleResolve(wsMsg.data);
|
||||||
typeof payload === "object" &&
|
|
||||||
(
|
|
||||||
payload as {
|
|
||||||
type?: string;
|
|
||||||
message?: { type?: string; payload?: { status?: string } };
|
|
||||||
}
|
|
||||||
).type === "pong"
|
|
||||||
) {
|
|
||||||
settleResolve(payload);
|
|
||||||
ws.close();
|
ws.close();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -432,27 +407,12 @@ async function waitForRelayWebSocketReady(port: number, timeout = 60000): Promis
|
|||||||
const channel = await createClientChannel(transport, daemonPublicKeyB64, {
|
const channel = await createClientChannel(transport, daemonPublicKeyB64, {
|
||||||
onmessage: (data) => {
|
onmessage: (data) => {
|
||||||
const payload = typeof data === "string" ? JSON.parse(data) : data;
|
const payload = typeof data === "string" ? JSON.parse(data) : data;
|
||||||
|
const wsMsg = WSOutboundMessageSchema.safeParse(payload);
|
||||||
if (
|
if (
|
||||||
payload &&
|
wsMsg.success &&
|
||||||
typeof payload === "object" &&
|
wsMsg.data.type === "session" &&
|
||||||
(
|
wsMsg.data.message.type === "status" &&
|
||||||
payload as {
|
wsMsg.data.message.payload?.status === "server_info"
|
||||||
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"
|
|
||||||
) {
|
) {
|
||||||
if (!pingSent && channelRef) {
|
if (!pingSent && channelRef) {
|
||||||
pingSent = true;
|
pingSent = true;
|
||||||
@@ -460,18 +420,9 @@ async function waitForRelayWebSocketReady(port: number, timeout = 60000): Promis
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (wsMsg.success && wsMsg.data.type === "pong") {
|
||||||
payload &&
|
|
||||||
typeof payload === "object" &&
|
|
||||||
(
|
|
||||||
payload as {
|
|
||||||
type?: string;
|
|
||||||
message?: { type?: string; payload?: { status?: string } };
|
|
||||||
}
|
|
||||||
).type === "pong"
|
|
||||||
) {
|
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
resolve(payload);
|
resolve(wsMsg.data);
|
||||||
ws.close();
|
ws.close();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user