@stablekernel/pi-background-run 0.1.0 → 0.2.1
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/README.md +38 -3
- package/extension/index.test.ts +573 -59
- package/extension/index.ts +452 -101
- package/package.json +3 -2
- package/skill/run-bg/SKILL.md +15 -7
package/extension/index.test.ts
CHANGED
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
|
|
14
14
|
import { test } from "node:test";
|
|
15
15
|
import assert from "node:assert/strict";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
mkdtempSync,
|
|
18
|
+
rmSync,
|
|
19
|
+
readFileSync,
|
|
20
|
+
writeFileSync,
|
|
21
|
+
existsSync,
|
|
22
|
+
readdirSync,
|
|
23
|
+
} from "node:fs";
|
|
17
24
|
import { join } from "node:path";
|
|
18
25
|
import { tmpdir } from "node:os";
|
|
19
26
|
import { pathToFileURL } from "node:url";
|
|
@@ -34,7 +41,10 @@ function makeFakePi(opts: { idle?: boolean; priorEntries?: any[] } = {}): {
|
|
|
34
41
|
} {
|
|
35
42
|
const wakes: CapturedWake[] = [];
|
|
36
43
|
const entries: any[] = opts.priorEntries ? [...opts.priorEntries] : [];
|
|
37
|
-
const tools = new Map<
|
|
44
|
+
const tools = new Map<
|
|
45
|
+
string,
|
|
46
|
+
{ execute: (...args: any[]) => Promise<any> }
|
|
47
|
+
>();
|
|
38
48
|
const handlers = new Map<string, ((...args: any[]) => Promise<any>)[]>();
|
|
39
49
|
const idle = opts.idle ?? true;
|
|
40
50
|
const ctx = {
|
|
@@ -68,19 +78,33 @@ function makeFakePi(opts: { idle?: boolean; priorEntries?: any[] } = {}): {
|
|
|
68
78
|
return { pi, wakes, entries, tools, ctx, handlers, fireSessionStart };
|
|
69
79
|
}
|
|
70
80
|
|
|
71
|
-
async function loadExtension(
|
|
81
|
+
async function loadExtension(
|
|
82
|
+
fakePi: any,
|
|
83
|
+
): Promise<Map<string, { execute: (...args: any[]) => Promise<any> }>> {
|
|
72
84
|
const url = pathToFileURL(join(process.cwd(), "extension/index.ts")).href;
|
|
73
85
|
const mod = await import(url);
|
|
74
86
|
mod.default(fakePi);
|
|
75
|
-
return fakePi.tools as Map<
|
|
87
|
+
return fakePi.tools as Map<
|
|
88
|
+
string,
|
|
89
|
+
{ execute: (...args: any[]) => Promise<any> }
|
|
90
|
+
>;
|
|
76
91
|
}
|
|
77
92
|
|
|
78
|
-
function waitForWakes(
|
|
93
|
+
function waitForWakes(
|
|
94
|
+
wakes: CapturedWake[],
|
|
95
|
+
count: number,
|
|
96
|
+
timeoutMs = 5000,
|
|
97
|
+
): Promise<void> {
|
|
79
98
|
return new Promise((resolve, reject) => {
|
|
80
99
|
const start = Date.now();
|
|
81
100
|
const tick = () => {
|
|
82
101
|
if (wakes.length >= count) return resolve();
|
|
83
|
-
if (Date.now() - start > timeoutMs)
|
|
102
|
+
if (Date.now() - start > timeoutMs)
|
|
103
|
+
return reject(
|
|
104
|
+
new Error(
|
|
105
|
+
`timed out waiting for ${count} wakes, got ${wakes.length}`,
|
|
106
|
+
),
|
|
107
|
+
);
|
|
84
108
|
setTimeout(tick, 50);
|
|
85
109
|
};
|
|
86
110
|
tick();
|
|
@@ -95,7 +119,13 @@ test("bgrun: successful command writes log + exit marker and wakes with ✅", as
|
|
|
95
119
|
await loadExtension(pi);
|
|
96
120
|
const bgrun = tools.get("bgrun")!;
|
|
97
121
|
|
|
98
|
-
const res = await bgrun.execute(
|
|
122
|
+
const res = await bgrun.execute(
|
|
123
|
+
"call-1",
|
|
124
|
+
{ command: "echo hello world" },
|
|
125
|
+
undefined,
|
|
126
|
+
undefined,
|
|
127
|
+
ctx,
|
|
128
|
+
);
|
|
99
129
|
const started = res.content[0].text as string;
|
|
100
130
|
assert.match(started, /^started: /);
|
|
101
131
|
const id = (started.match(/^started: ([^\n]+)/) || [])[1];
|
|
@@ -129,7 +159,13 @@ test("bgrun: failing command wakes with ❌ and the non-zero exit code", async (
|
|
|
129
159
|
await loadExtension(pi);
|
|
130
160
|
const bgrun = tools.get("bgrun")!;
|
|
131
161
|
|
|
132
|
-
await bgrun.execute(
|
|
162
|
+
await bgrun.execute(
|
|
163
|
+
"call-2",
|
|
164
|
+
{ command: "echo failing now; exit 7" },
|
|
165
|
+
undefined,
|
|
166
|
+
undefined,
|
|
167
|
+
ctx,
|
|
168
|
+
);
|
|
133
169
|
await waitForWakes(wakes, 1);
|
|
134
170
|
const wake = wakes[0].text;
|
|
135
171
|
assert.match(wake, /❌/);
|
|
@@ -148,7 +184,13 @@ test("bgrun: when agent is busy, wake is queued as followUp", async () => {
|
|
|
148
184
|
await loadExtension(pi);
|
|
149
185
|
const bgrun = tools.get("bgrun")!;
|
|
150
186
|
|
|
151
|
-
await bgrun.execute(
|
|
187
|
+
await bgrun.execute(
|
|
188
|
+
"call-busy",
|
|
189
|
+
{ command: "echo while-busy" },
|
|
190
|
+
undefined,
|
|
191
|
+
undefined,
|
|
192
|
+
ctx,
|
|
193
|
+
);
|
|
152
194
|
await waitForWakes(wakes, 1);
|
|
153
195
|
assert.equal(wakes[0].options?.deliverAs, "followUp");
|
|
154
196
|
} finally {
|
|
@@ -166,11 +208,23 @@ test("bgtail: returns last N lines, strips the exit marker", async () => {
|
|
|
166
208
|
const bgrun = tools.get("bgrun")!;
|
|
167
209
|
const bgtail = tools.get("bgtail")!;
|
|
168
210
|
|
|
169
|
-
const res = await bgrun.execute(
|
|
211
|
+
const res = await bgrun.execute(
|
|
212
|
+
"call-3",
|
|
213
|
+
{ command: "printf 'line1\\nline2\\nline3\\n'" },
|
|
214
|
+
undefined,
|
|
215
|
+
undefined,
|
|
216
|
+
ctx,
|
|
217
|
+
);
|
|
170
218
|
const id = (res.content[0].text as string).match(/^started: ([^\n]+)/)![1];
|
|
171
219
|
await waitForWakes(wakes, 1);
|
|
172
220
|
|
|
173
|
-
const tail = await bgtail.execute(
|
|
221
|
+
const tail = await bgtail.execute(
|
|
222
|
+
"call-3",
|
|
223
|
+
{ id, lines: 2 },
|
|
224
|
+
undefined,
|
|
225
|
+
undefined,
|
|
226
|
+
ctx,
|
|
227
|
+
);
|
|
174
228
|
const text = tail.content[0].text as string;
|
|
175
229
|
assert.ok(!text.includes("__BGRUN_EXIT__"), "marker stripped");
|
|
176
230
|
assert.match(text, /line2\nline3$|^line3$/);
|
|
@@ -189,15 +243,33 @@ test("bgstatus: shows running then done with exit code", async () => {
|
|
|
189
243
|
const bgrun = tools.get("bgrun")!;
|
|
190
244
|
const bgstatus = tools.get("bgstatus")!;
|
|
191
245
|
|
|
192
|
-
const res = await bgrun.execute(
|
|
246
|
+
const res = await bgrun.execute(
|
|
247
|
+
"call-4",
|
|
248
|
+
{ command: "sleep 0.2; echo done" },
|
|
249
|
+
undefined,
|
|
250
|
+
undefined,
|
|
251
|
+
ctx,
|
|
252
|
+
);
|
|
193
253
|
const id = (res.content[0].text as string).match(/^started: ([^\n]+)/)![1];
|
|
194
254
|
|
|
195
255
|
// While running, status should say running.
|
|
196
|
-
const running = await bgstatus.execute(
|
|
256
|
+
const running = await bgstatus.execute(
|
|
257
|
+
"call-4",
|
|
258
|
+
{ id },
|
|
259
|
+
undefined,
|
|
260
|
+
undefined,
|
|
261
|
+
ctx,
|
|
262
|
+
);
|
|
197
263
|
assert.match(running.content[0].text as string, /running/);
|
|
198
264
|
|
|
199
265
|
await waitForWakes(wakes, 1);
|
|
200
|
-
const done = await bgstatus.execute(
|
|
266
|
+
const done = await bgstatus.execute(
|
|
267
|
+
"call-4",
|
|
268
|
+
{ id },
|
|
269
|
+
undefined,
|
|
270
|
+
undefined,
|
|
271
|
+
ctx,
|
|
272
|
+
);
|
|
201
273
|
assert.match(done.content[0].text as string, /done/);
|
|
202
274
|
assert.match(done.content[0].text as string, /exit=0/);
|
|
203
275
|
} finally {
|
|
@@ -206,14 +278,21 @@ test("bgstatus: shows running then done with exit code", async () => {
|
|
|
206
278
|
}
|
|
207
279
|
});
|
|
208
280
|
|
|
209
|
-
test("bgstatus: list-all
|
|
281
|
+
test("bgstatus: list-all after 'restart' hides finished logs by default, notes them instead", async () => {
|
|
210
282
|
const dir = mkdtempSync(join(tmpdir(), "pi-bgrun-test-"));
|
|
211
283
|
process.env.PI_BGRUN_DIR = dir;
|
|
284
|
+
delete process.env.PI_BGRUN_FOREIGN_JOBS;
|
|
212
285
|
try {
|
|
213
286
|
const { pi, tools, ctx } = makeFakePi();
|
|
214
287
|
await loadExtension(pi);
|
|
215
288
|
const bgrun = tools.get("bgrun")!;
|
|
216
|
-
const res = await bgrun.execute(
|
|
289
|
+
const res = await bgrun.execute(
|
|
290
|
+
"call-5",
|
|
291
|
+
{ command: "echo persisted" },
|
|
292
|
+
undefined,
|
|
293
|
+
undefined,
|
|
294
|
+
ctx,
|
|
295
|
+
);
|
|
217
296
|
const id = (res.content[0].text as string).match(/^started: ([^\n]+)/)![1];
|
|
218
297
|
|
|
219
298
|
// Wait for completion by polling the log marker.
|
|
@@ -222,27 +301,88 @@ test("bgstatus: list-all scans the jobs dir after 'restart' (no in-memory record
|
|
|
222
301
|
const start = Date.now();
|
|
223
302
|
const tick = () => {
|
|
224
303
|
try {
|
|
225
|
-
if (readFileSync(logPath, "utf8").includes("__BGRUN_EXIT__=0"))
|
|
304
|
+
if (readFileSync(logPath, "utf8").includes("__BGRUN_EXIT__=0"))
|
|
305
|
+
return resolve();
|
|
226
306
|
} catch {}
|
|
227
|
-
if (Date.now() - start > 5000)
|
|
307
|
+
if (Date.now() - start > 5000)
|
|
308
|
+
return reject(new Error("log marker never appeared"));
|
|
228
309
|
setTimeout(tick, 50);
|
|
229
310
|
};
|
|
230
311
|
tick();
|
|
231
312
|
});
|
|
232
313
|
|
|
233
|
-
// Fresh instance — no in-memory records.
|
|
314
|
+
// Fresh instance — no in-memory records. Default listing must NOT spam the
|
|
315
|
+
// finished job; it gets a one-line count note instead.
|
|
234
316
|
const { pi: pi2, tools: tools2 } = makeFakePi();
|
|
235
317
|
await loadExtension(pi2);
|
|
236
318
|
const bgstatus2 = tools2.get("bgstatus")!;
|
|
237
|
-
const list = await bgstatus2.execute(
|
|
238
|
-
|
|
239
|
-
|
|
319
|
+
const list = await bgstatus2.execute(
|
|
320
|
+
"call-5",
|
|
321
|
+
{},
|
|
322
|
+
undefined,
|
|
323
|
+
undefined,
|
|
324
|
+
ctx,
|
|
325
|
+
);
|
|
326
|
+
const text = list.content[0].text as string;
|
|
327
|
+
assert.ok(!new RegExp(id).test(text), "finished job hidden by default");
|
|
328
|
+
assert.match(text, /\(1 more job log\(s\) on disk/);
|
|
329
|
+
|
|
330
|
+
// includeDone reveals it with the exit code recovered from the log.
|
|
331
|
+
const full = await bgstatus2.execute(
|
|
332
|
+
"call-5b",
|
|
333
|
+
{ includeDone: true },
|
|
334
|
+
undefined,
|
|
335
|
+
undefined,
|
|
336
|
+
ctx,
|
|
337
|
+
);
|
|
338
|
+
const fullText = full.content[0].text as string;
|
|
339
|
+
assert.match(fullText, new RegExp(id));
|
|
340
|
+
assert.match(fullText, /exit=0/);
|
|
341
|
+
assert.match(fullText, /\(from log\)/);
|
|
240
342
|
} finally {
|
|
241
343
|
delete process.env.PI_BGRUN_DIR;
|
|
242
344
|
rmSync(dir, { recursive: true, force: true });
|
|
243
345
|
}
|
|
244
346
|
});
|
|
245
347
|
|
|
348
|
+
test("bgstatus: showCompletedJobs config (env) lists finished jobs by default", async () => {
|
|
349
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-bgrun-test-"));
|
|
350
|
+
process.env.PI_BGRUN_DIR = dir;
|
|
351
|
+
process.env.PI_BGRUN_SHOW_COMPLETED = "1";
|
|
352
|
+
try {
|
|
353
|
+
const { pi, wakes, tools, ctx } = makeFakePi();
|
|
354
|
+
await loadExtension(pi);
|
|
355
|
+
const bgrun = tools.get("bgrun")!;
|
|
356
|
+
const bgstatus = tools.get("bgstatus")!;
|
|
357
|
+
|
|
358
|
+
const res = await bgrun.execute(
|
|
359
|
+
"call-sd1",
|
|
360
|
+
{ command: "echo shown-done", name: "done-job" },
|
|
361
|
+
undefined,
|
|
362
|
+
undefined,
|
|
363
|
+
ctx,
|
|
364
|
+
);
|
|
365
|
+
const id = (res.content[0].text as string).match(/^started: ([^\n]+)/)![1];
|
|
366
|
+
await waitForWakes(wakes, 1);
|
|
367
|
+
|
|
368
|
+
const list = await bgstatus.execute(
|
|
369
|
+
"call-sd2",
|
|
370
|
+
{},
|
|
371
|
+
undefined,
|
|
372
|
+
undefined,
|
|
373
|
+
ctx,
|
|
374
|
+
);
|
|
375
|
+
assert.match(
|
|
376
|
+
list.content[0].text as string,
|
|
377
|
+
new RegExp(`${id} — done-job: done exit=0`),
|
|
378
|
+
);
|
|
379
|
+
} finally {
|
|
380
|
+
delete process.env.PI_BGRUN_DIR;
|
|
381
|
+
delete process.env.PI_BGRUN_SHOW_COMPLETED;
|
|
382
|
+
rmSync(dir, { recursive: true, force: true });
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
246
386
|
test("bgrun: rejects empty command", async () => {
|
|
247
387
|
const { pi, tools, ctx } = makeFakePi();
|
|
248
388
|
await loadExtension(pi);
|
|
@@ -263,7 +403,13 @@ test("bgrun: appends bgrun-job entries (running then done)", async () => {
|
|
|
263
403
|
await loadExtension(pi);
|
|
264
404
|
const bgrun = tools.get("bgrun")!;
|
|
265
405
|
|
|
266
|
-
await bgrun.execute(
|
|
406
|
+
await bgrun.execute(
|
|
407
|
+
"call-e1",
|
|
408
|
+
{ command: "echo entry-test" },
|
|
409
|
+
undefined,
|
|
410
|
+
undefined,
|
|
411
|
+
ctx,
|
|
412
|
+
);
|
|
267
413
|
// One running entry appended at start.
|
|
268
414
|
const runningEntries = entries.filter((e) => e.data?.state === "running");
|
|
269
415
|
assert.equal(runningEntries.length, 1, "running entry appended at start");
|
|
@@ -288,24 +434,47 @@ test("session_start: reconstructs in-memory Map from bgrun-job entries", async (
|
|
|
288
434
|
const { pi: pi1, wakes, entries, tools: tools1, ctx: ctx1 } = makeFakePi();
|
|
289
435
|
await loadExtension(pi1);
|
|
290
436
|
const bgrun1 = tools1.get("bgrun")!;
|
|
291
|
-
const res = await bgrun1.execute(
|
|
437
|
+
const res = await bgrun1.execute(
|
|
438
|
+
"call-r1",
|
|
439
|
+
{ command: "echo reconstruct-me" },
|
|
440
|
+
undefined,
|
|
441
|
+
undefined,
|
|
442
|
+
ctx1,
|
|
443
|
+
);
|
|
292
444
|
const id = (res.content[0].text as string).match(/^started: ([^\n]+)/)![1];
|
|
293
445
|
await waitForWakes(wakes, 1);
|
|
294
446
|
|
|
295
447
|
// Second instance: simulate a restart. Load fresh, passing the prior entries,
|
|
296
448
|
// then fire session_start to trigger reconstruction.
|
|
297
|
-
const {
|
|
449
|
+
const {
|
|
450
|
+
pi: pi2,
|
|
451
|
+
tools: tools2,
|
|
452
|
+
ctx: ctx2,
|
|
453
|
+
fireSessionStart,
|
|
454
|
+
} = makeFakePi({ priorEntries: entries });
|
|
298
455
|
await loadExtension(pi2);
|
|
299
456
|
await fireSessionStart();
|
|
300
457
|
|
|
301
458
|
// Now bgstatus should find the job in the in-memory Map (not just dir scan).
|
|
302
459
|
const bgstatus2 = tools2.get("bgstatus")!;
|
|
303
|
-
const status = await bgstatus2.execute(
|
|
460
|
+
const status = await bgstatus2.execute(
|
|
461
|
+
"call-r2",
|
|
462
|
+
{ id },
|
|
463
|
+
undefined,
|
|
464
|
+
undefined,
|
|
465
|
+
ctx2,
|
|
466
|
+
);
|
|
304
467
|
const text = status.content[0].text as string;
|
|
305
468
|
assert.match(text, /done.*exit=0/);
|
|
306
469
|
// Verify it came from the in-memory Map (not "from log" marker).
|
|
307
|
-
assert.ok(
|
|
308
|
-
|
|
470
|
+
assert.ok(
|
|
471
|
+
!text.includes("from log"),
|
|
472
|
+
"reconstructed from entries, not dir scan",
|
|
473
|
+
);
|
|
474
|
+
assert.ok(
|
|
475
|
+
!text.includes("recovered from log"),
|
|
476
|
+
"reconstructed from entries, not log recovery",
|
|
477
|
+
);
|
|
309
478
|
} finally {
|
|
310
479
|
delete process.env.PI_BGRUN_DIR;
|
|
311
480
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -322,11 +491,20 @@ test("bgrun: name flows into job id, response, entry, wake, and status", async (
|
|
|
322
491
|
await loadExtension(pi);
|
|
323
492
|
const bgrun = tools.get("bgrun")!;
|
|
324
493
|
|
|
325
|
-
const res = await bgrun.execute(
|
|
494
|
+
const res = await bgrun.execute(
|
|
495
|
+
"call-n1",
|
|
496
|
+
{ command: "echo named job", name: "unit-tests" },
|
|
497
|
+
undefined,
|
|
498
|
+
undefined,
|
|
499
|
+
ctx,
|
|
500
|
+
);
|
|
326
501
|
const text = res.content[0].text as string;
|
|
327
502
|
const id = (text.match(/^started: ([^\n]+)/) || [])[1];
|
|
328
503
|
// Slug derives from the name, not the command.
|
|
329
|
-
assert.ok(
|
|
504
|
+
assert.ok(
|
|
505
|
+
id.startsWith("unit-tests-"),
|
|
506
|
+
`id should start with 'unit-tests-': ${id}`,
|
|
507
|
+
);
|
|
330
508
|
// Response includes the name.
|
|
331
509
|
assert.match(text, /name: unit-tests/);
|
|
332
510
|
// Details include the name.
|
|
@@ -354,15 +532,27 @@ test("bgrun: name is optional — behavior unchanged without it", async () => {
|
|
|
354
532
|
await loadExtension(pi);
|
|
355
533
|
const bgrun = tools.get("bgrun")!;
|
|
356
534
|
|
|
357
|
-
const res = await bgrun.execute(
|
|
535
|
+
const res = await bgrun.execute(
|
|
536
|
+
"call-n2",
|
|
537
|
+
{ command: "echo unnamed job" },
|
|
538
|
+
undefined,
|
|
539
|
+
undefined,
|
|
540
|
+
ctx,
|
|
541
|
+
);
|
|
358
542
|
const text = res.content[0].text as string;
|
|
359
543
|
// No 'name:' line in the response.
|
|
360
544
|
assert.ok(!/^ name:/m.test(text), "no name line when name omitted");
|
|
361
545
|
const id = (text.match(/^started: ([^\n]+)/) || [])[1];
|
|
362
|
-
assert.ok(
|
|
546
|
+
assert.ok(
|
|
547
|
+
id.startsWith("echo-unnamed-job-"),
|
|
548
|
+
`slug falls back to command: ${id}`,
|
|
549
|
+
);
|
|
363
550
|
|
|
364
551
|
await waitForWakes(wakes, 1);
|
|
365
|
-
assert.ok(
|
|
552
|
+
assert.ok(
|
|
553
|
+
!wakes[0].text.includes('"'),
|
|
554
|
+
"wake has no name quote when unnamed",
|
|
555
|
+
);
|
|
366
556
|
} finally {
|
|
367
557
|
delete process.env.PI_BGRUN_DIR;
|
|
368
558
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -378,12 +568,27 @@ test("bgrun: blank name is ignored, over-long name is truncated", async () => {
|
|
|
378
568
|
const bgrun = tools.get("bgrun")!;
|
|
379
569
|
|
|
380
570
|
// Blank name treated as absent.
|
|
381
|
-
const res1 = await bgrun.execute(
|
|
382
|
-
|
|
571
|
+
const res1 = await bgrun.execute(
|
|
572
|
+
"call-n3",
|
|
573
|
+
{ command: "echo blank", name: " " },
|
|
574
|
+
undefined,
|
|
575
|
+
undefined,
|
|
576
|
+
ctx,
|
|
577
|
+
);
|
|
578
|
+
assert.ok(
|
|
579
|
+
!/^ name:/m.test(res1.content[0].text as string),
|
|
580
|
+
"blank name ignored",
|
|
581
|
+
);
|
|
383
582
|
|
|
384
583
|
// Over-long name truncated to 80 chars.
|
|
385
584
|
const longName = "x".repeat(200);
|
|
386
|
-
const res2 = await bgrun.execute(
|
|
585
|
+
const res2 = await bgrun.execute(
|
|
586
|
+
"call-n4",
|
|
587
|
+
{ command: "echo long", name: longName },
|
|
588
|
+
undefined,
|
|
589
|
+
undefined,
|
|
590
|
+
ctx,
|
|
591
|
+
);
|
|
387
592
|
const text2 = res2.content[0].text as string;
|
|
388
593
|
const nameLine = (text2.match(/^ name: (.+)$/m) || [])[1];
|
|
389
594
|
assert.equal(nameLine.length, 80, "name truncated to 80 chars");
|
|
@@ -403,20 +608,40 @@ test("bgrun: name survives session_start reconstruction", async () => {
|
|
|
403
608
|
const { pi: pi1, wakes, entries, tools: tools1, ctx: ctx1 } = makeFakePi();
|
|
404
609
|
await loadExtension(pi1);
|
|
405
610
|
const bgrun1 = tools1.get("bgrun")!;
|
|
406
|
-
const res = await bgrun1.execute(
|
|
611
|
+
const res = await bgrun1.execute(
|
|
612
|
+
"call-n5",
|
|
613
|
+
{ command: "echo named-restart", name: "rebuild" },
|
|
614
|
+
undefined,
|
|
615
|
+
undefined,
|
|
616
|
+
ctx1,
|
|
617
|
+
);
|
|
407
618
|
const id = (res.content[0].text as string).match(/^started: ([^\n]+)/)![1];
|
|
408
619
|
await waitForWakes(wakes, 1);
|
|
409
620
|
|
|
410
621
|
// Second instance: reconstruct from entries, name should be restored.
|
|
411
|
-
const {
|
|
622
|
+
const {
|
|
623
|
+
pi: pi2,
|
|
624
|
+
tools: tools2,
|
|
625
|
+
ctx: ctx2,
|
|
626
|
+
fireSessionStart,
|
|
627
|
+
} = makeFakePi({ priorEntries: entries });
|
|
412
628
|
await loadExtension(pi2);
|
|
413
629
|
await fireSessionStart();
|
|
414
630
|
|
|
415
631
|
const bgstatus2 = tools2.get("bgstatus")!;
|
|
416
|
-
const status = await bgstatus2.execute(
|
|
632
|
+
const status = await bgstatus2.execute(
|
|
633
|
+
"call-n6",
|
|
634
|
+
{ id },
|
|
635
|
+
undefined,
|
|
636
|
+
undefined,
|
|
637
|
+
ctx2,
|
|
638
|
+
);
|
|
417
639
|
const text = status.content[0].text as string;
|
|
418
640
|
assert.match(text, /name: rebuild/);
|
|
419
|
-
assert.ok(
|
|
641
|
+
assert.ok(
|
|
642
|
+
!text.includes("recovered from log"),
|
|
643
|
+
"reconstructed from entries, not log",
|
|
644
|
+
);
|
|
420
645
|
} finally {
|
|
421
646
|
delete process.env.PI_BGRUN_DIR;
|
|
422
647
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -432,11 +657,89 @@ test("bgstatus: list shows name after job id", async () => {
|
|
|
432
657
|
const bgrun = tools.get("bgrun")!;
|
|
433
658
|
const bgstatus = tools.get("bgstatus")!;
|
|
434
659
|
|
|
435
|
-
await bgrun.execute(
|
|
660
|
+
await bgrun.execute(
|
|
661
|
+
"call-n7",
|
|
662
|
+
{ command: "sleep 0.1; echo listed", name: "nightly" },
|
|
663
|
+
undefined,
|
|
664
|
+
undefined,
|
|
665
|
+
ctx,
|
|
666
|
+
);
|
|
436
667
|
await waitForWakes(wakes, 1);
|
|
437
668
|
|
|
438
|
-
const list = await bgstatus.execute(
|
|
669
|
+
const list = await bgstatus.execute(
|
|
670
|
+
"call-n8",
|
|
671
|
+
{ includeDone: true },
|
|
672
|
+
undefined,
|
|
673
|
+
undefined,
|
|
674
|
+
ctx,
|
|
675
|
+
);
|
|
439
676
|
assert.match(list.content[0].text as string, /— nightly: done exit=0/);
|
|
677
|
+
// Without includeDone, finished jobs are hidden by default.
|
|
678
|
+
const runningOnly = await bgstatus.execute(
|
|
679
|
+
"call-n8b",
|
|
680
|
+
{},
|
|
681
|
+
undefined,
|
|
682
|
+
undefined,
|
|
683
|
+
ctx,
|
|
684
|
+
);
|
|
685
|
+
assert.ok(
|
|
686
|
+
!/— nightly: done/.test(runningOnly.content[0].text as string),
|
|
687
|
+
"done job hidden without includeDone",
|
|
688
|
+
);
|
|
689
|
+
} finally {
|
|
690
|
+
delete process.env.PI_BGRUN_DIR;
|
|
691
|
+
rmSync(dir, { recursive: true, force: true });
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
test("session_start: foreign jobs are NOT adopted by default (opt-in only)", async () => {
|
|
696
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-bgrun-test-"));
|
|
697
|
+
process.env.PI_BGRUN_DIR = dir;
|
|
698
|
+
delete process.env.PI_BGRUN_FOREIGN_JOBS;
|
|
699
|
+
try {
|
|
700
|
+
// A running foreign job (no exit marker, live pid — this test process).
|
|
701
|
+
const foreignId = `other-session-job-${Date.now()}-${process.pid}`;
|
|
702
|
+
writeFileSync(join(dir, `${foreignId}.log`), "someone else's job\n");
|
|
703
|
+
|
|
704
|
+
const { pi, tools, ctx, fireSessionStart } = makeFakePi();
|
|
705
|
+
ctx.hasUI = true;
|
|
706
|
+
const widgetCalls: (string[] | undefined)[] = [];
|
|
707
|
+
ctx.ui.setWidget = (_ns: string, lines: string[] | undefined) =>
|
|
708
|
+
widgetCalls.push(lines);
|
|
709
|
+
|
|
710
|
+
await loadExtension(pi);
|
|
711
|
+
await fireSessionStart();
|
|
712
|
+
|
|
713
|
+
// Widget never shows the foreign job.
|
|
714
|
+
const shown = widgetCalls.find((l) => Array.isArray(l));
|
|
715
|
+
assert.equal(
|
|
716
|
+
shown,
|
|
717
|
+
undefined,
|
|
718
|
+
"no widget content for foreign jobs by default",
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
// List-all gives a count note, not the job itself.
|
|
722
|
+
const bgstatus = tools.get("bgstatus")!;
|
|
723
|
+
const list = await bgstatus.execute(
|
|
724
|
+
"call-f1",
|
|
725
|
+
{},
|
|
726
|
+
undefined,
|
|
727
|
+
undefined,
|
|
728
|
+
ctx,
|
|
729
|
+
);
|
|
730
|
+
const text = list.content[0].text as string;
|
|
731
|
+
assert.ok(!text.includes(foreignId), "foreign job not listed by default");
|
|
732
|
+
assert.match(text, /\(1 more job log\(s\) on disk/);
|
|
733
|
+
|
|
734
|
+
// Single-id lookup still works — that's the explicit escape hatch.
|
|
735
|
+
const one = await bgstatus.execute(
|
|
736
|
+
"call-f2",
|
|
737
|
+
{ id: foreignId },
|
|
738
|
+
undefined,
|
|
739
|
+
undefined,
|
|
740
|
+
ctx,
|
|
741
|
+
);
|
|
742
|
+
assert.match(one.content[0].text as string, /: running/);
|
|
440
743
|
} finally {
|
|
441
744
|
delete process.env.PI_BGRUN_DIR;
|
|
442
745
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -446,6 +749,7 @@ test("bgstatus: list shows name after job id", async () => {
|
|
|
446
749
|
test("session_start: adopts running jobs from the jobs dir (other session's job) into the widget", async () => {
|
|
447
750
|
const dir = mkdtempSync(join(tmpdir(), "pi-bgrun-test-"));
|
|
448
751
|
process.env.PI_BGRUN_DIR = dir;
|
|
752
|
+
process.env.PI_BGRUN_FOREIGN_JOBS = "1";
|
|
449
753
|
try {
|
|
450
754
|
// A log with no exit marker whose pid is alive (this test process's own pid).
|
|
451
755
|
const adoptedId = `kafka-bootstrap-${Date.now()}-${process.pid}`;
|
|
@@ -454,7 +758,8 @@ test("session_start: adopts running jobs from the jobs dir (other session's job)
|
|
|
454
758
|
const { pi, tools, ctx, fireSessionStart } = makeFakePi();
|
|
455
759
|
ctx.hasUI = true;
|
|
456
760
|
const widgetCalls: (string[] | undefined)[] = [];
|
|
457
|
-
ctx.ui.setWidget = (_ns: string, lines: string[] | undefined) =>
|
|
761
|
+
ctx.ui.setWidget = (_ns: string, lines: string[] | undefined) =>
|
|
762
|
+
widgetCalls.push(lines);
|
|
458
763
|
|
|
459
764
|
await loadExtension(pi);
|
|
460
765
|
await fireSessionStart();
|
|
@@ -469,10 +774,73 @@ test("session_start: adopts running jobs from the jobs dir (other session's job)
|
|
|
469
774
|
|
|
470
775
|
// bgstatus single-id should also see it as running (in-memory now).
|
|
471
776
|
const bgstatus = tools.get("bgstatus")!;
|
|
472
|
-
const res = await bgstatus.execute(
|
|
777
|
+
const res = await bgstatus.execute(
|
|
778
|
+
"call-a1",
|
|
779
|
+
{ id: adoptedId },
|
|
780
|
+
undefined,
|
|
781
|
+
undefined,
|
|
782
|
+
ctx,
|
|
783
|
+
);
|
|
473
784
|
assert.match(res.content[0].text as string, /: running/);
|
|
474
785
|
} finally {
|
|
475
786
|
delete process.env.PI_BGRUN_DIR;
|
|
787
|
+
delete process.env.PI_BGRUN_FOREIGN_JOBS;
|
|
788
|
+
rmSync(dir, { recursive: true, force: true });
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
test("adopted job leaves the widget once its log shows the exit marker (revalidation)", async () => {
|
|
793
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-bgrun-test-"));
|
|
794
|
+
process.env.PI_BGRUN_DIR = dir;
|
|
795
|
+
process.env.PI_BGRUN_FOREIGN_JOBS = "1";
|
|
796
|
+
try {
|
|
797
|
+
// Foreign running job (live pid = this process, no marker).
|
|
798
|
+
const adoptedId = `foreign-finish-${Date.now()}-${process.pid}`;
|
|
799
|
+
const logPath = join(dir, `${adoptedId}.log`);
|
|
800
|
+
writeFileSync(logPath, "job still going\n");
|
|
801
|
+
|
|
802
|
+
const { pi, tools, ctx, fireSessionStart } = makeFakePi();
|
|
803
|
+
ctx.hasUI = true;
|
|
804
|
+
const widgetCalls: (string[] | undefined)[] = [];
|
|
805
|
+
ctx.ui.setWidget = (_ns: string, lines: string[] | undefined) =>
|
|
806
|
+
widgetCalls.push(lines);
|
|
807
|
+
|
|
808
|
+
await loadExtension(pi);
|
|
809
|
+
await fireSessionStart();
|
|
810
|
+
assert.ok(
|
|
811
|
+
widgetCalls.some((l) => Array.isArray(l)),
|
|
812
|
+
"widget shown after adoption",
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
// The foreign job finishes: marker appears in the log.
|
|
816
|
+
writeFileSync(logPath, "job still going\n__BGRUN_EXIT__=0\n");
|
|
817
|
+
|
|
818
|
+
// Any bgstatus call revalidates adopted jobs and refreshes the widget.
|
|
819
|
+
const bgstatus = tools.get("bgstatus")!;
|
|
820
|
+
const list = await bgstatus.execute(
|
|
821
|
+
"call-a2",
|
|
822
|
+
{},
|
|
823
|
+
undefined,
|
|
824
|
+
undefined,
|
|
825
|
+
ctx,
|
|
826
|
+
);
|
|
827
|
+
const text = list.content[0].text as string;
|
|
828
|
+
assert.ok(
|
|
829
|
+
!/: running/.test(text),
|
|
830
|
+
"adopted job no longer listed as running",
|
|
831
|
+
);
|
|
832
|
+
assert.match(
|
|
833
|
+
text,
|
|
834
|
+
/\(1 more job log\(s\) on disk/,
|
|
835
|
+
"finished adopted job folded into the disk note",
|
|
836
|
+
);
|
|
837
|
+
assert.ok(
|
|
838
|
+
widgetCalls.some((l) => l === undefined),
|
|
839
|
+
"widget cleared after adopted job finished",
|
|
840
|
+
);
|
|
841
|
+
} finally {
|
|
842
|
+
delete process.env.PI_BGRUN_DIR;
|
|
843
|
+
delete process.env.PI_BGRUN_FOREIGN_JOBS;
|
|
476
844
|
rmSync(dir, { recursive: true, force: true });
|
|
477
845
|
}
|
|
478
846
|
});
|
|
@@ -480,9 +848,13 @@ test("session_start: adopts running jobs from the jobs dir (other session's job)
|
|
|
480
848
|
test("session_start: does NOT adopt finished or dead-pid jobs", async () => {
|
|
481
849
|
const dir = mkdtempSync(join(tmpdir(), "pi-bgrun-test-"));
|
|
482
850
|
process.env.PI_BGRUN_DIR = dir;
|
|
851
|
+
process.env.PI_BGRUN_FOREIGN_JOBS = "1";
|
|
483
852
|
try {
|
|
484
853
|
// Finished (exit marker present).
|
|
485
|
-
writeFileSync(
|
|
854
|
+
writeFileSync(
|
|
855
|
+
join(dir, `done-job-${Date.now()}-${process.pid}.log`),
|
|
856
|
+
"out\n__BGRUN_EXIT__=0\n",
|
|
857
|
+
);
|
|
486
858
|
// No marker but pid is certainly dead (pid 1 is launchd — alive, so use a likely-dead high pid).
|
|
487
859
|
// Use pid 1-style trick instead: a dead pid we spawn and reap.
|
|
488
860
|
const { spawnSync } = await import("node:child_process");
|
|
@@ -490,7 +862,10 @@ test("session_start: does NOT adopt finished or dead-pid jobs", async () => {
|
|
|
490
862
|
assert.equal(dead.status, 0);
|
|
491
863
|
// Write log with a pid that no longer exists: use the reaped child's pid if captured, else 999999.
|
|
492
864
|
const deadPid = dead.pid ?? 999999;
|
|
493
|
-
writeFileSync(
|
|
865
|
+
writeFileSync(
|
|
866
|
+
join(dir, `dead-job-${Date.now()}-${deadPid}.log`),
|
|
867
|
+
"partial\n",
|
|
868
|
+
);
|
|
494
869
|
|
|
495
870
|
const { pi, ctx, fireSessionStart } = makeFakePi();
|
|
496
871
|
ctx.hasUI = true;
|
|
@@ -502,6 +877,31 @@ test("session_start: does NOT adopt finished or dead-pid jobs", async () => {
|
|
|
502
877
|
await loadExtension(pi);
|
|
503
878
|
await fireSessionStart();
|
|
504
879
|
assert.equal(widgetShown, false, "no widget for finished/dead jobs");
|
|
880
|
+
} finally {
|
|
881
|
+
delete process.env.PI_BGRUN_DIR;
|
|
882
|
+
delete process.env.PI_BGRUN_FOREIGN_JOBS;
|
|
883
|
+
rmSync(dir, { recursive: true, force: true });
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
test("session_start: with foreign adoption OFF, finished foreign logs are not adopted", async () => {
|
|
888
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-bgrun-test-"));
|
|
889
|
+
process.env.PI_BGRUN_DIR = dir;
|
|
890
|
+
delete process.env.PI_BGRUN_FOREIGN_JOBS;
|
|
891
|
+
try {
|
|
892
|
+
writeFileSync(
|
|
893
|
+
join(dir, `done-job-${Date.now()}-${process.pid}.log`),
|
|
894
|
+
"out\n__BGRUN_EXIT__=0\n",
|
|
895
|
+
);
|
|
896
|
+
const { pi, ctx, fireSessionStart } = makeFakePi();
|
|
897
|
+
ctx.hasUI = true;
|
|
898
|
+
let widgetShown = false;
|
|
899
|
+
ctx.ui.setWidget = (_ns: string, lines: string[] | undefined) => {
|
|
900
|
+
if (lines) widgetShown = true;
|
|
901
|
+
};
|
|
902
|
+
await loadExtension(pi);
|
|
903
|
+
await fireSessionStart();
|
|
904
|
+
assert.equal(widgetShown, false, "no adoption when disabled");
|
|
505
905
|
} finally {
|
|
506
906
|
delete process.env.PI_BGRUN_DIR;
|
|
507
907
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -516,14 +916,24 @@ test("bgrun: job id encodes the CHILD's pid, not pi's own pid", async () => {
|
|
|
516
916
|
await loadExtension(pi);
|
|
517
917
|
const bgrun = tools.get("bgrun")!;
|
|
518
918
|
|
|
519
|
-
const res = await bgrun.execute(
|
|
919
|
+
const res = await bgrun.execute(
|
|
920
|
+
"call-pid",
|
|
921
|
+
{ command: "echo pidcheck" },
|
|
922
|
+
undefined,
|
|
923
|
+
undefined,
|
|
924
|
+
ctx,
|
|
925
|
+
);
|
|
520
926
|
const id = (res.content[0].text as string).match(/^started: ([^\n]+)/)![1];
|
|
521
927
|
const idPid = Number(id.split("-").pop());
|
|
522
928
|
assert.ok(idPid > 0, `id ends with child pid: ${id}`);
|
|
523
929
|
assert.notEqual(idPid, process.pid, "id must NOT carry pi's own pid");
|
|
524
930
|
// Log file named after the id, no .tmp- leftovers.
|
|
525
931
|
assert.ok(existsSync(join(dir, `${id}.log`)), "log at final id-named path");
|
|
526
|
-
assert.equal(
|
|
932
|
+
assert.equal(
|
|
933
|
+
readdirSync(dir).filter((f) => f.startsWith(".tmp-")).length,
|
|
934
|
+
0,
|
|
935
|
+
"no temp log leftovers",
|
|
936
|
+
);
|
|
527
937
|
|
|
528
938
|
await waitForWakes(wakes, 1);
|
|
529
939
|
} finally {
|
|
@@ -550,9 +960,18 @@ test("bgclean: removes a FINISHED job's old log even when its id-pid is alive",
|
|
|
550
960
|
await loadExtension(pi);
|
|
551
961
|
const bgclean = tools.get("bgclean")!;
|
|
552
962
|
|
|
553
|
-
const result = await bgclean.execute(
|
|
963
|
+
const result = await bgclean.execute(
|
|
964
|
+
"call-stale",
|
|
965
|
+
{ days: 7 },
|
|
966
|
+
undefined,
|
|
967
|
+
undefined,
|
|
968
|
+
ctx,
|
|
969
|
+
);
|
|
554
970
|
assert.match(result.content[0].text as string, /removed 1/);
|
|
555
|
-
assert.ok(
|
|
971
|
+
assert.ok(
|
|
972
|
+
!existsSync(oldPath),
|
|
973
|
+
"finished job's log removed despite live id-pid",
|
|
974
|
+
);
|
|
556
975
|
} finally {
|
|
557
976
|
delete process.env.PI_BGRUN_DIR;
|
|
558
977
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -562,9 +981,13 @@ test("bgclean: removes a FINISHED job's old log even when its id-pid is alive",
|
|
|
562
981
|
test("session_start adoption: skips finished jobs even with a live id-pid", async () => {
|
|
563
982
|
const dir = mkdtempSync(join(tmpdir(), "pi-bgrun-test-"));
|
|
564
983
|
process.env.PI_BGRUN_DIR = dir;
|
|
984
|
+
process.env.PI_BGRUN_FOREIGN_JOBS = "1";
|
|
565
985
|
try {
|
|
566
986
|
// Finished job (exit marker) whose id-pid is this process (alive).
|
|
567
|
-
writeFileSync(
|
|
987
|
+
writeFileSync(
|
|
988
|
+
join(dir, `done-job-${Date.now()}-${process.pid}.log`),
|
|
989
|
+
"out\n__BGRUN_EXIT__=0\n",
|
|
990
|
+
);
|
|
568
991
|
const { pi, ctx, fireSessionStart } = makeFakePi();
|
|
569
992
|
ctx.hasUI = true;
|
|
570
993
|
let widgetShown = false;
|
|
@@ -573,7 +996,68 @@ test("session_start adoption: skips finished jobs even with a live id-pid", asyn
|
|
|
573
996
|
};
|
|
574
997
|
await loadExtension(pi);
|
|
575
998
|
await fireSessionStart();
|
|
576
|
-
assert.equal(
|
|
999
|
+
assert.equal(
|
|
1000
|
+
widgetShown,
|
|
1001
|
+
false,
|
|
1002
|
+
"finished job not adopted even though id-pid is alive",
|
|
1003
|
+
);
|
|
1004
|
+
} finally {
|
|
1005
|
+
delete process.env.PI_BGRUN_DIR;
|
|
1006
|
+
delete process.env.PI_BGRUN_FOREIGN_JOBS;
|
|
1007
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
test("auto-clean: throttled via .last-clean marker; manual bgclean always runs", async () => {
|
|
1012
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-bgrun-test-"));
|
|
1013
|
+
process.env.PI_BGRUN_DIR = dir;
|
|
1014
|
+
delete process.env.PI_BGRUN_FOREIGN_JOBS;
|
|
1015
|
+
try {
|
|
1016
|
+
const fs = await import("node:fs");
|
|
1017
|
+
const backdate = (path: string) => {
|
|
1018
|
+
const oldTime = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
|
1019
|
+
fs.utimesSync(path, oldTime, oldTime);
|
|
1020
|
+
};
|
|
1021
|
+
|
|
1022
|
+
// Old log A + first session_start (no marker yet) → sweep runs, A removed.
|
|
1023
|
+
const logA = join(dir, "old-a-1000000000-99999.log");
|
|
1024
|
+
fs.writeFileSync(logA, "old a\n__BGRUN_EXIT__=0\n");
|
|
1025
|
+
backdate(logA);
|
|
1026
|
+
{
|
|
1027
|
+
const { pi, ctx, fireSessionStart } = makeFakePi();
|
|
1028
|
+
await loadExtension(pi);
|
|
1029
|
+
await fireSessionStart();
|
|
1030
|
+
}
|
|
1031
|
+
assert.ok(!fs.existsSync(logA), "first sweep removed old log A");
|
|
1032
|
+
assert.ok(
|
|
1033
|
+
fs.existsSync(join(dir, ".last-clean")),
|
|
1034
|
+
"throttle marker written",
|
|
1035
|
+
);
|
|
1036
|
+
|
|
1037
|
+
// Old log B + second session_start while marker is fresh → throttled, B kept.
|
|
1038
|
+
const logB = join(dir, "old-b-1000000000-99998.log");
|
|
1039
|
+
fs.writeFileSync(logB, "old b\n__BGRUN_EXIT__=0\n");
|
|
1040
|
+
backdate(logB);
|
|
1041
|
+
{
|
|
1042
|
+
const { pi, ctx, fireSessionStart } = makeFakePi();
|
|
1043
|
+
await loadExtension(pi);
|
|
1044
|
+
await fireSessionStart();
|
|
1045
|
+
}
|
|
1046
|
+
assert.ok(fs.existsSync(logB), "second sweep throttled — old log B kept");
|
|
1047
|
+
|
|
1048
|
+
// Manual bgclean ignores the throttle and removes B.
|
|
1049
|
+
const { pi: pi3, tools: tools3, ctx: ctx3 } = makeFakePi();
|
|
1050
|
+
await loadExtension(pi3);
|
|
1051
|
+
const bgclean = tools3.get("bgclean")!;
|
|
1052
|
+
const result = await bgclean.execute(
|
|
1053
|
+
"call-t1",
|
|
1054
|
+
{},
|
|
1055
|
+
undefined,
|
|
1056
|
+
undefined,
|
|
1057
|
+
ctx3,
|
|
1058
|
+
);
|
|
1059
|
+
assert.match(result.content[0].text as string, /removed 1/);
|
|
1060
|
+
assert.ok(!fs.existsSync(logB), "manual bgclean removed log B");
|
|
577
1061
|
} finally {
|
|
578
1062
|
delete process.env.PI_BGRUN_DIR;
|
|
579
1063
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -590,7 +1074,13 @@ test("bgclean: removes old logs, keeps recent ones", async () => {
|
|
|
590
1074
|
const bgclean = tools.get("bgclean")!;
|
|
591
1075
|
|
|
592
1076
|
// Run a real job (recent log — should be kept).
|
|
593
|
-
await bgrun.execute(
|
|
1077
|
+
await bgrun.execute(
|
|
1078
|
+
"call-c1",
|
|
1079
|
+
{ command: "echo recent" },
|
|
1080
|
+
undefined,
|
|
1081
|
+
undefined,
|
|
1082
|
+
ctx,
|
|
1083
|
+
);
|
|
594
1084
|
await new Promise((r) => setTimeout(r, 200)); // let it finish
|
|
595
1085
|
|
|
596
1086
|
// Write an old log file (backdated mtime).
|
|
@@ -600,12 +1090,20 @@ test("bgclean: removes old logs, keeps recent ones", async () => {
|
|
|
600
1090
|
const oldTime = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // 30 days ago
|
|
601
1091
|
fs.utimesSync(oldPath, oldTime, oldTime);
|
|
602
1092
|
|
|
603
|
-
const result = await bgclean.execute(
|
|
1093
|
+
const result = await bgclean.execute(
|
|
1094
|
+
"call-c2",
|
|
1095
|
+
{ days: 7 },
|
|
1096
|
+
undefined,
|
|
1097
|
+
undefined,
|
|
1098
|
+
ctx,
|
|
1099
|
+
);
|
|
604
1100
|
const text = result.content[0].text as string;
|
|
605
1101
|
assert.match(text, /removed 1/);
|
|
606
1102
|
assert.ok(!fs.existsSync(oldPath), "old log removed");
|
|
607
1103
|
// The recent log should still exist.
|
|
608
|
-
const remaining = fs
|
|
1104
|
+
const remaining = fs
|
|
1105
|
+
.readdirSync(dir)
|
|
1106
|
+
.filter((f: string) => f.endsWith(".log"));
|
|
609
1107
|
assert.equal(remaining.length, 1, "recent log kept");
|
|
610
1108
|
} finally {
|
|
611
1109
|
delete process.env.PI_BGRUN_DIR;
|
|
@@ -633,7 +1131,13 @@ test("bgclean: does not remove a running job's log", async () => {
|
|
|
633
1131
|
const bgclean = tools.get("bgclean")!;
|
|
634
1132
|
|
|
635
1133
|
// Start a long-running job (10s) so it's still running when we clean.
|
|
636
|
-
const res = await bgrun.execute(
|
|
1134
|
+
const res = await bgrun.execute(
|
|
1135
|
+
"call-c4",
|
|
1136
|
+
{ command: "sleep 10" },
|
|
1137
|
+
undefined,
|
|
1138
|
+
undefined,
|
|
1139
|
+
ctx,
|
|
1140
|
+
);
|
|
637
1141
|
const id = (res.content[0].text as string).match(/^started: ([^\n]+)/)![1];
|
|
638
1142
|
const logPath = join(dir, `${id}.log`);
|
|
639
1143
|
|
|
@@ -645,13 +1149,23 @@ test("bgclean: does not remove a running job's log", async () => {
|
|
|
645
1149
|
await new Promise((r) => setTimeout(r, 100));
|
|
646
1150
|
fs.utimesSync(logPath, oldTime, oldTime);
|
|
647
1151
|
|
|
648
|
-
const result = await bgclean.execute(
|
|
1152
|
+
const result = await bgclean.execute(
|
|
1153
|
+
"call-c5",
|
|
1154
|
+
{ days: 7 },
|
|
1155
|
+
undefined,
|
|
1156
|
+
undefined,
|
|
1157
|
+
ctx,
|
|
1158
|
+
);
|
|
649
1159
|
const text = result.content[0].text as string;
|
|
650
1160
|
assert.match(text, /skipped 1 running/);
|
|
651
1161
|
assert.ok(fs.existsSync(logPath), "running job's log not removed");
|
|
652
1162
|
|
|
653
|
-
// Kill the orphaned sleep so it doesn't linger.
|
|
654
|
-
try {
|
|
1163
|
+
// Kill the orphaned sleep so it doesn't linger (best-effort: it may have exited already).
|
|
1164
|
+
try {
|
|
1165
|
+
process.kill((res.details as any).pid);
|
|
1166
|
+
} catch {
|
|
1167
|
+
// already gone — fine
|
|
1168
|
+
}
|
|
655
1169
|
} finally {
|
|
656
1170
|
delete process.env.PI_BGRUN_DIR;
|
|
657
1171
|
rmSync(dir, { recursive: true, force: true });
|