|
8 | 8 | import { beforeEach, describe, expect, it, vi } from 'vitest' |
9 | 9 | import { CodeLanguage } from '@/lib/execution/languages' |
10 | 10 |
|
11 | | -const { mockSelect, mockUpdate, mockProviderStrategy, mockEnsureSandboxImage } = vi.hoisted(() => ({ |
12 | | - mockSelect: vi.fn(), |
13 | | - mockUpdate: vi.fn(), |
14 | | - mockProviderStrategy: { current: 'prebuilt' as 'prebuilt' | 'runtime' }, |
15 | | - mockEnsureSandboxImage: vi.fn(), |
16 | | -})) |
| 11 | +const { mockSelect, mockUpdate, mockProviderStrategy, mockEnsureSandboxImage, mockIsMissingImage } = |
| 12 | + vi.hoisted(() => ({ |
| 13 | + mockSelect: vi.fn(), |
| 14 | + mockUpdate: vi.fn(), |
| 15 | + mockProviderStrategy: { current: 'prebuilt' as 'prebuilt' | 'runtime' }, |
| 16 | + mockEnsureSandboxImage: vi.fn(), |
| 17 | + mockIsMissingImage: vi.fn(), |
| 18 | + })) |
17 | 19 |
|
18 | 20 | vi.mock('@/lib/execution/remote-sandbox/image-registry', () => ({ |
19 | 21 | ensureSandboxImage: mockEnsureSandboxImage, |
@@ -57,12 +59,18 @@ vi.mock('@/lib/execution/remote-sandbox/provider', () => ({ |
57 | 59 | get dependencyStrategy() { |
58 | 60 | return mockProviderStrategy.current |
59 | 61 | }, |
| 62 | + get images() { |
| 63 | + return mockProviderStrategy.current === 'prebuilt' |
| 64 | + ? { isMissingImage: mockIsMissingImage } |
| 65 | + : undefined |
| 66 | + }, |
60 | 67 | }), |
61 | 68 | })) |
62 | 69 |
|
63 | 70 | import { |
64 | 71 | invalidateSandboxResolution, |
65 | 72 | provisionRuntimeDependencies, |
| 73 | + repairMissingSandboxImage, |
66 | 74 | resolveWorkspaceSandbox, |
67 | 75 | } from '@/lib/execution/remote-sandbox/resolve' |
68 | 76 |
|
@@ -368,3 +376,58 @@ describe('provisionRuntimeDependencies', () => { |
368 | 376 | expect(options.timeoutMs).toBeGreaterThan(0) |
369 | 377 | }) |
370 | 378 | }) |
| 379 | + |
| 380 | +/** |
| 381 | + * The registry and the provider template are two systems with no shared |
| 382 | + * transaction, so every attempt to keep them in step leaves some window. Create is |
| 383 | + * the one step that observes the truth, which is why the repair hangs off it. |
| 384 | + */ |
| 385 | +describe('repairMissingSandboxImage', () => { |
| 386 | + const SELECTED = { |
| 387 | + id: 'sbx-1', |
| 388 | + name: 'bigquery-etl', |
| 389 | + language: CodeLanguage.Python, |
| 390 | + dependencies: ['pandas'], |
| 391 | + specHash: 'hash-1', |
| 392 | + strategy: 'prebuilt' as const, |
| 393 | + imageRef: 'sim-sbx-abc', |
| 394 | + } |
| 395 | + |
| 396 | + it('rebuilds without a cooldown, because create observed the image is gone', async () => { |
| 397 | + mockIsMissingImage.mockResolvedValue(true) |
| 398 | + |
| 399 | + const message = await repairMissingSandboxImage(SELECTED, new Error('404')) |
| 400 | + |
| 401 | + expect(message).toMatch(/being rebuilt/) |
| 402 | + expect(mockEnsureSandboxImage).toHaveBeenCalledWith( |
| 403 | + { language: 'python', dependencies: ['pandas'] }, |
| 404 | + 'hash-1', |
| 405 | + { imageKnownGone: true } |
| 406 | + ) |
| 407 | + }) |
| 408 | + |
| 409 | + /** |
| 410 | + * The classifier has to stay narrow: treating an auth or rate-limit failure as a |
| 411 | + * missing image would rebuild every sandbox on a provider outage. |
| 412 | + */ |
| 413 | + it('leaves any other provider failure alone', async () => { |
| 414 | + mockIsMissingImage.mockResolvedValue(false) |
| 415 | + |
| 416 | + const message = await repairMissingSandboxImage(SELECTED, new Error('rate limited')) |
| 417 | + |
| 418 | + expect(message).toBeNull() |
| 419 | + expect(mockEnsureSandboxImage).not.toHaveBeenCalled() |
| 420 | + }) |
| 421 | + |
| 422 | + it('does nothing for a runtime-strategy sandbox, which has no image to miss', async () => { |
| 423 | + mockIsMissingImage.mockResolvedValue(true) |
| 424 | + |
| 425 | + const message = await repairMissingSandboxImage( |
| 426 | + { ...SELECTED, strategy: 'runtime', imageRef: undefined }, |
| 427 | + new Error('404') |
| 428 | + ) |
| 429 | + |
| 430 | + expect(message).toBeNull() |
| 431 | + expect(mockEnsureSandboxImage).not.toHaveBeenCalled() |
| 432 | + }) |
| 433 | +}) |
0 commit comments