diff --git a/AGENTS.md b/AGENTS.md index e1c4387c4..f3c30c7bd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,7 +47,7 @@ Unless user tells you exactly what to write: : new Worker(new URL("./.ts", import.meta.url).href, { type: "module" }); ``` When the process was started from the omp CLI — source `cli.ts`, npm-bundle `dist/cli.js`, or compiled binary — `workerHostEntry()` is `Bun.main` and the worker re-enters the single entry module, so no per-worker `--compile` entrypoints or bundle entries exist. Outside a CLI host (`bun test`, SDK embedding, standalone `omp-stats`) it returns `null` and the direct-module fallback loads the worker source. New worker kinds MUST add their selector to the dispatch table in `cli.ts` and keep the fallback branch. - History: `with { type: "file" }` only copied the entry as a raw asset (workers crashed silently in compiled binaries — issues #1011, #1027), and the later literal-path + extra-entrypoint pattern required keeping spawn literals and two build scripts in sync (issue #1150). The repro tests for those issues now pin the worker-host contract instead. + History: `with { type: "file" }` only copied the entry as a raw asset (workers crashed silently in compiled binaries — issues #1011, #1027), and the later literal-path + extra-entrypoint pattern required keeping spawn literals and two build scripts in sync (issue #1150). The smoke probe below is the live validation of this contract. Validate any new worker with the dedicated smoke probe: `omp --smoke-test` spawns the stats sync worker and the tiny-model subprocess, pings them, and exits — it's wired into `ci:test:smoke` and `scripts/install-tests/run-ci.sh` so binary, source-link, and tarball installs all exercise it. Add a sibling smoke if the new worker is on a different module graph. ## Bun Over Node @@ -226,6 +226,7 @@ Test the contract the system exposes — not the easiest internal detail to asse - Smoke tests are acceptable only when they catch a failure mode narrower tests would miss. "Package boots" or "command starts" alone is not enough. - Assert exact strings, ordering, and formatting only when downstream code parses or depends on the exact bytes. Otherwise assert semantic content. - Compile-time guarantees → type checks/type tests, not runtime placeholders. +- **Never source-grep.** A test that reads an implementation file (`.ts`/`.rs`/build script) and asserts on its *text* — `expect(src).toContain("someCall()")`, `.toMatch(/import .../)`, `.not.toContain("oldName")`, or "comment must say X" — is banned. It tests how code *looks*, not what it *does*: it breaks on harmless refactors (comment reflow, rename, import reorder) and passes while the behavior is broken. Assert the observable contract instead (run the code, check output/state/error), use the runtime smoke probe for wiring you cannot exercise in-process, and enforce structural invariants (no value-import of X, no self-import) with a type test or a lint/biome rule — never a string scan of the source. (Reading a file your code *wrote* — apply-patch result, generated bundle, temp fixture — and asserting on that output is fine; that is behavior, not a source grep.) - Don't add tests for tiny low-risk changes unless they protect a real contract or fix a regression-prone edge case. - Prefer focused package-local verification for the changed area. diff --git a/packages/coding-agent/test/extension-loader-self-import.test.ts b/packages/coding-agent/test/extension-loader-self-import.test.ts index f70221fed..b19cd8819 100644 --- a/packages/coding-agent/test/extension-loader-self-import.test.ts +++ b/packages/coding-agent/test/extension-loader-self-import.test.ts @@ -113,22 +113,4 @@ describe("extension loader host runtime binding", () => { expect(hookResult.hooks).toHaveLength(1); expect(hookResult.hooks[0].handlers.has("identity:event")).toBe(true); }); - - it("keeps runtime loaders free of bare package self-imports", async () => { - // Normal workspace resolution can make a bare self-import resolve to the same module, - // so keep a narrow static tripwire for the global-install mixed-version layout. - const loaderPaths = [ - path.join(import.meta.dir, "..", "src", "extensibility", "extensions", "loader.ts"), - path.join(import.meta.dir, "..", "src", "extensibility", "custom-tools", "loader.ts"), - path.join(import.meta.dir, "..", "src", "extensibility", "custom-commands", "loader.ts"), - path.join(import.meta.dir, "..", "src", "extensibility", "hooks", "loader.ts"), - ]; - - for (const loaderPath of loaderPaths) { - const source = await Bun.file(loaderPath).text(); - - expect(source).not.toMatch(/from\s+["']@oh-my-pi\/pi-coding-agent["']/); - expect(source).not.toMatch(/import\(\s*["']@oh-my-pi\/pi-coding-agent["']\s*\)/); - } - }); }); diff --git a/packages/coding-agent/test/issue-1011-repro.test.ts b/packages/coding-agent/test/issue-1011-repro.test.ts deleted file mode 100644 index 865ffd9f0..000000000 --- a/packages/coding-agent/test/issue-1011-repro.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import * as path from "node:path"; - -/** - * Regression for https://github.com/can1357/oh-my-pi/issues/1011 - * - * In v14.5.13 `spawnTabWorker` (in `src/tools/browser/tab-supervisor.ts`) - * resolved the worker entry as `new URL("./tab-worker-entry.ts", import.meta.url)` - * and passed `.href` to `new Worker(...)`. Bun's `--compile` static analyzer - * cannot discover that pattern, so the entry was never embedded in the - * single-file binary and the runtime symptom was "Timed out initializing - * browser tab worker". - * - * The original fix used an `import "./worker.ts" with { type: "file" }` - * trick. That copied the entry as a raw asset but could not resolve its - * relative imports inside the compiled binary, so the worker still failed - * to load (issue #1027 was the same root cause, retriggered). - * - * The current contract is simpler: when the process was started from the omp - * CLI (source, npm bundle, or compiled binary), spawn sites re-enter the - * declared worker-host entry — `new Worker(workerHostEntry(), { argv })` — and - * the CLI dispatches the hidden argv selector. Outside a CLI host (bun test, - * SDK embedding) they load the worker module directly. No separate worker - * module is ever bundled or listed as a `--compile` entrypoint. - */ -describe("issue #1011 — tab worker must re-enter the CLI entrypoint", () => { - const packageDir = path.resolve(import.meta.dir, ".."); - const supervisorPath = path.join(packageDir, "src/tools/browser/tab-supervisor.ts"); - const buildBinaryPath = path.join(packageDir, "scripts/build-binary.ts"); - const workerArg = "__omp_worker_tab"; - - it("tab-supervisor re-enters the worker-host entry with the argv selector", async () => { - const source = await Bun.file(supervisorPath).text(); - - expect( - source.includes("workerHostEntry()"), - "tab-supervisor.ts must spawn via the declared worker-host entry", - ).toBe(true); - expect(source).toContain(`argv: ["${workerArg}"]`); - expect( - source.includes('new URL("./tab-worker-entry.ts", import.meta.url)'), - "tab-supervisor.ts must keep the direct-module fallback for non-CLI hosts", - ).toBe(true); - }); - - it("build-binary.ts no longer lists tab-worker-entry as a separate --compile entrypoint", async () => { - const source = await Bun.file(buildBinaryPath).text(); - expect(source).not.toContain("./src/tools/browser/tab-worker-entry.ts"); - }); -}); diff --git a/packages/coding-agent/test/issue-1150-repro.test.ts b/packages/coding-agent/test/issue-1150-repro.test.ts deleted file mode 100644 index 15a65c029..000000000 --- a/packages/coding-agent/test/issue-1150-repro.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import * as path from "node:path"; - -/** - * Regression for https://github.com/can1357/oh-my-pi/issues/1150 - * - * In v15.1.3 `omp stats` crashed in the published Linux/macOS/Windows - * binaries with `BuildMessage: ModuleNotFound resolving - * "./packages/stats/src/sync-worker.ts" (entry point)`. The dev-mode build - * script `packages/coding-agent/scripts/build-binary.ts` listed the three - * worker entrypoints required by AGENTS.md, but the release script - * `scripts/ci-release-build-binaries.ts` — the one that actually builds the - * shipped artifacts — did not. The `new Worker("./packages//src/...")` - * literal at the spawn site fooled Bun's `--compile` static analyzer into - * keeping the call site, but without the matching `--compile` entrypoint - * the worker module was never emitted into bunfs and the runtime tried to - * bundle it on the fly, which fails in `$bunfs`. - * - * The current contract is simpler: every Worker re-enters the CLI entrypoint - * and selects its worker body via `WorkerOptions.argv`, so release builds no - * longer need to list the worker modules as extra `--compile` entrypoints. - * Runtime coverage lives in `omp --smoke-test`. - */ -describe("issue #1150 — release/dev builds route workers through the CLI entrypoint", () => { - const repoRoot = path.resolve(import.meta.dir, "../../.."); - const ciScriptPath = path.join(repoRoot, "scripts/ci-release-build-binaries.ts"); - const devScriptPath = path.join(repoRoot, "packages/coding-agent/scripts/build-binary.ts"); - - // Repo-root-relative CLI literal — every runtime worker spawn site uses this - // same entry plus a hidden argv selector. - const workerEntrypoints = [ - "./packages/stats/src/sync-worker.ts", - "./packages/coding-agent/src/tools/browser/tab-worker-entry.ts", - "./packages/coding-agent/src/eval/js/worker-entry.ts", - ]; - - it("release/dev build scripts do not list worker modules as explicit --compile entrypoints", async () => { - const releaseSource = await Bun.file(ciScriptPath).text(); - const devSource = await Bun.file(devScriptPath).text(); - for (const entry of workerEntrypoints) { - expect(releaseSource).not.toContain(`"${entry}"`); - } - for (const entry of [ - "../stats/src/sync-worker.ts", - "./src/tools/browser/tab-worker-entry.ts", - "./src/eval/js/worker-entry.ts", - ]) { - expect(devSource).not.toContain(`"${entry}"`); - } - }); -}); diff --git a/packages/coding-agent/test/issue-1606-repro.test.ts b/packages/coding-agent/test/issue-1606-repro.test.ts index fa57764b3..6afb49fb2 100644 --- a/packages/coding-agent/test/issue-1606-repro.test.ts +++ b/packages/coding-agent/test/issue-1606-repro.test.ts @@ -16,7 +16,7 @@ */ import { describe, expect, it } from "bun:test"; import * as path from "node:path"; -import { createTinyTitleSubprocess, TINY_WORKER_ARG } from "@oh-my-pi/pi-coding-agent/tiny/title-client"; +import { createTinyTitleSubprocess } from "@oh-my-pi/pi-coding-agent/tiny/title-client"; describe("issue #1606 — tiny model lives in an isolated subprocess", () => { it("ping/pongs through the spawned worker subprocess and tears it down cleanly", async () => { @@ -41,16 +41,6 @@ describe("issue #1606 — tiny model lives in an isolated subprocess", () => { expect(exitCode).toBe(0); }, 30_000); - it("CLI dispatches the flag that `title-client.ts` passes to the spawned child", async () => { - // `tinyWorkerSpawnCmd()` and the cli switch must agree on the exact - // flag, character-for-character — the spawned `bun`/binary sees only - // `argv` and there is no fallback path that "re-routes" the worker - // on misnamed flags. Pin the spelling on both ends. - const cliSource = await Bun.file(new URL("../src/cli.ts", import.meta.url)).text(); - expect(cliSource).toContain(`"${TINY_WORKER_ARG}"`); - expect(cliSource).toContain("runTinyWorker"); - }); - it("surfaces unexpected signal exits so in-flight callers don't await forever", async () => { // If the child dies from a signal we did NOT request — SIGSEGV from a // native crash (the original Windows shutdown bug, now relocated to diff --git a/packages/coding-agent/test/issue-2127-repro.test.ts b/packages/coding-agent/test/issue-2127-repro.test.ts index a7f60c878..29ada234d 100644 --- a/packages/coding-agent/test/issue-2127-repro.test.ts +++ b/packages/coding-agent/test/issue-2127-repro.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "bun:test"; -import * as path from "node:path"; import { EnhancedPasteController } from "@oh-my-pi/pi-coding-agent/utils/enhanced-paste"; /** @@ -103,23 +102,3 @@ describe("issue #2127 — enhanced-paste text must follow focus", () => { expect(editor.pasted).toEqual(["fallback body"]); }); }); - -describe("issue #2127 — InputController wires enhanced-paste through focus", () => { - const packageDir = path.resolve(import.meta.dir, ".."); - const controllerPath = path.join(packageDir, "src/modes/controllers/input-controller.ts"); - - it("input-controller routes the enhanced-paste text callback through ui.getFocused", async () => { - const source = await Bun.file(controllerPath).text(); - // Anchor the assertion on the EnhancedPasteController construction block: - // the `pasteText` callback must consult the focused component, not stash - // every payload into `this.ctx.editor` unconditionally. A future refactor - // that drops `getFocused()` from this callback re-introduces the bug. - const constructionStart = source.indexOf("new EnhancedPasteController("); - expect(constructionStart, "InputController must still construct EnhancedPasteController").toBeGreaterThan(-1); - const constructionSlice = source.slice(constructionStart, constructionStart + 2_000); - expect( - constructionSlice.includes("getFocused()"), - "The enhanced-paste callback must consult ui.getFocused() so modal Input prompts (OAuth API-key entry, OTPs, redirect URLs) receive the pasted text instead of the detached main editor (#2127).", - ).toBe(true); - }); -}); diff --git a/packages/coding-agent/test/issue-3031-repro.test.ts b/packages/coding-agent/test/issue-3031-repro.test.ts index bfa1329e9..253df3658 100644 --- a/packages/coding-agent/test/issue-3031-repro.test.ts +++ b/packages/coding-agent/test/issue-3031-repro.test.ts @@ -19,7 +19,6 @@ import { describe, expect, it } from "bun:test"; import * as path from "node:path"; import { createMnemopiEmbedSubprocess, - MNEMOPI_EMBED_WORKER_ARG, MnemopiEmbedClient, type MnemopiEmbedWorkerHandle, } from "@oh-my-pi/pi-coding-agent/mnemopi/embed-client"; @@ -51,16 +50,6 @@ describe("issue #3031 — mnemopi embeddings live in an isolated subprocess", () expect(exitCode).toBe(0); }, 30_000); - it("CLI dispatches the flag that `embed-client.ts` passes to the spawned child", async () => { - // `mnemopiEmbedWorkerSpawnCmd()` and the cli switch must agree on the - // exact flag, character-for-character — the spawned `bun`/binary sees - // only `argv` and there is no fallback path that "re-routes" the - // worker on misnamed flags. Pin the spelling on both ends. - const cliSource = await Bun.file(new URL("../src/cli.ts", import.meta.url)).text(); - expect(cliSource).toContain(`"${MNEMOPI_EMBED_WORKER_ARG}"`); - expect(cliSource).toContain("startMnemopiEmbedWorker"); - }); - it("surfaces unexpected signal exits so in-flight callers don't await forever", async () => { // If the child dies from a signal we did NOT request — SIGSEGV from // onnxruntime's NAPI fault (the original Windows shutdown bug, now @@ -106,27 +95,6 @@ describe("issue #3031 — mnemopi embeddings live in an isolated subprocess", () expect(errored).toBe(false); }, 10_000); - it("does not import fastembed-runtime from the main agent module graph", async () => { - // Issue #3031 caused: `mnemopi/state.ts` (or anything it transitively - // loaded) statically importing `core/fastembed-runtime`, which loads - // `onnxruntime-node` natively. Only the dedicated worker module is - // allowed to reach into that runtime. Scan the agent's mnemopi shim - // surface and the session entrypoint to lock the rule in. - const candidates = [ - "../src/mnemopi/state.ts", - "../src/mnemopi/backend.ts", - "../src/mnemopi/embed-client.ts", - "../src/mnemopi/embed-protocol.ts", - "../src/session/agent-session.ts", - "../src/cli.ts", - ]; - for (const rel of candidates) { - const source = await Bun.file(new URL(rel, import.meta.url)).text(); - expect(source).not.toContain("fastembed-runtime"); - expect(source).not.toContain('"fastembed"'); - expect(source).not.toContain('"onnxruntime-node"'); - } - }); it("carries (model, cacheDir) on every embed so a respawned worker can self-init", async () => { // Without this contract: after `shutdownMnemopiEmbedClient()` runs on // session dispose, mnemopi still holds the cached `LocalEmbeddingModel` diff --git a/packages/coding-agent/test/issue-3461-repro.test.ts b/packages/coding-agent/test/issue-3461-repro.test.ts deleted file mode 100644 index 84379041d..000000000 --- a/packages/coding-agent/test/issue-3461-repro.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import * as path from "node:path"; - -/** - * Regression for https://github.com/can1357/oh-my-pi/issues/3461 - * - * Ctrl+Z stopped working after any tool call: the TUI tore down but the - * process kept running (`Sl+`, not `T`), wedging the terminal until - * `kill -9`. Root cause: brush-core's `Process::wait` calls - * `tokio::signal::unix::signal(SIGTSTP)` to detect when its children get - * stopped. Per tokio's documented contract the first call for a SignalKind - * permanently replaces the kernel-default handler — so after the first - * `Shell::run` the parent's SIGTSTP no longer triggers the kernel STOP - * action, and `process.kill(0, "SIGTSTP")` from the Ctrl+Z handler became - * a no-op. - * - * The fix has two halves: - * - * - `handleCtrlZ` sends SIGSTOP (uncatchable) to the foreground process - * group, so the kernel parks omp regardless of installed handlers and the - * parent shell sees the whole job stop even when omp runs behind a wrapper - * (`npx`, `pnpm exec`, `bunx`, …) or as one stage of a pipeline. - * - MCP stdio servers spawn detached, so terminal job-control signals cannot - * stop their process trees and leave the JSONL read loop blocked on silent - * pipes — and so the pgid=0 suspend above doesn't reach them either. - * The unit test in `input-controller-suspend.test.ts` covers the JS handler's - * call shape; this file pins the runtime contract on the brush/MCP side so - * refactors force a deliberate revisit instead of silently regressing behavior. - */ -describe("issue #3461 — Ctrl+Z hangs after a command has been run", () => { - const packageDir = path.resolve(import.meta.dir, ".."); - const brushUnixSignal = path.resolve(packageDir, "../../crates/vendor/brush-core/src/sys/unix/signal.rs"); - const brushProcesses = path.resolve(packageDir, "../../crates/vendor/brush-core/src/processes.rs"); - const inputController = path.resolve(packageDir, "src/modes/controllers/input-controller.ts"); - const mcpStdioTransport = path.resolve(packageDir, "src/mcp/transports/stdio.ts"); - - it("brush-core installs a tokio SIGTSTP listener on every Process::wait", async () => { - const signalSrc = await Bun.file(brushUnixSignal).text(); - expect(signalSrc).toContain("tstp_signal_listener"); - expect(signalSrc).toContain("tokio::signal::unix::signal"); - // Pin the SIGTSTP constant specifically. A move to a non-job-control - // signal would invalidate the assumption this fix is built on. - expect(signalSrc).toMatch(/nix::libc::SIGTSTP/); - - const processesSrc = await Bun.file(brushProcesses).text(); - expect(processesSrc).toContain("tstp_signal_listener"); - }); - - it("handleCtrlZ sends SIGSTOP to the foreground process group, defeating the brush hijack and covering wrappers/pipelines", async () => { - const src = await Bun.file(inputController).text(); - // The original broken shape must not return. - expect(src).not.toMatch(/process\.kill\(\s*0\s*,\s*["']SIGTSTP["']\s*\)/); - // Self-only SIGSTOP was the v1 of this fix; it leaves wrapper/pipeline - // peers in the same process group running and the shell never sees the - // job stop. The handler now targets pgid=0. - expect(src).not.toMatch(/process\.kill\(\s*process\.pid\s*,\s*["']SIGSTOP["']\s*\)/); - // pgid=0 SIGSTOP is the only correct shape: uncatchable, and reaches - // every process the shell considers part of the foreground job. - expect(src).toMatch(/process\.kill\(\s*0\s*,\s*["']SIGSTOP["']\s*\)/); - }); - - it("MCP stdio servers spawn detached so terminal job-control signals cannot stop them", async () => { - const src = await Bun.file(mcpStdioTransport).text(); - expect(src).toMatch(/detached:\s*true/); - expect(src).toContain("no controlling terminal"); - }); -}); diff --git a/packages/coding-agent/test/startup-import-graph.test.ts b/packages/coding-agent/test/startup-import-graph.test.ts deleted file mode 100644 index 3d198d09a..000000000 --- a/packages/coding-agent/test/startup-import-graph.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import * as path from "node:path"; - -const sourceRoot = path.join(import.meta.dir, "..", "src"); - -describe("startup import graph", () => { - it("keeps normal startup off the aggregate modes barrel", async () => { - const mainSource = await Bun.file(path.join(sourceRoot, "main.ts")).text(); - - expect(mainSource).toContain('import { InteractiveMode } from "./modes/interactive-mode";'); - expect(mainSource).not.toContain('from "./modes"'); - }); - - it("keeps branch-only mode runners out of the modes barrel", async () => { - const modesBarrelSource = await Bun.file(path.join(sourceRoot, "modes/index.ts")).text(); - - expect(modesBarrelSource).toContain('from "./interactive-mode"'); - expect(modesBarrelSource).not.toContain("runAcpMode"); - expect(modesBarrelSource).not.toContain("runPrintMode"); - expect(modesBarrelSource).not.toContain("runRpcMode"); - expect(modesBarrelSource).not.toContain("./rpc/rpc-mode"); - }); - - it("keeps marketplace implementation behind the lightweight auto-update starter", async () => { - const mainSource = await Bun.file(path.join(sourceRoot, "main.ts")).text(); - const starterSource = await Bun.file( - path.join(sourceRoot, "extensibility/plugins/marketplace-auto-update.ts"), - ).text(); - - expect(mainSource).toContain('from "./extensibility/plugins/marketplace-auto-update"'); - expect(mainSource).not.toContain('from "./extensibility/plugins/marketplace"'); - expect(starterSource).toContain('await import("./marketplace")'); - }); - - it("keeps puppeteer-core/@puppeteer/browsers off the eager startup graph", async () => { - // The builtin tool registry (tools/index.ts) statically imports BrowserTool, which - // reaches attach.ts -> registry.ts -> launch.ts -> tab-supervisor.ts. A *value* - // import from puppeteer-core (e.g. the TargetType enum) executes puppeteer-core's - // barrel at boot, which re-exports the node launchers + BrowserConnector and pulls in - // @puppeteer/browsers (the Chromium downloader). A packaging quirk in that subtree then - // becomes a hard startup crash (e.g. `Cannot find module './browser-data/browser-data.js'`) - // instead of a recoverable ToolError on first browser use. These modules must therefore - // import puppeteer packages as `import type` only. tab-worker.ts is excluded: it runs - // solely inside the spawned browser worker, off the main startup graph. - const browserDir = path.join(sourceRoot, "tools", "browser"); - const eagerFiles = ["attach.ts", "registry.ts", "launch.ts", "tab-supervisor.ts"]; - const valueImportRe = /^import\s+(?!type\b)[^;]*?from\s+["'](?:puppeteer-core|@puppeteer\/browsers)["']/gm; - for (const file of eagerFiles) { - const src = await Bun.file(path.join(browserDir, file)).text(); - const offenders = src.match(valueImportRe) ?? []; - expect(offenders, `${file} value-imports puppeteer; use \`import type\``).toEqual([]); - } - }); -}); diff --git a/packages/coding-agent/test/stats-dashboard-bundle.test.ts b/packages/coding-agent/test/stats-dashboard-bundle.test.ts deleted file mode 100644 index 308763721..000000000 --- a/packages/coding-agent/test/stats-dashboard-bundle.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import * as path from "node:path"; - -describe("stats dashboard assets in distributed CLI builds", () => { - const repoRoot = path.resolve(import.meta.dir, "../../.."); - const bundleScriptPath = path.join(repoRoot, "packages/coding-agent/scripts/bundle-dist.ts"); - const cliPath = path.join(repoRoot, "packages/coding-agent/src/cli.ts"); - const statsServerPath = path.join(repoRoot, "packages/stats/src/server.ts"); - - it("embeds the stats client archive while building the npm CLI bundle", async () => { - const bundleScript = await Bun.file(bundleScriptPath).text(); - expect(bundleScript).toContain(`"scripts/generate-client-bundle.ts", "--generate"`); - expect(bundleScript).toContain(`"scripts/generate-client-bundle.ts", "--reset"`); - expect(bundleScript).toContain(`process.env.PI_BUNDLED="true"`); - }); - - it("uses embedded stats assets for prebuilt CLI distributions", async () => { - const statsServer = await Bun.file(statsServerPath).text(); - expect(statsServer).toContain("process.env.PI_BUNDLED"); - expect(statsServer).toContain("USE_EMBEDDED_CLIENT"); - expect(statsServer).toContain("Embedded stats client bundle missing"); - }); - - it("probes dashboard static assets in the install smoke test path", async () => { - const cliSource = await Bun.file(cliPath).text(); - expect(cliSource).toContain("startServer(0)"); - expect(cliSource).toContain("127.0.0.1"); - expect(cliSource).toContain("dashboard HTML was not served"); - }); -});