@agentteams/runner 0.0.93 → 0.0.94
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/dist/config.js +5 -1
- package/dist/config.js.map +1 -1
- package/dist/config.test.js +26 -0
- package/dist/config.test.js.map +1 -1
- package/dist/handlers/trigger-handler.test.js +26 -0
- package/dist/handlers/trigger-handler.test.js.map +1 -1
- package/dist/poller.d.ts +2 -2
- package/dist/poller.js +53 -13
- package/dist/poller.js.map +1 -1
- package/dist/poller.test.js +187 -53
- package/dist/poller.test.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/poller.js
CHANGED
|
@@ -18,8 +18,8 @@ export const startPolling = async (config, createHandler, dependencies = {}) =>
|
|
|
18
18
|
const autoUpdate = dependencies.maybeAutoUpdate ?? maybeAutoUpdate;
|
|
19
19
|
const performRestart = dependencies.executeRestartRequest ?? executeRestartRequest;
|
|
20
20
|
const now = dependencies.now ?? Date.now;
|
|
21
|
-
const
|
|
22
|
-
const
|
|
21
|
+
const registerTimeout = dependencies.setTimeout ?? global.setTimeout;
|
|
22
|
+
const unregisterTimeout = dependencies.clearTimeout ?? global.clearTimeout;
|
|
23
23
|
const registerSignal = dependencies.processOn ?? ((event, listener) => process.on(event, listener));
|
|
24
24
|
const exitProcess = dependencies.processExit ?? ((code) => process.exit(code));
|
|
25
25
|
const keepAlive = dependencies.keepAlive ??
|
|
@@ -149,9 +149,11 @@ export const startPolling = async (config, createHandler, dependencies = {}) =>
|
|
|
149
149
|
};
|
|
150
150
|
const pollOnce = async () => {
|
|
151
151
|
if (isPolling) {
|
|
152
|
-
return;
|
|
152
|
+
return 'SKIPPED';
|
|
153
153
|
}
|
|
154
154
|
isPolling = true;
|
|
155
|
+
// 이번 cycle에 처리할 일이 있었는지. 순수 idle만 false로 남는다.
|
|
156
|
+
let hadWork = false;
|
|
155
157
|
try {
|
|
156
158
|
maybeRunCleanup();
|
|
157
159
|
maybeRunConventionSync();
|
|
@@ -159,6 +161,9 @@ export const startPolling = async (config, createHandler, dependencies = {}) =>
|
|
|
159
161
|
// snapshot 1회 조회로 가져온다. 실패 시 아래 catch에서 polling cycle 전체가 실패
|
|
160
162
|
// 처리되어 명확한 로그를 남긴다(조용히 pending 확인만 생략하지 않는다).
|
|
161
163
|
const pollState = await client.fetchPollState();
|
|
164
|
+
hadWork =
|
|
165
|
+
pollState.data.orphanedCancelRequestedTriggerIds.length > 0 ||
|
|
166
|
+
pollState.data.pendingWorktreeRemovals.length > 0;
|
|
162
167
|
// 후속 mutation은 기존과 동일하게 개별 처리하며, 처리 순서도 보존한다.
|
|
163
168
|
await autoCancelOrphanedTriggers(pollState.data.orphanedCancelRequestedTriggerIds);
|
|
164
169
|
await processWorktreeRemovals(pollState.data.pendingWorktreeRemovals);
|
|
@@ -174,10 +179,11 @@ export const startPolling = async (config, createHandler, dependencies = {}) =>
|
|
|
174
179
|
logger.warn('Failed to ack restart request — will retry on next poll', {
|
|
175
180
|
error: error instanceof Error ? error.message : String(error),
|
|
176
181
|
});
|
|
177
|
-
|
|
182
|
+
// ack 재시도를 base 간격으로 빠르게 잇기 위해 활동으로 취급한다.
|
|
183
|
+
return 'ACTIVE';
|
|
178
184
|
}
|
|
179
185
|
performRestart({ processExit: exitProcess });
|
|
180
|
-
return;
|
|
186
|
+
return 'ACTIVE';
|
|
181
187
|
}
|
|
182
188
|
if (!trigger) {
|
|
183
189
|
// idle 상태에서 자동 업데이트 시도
|
|
@@ -191,16 +197,16 @@ export const startPolling = async (config, createHandler, dependencies = {}) =>
|
|
|
191
197
|
error: error instanceof Error ? error.message : String(error),
|
|
192
198
|
});
|
|
193
199
|
}
|
|
194
|
-
return;
|
|
200
|
+
return hadWork ? 'ACTIVE' : 'IDLE';
|
|
195
201
|
}
|
|
196
202
|
const claim = await client.claimTrigger(trigger.id);
|
|
197
203
|
if (claim.conflict) {
|
|
198
204
|
logger.info('Trigger already claimed by another daemon', { triggerId: trigger.id });
|
|
199
|
-
return;
|
|
205
|
+
return 'ACTIVE';
|
|
200
206
|
}
|
|
201
207
|
if (!claim.ok) {
|
|
202
208
|
logger.warn('Claim was rejected', { triggerId: trigger.id });
|
|
203
|
-
return;
|
|
209
|
+
return 'ACTIVE';
|
|
204
210
|
}
|
|
205
211
|
void onTrigger(trigger).catch((error) => {
|
|
206
212
|
logger.error('Trigger handler execution failed', {
|
|
@@ -208,31 +214,65 @@ export const startPolling = async (config, createHandler, dependencies = {}) =>
|
|
|
208
214
|
error: error instanceof Error ? error.message : String(error),
|
|
209
215
|
});
|
|
210
216
|
});
|
|
217
|
+
return 'ACTIVE';
|
|
211
218
|
}
|
|
212
219
|
catch (error) {
|
|
213
220
|
logger.error('Polling cycle failed', {
|
|
214
221
|
error: error instanceof Error ? error.message : String(error),
|
|
215
222
|
});
|
|
223
|
+
return hadWork ? 'ACTIVE' : 'IDLE';
|
|
216
224
|
}
|
|
217
225
|
finally {
|
|
218
226
|
isPolling = false;
|
|
219
227
|
}
|
|
220
228
|
};
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
229
|
+
// idle 적응형 백오프: 연속 idle cycle마다 다음 폴 간격을 선형 증가시키고
|
|
230
|
+
// (base × (1 + idleStreak × 0.5), 상한 clamp), 활동이 감지되면 즉시 base로 리셋한다.
|
|
231
|
+
let stopped = false;
|
|
232
|
+
let idleStreak = 0;
|
|
233
|
+
let pendingPollTimer = null;
|
|
234
|
+
const computeNextPollDelay = (outcome) => {
|
|
235
|
+
if (outcome === 'ACTIVE') {
|
|
236
|
+
idleStreak = 0;
|
|
237
|
+
return config.pollingIntervalMs;
|
|
238
|
+
}
|
|
239
|
+
idleStreak += 1;
|
|
240
|
+
return Math.min(config.pollingIntervalMs * (1 + idleStreak * 0.5), config.maxPollingIntervalMs);
|
|
241
|
+
};
|
|
242
|
+
const runPollCycle = async () => {
|
|
243
|
+
if (stopped) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const outcome = await pollOnce();
|
|
247
|
+
if (outcome === 'SKIPPED') {
|
|
248
|
+
// 진행 중인 cycle이 완료 시점에 다음 폴을 예약하므로 여기서 이중 예약하지 않는다.
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (stopped) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
pendingPollTimer = registerTimeout(() => {
|
|
255
|
+
void runPollCycle();
|
|
256
|
+
}, computeNextPollDelay(outcome));
|
|
257
|
+
};
|
|
224
258
|
// daemon 시작과 함께 절전 방지를 한 번 acquire한다. polling cycle마다 반복하지 않는다.
|
|
225
259
|
const releasePowerSaveBlocker = powerSaveBlocker.acquire('daemon-polling');
|
|
226
260
|
logger.info('Daemon polling started', {
|
|
227
261
|
apiUrl: config.apiUrl,
|
|
228
262
|
pollingIntervalMs: config.pollingIntervalMs,
|
|
263
|
+
maxPollingIntervalMs: config.maxPollingIntervalMs,
|
|
229
264
|
timeoutMs: config.timeoutMs,
|
|
230
265
|
runnerCmd: config.runnerCmd,
|
|
231
266
|
});
|
|
232
|
-
|
|
267
|
+
// 부트스트랩: 즉시 1회 폴 후 자기예약 루프가 다음 폴을 이어간다.
|
|
268
|
+
await runPollCycle();
|
|
233
269
|
const shutdown = () => {
|
|
270
|
+
stopped = true;
|
|
271
|
+
if (pendingPollTimer !== null) {
|
|
272
|
+
unregisterTimeout(pendingPollTimer);
|
|
273
|
+
pendingPollTimer = null;
|
|
274
|
+
}
|
|
234
275
|
releasePowerSaveBlocker();
|
|
235
|
-
unregisterInterval(interval);
|
|
236
276
|
logger.info('Daemon stopped');
|
|
237
277
|
exitProcess(0);
|
|
238
278
|
};
|
package/dist/poller.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"poller.js","sourceRoot":"","sources":["../src/poller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAErC,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,sBAAsB,EAAyB,MAAM,+BAA+B,CAAC;AAM9F,MAAM,mBAAmB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAChD,MAAM,2BAA2B,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"poller.js","sourceRoot":"","sources":["../src/poller.ts"],"names":[],"mappings":"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"}
|
package/dist/poller.test.js
CHANGED
|
@@ -9,6 +9,7 @@ const config = {
|
|
|
9
9
|
daemonToken: 'daemon-token',
|
|
10
10
|
apiUrl: 'https://api.example',
|
|
11
11
|
pollingIntervalMs: 5000,
|
|
12
|
+
maxPollingIntervalMs: 12_000,
|
|
12
13
|
timeoutMs: 1000,
|
|
13
14
|
idleTimeoutMs: 500,
|
|
14
15
|
runnerCmd: 'opencode',
|
|
@@ -61,6 +62,21 @@ const makeClient = (overrides = {}) => ({
|
|
|
61
62
|
notifyUpdate: overrides.notifyUpdate ?? (async () => undefined),
|
|
62
63
|
ackRestartRequest: overrides.ackRestartRequest ?? (async () => undefined),
|
|
63
64
|
});
|
|
65
|
+
// setTimeout 주입 목: 예약된 폴 콜백과 delay를 캡처해 수동으로 구동한다.
|
|
66
|
+
// 자기예약 루프는 매 cycle 완료 후 새 timeout을 예약하므로, 다음 폴은 scheduled의
|
|
67
|
+
// 마지막 항목을 fire하면 된다.
|
|
68
|
+
const createTimeoutRecorder = () => {
|
|
69
|
+
const scheduled = [];
|
|
70
|
+
const cleared = [];
|
|
71
|
+
const setTimeoutMock = ((callback, delayMs) => {
|
|
72
|
+
scheduled.push({ callback, delayMs: delayMs ?? 0 });
|
|
73
|
+
return { ref() { }, unref() { } };
|
|
74
|
+
});
|
|
75
|
+
const clearTimeoutMock = ((handle) => {
|
|
76
|
+
cleared.push(handle);
|
|
77
|
+
});
|
|
78
|
+
return { scheduled, cleared, setTimeoutMock, clearTimeoutMock };
|
|
79
|
+
};
|
|
64
80
|
const createRecordingBlocker = () => {
|
|
65
81
|
const events = [];
|
|
66
82
|
return {
|
|
@@ -79,7 +95,7 @@ test('startPolling handles a claimed trigger, registers auth paths, and runs sch
|
|
|
79
95
|
infos.push({ message, meta });
|
|
80
96
|
});
|
|
81
97
|
const signals = new Map();
|
|
82
|
-
const
|
|
98
|
+
const timeouts = createTimeoutRecorder();
|
|
83
99
|
const cleanupCalls = [];
|
|
84
100
|
const handledTriggers = [];
|
|
85
101
|
const savedAuthPaths = [];
|
|
@@ -101,11 +117,8 @@ test('startPolling handles a claimed trigger, registers auth paths, and runs sch
|
|
|
101
117
|
cleanupCalls.push(authPath);
|
|
102
118
|
},
|
|
103
119
|
runConventionSync: async () => undefined,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return { ref() { }, unref() { } };
|
|
107
|
-
}),
|
|
108
|
-
clearInterval: (() => undefined),
|
|
120
|
+
setTimeout: timeouts.setTimeoutMock,
|
|
121
|
+
clearTimeout: timeouts.clearTimeoutMock,
|
|
109
122
|
processOn: ((event, listener) => {
|
|
110
123
|
signals.set(event, listener);
|
|
111
124
|
}),
|
|
@@ -125,9 +138,11 @@ test('startPolling handles a claimed trigger, registers auth paths, and runs sch
|
|
|
125
138
|
await new Promise((resolve) => setImmediate(resolve));
|
|
126
139
|
assert.deepEqual(handledTriggers, ['trigger-1']);
|
|
127
140
|
assert.deepEqual(cleanupCalls, []);
|
|
128
|
-
assert.equal(
|
|
141
|
+
assert.equal(timeouts.scheduled.length, 1);
|
|
142
|
+
// 부트스트랩 cycle에서 트리거를 처리했으므로 다음 폴은 base 간격으로 예약된다.
|
|
143
|
+
assert.equal(timeouts.scheduled[0]?.delayMs, config.pollingIntervalMs);
|
|
129
144
|
nowValue = 24 * 60 * 60 * 1000 + 1;
|
|
130
|
-
|
|
145
|
+
timeouts.scheduled[0]?.callback();
|
|
131
146
|
await new Promise((resolve) => setImmediate(resolve));
|
|
132
147
|
assert.deepEqual(cleanupCalls, ['/auth/path']);
|
|
133
148
|
assert.deepEqual(savedAuthPaths, ['/auth/path']);
|
|
@@ -186,8 +201,8 @@ test('startPolling processes orphaned cancel, worktree removal, and pending clai
|
|
|
186
201
|
// 워크트리 경로가 매칭되지 않도록 존재하지 않는 authPath를 사용 → 멱등 제거 완료 경로.
|
|
187
202
|
loadAuthPaths: () => ['/nonexistent/auth/path'],
|
|
188
203
|
saveAuthPath: () => '/tmp/auth-paths.json',
|
|
189
|
-
|
|
190
|
-
|
|
204
|
+
setTimeout: (() => ({ ref() { }, unref() { } })),
|
|
205
|
+
clearTimeout: (() => undefined),
|
|
191
206
|
processOn: (() => undefined),
|
|
192
207
|
processExit: (() => {
|
|
193
208
|
throw new Error('should not exit');
|
|
@@ -211,7 +226,7 @@ test('startPolling processes orphaned cancel, worktree removal, and pending clai
|
|
|
211
226
|
await pollingPromise;
|
|
212
227
|
});
|
|
213
228
|
test('startPolling runs convention sync per auth path every 6 hours', async () => {
|
|
214
|
-
const
|
|
229
|
+
const timeouts = createTimeoutRecorder();
|
|
215
230
|
const conventionSyncCalls = [];
|
|
216
231
|
let nowValue = 6 * 60 * 60 * 1000 + 1;
|
|
217
232
|
let keepAliveResolve = null;
|
|
@@ -222,11 +237,8 @@ test('startPolling runs convention sync per auth path every 6 hours', async () =
|
|
|
222
237
|
conventionSyncCalls.push(authPath);
|
|
223
238
|
},
|
|
224
239
|
maybeAutoUpdate: async () => ({ cliUpdated: false, runnerUpdated: false }),
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return { ref() { }, unref() { } };
|
|
228
|
-
}),
|
|
229
|
-
clearInterval: (() => undefined),
|
|
240
|
+
setTimeout: timeouts.setTimeoutMock,
|
|
241
|
+
clearTimeout: timeouts.clearTimeoutMock,
|
|
230
242
|
processOn: (() => undefined),
|
|
231
243
|
processExit: (() => {
|
|
232
244
|
throw new Error('should not exit');
|
|
@@ -240,13 +252,13 @@ test('startPolling runs convention sync per auth path every 6 hours', async () =
|
|
|
240
252
|
});
|
|
241
253
|
await new Promise((resolve) => setImmediate(resolve));
|
|
242
254
|
assert.deepEqual(conventionSyncCalls, ['/auth/path']);
|
|
243
|
-
assert.equal(
|
|
255
|
+
assert.equal(timeouts.scheduled.length, 1);
|
|
244
256
|
nowValue += 6 * 60 * 60 * 1000 - 1;
|
|
245
|
-
|
|
257
|
+
timeouts.scheduled.at(-1)?.callback();
|
|
246
258
|
await new Promise((resolve) => setImmediate(resolve));
|
|
247
259
|
assert.deepEqual(conventionSyncCalls, ['/auth/path']);
|
|
248
260
|
nowValue += 1;
|
|
249
|
-
|
|
261
|
+
timeouts.scheduled.at(-1)?.callback();
|
|
250
262
|
await new Promise((resolve) => setImmediate(resolve));
|
|
251
263
|
assert.deepEqual(conventionSyncCalls, ['/auth/path', '/auth/path']);
|
|
252
264
|
const resolveKeepAlive = keepAliveResolve ??
|
|
@@ -256,30 +268,36 @@ test('startPolling runs convention sync per auth path every 6 hours', async () =
|
|
|
256
268
|
resolveKeepAlive();
|
|
257
269
|
await pollingPromise;
|
|
258
270
|
});
|
|
259
|
-
test('startPolling logs conflicts and suppresses overlapping polling cycles', async () => {
|
|
271
|
+
test('startPolling logs conflicts and suppresses overlapping polling cycles without double-scheduling', async () => {
|
|
260
272
|
const infos = [];
|
|
261
273
|
mock.method(logger, 'info', (message, meta) => {
|
|
262
274
|
infos.push({ message, meta });
|
|
263
275
|
});
|
|
276
|
+
// 부트스트랩 cycle은 즉시 idle로 완료시키고, 두 번째 cycle의 fetch를 행 걸어
|
|
277
|
+
// 그 사이 재진입(pollOnce 중복 호출)이 억제되는지 확인한다.
|
|
278
|
+
let fetchCalls = 0;
|
|
264
279
|
let releaseFetch = null;
|
|
265
280
|
const client = makeClient({
|
|
266
|
-
fetchPollState: async () =>
|
|
267
|
-
|
|
268
|
-
|
|
281
|
+
fetchPollState: async () => {
|
|
282
|
+
fetchCalls += 1;
|
|
283
|
+
if (fetchCalls === 1) {
|
|
284
|
+
return pollState();
|
|
285
|
+
}
|
|
286
|
+
return await new Promise((resolve) => {
|
|
287
|
+
releaseFetch = () => resolve(pollState({ pendingTrigger: trigger }));
|
|
288
|
+
});
|
|
289
|
+
},
|
|
269
290
|
claimTrigger: async () => ({ ok: false, conflict: true }),
|
|
270
291
|
});
|
|
271
|
-
const
|
|
292
|
+
const timeouts = createTimeoutRecorder();
|
|
272
293
|
let keepAliveResolve = null;
|
|
273
294
|
const pollingPromise = startPolling(config, () => async () => {
|
|
274
295
|
throw new Error('handler should not run');
|
|
275
296
|
}, {
|
|
276
297
|
createClient: () => client,
|
|
277
298
|
runCleanup: async () => undefined,
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return {};
|
|
281
|
-
}),
|
|
282
|
-
clearInterval: (() => undefined),
|
|
299
|
+
setTimeout: timeouts.setTimeoutMock,
|
|
300
|
+
clearTimeout: timeouts.clearTimeoutMock,
|
|
283
301
|
processOn: (() => undefined),
|
|
284
302
|
processExit: (() => {
|
|
285
303
|
throw new Error('should not exit');
|
|
@@ -292,7 +310,13 @@ test('startPolling logs conflicts and suppresses overlapping polling cycles', as
|
|
|
292
310
|
}),
|
|
293
311
|
});
|
|
294
312
|
await new Promise((resolve) => setImmediate(resolve));
|
|
295
|
-
|
|
313
|
+
assert.equal(timeouts.scheduled.length, 1, 'bootstrap cycle should schedule the next poll');
|
|
314
|
+
// 두 번째 cycle 시작(fetch 행) 후 같은 콜백을 한 번 더 fire → 재진입 억제.
|
|
315
|
+
timeouts.scheduled[0]?.callback();
|
|
316
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
317
|
+
timeouts.scheduled[0]?.callback();
|
|
318
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
319
|
+
assert.equal(timeouts.scheduled.length, 1, 'suppressed overlapping cycle must not schedule another poll');
|
|
296
320
|
const releasePendingFetch = releaseFetch ??
|
|
297
321
|
(() => {
|
|
298
322
|
throw new Error('fetch release was not registered');
|
|
@@ -300,6 +324,9 @@ test('startPolling logs conflicts and suppresses overlapping polling cycles', as
|
|
|
300
324
|
releasePendingFetch();
|
|
301
325
|
await new Promise((resolve) => setImmediate(resolve));
|
|
302
326
|
assert.equal(infos.some((entry) => entry.message === 'Trigger already claimed by another daemon'), true);
|
|
327
|
+
// 행 걸렸던 cycle이 완료되면서 다음 폴 1개만 예약되고, pending 트리거를 봤으므로 base 간격이다.
|
|
328
|
+
assert.equal(timeouts.scheduled.length, 2, 'the in-flight cycle owns scheduling the next poll');
|
|
329
|
+
assert.equal(timeouts.scheduled[1]?.delayMs, config.pollingIntervalMs);
|
|
303
330
|
const resolveKeepAlive = keepAliveResolve ??
|
|
304
331
|
(() => {
|
|
305
332
|
throw new Error('keepAlive resolver was not registered');
|
|
@@ -307,17 +334,17 @@ test('startPolling logs conflicts and suppresses overlapping polling cycles', as
|
|
|
307
334
|
resolveKeepAlive();
|
|
308
335
|
await pollingPromise;
|
|
309
336
|
});
|
|
310
|
-
test('startPolling clears the
|
|
337
|
+
test('startPolling clears the pending poll timer and exits on shutdown signals', async () => {
|
|
311
338
|
const signals = new Map();
|
|
312
339
|
const cleared = [];
|
|
313
340
|
let keepAliveResolve = null;
|
|
314
341
|
let exitCode = null;
|
|
315
|
-
const
|
|
342
|
+
const timeoutHandle = {};
|
|
316
343
|
const pollingPromise = startPolling(config, () => async () => undefined, {
|
|
317
344
|
createClient: () => makeClient(),
|
|
318
345
|
runCleanup: async () => undefined,
|
|
319
|
-
|
|
320
|
-
|
|
346
|
+
setTimeout: (() => timeoutHandle),
|
|
347
|
+
clearTimeout: ((handle) => {
|
|
321
348
|
cleared.push(handle);
|
|
322
349
|
}),
|
|
323
350
|
processOn: ((event, listener) => {
|
|
@@ -344,7 +371,7 @@ test('startPolling clears the interval and exits on shutdown signals', async ()
|
|
|
344
371
|
assert.ok(sigterm);
|
|
345
372
|
sigterm();
|
|
346
373
|
assert.equal(exitCode, 0);
|
|
347
|
-
assert.deepEqual(cleared, [
|
|
374
|
+
assert.deepEqual(cleared, [timeoutHandle]);
|
|
348
375
|
const resolveKeepAlive = keepAliveResolve ??
|
|
349
376
|
(() => {
|
|
350
377
|
throw new Error('keepAlive resolver was not registered');
|
|
@@ -364,8 +391,8 @@ test('startPolling delegates auto-update via injected dependency', async () => {
|
|
|
364
391
|
autoUpdateCallCount++;
|
|
365
392
|
return { cliUpdated: false, runnerUpdated: false };
|
|
366
393
|
},
|
|
367
|
-
|
|
368
|
-
|
|
394
|
+
setTimeout: (() => ({})),
|
|
395
|
+
clearTimeout: (() => undefined),
|
|
369
396
|
processOn: (() => undefined),
|
|
370
397
|
processExit: (() => undefined),
|
|
371
398
|
now: () => 0,
|
|
@@ -411,8 +438,8 @@ test('startPolling restores persisted auth paths for worktree removals after res
|
|
|
411
438
|
removeWorktree: (authPath, worktreePath, worktreeId) => {
|
|
412
439
|
removedWorktrees.push({ authPath, worktreePath, worktreeId });
|
|
413
440
|
},
|
|
414
|
-
|
|
415
|
-
|
|
441
|
+
setTimeout: (() => ({ ref() { }, unref() { } })),
|
|
442
|
+
clearTimeout: (() => undefined),
|
|
416
443
|
processOn: (() => undefined),
|
|
417
444
|
processExit: (() => {
|
|
418
445
|
throw new Error('should not exit');
|
|
@@ -462,8 +489,8 @@ test('startPolling reports REMOVED when no persisted auth path matches the workt
|
|
|
462
489
|
removeWorktree: () => {
|
|
463
490
|
throw new Error('removeWorktree should not be called');
|
|
464
491
|
},
|
|
465
|
-
|
|
466
|
-
|
|
492
|
+
setTimeout: (() => ({ ref() { }, unref() { } })),
|
|
493
|
+
clearTimeout: (() => undefined),
|
|
467
494
|
processOn: (() => undefined),
|
|
468
495
|
processExit: (() => {
|
|
469
496
|
throw new Error('should not exit');
|
|
@@ -516,8 +543,8 @@ test('startPolling reports FAILED when worktree removal throws after matching a
|
|
|
516
543
|
removeWorktree: () => {
|
|
517
544
|
throw new Error('permission denied');
|
|
518
545
|
},
|
|
519
|
-
|
|
520
|
-
|
|
546
|
+
setTimeout: (() => ({ ref() { }, unref() { } })),
|
|
547
|
+
clearTimeout: (() => undefined),
|
|
521
548
|
processOn: (() => undefined),
|
|
522
549
|
processExit: (() => {
|
|
523
550
|
throw new Error('should not exit');
|
|
@@ -563,8 +590,8 @@ test('startPolling acks and runs restart when meta.restartRequested is true with
|
|
|
563
590
|
executeRestartRequest: () => {
|
|
564
591
|
restartCalled += 1;
|
|
565
592
|
},
|
|
566
|
-
|
|
567
|
-
|
|
593
|
+
setTimeout: (() => ({})),
|
|
594
|
+
clearTimeout: (() => undefined),
|
|
568
595
|
processOn: (() => undefined),
|
|
569
596
|
processExit: (() => undefined),
|
|
570
597
|
now: () => 0,
|
|
@@ -606,8 +633,8 @@ test('startPolling restarts even when a pending trigger is present, ignoring the
|
|
|
606
633
|
executeRestartRequest: () => {
|
|
607
634
|
restartCalled += 1;
|
|
608
635
|
},
|
|
609
|
-
|
|
610
|
-
|
|
636
|
+
setTimeout: (() => ({})),
|
|
637
|
+
clearTimeout: (() => undefined),
|
|
611
638
|
processOn: (() => undefined),
|
|
612
639
|
processExit: (() => undefined),
|
|
613
640
|
now: () => 0,
|
|
@@ -642,8 +669,8 @@ test('startPolling skips restart and leaves the flag for retry when ack fails',
|
|
|
642
669
|
executeRestartRequest: () => {
|
|
643
670
|
restartCalled += 1;
|
|
644
671
|
},
|
|
645
|
-
|
|
646
|
-
|
|
672
|
+
setTimeout: (() => ({})),
|
|
673
|
+
clearTimeout: (() => undefined),
|
|
647
674
|
processOn: (() => undefined),
|
|
648
675
|
processExit: (() => undefined),
|
|
649
676
|
now: () => 0,
|
|
@@ -669,8 +696,8 @@ test('startPolling acquires the power save blocker on start and releases it on s
|
|
|
669
696
|
const pollingPromise = startPolling(config, () => async () => undefined, {
|
|
670
697
|
createClient: () => makeClient(),
|
|
671
698
|
runCleanup: async () => undefined,
|
|
672
|
-
|
|
673
|
-
|
|
699
|
+
setTimeout: (() => ({})),
|
|
700
|
+
clearTimeout: (() => undefined),
|
|
674
701
|
processOn: ((event, listener) => {
|
|
675
702
|
signals.set(event, listener);
|
|
676
703
|
}),
|
|
@@ -702,8 +729,8 @@ test('startPolling releases the power save blocker when keepAlive resolves', asy
|
|
|
702
729
|
const pollingPromise = startPolling(config, () => async () => undefined, {
|
|
703
730
|
createClient: () => makeClient(),
|
|
704
731
|
runCleanup: async () => undefined,
|
|
705
|
-
|
|
706
|
-
|
|
732
|
+
setTimeout: (() => ({})),
|
|
733
|
+
clearTimeout: (() => undefined),
|
|
707
734
|
processOn: (() => undefined),
|
|
708
735
|
processExit: (() => undefined),
|
|
709
736
|
now: () => 0,
|
|
@@ -724,4 +751,111 @@ test('startPolling releases the power save blocker when keepAlive resolves', asy
|
|
|
724
751
|
await pollingPromise;
|
|
725
752
|
assert.deepEqual(events, ['acquire:daemon-polling', 'release:daemon-polling'], 'blocker should be released after keepAlive resolves');
|
|
726
753
|
});
|
|
754
|
+
// idle 백오프 테스트 공통 의존성. 폴 스케줄만 관찰하고 나머지는 no-op으로 둔다.
|
|
755
|
+
const backoffDependencies = (timeouts, keepAlive) => ({
|
|
756
|
+
runCleanup: async () => undefined,
|
|
757
|
+
runConventionSync: async () => undefined,
|
|
758
|
+
maybeAutoUpdate: async () => ({ cliUpdated: false, runnerUpdated: false }),
|
|
759
|
+
setTimeout: timeouts.setTimeoutMock,
|
|
760
|
+
clearTimeout: timeouts.clearTimeoutMock,
|
|
761
|
+
processOn: (() => undefined),
|
|
762
|
+
processExit: (() => {
|
|
763
|
+
throw new Error('should not exit');
|
|
764
|
+
}),
|
|
765
|
+
now: () => 0,
|
|
766
|
+
loadAuthPaths: () => [],
|
|
767
|
+
saveAuthPath: () => '/tmp/auth-paths.json',
|
|
768
|
+
keepAlive,
|
|
769
|
+
});
|
|
770
|
+
test('startPolling backs off the poll interval while idle and clamps at maxPollingIntervalMs', async () => {
|
|
771
|
+
const timeouts = createTimeoutRecorder();
|
|
772
|
+
let keepAliveResolve = null;
|
|
773
|
+
const pollingPromise = startPolling(config, () => async () => undefined, {
|
|
774
|
+
createClient: () => makeClient(),
|
|
775
|
+
...backoffDependencies(timeouts, () => new Promise((resolve) => {
|
|
776
|
+
keepAliveResolve = resolve;
|
|
777
|
+
})),
|
|
778
|
+
});
|
|
779
|
+
// base 5000 / max 12000: idle 연속 시 5000×(1+n×0.5) → 7500, 10000, 12500→12000 clamp.
|
|
780
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
781
|
+
assert.equal(timeouts.scheduled.length, 1);
|
|
782
|
+
assert.equal(timeouts.scheduled[0]?.delayMs, 7500);
|
|
783
|
+
timeouts.scheduled.at(-1)?.callback();
|
|
784
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
785
|
+
assert.equal(timeouts.scheduled[1]?.delayMs, 10_000);
|
|
786
|
+
timeouts.scheduled.at(-1)?.callback();
|
|
787
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
788
|
+
assert.equal(timeouts.scheduled[2]?.delayMs, 12_000, 'delay should clamp at maxPollingIntervalMs');
|
|
789
|
+
timeouts.scheduled.at(-1)?.callback();
|
|
790
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
791
|
+
assert.equal(timeouts.scheduled[3]?.delayMs, 12_000, 'delay should stay clamped while idle continues');
|
|
792
|
+
const resolveKeepAlive = keepAliveResolve ??
|
|
793
|
+
(() => {
|
|
794
|
+
throw new Error('keepAlive resolver was not registered');
|
|
795
|
+
});
|
|
796
|
+
resolveKeepAlive();
|
|
797
|
+
await pollingPromise;
|
|
798
|
+
});
|
|
799
|
+
test('startPolling resets the poll interval to base after an active cycle', async () => {
|
|
800
|
+
const timeouts = createTimeoutRecorder();
|
|
801
|
+
let keepAliveResolve = null;
|
|
802
|
+
// 폴 순서: idle → idle → pending 트리거(활동) → idle.
|
|
803
|
+
const responses = [pollState(), pollState(), pollState({ pendingTrigger: trigger }), pollState()];
|
|
804
|
+
const client = makeClient({
|
|
805
|
+
fetchPollState: async () => responses.shift() ?? pollState(),
|
|
806
|
+
});
|
|
807
|
+
const pollingPromise = startPolling(config, () => async () => undefined, {
|
|
808
|
+
createClient: () => client,
|
|
809
|
+
...backoffDependencies(timeouts, () => new Promise((resolve) => {
|
|
810
|
+
keepAliveResolve = resolve;
|
|
811
|
+
})),
|
|
812
|
+
});
|
|
813
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
814
|
+
timeouts.scheduled.at(-1)?.callback();
|
|
815
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
816
|
+
timeouts.scheduled.at(-1)?.callback();
|
|
817
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
818
|
+
timeouts.scheduled.at(-1)?.callback();
|
|
819
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
820
|
+
assert.deepEqual(timeouts.scheduled.map((entry) => entry.delayMs),
|
|
821
|
+
// idle 백오프(7500→10000) → 활동 시 base(5000) 리셋 → idle streak 처음부터(7500).
|
|
822
|
+
[7500, 10_000, 5000, 7500]);
|
|
823
|
+
const resolveKeepAlive = keepAliveResolve ??
|
|
824
|
+
(() => {
|
|
825
|
+
throw new Error('keepAlive resolver was not registered');
|
|
826
|
+
});
|
|
827
|
+
resolveKeepAlive();
|
|
828
|
+
await pollingPromise;
|
|
829
|
+
});
|
|
830
|
+
test('startPolling stops scheduling new polls after shutdown', async () => {
|
|
831
|
+
const timeouts = createTimeoutRecorder();
|
|
832
|
+
const signals = new Map();
|
|
833
|
+
let keepAliveResolve = null;
|
|
834
|
+
const pollingPromise = startPolling(config, () => async () => undefined, {
|
|
835
|
+
createClient: () => makeClient(),
|
|
836
|
+
...backoffDependencies(timeouts, () => new Promise((resolve) => {
|
|
837
|
+
keepAliveResolve = resolve;
|
|
838
|
+
})),
|
|
839
|
+
processOn: ((event, listener) => {
|
|
840
|
+
signals.set(event, listener);
|
|
841
|
+
}),
|
|
842
|
+
processExit: (() => undefined),
|
|
843
|
+
});
|
|
844
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
845
|
+
assert.equal(timeouts.scheduled.length, 1);
|
|
846
|
+
const sigterm = signals.get('SIGTERM');
|
|
847
|
+
assert.ok(sigterm);
|
|
848
|
+
sigterm();
|
|
849
|
+
assert.equal(timeouts.cleared.length, 1, 'pending poll timer should be cleared on shutdown');
|
|
850
|
+
// 셧다운 이후 잔여 타이머 콜백이 fire되어도 새 폴을 예약하지 않는다.
|
|
851
|
+
timeouts.scheduled[0]?.callback();
|
|
852
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
853
|
+
assert.equal(timeouts.scheduled.length, 1, 'no new poll may be scheduled after shutdown');
|
|
854
|
+
const resolveKeepAlive = keepAliveResolve ??
|
|
855
|
+
(() => {
|
|
856
|
+
throw new Error('keepAlive resolver was not registered');
|
|
857
|
+
});
|
|
858
|
+
resolveKeepAlive();
|
|
859
|
+
await pollingPromise;
|
|
860
|
+
});
|
|
727
861
|
//# sourceMappingURL=poller.test.js.map
|