@b4run/testing 0.8.28
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/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/aimock-runner.d.ts +48 -0
- package/dist/aimock-runner.d.ts.map +1 -0
- package/dist/aimock-runner.js +81 -0
- package/dist/checkpointer-conformance.d.ts +43 -0
- package/dist/checkpointer-conformance.d.ts.map +1 -0
- package/dist/checkpointer-conformance.js +381 -0
- package/dist/fake-embedder.d.ts +9 -0
- package/dist/fake-embedder.d.ts.map +1 -0
- package/dist/fake-embedder.js +38 -0
- package/dist/fixture-builder.d.ts +29 -0
- package/dist/fixture-builder.d.ts.map +1 -0
- package/dist/fixture-builder.js +43 -0
- package/dist/fixture-file.d.ts +14 -0
- package/dist/fixture-file.d.ts.map +1 -0
- package/dist/fixture-file.js +49 -0
- package/dist/harness.d.ts +35 -0
- package/dist/harness.d.ts.map +1 -0
- package/dist/harness.js +211 -0
- package/dist/http-inject.d.ts +22 -0
- package/dist/http-inject.d.ts.map +1 -0
- package/dist/http-inject.js +55 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/matchers.d.ts +56 -0
- package/dist/matchers.d.ts.map +1 -0
- package/dist/matchers.js +262 -0
- package/dist/memory-conformance.d.ts +13 -0
- package/dist/memory-conformance.d.ts.map +1 -0
- package/dist/memory-conformance.js +1333 -0
- package/dist/memory.d.ts +11 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +20 -0
- package/dist/middleware-harness.d.ts +11 -0
- package/dist/middleware-harness.d.ts.map +1 -0
- package/dist/middleware-harness.js +35 -0
- package/dist/permissions-conformance.d.ts +24 -0
- package/dist/permissions-conformance.d.ts.map +1 -0
- package/dist/permissions-conformance.js +156 -0
- package/dist/record-fixtures.d.ts +21 -0
- package/dist/record-fixtures.d.ts.map +1 -0
- package/dist/record-fixtures.js +31 -0
- package/dist/record.d.ts +12 -0
- package/dist/record.d.ts.map +1 -0
- package/dist/record.js +22 -0
- package/dist/regex-safety.d.ts +2 -0
- package/dist/regex-safety.d.ts.map +1 -0
- package/dist/regex-safety.js +66 -0
- package/dist/run-result.d.ts +111 -0
- package/dist/run-result.d.ts.map +1 -0
- package/dist/run-result.js +287 -0
- package/dist/subprocess.d.ts +25 -0
- package/dist/subprocess.d.ts.map +1 -0
- package/dist/subprocess.js +223 -0
- package/dist/thread-access-harness.d.ts +40 -0
- package/dist/thread-access-harness.d.ts.map +1 -0
- package/dist/thread-access-harness.js +42 -0
- package/dist/threads-conformance.d.ts +18 -0
- package/dist/threads-conformance.d.ts.map +1 -0
- package/dist/threads-conformance.js +315 -0
- package/dist/tool-harness.d.ts +16 -0
- package/dist/tool-harness.d.ts.map +1 -0
- package/dist/tool-harness.js +25 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/workspace-harness.d.ts +15 -0
- package/dist/workspace-harness.d.ts.map +1 -0
- package/dist/workspace-harness.js +43 -0
- package/package.json +71 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
function hash(s) {
|
|
2
|
+
let h = 2166136261;
|
|
3
|
+
for (let i = 0; i < s.length; i++) {
|
|
4
|
+
h ^= s.charCodeAt(i);
|
|
5
|
+
h = Math.imul(h, 16777619);
|
|
6
|
+
}
|
|
7
|
+
return h >>> 0;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Deterministic, network-free embedder for tests: a normalized bag-of-token-hash
|
|
11
|
+
* vector, so strings sharing tokens are nearer in cosine. NOT for production.
|
|
12
|
+
*/
|
|
13
|
+
export function fakeEmbedder(opts) {
|
|
14
|
+
const dims = opts?.dims ?? 64;
|
|
15
|
+
return {
|
|
16
|
+
id: `fake:${dims}`,
|
|
17
|
+
dims,
|
|
18
|
+
async embed(texts) {
|
|
19
|
+
return texts.map((t) => {
|
|
20
|
+
const v = new Float32Array(dims);
|
|
21
|
+
for (const tok of t
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.split(/[^a-z0-9]+/)
|
|
24
|
+
.filter((x) => x.length > 1)) {
|
|
25
|
+
const idx = hash(tok) % dims;
|
|
26
|
+
v[idx] = v[idx] + 1;
|
|
27
|
+
}
|
|
28
|
+
let n = 0;
|
|
29
|
+
for (const x of v)
|
|
30
|
+
n += x * x;
|
|
31
|
+
n = Math.sqrt(n) || 1;
|
|
32
|
+
for (let i = 0; i < dims; i++)
|
|
33
|
+
v[i] = v[i] / n;
|
|
34
|
+
return v;
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface AimockToolCall {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly arguments: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
export type AimockResponse = {
|
|
7
|
+
content: string;
|
|
8
|
+
} | {
|
|
9
|
+
toolCalls: AimockToolCall[];
|
|
10
|
+
};
|
|
11
|
+
export interface AimockFixture {
|
|
12
|
+
readonly match: {
|
|
13
|
+
readonly userMessage?: string;
|
|
14
|
+
readonly turnIndex?: number;
|
|
15
|
+
readonly hasToolResult?: boolean;
|
|
16
|
+
};
|
|
17
|
+
readonly response: AimockResponse;
|
|
18
|
+
}
|
|
19
|
+
export type FixtureSet = AimockFixture[];
|
|
20
|
+
export interface ScriptBuilder {
|
|
21
|
+
user(text: string): ScriptBuilder;
|
|
22
|
+
callsTool(name: string, args: Record<string, unknown>, opts?: {
|
|
23
|
+
id?: string;
|
|
24
|
+
}): ScriptBuilder;
|
|
25
|
+
replies(content: string): ScriptBuilder;
|
|
26
|
+
build(): FixtureSet;
|
|
27
|
+
}
|
|
28
|
+
export declare function script(): ScriptBuilder;
|
|
29
|
+
//# sourceMappingURL=fixture-builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixture-builder.d.ts","sourceRoot":"","sources":["../src/fixture-builder.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC5C;AAED,MAAM,MAAM,cAAc,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,SAAS,EAAE,cAAc,EAAE,CAAA;CAAE,CAAA;AAElF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;QAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;QAC3B,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAA;KACjC,CAAA;IACD,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAA;CAClC;AAED,MAAM,MAAM,UAAU,GAAG,aAAa,EAAE,CAAA;AAExC,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAAA;IACjC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,aAAa,CAAA;IAC7F,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CAAA;IACvC,KAAK,IAAI,UAAU,CAAA;CACpB;AAED,wBAAgB,MAAM,IAAI,aAAa,CA4CtC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function script() {
|
|
2
|
+
const fixtures = [];
|
|
3
|
+
let groupIndex = -1;
|
|
4
|
+
let currentUser;
|
|
5
|
+
let stepInGroup = 0; // kept for tool call id generation only
|
|
6
|
+
function pushResponse(response) {
|
|
7
|
+
if (currentUser === undefined) {
|
|
8
|
+
throw new Error("script(): call .user(text) before .callsTool()/.replies()");
|
|
9
|
+
}
|
|
10
|
+
// turnIndex = number of assistant messages already in the thread before this
|
|
11
|
+
// LLM call (0 for the first call, 1 for the reply-after-tool, etc.).
|
|
12
|
+
// hasToolResult = true only when there is already a tool-role message in the
|
|
13
|
+
// thread, which is the case for the post-tool reply but not the initial call.
|
|
14
|
+
// Both values are stable when the harness creates a fresh thread per test
|
|
15
|
+
// (via h.reset()), so they unambiguously select the right fixture.
|
|
16
|
+
fixtures.push({
|
|
17
|
+
match: { userMessage: currentUser, turnIndex: stepInGroup, hasToolResult: stepInGroup > 0 },
|
|
18
|
+
response,
|
|
19
|
+
});
|
|
20
|
+
stepInGroup += 1;
|
|
21
|
+
}
|
|
22
|
+
const builder = {
|
|
23
|
+
user(text) {
|
|
24
|
+
groupIndex += 1;
|
|
25
|
+
currentUser = text;
|
|
26
|
+
stepInGroup = 0;
|
|
27
|
+
return builder;
|
|
28
|
+
},
|
|
29
|
+
callsTool(name, args, opts) {
|
|
30
|
+
const id = opts?.id ?? `call_${name}_${groupIndex}_${stepInGroup}`;
|
|
31
|
+
pushResponse({ toolCalls: [{ id, name, arguments: args }] });
|
|
32
|
+
return builder;
|
|
33
|
+
},
|
|
34
|
+
replies(content) {
|
|
35
|
+
pushResponse({ content });
|
|
36
|
+
return builder;
|
|
37
|
+
},
|
|
38
|
+
build() {
|
|
39
|
+
return fixtures.slice();
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
return builder;
|
|
43
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FixtureSet, ScriptBuilder } from "./fixture-builder.js";
|
|
2
|
+
/**
|
|
3
|
+
* Write a FixtureSet (or ScriptBuilder) to a JSON file.
|
|
4
|
+
* The file is written as `{ "fixtures": [...] }` (pretty-printed, 2-space indent).
|
|
5
|
+
* Parent directories are created automatically.
|
|
6
|
+
*/
|
|
7
|
+
export declare function writeFixtures(path: string, fixtures: FixtureSet | ScriptBuilder): void;
|
|
8
|
+
/**
|
|
9
|
+
* Load a FixtureSet from a JSON file.
|
|
10
|
+
* Accepts `{ "fixtures": [...] }` (wrapped) or a bare array.
|
|
11
|
+
* Throws a clear error if the file is missing or the JSON is not a fixture set.
|
|
12
|
+
*/
|
|
13
|
+
export declare function loadFixtures(path: string): FixtureSet;
|
|
14
|
+
//# sourceMappingURL=fixture-file.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixture-file.d.ts","sourceRoot":"","sources":["../src/fixture-file.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAQrE;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,GAAG,aAAa,GAAG,IAAI,CAItF;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CA+BrD"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
/** Normalise a ScriptBuilder or bare FixtureSet to a FixtureSet. */
|
|
4
|
+
function toFixtureSet(f) {
|
|
5
|
+
if (Array.isArray(f))
|
|
6
|
+
return f;
|
|
7
|
+
return f.build();
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Write a FixtureSet (or ScriptBuilder) to a JSON file.
|
|
11
|
+
* The file is written as `{ "fixtures": [...] }` (pretty-printed, 2-space indent).
|
|
12
|
+
* Parent directories are created automatically.
|
|
13
|
+
*/
|
|
14
|
+
export function writeFixtures(path, fixtures) {
|
|
15
|
+
const set = toFixtureSet(fixtures);
|
|
16
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
17
|
+
writeFileSync(path, JSON.stringify({ fixtures: set }, null, 2), "utf-8");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Load a FixtureSet from a JSON file.
|
|
21
|
+
* Accepts `{ "fixtures": [...] }` (wrapped) or a bare array.
|
|
22
|
+
* Throws a clear error if the file is missing or the JSON is not a fixture set.
|
|
23
|
+
*/
|
|
24
|
+
export function loadFixtures(path) {
|
|
25
|
+
let raw;
|
|
26
|
+
try {
|
|
27
|
+
raw = readFileSync(path, "utf-8");
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new Error(`fixture file not found: ${path}`);
|
|
31
|
+
}
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = JSON.parse(raw);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new Error(`fixture file contains invalid JSON: ${path}`);
|
|
38
|
+
}
|
|
39
|
+
if (Array.isArray(parsed)) {
|
|
40
|
+
return parsed;
|
|
41
|
+
}
|
|
42
|
+
if (parsed !== null &&
|
|
43
|
+
typeof parsed === "object" &&
|
|
44
|
+
"fixtures" in parsed &&
|
|
45
|
+
Array.isArray(parsed.fixtures)) {
|
|
46
|
+
return parsed.fixtures;
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`fixture file does not contain a fixture set (expected array or { fixtures: [...] }): ${path}`);
|
|
49
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type B4ResumeEntry } from "@b4run/cli/runtime";
|
|
2
|
+
import type { FixtureSet, ScriptBuilder } from "./fixture-builder.js";
|
|
3
|
+
import { type AgentRunResult } from "./run-result.js";
|
|
4
|
+
export interface AgentHarnessOptions {
|
|
5
|
+
readonly appRoot: string;
|
|
6
|
+
readonly route: string;
|
|
7
|
+
readonly fixtures?: FixtureSet;
|
|
8
|
+
/**
|
|
9
|
+
* When true, proxy all LLM requests through a real upstream (OPENAI_API_KEY
|
|
10
|
+
* must be set). Requires OPENAI_API_KEY to be present in the environment.
|
|
11
|
+
*/
|
|
12
|
+
readonly live?: boolean;
|
|
13
|
+
/** Capture real-model traffic for getRecordedFixtures(). Proxies to recordUpstream. */
|
|
14
|
+
readonly record?: boolean;
|
|
15
|
+
/** Upstream base URL for record mode (no /v1 suffix). Default https://api.openai.com. */
|
|
16
|
+
readonly recordUpstream?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface AgentHarness {
|
|
19
|
+
readonly baseUrl: string;
|
|
20
|
+
run(opts: {
|
|
21
|
+
input: string;
|
|
22
|
+
fixtures?: FixtureSet | ScriptBuilder;
|
|
23
|
+
}): Promise<AgentRunResult>;
|
|
24
|
+
resume(opts: {
|
|
25
|
+
resume: readonly B4ResumeEntry[];
|
|
26
|
+
fixtures?: FixtureSet | ScriptBuilder;
|
|
27
|
+
}): Promise<AgentRunResult>;
|
|
28
|
+
reset(): void;
|
|
29
|
+
close(): Promise<void>;
|
|
30
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
31
|
+
/** Fixtures captured from the most recent run() (record mode only); re-keyed for replay. */
|
|
32
|
+
getRecordedFixtures(): FixtureSet;
|
|
33
|
+
}
|
|
34
|
+
export declare function createAgentHarness(options: AgentHarnessOptions): Promise<AgentHarness>;
|
|
35
|
+
//# sourceMappingURL=harness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,aAAa,EASnB,MAAM,oBAAoB,CAAA;AAI3B,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAErE,OAAO,EAAE,KAAK,cAAc,EAAoB,MAAM,iBAAiB,CAAA;AA0BvE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAA;IAC9B;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;IACvB,uFAAuF;IACvF,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB,yFAAyF;IACzF,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CACjC;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,GAAG,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,UAAU,GAAG,aAAa,CAAA;KAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;IAC5F,MAAM,CAAC,IAAI,EAAE;QACX,MAAM,EAAE,SAAS,aAAa,EAAE,CAAA;QAChC,QAAQ,CAAC,EAAE,UAAU,GAAG,aAAa,CAAA;KACtC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;IAC3B,KAAK,IAAI,IAAI,CAAA;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtC,4FAA4F;IAC5F,mBAAmB,IAAI,UAAU,CAAA;CAClC;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,YAAY,CAAC,CA2L5F"}
|
package/dist/harness.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { __resetMaterializedAgentsForTests, __resetRouteLoadCachesForTests, createRuntimeRegistry, readPendingInterrupts, resolveCheckpointer, resolvePendingResume, resolveSandboxManager, runTypegen, streamResolvedRoute, } from "@b4run/cli/runtime";
|
|
3
|
+
import { __clearB4ConfigCacheForTests } from "@b4run/core";
|
|
4
|
+
import { discoverRoutes } from "@b4run/core/node";
|
|
5
|
+
import { createAimock } from "./aimock-runner.js";
|
|
6
|
+
import { recordingsToFixtures } from "./record-fixtures.js";
|
|
7
|
+
import { collectRunResult } from "./run-result.js";
|
|
8
|
+
/** Normalise a ScriptBuilder or bare FixtureSet to a FixtureSet. */
|
|
9
|
+
function toFixtureSet(f) {
|
|
10
|
+
if (Array.isArray(f))
|
|
11
|
+
return f;
|
|
12
|
+
return f.build();
|
|
13
|
+
}
|
|
14
|
+
/** Extract the system prompt from a slice of aimock journal requests. */
|
|
15
|
+
function systemPromptFromRequests(reqs) {
|
|
16
|
+
for (const req of reqs) {
|
|
17
|
+
const messages = req.body?.messages ?? [];
|
|
18
|
+
for (const m of messages) {
|
|
19
|
+
// OpenAI chat models use role "system"; gpt-5 / reasoning models send the
|
|
20
|
+
// system prompt under role "developer". Accept either so the captured
|
|
21
|
+
// systemPrompt is populated regardless of which model the route uses.
|
|
22
|
+
if ((m.role === "system" || m.role === "developer") && typeof m.content === "string") {
|
|
23
|
+
return m.content;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
export async function createAgentHarness(options) {
|
|
30
|
+
const live = options.live ?? false;
|
|
31
|
+
const record = options.record ?? false;
|
|
32
|
+
// Guard: live mode requires a real API key before doing anything else.
|
|
33
|
+
if (live && !process.env.OPENAI_API_KEY) {
|
|
34
|
+
throw new Error("createAgentHarness({ live: true }) requires OPENAI_API_KEY to be set in the environment");
|
|
35
|
+
}
|
|
36
|
+
const prevBaseUrl = process.env.OPENAI_BASE_URL;
|
|
37
|
+
const prevKey = process.env.OPENAI_API_KEY;
|
|
38
|
+
// Start aimock once — port (and thus the cached agent's baseURL) stays stable for the harness lifetime.
|
|
39
|
+
// In live mode: proxy all requests through to the real OpenAI upstream.
|
|
40
|
+
// In record mode: proxy to recordUpstream and capture responses for getRecordedFixtures().
|
|
41
|
+
const aimock = live
|
|
42
|
+
? await createAimock({ fixtures: [], proxy: { openai: "https://api.openai.com" } })
|
|
43
|
+
: record
|
|
44
|
+
? await createAimock({
|
|
45
|
+
fixtures: [],
|
|
46
|
+
proxy: { openai: options.recordUpstream ?? "https://api.openai.com" },
|
|
47
|
+
record: true,
|
|
48
|
+
})
|
|
49
|
+
: await createAimock({ fixtures: options.fixtures ?? [] });
|
|
50
|
+
process.env.OPENAI_BASE_URL = aimock.baseUrl;
|
|
51
|
+
// Only inject a dummy key in mock mode; live and record modes use real or no key.
|
|
52
|
+
if (!live && !record) {
|
|
53
|
+
process.env.OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? "test-not-used";
|
|
54
|
+
}
|
|
55
|
+
// All construction steps after aimock starts are wrapped so we can clean up on failure.
|
|
56
|
+
let resolved;
|
|
57
|
+
let sandboxManager;
|
|
58
|
+
try {
|
|
59
|
+
// typegen once → generated tool schemas exist (dev-boot fidelity)
|
|
60
|
+
const manifest = await discoverRoutes({ appRoot: options.appRoot });
|
|
61
|
+
await runTypegen({ appRoot: options.appRoot, manifest });
|
|
62
|
+
const registry = await createRuntimeRegistry(options.appRoot);
|
|
63
|
+
resolved = registry.lookup(options.route);
|
|
64
|
+
if (!resolved) {
|
|
65
|
+
throw new Error(`createAgentHarness: unknown route "${options.route}"`);
|
|
66
|
+
}
|
|
67
|
+
sandboxManager = await resolveSandboxManager(options.appRoot);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
// Unified cleanup: stop aimock and restore env vars before re-throwing.
|
|
71
|
+
if (sandboxManager)
|
|
72
|
+
await sandboxManager.releaseAll();
|
|
73
|
+
await aimock.close();
|
|
74
|
+
if (prevBaseUrl === undefined)
|
|
75
|
+
delete process.env.OPENAI_BASE_URL;
|
|
76
|
+
else
|
|
77
|
+
process.env.OPENAI_BASE_URL = prevBaseUrl;
|
|
78
|
+
if (prevKey === undefined)
|
|
79
|
+
delete process.env.OPENAI_API_KEY;
|
|
80
|
+
else
|
|
81
|
+
process.env.OPENAI_API_KEY = prevKey;
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
const baseUrl = aimock.baseUrl;
|
|
85
|
+
let threadId = randomUUID();
|
|
86
|
+
let closed = false;
|
|
87
|
+
let lastRunJournalStart = 0;
|
|
88
|
+
let lastRunFixtureStart = 0;
|
|
89
|
+
/** Core drive helper — runs a single turn and merges systemPrompt. */
|
|
90
|
+
async function drive(driveOpts) {
|
|
91
|
+
// In live and record modes, fixtures are proxied to the upstream — skip registration.
|
|
92
|
+
if (!live && !record && driveOpts.fixtures) {
|
|
93
|
+
const newFixtures = toFixtureSet(driveOpts.fixtures);
|
|
94
|
+
if (newFixtures.length > 0) {
|
|
95
|
+
aimock.addFixtures(newFixtures);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const snapshotLen = aimock.getRequests().length;
|
|
99
|
+
lastRunJournalStart = snapshotLen;
|
|
100
|
+
lastRunFixtureStart = aimock.getFixtureCount();
|
|
101
|
+
const r = resolved;
|
|
102
|
+
if (!r) {
|
|
103
|
+
throw new Error(`createAgentHarness: unknown route "${options.route}"`);
|
|
104
|
+
}
|
|
105
|
+
let resolvedResume;
|
|
106
|
+
if (driveOpts.resume) {
|
|
107
|
+
const checkpointer = await resolveCheckpointer(options.appRoot);
|
|
108
|
+
const pending = await readPendingInterrupts(checkpointer, threadId);
|
|
109
|
+
if (!pending) {
|
|
110
|
+
throw new Error(`createAgentHarness: no checkpoint found for thread "${threadId}"`);
|
|
111
|
+
}
|
|
112
|
+
const resolution = resolvePendingResume(driveOpts.resume, pending);
|
|
113
|
+
if (!resolution.ok) {
|
|
114
|
+
throw new Error(`createAgentHarness: ${resolution.message} (${resolution.code})`);
|
|
115
|
+
}
|
|
116
|
+
if (resolution.mode !== "resume") {
|
|
117
|
+
throw new Error("createAgentHarness: no pending interrupts to resume");
|
|
118
|
+
}
|
|
119
|
+
resolvedResume = resolution.resume;
|
|
120
|
+
}
|
|
121
|
+
const streamArgs = {
|
|
122
|
+
appRoot: options.appRoot,
|
|
123
|
+
input: driveOpts.input !== undefined
|
|
124
|
+
? { messages: [{ role: "user", content: driveOpts.input }] }
|
|
125
|
+
: { messages: [] },
|
|
126
|
+
routeFile: r.routeFile,
|
|
127
|
+
routeId: r.routeId,
|
|
128
|
+
routePath: r.routePath,
|
|
129
|
+
threadId,
|
|
130
|
+
...(sandboxManager ? { sandboxManager } : {}),
|
|
131
|
+
...(resolvedResume ? { resume: resolvedResume } : {}),
|
|
132
|
+
};
|
|
133
|
+
const stream = streamResolvedRoute(streamArgs);
|
|
134
|
+
const result = await collectRunResult(stream, threadId);
|
|
135
|
+
const turnReqs = aimock.getRequests().slice(snapshotLen);
|
|
136
|
+
return { ...result, systemPrompt: systemPromptFromRequests(turnReqs) };
|
|
137
|
+
}
|
|
138
|
+
const harness = {
|
|
139
|
+
get baseUrl() {
|
|
140
|
+
return baseUrl;
|
|
141
|
+
},
|
|
142
|
+
async run(runOpts) {
|
|
143
|
+
return drive({
|
|
144
|
+
input: runOpts.input,
|
|
145
|
+
...(runOpts.fixtures !== undefined ? { fixtures: runOpts.fixtures } : {}),
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
async resume(resumeOpts) {
|
|
149
|
+
return drive({
|
|
150
|
+
resume: resumeOpts.resume,
|
|
151
|
+
...(resumeOpts.fixtures !== undefined ? { fixtures: resumeOpts.fixtures } : {}),
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
reset() {
|
|
155
|
+
threadId = randomUUID();
|
|
156
|
+
// Start each scenario from a clean fixture set. Fixtures are registered
|
|
157
|
+
// additively per run() and findFixture is first-match-in-array-order, so
|
|
158
|
+
// without this a loosely-matched fixture from a prior scenario (e.g. a raw
|
|
159
|
+
// FixtureSet with no `userMessage`) would shadow the next run's turn-0
|
|
160
|
+
// call. Live mode proxies to the real upstream and registers no fixtures.
|
|
161
|
+
if (!live) {
|
|
162
|
+
aimock.clearFixtures();
|
|
163
|
+
if (options.fixtures && options.fixtures.length > 0) {
|
|
164
|
+
aimock.addFixtures(options.fixtures);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
getRecordedFixtures() {
|
|
169
|
+
return recordingsToFixtures(aimock.getRecordingsSince(lastRunJournalStart, lastRunFixtureStart));
|
|
170
|
+
},
|
|
171
|
+
async close() {
|
|
172
|
+
if (closed)
|
|
173
|
+
return;
|
|
174
|
+
closed = true;
|
|
175
|
+
if (sandboxManager)
|
|
176
|
+
await sandboxManager.releaseAll();
|
|
177
|
+
await aimock.close();
|
|
178
|
+
// restore env to avoid cross-test bleed
|
|
179
|
+
if (prevBaseUrl === undefined)
|
|
180
|
+
delete process.env.OPENAI_BASE_URL;
|
|
181
|
+
else
|
|
182
|
+
process.env.OPENAI_BASE_URL = prevBaseUrl;
|
|
183
|
+
if (prevKey === undefined)
|
|
184
|
+
delete process.env.OPENAI_API_KEY;
|
|
185
|
+
else
|
|
186
|
+
process.env.OPENAI_API_KEY = prevKey;
|
|
187
|
+
// Reset the per-descriptor LLM cache so the next harness constructs a
|
|
188
|
+
// fresh ChatOpenAI instance pointing to its own aimock URL. Without this,
|
|
189
|
+
// successive harnesses that share the same B4Agent descriptor object
|
|
190
|
+
// (ESM module cache returns the same export) would reuse an LLM already
|
|
191
|
+
// bound to the previous (stopped) aimock server.
|
|
192
|
+
__resetMaterializedAgentsForTests();
|
|
193
|
+
// loadB4Config is now memoized per appRoot for the process lifetime
|
|
194
|
+
// (perf(core): memoize loadB4Config per appRoot). Test suites that
|
|
195
|
+
// rewrite a fixture app's b4.config.ts and drive it again through a
|
|
196
|
+
// fresh harness in the same process (e.g. switching a memory backend
|
|
197
|
+
// via env + config) need that mutation to actually take effect — clear
|
|
198
|
+
// the memo here so the next createAgentHarness call reloads from disk.
|
|
199
|
+
__clearB4ConfigCacheForTests();
|
|
200
|
+
// Same reasoning for the per-route module cache and per-appRoot route
|
|
201
|
+
// manifest memo (perf(cli): load route modules/tools/state once per
|
|
202
|
+
// process): a fixture app mutated between harnesses (new tool file,
|
|
203
|
+
// added route) must be re-discovered by the next harness.
|
|
204
|
+
__resetRouteLoadCachesForTests();
|
|
205
|
+
},
|
|
206
|
+
[Symbol.asyncDispose]() {
|
|
207
|
+
return this.close();
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
return harness;
|
|
211
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ThreadAccessPolicy } from "@b4run/sdk";
|
|
2
|
+
export interface InjectResult {
|
|
3
|
+
readonly statusCode: number;
|
|
4
|
+
readonly body: string;
|
|
5
|
+
readonly headers: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
export interface AgentProtocolInjector {
|
|
8
|
+
inject(opts: {
|
|
9
|
+
method: string;
|
|
10
|
+
url: string;
|
|
11
|
+
payload?: unknown;
|
|
12
|
+
headers?: Record<string, string>;
|
|
13
|
+
}): Promise<InjectResult>;
|
|
14
|
+
close(): Promise<void>;
|
|
15
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export declare function createAgentProtocolInjector(options: {
|
|
18
|
+
appRoot: string;
|
|
19
|
+
/** Injected rather than probed from disk, so a test needs no policy file. */
|
|
20
|
+
threadAccess?: ThreadAccessPolicy;
|
|
21
|
+
}): Promise<AgentProtocolInjector>;
|
|
22
|
+
//# sourceMappingURL=http-inject.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-inject.d.ts","sourceRoot":"","sources":["../src/http-inject.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAEpD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC1C;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,IAAI,EAAE;QACX,MAAM,EAAE,MAAM,CAAA;QACd,GAAG,EAAE,MAAM,CAAA;QACX,OAAO,CAAC,EAAE,OAAO,CAAA;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KACjC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IACzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvC;AAED,wBAAsB,2BAA2B,CAAC,OAAO,EAAE;IACzD,OAAO,EAAE,MAAM,CAAA;IACf,6EAA6E;IAC7E,YAAY,CAAC,EAAE,kBAAkB,CAAA;CAClC,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAsDjC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createRuntimeFetchHandler } from "@b4run/cli/runtime";
|
|
2
|
+
import { __clearB4ConfigCacheForTests } from "@b4run/core";
|
|
3
|
+
export async function createAgentProtocolInjector(options) {
|
|
4
|
+
const core = await createRuntimeFetchHandler({
|
|
5
|
+
appRoot: options.appRoot,
|
|
6
|
+
...(options.threadAccess ? { threadAccess: options.threadAccess } : {}),
|
|
7
|
+
});
|
|
8
|
+
const injector = {
|
|
9
|
+
async inject(opts) {
|
|
10
|
+
const requestHeaders = new Headers({
|
|
11
|
+
"content-type": "application/json",
|
|
12
|
+
...opts.headers,
|
|
13
|
+
});
|
|
14
|
+
const request = new Request(new URL(opts.url, "http://localhost"), {
|
|
15
|
+
headers: requestHeaders,
|
|
16
|
+
method: opts.method,
|
|
17
|
+
...(opts.payload !== undefined ? { body: JSON.stringify(opts.payload) } : {}),
|
|
18
|
+
});
|
|
19
|
+
const response = await core.fetch(request);
|
|
20
|
+
// Read the (possibly SSE) body to completion — the harness's `inject`
|
|
21
|
+
// contract is synchronous-style, matching the pre-refactor
|
|
22
|
+
// light-my-request behavior of returning the fully buffered body.
|
|
23
|
+
const body = await response.text();
|
|
24
|
+
const headers = {};
|
|
25
|
+
for (const [key, value] of response.headers) {
|
|
26
|
+
// `getSetCookie` preserves multiple Set-Cookie headers, which
|
|
27
|
+
// `Headers` iteration would otherwise join into one comma-separated
|
|
28
|
+
// value.
|
|
29
|
+
if (key.toLowerCase() === "set-cookie")
|
|
30
|
+
continue;
|
|
31
|
+
headers[key] = value;
|
|
32
|
+
}
|
|
33
|
+
const setCookie = response.headers.getSetCookie?.() ?? [];
|
|
34
|
+
if (setCookie.length > 0)
|
|
35
|
+
headers["set-cookie"] = setCookie;
|
|
36
|
+
return {
|
|
37
|
+
body,
|
|
38
|
+
headers,
|
|
39
|
+
statusCode: response.status,
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
async close() {
|
|
43
|
+
await core.close();
|
|
44
|
+
// See harness.ts's close() for why: loadB4Config is memoized per
|
|
45
|
+
// appRoot for the process lifetime, so a fixture app's b4.config.ts
|
|
46
|
+
// mutated and re-served through a fresh injector in the same process
|
|
47
|
+
// needs the memo cleared to pick up the change.
|
|
48
|
+
__clearB4ConfigCacheForTests();
|
|
49
|
+
},
|
|
50
|
+
[Symbol.asyncDispose]() {
|
|
51
|
+
return this.close();
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
return injector;
|
|
55
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { type Aimock, createAimock } from "./aimock-runner.js";
|
|
2
|
+
export { runCheckpointerConformance } from "./checkpointer-conformance.js";
|
|
3
|
+
export { fakeEmbedder } from "./fake-embedder.js";
|
|
4
|
+
export { type AimockFixture, type AimockResponse, type AimockToolCall, type FixtureSet, type ScriptBuilder, script, } from "./fixture-builder.js";
|
|
5
|
+
export { loadFixtures, writeFixtures } from "./fixture-file.js";
|
|
6
|
+
export { type AgentHarness, type AgentHarnessOptions, createAgentHarness } from "./harness.js";
|
|
7
|
+
export { type AgentProtocolInjector, createAgentProtocolInjector, type InjectResult, } from "./http-inject.js";
|
|
8
|
+
export { expectFinalMessage, expectInterrupt, expectNoInterrupt, expectNoToolErrors, expectOffloaded, expectPlan, expectState, expectStreamedTokens, expectSubagent, expectSystemPrompt, expectToolCalled, expectToolSequence, type InterruptInfo, type SubagentEvent, type SubagentRun, type Todo, } from "./matchers.js";
|
|
9
|
+
export { seedMemory } from "./memory.js";
|
|
10
|
+
export { runMemoryStoreConformance } from "./memory-conformance.js";
|
|
11
|
+
export { createMiddlewareHarness, type MiddlewareHarness, } from "./middleware-harness.js";
|
|
12
|
+
export { type PermissionsStoreInit, runPermissionsStoreConformance, } from "./permissions-conformance.js";
|
|
13
|
+
export { type RecordOptions, record } from "./record.js";
|
|
14
|
+
export { type AgentRunResult, collectRunResult, deriveToolResults, type ObservedToolCall, type ObservedToolResult, } from "./run-result.js";
|
|
15
|
+
export { createSubprocessApp, type SubprocessApp } from "./subprocess.js";
|
|
16
|
+
export { createThreadAccessHarness, type ThreadAccessCheckSpec, type ThreadAccessHarness, } from "./thread-access-harness.js";
|
|
17
|
+
export { runThreadsStoreConformance } from "./threads-conformance.js";
|
|
18
|
+
export { createToolHarness, type ToolHarness, type ToolHarnessOptions, } from "./tool-harness.js";
|
|
19
|
+
export { createWorkspaceHarness, type WorkspaceHarness, type WorkspaceHarnessOptions, } from "./workspace-harness.js";
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAC9D,OAAO,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAA;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,MAAM,GACP,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAC/D,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAC9F,OAAO,EACL,KAAK,qBAAqB,EAC1B,2BAA2B,EAC3B,KAAK,YAAY,GAClB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,UAAU,EACV,WAAW,EACX,oBAAoB,EACpB,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,IAAI,GACV,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAA;AACnE,OAAO,EACL,uBAAuB,EACvB,KAAK,iBAAiB,GACvB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,KAAK,oBAAoB,EACzB,8BAA8B,GAC/B,MAAM,8BAA8B,CAAA;AACrC,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACxD,OAAO,EACL,KAAK,cAAc,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,mBAAmB,EAAE,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAA;AACzE,OAAO,EACL,yBAAyB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,GACzB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAA;AACrE,OAAO,EACL,iBAAiB,EACjB,KAAK,WAAW,EAChB,KAAK,kBAAkB,GACxB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EACL,sBAAsB,EACtB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,GAC7B,MAAM,wBAAwB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { createAimock } from "./aimock-runner.js";
|
|
2
|
+
export { runCheckpointerConformance } from "./checkpointer-conformance.js";
|
|
3
|
+
export { fakeEmbedder } from "./fake-embedder.js";
|
|
4
|
+
export { script, } from "./fixture-builder.js";
|
|
5
|
+
export { loadFixtures, writeFixtures } from "./fixture-file.js";
|
|
6
|
+
export { createAgentHarness } from "./harness.js";
|
|
7
|
+
export { createAgentProtocolInjector, } from "./http-inject.js";
|
|
8
|
+
export { expectFinalMessage, expectInterrupt, expectNoInterrupt, expectNoToolErrors, expectOffloaded, expectPlan, expectState, expectStreamedTokens, expectSubagent, expectSystemPrompt, expectToolCalled, expectToolSequence, } from "./matchers.js";
|
|
9
|
+
export { seedMemory } from "./memory.js";
|
|
10
|
+
export { runMemoryStoreConformance } from "./memory-conformance.js";
|
|
11
|
+
export { createMiddlewareHarness, } from "./middleware-harness.js";
|
|
12
|
+
export { runPermissionsStoreConformance, } from "./permissions-conformance.js";
|
|
13
|
+
export { record } from "./record.js";
|
|
14
|
+
export { collectRunResult, deriveToolResults, } from "./run-result.js";
|
|
15
|
+
export { createSubprocessApp } from "./subprocess.js";
|
|
16
|
+
export { createThreadAccessHarness, } from "./thread-access-harness.js";
|
|
17
|
+
export { runThreadsStoreConformance } from "./threads-conformance.js";
|
|
18
|
+
export { createToolHarness, } from "./tool-harness.js";
|
|
19
|
+
export { createWorkspaceHarness, } from "./workspace-harness.js";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { AgentRunResult, InterruptInfo, SubagentEvent, SubagentRun, Todo } from "./run-result.js";
|
|
2
|
+
export type { InterruptInfo, SubagentEvent, SubagentRun, Todo };
|
|
3
|
+
export declare function expectToolCalled(run: AgentRunResult, name: string): {
|
|
4
|
+
withArgs(partial: Record<string, unknown>): void;
|
|
5
|
+
times(n: number): void;
|
|
6
|
+
never(): never;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Assert the named tools were called IN ORDER. By default a subsequence (other
|
|
10
|
+
* tool calls may appear between them); `{ strict: true }` requires them to be
|
|
11
|
+
* contiguous.
|
|
12
|
+
*/
|
|
13
|
+
export declare function expectToolSequence(run: AgentRunResult, names: readonly string[], opts?: {
|
|
14
|
+
readonly strict?: boolean;
|
|
15
|
+
}): void;
|
|
16
|
+
export declare function expectFinalMessage(run: AgentRunResult): {
|
|
17
|
+
toContain(s: string): void;
|
|
18
|
+
toMatch(re: RegExp): void;
|
|
19
|
+
toEqual(s: string): void;
|
|
20
|
+
};
|
|
21
|
+
export declare function expectStreamedTokens(run: AgentRunResult): void;
|
|
22
|
+
export declare function expectOffloaded(run: AgentRunResult, toolName: string): void;
|
|
23
|
+
export declare function expectInterrupt(run: AgentRunResult): {
|
|
24
|
+
ofKind(kind: string): /*elided*/ any;
|
|
25
|
+
withDetail(partial: Record<string, unknown>): /*elided*/ any;
|
|
26
|
+
};
|
|
27
|
+
export declare function expectNoInterrupt(run: AgentRunResult): void;
|
|
28
|
+
export declare function expectSubagent(run: AgentRunResult, name: string): {
|
|
29
|
+
called(): /*elided*/ any;
|
|
30
|
+
calledTool(toolName: string): /*elided*/ any;
|
|
31
|
+
finalMessageContains(text: string): /*elided*/ any;
|
|
32
|
+
};
|
|
33
|
+
export declare function expectPlan(run: AgentRunResult): {
|
|
34
|
+
toHaveLength(n: number): void;
|
|
35
|
+
toHaveTodo(content: string): void;
|
|
36
|
+
toHaveStatus(content: string, status: string): void;
|
|
37
|
+
};
|
|
38
|
+
export declare function expectSystemPrompt(run: AgentRunResult): {
|
|
39
|
+
toContain(text: string): void;
|
|
40
|
+
toMatch(re: RegExp): void;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Assert no tool returned an error result. A HITL permission interrupt is NOT a
|
|
44
|
+
* tool error — it produces no ToolMessage, so it never appears in toolResults.
|
|
45
|
+
*/
|
|
46
|
+
export declare function expectNoToolErrors(run: AgentRunResult): void;
|
|
47
|
+
export declare function expectState(run: AgentRunResult): {
|
|
48
|
+
messages: {
|
|
49
|
+
toHaveLength(n: number): void;
|
|
50
|
+
};
|
|
51
|
+
field(name: string): {
|
|
52
|
+
toBeTruthy(): void;
|
|
53
|
+
toEqual(expected: unknown): void;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=matchers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"matchers.d.ts","sourceRoot":"","sources":["../src/matchers.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACb,aAAa,EACb,WAAW,EACX,IAAI,EACL,MAAM,iBAAiB,CAAA;AAExB,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,IAAI,EAAE,CAAA;AAmC/D,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM;IAQ9D,QAAQ,UAAU,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAOzC,KAAK,IAAI,MAAM;IAGf,KAAK;EAIR;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,cAAc,EACnB,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,IAAI,CAAC,EAAE;IAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GACnC,IAAI,CAWN;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,cAAc;IAElD,SAAS,IAAI,MAAM;IAMnB,OAAO,KAAK,MAAM;IAIlB,OAAO,IAAI,MAAM;EAKpB;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,IAAI,CAE9D;AAkCD,wBAAgB,eAAe,CAAC,GAAG,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAQ3E;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,cAAc;iBAKlC,MAAM;wBAQC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;EAa9C;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,IAAI,CAM3D;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM;;yBAWvC,MAAM;+BAUA,MAAM;EAYpC;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,cAAc;IAE1C,YAAY,IAAI,MAAM;IAGtB,UAAU,UAAU,MAAM;IAO1B,YAAY,UAAU,MAAM,UAAU,MAAM;EAY/C;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,cAAc;IAElD,SAAS,OAAO,MAAM;IAOtB,OAAO,KAAK,MAAM;EAOrB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,cAAc,GAAG,IAAI,CAY5D;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,cAAc;IAG3C,QAAQ;QACN,YAAY,IAAI,MAAM;;IAKxB,KAAK,OAAO,MAAM;QAGd,UAAU;QAGV,OAAO,WAAW,OAAO;;EAUhC"}
|