agent-dealer 1.2.2 → 1.2.3

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 (32) hide show
  1. package/bundle/server/dist/capacity/claude-events.js +27 -10
  2. package/bundle/server/dist/capacity/claude-local-cache.js +788 -0
  3. package/bundle/server/dist/capacity/claude-local-cache.test.js +520 -0
  4. package/bundle/server/dist/capacity/cursor-individual-credentials.js +203 -16
  5. package/bundle/server/dist/capacity/cursor-individual-credentials.test.js +186 -2
  6. package/bundle/server/dist/capacity/cursor-individual.js +69 -2
  7. package/bundle/server/dist/capacity/cursor-individual.test.js +268 -2
  8. package/bundle/server/dist/capacity/muse-host.js +748 -0
  9. package/bundle/server/dist/capacity/muse-host.test.js +480 -0
  10. package/bundle/server/dist/capacity/muse-lifecycle.test.js +97 -0
  11. package/bundle/server/dist/capacity/muse.js +307 -60
  12. package/bundle/server/dist/capacity/muse.test.js +8 -51
  13. package/bundle/server/dist/coordinator/muse-spawn.js +143 -1
  14. package/bundle/server/dist/index.js +15 -2
  15. package/bundle/server/dist/routes/index.js +26 -9
  16. package/bundle/server/dist/routes/runtime-capacity.test.js +12 -6
  17. package/bundle/server/dist/runners/muse-code-jsonl.js +7 -1
  18. package/bundle/server/dist/runners/muse-serve-session.js +313 -0
  19. package/bundle/server/dist/runners/muse-serve-session.test.js +325 -0
  20. package/bundle/server/package.json +2 -2
  21. package/bundle/server/static-ui/assets/codex-YZSpL-SB.png +0 -0
  22. package/bundle/server/static-ui/assets/{index-DUYzboWh.css → index-CYqRh_-S.css} +1 -1
  23. package/bundle/server/static-ui/assets/index-UO4lHZw4.js +60 -0
  24. package/bundle/server/static-ui/assets/muse-CtdcC6ve.svg +33 -0
  25. package/bundle/server/static-ui/index.html +2 -2
  26. package/bundle/shared/package.json +1 -1
  27. package/dist/doctor.d.ts +57 -0
  28. package/dist/doctor.js +206 -1
  29. package/dist/doctor.test.d.ts +1 -0
  30. package/dist/doctor.test.js +246 -0
  31. package/package.json +1 -1
  32. package/bundle/server/static-ui/assets/index-0GDYfbaM.js +0 -60
@@ -0,0 +1,480 @@
1
+ // packages/server/src/capacity/muse-host.test.ts
2
+ //
3
+ // NOT-270: owned long-lived Muse host — fake-subprocess coverage only,
4
+ // never a live model call. The fake replays the exact sanitized MSP shapes
5
+ // captured by NOT-269 (`usage/read` result with the `window` + `weekly`
6
+ // pair; `usage/changed` params carrying `SubscriptionUsage` directly).
7
+ // All payload values are synthetic stand-ins, never account data.
8
+ //
9
+ // Proven lifecycle under test (docs/research/NOT-269-muse-5h-1w-lifecycle.md):
10
+ // - one server-owned host per process; concurrent readers share it;
11
+ // - `usage/changed` ingested as received, final `usage/read` on the same
12
+ // host, races resolved by newest `observedAtMs`;
13
+ // - fresh-host `missing` never overwrites a newer known pair;
14
+ // - restart/crash/timeout/auth preserve last-good rows and leak no child;
15
+ // - recovery clears the failure sentinel;
16
+ // - a capacity read never sends a session, prompt, turn, tool, or other
17
+ // billable method.
18
+ import { test, beforeEach } from "node:test";
19
+ import assert from "node:assert/strict";
20
+ import fs from "node:fs";
21
+ import os from "node:os";
22
+ import path from "node:path";
23
+ process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-muse-host-"));
24
+ import { getMuseCapacityHost, maybeRefreshMuseCapacityFromHost, MuseCapacityHost, prepareMuseServeHome, refreshMuseCapacityFromHost, resetMuseCapacityHostForTests, resetMuseCapacityRefreshState, shutdownMuseCapacityHost, } from "./muse-host.js";
25
+ import { ingestMuseUsagePayload, museRefreshThrottleMs, noteMuseCapacityFailure, } from "./muse.js";
26
+ const { migrate } = await import("../db/index.js");
27
+ const { clearAllCapacitySnapshots, listCapacitySnapshots } = await import("../repository/runtime-capacity.js");
28
+ const { getRuntimeCapacitySnapshot } = await import("./service.js");
29
+ const FAKE = new URL("./fixtures/fake-muse-serve.mjs", import.meta.url).pathname;
30
+ beforeEach(async () => {
31
+ migrate();
32
+ clearAllCapacitySnapshots();
33
+ await resetMuseCapacityHostForTests();
34
+ });
35
+ function hostOpts(mode, extra = {}) {
36
+ const now = Date.now();
37
+ return {
38
+ command: process.execPath,
39
+ args: [FAKE],
40
+ env: {
41
+ META_API_KEY: "test-fake-key",
42
+ FAKE_MSP_MODE: mode,
43
+ FAKE_MSP_NOW_MS: String(now),
44
+ ...(extra.env ?? {}),
45
+ },
46
+ timeoutMs: extra.timeoutMs ?? 10_000,
47
+ };
48
+ }
49
+ /** Synthetic stand-in observation (NOT-269 sanitized shape, not account data). */
50
+ function pairPayload(observedAtMs, windowUsed, weeklyUsed) {
51
+ return {
52
+ protocol: "msp/1.3",
53
+ usage: {
54
+ observedAtMs,
55
+ tier: "synthetic-stand-in",
56
+ window: {
57
+ usedPercent: windowUsed,
58
+ resetsAtMs: observedAtMs + 2 * 3600_000,
59
+ windowDurationMins: 300,
60
+ },
61
+ weekly: { usedPercent: weeklyUsed, resetsAtMs: observedAtMs + 3 * 24 * 3600_000 },
62
+ },
63
+ };
64
+ }
65
+ function keys() {
66
+ return listCapacitySnapshots("muse_code").map((w) => w.windowKey).sort();
67
+ }
68
+ test("observed host refresh serves exactly the 5H+1W pair to the browser shape", async () => {
69
+ const snap = await refreshMuseCapacityFromHost({ ...hostOpts("persistent-full"), nowMs: Date.now() });
70
+ const muse = snap.runtimes.find((r) => r.runtime === "muse_code");
71
+ // The entry exists only when muse_code is configured; assert the stored
72
+ // rows directly so this stays independent of agent configuration.
73
+ assert.deepEqual(keys(), ["rolling_all_models", "weekly_all_models"]);
74
+ assert.ok(!keys().includes("muse_account_usage"), "no sentinel beside valid windows");
75
+ const stored = listCapacitySnapshots("muse_code");
76
+ const rolling = stored.find((w) => w.windowKey === "rolling_all_models");
77
+ const weekly = stored.find((w) => w.windowKey === "weekly_all_models");
78
+ assert.equal(rolling.remainingPercent, 40);
79
+ assert.equal(weekly.remainingPercent, 75);
80
+ assert.equal(rolling.unavailableReason, null);
81
+ assert.equal(weekly.unavailableReason, null);
82
+ const served = getRuntimeCapacitySnapshot();
83
+ const raw = JSON.stringify(served);
84
+ assert.ok(!raw.includes("contributor"), "no tier metadata reaches the browser shape");
85
+ assert.ok(!raw.includes("muse_account_usage"), "no sentinel label reaches the browser shape");
86
+ // Stored rows guarantee the entry, so assert the exact served pair.
87
+ const servedMuse = served.runtimes.find((r) => r.runtime === "muse_code");
88
+ assert.ok(servedMuse, "muse_code entry served");
89
+ assert.deepEqual(servedMuse.windows.map((w) => w.windowKey).sort(), ["rolling_all_models", "weekly_all_models"]);
90
+ assert.ok(servedMuse.windows.every((w) => w.unavailableReason === null), "both served windows are known");
91
+ assert.equal(servedMuse.windows.find((w) => w.windowKey === "rolling_all_models")?.remainingPercent, 40, "served 5H matches the stored row");
92
+ assert.equal(servedMuse.windows.find((w) => w.windowKey === "weekly_all_models")?.remainingPercent, 75, "served 1W matches the stored row");
93
+ assert.equal(muse?.runtime, "muse_code");
94
+ await shutdownMuseCapacityHost();
95
+ });
96
+ test("snapshot labels and critical roles are exactly 5H/five_hour and 1W/weekly", async () => {
97
+ await refreshMuseCapacityFromHost({ ...hostOpts("persistent-full"), nowMs: Date.now() });
98
+ const stored = listCapacitySnapshots("muse_code");
99
+ const byKey = new Map(stored.map((w) => [w.windowKey, w]));
100
+ assert.equal(byKey.get("rolling_all_models").displayLabel, "5H");
101
+ assert.equal(byKey.get("rolling_all_models").criticalRole, "five_hour");
102
+ assert.equal(byKey.get("weekly_all_models").displayLabel, "1W");
103
+ assert.equal(byKey.get("weekly_all_models").criticalRole, "weekly");
104
+ await shutdownMuseCapacityHost();
105
+ });
106
+ test("host lifetime traffic is the handshake plus usage/read — never billable", async () => {
107
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-muse-host-record-"));
108
+ const recordPath = path.join(dir, "methods.jsonl");
109
+ const host = new MuseCapacityHost(hostOpts("persistent-full", { env: { FAKE_MSP_RECORD: recordPath } }));
110
+ try {
111
+ assert.equal((await host.readUsage()).status, "observed");
112
+ assert.equal((await host.readUsage()).status, "observed");
113
+ assert.equal((await host.readUsage()).status, "observed");
114
+ const lines = fs
115
+ .readFileSync(recordPath, "utf8")
116
+ .split("\n")
117
+ .filter(Boolean)
118
+ .map((l) => JSON.parse(l));
119
+ const methods = lines.map((l) => String(l.method));
120
+ // One handshake for the whole host lifetime, then one usage/read per refresh.
121
+ assert.deepEqual(methods, ["initialize", "initialized", "usage/read", "usage/read", "usage/read"]);
122
+ for (const m of methods) {
123
+ assert.ok(!/session|prompt|turn|exec|tool|message|thread/i.test(m), `billable method ${m}`);
124
+ }
125
+ }
126
+ finally {
127
+ await host.shutdown();
128
+ }
129
+ });
130
+ test("concurrent readers share the one host connection", async () => {
131
+ const host = new MuseCapacityHost(hostOpts("persistent-full"));
132
+ try {
133
+ const results = await Promise.all([
134
+ host.readUsage(),
135
+ host.readUsage(),
136
+ host.readUsage(),
137
+ host.readUsage(),
138
+ host.readUsage(),
139
+ ]);
140
+ assert.ok(results.every((r) => r.status === "observed"));
141
+ assert.equal(host.connectionEpoch, 1, "exactly one host per concurrent burst");
142
+ assert.ok(host.isConnected());
143
+ }
144
+ finally {
145
+ await host.shutdown();
146
+ }
147
+ });
148
+ test("fresh-host missing never overwrites or deletes a newer known pair", async () => {
149
+ await refreshMuseCapacityFromHost({ ...hostOpts("persistent-full"), nowMs: Date.now() });
150
+ assert.deepEqual(keys(), ["rolling_all_models", "weekly_all_models"]);
151
+ await resetMuseCapacityHostForTests();
152
+ const outcome = await getMuseCapacityHost(hostOpts("persistent-missing")).readUsage();
153
+ assert.equal(outcome.status, "missing");
154
+ assert.deepEqual(keys(), ["rolling_all_models", "weekly_all_models"]);
155
+ const stored = listCapacitySnapshots("muse_code");
156
+ assert.equal(stored.find((w) => w.windowKey === "rolling_all_models").remainingPercent, 40);
157
+ await shutdownMuseCapacityHost();
158
+ });
159
+ test("usage/changed and final usage/read races resolve by newest observedAtMs", async () => {
160
+ const now = Date.now();
161
+ const first = await ingestMuseUsagePayload(pairPayload(now, 42, 17));
162
+ assert.deepEqual(first.written.sort(), ["rolling_all_models", "weekly_all_models"]);
163
+ // An older finishing read cannot clobber the newer rows.
164
+ const older = await ingestMuseUsagePayload(pairPayload(now - 60_000, 90, 90));
165
+ assert.deepEqual(older.written, []);
166
+ assert.deepEqual(older.skippedStale.sort(), ["rolling_all_models", "weekly_all_models"]);
167
+ const stored = listCapacitySnapshots("muse_code");
168
+ assert.equal(stored.find((w) => w.windowKey === "rolling_all_models").remainingPercent, 58);
169
+ assert.equal(stored.find((w) => w.windowKey === "weekly_all_models").remainingPercent, 83);
170
+ // A newer observation still wins afterwards.
171
+ const newer = await ingestMuseUsagePayload(pairPayload(now + 60_000, 10, 10));
172
+ assert.deepEqual(newer.written.sort(), ["rolling_all_models", "weekly_all_models"]);
173
+ const after = listCapacitySnapshots("muse_code");
174
+ assert.equal(after.find((w) => w.windowKey === "rolling_all_models").remainingPercent, 90);
175
+ });
176
+ test("bare usage/changed params ingest without a {usage} envelope", async () => {
177
+ const now = Date.now();
178
+ const params = {
179
+ observedAtMs: now,
180
+ tier: "synthetic-stand-in",
181
+ window: { usedPercent: 42, resetsAtMs: now + 3600_000, windowDurationMins: 300 },
182
+ weekly: { usedPercent: 17, resetsAtMs: now + 24 * 3600_000 },
183
+ };
184
+ const outcome = await ingestMuseUsagePayload(params);
185
+ assert.deepEqual(outcome.written.sort(), ["rolling_all_models", "weekly_all_models"]);
186
+ });
187
+ test("a same-stamp malformed sibling never relabels the valid half", async () => {
188
+ const now = Date.now();
189
+ await ingestMuseUsagePayload(pairPayload(now, 42, 17));
190
+ const partial = {
191
+ usage: {
192
+ observedAtMs: now,
193
+ tier: "synthetic-stand-in",
194
+ window: { usedPercent: 42, resetsAtMs: now + 3600_000, windowDurationMins: 300 },
195
+ weekly: { usedPercent: "high", resetsAtMs: now + 24 * 3600_000 },
196
+ },
197
+ };
198
+ const outcome = await ingestMuseUsagePayload(partial);
199
+ const stored = listCapacitySnapshots("muse_code");
200
+ const weekly = stored.find((w) => w.windowKey === "weekly_all_models");
201
+ assert.equal(weekly.remainingPercent, 83, "valid weekly row keeps its value");
202
+ assert.equal(weekly.unavailableReason, null, "no diagnostic relabels the valid half");
203
+ assert.ok(!outcome.written.includes("weekly_all_models"));
204
+ });
205
+ test("empty payload ingests nothing and deletes nothing", async () => {
206
+ const now = Date.now();
207
+ await ingestMuseUsagePayload(pairPayload(now, 42, 17));
208
+ const outcome = await ingestMuseUsagePayload({ protocol: "msp/1.3" });
209
+ assert.deepEqual(outcome.written, []);
210
+ assert.deepEqual(keys(), ["rolling_all_models", "weekly_all_models"]);
211
+ });
212
+ test("crash preserves last-good rows and writes no sentinel beside them", async () => {
213
+ const host = new MuseCapacityHost(hostOpts("persistent-full"));
214
+ try {
215
+ assert.equal((await host.readUsage()).status, "observed");
216
+ }
217
+ finally {
218
+ await host.shutdown();
219
+ }
220
+ await resetMuseCapacityHostForTests();
221
+ const crashed = new MuseCapacityHost(hostOpts("crash", { timeoutMs: 3000 }));
222
+ try {
223
+ const outcome = await crashed.readUsage();
224
+ assert.equal(outcome.status, "missing");
225
+ assert.ok(!crashed.isConnected(), "dead child released, none leaked");
226
+ }
227
+ finally {
228
+ await crashed.shutdown();
229
+ }
230
+ assert.deepEqual(keys(), ["rolling_all_models", "weekly_all_models"]);
231
+ });
232
+ test("timeout kills the hung child and preserves last-good rows", async () => {
233
+ await ingestMuseUsagePayload(pairPayload(Date.now(), 42, 17));
234
+ assert.deepEqual(keys(), ["rolling_all_models", "weekly_all_models"]);
235
+ const host = new MuseCapacityHost(hostOpts("hang", { timeoutMs: 400 }));
236
+ try {
237
+ const outcome = await host.readUsage();
238
+ assert.equal(outcome.status, "missing");
239
+ assert.ok(!host.isConnected(), "hung child killed, none leaked");
240
+ }
241
+ finally {
242
+ await host.shutdown();
243
+ }
244
+ assert.deepEqual(keys(), ["rolling_all_models", "weekly_all_models"]);
245
+ const stored = listCapacitySnapshots("muse_code");
246
+ assert.equal(stored.find((w) => w.windowKey === "rolling_all_models").remainingPercent, 58);
247
+ });
248
+ test("auth failure reads missing and preserves last-good rows", async () => {
249
+ await ingestMuseUsagePayload(pairPayload(Date.now(), 42, 17));
250
+ const host = new MuseCapacityHost(hostOpts("auth-error", { timeoutMs: 5000 }));
251
+ try {
252
+ assert.equal((await host.readUsage()).status, "missing");
253
+ }
254
+ finally {
255
+ await host.shutdown();
256
+ }
257
+ assert.deepEqual(keys(), ["rolling_all_models", "weekly_all_models"]);
258
+ });
259
+ test("failure with no known rows persists the sentinel; recovery clears it", async () => {
260
+ const bad = new MuseCapacityHost(hostOpts("crash", { timeoutMs: 3000 }));
261
+ try {
262
+ assert.equal((await bad.readUsage()).status, "missing");
263
+ }
264
+ finally {
265
+ await bad.shutdown();
266
+ }
267
+ assert.deepEqual(keys(), ["muse_account_usage"]);
268
+ await resetMuseCapacityHostForTests();
269
+ const snap = await refreshMuseCapacityFromHost({
270
+ ...hostOpts("persistent-full"),
271
+ nowMs: Date.now(),
272
+ });
273
+ assert.deepEqual(keys(), ["rolling_all_models", "weekly_all_models"]);
274
+ assert.ok(snap !== null);
275
+ await shutdownMuseCapacityHost();
276
+ });
277
+ test("noteMuseCapacityFailure never sits beside valid windows", async () => {
278
+ assert.equal(await noteMuseCapacityFailure("missing"), "sentinel");
279
+ assert.deepEqual(keys(), ["muse_account_usage"]);
280
+ await ingestMuseUsagePayload(pairPayload(Date.now(), 42, 17));
281
+ assert.deepEqual(keys(), ["rolling_all_models", "weekly_all_models"]);
282
+ assert.equal(await noteMuseCapacityFailure("missing"), "preserved");
283
+ assert.equal(await noteMuseCapacityFailure("unsupported"), "preserved");
284
+ assert.deepEqual(keys(), ["rolling_all_models", "weekly_all_models"]);
285
+ });
286
+ test("failure never adds a sentinel beside per-window unparsable rows", async () => {
287
+ const now = Date.now();
288
+ const outcome = await ingestMuseUsagePayload({
289
+ usage: {
290
+ observedAtMs: now,
291
+ tier: "synthetic-stand-in",
292
+ window: { usedPercent: "high", resetsAtMs: now + 3600_000, windowDurationMins: 300 },
293
+ weekly: { usedPercent: "high", resetsAtMs: now + 24 * 3600_000 },
294
+ },
295
+ });
296
+ assert.deepEqual(outcome.written.sort(), ["rolling_all_models", "weekly_all_models"]);
297
+ assert.deepEqual(keys(), ["rolling_all_models", "weekly_all_models"]);
298
+ const stored = listCapacitySnapshots("muse_code");
299
+ assert.ok(stored.every((w) => w.unavailableReason === "unparsable"), "both halves are per-window diagnostics");
300
+ assert.equal(await noteMuseCapacityFailure("missing"), "preserved");
301
+ assert.deepEqual(keys(), ["rolling_all_models", "weekly_all_models"]);
302
+ });
303
+ test("restart after observation restarts empty and re-observes", async () => {
304
+ const host = new MuseCapacityHost(hostOpts("exit-after-first-read"));
305
+ try {
306
+ assert.equal((await host.readUsage()).status, "observed");
307
+ const firstEpoch = host.connectionEpoch;
308
+ assert.equal(firstEpoch, 1);
309
+ const deadline = Date.now() + 8000;
310
+ while (host.isConnected() && Date.now() < deadline) {
311
+ await new Promise((r) => setTimeout(r, 25));
312
+ }
313
+ assert.ok(!host.isConnected(), "exited child detected, none leaked");
314
+ // Next read transparently restarts (state lost — the restarted fake
315
+ // re-observes, proving the host recovers without a second host).
316
+ assert.equal((await host.readUsage()).status, "observed");
317
+ assert.equal(host.connectionEpoch, 2);
318
+ assert.ok(host.isConnected());
319
+ }
320
+ finally {
321
+ await host.shutdown();
322
+ }
323
+ assert.deepEqual(keys(), ["rolling_all_models", "weekly_all_models"]);
324
+ });
325
+ test("a stale child's late close cannot kill its replacement", async () => {
326
+ const host = new MuseCapacityHost(hostOpts("slow-term-full"));
327
+ try {
328
+ assert.equal((await host.readUsage()).status, "observed");
329
+ assert.equal(host.connectionEpoch, 1);
330
+ // Shut down (SIGTERMs the slow child: it exits ~500 ms later) and
331
+ // restart immediately — the stale `close` lands on the live host.
332
+ await host.shutdown();
333
+ assert.equal((await host.readUsage()).status, "observed");
334
+ assert.equal(host.connectionEpoch, 2);
335
+ await new Promise((r) => setTimeout(r, 900));
336
+ assert.ok(host.isConnected(), "stale close did not kill the replacement");
337
+ assert.equal(host.connectionEpoch, 2, "no second host per restart");
338
+ assert.equal((await host.readUsage()).status, "observed");
339
+ assert.equal(host.connectionEpoch, 2, "replacement serves reads itself");
340
+ }
341
+ finally {
342
+ await host.shutdown();
343
+ }
344
+ });
345
+ test("clean shutdown releases the child process", async () => {
346
+ const host = new MuseCapacityHost(hostOpts("persistent-full"));
347
+ assert.equal((await host.readUsage()).status, "observed");
348
+ assert.ok(host.isConnected());
349
+ await host.shutdown();
350
+ assert.ok(!host.isConnected(), "no host process left behind");
351
+ });
352
+ test("an unobserved host is released after the read — no lifetime child that can only answer missing", async () => {
353
+ const host = new MuseCapacityHost(hostOpts("persistent-missing"));
354
+ try {
355
+ assert.equal((await host.readUsage()).status, "missing");
356
+ assert.ok(!host.isConnected(), "unobserved host released, none parked");
357
+ // The next refresh transparently respawns (state was empty anyway).
358
+ assert.equal((await host.readUsage()).status, "missing");
359
+ assert.equal(host.connectionEpoch, 2, "respawned on demand, still one host at a time");
360
+ assert.ok(!host.isConnected(), "still-unobserved host released again");
361
+ }
362
+ finally {
363
+ await host.shutdown();
364
+ }
365
+ // Releasing the empty host never deletes last-good rows.
366
+ assert.deepEqual(keys(), ["muse_account_usage"]);
367
+ });
368
+ test("an observed host stays resident across reads", async () => {
369
+ const host = new MuseCapacityHost(hostOpts("persistent-full"));
370
+ try {
371
+ assert.equal((await host.readUsage()).status, "observed");
372
+ assert.ok(host.isConnected(), "observed host held open");
373
+ assert.equal((await host.readUsage()).status, "observed");
374
+ assert.equal(host.connectionEpoch, 1, "no respawn while observed");
375
+ assert.ok(host.isConnected());
376
+ }
377
+ finally {
378
+ await host.shutdown();
379
+ }
380
+ });
381
+ test("production refresh is throttled and can be disabled", async () => {
382
+ resetMuseCapacityRefreshState();
383
+ const opts = { ...hostOpts("persistent-full"), nowMs: Date.now() };
384
+ try {
385
+ assert.equal(museRefreshThrottleMs(), 5 * 60 * 1000);
386
+ const first = await maybeRefreshMuseCapacityFromHost(opts);
387
+ assert.ok(first, "first refresh runs");
388
+ assert.equal(await maybeRefreshMuseCapacityFromHost(opts), null, "second refresh throttled");
389
+ process.env.AGENT_DEALER_MUSE_CAPACITY_REFRESH_MS = "off";
390
+ resetMuseCapacityRefreshState();
391
+ assert.equal(await maybeRefreshMuseCapacityFromHost(opts), null, "refresh disabled");
392
+ process.env.AGENT_DEALER_MUSE_CAPACITY_REFRESH_MS = "garbage";
393
+ assert.equal(museRefreshThrottleMs(), 5 * 60 * 1000);
394
+ }
395
+ finally {
396
+ delete process.env.AGENT_DEALER_MUSE_CAPACITY_REFRESH_MS;
397
+ await resetMuseCapacityHostForTests();
398
+ }
399
+ await shutdownMuseCapacityHost();
400
+ });
401
+ test("serve host launches under a server-owned config home with worker settings", async () => {
402
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-muse-config-"));
403
+ const configRecord = path.join(dir, "config.json");
404
+ const host = new MuseCapacityHost(hostOpts("persistent-full", { env: { FAKE_MSP_RECORD_CONFIG: configRecord } }));
405
+ try {
406
+ assert.equal((await host.readUsage()).status, "observed");
407
+ const rec = JSON.parse(fs.readFileSync(configRecord, "utf8"));
408
+ const ambient = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
409
+ assert.ok(rec.configHome, "host launches with an XDG config home");
410
+ assert.notEqual(rec.configHome, ambient, "config home is server-owned, not ambient");
411
+ assert.ok(String(rec.configHome).includes("dealer-muse-serve"), `server-owned home, got ${rec.configHome}`);
412
+ assert.ok(rec.dataHome, "host launches with an isolated data home");
413
+ assert.ok(!String(rec.dataHome).includes(".config"), "data home is not the operator's");
414
+ assert.equal(rec.settingsError, null, "settings.json parses on the launched host");
415
+ const settings = rec.settings;
416
+ // Same worker posture as the exec lane's per-attempt settings: no MCP
417
+ // servers, no subagent delegation, no workflows, no reminder child runs.
418
+ assert.ok(!("mcpServers" in settings), "workers get no MCP servers");
419
+ const run = settings.run;
420
+ assert.equal(run.workflow_trigger_mode, "off");
421
+ assert.equal(run.subagent_delegation_mode, "off");
422
+ const caps = settings.runtime_capabilities;
423
+ assert.ok(Object.keys(caps).length > 0, "reminder switches are present");
424
+ for (const [name, cap] of Object.entries(caps)) {
425
+ assert.equal(cap.enabled, false, `${name} stays disabled`);
426
+ }
427
+ }
428
+ finally {
429
+ await host.shutdown();
430
+ }
431
+ });
432
+ test("prepareMuseServeHome links the ambient login, or records API-key auth", async () => {
433
+ // File-backed login: linked, never copied.
434
+ const ambientRoot = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-muse-ambient-"));
435
+ const ambientAuth = path.join(ambientRoot, "muse", "auth.json");
436
+ fs.mkdirSync(path.dirname(ambientAuth), { recursive: true });
437
+ fs.writeFileSync(ambientAuth, "{}\n");
438
+ const home = prepareMuseServeHome(ambientAuth);
439
+ try {
440
+ assert.equal(home.authLinked, true);
441
+ const linkPath = path.join(home.configHome, "muse", "auth.json");
442
+ assert.ok(fs.lstatSync(linkPath).isSymbolicLink(), "login is a symlink, never a copy");
443
+ assert.equal(fs.readlinkSync(linkPath), ambientAuth);
444
+ }
445
+ finally {
446
+ fs.rmSync(path.dirname(home.configHome), { recursive: true, force: true });
447
+ }
448
+ // API-key auth: no link, still a complete home.
449
+ const keyHome = prepareMuseServeHome(path.join(ambientRoot, "no-such-auth.json"));
450
+ try {
451
+ assert.equal(keyHome.authLinked, false);
452
+ const settings = JSON.parse(fs.readFileSync(path.join(keyHome.configHome, "muse", "settings.json"), "utf8"));
453
+ assert.equal(settings.run.workflow_trigger_mode, "off");
454
+ assert.ok(!("mcpServers" in settings));
455
+ }
456
+ finally {
457
+ fs.rmSync(path.dirname(keyHome.configHome), { recursive: true, force: true });
458
+ }
459
+ fs.rmSync(ambientRoot, { recursive: true, force: true });
460
+ });
461
+ test("no credential short-circuits to missing without spawning", async () => {
462
+ const noAuthDir = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-muse-host-noauth-"));
463
+ const env = { ...process.env, FAKE_MSP_MODE: "persistent-full" };
464
+ delete env.META_API_KEY;
465
+ const host = new MuseCapacityHost({
466
+ // Would report `unsupported` if ever spawned — `missing` proves it was not.
467
+ command: "/nonexistent/muse-xyz",
468
+ timeoutMs: 2000,
469
+ env,
470
+ authFilePath: path.join(noAuthDir, "auth.json"),
471
+ });
472
+ try {
473
+ assert.equal((await host.readUsage()).status, "missing");
474
+ assert.equal(host.connectionEpoch, 0, "no host spawned");
475
+ }
476
+ finally {
477
+ await host.shutdown();
478
+ }
479
+ assert.deepEqual(keys(), ["muse_account_usage"]);
480
+ });
@@ -0,0 +1,97 @@
1
+ // packages/server/src/capacity/muse-lifecycle.test.ts
2
+ //
3
+ // NOT-269: deterministic lifecycle contract for Muse 5H/1W acquisition.
4
+ // Evidence: docs/research/NOT-269-muse-5h-1w-lifecycle.md (Muse Code 1.4.0,
5
+ // stable schema fingerprint sha256:36466f63…, live fresh-host + session-log +
6
+ // echo-exec probes). Fake-subprocess coverage only — never a live provider
7
+ // request, never account data: all payload values below are synthetic
8
+ // redacted stand-ins (obviously-fake percents), asserting shapes and the
9
+ // user contract (Muse primary capacity is exactly 5H + 1W).
10
+ //
11
+ // Proven rules encoded here:
12
+ // - a conforming observation always carries the window+weekly pair;
13
+ // - a partial observation never fabricates the missing half;
14
+ // - the first observation arrives as a `usage/changed` notification;
15
+ // - a fresh/unobserved host reads `missing` (restart is fresh — the client
16
+ // holds no cross-process usage state);
17
+ // - resume/turn methods are refused before write, so refresh can never
18
+ // manufacture a billable call.
19
+ import { test } from "node:test";
20
+ import assert from "node:assert/strict";
21
+ import { assertMuseReadOnlyMethod, museUsageToReadings, readMuseCapacity, requestMuseUsage, } from "./muse.js";
22
+ import { normalizeAdapterWindow } from "./adapter.js";
23
+ const FAKE = new URL("./fixtures/fake-muse-serve.mjs", import.meta.url).pathname;
24
+ function fakeOpts(mode) {
25
+ const now = Date.now();
26
+ return {
27
+ command: process.execPath,
28
+ args: [FAKE],
29
+ env: { META_API_KEY: "test-fake-key", FAKE_MSP_MODE: mode, FAKE_MSP_NOW_MS: String(now) },
30
+ timeoutMs: 10_000,
31
+ nowMs: now,
32
+ };
33
+ }
34
+ /** Synthetic redacted stand-in for a conforming observation (not account data). */
35
+ function syntheticFullPair(now) {
36
+ return {
37
+ protocol: "msp/1.3",
38
+ usage: {
39
+ observedAtMs: now,
40
+ tier: "synthetic-stand-in",
41
+ window: { usedPercent: 42, resetsAtMs: now + 2 * 3600_000, windowDurationMins: 300 },
42
+ weekly: { usedPercent: 17, resetsAtMs: now + 3 * 24 * 3600_000 },
43
+ },
44
+ };
45
+ }
46
+ test("lifecycle: a full observation carries exactly the 5H+1W pair", () => {
47
+ const now = Date.now();
48
+ const parsed = museUsageToReadings(syntheticFullPair(now), new Date(now).toISOString());
49
+ assert.ok(parsed);
50
+ assert.equal(parsed.windows.length, 2);
51
+ const labels = parsed.windows
52
+ .map((w) => normalizeAdapterWindow("muse_code", w, now).displayLabel)
53
+ .sort();
54
+ assert.deepEqual(labels, ["1W", "5H"]);
55
+ const rolling = parsed.windows.find((w) => w.windowKey === "rolling_all_models");
56
+ const weekly = parsed.windows.find((w) => w.windowKey === "weekly_all_models");
57
+ assert.equal(normalizeAdapterWindow("muse_code", rolling, now).remainingPercent, 58);
58
+ assert.equal(normalizeAdapterWindow("muse_code", weekly, now).remainingPercent, 83);
59
+ assert.equal(rolling.criticalRole, "five_hour");
60
+ assert.equal(weekly.criticalRole, "weekly");
61
+ });
62
+ test("lifecycle: a partial observation never fabricates the missing half", () => {
63
+ const now = Date.now();
64
+ const parsed = museUsageToReadings({
65
+ usage: {
66
+ observedAtMs: now,
67
+ window: { usedPercent: 42, resetsAtMs: now + 3600_000, windowDurationMins: 300 },
68
+ },
69
+ }, new Date(now).toISOString());
70
+ assert.ok(parsed);
71
+ assert.equal(parsed.windows.length, 1);
72
+ assert.equal(parsed.windows[0].windowKey, "rolling_all_models");
73
+ });
74
+ test("lifecycle: the first observation arrives as usage/changed with the pair", async () => {
75
+ const read = await requestMuseUsage(fakeOpts("full"));
76
+ assert.equal(read.failure, null);
77
+ assert.equal(read.updates.length, 1);
78
+ // Stable schema refs SubscriptionUsage directly at
79
+ // /notifications/usage/changed/params — not wrapped in {usage}.
80
+ const params = read.updates[0];
81
+ assert.ok(params && typeof params === "object");
82
+ assert.deepEqual(Object.keys(params).sort(), ["observedAtMs", "tier", "weekly", "window"]);
83
+ });
84
+ test("lifecycle: a fresh host with no observation reads missing — twice", async () => {
85
+ for (let i = 0; i < 2; i++) {
86
+ const result = await readMuseCapacity(fakeOpts("missing"));
87
+ assert.equal(result.windows.length, 0);
88
+ assert.equal(result.unavailable.length, 1);
89
+ assert.equal(result.unavailable[0].windowKey, "muse_account_usage");
90
+ assert.equal(result.unavailable[0].reason, "missing");
91
+ }
92
+ });
93
+ test("lifecycle: refresh can never manufacture a session, turn, or prompt", () => {
94
+ for (const m of ["session/start", "session/resume", "session/prompt", "turn/start", "exec"]) {
95
+ assert.throws(() => assertMuseReadOnlyMethod(m), /refusing non-read method/);
96
+ }
97
+ });