|
| 1 | +import { test, vi, expect } from 'vitest'; |
| 2 | +import type { DeviceLease } from '@agent-device/contracts/device'; |
| 3 | +import { disposeLimrunLeaseOwner, type LimrunDisposalHost } from './lease-release.ts'; |
| 4 | +import type { LimrunCreateOutcome, LimrunLeaseOwner } from './lease-ownership.ts'; |
| 5 | + |
| 6 | +/* |
| 7 | + * Each wait is longer than the budgets it steps past: a disposal's create wait, and everything a disposal |
| 8 | + * left without a create answer may still wait for — one delete, the local teardown, one capture record. |
| 9 | + */ |
| 10 | +const CREATE_ANSWER_WAIT_MS = 130_000; |
| 11 | +const DISPOSAL_SETTLE_WAIT_MS = 110_000; |
| 12 | + |
| 13 | +/** |
| 14 | + * A create that answers after its disposal stopped waiting owes its end to the disposal already over. If that |
| 15 | + * disposal's delete is still with Limrun, the late end joins it: one instance is deleted by one request. |
| 16 | + */ |
| 17 | +test('a create that answers late joins the delete its disposal already started', async () => { |
| 18 | + // Loaded before the clock is replaced, so the disposal's own budget timers are fake timers. |
| 19 | + await import('./lifecycle-budget.ts'); |
| 20 | + let reportDelete!: () => void; |
| 21 | + const deleteHost = vi.fn<LimrunDisposalHost['deleteInstance']>(async () => { |
| 22 | + await new Promise<void>((resolve) => { |
| 23 | + reportDelete = resolve; |
| 24 | + }); |
| 25 | + return undefined; |
| 26 | + }); |
| 27 | + const host: LimrunDisposalHost = { |
| 28 | + deleteInstance: deleteHost, |
| 29 | + endCapture: async () => undefined, |
| 30 | + }; |
| 31 | + let reportCreate!: () => void; |
| 32 | + const creation = new Promise<LimrunCreateOutcome>((resolve) => { |
| 33 | + // A create that rented the instance and then failed, after its disposal had given up on it. |
| 34 | + reportCreate = () => resolve({ status: 'failed', error: new Error('instance boot failed') }); |
| 35 | + }); |
| 36 | + const owner = ownerWithRentedInstance(creation); |
| 37 | + |
| 38 | + vi.useFakeTimers(); |
| 39 | + try { |
| 40 | + const disposal = disposeLimrunLeaseOwner(host, owner); |
| 41 | + // The disposal stops waiting on the create and asks Limrun once to end the instance it did rent. |
| 42 | + await vi.advanceTimersByTimeAsync(CREATE_ANSWER_WAIT_MS); |
| 43 | + expect(deleteHost).toHaveBeenCalledOnce(); |
| 44 | + // Its own delete then runs out its deadline, and the disposal refuses its caller. |
| 45 | + await vi.advanceTimersByTimeAsync(DISPOSAL_SETTLE_WAIT_MS); |
| 46 | + await expect(disposal).resolves.toMatchObject({ cleanup: 'uncertain' }); |
| 47 | + |
| 48 | + // The create answers now. The end it owes is this disposal's, and it joins the request Limrun already |
| 49 | + // has for this instance rather than sending a second one. |
| 50 | + reportCreate(); |
| 51 | + await vi.advanceTimersByTimeAsync(DISPOSAL_SETTLE_WAIT_MS); |
| 52 | + expect(deleteHost).toHaveBeenCalledOnce(); |
| 53 | + |
| 54 | + // The one request confirming is enough: the joined end records the instance as deleted. |
| 55 | + reportDelete(); |
| 56 | + await vi.advanceTimersByTimeAsync(DISPOSAL_SETTLE_WAIT_MS); |
| 57 | + } finally { |
| 58 | + vi.useRealTimers(); |
| 59 | + } |
| 60 | + expect(owner.deletedInstanceId).toBe('android-instance-1'); |
| 61 | +}); |
| 62 | + |
| 63 | +/** An owner whose create has rented an instance but has not answered, taken by a disposal. */ |
| 64 | +function ownerWithRentedInstance(creation: Promise<LimrunCreateOutcome>): LimrunLeaseOwner { |
| 65 | + const lease = { |
| 66 | + leaseId: 'lease-release-join', |
| 67 | + tenantId: 'team-a', |
| 68 | + runId: 'run-a', |
| 69 | + backend: 'android-instance', |
| 70 | + leaseProvider: 'limrun', |
| 71 | + createdAt: 1, |
| 72 | + heartbeatAt: 1, |
| 73 | + expiresAt: 60_001, |
| 74 | + } satisfies DeviceLease; |
| 75 | + return { |
| 76 | + lease, |
| 77 | + platform: 'android', |
| 78 | + creation, |
| 79 | + state: { phase: 'releasing', by: 'release' }, |
| 80 | + rentedInstanceId: 'android-instance-1', |
| 81 | + session: undefined, |
| 82 | + localRelease: undefined, |
| 83 | + keepAlive: undefined, |
| 84 | + capture: undefined, |
| 85 | + appAlias: undefined, |
| 86 | + deletedInstanceId: undefined, |
| 87 | + pendingDelete: undefined, |
| 88 | + } as unknown as LimrunLeaseOwner; |
| 89 | +} |
0 commit comments