@fusionkit/cli 0.1.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 +8 -0
- package/dist/cli.js +34 -0
- package/dist/commands/ensemble-gateway.d.ts +2 -0
- package/dist/commands/ensemble-gateway.js +114 -0
- package/dist/commands/ensemble-records.d.ts +33 -0
- package/dist/commands/ensemble-records.js +207 -0
- package/dist/commands/ensemble.d.ts +2 -0
- package/dist/commands/ensemble.js +254 -0
- package/dist/commands/fusion.d.ts +2 -0
- package/dist/commands/fusion.js +112 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +24 -0
- package/dist/commands/lifecycle.d.ts +2 -0
- package/dist/commands/lifecycle.js +124 -0
- package/dist/commands/local.d.ts +2 -0
- package/dist/commands/local.js +25 -0
- package/dist/commands/plane.d.ts +2 -0
- package/dist/commands/plane.js +30 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +149 -0
- package/dist/commands/runner.d.ts +2 -0
- package/dist/commands/runner.js +33 -0
- package/dist/commands/secrets.d.ts +2 -0
- package/dist/commands/secrets.js +21 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.js +69 -0
- package/dist/fusion-quickstart.d.ts +182 -0
- package/dist/fusion-quickstart.js +673 -0
- package/dist/gateway.d.ts +63 -0
- package/dist/gateway.js +304 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +28 -0
- package/dist/local.d.ts +40 -0
- package/dist/local.js +144 -0
- package/dist/render.d.ts +7 -0
- package/dist/render.js +131 -0
- package/dist/shared/errors.d.ts +6 -0
- package/dist/shared/errors.js +9 -0
- package/dist/shared/options.d.ts +24 -0
- package/dist/shared/options.js +106 -0
- package/dist/shared/plane.d.ts +13 -0
- package/dist/shared/plane.js +46 -0
- package/dist/shared/preflight.d.ts +15 -0
- package/dist/shared/preflight.js +48 -0
- package/dist/shared/proc.d.ts +41 -0
- package/dist/shared/proc.js +122 -0
- package/dist/test/cli.test.d.ts +1 -0
- package/dist/test/cli.test.js +867 -0
- package/dist/test/e2e.test.d.ts +1 -0
- package/dist/test/e2e.test.js +250 -0
- package/dist/test/fusion-quickstart.test.d.ts +1 -0
- package/dist/test/fusion-quickstart.test.js +189 -0
- package/dist/test/gateway-e2e.test.d.ts +1 -0
- package/dist/test/gateway-e2e.test.js +606 -0
- package/dist/test/handoff.test.d.ts +1 -0
- package/dist/test/handoff.test.js +212 -0
- package/dist/test/local.test.d.ts +1 -0
- package/dist/test/local.test.js +39 -0
- package/dist/test/proc.test.d.ts +1 -0
- package/dist/test/proc.test.js +22 -0
- package/package.json +48 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { after, before, test } from "node:test";
|
|
6
|
+
import { defaultPolicy, generateMasterKeyHex, masterKeyFromMaterial, Plane, SecretStore, startPlaneServer } from "@fusionkit/plane";
|
|
7
|
+
import { buildReceiptStory, generateEd25519KeyPair, verifyReceiptBundle } from "@fusionkit/protocol";
|
|
8
|
+
import { Runner } from "@fusionkit/runner";
|
|
9
|
+
import { PlaneClient, PlaneClientError } from "@fusionkit/sdk";
|
|
10
|
+
import { git, makeRepo } from "@fusionkit/testkit";
|
|
11
|
+
import { captureWorkspace, pullRun } from "@fusionkit/workspace";
|
|
12
|
+
const SECRET_VALUE = "super-sensitive-value-1234";
|
|
13
|
+
let planeDir;
|
|
14
|
+
let runnerDir;
|
|
15
|
+
let repoDir;
|
|
16
|
+
let server;
|
|
17
|
+
let client;
|
|
18
|
+
let runner;
|
|
19
|
+
before(async () => {
|
|
20
|
+
planeDir = mkdtempSync(join(tmpdir(), "warrant-e2e-plane-"));
|
|
21
|
+
runnerDir = mkdtempSync(join(tmpdir(), "warrant-e2e-runner-"));
|
|
22
|
+
repoDir = makeRepo({ files: { "README.md": "# e2e fixture\n" } });
|
|
23
|
+
const policy = defaultPolicy();
|
|
24
|
+
policy.runners.allowPools = ["eng-prod"];
|
|
25
|
+
policy.agents.allow = ["mock", "command"];
|
|
26
|
+
policy.secrets.releasable = [
|
|
27
|
+
{ name: "MOCK_SECRET", scope: "e2e-test", pools: ["eng-prod"] }
|
|
28
|
+
];
|
|
29
|
+
policy.consent = [{ when: "secret-release", approvers: ["security"] }];
|
|
30
|
+
const keys = generateEd25519KeyPair();
|
|
31
|
+
const secretStore = new SecretStore(join(planeDir, "secrets.enc"), masterKeyFromMaterial(generateMasterKeyHex()));
|
|
32
|
+
secretStore.set("MOCK_SECRET", SECRET_VALUE);
|
|
33
|
+
const plane = new Plane({
|
|
34
|
+
dataDir: join(planeDir, "data"),
|
|
35
|
+
policy,
|
|
36
|
+
planePrivateKeyPem: keys.privateKeyPem,
|
|
37
|
+
planePublicKeyPem: keys.publicKeyPem,
|
|
38
|
+
adminToken: "admin-token-test",
|
|
39
|
+
enrollToken: "enroll-token-test",
|
|
40
|
+
secretStore
|
|
41
|
+
});
|
|
42
|
+
const started = await startPlaneServer(plane, 0);
|
|
43
|
+
server = started.server;
|
|
44
|
+
const planeUrl = `http://127.0.0.1:${started.port}`;
|
|
45
|
+
client = new PlaneClient(planeUrl, "admin-token-test");
|
|
46
|
+
runner = new Runner({
|
|
47
|
+
planeUrl,
|
|
48
|
+
pool: "eng-prod",
|
|
49
|
+
dataDir: runnerDir,
|
|
50
|
+
enrollToken: "enroll-token-test"
|
|
51
|
+
});
|
|
52
|
+
await runner.ensureEnrolled();
|
|
53
|
+
});
|
|
54
|
+
after(() => {
|
|
55
|
+
server.close(() => undefined);
|
|
56
|
+
rmSync(planeDir, { recursive: true, force: true });
|
|
57
|
+
rmSync(runnerDir, { recursive: true, force: true });
|
|
58
|
+
rmSync(repoDir, { recursive: true, force: true });
|
|
59
|
+
});
|
|
60
|
+
function buildRequest(prompt) {
|
|
61
|
+
const captured = captureWorkspace(repoDir);
|
|
62
|
+
return {
|
|
63
|
+
requestedBy: { kind: "human", id: "e2e-tester" },
|
|
64
|
+
agentKind: "mock",
|
|
65
|
+
prompt,
|
|
66
|
+
pool: "eng-prod",
|
|
67
|
+
secretNames: ["MOCK_SECRET"],
|
|
68
|
+
workspace: captured.manifest,
|
|
69
|
+
network: { defaultDeny: true, allowHosts: [] },
|
|
70
|
+
budget: {},
|
|
71
|
+
disclosure: "minimal-context"
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function buildNoSecretRequest(prompt) {
|
|
75
|
+
const captured = captureWorkspace(repoDir);
|
|
76
|
+
return {
|
|
77
|
+
requestedBy: { kind: "human", id: "e2e-tester" },
|
|
78
|
+
agentKind: "mock",
|
|
79
|
+
prompt,
|
|
80
|
+
pool: "eng-prod",
|
|
81
|
+
secretNames: [],
|
|
82
|
+
workspace: captured.manifest,
|
|
83
|
+
network: { defaultDeny: true, allowHosts: [] },
|
|
84
|
+
budget: {},
|
|
85
|
+
disclosure: "minimal-context"
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function buildSecretEchoRequest(prompt) {
|
|
89
|
+
const captured = captureWorkspace(repoDir);
|
|
90
|
+
return {
|
|
91
|
+
requestedBy: { kind: "human", id: "e2e-tester" },
|
|
92
|
+
agentKind: "command",
|
|
93
|
+
prompt,
|
|
94
|
+
pool: "eng-prod",
|
|
95
|
+
secretNames: ["MOCK_SECRET"],
|
|
96
|
+
workspace: captured.manifest,
|
|
97
|
+
network: { defaultDeny: true, allowHosts: [] },
|
|
98
|
+
budget: {},
|
|
99
|
+
disclosure: "minimal-context",
|
|
100
|
+
execution: {
|
|
101
|
+
kind: "shell",
|
|
102
|
+
script: "printf \"$MOCK_SECRET\" && printf \"$MOCK_SECRET\\n\" > leaked-secret.txt",
|
|
103
|
+
shell: "sh"
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async function uploadWorkspaceBlobs() {
|
|
108
|
+
const captured = captureWorkspace(repoDir);
|
|
109
|
+
await client.putBlob(captured.bundle);
|
|
110
|
+
if (captured.dirtyDiff)
|
|
111
|
+
await client.putBlob(captured.dirtyDiff);
|
|
112
|
+
for (const file of captured.untracked)
|
|
113
|
+
await client.putBlob(file.content);
|
|
114
|
+
}
|
|
115
|
+
let completedBundle;
|
|
116
|
+
test("dry run disclosure report moves nothing", async () => {
|
|
117
|
+
const report = await client.dryRun(buildRequest("dry run probe"));
|
|
118
|
+
assert.equal(report.dryRun, true);
|
|
119
|
+
assert.equal(report.policyDecision.decision, "ask");
|
|
120
|
+
assert.deepEqual(report.secrets.map((s) => s.name), ["MOCK_SECRET"]);
|
|
121
|
+
assert.equal(report.network.defaultDeny, true);
|
|
122
|
+
});
|
|
123
|
+
test("policy denies a disallowed agent kind, fail closed", async () => {
|
|
124
|
+
const request = { ...buildRequest("nope"), agentKind: "codex" };
|
|
125
|
+
await assert.rejects(() => client.requestRun(request), (error) => {
|
|
126
|
+
assert.ok(error instanceof PlaneClientError);
|
|
127
|
+
assert.equal(error.status, 403);
|
|
128
|
+
return true;
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
test("policy denies unreleasable secrets without creating receipt residue", async () => {
|
|
132
|
+
const before = await client.listRuns();
|
|
133
|
+
const request = { ...buildRequest("unknown secret"), secretNames: ["UNKNOWN_SECRET"] };
|
|
134
|
+
await assert.rejects(() => client.requestRun(request), (error) => {
|
|
135
|
+
assert.ok(error instanceof PlaneClientError);
|
|
136
|
+
assert.equal(error.status, 403);
|
|
137
|
+
return true;
|
|
138
|
+
});
|
|
139
|
+
const after = await client.listRuns();
|
|
140
|
+
assert.equal(after.runs.length, before.runs.length);
|
|
141
|
+
assert.ok(!JSON.stringify(after).includes("UNKNOWN_SECRET"));
|
|
142
|
+
});
|
|
143
|
+
test("no-secret run records empty secret evidence", async () => {
|
|
144
|
+
await uploadWorkspaceBlobs();
|
|
145
|
+
const created = await client.requestRun(buildNoSecretRequest("no secret run"));
|
|
146
|
+
assert.equal(created.status, "created");
|
|
147
|
+
const processed = await runner.runOnce();
|
|
148
|
+
assert.equal(processed, created.runId);
|
|
149
|
+
const bundle = await client.getBundle(created.runId);
|
|
150
|
+
assert.equal(bundle.receipt.secretsReleased.length, 0);
|
|
151
|
+
assert.equal(bundle.events.some((event) => event.event.type === "secret.released"), false);
|
|
152
|
+
assert.deepEqual(buildReceiptStory(bundle).secrets, []);
|
|
153
|
+
});
|
|
154
|
+
test("governed run: consent, execution, secret injection, egress blocking, receipt", async () => {
|
|
155
|
+
await uploadWorkspaceBlobs();
|
|
156
|
+
const created = await client.requestRun(buildRequest("e2e task: touch files and probe network"));
|
|
157
|
+
assert.equal(created.status, "awaiting_approval");
|
|
158
|
+
// Runner cannot claim before approval.
|
|
159
|
+
assert.equal(await runner.runOnce(), undefined);
|
|
160
|
+
await client.approve(created.runId, { kind: "human", id: "security-lead" });
|
|
161
|
+
const processed = await runner.runOnce();
|
|
162
|
+
assert.equal(processed, created.runId);
|
|
163
|
+
const view = await client.getRun(created.runId);
|
|
164
|
+
assert.equal(view.status, "completed");
|
|
165
|
+
completedBundle = await client.getBundle(created.runId);
|
|
166
|
+
const { receipt, events } = completedBundle;
|
|
167
|
+
// Receipt answers the five questions.
|
|
168
|
+
assert.equal(receipt.status, "completed");
|
|
169
|
+
assert.equal(receipt.runner.attestationTier, "mock");
|
|
170
|
+
assert.deepEqual(receipt.secretsReleased.map((s) => s.name), ["MOCK_SECRET"]);
|
|
171
|
+
assert.ok(receipt.networkAccessed.some((n) => n.host === "denied.example.com" && n.decision === "blocked"), "egress probe must be recorded as blocked");
|
|
172
|
+
assert.ok(receipt.workspaceOut.diffHash, "run must produce a diff");
|
|
173
|
+
assert.ok(events.some((e) => e.event.type === "file.changed" && e.event.path === "MOCK_AGENT.md"));
|
|
174
|
+
assert.ok(events.some((e) => e.event.type === "consent.granted"));
|
|
175
|
+
assert.ok(events.some((e) => e.event.type === "boundary.crossed"));
|
|
176
|
+
// Secret value never appears anywhere in the bundle.
|
|
177
|
+
assert.ok(!JSON.stringify(completedBundle).includes(SECRET_VALUE));
|
|
178
|
+
// The agent saw the secret (injection worked) without the value leaking.
|
|
179
|
+
const diff = await client.getBlob(receipt.workspaceOut.diffHash);
|
|
180
|
+
const diffText = diff.toString("utf8");
|
|
181
|
+
assert.ok(diffText.includes("secret:present"));
|
|
182
|
+
assert.ok(!diffText.includes(SECRET_VALUE));
|
|
183
|
+
});
|
|
184
|
+
test("receipt bundle verifies offline; tampering is detected", () => {
|
|
185
|
+
const verification = verifyReceiptBundle(completedBundle);
|
|
186
|
+
assert.deepEqual(verification.problems, []);
|
|
187
|
+
assert.equal(verification.ok, true);
|
|
188
|
+
const tampered = structuredClone(completedBundle);
|
|
189
|
+
const secretEvent = tampered.events.find((e) => e.event.type === "secret.released");
|
|
190
|
+
assert.ok(secretEvent, "fixture must include a secret.released event");
|
|
191
|
+
tampered.events = tampered.events.filter((e) => e !== secretEvent);
|
|
192
|
+
tampered.events.forEach((e, i) => {
|
|
193
|
+
e.seq = i;
|
|
194
|
+
});
|
|
195
|
+
const tamperedResult = verifyReceiptBundle(tampered);
|
|
196
|
+
assert.equal(tamperedResult.ok, false);
|
|
197
|
+
const forgedReceipt = structuredClone(completedBundle);
|
|
198
|
+
forgedReceipt.receipt.secretsReleased = [];
|
|
199
|
+
const forgedResult = verifyReceiptBundle(forgedReceipt);
|
|
200
|
+
assert.equal(forgedResult.ok, false);
|
|
201
|
+
});
|
|
202
|
+
test("released secret values are redacted from diff and log artifacts", async () => {
|
|
203
|
+
await uploadWorkspaceBlobs();
|
|
204
|
+
const created = await client.requestRun(buildSecretEchoRequest("print and write a secret so artifacts must redact it"));
|
|
205
|
+
assert.equal(created.status, "awaiting_approval");
|
|
206
|
+
await client.approve(created.runId, { kind: "human", id: "security-lead" });
|
|
207
|
+
const processed = await runner.runOnce();
|
|
208
|
+
assert.equal(processed, created.runId);
|
|
209
|
+
const bundle = await client.getBundle(created.runId);
|
|
210
|
+
assert.equal(bundle.receipt.status, "completed");
|
|
211
|
+
assert.deepEqual(bundle.receipt.secretsReleased.map((secret) => secret.name), ["MOCK_SECRET"]);
|
|
212
|
+
assert.ok(!JSON.stringify(bundle).includes(SECRET_VALUE));
|
|
213
|
+
const disclosedHashes = new Set(bundle.receipt.boundaryDisclosures.map((item) => item.contentHash));
|
|
214
|
+
assert.ok(disclosedHashes.size >= 2, "expected diff and log disclosures");
|
|
215
|
+
for (const hash of disclosedHashes) {
|
|
216
|
+
const content = (await client.getBlob(hash)).toString("utf8");
|
|
217
|
+
assert.ok(!content.includes(SECRET_VALUE));
|
|
218
|
+
assert.ok(content.includes("[REDACTED:MOCK_SECRET]"));
|
|
219
|
+
}
|
|
220
|
+
const jsonl = await client.exportJsonl();
|
|
221
|
+
assert.ok(!jsonl.includes(SECRET_VALUE));
|
|
222
|
+
});
|
|
223
|
+
test("pull applies the run output divergence-safely", async () => {
|
|
224
|
+
const diff = await client.getBlob(completedBundle.receipt.workspaceOut.diffHash);
|
|
225
|
+
// Clean repo at base ref: fast path applies in place.
|
|
226
|
+
const clean = pullRun(repoDir, completedBundle.receipt.runId, completedBundle.contract.workspace.baseRef, diff);
|
|
227
|
+
assert.deepEqual(clean, { mode: "applied" });
|
|
228
|
+
const content = readFileSync(join(repoDir, "MOCK_AGENT.md"), "utf8");
|
|
229
|
+
assert.ok(content.includes("e2e task"));
|
|
230
|
+
assert.ok(content.includes("secret:present"));
|
|
231
|
+
// Diverged repo: results land on a branch, checkout untouched.
|
|
232
|
+
git(repoDir, ["add", "-A"]);
|
|
233
|
+
git(repoDir, ["commit", "--quiet", "-m", "absorb pulled output"]);
|
|
234
|
+
writeFileSync(join(repoDir, "local-edit.txt"), "concurrent local work\n");
|
|
235
|
+
git(repoDir, ["add", "-A"]);
|
|
236
|
+
git(repoDir, ["commit", "--quiet", "-m", "diverge"]);
|
|
237
|
+
const diverged = pullRun(repoDir, completedBundle.receipt.runId, completedBundle.contract.workspace.baseRef, diff);
|
|
238
|
+
assert.equal(diverged.mode, "branch");
|
|
239
|
+
});
|
|
240
|
+
test("audit export emits JSONL for every event", async () => {
|
|
241
|
+
const jsonl = await client.exportJsonl();
|
|
242
|
+
const lines = jsonl.trim().split("\n");
|
|
243
|
+
assert.ok(lines.length >= completedBundle.events.length);
|
|
244
|
+
for (const line of lines) {
|
|
245
|
+
const parsed = JSON.parse(line);
|
|
246
|
+
assert.ok(parsed.runId.startsWith("run_"));
|
|
247
|
+
assert.match(parsed.hash, /^[0-9a-f]{64}$/);
|
|
248
|
+
}
|
|
249
|
+
assert.ok(!jsonl.includes(SECRET_VALUE));
|
|
250
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { after, test } from "node:test";
|
|
8
|
+
import { defaultKeyEnv, loadEnvFileInto, startFusionStack } from "../fusion-quickstart.js";
|
|
9
|
+
const SENTINEL = "FUSION_OK";
|
|
10
|
+
/**
|
|
11
|
+
* Create a real git repo with a genuinely failing test, so each panel model has
|
|
12
|
+
* a concrete coding task to fuse over.
|
|
13
|
+
*/
|
|
14
|
+
function materializeSampleRepo(root) {
|
|
15
|
+
mkdirSync(root, { recursive: true });
|
|
16
|
+
const git = (args) => {
|
|
17
|
+
execFileSync("git", args, { cwd: root });
|
|
18
|
+
};
|
|
19
|
+
git(["init", "--quiet", "--initial-branch=main"]);
|
|
20
|
+
git(["config", "user.email", "fusion@warrant.local"]);
|
|
21
|
+
git(["config", "user.name", "warrant-fusion"]);
|
|
22
|
+
writeFileSync(join(root, "package.json"), JSON.stringify({ name: "fusion-sample", private: true, scripts: { test: "node --test" } }, null, 2) + "\n");
|
|
23
|
+
writeFileSync(join(root, "calculator.js"), "exports.add = (left, right) => left - right;\n");
|
|
24
|
+
writeFileSync(join(root, "calculator.test.js"), [
|
|
25
|
+
"const assert = require('node:assert/strict');",
|
|
26
|
+
"const { add } = require('./calculator.js');",
|
|
27
|
+
"assert.equal(add(2, 3), 5);",
|
|
28
|
+
""
|
|
29
|
+
].join("\n"));
|
|
30
|
+
writeFileSync(join(root, "README.md"), "# fusion sample\n\n`add` is buggy (it subtracts); `npm test` fails until it is fixed.\n");
|
|
31
|
+
git(["add", "-A"]);
|
|
32
|
+
git(["commit", "--quiet", "-m", "failing calculator sample"]);
|
|
33
|
+
return root;
|
|
34
|
+
}
|
|
35
|
+
async function readBody(req) {
|
|
36
|
+
const chunks = [];
|
|
37
|
+
for await (const chunk of req)
|
|
38
|
+
chunks.push(chunk);
|
|
39
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
40
|
+
}
|
|
41
|
+
async function closeServer(server) {
|
|
42
|
+
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
43
|
+
}
|
|
44
|
+
const tmpRoots = [];
|
|
45
|
+
function freshDir(prefix) {
|
|
46
|
+
const dir = mkdtempSync(join(tmpdir(), prefix));
|
|
47
|
+
tmpRoots.push(dir);
|
|
48
|
+
return dir;
|
|
49
|
+
}
|
|
50
|
+
after(() => {
|
|
51
|
+
for (const dir of tmpRoots)
|
|
52
|
+
rmSync(dir, { recursive: true, force: true });
|
|
53
|
+
});
|
|
54
|
+
test("loadEnvFileInto fills missing keys from a .env without overriding existing ones", () => {
|
|
55
|
+
const dir = freshDir("fusion-env-");
|
|
56
|
+
const envPath = join(dir, ".env");
|
|
57
|
+
writeFileSync(envPath, ["# comment", "export OPENAI_API_KEY=sk-from-file", 'ANTHROPIC_API_KEY="sk-ant-quoted"', "", "BARE=1"].join("\n"));
|
|
58
|
+
const env = { OPENAI_API_KEY: "sk-already-set" };
|
|
59
|
+
loadEnvFileInto(envPath, env);
|
|
60
|
+
assert.equal(env.OPENAI_API_KEY, "sk-already-set", "existing values must win");
|
|
61
|
+
assert.equal(env.ANTHROPIC_API_KEY, "sk-ant-quoted", "quotes are stripped");
|
|
62
|
+
assert.equal(env.BARE, "1");
|
|
63
|
+
loadEnvFileInto(join(dir, "missing.env"), env); // no-op when absent
|
|
64
|
+
});
|
|
65
|
+
test("defaultKeyEnv maps cloud providers to their conventional key env vars", () => {
|
|
66
|
+
assert.equal(defaultKeyEnv("openai"), "OPENAI_API_KEY");
|
|
67
|
+
assert.equal(defaultKeyEnv("anthropic"), "ANTHROPIC_API_KEY");
|
|
68
|
+
assert.equal(defaultKeyEnv("google"), "GEMINI_API_KEY");
|
|
69
|
+
assert.equal(defaultKeyEnv("openai-compatible"), undefined);
|
|
70
|
+
assert.equal(defaultKeyEnv("mlx"), undefined);
|
|
71
|
+
});
|
|
72
|
+
test("materializeSampleRepo creates a real git repo whose tests fail until add() is fixed", () => {
|
|
73
|
+
const repo = materializeSampleRepo(join(freshDir("fusion-sample-"), "repo"));
|
|
74
|
+
assert.match(readFileSync(join(repo, "calculator.js"), "utf8"), /left - right/);
|
|
75
|
+
const env = { ...process.env };
|
|
76
|
+
delete env.NODE_TEST_CONTEXT;
|
|
77
|
+
const tests = spawnSync("node", ["--test"], { cwd: repo, encoding: "utf8", env });
|
|
78
|
+
assert.notEqual(tests.status, 0, "the sample repo's tests must fail before a fix");
|
|
79
|
+
const isRepo = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: repo, encoding: "utf8" });
|
|
80
|
+
assert.equal(isRepo.stdout.trim(), "true");
|
|
81
|
+
});
|
|
82
|
+
/** A fake OpenAI-compatible endpoint that answers directly (no tool calls). */
|
|
83
|
+
async function startFakeAnswerModel(answer) {
|
|
84
|
+
let calls = 0;
|
|
85
|
+
const server = createServer((req, res) => {
|
|
86
|
+
void (async () => {
|
|
87
|
+
const path = new URL(req.url ?? "/", "http://localhost").pathname;
|
|
88
|
+
if (req.method === "GET" && path === "/v1/models") {
|
|
89
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
90
|
+
res.end(JSON.stringify({ object: "list", data: [{ id: "fake", object: "model" }] }));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (req.method === "POST" && path === "/v1/chat/completions") {
|
|
94
|
+
await readBody(req);
|
|
95
|
+
calls += 1;
|
|
96
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
97
|
+
res.end(JSON.stringify({
|
|
98
|
+
id: "chatcmpl_fake",
|
|
99
|
+
object: "chat.completion",
|
|
100
|
+
created: 0,
|
|
101
|
+
model: "fake",
|
|
102
|
+
choices: [{ index: 0, message: { role: "assistant", content: answer }, finish_reason: "stop" }],
|
|
103
|
+
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }
|
|
104
|
+
}));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
res.writeHead(404).end();
|
|
108
|
+
})().catch((error) => res.writeHead(500).end(String(error)));
|
|
109
|
+
});
|
|
110
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
111
|
+
const address = server.address();
|
|
112
|
+
assert.ok(typeof address === "object" && address !== null);
|
|
113
|
+
return {
|
|
114
|
+
url: `http://127.0.0.1:${address.port}`,
|
|
115
|
+
solveCalls: () => calls,
|
|
116
|
+
judgeCalls: () => 0,
|
|
117
|
+
close: () => closeServer(server)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
test("agent front door: panel produces a trajectory the judge step consumes", async () => {
|
|
121
|
+
const repo = materializeSampleRepo(join(freshDir("fusion-agent-"), "repo"));
|
|
122
|
+
const model = await startFakeAnswerModel("This repo is a calculator sample.");
|
|
123
|
+
// Fake FusionKit judge step: records the candidate trajectories + conversation it
|
|
124
|
+
// receives and returns a terminal assistant answer (no tool calls) as an OpenAI
|
|
125
|
+
// chat completion, which is what the new front door proxies to the harness.
|
|
126
|
+
let stepTrajectories = [];
|
|
127
|
+
let stepMessages = [];
|
|
128
|
+
const synth = createServer((req, res) => {
|
|
129
|
+
void (async () => {
|
|
130
|
+
const path = new URL(req.url ?? "/", "http://localhost").pathname;
|
|
131
|
+
if (req.method === "POST" && path === "/v1/fusion/trajectory:step") {
|
|
132
|
+
const body = JSON.parse(await readBody(req));
|
|
133
|
+
stepTrajectories = body.trajectories ?? [];
|
|
134
|
+
stepMessages = body.messages ?? [];
|
|
135
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
136
|
+
res.end(JSON.stringify({
|
|
137
|
+
id: "chatcmpl-step",
|
|
138
|
+
object: "chat.completion",
|
|
139
|
+
created: 0,
|
|
140
|
+
model: "fusion-panel",
|
|
141
|
+
choices: [
|
|
142
|
+
{
|
|
143
|
+
index: 0,
|
|
144
|
+
message: { role: "assistant", content: `${SENTINEL}: this repo is a calculator sample` },
|
|
145
|
+
finish_reason: "stop"
|
|
146
|
+
}
|
|
147
|
+
],
|
|
148
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
149
|
+
}));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
res.writeHead(404).end();
|
|
153
|
+
})().catch((error) => res.writeHead(500).end(String(error)));
|
|
154
|
+
});
|
|
155
|
+
await new Promise((resolve) => synth.listen(0, "127.0.0.1", resolve));
|
|
156
|
+
const synthAddress = synth.address();
|
|
157
|
+
assert.ok(typeof synthAddress === "object" && synthAddress !== null);
|
|
158
|
+
const synthesisUrl = `http://127.0.0.1:${synthAddress.port}`;
|
|
159
|
+
const stack = await startFusionStack({
|
|
160
|
+
repo,
|
|
161
|
+
outputRoot: freshDir("fusion-agent-runs-"),
|
|
162
|
+
models: [{ id: "alpha", model: "fake", provider: "openai-compatible", baseUrl: model.url }],
|
|
163
|
+
endpoints: { alpha: model.url },
|
|
164
|
+
synthesisUrl,
|
|
165
|
+
log: () => { }
|
|
166
|
+
});
|
|
167
|
+
try {
|
|
168
|
+
const response = await fetch(`${stack.fusionUrl}/v1/chat/completions`, {
|
|
169
|
+
method: "POST",
|
|
170
|
+
headers: { "content-type": "application/json" },
|
|
171
|
+
body: JSON.stringify({ model: "fusion-panel", messages: [{ role: "user", content: "What's in this repo?" }] })
|
|
172
|
+
});
|
|
173
|
+
assert.equal(response.status, 200);
|
|
174
|
+
const body = (await response.json());
|
|
175
|
+
assert.match(body.choices[0]?.message.content ?? "", new RegExp(SENTINEL));
|
|
176
|
+
assert.ok(model.solveCalls() >= 1, "the panel model agent must run");
|
|
177
|
+
assert.equal(stepTrajectories.length, 1, "the panel's one trajectory must reach the judge step");
|
|
178
|
+
const trajectory = stepTrajectories[0];
|
|
179
|
+
assert.equal(trajectory.model_id, "alpha");
|
|
180
|
+
assert.ok(Array.isArray(trajectory.steps) && trajectory.steps.length >= 1, "trajectory must carry steps");
|
|
181
|
+
assert.match(trajectory.final_output ?? "", /calculator sample/);
|
|
182
|
+
assert.ok(stepMessages.some((message) => message.role === "user"), "the conversation must reach the judge");
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
await stack.close();
|
|
186
|
+
await model.close();
|
|
187
|
+
await closeServer(synth);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|