Skip to content
Merged
17 changes: 9 additions & 8 deletions cli/app/operations/project-creation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import "#veryfront/schemas/_test-setup.ts";
import { assertEquals } from "#veryfront/testing/assert.ts";
import { describe, it } from "#veryfront/testing/bdd.ts";
import { _resetEnvironmentConfig } from "#veryfront/config/environment-config.ts";
import { withCwd } from "#veryfront/testing/cwd.ts";
import { join } from "veryfront/platform/path";
import { createProject } from "./project-creation.ts";
import { createInitialState } from "../state.ts";
Expand All @@ -21,7 +22,6 @@ function restoreEnv(name: string, value: string | undefined): void {
describe("TUI project creation", () => {
it("links the created project when the reservation omits the id", async () => {
const originalFetch = globalThis.fetch;
const originalCwd = Deno.cwd();
const envKeys = ["VERYFRONT_API_URL", "VERYFRONT_API_BASE_URL", "XDG_CONFIG_HOME"];
const savedEnv = envKeys.map((key) => Deno.env.get(key));
const workDir = await Deno.makeTempDir();
Expand All @@ -35,7 +35,6 @@ describe("TUI project creation", () => {
Deno.env.delete("VERYFRONT_API_BASE_URL");
Deno.env.set("XDG_CONFIG_HOME", configHome);
_resetEnvironmentConfig();
Deno.chdir(workDir);

globalThis.fetch = ((input: string | URL | Request, init?: RequestInit) => {
const request = input instanceof Request ? input : new Request(input, init);
Expand All @@ -57,11 +56,14 @@ describe("TUI project creation", () => {
throw new Error(`Unexpected request: ${request.method} ${url.pathname}`);
}) as typeof fetch;

const state = await createProject(
{ state: createInitialState(), render: () => {} },
"My App",
"minimal",
);
// Only the call itself needs the directory: it resolves the new project
// relative to the process cwd. Everything around it uses absolute paths.
const state = await withCwd(workDir, () =>
createProject(
{ state: createInitialState(), render: () => {} },
"My App",
"minimal",
));

const link = JSON.parse(
await Deno.readTextFile(
Expand All @@ -81,7 +83,6 @@ describe("TUI project creation", () => {
);
} finally {
globalThis.fetch = originalFetch;
Deno.chdir(originalCwd);
envKeys.forEach((key, index) => restoreEnv(key, savedEnv[index]));
_resetEnvironmentConfig();
await Deno.remove(workDir, { recursive: true });
Expand Down
57 changes: 29 additions & 28 deletions cli/commands/schedule/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { clearProjectAgentRuntimeRegistries } from "../../../src/agent/project/a
import { _resetEnvironmentConfig } from "#veryfront/config/environment-config.ts";
import { stop as stopEsbuild } from "veryfront/extensions/bundler";
import { VeryfrontError } from "veryfront/errors";
import { withCwd } from "#veryfront/testing/cwd.ts";
import type { CreateScheduleRunFromSourceResult, Run, VeryfrontRunsClient } from "veryfront/runs";
import { setJsonMode } from "../../shared/json-output.ts";
import type { ParsedArgs } from "../../shared/types.ts";
Expand All @@ -22,10 +23,6 @@ import {
waitForRemoteScheduleRun,
} from "./handler.ts";

// Derived from the module URL rather than load-time Deno.cwd(): under
// `deno test --parallel` this module can be evaluated while a sibling test
// file is chdir'd into a soon-to-be-deleted temp directory.
const originalCwd = new URL("../../../", import.meta.url);
const originalExit = Deno.exit;
const originalFetch = globalThis.fetch;
const originalConsoleLog = console.log;
Expand Down Expand Up @@ -128,7 +125,8 @@ function restoreEnvironment(): void {

describe("schedule command", () => {
afterEach(() => {
Deno.chdir(originalCwd);
// No chdir here: withCwd already handed the directory back, and reaching
// for it outside a turn would yank it from whichever test file holds it now.
// deno-lint-ignore no-explicit-any
(Deno as any).exit = originalExit;
globalThis.fetch = originalFetch;
Expand Down Expand Up @@ -192,7 +190,6 @@ describe("schedule command", () => {
Deno.env.delete("VERYFRONT_PROJECT_SLUG");
Deno.env.set("XDG_CONFIG_HOME", configHome);
_resetEnvironmentConfig();
Deno.chdir(projectDir);
setJsonMode(true);
console.log = (...args: unknown[]) => output.push(args.map(String).join(" "));
globalThis.fetch = (async (
Expand All @@ -215,16 +212,19 @@ describe("schedule command", () => {
};

let exitCode: number | undefined;
try {
await handleScheduleCommand({
_: ["schedule", "run", "process-job-submissions"],
remote: true,
json: true,
} as ParsedArgs);
} catch (error) {
if (!(error instanceof ExitSentinel)) throw error;
exitCode = error.code;
}
// Held only for the command, which resolves veryfront.json from the cwd.
await withCwd(projectDir, async () => {
try {
await handleScheduleCommand({
_: ["schedule", "run", "process-job-submissions"],
remote: true,
json: true,
} as ParsedArgs);
} catch (error) {
if (!(error instanceof ExitSentinel)) throw error;
exitCode = error.code;
}
});

assertEquals(exitCode, 0);
assertEquals(requests.map((request) => request.url), [
Expand Down Expand Up @@ -263,7 +263,6 @@ describe("schedule command", () => {
},
});
} finally {
Deno.chdir(originalCwd);
await stopEsbuild();
await Deno.remove(projectDir, { recursive: true });
await Deno.remove(configHome, { recursive: true });
Expand Down Expand Up @@ -319,7 +318,6 @@ describe("schedule command", () => {
].join("\n"),
);

Deno.chdir(projectDir);
setJsonMode(true);
console.log = (...args: unknown[]) => output.push(args.map(String).join(" "));
// deno-lint-ignore no-explicit-any
Expand All @@ -328,22 +326,25 @@ describe("schedule command", () => {
};

let exitCode: number | undefined;
try {
await handleScheduleCommand({
_: ["schedule", "run", "timed-task"],
json: true,
} as ParsedArgs);
} catch (error) {
if (!(error instanceof ExitSentinel)) throw error;
exitCode = error.code;
}
// Held only for the command, which discovers schedules/ and tasks/
// relative to the cwd.
await withCwd(projectDir, async () => {
try {
await handleScheduleCommand({
_: ["schedule", "run", "timed-task"],
json: true,
} as ParsedArgs);
} catch (error) {
if (!(error instanceof ExitSentinel)) throw error;
exitCode = error.code;
}
});

assertEquals(exitCode, 0);
assertEquals(JSON.parse(output.at(-1) ?? "{}").data.output, {
signalPresent: true,
});
} finally {
Deno.chdir(originalCwd);
await stopEsbuild();
await Deno.remove(projectDir, { recursive: true });
}
Expand Down
13 changes: 2 additions & 11 deletions cli/commands/skills/validate.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { withCwd } from "#veryfront/testing/cwd.ts";
import "#veryfront/schemas/_test-setup.ts";
import { assertEquals } from "#veryfront/testing/assert.ts";
import { describe, it } from "#veryfront/testing/bdd.ts";
Expand Down Expand Up @@ -25,16 +26,6 @@ async function withTempSkill(
}
}

async function withTempCwd(dir: string, fn: () => Promise<void>): Promise<void> {
const previous = Deno.cwd();
try {
Deno.chdir(dir);
await fn();
} finally {
Deno.chdir(previous);
}
}

describe("Skills Validate", () => {
it("accepts a project skill with SKILL.md frontmatter", async () => {
await withTempSkill({
Expand Down Expand Up @@ -66,7 +57,7 @@ description: Review code changes.
Review the submitted changes.
`,
}, async (dir) => {
await withTempCwd(dir, async () => {
await withCwd(dir, async () => {
const issues = await validateSkillDirectory(".");
assertEquals(issues, []);
});
Expand Down
30 changes: 5 additions & 25 deletions cli/commands/webhook/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,13 @@ import { clearProjectAgentRuntimeRegistries } from "#veryfront/agent/project/age
import { clearTranspileCache } from "#veryfront/discovery/transpiler.ts";
import { stop as stopEsbuild } from "veryfront/extensions/bundler";
import { VeryfrontError } from "veryfront/errors";
import { withCwd } from "#veryfront/testing/cwd.ts";
import { setJsonMode } from "../../shared/json-output.ts";
import type { ParsedArgs } from "../../shared/types.ts";
import { handleWebhookCommand, toWebhookAgentOptions } from "./handler.ts";

// Derived from the module URL rather than load-time Deno.cwd(): under
// `deno test --parallel` this module can be evaluated while a sibling test
// file is chdir'd into a soon-to-be-deleted temp directory.
const originalCwd = new URL("../../../", import.meta.url);
const originalExit = Deno.exit;
const originalConsoleLog = console.log;
let cwdCommandTail: Promise<void> = Promise.resolve();

class ExitSentinel extends Error {
constructor(readonly code: number) {
Expand Down Expand Up @@ -50,34 +46,20 @@ async function runCommand(args: ParsedArgs): Promise<{
return { exitCode, output };
}

async function runCommandInProjectCwd(
function runCommandInProjectCwd(
projectDir: string,
args: ParsedArgs,
): Promise<{
exitCode: number | undefined;
output: string[];
}> {
const previousTail = cwdCommandTail.catch(() => {});
let release!: () => void;
cwdCommandTail = previousTail.then(() =>
new Promise<void>((resolve) => {
release = resolve;
})
);
await previousTail;

try {
Deno.chdir(projectDir);
return await runCommand(args);
} finally {
Deno.chdir(originalCwd);
release();
}
return withCwd(projectDir, () => runCommand(args));
}

describe("webhook command", () => {
afterEach(() => {
Deno.chdir(originalCwd);
// No chdir here: withCwd already handed the directory back, and reaching
// for it outside a turn would yank it from whichever test file holds it now.
// deno-lint-ignore no-explicit-any
(Deno as any).exit = originalExit;
console.log = originalConsoleLog;
Expand Down Expand Up @@ -142,7 +124,6 @@ describe("webhook command", () => {
},
});
} finally {
Deno.chdir(originalCwd);
await Deno.remove(projectDir, { recursive: true });
}
});
Expand Down Expand Up @@ -224,7 +205,6 @@ describe("webhook command", () => {
},
});
} finally {
Deno.chdir(originalCwd);
await Deno.remove(projectDir, { recursive: true });
}
});
Expand Down
17 changes: 9 additions & 8 deletions cli/router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import "#veryfront/schemas/_test-setup.ts";
import { _resetEnvironmentConfig } from "#veryfront/config/environment-config.ts";
import { assertEquals } from "#veryfront/testing/assert.ts";
import { describe, it } from "#veryfront/testing/bdd.ts";
import { withCwd } from "#veryfront/testing/cwd.ts";
import { COMMANDS } from "./help/command-definitions.ts";
import { parseLoginMethod } from "./auth/utils.ts";
import { routeCommand } from "./router.ts";
Expand Down Expand Up @@ -552,7 +553,6 @@ describe("cli/router helpers", () => {
});

it("reports missing credentials for schedule remote JSON runs as JSON command failure", async () => {
const originalCwd = Deno.cwd();
const projectDir = await Deno.makeTempDir({ prefix: "vf-schedule-json-auth-" });
const configHome = await Deno.makeTempDir({ prefix: "vf-schedule-json-auth-config-" });
const environmentNames = [
Expand All @@ -573,18 +573,20 @@ describe("cli/router helpers", () => {
`${projectDir}/veryfront.json`,
JSON.stringify({ projectSlug: "json-auth-project" }),
);
Deno.chdir(projectDir);
Deno.env.delete("VERYFRONT_API_URL");
Deno.env.delete("VERYFRONT_API_TOKEN");
Deno.env.delete("VERYFRONT_PROJECT_SLUG");
Deno.env.set("XDG_CONFIG_HOME", configHome);
_resetEnvironmentConfig();

const code = await runAndCaptureExit({
_: ["schedule", "run", "process-job-submissions"],
remote: true,
json: true,
} as ParsedArgs);
// Scoped to the call that resolves veryfront.json from the cwd, rather
// than held across the whole test.
const code = await withCwd(projectDir, () =>
runAndCaptureExit({
_: ["schedule", "run", "process-job-submissions"],
remote: true,
json: true,
} as ParsedArgs));
assertEquals(code, 1);
assertEquals(consoleOutput.length, 1);
const parsed = JSON.parse(consoleOutput[0] ?? "{}");
Expand All @@ -596,7 +598,6 @@ describe("cli/router helpers", () => {
assertEquals(parsed.error.message, "Authentication required for this operation.");
assertEquals(consoleErrorOutput, []);
} finally {
Deno.chdir(originalCwd);
for (const name of environmentNames) {
const value = originalEnvironment[name];
if (value === undefined) Deno.env.delete(name);
Expand Down
Loading