@zgeoff/atc 3.4.1 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agents/agent-adapter.ts +4 -0
- package/src/agents/build-claude-query-options.ts +1 -0
- package/src/agents/build-headless-env.ts +4 -0
- package/src/agents/claude-adapter.ts +4 -3
- package/src/agents/gateway-adapter.ts +2 -2
- package/src/agents/plan-claude-line-input.ts +44 -0
- package/src/client/daemon-client.ts +6 -1
- package/src/collect-restart-env.ts +6 -3
- package/src/daemon/build-scoped-context.ts +2 -0
- package/src/daemon/check-session-scope.ts +235 -0
- package/src/daemon/daemon-connection.ts +74 -0
- package/src/daemon/daemon-context.ts +21 -0
- package/src/daemon/daemon.ts +21 -0
- package/src/daemon/find-host-branch.ts +28 -0
- package/src/daemon/materialize-workspace.ts +6 -1
- package/src/daemon/merge-recorded-scope.ts +50 -0
- package/src/daemon/published-records.ts +269 -0
- package/src/daemon/request-access-classes.ts +1 -0
- package/src/daemon/run-host-git.ts +52 -0
- package/src/daemon/sessions.ts +185 -53
- package/src/daemon/start-headless-turn.ts +3 -0
- package/src/federation/id-rules.ts +11 -2
- package/src/mcp/mcp-tools.ts +51 -1
- package/src/mcp/require-daemon-features.ts +1 -0
- package/src/mcp/run-tool.ts +14 -0
- package/src/protocol/daemon-features.ts +5 -0
- package/src/protocol/parse-declared-scope.ts +158 -0
- package/src/protocol/protocol.ts +1 -0
- package/src/protocol/published-record.ts +62 -0
- package/src/protocol/request-param-schemas.ts +16 -0
- package/src/store/parse-published-record.ts +59 -0
- package/src/store/run-migrations.ts +23 -0
- package/src/store/state-store.ts +32 -0
- package/src/test-utils/build-mock-published-record.ts +39 -0
- package/src/test-utils/build-stub-brokered-agent-adapter.ts +3 -1
- package/src/test-utils/build-stub-daemon-context.ts +2 -0
- package/src/test-utils/build-stub-daemon-requests.ts +3 -3
- package/src/test-utils/build-stub-harness-relay.ts +2 -2
- package/src/test-utils/build-stub-held-gh.ts +5 -7
- package/src/test-utils/build-stub-held-inbox-source.ts +2 -2
- package/src/test-utils/build-stub-held-zoxide.ts +4 -6
- package/src/test-utils/build-stub-host-hold.ts +7 -6
- package/src/test-utils/build-stub-launch-admission.ts +20 -0
- package/src/test-utils/build-stub-pregranted-broker-host.ts +22 -0
- package/src/test-utils/build-stub-terminal.ts +3 -3
- package/src/test-utils/create-git-fixture.ts +2 -2
- package/src/test-utils/create-stub-claude.ts +3 -1
- package/src/test-utils/create-stub-imp-port.ts +46 -37
- package/src/test-utils/create-stub-systemd.ts +6 -6
- package/src/test-utils/isolate-home.ts +8 -5
- package/src/test-utils/read-query-plan.ts +10 -6
- package/src/test-utils/register-test-cleanup.ts +10 -10
- package/src/test-utils/{run-legacy-daemon.ts → run-stub-legacy-daemon.ts} +1 -1
- package/src/test-utils/setup-mcp-home.ts +7 -6
- package/src/test-utils/setup-mcp-http.ts +6 -6
- package/src/test-utils/setup-temp-dir.ts +4 -10
- package/src/test-utils/start-cut-proxy.ts +2 -4
- package/src/test-utils/start-daemon-process.ts +13 -13
- package/src/test-utils/start-git-http-server.ts +2 -4
- package/src/test-utils/start-mcp-stdio.ts +5 -5
- package/src/test-utils/start-stub-answering-listener.ts +3 -3
- package/src/test-utils/start-stub-closing-listener.ts +3 -3
- package/src/test-utils/start-stub-dropping-daemon.ts +2 -3
- package/src/test-utils/start-stub-impd-info.ts +3 -7
- package/src/test-utils/start-stub-impd.ts +3 -5
- package/src/test-utils/start-stub-legacy-daemon.ts +2 -3
- package/src/test-utils/start-stub-mcp-server.ts +3 -5
- package/src/test-utils/start-stub-recording-listener.ts +3 -3
- package/src/test-utils/start-stub-refusing-listener.ts +3 -3
- package/src/test-utils/start-stub-reporter-socket.ts +3 -9
- package/src/test-utils/start-stub-restore-daemon.ts +2 -7
- package/src/test-utils/start-stub-session-bridge.ts +3 -9
- package/src/test-utils/start-stub-silent-server.ts +3 -6
- package/src/test-utils/start-stub-source-daemon.ts +3 -3
- package/src/test-utils/start-stub-stalled-client.ts +3 -3
- package/src/test-utils/start-stub-stalled-listener.ts +3 -3
- package/src/test-utils/start-stub-tcp-daemon.ts +3 -3
- package/src/test-utils/start-stub-telemetry-collector.ts +2 -2
- package/src/test-utils/start-stub-unanswering-listener.ts +3 -3
- package/src/test-utils/start-stub-unanswering-unix-listener.ts +3 -3
- package/src/test-utils/start-test-daemon.ts +62 -18
- package/src/test-utils/start-tui-harness.ts +17 -10
- package/src/test-utils/subscribe-to-socket-lines.ts +5 -9
- package/src/test-utils/{can-bind-addresses.ts → try-bind-addresses.ts} +4 -3
- /package/src/test-utils/{run-source-daemon.ts → run-stub-source-daemon.ts} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A terminal that keeps every chunk written to it, in order, and draws
|
|
3
|
-
* nothing. `getText` returns everything written. `
|
|
4
|
-
*
|
|
3
|
+
* nothing. `getText` returns everything written. `countChunks` returns how
|
|
4
|
+
* many chunks are written so far, and `getTextSince` returns only
|
|
5
5
|
* what was written after that position, so a test reads only the output of
|
|
6
6
|
* the action it checks.
|
|
7
7
|
*/
|
|
@@ -13,7 +13,7 @@ export function buildStubTerminal() {
|
|
|
13
13
|
chunks.push(chunk);
|
|
14
14
|
},
|
|
15
15
|
getText: (): string => chunks.join(''),
|
|
16
|
-
|
|
16
|
+
countChunks: (): number => chunks.length,
|
|
17
17
|
getTextSince: (mark: number): string => chunks.slice(mark).join(''),
|
|
18
18
|
};
|
|
19
19
|
}
|
|
@@ -23,7 +23,7 @@ interface GitFixtureConfig {
|
|
|
23
23
|
* The first call in a process builds a template pair under the test home,
|
|
24
24
|
* and each call copies it, so a fixture costs a copy instead of a run of
|
|
25
25
|
* git commands. The directory is removed once the current test finishes,
|
|
26
|
-
* so it must run inside a test;
|
|
26
|
+
* so it must run inside a test; `remove` removes it sooner, and a second
|
|
27
27
|
* removal does nothing.
|
|
28
28
|
*/
|
|
29
29
|
export async function createGitFixture(config: GitFixtureConfig = {}) {
|
|
@@ -56,7 +56,7 @@ export async function createGitFixture(config: GitFixtureConfig = {}) {
|
|
|
56
56
|
upstream,
|
|
57
57
|
work,
|
|
58
58
|
sha: template.sha,
|
|
59
|
-
|
|
59
|
+
remove: tmp.teardown,
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -29,7 +29,8 @@ interface StubClaudeConfig {
|
|
|
29
29
|
* first stub to find it removes it; `fake-claude-tap` runs `atc tap` into
|
|
30
30
|
* `$HOME/tap.jsonl`; `fake-claude-events.jsonl` holds more hook payloads to
|
|
31
31
|
* report, one per line, after the `Notification`; `fake-claude-exit` exits
|
|
32
|
-
* once they are reported
|
|
32
|
+
* once they are reported; `fake-claude-composer-last` runs the composer once
|
|
33
|
+
* they are reported.
|
|
33
34
|
*/
|
|
34
35
|
export function createStubClaude(dir: string, config: StubClaudeConfig): string {
|
|
35
36
|
const atc = config.atc.map((part) => `"${part}"`).join(' ');
|
|
@@ -69,6 +70,7 @@ if [ -f "$HOME/fake-claude-events.jsonl" ]; then
|
|
|
69
70
|
done < "$HOME/fake-claude-events.jsonl"
|
|
70
71
|
fi
|
|
71
72
|
if [ -f "$HOME/fake-claude-exit" ]; then exit 0; fi
|
|
73
|
+
if [ -f "$HOME/fake-claude-composer-last" ]; then exec "${process.execPath}" "${config.composer}"; fi
|
|
72
74
|
while read -r line; do echo "GOT:$line"; done
|
|
73
75
|
`,
|
|
74
76
|
);
|
|
@@ -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.
|
|
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
|
/**
|
|
@@ -121,6 +119,9 @@ class StubImpPort implements ImpPort {
|
|
|
121
119
|
// Feature reads still to fail as an unreachable impd before they answer.
|
|
122
120
|
private featureFailures = 0;
|
|
123
121
|
|
|
122
|
+
// Feature reads that failed as an unreachable impd.
|
|
123
|
+
private failedFeatureReads = 0;
|
|
124
|
+
|
|
124
125
|
// Session connections whose opening still throws before it returns, as
|
|
125
126
|
// an invalid authorization header makes the socket's constructor throw.
|
|
126
127
|
private openFailures = 0;
|
|
@@ -198,6 +199,7 @@ class StubImpPort implements ImpPort {
|
|
|
198
199
|
|
|
199
200
|
if (this.featureFailures > 0) {
|
|
200
201
|
this.featureFailures -= 1;
|
|
202
|
+
this.failedFeatureReads += 1;
|
|
201
203
|
|
|
202
204
|
return Promise.reject(new ImpPortError('UNREACHABLE', 'impd did not answer'));
|
|
203
205
|
}
|
|
@@ -471,7 +473,7 @@ class StubImpPort implements ImpPort {
|
|
|
471
473
|
}
|
|
472
474
|
|
|
473
475
|
for (const proc of imp.sessions.values()) {
|
|
474
|
-
proc.connection?.
|
|
476
|
+
proc.connection?.stop({ kind: 'closed', reason: 'imp destroyed', closeCode: 1000 });
|
|
475
477
|
proc.connection = null;
|
|
476
478
|
proc.exited ??= { code: null };
|
|
477
479
|
|
|
@@ -500,7 +502,7 @@ class StubImpPort implements ImpPort {
|
|
|
500
502
|
sent: 0,
|
|
501
503
|
finished: false,
|
|
502
504
|
process: null,
|
|
503
|
-
|
|
505
|
+
stop: (result) => {
|
|
504
506
|
if (connection.finished) {
|
|
505
507
|
return;
|
|
506
508
|
}
|
|
@@ -519,7 +521,7 @@ class StubImpPort implements ImpPort {
|
|
|
519
521
|
// lets it go; a closed gate sends nothing.
|
|
520
522
|
const sendToImpd = () => {
|
|
521
523
|
if (gate !== undefined && !tryPassGate(gate)) {
|
|
522
|
-
connection.
|
|
524
|
+
connection.stop({ kind: 'closed', reason: 'closed before sending', closeCode: 1000 });
|
|
523
525
|
|
|
524
526
|
return;
|
|
525
527
|
}
|
|
@@ -544,7 +546,7 @@ class StubImpPort implements ImpPort {
|
|
|
544
546
|
this.upgradeFailures -= 1;
|
|
545
547
|
|
|
546
548
|
setTimeout(() => {
|
|
547
|
-
connection.
|
|
549
|
+
connection.stop({ kind: 'unreachable', detail: 'the upgrade failed' });
|
|
548
550
|
}, 0);
|
|
549
551
|
} else if (this.upgrades === null) {
|
|
550
552
|
sendToImpd();
|
|
@@ -571,7 +573,7 @@ class StubImpPort implements ImpPort {
|
|
|
571
573
|
}
|
|
572
574
|
},
|
|
573
575
|
close: () => {
|
|
574
|
-
connection.
|
|
576
|
+
connection.stop({ kind: 'closed', reason: 'closed by the client', closeCode: 1000 });
|
|
575
577
|
},
|
|
576
578
|
};
|
|
577
579
|
}
|
|
@@ -848,7 +850,7 @@ class StubImpPort implements ImpPort {
|
|
|
848
850
|
stopConnection(name: string, session: string, closeCode: number): void {
|
|
849
851
|
const proc = this.getImp(name).sessions.get(session);
|
|
850
852
|
|
|
851
|
-
proc?.connection?.
|
|
853
|
+
proc?.connection?.stop({ kind: 'closed', reason: `code ${closeCode}`, closeCode });
|
|
852
854
|
}
|
|
853
855
|
|
|
854
856
|
/**
|
|
@@ -1063,7 +1065,7 @@ class StubImpPort implements ImpPort {
|
|
|
1063
1065
|
hold.done.resolve();
|
|
1064
1066
|
};
|
|
1065
1067
|
|
|
1066
|
-
return { entered: hold.entered.promise, stop
|
|
1068
|
+
return { entered: hold.entered.promise, stop };
|
|
1067
1069
|
}
|
|
1068
1070
|
|
|
1069
1071
|
// How many commands wait for their command hold to stop.
|
|
@@ -1109,6 +1111,11 @@ class StubImpPort implements ImpPort {
|
|
|
1109
1111
|
this.featureFailures = count;
|
|
1110
1112
|
}
|
|
1111
1113
|
|
|
1114
|
+
// How many feature reads failed as an unreachable impd.
|
|
1115
|
+
countFailedFeatureReads(): number {
|
|
1116
|
+
return this.failedFeatureReads;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1112
1119
|
/**
|
|
1113
1120
|
* Refuses the next session request with an impd code and its data, or the
|
|
1114
1121
|
* message of the refusal when it carries one instead.
|
|
@@ -1163,7 +1170,10 @@ class StubImpPort implements ImpPort {
|
|
|
1163
1170
|
return proc.end;
|
|
1164
1171
|
}
|
|
1165
1172
|
|
|
1166
|
-
|
|
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(() => {
|
|
1167
1177
|
this.stopCommandHold();
|
|
1168
1178
|
|
|
1169
1179
|
for (const imp of this.imps.values()) {
|
|
@@ -1178,7 +1188,7 @@ class StubImpPort implements ImpPort {
|
|
|
1178
1188
|
}
|
|
1179
1189
|
|
|
1180
1190
|
this.imps.clear();
|
|
1181
|
-
}
|
|
1191
|
+
});
|
|
1182
1192
|
|
|
1183
1193
|
// impd's refusal of a grant or revoke: the scope and pattern checks run
|
|
1184
1194
|
// before anything is looked up, then a missing imp or secret.
|
|
@@ -1269,13 +1279,13 @@ class StubImpPort implements ImpPort {
|
|
|
1269
1279
|
|
|
1270
1280
|
for (const proc of imp.sessions.values()) {
|
|
1271
1281
|
tryKill(proc.pty, 'SIGSTOP');
|
|
1272
|
-
proc.connection?.
|
|
1282
|
+
proc.connection?.stop({ kind: 'detached', reason: 'lost', offset: proc.end });
|
|
1273
1283
|
}
|
|
1274
1284
|
}
|
|
1275
1285
|
|
|
1276
1286
|
private bootCold(imp: StubImp, cause: ColdBootCause): void {
|
|
1277
1287
|
for (const proc of imp.sessions.values()) {
|
|
1278
|
-
proc.connection?.
|
|
1288
|
+
proc.connection?.stop({ kind: 'detached', reason: 'lost', offset: proc.end });
|
|
1279
1289
|
proc.ended = true;
|
|
1280
1290
|
|
|
1281
1291
|
tryKill(proc.pty, 'SIGCONT');
|
|
@@ -1302,7 +1312,7 @@ class StubImpPort implements ImpPort {
|
|
|
1302
1312
|
if (this.drops.count > 0) {
|
|
1303
1313
|
this.drops.count -= 1;
|
|
1304
1314
|
|
|
1305
|
-
connection.
|
|
1315
|
+
connection.stop({
|
|
1306
1316
|
kind: 'closed',
|
|
1307
1317
|
reason: `code ${this.drops.closeCode}`,
|
|
1308
1318
|
closeCode: this.drops.closeCode,
|
|
@@ -1316,7 +1326,7 @@ class StubImpPort implements ImpPort {
|
|
|
1316
1326
|
if (failure !== null) {
|
|
1317
1327
|
this.nextFailure = null;
|
|
1318
1328
|
|
|
1319
|
-
connection.
|
|
1329
|
+
connection.stop({
|
|
1320
1330
|
kind: 'failed',
|
|
1321
1331
|
code: failure.code,
|
|
1322
1332
|
message: failure.message ?? `impd refused the session (${failure.code})`,
|
|
@@ -1329,7 +1339,7 @@ class StubImpPort implements ImpPort {
|
|
|
1329
1339
|
const imp = this.imps.get(request.name);
|
|
1330
1340
|
|
|
1331
1341
|
if (imp === undefined) {
|
|
1332
|
-
connection.
|
|
1342
|
+
connection.stop({
|
|
1333
1343
|
kind: 'failed',
|
|
1334
1344
|
code: 'NOT_FOUND',
|
|
1335
1345
|
message: `no imp ${request.name}`,
|
|
@@ -1340,7 +1350,7 @@ class StubImpPort implements ImpPort {
|
|
|
1340
1350
|
}
|
|
1341
1351
|
|
|
1342
1352
|
if (imp.state !== 'running' && request.kind === 'attach' && !request.wake) {
|
|
1343
|
-
connection.
|
|
1353
|
+
connection.stop({
|
|
1344
1354
|
kind: 'failed',
|
|
1345
1355
|
code: 'INVALID_STATE',
|
|
1346
1356
|
message: `imp ${imp.name} is ${imp.state}`,
|
|
@@ -1360,7 +1370,7 @@ class StubImpPort implements ImpPort {
|
|
|
1360
1370
|
const brokerProblem = this.findBrokerProblem(imp, request, running);
|
|
1361
1371
|
|
|
1362
1372
|
if (brokerProblem !== null) {
|
|
1363
|
-
connection.
|
|
1373
|
+
connection.stop({
|
|
1364
1374
|
kind: 'failed',
|
|
1365
1375
|
code: 'PRECONDITION_FAILED',
|
|
1366
1376
|
message: `the broker is not ready in imp ${imp.name}`,
|
|
@@ -1373,7 +1383,7 @@ class StubImpPort implements ImpPort {
|
|
|
1373
1383
|
if (running === undefined && request.kind === 'attach') {
|
|
1374
1384
|
const previous = imp.previous.get(request.session);
|
|
1375
1385
|
|
|
1376
|
-
connection.
|
|
1386
|
+
connection.stop({
|
|
1377
1387
|
kind: 'failed',
|
|
1378
1388
|
code: 'NO_SESSION',
|
|
1379
1389
|
message: `no session ${request.session}`,
|
|
@@ -1505,7 +1515,7 @@ class StubImpPort implements ImpPort {
|
|
|
1505
1515
|
// delivers it.
|
|
1506
1516
|
if (connection !== null) {
|
|
1507
1517
|
imp.sessions.delete(request.session);
|
|
1508
|
-
connection.
|
|
1518
|
+
connection.stop({ kind: 'exit', code: exitCode, signal: null, offset: proc.end });
|
|
1509
1519
|
}
|
|
1510
1520
|
});
|
|
1511
1521
|
|
|
@@ -1534,7 +1544,7 @@ class StubImpPort implements ImpPort {
|
|
|
1534
1544
|
firstOffset: bufferStart,
|
|
1535
1545
|
};
|
|
1536
1546
|
} else if (resumeFrom.offset > proc.end) {
|
|
1537
|
-
connection.
|
|
1547
|
+
connection.stop({
|
|
1538
1548
|
kind: 'failed',
|
|
1539
1549
|
code: 'INVALID_RESUME',
|
|
1540
1550
|
message: 'the resume offset is past the end',
|
|
@@ -1557,7 +1567,7 @@ class StubImpPort implements ImpPort {
|
|
|
1557
1567
|
prelude = PRELUDE;
|
|
1558
1568
|
}
|
|
1559
1569
|
|
|
1560
|
-
proc.connection?.
|
|
1570
|
+
proc.connection?.stop({
|
|
1561
1571
|
kind: 'detached',
|
|
1562
1572
|
reason: 'taken_over',
|
|
1563
1573
|
offset: proc.connection.sent,
|
|
@@ -1610,8 +1620,13 @@ class StubImpPort implements ImpPort {
|
|
|
1610
1620
|
}
|
|
1611
1621
|
|
|
1612
1622
|
if (proc.exited !== null) {
|
|
1613
|
-
|
|
1614
|
-
|
|
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 });
|
|
1615
1630
|
|
|
1616
1631
|
return;
|
|
1617
1632
|
}
|
|
@@ -1641,16 +1656,10 @@ class StubImpPort implements ImpPort {
|
|
|
1641
1656
|
* broker variable, and when it would join a process that started without
|
|
1642
1657
|
* the broker required. Once the current test finishes, the stand-in kills
|
|
1643
1658
|
* every process and stops every forward it holds, so it must be created inside
|
|
1644
|
-
* a test;
|
|
1659
|
+
* a test; `stop` does so sooner, and a second stop does nothing.
|
|
1645
1660
|
*/
|
|
1646
1661
|
export function createStubImpPort(principal = 'token:atc'): StubImpPort {
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
registerTestCleanup(() => {
|
|
1650
|
-
port[Symbol.dispose]();
|
|
1651
|
-
});
|
|
1652
|
-
|
|
1653
|
-
return port;
|
|
1662
|
+
return new StubImpPort(principal);
|
|
1654
1663
|
}
|
|
1655
1664
|
|
|
1656
1665
|
interface StubLease {
|
|
@@ -1694,7 +1703,7 @@ interface StubConnection {
|
|
|
1694
1703
|
sent: number;
|
|
1695
1704
|
finished: boolean;
|
|
1696
1705
|
process: StubProcess | null;
|
|
1697
|
-
readonly
|
|
1706
|
+
readonly stop: (outcome: ImpSessionOutcome) => void;
|
|
1698
1707
|
}
|
|
1699
1708
|
|
|
1700
1709
|
interface StubRelay {
|
|
@@ -1734,7 +1743,7 @@ function tryEmit(connection: StubConnection, emit: () => void): boolean {
|
|
|
1734
1743
|
|
|
1735
1744
|
return true;
|
|
1736
1745
|
} catch (error) {
|
|
1737
|
-
connection.
|
|
1746
|
+
connection.stop({
|
|
1738
1747
|
kind: 'local_error',
|
|
1739
1748
|
detail: error instanceof Error ? error.message : String(error),
|
|
1740
1749
|
});
|
|
@@ -1743,14 +1752,14 @@ function tryEmit(connection: StubConnection, emit: () => void): boolean {
|
|
|
1743
1752
|
}
|
|
1744
1753
|
}
|
|
1745
1754
|
|
|
1746
|
-
function findSessionName(imp: StubImp, proc: StubProcess): string {
|
|
1755
|
+
function findSessionName(imp: StubImp, proc: StubProcess): string | undefined {
|
|
1747
1756
|
for (const [name, held] of imp.sessions) {
|
|
1748
1757
|
if (held === proc) {
|
|
1749
1758
|
return name;
|
|
1750
1759
|
}
|
|
1751
1760
|
}
|
|
1752
1761
|
|
|
1753
|
-
return
|
|
1762
|
+
return undefined;
|
|
1754
1763
|
}
|
|
1755
1764
|
|
|
1756
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 `
|
|
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
|
|
25
|
+
readonly writeUnitCgroup: (pid: number, unit: string) => void;
|
|
26
26
|
|
|
27
|
-
readonly
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 = [
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
*
|
|
11
|
-
* hook, last registered first,
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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;
|
|
@@ -20,7 +20,7 @@ function main() {
|
|
|
20
20
|
const stateDir = process.argv.at(3);
|
|
21
21
|
|
|
22
22
|
if (socketPath === undefined || stateDir === undefined) {
|
|
23
|
-
throw new Error('usage: run-legacy-daemon.ts <socket path> <state dir>');
|
|
23
|
+
throw new Error('usage: run-stub-legacy-daemon.ts <socket path> <state dir>');
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
startStubLegacyDaemon(socketPath, { protocol: PROTOCOL_V + 1, owner: 'caller' });
|
|
@@ -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.
|
|
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
|
-
*
|
|
23
|
-
* test;
|
|
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
|
|
33
|
+
const teardown = registerTestCleanup(() => stack.disposeAsync());
|
|
33
34
|
|
|
34
|
-
stack.
|
|
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
|
-
|
|
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 `
|
|
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;
|
|
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
|
|
40
|
+
const teardown = registerTestCleanup(() => stack.disposeAsync());
|
|
41
41
|
|
|
42
|
-
stack.
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
20
|
+
const teardown = registerTestCleanup(() => {
|
|
22
21
|
rmSync(dir, { recursive: true, force: true });
|
|
23
22
|
});
|
|
24
23
|
|
|
25
24
|
return {
|
|
26
25
|
dir,
|
|
27
|
-
|
|
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`
|
|
53
|
-
*
|
|
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
|
|