@velum-labs/routekit-eval-service 1.0.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/LICENSE +201 -0
- package/README.md +18 -0
- package/dist/dimension-evidence.d.ts +27 -0
- package/dist/dimension-evidence.js +263 -0
- package/dist/effect-api.d.ts +7 -0
- package/dist/effect-api.js +4 -0
- package/dist/errors.d.ts +48 -0
- package/dist/errors.js +36 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +4 -0
- package/dist/layer-options.d.ts +10 -0
- package/dist/layer-options.js +1 -0
- package/dist/production-runner.d.ts +21 -0
- package/dist/production-runner.js +39 -0
- package/dist/service.d.ts +56 -0
- package/dist/service.js +345 -0
- package/dist/test/dimension-evidence.test.d.ts +1 -0
- package/dist/test/dimension-evidence.test.js +155 -0
- package/dist/test/production-runner.test.d.ts +1 -0
- package/dist/test/production-runner.test.js +218 -0
- package/dist/test/service.test.d.ts +1 -0
- package/dist/test/service.test.js +162 -0
- package/package.json +54 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { after, test } from "node:test";
|
|
7
|
+
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";
|
|
8
|
+
import { Effect, Exit } from "effect";
|
|
9
|
+
import { EvalServiceEstimateError, EvalServiceSpendLimitError } from "../errors.js";
|
|
10
|
+
import { EvalServiceCredentialError, makeRouteKitEvalServiceLayer } from "../production-runner.js";
|
|
11
|
+
import { EvalService } from "../service.js";
|
|
12
|
+
const roots = [];
|
|
13
|
+
after(async () => Promise.all(roots.map((root) => rm(root, { recursive: true, force: true }))));
|
|
14
|
+
const requestFor = (suitePath, gatewayUrl = "http://127.0.0.1:8080") => ({
|
|
15
|
+
version: 1,
|
|
16
|
+
profileId: "support",
|
|
17
|
+
suitePath,
|
|
18
|
+
candidateModels: ["openai/cheap", "anthropic/strong"],
|
|
19
|
+
judgeModel: "openai/judge",
|
|
20
|
+
gatewayUrl
|
|
21
|
+
});
|
|
22
|
+
const withEvalService = (options, use) => Effect.gen(function* () {
|
|
23
|
+
return yield* use(yield* EvalService);
|
|
24
|
+
}).pipe(Effect.provide(makeRouteKitEvalServiceLayer({}, options)));
|
|
25
|
+
const readBody = async (request) => {
|
|
26
|
+
const chunks = [];
|
|
27
|
+
for await (const chunk of request)
|
|
28
|
+
chunks.push(Buffer.from(chunk));
|
|
29
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
30
|
+
};
|
|
31
|
+
const writeManifest = async (root, candidateModels, judgeModel, caseIds) => {
|
|
32
|
+
await writeFile(path.join(root, "routekit.eval-manifest.json"), `${JSON.stringify({
|
|
33
|
+
version: 1,
|
|
34
|
+
profileId: "support",
|
|
35
|
+
candidateModels,
|
|
36
|
+
judgeModel,
|
|
37
|
+
caseCount: caseIds.length,
|
|
38
|
+
caseIds,
|
|
39
|
+
maxOutputTokens: 1_024,
|
|
40
|
+
expectedCallCount: caseIds.length * candidateModels.length * 2
|
|
41
|
+
})}\n`);
|
|
42
|
+
};
|
|
43
|
+
test("production runner estimates the exact imported nested-loop testdrive suite from its manifest", async () => {
|
|
44
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "routekit-eval-runner-estimate-"));
|
|
45
|
+
roots.push(root);
|
|
46
|
+
const suite = path.join(root, "support.eval.ts");
|
|
47
|
+
const candidates = ["openai/gpt-5.5", "openai/gpt-5.1", "openai/gpt-4.1-mini"];
|
|
48
|
+
const caseIds = ["one", "two", "three", "four", "five"];
|
|
49
|
+
await mkdir(path.join(root, "data"));
|
|
50
|
+
await writeFile(path.join(root, "data", "cases.json"), `${JSON.stringify(caseIds.map((id) => ({ id, prompt: id })))}\n`);
|
|
51
|
+
await writeManifest(root, candidates, "openai/gpt-5.5", caseIds);
|
|
52
|
+
await writeFile(suite, [
|
|
53
|
+
'import assert from "node:assert/strict";',
|
|
54
|
+
'import { test } from "node:test";',
|
|
55
|
+
'import { setupAgent, setupJudge } from "routekit/eval";',
|
|
56
|
+
'import cases from "./data/cases.json" with { type: "json" };',
|
|
57
|
+
'import manifest from "./routekit.eval-manifest.json" with { type: "json" };',
|
|
58
|
+
"const candidateModels = manifest.candidateModels;",
|
|
59
|
+
"assert.equal(cases.length, manifest.caseCount);",
|
|
60
|
+
"const judge = setupJudge({ agent: setupAgent({ model: manifest.judgeModel }) });",
|
|
61
|
+
"for (const model of candidateModels) {",
|
|
62
|
+
" const candidate = setupAgent({ model });",
|
|
63
|
+
" for (const testCase of cases) {",
|
|
64
|
+
" test(`${model} / ${testCase.id}`, async () => {",
|
|
65
|
+
" const run = await candidate.run({ prompt: testCase.prompt, caseId: testCase.id });",
|
|
66
|
+
" run.toComplete();",
|
|
67
|
+
' await judge.autoEvals({ criteria: "correct", prompt: testCase.prompt, run });',
|
|
68
|
+
" });",
|
|
69
|
+
" }",
|
|
70
|
+
"}"
|
|
71
|
+
].join("\n"));
|
|
72
|
+
const result = await Effect.runPromise(withEvalService({}, (service) => Effect.gen(function* () {
|
|
73
|
+
yield* service.validate(suite);
|
|
74
|
+
return yield* service.estimate({
|
|
75
|
+
...requestFor(suite),
|
|
76
|
+
candidateModels: candidates,
|
|
77
|
+
judgeModel: "openai/gpt-5.5"
|
|
78
|
+
}, "pilot");
|
|
79
|
+
})).pipe(Effect.provide(NodeHttpClient.layerUndici)));
|
|
80
|
+
assert.equal(result.callCount, 30);
|
|
81
|
+
assert.equal(result.pricingKnown, true);
|
|
82
|
+
assert.equal((result.maximumCostUsd ?? 0) > 0, true);
|
|
83
|
+
});
|
|
84
|
+
test("production runner rejects a manifest for a different profile", async () => {
|
|
85
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "routekit-eval-runner-profile-"));
|
|
86
|
+
roots.push(root);
|
|
87
|
+
const suite = path.join(root, "support.eval.ts");
|
|
88
|
+
await writeFile(suite, 'import { test } from "node:test"; test("case", () => {});\n');
|
|
89
|
+
await writeManifest(root, ["openai/cheap", "anthropic/strong"], "openai/judge", ["case"]);
|
|
90
|
+
const manifestPath = path.join(root, "routekit.eval-manifest.json");
|
|
91
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
92
|
+
await writeFile(manifestPath, `${JSON.stringify({ ...manifest, profileId: "different-profile" })}\n`);
|
|
93
|
+
const exit = await Effect.runPromise(withEvalService({}, (service) => service.estimate(requestFor(suite), "pilot")).pipe(Effect.provide(NodeHttpClient.layerUndici), Effect.exit));
|
|
94
|
+
assert.equal(Exit.isFailure(exit), true);
|
|
95
|
+
if (Exit.isFailure(exit)) {
|
|
96
|
+
assert.match(String(exit.cause), /profile or models do not match/u);
|
|
97
|
+
assert.match(String(exit.cause), new RegExp(EvalServiceEstimateError.name));
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
test("production runner fails paid execution clearly when no credential was injected", async () => {
|
|
101
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "routekit-eval-runner-token-"));
|
|
102
|
+
roots.push(root);
|
|
103
|
+
const suite = path.join(root, "support.eval.ts");
|
|
104
|
+
await writeFile(suite, 'import { test } from "node:test"; test("case", () => {});\n');
|
|
105
|
+
await writeManifest(root, ["openai/cheap", "anthropic/strong"], "openai/judge", ["case"]);
|
|
106
|
+
const exit = await Effect.runPromise(withEvalService({}, (service) => service.runComparison(requestFor(suite), "pilot")).pipe(Effect.provide(NodeHttpClient.layerUndici), Effect.exit));
|
|
107
|
+
assert.equal(Exit.isFailure(exit), true);
|
|
108
|
+
if (Exit.isFailure(exit)) {
|
|
109
|
+
assert.match(String(exit.cause), /requires an injected bearer credential/u);
|
|
110
|
+
assert.match(String(exit.cause), new RegExp(EvalServiceCredentialError.name));
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
test("production runner enforces spendLimitUsd before live execution", async () => {
|
|
114
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "routekit-eval-runner-spend-limit-"));
|
|
115
|
+
roots.push(root);
|
|
116
|
+
const suite = path.join(root, "support.eval.ts");
|
|
117
|
+
await writeFile(suite, [
|
|
118
|
+
'import { test } from "node:test";',
|
|
119
|
+
'import { setupAgent, setupJudge } from "routekit/eval";',
|
|
120
|
+
'const judge = setupJudge({ agent: setupAgent({ model: "openai/gpt-5.5" }) });',
|
|
121
|
+
'test("support case", async () => {',
|
|
122
|
+
' const run = await setupAgent({ model: "openai/gpt-5.1" }).run("Help");',
|
|
123
|
+
" run.toComplete();",
|
|
124
|
+
' await judge.autoEvals({ criteria: "Helpful", prompt: "Help", run });',
|
|
125
|
+
"});"
|
|
126
|
+
].join("\n"));
|
|
127
|
+
await writeManifest(root, ["openai/gpt-5.1"], "openai/gpt-5.5", ["support-case"]);
|
|
128
|
+
const exit = await Effect.runPromise(withEvalService({ bearerCredential: "unused-token" }, (service) => service.runComparison({
|
|
129
|
+
...requestFor(suite),
|
|
130
|
+
candidateModels: ["openai/gpt-5.1"],
|
|
131
|
+
judgeModel: "openai/gpt-5.5",
|
|
132
|
+
spendLimitUsd: 0
|
|
133
|
+
}, "pilot")).pipe(Effect.provide(NodeHttpClient.layerUndici), Effect.exit));
|
|
134
|
+
assert.equal(Exit.isFailure(exit), true);
|
|
135
|
+
if (Exit.isFailure(exit)) {
|
|
136
|
+
assert.match(String(exit.cause), /exceeds spendLimitUsd/u);
|
|
137
|
+
assert.match(String(exit.cause), new RegExp(EvalServiceSpendLimitError.name));
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
test("production runner fails closed when a spend limit has unknown pricing", async () => {
|
|
141
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "routekit-eval-runner-spend-unknown-"));
|
|
142
|
+
roots.push(root);
|
|
143
|
+
const suite = path.join(root, "support.eval.ts");
|
|
144
|
+
await writeFile(suite, [
|
|
145
|
+
'import { test } from "node:test";',
|
|
146
|
+
'import { setupAgent, setupJudge } from "routekit/eval";',
|
|
147
|
+
'const judge = setupJudge({ agent: setupAgent({ model: "unknown/judge" }) });',
|
|
148
|
+
'test("support case", async () => {',
|
|
149
|
+
' const run = await setupAgent({ model: "unknown/candidate" }).run("Help");',
|
|
150
|
+
" run.toComplete();",
|
|
151
|
+
' await judge.autoEvals({ criteria: "Helpful", prompt: "Help", run });',
|
|
152
|
+
"});"
|
|
153
|
+
].join("\n"));
|
|
154
|
+
await writeManifest(root, ["unknown/candidate"], "unknown/judge", ["support-case"]);
|
|
155
|
+
const exit = await Effect.runPromise(withEvalService({ bearerCredential: "unused-token" }, (service) => service.runComparison({
|
|
156
|
+
...requestFor(suite),
|
|
157
|
+
candidateModels: ["unknown/candidate"],
|
|
158
|
+
judgeModel: "unknown/judge",
|
|
159
|
+
spendLimitUsd: 100
|
|
160
|
+
}, "pilot")).pipe(Effect.provide(NodeHttpClient.layerUndici), Effect.exit));
|
|
161
|
+
assert.equal(Exit.isFailure(exit), true);
|
|
162
|
+
if (Exit.isFailure(exit)) {
|
|
163
|
+
assert.match(String(exit.cause), /pricing is unknown/u);
|
|
164
|
+
assert.match(String(exit.cause), new RegExp(EvalServiceSpendLimitError.name));
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
test("production runner executes candidate and judge traffic through the live engine", async () => {
|
|
168
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "routekit-eval-runner-live-"));
|
|
169
|
+
roots.push(root);
|
|
170
|
+
const suite = path.join(root, "support.eval.ts");
|
|
171
|
+
await writeFile(suite, [
|
|
172
|
+
'import { test } from "node:test";',
|
|
173
|
+
'import { setupAgent, setupJudge } from "routekit/eval";',
|
|
174
|
+
'const judge = setupJudge({ agent: setupAgent({ model: "openai/judge" }), minScore: 0.8 });',
|
|
175
|
+
'test("support case", async () => {',
|
|
176
|
+
' const run = await setupAgent({ model: "openai/cheap" }).run({ prompt: "Help", caseId: "support-case" });',
|
|
177
|
+
" run.toComplete();",
|
|
178
|
+
' await judge.autoEvals({ criteria: "Helpful", prompt: "Help", run });',
|
|
179
|
+
"});"
|
|
180
|
+
].join("\n"));
|
|
181
|
+
await writeManifest(root, ["openai/cheap"], "openai/judge", ["support-case"]);
|
|
182
|
+
const calls = [];
|
|
183
|
+
const gateway = createServer((incoming, outgoing) => {
|
|
184
|
+
void (async () => {
|
|
185
|
+
const body = JSON.parse(await readBody(incoming));
|
|
186
|
+
calls.push({
|
|
187
|
+
authorization: incoming.headers.authorization,
|
|
188
|
+
maxOutputTokens: body.max_completion_tokens,
|
|
189
|
+
model: body.model
|
|
190
|
+
});
|
|
191
|
+
const content = body.model === "openai/judge"
|
|
192
|
+
? JSON.stringify({ pass: true, reason: "helpful", score: 0.9 })
|
|
193
|
+
: "Helpful answer";
|
|
194
|
+
outgoing.writeHead(200, { "content-type": "application/json" });
|
|
195
|
+
outgoing.end(JSON.stringify({
|
|
196
|
+
model: body.model,
|
|
197
|
+
choices: [{ message: { role: "assistant", content } }]
|
|
198
|
+
}));
|
|
199
|
+
})();
|
|
200
|
+
});
|
|
201
|
+
await new Promise((resolve) => gateway.listen(0, "127.0.0.1", resolve));
|
|
202
|
+
const address = gateway.address();
|
|
203
|
+
assert.ok(address !== null && typeof address !== "string");
|
|
204
|
+
try {
|
|
205
|
+
const result = await Effect.runPromise(withEvalService({ bearerCredential: "parent-only-token" }, (service) => service.runComparison({
|
|
206
|
+
...requestFor(suite, `http://127.0.0.1:${String(address.port)}`),
|
|
207
|
+
candidateModels: ["openai/cheap"]
|
|
208
|
+
}, "pilot")).pipe(Effect.provide(NodeHttpClient.layerUndici)));
|
|
209
|
+
assert.deepEqual(calls.map(({ model }) => model), ["openai/cheap", "openai/judge"]);
|
|
210
|
+
assert.equal(calls.every(({ authorization }) => authorization === "Bearer parent-only-token"), true);
|
|
211
|
+
assert.equal(calls.every(({ maxOutputTokens }) => maxOutputTokens === 1_024), true);
|
|
212
|
+
assert.deepEqual(result.models.map(({ model }) => model), ["openai/cheap"]);
|
|
213
|
+
assert.equal(JSON.stringify(result).includes("parent-only-token"), false);
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
await new Promise((resolve, reject) => gateway.close((error) => (error === undefined ? resolve() : reject(error))));
|
|
217
|
+
}
|
|
218
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { after, test } from "node:test";
|
|
6
|
+
import { layer as NodeServicesLayer } from "@effect/platform-node/NodeServices";
|
|
7
|
+
import { EvalEngine } from "@velum-labs/routekit-eval-engine";
|
|
8
|
+
import { Effect, Fiber, Layer } from "effect";
|
|
9
|
+
import { EvalService, makeEvalServiceLayer } from "../service.js";
|
|
10
|
+
const roots = [];
|
|
11
|
+
after(async () => Promise.all(roots.map((root) => rm(root, { recursive: true, force: true }))));
|
|
12
|
+
const dimensions = [
|
|
13
|
+
"gateway-protocol",
|
|
14
|
+
"eval-routing",
|
|
15
|
+
"account-pooling",
|
|
16
|
+
"typescript-maintenance",
|
|
17
|
+
"release-operations"
|
|
18
|
+
];
|
|
19
|
+
const basis = {
|
|
20
|
+
version: 2,
|
|
21
|
+
basisDigest: "matrix-definition-set",
|
|
22
|
+
dimensions: dimensions.map((id) => ({
|
|
23
|
+
id,
|
|
24
|
+
description: `Requests about ${id}`,
|
|
25
|
+
includes: [`Tasks specifically involving ${id}`],
|
|
26
|
+
excludes: [`Tasks unrelated to ${id}`]
|
|
27
|
+
}))
|
|
28
|
+
};
|
|
29
|
+
const suites = (root) => dimensions.map((dimensionId) => ({
|
|
30
|
+
dimensionId,
|
|
31
|
+
suitePath: path.join(root, dimensionId, `${dimensionId}.eval.ts`)
|
|
32
|
+
}));
|
|
33
|
+
const prepareSuites = async (root, caseIds = ["case-1"]) => {
|
|
34
|
+
await Promise.all(dimensions.map(async (dimensionId) => {
|
|
35
|
+
const directory = path.join(root, dimensionId);
|
|
36
|
+
await mkdir(directory, { recursive: true });
|
|
37
|
+
await writeFile(path.join(directory, `${dimensionId}.eval.ts`), 'import { test } from "node:test"; test("case", () => {});\n');
|
|
38
|
+
await writeFile(path.join(directory, "routekit.eval-manifest.json"), `${JSON.stringify({
|
|
39
|
+
version: 1,
|
|
40
|
+
profileId: dimensionId,
|
|
41
|
+
candidateModels: ["openai/cheap", "anthropic/strong"],
|
|
42
|
+
judgeModel: "openai/judge",
|
|
43
|
+
caseCount: caseIds.length,
|
|
44
|
+
caseIds,
|
|
45
|
+
maxOutputTokens: 256,
|
|
46
|
+
expectedCallCount: caseIds.length * 4
|
|
47
|
+
})}\n`);
|
|
48
|
+
}));
|
|
49
|
+
};
|
|
50
|
+
const mockEngineLayer = (runComparison, onValidate) => Layer.succeed(EvalEngine, EvalEngine.of({
|
|
51
|
+
discover: (target) => Effect.succeed({
|
|
52
|
+
searchRoot: target,
|
|
53
|
+
workingDirectory: path.dirname(target),
|
|
54
|
+
files: [target]
|
|
55
|
+
}),
|
|
56
|
+
validate: (target) => {
|
|
57
|
+
const dimensionId = path.basename(target, ".eval.ts");
|
|
58
|
+
onValidate?.(dimensionId);
|
|
59
|
+
return Effect.succeed({
|
|
60
|
+
searchRoot: target,
|
|
61
|
+
workingDirectory: path.dirname(target),
|
|
62
|
+
files: [target],
|
|
63
|
+
suiteDigest: `suite-${dimensionId}`
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
runComparison
|
|
67
|
+
}));
|
|
68
|
+
const resultFor = (request) => ({
|
|
69
|
+
version: 1,
|
|
70
|
+
comparisonId: `comparison-${request.profileId}`,
|
|
71
|
+
profileId: request.profileId,
|
|
72
|
+
suiteDigest: `suite-${request.profileId}`,
|
|
73
|
+
judgeModel: request.judgeModel,
|
|
74
|
+
startedAt: "2026-08-18T00:00:00.000Z",
|
|
75
|
+
finishedAt: "2026-08-18T00:01:00.000Z",
|
|
76
|
+
models: request.candidateModels.map((model) => ({
|
|
77
|
+
model,
|
|
78
|
+
cases: [
|
|
79
|
+
{
|
|
80
|
+
caseId: "case-1",
|
|
81
|
+
outcome: "passed",
|
|
82
|
+
measurement: {
|
|
83
|
+
judgeScore: model === "openai/cheap" ? 0.9 : 0.95,
|
|
84
|
+
costUsd: model === "openai/cheap" ? 0.001 : 0.1,
|
|
85
|
+
durationMs: model === "openai/cheap" ? 200 : 100
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}))
|
|
90
|
+
});
|
|
91
|
+
const qualification = (root) => ({
|
|
92
|
+
basis,
|
|
93
|
+
candidateModels: ["openai/cheap", "anthropic/strong"],
|
|
94
|
+
classifierModel: "openai/classifier",
|
|
95
|
+
judgeModel: "openai/judge",
|
|
96
|
+
objective: { kind: "highest-quality" },
|
|
97
|
+
maximumUnknownWeight: 0.2,
|
|
98
|
+
suites: suites(root)
|
|
99
|
+
});
|
|
100
|
+
test("dimension matrix qualification inspects every manifest before publishing one activation", async () => {
|
|
101
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "routekit-dimension-matrix-"));
|
|
102
|
+
roots.push(root);
|
|
103
|
+
await prepareSuites(root);
|
|
104
|
+
const events = [];
|
|
105
|
+
const snapshotRoot = path.join(root, "snapshots");
|
|
106
|
+
const engine = mockEngineLayer((request) => Effect.sync(() => {
|
|
107
|
+
events.push(`run:${request.profileId}`);
|
|
108
|
+
return resultFor(request);
|
|
109
|
+
}), (dimensionId) => events.push(`inspect:${dimensionId}`));
|
|
110
|
+
const layer = makeEvalServiceLayer({
|
|
111
|
+
gatewayUrl: "http://127.0.0.1:8080/v1",
|
|
112
|
+
snapshotRoot
|
|
113
|
+
}).pipe(Layer.provide(engine), Layer.provide(NodeServicesLayer));
|
|
114
|
+
const result = await Effect.runPromise(Effect.gen(function* () {
|
|
115
|
+
return yield* (yield* EvalService).qualifyDimensionMatrix(qualification(root));
|
|
116
|
+
}).pipe(Effect.provide(layer)));
|
|
117
|
+
assert.equal(result.comparisons.length, dimensions.length);
|
|
118
|
+
assert.equal(result.snapshot.evidence.length, dimensions.length * 2);
|
|
119
|
+
assert.deepEqual(events, [
|
|
120
|
+
...dimensions.map((id) => `inspect:${id}`),
|
|
121
|
+
...dimensions.map((id) => `run:${id}`)
|
|
122
|
+
]);
|
|
123
|
+
const persisted = JSON.parse(await readFile(path.join(snapshotRoot, "published-routing.json"), "utf8"));
|
|
124
|
+
assert.equal(persisted.evidenceDigest, result.snapshot.evidenceDigest);
|
|
125
|
+
});
|
|
126
|
+
test("dimension matrix qualification never publishes incomplete comparison evidence", async () => {
|
|
127
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "routekit-dimension-matrix-incomplete-"));
|
|
128
|
+
roots.push(root);
|
|
129
|
+
await prepareSuites(root, ["case-1", "case-2"]);
|
|
130
|
+
const snapshotRoot = path.join(root, "snapshots");
|
|
131
|
+
const engine = mockEngineLayer((request) => Effect.succeed(resultFor(request)));
|
|
132
|
+
const layer = makeEvalServiceLayer({
|
|
133
|
+
gatewayUrl: "http://127.0.0.1:8080/v1",
|
|
134
|
+
snapshotRoot
|
|
135
|
+
}).pipe(Layer.provide(engine), Layer.provide(NodeServicesLayer));
|
|
136
|
+
const exit = await Effect.runPromiseExit(Effect.gen(function* () {
|
|
137
|
+
return yield* (yield* EvalService).qualifyDimensionMatrix(qualification(root));
|
|
138
|
+
}).pipe(Effect.provide(layer)));
|
|
139
|
+
assert.equal(exit._tag, "Failure");
|
|
140
|
+
await assert.rejects(readFile(path.join(snapshotRoot, "published-routing.json"), "utf8"), /ENOENT/u);
|
|
141
|
+
});
|
|
142
|
+
test("interrupted qualification leaves no half-published activation for a daemon restart", async () => {
|
|
143
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "routekit-dimension-matrix-interrupted-"));
|
|
144
|
+
roots.push(root);
|
|
145
|
+
await prepareSuites(root);
|
|
146
|
+
const snapshotRoot = path.join(root, "snapshots");
|
|
147
|
+
let runs = 0;
|
|
148
|
+
const engine = mockEngineLayer((request) => {
|
|
149
|
+
runs += 1;
|
|
150
|
+
return runs === 1 ? Effect.succeed(resultFor(request)) : Effect.never;
|
|
151
|
+
});
|
|
152
|
+
const layer = makeEvalServiceLayer({
|
|
153
|
+
gatewayUrl: "http://127.0.0.1:8080/v1",
|
|
154
|
+
snapshotRoot
|
|
155
|
+
}).pipe(Layer.provide(engine), Layer.provide(NodeServicesLayer));
|
|
156
|
+
await Effect.runPromise(Effect.gen(function* () {
|
|
157
|
+
const fiber = yield* Effect.forkChild((yield* EvalService).qualifyDimensionMatrix(qualification(root)));
|
|
158
|
+
yield* Effect.yieldNow;
|
|
159
|
+
yield* Fiber.interrupt(fiber);
|
|
160
|
+
}).pipe(Effect.provide(layer)));
|
|
161
|
+
await assert.rejects(readFile(path.join(snapshotRoot, "published-routing.json"), "utf8"), /ENOENT/u);
|
|
162
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@velum-labs/routekit-eval-service",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/velum-labs/routekit.git",
|
|
8
|
+
"directory": "packages/eval-service"
|
|
9
|
+
},
|
|
10
|
+
"description": "Effect composition for RouteKit eval comparisons and routing policy publication.",
|
|
11
|
+
"license": "Apache-2.0",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./effect": {
|
|
19
|
+
"types": "./dist/effect-api.d.ts",
|
|
20
|
+
"default": "./dist/effect-api.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"registry": "https://registry.npmjs.org",
|
|
29
|
+
"access": "public",
|
|
30
|
+
"provenance": true
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@effect/platform-node": "4.0.0-rc.108",
|
|
34
|
+
"effect": "4.0.0-rc.108",
|
|
35
|
+
"@velum-labs/routekit-eval-contracts": "1.0.0",
|
|
36
|
+
"@velum-labs/routekit-registry": "1.0.0",
|
|
37
|
+
"@velum-labs/routekit-eval-engine": "1.0.0",
|
|
38
|
+
"@velum-labs/routekit-eval-store": "1.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"typescript": "7.0.2"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"routekit",
|
|
45
|
+
"eval",
|
|
46
|
+
"routing",
|
|
47
|
+
"effect"
|
|
48
|
+
],
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsc -b",
|
|
51
|
+
"clean": "tsc -b --clean",
|
|
52
|
+
"test": "node --test \"dist/test/*.test.js\""
|
|
53
|
+
}
|
|
54
|
+
}
|