@capybara-hq/testing 0.1.0-rc.2 → 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.
@@ -8,7 +8,10 @@ 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
- /** 解析裸包名插件的目录,默认调用方的 `process.cwd()`(与 `createKernel` 的 resolveFrom 同义)。 */
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>;
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 Kernel, type KernelConfig, type KernelDb, 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";
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";
@@ -1,4 +1,4 @@
1
- import { type ConversationEvent, type Kernel, type KernelConfig, type KernelDb, type KernelHttp, type LogLevel } from "@capybara-hq/kernel";
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. Logs are discarded unless `log` is given (JSON lines at
38
- * `log.level` and above go to `log.write`, e.g. to assert a secret never reaches the log).
39
- * Extensions named by package resolve from `resolveFrom` (default `process.cwd()`). Stop it with
40
- * `stop()`.
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<Kernel>;
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: KernelDb, login: string, displayName?: string): Promise<string>;
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<Kernel, "http">, name: string, password?: string): Promise<KernelRequest>;
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<Kernel, "http">, owner: KernelRequest, orgId: string, name: string, role?: "member" | "admin"): Promise<{
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: KernelDb, conversationId: string): Promise<ConversationEvent[]>;
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: KernelDb, conversationId: string, pred: (e: ConversationEvent) => boolean, timeoutMs?: number, label?: string): Promise<ConversationEvent>;
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: KernelDb, conversationId: string, n?: number, timeoutMs?: number): Promise<ConversationEvent[]>;
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: KernelDb, card: {
124
+ export declare function cardRecorded(db: KernelSchemaDb, card: {
104
125
  messageId: string;
105
126
  } | {
106
127
  kind: "approval" | "question";
@@ -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, createKernel, createLogger, KernelConfigSchema, MARKETPLACE_OFFICIAL_URL_DEFAULT, } from "@capybara-hq/kernel";
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. Logs are discarded unless `log` is given (JSON lines at
64
- * `log.level` and above go to `log.write`, e.g. to assert a secret never reaches the log).
65
- * Extensions named by package resolve from `resolveFrom` (default `process.cwd()`). Stop it with
66
- * `stop()`.
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 k = await createKernel(config, {
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
- await k.start();
76
- return k;
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
- return (await new AccountsService(db).createLocal({ login, password: TEST_PASSWORD, displayName })).id;
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) {
@@ -1,5 +1,10 @@
1
- import { createKernel, KernelConfigSchema } from "@capybara-hq/kernel";
2
- let kernel;
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
- kernel = await createKernel(config, {
35
+ const app = await startKernelApp(config, {
31
36
  resolveFrom: c.resolveFrom,
32
37
  ...(c.bootstrap !== undefined && { bootstrap: c.bootstrap }),
33
38
  ...(c.bootstrapManifest !== undefined && { bootstrapManifest: c.bootstrapManifest }),
34
- });
35
- await kernel.start();
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
- /** 停内核后退出;内核停机卡住时 10 秒后强制退出。 */
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
- (kernel?.stop() ?? Promise.resolve()).finally(() => process.exit(0));
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.2",
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.1"
50
+ "@capybara-hq/kernel": "^0.1.0-rc.2"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/pg": "^8.23.1",
@@ -56,8 +56,8 @@
56
56
  },
57
57
  "peerDependencies": {
58
58
  "vitest": "^5.0.5",
59
- "@capybara-hq/plugin-tools": "^0.1.0-rc.2",
60
- "@capybara-hq/plugin-sdk": "^0.1.0-rc.0"
59
+ "@capybara-hq/plugin-sdk": "^0.1.0-rc.0",
60
+ "@capybara-hq/plugin-tools": "^0.1.0-rc.2"
61
61
  },
62
62
  "peerDependenciesMeta": {
63
63
  "@capybara-hq/plugin-tools": {