@agentteams/runner 0.0.102 → 0.0.105

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 (37) hide show
  1. package/dist/autostart.d.ts +21 -6
  2. package/dist/autostart.js +262 -43
  3. package/dist/autostart.js.map +1 -1
  4. package/dist/autostart.test.js +155 -22
  5. package/dist/autostart.test.js.map +1 -1
  6. package/dist/commands/restart.d.ts +9 -1
  7. package/dist/commands/restart.js +16 -4
  8. package/dist/commands/restart.js.map +1 -1
  9. package/dist/commands/restart.test.d.ts +1 -0
  10. package/dist/commands/restart.test.js +31 -0
  11. package/dist/commands/restart.test.js.map +1 -0
  12. package/dist/commands/start.js +5 -0
  13. package/dist/commands/start.js.map +1 -1
  14. package/dist/daemon-control.d.ts +32 -6
  15. package/dist/daemon-control.js +198 -13
  16. package/dist/daemon-control.js.map +1 -1
  17. package/dist/daemon-control.test.js +260 -26
  18. package/dist/daemon-control.test.js.map +1 -1
  19. package/dist/executable.js +10 -0
  20. package/dist/executable.js.map +1 -1
  21. package/dist/executable.test.js +31 -9
  22. package/dist/executable.test.js.map +1 -1
  23. package/dist/poller.js +12 -11
  24. package/dist/poller.js.map +1 -1
  25. package/dist/poller.test.js +51 -7
  26. package/dist/poller.test.js.map +1 -1
  27. package/dist/restart-handoff.d.ts +71 -0
  28. package/dist/restart-handoff.js +169 -0
  29. package/dist/restart-handoff.js.map +1 -0
  30. package/dist/restart-handoff.test.d.ts +1 -0
  31. package/dist/restart-handoff.test.js +228 -0
  32. package/dist/restart-handoff.test.js.map +1 -0
  33. package/dist/runners/cursor-cli.test.js +3 -1
  34. package/dist/runners/cursor-cli.test.js.map +1 -1
  35. package/dist/utils/resolve-member-repo.test.js +1 -1
  36. package/dist/utils/resolve-member-repo.test.js.map +1 -1
  37. package/package.json +2 -2
@@ -1,11 +1,32 @@
1
1
  import { setTimeout as delay } from 'node:timers/promises';
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { platform as getPlatform } from 'node:os';
3
- import { getAutostartStatus, launchWindowsHiddenDaemon, restartAutostartService, scheduleWindowsTaskRestart, } from './autostart.js';
4
+ import { getAutostartStatus, launchWindowsHiddenDaemon, registerWindowsTask, restartAutostartService, scheduleWindowsTaskRestart, } from './autostart.js';
4
5
  import { logger } from './logger.js';
5
6
  import { getDaemonStatus } from './pid.js';
6
7
  import { spawnExecutable } from './executable.js';
8
+ import { acknowledgePreparedRestartHandoff, buildRestartHandoffEnv, getRestartHandoffPath, waitForPreparedRestartHandoff, } from './restart-handoff.js';
9
+ import { promises as fs } from 'node:fs';
7
10
  const restartPollIntervalMs = 100;
8
11
  const stopTimeoutMs = 10_000;
12
+ const startTimeoutMs = 15_000;
13
+ // A restart only *triggers* the replacement runner (a detached spawn, a
14
+ // Task Scheduler `/Run`, or a supervised respawn); the new process needs a
15
+ // moment to boot and write its PID file. Poll until it reports running so the
16
+ // command can report accurately instead of racing an async startup.
17
+ export const waitForDaemonToStart = async (deps = {}) => {
18
+ const resolvedGetDaemonStatus = deps.getDaemonStatus ?? getDaemonStatus;
19
+ const resolvedSleep = deps.sleep ?? ((milliseconds) => delay(milliseconds));
20
+ const resolvedNow = deps.now ?? (() => Date.now());
21
+ const timeoutMs = deps.timeoutMs ?? startTimeoutMs;
22
+ const deadline = resolvedNow() + timeoutMs;
23
+ let status = await resolvedGetDaemonStatus();
24
+ while (!status.running && resolvedNow() < deadline) {
25
+ await resolvedSleep(restartPollIntervalMs);
26
+ status = await resolvedGetDaemonStatus();
27
+ }
28
+ return status;
29
+ };
9
30
  const waitForDaemonToStop = async (pid, deps) => {
10
31
  deps.kill(pid, 'SIGTERM');
11
32
  const deadline = Date.now() + stopTimeoutMs;
@@ -18,55 +39,219 @@ const waitForDaemonToStop = async (pid, deps) => {
18
39
  }
19
40
  throw new Error(`Timed out waiting for AgentRunner process ${pid} to stop.`);
20
41
  };
21
- export const spawnDetachedDaemon = () => {
42
+ export const spawnDetachedDaemon = (launch) => {
43
+ const env = launch ? buildRestartHandoffEnv(launch) : process.env;
22
44
  // On Windows, use the dedicated hidden PowerShell launcher so the `.cmd`
23
45
  // shim never creates a visible console host.
24
46
  if (getPlatform() === 'win32') {
25
- launchWindowsHiddenDaemon();
47
+ launchWindowsHiddenDaemon({ env });
26
48
  return;
27
49
  }
28
50
  const child = spawnExecutable('agentrunner', ['start'], {
29
51
  detached: true,
30
52
  stdio: 'ignore',
31
- env: process.env,
53
+ env,
32
54
  cwd: process.cwd(),
33
55
  });
34
56
  child.unref();
35
57
  return child;
36
58
  };
59
+ export const prepareDetachedRestartHandoff = async (deps = {}) => {
60
+ const handoffId = deps.handoffId ?? randomUUID();
61
+ const parentPid = deps.parentPid ?? process.pid;
62
+ const markerPath = deps.markerPath ?? getRestartHandoffPath(handoffId);
63
+ const launch = { handoffId, parentPid, markerPath };
64
+ try {
65
+ await (deps.unlink ?? fs.unlink)(markerPath);
66
+ }
67
+ catch {
68
+ // Missing or stale marker is fine.
69
+ }
70
+ let child;
71
+ try {
72
+ child = (deps.spawnDetachedDaemon ?? spawnDetachedDaemon)(launch);
73
+ }
74
+ catch (error) {
75
+ return {
76
+ status: 'retryable-failure',
77
+ handoffId,
78
+ replacementReady: false,
79
+ acknowledged: false,
80
+ retryableFailure: true,
81
+ reason: 'replacement-preparation-failed',
82
+ error: error instanceof Error ? error.message : String(error),
83
+ };
84
+ }
85
+ if (!child?.pid) {
86
+ return {
87
+ status: 'retryable-failure',
88
+ handoffId,
89
+ replacementReady: false,
90
+ acknowledged: false,
91
+ retryableFailure: true,
92
+ reason: 'replacement-preparation-failed',
93
+ error: 'Replacement runner process did not expose a PID.',
94
+ };
95
+ }
96
+ return (deps.waitForPreparedRestartHandoff ?? waitForPreparedRestartHandoff)(launch);
97
+ };
37
98
  // Run from within the daemon itself when a web restart request is received.
38
99
  // We can't call restartDaemon() here because that would SIGTERM our own PID
39
100
  // before we get a chance to spawn the replacement; instead we either exit and
40
101
  // let the OS supervisor restart us, or spawn a replacement and exit cleanly.
41
- export const executeRestartRequest = (deps = {}) => {
102
+ export const executeRestartRequest = async (deps = {}) => {
42
103
  const resolvedGetAutostartStatus = deps.getAutostartStatus ?? getAutostartStatus;
43
104
  const resolvedScheduleWindowsTaskRestart = deps.scheduleWindowsTaskRestart ?? scheduleWindowsTaskRestart;
105
+ const resolvedRegisterWindowsTask = deps.registerWindowsTask ?? registerWindowsTask;
44
106
  const resolvedSpawnDetachedDaemon = deps.spawnDetachedDaemon ?? spawnDetachedDaemon;
107
+ const acknowledgeRestart = deps.acknowledgeRestart ?? (async () => undefined);
108
+ const acknowledgePreparedHandoff = deps.acknowledgePreparedHandoff ?? acknowledgePreparedRestartHandoff;
45
109
  const resolvedPlatform = (deps.platform ?? getPlatform)();
46
110
  const exitProcess = deps.processExit ?? ((code) => process.exit(code));
47
111
  const resolvedLogger = deps.logger ?? logger;
48
- const autostartStatus = resolvedGetAutostartStatus();
49
- if (autostartStatus.registered && autostartStatus.platform === 'task-scheduler') {
50
- resolvedLogger.info('Restart requested — scheduling explicit Task Scheduler restart');
51
- resolvedScheduleWindowsTaskRestart();
112
+ let autostartStatus = resolvedGetAutostartStatus();
113
+ if (resolvedPlatform === 'win32' || autostartStatus.platform === 'task-scheduler') {
114
+ if (!autostartStatus.registered) {
115
+ if (!deps.config) {
116
+ return {
117
+ status: 'retryable-failure',
118
+ handoffId: randomUUID(),
119
+ replacementReady: false,
120
+ acknowledged: false,
121
+ retryableFailure: true,
122
+ reason: 'autostart-repair-failed',
123
+ error: 'Windows Task Scheduler autostart is missing and runtime configuration is unavailable.',
124
+ };
125
+ }
126
+ try {
127
+ resolvedLogger.info('Windows Task Scheduler autostart is missing — repairing it before restart');
128
+ await resolvedRegisterWindowsTask({ token: deps.config.daemonToken, apiUrl: deps.config.apiUrl }, { startImmediately: false });
129
+ autostartStatus = { registered: true, platform: 'task-scheduler' };
130
+ }
131
+ catch (error) {
132
+ return {
133
+ status: 'retryable-failure',
134
+ handoffId: randomUUID(),
135
+ replacementReady: false,
136
+ acknowledged: false,
137
+ retryableFailure: true,
138
+ reason: 'autostart-repair-failed',
139
+ error: error instanceof Error ? error.message : String(error),
140
+ };
141
+ }
142
+ }
143
+ resolvedLogger.info('Restart requested — preparing an out-of-job Task Scheduler handoff');
144
+ const preparation = await resolvedScheduleWindowsTaskRestart();
145
+ if (preparation.status === 'retryable-failure') {
146
+ resolvedLogger.info('Restart handoff preparation failed — keeping the current runner alive for retry');
147
+ return preparation;
148
+ }
149
+ try {
150
+ await acknowledgeRestart();
151
+ }
152
+ catch (error) {
153
+ return {
154
+ status: 'retryable-failure',
155
+ handoffId: preparation.handoffId,
156
+ replacementReady: false,
157
+ acknowledged: false,
158
+ retryableFailure: true,
159
+ reason: 'acknowledgement-failed',
160
+ error: error instanceof Error ? error.message : String(error),
161
+ };
162
+ }
163
+ if (!(await acknowledgePreparedHandoff(preparation))) {
164
+ return {
165
+ status: 'retryable-failure',
166
+ handoffId: preparation.handoffId,
167
+ replacementReady: false,
168
+ acknowledged: false,
169
+ retryableFailure: true,
170
+ reason: 'replacement-confirmation-failed',
171
+ error: 'The prepared Windows restart helper was no longer available after acknowledgement.',
172
+ };
173
+ }
52
174
  exitProcess(0);
53
- return;
175
+ return {
176
+ status: 'acknowledged',
177
+ handoffId: preparation.handoffId,
178
+ replacementReady: true,
179
+ acknowledged: true,
180
+ retryableFailure: false,
181
+ };
54
182
  }
55
183
  const supervisedRespawn = autostartStatus.registered && (autostartStatus.platform === 'launchd' || autostartStatus.platform === 'systemd');
56
184
  if (supervisedRespawn) {
185
+ const handoffId = randomUUID();
186
+ try {
187
+ await acknowledgeRestart();
188
+ }
189
+ catch (error) {
190
+ return {
191
+ status: 'retryable-failure',
192
+ handoffId,
193
+ replacementReady: false,
194
+ acknowledged: false,
195
+ retryableFailure: true,
196
+ reason: 'acknowledgement-failed',
197
+ error: error instanceof Error ? error.message : String(error),
198
+ };
199
+ }
57
200
  resolvedLogger.info('Restart requested — exiting non-zero so the OS supervisor restarts the daemon', {
58
201
  platform: autostartStatus.platform,
59
202
  });
60
203
  exitProcess(1);
61
- return;
204
+ return {
205
+ status: 'acknowledged',
206
+ handoffId,
207
+ replacementReady: true,
208
+ acknowledged: true,
209
+ retryableFailure: false,
210
+ };
62
211
  }
63
- resolvedLogger.info('Restart requested — spawning new daemon and exiting', {
212
+ resolvedLogger.info('Restart requested — preparing a detached replacement runner', {
64
213
  platform: autostartStatus.platform,
65
214
  registered: autostartStatus.registered,
66
215
  osPlatform: resolvedPlatform,
67
216
  });
68
- resolvedSpawnDetachedDaemon();
217
+ const preparation = await (deps.prepareDetachedDaemon ??
218
+ (() => prepareDetachedRestartHandoff({ spawnDetachedDaemon: resolvedSpawnDetachedDaemon })))();
219
+ if (preparation.status === 'retryable-failure') {
220
+ return preparation;
221
+ }
222
+ try {
223
+ await acknowledgeRestart();
224
+ }
225
+ catch (error) {
226
+ return {
227
+ status: 'retryable-failure',
228
+ handoffId: preparation.handoffId,
229
+ replacementReady: false,
230
+ acknowledged: false,
231
+ retryableFailure: true,
232
+ reason: 'acknowledgement-failed',
233
+ error: error instanceof Error ? error.message : String(error),
234
+ };
235
+ }
236
+ if (!(await acknowledgePreparedHandoff(preparation))) {
237
+ return {
238
+ status: 'retryable-failure',
239
+ handoffId: preparation.handoffId,
240
+ replacementReady: false,
241
+ acknowledged: false,
242
+ retryableFailure: true,
243
+ reason: 'replacement-confirmation-failed',
244
+ error: 'The prepared replacement runner was no longer available after acknowledgement.',
245
+ };
246
+ }
69
247
  exitProcess(0);
248
+ return {
249
+ status: 'acknowledged',
250
+ handoffId: preparation.handoffId,
251
+ replacementReady: true,
252
+ acknowledged: true,
253
+ retryableFailure: false,
254
+ };
70
255
  };
71
256
  export const restartDaemon = async (deps = {}) => {
72
257
  const resolvedGetDaemonStatus = deps.getDaemonStatus ?? getDaemonStatus;
@@ -1 +1 @@
1
- {"version":3,"file":"daemon-control.js","sourceRoot":"","sources":["../src/daemon-control.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"daemon-control.js","sourceRoot":"","sources":["../src/daemon-control.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
- import { executeRestartRequest, restartDaemon } from './daemon-control.js';
3
+ import { executeRestartRequest, prepareDetachedRestartHandoff, restartDaemon, waitForDaemonToStart, } from './daemon-control.js';
4
4
  test('restartDaemon stops running daemon and restarts via autostart when registered', async () => {
5
5
  const signals = [];
6
6
  let statusChecks = 0;
@@ -64,81 +64,315 @@ test('restartDaemon delegates Windows task restart without signaling the daemon
64
64
  });
65
65
  assert.deepEqual(events, ['task-restart']);
66
66
  });
67
- test('executeRestartRequest exits non-zero when supervised by launchd', () => {
67
+ test('waitForDaemonToStart returns once the runner reports running', async () => {
68
+ let checks = 0;
69
+ const sleeps = [];
70
+ const status = await waitForDaemonToStart({
71
+ getDaemonStatus: async () => {
72
+ checks += 1;
73
+ // Runner is still booting for the first two polls, then writes its PID.
74
+ return checks < 3 ? { running: false, pid: null } : { running: true, pid: 999 };
75
+ },
76
+ sleep: async (milliseconds) => {
77
+ sleeps.push(milliseconds);
78
+ },
79
+ now: () => 0,
80
+ });
81
+ assert.deepEqual(status, { running: true, pid: 999 });
82
+ assert.equal(checks, 3);
83
+ assert.equal(sleeps.length, 2);
84
+ });
85
+ test('waitForDaemonToStart gives up after the deadline and reports not running', async () => {
86
+ let checks = 0;
87
+ const now = [0, 5, 10, 20];
88
+ let tick = 0;
89
+ const status = await waitForDaemonToStart({
90
+ getDaemonStatus: async () => {
91
+ checks += 1;
92
+ return { running: false, pid: null };
93
+ },
94
+ sleep: async () => undefined,
95
+ now: () => now[Math.min(tick++, now.length - 1)] ?? 0,
96
+ timeoutMs: 15,
97
+ });
98
+ assert.equal(status.running, false);
99
+ // Stops polling once now() passes the deadline instead of looping forever.
100
+ assert.ok(checks >= 1);
101
+ });
102
+ test('executeRestartRequest exits non-zero when supervised by launchd', async () => {
68
103
  const exitCodes = [];
69
104
  let spawned = false;
70
- executeRestartRequest({
105
+ await executeRestartRequest({
71
106
  getAutostartStatus: () => ({ registered: true, platform: 'launchd' }),
72
107
  spawnDetachedDaemon: () => {
73
108
  spawned = true;
74
109
  },
75
110
  platform: () => 'darwin',
76
- processExit: ((code) => {
111
+ processExit: (code) => {
77
112
  exitCodes.push(code);
78
- return undefined;
79
- }),
113
+ },
80
114
  logger: { info: () => undefined },
81
115
  });
82
116
  assert.deepEqual(exitCodes, [1]);
83
117
  assert.equal(spawned, false);
84
118
  });
85
- test('executeRestartRequest exits non-zero when supervised by systemd', () => {
119
+ test('executeRestartRequest exits non-zero when supervised by systemd', async () => {
86
120
  const exitCodes = [];
87
121
  let spawned = false;
88
- executeRestartRequest({
122
+ await executeRestartRequest({
89
123
  getAutostartStatus: () => ({ registered: true, platform: 'systemd' }),
90
124
  spawnDetachedDaemon: () => {
91
125
  spawned = true;
92
126
  },
93
127
  platform: () => 'linux',
94
- processExit: ((code) => {
128
+ processExit: (code) => {
95
129
  exitCodes.push(code);
96
- return undefined;
97
- }),
130
+ },
98
131
  logger: { info: () => undefined },
99
132
  });
100
133
  assert.deepEqual(exitCodes, [1]);
101
134
  assert.equal(spawned, false);
102
135
  });
103
- test('executeRestartRequest schedules an explicit restart and exits cleanly for Windows Task Scheduler', () => {
136
+ test('executeRestartRequest schedules an explicit restart and exits cleanly for Windows Task Scheduler', async () => {
104
137
  const exitCodes = [];
105
138
  let spawned = false;
106
139
  let scheduled = false;
107
- executeRestartRequest({
140
+ await executeRestartRequest({
108
141
  getAutostartStatus: () => ({ registered: true, platform: 'task-scheduler' }),
109
- scheduleWindowsTaskRestart: () => {
142
+ scheduleWindowsTaskRestart: async () => {
110
143
  scheduled = true;
144
+ return {
145
+ status: 'prepared',
146
+ handoffId: 'windows-success',
147
+ markerPath: '/tmp/restart-handoff-windows-success.json',
148
+ replacementPid: 9002,
149
+ replacementReady: true,
150
+ acknowledged: false,
151
+ retryableFailure: false,
152
+ };
111
153
  },
112
154
  spawnDetachedDaemon: () => {
113
155
  spawned = true;
114
156
  },
115
157
  platform: () => 'win32',
116
- processExit: ((code) => {
158
+ acknowledgePreparedHandoff: async () => true,
159
+ processExit: (code) => {
117
160
  exitCodes.push(code);
118
- return undefined;
119
- }),
161
+ },
120
162
  logger: { info: () => undefined },
121
163
  });
122
164
  assert.equal(spawned, false);
123
165
  assert.equal(scheduled, true);
124
166
  assert.deepEqual(exitCodes, [0]);
125
167
  });
126
- test('executeRestartRequest spawns a detached daemon when running manually on macOS', () => {
168
+ test('executeRestartRequest keeps the daemon alive when the Windows restart helper fails to schedule', async () => {
127
169
  const exitCodes = [];
128
- let spawned = false;
129
- executeRestartRequest({
170
+ await executeRestartRequest({
171
+ getAutostartStatus: () => ({ registered: true, platform: 'task-scheduler' }),
172
+ // Helper creation failed — must NOT exit, or the runner dies with no replacement.
173
+ scheduleWindowsTaskRestart: async () => ({
174
+ status: 'retryable-failure',
175
+ handoffId: 'windows-failure',
176
+ replacementReady: false,
177
+ acknowledged: false,
178
+ retryableFailure: true,
179
+ reason: 'helper-preparation-failed',
180
+ error: 'helper creation failed',
181
+ }),
182
+ platform: () => 'win32',
183
+ processExit: (code) => {
184
+ exitCodes.push(code);
185
+ },
186
+ logger: { info: () => undefined },
187
+ });
188
+ assert.deepEqual(exitCodes, [], 'daemon must not exit when the restart helper could not be created');
189
+ });
190
+ test('executeRestartRequest acks and exits only after a manual replacement is prepared', async () => {
191
+ const events = [];
192
+ await executeRestartRequest({
130
193
  getAutostartStatus: () => ({ registered: false, platform: 'launchd' }),
131
- spawnDetachedDaemon: () => {
132
- spawned = true;
194
+ prepareDetachedDaemon: async () => {
195
+ events.push('prepared');
196
+ return {
197
+ status: 'prepared',
198
+ handoffId: 'manual-success',
199
+ markerPath: '/tmp/restart-handoff-manual-success.json',
200
+ replacementPid: 9002,
201
+ replacementReady: true,
202
+ acknowledged: false,
203
+ retryableFailure: false,
204
+ };
205
+ },
206
+ acknowledgeRestart: async () => {
207
+ events.push('ack');
208
+ },
209
+ acknowledgePreparedHandoff: async () => {
210
+ events.push('commit');
211
+ return true;
133
212
  },
134
213
  platform: () => 'darwin',
135
- processExit: ((code) => {
214
+ processExit: (code) => {
215
+ events.push(`exit:${code}`);
216
+ },
217
+ logger: { info: () => undefined },
218
+ });
219
+ assert.deepEqual(events, ['prepared', 'ack', 'commit', 'exit:0']);
220
+ });
221
+ test('executeRestartRequest keeps the daemon alive when the prepared standby disappears during acknowledgement', async () => {
222
+ const events = [];
223
+ const result = await executeRestartRequest({
224
+ getAutostartStatus: () => ({ registered: false, platform: 'manual' }),
225
+ prepareDetachedDaemon: async () => ({
226
+ status: 'prepared',
227
+ handoffId: 'manual-disappeared',
228
+ markerPath: '/tmp/restart-handoff-manual-disappeared.json',
229
+ replacementPid: 9002,
230
+ replacementReady: true,
231
+ acknowledged: false,
232
+ retryableFailure: false,
233
+ }),
234
+ acknowledgeRestart: async () => {
235
+ events.push('ack');
236
+ },
237
+ acknowledgePreparedHandoff: async () => {
238
+ events.push('commit-failed');
239
+ return false;
240
+ },
241
+ processExit: (code) => {
242
+ events.push(`exit:${code}`);
243
+ },
244
+ platform: () => 'darwin',
245
+ logger: { info: () => undefined },
246
+ });
247
+ assert.deepEqual(events, ['ack', 'commit-failed']);
248
+ assert.equal(result.status, 'retryable-failure');
249
+ });
250
+ test('prepareDetachedRestartHandoff reports spawn failure without waiting for readiness', async () => {
251
+ let waited = false;
252
+ const result = await prepareDetachedRestartHandoff({
253
+ handoffId: 'manual-spawn-failure',
254
+ parentPid: 4321,
255
+ markerPath: '/tmp/restart-handoff.json',
256
+ unlink: async () => undefined,
257
+ spawnDetachedDaemon: () => {
258
+ throw new Error('spawn failed');
259
+ },
260
+ waitForPreparedRestartHandoff: async () => {
261
+ waited = true;
262
+ throw new Error('should not wait');
263
+ },
264
+ });
265
+ assert.equal(result.status, 'retryable-failure');
266
+ assert.equal(result.reason, 'replacement-preparation-failed');
267
+ assert.equal(waited, false);
268
+ });
269
+ test('executeRestartRequest keeps supervised runners alive when acknowledgement fails', async () => {
270
+ const exitCodes = [];
271
+ const result = await executeRestartRequest({
272
+ getAutostartStatus: () => ({ registered: true, platform: 'systemd' }),
273
+ acknowledgeRestart: async () => {
274
+ throw new Error('network down');
275
+ },
276
+ platform: () => 'linux',
277
+ processExit: (code) => {
136
278
  exitCodes.push(code);
137
- return undefined;
279
+ },
280
+ logger: { info: () => undefined },
281
+ });
282
+ assert.equal(result.status, 'retryable-failure');
283
+ assert.equal(result.reason, 'acknowledgement-failed');
284
+ assert.deepEqual(exitCodes, []);
285
+ });
286
+ test('executeRestartRequest preserves the current runner and request when the Windows helper reports a post-create failure', async () => {
287
+ const events = [];
288
+ const result = await executeRestartRequest({
289
+ getAutostartStatus: () => ({ registered: true, platform: 'task-scheduler' }),
290
+ scheduleWindowsTaskRestart: async () => ({
291
+ status: 'retryable-failure',
292
+ handoffId: 'handoff-query-failure',
293
+ replacementReady: false,
294
+ acknowledged: false,
295
+ retryableFailure: true,
296
+ reason: 'helper-preparation-failed',
297
+ error: 'schtasks /Query failed with exit 1',
138
298
  }),
299
+ acknowledgeRestart: async () => {
300
+ events.push('ack');
301
+ },
302
+ spawnDetachedDaemon: () => {
303
+ events.push('spawn');
304
+ },
305
+ processExit: (code) => {
306
+ events.push(`exit:${code}`);
307
+ },
308
+ platform: () => 'win32',
139
309
  logger: { info: () => undefined },
140
310
  });
141
- assert.equal(spawned, true);
142
- assert.deepEqual(exitCodes, [0]);
311
+ assert.deepEqual(events, [], 'a post-create helper failure must not ack or terminate the current runner');
312
+ assert.equal(result.status, 'retryable-failure');
313
+ });
314
+ test('executeRestartRequest repairs a missing Windows scheduled task before preparing handoff', async () => {
315
+ const events = [];
316
+ const result = await executeRestartRequest({
317
+ getAutostartStatus: () => ({ registered: false, platform: 'task-scheduler' }),
318
+ registerWindowsTask: async () => {
319
+ events.push('register');
320
+ return { registered: true, servicePath: 'task.xml', platform: 'task-scheduler' };
321
+ },
322
+ scheduleWindowsTaskRestart: async () => {
323
+ events.push('prepare');
324
+ return {
325
+ status: 'retryable-failure',
326
+ handoffId: 'handoff-after-repair',
327
+ replacementReady: false,
328
+ acknowledged: false,
329
+ retryableFailure: true,
330
+ reason: 'helper-preparation-failed',
331
+ error: 'replacement did not become ready',
332
+ };
333
+ },
334
+ acknowledgeRestart: async () => {
335
+ events.push('ack');
336
+ },
337
+ spawnDetachedDaemon: () => {
338
+ events.push('spawn');
339
+ },
340
+ processExit: (code) => {
341
+ events.push(`exit:${code}`);
342
+ },
343
+ platform: () => 'win32',
344
+ config: { daemonToken: 'secret-token', apiUrl: 'https://api.example' },
345
+ logger: { info: () => undefined },
346
+ });
347
+ assert.deepEqual(events, ['register', 'prepare']);
348
+ assert.equal(result.status, 'retryable-failure');
349
+ });
350
+ test('executeRestartRequest does not ack or exit when a manual replacement never becomes ready', async () => {
351
+ const events = [];
352
+ const result = await executeRestartRequest({
353
+ getAutostartStatus: () => ({ registered: false, platform: 'manual' }),
354
+ prepareDetachedDaemon: async () => ({
355
+ status: 'retryable-failure',
356
+ handoffId: 'manual-timeout',
357
+ replacementReady: false,
358
+ acknowledged: false,
359
+ retryableFailure: true,
360
+ reason: 'replacement-preparation-failed',
361
+ error: 'replacement readiness timed out',
362
+ }),
363
+ acknowledgeRestart: async () => {
364
+ events.push('ack');
365
+ },
366
+ spawnDetachedDaemon: () => {
367
+ events.push('spawn');
368
+ },
369
+ processExit: (code) => {
370
+ events.push(`exit:${code}`);
371
+ },
372
+ platform: () => 'darwin',
373
+ logger: { info: () => undefined },
374
+ });
375
+ assert.deepEqual(events, [], 'spawn success alone must not ack or terminate the current runner');
376
+ assert.equal(result.status, 'retryable-failure');
143
377
  });
144
378
  //# sourceMappingURL=daemon-control.test.js.map