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.
Files changed (110) hide show
  1. package/.omp-plugin/marketplace.json +2 -2
  2. package/README.md +32 -2
  3. package/omp-extension/router-configure.ts +9 -7
  4. package/package.json +1 -1
  5. package/src/cli/config-cmd.ts +8 -7
  6. package/src/cli/explain.ts +10 -5
  7. package/src/cli/export.ts +6 -5
  8. package/src/cli/models.ts +10 -7
  9. package/src/cli/report.ts +6 -1
  10. package/src/cli/stats.ts +7 -7
  11. package/src/config/load.ts +10 -1
  12. package/src/config/types.ts +10 -1
  13. package/src/context/bridge.ts +7 -7
  14. package/src/context/index.ts +3 -3
  15. package/src/context/store.ts +39 -56
  16. package/src/context/types.ts +7 -6
  17. package/src/cost/blended.ts +28 -7
  18. package/src/cost/feedback.ts +33 -37
  19. package/src/cost/ledger-sql.ts +547 -0
  20. package/src/cost/ledger.ts +30 -459
  21. package/src/cost/report.ts +171 -129
  22. package/src/cost/retention.ts +10 -10
  23. package/src/cost/summary.ts +15 -10
  24. package/src/cost/types.ts +43 -62
  25. package/src/cost/views.ts +79 -49
  26. package/src/lib.ts +6 -2
  27. package/src/router/candidates.ts +7 -15
  28. package/src/router/classify.ts +6 -4
  29. package/src/router/index.ts +95 -9
  30. package/src/router/select.ts +38 -21
  31. package/src/router/state.ts +90 -102
  32. package/src/router/types.ts +11 -5
  33. package/src/server/advise.ts +6 -4
  34. package/src/server/compaction-digest.ts +1 -1
  35. package/src/server/digest.ts +9 -10
  36. package/src/server/http.ts +101 -41
  37. package/src/server/providers.ts +18 -4
  38. package/src/server/turn.ts +32 -9
  39. package/src/tokens/estimate.ts +16 -6
  40. package/src/upstream/ollama-usage.ts +21 -11
  41. package/src/util/schema.ts +201 -0
  42. package/src/util/sql.ts +246 -0
  43. package/src/wire/anthropic/messages.ts +3 -4
  44. package/src/wire/openai/request.ts +1 -0
  45. package/src/wire/types.ts +7 -0
  46. package/test/anthropic-wire.test.ts +9 -9
  47. package/test/benchmark-feeds.test.ts +7 -7
  48. package/test/cache-control.test.ts +7 -7
  49. package/test/cache-estimate.test.ts +5 -5
  50. package/test/catalog-view.test.ts +4 -4
  51. package/test/catalog.test.ts +11 -11
  52. package/test/classify.test.ts +24 -24
  53. package/test/compaction.test.ts +20 -20
  54. package/test/config-wizard.test.ts +32 -32
  55. package/test/config.test.ts +10 -10
  56. package/test/connect-harnesses.test.ts +11 -11
  57. package/test/context-bridge.test.ts +40 -30
  58. package/test/context-prune.test.ts +43 -36
  59. package/test/context-query.test.ts +8 -8
  60. package/test/controls.test.ts +54 -27
  61. package/test/cost.test.ts +12 -12
  62. package/test/digest.test.ts +55 -44
  63. package/test/embed-lifecycle.test.ts +5 -5
  64. package/test/embed-logic.test.ts +26 -26
  65. package/test/escalate.test.ts +17 -17
  66. package/test/eval.test.ts +13 -13
  67. package/test/executable.test.ts +6 -6
  68. package/test/exploration.test.ts +19 -20
  69. package/test/failover.test.ts +22 -21
  70. package/test/fakes.ts +105 -0
  71. package/test/features.test.ts +21 -21
  72. package/test/harness-requests.test.ts +3 -3
  73. package/test/harness-switch.test.ts +5 -5
  74. package/test/hold-exploration.test.ts +13 -13
  75. package/test/hot-reload.test.ts +5 -5
  76. package/test/learned.test.ts +5 -5
  77. package/test/ledger-sql.test.ts +342 -0
  78. package/test/mcp-entry.test.ts +5 -5
  79. package/test/migrations.test.ts +28 -22
  80. package/test/models-yml.test.ts +18 -18
  81. package/test/ollama.test.ts +40 -34
  82. package/test/omp-credentials.test.ts +16 -16
  83. package/test/policy.test.ts +3 -3
  84. package/test/reconfigure.test.ts +4 -4
  85. package/test/redaction.test.ts +41 -35
  86. package/test/remote.test.ts +12 -12
  87. package/test/report-logic.test.ts +8 -8
  88. package/test/report.test.ts +95 -87
  89. package/test/retention.test.ts +79 -66
  90. package/test/schema.test.ts +123 -0
  91. package/test/scope.test.ts +8 -8
  92. package/test/select.test.ts +216 -257
  93. package/test/skills.test.ts +3 -3
  94. package/test/sql-shim.test.ts +154 -0
  95. package/test/state.test.ts +43 -36
  96. package/test/summary.test.ts +38 -27
  97. package/test/tier-plan.test.ts +45 -62
  98. package/test/toast-logic.test.ts +31 -31
  99. package/test/tokens.test.ts +95 -80
  100. package/test/trust-attribution.test.ts +217 -187
  101. package/test/trust-window.test.ts +37 -32
  102. package/test/turn.test.ts +55 -23
  103. package/test/upstreams.test.ts +13 -13
  104. package/test/views.test.ts +81 -59
  105. package/test/wire-request.test.ts +17 -17
  106. package/test/wire-responses.test.ts +4 -4
  107. package/tools/agentdox-e2e.ts +5 -2
  108. package/tools/export-benchmarks.ts +5 -5
  109. package/tools/ledger-parity.ts +266 -0
  110. package/tools/replay.ts +16 -8
@@ -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 db = openDb(":memory:");
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
- const ledger = createLedger(db, { ...DEFAULT_CONFIG, ledger: { ...DEFAULT_CONFIG.ledger, path: ":memory:" } });
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 db = openDb(":memory:");
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
- const ledger = createLedger(db, { ...DEFAULT_CONFIG, ledger: { ...DEFAULT_CONFIG.ledger, path: ":memory:" } });
291
- ledger.record(entry({}));
292
- const report = buildUsageReport(db, { windowDays: 7 });
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
  });
@@ -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");