@kb-labs/shared-testing-e2e 2.93.0 → 2.96.0

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/cli.d.ts ADDED
@@ -0,0 +1,107 @@
1
+ import { CLIInput } from '@kb-labs/shared-command-kit';
2
+ import { UIFacade, TableColumn, MessageOptions, UILogEntry, ChainItem, PluginContextV3 } from '@kb-labs/plugin-contracts';
3
+
4
+ interface MockCLIInputOptions<F> {
5
+ flags?: Partial<F>;
6
+ argv?: string[];
7
+ }
8
+ /**
9
+ * Build a typed CLIInput for handler-level tests.
10
+ * Flags not provided default to undefined (matching real CLI behavior when flag is omitted).
11
+ */
12
+ declare function mockCLIInput<F>(opts?: MockCLIInputOptions<F>): CLIInput<F>;
13
+
14
+ /**
15
+ * Generic mock factory — creates a partial override of any object/interface.
16
+ * Useful for mocking HTTP clients in handler tests without spawning real daemons.
17
+ *
18
+ * @example
19
+ * const client = mockObject(defaultWorkflowClient, {
20
+ * runWorkflow: async () => ({ runId: 'r-123', status: 'queued' }),
21
+ * })
22
+ */
23
+ declare function mockObject<T extends object>(defaults: T, overrides?: Partial<T>): T;
24
+
25
+ interface UICapture {
26
+ /** Arguments passed to ctx.ui.json() */
27
+ json: unknown[];
28
+ /** Arguments passed to ctx.ui.table() */
29
+ table: Array<{
30
+ rows: Record<string, unknown>[];
31
+ columns?: TableColumn[];
32
+ }>;
33
+ /** Arguments passed to ctx.ui.success() */
34
+ success: Array<{
35
+ message: string;
36
+ opts?: MessageOptions;
37
+ }>;
38
+ /** First argument of ctx.ui.warn() calls */
39
+ warnings: Array<{
40
+ message: string;
41
+ opts?: MessageOptions;
42
+ }>;
43
+ /** First argument of ctx.ui.error() calls (message or Error.message) */
44
+ errors: string[];
45
+ /** Arguments passed to ctx.ui.info() */
46
+ infos: Array<{
47
+ message: string;
48
+ opts?: MessageOptions;
49
+ }>;
50
+ /** Arguments passed to ctx.ui.log() */
51
+ logs: UILogEntry[];
52
+ /** Raw strings passed to ctx.ui.write() */
53
+ writes: string[];
54
+ /** Arguments passed to ctx.ui.chain() — each element is the full ChainItem[] array */
55
+ chain: ChainItem[][];
56
+ }
57
+ interface CapturedUI {
58
+ ui: UIFacade;
59
+ captured: UICapture;
60
+ }
61
+ /**
62
+ * Creates a UIFacade that records all calls for assertion in handler tests.
63
+ * Non-captured methods fall through to noopUI (spinner, confirm, etc.).
64
+ */
65
+ declare function createCapturedUI(): CapturedUI;
66
+
67
+ /**
68
+ * @module mock-context
69
+ *
70
+ * Lightweight PluginContextV3 factory for CLI handler tests.
71
+ *
72
+ * Intentionally self-contained — no dependency on @kb-labs/shared-testing
73
+ * or any workspace package beyond plugin-contracts. This keeps the packed
74
+ * tgz usable in isolated e2e environments.
75
+ *
76
+ * Platform, runtime and api fields are minimal vi.fn() stubs covering the
77
+ * subset of APIs that CLI command handlers realistically call. Commands that
78
+ * need richer mocks (LLM, vector store, etc.) should use createTestContext()
79
+ * from @kb-labs/shared-testing directly.
80
+ */
81
+
82
+ interface MockContextOptions {
83
+ /** Override specific UIFacade methods (merged with noopUI). */
84
+ ui?: Partial<UIFacade>;
85
+ /** Working directory (default: process.cwd()) */
86
+ cwd?: string;
87
+ /** Plugin id for trace context */
88
+ pluginId?: string;
89
+ }
90
+ /**
91
+ * Creates a minimal PluginContextV3 for CLI handler tests.
92
+ *
93
+ * - ui: combined with noopUI so all methods are safe to call
94
+ * - platform.logger: vi.fn() stubs (info/warn/error/debug/child)
95
+ * - runtime.fs: vi.fn() stubs for common FS operations
96
+ * - runtime.fetch / runtime.env: vi.fn() stubs
97
+ * - api: vi.fn() stubs for lifecycle and state
98
+ *
99
+ * Use with createCapturedUI() to assert on command output:
100
+ * ```typescript
101
+ * const { ui, captured } = createCapturedUI();
102
+ * const ctx = createMockContext({ ui });
103
+ * ```
104
+ */
105
+ declare function createMockContext(opts?: MockContextOptions): PluginContextV3;
106
+
107
+ export { type CapturedUI, type MockCLIInputOptions, type MockContextOptions, type UICapture, createCapturedUI, createMockContext, mockCLIInput, mockObject };
package/dist/cli.js ADDED
@@ -0,0 +1,149 @@
1
+ import { noopUI } from '@kb-labs/plugin-contracts';
2
+ import { vi } from 'vitest';
3
+
4
+ // src/cli/mock-cli-input.ts
5
+ function mockCLIInput(opts = {}) {
6
+ return {
7
+ flags: opts.flags ?? {},
8
+ argv: opts.argv ?? []
9
+ };
10
+ }
11
+
12
+ // src/cli/mock-object.ts
13
+ function mockObject(defaults, overrides) {
14
+ return { ...defaults, ...overrides };
15
+ }
16
+ function createCapturedUI() {
17
+ const captured = {
18
+ json: [],
19
+ table: [],
20
+ success: [],
21
+ warnings: [],
22
+ errors: [],
23
+ infos: [],
24
+ logs: [],
25
+ writes: [],
26
+ chain: []
27
+ };
28
+ const ui = {
29
+ ...noopUI,
30
+ write(text) {
31
+ captured.writes.push(text);
32
+ },
33
+ info(message, opts) {
34
+ captured.infos.push({ message, opts });
35
+ },
36
+ success(message, opts) {
37
+ captured.success.push({ message, opts });
38
+ },
39
+ warn(message, opts) {
40
+ captured.warnings.push({ message, opts });
41
+ },
42
+ error(error, opts) {
43
+ captured.errors.push(typeof error === "string" ? error : error.message);
44
+ if (opts) {
45
+ captured.warnings.push({ message: typeof error === "string" ? error : error.message, opts });
46
+ }
47
+ },
48
+ json(data) {
49
+ captured.json.push(data);
50
+ },
51
+ table(data, columns) {
52
+ captured.table.push({ rows: data, columns });
53
+ },
54
+ log(entry) {
55
+ captured.logs.push(entry);
56
+ },
57
+ chain(items) {
58
+ captured.chain.push(items);
59
+ }
60
+ };
61
+ return { ui, captured };
62
+ }
63
+ function createMockContext(opts = {}) {
64
+ const ui = opts.ui ? { ...noopUI, ...opts.ui } : noopUI;
65
+ const noopLogger = {
66
+ info: vi.fn(),
67
+ warn: vi.fn(),
68
+ error: vi.fn(),
69
+ debug: vi.fn(),
70
+ child: vi.fn().mockReturnThis()
71
+ };
72
+ const platform = {
73
+ logger: noopLogger
74
+ };
75
+ const runtime = {
76
+ fs: {
77
+ readFile: vi.fn(async () => ""),
78
+ readFileBuffer: vi.fn(async () => new Uint8Array()),
79
+ writeFile: vi.fn(async () => {
80
+ }),
81
+ readdir: vi.fn(async () => []),
82
+ readdirWithStats: vi.fn(async () => []),
83
+ mkdir: vi.fn(async () => {
84
+ }),
85
+ rm: vi.fn(async () => {
86
+ }),
87
+ copy: vi.fn(async () => {
88
+ }),
89
+ move: vi.fn(async () => {
90
+ }),
91
+ glob: vi.fn(async () => []),
92
+ stat: vi.fn(async () => ({ isFile: () => false, isDirectory: () => false, size: 0, mtime: 0, ctime: 0 })),
93
+ exists: vi.fn(async () => false),
94
+ resolve: (p) => p,
95
+ relative: (p) => p,
96
+ join: (...s) => s.join("/"),
97
+ dirname: (p) => p.split("/").slice(0, -1).join("/"),
98
+ basename: (p) => p.split("/").pop() ?? "",
99
+ extname: (p) => {
100
+ const b = p.split("/").pop() ?? "";
101
+ const i = b.lastIndexOf(".");
102
+ return i > 0 ? b.slice(i) : "";
103
+ }
104
+ },
105
+ fetch: vi.fn(async () => new Response("mock")),
106
+ env: vi.fn((_key) => void 0)
107
+ };
108
+ const api = {
109
+ lifecycle: { onCleanup: vi.fn() },
110
+ state: {
111
+ get: vi.fn(async () => void 0),
112
+ set: vi.fn(async () => {
113
+ }),
114
+ delete: vi.fn(async () => {
115
+ }),
116
+ has: vi.fn(async () => false)
117
+ },
118
+ shell: { exec: vi.fn(async () => ({ code: 0, stdout: "", stderr: "", ok: true })) },
119
+ events: { emit: vi.fn(async () => {
120
+ }) }
121
+ };
122
+ return {
123
+ host: "cli",
124
+ requestId: "test-request-id",
125
+ pluginId: opts.pluginId ?? "test-plugin",
126
+ pluginVersion: "0.0.0",
127
+ cwd: opts.cwd ?? process.cwd(),
128
+ trace: {
129
+ traceId: "test-trace-id",
130
+ spanId: "test-span-id",
131
+ parentSpanId: void 0,
132
+ addEvent: () => {
133
+ },
134
+ setAttribute: () => {
135
+ },
136
+ recordError: () => {
137
+ }
138
+ },
139
+ hostContext: { host: "cli", argv: [] },
140
+ ui,
141
+ platform,
142
+ runtime,
143
+ api
144
+ };
145
+ }
146
+
147
+ export { createCapturedUI, createMockContext, mockCLIInput, mockObject };
148
+ //# sourceMappingURL=cli.js.map
149
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/mock-cli-input.ts","../src/cli/mock-object.ts","../src/cli/captured-ui.ts","../src/cli/mock-context.ts"],"names":["noopUI"],"mappings":"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","file":"cli.js","sourcesContent":["import type { CLIInput } from '@kb-labs/shared-command-kit';\n\nexport interface MockCLIInputOptions<F> {\n flags?: Partial<F>;\n argv?: string[];\n}\n\n/**\n * Build a typed CLIInput for handler-level tests.\n * Flags not provided default to undefined (matching real CLI behavior when flag is omitted).\n */\nexport function mockCLIInput<F>(opts: MockCLIInputOptions<F> = {}): CLIInput<F> {\n return {\n flags: (opts.flags ?? {}) as F,\n argv: opts.argv ?? [],\n };\n}\n","/**\n * Generic mock factory — creates a partial override of any object/interface.\n * Useful for mocking HTTP clients in handler tests without spawning real daemons.\n *\n * @example\n * const client = mockObject(defaultWorkflowClient, {\n * runWorkflow: async () => ({ runId: 'r-123', status: 'queued' }),\n * })\n */\nexport function mockObject<T extends object>(defaults: T, overrides?: Partial<T>): T {\n return { ...defaults, ...overrides };\n}\n","import { noopUI } from '@kb-labs/plugin-contracts';\nimport type { UIFacade, UILogEntry, MessageOptions, TableColumn, ChainItem } from '@kb-labs/plugin-contracts';\n\nexport interface UICapture {\n /** Arguments passed to ctx.ui.json() */\n json: unknown[];\n /** Arguments passed to ctx.ui.table() */\n table: Array<{ rows: Record<string, unknown>[]; columns?: TableColumn[] }>;\n /** Arguments passed to ctx.ui.success() */\n success: Array<{ message: string; opts?: MessageOptions }>;\n /** First argument of ctx.ui.warn() calls */\n warnings: Array<{ message: string; opts?: MessageOptions }>;\n /** First argument of ctx.ui.error() calls (message or Error.message) */\n errors: string[];\n /** Arguments passed to ctx.ui.info() */\n infos: Array<{ message: string; opts?: MessageOptions }>;\n /** Arguments passed to ctx.ui.log() */\n logs: UILogEntry[];\n /** Raw strings passed to ctx.ui.write() */\n writes: string[];\n /** Arguments passed to ctx.ui.chain() — each element is the full ChainItem[] array */\n chain: ChainItem[][];\n}\n\nexport interface CapturedUI {\n ui: UIFacade;\n captured: UICapture;\n}\n\n/**\n * Creates a UIFacade that records all calls for assertion in handler tests.\n * Non-captured methods fall through to noopUI (spinner, confirm, etc.).\n */\nexport function createCapturedUI(): CapturedUI {\n const captured: UICapture = {\n json: [],\n table: [],\n success: [],\n warnings: [],\n errors: [],\n infos: [],\n logs: [],\n writes: [],\n chain: [],\n };\n\n const ui: UIFacade = {\n ...noopUI,\n\n write(text: string): void {\n captured.writes.push(text);\n },\n\n info(message: string, opts?: MessageOptions): void {\n captured.infos.push({ message, opts });\n },\n\n success(message: string, opts?: MessageOptions): void {\n captured.success.push({ message, opts });\n },\n\n warn(message: string, opts?: MessageOptions): void {\n captured.warnings.push({ message, opts });\n },\n\n error(error: Error | string, opts?: MessageOptions): void {\n captured.errors.push(typeof error === 'string' ? error : error.message);\n if (opts) {\n captured.warnings.push({ message: typeof error === 'string' ? error : error.message, opts });\n }\n },\n\n json(data: unknown): void {\n captured.json.push(data);\n },\n\n table(data: Record<string, unknown>[], columns?: TableColumn[]): void {\n captured.table.push({ rows: data, columns });\n },\n\n log(entry: UILogEntry): void {\n captured.logs.push(entry);\n },\n\n chain(items: ChainItem[]): void {\n captured.chain.push(items);\n },\n };\n\n return { ui, captured };\n}\n","/**\n * @module mock-context\n *\n * Lightweight PluginContextV3 factory for CLI handler tests.\n *\n * Intentionally self-contained — no dependency on @kb-labs/shared-testing\n * or any workspace package beyond plugin-contracts. This keeps the packed\n * tgz usable in isolated e2e environments.\n *\n * Platform, runtime and api fields are minimal vi.fn() stubs covering the\n * subset of APIs that CLI command handlers realistically call. Commands that\n * need richer mocks (LLM, vector store, etc.) should use createTestContext()\n * from @kb-labs/shared-testing directly.\n */\n\nimport { vi } from 'vitest';\nimport { noopUI } from '@kb-labs/plugin-contracts';\nimport type { PluginContextV3, UIFacade } from '@kb-labs/plugin-contracts';\n\nexport interface MockContextOptions {\n /** Override specific UIFacade methods (merged with noopUI). */\n ui?: Partial<UIFacade>;\n /** Working directory (default: process.cwd()) */\n cwd?: string;\n /** Plugin id for trace context */\n pluginId?: string;\n}\n\n/**\n * Creates a minimal PluginContextV3 for CLI handler tests.\n *\n * - ui: combined with noopUI so all methods are safe to call\n * - platform.logger: vi.fn() stubs (info/warn/error/debug/child)\n * - runtime.fs: vi.fn() stubs for common FS operations\n * - runtime.fetch / runtime.env: vi.fn() stubs\n * - api: vi.fn() stubs for lifecycle and state\n *\n * Use with createCapturedUI() to assert on command output:\n * ```typescript\n * const { ui, captured } = createCapturedUI();\n * const ctx = createMockContext({ ui });\n * ```\n */\nexport function createMockContext(opts: MockContextOptions = {}): PluginContextV3 {\n const ui: UIFacade = opts.ui ? { ...noopUI, ...opts.ui } : noopUI;\n\n // Minimal logger stub — commands may call ctx.platform.logger.*\n const noopLogger = {\n info: vi.fn(),\n warn: vi.fn(),\n error: vi.fn(),\n debug: vi.fn(),\n child: vi.fn().mockReturnThis(),\n };\n\n // Minimal platform — only logger is wired; everything else is an empty stub.\n // CLI commands that need LLM/cache/storage should use @kb-labs/shared-testing.\n const platform = {\n logger: noopLogger,\n } as unknown as PluginContextV3['platform'];\n\n // Minimal runtime — covers fs.glob / fs.readFile used by some commands (e.g. review)\n const runtime = {\n fs: {\n readFile: vi.fn(async () => ''),\n readFileBuffer: vi.fn(async () => new Uint8Array()),\n writeFile: vi.fn(async () => {}),\n readdir: vi.fn(async () => []),\n readdirWithStats: vi.fn(async () => []),\n mkdir: vi.fn(async () => {}),\n rm: vi.fn(async () => {}),\n copy: vi.fn(async () => {}),\n move: vi.fn(async () => {}),\n glob: vi.fn(async () => []),\n stat: vi.fn(async () => ({ isFile: () => false, isDirectory: () => false, size: 0, mtime: 0, ctime: 0 })),\n exists: vi.fn(async () => false),\n resolve: (p: string) => p,\n relative: (p: string) => p,\n join: (...s: string[]) => s.join('/'),\n dirname: (p: string) => p.split('/').slice(0, -1).join('/'),\n basename: (p: string) => p.split('/').pop() ?? '',\n extname: (p: string) => { const b = p.split('/').pop() ?? ''; const i = b.lastIndexOf('.'); return i > 0 ? b.slice(i) : ''; },\n },\n fetch: vi.fn(async () => new Response('mock')),\n env: vi.fn((_key: string) => undefined),\n } as unknown as PluginContextV3['runtime'];\n\n // Minimal api — covers the lifecycle and state methods most commands might call\n const api = {\n lifecycle: { onCleanup: vi.fn() },\n state: {\n get: vi.fn(async () => undefined),\n set: vi.fn(async () => {}),\n delete: vi.fn(async () => {}),\n has: vi.fn(async () => false),\n },\n shell: { exec: vi.fn(async () => ({ code: 0, stdout: '', stderr: '', ok: true })) },\n events: { emit: vi.fn(async () => {}) },\n } as unknown as PluginContextV3['api'];\n\n return {\n host: 'cli',\n requestId: 'test-request-id',\n pluginId: opts.pluginId ?? 'test-plugin',\n pluginVersion: '0.0.0',\n cwd: opts.cwd ?? process.cwd(),\n trace: {\n traceId: 'test-trace-id',\n spanId: 'test-span-id',\n parentSpanId: undefined,\n addEvent: () => {},\n setAttribute: () => {},\n recordError: () => {},\n },\n hostContext: { host: 'cli', argv: [] } as unknown as PluginContextV3['hostContext'],\n ui,\n platform,\n runtime,\n api,\n };\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,78 @@
1
1
  import { WebSocket } from 'ws';
2
2
 
3
+ /**
4
+ * Structured log event types for KB Labs diagnostic layer.
5
+ *
6
+ * Events are emitted via ILogger when KB_DEBUG=true, serialized to stdout
7
+ * by pino, and collected by DiagCollector in e2e tests.
8
+ *
9
+ * Consumers filter log lines by event prefix:
10
+ * - DiagCollector: '"event":"kb.diag.'
11
+ * - PerfCollector: '"event":"kb.perf.' (future)
12
+ * - AuthAuditor: '"event":"kb.auth.' (future)
13
+ */
14
+ /** General-purpose structured log event envelope. */
15
+ interface KbLogEvent<T = unknown> {
16
+ /** Namespaced event type: 'kb.<domain>.<sub>' */
17
+ event: string;
18
+ /** Schema version — bump when payload shape changes incompatibly. */
19
+ v: 1;
20
+ data: T;
21
+ ts: number;
22
+ }
23
+ interface AdapterStageTrace {
24
+ adapter: string;
25
+ stage: 'resourceBrokerFactory' | 'analyticsFactory' | 'routerFactory' | 'postAssemblyFactory';
26
+ applied: boolean;
27
+ }
28
+ interface GovernanceMiddlewareInfo {
29
+ name: string;
30
+ slot: string;
31
+ priority: number;
32
+ }
33
+ interface PluginGovernanceTrace {
34
+ adapter: string;
35
+ pluginId: string;
36
+ middlewaresApplied: GovernanceMiddlewareInfo[];
37
+ governanceStrategy: 'wrap' | 'pass-through';
38
+ }
39
+ interface ConfigLayersDiag {
40
+ platformConfigPath?: string;
41
+ projectConfigPath?: string;
42
+ overlayPaths: string[];
43
+ fieldSources: Record<string, 'platform' | 'project' | 'both'>;
44
+ ignoredProjectFields: string[];
45
+ }
46
+ interface DiscoveredPluginInfo {
47
+ id: string;
48
+ version: string;
49
+ source: string;
50
+ }
51
+ interface SkippedPluginInfo {
52
+ code: string;
53
+ message: string;
54
+ pluginId?: string;
55
+ }
56
+ interface PluginDiscoveryDiag {
57
+ loaded: DiscoveredPluginInfo[];
58
+ skipped: SkippedPluginInfo[];
59
+ }
60
+ interface ServiceDiag {
61
+ port?: number;
62
+ pid?: number;
63
+ url?: string;
64
+ state: string;
65
+ }
66
+ interface DiagSnapshot {
67
+ schemaVersion: 1;
68
+ capturedAt: string;
69
+ pipeline: AdapterStageTrace[];
70
+ governance: PluginGovernanceTrace[];
71
+ config: Partial<ConfigLayersDiag>;
72
+ discovery: Partial<PluginDiscoveryDiag>;
73
+ services: Record<string, ServiceDiag>;
74
+ }
75
+
3
76
  /**
4
77
  * Type definitions for the e2e test harness.
5
78
  *
@@ -80,6 +153,31 @@ interface EnsureOptions {
80
153
  force?: boolean;
81
154
  }
82
155
 
156
+ /**
157
+ * Collects KB_DEBUG diagnostic events emitted by service processes.
158
+ *
159
+ * Instantiated by KbDevController when KB_DEBUG=true. Each stdout line from
160
+ * subprocesses passes through parseLine(); lines that carry a kb.diag.* event
161
+ * are parsed and routed into per-domain buckets. buildSnapshot() assembles the
162
+ * final DiagSnapshot for attachment to failed tests.
163
+ */
164
+ declare class DiagCollector {
165
+ private readonly pipelineTraces;
166
+ private readonly governanceTraces;
167
+ private configDiag;
168
+ private discoveryDiag;
169
+ private serviceMap;
170
+ /**
171
+ * Parse a single stdout line from a service process.
172
+ * Lines that do not contain a kb.diag.* event are ignored cheaply.
173
+ */
174
+ parseLine(line: string): void;
175
+ /** Merge live service status into the snapshot (call after ensureServices/status). */
176
+ ingestStatusSnapshot(snap: StatusSnapshot): void;
177
+ buildSnapshot(): DiagSnapshot;
178
+ writeSnapshot(path: string): Promise<void>;
179
+ }
180
+
83
181
  /**
84
182
  * Controller that drives kb-dev as a subprocess and exposes its JSON
85
183
  * agent protocol as a typed TypeScript API for e2e tests.
@@ -98,7 +196,11 @@ declare class KbDevController {
98
196
  private readonly startedByUs;
99
197
  /** Cached last status snapshot (invalidated on any mutating call). */
100
198
  private lastStatus?;
199
+ /** Collects KB_DEBUG diagnostic events from service stdout. Undefined when KB_DEBUG is not set. */
200
+ private readonly diagCollector;
101
201
  constructor(opts?: KbDevControllerOptions);
202
+ /** Returns the DiagCollector instance, or undefined when KB_DEBUG is not set. */
203
+ getDiagCollector(): DiagCollector | undefined;
102
204
  /**
103
205
  * Ensure every listed service is `alive`. Idempotent — if a service is
104
206
  * already running, kb-dev reports `skipped` and we don't respawn.
@@ -140,6 +242,17 @@ declare class KbDevController {
140
242
  private run;
141
243
  }
142
244
 
245
+ /**
246
+ * Register a Vitest onTestFailed hook that writes a DiagSnapshot when a test fails.
247
+ *
248
+ * Safe to call in non-Vitest environments (Playwright, Node scripts) — the vitest
249
+ * import is dynamic and failures are caught silently.
250
+ *
251
+ * Usage in beforeAll:
252
+ * await registerDiagSnapshotHook(ctrl);
253
+ */
254
+ declare function registerDiagSnapshotHook(ctrl: KbDevController): Promise<void>;
255
+
143
256
  /**
144
257
  * Minimal HTTP client wrapper used by e2e tests.
145
258
  *
@@ -324,4 +437,68 @@ declare function findWorkspaceRoot(startDir?: string): string | null;
324
437
  */
325
438
  declare function resolveWorkspaceRoot(): string;
326
439
 
327
- export { type AgentCredentials, type EnsureOptions, type HostCredentials, HttpClient, type HttpClientOptions, type HttpResponse, type IsolatedProjectRoot, type IsolatedProjectRootOptions, type KbDevAction, KbDevController, type KbDevControllerOptions, type KbDevResult, type ServiceHealth, type ServiceId, type ServiceState, type ServiceStatus, type SseEvent, type SseOptions, type StatusSnapshot, type StatusSummary, type WsHandle, type WsOptions, closeAllTrackedSockets, connectWs, createIsolatedProjectRoot, findWorkspaceRoot, httpClient, makeTestNamespace, readSse, registerAgent, registerHost, resolveWorkspaceRoot };
440
+ interface CollectSseOptions {
441
+ /** Stop after collecting this many events. */
442
+ count?: number;
443
+ /** Stop after receiving an event with this type (inclusive). */
444
+ untilEvent?: string;
445
+ /** Wall-clock timeout in ms. Default 30_000. */
446
+ timeoutMs?: number;
447
+ /** Extra HTTP headers forwarded to the SSE request. */
448
+ headers?: Record<string, string>;
449
+ }
450
+ /**
451
+ * Collects SSE events from `url` into an array.
452
+ * Stops on `count`, `untilEvent`, stream close, or timeout.
453
+ */
454
+ declare function collectSseEvents(url: string, opts?: CollectSseOptions): Promise<SseEvent[]>;
455
+ /**
456
+ * Returns the first event matching `eventType`, or rejects on timeout.
457
+ */
458
+ declare function waitForSseEvent(url: string, eventType: string, opts?: {
459
+ timeoutMs?: number;
460
+ headers?: Record<string, string>;
461
+ }): Promise<SseEvent>;
462
+ /**
463
+ * Asserts the SSE connection at `url` closes on its own without hanging.
464
+ * Rejects if the stream is still open after `timeoutMs`.
465
+ */
466
+ declare function expectSseTerminates(url: string, opts?: {
467
+ timeoutMs?: number;
468
+ headers?: Record<string, string>;
469
+ }): Promise<void>;
470
+
471
+ /**
472
+ * Asserts SSE events arrive in the exact order of `expectedTypes`.
473
+ * Extra events between expected ones are allowed; missing events throw.
474
+ */
475
+ declare function assertSseOrder(events: SseEvent[], expectedTypes: string[]): void;
476
+ /**
477
+ * Asserts no two events share the same event type AND id combination.
478
+ * Events without an id field are compared by type + data.
479
+ */
480
+ declare function assertNoSseDuplicates(events: SseEvent[]): void;
481
+
482
+ /**
483
+ * Opens a WebSocket, runs `fn`, then closes the socket.
484
+ * The socket is always closed in a finally block — even if `fn` throws.
485
+ */
486
+ declare function withWs<T>(url: string, fn: (ws: WsHandle) => Promise<T>, opts?: WsOptions): Promise<T>;
487
+
488
+ /**
489
+ * Waits for the next WebSocket message that satisfies `predicate`.
490
+ * Times out after `timeoutMs` (default 8000).
491
+ */
492
+ declare function expectWsMessage<T = unknown>(ws: WsHandle, predicate: (msg: T) => boolean, opts?: {
493
+ timeoutMs?: number;
494
+ label?: string;
495
+ }): Promise<T>;
496
+ /**
497
+ * Asserts the WebSocket closes with the given code (default 1000).
498
+ * Times out after `timeoutMs` (default 5000).
499
+ */
500
+ declare function expectWsClose(ws: WsHandle, code?: number, opts?: {
501
+ timeoutMs?: number;
502
+ }): Promise<void>;
503
+
504
+ export { type AdapterStageTrace, type AgentCredentials, type CollectSseOptions, type ConfigLayersDiag, DiagCollector, type DiagSnapshot, type DiscoveredPluginInfo, type EnsureOptions, type GovernanceMiddlewareInfo, type HostCredentials, HttpClient, type HttpClientOptions, type HttpResponse, type IsolatedProjectRoot, type IsolatedProjectRootOptions, type KbDevAction, KbDevController, type KbDevControllerOptions, type KbDevResult, type KbLogEvent, type PluginDiscoveryDiag, type PluginGovernanceTrace, type ServiceDiag, type ServiceHealth, type ServiceId, type ServiceState, type ServiceStatus, type SkippedPluginInfo, type SseEvent, type SseOptions, type StatusSnapshot, type StatusSummary, type WsHandle, type WsOptions, assertNoSseDuplicates, assertSseOrder, closeAllTrackedSockets, collectSseEvents, connectWs, createIsolatedProjectRoot, expectSseTerminates, expectWsClose, expectWsMessage, findWorkspaceRoot, httpClient, makeTestNamespace, readSse, registerAgent, registerDiagSnapshotHook, registerHost, resolveWorkspaceRoot, waitForSseEvent, withWs };
package/dist/index.js CHANGED
@@ -1,14 +1,77 @@
1
1
  import { spawn } from 'child_process';
2
2
  import { existsSync } from 'fs';
3
3
  import { resolve, dirname, join } from 'path';
4
+ import { writeFile, mkdtemp, mkdir, readFile, copyFile, rm } from 'fs/promises';
4
5
  import { fileURLToPath } from 'url';
5
6
  import { WebSocket } from 'ws';
6
- import { mkdtemp, mkdir, readFile, copyFile, writeFile, rm } from 'fs/promises';
7
7
  import { tmpdir } from 'os';
8
8
  import { createHash } from 'crypto';
9
9
  import { nanoid } from 'nanoid';
10
10
 
11
11
  // src/kb-dev-controller.ts
12
+ var DiagCollector = class {
13
+ pipelineTraces = [];
14
+ governanceTraces = [];
15
+ configDiag = {};
16
+ discoveryDiag = {};
17
+ serviceMap = {};
18
+ /**
19
+ * Parse a single stdout line from a service process.
20
+ * Lines that do not contain a kb.diag.* event are ignored cheaply.
21
+ */
22
+ parseLine(line) {
23
+ if (!line.includes('"event":"kb.diag.')) {
24
+ return;
25
+ }
26
+ try {
27
+ const obj = JSON.parse(line);
28
+ if (typeof obj.event !== "string" || obj.v !== 1) {
29
+ return;
30
+ }
31
+ switch (obj.event) {
32
+ case "kb.diag.pipeline":
33
+ this.pipelineTraces.push(...obj.data);
34
+ break;
35
+ case "kb.diag.governance":
36
+ this.governanceTraces.push(obj.data);
37
+ break;
38
+ case "kb.diag.config":
39
+ this.configDiag = obj.data;
40
+ break;
41
+ case "kb.diag.discovery":
42
+ this.discoveryDiag = obj.data;
43
+ break;
44
+ }
45
+ } catch {
46
+ }
47
+ }
48
+ /** Merge live service status into the snapshot (call after ensureServices/status). */
49
+ ingestStatusSnapshot(snap) {
50
+ for (const [id, svc] of Object.entries(snap.services)) {
51
+ this.serviceMap[id] = {
52
+ port: svc.port,
53
+ pid: svc.pid,
54
+ url: svc.url,
55
+ state: svc.state
56
+ };
57
+ }
58
+ }
59
+ buildSnapshot() {
60
+ return {
61
+ schemaVersion: 1,
62
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
63
+ pipeline: [...this.pipelineTraces],
64
+ governance: [...this.governanceTraces],
65
+ config: { ...this.configDiag },
66
+ discovery: { ...this.discoveryDiag },
67
+ services: { ...this.serviceMap }
68
+ };
69
+ }
70
+ async writeSnapshot(path) {
71
+ const snapshot = this.buildSnapshot();
72
+ await writeFile(path, JSON.stringify(snapshot, null, 2), "utf8");
73
+ }
74
+ };
12
75
  function findWorkspaceRoot(startDir) {
13
76
  let dir = startDir ?? process.cwd();
14
77
  for (let i = 0; i < 20; i++) {
@@ -69,6 +132,8 @@ var KbDevController = class {
69
132
  startedByUs = /* @__PURE__ */ new Set();
70
133
  /** Cached last status snapshot (invalidated on any mutating call). */
71
134
  lastStatus;
135
+ /** Collects KB_DEBUG diagnostic events from service stdout. Undefined when KB_DEBUG is not set. */
136
+ diagCollector;
72
137
  constructor(opts = {}) {
73
138
  this.projectRoot = opts.projectRoot ?? resolveWorkspaceRoot();
74
139
  this.kbDevBin = opts.kbDevBin ?? resolve(this.projectRoot, "tools/kb-dev/kb-dev");
@@ -83,6 +148,11 @@ var KbDevController = class {
83
148
  KB_PROJECT_ROOT: this.projectRoot
84
149
  };
85
150
  this.logSink = opts.logSink;
151
+ this.diagCollector = this.env.KB_DEBUG === "true" ? new DiagCollector() : void 0;
152
+ }
153
+ /** Returns the DiagCollector instance, or undefined when KB_DEBUG is not set. */
154
+ getDiagCollector() {
155
+ return this.diagCollector;
86
156
  }
87
157
  // ── Public API ────────────────────────────────────────────────────────────
88
158
  /**
@@ -108,6 +178,7 @@ var KbDevController = class {
108
178
  }
109
179
  const snapshot = await this.waitUntilAlive(ids, timeoutMs);
110
180
  this.lastStatus = snapshot;
181
+ this.diagCollector?.ingestStatusSnapshot(snapshot);
111
182
  return snapshot;
112
183
  }
113
184
  /**
@@ -136,6 +207,7 @@ ${(svc.logsTail ?? []).join("\n")}`
136
207
  async status() {
137
208
  const result = await this.runJson(["status", "--json"], 1e4);
138
209
  this.lastStatus = result;
210
+ this.diagCollector?.ingestStatusSnapshot(result);
139
211
  return result;
140
212
  }
141
213
  /**
@@ -289,6 +361,9 @@ ${result.stderr}`
289
361
  if (line) {
290
362
  this.ring.push(`[${stream}] ${line}`);
291
363
  this.logSink?.(`[kb-dev ${args[0]}] [${stream}] ${line}`);
364
+ if (stream === "stdout") {
365
+ this.diagCollector?.parseLine(line);
366
+ }
292
367
  }
293
368
  }
294
369
  };
@@ -314,6 +389,36 @@ function sleep(ms) {
314
389
  });
315
390
  }
316
391
 
392
+ // src/diag-snapshot-hook.ts
393
+ var SNAPSHOT_PATH = "/tmp/kb-e2e-diag.json";
394
+ async function registerDiagSnapshotHook(ctrl) {
395
+ const vitest = await import('vitest').catch(() => null);
396
+ if (!vitest?.onTestFailed) {
397
+ return;
398
+ }
399
+ vitest.onTestFailed(async () => {
400
+ const collector = ctrl.getDiagCollector();
401
+ if (!collector) {
402
+ return;
403
+ }
404
+ try {
405
+ await collector.writeSnapshot(SNAPSHOT_PATH);
406
+ try {
407
+ const { inject } = await import('vitest');
408
+ const attachments = inject("testAttachments");
409
+ await attachments?.attach("kb-e2e-diag.json", {
410
+ path: SNAPSHOT_PATH,
411
+ contentType: "application/json"
412
+ });
413
+ } catch {
414
+ process.stderr.write(`[KB_DEBUG] DiagSnapshot written to ${SNAPSHOT_PATH}
415
+ `);
416
+ }
417
+ } catch {
418
+ }
419
+ });
420
+ }
421
+
317
422
  // src/http-client.ts
318
423
  var HttpClient = class {
319
424
  constructor(baseUrl, defaults = {}) {
@@ -439,49 +544,69 @@ function wrapHandle(ws) {
439
544
  waitForMessage(opts = {}) {
440
545
  const timeoutMs = opts.timeoutMs ?? 5e3;
441
546
  return new Promise((resolvePromise, reject) => {
442
- const timer = setTimeout(() => {
547
+ const cleanup = () => {
443
548
  ws.off("message", onMessage);
549
+ ws.off("close", onClose);
550
+ };
551
+ const timer = setTimeout(() => {
552
+ cleanup();
444
553
  reject(new Error(`WebSocket message timeout after ${timeoutMs}ms`));
445
554
  }, timeoutMs);
555
+ const onClose = (code, reason) => {
556
+ clearTimeout(timer);
557
+ cleanup();
558
+ reject(new Error(`WebSocket closed (code=${code}) before message: ${reason.toString() || "(no reason)"}`));
559
+ };
446
560
  const onMessage = (raw) => {
447
561
  let msg;
448
562
  try {
449
563
  msg = JSON.parse(raw.toString());
450
564
  } catch (err) {
451
565
  clearTimeout(timer);
452
- ws.off("message", onMessage);
566
+ cleanup();
453
567
  reject(new Error(`Non-JSON WebSocket message: ${err.message}`));
454
568
  return;
455
569
  }
456
570
  if (!opts.predicate || opts.predicate(msg)) {
457
571
  clearTimeout(timer);
458
- ws.off("message", onMessage);
572
+ cleanup();
459
573
  resolvePromise(msg);
460
574
  }
461
575
  };
462
576
  ws.on("message", onMessage);
577
+ ws.on("close", onClose);
463
578
  });
464
579
  },
465
580
  collect(count, timeoutMs = 5e3) {
466
581
  return new Promise((resolvePromise, reject) => {
467
582
  const msgs = [];
468
- const timer = setTimeout(() => {
583
+ const cleanup = () => {
469
584
  ws.off("message", onMessage);
585
+ ws.off("close", onClose);
586
+ };
587
+ const timer = setTimeout(() => {
588
+ cleanup();
470
589
  reject(
471
590
  new Error(
472
591
  `Timeout: expected ${count} WS messages, got ${msgs.length}: ${JSON.stringify(msgs)}`
473
592
  )
474
593
  );
475
594
  }, timeoutMs);
595
+ const onClose = (code, reason) => {
596
+ clearTimeout(timer);
597
+ cleanup();
598
+ reject(new Error(`WebSocket closed (code=${code}) before collecting ${count} messages: ${reason.toString() || "(no reason)"}`));
599
+ };
476
600
  const onMessage = (raw) => {
477
601
  msgs.push(JSON.parse(raw.toString()));
478
602
  if (msgs.length >= count) {
479
603
  clearTimeout(timer);
480
- ws.off("message", onMessage);
604
+ cleanup();
481
605
  resolvePromise(msgs);
482
606
  }
483
607
  };
484
608
  ws.on("message", onMessage);
609
+ ws.on("close", onClose);
485
610
  });
486
611
  },
487
612
  close(code = 1e3) {
@@ -496,28 +621,33 @@ function wrapHandle(ws) {
496
621
  // src/sse-reader.ts
497
622
  async function* readSse(url, opts = {}) {
498
623
  const timeoutMs = opts.timeoutMs ?? 3e4;
499
- const controller = new AbortController();
500
- const timer = setTimeout(() => controller.abort(), timeoutMs);
624
+ const deadline = Date.now() + timeoutMs;
501
625
  let res;
502
626
  try {
503
627
  res = await fetch(url, {
504
- headers: { accept: "text/event-stream", ...opts.headers },
505
- signal: controller.signal
628
+ headers: { accept: "text/event-stream", ...opts.headers }
506
629
  });
507
630
  } catch (err) {
508
- clearTimeout(timer);
509
631
  throw new Error(`SSE fetch failed: ${err.message}`);
510
632
  }
511
633
  if (!res.ok || !res.body) {
512
- clearTimeout(timer);
513
634
  throw new Error(`SSE response not ok: ${res.status} ${res.statusText}`);
514
635
  }
515
636
  const reader = res.body.getReader();
516
637
  const decoder = new TextDecoder();
517
638
  let buffer = "";
639
+ const TIMEOUT_MARKER = { done: true };
518
640
  try {
519
641
  while (true) {
520
- const { value, done } = await reader.read();
642
+ const remaining = deadline - Date.now();
643
+ if (remaining <= 0) {
644
+ return;
645
+ }
646
+ const chunk = await Promise.race([
647
+ reader.read(),
648
+ new Promise((resolve4) => setTimeout(() => resolve4(TIMEOUT_MARKER), remaining))
649
+ ]);
650
+ const { value, done } = chunk;
521
651
  if (done) {
522
652
  return;
523
653
  }
@@ -537,9 +667,8 @@ async function* readSse(url, opts = {}) {
537
667
  }
538
668
  }
539
669
  } finally {
540
- clearTimeout(timer);
541
670
  try {
542
- reader.cancel();
671
+ await reader.cancel();
543
672
  } catch {
544
673
  }
545
674
  }
@@ -676,6 +805,118 @@ function makeTestNamespace(fileUrlOrPath, prefix = "e2e") {
676
805
  return `${prefix}-${hash}-${nanoid(10)}`;
677
806
  }
678
807
 
679
- export { HttpClient, KbDevController, closeAllTrackedSockets, connectWs, createIsolatedProjectRoot, findWorkspaceRoot, httpClient, makeTestNamespace, readSse, registerAgent, registerHost, resolveWorkspaceRoot };
808
+ // src/sse/collect.ts
809
+ async function collectSseEvents(url, opts = {}) {
810
+ const events = [];
811
+ for await (const event of readSse(url, {
812
+ untilEvent: opts.untilEvent,
813
+ timeoutMs: opts.timeoutMs,
814
+ headers: opts.headers
815
+ })) {
816
+ events.push(event);
817
+ if (opts.count !== void 0 && events.length >= opts.count) {
818
+ break;
819
+ }
820
+ }
821
+ return events;
822
+ }
823
+ async function waitForSseEvent(url, eventType, opts = {}) {
824
+ for await (const event of readSse(url, { timeoutMs: opts.timeoutMs ?? 3e4, headers: opts.headers })) {
825
+ if (event.event === eventType) {
826
+ return event;
827
+ }
828
+ }
829
+ throw new Error(`SSE stream ended before receiving event type "${eventType}"`);
830
+ }
831
+ async function expectSseTerminates(url, opts = {}) {
832
+ const timeoutMs = opts.timeoutMs ?? 5e3;
833
+ await Promise.race([
834
+ (async () => {
835
+ for await (const _event of readSse(url, { timeoutMs, headers: opts.headers })) {
836
+ }
837
+ })(),
838
+ new Promise(
839
+ (_, reject) => setTimeout(() => reject(new Error(`SSE stream did not terminate within ${timeoutMs}ms`)), timeoutMs)
840
+ )
841
+ ]);
842
+ }
843
+
844
+ // src/sse/assert.ts
845
+ function assertSseOrder(events, expectedTypes) {
846
+ let cursor = 0;
847
+ for (const event of events) {
848
+ if (cursor >= expectedTypes.length) {
849
+ break;
850
+ }
851
+ if (event.event === expectedTypes[cursor]) {
852
+ cursor++;
853
+ }
854
+ }
855
+ if (cursor < expectedTypes.length) {
856
+ const received = events.map((e) => e.event);
857
+ throw new Error(
858
+ `SSE order assertion failed.
859
+ Expected (in order): ${expectedTypes.join(", ")}
860
+ Received: ${received.join(", ")}
861
+ Missing: ${expectedTypes[cursor]}`
862
+ );
863
+ }
864
+ }
865
+ function assertNoSseDuplicates(events) {
866
+ const seen = /* @__PURE__ */ new Set();
867
+ for (const event of events) {
868
+ const key = event.id ? `${event.event}::${event.id}` : `${event.event}::${event.data}`;
869
+ if (seen.has(key)) {
870
+ throw new Error(`Duplicate SSE event detected: type="${event.event}" id="${event.id ?? "(none)"}"`);
871
+ }
872
+ seen.add(key);
873
+ }
874
+ }
875
+
876
+ // src/ws/lifecycle.ts
877
+ async function withWs(url, fn, opts) {
878
+ const ws = await connectWs(url, opts);
879
+ try {
880
+ return await fn(ws);
881
+ } finally {
882
+ try {
883
+ ws.close(1e3);
884
+ } catch {
885
+ }
886
+ }
887
+ }
888
+
889
+ // src/ws/assert.ts
890
+ async function expectWsMessage(ws, predicate, opts = {}) {
891
+ const timeoutMs = opts.timeoutMs ?? 8e3;
892
+ const label = opts.label ?? "expected WS message";
893
+ return ws.waitForMessage({
894
+ timeoutMs,
895
+ predicate
896
+ }).catch((err) => {
897
+ const msg = err instanceof Error ? err.message : String(err);
898
+ const isTimeout = msg.includes("timeout");
899
+ throw new Error(isTimeout ? `Timed out waiting for ${label} after ${timeoutMs}ms` : `${label}: ${msg}`);
900
+ });
901
+ }
902
+ function expectWsClose(ws, code = 1e3, opts = {}) {
903
+ const timeoutMs = opts.timeoutMs ?? 5e3;
904
+ return new Promise((resolve4, reject) => {
905
+ const timer = setTimeout(
906
+ () => reject(new Error(`WebSocket did not close with code ${code} within ${timeoutMs}ms`)),
907
+ timeoutMs
908
+ );
909
+ ws.socket.once("close", (closeCode) => {
910
+ clearTimeout(timer);
911
+ if (code !== void 0 && closeCode !== code) {
912
+ reject(new Error(`WebSocket closed with code ${closeCode}, expected ${code}`));
913
+ } else {
914
+ resolve4();
915
+ }
916
+ });
917
+ });
918
+ }
919
+
920
+ export { DiagCollector, HttpClient, KbDevController, assertNoSseDuplicates, assertSseOrder, closeAllTrackedSockets, collectSseEvents, connectWs, createIsolatedProjectRoot, expectSseTerminates, expectWsClose, expectWsMessage, findWorkspaceRoot, httpClient, makeTestNamespace, readSse, registerAgent, registerDiagSnapshotHook, registerHost, resolveWorkspaceRoot, waitForSseEvent, withWs };
680
921
  //# sourceMappingURL=index.js.map
681
922
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/workspace-root.ts","../src/kb-dev-controller.ts","../src/http-client.ts","../src/ws-client.ts","../src/sse-reader.ts","../src/jwt-helpers.ts","../src/isolated-project-root.ts","../src/namespace.ts"],"names":["resolve","existsSync"],"mappings":"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","file":"index.js","sourcesContent":["import { existsSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n/**\n * Walk up from a starting directory until we find a KB Labs workspace root.\n * Recognises both `.kb/devservices.yaml` (runtime, generated by kb-create) and\n * `.kb/devservices.dev.yaml` (committed, present in CI checkouts).\n * Returns the absolute path of the project root, or `null` if not found.\n */\nexport function findWorkspaceRoot(startDir?: string): string | null {\n let dir = startDir ?? process.cwd();\n // Safety: bail after 20 levels so we never climb past `/`.\n for (let i = 0; i < 20; i++) {\n if (\n existsSync(resolve(dir, '.kb/devservices.yaml')) ||\n existsSync(resolve(dir, '.kb/devservices.dev.yaml'))\n ) {\n return dir;\n }\n const parent = dirname(dir);\n if (parent === dir) {\n return null;\n }\n dir = parent;\n }\n return null;\n}\n\n/**\n * Resolve the workspace root for the current package, honoring env overrides.\n * Precedence:\n * 1. `KB_PROJECT_ROOT` env var (matches kb-dev itself).\n * 2. Walk up from `process.cwd()`.\n * 3. Walk up from this source file's directory (handles running tests\n * from deeply nested package dirs).\n *\n * Throws if no root can be located.\n */\nexport function resolveWorkspaceRoot(): string {\n const fromEnv = process.env.KB_PROJECT_ROOT;\n if (fromEnv && existsSync(resolve(fromEnv, '.kb/devservices.yaml'))) {\n return fromEnv;\n }\n const fromCwd = findWorkspaceRoot(process.cwd());\n if (fromCwd) {\n return fromCwd;\n }\n const here = dirname(fileURLToPath(import.meta.url));\n const fromHere = findWorkspaceRoot(here);\n if (fromHere) {\n return fromHere;\n }\n throw new Error(\n 'Could not locate KB Labs workspace root (.kb/devservices.yaml). ' +\n 'Set KB_PROJECT_ROOT or run from inside the workspace.',\n );\n}\n","import { spawn } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\n\nimport type {\n EnsureOptions,\n KbDevControllerOptions,\n KbDevResult,\n ServiceId,\n ServiceStatus,\n StatusSnapshot,\n} from './types.js';\nimport { resolveWorkspaceRoot } from './workspace-root.js';\n\n/**\n * Ring buffer for the last N lines of subprocess output.\n * Used so that on failure the harness can dump recent kb-dev noise\n * into the test output without swamping it with tens of thousands of lines.\n */\nclass RingBuffer {\n private buf: string[] = [];\n constructor(private readonly capacity: number) {}\n push(line: string): void {\n this.buf.push(line);\n if (this.buf.length > this.capacity) {\n this.buf.splice(0, this.buf.length - this.capacity);\n }\n }\n snapshot(): string[] {\n return [...this.buf];\n }\n}\n\ninterface SubprocessResult {\n code: number;\n stdout: string;\n stderr: string;\n}\n\n/**\n * Controller that drives kb-dev as a subprocess and exposes its JSON\n * agent protocol as a typed TypeScript API for e2e tests.\n *\n * Lifecycle: one controller per test file (beforeAll/afterAll).\n * Services booted through this controller are shared across every\n * `describe`/`it` in that file.\n */\nexport class KbDevController {\n readonly projectRoot: string;\n readonly kbDevBin: string;\n private readonly env: Record<string, string>;\n private readonly logSink?: (line: string) => void;\n private readonly ring = new RingBuffer(400);\n /** Services this controller started (and therefore should clean up). */\n private readonly startedByUs = new Set<ServiceId>();\n /** Cached last status snapshot (invalidated on any mutating call). */\n private lastStatus?: StatusSnapshot;\n\n constructor(opts: KbDevControllerOptions = {}) {\n this.projectRoot = opts.projectRoot ?? resolveWorkspaceRoot();\n this.kbDevBin = opts.kbDevBin ?? resolve(this.projectRoot, 'tools/kb-dev/kb-dev');\n if (!existsSync(this.kbDevBin)) {\n throw new Error(\n `kb-dev binary not found at ${this.kbDevBin}. ` +\n 'Build it with: cd tools/kb-dev && make build',\n );\n }\n this.env = {\n ...process.env as Record<string, string>,\n ...(opts.env ?? {}),\n KB_PROJECT_ROOT: this.projectRoot,\n };\n this.logSink = opts.logSink;\n }\n\n // ── Public API ────────────────────────────────────────────────────────────\n\n /**\n * Ensure every listed service is `alive`. Idempotent — if a service is\n * already running, kb-dev reports `skipped` and we don't respawn.\n *\n * Throws with a formatted hint + recent log tail on failure.\n */\n async ensureServices(\n ids: ServiceId[],\n opts: EnsureOptions = {},\n ): Promise<StatusSnapshot> {\n const timeoutMs = opts.timeoutMs ?? 60_000;\n const args = ['ensure', ...ids, '--json'];\n if (opts.force) {args.push('--force');}\n\n const result = await this.runJson<KbDevResult>(args, timeoutMs);\n if (!result.ok) {\n throw this.formatError(`kb-dev ensure ${ids.join(' ')}`, result);\n }\n for (const action of result.actions ?? []) {\n if (action.action === 'started') {\n this.startedByUs.add(action.service as ServiceId);\n }\n }\n // Poll status until every requested service is alive (ensure returns\n // when spawn completes, but health may still be warming up).\n const snapshot = await this.waitUntilAlive(ids, timeoutMs);\n this.lastStatus = snapshot;\n return snapshot;\n }\n\n /**\n * Block until a single service reports `alive` or the timeout expires.\n */\n async ready(id: ServiceId, timeoutMs = 60_000): Promise<void> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n const snap = await this.status();\n const svc = snap.services[id];\n if (svc?.state === 'alive') {return;}\n if (svc?.state === 'failed') {\n throw new Error(\n `Service ${id} entered 'failed' state.\\n` +\n `Logs tail:\\n${(svc.logsTail ?? []).join('\\n')}`,\n );\n }\n await sleep(500);\n }\n throw new Error(`Timeout waiting for ${id} to become alive (${timeoutMs}ms)`);\n }\n\n /** Query current status for all services. */\n async status(): Promise<StatusSnapshot> {\n const result = await this.runJson<StatusSnapshot>(['status', '--json'], 10_000);\n this.lastStatus = result;\n return result;\n }\n\n /**\n * Stop the given services (or everything this controller started, if none given).\n * Always best-effort — errors are logged but never thrown.\n */\n async stopServices(ids?: ServiceId[]): Promise<void> {\n const toStop = ids ?? Array.from(this.startedByUs);\n if (toStop.length === 0) {return;}\n try {\n await this.runJson<KbDevResult>(['stop', ...toStop, '--json'], 30_000);\n } catch (err) {\n // Stop failures should never mask the real test failure.\n this.logSink?.(`[kb-dev] stop failed (ignored): ${(err as Error).message}`);\n }\n for (const id of toStop) {this.startedByUs.delete(id);}\n this.lastStatus = undefined;\n }\n\n /** Disposes the controller — stops every service it started. */\n async dispose(): Promise<void> {\n await this.stopServices();\n }\n\n /**\n * Return the HTTP base URL for a service, e.g. `http://localhost:4000`.\n * Throws if the service is not in the last known status snapshot or has no URL.\n */\n getServiceUrl(id: ServiceId): string {\n const svc = this.requireService(id);\n if (!svc.url) {\n throw new Error(`Service ${id} has no URL in its status (state=${svc.state})`);\n }\n return svc.url;\n }\n\n /** Return the port for a service, or throw if unknown. */\n getServicePort(id: ServiceId): number {\n const svc = this.requireService(id);\n if (!svc.port) {\n throw new Error(`Service ${id} has no port in its status (state=${svc.state})`);\n }\n return svc.port;\n }\n\n /** Snapshot of recent subprocess log lines, newest last. */\n dumpRecentLogs(): string[] {\n return this.ring.snapshot();\n }\n\n // ── Internals ─────────────────────────────────────────────────────────────\n\n private requireService(id: ServiceId): ServiceStatus {\n if (!this.lastStatus) {\n throw new Error(\n `No status snapshot yet for ${id}. Call ensureServices() or status() first.`,\n );\n }\n const svc = this.lastStatus.services[id];\n if (!svc) {\n throw new Error(`Service ${id} not found in status snapshot`);\n }\n return svc;\n }\n\n private async waitUntilAlive(\n ids: ServiceId[],\n timeoutMs: number,\n ): Promise<StatusSnapshot> {\n const deadline = Date.now() + timeoutMs;\n let lastSnap: StatusSnapshot | undefined;\n while (Date.now() < deadline) {\n lastSnap = await this.status();\n const allAlive = ids.every((id) => lastSnap?.services[id]?.state === 'alive');\n if (allAlive) {return lastSnap;}\n // Fail fast on hard failures — no point waiting for a service already marked `failed`.\n const failed = ids.find((id) => lastSnap?.services[id]?.state === 'failed');\n if (failed) {\n const svc = lastSnap!.services[failed]!;\n throw new Error(\n `Service ${failed} failed during ensure.\\n` +\n `Logs tail:\\n${(svc.logsTail ?? []).join('\\n')}`,\n );\n }\n await sleep(500);\n }\n throw new Error(\n `Timeout waiting for services to become alive: ${ids.join(', ')}.\\n` +\n `Last known states: ${ids\n .map((id) => `${id}=${lastSnap?.services[id]?.state ?? 'unknown'}`)\n .join(', ')}`,\n );\n }\n\n private formatError(prefix: string, result: KbDevResult): Error {\n const lines: string[] = [`${prefix} failed`];\n if (result.hint) {lines.push(`Hint: ${result.hint}`);}\n for (const action of result.actions ?? []) {\n if (action.action === 'failed' || action.error) {\n lines.push(` - ${action.service}: ${action.error ?? 'failed'}`);\n for (const log of action.logsTail ?? []) {\n lines.push(` ${log}`);\n }\n }\n }\n const recent = this.ring.snapshot();\n if (recent.length > 0) {\n lines.push('Recent kb-dev output:');\n for (const l of recent.slice(-40)) {lines.push(` ${l}`);}\n }\n return new Error(lines.join('\\n'));\n }\n\n /**\n * Run a kb-dev subcommand and parse the stdout as JSON.\n * kb-dev prints only the JSON object on stdout when `--json` is set;\n * any logs go to stderr.\n */\n private async runJson<T>(args: string[], timeoutMs: number): Promise<T> {\n const result = await this.run(args, timeoutMs);\n if (result.code !== 0 && !result.stdout.trim()) {\n throw new Error(\n `kb-dev ${args.join(' ')} exited with code ${result.code}\\n` +\n `stderr:\\n${result.stderr}`,\n );\n }\n try {\n return JSON.parse(result.stdout) as T;\n } catch (err) {\n throw new Error(\n `Failed to parse JSON from \\`kb-dev ${args.join(' ')}\\`:\\n` +\n ` error: ${(err as Error).message}\\n` +\n ` stdout: ${result.stdout.slice(0, 2000)}\\n` +\n ` stderr: ${result.stderr.slice(0, 2000)}`,\n );\n }\n }\n\n private run(args: string[], timeoutMs: number): Promise<SubprocessResult> {\n return new Promise((resolvePromise) => {\n const child = spawn(this.kbDevBin, args, {\n cwd: this.projectRoot,\n env: this.env,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n let stdout = '';\n let stderr = '';\n\n const onLine = (chunk: Buffer, stream: 'stdout' | 'stderr') => {\n const text = chunk.toString();\n if (stream === 'stdout') {stdout += text;}\n else {stderr += text;}\n for (const line of text.split('\\n')) {\n if (line) {\n this.ring.push(`[${stream}] ${line}`);\n this.logSink?.(`[kb-dev ${args[0]}] [${stream}] ${line}`);\n }\n }\n };\n\n child.stdout.on('data', (d: Buffer) => onLine(d, 'stdout'));\n child.stderr.on('data', (d: Buffer) => onLine(d, 'stderr'));\n\n const timer = setTimeout(() => {\n child.kill('SIGKILL');\n }, timeoutMs);\n\n child.on('close', (code) => {\n clearTimeout(timer);\n resolvePromise({ code: code ?? -1, stdout, stderr });\n });\n child.on('error', (err) => {\n clearTimeout(timer);\n resolvePromise({ code: -1, stdout, stderr: stderr + '\\n' + err.message });\n });\n });\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((r) => { setTimeout(r, ms); });\n}\n","/**\n * Minimal HTTP client wrapper used by e2e tests.\n *\n * Intentionally thin — tests should be able to read one line and know\n * exactly what request went out. No retries, no interceptors, no magic.\n */\n\nexport interface HttpClientOptions {\n /** Extra headers included on every request. */\n headers?: Record<string, string>;\n /** Per-request timeout in ms. Default 15_000. */\n timeoutMs?: number;\n}\n\nexport interface HttpResponse<T = unknown> {\n status: number;\n ok: boolean;\n headers: Headers;\n /** Parsed JSON body, or `undefined` if the response had no body / non-JSON content. */\n body: T | undefined;\n /** Raw text body, always populated. */\n text: string;\n}\n\nexport class HttpClient {\n constructor(\n private readonly baseUrl: string,\n private readonly defaults: HttpClientOptions = {},\n ) {}\n\n async get<T = unknown>(path: string, opts: HttpClientOptions = {}): Promise<HttpResponse<T>> {\n return this.request<T>('GET', path, undefined, opts);\n }\n\n async post<T = unknown>(\n path: string,\n body: unknown,\n opts: HttpClientOptions = {},\n ): Promise<HttpResponse<T>> {\n return this.request<T>('POST', path, body, opts);\n }\n\n async delete<T = unknown>(\n path: string,\n opts: HttpClientOptions = {},\n ): Promise<HttpResponse<T>> {\n return this.request<T>('DELETE', path, undefined, opts);\n }\n\n async put<T = unknown>(\n path: string,\n body: unknown,\n opts: HttpClientOptions = {},\n ): Promise<HttpResponse<T>> {\n return this.request<T>('PUT', path, body, opts);\n }\n\n async options<T = unknown>(\n path: string,\n opts: HttpClientOptions = {},\n ): Promise<HttpResponse<T>> {\n return this.request<T>('OPTIONS', path, undefined, opts);\n }\n\n /** Build an absolute URL for a path under this client's base. */\n url(path: string): string {\n if (path.startsWith('http://') || path.startsWith('https://')) {return path;}\n return `${this.baseUrl.replace(/\\/$/, '')}${path.startsWith('/') ? path : `/${path}`}`;\n }\n\n private async request<T>(\n method: string,\n path: string,\n body: unknown,\n opts: HttpClientOptions,\n ): Promise<HttpResponse<T>> {\n const timeoutMs = opts.timeoutMs ?? this.defaults.timeoutMs ?? 15_000;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n const headers: Record<string, string> = {\n ...this.defaults.headers,\n ...opts.headers,\n };\n if (body !== undefined && !headers['content-type'] && !headers['Content-Type']) {\n headers['content-type'] = 'application/json';\n }\n\n try {\n const res = await fetch(this.url(path), {\n method,\n headers,\n body: body === undefined ? undefined : JSON.stringify(body),\n signal: controller.signal,\n });\n const text = await res.text();\n let parsed: T | undefined;\n const contentType = res.headers.get('content-type') ?? '';\n if (text && contentType.includes('json')) {\n try {\n parsed = JSON.parse(text) as T;\n } catch {\n parsed = undefined;\n }\n }\n return {\n status: res.status,\n ok: res.ok,\n headers: res.headers,\n body: parsed,\n text,\n };\n } finally {\n clearTimeout(timer);\n }\n }\n}\n\nexport function httpClient(baseUrl: string, opts: HttpClientOptions = {}): HttpClient {\n return new HttpClient(baseUrl, opts);\n}\n","import { WebSocket, type RawData } from 'ws';\n\n/**\n * Test-friendly WebSocket wrapper.\n *\n * Adapted from `infra/kb-labs-gateway/apps/gateway-app/src/__tests__/live-gateway.e2e.test.ts`\n * — same tracking semantics so the harness can clean up leaked sockets\n * between tests even when an assertion fails mid-flight.\n */\n\nexport interface WsOptions {\n headers?: Record<string, string>;\n /** ms. Default 8000. */\n openTimeoutMs?: number;\n}\n\n/** Opaque handle returned from `connectWs`. */\nexport interface WsHandle {\n readonly socket: WebSocket;\n /** Send a JSON-stringified message. */\n send(data: unknown): void;\n /** Wait for the next message, optionally satisfying a predicate. */\n waitForMessage<T = unknown>(\n opts?: { timeoutMs?: number; predicate?: (msg: T) => boolean },\n ): Promise<T>;\n /** Collect the next N messages (parsed as JSON). */\n collect<T = unknown>(count: number, timeoutMs?: number): Promise<T[]>;\n close(code?: number): void;\n}\n\n/**\n * Shared registry of open sockets. Call `closeAllTrackedSockets()` from an\n * `afterEach` to guarantee cleanup.\n */\nconst openSockets = new Set<WebSocket>();\n\nexport async function closeAllTrackedSockets(graceMs = 150): Promise<void> {\n const toClose = [...openSockets];\n for (const ws of toClose) {\n if (ws.readyState === ws.OPEN || ws.readyState === ws.CONNECTING) {\n try {\n ws.close(1000);\n } catch {\n // intentionally empty: best-effort cleanup, socket may already be dead\n }\n }\n }\n openSockets.clear();\n if (toClose.length > 0) {\n await new Promise((r) => { setTimeout(r, graceMs); });\n }\n}\n\nexport function connectWs(url: string, opts: WsOptions = {}): Promise<WsHandle> {\n const openTimeoutMs = opts.openTimeoutMs ?? 8000;\n return new Promise((resolvePromise, reject) => {\n const ws = new WebSocket(url, {\n headers: opts.headers,\n });\n openSockets.add(ws);\n ws.on('close', () => openSockets.delete(ws));\n\n const timer = setTimeout(() => {\n reject(new Error(`WebSocket open timeout after ${openTimeoutMs}ms: ${url}`));\n try { ws.close(); } catch {\n // intentionally empty: best-effort cleanup on timeout\n }\n }, openTimeoutMs);\n\n ws.once('open', () => {\n clearTimeout(timer);\n resolvePromise(wrapHandle(ws));\n });\n ws.once('error', (err) => {\n clearTimeout(timer);\n reject(err);\n });\n });\n}\n\nfunction wrapHandle(ws: WebSocket): WsHandle {\n return {\n socket: ws,\n send(data: unknown) {\n ws.send(typeof data === 'string' ? data : JSON.stringify(data));\n },\n waitForMessage<T = unknown>(\n opts: { timeoutMs?: number; predicate?: (msg: T) => boolean } = {},\n ): Promise<T> {\n const timeoutMs = opts.timeoutMs ?? 5000;\n return new Promise((resolvePromise, reject) => {\n const timer = setTimeout(() => {\n ws.off('message', onMessage);\n reject(new Error(`WebSocket message timeout after ${timeoutMs}ms`));\n }, timeoutMs);\n const onMessage = (raw: RawData) => {\n let msg: T;\n try {\n msg = JSON.parse(raw.toString()) as T;\n } catch (err) {\n clearTimeout(timer);\n ws.off('message', onMessage);\n reject(new Error(`Non-JSON WebSocket message: ${(err as Error).message}`));\n return;\n }\n if (!opts.predicate || opts.predicate(msg)) {\n clearTimeout(timer);\n ws.off('message', onMessage);\n resolvePromise(msg);\n }\n };\n ws.on('message', onMessage);\n });\n },\n collect<T = unknown>(count: number, timeoutMs = 5000): Promise<T[]> {\n return new Promise((resolvePromise, reject) => {\n const msgs: T[] = [];\n const timer = setTimeout(() => {\n ws.off('message', onMessage);\n reject(\n new Error(\n `Timeout: expected ${count} WS messages, got ${msgs.length}: ${JSON.stringify(msgs)}`,\n ),\n );\n }, timeoutMs);\n const onMessage = (raw: RawData) => {\n msgs.push(JSON.parse(raw.toString()) as T);\n if (msgs.length >= count) {\n clearTimeout(timer);\n ws.off('message', onMessage);\n resolvePromise(msgs);\n }\n };\n ws.on('message', onMessage);\n });\n },\n close(code = 1000) {\n try {\n ws.close(code);\n } catch {\n // intentionally empty: best-effort cleanup\n }\n },\n };\n}\n","/**\n * Minimal Server-Sent Events reader.\n *\n * Consumes `text/event-stream` responses and yields one `SseEvent` per\n * `event:`/`data:` pair. Terminates on close, explicit `untilEvent`, or timeout.\n */\n\nexport interface SseEvent {\n /** SSE `event:` field. Empty string if absent. */\n event: string;\n /** SSE `data:` field, concatenated with newlines if multi-line. */\n data: string;\n /** Optional `id:` field. */\n id?: string;\n /** Parsed JSON body if `data` is valid JSON, else undefined. */\n json?: unknown;\n}\n\nexport interface SseOptions {\n headers?: Record<string, string>;\n /** If set, the iterator terminates after the first event with this name. */\n untilEvent?: string;\n /** Wall-clock timeout in ms. Default 30_000. */\n timeoutMs?: number;\n}\n\n/**\n * Async iterator over an SSE stream. Caller is responsible for iterating\n * (e.g. `for await (const event of readSse(url)) { ... }`).\n */\nexport async function* readSse(url: string, opts: SseOptions = {}): AsyncGenerator<SseEvent> {\n const timeoutMs = opts.timeoutMs ?? 30_000;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n let res: Response;\n try {\n res = await fetch(url, {\n headers: { accept: 'text/event-stream', ...opts.headers },\n signal: controller.signal,\n });\n } catch (err) {\n clearTimeout(timer);\n throw new Error(`SSE fetch failed: ${(err as Error).message}`);\n }\n\n if (!res.ok || !res.body) {\n clearTimeout(timer);\n throw new Error(`SSE response not ok: ${res.status} ${res.statusText}`);\n }\n\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) {return;}\n buffer += decoder.decode(value, { stream: true });\n\n // SSE events are separated by a blank line (\\n\\n).\n let sepIdx: number;\n while ((sepIdx = buffer.indexOf('\\n\\n')) !== -1) {\n const rawEvent = buffer.slice(0, sepIdx);\n buffer = buffer.slice(sepIdx + 2);\n const parsed = parseEvent(rawEvent);\n if (!parsed) {continue;}\n yield parsed;\n if (opts.untilEvent && parsed.event === opts.untilEvent) {\n return;\n }\n }\n }\n } finally {\n clearTimeout(timer);\n try { reader.cancel(); } catch {\n // intentionally empty: reader may already be closed\n }\n }\n}\n\nfunction parseEvent(raw: string): SseEvent | null {\n if (!raw.trim()) {return null;}\n let eventName = '';\n const dataLines: string[] = [];\n let id: string | undefined;\n\n for (const line of raw.split('\\n')) {\n if (!line || line.startsWith(':')) {continue;} // comment / keep-alive\n const colonIdx = line.indexOf(':');\n if (colonIdx === -1) {continue;}\n const field = line.slice(0, colonIdx);\n // Per spec: strip a single leading space from value.\n const value = line.slice(colonIdx + 1).replace(/^ /, '');\n if (field === 'event') {eventName = value;}\n else if (field === 'data') {dataLines.push(value);}\n else if (field === 'id') {id = value;}\n }\n\n if (eventName === '' && dataLines.length === 0) {return null;}\n const data = dataLines.join('\\n');\n let json: unknown;\n if (data) {\n try { json = JSON.parse(data); } catch { /* not JSON, leave undefined */ }\n }\n return { event: eventName, data, id, json };\n}\n","/**\n * Gateway auth helpers.\n *\n * Extracted from `infra/kb-labs-gateway/apps/gateway-app/src/__tests__/live-gateway.e2e.test.ts`\n * (`getJwtToken`, `registerHost`) so every e2e test can reuse the same flow.\n *\n * Flow:\n * 1. POST /auth/register { name, namespaceId } -> { clientId, clientSecret, hostId }\n * 2. POST /auth/token { clientId, clientSecret } -> { accessToken }\n */\n\nimport type { HttpClient } from './http-client.js';\n\nexport interface AgentCredentials {\n accessToken: string;\n clientId: string;\n hostId: string;\n}\n\nexport interface HostCredentials {\n hostId: string;\n machineToken: string;\n}\n\nexport async function registerAgent(\n client: HttpClient,\n opts: { name?: string; namespaceId: string } = { namespaceId: 'e2e-default' },\n): Promise<AgentCredentials> {\n const name = opts.name ?? 'e2e-agent';\n const regRes = await client.post<{ clientId: string; clientSecret: string; hostId: string }>(\n '/auth/register',\n { name, namespaceId: opts.namespaceId },\n );\n if (!regRes.ok || !regRes.body) {\n throw new Error(`/auth/register failed: ${regRes.status} ${regRes.text}`);\n }\n const { clientId, clientSecret, hostId } = regRes.body;\n\n const tokenRes = await client.post<{ accessToken: string }>(\n '/auth/token',\n { clientId, clientSecret },\n );\n if (!tokenRes.ok || !tokenRes.body) {\n throw new Error(`/auth/token failed: ${tokenRes.status} ${tokenRes.text}`);\n }\n\n return { accessToken: tokenRes.body.accessToken, clientId, hostId };\n}\n\nexport async function registerHost(\n client: HttpClient,\n opts: {\n name?: string;\n namespaceId: string;\n capabilities?: string[];\n workspacePaths?: string[];\n },\n): Promise<HostCredentials> {\n const res = await client.post<HostCredentials>('/hosts/register', {\n name: opts.name ?? 'e2e-host',\n namespaceId: opts.namespaceId,\n capabilities: opts.capabilities ?? ['filesystem'],\n workspacePaths: opts.workspacePaths ?? [],\n });\n if (!res.ok || !res.body) {\n throw new Error(`/hosts/register failed: ${res.status} ${res.text}`);\n }\n return res.body;\n}\n","import { mkdir, mkdtemp, rm, writeFile, copyFile, readFile } from 'node:fs/promises';\nimport { existsSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { join, resolve } from 'node:path';\n\n/**\n * An isolated temporary project root with its own `.kb/` directory.\n *\n * Used by marketplace/plugin e2e tests so real workspace `.kb/marketplace.lock`\n * and `.kb/plugins.json` are never mutated.\n *\n * Pass the returned `root` via `KB_PROJECT_ROOT` env when starting kb-dev.\n */\nexport interface IsolatedProjectRoot {\n /** Absolute path to the temp project root. */\n root: string;\n /** Absolute path to `<root>/.kb/devservices.yaml`. */\n devConfigPath: string;\n /** Delete the temp dir. Safe to call multiple times. */\n cleanup(): Promise<void>;\n}\n\nexport interface IsolatedProjectRootOptions {\n /**\n * If given, copies the devservices.yaml from this existing workspace root.\n * If omitted, a minimal config with only `state-daemon` is written.\n */\n copyDevConfigFrom?: string;\n /** Prefix for the temp dir name (for human diagnosis). */\n prefix?: string;\n}\n\nconst MINIMAL_DEV_CONFIG = `name: KB Labs E2E Isolated Root\n\ngroups:\n infra: [state-daemon]\n\nservices:\n state-daemon:\n name: State Daemon\n description: Distributed state management\n group: infra\n type: node\n command: node ./plugins/state/daemon/core-state-daemon/dist/bin.cjs\n healthCheck: http://localhost:7777/health\n port: 7777\n url: http://localhost:7777\n env:\n KB_STATE_DAEMON_PORT: \"7777\"\n KB_STATE_DAEMON_HOST: localhost\n\nsettings:\n logsDir: .kb/logs/tmp\n pidDir: .kb/tmp\n startTimeout: 30000\n healthCheckInterval: 1000\n`;\n\nexport async function createIsolatedProjectRoot(\n opts: IsolatedProjectRootOptions = {},\n): Promise<IsolatedProjectRoot> {\n const prefix = opts.prefix ?? 'kb-e2e-';\n const root = await mkdtemp(join(tmpdir(), prefix));\n const kbDir = join(root, '.kb');\n await mkdir(kbDir, { recursive: true });\n await mkdir(join(kbDir, 'tmp'), { recursive: true });\n await mkdir(join(kbDir, 'logs', 'tmp'), { recursive: true });\n\n const devConfigPath = join(kbDir, 'devservices.yaml');\n\n if (opts.copyDevConfigFrom) {\n const source = resolve(opts.copyDevConfigFrom, '.kb/devservices.yaml');\n if (!existsSync(source)) {\n throw new Error(`copyDevConfigFrom: no devservices.yaml at ${source}`);\n }\n // Read to validate it exists and is readable.\n await readFile(source, 'utf8');\n await copyFile(source, devConfigPath);\n } else {\n await writeFile(devConfigPath, MINIMAL_DEV_CONFIG, 'utf8');\n }\n\n let disposed = false;\n const cleanup = async () => {\n if (disposed) {return;}\n disposed = true;\n try {\n await rm(root, { recursive: true, force: true });\n } catch {\n // intentionally empty: best-effort cleanup, tmpdir will be reaped eventually\n }\n };\n\n return { root, devConfigPath, cleanup };\n}\n","import { createHash } from 'node:crypto';\nimport { nanoid } from 'nanoid';\n\n/**\n * Build a deterministic-yet-unique namespace prefix for e2e test resources.\n *\n * Shape: `e2e-<fileHash>-<nanoid>`\n *\n * - `fileHash` is 6 hex chars of sha1(filePath) — lets you grep leaked resources\n * back to the test file that created them.\n * - `nanoid` is a 10-char random suffix — lets parallel runs never collide.\n *\n * Pass `import.meta.url` from the test file.\n */\nexport function makeTestNamespace(fileUrlOrPath: string, prefix = 'e2e'): string {\n const hash = createHash('sha1').update(fileUrlOrPath).digest('hex').slice(0, 6);\n return `${prefix}-${hash}-${nanoid(10)}`;\n}\n"]}
1
+ {"version":3,"sources":["../src/diag-collector.ts","../src/workspace-root.ts","../src/kb-dev-controller.ts","../src/diag-snapshot-hook.ts","../src/http-client.ts","../src/ws-client.ts","../src/sse-reader.ts","../src/jwt-helpers.ts","../src/isolated-project-root.ts","../src/namespace.ts","../src/sse/collect.ts","../src/sse/assert.ts","../src/ws/lifecycle.ts","../src/ws/assert.ts"],"names":["resolve","existsSync","writeFile"],"mappings":"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","file":"index.js","sourcesContent":["import { writeFile } from 'node:fs/promises';\n\nimport type {\n AdapterStageTrace,\n ConfigLayersDiag,\n DiagSnapshot,\n KbLogEvent,\n PluginDiscoveryDiag,\n PluginGovernanceTrace,\n ServiceDiag,\n} from './diag-types.js';\nimport type { StatusSnapshot } from './types.js';\n\n/**\n * Collects KB_DEBUG diagnostic events emitted by service processes.\n *\n * Instantiated by KbDevController when KB_DEBUG=true. Each stdout line from\n * subprocesses passes through parseLine(); lines that carry a kb.diag.* event\n * are parsed and routed into per-domain buckets. buildSnapshot() assembles the\n * final DiagSnapshot for attachment to failed tests.\n */\nexport class DiagCollector {\n private readonly pipelineTraces: AdapterStageTrace[] = [];\n private readonly governanceTraces: PluginGovernanceTrace[] = [];\n private configDiag: Partial<ConfigLayersDiag> = {};\n private discoveryDiag: Partial<PluginDiscoveryDiag> = {};\n private serviceMap: Record<string, ServiceDiag> = {};\n\n /**\n * Parse a single stdout line from a service process.\n * Lines that do not contain a kb.diag.* event are ignored cheaply.\n */\n parseLine(line: string): void {\n // Fast string check before JSON.parse to avoid cost on ordinary log lines\n if (!line.includes('\"event\":\"kb.diag.')) { return; }\n try {\n const obj = JSON.parse(line) as KbLogEvent;\n if (typeof obj.event !== 'string' || obj.v !== 1) { return; }\n switch (obj.event) {\n case 'kb.diag.pipeline':\n this.pipelineTraces.push(...(obj.data as AdapterStageTrace[]));\n break;\n case 'kb.diag.governance':\n this.governanceTraces.push(obj.data as PluginGovernanceTrace);\n break;\n case 'kb.diag.config':\n this.configDiag = obj.data as ConfigLayersDiag;\n break;\n case 'kb.diag.discovery':\n this.discoveryDiag = obj.data as PluginDiscoveryDiag;\n break;\n }\n } catch {\n // Malformed line — silently ignore; never throw inside logSink\n }\n }\n\n /** Merge live service status into the snapshot (call after ensureServices/status). */\n ingestStatusSnapshot(snap: StatusSnapshot): void {\n for (const [id, svc] of Object.entries(snap.services)) {\n this.serviceMap[id] = {\n port: svc.port,\n pid: svc.pid,\n url: svc.url,\n state: svc.state,\n };\n }\n }\n\n buildSnapshot(): DiagSnapshot {\n return {\n schemaVersion: 1,\n capturedAt: new Date().toISOString(),\n pipeline: [...this.pipelineTraces],\n governance: [...this.governanceTraces],\n config: { ...this.configDiag },\n discovery: { ...this.discoveryDiag },\n services: { ...this.serviceMap },\n };\n }\n\n async writeSnapshot(path: string): Promise<void> {\n const snapshot = this.buildSnapshot();\n await writeFile(path, JSON.stringify(snapshot, null, 2), 'utf8');\n }\n}\n","import { existsSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n/**\n * Walk up from a starting directory until we find a KB Labs workspace root.\n * Recognises both `.kb/devservices.yaml` (runtime, generated by kb-create) and\n * `.kb/devservices.dev.yaml` (committed, present in CI checkouts).\n * Returns the absolute path of the project root, or `null` if not found.\n */\nexport function findWorkspaceRoot(startDir?: string): string | null {\n let dir = startDir ?? process.cwd();\n // Safety: bail after 20 levels so we never climb past `/`.\n for (let i = 0; i < 20; i++) {\n if (\n existsSync(resolve(dir, '.kb/devservices.yaml')) ||\n existsSync(resolve(dir, '.kb/devservices.dev.yaml'))\n ) {\n return dir;\n }\n const parent = dirname(dir);\n if (parent === dir) {\n return null;\n }\n dir = parent;\n }\n return null;\n}\n\n/**\n * Resolve the workspace root for the current package, honoring env overrides.\n * Precedence:\n * 1. `KB_PROJECT_ROOT` env var (matches kb-dev itself).\n * 2. Walk up from `process.cwd()`.\n * 3. Walk up from this source file's directory (handles running tests\n * from deeply nested package dirs).\n *\n * Throws if no root can be located.\n */\nexport function resolveWorkspaceRoot(): string {\n const fromEnv = process.env.KB_PROJECT_ROOT;\n if (fromEnv && existsSync(resolve(fromEnv, '.kb/devservices.yaml'))) {\n return fromEnv;\n }\n const fromCwd = findWorkspaceRoot(process.cwd());\n if (fromCwd) {\n return fromCwd;\n }\n const here = dirname(fileURLToPath(import.meta.url));\n const fromHere = findWorkspaceRoot(here);\n if (fromHere) {\n return fromHere;\n }\n throw new Error(\n 'Could not locate KB Labs workspace root (.kb/devservices.yaml). ' +\n 'Set KB_PROJECT_ROOT or run from inside the workspace.',\n );\n}\n","import { spawn } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\n\nimport { DiagCollector } from './diag-collector.js';\nimport type {\n EnsureOptions,\n KbDevControllerOptions,\n KbDevResult,\n ServiceId,\n ServiceStatus,\n StatusSnapshot,\n} from './types.js';\nimport { resolveWorkspaceRoot } from './workspace-root.js';\n\n/**\n * Ring buffer for the last N lines of subprocess output.\n * Used so that on failure the harness can dump recent kb-dev noise\n * into the test output without swamping it with tens of thousands of lines.\n */\nclass RingBuffer {\n private buf: string[] = [];\n constructor(private readonly capacity: number) {}\n push(line: string): void {\n this.buf.push(line);\n if (this.buf.length > this.capacity) {\n this.buf.splice(0, this.buf.length - this.capacity);\n }\n }\n snapshot(): string[] {\n return [...this.buf];\n }\n}\n\ninterface SubprocessResult {\n code: number;\n stdout: string;\n stderr: string;\n}\n\n/**\n * Controller that drives kb-dev as a subprocess and exposes its JSON\n * agent protocol as a typed TypeScript API for e2e tests.\n *\n * Lifecycle: one controller per test file (beforeAll/afterAll).\n * Services booted through this controller are shared across every\n * `describe`/`it` in that file.\n */\nexport class KbDevController {\n readonly projectRoot: string;\n readonly kbDevBin: string;\n private readonly env: Record<string, string>;\n private readonly logSink?: (line: string) => void;\n private readonly ring = new RingBuffer(400);\n /** Services this controller started (and therefore should clean up). */\n private readonly startedByUs = new Set<ServiceId>();\n /** Cached last status snapshot (invalidated on any mutating call). */\n private lastStatus?: StatusSnapshot;\n /** Collects KB_DEBUG diagnostic events from service stdout. Undefined when KB_DEBUG is not set. */\n private readonly diagCollector: DiagCollector | undefined;\n\n constructor(opts: KbDevControllerOptions = {}) {\n this.projectRoot = opts.projectRoot ?? resolveWorkspaceRoot();\n this.kbDevBin = opts.kbDevBin ?? resolve(this.projectRoot, 'tools/kb-dev/kb-dev');\n if (!existsSync(this.kbDevBin)) {\n throw new Error(\n `kb-dev binary not found at ${this.kbDevBin}. ` +\n 'Build it with: cd tools/kb-dev && make build',\n );\n }\n this.env = {\n ...process.env as Record<string, string>,\n ...(opts.env ?? {}),\n KB_PROJECT_ROOT: this.projectRoot,\n };\n this.logSink = opts.logSink;\n // env is fully resolved at this point — reads KB_DEBUG from process.env or opts.env\n this.diagCollector = this.env.KB_DEBUG === 'true' ? new DiagCollector() : undefined;\n }\n\n /** Returns the DiagCollector instance, or undefined when KB_DEBUG is not set. */\n getDiagCollector(): DiagCollector | undefined {\n return this.diagCollector;\n }\n\n // ── Public API ────────────────────────────────────────────────────────────\n\n /**\n * Ensure every listed service is `alive`. Idempotent — if a service is\n * already running, kb-dev reports `skipped` and we don't respawn.\n *\n * Throws with a formatted hint + recent log tail on failure.\n */\n async ensureServices(\n ids: ServiceId[],\n opts: EnsureOptions = {},\n ): Promise<StatusSnapshot> {\n const timeoutMs = opts.timeoutMs ?? 60_000;\n const args = ['ensure', ...ids, '--json'];\n if (opts.force) {args.push('--force');}\n\n const result = await this.runJson<KbDevResult>(args, timeoutMs);\n if (!result.ok) {\n throw this.formatError(`kb-dev ensure ${ids.join(' ')}`, result);\n }\n for (const action of result.actions ?? []) {\n if (action.action === 'started') {\n this.startedByUs.add(action.service as ServiceId);\n }\n }\n // Poll status until every requested service is alive (ensure returns\n // when spawn completes, but health may still be warming up).\n const snapshot = await this.waitUntilAlive(ids, timeoutMs);\n this.lastStatus = snapshot;\n this.diagCollector?.ingestStatusSnapshot(snapshot);\n return snapshot;\n }\n\n /**\n * Block until a single service reports `alive` or the timeout expires.\n */\n async ready(id: ServiceId, timeoutMs = 60_000): Promise<void> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n const snap = await this.status();\n const svc = snap.services[id];\n if (svc?.state === 'alive') {return;}\n if (svc?.state === 'failed') {\n throw new Error(\n `Service ${id} entered 'failed' state.\\n` +\n `Logs tail:\\n${(svc.logsTail ?? []).join('\\n')}`,\n );\n }\n await sleep(500);\n }\n throw new Error(`Timeout waiting for ${id} to become alive (${timeoutMs}ms)`);\n }\n\n /** Query current status for all services. */\n async status(): Promise<StatusSnapshot> {\n const result = await this.runJson<StatusSnapshot>(['status', '--json'], 10_000);\n this.lastStatus = result;\n this.diagCollector?.ingestStatusSnapshot(result);\n return result;\n }\n\n /**\n * Stop the given services (or everything this controller started, if none given).\n * Always best-effort — errors are logged but never thrown.\n */\n async stopServices(ids?: ServiceId[]): Promise<void> {\n const toStop = ids ?? Array.from(this.startedByUs);\n if (toStop.length === 0) {return;}\n try {\n await this.runJson<KbDevResult>(['stop', ...toStop, '--json'], 30_000);\n } catch (err) {\n // Stop failures should never mask the real test failure.\n this.logSink?.(`[kb-dev] stop failed (ignored): ${(err as Error).message}`);\n }\n for (const id of toStop) {this.startedByUs.delete(id);}\n this.lastStatus = undefined;\n }\n\n /** Disposes the controller — stops every service it started. */\n async dispose(): Promise<void> {\n await this.stopServices();\n }\n\n /**\n * Return the HTTP base URL for a service, e.g. `http://localhost:4000`.\n * Throws if the service is not in the last known status snapshot or has no URL.\n */\n getServiceUrl(id: ServiceId): string {\n const svc = this.requireService(id);\n if (!svc.url) {\n throw new Error(`Service ${id} has no URL in its status (state=${svc.state})`);\n }\n return svc.url;\n }\n\n /** Return the port for a service, or throw if unknown. */\n getServicePort(id: ServiceId): number {\n const svc = this.requireService(id);\n if (!svc.port) {\n throw new Error(`Service ${id} has no port in its status (state=${svc.state})`);\n }\n return svc.port;\n }\n\n /** Snapshot of recent subprocess log lines, newest last. */\n dumpRecentLogs(): string[] {\n return this.ring.snapshot();\n }\n\n // ── Internals ─────────────────────────────────────────────────────────────\n\n private requireService(id: ServiceId): ServiceStatus {\n if (!this.lastStatus) {\n throw new Error(\n `No status snapshot yet for ${id}. Call ensureServices() or status() first.`,\n );\n }\n const svc = this.lastStatus.services[id];\n if (!svc) {\n throw new Error(`Service ${id} not found in status snapshot`);\n }\n return svc;\n }\n\n private async waitUntilAlive(\n ids: ServiceId[],\n timeoutMs: number,\n ): Promise<StatusSnapshot> {\n const deadline = Date.now() + timeoutMs;\n let lastSnap: StatusSnapshot | undefined;\n while (Date.now() < deadline) {\n lastSnap = await this.status();\n const allAlive = ids.every((id) => lastSnap?.services[id]?.state === 'alive');\n if (allAlive) {return lastSnap;}\n // Fail fast on hard failures — no point waiting for a service already marked `failed`.\n const failed = ids.find((id) => lastSnap?.services[id]?.state === 'failed');\n if (failed) {\n const svc = lastSnap!.services[failed]!;\n throw new Error(\n `Service ${failed} failed during ensure.\\n` +\n `Logs tail:\\n${(svc.logsTail ?? []).join('\\n')}`,\n );\n }\n await sleep(500);\n }\n throw new Error(\n `Timeout waiting for services to become alive: ${ids.join(', ')}.\\n` +\n `Last known states: ${ids\n .map((id) => `${id}=${lastSnap?.services[id]?.state ?? 'unknown'}`)\n .join(', ')}`,\n );\n }\n\n private formatError(prefix: string, result: KbDevResult): Error {\n const lines: string[] = [`${prefix} failed`];\n if (result.hint) {lines.push(`Hint: ${result.hint}`);}\n for (const action of result.actions ?? []) {\n if (action.action === 'failed' || action.error) {\n lines.push(` - ${action.service}: ${action.error ?? 'failed'}`);\n for (const log of action.logsTail ?? []) {\n lines.push(` ${log}`);\n }\n }\n }\n const recent = this.ring.snapshot();\n if (recent.length > 0) {\n lines.push('Recent kb-dev output:');\n for (const l of recent.slice(-40)) {lines.push(` ${l}`);}\n }\n return new Error(lines.join('\\n'));\n }\n\n /**\n * Run a kb-dev subcommand and parse the stdout as JSON.\n * kb-dev prints only the JSON object on stdout when `--json` is set;\n * any logs go to stderr.\n */\n private async runJson<T>(args: string[], timeoutMs: number): Promise<T> {\n const result = await this.run(args, timeoutMs);\n if (result.code !== 0 && !result.stdout.trim()) {\n throw new Error(\n `kb-dev ${args.join(' ')} exited with code ${result.code}\\n` +\n `stderr:\\n${result.stderr}`,\n );\n }\n try {\n return JSON.parse(result.stdout) as T;\n } catch (err) {\n throw new Error(\n `Failed to parse JSON from \\`kb-dev ${args.join(' ')}\\`:\\n` +\n ` error: ${(err as Error).message}\\n` +\n ` stdout: ${result.stdout.slice(0, 2000)}\\n` +\n ` stderr: ${result.stderr.slice(0, 2000)}`,\n );\n }\n }\n\n private run(args: string[], timeoutMs: number): Promise<SubprocessResult> {\n return new Promise((resolvePromise) => {\n const child = spawn(this.kbDevBin, args, {\n cwd: this.projectRoot,\n env: this.env,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n let stdout = '';\n let stderr = '';\n\n const onLine = (chunk: Buffer, stream: 'stdout' | 'stderr') => {\n const text = chunk.toString();\n if (stream === 'stdout') {stdout += text;}\n else {stderr += text;}\n for (const line of text.split('\\n')) {\n if (line) {\n this.ring.push(`[${stream}] ${line}`);\n this.logSink?.(`[kb-dev ${args[0]}] [${stream}] ${line}`);\n if (stream === 'stdout') {\n this.diagCollector?.parseLine(line);\n }\n }\n }\n };\n\n child.stdout.on('data', (d: Buffer) => onLine(d, 'stdout'));\n child.stderr.on('data', (d: Buffer) => onLine(d, 'stderr'));\n\n const timer = setTimeout(() => {\n child.kill('SIGKILL');\n }, timeoutMs);\n\n child.on('close', (code) => {\n clearTimeout(timer);\n resolvePromise({ code: code ?? -1, stdout, stderr });\n });\n child.on('error', (err) => {\n clearTimeout(timer);\n resolvePromise({ code: -1, stdout, stderr: stderr + '\\n' + err.message });\n });\n });\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((r) => { setTimeout(r, ms); });\n}\n","import type { KbDevController } from './kb-dev-controller.js';\n\nconst SNAPSHOT_PATH = '/tmp/kb-e2e-diag.json';\n\n/**\n * Register a Vitest onTestFailed hook that writes a DiagSnapshot when a test fails.\n *\n * Safe to call in non-Vitest environments (Playwright, Node scripts) — the vitest\n * import is dynamic and failures are caught silently.\n *\n * Usage in beforeAll:\n * await registerDiagSnapshotHook(ctrl);\n */\nexport async function registerDiagSnapshotHook(ctrl: KbDevController): Promise<void> {\n // Dynamic import keeps this file free of static vitest dep (Playwright-safe)\n const vitest = await import('vitest').catch(() => null);\n if (!vitest?.onTestFailed) { return; }\n\n vitest.onTestFailed(async () => {\n const collector = ctrl.getDiagCollector();\n if (!collector) { return; }\n\n try {\n await collector.writeSnapshot(SNAPSHOT_PATH);\n\n // Vitest 3.x attachment API — degrade gracefully if unavailable\n try {\n const { inject } = await import('vitest');\n const attachments = (inject as (key: string) => unknown | undefined)('testAttachments') as\n | { attach(name: string, opts: { path: string; contentType: string }): Promise<void> }\n | undefined;\n await attachments?.attach('kb-e2e-diag.json', {\n path: SNAPSHOT_PATH,\n contentType: 'application/json',\n });\n } catch {\n process.stderr.write(`[KB_DEBUG] DiagSnapshot written to ${SNAPSHOT_PATH}\\n`);\n }\n } catch {\n // Never let a diagnostic failure propagate into the test result\n }\n });\n}\n","/**\n * Minimal HTTP client wrapper used by e2e tests.\n *\n * Intentionally thin — tests should be able to read one line and know\n * exactly what request went out. No retries, no interceptors, no magic.\n */\n\nexport interface HttpClientOptions {\n /** Extra headers included on every request. */\n headers?: Record<string, string>;\n /** Per-request timeout in ms. Default 15_000. */\n timeoutMs?: number;\n}\n\nexport interface HttpResponse<T = unknown> {\n status: number;\n ok: boolean;\n headers: Headers;\n /** Parsed JSON body, or `undefined` if the response had no body / non-JSON content. */\n body: T | undefined;\n /** Raw text body, always populated. */\n text: string;\n}\n\nexport class HttpClient {\n constructor(\n private readonly baseUrl: string,\n private readonly defaults: HttpClientOptions = {},\n ) {}\n\n async get<T = unknown>(path: string, opts: HttpClientOptions = {}): Promise<HttpResponse<T>> {\n return this.request<T>('GET', path, undefined, opts);\n }\n\n async post<T = unknown>(\n path: string,\n body: unknown,\n opts: HttpClientOptions = {},\n ): Promise<HttpResponse<T>> {\n return this.request<T>('POST', path, body, opts);\n }\n\n async delete<T = unknown>(\n path: string,\n opts: HttpClientOptions = {},\n ): Promise<HttpResponse<T>> {\n return this.request<T>('DELETE', path, undefined, opts);\n }\n\n async put<T = unknown>(\n path: string,\n body: unknown,\n opts: HttpClientOptions = {},\n ): Promise<HttpResponse<T>> {\n return this.request<T>('PUT', path, body, opts);\n }\n\n async options<T = unknown>(\n path: string,\n opts: HttpClientOptions = {},\n ): Promise<HttpResponse<T>> {\n return this.request<T>('OPTIONS', path, undefined, opts);\n }\n\n /** Build an absolute URL for a path under this client's base. */\n url(path: string): string {\n if (path.startsWith('http://') || path.startsWith('https://')) {return path;}\n return `${this.baseUrl.replace(/\\/$/, '')}${path.startsWith('/') ? path : `/${path}`}`;\n }\n\n private async request<T>(\n method: string,\n path: string,\n body: unknown,\n opts: HttpClientOptions,\n ): Promise<HttpResponse<T>> {\n const timeoutMs = opts.timeoutMs ?? this.defaults.timeoutMs ?? 15_000;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n const headers: Record<string, string> = {\n ...this.defaults.headers,\n ...opts.headers,\n };\n if (body !== undefined && !headers['content-type'] && !headers['Content-Type']) {\n headers['content-type'] = 'application/json';\n }\n\n try {\n const res = await fetch(this.url(path), {\n method,\n headers,\n body: body === undefined ? undefined : JSON.stringify(body),\n signal: controller.signal,\n });\n const text = await res.text();\n let parsed: T | undefined;\n const contentType = res.headers.get('content-type') ?? '';\n if (text && contentType.includes('json')) {\n try {\n parsed = JSON.parse(text) as T;\n } catch {\n parsed = undefined;\n }\n }\n return {\n status: res.status,\n ok: res.ok,\n headers: res.headers,\n body: parsed,\n text,\n };\n } finally {\n clearTimeout(timer);\n }\n }\n}\n\nexport function httpClient(baseUrl: string, opts: HttpClientOptions = {}): HttpClient {\n return new HttpClient(baseUrl, opts);\n}\n","import { WebSocket, type RawData } from 'ws';\n\n/**\n * Test-friendly WebSocket wrapper.\n *\n * Adapted from `infra/kb-labs-gateway/apps/gateway-app/src/__tests__/live-gateway.e2e.test.ts`\n * — same tracking semantics so the harness can clean up leaked sockets\n * between tests even when an assertion fails mid-flight.\n */\n\nexport interface WsOptions {\n headers?: Record<string, string>;\n /** ms. Default 8000. */\n openTimeoutMs?: number;\n}\n\n/** Opaque handle returned from `connectWs`. */\nexport interface WsHandle {\n readonly socket: WebSocket;\n /** Send a JSON-stringified message. */\n send(data: unknown): void;\n /** Wait for the next message, optionally satisfying a predicate. */\n waitForMessage<T = unknown>(\n opts?: { timeoutMs?: number; predicate?: (msg: T) => boolean },\n ): Promise<T>;\n /** Collect the next N messages (parsed as JSON). */\n collect<T = unknown>(count: number, timeoutMs?: number): Promise<T[]>;\n close(code?: number): void;\n}\n\n/**\n * Shared registry of open sockets. Call `closeAllTrackedSockets()` from an\n * `afterEach` to guarantee cleanup.\n */\nconst openSockets = new Set<WebSocket>();\n\nexport async function closeAllTrackedSockets(graceMs = 150): Promise<void> {\n const toClose = [...openSockets];\n for (const ws of toClose) {\n if (ws.readyState === ws.OPEN || ws.readyState === ws.CONNECTING) {\n try {\n ws.close(1000);\n } catch {\n // intentionally empty: best-effort cleanup, socket may already be dead\n }\n }\n }\n openSockets.clear();\n if (toClose.length > 0) {\n await new Promise((r) => { setTimeout(r, graceMs); });\n }\n}\n\nexport function connectWs(url: string, opts: WsOptions = {}): Promise<WsHandle> {\n const openTimeoutMs = opts.openTimeoutMs ?? 8000;\n return new Promise((resolvePromise, reject) => {\n const ws = new WebSocket(url, {\n headers: opts.headers,\n });\n openSockets.add(ws);\n ws.on('close', () => openSockets.delete(ws));\n\n const timer = setTimeout(() => {\n reject(new Error(`WebSocket open timeout after ${openTimeoutMs}ms: ${url}`));\n try { ws.close(); } catch {\n // intentionally empty: best-effort cleanup on timeout\n }\n }, openTimeoutMs);\n\n ws.once('open', () => {\n clearTimeout(timer);\n resolvePromise(wrapHandle(ws));\n });\n ws.once('error', (err) => {\n clearTimeout(timer);\n reject(err);\n });\n });\n}\n\nfunction wrapHandle(ws: WebSocket): WsHandle {\n return {\n socket: ws,\n send(data: unknown) {\n ws.send(typeof data === 'string' ? data : JSON.stringify(data));\n },\n waitForMessage<T = unknown>(\n opts: { timeoutMs?: number; predicate?: (msg: T) => boolean } = {},\n ): Promise<T> {\n const timeoutMs = opts.timeoutMs ?? 5000;\n return new Promise((resolvePromise, reject) => {\n const cleanup = () => {\n ws.off('message', onMessage);\n ws.off('close', onClose);\n };\n const timer = setTimeout(() => {\n cleanup();\n reject(new Error(`WebSocket message timeout after ${timeoutMs}ms`));\n }, timeoutMs);\n const onClose = (code: number, reason: Buffer) => {\n clearTimeout(timer);\n cleanup();\n reject(new Error(`WebSocket closed (code=${code}) before message: ${reason.toString() || '(no reason)'}`));\n };\n const onMessage = (raw: RawData) => {\n let msg: T;\n try {\n msg = JSON.parse(raw.toString()) as T;\n } catch (err) {\n clearTimeout(timer);\n cleanup();\n reject(new Error(`Non-JSON WebSocket message: ${(err as Error).message}`));\n return;\n }\n if (!opts.predicate || opts.predicate(msg)) {\n clearTimeout(timer);\n cleanup();\n resolvePromise(msg);\n }\n };\n ws.on('message', onMessage);\n ws.on('close', onClose);\n });\n },\n collect<T = unknown>(count: number, timeoutMs = 5000): Promise<T[]> {\n return new Promise((resolvePromise, reject) => {\n const msgs: T[] = [];\n const cleanup = () => { ws.off('message', onMessage); ws.off('close', onClose); };\n const timer = setTimeout(() => {\n cleanup();\n reject(\n new Error(\n `Timeout: expected ${count} WS messages, got ${msgs.length}: ${JSON.stringify(msgs)}`,\n ),\n );\n }, timeoutMs);\n const onClose = (code: number, reason: Buffer) => {\n clearTimeout(timer);\n cleanup();\n reject(new Error(`WebSocket closed (code=${code}) before collecting ${count} messages: ${reason.toString() || '(no reason)'}`));\n };\n const onMessage = (raw: RawData) => {\n msgs.push(JSON.parse(raw.toString()) as T);\n if (msgs.length >= count) {\n clearTimeout(timer);\n cleanup();\n resolvePromise(msgs);\n }\n };\n ws.on('message', onMessage);\n ws.on('close', onClose);\n });\n },\n close(code = 1000) {\n try {\n ws.close(code);\n } catch {\n // intentionally empty: best-effort cleanup\n }\n },\n };\n}\n","/**\n * Minimal Server-Sent Events reader.\n *\n * Consumes `text/event-stream` responses and yields one `SseEvent` per\n * `event:`/`data:` pair. Terminates on close, explicit `untilEvent`, or timeout.\n */\n\nexport interface SseEvent {\n /** SSE `event:` field. Empty string if absent. */\n event: string;\n /** SSE `data:` field, concatenated with newlines if multi-line. */\n data: string;\n /** Optional `id:` field. */\n id?: string;\n /** Parsed JSON body if `data` is valid JSON, else undefined. */\n json?: unknown;\n}\n\nexport interface SseOptions {\n headers?: Record<string, string>;\n /** If set, the iterator terminates after the first event with this name. */\n untilEvent?: string;\n /** Wall-clock timeout in ms. Default 30_000. */\n timeoutMs?: number;\n}\n\n/**\n * Async iterator over an SSE stream. Caller is responsible for iterating\n * (e.g. `for await (const event of readSse(url)) { ... }`).\n */\nexport async function* readSse(url: string, opts: SseOptions = {}): AsyncGenerator<SseEvent> {\n const timeoutMs = opts.timeoutMs ?? 30_000;\n // Use a race-based timeout instead of AbortController on the fetch /\n // reader: AbortController surfaces AbortError to callers that we cannot\n // suppress reliably (the response stream pipes through Playwright's\n // fixture cleanup, where re-throws happen outside this generator's\n // try/catch). Race the read against a sleep and treat timeout as a\n // clean end-of-stream — that is what every collectSseEvents-style\n // caller expects.\n const deadline = Date.now() + timeoutMs;\n\n let res: Response;\n try {\n res = await fetch(url, {\n headers: { accept: 'text/event-stream', ...opts.headers },\n });\n } catch (err) {\n throw new Error(`SSE fetch failed: ${(err as Error).message}`);\n }\n\n if (!res.ok || !res.body) {\n throw new Error(`SSE response not ok: ${res.status} ${res.statusText}`);\n }\n\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n type ReadResult = { value?: Uint8Array; done: boolean };\n const TIMEOUT_MARKER: ReadResult = { done: true };\n\n try {\n while (true) {\n const remaining = deadline - Date.now();\n if (remaining <= 0) { return; }\n\n // Race the next chunk against the deadline. A timeout returns a\n // synthetic \"done\" marker — no AbortError ever surfaces.\n const chunk: ReadResult = await Promise.race([\n reader.read() as Promise<ReadResult>,\n new Promise<ReadResult>((resolve) => setTimeout(() => resolve(TIMEOUT_MARKER), remaining)),\n ]);\n\n const { value, done } = chunk;\n if (done) { return; }\n buffer += decoder.decode(value, { stream: true });\n\n // SSE events are separated by a blank line (\\n\\n).\n let sepIdx: number;\n while ((sepIdx = buffer.indexOf('\\n\\n')) !== -1) {\n const rawEvent = buffer.slice(0, sepIdx);\n buffer = buffer.slice(sepIdx + 2);\n const parsed = parseEvent(rawEvent);\n if (!parsed) { continue; }\n yield parsed;\n if (opts.untilEvent && parsed.event === opts.untilEvent) {\n return;\n }\n }\n }\n } finally {\n try { await reader.cancel(); } catch {\n // intentionally empty: reader may already be closed\n }\n }\n}\n\nfunction parseEvent(raw: string): SseEvent | null {\n if (!raw.trim()) {return null;}\n let eventName = '';\n const dataLines: string[] = [];\n let id: string | undefined;\n\n for (const line of raw.split('\\n')) {\n if (!line || line.startsWith(':')) {continue;} // comment / keep-alive\n const colonIdx = line.indexOf(':');\n if (colonIdx === -1) {continue;}\n const field = line.slice(0, colonIdx);\n // Per spec: strip a single leading space from value.\n const value = line.slice(colonIdx + 1).replace(/^ /, '');\n if (field === 'event') {eventName = value;}\n else if (field === 'data') {dataLines.push(value);}\n else if (field === 'id') {id = value;}\n }\n\n if (eventName === '' && dataLines.length === 0) {return null;}\n const data = dataLines.join('\\n');\n let json: unknown;\n if (data) {\n try { json = JSON.parse(data); } catch { /* not JSON, leave undefined */ }\n }\n return { event: eventName, data, id, json };\n}\n","/**\n * Gateway auth helpers.\n *\n * Extracted from `infra/kb-labs-gateway/apps/gateway-app/src/__tests__/live-gateway.e2e.test.ts`\n * (`getJwtToken`, `registerHost`) so every e2e test can reuse the same flow.\n *\n * Flow:\n * 1. POST /auth/register { name, namespaceId } -> { clientId, clientSecret, hostId }\n * 2. POST /auth/token { clientId, clientSecret } -> { accessToken }\n */\n\nimport type { HttpClient } from './http-client.js';\n\nexport interface AgentCredentials {\n accessToken: string;\n clientId: string;\n hostId: string;\n}\n\nexport interface HostCredentials {\n hostId: string;\n machineToken: string;\n}\n\nexport async function registerAgent(\n client: HttpClient,\n opts: { name?: string; namespaceId: string } = { namespaceId: 'e2e-default' },\n): Promise<AgentCredentials> {\n const name = opts.name ?? 'e2e-agent';\n const regRes = await client.post<{ clientId: string; clientSecret: string; hostId: string }>(\n '/auth/register',\n { name, namespaceId: opts.namespaceId },\n );\n if (!regRes.ok || !regRes.body) {\n throw new Error(`/auth/register failed: ${regRes.status} ${regRes.text}`);\n }\n const { clientId, clientSecret, hostId } = regRes.body;\n\n const tokenRes = await client.post<{ accessToken: string }>(\n '/auth/token',\n { clientId, clientSecret },\n );\n if (!tokenRes.ok || !tokenRes.body) {\n throw new Error(`/auth/token failed: ${tokenRes.status} ${tokenRes.text}`);\n }\n\n return { accessToken: tokenRes.body.accessToken, clientId, hostId };\n}\n\nexport async function registerHost(\n client: HttpClient,\n opts: {\n name?: string;\n namespaceId: string;\n capabilities?: string[];\n workspacePaths?: string[];\n },\n): Promise<HostCredentials> {\n const res = await client.post<HostCredentials>('/hosts/register', {\n name: opts.name ?? 'e2e-host',\n namespaceId: opts.namespaceId,\n capabilities: opts.capabilities ?? ['filesystem'],\n workspacePaths: opts.workspacePaths ?? [],\n });\n if (!res.ok || !res.body) {\n throw new Error(`/hosts/register failed: ${res.status} ${res.text}`);\n }\n return res.body;\n}\n","import { mkdir, mkdtemp, rm, writeFile, copyFile, readFile } from 'node:fs/promises';\nimport { existsSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { join, resolve } from 'node:path';\n\n/**\n * An isolated temporary project root with its own `.kb/` directory.\n *\n * Used by marketplace/plugin e2e tests so real workspace `.kb/marketplace.lock`\n * and `.kb/plugins.json` are never mutated.\n *\n * Pass the returned `root` via `KB_PROJECT_ROOT` env when starting kb-dev.\n */\nexport interface IsolatedProjectRoot {\n /** Absolute path to the temp project root. */\n root: string;\n /** Absolute path to `<root>/.kb/devservices.yaml`. */\n devConfigPath: string;\n /** Delete the temp dir. Safe to call multiple times. */\n cleanup(): Promise<void>;\n}\n\nexport interface IsolatedProjectRootOptions {\n /**\n * If given, copies the devservices.yaml from this existing workspace root.\n * If omitted, a minimal config with only `state-daemon` is written.\n */\n copyDevConfigFrom?: string;\n /** Prefix for the temp dir name (for human diagnosis). */\n prefix?: string;\n}\n\nconst MINIMAL_DEV_CONFIG = `name: KB Labs E2E Isolated Root\n\ngroups:\n infra: [state-daemon]\n\nservices:\n state-daemon:\n name: State Daemon\n description: Distributed state management\n group: infra\n type: node\n command: node ./plugins/state/daemon/core-state-daemon/dist/bin.cjs\n healthCheck: http://localhost:7777/health\n port: 7777\n url: http://localhost:7777\n env:\n KB_STATE_DAEMON_PORT: \"7777\"\n KB_STATE_DAEMON_HOST: localhost\n\nsettings:\n logsDir: .kb/logs/tmp\n pidDir: .kb/tmp\n startTimeout: 30000\n healthCheckInterval: 1000\n`;\n\nexport async function createIsolatedProjectRoot(\n opts: IsolatedProjectRootOptions = {},\n): Promise<IsolatedProjectRoot> {\n const prefix = opts.prefix ?? 'kb-e2e-';\n const root = await mkdtemp(join(tmpdir(), prefix));\n const kbDir = join(root, '.kb');\n await mkdir(kbDir, { recursive: true });\n await mkdir(join(kbDir, 'tmp'), { recursive: true });\n await mkdir(join(kbDir, 'logs', 'tmp'), { recursive: true });\n\n const devConfigPath = join(kbDir, 'devservices.yaml');\n\n if (opts.copyDevConfigFrom) {\n const source = resolve(opts.copyDevConfigFrom, '.kb/devservices.yaml');\n if (!existsSync(source)) {\n throw new Error(`copyDevConfigFrom: no devservices.yaml at ${source}`);\n }\n // Read to validate it exists and is readable.\n await readFile(source, 'utf8');\n await copyFile(source, devConfigPath);\n } else {\n await writeFile(devConfigPath, MINIMAL_DEV_CONFIG, 'utf8');\n }\n\n let disposed = false;\n const cleanup = async () => {\n if (disposed) {return;}\n disposed = true;\n try {\n await rm(root, { recursive: true, force: true });\n } catch {\n // intentionally empty: best-effort cleanup, tmpdir will be reaped eventually\n }\n };\n\n return { root, devConfigPath, cleanup };\n}\n","import { createHash } from 'node:crypto';\nimport { nanoid } from 'nanoid';\n\n/**\n * Build a deterministic-yet-unique namespace prefix for e2e test resources.\n *\n * Shape: `e2e-<fileHash>-<nanoid>`\n *\n * - `fileHash` is 6 hex chars of sha1(filePath) — lets you grep leaked resources\n * back to the test file that created them.\n * - `nanoid` is a 10-char random suffix — lets parallel runs never collide.\n *\n * Pass `import.meta.url` from the test file.\n */\nexport function makeTestNamespace(fileUrlOrPath: string, prefix = 'e2e'): string {\n const hash = createHash('sha1').update(fileUrlOrPath).digest('hex').slice(0, 6);\n return `${prefix}-${hash}-${nanoid(10)}`;\n}\n","import { readSse } from '../sse-reader.js';\nimport type { SseEvent } from '../sse-reader.js';\n\nexport type { SseEvent };\n\nexport interface CollectSseOptions {\n /** Stop after collecting this many events. */\n count?: number;\n /** Stop after receiving an event with this type (inclusive). */\n untilEvent?: string;\n /** Wall-clock timeout in ms. Default 30_000. */\n timeoutMs?: number;\n /** Extra HTTP headers forwarded to the SSE request. */\n headers?: Record<string, string>;\n}\n\n/**\n * Collects SSE events from `url` into an array.\n * Stops on `count`, `untilEvent`, stream close, or timeout.\n */\nexport async function collectSseEvents(url: string, opts: CollectSseOptions = {}): Promise<SseEvent[]> {\n const events: SseEvent[] = [];\n\n for await (const event of readSse(url, {\n untilEvent: opts.untilEvent,\n timeoutMs: opts.timeoutMs,\n headers: opts.headers,\n })) {\n events.push(event);\n if (opts.count !== undefined && events.length >= opts.count) {\n break;\n }\n }\n\n return events;\n}\n\n/**\n * Returns the first event matching `eventType`, or rejects on timeout.\n */\nexport async function waitForSseEvent(\n url: string,\n eventType: string,\n opts: { timeoutMs?: number; headers?: Record<string, string> } = {},\n): Promise<SseEvent> {\n for await (const event of readSse(url, { timeoutMs: opts.timeoutMs ?? 30_000, headers: opts.headers })) {\n if (event.event === eventType) {\n return event;\n }\n }\n throw new Error(`SSE stream ended before receiving event type \"${eventType}\"`);\n}\n\n/**\n * Asserts the SSE connection at `url` closes on its own without hanging.\n * Rejects if the stream is still open after `timeoutMs`.\n */\nexport async function expectSseTerminates(\n url: string,\n opts: { timeoutMs?: number; headers?: Record<string, string> } = {},\n): Promise<void> {\n const timeoutMs = opts.timeoutMs ?? 5_000;\n\n await Promise.race([\n (async () => {\n // Drain all events — the stream must close naturally.\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n for await (const _event of readSse(url, { timeoutMs, headers: opts.headers })) {\n // just consume\n }\n })(),\n new Promise<void>((_, reject) =>\n setTimeout(() => reject(new Error(`SSE stream did not terminate within ${timeoutMs}ms`)), timeoutMs),\n ),\n ]);\n}\n","import type { SseEvent } from '../sse-reader.js';\n\n/**\n * Asserts SSE events arrive in the exact order of `expectedTypes`.\n * Extra events between expected ones are allowed; missing events throw.\n */\nexport function assertSseOrder(events: SseEvent[], expectedTypes: string[]): void {\n let cursor = 0;\n for (const event of events) {\n if (cursor >= expectedTypes.length) { break; }\n if (event.event === expectedTypes[cursor]) {\n cursor++;\n }\n }\n if (cursor < expectedTypes.length) {\n const received = events.map((e) => e.event);\n throw new Error(\n `SSE order assertion failed.\\n` +\n `Expected (in order): ${expectedTypes.join(', ')}\\n` +\n `Received: ${received.join(', ')}\\n` +\n `Missing: ${expectedTypes[cursor]}`,\n );\n }\n}\n\n/**\n * Asserts no two events share the same event type AND id combination.\n * Events without an id field are compared by type + data.\n */\nexport function assertNoSseDuplicates(events: SseEvent[]): void {\n const seen = new Set<string>();\n for (const event of events) {\n const key = event.id ? `${event.event}::${event.id}` : `${event.event}::${event.data}`;\n if (seen.has(key)) {\n throw new Error(`Duplicate SSE event detected: type=\"${event.event}\" id=\"${event.id ?? '(none)'}\"`);\n }\n seen.add(key);\n }\n}\n","import { connectWs } from '../ws-client.js';\nimport type { WsHandle, WsOptions } from '../ws-client.js';\n\nexport type { WsHandle, WsOptions };\n\n/**\n * Opens a WebSocket, runs `fn`, then closes the socket.\n * The socket is always closed in a finally block — even if `fn` throws.\n */\nexport async function withWs<T>(\n url: string,\n fn: (ws: WsHandle) => Promise<T>,\n opts?: WsOptions,\n): Promise<T> {\n const ws = await connectWs(url, opts);\n try {\n return await fn(ws);\n } finally {\n try {\n ws.close(1000);\n } catch {\n // intentionally empty: best-effort cleanup\n }\n }\n}\n","import type { WsHandle } from '../ws-client.js';\n\n/**\n * Waits for the next WebSocket message that satisfies `predicate`.\n * Times out after `timeoutMs` (default 8000).\n */\nexport async function expectWsMessage<T = unknown>(\n ws: WsHandle,\n predicate: (msg: T) => boolean,\n opts: { timeoutMs?: number; label?: string } = {},\n): Promise<T> {\n const timeoutMs = opts.timeoutMs ?? 8_000;\n const label = opts.label ?? 'expected WS message';\n\n return ws.waitForMessage<T>({\n timeoutMs,\n predicate,\n }).catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : String(err);\n const isTimeout = msg.includes('timeout');\n throw new Error(isTimeout ? `Timed out waiting for ${label} after ${timeoutMs}ms` : `${label}: ${msg}`);\n });\n}\n\n/**\n * Asserts the WebSocket closes with the given code (default 1000).\n * Times out after `timeoutMs` (default 5000).\n */\nexport function expectWsClose(\n ws: WsHandle,\n code = 1000,\n opts: { timeoutMs?: number } = {},\n): Promise<void> {\n const timeoutMs = opts.timeoutMs ?? 5_000;\n\n return new Promise<void>((resolve, reject) => {\n const timer = setTimeout(\n () => reject(new Error(`WebSocket did not close with code ${code} within ${timeoutMs}ms`)),\n timeoutMs,\n );\n\n ws.socket.once('close', (closeCode: number) => {\n clearTimeout(timer);\n if (code !== undefined && closeCode !== code) {\n reject(new Error(`WebSocket closed with code ${closeCode}, expected ${code}`));\n } else {\n resolve();\n }\n });\n });\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kb-labs/shared-testing-e2e",
3
3
  "description": "E2E test harness for KB Labs platform — kb-dev controller, HTTP/WS/SSE helpers, isolated project roots",
4
- "version": "2.93.0",
4
+ "version": "2.96.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -9,6 +9,10 @@
9
9
  ".": {
10
10
  "types": "./dist/index.d.ts",
11
11
  "import": "./dist/index.js"
12
+ },
13
+ "./cli": {
14
+ "types": "./dist/cli.d.ts",
15
+ "import": "./dist/cli.js"
12
16
  }
13
17
  },
14
18
  "files": [
@@ -19,7 +23,10 @@
19
23
  ],
20
24
  "dependencies": {
21
25
  "nanoid": "^5.0.0",
22
- "ws": "^8.18.0"
26
+ "vitest": "^3.2.6",
27
+ "ws": "^8.20.1",
28
+ "@kb-labs/plugin-contracts": "2.96.0",
29
+ "@kb-labs/shared-command-kit": "2.96.0"
23
30
  },
24
31
  "devDependencies": {
25
32
  "@types/node": "^24.3.3",
@@ -27,8 +34,7 @@
27
34
  "rimraf": "^6.0.1",
28
35
  "tsup": "^8.5.0",
29
36
  "typescript": "^5.6.3",
30
- "vitest": "^3.2.4",
31
- "@kb-labs/devkit": "2.93.0"
37
+ "@kb-labs/devkit": "2.96.0"
32
38
  },
33
39
  "engines": {
34
40
  "node": ">=20.0.0",