From 5621e93d730291cbf1eecf0fa987e4d28d542be1 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Wed, 7 Oct 2026 22:21:15 -0700 Subject: [PATCH] test(cli): remove module mocks so the suite passes in any file order mock.module is process-wide in bun. Without --isolate, fakes of lib/config, lib/host-target and others reached later test files (resolve-auth and workspaces archive failed, depending on order). The CLI read SUPERSET_HOME_DIR once at import, which is why tests faked the config module. It is now read per call, so tests write a real config into a temp home. Host commands are tested against a local server that speaks the real protocol. spawnHostService and writeConfig take the function they call as an optional parameter. --- .../commands/agents/create/command.test.ts | 91 ++++--------- .../src/commands/browser/open/command.test.ts | 11 +- .../cli/src/commands/config/check/command.ts | 4 +- .../src/commands/scripts/add/command.test.ts | 22 +-- .../cli/src/commands/scripts/manage.test.ts | 22 +-- packages/cli/src/commands/start/command.ts | 4 +- .../commands/start/manifest-liveness.test.ts | 2 - .../cli/src/commands/status/command.test.ts | 10 +- .../workspaces/archive/command.test.ts | 1 - .../workspaces/create/command.test.ts | 74 ++++------ .../commands/workspaces/get/command.test.ts | 56 ++++---- packages/cli/src/lib/analytics.ts | 9 +- .../cloud-workspaces/resolveWorkspaceHost.ts | 14 +- packages/cli/src/lib/config.test.ts | 128 +++++++----------- packages/cli/src/lib/config.ts | 44 +++--- .../resolveCloudWorkspaceTarget.ts | 17 ++- packages/cli/src/lib/host/manifest.ts | 4 +- packages/cli/src/lib/host/spawn.test.ts | 37 +++-- packages/cli/src/lib/host/spawn.ts | 7 +- packages/cli/src/lib/host/test-helpers.ts | 74 ++++++++++ 20 files changed, 297 insertions(+), 334 deletions(-) create mode 100644 packages/cli/src/lib/host/test-helpers.ts diff --git a/packages/cli/src/commands/agents/create/command.test.ts b/packages/cli/src/commands/agents/create/command.test.ts index e3e9f0da07b..a0f91fed612 100644 --- a/packages/cli/src/commands/agents/create/command.test.ts +++ b/packages/cli/src/commands/agents/create/command.test.ts @@ -1,4 +1,6 @@ -import { afterEach, describe, expect, mock, test } from "bun:test"; +import { afterEach, describe, expect, test } from "bun:test"; +import { withLocalHostService } from "../../../lib/host/test-helpers"; +import createAgentCommand from "./command"; let runInput: Record | undefined; let transcriptInput: Record | undefined; @@ -6,64 +8,25 @@ let transcriptText = ""; let transcriptFails = false; let bindings: Array> = []; -const hostTarget = () => ({ - hostId: "host-1", - client: { - terminal: { - transcript: { - query: async (input: Record) => { - transcriptInput = input; - if (transcriptFails) throw new Error("host unreachable"); - return { text: transcriptText, source: "stream", streamBytes: 42 }; - }, - }, - }, - terminalAgents: { - listByWorkspace: { query: async () => bindings }, - }, - settings: { - agentConfigs: { - list: { - query: async () => [ - { id: "config-1", presetId: "claude", label: "Claude" }, - ], - }, - }, - }, - agents: { - run: { - mutate: async (input: Record) => { - runInput = input; - return { - kind: "terminal", - sessionId: "terminal-1", - label: "Codex", - }; - }, - }, - }, +const WORKSPACE_ID = "00000000-0000-4000-8000-000000000001"; + +withLocalHostService("org-1", { + "workspace.list": () => [{ id: WORKSPACE_ID }], + "terminal.transcript": (input) => { + transcriptInput = input; + if (transcriptFails) throw new Error("host unreachable"); + return { text: transcriptText, source: "stream", streamBytes: 42 }; + }, + "terminalAgents.listByWorkspace": () => bindings, + "settings.agentConfigs.list": () => [ + { id: "config-1", presetId: "claude", label: "Claude" }, + ], + "agents.run": (input) => { + runInput = input; + return { kind: "terminal", sessionId: "terminal-1", label: "Codex" }; }, }); -mock.module("../../../lib/host-workspaces", () => ({ - resolveWorkspaceTarget: async () => ({ - hostId: "host-1", - workspace: { id: "00000000-0000-4000-8000-000000000001" }, - target: hostTarget(), - }), -})); - -mock.module("../../../lib/host-target", () => ({ - requireHostTarget: () => "host-1", - resolveHostTarget: hostTarget, -})); - -mock.module("../../../lib/upload-attachments", () => ({ - uploadAttachments: async () => [], -})); - -const { default: createAgentCommand } = await import("./command"); - function invoke( effort?: string, overrides: Record = { prompt: "Review this diff" }, @@ -75,8 +38,8 @@ function invoke( } as never, args: {} as never, options: { - workspace: "00000000-0000-4000-8000-000000000001", - host: "host-1", + workspace: WORKSPACE_ID, + local: true, agent: "codex", effort, ...overrides, @@ -101,7 +64,7 @@ describe("agents create", () => { }); expect(runInput).toEqual({ - workspaceId: "00000000-0000-4000-8000-000000000001", + workspaceId: WORKSPACE_ID, agent: "codex", prompt: "Review this diff", model: "gpt-5.6-sol", @@ -114,7 +77,7 @@ describe("agents create", () => { await invoke("xhigh"); expect(runInput).toEqual({ - workspaceId: "00000000-0000-4000-8000-000000000001", + workspaceId: WORKSPACE_ID, agent: "codex", prompt: "Review this diff", model: undefined, @@ -127,7 +90,7 @@ describe("agents create", () => { await invoke(); expect(runInput).toEqual({ - workspaceId: "00000000-0000-4000-8000-000000000001", + workspaceId: WORKSPACE_ID, agent: "codex", prompt: "Review this diff", model: undefined, @@ -140,7 +103,7 @@ describe("agents create", () => { await invoke(undefined, { resumeSession: "abc-123" }); expect(runInput).toEqual({ - workspaceId: "00000000-0000-4000-8000-000000000001", + workspaceId: WORKSPACE_ID, agent: "codex", prompt: "", resumeSessionId: "abc-123", @@ -154,7 +117,7 @@ describe("agents create", () => { await invoke(undefined, { forkSession: "thread-source" }); expect(runInput).toEqual({ - workspaceId: "00000000-0000-4000-8000-000000000001", + workspaceId: WORKSPACE_ID, agent: "codex", prompt: "", forkSessionId: "thread-source", @@ -192,7 +155,7 @@ describe("agents create", () => { await invoke(undefined, { fromTerminal: "terminal-source" }); expect(transcriptInput).toEqual({ - workspaceId: "00000000-0000-4000-8000-000000000001", + workspaceId: WORKSPACE_ID, terminalId: "terminal-source", }); const prompt = String(runInput?.prompt); diff --git a/packages/cli/src/commands/browser/open/command.test.ts b/packages/cli/src/commands/browser/open/command.test.ts index c0a92c4318e..f8a2b937136 100644 --- a/packages/cli/src/commands/browser/open/command.test.ts +++ b/packages/cli/src/commands/browser/open/command.test.ts @@ -1,19 +1,16 @@ import { describe, expect, mock, test } from "bun:test"; +import { withLocalHostService } from "../../../lib/host/test-helpers"; +import command from "./command"; const open = mock(async (input: { url: string }) => ({ paneId: "pane-1", url: input.url, })); -mock.module("../shared", () => ({ - resolveBrowserTarget: async () => ({ - client: { browser: { open: { mutate: open } } }, - }), -})); -const { default: command } = await import("./command"); +withLocalHostService("org-1", { "browser.open": open as never }); function invoke(show?: boolean, target?: string) { return command.run({ - ctx: {} as never, + ctx: { config: { organizationId: "org-1" }, bearer: "bearer" } as never, args: {} as never, options: { workspace: "agent-workspace", diff --git a/packages/cli/src/commands/config/check/command.ts b/packages/cli/src/commands/config/check/command.ts index 5ad79eb3575..f60e53f6aef 100644 --- a/packages/cli/src/commands/config/check/command.ts +++ b/packages/cli/src/commands/config/check/command.ts @@ -1,5 +1,5 @@ import { command } from "../../../lib/command"; -import { SUPERSET_CONFIG_PATH } from "../../../lib/config"; +import { getSupersetConfigPath } from "../../../lib/config"; import { type ConfigCheckResult, checkConfigFile, @@ -22,7 +22,7 @@ export default command({ "Validate the config file under ~/.superset before a hand edit or a bad restore surfaces as an auth failure; exits 1 on an error", skipMiddleware: true, run: async () => { - const result = checkConfigFile(SUPERSET_CONFIG_PATH); + const result = checkConfigFile(getSupersetConfigPath()); if (!result.valid) process.exitCode = 1; return { data: result, message: describe(result) }; }, diff --git a/packages/cli/src/commands/scripts/add/command.test.ts b/packages/cli/src/commands/scripts/add/command.test.ts index af0ee5273f9..6e0fb14f4fa 100644 --- a/packages/cli/src/commands/scripts/add/command.test.ts +++ b/packages/cli/src/commands/scripts/add/command.test.ts @@ -1,20 +1,12 @@ -import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { writeConfig } from "../../../lib/config"; import { readSettingsRow } from "../../../lib/settings"; import { createLocalSettingsDb, withTempSupersetHome, } from "../../../lib/settings/test-helpers"; - -let activeOrganizationId: string | undefined = "org-a"; - -const realConfig = await import("../../../lib/config"); -mock.module("../../../lib/config", () => ({ - ...realConfig, - readConfig: () => ({ organizationId: activeOrganizationId }), -})); - -const { default: addScriptCommand } = await import("./command"); -const { default: scriptsMeta } = await import("../meta"); +import scriptsMeta from "../meta"; +import addScriptCommand from "./command"; const home = withTempSupersetHome("superset-cli-script-command-"); let previousOrgOverride: string | undefined; @@ -35,10 +27,10 @@ function invoke(overrides: Record = {}) { beforeEach(() => { previousOrgOverride = process.env.SUPERSET_ORGANIZATION_ID; delete process.env.SUPERSET_ORGANIZATION_ID; - activeOrganizationId = "org-a"; // Port 9 (discard) has no listener, so no desktop app acknowledges. process.env.DESKTOP_NOTIFICATIONS_PORT = "9"; createLocalSettingsDb(home.dir); + writeConfig({ organizationId: "org-a" }); }); afterEach(() => { @@ -78,13 +70,13 @@ describe("scripts add", () => { }); test("requires an active organization", async () => { - activeOrganizationId = undefined; + writeConfig({}); await expect(invoke()).rejects.toThrow(/No active organization/); expect(readSettingsRow()).toBeUndefined(); }); test("honors the SUPERSET_ORGANIZATION_ID override like other commands", async () => { - activeOrganizationId = undefined; + writeConfig({}); process.env.SUPERSET_ORGANIZATION_ID = "org-env"; await invoke(); expect( diff --git a/packages/cli/src/commands/scripts/manage.test.ts b/packages/cli/src/commands/scripts/manage.test.ts index 816f5e6e621..0d06e9af07a 100644 --- a/packages/cli/src/commands/scripts/manage.test.ts +++ b/packages/cli/src/commands/scripts/manage.test.ts @@ -4,36 +4,28 @@ import { beforeEach, describe, expect, - mock, test, } from "bun:test"; +import { writeConfig } from "../../lib/config"; import { readSettingsRow } from "../../lib/settings"; import { writeSettings } from "../../lib/settings/local-settings"; import { createLocalSettingsDb, withTempSupersetHome, } from "../../lib/settings/test-helpers"; +import addCommand from "./add/command"; +import deleteCommand from "./delete/command"; +import editCommand from "./edit/command"; +import listCommand from "./list/command"; -let activeOrganizationId: string | undefined = "org-a"; let desktopRefreshed = false; -const realConfig = await import("../../lib/config"); -mock.module("../../lib/config", () => ({ - ...realConfig, - readConfig: () => ({ organizationId: activeOrganizationId }), -})); - const desktop = Bun.serve({ port: 0, fetch: () => new Response(null, { status: desktopRefreshed ? 200 : 503 }), }); afterAll(() => desktop.stop(true)); -const { default: addCommand } = await import("./add/command"); -const { default: listCommand } = await import("./list/command"); -const { default: editCommand } = await import("./edit/command"); -const { default: deleteCommand } = await import("./delete/command"); - const home = withTempSupersetHome("superset-cli-scripts-manage-"); let previousOrgOverride: string | undefined; @@ -70,10 +62,10 @@ function seedImported() { beforeEach(() => { previousOrgOverride = process.env.SUPERSET_ORGANIZATION_ID; delete process.env.SUPERSET_ORGANIZATION_ID; - activeOrganizationId = "org-a"; desktopRefreshed = false; process.env.DESKTOP_NOTIFICATIONS_PORT = String(desktop.port); createLocalSettingsDb(home.dir); + writeConfig({ organizationId: "org-a" }); }); afterEach(() => { @@ -185,7 +177,7 @@ describe("scripts edit", () => { test("requires an active organization", async () => { seedImported(); - activeOrganizationId = undefined; + writeConfig({}); await expect( run(editCommand, { args: { id: "gh-1" }, options: { name: "x" } }), ).rejects.toThrow(/No active organization/); diff --git a/packages/cli/src/commands/start/command.ts b/packages/cli/src/commands/start/command.ts index 1b20d7a9659..2c982730bee 100644 --- a/packages/cli/src/commands/start/command.ts +++ b/packages/cli/src/commands/start/command.ts @@ -1,7 +1,7 @@ import * as p from "@clack/prompts"; import { boolean, CLIError, number, string } from "@superset/cli-framework"; import { command } from "../../lib/command"; -import { SUPERSET_CONFIG_PATH } from "../../lib/config"; +import { getSupersetConfigPath } from "../../lib/config"; import { waitForUnresponsiveHost } from "../../lib/host/liveness"; import { isProcessAlive, @@ -65,7 +65,7 @@ export default command({ organizationId: organization.id, sessionToken: ctx.bearer, authConfigPath: - ctx.authSource === "oauth" ? SUPERSET_CONFIG_PATH : undefined, + ctx.authSource === "oauth" ? getSupersetConfigPath() : undefined, api: ctx.api, port: options.port, daemon: options.daemon ?? false, diff --git a/packages/cli/src/commands/start/manifest-liveness.test.ts b/packages/cli/src/commands/start/manifest-liveness.test.ts index 627d97ebcac..2db5d703986 100644 --- a/packages/cli/src/commands/start/manifest-liveness.test.ts +++ b/packages/cli/src/commands/start/manifest-liveness.test.ts @@ -13,8 +13,6 @@ process.env.SUPERSET_HOME_DIR = tempHome; // before the manifest check under test ever runs. delete process.env.SUPERSET_ORGANIZATION_ID; -// Imports below must come after SUPERSET_HOME_DIR is set: config.ts and -// manifest.ts both read it once at module load. const { readManifest, writeManifest } = await import("../../lib/host/manifest"); const startCommand = (await import("./command")).default; diff --git a/packages/cli/src/commands/status/command.test.ts b/packages/cli/src/commands/status/command.test.ts index c6a2756e16e..c98cd99cb0a 100644 --- a/packages/cli/src/commands/status/command.test.ts +++ b/packages/cli/src/commands/status/command.test.ts @@ -1,4 +1,4 @@ -import { afterAll, describe, expect, mock, test } from "bun:test"; +import { afterAll, afterEach, describe, expect, mock, test } from "bun:test"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -8,9 +8,9 @@ const originalSupersetHomeDir = process.env.SUPERSET_HOME_DIR; const tempHome = mkdtempSync(join(tmpdir(), "superset-cli-status-")); process.env.SUPERSET_HOME_DIR = tempHome; -// Imports below must come after SUPERSET_HOME_DIR is set: config.ts and -// manifest.ts both read it once at module load. -const { writeManifest } = await import("../../lib/host/manifest"); +const { removeManifest, writeManifest } = await import( + "../../lib/host/manifest" +); const statusCommand = (await import("./command")).default; afterAll(() => { @@ -24,6 +24,8 @@ afterAll(() => { const ORG = { id: "org-1", slug: "org-1", name: "Palette" }; +afterEach(() => removeManifest(ORG.id)); + function makeCtx(): CliContext { const query = mock(async () => [ORG]); const hostListQuery = mock(async () => []); diff --git a/packages/cli/src/commands/workspaces/archive/command.test.ts b/packages/cli/src/commands/workspaces/archive/command.test.ts index 9f3e32b3d9b..abbf5211eaa 100644 --- a/packages/cli/src/commands/workspaces/archive/command.test.ts +++ b/packages/cli/src/commands/workspaces/archive/command.test.ts @@ -4,7 +4,6 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { buildTree, routeCommand } from "@superset/cli-framework"; -// The cloud availability check caches to SUPERSET_HOME_DIR, read at import. const previousHome = process.env.SUPERSET_HOME_DIR; const home = mkdtempSync(join(tmpdir(), "superset-ws-archive-")); process.env.SUPERSET_HOME_DIR = home; diff --git a/packages/cli/src/commands/workspaces/create/command.test.ts b/packages/cli/src/commands/workspaces/create/command.test.ts index 940f33ac895..fc2950d9a3b 100644 --- a/packages/cli/src/commands/workspaces/create/command.test.ts +++ b/packages/cli/src/commands/workspaces/create/command.test.ts @@ -1,4 +1,6 @@ -import { afterEach, describe, expect, mock, test } from "bun:test"; +import { afterEach, describe, expect, test } from "bun:test"; +import { withLocalHostService } from "../../../lib/host/test-helpers"; +import createWorkspaceCommand from "./command"; let localCreateError: Error | undefined; let createProcedure: string | undefined; @@ -7,56 +9,23 @@ let sessionInput: Record | undefined; let cloudAvailable = false; -mock.module("../../../lib/cloud-workspaces", () => ({ - resolveWorkspaceHost: async (flags: { host?: string; local?: boolean }) => - flags.local - ? "host-1" - : (flags.host ?? (cloudAvailable ? undefined : "host-1")), - resolveCloudEnvironment: () => { - throw new Error("Unexpected environment lookup"); +withLocalHostService("org-1", { + "workspaces.createLocal": (input) => { + createProcedure = "createLocal"; + if (localCreateError) throw localCreateError; + createInput = input; + return { workspace: { name: "local" }, alreadyExists: false }; }, -})); - -mock.module("../../../lib/host-target", () => ({ - requireHostTarget: () => "host-1", - resolveHostTarget: () => ({ - hostId: "host-1", - client: { - workspaces: { - createLocal: { - mutate: async (input: Record) => { - createProcedure = "createLocal"; - if (localCreateError) throw localCreateError; - createInput = input; - return { workspace: { name: "local" }, alreadyExists: false }; - }, - }, - createSession: { - mutate: async (input: Record) => { - sessionInput = input; - return { workspace: { name: "scratch" } }; - }, - }, - create: { - mutate: async (input: Record) => { - createProcedure = "create"; - createInput = input; - return { - workspace: { name: "agent-effort" }, - alreadyExists: false, - }; - }, - }, - }, - }, - }), -})); - -mock.module("../../../lib/upload-attachments", () => ({ - uploadAttachments: async () => [], -})); - -const { default: createWorkspaceCommand } = await import("./command"); + "workspaces.createSession": (input) => { + sessionInput = input; + return { workspace: { name: "scratch" } }; + }, + "workspaces.create": (input) => { + createProcedure = "create"; + createInput = input; + return { workspace: { name: "agent-effort" }, alreadyExists: false }; + }, +}); function invoke( overrides: { @@ -75,6 +44,11 @@ function invoke( ) { return createWorkspaceCommand.run({ ctx: { + api: { + cloudWorkspace: { + available: { query: async () => ({ available: cloudAvailable }) }, + }, + }, config: { organizationId: "org-1" }, bearer: "bearer", } as never, diff --git a/packages/cli/src/commands/workspaces/get/command.test.ts b/packages/cli/src/commands/workspaces/get/command.test.ts index cb7004573a9..9130b4afad8 100644 --- a/packages/cli/src/commands/workspaces/get/command.test.ts +++ b/packages/cli/src/commands/workspaces/get/command.test.ts @@ -1,22 +1,11 @@ -import { afterEach, describe, expect, mock, test } from "bun:test"; +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { withLocalHostService } from "../../../lib/host/test-helpers"; +import getCommand from "./command"; -// Control what `resolveWorkspaceTarget` resolves per test. The command imports -// it from the lib barrel, so mock that module before importing the SUT. -type FindResult = { - hostId: string; - workspace: Record | undefined; -}; -let findResult: FindResult = { hostId: "host-1", workspace: undefined }; -mock.module("../../../lib/host-workspaces", () => ({ - resolveWorkspaceTarget: async () => { - if (!findResult.workspace) { - throw new Error(`Workspace not found on host ${findResult.hostId}`); - } - return { ...findResult, target: {} }; - }, -})); - -const { default: getCommand } = await import("./command"); +let workspaces: Array> = []; +let previousWorkspaceId: string | undefined; + +withLocalHostService("org-1", { "workspace.list": () => workspaces }); const WORKSPACE = { id: "b502bf30-8693-4815-be65-795035e0ce5f", @@ -64,14 +53,23 @@ function invoke(args: { id?: string }, options: { field?: string } = {}) { }); } -afterEach(() => { - findResult = { hostId: "host-1", workspace: undefined }; +beforeEach(() => { + previousWorkspaceId = process.env.SUPERSET_WORKSPACE_ID; delete process.env.SUPERSET_WORKSPACE_ID; }); +afterEach(() => { + workspaces = []; + if (previousWorkspaceId === undefined) { + delete process.env.SUPERSET_WORKSPACE_ID; + } else { + process.env.SUPERSET_WORKSPACE_ID = previousWorkspaceId; + } +}); + describe("workspaces get", () => { test("resolves by explicit id and enriches project/host names", async () => { - findResult = { hostId: "host-1", workspace: { ...WORKSPACE } }; + workspaces = [WORKSPACE]; const result = (await invoke({ id: WORKSPACE.id })) as { data: Record; message: string; @@ -86,7 +84,7 @@ describe("workspaces get", () => { test("defaults the id to $SUPERSET_WORKSPACE_ID when no arg is given", async () => { process.env.SUPERSET_WORKSPACE_ID = WORKSPACE.id; - findResult = { hostId: "host-1", workspace: { ...WORKSPACE } }; + workspaces = [WORKSPACE]; const result = (await invoke({})) as { data: Record }; expect(result.data.id).toBe(WORKSPACE.id); }); @@ -96,7 +94,7 @@ describe("workspaces get", () => { }); test("--field prints the raw value as the message, data stays full", async () => { - findResult = { hostId: "host-1", workspace: { ...WORKSPACE } }; + workspaces = [WORKSPACE]; const result = (await invoke({ id: WORKSPACE.id }, { field: "name" })) as { data: Record; message: string; @@ -106,7 +104,7 @@ describe("workspaces get", () => { }); test("--field with a null value yields an empty message", async () => { - findResult = { hostId: "host-1", workspace: { ...WORKSPACE } }; + workspaces = [WORKSPACE]; const result = (await invoke( { id: WORKSPACE.id }, { field: "taskId" }, @@ -117,29 +115,25 @@ describe("workspaces get", () => { }); test("--field rejects an unknown field name", async () => { - findResult = { hostId: "host-1", workspace: { ...WORKSPACE } }; + workspaces = [WORKSPACE]; await expect( invoke({ id: WORKSPACE.id }, { field: "bogus" }), ).rejects.toThrow(/Unknown field: bogus/); }); test("--field rejects inherited Object.prototype keys", async () => { - findResult = { hostId: "host-1", workspace: { ...WORKSPACE } }; + workspaces = [WORKSPACE]; await expect( invoke({ id: WORKSPACE.id }, { field: "toString" }), ).rejects.toThrow(/Unknown field: toString/); }); test("errors when the workspace is not on the target host", async () => { - findResult = { hostId: "host-1", workspace: undefined }; await expect(invoke({ id: WORKSPACE.id })).rejects.toThrow(/not found/); }); test("falls back to ids when the row has no project name and host lookup fails", async () => { - findResult = { - hostId: "host-1", - workspace: { ...WORKSPACE, projectName: null }, - }; + workspaces = [{ ...WORKSPACE, projectName: null }]; const result = (await getCommand.run({ ctx: { api: { diff --git a/packages/cli/src/lib/analytics.ts b/packages/cli/src/lib/analytics.ts index 975b45c4a18..f313a298c92 100644 --- a/packages/cli/src/lib/analytics.ts +++ b/packages/cli/src/lib/analytics.ts @@ -1,17 +1,12 @@ import { readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import type { ApiClient } from "./api-client"; -import { SUPERSET_HOME_DIR } from "./config"; import { env } from "./env"; - -const REPORTED_COMMANDS_PATH = join( - SUPERSET_HOME_DIR, - "reported-commands.json", -); +import { getSupersetHomeDir } from "./settings/paths"; export function isFirstReportToday( command: string, - path = REPORTED_COMMANDS_PATH, + path = join(getSupersetHomeDir(), "reported-commands.json"), ): boolean { const day = new Date().toISOString().slice(0, 10); let commands: string[] = []; diff --git a/packages/cli/src/lib/cloud-workspaces/resolveWorkspaceHost.ts b/packages/cli/src/lib/cloud-workspaces/resolveWorkspaceHost.ts index 4f25973b174..44a5c4a9c6a 100644 --- a/packages/cli/src/lib/cloud-workspaces/resolveWorkspaceHost.ts +++ b/packages/cli/src/lib/cloud-workspaces/resolveWorkspaceHost.ts @@ -2,13 +2,16 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { getHostId } from "@superset/shared/host-info"; import type { ApiClient } from "../api-client"; -import { SUPERSET_HOME_DIR } from "../config"; import { resolveHostFilter } from "../host-target"; +import { getSupersetHomeDir } from "../settings/paths"; -const AVAILABILITY_PATH = join(SUPERSET_HOME_DIR, "cloud-availability.json"); const AVAILABILITY_TTL_MS = 60 * 60_000; const AVAILABILITY_TIMEOUT_MS = 2_000; +function availabilityPath(): string { + return join(getSupersetHomeDir(), "cloud-availability.json"); +} + /** * The host a workspace command targets, or undefined for the cloud. `--local` * and `--host` always win. Without them the default is the cloud only for an @@ -56,7 +59,7 @@ async function cloudWorkspacesAvailable( } cache[organizationId] = { available, checkedAt: Date.now() }; try { - writeFileSync(AVAILABILITY_PATH, JSON.stringify(cache), { mode: 0o600 }); + writeFileSync(availabilityPath(), JSON.stringify(cache), { mode: 0o600 }); } catch { // Uncached, the next command asks again. } @@ -67,9 +70,10 @@ function readCache(): Record< string, { available: boolean; checkedAt: number } > { - if (!existsSync(AVAILABILITY_PATH)) return {}; + const path = availabilityPath(); + if (!existsSync(path)) return {}; try { - return JSON.parse(readFileSync(AVAILABILITY_PATH, "utf8")); + return JSON.parse(readFileSync(path, "utf8")); } catch { return {}; } diff --git a/packages/cli/src/lib/config.test.ts b/packages/cli/src/lib/config.test.ts index 774489caee0..e6fe5ae7349 100644 --- a/packages/cli/src/lib/config.test.ts +++ b/packages/cli/src/lib/config.test.ts @@ -1,131 +1,95 @@ -import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test"; +import { describe, expect, test } from "bun:test"; import * as nodeFs from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { getSupersetConfigPath, writeConfig } from "./config"; +import { withTempSupersetHome } from "./settings/test-helpers"; -// Snapshot real fs functions BEFORE `mock.module` so test setup + assertions -// keep working even though `node:fs` is about to be replaced for the SUT. -const realFs = { - chmodSync: nodeFs.chmodSync, - existsSync: nodeFs.existsSync, - mkdirSync: nodeFs.mkdirSync, - mkdtempSync: nodeFs.mkdtempSync, - readFileSync: nodeFs.readFileSync, - renameSync: nodeFs.renameSync, - rmSync: nodeFs.rmSync, - statSync: nodeFs.statSync, - unlinkSync: nodeFs.unlinkSync, - writeFileSync: nodeFs.writeFileSync, -}; - -const originalSupersetHomeDir = process.env.SUPERSET_HOME_DIR; -const tempHome = realFs.mkdtempSync(join(tmpdir(), "superset-cli-config-")); -process.env.SUPERSET_HOME_DIR = tempHome; - -// Per-test mutable state for the mocked fs. -let renameShouldFail = false; -const writtenPaths: string[] = []; -const unlinkedPaths: string[] = []; - -mock.module("node:fs", () => ({ - ...nodeFs, - writeFileSync: ( - path: nodeFs.PathOrFileDescriptor, - data: string | NodeJS.ArrayBufferView, - options?: nodeFs.WriteFileOptions, - ) => { - writtenPaths.push(String(path)); - return realFs.writeFileSync(path, data, options); - }, - renameSync: (oldPath: nodeFs.PathLike, newPath: nodeFs.PathLike) => { - if (renameShouldFail) throw new Error("rename failed"); - return realFs.renameSync(oldPath, newPath); - }, - unlinkSync: (path: nodeFs.PathLike) => { - unlinkedPaths.push(String(path)); - return realFs.unlinkSync(path); - }, -})); - -const { SUPERSET_CONFIG_PATH, writeConfig } = await import("./config"); - -beforeEach(() => { - writtenPaths.length = 0; - unlinkedPaths.length = 0; - renameShouldFail = false; - if (realFs.existsSync(SUPERSET_CONFIG_PATH)) { - realFs.unlinkSync(SUPERSET_CONFIG_PATH); - } -}); - -afterAll(() => { - realFs.rmSync(tempHome, { recursive: true, force: true }); - if (originalSupersetHomeDir === undefined) { - delete process.env.SUPERSET_HOME_DIR; - } else { - process.env.SUPERSET_HOME_DIR = originalSupersetHomeDir; - } -}); +withTempSupersetHome("superset-cli-config-"); describe("config writes", () => { test("writeConfig uses unique temp files", () => { - writeConfig({ apiKey: "sk_live_one" }); - writeConfig({ apiKey: "sk_live_two" }); + const writtenPaths: string[] = []; + const recordingFs = { + ...nodeFs, + writeFileSync: ((path, data, options) => { + writtenPaths.push(String(path)); + return nodeFs.writeFileSync(path, data, options); + }) as typeof nodeFs.writeFileSync, + }; + + writeConfig({ apiKey: "sk_live_one" }, recordingFs); + writeConfig({ apiKey: "sk_live_two" }, recordingFs); const tempWrites = writtenPaths.filter((p) => p.endsWith(".config.tmp")); expect(tempWrites).toHaveLength(2); expect(tempWrites[0]).not.toBe(tempWrites[1]); expect( - JSON.parse(realFs.readFileSync(SUPERSET_CONFIG_PATH, "utf-8")), + JSON.parse(nodeFs.readFileSync(getSupersetConfigPath(), "utf-8")), ).toEqual({ apiKey: "sk_live_two", }); }); test("writeConfig preserves old config if rename fails", () => { - realFs.writeFileSync( - SUPERSET_CONFIG_PATH, + nodeFs.writeFileSync( + getSupersetConfigPath(), JSON.stringify({ apiKey: "sk_live_old" }), ); - renameShouldFail = true; - - expect(() => writeConfig({ apiKey: "sk_live_new" })).toThrow( + const unlinkedPaths: string[] = []; + const failingFs = { + ...nodeFs, + renameSync: () => { + throw new Error("rename failed"); + }, + unlinkSync: ((path) => { + unlinkedPaths.push(String(path)); + return nodeFs.unlinkSync(path); + }) as typeof nodeFs.unlinkSync, + }; + + expect(() => writeConfig({ apiKey: "sk_live_new" }, failingFs)).toThrow( /rename failed/, ); expect( - JSON.parse(realFs.readFileSync(SUPERSET_CONFIG_PATH, "utf-8")), + JSON.parse(nodeFs.readFileSync(getSupersetConfigPath(), "utf-8")), ).toEqual({ apiKey: "sk_live_old", }); expect(unlinkedPaths).toHaveLength(1); - expect(realFs.existsSync(unlinkedPaths[0] ?? "")).toBe(false); + expect(nodeFs.existsSync(unlinkedPaths[0] ?? "")).toBe(false); }); test("writeConfig writes the exported Superset config path", () => { writeConfig({ organizationId: "org_123" }); expect( - JSON.parse(realFs.readFileSync(SUPERSET_CONFIG_PATH, "utf-8")), + JSON.parse(nodeFs.readFileSync(getSupersetConfigPath(), "utf-8")), ).toEqual({ organizationId: "org_123", }); }); test("writeConfig writes through a symlinked config.json", () => { - const dotfiles = realFs.mkdtempSync(join(tmpdir(), "superset-dotfiles-")); + const dotfiles = nodeFs.mkdtempSync(join(tmpdir(), "superset-dotfiles-")); const real = join(dotfiles, "superset-config.json"); - realFs.writeFileSync(real, JSON.stringify({ apiKey: "sk_live_old" })); - realFs.rmSync(SUPERSET_CONFIG_PATH, { force: true }); - nodeFs.symlinkSync(real, SUPERSET_CONFIG_PATH); + nodeFs.writeFileSync(real, JSON.stringify({ apiKey: "sk_live_old" })); + nodeFs.rmSync(getSupersetConfigPath(), { force: true }); + nodeFs.symlinkSync(real, getSupersetConfigPath()); writeConfig({ organizationId: "org_linked" }); - expect(realFs.statSync(SUPERSET_CONFIG_PATH).isSymbolicLink()).toBe(false); - expect(nodeFs.lstatSync(SUPERSET_CONFIG_PATH).isSymbolicLink()).toBe(true); - expect(JSON.parse(realFs.readFileSync(real, "utf-8"))).toEqual({ + expect(nodeFs.statSync(getSupersetConfigPath()).isSymbolicLink()).toBe( + false, + ); + expect(nodeFs.lstatSync(getSupersetConfigPath()).isSymbolicLink()).toBe( + true, + ); + expect(JSON.parse(nodeFs.readFileSync(real, "utf-8"))).toEqual({ organizationId: "org_linked", }); + nodeFs.rmSync(dotfiles, { recursive: true, force: true }); }); }); diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index 6eff6baab5c..e88e0cc8c1d 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -9,10 +9,10 @@ import { unlinkSync, writeFileSync, } from "node:fs"; -import { homedir } from "node:os"; import { dirname, join } from "node:path"; import { resolveWriteTarget } from "@superset/agent-setup/write-file-if-changed"; import { env } from "./env"; +import { getSupersetHomeDir } from "./settings/paths"; /** A token this close to `auth.expiresAt` is treated as expired. */ export const AUTH_REFRESH_LEEWAY_MS = 5 * 60 * 1000; @@ -27,27 +27,29 @@ export type SupersetConfig = { organizationId?: string; }; -export const SUPERSET_HOME_DIR = - process.env.SUPERSET_HOME_DIR ?? join(homedir(), ".superset"); -export const SUPERSET_CONFIG_PATH = join(SUPERSET_HOME_DIR, "config.json"); +export function getSupersetConfigPath(): string { + return join(getSupersetHomeDir(), "config.json"); +} function ensureDir() { - if (!existsSync(SUPERSET_HOME_DIR)) { - mkdirSync(SUPERSET_HOME_DIR, { recursive: true, mode: 0o700 }); + const homeDir = getSupersetHomeDir(); + if (!existsSync(homeDir)) { + mkdirSync(homeDir, { recursive: true, mode: 0o700 }); } try { - const stat = statSync(SUPERSET_HOME_DIR); - if ((stat.mode & 0o077) !== 0) chmodSync(SUPERSET_HOME_DIR, 0o700); + const stat = statSync(homeDir); + if ((stat.mode & 0o077) !== 0) chmodSync(homeDir, 0o700); } catch {} } export function readConfig(): SupersetConfig { - if (!existsSync(SUPERSET_CONFIG_PATH)) return {}; + const configPath = getSupersetConfigPath(); + if (!existsSync(configPath)) return {}; try { - const stat = statSync(SUPERSET_CONFIG_PATH); - if ((stat.mode & 0o077) !== 0) chmodSync(SUPERSET_CONFIG_PATH, 0o600); + const stat = statSync(configPath); + if ((stat.mode & 0o077) !== 0) chmodSync(configPath, 0o600); } catch {} - return JSON.parse(readFileSync(SUPERSET_CONFIG_PATH, "utf-8")); + return JSON.parse(readFileSync(configPath, "utf-8")); } /** @@ -62,22 +64,30 @@ export function resolveOrganizationId( return process.env.SUPERSET_ORGANIZATION_ID?.trim() || config.organizationId; } -export function writeConfig(config: SupersetConfig): void { +type ConfigWriteFs = Pick< + typeof import("node:fs"), + "writeFileSync" | "renameSync" | "unlinkSync" +>; + +export function writeConfig( + config: SupersetConfig, + fs: ConfigWriteFs = { writeFileSync, renameSync, unlinkSync }, +): void { ensureDir(); - const configPath = resolveWriteTarget(SUPERSET_CONFIG_PATH); + const configPath = resolveWriteTarget(getSupersetConfigPath()); const tempPath = join( dirname(configPath), `.${randomUUID()}.${process.pid}.config.tmp`, ); - writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 0o600 }); + fs.writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 0o600 }); try { chmodSync(tempPath, 0o600); } catch {} try { - renameSync(tempPath, configPath); + fs.renameSync(tempPath, configPath); } catch (error) { try { - unlinkSync(tempPath); + fs.unlinkSync(tempPath); } catch {} throw error; } diff --git a/packages/cli/src/lib/host-target/resolveCloudWorkspaceTarget.ts b/packages/cli/src/lib/host-target/resolveCloudWorkspaceTarget.ts index 44f09a5f458..f1bcc7b9d6a 100644 --- a/packages/cli/src/lib/host-target/resolveCloudWorkspaceTarget.ts +++ b/packages/cli/src/lib/host-target/resolveCloudWorkspaceTarget.ts @@ -6,7 +6,7 @@ import { getHostId } from "@superset/shared/host-info"; import { createTRPCClient, httpBatchLink, TRPCClientError } from "@trpc/client"; import SuperJSON from "superjson"; import type { ApiClient } from "../api-client"; -import { SUPERSET_HOME_DIR } from "../config"; +import { getSupersetHomeDir } from "../settings/paths"; import type { HostServiceClient, ResolvedHostTarget, @@ -22,11 +22,14 @@ interface Ticket { expiresAt: number; } -const TICKETS_PATH = join(SUPERSET_HOME_DIR, "cloud-workspace-tickets.json"); const TICKET_MARGIN_MS = 60_000; /** A stopped sandbox behind a cached ticket should fail over to a wake, not hang. */ const REACH_TIMEOUT_MS = 8_000; +function ticketsPath(): string { + return join(getSupersetHomeDir(), "cloud-workspace-tickets.json"); +} + /** * host-service inside a cloud workspace's sandbox, reached through the gate * with a ticket. The box answers in tens of milliseconds; asking the API to @@ -148,9 +151,10 @@ function targetFor(workspaceId: string, ticket: Ticket): ResolvedHostTarget { } function readTickets(): Record { - if (!existsSync(TICKETS_PATH)) return {}; + const path = ticketsPath(); + if (!existsSync(path)) return {}; try { - return JSON.parse(readFileSync(TICKETS_PATH, "utf8")); + return JSON.parse(readFileSync(path, "utf8")); } catch { return {}; } @@ -166,9 +170,10 @@ function saveTicket(key: string, ticket: Ticket): void { token: ticket.token, expiresAt: ticket.expiresAt, }; + const path = ticketsPath(); try { - writeFileSync(TICKETS_PATH, JSON.stringify(kept), { mode: 0o600 }); - chmodSync(TICKETS_PATH, 0o600); + writeFileSync(path, JSON.stringify(kept), { mode: 0o600 }); + chmodSync(path, 0o600); } catch { // A ticket that cannot be cached is minted again next call. } diff --git a/packages/cli/src/lib/host/manifest.ts b/packages/cli/src/lib/host/manifest.ts index 67fb64ad630..2180191666a 100644 --- a/packages/cli/src/lib/host/manifest.ts +++ b/packages/cli/src/lib/host/manifest.ts @@ -7,7 +7,7 @@ import { writeFileSync, } from "node:fs"; import { join } from "node:path"; -import { SUPERSET_HOME_DIR } from "../config"; +import { getSupersetHomeDir } from "../settings/paths"; /** * Manifest format matches the desktop app's HostServiceManifest @@ -23,7 +23,7 @@ export interface HostServiceManifest { } function manifestDir(organizationId: string): string { - return join(SUPERSET_HOME_DIR, "host", organizationId); + return join(getSupersetHomeDir(), "host", organizationId); } function manifestPath(organizationId: string): string { diff --git a/packages/cli/src/lib/host/spawn.test.ts b/packages/cli/src/lib/host/spawn.test.ts index 1db888f9ff7..ee1df11fe92 100644 --- a/packages/cli/src/lib/host/spawn.test.ts +++ b/packages/cli/src/lib/host/spawn.test.ts @@ -1,11 +1,15 @@ import { afterAll, afterEach, describe, expect, mock, test } from "bun:test"; -// Snapshot the real module BEFORE mock.module: bun module mocks are process-wide, -// so a partial replacement breaks other test files that import e.g. execFileSync. -import * as realChildProcess from "node:child_process"; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import type { ApiClient } from "../api-client"; +import { getSupersetConfigPath } from "../config"; +import { hostServiceLogPath } from "./manifest"; +import { + describeHostExit, + type SpawnHostOptions, + spawnHostService, +} from "./spawn"; const originalFetch = globalThis.fetch; const originalSupersetHomeDir = process.env.SUPERSET_HOME_DIR; @@ -46,14 +50,9 @@ const spawnMock = mock( }, ); -mock.module("node:child_process", () => ({ - ...realChildProcess, - spawn: spawnMock, -})); - -const { SUPERSET_CONFIG_PATH } = await import("../config"); -const { hostServiceLogPath } = await import("./manifest"); -const { describeHostExit, spawnHostService } = await import("./spawn"); +function spawnHost(options: SpawnHostOptions) { + return spawnHostService(options, spawnMock as never); +} function createApi(): ApiClient { return { @@ -91,7 +90,7 @@ describe("spawnHostService", () => { process.env.SUPERSET_HOST_BIN = join(tempHome, "missing-host"); try { await expect( - spawnHostService({ + spawnHost({ organizationId: "00000000-0000-0000-0000-000000000001", sessionToken: "session-token", api: createApi(), @@ -109,7 +108,7 @@ describe("spawnHostService", () => { process.env.SUPERSET_CLI_CHANNEL = "desktop-bundled"; try { await expect( - spawnHostService({ + spawnHost({ organizationId: "00000000-0000-0000-0000-000000000001", sessionToken: "session-token", api: createApi(), @@ -133,7 +132,7 @@ describe("spawnHostService", () => { ) as unknown as typeof fetch; await expect( - spawnHostService({ + spawnHost({ organizationId, sessionToken: "session-token", api: createApi(), @@ -153,10 +152,10 @@ describe("spawnHostService", () => { async () => new Response("ok", { status: 200 }), ) as unknown as typeof fetch; - await spawnHostService({ + await spawnHost({ organizationId: "00000000-0000-0000-0000-000000000001", sessionToken: "session-token", - authConfigPath: SUPERSET_CONFIG_PATH, + authConfigPath: getSupersetConfigPath(), api: createApi(), port: 54879, daemon: true, @@ -165,7 +164,7 @@ describe("spawnHostService", () => { expect(spawnCalls[0]?.options.env?.SUPERSET_HOST_AUTO_UPDATE).toBe("false"); expect(spawnMock).toHaveBeenCalledTimes(1); expect(spawnCalls[0]?.options.env?.SUPERSET_AUTH_CONFIG_PATH).toBe( - SUPERSET_CONFIG_PATH, + getSupersetConfigPath(), ); expect(spawnCalls[0]?.options.env?.AUTH_TOKEN).toBe("session-token"); }); @@ -175,7 +174,7 @@ describe("spawnHostService", () => { async () => new Response("ok", { status: 200 }), ) as unknown as typeof fetch; - const { exited } = await spawnHostService({ + const { exited } = await spawnHost({ organizationId: "00000000-0000-0000-0000-000000000001", sessionToken: "session-token", api: createApi(), @@ -202,7 +201,7 @@ test("passes the auto-update opt-in to the host", async () => { globalThis.fetch = mock( async () => new Response("ok"), ) as unknown as typeof fetch; - await spawnHostService({ + await spawnHost({ organizationId: "00000000-0000-0000-0000-000000000001", sessionToken: "session-token", api: createApi(), diff --git a/packages/cli/src/lib/host/spawn.ts b/packages/cli/src/lib/host/spawn.ts index e467b2fa5f1..642322a9214 100644 --- a/packages/cli/src/lib/host/spawn.ts +++ b/packages/cli/src/lib/host/spawn.ts @@ -9,8 +9,8 @@ import { openRotatingLogFd, } from "@superset/shared/rotating-log"; import type { ApiClient } from "../api-client"; -import { SUPERSET_HOME_DIR } from "../config"; import { env, isDesktopBundled } from "../env"; +import { getSupersetHomeDir } from "../settings/paths"; import { ensureManifestDir, type HostServiceManifest, @@ -111,6 +111,7 @@ function resolveMigrationsFolder(): string { export async function spawnHostService( options: SpawnHostOptions, + spawnProcess: typeof spawn = spawn, ): Promise { const hostBin = resolveHostBinary(); if (!existsSync(hostBin)) { @@ -139,7 +140,7 @@ export async function spawnHostService( MAX_HOST_LOG_BYTES, ) : -1; - const child = spawn(hostBin, [], { + const child = spawnProcess(hostBin, [], { stdio: options.daemon ? logFd === -1 ? "ignore" @@ -168,7 +169,7 @@ export async function spawnHostService( // (host-service-coordinator.ts); without it the host's PTYs get no // SUPERSET_HOME_DIR and every managed agent hook self-disables on // its own guard (#6254). - SUPERSET_HOME_DIR, + SUPERSET_HOME_DIR: getSupersetHomeDir(), }, }); diff --git a/packages/cli/src/lib/host/test-helpers.ts b/packages/cli/src/lib/host/test-helpers.ts new file mode 100644 index 00000000000..5f79371c3be --- /dev/null +++ b/packages/cli/src/lib/host/test-helpers.ts @@ -0,0 +1,74 @@ +import { afterAll, beforeEach } from "bun:test"; +import SuperJSON from "superjson"; +import { withTempSupersetHome } from "../settings/test-helpers"; +import { writeManifest } from "./manifest"; + +type Procedure = (input: Record) => unknown; + +/** + * Give every test in the calling file a running host-service on this machine: + * a local tRPC endpoint that answers `procedures` (keyed by dotted path), and + * a manifest for it in a fresh SUPERSET_HOME_DIR. + */ +export function withLocalHostService( + organizationId: string, + procedures: Record, +): void { + withTempSupersetHome("superset-cli-host-"); + + const server = Bun.serve({ + port: 0, + hostname: "127.0.0.1", + async fetch(request) { + const url = new URL(request.url); + const paths = url.pathname.replace(/^\/trpc\//, "").split(","); + const inputs: Record< + string, + Parameters[0] + > = + request.method === "GET" + ? JSON.parse(url.searchParams.get("input") ?? "{}") + : await request.json(); + return Response.json( + await Promise.all( + paths.map(async (path, index) => { + try { + const procedure = procedures[path]; + if (!procedure) { + throw new Error(`No procedure found on path "${path}"`); + } + const serialized = inputs[index]; + const output = await procedure( + serialized ? SuperJSON.deserialize(serialized) : {}, + ); + return { result: { data: SuperJSON.serialize(output) } }; + } catch (error) { + return { + error: SuperJSON.serialize({ + message: error instanceof Error ? error.message : "failed", + code: -32603, + data: { + code: "INTERNAL_SERVER_ERROR", + httpStatus: 500, + path, + }, + }), + }; + } + }), + ), + ); + }, + }); + afterAll(() => server.stop(true)); + + beforeEach(() => { + writeManifest({ + pid: process.pid, + endpoint: `http://127.0.0.1:${server.port}`, + authToken: "host-secret", + startedAt: Date.now(), + organizationId, + }); + }); +}