Files
clawhub/scripts/dev-worktree.test.ts
Patrick Erichsen dfca6f5d9d feat: add disposable Vercel and Convex PR previews (#3017)
Adds isolated Convex-backed Vercel PR previews with shared local/preview seeding, preview-safe routing, and production guards.
2026-07-08 20:54:09 -07:00

304 lines
9.0 KiB
TypeScript

import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import {
applyLocalDevWorkerToken,
buildLocalConvexEnvChanges,
buildDevWorkersArgs,
buildForegroundArgs,
buildEnvFileCandidates,
buildViteArgs,
applyLocalConvexEnvForUrl,
isLocalConvexUrl,
isRunningPid,
parseArgs,
parseEnv,
shouldSeedLocalData,
shouldStartDevWorkers,
} from "./dev-worktree";
import { isConvexFunctionUnavailableOutput } from "./seed";
describe("dev-worktree helpers", () => {
it("seeds local fixtures and public corpus when Worktrunk starts dev services", () => {
const config = readFileSync(".config/wt.toml", "utf8");
expect(config).toContain(
'[post-start]\ndev = "bun scripts/dev-worktree.ts --detach --seed --port',
);
expect(config).toContain(
'[aliases]\ndev = "wt --yes hook pre-start && bun scripts/dev-worktree.ts --detach --seed --port',
);
});
it("parses env files without treating inline comments as values", () => {
expect(
parseEnv(`
CONVEX_DEPLOYMENT=local:local-amantus-clawdhub # team: amantus, project: clawdhub
SITE_URL=http://localhost:3000
HASH_VALUE=abc#123
QUOTED_HASH="value # kept"
`),
).toEqual({
CONVEX_DEPLOYMENT: "local:local-amantus-clawdhub",
SITE_URL: "http://localhost:3000",
HASH_VALUE: "abc#123",
QUOTED_HASH: "value # kept",
});
});
it("uses only the local checkout env unless an env file is explicit", () => {
expect(
buildEnvFileCandidates({
explicit: null,
cwd: "/tmp/worktrees/feature",
worktrees: [
"/Users/me/Git/openclaw/clawhub",
"/tmp/worktrees/feature",
"/tmp/worktrees/other-feature",
],
}),
).toEqual([".env.local"]);
});
it("keeps explicit env files authoritative", () => {
expect(
buildEnvFileCandidates({
explicit: "/secure/shared.env",
cwd: "/tmp/worktrees/feature",
worktrees: ["/Users/me/Git/openclaw/clawhub"],
}),
).toEqual(["/secure/shared.env"]);
});
it("recognizes Convex functions that are not queryable yet", () => {
expect(
isConvexFunctionUnavailableOutput(`
Failed to run function "devSeed:seedLocalFixtures":
Could not find function for 'devSeed:seedLocalFixtures'. Did you forget to run \`npx convex dev\`?
No functions found.
`),
).toBe(true);
expect(isConvexFunctionUnavailableOutput("AUTH_GITHUB_ID is required")).toBe(false);
});
it("parses detach mode for Codex setup startup", () => {
expect(parseArgs(["--detach", "--port", "3999"])).toMatchObject({
detach: true,
port: "3999",
workers: true,
});
});
it("allows local background workers to be disabled explicitly", () => {
expect(parseArgs(["--no-workers"])).toMatchObject({
workers: false,
});
});
it("does not pass detach mode to the foreground child process", () => {
expect(buildForegroundArgs(["--detach", "--port", "3999"])).toEqual(["--port", "3999"]);
});
it("binds Vite to the same loopback host advertised by Worktrunk", () => {
expect(buildViteArgs("3999")).toEqual([
"--bun",
"vite",
"dev",
"--host",
"127.0.0.1",
"--port",
"3999",
"--strictPort",
]);
});
it("starts dev workers with the same env file as the app", () => {
expect(buildDevWorkersArgs("/tmp/clawhub/.env.local")).toEqual([
"scripts/dev-workers.ts",
"--env-file",
"/tmp/clawhub/.env.local",
]);
});
it("overrides the shared worker token with a fixed local-dev value", () => {
const env: NodeJS.ProcessEnv = { SECURITY_SCAN_WORKER_TOKEN: "prod-like-token" };
expect(applyLocalDevWorkerToken(env)).toBe("local-dev-worker-token");
expect(env.SECURITY_SCAN_WORKER_TOKEN).toBe("local-dev-worker-token");
});
it("builds local Convex env for dev auth without overriding built-ins", () => {
const changes = buildLocalConvexEnvChanges({
CONVEX_DEPLOYMENT: "anonymous:anonymous-agent",
CONVEX_SITE_URL: "http://127.0.0.1:3211",
});
const byName = Object.fromEntries(changes.map((change) => [change.name, change.value]));
expect(byName.DEV_AUTH_ENABLED).toBe("1");
expect(byName.DEV_AUTH_CONVEX_DEPLOYMENT).toBe("anonymous:anonymous-agent");
expect(byName.SECURITY_SCAN_WORKER_TOKEN).toBe("local-dev-worker-token");
expect(byName.SECURITY_SCAN_DEFAULT_VT_WAIT_MS).toBe("0");
expect(byName.AUTH_GITHUB_ID).toBe("local-dev");
expect(byName.AUTH_GITHUB_SECRET).toBe("local-dev");
expect(byName.JWT_PRIVATE_KEY).toContain("BEGIN PRIVATE KEY");
expect(JSON.parse(byName.JWKS)).toMatchObject({
keys: [expect.objectContaining({ use: "sig" })],
});
expect(byName.CONVEX_SITE_URL).toBeUndefined();
});
it("does not apply local dev auth overrides for cloud Convex URLs", () => {
const env: NodeJS.ProcessEnv = {
SECURITY_SCAN_WORKER_TOKEN: "remote-worker-token",
JWT_PRIVATE_KEY: "remote-private-key",
};
expect(applyLocalConvexEnvForUrl(env, "https://example.convex.cloud")).toBe(false);
expect(env.SECURITY_SCAN_WORKER_TOKEN).toBe("remote-worker-token");
expect(env.JWT_PRIVATE_KEY).toBe("remote-private-key");
expect(env.DEV_AUTH_ENABLED).toBeUndefined();
});
it("recognizes local Convex URLs", () => {
expect(isLocalConvexUrl("http://127.0.0.1:3210")).toBe(true);
expect(isLocalConvexUrl("http://localhost:3210")).toBe(true);
expect(isLocalConvexUrl("http://[::1]:3210")).toBe(true);
expect(isLocalConvexUrl("https://example.convex.cloud")).toBe(false);
expect(isLocalConvexUrl("not-a-url")).toBe(false);
});
it("starts dev workers by default unless disabled", () => {
expect(shouldStartDevWorkers({ workers: true }, "http://127.0.0.1:3210")).toEqual({
start: true,
reason: null,
});
expect(shouldStartDevWorkers({ workers: false }, "http://127.0.0.1:3210")).toEqual({
start: false,
reason: "--no-workers was passed",
});
});
it("seeds local data only for local Convex URLs", () => {
expect(
shouldSeedLocalData(
{ seed: true, seedOnly: false },
"http://127.0.0.1:3210",
"anonymous:anonymous-agent",
),
).toEqual({
seed: true,
fatal: false,
reason: null,
});
expect(
shouldSeedLocalData(
{ seed: false, seedOnly: false },
"http://127.0.0.1:3210",
"anonymous:anonymous-agent",
),
).toEqual({
seed: false,
fatal: false,
reason: "--seed was not passed",
});
expect(
shouldSeedLocalData(
{ seed: true, seedOnly: false },
"https://example.convex.cloud",
"prod:shared-deployment",
),
).toEqual({
seed: false,
fatal: false,
reason: "VITE_CONVEX_URL is not local",
});
expect(
shouldSeedLocalData(
{ seed: true, seedOnly: true },
"https://example.convex.cloud",
"prod:shared-deployment",
),
).toEqual({
seed: false,
fatal: true,
reason: "VITE_CONVEX_URL is not local",
});
expect(
shouldSeedLocalData({ seed: true, seedOnly: false }, "http://127.0.0.1:3210", null),
).toEqual({
seed: false,
fatal: false,
reason: "CONVEX_DEPLOYMENT is missing or not local",
});
expect(
shouldSeedLocalData(
{ seed: true, seedOnly: false },
"http://127.0.0.1:3210",
"prod:shared-deployment",
),
).toEqual({
seed: false,
fatal: false,
reason: "CONVEX_DEPLOYMENT is missing or not local",
});
expect(
shouldSeedLocalData(
{ seed: true, seedOnly: true },
"http://127.0.0.1:3210",
"prod:shared-deployment",
),
).toEqual({
seed: false,
fatal: true,
reason: "CONVEX_DEPLOYMENT is missing or not local",
});
expect(
shouldSeedLocalData(
{ seed: true, seedOnly: false },
"http://127.0.0.1:3210",
"anonymous:anonymous-agent",
"anonymous:anonymous-agent\nhttp://127.0.0.1:3210",
),
).toEqual({
seed: false,
fatal: false,
reason: "local seed already completed for this Convex deployment",
});
expect(
shouldSeedLocalData(
{ seed: true, seedOnly: true },
"http://127.0.0.1:3210",
"anonymous:anonymous-agent",
"anonymous:anonymous-agent\nhttp://127.0.0.1:3210",
),
).toEqual({
seed: true,
fatal: false,
reason: null,
});
});
it("does not start dev workers by default for non-local Convex URLs", () => {
expect(shouldStartDevWorkers({ workers: true }, "https://example.convex.cloud")).toEqual({
start: false,
reason: "VITE_CONVEX_URL is not local",
});
});
it("treats invalid detached pid file values as not running", () => {
expect(isRunningPid(null)).toBe(false);
expect(isRunningPid(0)).toBe(false);
expect(isRunningPid(Number.NaN)).toBe(false);
expect(isRunningPid(process.pid)).toBe(true);
});
});