From c0594c518df2774e3a9f9f8f1e24b82f3e337918 Mon Sep 17 00:00:00 2001 From: wangdl Date: Thu, 18 Jun 2026 11:41:35 +0800 Subject: [PATCH] test: add concurrent resolveSnapshot race test (API-AI-R01) - Simulates two concurrent getSnapshot calls with no valid snapshot - Documents the known race: both trigger buildSnapshot, second update overwrites first - Verifies both calls complete without error (accepted-risk behavior) Co-Authored-By: Claude Opus 4.7 --- .../internal/runtime-internal.service.spec.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/modules/ai-runtime/internal/runtime-internal.service.spec.ts b/src/modules/ai-runtime/internal/runtime-internal.service.spec.ts index f69ac02..bb44534 100644 --- a/src/modules/ai-runtime/internal/runtime-internal.service.spec.ts +++ b/src/modules/ai-runtime/internal/runtime-internal.service.spec.ts @@ -329,6 +329,38 @@ describe('RuntimeInternalService', () => { }); }); + // ═══════════════════════════════════════════════════════════════════ + // resolveSnapshot: concurrent race (API-AI-R01) + // ═══════════════════════════════════════════════════════════════════ + + describe('resolveSnapshot concurrent race', () => { + it('handles concurrent getSnapshot without error (known race, accepted risk)', async () => { + // Simulate: two calls with no valid snapshot → both trigger buildSnapshot + const job = { id: 'j1', userId: 'u1', targetType: 'material', targetId: 'm1', snapshotId: null }; + mockAiRuntimeJob.findUnique.mockResolvedValue(job); + + // Each buildSnapshot call returns a new id (simulating two builds) + mockSnapshotBuilder.buildSnapshot + .mockResolvedValueOnce({ ...mockSnapshot, id: 'snap-A' }) + .mockResolvedValueOnce({ ...mockSnapshot, id: 'snap-B' }); + // First update writes snap-A, second overwrites with snap-B (the race) + mockAiRuntimeJob.update.mockResolvedValue({}); + + const [r1, r2] = await Promise.all([ + service.getSnapshot('j1'), + service.getSnapshot('j1'), + ]); + + // Both calls succeed; one snapshot may be orphaned (documented risk) + expect(r1.snapshotId).toBeDefined(); + expect(r2.snapshotId).toBeDefined(); + // Two buildSnapshot calls issued (the race) + expect(mockSnapshotBuilder.buildSnapshot).toHaveBeenCalledTimes(2); + // Job updated twice (second overwrites first — orphan snap-A) + expect(mockAiRuntimeJob.update).toHaveBeenCalledTimes(2); + }); + }); + // ═══════════════════════════════════════════════════════════════════ // resolveCredential // ═══════════════════════════════════════════════════════════════════