@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 +7 -1
- package/dist/dual-replica.d.ts +31 -9
- package/dist/dual-replica.js +37 -12
- package/dist/fake-e2b.d.ts +2 -1
- package/dist/fixtures.d.ts +5 -3
- package/dist/fixtures.js +5 -3
- package/dist/replica-runner.js +1 -1
- package/package.json +5 -4
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
|
|
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).
|
package/dist/dual-replica.d.ts
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
import type { BootstrapManifest } from "@capybara-hq/plugin-sdk";
|
|
2
2
|
/** 双副本测试工具的选项。 */
|
|
3
3
|
export interface DualReplicaOptions {
|
|
4
|
-
/**
|
|
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 `
|
|
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
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
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
|
-
*
|
|
107
|
-
*
|
|
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,
|
package/dist/dual-replica.js
CHANGED
|
@@ -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
|
-
*
|
|
26
|
-
*
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
|
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) {
|
package/dist/fake-e2b.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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}`)、
|
package/dist/fixtures.d.ts
CHANGED
|
@@ -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")]`).
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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")]`).
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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");
|
package/dist/replica-runner.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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-
|
|
59
|
-
"@capybara-hq/plugin-
|
|
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": {
|