@b4run/testing 0.12.0 → 0.13.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/harness.d.ts CHANGED
@@ -36,6 +36,15 @@ export interface AgentHarnessOptions {
36
36
  readonly record?: boolean;
37
37
  /** Upstream base URL for record mode (no /v1 suffix). Default https://api.openai.com. */
38
38
  readonly recordUpstream?: string;
39
+ /**
40
+ * A JSON Schema the root model's final message must match — the value a
41
+ * Hashbrown client sends as `hashbrown.responseSchema` on every AG-UI run.
42
+ * Bound on every turn exactly as the server binds it, so a scripted, live or
43
+ * recorded run sends the model the same request production does. Only an
44
+ * `agent` route on a provider that supports it can take one; any other
45
+ * route fails the run rather than running unconstrained.
46
+ */
47
+ readonly responseSchema?: Readonly<Record<string, unknown>>;
39
48
  }
40
49
  export interface AgentHarness {
41
50
  readonly baseUrl: string;
@@ -1 +1 @@
1
- {"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,aAAa,EASnB,MAAM,oBAAoB,CAAA;AAG3B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAErE,OAAO,EAAE,KAAK,cAAc,EAAoB,MAAM,iBAAiB,CAAA;AA0BvE,qFAAqF;AACrF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,oEAAoE;IACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,aAAa,EAAE,CAAA;CAC3C;AAED;;;;GAIG;AACH,MAAM,MAAM,6BAA6B,GACrC,WAAW,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,GACxC,CAAC,CACC,GAAG,EAAE,mBAAmB,KAEtB,WAAW,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,GACxC,SAAS,GACT,OAAO,CAAC,WAAW,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAA;AAEtE,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;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,6BAA6B,CAAA;IAC1D;;;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;AAYD,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,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxE,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,CA0M5F"}
1
+ {"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,aAAa,EAUnB,MAAM,oBAAoB,CAAA;AAG3B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAErE,OAAO,EAAE,KAAK,cAAc,EAAoB,MAAM,iBAAiB,CAAA;AA0BvE,qFAAqF;AACrF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,oEAAoE;IACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,aAAa,EAAE,CAAA;CAC3C;AAED;;;;GAIG;AACH,MAAM,MAAM,6BAA6B,GACrC,WAAW,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,GACxC,CAAC,CACC,GAAG,EAAE,mBAAmB,KAEtB,WAAW,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,GACxC,SAAS,GACT,OAAO,CAAC,WAAW,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAA;AAEtE,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;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,6BAA6B,CAAA;IAC1D;;;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;IAChC;;;;;;;OAOG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CAC5D;AAYD,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,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxE,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,CA2N5F"}
package/dist/harness.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { __resetMaterializedAgentsForTests, __resetRouteLoadCachesForTests, createRuntimeRegistry, readPendingInterrupts, resolveCheckpointer, resolvePendingResume, resolveSandboxManager, runTypegen, streamResolvedRoute, } from "@b4run/cli/runtime";
2
+ import { __resetMaterializedAgentsForTests, __resetRouteLoadCachesForTests, createRuntimeRegistry, readPendingInterrupts, readResponseFormat, resolveCheckpointer, resolvePendingResume, resolveSandboxManager, runTypegen, streamResolvedRoute, } from "@b4run/cli/runtime";
3
3
  import { __clearB4ConfigCacheForTests } from "@b4run/core";
4
4
  import { discoverRoutes } from "@b4run/core/node";
5
5
  import { createAimock } from "./aimock-runner.js";
@@ -37,6 +37,18 @@ async function resolveMiddlewareContext(option, run) {
37
37
  export async function createAgentHarness(options) {
38
38
  const live = options.live ?? false;
39
39
  const record = options.record ?? false;
40
+ const responseSchema = options.responseSchema;
41
+ if (responseSchema !== undefined &&
42
+ (typeof responseSchema !== "object" || responseSchema === null || Array.isArray(responseSchema))) {
43
+ throw new Error("createAgentHarness: `responseSchema` must be a JSON Schema object");
44
+ }
45
+ // Read through the server's own parser so the bound format — including the
46
+ // provider-facing schema name — is the one an AG-UI run would carry.
47
+ const responseFormatResult = readResponseFormat(responseSchema !== undefined ? { hashbrown: { responseSchema } } : undefined);
48
+ if (!responseFormatResult.ok) {
49
+ throw new Error(`createAgentHarness: ${responseFormatResult.message}`);
50
+ }
51
+ const responseFormat = responseFormatResult.responseFormat;
40
52
  // Guard: live mode requires a real API key before doing anything else.
41
53
  if (live && !process.env.OPENAI_API_KEY) {
42
54
  throw new Error("createAgentHarness({ live: true }) requires OPENAI_API_KEY to be set in the environment");
@@ -144,6 +156,7 @@ export async function createAgentHarness(options) {
144
156
  ...(sandboxManager ? { sandboxManager } : {}),
145
157
  ...(resolvedResume ? { resume: resolvedResume } : {}),
146
158
  ...(middlewareContext !== undefined ? { middlewareContext } : {}),
159
+ ...(responseFormat !== undefined ? { responseFormat } : {}),
147
160
  };
148
161
  const stream = streamResolvedRoute(streamArgs);
149
162
  const result = await collectRunResult(stream, threadId);
@@ -1 +1 @@
1
- {"version":3,"file":"record-fixtures.d.ts","sourceRoot":"","sources":["../src/record-fixtures.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,cAAc,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAErF,2FAA2F;AAC3F,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE;QAChB,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC;YAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;SAAE,CAAC,CAAA;KACxF,CAAA;IACD,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAA;CAClC;AAaD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,SAAS,SAAS,EAAE,GAAG,UAAU,CAYjF"}
1
+ {"version":3,"file":"record-fixtures.d.ts","sourceRoot":"","sources":["../src/record-fixtures.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAiB,cAAc,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAErF,2FAA2F;AAC3F,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE;QAChB,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC;YAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;YACrB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;SAC1B,CAAC,CAAA;KACH,CAAA;IACD,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAA;CAClC;AAaD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,SAAS,SAAS,EAAE,GAAG,UAAU,CAcjF"}
@@ -1,3 +1,4 @@
1
+ import { LLMock } from "@copilotkit/aimock";
1
2
  function firstUserMessage(req) {
2
3
  for (const m of req.messages ?? []) {
3
4
  if (m.role === "user" && typeof m.content === "string")
@@ -17,7 +18,7 @@ function hasToolResult(req) {
17
18
  * thread); `hasToolResult` is whether a tool-role message is already present.
18
19
  */
19
20
  export function recordingsToFixtures(recordings) {
20
- return recordings.map((rec, turnIndex) => {
21
+ const fixtures = recordings.map((rec, turnIndex) => {
21
22
  const userMessage = firstUserMessage(rec.request);
22
23
  return {
23
24
  match: {
@@ -28,4 +29,62 @@ export function recordingsToFixtures(recordings) {
28
29
  response: rec.response,
29
30
  };
30
31
  });
32
+ assertReplayable(fixtures);
33
+ return fixtures;
34
+ }
35
+ /** aimock's own load-time error for one fixture, or undefined when it loads. */
36
+ function replayError(fixtures) {
37
+ try {
38
+ // The same call replay makes (see createAimock), on a mock that is never
39
+ // started — so a recording is held to exactly the rules its replay will be.
40
+ new LLMock({ port: 0 }).addFixturesFromJSON(fixtures);
41
+ return undefined;
42
+ }
43
+ catch (error) {
44
+ return error instanceof Error ? error.message : String(error);
45
+ }
46
+ }
47
+ /**
48
+ * Refuse a recording that replay would refuse (#778). aimock rejects a
49
+ * fixture whose `content` is an empty string, because its recorder writes one
50
+ * for several different upstream outcomes — a genuinely empty reply, a
51
+ * refusal it does not capture, a reply cut off at the token limit — and a
52
+ * tape cannot say which. Failing here, while the recording is being made,
53
+ * names the turn instead of leaving a tape that breaks on its next replay.
54
+ */
55
+ function assertReplayable(fixtures) {
56
+ const whole = replayError(fixtures);
57
+ if (whole === undefined)
58
+ return;
59
+ const turns = fixtures.flatMap((fixture, turnIndex) => {
60
+ const error = replayError([fixture]);
61
+ if (error === undefined)
62
+ return [];
63
+ const detail = describeValidationError(error);
64
+ const where = fixture.match.userMessage !== undefined
65
+ ? `turn ${turnIndex} of ${JSON.stringify(fixture.match.userMessage)}`
66
+ : `turn ${turnIndex}`;
67
+ return [` - ${where}: ${detail}`];
68
+ });
69
+ const lines = turns.length > 0 ? turns.join("\n") : ` - ${describeValidationError(whole)}`;
70
+ throw new Error(`Recorded fixtures would not replay: aimock rejects them on load.\n${lines}\n` +
71
+ "An empty assistant message usually means the model refused (a strict response " +
72
+ "schema reports refusals outside `content`), ran out of output tokens, or was " +
73
+ "given nothing left to do. If the run's answer is a tool call, export " +
74
+ "`returnDirect = true` from that tool so the run ends on its result instead of " +
75
+ "asking the model for a closing message.");
76
+ }
77
+ /** Reduce aimock's `Fixture validation failed: [...]` to its messages. */
78
+ function describeValidationError(error) {
79
+ const prefix = "Fixture validation failed: ";
80
+ if (!error.startsWith(prefix))
81
+ return error;
82
+ try {
83
+ const issues = JSON.parse(error.slice(prefix.length));
84
+ const messages = issues.map((issue) => issue.message).filter((m) => typeof m === "string");
85
+ return messages.length > 0 ? messages.join("; ") : error;
86
+ }
87
+ catch {
88
+ return error;
89
+ }
31
90
  }
@@ -1,4 +1,4 @@
1
- import type { ThreadAccessPolicy, ThreadAccessResult, ThreadAction, ThreadOperation, ThreadSubject } from "@b4run/sdk";
1
+ import type { ThreadAccessPolicy, ThreadAccessRequestedWorkspace, ThreadAccessResult, ThreadAction, ThreadOperation, ThreadSubject } from "@b4run/sdk";
2
2
  /** A partial `ThreadAccessRequest`: only `action` is required. */
3
3
  export interface ThreadAccessCheckSpec {
4
4
  readonly action: ThreadAction;
@@ -10,6 +10,13 @@ export interface ThreadAccessCheckSpec {
10
10
  readonly method?: string;
11
11
  readonly url?: string;
12
12
  readonly requestedMetadata?: Readonly<Record<string, unknown>>;
13
+ /**
14
+ * The workspace the request stages or chooses — see
15
+ * `ThreadAccessRequest.requestedWorkspace`: `{ sourceDigest }` for a
16
+ * `workspace.source.put`, the whole reference for a `thread.create` that
17
+ * names one.
18
+ */
19
+ readonly requestedWorkspace?: ThreadAccessRequestedWorkspace;
13
20
  /**
14
21
  * Whether this request continues a parked turn — see
15
22
  * `ThreadAccessRequest.resuming`. Defaults to `false`, the value every
@@ -1 +1 @@
1
- {"version":3,"file":"thread-access-harness.d.ts","sourceRoot":"","sources":["../src/thread-access-harness.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,kBAAkB,EAElB,kBAAkB,EAClB,YAAY,EACZ,eAAe,EACf,aAAa,EACd,MAAM,YAAY,CAAA;AAEnB,kEAAkE;AAClE,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAA;IAC7B,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,CAAC,EAAE,eAAe,CAAA;IACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAA;IAC/B,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACnD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,iBAAiB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IAC9D;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,KAAK,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAA;CAChE;AAeD;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE;IACjD,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAA;CACpC,GAAG,mBAAmB,CAmBtB"}
1
+ {"version":3,"file":"thread-access-harness.d.ts","sourceRoot":"","sources":["../src/thread-access-harness.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,kBAAkB,EAElB,8BAA8B,EAC9B,kBAAkB,EAClB,YAAY,EACZ,eAAe,EACf,aAAa,EACd,MAAM,YAAY,CAAA;AAEnB,kEAAkE;AAClE,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAA;IAC7B,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,CAAC,EAAE,eAAe,CAAA;IACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAA;IAC/B,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACnD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,iBAAiB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IAC9D;;;;;OAKG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,8BAA8B,CAAA;IAC5D;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,KAAK,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAA;CAChE;AAeD;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE;IACjD,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAA;CACpC,GAAG,mBAAmB,CAoBtB"}
@@ -30,6 +30,7 @@ export function createThreadAccessHarness(options) {
30
30
  method: spec.method ?? defaultMethod(spec.action),
31
31
  operation,
32
32
  requestedMetadata: spec.requestedMetadata,
33
+ requestedWorkspace: spec.requestedWorkspace,
33
34
  resuming: spec.resuming ?? false,
34
35
  thread: spec.thread,
35
36
  threadId: spec.threadId,