@capybara-hq/testing 0.1.0-rc.1 → 0.1.0-rc.3
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/dist/dual-replica.d.ts +10 -6
- package/dist/dual-replica.js +3 -1
- package/dist/fake-e2b.d.ts +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/kernel-helpers.d.ts +34 -13
- package/dist/kernel-helpers.js +38 -10
- package/dist/replica-runner.js +17 -9
- package/package.json +3 -3
package/dist/dual-replica.d.ts
CHANGED
|
@@ -8,19 +8,23 @@ export interface DualReplicaOptions {
|
|
|
8
8
|
* this package is installed from npm; nothing needs to be set in `NODE_OPTIONS`.
|
|
9
9
|
*/
|
|
10
10
|
extensions: string[];
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Where extensions named by package resolve from; default the caller's `process.cwd()` (the
|
|
13
|
+
* `resolveFrom` of `createKernelApp`).
|
|
14
|
+
*/
|
|
12
15
|
resolveFrom?: string;
|
|
13
16
|
/** 部署级插件配置,按插件名索引。 */
|
|
14
17
|
extensionConfig?: Record<string, unknown>;
|
|
15
18
|
/**
|
|
16
|
-
* The bootstrap step (package ecosystem spec §7): every replica's kernel gets `
|
|
19
|
+
* The bootstrap step (package ecosystem spec §7): every replica's kernel gets `bootstrap: { dir }`
|
|
17
20
|
* and `bootstrapManifest: manifest`, so the replicas run it at start (once across them, under the
|
|
18
|
-
* advisory lock) before loading extensions, as
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
+
* advisory lock) before loading extensions, as `capybara start` does. `dir` is the optional
|
|
22
|
+
* offline directory (its `*.tgz` files are matched to the entries by sha512); the other entries
|
|
23
|
+
* are downloaded from `packages.npmRegistry` (set it in `config`, e.g. to a
|
|
24
|
+
* `startFakeNpmRegistry`). Build both with the kernel tests' `bootstrapFixture` helper.
|
|
21
25
|
*/
|
|
22
26
|
bootstrap?: {
|
|
23
|
-
dir
|
|
27
|
+
dir?: string;
|
|
24
28
|
manifest: BootstrapManifest;
|
|
25
29
|
};
|
|
26
30
|
/**
|
package/dist/dual-replica.js
CHANGED
|
@@ -167,7 +167,9 @@ export async function createDualReplicaHarness(opts) {
|
|
|
167
167
|
masterKey: randomBytes(32).toString("base64"),
|
|
168
168
|
extensions: opts.extensions,
|
|
169
169
|
...(opts.bootstrap !== undefined && {
|
|
170
|
-
|
|
170
|
+
bootstrap: {
|
|
171
|
+
...(opts.bootstrap.dir !== undefined && { dir: resolve(opts.bootstrap.dir) }),
|
|
172
|
+
},
|
|
171
173
|
bootstrapManifest: opts.bootstrap.manifest,
|
|
172
174
|
}),
|
|
173
175
|
// 副本进程的 cwd 不是调用方的 cwd,相对路径必须先按调用方 cwd 绝对化再经 IPC 传过去
|
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/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* package and the SDK: the database helpers and types, and model-visible texts, queue names and
|
|
4
4
|
* keys the kernel owns (assert on them instead of copying their values).
|
|
5
5
|
*/
|
|
6
|
-
export { type ConversationEvent, createDb, gatewayReplicasKey, type
|
|
6
|
+
export { type ConversationEvent, createDb, gatewayReplicasKey, type KernelConfig, type KernelSchemaDb, kernelDb, MAINTENANCE_QUEUE_NAME, runJobId, SANDBOX_LOCK_TTL_MS, SANDBOX_RECREATED_NOTICE, SANDBOX_STARTED_NOTICE, SANDBOX_SUSPEND_JOB, UNKNOWN_RESULT_TEXT, } from "@capybara-hq/kernel";
|
|
7
7
|
export { createFreshDatabase, startTestPostgres, startTestRedis, startTestS3, TEST_S3_IMAGE, type TestContainer, type TestS3, } from "./containers.js";
|
|
8
8
|
export { createDualReplicaHarness, type DualReplicaOptions, type ExtraReplicaHandle, type Harness, REPLICA_HEADER, type ReplicaHandle, type ReplicaResponse, replicaOf, } from "./dual-replica.js";
|
|
9
9
|
export { assertExtensionSecretsReachable, extensionDeploymentSecretValues, } from "./extension-config.js";
|
|
@@ -19,7 +19,7 @@ export { type FakeOAuthClient, type FakeOAuthServer, type FakeOAuthServerOptions
|
|
|
19
19
|
export { defaultFakeOpenApiSpec, type FakeOpenApiRequest, type FakeOpenApiServer, type FakeOpenApiServerOptions, type FakePet, startFakeOpenApiServer, } from "./fake-openapi-server.js";
|
|
20
20
|
export { type FakeSlackAck, type FakeSlackCall, type FakeSlackFailure, type FakeSlackMessage, type FakeSlackOptions, type FakeSlackServer, type FakeSlackUser, findSlackButton, signSlackWebhook, slackBlockAction, slackDmEvent, slackEventCallback, slackInteractiveForm, slackMentionEvent, startFakeSlack, } from "./fake-slack.js";
|
|
21
21
|
export { FAKE_EXTENSIONS, type FakeExtensionName, fakeExtensionDir, fakeExtensionEntry, type PackedFixture, packFixture, } from "./fixtures.js";
|
|
22
|
-
export { cardRecorded, cookieRequest, createAgent, createConversation, createLocalAccount, createOrg, eventsOf, joinOrg, type KernelRequest, type KernelRequestOptions, type KernelResponse, login, pidOwningLocalPort, postMessage, sleep, startTestKernel, TEST_PASSWORD, testKernelConfig, truncateTables, waitFor, waitForEvent, waitForTerminal, } from "./kernel-helpers.js";
|
|
22
|
+
export { cardRecorded, cookieRequest, createAgent, createConversation, createLocalAccount, createOrg, eventsOf, joinOrg, type KernelApp, type KernelRequest, type KernelRequestOptions, type KernelResponse, login, pidOwningLocalPort, postMessage, sleep, startTestKernel, TEST_PASSWORD, type TestKernel, testKernelConfig, truncateTables, waitFor, waitForEvent, waitForTerminal, } from "./kernel-helpers.js";
|
|
23
23
|
export { isPngBase64, pdfBase64, pdfSampleWord, redPngBase64, solidPngBase64, } from "./media-samples.js";
|
|
24
24
|
export { type ConformanceTarget, checkDeclaredParameters, describeModelProviderConformance, MEDIA_SAMPLES, thinkingReplayParams, } from "./model-conformance.js";
|
|
25
25
|
export { openSse, type SseClient, type SseMessage } from "./sse.js";
|
package/dist/kernel-helpers.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ConversationEvent, type
|
|
1
|
+
import { type ConversationEvent, type KernelConfig, type KernelHttp, type KernelSchemaDb, type LogLevel, startKernelApp } from "@capybara-hq/kernel";
|
|
2
2
|
import { type Kysely } from "kysely";
|
|
3
3
|
/**
|
|
4
4
|
* Helpers for extension tests that run a real kernel: in process (`startTestKernel` +
|
|
@@ -33,11 +33,32 @@ export declare function testKernelConfig(urls: {
|
|
|
33
33
|
* it before creating anything.
|
|
34
34
|
*/
|
|
35
35
|
export declare function assertOfflineOfficialMarketplace(config: KernelConfig, allow?: boolean): void;
|
|
36
|
+
/** The kernel's Nest application (what `createKernelApp` / `startKernelApp` return). */
|
|
37
|
+
export type KernelApp = Awaited<ReturnType<typeof startKernelApp>>;
|
|
38
|
+
/** A running in-process kernel ({@link startTestKernel}). */
|
|
39
|
+
export interface TestKernel {
|
|
40
|
+
/**
|
|
41
|
+
* The kernel's Nest application. Core's own tests reach a kernel service with `app.get(Class)`;
|
|
42
|
+
* a plugin test cannot import kernel classes (`plugin-test-imports`), so it drives the kernel
|
|
43
|
+
* through `http` and the helpers of this package.
|
|
44
|
+
*/
|
|
45
|
+
readonly app: KernelApp;
|
|
46
|
+
/** In-process HTTP: `inject` requests, list the routes. */
|
|
47
|
+
readonly http: KernelHttp;
|
|
48
|
+
/**
|
|
49
|
+
* The port the kernel listens on (an ephemeral one with `http.port: 0`); undefined after
|
|
50
|
+
* `close()`.
|
|
51
|
+
*/
|
|
52
|
+
readonly port: number | undefined;
|
|
53
|
+
/** Stops the kernel (the stop phases within their budgets, then Redis and the pool); idempotent. */
|
|
54
|
+
close(): Promise<void>;
|
|
55
|
+
}
|
|
36
56
|
/**
|
|
37
|
-
* Creates and starts an in-process kernel
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* `
|
|
57
|
+
* Creates and starts an in-process kernel (`startKernelApp`; it listens on `config.http.port`).
|
|
58
|
+
* The kernel gets no process-wide signal handlers: a test stops it with `close()`. Logs are
|
|
59
|
+
* discarded unless `log` is given (JSON lines at `log.level` and above go to `log.write`, e.g. to
|
|
60
|
+
* assert a secret never reaches the log). Extensions named by package resolve from `resolveFrom`
|
|
61
|
+
* (default `process.cwd()`).
|
|
41
62
|
*/
|
|
42
63
|
export declare function startTestKernel(config: KernelConfig, opts?: {
|
|
43
64
|
resolveFrom?: string;
|
|
@@ -47,7 +68,7 @@ export declare function startTestKernel(config: KernelConfig, opts?: {
|
|
|
47
68
|
};
|
|
48
69
|
/** Only for a test about the real default: allow the official marketplace at its real URL. */
|
|
49
70
|
allowOfficialMarketplace?: boolean;
|
|
50
|
-
}): Promise<
|
|
71
|
+
}): Promise<TestKernel>;
|
|
51
72
|
/** Options of a {@link KernelRequest} call (`http.inject` options; `csrf: false` drops the header). */
|
|
52
73
|
export type KernelRequestOptions = Parameters<KernelHttp["inject"]>[0] & {
|
|
53
74
|
csrf?: boolean;
|
|
@@ -61,16 +82,16 @@ export type KernelRequest = (opts: KernelRequestOptions) => Promise<KernelRespon
|
|
|
61
82
|
/** A request function with its own cookie jar (signed out until a response sets cookies). */
|
|
62
83
|
export declare function cookieRequest(http: KernelHttp): KernelRequest;
|
|
63
84
|
/** Creates a local account with {@link TEST_PASSWORD}; returns its id. */
|
|
64
|
-
export declare function createLocalAccount(db:
|
|
85
|
+
export declare function createLocalAccount(db: KernelSchemaDb, login: string, displayName?: string): Promise<string>;
|
|
65
86
|
/** Signs in over HTTP and returns the user's request function. */
|
|
66
|
-
export declare function login(k: Pick<
|
|
87
|
+
export declare function login(k: Pick<TestKernel, "http">, name: string, password?: string): Promise<KernelRequest>;
|
|
67
88
|
/** Creates an org as `req`'s user (who becomes its owner); returns its id. */
|
|
68
89
|
export declare function createOrg(req: KernelRequest, name?: string): Promise<string>;
|
|
69
90
|
/**
|
|
70
91
|
* A new account joins the org through an invitation (created by `owner`, accepted with `login`
|
|
71
92
|
* and {@link TEST_PASSWORD}); returns the account id and its signed-in request function.
|
|
72
93
|
*/
|
|
73
|
-
export declare function joinOrg(k: Pick<
|
|
94
|
+
export declare function joinOrg(k: Pick<TestKernel, "http">, owner: KernelRequest, orgId: string, name: string, role?: "member" | "admin"): Promise<{
|
|
74
95
|
accountId: string;
|
|
75
96
|
req: KernelRequest;
|
|
76
97
|
}>;
|
|
@@ -90,17 +111,17 @@ export declare function createConversation(req: KernelRequest, orgId: string, ag
|
|
|
90
111
|
/** Posts a user message; returns its event seq. */
|
|
91
112
|
export declare function postMessage(req: KernelRequest, orgId: string, conversationId: string, text: string): Promise<number>;
|
|
92
113
|
/** Every event of a conversation, in seq order (read from the database). */
|
|
93
|
-
export declare function eventsOf(db:
|
|
114
|
+
export declare function eventsOf(db: KernelSchemaDb, conversationId: string): Promise<ConversationEvent[]>;
|
|
94
115
|
/** Waits for the first event matching `pred` and returns it. */
|
|
95
|
-
export declare function waitForEvent(db:
|
|
116
|
+
export declare function waitForEvent(db: KernelSchemaDb, conversationId: string, pred: (e: ConversationEvent) => boolean, timeoutMs?: number, label?: string): Promise<ConversationEvent>;
|
|
96
117
|
/** Waits for the conversation's `n`-th terminal event (run_finished / run_failed / run_aborted). */
|
|
97
|
-
export declare function waitForTerminal(db:
|
|
118
|
+
export declare function waitForTerminal(db: KernelSchemaDb, conversationId: string, n?: number, timeoutMs?: number): Promise<ConversationEvent[]>;
|
|
98
119
|
/**
|
|
99
120
|
* Waits until an IM card's message id is recorded (`channel_card_messages`). The kernel writes it in
|
|
100
121
|
* the transaction after the card is sent; a click before that is treated as "not a card of this
|
|
101
122
|
* conversation", so a test waits for this before it clicks, not only for the platform to get it.
|
|
102
123
|
*/
|
|
103
|
-
export declare function cardRecorded(db:
|
|
124
|
+
export declare function cardRecorded(db: KernelSchemaDb, card: {
|
|
104
125
|
messageId: string;
|
|
105
126
|
} | {
|
|
106
127
|
kind: "approval" | "question";
|
package/dist/kernel-helpers.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
2
3
|
import { readdir, readFile, readlink } from "node:fs/promises";
|
|
3
4
|
import { promisify } from "node:util";
|
|
4
|
-
import { AccountsService,
|
|
5
|
+
import { AccountsModule, AccountsService, closeKernelApp, createLogger, KernelConfigSchema, kernelAppHttp, kernelAppPort, MARKETPLACE_OFFICIAL_URL_DEFAULT, openKernelCliOverPool, startKernelApp, } from "@capybara-hq/kernel";
|
|
5
6
|
import { sql } from "kysely";
|
|
6
7
|
/**
|
|
7
8
|
* Helpers for extension tests that run a real kernel: in process (`startTestKernel` +
|
|
@@ -60,20 +61,33 @@ export function assertOfflineOfficialMarketplace(config, allow = false) {
|
|
|
60
61
|
"marketplaces.official.url at a fake, or build the config with testKernelConfig");
|
|
61
62
|
}
|
|
62
63
|
/**
|
|
63
|
-
* Creates and starts an in-process kernel
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* `
|
|
64
|
+
* Creates and starts an in-process kernel (`startKernelApp`; it listens on `config.http.port`).
|
|
65
|
+
* The kernel gets no process-wide signal handlers: a test stops it with `close()`. Logs are
|
|
66
|
+
* discarded unless `log` is given (JSON lines at `log.level` and above go to `log.write`, e.g. to
|
|
67
|
+
* assert a secret never reaches the log). Extensions named by package resolve from `resolveFrom`
|
|
68
|
+
* (default `process.cwd()`).
|
|
67
69
|
*/
|
|
68
70
|
export async function startTestKernel(config, opts = {}) {
|
|
69
71
|
assertOfflineOfficialMarketplace(config, opts.allowOfficialMarketplace);
|
|
70
|
-
const
|
|
72
|
+
const app = await startKernelApp(config, {
|
|
71
73
|
env: {},
|
|
72
74
|
logger: createLogger(opts.log?.level ?? "error", { write: opts.log?.write ?? (() => { }) }),
|
|
73
75
|
...(opts.resolveFrom !== undefined && { resolveFrom: opts.resolveFrom }),
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
},
|
|
77
|
+
// A signal source of its own: a test kernel never handles the process's SIGTERM / SIGINT
|
|
78
|
+
{ signals: new EventEmitter() });
|
|
79
|
+
return testKernelOf(app);
|
|
80
|
+
}
|
|
81
|
+
/** The {@link TestKernel} view of a started kernel application. */
|
|
82
|
+
function testKernelOf(app) {
|
|
83
|
+
return {
|
|
84
|
+
app,
|
|
85
|
+
http: kernelAppHttp(app),
|
|
86
|
+
get port() {
|
|
87
|
+
return kernelAppPort(app);
|
|
88
|
+
},
|
|
89
|
+
close: () => closeKernelApp(app),
|
|
90
|
+
};
|
|
77
91
|
}
|
|
78
92
|
/** A request function with its own cookie jar (signed out until a response sets cookies). */
|
|
79
93
|
export function cookieRequest(http) {
|
|
@@ -110,7 +124,21 @@ function expectStatus(res, status, what) {
|
|
|
110
124
|
}
|
|
111
125
|
/** Creates a local account with {@link TEST_PASSWORD}; returns its id. */
|
|
112
126
|
export async function createLocalAccount(db, login, displayName = login) {
|
|
113
|
-
|
|
127
|
+
// the accounts module over the caller's Kysely; closing the context leaves it open
|
|
128
|
+
const app = await openKernelCliOverPool(db, {
|
|
129
|
+
logger: createLogger("error"),
|
|
130
|
+
modules: [AccountsModule],
|
|
131
|
+
});
|
|
132
|
+
try {
|
|
133
|
+
return (await app.get(AccountsService).createLocal({
|
|
134
|
+
login,
|
|
135
|
+
password: TEST_PASSWORD,
|
|
136
|
+
displayName,
|
|
137
|
+
})).id;
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
await app.close();
|
|
141
|
+
}
|
|
114
142
|
}
|
|
115
143
|
/** Signs in over HTTP and returns the user's request function. */
|
|
116
144
|
export async function login(k, name, password = TEST_PASSWORD) {
|
package/dist/replica-runner.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { KernelConfigSchema, kernelAppPort, startKernelApp } from "@capybara-hq/kernel";
|
|
3
|
+
/**
|
|
4
|
+
* The replica's signal source: the stop message (or the parent going away) raises SIGTERM on it, so
|
|
5
|
+
* the kernel stops through the same shutdown path as `capybara start` — also while it is starting.
|
|
6
|
+
*/
|
|
7
|
+
const signals = new EventEmitter();
|
|
3
8
|
const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
4
9
|
/** 深合并:普通对象逐键递归,其余(含数组)由 `over` 整体替换。 */
|
|
5
10
|
function deepMerge(base, over) {
|
|
@@ -27,22 +32,25 @@ async function startKernel(c) {
|
|
|
27
32
|
log: { level: "error" },
|
|
28
33
|
};
|
|
29
34
|
const config = KernelConfigSchema.parse(deepMerge(base, c.overrides));
|
|
30
|
-
|
|
35
|
+
const app = await startKernelApp(config, {
|
|
31
36
|
resolveFrom: c.resolveFrom,
|
|
32
|
-
...(c.
|
|
37
|
+
...(c.bootstrap !== undefined && { bootstrap: c.bootstrap }),
|
|
33
38
|
...(c.bootstrapManifest !== undefined && { bootstrapManifest: c.bootstrapManifest }),
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
process.send?.({ type: "ready", port: kernel.port });
|
|
39
|
+
}, { signals, exit: (code) => process.exit(code) });
|
|
40
|
+
process.send?.({ type: "ready", port: kernelAppPort(app) });
|
|
37
41
|
}
|
|
38
42
|
let exiting = false;
|
|
39
|
-
/**
|
|
43
|
+
/** Stops the kernel, then exits; a stop stuck for 10 s exits with 1. */
|
|
40
44
|
function shutdown() {
|
|
41
45
|
if (exiting)
|
|
42
46
|
return;
|
|
43
47
|
exiting = true;
|
|
44
48
|
setTimeout(() => process.exit(1), 10_000).unref();
|
|
45
|
-
|
|
49
|
+
// No kernel handlers yet (still building) or any more (the start failed): nothing to stop
|
|
50
|
+
if (signals.listenerCount("SIGTERM") === 0)
|
|
51
|
+
process.exit(0);
|
|
52
|
+
else
|
|
53
|
+
signals.emit("SIGTERM", "SIGTERM");
|
|
46
54
|
}
|
|
47
55
|
// 父进程(含被 SIGKILL)消失后 IPC 通道断开,副本自行退出,不留孤儿
|
|
48
56
|
process.on("disconnect", shutdown);
|
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.3",
|
|
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": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"tsx": "^4.23.15",
|
|
48
48
|
"ws": "^8.22.0",
|
|
49
49
|
"zod": "^4.6.7",
|
|
50
|
-
"@capybara-hq/kernel": "^0.1.0-rc.
|
|
50
|
+
"@capybara-hq/kernel": "^0.1.0-rc.2"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/pg": "^8.23.1",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"vitest": "^5.0.5",
|
|
59
59
|
"@capybara-hq/plugin-sdk": "^0.1.0-rc.0",
|
|
60
|
-
"@capybara-hq/plugin-tools": "^0.1.0-rc.
|
|
60
|
+
"@capybara-hq/plugin-tools": "^0.1.0-rc.2"
|
|
61
61
|
},
|
|
62
62
|
"peerDependenciesMeta": {
|
|
63
63
|
"@capybara-hq/plugin-tools": {
|