// Local CLI: `executor web` used to mean "start a foreground server". The// first-time CLI setup path is now explicit: install the durable background// service first, then use `executor web` to open it. A fresh `web` invocation// should guide the user without minting local-server credentials/manifest state// or binding ports.import { execFile } from "node:child_process";import { existsSync, mkdtempSync, rmSync } from "node:fs";import { tmpdir } from "node:os";import { join } from "node:path";import { fileURLToPath } from "node:url";import { promisify } from "node:util"; import { expect } from "@effect/vitest";import { Effect } from "effect"; import { scenario } from "../src/scenario"; const execFileAsync = promisify(execFile);const repoRoot = fileURLToPath(new URL("../../", import.meta.url)); scenario( "CLI web · a fresh install points users at setup instead of starting a foreground server", { timeout: 120_000 }, Effect.promise(async () => { const root = mkdtempSync(join(tmpdir(), "executor-web-transition-")); const dataDir = join(root, "data"); try { const { stdout, stderr } = await execFileAsync("bun", ["run", "dev:cli", "web"], { cwd: repoRoot, env: { ...process.env, EXECUTOR_DATA_DIR: dataDir }, }); const output = `${stdout}\n${stderr}`; expect(output, "plain web should not start the old foreground server").not.toContain("Open:"); expect(output, "plain web should explain that no service is running").toContain( "Executor is not running.", ); expect(output, "plain web should direct first-time users to durable setup").toContain( "install", ); expect( output, "plain web should keep the temporary-server escape hatch discoverable", ).toContain("web --foreground"); expect( existsSync(join(dataDir, "server-control", "auth.json")), "plain web should not mint a local auth token", ).toBe(false); expect( existsSync(join(dataDir, "server-control", "server.json")), "plain web should not write a foreground server manifest", ).toBe(false); } finally { rmSync(root, { recursive: true, force: true }); } }),); scenario( "CLI web · --scope without --foreground warns instead of silently ignoring it", { timeout: 120_000 }, Effect.promise(async () => { // Regression guard for "Had to setup my config again": `executor web --scope X` // without `--foreground` cannot honor the scope (it opens the running service, // which keeps its own scope). That used to be a SILENT no-op — the user lands // on a different workspace and thinks their config vanished. It must warn. const root = mkdtempSync(join(tmpdir(), "executor-web-scope-")); const dataDir = join(root, "data"); const scopeDir = join(root, "workspace"); try { const { stdout, stderr } = await execFileAsync( "bun", ["run", "dev:cli", "web", "--scope", scopeDir], { cwd: repoRoot, env: { ...process.env, EXECUTOR_DATA_DIR: dataDir } }, ); const output = `${stdout}\n${stderr}`; expect(output, "the dropped --scope is called out, not silently ignored").toContain( "Ignoring --scope", ); expect(output, "the warning names the scope it dropped").toContain(scopeDir); expect(output, "the warning points at the flag that actually applies scope").toContain( "--foreground", ); } finally { rmSync(root, { recursive: true, force: true }); } }),);
// Local CLI: `executor web` used to mean "start a foreground server". The
// first-time CLI setup path is now explicit: install the durable background
// service first, then use `executor web` to open it. A fresh `web` invocation
// should guide the user without minting local-server credentials/manifest state
// or binding ports.
import { execFile } from "node:child_process";
import { existsSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { expect } from "@effect/vitest";
import { Effect } from "effect";
import { scenario } from "../src/scenario";
const execFileAsync = promisify(execFile);
const repoRoot = fileURLToPath(new URL("../../", import.meta.url));
scenario(
"CLI web · a fresh install points users at setup instead of starting a foreground server",
{ timeout: 120_000 },
Effect.promise(async () => {
const root = mkdtempSync(join(tmpdir(), "executor-web-transition-"));
const dataDir = join(root, "data");
try {
const { stdout, stderr } = await execFileAsync("bun", ["run", "dev:cli", "web"], {
cwd: repoRoot,
env: { ...process.env, EXECUTOR_DATA_DIR: dataDir },
});
const output = `${stdout}\n${stderr}`;
expect(output, "plain web should not start the old foreground server").not.toContain("Open:");
expect(output, "plain web should explain that no service is running").toContain(
"Executor is not running.",
);
expect(output, "plain web should direct first-time users to durable setup").toContain(
"install",
);
expect(
output,
"plain web should keep the temporary-server escape hatch discoverable",
).toContain("web --foreground");
expect(
existsSync(join(dataDir, "server-control", "auth.json")),
"plain web should not mint a local auth token",
).toBe(false);
expect(
existsSync(join(dataDir, "server-control", "server.json")),
"plain web should not write a foreground server manifest",
).toBe(false);
} finally {
rmSync(root, { recursive: true, force: true });
}
}),
);
scenario(
"CLI web · --scope without --foreground warns instead of silently ignoring it",
{ timeout: 120_000 },
Effect.promise(async () => {
// Regression guard for "Had to setup my config again": `executor web --scope X`
// without `--foreground` cannot honor the scope (it opens the running service,
// which keeps its own scope). That used to be a SILENT no-op — the user lands
// on a different workspace and thinks their config vanished. It must warn.
const root = mkdtempSync(join(tmpdir(), "executor-web-scope-"));
const dataDir = join(root, "data");
const scopeDir = join(root, "workspace");
try {
const { stdout, stderr } = await execFileAsync(
"bun",
["run", "dev:cli", "web", "--scope", scopeDir],
{ cwd: repoRoot, env: { ...process.env, EXECUTOR_DATA_DIR: dataDir } },
);
const output = `${stdout}\n${stderr}`;
expect(output, "the dropped --scope is called out, not silently ignored").toContain(
"Ignoring --scope",
);
expect(output, "the warning names the scope it dropped").toContain(scopeDir);
expect(output, "the warning points at the flag that actually applies scope").toContain(
"--foreground",
);
} finally {
rmSync(root, { recursive: true, force: true });
}
}),
);