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)
238 lines
8.7 KiB
TypeScript
238 lines
8.7 KiB
TypeScript
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";
|
|
|
|
function emptyDraft(): ProjectConfigDraft {
|
|
return {
|
|
setupText: "",
|
|
setupOriginalKind: "missing",
|
|
teardownText: "",
|
|
teardownOriginalKind: "missing",
|
|
scripts: [],
|
|
};
|
|
}
|
|
|
|
describe("configToDraft", () => {
|
|
it("returns an empty draft for null config", () => {
|
|
expect(configToDraft(null)).toEqual(emptyDraft());
|
|
});
|
|
|
|
it("renders a string lifecycle command as a single textarea text and remembers the kind", () => {
|
|
const draft = configToDraft({
|
|
worktree: { setup: "npm install" },
|
|
});
|
|
expect(draft.setupText).toBe("npm install");
|
|
expect(draft.setupOriginalKind).toBe("string");
|
|
expect(draft.teardownText).toBe("");
|
|
expect(draft.teardownOriginalKind).toBe("missing");
|
|
});
|
|
|
|
it("renders an array lifecycle command as newline-separated text", () => {
|
|
const draft = configToDraft({
|
|
worktree: { teardown: ["docker compose down", "rm -rf .cache"] },
|
|
});
|
|
expect(draft.teardownText).toBe("docker compose down\nrm -rf .cache");
|
|
expect(draft.teardownOriginalKind).toBe("array");
|
|
});
|
|
|
|
it("converts a scripts record into draft rows with stable local ids", () => {
|
|
const draft = configToDraft({
|
|
scripts: {
|
|
dev: { type: "long-running", command: "npm run dev", port: 3000 },
|
|
build: { command: ["npm", "run", "build"] },
|
|
},
|
|
});
|
|
expect(draft.scripts).toHaveLength(2);
|
|
const [devRow, buildRow] = draft.scripts;
|
|
expect(devRow.name).toBe("dev");
|
|
expect(devRow.commandText).toBe("npm run dev");
|
|
expect(devRow.commandOriginalKind).toBe("string");
|
|
expect(devRow.type).toBe("long-running");
|
|
expect(devRow.portText).toBe("3000");
|
|
expect(devRow.id).toMatch(/^script-draft-\d+$/);
|
|
expect(buildRow.name).toBe("build");
|
|
expect(buildRow.commandText).toBe("npm\nrun\nbuild");
|
|
expect(buildRow.commandOriginalKind).toBe("array");
|
|
expect(buildRow.portText).toBe("");
|
|
expect(buildRow.id).not.toBe(devRow.id);
|
|
});
|
|
});
|
|
|
|
describe("applyDraftToConfig", () => {
|
|
it("preserves the original string kind when editing an existing setup field", () => {
|
|
const base: PaseoConfigRaw = { worktree: { setup: "npm install" } };
|
|
const draft = configToDraft(base);
|
|
draft.setupText = "npm install\nnpm run prepare";
|
|
const next = applyDraftToConfig({ draft, base });
|
|
expect(next.worktree?.setup).toBe("npm install\nnpm run prepare");
|
|
});
|
|
|
|
it("preserves the original array kind when editing an existing teardown field", () => {
|
|
const base: PaseoConfigRaw = {
|
|
worktree: { teardown: ["docker compose down"] },
|
|
};
|
|
const draft = configToDraft(base);
|
|
draft.teardownText = "docker compose down\nrm -rf .cache";
|
|
const next = applyDraftToConfig({ draft, base });
|
|
expect(next.worktree?.teardown).toEqual(["docker compose down", "rm -rf .cache"]);
|
|
});
|
|
|
|
it("writes a string for a newly added lifecycle field with one non-empty line", () => {
|
|
const base: PaseoConfigRaw = {};
|
|
const draft = configToDraft(base);
|
|
draft.setupText = "npm install";
|
|
const next = applyDraftToConfig({ draft, base });
|
|
expect(next.worktree?.setup).toBe("npm install");
|
|
});
|
|
|
|
it("writes an array for a newly added lifecycle field with multiple non-empty lines", () => {
|
|
const base: PaseoConfigRaw = {};
|
|
const draft = configToDraft(base);
|
|
draft.setupText = "npm install\nnpm run prepare";
|
|
const next = applyDraftToConfig({ draft, base });
|
|
expect(next.worktree?.setup).toEqual(["npm install", "npm run prepare"]);
|
|
});
|
|
|
|
it("omits a lifecycle field whose draft text is empty", () => {
|
|
const base: PaseoConfigRaw = { worktree: { setup: "npm install" } };
|
|
const draft = configToDraft(base);
|
|
draft.setupText = "";
|
|
const next = applyDraftToConfig({ draft, base });
|
|
expect(next.worktree?.setup).toBeUndefined();
|
|
});
|
|
|
|
it("preserves unknown top-level, worktree, and script entry fields on round-trip", () => {
|
|
const base = PaseoConfigRawSchema.parse({
|
|
worktree: {
|
|
setup: "npm install",
|
|
terminals: [{ name: "dev", command: "npm run dev" }],
|
|
customWorktreeField: "keep",
|
|
},
|
|
scripts: {
|
|
dev: {
|
|
type: "long-running",
|
|
command: "npm run dev",
|
|
port: 3000,
|
|
customScriptField: { nested: true },
|
|
},
|
|
},
|
|
customTopLevel: "preserved",
|
|
});
|
|
|
|
const draft = configToDraft(base);
|
|
const next = applyDraftToConfig({ draft, base });
|
|
|
|
expect((next as Record<string, unknown>).customTopLevel).toBe("preserved");
|
|
expect((next.worktree as Record<string, unknown>).customWorktreeField).toBe("keep");
|
|
expect((next.worktree as Record<string, unknown>).terminals).toEqual([
|
|
{ name: "dev", command: "npm run dev" },
|
|
]);
|
|
const devEntry = (next.scripts ?? {}).dev as Record<string, unknown>;
|
|
expect(devEntry.customScriptField).toEqual({ nested: true });
|
|
});
|
|
|
|
it("preserves all scripts on round-trip, including ones never edited in this session", () => {
|
|
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" },
|
|
},
|
|
});
|
|
|
|
const draft = configToDraft(base);
|
|
// Edit only "dev". Leave "build" and "lint" untouched.
|
|
const devRow = draft.scripts.find((row) => row.name === "dev");
|
|
if (!devRow) throw new Error("expected dev row in draft");
|
|
devRow.commandText = "npm run dev -- --watch";
|
|
|
|
const next = applyDraftToConfig({ draft, base });
|
|
const scripts = next.scripts ?? {};
|
|
expect(Object.keys(scripts).sort()).toEqual(["build", "dev", "lint"]);
|
|
|
|
const devEntry = scripts.dev as Record<string, unknown>;
|
|
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 Record<string, unknown>;
|
|
expect(buildEntry.command).toEqual(["npm", "run", "build"]);
|
|
expect(buildEntry.customBuildField).toEqual({ nested: 1 });
|
|
|
|
const lintEntry = scripts.lint as Record<string, unknown>;
|
|
expect(lintEntry.command).toBe("npm run lint");
|
|
expect(lintEntry.type).toBe("task");
|
|
});
|
|
|
|
it("normalizes script command text into the original command kind", () => {
|
|
const base = PaseoConfigRawSchema.parse({
|
|
scripts: {
|
|
build: { command: ["npm", "run", "build"] },
|
|
},
|
|
});
|
|
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 Record<string, unknown>;
|
|
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 = PaseoConfigRawSchema.parse({});
|
|
const draft = configToDraft(base);
|
|
draft.scripts = [
|
|
{
|
|
id: "row-1",
|
|
name: "dev",
|
|
commandText: "npm run dev",
|
|
commandOriginalKind: "missing",
|
|
type: "long-running",
|
|
portText: "3000",
|
|
rawEntry: {},
|
|
},
|
|
{
|
|
id: "row-2",
|
|
name: "tunnel",
|
|
commandText: "ngrok",
|
|
commandOriginalKind: "missing",
|
|
type: "long-running",
|
|
portText: "auto",
|
|
rawEntry: {},
|
|
},
|
|
];
|
|
const next = applyDraftToConfig({ draft, base });
|
|
const dev = (next.scripts ?? {}).dev as Record<string, unknown>;
|
|
const tunnel = (next.scripts ?? {}).tunnel as Record<string, unknown>;
|
|
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 = PaseoConfigRawSchema.parse({
|
|
scripts: {
|
|
dev: { command: "npm run dev" },
|
|
build: { command: "npm run build" },
|
|
},
|
|
});
|
|
const draft = configToDraft(base);
|
|
// remove build, add a row with empty name.
|
|
draft.scripts = draft.scripts
|
|
.filter((row) => row.name !== "build")
|
|
.concat({
|
|
id: "row-empty",
|
|
name: " ",
|
|
commandText: "echo hi",
|
|
commandOriginalKind: "missing",
|
|
type: "",
|
|
portText: "",
|
|
rawEntry: {},
|
|
});
|
|
const next = applyDraftToConfig({ draft, base });
|
|
const scripts = next.scripts ?? {};
|
|
expect(Object.keys(scripts)).toEqual(["dev"]);
|
|
});
|
|
});
|