auto-model-router 0.31.0 → 0.32.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/.omp-plugin/marketplace.json +2 -2
- package/README.md +32 -2
- package/omp-extension/router-configure.ts +9 -7
- package/package.json +1 -1
- package/src/cli/config-cmd.ts +8 -7
- package/src/cli/explain.ts +10 -5
- package/src/cli/export.ts +6 -5
- package/src/cli/models.ts +10 -7
- package/src/cli/report.ts +6 -1
- package/src/cli/stats.ts +7 -7
- package/src/config/load.ts +10 -1
- package/src/config/types.ts +10 -1
- package/src/context/bridge.ts +7 -7
- package/src/context/index.ts +3 -3
- package/src/context/store.ts +39 -56
- package/src/context/types.ts +7 -6
- package/src/cost/blended.ts +28 -7
- package/src/cost/feedback.ts +33 -37
- package/src/cost/ledger-sql.ts +547 -0
- package/src/cost/ledger.ts +30 -459
- package/src/cost/report.ts +171 -129
- package/src/cost/retention.ts +10 -10
- package/src/cost/summary.ts +15 -10
- package/src/cost/types.ts +43 -62
- package/src/cost/views.ts +79 -49
- package/src/lib.ts +6 -2
- package/src/router/candidates.ts +7 -15
- package/src/router/classify.ts +6 -4
- package/src/router/index.ts +95 -9
- package/src/router/select.ts +38 -21
- package/src/router/state.ts +90 -102
- package/src/router/types.ts +11 -5
- package/src/server/advise.ts +6 -4
- package/src/server/compaction-digest.ts +1 -1
- package/src/server/digest.ts +9 -10
- package/src/server/http.ts +101 -41
- package/src/server/providers.ts +18 -4
- package/src/server/turn.ts +32 -9
- package/src/tokens/estimate.ts +16 -6
- package/src/upstream/ollama-usage.ts +21 -11
- package/src/util/schema.ts +201 -0
- package/src/util/sql.ts +246 -0
- package/src/wire/anthropic/messages.ts +3 -4
- package/src/wire/openai/request.ts +1 -0
- package/src/wire/types.ts +7 -0
- package/test/anthropic-wire.test.ts +9 -9
- package/test/benchmark-feeds.test.ts +7 -7
- package/test/cache-control.test.ts +7 -7
- package/test/cache-estimate.test.ts +5 -5
- package/test/catalog-view.test.ts +4 -4
- package/test/catalog.test.ts +11 -11
- package/test/classify.test.ts +24 -24
- package/test/compaction.test.ts +20 -20
- package/test/config-wizard.test.ts +32 -32
- package/test/config.test.ts +10 -10
- package/test/connect-harnesses.test.ts +11 -11
- package/test/context-bridge.test.ts +40 -30
- package/test/context-prune.test.ts +43 -36
- package/test/context-query.test.ts +8 -8
- package/test/controls.test.ts +54 -27
- package/test/cost.test.ts +12 -12
- package/test/digest.test.ts +55 -44
- package/test/embed-lifecycle.test.ts +5 -5
- package/test/embed-logic.test.ts +26 -26
- package/test/escalate.test.ts +17 -17
- package/test/eval.test.ts +13 -13
- package/test/executable.test.ts +6 -6
- package/test/exploration.test.ts +19 -20
- package/test/failover.test.ts +22 -21
- package/test/fakes.ts +105 -0
- package/test/features.test.ts +21 -21
- package/test/harness-requests.test.ts +3 -3
- package/test/harness-switch.test.ts +5 -5
- package/test/hold-exploration.test.ts +13 -13
- package/test/hot-reload.test.ts +5 -5
- package/test/learned.test.ts +5 -5
- package/test/ledger-sql.test.ts +342 -0
- package/test/mcp-entry.test.ts +5 -5
- package/test/migrations.test.ts +28 -22
- package/test/models-yml.test.ts +18 -18
- package/test/ollama.test.ts +40 -34
- package/test/omp-credentials.test.ts +16 -16
- package/test/policy.test.ts +3 -3
- package/test/reconfigure.test.ts +4 -4
- package/test/redaction.test.ts +41 -35
- package/test/remote.test.ts +12 -12
- package/test/report-logic.test.ts +8 -8
- package/test/report.test.ts +95 -87
- package/test/retention.test.ts +79 -66
- package/test/schema.test.ts +123 -0
- package/test/scope.test.ts +8 -8
- package/test/select.test.ts +216 -257
- package/test/skills.test.ts +3 -3
- package/test/sql-shim.test.ts +154 -0
- package/test/state.test.ts +43 -36
- package/test/summary.test.ts +38 -27
- package/test/tier-plan.test.ts +45 -62
- package/test/toast-logic.test.ts +31 -31
- package/test/tokens.test.ts +95 -80
- package/test/trust-attribution.test.ts +217 -187
- package/test/trust-window.test.ts +37 -32
- package/test/turn.test.ts +55 -23
- package/test/upstreams.test.ts +13 -13
- package/test/views.test.ts +81 -59
- package/test/wire-request.test.ts +17 -17
- package/test/wire-responses.test.ts +4 -4
- package/tools/agentdox-e2e.ts +5 -2
- package/tools/export-benchmarks.ts +5 -5
- package/tools/ledger-parity.ts +266 -0
- package/tools/replay.ts +16 -8
package/test/redaction.test.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
2
|
import { tmpdir } from "node:os";
|
|
4
3
|
import { join } from "node:path";
|
|
5
4
|
|
|
5
|
+
import { openSqlDb } from "../src/util/sql.ts";
|
|
6
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
|
|
6
8
|
import { DEFAULT_CONFIG } from "../src/config/defaults.ts";
|
|
7
9
|
import { loadConfig } from "../src/config/load.ts";
|
|
8
10
|
import { compileRedactionRules, redactionRulesFor, validateRedactionPattern, validateRedactionRule } from "../src/config/redaction.ts";
|
|
9
|
-
import { createLedger } from "../src/cost/ledger.ts";
|
|
10
11
|
import { buildUsageReport, renderUsageReport } from "../src/cost/report.ts";
|
|
12
|
+
import { createSqlLedger } from "../src/cost/ledger-sql.ts";
|
|
13
|
+
import { migrateStore } from "../src/util/schema.ts";
|
|
11
14
|
import { EMPTY_USAGE, type LedgerEntry } from "../src/cost/types.ts";
|
|
12
15
|
import { redactUpstreamBody } from "../src/server/redact.ts";
|
|
13
|
-
import { openDb } from "../src/util/sqlite.ts";
|
|
14
16
|
import { parseMessagesRequest } from "../src/wire/anthropic/messages.ts";
|
|
15
17
|
import { parseChatRequest } from "../src/wire/openai/request.ts";
|
|
16
18
|
import { parseResponsesRequest } from "../src/wire/openai/responses.ts";
|
|
@@ -27,7 +29,7 @@ function rules(...specs: { name: string; pattern: string; replacement?: string }
|
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
describe("pattern guard", () => {
|
|
30
|
-
test("the shapes a real rule uses all compile", () => {
|
|
32
|
+
test("the shapes a real rule uses all compile", async () => {
|
|
31
33
|
for (const pattern of [
|
|
32
34
|
"sk-live-[a-z0-9]{16,}",
|
|
33
35
|
"AKIA[0-9A-Z]{16}",
|
|
@@ -40,7 +42,7 @@ describe("pattern guard", () => {
|
|
|
40
42
|
}
|
|
41
43
|
});
|
|
42
44
|
|
|
43
|
-
test("a pattern that can backtrack catastrophically is refused, with the reason", () => {
|
|
45
|
+
test("a pattern that can backtrack catastrophically is refused, with the reason", async () => {
|
|
44
46
|
const nested = validateRedactionPattern("(a+)+$");
|
|
45
47
|
expect(nested).toContain("nested unbounded quantifiers");
|
|
46
48
|
expect(nested).toContain("exponential");
|
|
@@ -54,14 +56,14 @@ describe("pattern guard", () => {
|
|
|
54
56
|
expect(validateRedactionPattern("(?<k>x)\\k<k>")).toContain("backreferences");
|
|
55
57
|
});
|
|
56
58
|
|
|
57
|
-
test("a pattern that cannot compile, is empty, matches nothing, or is enormous is refused", () => {
|
|
59
|
+
test("a pattern that cannot compile, is empty, matches nothing, or is enormous is refused", async () => {
|
|
58
60
|
expect(validateRedactionPattern("(unclosed")).toContain("does not compile");
|
|
59
61
|
expect(validateRedactionPattern("")).toBe("pattern must not be empty");
|
|
60
62
|
expect(validateRedactionPattern("x*")).toContain("matches the empty string");
|
|
61
63
|
expect(validateRedactionPattern(`a${"b".repeat(600)}`)).toContain("the limit is 512");
|
|
62
64
|
});
|
|
63
65
|
|
|
64
|
-
test("a legacy pattern the unicode flag alone rejects still loads", () => {
|
|
66
|
+
test("a legacy pattern the unicode flag alone rejects still loads", async () => {
|
|
65
67
|
// `\d{1,2}` is fine either way; an unescaped `{` is a syntax error under
|
|
66
68
|
// `u` and a literal brace without it. An operator's working rule must not
|
|
67
69
|
// break on an upgrade, so `u` is preferred, not required.
|
|
@@ -70,19 +72,19 @@ describe("pattern guard", () => {
|
|
|
70
72
|
expect(rules({ name: "unicode", pattern: "sk-[a-z0-9]+" })[0]!.regex.flags).toBe("gu");
|
|
71
73
|
});
|
|
72
74
|
|
|
73
|
-
test("a rule name is checked too, since it is echoed into the prompt", () => {
|
|
75
|
+
test("a rule name is checked too, since it is echoed into the prompt", async () => {
|
|
74
76
|
expect(validateRedactionRule({ name: "api key", pattern: "sk-\\w+" })).toBeNull();
|
|
75
77
|
expect(validateRedactionRule({ name: "", pattern: "sk-\\w+" })).toContain("name must be");
|
|
76
78
|
expect(validateRedactionRule({ name: "[redacted]\ninjected", pattern: "sk-\\w+" })).toContain("name must be");
|
|
77
79
|
});
|
|
78
80
|
|
|
79
|
-
test("compiling a rule set names the offending rule and refuses the whole set", () => {
|
|
81
|
+
test("compiling a rule set names the offending rule and refuses the whole set", async () => {
|
|
80
82
|
expect(() => rules({ name: "ok", pattern: "sk-\\w+" }, { name: "greedy", pattern: "(x+)+" })).toThrow(
|
|
81
83
|
/redaction rule "greedy": nested unbounded quantifiers/,
|
|
82
84
|
);
|
|
83
85
|
});
|
|
84
86
|
|
|
85
|
-
test("the config file rejects a backtracking rule at load, naming its path", () => {
|
|
87
|
+
test("the config file rejects a backtracking rule at load, naming its path", async () => {
|
|
86
88
|
const dir = mkdtempSync(join(tmpdir(), "amr-redact-"));
|
|
87
89
|
try {
|
|
88
90
|
const path = join(dir, "config.yml");
|
|
@@ -99,7 +101,7 @@ describe("pattern guard", () => {
|
|
|
99
101
|
}
|
|
100
102
|
});
|
|
101
103
|
|
|
102
|
-
test("compiled rules are memoised on the rule text, and off means none", () => {
|
|
104
|
+
test("compiled rules are memoised on the rule text, and off means none", async () => {
|
|
103
105
|
const cfg = { enabled: true, rules: [{ name: "key", pattern: "sk-\\w+" }], scanTools: false };
|
|
104
106
|
expect(redactionRulesFor(cfg)).toBe(redactionRulesFor({ ...cfg, rules: [{ name: "key", pattern: "sk-\\w+" }] }));
|
|
105
107
|
expect(redactionRulesFor({ ...cfg, enabled: false })).toEqual([]);
|
|
@@ -112,19 +114,19 @@ describe("pattern guard", () => {
|
|
|
112
114
|
describe("each rule shape", () => {
|
|
113
115
|
const body = () => ({ messages: [{ role: "user", content: `token ${SECRET} and 10.1.2.3` }] });
|
|
114
116
|
|
|
115
|
-
test("a rule with no replacement redacts to [redacted:<name>]", () => {
|
|
117
|
+
test("a rule with no replacement redacts to [redacted:<name>]", async () => {
|
|
116
118
|
const b = body();
|
|
117
119
|
expect(redactUpstreamBody(b, rules({ name: "api key", pattern: "sk-live-[a-z0-9]+" }), { scanTools: false })).toBe(1);
|
|
118
120
|
expect(b.messages[0]!.content).toBe("token [redacted:api key] and 10.1.2.3");
|
|
119
121
|
});
|
|
120
122
|
|
|
121
|
-
test("a rule with a replacement uses it", () => {
|
|
123
|
+
test("a rule with a replacement uses it", async () => {
|
|
122
124
|
const b = body();
|
|
123
125
|
redactUpstreamBody(b, rules({ name: "ip", pattern: "(?:\\d{1,3}\\.){3}\\d{1,3}", replacement: "0.0.0.0" }), { scanTools: false });
|
|
124
126
|
expect(b.messages[0]!.content).toBe(`token ${SECRET} and 0.0.0.0`);
|
|
125
127
|
});
|
|
126
128
|
|
|
127
|
-
test("several rules all apply, and every match counts", () => {
|
|
129
|
+
test("several rules all apply, and every match counts", async () => {
|
|
128
130
|
const b = { messages: [{ role: "user", content: `${SECRET} ${SECRET} 10.1.2.3` }] };
|
|
129
131
|
const n = redactUpstreamBody(
|
|
130
132
|
b,
|
|
@@ -135,7 +137,7 @@ describe("each rule shape", () => {
|
|
|
135
137
|
expect(b.messages[0]!.content).toBe("[redacted:key] [redacted:key] [redacted:ip]");
|
|
136
138
|
});
|
|
137
139
|
|
|
138
|
-
test("nothing to match leaves the body untouched and counts zero", () => {
|
|
140
|
+
test("nothing to match leaves the body untouched and counts zero", async () => {
|
|
139
141
|
const b = { messages: [{ role: "user", content: "nothing secret here" }] };
|
|
140
142
|
expect(redactUpstreamBody(b, rules({ name: "key", pattern: "sk-live-[a-z0-9]+" }), { scanTools: false })).toBe(0);
|
|
141
143
|
expect(b.messages[0]!.content).toBe("nothing secret here");
|
|
@@ -157,7 +159,7 @@ describe("what the walker reaches", () => {
|
|
|
157
159
|
};
|
|
158
160
|
}
|
|
159
161
|
|
|
160
|
-
test("message text is redacted; tool arguments and tool results wait for scanTools", () => {
|
|
162
|
+
test("message text is redacted; tool arguments and tool results wait for scanTools", async () => {
|
|
161
163
|
const b = fullBody();
|
|
162
164
|
expect(redactUpstreamBody(b, KEY, { scanTools: false })).toBe(2);
|
|
163
165
|
const messages = b.messages as Record<string, unknown>[];
|
|
@@ -171,7 +173,7 @@ describe("what the walker reaches", () => {
|
|
|
171
173
|
expect(b.model).toBe("sk-live-notreally/model");
|
|
172
174
|
});
|
|
173
175
|
|
|
174
|
-
test("with scanTools, arguments and results go too and nothing else changes", () => {
|
|
176
|
+
test("with scanTools, arguments and results go too and nothing else changes", async () => {
|
|
175
177
|
const b = fullBody();
|
|
176
178
|
expect(redactUpstreamBody(b, KEY, { scanTools: true })).toBe(4);
|
|
177
179
|
const messages = b.messages as Record<string, unknown>[];
|
|
@@ -184,7 +186,7 @@ describe("what the walker reaches", () => {
|
|
|
184
186
|
expect(messages[3]!.tool_call_id).toBe("c1");
|
|
185
187
|
});
|
|
186
188
|
|
|
187
|
-
test("no rules is a no-op, and a body without messages is ignored", () => {
|
|
189
|
+
test("no rules is a no-op, and a body without messages is ignored", async () => {
|
|
188
190
|
const b = fullBody();
|
|
189
191
|
expect(redactUpstreamBody(b, [], { scanTools: true })).toBe(0);
|
|
190
192
|
expect(JSON.stringify(b)).toContain(SECRET);
|
|
@@ -204,7 +206,7 @@ describe("every wire renders into the shape redaction reads", () => {
|
|
|
204
206
|
stripAssistantReasoning: false,
|
|
205
207
|
};
|
|
206
208
|
|
|
207
|
-
test("chat completions, Responses and Anthropic Messages all lose the secret", () => {
|
|
209
|
+
test("chat completions, Responses and Anthropic Messages all lose the secret", async () => {
|
|
208
210
|
const chat = parseChatRequest(
|
|
209
211
|
{ model: "auto", messages: [{ role: "system", content: `rules ${SECRET}` }, { role: "user", content: `use ${SECRET}` }] },
|
|
210
212
|
new Headers(),
|
|
@@ -265,36 +267,40 @@ describe("the ledger keeps the count and never the match", () => {
|
|
|
265
267
|
};
|
|
266
268
|
}
|
|
267
269
|
|
|
268
|
-
test("the column round-trips, absent stays absent, and the report totals it", () => {
|
|
269
|
-
const
|
|
270
|
+
test("the column round-trips, absent stays absent, and the report totals it", async () => {
|
|
271
|
+
const path = join(tmpdir(), `redaction-${process.pid}-${Date.now()}.db`);
|
|
272
|
+
const db = openSqlDb(path);
|
|
270
273
|
try {
|
|
271
|
-
|
|
274
|
+
await migrateStore(db);
|
|
275
|
+
const ledger = createSqlLedger(db, { ...DEFAULT_CONFIG, ledger: { ...DEFAULT_CONFIG.ledger, path } }, { findModel: () => null });
|
|
272
276
|
const now = Date.now();
|
|
273
|
-
ledger.record(entry({ createdAtMs: now - 3, redactions: 3 }));
|
|
274
|
-
ledger.record(entry({ createdAtMs: now - 2, redactions: 0 }));
|
|
275
|
-
ledger.record(entry({ createdAtMs: now - 1 })); // redaction off: NULL, which is not 0
|
|
276
|
-
const recorded = ledger.recentEntries(10);
|
|
277
|
+
await ledger.record(entry({ createdAtMs: now - 3, redactions: 3 }));
|
|
278
|
+
await ledger.record(entry({ createdAtMs: now - 2, redactions: 0 }));
|
|
279
|
+
await ledger.record(entry({ createdAtMs: now - 1 })); // redaction off: NULL, which is not 0
|
|
280
|
+
const recorded = await ledger.recentEntries(10);
|
|
277
281
|
expect(recorded.map((e) => e.redactions)).toEqual([undefined, 0, 3]);
|
|
278
|
-
const totals = buildUsageReport(db, { windowDays: 7 }).totals;
|
|
282
|
+
const totals = (await buildUsageReport(db, { windowDays: 7 })).totals;
|
|
279
283
|
expect(totals.redactions).toBe(3);
|
|
280
284
|
expect(totals.redactedTurns).toBe(1);
|
|
281
|
-
expect(renderUsageReport(buildUsageReport(db, { windowDays: 7 }))).toContain("redaction: 1 turns had something removed (3 strings)");
|
|
285
|
+
expect(renderUsageReport(await buildUsageReport(db, { windowDays: 7 }))).toContain("redaction: 1 turns had something removed (3 strings)");
|
|
282
286
|
} finally {
|
|
283
|
-
db.close();
|
|
287
|
+
await db.close();
|
|
284
288
|
}
|
|
285
289
|
});
|
|
286
290
|
|
|
287
|
-
test("a window with no redaction says nothing about it", () => {
|
|
288
|
-
const
|
|
291
|
+
test("a window with no redaction says nothing about it", async () => {
|
|
292
|
+
const path = join(tmpdir(), `redaction-none-${process.pid}-${Date.now()}.db`);
|
|
293
|
+
const db = openSqlDb(path);
|
|
289
294
|
try {
|
|
290
|
-
|
|
291
|
-
ledger
|
|
292
|
-
|
|
295
|
+
await migrateStore(db);
|
|
296
|
+
const ledger = createSqlLedger(db, { ...DEFAULT_CONFIG, ledger: { ...DEFAULT_CONFIG.ledger, path } }, { findModel: () => null });
|
|
297
|
+
await ledger.record(entry({}));
|
|
298
|
+
const report = await buildUsageReport(db, { windowDays: 7 });
|
|
293
299
|
expect(report.totals.redactions).toBe(0);
|
|
294
300
|
expect(report.totals.redactedTurns).toBe(0);
|
|
295
301
|
expect(renderUsageReport(report)).not.toContain("redaction:");
|
|
296
302
|
} finally {
|
|
297
|
-
db.close();
|
|
303
|
+
await db.close();
|
|
298
304
|
}
|
|
299
305
|
});
|
|
300
306
|
});
|
package/test/remote.test.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { hasRefresh, refreshAccountOf } from "../omp-extension/remote-logic.ts";
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
describe("remote-logic", () => {
|
|
21
|
-
test("remote.json is parsed defensively and turned into omp's provider registration", () => {
|
|
21
|
+
test("remote.json is parsed defensively and turned into omp's provider registration", async () => {
|
|
22
22
|
expect(parseRemoteRouter("nope")).toBeNull();
|
|
23
23
|
expect(parseRemoteRouter(JSON.stringify({ url: "https://t/", key: "" }))).toBeNull();
|
|
24
24
|
const t = parseRemoteRouter(JSON.stringify({ url: "https://team.example/", key: "amrt_k", userId: "u_1", name: "Ada" }))!;
|
|
@@ -44,7 +44,7 @@ describe("remote-logic", () => {
|
|
|
44
44
|
rmSync(dir, { recursive: true, force: true });
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
test("text edits keep files byte-identical apart from the lines they add", () => {
|
|
47
|
+
test("text edits keep files byte-identical apart from the lines they add", async () => {
|
|
48
48
|
expect(addExtensions("", ["/a.ts"])).toBe("extensions:\n - /a.ts\n");
|
|
49
49
|
expect(addExtensions("foo: 1\nextensions:\n - /x.ts\nbar: 2\n", ["/x.ts", "/a.ts"])).toBe("foo: 1\nextensions:\n - /a.ts\n - /x.ts\nbar: 2\n");
|
|
50
50
|
expect(addExtensions("foo: 1\r\n", ["/a.ts"])).toBe("foo: 1\r\nextensions:\r\n - /a.ts\r\n");
|
|
@@ -68,7 +68,7 @@ describe("connect", () => {
|
|
|
68
68
|
return { home, o };
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
test("writes remote.json and configures omp, Hermes, Codex, Aider and Claude Code idempotently", () => {
|
|
71
|
+
test("writes remote.json and configures omp, Hermes, Codex, Aider and Claude Code idempotently", async () => {
|
|
72
72
|
const { home, o } = scenario();
|
|
73
73
|
const r1 = connectRemote(o);
|
|
74
74
|
expect(existsSync(r1.remoteFile)).toBe(true);
|
|
@@ -93,7 +93,7 @@ describe("connect", () => {
|
|
|
93
93
|
rmSync(home, { recursive: true, force: true });
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
test("--harness restricts, --dry-run writes nothing, --profile appends once to the shell rc", () => {
|
|
96
|
+
test("--harness restricts, --dry-run writes nothing, --profile appends once to the shell rc", async () => {
|
|
97
97
|
const { home, o } = scenario({ only: ["omp"], dryRun: true });
|
|
98
98
|
const r = connectRemote(o);
|
|
99
99
|
expect(existsSync(r.remoteFile)).toBe(false);
|
|
@@ -118,7 +118,7 @@ describe("omp models.yml for a remote router", () => {
|
|
|
118
118
|
const NL = String.fromCharCode(10);
|
|
119
119
|
const yaml = (...lines: string[]): string => lines.join(NL) + NL;
|
|
120
120
|
|
|
121
|
-
test("the block names the remote, the key and the three virtual models; the scope follows the workspace unless pinned", () => {
|
|
121
|
+
test("the block names the remote, the key and the three virtual models; the scope follows the workspace unless pinned", async () => {
|
|
122
122
|
const block = renderRemoteModelsYml("https://team.example/", "amrt_k", BLEND);
|
|
123
123
|
expect(block).toContain("baseUrl: https://team.example/v1");
|
|
124
124
|
expect(block).toContain("apiKey: amrt_k");
|
|
@@ -141,7 +141,7 @@ describe("omp models.yml for a remote router", () => {
|
|
|
141
141
|
expect(pinned).toContain(`X-Agentdox-Origin: ${ORIGIN_ENV}`);
|
|
142
142
|
});
|
|
143
143
|
|
|
144
|
-
test("merging keeps other providers, replaces our own block, and is idempotent", () => {
|
|
144
|
+
test("merging keeps other providers, replaces our own block, and is idempotent", async () => {
|
|
145
145
|
const block = renderRemoteModelsYml("https://team.example", "k1", BLEND);
|
|
146
146
|
const empty = mergeModelsYml("", block);
|
|
147
147
|
expect(empty.startsWith("providers:")).toBe(true);
|
|
@@ -160,7 +160,7 @@ describe("omp models.yml for a remote router", () => {
|
|
|
160
160
|
expect(rekeyed).toContain("openai:");
|
|
161
161
|
});
|
|
162
162
|
|
|
163
|
-
test("a hand-written provider of the same name is left alone", () => {
|
|
163
|
+
test("a hand-written provider of the same name is left alone", async () => {
|
|
164
164
|
expect(hasForeignRouterProvider(yaml("providers:", " auto-model-router:", " baseUrl: http://127.0.0.1:1/v1"))).toBe(true);
|
|
165
165
|
expect(hasForeignRouterProvider(mergeModelsYml("", renderRemoteModelsYml("https://t", "k", BLEND)))).toBe(false);
|
|
166
166
|
expect(hasForeignRouterProvider(yaml("providers:", " openai: {}"))).toBe(false);
|
|
@@ -171,7 +171,7 @@ describe("short-lived remote credentials", () => {
|
|
|
171
171
|
const NL = String.fromCharCode(10);
|
|
172
172
|
const remote = { url: "https://team.example", key: "amrt_old", userId: "u_ada", name: "Ada", joinedAtMs: 1, refreshToken: "amrr_r1", keyExpiresAtMs: 0, refreshExpiresAtMs: 0, device: "laptop" };
|
|
173
173
|
|
|
174
|
-
test("remote.json round-trips the credential fields, and a permanent key has none", () => {
|
|
174
|
+
test("remote.json round-trips the credential fields, and a permanent key has none", async () => {
|
|
175
175
|
const parsed = parseRemoteRouter(JSON.stringify(remote))!;
|
|
176
176
|
expect(parsed).toMatchObject({ refreshToken: "amrr_r1", keyExpiresAtMs: 0, refreshExpiresAtMs: 0, device: "laptop" });
|
|
177
177
|
const permanent = parseRemoteRouter(JSON.stringify({ url: "https://t", key: "k" }))!;
|
|
@@ -179,7 +179,7 @@ describe("short-lived remote credentials", () => {
|
|
|
179
179
|
expect(shouldRefresh(permanent)).toBe(false);
|
|
180
180
|
});
|
|
181
181
|
|
|
182
|
-
test("a key is refreshed a day ahead of expiry, never without a refresh token", () => {
|
|
182
|
+
test("a key is refreshed a day ahead of expiry, never without a refresh token", async () => {
|
|
183
183
|
const now = 1_000_000_000_000;
|
|
184
184
|
const day = 24 * 3_600_000;
|
|
185
185
|
expect(shouldRefresh({ ...remote, keyExpiresAtMs: now + 3 * day }, now)).toBe(false);
|
|
@@ -206,7 +206,7 @@ describe("short-lived remote credentials", () => {
|
|
|
206
206
|
}
|
|
207
207
|
expect(err?.code).toBe("refresh_reused");
|
|
208
208
|
expect(err?.message).toBe("already used");
|
|
209
|
-
await expect(refreshCredential({ ...remote, refreshToken: "" }, ok)).rejects.toBeInstanceOf(RefreshError);
|
|
209
|
+
(await expect(refreshCredential({ ...remote, refreshToken: "" }, ok))).rejects.toBeInstanceOf(RefreshError);
|
|
210
210
|
});
|
|
211
211
|
|
|
212
212
|
test("refreshAndRewrite re-writes remote.json and the managed models.yml block, keeping its scope and join time", async () => {
|
|
@@ -252,7 +252,7 @@ describe("the refresh token lives in the OS credential store", () => {
|
|
|
252
252
|
const vault = new Map<string, string>();
|
|
253
253
|
const backend = { save: (a: string, s: string) => void vault.set(a, s), load: (a: string) => vault.get(a) ?? null, remove: (a: string) => void vault.delete(a) };
|
|
254
254
|
|
|
255
|
-
test("the store is picked from the platform and its tools; the file is the fallback everywhere", () => {
|
|
255
|
+
test("the store is picked from the platform and its tools; the file is the fallback everywhere", async () => {
|
|
256
256
|
expect(pickStore("win32", (b) => b === "powershell")).toBe("dpapi");
|
|
257
257
|
expect(pickStore("win32", () => false)).toBe("file");
|
|
258
258
|
expect(pickStore("darwin", (b) => b === "security")).toBe("keychain");
|
|
@@ -261,7 +261,7 @@ describe("the refresh token lives in the OS credential store", () => {
|
|
|
261
261
|
expect(refreshAccountOf("https://team.example:8790/", "u_ada")).toBe("u_ada@team.example:8790");
|
|
262
262
|
});
|
|
263
263
|
|
|
264
|
-
test("save/load through a store, and the file fallback keeps the token owner-readable", () => {
|
|
264
|
+
test("save/load through a store, and the file fallback keeps the token owner-readable", async () => {
|
|
265
265
|
const home = mkdtempSync(join(tmpdir(), "amr-store-"));
|
|
266
266
|
try {
|
|
267
267
|
expect(saveRefreshToken(home, "u@t", "amrr_x", "keychain", { backend })).toBe("keychain");
|
|
@@ -3,11 +3,11 @@ import { describe, expect, test } from "bun:test";
|
|
|
3
3
|
import { fetchReport, parseReportArgs, renderSoftFailureSpikes, renderStatus, type HealthSnapshot } from "../omp-extension/report-logic.ts";
|
|
4
4
|
|
|
5
5
|
describe("parseReportArgs", () => {
|
|
6
|
-
test("defaults to 7 days scoped to the harness", () => {
|
|
6
|
+
test("defaults to 7 days scoped to the harness", async () => {
|
|
7
7
|
expect(parseReportArgs("", "omp")).toEqual({ windowDays: 7, harnessId: "omp" });
|
|
8
8
|
});
|
|
9
9
|
|
|
10
|
-
test("accepts bare numbers and d/h/w suffixes", () => {
|
|
10
|
+
test("accepts bare numbers and d/h/w suffixes", async () => {
|
|
11
11
|
expect(parseReportArgs("30", "")).toEqual({ windowDays: 30, harnessId: "" });
|
|
12
12
|
expect(parseReportArgs("14d", "")).toEqual({ windowDays: 14, harnessId: "" });
|
|
13
13
|
expect(parseReportArgs("24h", "")).toEqual({ windowDays: 1, harnessId: "" });
|
|
@@ -15,12 +15,12 @@ describe("parseReportArgs", () => {
|
|
|
15
15
|
expect(parseReportArgs("2w", "")).toEqual({ windowDays: 14, harnessId: "" });
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
test("--all drops the harness scope and --harness= sets one", () => {
|
|
18
|
+
test("--all drops the harness scope and --harness= sets one", async () => {
|
|
19
19
|
expect(parseReportArgs("7d --all", "omp").harnessId).toBe("");
|
|
20
20
|
expect(parseReportArgs("--harness=hermes", "omp").harnessId).toBe("hermes");
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
test("ignores junk and clamps the window", () => {
|
|
23
|
+
test("ignores junk and clamps the window", async () => {
|
|
24
24
|
expect(parseReportArgs("bogus 0 -3", "x")).toEqual({ windowDays: 7, harnessId: "x" });
|
|
25
25
|
expect(parseReportArgs("9999", "").windowDays).toBe(365);
|
|
26
26
|
});
|
|
@@ -45,13 +45,13 @@ describe("fetchReport", () => {
|
|
|
45
45
|
seen.push(url);
|
|
46
46
|
return new Response("nope", { status: 503 });
|
|
47
47
|
};
|
|
48
|
-
await expect(fetchReport("http://h", { windowDays: 7, harnessId: "" }, {}, fake)).rejects.toThrow("503");
|
|
48
|
+
(await expect(fetchReport("http://h", { windowDays: 7, harnessId: "" }, {}, fake))).rejects.toThrow("503");
|
|
49
49
|
expect(seen).toEqual(["http://h/v1/router/report?days=7"]);
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
describe("renderStatus", () => {
|
|
54
|
-
test("summarises keys, catalog, ollama and agentdox", () => {
|
|
54
|
+
test("summarises keys, catalog, ollama and agentdox", async () => {
|
|
55
55
|
const now = 1_000_000_000;
|
|
56
56
|
const h: HealthSnapshot = {
|
|
57
57
|
status: "ok",
|
|
@@ -89,14 +89,14 @@ describe("renderStatus", () => {
|
|
|
89
89
|
expect(text).toContain("recording turns");
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
-
test("reports a quiet hour and omits the line for routers that predate the check", () => {
|
|
92
|
+
test("reports a quiet hour and omits the line for routers that predate the check", async () => {
|
|
93
93
|
expect(renderStatus("http://h", { status: "ok", softFailures: { spikes: [] } })).toContain("soft failures: no model spiking in the last hour");
|
|
94
94
|
expect(renderStatus("http://h", { status: "ok" })).not.toContain("soft failures");
|
|
95
95
|
expect(renderSoftFailureSpikes(null)).toEqual([]);
|
|
96
96
|
expect(renderSoftFailureSpikes([{ slug: "a/b", recentRate: 0.5, recentDispatches: 6 }], 30 * 60_000, 7)).toEqual(["a/b: 50% of 6 failed in the last 30m (7d baseline 0% of 0)"]);
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
-
test("degrades cleanly when sections are absent", () => {
|
|
99
|
+
test("degrades cleanly when sections are absent", async () => {
|
|
100
100
|
const text = renderStatus("http://h", { status: "ok", apiKeyConfigured: false });
|
|
101
101
|
expect(text).toContain("key MISSING");
|
|
102
102
|
expect(renderStatus("http://h", { status: "ok", apiKeyConfigured: false, serving: ["ollama"] })).toContain("routing over ollama cloud only");
|