Skip to content

Commit f1809e7

Browse files
committed
test: mock the writer's model turns in-process
1 parent 91beee7 commit f1809e7

6 files changed

Lines changed: 247 additions & 2 deletions

File tree

‎apps/hub/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"./environment": "./src/environment.ts",
1111
"./migrate": "./src/migrate.ts",
1212
"./workflows/install-for-tenant": "./src/workflows/install-for-tenant.ts",
13+
"./workflows/entries/writer": "./src/workflows/entries/writer.ts",
1314
"./config/models": "./src/config/models.ts",
1415
"./settings/policy": "./src/settings/policy.ts"
1516
},

‎bun.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"format": "bun fmt --no-script",
3333
"format:check": "bun fmt --no-script --check",
3434
"typecheck": "bun check --no-script && bun check --no-script -p packages/client && bun check --no-script -p apps/web",
35-
"test": "bun --conditions=intx-src test ./apps/hub ./apps/web/src ./packages ./workflows ./test/integration",
35+
"test": "bun --conditions=intx-src test ./apps/hub ./apps/web/src ./packages ./workflows ./test/integration ./test/workflows",
3636
"test:e2e": "bun --env-file=.env.hub --conditions=intx-src test ./test/e2e",
3737
"vendor:verify": "bun scripts/verify-vendor.ts",
3838
"pins:verify": "bun scripts/verify-pins.ts",
@@ -82,6 +82,7 @@
8282
"@corbits-builder/writer": "workspace:*",
8383
"@emulators/core": "0.12.3",
8484
"@emulators/google": "0.12.3",
85+
"@intx/inference-testing": "workspace:*",
8586
"@intx/sidecar-app": "workspace:*",
8687
"@playwright/test": "1.64.1",
8788
"@types/bun": "1.4.4",

‎test/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ cross modules:
55

66
- `integration/` checks contracts and architecture across packages; `bun run test` runs it.
77
- `e2e/` runs the hub against a real Postgres; `bun run test:e2e` runs it.
8+
- `workflows/` runs packaged agents in-process on model turns mocked by `@intx/inference-testing`, scripted in `fixtures/`; `bun run test` runs it.
89

910
Later folders, added with their first file: `ux/` for interface tests,
10-
`workflows/` for agent-loop tests, `fixtures/` for recorded model sessions,
1111
`harness/` and `support/` for what those tests share.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { Harness } from "@intx/inference-testing";
2+
3+
export const constraints = {
4+
kind: "constraints",
5+
title: "Constraints",
6+
content: "# Constraints\n\nRuns in the browser.",
7+
};
8+
9+
export const writeConstraintsCallId = "call_write_constraints";
10+
11+
// The writer's sequence in every stored run: write the one assigned document, then reply.
12+
export function scriptWriterModelTurns(harness: Harness) {
13+
harness.scenario.replyOnce("openai", {
14+
toolCalls: [
15+
{
16+
callId: writeConstraintsCallId,
17+
name: "write_artifact",
18+
args: constraints,
19+
},
20+
],
21+
});
22+
harness.scenario.replyOnce("openai", { text: "Filed the constraints." });
23+
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import { afterEach, expect, test } from "bun:test";
2+
import { mkdtemp, rm } from "node:fs/promises";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
import { createWorkflowDirectorRegistry } from "@intx/agent";
6+
import { noopAuditStore } from "@intx/agent/testing";
7+
import { createIsogitStore } from "@intx/storage-isogit/node";
8+
import { HUB_CREDENTIAL_HANDLE } from "@intx/types/credential-delivery";
9+
import { createRuntimeCapabilities } from "@intx/types/runtime-capabilities";
10+
import { setupHarness } from "@intx/inference-testing";
11+
import { runLocal } from "@intx/workflow";
12+
import {
13+
attachStepTools,
14+
createCredentialsBackedAuthorize,
15+
createGrantEvaluator,
16+
createLocalCredentialsSnapshot,
17+
createToolBearingAgentFactory,
18+
createWorkflowStepInvoker,
19+
} from "@intx/workflow-host/local";
20+
import { writerTaskMessage } from "@corbits-builder/writer/contract";
21+
import type { WorkflowProjectScope } from "@corbits-builder/schemas/installed-workflows";
22+
import type { NativeSpecialistDeployment } from "@corbits-builder/schemas/native-specialist";
23+
import { workflowBindings } from "@corbits-builder/hub/config/models";
24+
import { driveUntilSettled } from "../../vendor/interchange/tests/workflow/local-step-env.ts";
25+
import {
26+
constraints,
27+
scriptWriterModelTurns,
28+
writeConstraintsCallId,
29+
} from "../fixtures/writer-model-turns.ts";
30+
31+
const deployment: NativeSpecialistDeployment = {
32+
source: {
33+
kind: "asset",
34+
assetId: "asset-1",
35+
package: { format: "tarball" },
36+
},
37+
entry: "index.js",
38+
pin: "@corbits-builder/writer-native@0.7.0",
39+
sourceOfferingIds: ["offering-1"],
40+
defaultSourceOfferingId: "offering-1",
41+
};
42+
43+
const scope: WorkflowProjectScope = {
44+
tenantId: "tenant-1",
45+
projectId: "project-1",
46+
domain: "builder.test",
47+
role: "writer",
48+
workflows: {
49+
writer: deployment,
50+
reviewer: deployment,
51+
coder: deployment,
52+
designer: deployment,
53+
},
54+
openNetwork: false,
55+
build: {
56+
assetDirectory: "/unused",
57+
browserCli: "/unused",
58+
childPath: "/unused",
59+
dependencyRoots: [],
60+
ripgrepPath: "/unused",
61+
runtimeVersion: "1.4.4",
62+
},
63+
};
64+
65+
// The hub's side of the writer's artifact tools: its project scope and the documents filed to it.
66+
function hubArtifacts() {
67+
const filed: { kind: string; version: number; content: string }[] = [];
68+
const artifacts = `/api/tenants/${scope.tenantId}/projects/${scope.projectId}/artifacts`;
69+
const fetch = async (input: string | URL | Request, init?: RequestInit) => {
70+
const path = String(input);
71+
if (path === "/api/workflow-project-scope") return Response.json(scope);
72+
if (path !== artifacts || init?.method !== "POST")
73+
return new Response(null, { status: 404 });
74+
const { kind, content } = JSON.parse(String(init.body)) as {
75+
kind: string;
76+
content: string;
77+
};
78+
const version = filed.filter((entry) => entry.kind === kind).length + 1;
79+
filed.push({ kind, version, content });
80+
return Response.json({
81+
artifact: {
82+
artifactId: `${kind}-1`,
83+
kind,
84+
version,
85+
sha256: "0".repeat(64),
86+
},
87+
});
88+
};
89+
return {
90+
filed,
91+
credentials: {
92+
resolve: async (handle: string) => {
93+
if (handle !== HUB_CREDENTIAL_HANDLE)
94+
throw new Error(`unknown_credential:${handle}`);
95+
return { kind: "http" as const, fetch, dispose: () => undefined };
96+
},
97+
},
98+
};
99+
}
100+
101+
const cleanup: (() => unknown)[] = [];
102+
afterEach(async () => {
103+
for (const step of cleanup.splice(0).reverse()) await step();
104+
});
105+
106+
// The writer entry the hub packages, run by Interchange's local runner with the real step invoker.
107+
// Its model turns are mocked in-process by Interchange's inference harness; the hub's artifact API
108+
// is the in-process stand-in above; no hub, sidecar or model server runs.
109+
test("a writer on in-process mocked model turns files its document", async () => {
110+
const harness = setupHarness();
111+
cleanup.push(() => harness.dispose());
112+
scriptWriterModelTurns(harness);
113+
const baseDir = await mkdtemp(join(tmpdir(), "builder-writer-fake-"));
114+
cleanup.push(() => rm(baseDir, { recursive: true, force: true }));
115+
116+
process.env.BUILDER_ROLE_INFERENCE = JSON.stringify(
117+
workflowBindings({ profile: "anthropic_default" }).writer,
118+
);
119+
cleanup.push(() => delete process.env.BUILDER_ROLE_INFERENCE);
120+
const entry = await import("@corbits-builder/hub/workflows/entries/writer");
121+
const directors = createWorkflowDirectorRegistry([entry.director], {
122+
[entry.historyCompactor.name]: entry.historyCompactor,
123+
[entry.degenerateTurnCompactor.name]: entry.degenerateTurnCompactor,
124+
});
125+
126+
const hub = hubArtifacts();
127+
const source = {
128+
id: "openai-compatible:builder-fake",
129+
provider: "openai-compatible",
130+
baseURL: "https://model.builder.test/v1",
131+
credentialId: "fake-model-key",
132+
model: "builder-fake",
133+
};
134+
const authorize = createCredentialsBackedAuthorize(
135+
{
136+
current: createLocalCredentialsSnapshot({
137+
definition: entry.default,
138+
directors,
139+
}),
140+
},
141+
createGrantEvaluator({ floorFor: () => [] }),
142+
);
143+
const invokeStep = createWorkflowStepInvoker({
144+
workflowAuthorize: authorize,
145+
agentFactory: createToolBearingAgentFactory(),
146+
buildEnv: async (request) => {
147+
const name = `${request.authzContext.runId}-${request.authzContext.stepId}`;
148+
const env = {
149+
sources: [source],
150+
defaultSource: source.id,
151+
storage: await createIsogitStore(join(baseDir, name, "ctx")),
152+
workdir: join(baseDir, name, "workspace"),
153+
audit: noopAuditStore(),
154+
directors,
155+
readCurrentMaterial: () => ({ secret: "fake-model-key" }),
156+
deps: harness.deps,
157+
capabilities: createRuntimeCapabilities({
158+
credentials: hub.credentials,
159+
}),
160+
};
161+
attachStepTools(env, {
162+
factories: request.agent.toolFactories.map((factory) => ({
163+
packageName: factory.id,
164+
declaredCredentials: [],
165+
factory,
166+
})),
167+
pluginFactories: [],
168+
});
169+
return env;
170+
},
171+
});
172+
173+
const result = await driveUntilSettled(
174+
harness,
175+
runLocal(entry.default, {
176+
authorize,
177+
directors,
178+
invokeStep,
179+
hasUpstreamSignalResolver: false,
180+
triggerPayload: writerTaskMessage({
181+
task: "Write the constraints for a habit tracker.",
182+
documents: [
183+
{
184+
ref: "brief-1@1@" + "0".repeat(64),
185+
kind: "problem_brief",
186+
title: "Problem brief",
187+
content: "A habit tracker for one person.",
188+
},
189+
],
190+
artifact: "constraints",
191+
}),
192+
}),
193+
);
194+
195+
expect(result.terminalStatus).toBe("completed");
196+
expect(hub.filed).toEqual([
197+
{ kind: "constraints", version: 1, content: constraints.content },
198+
]);
199+
const requests = await Promise.all(
200+
harness.scenario
201+
.matchedRequests()
202+
.map(
203+
(request) =>
204+
request.json() as Promise<{ messages: Record<string, unknown>[] }>,
205+
),
206+
);
207+
expect(requests).toHaveLength(2);
208+
const receipt = requests[1]?.messages.at(-1);
209+
expect(receipt).toMatchObject({
210+
role: "tool",
211+
tool_call_id: writeConstraintsCallId,
212+
});
213+
expect(JSON.parse(String(receipt?.content))).toEqual({
214+
artifactId: "constraints-1",
215+
kind: "constraints",
216+
version: 1,
217+
sha256: "0".repeat(64),
218+
});
219+
});

0 commit comments

Comments
 (0)