Skip to content

Commit 37c4727

Browse files
committed
test(cli): the jobs pin reads an uninstall, a reinstall that drops a job, and a control package (measures the pre-fix reach)
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
1 parent c866c5a commit 37c4727

1 file changed

Lines changed: 126 additions & 5 deletions

File tree

‎packages/cli/test/package-install-local-jobs.integration.test.ts‎

Lines changed: 126 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,21 @@ const BODY_APP_ID = 'com.example.jobsapp';
7575
const BODY_TICK = 'jobs_app_tick';
7676
const BODY_JOB = 'jobs_app_tick_body';
7777

78+
/** Another installed package — the control a package's uninstall or reinstall must leave running. */
79+
const OTHER_APP_ID = 'com.example.otherjobs';
80+
const OTHER_TICK = 'other_jobs_tick';
81+
const OTHER_JOB = 'other_jobs_tick_body';
82+
83+
/** A package reinstalled with a version that DROPS one of its two jobs. */
84+
const DROP_APP_ID = 'com.example.dropjobs';
85+
const DROP_TICK = 'drop_jobs_tick';
86+
const DROP_KEPT = 'drop_jobs_kept';
87+
const DROP_GONE = 'drop_jobs_gone';
88+
89+
/** "Writes no further row": let an in-flight run land, take the floor, then read again this much later. */
90+
const SETTLE_MS = 1_500;
91+
const QUIET_WAIT_MS = 4_000;
92+
7893
const HANDLER_APP_ID = 'com.example.handlerjobs';
7994
const HANDLER_TICK = 'handler_jobs_tick';
8095
const HANDLER_JOB = 'handler_jobs_tick_handler';
@@ -115,6 +130,21 @@ const BODY_ARTIFACT = {
115130
jobs: [bodyJob(BODY_JOB, BODY_TICK)],
116131
};
117132

133+
const OTHER_ARTIFACT = {
134+
manifest: { id: OTHER_APP_ID, namespace: 'other_jobs', version: '0.1.0', type: 'app', name: 'Other Jobs' },
135+
objects: [tickObject(OTHER_TICK)],
136+
jobs: [bodyJob(OTHER_JOB, OTHER_TICK)],
137+
};
138+
139+
/** One version of the drop package, declaring `jobs` (all body jobs into one object). */
140+
function dropArtifact(version: string, jobs: string[]) {
141+
return {
142+
manifest: { id: DROP_APP_ID, namespace: 'drop_jobs', version, type: 'app', name: 'Drop Jobs' },
143+
objects: [tickObject(DROP_TICK)],
144+
jobs: jobs.map((name) => bodyJob(name, DROP_TICK)),
145+
};
146+
}
147+
118148
/** The handler-only package: its one enabled job names a function no JSON door carries. */
119149
const HANDLER_ARTIFACT = {
120150
manifest: { id: HANDLER_APP_ID, namespace: 'handler_jobs', version: '0.1.0', type: 'app', name: 'Handler Jobs' },
@@ -304,13 +334,41 @@ async function awaitRuns(live: LiveStart, token: string, object: string, job: st
304334
return { answer, floor };
305335
}
306336

337+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
338+
339+
/**
340+
* The reading for an act that must STOP `job`: let a run already in flight
341+
* land, take the row count as the floor, wait, and count again. A job that
342+
* was stopped leaves `after === floor`; one still scheduled (every second)
343+
* adds rows in between.
344+
*/
345+
async function quietAfter(live: LiveStart, token: string, object: string, job: string): Promise<{ floor: number; after: number }> {
346+
await sleep(SETTLE_MS);
347+
const floor = rowsOf(await jobRows(live, token, object, job)).length;
348+
await sleep(QUIET_WAIT_MS);
349+
const after = rowsOf(await jobRows(live, token, object, job)).length;
350+
return { floor, after };
351+
}
352+
307353
interface InstallRun { exit: number | null; output: string }
308354
const readings: {
309355
bodyInstall?: InstallRun;
310356
handlerInstall?: InstallRun;
311357
installed?: Answer;
312358
afterInstall?: { answer: Answer; floor: number };
313359
afterRestart?: { answer: Answer; floor: number };
360+
otherInstall?: InstallRun;
361+
dropInstall?: InstallRun;
362+
dropReinstall?: InstallRun;
363+
dropBefore?: { answer: Answer; floor: number };
364+
dropGoneHot?: { floor: number; after: number };
365+
dropKeptHot?: { answer: Answer; floor: number };
366+
uninstall?: Answer;
367+
bodyGoneHot?: { floor: number; after: number };
368+
otherHot?: { answer: Answer; floor: number };
369+
bodyGoneRestart?: { floor: number; after: number };
370+
dropGoneRestart?: { floor: number; after: number };
371+
dropKeptRestart?: { answer: Answer; floor: number };
314372
controlBody?: { answer: Answer; floor: number };
315373
controlHandler?: { answer: Answer; floor: number };
316374
output: Record<string, string>;
@@ -329,6 +387,12 @@ beforeAll(async () => {
329387
write(bodyApp, BODY_ARTIFACT);
330388
write(handlerApp, HANDLER_ARTIFACT);
331389
write(controlApp, CONTROL_ARTIFACT);
390+
const otherApp = join(root, 'other-app');
391+
const dropAppV1 = join(root, 'drop-app-v1');
392+
const dropAppV2 = join(root, 'drop-app-v2');
393+
write(otherApp, OTHER_ARTIFACT);
394+
write(dropAppV1, dropArtifact('0.1.0', [DROP_KEPT, DROP_GONE]));
395+
write(dropAppV2, dropArtifact('0.2.0', [DROP_KEPT]));
332396
writeFileSync(join(controlApp, 'dist', 'runtime.mjs'), CONTROL_RUNTIME_MODULE, 'utf8');
333397

334398
// The runtime boots the HOST artifact — never a package — so the packages
@@ -344,18 +408,40 @@ beforeAll(async () => {
344408
const first = await bootStart(runtimeDir, home, port, ['--artifact', hostArtifact]);
345409
const token = await authenticate(first);
346410
readings.bodyInstall = await packageInstall(bodyApp, first);
411+
readings.otherInstall = await packageInstall(otherApp, first);
412+
readings.dropInstall = await packageInstall(dropAppV1, first);
347413
readings.handlerInstall = await packageInstall(handlerApp, first);
348414
readings.installed = await http(first, 'GET', '/api/v1/marketplace/install-local', token);
349415
readings.afterInstall = await awaitRuns(first, token, BODY_TICK, BODY_JOB, 0);
416+
417+
// Reinstall the drop package with a version that no longer declares DROP_GONE.
418+
readings.dropBefore = await awaitRuns(first, token, DROP_TICK, DROP_GONE, 0);
419+
readings.dropReinstall = await packageInstall(dropAppV2, first);
420+
readings.dropGoneHot = await quietAfter(first, token, DROP_TICK, DROP_GONE);
421+
readings.dropKeptHot = await awaitRuns(first, token, DROP_TICK, DROP_KEPT,
422+
rowsOf(await jobRows(first, token, DROP_TICK, DROP_KEPT)).length);
423+
424+
// Uninstall the body package; the other package is the control.
425+
readings.uninstall = await http(first, 'DELETE', `/api/v1/marketplace/install-local/${BODY_APP_ID}`, token);
426+
readings.bodyGoneHot = await quietAfter(first, token, BODY_TICK, BODY_JOB);
427+
readings.otherHot = await awaitRuns(first, token, OTHER_TICK, OTHER_JOB,
428+
rowsOf(await jobRows(first, token, OTHER_TICK, OTHER_JOB)).length);
350429
readings.output.install = first.output();
351430
await stopGroup(first.child);
352431

353432
// ── boot 2: same host, home and cwd — the ledger rehydrates on kernel:ready ──
354433
const second = await bootStart(runtimeDir, home, port, ['--artifact', hostArtifact]);
355434
const token2 = await authenticate(second);
356-
// The rows boot 1 left behind are the floor: only a run in THIS boot lifts it.
357-
const floor = rowsOf(await jobRows(second, token2, BODY_TICK, BODY_JOB)).length;
358-
readings.afterRestart = await awaitRuns(second, token2, BODY_TICK, BODY_JOB, floor);
435+
// The rows boot 1 left behind are the floors: only a run in THIS boot lifts one.
436+
const floorOf = async (object: string, job: string) => rowsOf(await jobRows(second, token2, object, job)).length;
437+
const bodyFloor = await floorOf(BODY_TICK, BODY_JOB);
438+
const goneFloor = await floorOf(DROP_TICK, DROP_GONE);
439+
const restartedAt = Date.now();
440+
readings.afterRestart = await awaitRuns(second, token2, OTHER_TICK, OTHER_JOB, await floorOf(OTHER_TICK, OTHER_JOB));
441+
readings.dropKeptRestart = await awaitRuns(second, token2, DROP_TICK, DROP_KEPT, await floorOf(DROP_TICK, DROP_KEPT));
442+
await sleep(Math.max(0, QUIET_WAIT_MS - (Date.now() - restartedAt)));
443+
readings.bodyGoneRestart = { floor: bodyFloor, after: await floorOf(BODY_TICK, BODY_JOB) };
444+
readings.dropGoneRestart = { floor: goneFloor, after: await floorOf(DROP_TICK, DROP_GONE) };
359445
readings.output.restart = second.output();
360446
await stopGroup(second.child);
361447

@@ -367,7 +453,7 @@ beforeAll(async () => {
367453
readings.controlBody = await awaitRuns(third, token3, BODY_TICK, BODY_JOB, 0);
368454
readings.output.control = third.output();
369455
await stopGroup(third.child);
370-
}, 3 * BOOT_TIMEOUT_MS + 6 * RUN_WAIT_MS);
456+
}, 3 * BOOT_TIMEOUT_MS + 12 * RUN_WAIT_MS);
371457

372458
afterAll(async () => {
373459
for (const child of groups) await stopGroup(child);
@@ -389,12 +475,47 @@ describe('#21489: install-local runs job bodies and refuses handler-only jobs',
389475
expect(rowsOf(answer).length, `the installed body job never ran${transcript('install')}`).toBeGreaterThan(0);
390476
});
391477

392-
it('after restart, the rehydrated body job runs again', () => {
478+
it("after restart, a rehydrated package's body job runs again", () => {
479+
expect(readings.otherInstall!.exit, readings.otherInstall!.output).toBe(0);
393480
const { answer, floor } = readings.afterRestart!;
394481
expect(answer.status, JSON.stringify(answer.body)).toBe(200);
395482
expect(rowsOf(answer).length, `no run after the restart${transcript('restart')}`).toBeGreaterThan(floor);
396483
});
397484

485+
it("uninstall: the DELETE answers 200, and the uninstalled package's body job writes no further row — hot", () => {
486+
expect(readings.uninstall!.status, JSON.stringify(readings.uninstall!.body)).toBe(200);
487+
const { floor, after } = readings.bodyGoneHot!;
488+
expect(floor, 'precondition: the job had run before the uninstall').toBeGreaterThan(0);
489+
expect(after, `the uninstalled package's job kept running${transcript('install')}`).toBe(floor);
490+
});
491+
492+
it('… and none after a restart', () => {
493+
const { floor, after } = readings.bodyGoneRestart!;
494+
expect(after, `the uninstalled package's job ran after the restart${transcript('restart')}`).toBe(floor);
495+
});
496+
497+
it("control: another package's job keeps running across that uninstall", () => {
498+
const { answer, floor } = readings.otherHot!;
499+
expect(rowsOf(answer).length, `the control package's job stopped${transcript('install')}`).toBeGreaterThan(floor);
500+
});
501+
502+
it('reinstall: a job the new version DROPPED writes no further row — hot, and none after a restart', () => {
503+
expect(readings.dropInstall!.exit, readings.dropInstall!.output).toBe(0);
504+
expect(readings.dropReinstall!.exit, readings.dropReinstall!.output).toBe(0);
505+
expect(rowsOf(readings.dropBefore!.answer).length, 'precondition: the dropped job had run').toBeGreaterThan(0);
506+
const hot = readings.dropGoneHot!;
507+
expect(hot.after, `the dropped job kept running after the reinstall${transcript('install')}`).toBe(hot.floor);
508+
const restart = readings.dropGoneRestart!;
509+
expect(restart.after, `the dropped job ran after the restart${transcript('restart')}`).toBe(restart.floor);
510+
});
511+
512+
it('… while the job the new version KEPT keeps running, hot and after a restart', () => {
513+
const hot = readings.dropKeptHot!;
514+
expect(rowsOf(hot.answer).length, `the kept job stopped${transcript('install')}`).toBeGreaterThan(hot.floor);
515+
const restart = readings.dropKeptRestart!;
516+
expect(rowsOf(restart.answer).length, `the kept job did not run after the restart${transcript('restart')}`).toBeGreaterThan(restart.floor);
517+
});
518+
398519
it('the handler-only package is REFUSED, with its code and remedy, and nothing of it is installed', () => {
399520
const run = readings.handlerInstall!;
400521
expect(run.exit, run.output).toBe(1);

0 commit comments

Comments
 (0)