// Local CLI: what `executor call` tells a user whose stored hosted-server// login no longer works. A stub standing in for the hosted server answers// every request 401 (the wire shape of an expired or revoked session), and// the default profile carries an expired OAuth token with no way to refresh.//// The CLI must treat that 401 as a sign-in problem, not a transport problem:// say which server the user is signed out of and that `executor login` fixes// it. The user typed no flags, so the hint is plain `executor login`; profile// names are internal plumbing and must not leak. And the raw// `Decode error (401 GET .../api/tools)` from before the 401 mapping must// never resurface.//// The session runs in an interactive shell with an `executor` shim on PATH so// the terminal.cast reads like the real moment: the user types the command,// the error is the whole answer.import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";import { createServer } from "node:http";import type { AddressInfo } from "node:net";import { tmpdir } from "node:os";import { delimiter, join } from "node:path";import { fileURLToPath } from "node:url"; import { expect } from "@effect/vitest";import { Effect } from "effect"; import { scenario } from "../src/scenario";import { Cli, RunDir } from "../src/services"; const repoRoot = fileURLToPath(new URL("../../", import.meta.url)); /** Every route answers like a hosted server rejecting an expired session. */const listenAsUnauthorizedServer = () => Effect.promise( () => new Promise<{ origin: string; close: () => void }>((resolve) => { const server = createServer((_request, response) => { response.writeHead(401, { "content-type": "application/json" }); response.end(JSON.stringify({ error: "unauthorized" })); }); server.listen(0, "127.0.0.1", () => { const { port } = server.address() as AddressInfo; resolve({ origin: `http://127.0.0.1:${port}`, close: () => server.close(), }); }); }), ); /** A data dir whose default profile is a hosted server with an expired, * unrefreshable OAuth token: the state `executor server list` shows as * `stored-auth` after a login has gone stale. The `profile:` key matches * what `executor login` persists via upsertCliServerConnectionProfile. */const writeLoggedOutProfile = (dataDir: string, origin: string) => { writeFileSync( join(dataDir, "server-connections.json"), JSON.stringify( { version: 1, defaultProfile: "hosted", profiles: [ { name: "hosted", connection: { kind: "http", key: "profile:hosted", origin, displayName: "hosted", auth: { kind: "oauth", accessToken: "expired-access-token", expiresAt: 1 }, }, }, ], }, null, 2, ), );}; /** An `executor` on PATH that runs this checkout's CLI, so the recording * shows the command a user actually types. */const writeExecutorShim = (binDir: string) => { const shim = join(binDir, "executor"); writeFileSync(shim, `#!/bin/sh\nexec bun run "${join(repoRoot, "apps/cli/src/main.ts")}" "$@"\n`); chmodSync(shim, 0o755);}; scenario( "CLI call · a signed-out hosted profile explains how to log back in", { timeout: 120_000 }, Effect.gen(function* () { const cli = yield* Cli; const runDir = yield* RunDir; const hosted = yield* listenAsUnauthorizedServer(); const dataDir = mkdtempSync(join(tmpdir(), "executor-cli-logged-out-")); writeLoggedOutProfile(dataDir, hosted.origin); writeExecutorShim(dataDir); yield* cli .session( ["bash", "--noprofile", "--norc"], async (term) => { await term.screen.waitUntil((current) => current.text.includes("$"), { timeoutMs: 30_000, }); // The user browses the catalog the way the docs teach: call --help. await term.keyboard.type("executor call github issues --help", { paceMs: 40 }); await term.keyboard.press("Enter"); // The command exits after printing its sign-in error. const snapshot = await term.screen.waitUntil( (current) => current.text.includes("sign in"), { timeoutMs: 60_000 }, ); const screen = snapshot.text; // The moment reads as a sign-in problem naming the server... expect(screen, "the error says the user is signed out").toContain("You're signed out"); // ...fixed by plain `executor login` (dev mode prefixes the entry // script, so assert the stable tail of the sentence). expect(screen, "it hands over the bare login command").toContain( "login` to sign in again.", ); // No flags were typed, so no plumbing comes back. expect(screen, "profile names stay internal").not.toContain("--server"); expect(screen, "profiles are not mentioned at all").not.toContain("profile"); // The pre-fix rendering must not resurface. expect(screen, "the raw transport error stays internal").not.toContain("Decode error"); expect(screen, "the API endpoint does not leak into the message").not.toContain( "/api/tools", ); }, { cwd: repoRoot, env: { EXECUTOR_DATA_DIR: dataDir, EXECUTOR_SCOPE_DIR: dataDir, PATH: `${dataDir}${delimiter}${process.env.PATH ?? ""}`, PS1: "$ ", // Keep macOS's "default shell is now zsh" banner out of the cast. BASH_SILENCE_DEPRECATION_WARNING: "1", }, record: join(runDir, "terminal.cast"), viewport: { cols: 100, rows: 24 }, }, ) .pipe( Effect.ensuring( Effect.sync(() => { hosted.close(); rmSync(dataDir, { recursive: true, force: true }); }), ), ); }),);
// Local CLI: what `executor call` tells a user whose stored hosted-server
// login no longer works. A stub standing in for the hosted server answers
// every request 401 (the wire shape of an expired or revoked session), and
// the default profile carries an expired OAuth token with no way to refresh.
//
// The CLI must treat that 401 as a sign-in problem, not a transport problem:
// say which server the user is signed out of and that `executor login` fixes
// it. The user typed no flags, so the hint is plain `executor login`; profile
// names are internal plumbing and must not leak. And the raw
// `Decode error (401 GET .../api/tools)` from before the 401 mapping must
// never resurface.
//
// The session runs in an interactive shell with an `executor` shim on PATH so
// the terminal.cast reads like the real moment: the user types the command,
// the error is the whole answer.
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { createServer } from "node:http";
import type { AddressInfo } from "node:net";
import { tmpdir } from "node:os";
import { delimiter, join } from "node:path";
import { fileURLToPath } from "node:url";
import { expect } from "@effect/vitest";
import { Effect } from "effect";
import { scenario } from "../src/scenario";
import { Cli, RunDir } from "../src/services";
const repoRoot = fileURLToPath(new URL("../../", import.meta.url));
/** Every route answers like a hosted server rejecting an expired session. */
const listenAsUnauthorizedServer = () =>
Effect.promise(
() =>
new Promise<{ origin: string; close: () => void }>((resolve) => {
const server = createServer((_request, response) => {
response.writeHead(401, { "content-type": "application/json" });
response.end(JSON.stringify({ error: "unauthorized" }));
});
server.listen(0, "127.0.0.1", () => {
const { port } = server.address() as AddressInfo;
resolve({
origin: `http://127.0.0.1:${port}`,
close: () => server.close(),
});
});
}),
);
/** A data dir whose default profile is a hosted server with an expired,
* unrefreshable OAuth token: the state `executor server list` shows as
* `stored-auth` after a login has gone stale. The `profile:` key matches
* what `executor login` persists via upsertCliServerConnectionProfile. */
const writeLoggedOutProfile = (dataDir: string, origin: string) => {
writeFileSync(
join(dataDir, "server-connections.json"),
JSON.stringify(
{
version: 1,
defaultProfile: "hosted",
profiles: [
{
name: "hosted",
connection: {
kind: "http",
key: "profile:hosted",
origin,
displayName: "hosted",
auth: { kind: "oauth", accessToken: "expired-access-token", expiresAt: 1 },
},
},
],
},
null,
2,
),
);
};
/** An `executor` on PATH that runs this checkout's CLI, so the recording
* shows the command a user actually types. */
const writeExecutorShim = (binDir: string) => {
const shim = join(binDir, "executor");
writeFileSync(shim, `#!/bin/sh\nexec bun run "${join(repoRoot, "apps/cli/src/main.ts")}" "$@"\n`);
chmodSync(shim, 0o755);
};
scenario(
"CLI call · a signed-out hosted profile explains how to log back in",
{ timeout: 120_000 },
Effect.gen(function* () {
const cli = yield* Cli;
const runDir = yield* RunDir;
const hosted = yield* listenAsUnauthorizedServer();
const dataDir = mkdtempSync(join(tmpdir(), "executor-cli-logged-out-"));
writeLoggedOutProfile(dataDir, hosted.origin);
writeExecutorShim(dataDir);
yield* cli
.session(
["bash", "--noprofile", "--norc"],
async (term) => {
await term.screen.waitUntil((current) => current.text.includes("$"), {
timeoutMs: 30_000,
});
// The user browses the catalog the way the docs teach: call --help.
await term.keyboard.type("executor call github issues --help", { paceMs: 40 });
await term.keyboard.press("Enter");
// The command exits after printing its sign-in error.
const snapshot = await term.screen.waitUntil(
(current) => current.text.includes("sign in"),
{ timeoutMs: 60_000 },
);
const screen = snapshot.text;
// The moment reads as a sign-in problem naming the server...
expect(screen, "the error says the user is signed out").toContain("You're signed out");
// ...fixed by plain `executor login` (dev mode prefixes the entry
// script, so assert the stable tail of the sentence).
expect(screen, "it hands over the bare login command").toContain(
"login` to sign in again.",
);
// No flags were typed, so no plumbing comes back.
expect(screen, "profile names stay internal").not.toContain("--server");
expect(screen, "profiles are not mentioned at all").not.toContain("profile");
// The pre-fix rendering must not resurface.
expect(screen, "the raw transport error stays internal").not.toContain("Decode error");
expect(screen, "the API endpoint does not leak into the message").not.toContain(
"/api/tools",
);
},
{
cwd: repoRoot,
env: {
EXECUTOR_DATA_DIR: dataDir,
EXECUTOR_SCOPE_DIR: dataDir,
PATH: `${dataDir}${delimiter}${process.env.PATH ?? ""}`,
PS1: "$ ",
// Keep macOS's "default shell is now zsh" banner out of the cast.
BASH_SILENCE_DEPRECATION_WARNING: "1",
},
record: join(runDir, "terminal.cast"),
viewport: { cols: 100, rows: 24 },
},
)
.pipe(
Effect.ensuring(
Effect.sync(() => {
hosted.close();
rmSync(dataDir, { recursive: true, force: true });
}),
),
);
}),
);