@capybara-hq/testing 0.1.0-rc.0 → 0.1.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,7 +14,8 @@ npm install --save-dev @capybara-hq/testing
14
14
  platform), live in
15
15
  `fixtures/extensions/<name>` in the plugin pack layout; `packFixture(name, outDir)` packs one
16
16
  into a plugin tgz (needs the optional peer `@capybara-hq/plugin-tools` installed), `fakeExtensionEntry(name)` gives its
17
- `src/index.ts` for loading by path in a workspace. A test that edits a fake before packing
17
+ `src/index.ts` for loading by path (in `createDualReplicaHarness` wherever this package is
18
+ installed; in an in-process kernel when vitest transforms the kernel). A test that edits a fake before packing
18
19
  (bump the version, add a migration) packs a copy of `fakeExtensionDir(name)` instead, e.g. with
19
20
  the kernel tests' `packPluginDir(fakeExtensionDir("fake-tools-ui"), edit)`, which copies the
20
21
  directory to a temp dir and runs `edit` on the copy.
@@ -24,6 +25,11 @@ npm install --save-dev @capybara-hq/testing
24
25
  `openSse`, `truncateTables`, `pidOwningLocalPort`, and the kernel's `createDb` / `kernelDb`. With
25
26
  these a plugin's tests import only the SDK and this package (plus model-fake, the scripted model
26
27
  provider, to drive agent runs).
28
+ - `createDualReplicaHarness` starts each replica as a plain `node` child process. When an entry of
29
+ `extensions` is a TypeScript file (the plugin under test's `src/index.ts`, whose imports say
30
+ `./x.js`, or `fakeExtensionEntry(name)`), the replicas run under tsx, a dependency of this
31
+ package, resolved from it; no `NODE_OPTIONS` loader is needed. Packed plugins and package names
32
+ load in plain Node, as in production.
27
33
  - How extension tests are written: "Test template (dual-replica)" in
28
34
  [.agents/skills/writing-extensions](https://github.com/capybara-hq/core/tree/main/.agents/skills/writing-extensions).
29
35
  - The repository: [capybara-hq/core](https://github.com/capybara-hq/core).
@@ -1,21 +1,27 @@
1
1
  import type { BootstrapManifest } from "@capybara-hq/plugin-sdk";
2
2
  /** 双副本测试工具的选项。 */
3
3
  export interface DualReplicaOptions {
4
- /** Extensions to load (absolute paths, file URLs, or package names resolved from `resolveFrom`). */
4
+ /**
5
+ * Extensions to load (absolute paths, file URLs, or package names resolved from `resolveFrom`).
6
+ * A TypeScript entry (the plugin under test's `src/index.ts`, `fakeExtensionEntry(...)`) makes
7
+ * the replicas run under tsx, which this package ships ({@link needsTypeScript}), also when
8
+ * this package is installed from npm; nothing needs to be set in `NODE_OPTIONS`.
9
+ */
5
10
  extensions: string[];
6
11
  /** 解析裸包名插件的目录,默认调用方的 `process.cwd()`(与 `createKernel` 的 resolveFrom 同义)。 */
7
12
  resolveFrom?: string;
8
13
  /** 部署级插件配置,按插件名索引。 */
9
14
  extensionConfig?: Record<string, unknown>;
10
15
  /**
11
- * The bootstrap step (package ecosystem spec §7): every replica's kernel gets `bootstrapDir: dir`
16
+ * The bootstrap step (package ecosystem spec §7): every replica's kernel gets `bootstrap: { dir }`
12
17
  * and `bootstrapManifest: manifest`, so the replicas run it at start (once across them, under the
13
- * advisory lock) before loading extensions, as a deployment without an init container does. `dir`
14
- * holds the packages the manifest pins (`<name>-<version>.tgz`); build both with the kernel
15
- * tests' `bootstrapFixture` helper (it packs plugin directories with `pnpm pack-plugin`).
18
+ * advisory lock) before loading extensions, as `capybara start` does. `dir` is the optional
19
+ * offline directory (its `*.tgz` files are matched to the entries by sha512); the other entries
20
+ * are downloaded from `packages.npmRegistry` (set it in `config`, e.g. to a
21
+ * `startFakeNpmRegistry`). Build both with the kernel tests' `bootstrapFixture` helper.
16
22
  */
17
23
  bootstrap?: {
18
- dir: string;
24
+ dir?: string;
19
25
  manifest: BootstrapManifest;
20
26
  };
21
27
  /**
@@ -103,10 +109,26 @@ export interface Harness {
103
109
  stop(): Promise<void>;
104
110
  }
105
111
  /**
106
- * 子进程的 node 参数。`.ts` 的 runner(monorepo / 开发态)需要 tsx 与源码导出条件;
107
- * 编译后的 `.js` runner(发布态)直接用 node,不依赖 tsx。
112
+ * tsx's ESM loader, resolved from this package (tsx is one of its dependencies), as a URL for
113
+ * `--import`. Absolute, so it never depends on what the replica's cwd or the caller's repository
114
+ * has installed.
115
+ */
116
+ export declare const TSX_LOADER: string;
117
+ /**
118
+ * Whether a replica needs a TypeScript loader for `extensions`: true when one is a TypeScript file,
119
+ * such as the plugin under test by path (`plugins/<name>/src/index.ts`, whose imports say `./x.js`)
120
+ * or a fake extension of this package (`fakeExtensionEntry`). Plain Node strips types, but does
121
+ * not map `./x.js` to `./x.ts` and refuses to strip types under `node_modules`, where this package
122
+ * and its fakes live once installed from npm.
123
+ */
124
+ export declare function needsTypeScript(extensions: readonly string[]): boolean;
125
+ /**
126
+ * The replica's node arguments. A `.ts` runner (this repository, source) always runs under tsx
127
+ * with the source export condition. A compiled `.js` runner (installed from npm) gets tsx only when
128
+ * `typescript` is set ({@link needsTypeScript}): extensions loaded as packed plugins or by package
129
+ * name then run in plain Node, as in production, so a packaging bug is not hidden by the loader.
108
130
  */
109
- export declare function runnerArgv(runnerPath: string): string[];
131
+ export declare function runnerArgv(runnerPath: string, typescript?: boolean): string[];
110
132
  /**
111
133
  * 子进程的环境变量。`.ts` 的 runner 额外设置 `TSX_TSCONFIG_PATH` 指向仓库的 tsconfig.base.json:
112
134
  * tsx 只认子进程 cwd 下(且 include 覆盖到的)tsconfig,否则内核源码丢掉 experimentalDecorators,
@@ -1,7 +1,8 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { randomBytes } from "node:crypto";
3
+ import { createRequire } from "node:module";
3
4
  import { extname, resolve } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
6
  import { createFreshDatabase, sharedPgAdminUrl, startPostgresContainer, startTestRedis, } from "./containers.js";
6
7
  /** 副本在响应头里标明自己的序号(runner 把 `http.replicaId` 设为 `"0"` / `"1"`)。 */
7
8
  export const REPLICA_HEADER = "x-capybara-replica";
@@ -22,13 +23,34 @@ const STDERR_TAIL = 4000;
22
23
  /** runner 与本文件同目录;源码下是 .ts,编译后是 .js。 */
23
24
  const runnerPath = fileURLToPath(new URL(`./replica-runner${extname(fileURLToPath(import.meta.url))}`, import.meta.url));
24
25
  /**
25
- * 子进程的 node 参数。`.ts` 的 runner(monorepo / 开发态)需要 tsx 与源码导出条件;
26
- * 编译后的 `.js` runner(发布态)直接用 node,不依赖 tsx。
26
+ * tsx's ESM loader, resolved from this package (tsx is one of its dependencies), as a URL for
27
+ * `--import`. Absolute, so it never depends on what the replica's cwd or the caller's repository
28
+ * has installed.
27
29
  */
28
- export function runnerArgv(runnerPath) {
29
- return runnerPath.endsWith(".ts")
30
- ? ["--import", "tsx", "--conditions=@capybara-hq/source", runnerPath]
31
- : [runnerPath];
30
+ export const TSX_LOADER = pathToFileURL(createRequire(import.meta.url).resolve("tsx")).href;
31
+ /** An extension entry that is a TypeScript file (a path or a file URL), as opposed to a package. */
32
+ const TS_ENTRY = /\.[cm]?tsx?$/i;
33
+ /**
34
+ * Whether a replica needs a TypeScript loader for `extensions`: true when one is a TypeScript file,
35
+ * such as the plugin under test by path (`plugins/<name>/src/index.ts`, whose imports say `./x.js`)
36
+ * or a fake extension of this package (`fakeExtensionEntry`). Plain Node strips types, but does
37
+ * not map `./x.js` to `./x.ts` and refuses to strip types under `node_modules`, where this package
38
+ * and its fakes live once installed from npm.
39
+ */
40
+ export function needsTypeScript(extensions) {
41
+ return extensions.some((e) => TS_ENTRY.test(e));
42
+ }
43
+ /**
44
+ * The replica's node arguments. A `.ts` runner (this repository, source) always runs under tsx
45
+ * with the source export condition. A compiled `.js` runner (installed from npm) gets tsx only when
46
+ * `typescript` is set ({@link needsTypeScript}): extensions loaded as packed plugins or by package
47
+ * name then run in plain Node, as in production, so a packaging bug is not hidden by the loader.
48
+ */
49
+ export function runnerArgv(runnerPath, typescript = false) {
50
+ if (runnerPath.endsWith(".ts")) {
51
+ return ["--import", TSX_LOADER, "--conditions=@capybara-hq/source", runnerPath];
52
+ }
53
+ return typescript ? ["--import", TSX_LOADER, runnerPath] : [runnerPath];
32
54
  }
33
55
  /** 仓库根的 tsconfig.base.json(仅 monorepo 源码态存在)。 */
34
56
  const REPO_TSCONFIG = fileURLToPath(new URL("../../../tsconfig.base.json", import.meta.url));
@@ -41,8 +63,8 @@ export function runnerEnv(runnerPath, env = process.env) {
41
63
  return runnerPath.endsWith(".ts") ? { ...env, TSX_TSCONFIG_PATH: REPO_TSCONFIG } : env;
42
64
  }
43
65
  /** 启动一个副本子进程,等它通过 IPC 报告 ready。失败时抛出带 输出尾部的错误(不含配置)。 */
44
- async function startReplica(config, index) {
45
- const child = spawn(process.execPath, runnerArgv(runnerPath), {
66
+ async function startReplica(config, index, typescript) {
67
+ const child = spawn(process.execPath, runnerArgv(runnerPath, typescript), {
46
68
  stdio: ["ignore", "pipe", "pipe", "ipc"],
47
69
  env: runnerEnv(runnerPath),
48
70
  cwd: fileURLToPath(new URL("..", import.meta.url)),
@@ -145,7 +167,9 @@ export async function createDualReplicaHarness(opts) {
145
167
  masterKey: randomBytes(32).toString("base64"),
146
168
  extensions: opts.extensions,
147
169
  ...(opts.bootstrap !== undefined && {
148
- bootstrapDir: resolve(opts.bootstrap.dir),
170
+ bootstrap: {
171
+ ...(opts.bootstrap.dir !== undefined && { dir: resolve(opts.bootstrap.dir) }),
172
+ },
149
173
  bootstrapManifest: opts.bootstrap.manifest,
150
174
  }),
151
175
  // 副本进程的 cwd 不是调用方的 cwd,相对路径必须先按调用方 cwd 绝对化再经 IPC 传过去
@@ -154,7 +178,8 @@ export async function createDualReplicaHarness(opts) {
154
178
  overrides: opts.config ?? {},
155
179
  };
156
180
  const roles = opts.roles ?? [ALL_ROLES, ALL_ROLES];
157
- const started = await Promise.allSettled(roles.map((r, i) => startReplica({ ...config, roles: [...r] }, i)));
181
+ const typescript = needsTypeScript(opts.extensions);
182
+ const started = await Promise.allSettled(roles.map((r, i) => startReplica({ ...config, roles: [...r] }, i, typescript)));
158
183
  for (const s of started)
159
184
  if (s.status === "fulfilled")
160
185
  runners.push(s.value.runner);
@@ -210,7 +235,7 @@ export async function createDualReplicaHarness(opts) {
210
235
  if (stopPromise)
211
236
  throw new Error("harness is stopped");
212
237
  const index = handles.length;
213
- const started = await startReplica({ ...config, roles: [...extraRoles] }, index);
238
+ const started = await startReplica({ ...config, roles: [...extraRoles] }, index, typescript);
214
239
  runners.push(started.runner);
215
240
  // A stop() that began while this replica was starting must not leave it running
216
241
  if (stopPromise) {
@@ -1,5 +1,6 @@
1
1
  /**
2
- * 可编程的假 E2B(`plugins/sandbox-e2b` 测试用),协议按 `e2b` JS SDK 2.58.0 与 E2B 文档(2026-10-21 核实):
2
+ * A programmable fake E2B (for the tests of the sandbox-e2b plugin in capybara-hq/plugins), following
3
+ * the `e2b` JS SDK 2.58.0 and the E2B docs (checked 2026-10-21):
3
4
  *
4
5
  * - **控制面 API**(请求头 `X-API-KEY`,不符 → 401 `{code, message}`):
5
6
  * `POST /v2/sandboxes`(建沙箱,回 201 `{sandboxID, templateID, envdVersion, envdAccessToken, domain}`)、
@@ -25,9 +25,11 @@ export type FakeExtensionName = (typeof FAKE_EXTENSIONS)[number];
25
25
  export declare function fakeExtensionDir(name: FakeExtensionName): string;
26
26
  /**
27
27
  * The extension entry `src/index.ts` of a fake, for kernel configs that load extensions by path
28
- * (`extensions: [fakeExtensionEntry("fake-connector")]`). Loading TypeScript by path works in this
29
- * repository's tests; a consumer that installs this package from npm should pack the fake with
30
- * {@link packFixture} instead (Node does not strip types under node_modules).
28
+ * (`extensions: [fakeExtensionEntry("fake-connector")]`). `createDualReplicaHarness` loads it in
29
+ * its replicas wherever this package is installed (they run under tsx for TypeScript entries). An
30
+ * in-process kernel under vitest loads it when vitest transforms the kernel (in this repository, or
31
+ * with the core packages inlined: `server.deps.inline`); elsewhere, pack the fake with
32
+ * {@link packFixture} (Node does not strip types under node_modules).
31
33
  */
32
34
  export declare function fakeExtensionEntry(name: FakeExtensionName): string;
33
35
  /** The result of {@link packFixture}. */
package/dist/fixtures.js CHANGED
@@ -42,9 +42,11 @@ export function fakeExtensionDir(name) {
42
42
  }
43
43
  /**
44
44
  * The extension entry `src/index.ts` of a fake, for kernel configs that load extensions by path
45
- * (`extensions: [fakeExtensionEntry("fake-connector")]`). Loading TypeScript by path works in this
46
- * repository's tests; a consumer that installs this package from npm should pack the fake with
47
- * {@link packFixture} instead (Node does not strip types under node_modules).
45
+ * (`extensions: [fakeExtensionEntry("fake-connector")]`). `createDualReplicaHarness` loads it in
46
+ * its replicas wherever this package is installed (they run under tsx for TypeScript entries). An
47
+ * in-process kernel under vitest loads it when vitest transforms the kernel (in this repository, or
48
+ * with the core packages inlined: `server.deps.inline`); elsewhere, pack the fake with
49
+ * {@link packFixture} (Node does not strip types under node_modules).
48
50
  */
49
51
  export function fakeExtensionEntry(name) {
50
52
  return join(FIXTURES_DIR, name, "src/index.ts");
@@ -29,7 +29,7 @@ async function startKernel(c) {
29
29
  const config = KernelConfigSchema.parse(deepMerge(base, c.overrides));
30
30
  kernel = await createKernel(config, {
31
31
  resolveFrom: c.resolveFrom,
32
- ...(c.bootstrapDir !== undefined && { bootstrapDir: c.bootstrapDir }),
32
+ ...(c.bootstrap !== undefined && { bootstrap: c.bootstrap }),
33
33
  ...(c.bootstrapManifest !== undefined && { bootstrapManifest: c.bootstrapManifest }),
34
34
  });
35
35
  await kernel.start();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capybara-hq/testing",
3
- "version": "0.1.0-rc.0",
3
+ "version": "0.1.0-rc.2",
4
4
  "description": "Test helpers for Capybara: test containers, dual-replica harness and fake third-party servers",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -44,9 +44,10 @@
44
44
  "pg": "^8.23.1",
45
45
  "protobufjs": "^7.5.4",
46
46
  "testcontainers": "^12.2.1",
47
+ "tsx": "^4.23.15",
47
48
  "ws": "^8.22.0",
48
49
  "zod": "^4.6.7",
49
- "@capybara-hq/kernel": "^0.1.0-rc.0"
50
+ "@capybara-hq/kernel": "^0.1.0-rc.1"
50
51
  },
51
52
  "devDependencies": {
52
53
  "@types/pg": "^8.23.1",
@@ -55,8 +56,8 @@
55
56
  },
56
57
  "peerDependencies": {
57
58
  "vitest": "^5.0.5",
58
- "@capybara-hq/plugin-sdk": "^0.1.0-rc.0",
59
- "@capybara-hq/plugin-tools": "^0.1.0-rc.0"
59
+ "@capybara-hq/plugin-tools": "^0.1.0-rc.2",
60
+ "@capybara-hq/plugin-sdk": "^0.1.0-rc.0"
60
61
  },
61
62
  "peerDependenciesMeta": {
62
63
  "@capybara-hq/plugin-tools": {