@zgeoff/atc 3.4.2 → 3.6.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 (83) hide show
  1. package/package.json +1 -1
  2. package/src/agents/agent-adapter.ts +4 -0
  3. package/src/agents/build-claude-query-options.ts +1 -0
  4. package/src/agents/build-headless-env.ts +4 -0
  5. package/src/agents/gateway-adapter.ts +38 -20
  6. package/src/build-migrated-config.ts +1 -0
  7. package/src/client/daemon-client.ts +6 -1
  8. package/src/collect-restart-env.ts +6 -3
  9. package/src/daemon/build-scoped-context.ts +2 -0
  10. package/src/daemon/check-session-scope.ts +235 -0
  11. package/src/daemon/daemon-connection.ts +74 -0
  12. package/src/daemon/daemon-context.ts +21 -0
  13. package/src/daemon/daemon.ts +21 -0
  14. package/src/daemon/find-host-branch.ts +28 -0
  15. package/src/daemon/materialize-workspace.ts +6 -1
  16. package/src/daemon/merge-recorded-scope.ts +50 -0
  17. package/src/daemon/published-records.ts +269 -0
  18. package/src/daemon/request-access-classes.ts +1 -0
  19. package/src/daemon/run-host-git.ts +52 -0
  20. package/src/daemon/sessions.ts +185 -53
  21. package/src/daemon/start-headless-turn.ts +3 -0
  22. package/src/federation/id-rules.ts +11 -2
  23. package/src/mcp/mcp-tools.ts +50 -0
  24. package/src/mcp/require-daemon-features.ts +1 -0
  25. package/src/mcp/run-tool.ts +14 -0
  26. package/src/protocol/daemon-features.ts +5 -0
  27. package/src/protocol/parse-declared-scope.ts +158 -0
  28. package/src/protocol/protocol.ts +1 -0
  29. package/src/protocol/published-record.ts +62 -0
  30. package/src/protocol/request-param-schemas.ts +16 -0
  31. package/src/shared/check-gateway-auth.ts +16 -10
  32. package/src/shared/collect-agents.ts +0 -1
  33. package/src/shared/collect-gateways.ts +4 -2
  34. package/src/store/parse-published-record.ts +59 -0
  35. package/src/store/run-migrations.ts +23 -0
  36. package/src/store/state-store.ts +32 -0
  37. package/src/test-utils/build-mock-published-record.ts +39 -0
  38. package/src/test-utils/build-stub-brokered-agent-adapter.ts +3 -1
  39. package/src/test-utils/build-stub-daemon-context.ts +2 -0
  40. package/src/test-utils/build-stub-daemon-requests.ts +3 -3
  41. package/src/test-utils/build-stub-harness-relay.ts +2 -2
  42. package/src/test-utils/build-stub-held-inbox-source.ts +2 -2
  43. package/src/test-utils/build-stub-host-hold.ts +7 -6
  44. package/src/test-utils/build-stub-launch-admission.ts +20 -0
  45. package/src/test-utils/build-stub-pregranted-broker-host.ts +22 -0
  46. package/src/test-utils/build-stub-terminal.ts +3 -3
  47. package/src/test-utils/create-git-fixture.ts +2 -2
  48. package/src/test-utils/create-stub-imp-port.ts +37 -37
  49. package/src/test-utils/create-stub-systemd.ts +6 -6
  50. package/src/test-utils/isolate-home.ts +8 -5
  51. package/src/test-utils/read-query-plan.ts +10 -6
  52. package/src/test-utils/register-test-cleanup.ts +10 -10
  53. package/src/test-utils/setup-mcp-home.ts +7 -6
  54. package/src/test-utils/setup-mcp-http.ts +6 -6
  55. package/src/test-utils/setup-temp-dir.ts +4 -10
  56. package/src/test-utils/start-cut-proxy.ts +2 -4
  57. package/src/test-utils/start-daemon-process.ts +13 -13
  58. package/src/test-utils/start-git-http-server.ts +2 -4
  59. package/src/test-utils/start-mcp-stdio.ts +5 -5
  60. package/src/test-utils/start-stub-answering-listener.ts +3 -3
  61. package/src/test-utils/start-stub-closing-listener.ts +3 -3
  62. package/src/test-utils/start-stub-dropping-daemon.ts +2 -3
  63. package/src/test-utils/start-stub-impd-info.ts +3 -7
  64. package/src/test-utils/start-stub-impd.ts +3 -5
  65. package/src/test-utils/start-stub-legacy-daemon.ts +2 -3
  66. package/src/test-utils/start-stub-mcp-server.ts +3 -5
  67. package/src/test-utils/start-stub-recording-listener.ts +3 -3
  68. package/src/test-utils/start-stub-refusing-listener.ts +3 -3
  69. package/src/test-utils/start-stub-reporter-socket.ts +3 -9
  70. package/src/test-utils/start-stub-restore-daemon.ts +2 -7
  71. package/src/test-utils/start-stub-session-bridge.ts +3 -9
  72. package/src/test-utils/start-stub-silent-server.ts +3 -6
  73. package/src/test-utils/start-stub-source-daemon.ts +2 -2
  74. package/src/test-utils/start-stub-stalled-client.ts +3 -3
  75. package/src/test-utils/start-stub-stalled-listener.ts +3 -3
  76. package/src/test-utils/start-stub-tcp-daemon.ts +3 -3
  77. package/src/test-utils/start-stub-telemetry-collector.ts +2 -2
  78. package/src/test-utils/start-stub-unanswering-listener.ts +3 -3
  79. package/src/test-utils/start-stub-unanswering-unix-listener.ts +3 -3
  80. package/src/test-utils/start-test-daemon.ts +62 -18
  81. package/src/test-utils/start-tui-harness.ts +17 -10
  82. package/src/test-utils/subscribe-to-socket-lines.ts +5 -9
  83. package/src/test-utils/{can-bind-addresses.ts → try-bind-addresses.ts} +4 -3
@@ -43,12 +43,10 @@ const GUEST_PATH = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
43
43
 
44
44
  // A hold on the commands whose argv holds its text: entered resolves with
45
45
  // the argv of the first command it holds, and stop lets every command it
46
- // holds run. Disposing the hold stops it, so a test that holds it with
47
- // `using` after its daemon lets the daemon stop even when the test fails.
46
+ // holds run.
48
47
  interface StubCommandHold {
49
48
  readonly entered: Promise<string>;
50
49
  readonly stop: () => void;
51
- readonly [Symbol.dispose]: () => void;
52
50
  }
53
51
 
54
52
  /**
@@ -475,7 +473,7 @@ class StubImpPort implements ImpPort {
475
473
  }
476
474
 
477
475
  for (const proc of imp.sessions.values()) {
478
- proc.connection?.finish({ kind: 'closed', reason: 'imp destroyed', closeCode: 1000 });
476
+ proc.connection?.stop({ kind: 'closed', reason: 'imp destroyed', closeCode: 1000 });
479
477
  proc.connection = null;
480
478
  proc.exited ??= { code: null };
481
479
 
@@ -504,7 +502,7 @@ class StubImpPort implements ImpPort {
504
502
  sent: 0,
505
503
  finished: false,
506
504
  process: null,
507
- finish: (result) => {
505
+ stop: (result) => {
508
506
  if (connection.finished) {
509
507
  return;
510
508
  }
@@ -523,7 +521,7 @@ class StubImpPort implements ImpPort {
523
521
  // lets it go; a closed gate sends nothing.
524
522
  const sendToImpd = () => {
525
523
  if (gate !== undefined && !tryPassGate(gate)) {
526
- connection.finish({ kind: 'closed', reason: 'closed before sending', closeCode: 1000 });
524
+ connection.stop({ kind: 'closed', reason: 'closed before sending', closeCode: 1000 });
527
525
 
528
526
  return;
529
527
  }
@@ -548,7 +546,7 @@ class StubImpPort implements ImpPort {
548
546
  this.upgradeFailures -= 1;
549
547
 
550
548
  setTimeout(() => {
551
- connection.finish({ kind: 'unreachable', detail: 'the upgrade failed' });
549
+ connection.stop({ kind: 'unreachable', detail: 'the upgrade failed' });
552
550
  }, 0);
553
551
  } else if (this.upgrades === null) {
554
552
  sendToImpd();
@@ -575,7 +573,7 @@ class StubImpPort implements ImpPort {
575
573
  }
576
574
  },
577
575
  close: () => {
578
- connection.finish({ kind: 'closed', reason: 'closed by the client', closeCode: 1000 });
576
+ connection.stop({ kind: 'closed', reason: 'closed by the client', closeCode: 1000 });
579
577
  },
580
578
  };
581
579
  }
@@ -852,7 +850,7 @@ class StubImpPort implements ImpPort {
852
850
  stopConnection(name: string, session: string, closeCode: number): void {
853
851
  const proc = this.getImp(name).sessions.get(session);
854
852
 
855
- proc?.connection?.finish({ kind: 'closed', reason: `code ${closeCode}`, closeCode });
853
+ proc?.connection?.stop({ kind: 'closed', reason: `code ${closeCode}`, closeCode });
856
854
  }
857
855
 
858
856
  /**
@@ -1067,7 +1065,7 @@ class StubImpPort implements ImpPort {
1067
1065
  hold.done.resolve();
1068
1066
  };
1069
1067
 
1070
- return { entered: hold.entered.promise, stop, [Symbol.dispose]: stop };
1068
+ return { entered: hold.entered.promise, stop };
1071
1069
  }
1072
1070
 
1073
1071
  // How many commands wait for their command hold to stop.
@@ -1172,7 +1170,10 @@ class StubImpPort implements ImpPort {
1172
1170
  return proc.end;
1173
1171
  }
1174
1172
 
1175
- [Symbol.dispose](): void {
1173
+ // Kills every process and stops every forward the stand-in holds. It runs
1174
+ // once the current test finishes; calling it sooner runs it then, and a
1175
+ // second call does nothing.
1176
+ readonly stop: () => void = registerTestCleanup(() => {
1176
1177
  this.stopCommandHold();
1177
1178
 
1178
1179
  for (const imp of this.imps.values()) {
@@ -1187,7 +1188,7 @@ class StubImpPort implements ImpPort {
1187
1188
  }
1188
1189
 
1189
1190
  this.imps.clear();
1190
- }
1191
+ });
1191
1192
 
1192
1193
  // impd's refusal of a grant or revoke: the scope and pattern checks run
1193
1194
  // before anything is looked up, then a missing imp or secret.
@@ -1278,13 +1279,13 @@ class StubImpPort implements ImpPort {
1278
1279
 
1279
1280
  for (const proc of imp.sessions.values()) {
1280
1281
  tryKill(proc.pty, 'SIGSTOP');
1281
- proc.connection?.finish({ kind: 'detached', reason: 'lost', offset: proc.end });
1282
+ proc.connection?.stop({ kind: 'detached', reason: 'lost', offset: proc.end });
1282
1283
  }
1283
1284
  }
1284
1285
 
1285
1286
  private bootCold(imp: StubImp, cause: ColdBootCause): void {
1286
1287
  for (const proc of imp.sessions.values()) {
1287
- proc.connection?.finish({ kind: 'detached', reason: 'lost', offset: proc.end });
1288
+ proc.connection?.stop({ kind: 'detached', reason: 'lost', offset: proc.end });
1288
1289
  proc.ended = true;
1289
1290
 
1290
1291
  tryKill(proc.pty, 'SIGCONT');
@@ -1311,7 +1312,7 @@ class StubImpPort implements ImpPort {
1311
1312
  if (this.drops.count > 0) {
1312
1313
  this.drops.count -= 1;
1313
1314
 
1314
- connection.finish({
1315
+ connection.stop({
1315
1316
  kind: 'closed',
1316
1317
  reason: `code ${this.drops.closeCode}`,
1317
1318
  closeCode: this.drops.closeCode,
@@ -1325,7 +1326,7 @@ class StubImpPort implements ImpPort {
1325
1326
  if (failure !== null) {
1326
1327
  this.nextFailure = null;
1327
1328
 
1328
- connection.finish({
1329
+ connection.stop({
1329
1330
  kind: 'failed',
1330
1331
  code: failure.code,
1331
1332
  message: failure.message ?? `impd refused the session (${failure.code})`,
@@ -1338,7 +1339,7 @@ class StubImpPort implements ImpPort {
1338
1339
  const imp = this.imps.get(request.name);
1339
1340
 
1340
1341
  if (imp === undefined) {
1341
- connection.finish({
1342
+ connection.stop({
1342
1343
  kind: 'failed',
1343
1344
  code: 'NOT_FOUND',
1344
1345
  message: `no imp ${request.name}`,
@@ -1349,7 +1350,7 @@ class StubImpPort implements ImpPort {
1349
1350
  }
1350
1351
 
1351
1352
  if (imp.state !== 'running' && request.kind === 'attach' && !request.wake) {
1352
- connection.finish({
1353
+ connection.stop({
1353
1354
  kind: 'failed',
1354
1355
  code: 'INVALID_STATE',
1355
1356
  message: `imp ${imp.name} is ${imp.state}`,
@@ -1369,7 +1370,7 @@ class StubImpPort implements ImpPort {
1369
1370
  const brokerProblem = this.findBrokerProblem(imp, request, running);
1370
1371
 
1371
1372
  if (brokerProblem !== null) {
1372
- connection.finish({
1373
+ connection.stop({
1373
1374
  kind: 'failed',
1374
1375
  code: 'PRECONDITION_FAILED',
1375
1376
  message: `the broker is not ready in imp ${imp.name}`,
@@ -1382,7 +1383,7 @@ class StubImpPort implements ImpPort {
1382
1383
  if (running === undefined && request.kind === 'attach') {
1383
1384
  const previous = imp.previous.get(request.session);
1384
1385
 
1385
- connection.finish({
1386
+ connection.stop({
1386
1387
  kind: 'failed',
1387
1388
  code: 'NO_SESSION',
1388
1389
  message: `no session ${request.session}`,
@@ -1514,7 +1515,7 @@ class StubImpPort implements ImpPort {
1514
1515
  // delivers it.
1515
1516
  if (connection !== null) {
1516
1517
  imp.sessions.delete(request.session);
1517
- connection.finish({ kind: 'exit', code: exitCode, signal: null, offset: proc.end });
1518
+ connection.stop({ kind: 'exit', code: exitCode, signal: null, offset: proc.end });
1518
1519
  }
1519
1520
  });
1520
1521
 
@@ -1543,7 +1544,7 @@ class StubImpPort implements ImpPort {
1543
1544
  firstOffset: bufferStart,
1544
1545
  };
1545
1546
  } else if (resumeFrom.offset > proc.end) {
1546
- connection.finish({
1547
+ connection.stop({
1547
1548
  kind: 'failed',
1548
1549
  code: 'INVALID_RESUME',
1549
1550
  message: 'the resume offset is past the end',
@@ -1566,7 +1567,7 @@ class StubImpPort implements ImpPort {
1566
1567
  prelude = PRELUDE;
1567
1568
  }
1568
1569
 
1569
- proc.connection?.finish({
1570
+ proc.connection?.stop({
1570
1571
  kind: 'detached',
1571
1572
  reason: 'taken_over',
1572
1573
  offset: proc.connection.sent,
@@ -1619,8 +1620,13 @@ class StubImpPort implements ImpPort {
1619
1620
  }
1620
1621
 
1621
1622
  if (proc.exited !== null) {
1622
- imp.sessions.delete(findSessionName(imp, proc));
1623
- connection.finish({ kind: 'exit', code: proc.exited.code, signal: null, offset: proc.end });
1623
+ const name = findSessionName(imp, proc);
1624
+
1625
+ if (name !== undefined) {
1626
+ imp.sessions.delete(name);
1627
+ }
1628
+
1629
+ connection.stop({ kind: 'exit', code: proc.exited.code, signal: null, offset: proc.end });
1624
1630
 
1625
1631
  return;
1626
1632
  }
@@ -1650,16 +1656,10 @@ class StubImpPort implements ImpPort {
1650
1656
  * broker variable, and when it would join a process that started without
1651
1657
  * the broker required. Once the current test finishes, the stand-in kills
1652
1658
  * every process and stops every forward it holds, so it must be created inside
1653
- * a test; disposal does so sooner, and a second disposal does nothing.
1659
+ * a test; `stop` does so sooner, and a second stop does nothing.
1654
1660
  */
1655
1661
  export function createStubImpPort(principal = 'token:atc'): StubImpPort {
1656
- const port = new StubImpPort(principal);
1657
-
1658
- registerTestCleanup(() => {
1659
- port[Symbol.dispose]();
1660
- });
1661
-
1662
- return port;
1662
+ return new StubImpPort(principal);
1663
1663
  }
1664
1664
 
1665
1665
  interface StubLease {
@@ -1703,7 +1703,7 @@ interface StubConnection {
1703
1703
  sent: number;
1704
1704
  finished: boolean;
1705
1705
  process: StubProcess | null;
1706
- readonly finish: (outcome: ImpSessionOutcome) => void;
1706
+ readonly stop: (outcome: ImpSessionOutcome) => void;
1707
1707
  }
1708
1708
 
1709
1709
  interface StubRelay {
@@ -1743,7 +1743,7 @@ function tryEmit(connection: StubConnection, emit: () => void): boolean {
1743
1743
 
1744
1744
  return true;
1745
1745
  } catch (error) {
1746
- connection.finish({
1746
+ connection.stop({
1747
1747
  kind: 'local_error',
1748
1748
  detail: error instanceof Error ? error.message : String(error),
1749
1749
  });
@@ -1752,14 +1752,14 @@ function tryEmit(connection: StubConnection, emit: () => void): boolean {
1752
1752
  }
1753
1753
  }
1754
1754
 
1755
- function findSessionName(imp: StubImp, proc: StubProcess): string {
1755
+ function findSessionName(imp: StubImp, proc: StubProcess): string | undefined {
1756
1756
  for (const [name, held] of imp.sessions) {
1757
1757
  if (held === proc) {
1758
1758
  return name;
1759
1759
  }
1760
1760
  }
1761
1761
 
1762
- return '';
1762
+ return undefined;
1763
1763
  }
1764
1764
 
1765
1765
  function toSignal(name: string): NodeJS.Signals {
@@ -8,7 +8,7 @@ interface StubSystemd {
8
8
  // first on the PATH of every process that could reach a real one.
9
9
  readonly binDir: string;
10
10
 
11
- // The directory `ATC_PROC_ROOT` points at, where `placeInUnit` writes.
11
+ // The directory `ATC_PROC_ROOT` points at, where `writeUnitCgroup` writes.
12
12
  readonly procRoot: string;
13
13
 
14
14
  // The arguments of every `systemctl` call, one line per call.
@@ -22,9 +22,9 @@ interface StubSystemd {
22
22
 
23
23
  // Places the pid in a user service of the given name, as the cgroup of a
24
24
  // daemon run by a user manager reads.
25
- readonly placeInUnit: (pid: number, unit: string) => void;
25
+ readonly writeUnitCgroup: (pid: number, unit: string) => void;
26
26
 
27
- readonly [Symbol.dispose]: () => void;
27
+ readonly remove: () => void;
28
28
  }
29
29
 
30
30
  /**
@@ -36,7 +36,7 @@ interface StubSystemd {
36
36
  * `--` in the background with only the `--setenv` variables, as a transient
37
37
  * unit sees them, and appends its output to the `StandardOutput=append:`
38
38
  * file. Every call is logged. The stand-ins' directory is removed once the
39
- * current test finishes, so it must run inside a test; disposal removes it
39
+ * current test finishes, so it must run inside a test; `remove` removes it
40
40
  * sooner, and a second removal does nothing.
41
41
  */
42
42
  export function createStubSystemd(atcCommand: readonly string[]): StubSystemd {
@@ -118,7 +118,7 @@ exit 0
118
118
  writeMainPID: (pid) => {
119
119
  writeFileSync(join(root, 'mainpid'), String(pid));
120
120
  },
121
- placeInUnit: (pid, unit) => {
121
+ writeUnitCgroup: (pid, unit) => {
122
122
  mkdirSync(join(procRoot, String(pid)), { recursive: true });
123
123
 
124
124
  writeFileSync(
@@ -126,6 +126,6 @@ exit 0
126
126
  `0::/user.slice/user-${process.getuid?.() ?? 0}.slice/user@${process.getuid?.() ?? 0}.service/app.slice/${unit}\n`,
127
127
  );
128
128
  },
129
- [Symbol.dispose]: remove,
129
+ remove,
130
130
  };
131
131
  }
@@ -16,7 +16,7 @@ if (marker === undefined) {
16
16
  stopRun('a bare `bun test` runs against your real home; run `bun run test` instead');
17
17
  }
18
18
 
19
- assertTestHome(marker);
19
+ requireTestHome(marker);
20
20
 
21
21
  afterEach(() => {
22
22
  removeEnvOverrides();
@@ -26,7 +26,7 @@ afterEach(() => {
26
26
  // points under it and no enclosing session's variables remain; a stale or
27
27
  // hand-set marker stops the run before any test imports atc. An inherited
28
28
  // CLAUDE_CONFIG_DIR would move the Claude config folder out of the home.
29
- function assertTestHome(root: string): void {
29
+ function requireTestHome(root: string): void {
30
30
  const expected: Readonly<Record<string, string>> = {
31
31
  HOME: join(root, 'home'),
32
32
  XDG_RUNTIME_DIR: join(root, 'runtime'),
@@ -42,9 +42,12 @@ function assertTestHome(root: string): void {
42
42
  .filter(([name, path]) => normalize(process.env[name] ?? '') !== path)
43
43
  .map(([name]) => name);
44
44
 
45
- const leftover = ['ATC_SESSION_ID', 'ATC_SOCKET', 'CLAUDE_CONFIG_DIR'].filter(
46
- (name) => name in process.env,
47
- );
45
+ const leftover = [
46
+ 'ATC_SESSION_ID',
47
+ 'ATC_SOCKET',
48
+ 'ATC_SESSION_RECORD',
49
+ 'CLAUDE_CONFIG_DIR',
50
+ ].filter((name) => name in process.env);
48
51
 
49
52
  if (mismatched.length > 0 || leftover.length > 0) {
50
53
  stopRun(
@@ -9,14 +9,18 @@ import type { CompiledQuery } from 'kysely';
9
9
  * before returning, so the query never runs and nothing is written.
10
10
  */
11
11
  export function readQueryPlan(dbPath: string, query: CompiledQuery): string {
12
- using db = new Database(dbPath, { readonly: true });
12
+ const db = new Database(dbPath, { readonly: true });
13
13
 
14
14
  // oxlint-disable-next-line no-unsafe-type-assertion -- kysely's sqlite compiler only ever binds bun:sqlite-legal values
15
15
  const bindings = query.parameters as SQLQueryBindings[];
16
16
 
17
- return db
18
- .query<{ detail: string }, SQLQueryBindings[]>(`EXPLAIN QUERY PLAN ${query.sql}`)
19
- .all(...bindings)
20
- .map((row) => row.detail)
21
- .join('\n');
17
+ try {
18
+ return db
19
+ .query<{ detail: string }, SQLQueryBindings[]>(`EXPLAIN QUERY PLAN ${query.sql}`)
20
+ .all(...bindings)
21
+ .map((row) => row.detail)
22
+ .join('\n');
23
+ } finally {
24
+ db.close();
25
+ }
22
26
  }
@@ -7,16 +7,16 @@ let pending: (() => unknown)[] | null = null;
7
7
  /**
8
8
  * Registers the release of a resource a test just acquired, to run once the
9
9
  * current test finishes, and returns that release for the resource's own
10
- * stop and dispose members. Every release a test registers runs from one
11
- * hook, last registered first, as `using` declarations release, so a
12
- * directory made before a process that lives in it is removed only after
13
- * that process stops. A release that throws stops none of the others: the
14
- * hook rethrows once every release has run, as an `AggregateError` when
15
- * more than one threw. The returned release runs its callback once: every
16
- * later call, from the hook or from the test, returns what the first call
17
- * returned. Bun runs the hook after every `afterEach`, so a release works
18
- * from the paths and handles it captured. Throws outside a test, where
19
- * nothing would run the hook.
10
+ * early release, such as a `stop` member. Every release a test registers
11
+ * runs from one hook, last registered first, so a directory made before a
12
+ * process that lives in it is removed only after that process stops. A
13
+ * release that throws stops none of the others: the hook rethrows once
14
+ * every release has run, as an `AggregateError` when more than one threw.
15
+ * The returned release runs its callback once: every later call, from the
16
+ * hook or from the test, returns what the first call returned. Bun runs the
17
+ * hook after every `afterEach`, so a release works from the paths and
18
+ * handles it captured. Throws outside a test, where nothing would run the
19
+ * hook.
20
20
  */
21
21
  export function registerTestCleanup<T>(release: () => T): () => T {
22
22
  let released: { readonly value: T } | null = null;
@@ -15,12 +15,13 @@ import { waitFor } from './wait-for';
15
15
  * no extra arguments. A server started with this home as its `HOME` and
16
16
  * `XDG_RUNTIME_DIR` boots its daemon here. Each stand-in appends its pid,
17
17
  * which is its process group as a session leader, to `stub-pids` in the
18
- * home. Disposal stops the daemon, when one wrote its pid file here, and
18
+ * home. `teardown` stops the daemon, when one wrote its pid file here, and
19
19
  * waits for it to exit; then it kills the daemon's process group and every
20
20
  * group `stub-pids` records, such as a reporter a stopped session left
21
21
  * behind, waits until each group is empty, and removes the home. That
22
- * disposal runs once the current test finishes, so it must run inside a
23
- * test; disposing sooner runs it then, and a second disposal does nothing.
22
+ * teardown runs once the current test finishes, so it must run inside a
23
+ * test; calling `teardown` sooner runs it then, and a second teardown does
24
+ * nothing.
24
25
  */
25
26
  export function setupMCPHome() {
26
27
  const tmp = setupTempDir('atc-mcp-');
@@ -29,9 +30,9 @@ export function setupMCPHome() {
29
30
  // stand-ins stop before the home that records them is removed.
30
31
  const stack = new AsyncDisposableStack();
31
32
 
32
- const dispose = registerTestCleanup(() => stack.disposeAsync());
33
+ const teardown = registerTestCleanup(() => stack.disposeAsync());
33
34
 
34
- stack.use(tmp);
35
+ stack.defer(tmp.teardown);
35
36
 
36
37
  const home = tmp.dir;
37
38
 
@@ -60,7 +61,7 @@ export function setupMCPHome() {
60
61
  home,
61
62
  claudeBin,
62
63
  grokBin,
63
- [Symbol.asyncDispose]: dispose,
64
+ teardown,
64
65
  };
65
66
  }
66
67
 
@@ -21,13 +21,13 @@ interface MCPHTTPSetupOptions {
21
21
  * collected. The authorization server's database sits where atc keeps it
22
22
  * under `home`, so a CLI run with that home opens the same file. `store` is
23
23
  * the authorization server's database opened a second time the way
24
- * `atc clients` and `atc grants` open it, and `addClient` adds a client
24
+ * `atc clients` and `atc grants` open it, and `createClient` adds a client
25
25
  * through it and returns the client id. `restartDaemon` stops the daemon and
26
26
  * starts a fresh one on the same socket and database, the way an operator
27
27
  * restarts it, with the principals it is given; `countDaemonClients` reads
28
28
  * how many connections the current daemon holds open. Everything it starts
29
29
  * stops, and the directory is removed, once the current test finishes, so it
30
- * must run inside a test; disposal does so sooner, and a second disposal
30
+ * must run inside a test; `teardown` does so sooner, and a second teardown
31
31
  * does nothing.
32
32
  */
33
33
  export async function setupMCPHTTP(options: MCPHTTPSetupOptions = {}) {
@@ -37,9 +37,9 @@ export async function setupMCPHTTP(options: MCPHTTPSetupOptions = {}) {
37
37
  // before the one it depends on, and the directory goes last.
38
38
  const stack = new AsyncDisposableStack();
39
39
 
40
- const dispose = registerTestCleanup(() => stack.disposeAsync());
40
+ const teardown = registerTestCleanup(() => stack.disposeAsync());
41
41
 
42
- stack.use(tmp);
42
+ stack.defer(tmp.teardown);
43
43
 
44
44
  const socketPath = join(tmp.dir, 'daemon.sock');
45
45
  const stateDir = join(tmp.dir, '.local', 'state', 'atc');
@@ -102,7 +102,7 @@ export async function setupMCPHTTP(options: MCPHTTPSetupOptions = {}) {
102
102
  requests,
103
103
  caller,
104
104
  store,
105
- async addClient(name: string, redirectURIs: readonly string[]): Promise<string> {
105
+ async createClient(name: string, redirectURIs: readonly string[]): Promise<string> {
106
106
  const created = await store.auth.api.createFixedClient({
107
107
  body: { name, redirectURIs: [...redirectURIs] },
108
108
  });
@@ -117,6 +117,6 @@ export async function setupMCPHTTP(options: MCPHTTPSetupOptions = {}) {
117
117
 
118
118
  daemon = await startTestDaemon(principals);
119
119
  },
120
- [Symbol.asyncDispose]: dispose,
120
+ teardown,
121
121
  };
122
122
  }
@@ -5,30 +5,24 @@ import { registerTestCleanup } from './register-test-cleanup';
5
5
 
6
6
  interface TempDir {
7
7
  readonly dir: string;
8
- readonly [Symbol.dispose]: () => void;
9
- readonly [Symbol.asyncDispose]: () => Promise<void>;
8
+ readonly teardown: () => void;
10
9
  }
11
10
 
12
11
  /**
13
12
  * Creates a directory under the system temp root, named by the prefix, and
14
13
  * registers its removal to run once the current test finishes, so it must
15
- * run inside a test. Disposal removes the tree sooner; whichever comes
14
+ * run inside a test. `teardown` removes the tree sooner; whichever comes
16
15
  * second does nothing.
17
16
  */
18
17
  export function setupTempDir(prefix: string): TempDir {
19
18
  const dir = mkdtempSync(join(tmpdir(), prefix));
20
19
 
21
- const remove = registerTestCleanup(() => {
20
+ const teardown = registerTestCleanup(() => {
22
21
  rmSync(dir, { recursive: true, force: true });
23
22
  });
24
23
 
25
24
  return {
26
25
  dir,
27
- [Symbol.dispose]: remove,
28
- [Symbol.asyncDispose]: () => {
29
- remove();
30
-
31
- return Promise.resolve();
32
- },
26
+ teardown,
33
27
  };
34
28
  }
@@ -25,7 +25,6 @@ export interface CutProxy {
25
25
  // How many answers the proxy has swallowed in hold mode.
26
26
  readonly countHeld: () => number;
27
27
  readonly stop: () => void;
28
- readonly [Symbol.dispose]: () => void;
29
28
  }
30
29
 
31
30
  // One proxied connection: the target side once it is connected, what the
@@ -49,8 +48,8 @@ interface ProxyLink {
49
48
  * the method it loses the target's next answer after the request reached
50
49
  * the target: the request runs, and the client never sees its response.
51
50
  * It stops, ending every link, once the current test finishes, so it must
52
- * run inside a test; `stop` or disposal stops it sooner, and a second stop
53
- * does nothing.
51
+ * run inside a test; `stop` stops it sooner, and a second stop does
52
+ * nothing.
54
53
  */
55
54
  export function startCutProxy(options: CutProxyOptions): CutProxy {
56
55
  let cutsLeft = options.cuts;
@@ -139,7 +138,6 @@ export function startCutProxy(options: CutProxyOptions): CutProxy {
139
138
  countRequests: () => requests,
140
139
  countHeld: () => held,
141
140
  stop,
142
- [Symbol.dispose]: stop,
143
141
  };
144
142
  }
145
143
 
@@ -39,12 +39,12 @@ interface DaemonBoot {
39
39
  * sending no handshake, and fails at once with the daemon's stderr when the
40
40
  * daemon exits before it listens. `restart` stops the daemon with the given
41
41
  * signal, waits for it to exit, and starts another on the same home with the
42
- * same config. `readStderr` reads what the current boot printed. Disposal
42
+ * same config. `readStderr` reads what the current boot printed. `stop`
43
43
  * closes every client it opened, kills the daemon and waits for it to exit,
44
44
  * then kills the daemon the home's state directory records, which a restart
45
- * the test asked atc for may have started. That disposal runs once the
46
- * current test finishes, so it must run inside a test; disposing sooner
47
- * runs it then, and a second disposal does nothing.
45
+ * the test asked atc for may have started. That stop runs once the current
46
+ * test finishes, so it must run inside a test; calling `stop` sooner runs it
47
+ * then, and a second stop does nothing.
48
48
  */
49
49
  export function startDaemonProcess(config: Readonly<DaemonProcessConfig>) {
50
50
  const socketPath = join(config.home, 'atc-daemon.sock');
@@ -53,7 +53,7 @@ export function startDaemonProcess(config: Readonly<DaemonProcessConfig>) {
53
53
  const clients = new Set<DaemonClient>();
54
54
 
55
55
  let boots = 0;
56
- let disposed = false;
56
+ let stopped = false;
57
57
 
58
58
  const boot = (): DaemonBoot => {
59
59
  boots++;
@@ -79,8 +79,8 @@ export function startDaemonProcess(config: Readonly<DaemonProcessConfig>) {
79
79
 
80
80
  let current = boot();
81
81
 
82
- const dispose = registerTestCleanup(async () => {
83
- disposed = true;
82
+ const stop = registerTestCleanup(async () => {
83
+ stopped = true;
84
84
 
85
85
  for (const client of clients) {
86
86
  client.stop();
@@ -107,12 +107,12 @@ export function startDaemonProcess(config: Readonly<DaemonProcessConfig>) {
107
107
  }
108
108
  };
109
109
 
110
- // A client that opens after the wait for it ended, or after disposal,
110
+ // A client that opens after the wait for it ended, or after the stop,
111
111
  // closes at once, so nothing outlives the daemon it reached.
112
112
  const openTrackedClient = async (isAbandoned: () => boolean) => {
113
113
  const client = await DaemonClient.open(socketPath);
114
114
 
115
- if (disposed || isAbandoned()) {
115
+ if (stopped || isAbandoned()) {
116
116
  client.stop();
117
117
  throw new Error('the wait for the daemon ended before this client opened');
118
118
  }
@@ -132,7 +132,7 @@ export function startDaemonProcess(config: Readonly<DaemonProcessConfig>) {
132
132
  const openAfterExit = async () => {
133
133
  await watched.exited;
134
134
 
135
- if (settled || disposed) {
135
+ if (settled || stopped) {
136
136
  return null;
137
137
  }
138
138
 
@@ -146,10 +146,10 @@ export function startDaemonProcess(config: Readonly<DaemonProcessConfig>) {
146
146
  };
147
147
 
148
148
  // Polls the socket while the daemon runs. Once the other wait settles or
149
- // the daemon is disposed, each attempt resolves empty, which ends the
149
+ // the daemon stops, each attempt resolves empty, which ends the
150
150
  // polling.
151
151
  const openWhenListening = () =>
152
- waitFor(() => (settled || disposed ? null : openTrackedClient(isSettled)), {
152
+ waitFor(() => (settled || stopped ? null : openTrackedClient(isSettled)), {
153
153
  timeoutMs: 15_000,
154
154
  intervalMs: 50,
155
155
  });
@@ -188,6 +188,6 @@ export function startDaemonProcess(config: Readonly<DaemonProcessConfig>) {
188
188
 
189
189
  current = boot();
190
190
  },
191
- [Symbol.asyncDispose]: dispose,
191
+ stop,
192
192
  };
193
193
  }
@@ -7,7 +7,6 @@ interface GitHTTPServer {
7
7
  // Every Authorization header a request carried, in arrival order.
8
8
  readonly authorizations: string[];
9
9
  readonly stop: () => Promise<void>;
10
- readonly [Symbol.asyncDispose]: () => Promise<void>;
11
10
  }
12
11
 
13
12
  interface GitHTTPServerOptions {
@@ -30,8 +29,8 @@ interface GitHTTPServerOptions {
30
29
  * clone through it slow. An `onRequest` callback runs, and is awaited, while
31
30
  * each authenticated request is held, so it sees the client that sent the
32
31
  * request still running. It stops once the current test finishes, so it
33
- * must run inside a test; `stop` or disposal stops it sooner, and a second
34
- * stop does nothing.
32
+ * must run inside a test; `stop` stops it sooner, and a second stop does
33
+ * nothing.
35
34
  */
36
35
  export function startGitHTTPServer(
37
36
  root: string,
@@ -97,7 +96,6 @@ export function startGitHTTPServer(
97
96
  url: `http://127.0.0.1:${server.port}/`,
98
97
  authorizations,
99
98
  stop,
100
- [Symbol.asyncDispose]: stop,
101
99
  };
102
100
  }
103
101