Skip to content

Commit bf4ee4f

Browse files
committed
test(webapp): assert the cached idempotency loser resolves to the recreated run
Strengthen the reacquire cases (expired + failed winner) to assert the cached loser's run.friendlyId is the RECREATED run (the other loser's fresh child), not the cleared winner — so a stale isCached pointing at the cleared winner can't slip past the count check. (CodeRabbit review.)
1 parent cc1d8af commit bf4ee4f

1 file changed

Lines changed: 66 additions & 48 deletions

File tree

apps/webapp/test/idempotencyGlobalScopeCrossDbConcurrent.test.ts

Lines changed: 66 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -792,32 +792,33 @@ describe("run-ops split — CONCURRENT global-scope idempotency dedup across two
792792
const taskIdentifier = "child-task";
793793
const concern = new IdempotencyKeyConcern(prisma14 as never, {} as never, {} as never);
794794

795-
const { bRes, cRes } = await driveWinnerPlusTwoLosers({
796-
concern,
797-
router,
798-
buffer: bufferHarness,
799-
env,
800-
idempotencyKey,
801-
taskIdentifier,
802-
// Winner child is a genuinely-expired run: an EXPIRED status AND an already-expired key, so
803-
// handleExistingRun's expiry branch clears it. Both attributes matter for determinism: the
804-
// expiry drives the clear; the EXPIRED status keeps the SECOND loser on the clear path even
805-
// after the first loser's clear has NULLed the key + expiry (a plain PENDING winner would
806-
// then look "live" to the second reader and dedup by accident, masking the duplicate).
807-
winnerSeed: { status: "EXPIRED", idempotencyKeyExpiresAt: new Date(Date.now() - 60_000) },
808-
a: {
809-
spec: { parentResidency: "NEW", idempotencyKey, scope: "global", taskIdentifier },
810-
childResidency: "NEW",
811-
},
812-
b: {
813-
spec: { parentResidency: "LEGACY", idempotencyKey, scope: "global", taskIdentifier },
814-
childResidency: "LEGACY",
815-
},
816-
c: {
817-
spec: { parentResidency: "NEW", idempotencyKey, scope: "global", taskIdentifier },
818-
childResidency: "NEW",
819-
},
820-
});
795+
const { bRes, cRes, childWFriendly, childBFriendly, childCFriendly } =
796+
await driveWinnerPlusTwoLosers({
797+
concern,
798+
router,
799+
buffer: bufferHarness,
800+
env,
801+
idempotencyKey,
802+
taskIdentifier,
803+
// Winner child is a genuinely-expired run: an EXPIRED status AND an already-expired key, so
804+
// handleExistingRun's expiry branch clears it. Both attributes matter for determinism: the
805+
// expiry drives the clear; the EXPIRED status keeps the SECOND loser on the clear path even
806+
// after the first loser's clear has NULLed the key + expiry (a plain PENDING winner would
807+
// then look "live" to the second reader and dedup by accident, masking the duplicate).
808+
winnerSeed: { status: "EXPIRED", idempotencyKeyExpiresAt: new Date(Date.now() - 60_000) },
809+
a: {
810+
spec: { parentResidency: "NEW", idempotencyKey, scope: "global", taskIdentifier },
811+
childResidency: "NEW",
812+
},
813+
b: {
814+
spec: { parentResidency: "LEGACY", idempotencyKey, scope: "global", taskIdentifier },
815+
childResidency: "LEGACY",
816+
},
817+
c: {
818+
spec: { parentResidency: "NEW", idempotencyKey, scope: "global", taskIdentifier },
819+
childResidency: "NEW",
820+
},
821+
});
821822

822823
// The winner's key was cleared, so only the recreated child(ren) still carry the key. Exactly
823824
// ONE new child ⇒ the reacquire serialised the two losers' recreate across the split.
@@ -826,6 +827,14 @@ describe("run-ops split — CONCURRENT global-scope idempotency dedup across two
826827
// Exactly one loser recreated (claim reacquired); the other resolved to it as a cached hit.
827828
const cachedCount = [bRes, cRes].filter((r) => r.isCached).length;
828829
expect(cachedCount).toBe(1);
830+
// The cached loser must resolve to the RECREATED run (the other loser's fresh child), NOT the
831+
// cleared winner — a stale isCached still pointing at childWFriendly would otherwise slip past.
832+
const recreaterFriendly = bRes.isCached ? childCFriendly : childBFriendly;
833+
const cachedLoser = bRes.isCached ? bRes : cRes;
834+
if (cachedLoser.isCached === true) {
835+
expect(cachedLoser.run.friendlyId).toBe(recreaterFriendly);
836+
expect(cachedLoser.run.friendlyId).not.toBe(childWFriendly);
837+
}
829838
// Both losers genuinely contended on the claim (winner + two losers ⇒ ≥3 attempts).
830839
expect(bufferHarness.claimCalls).toBeGreaterThanOrEqual(3);
831840
}
@@ -847,33 +856,42 @@ describe("run-ops split — CONCURRENT global-scope idempotency dedup across two
847856
const taskIdentifier = "child-task";
848857
const concern = new IdempotencyKeyConcern(prisma14 as never, {} as never, {} as never);
849858

850-
const { bRes, cRes } = await driveWinnerPlusTwoLosers({
851-
concern,
852-
router,
853-
buffer: bufferHarness,
854-
env,
855-
idempotencyKey,
856-
taskIdentifier,
857-
// Winner child carries a LIVE (future) key but a failed status → status-clear branch fires.
858-
winnerSeed: { status: "COMPLETED_WITH_ERRORS" },
859-
a: {
860-
spec: { parentResidency: "NEW", idempotencyKey, scope: "global", taskIdentifier },
861-
childResidency: "NEW",
862-
},
863-
b: {
864-
spec: { parentResidency: "LEGACY", idempotencyKey, scope: "global", taskIdentifier },
865-
childResidency: "LEGACY",
866-
},
867-
c: {
868-
spec: { parentResidency: "NEW", idempotencyKey, scope: "global", taskIdentifier },
869-
childResidency: "NEW",
870-
},
871-
});
859+
const { bRes, cRes, childWFriendly, childBFriendly, childCFriendly } =
860+
await driveWinnerPlusTwoLosers({
861+
concern,
862+
router,
863+
buffer: bufferHarness,
864+
env,
865+
idempotencyKey,
866+
taskIdentifier,
867+
// Winner child carries a LIVE (future) key but a failed status → status-clear branch fires.
868+
winnerSeed: { status: "COMPLETED_WITH_ERRORS" },
869+
a: {
870+
spec: { parentResidency: "NEW", idempotencyKey, scope: "global", taskIdentifier },
871+
childResidency: "NEW",
872+
},
873+
b: {
874+
spec: { parentResidency: "LEGACY", idempotencyKey, scope: "global", taskIdentifier },
875+
childResidency: "LEGACY",
876+
},
877+
c: {
878+
spec: { parentResidency: "NEW", idempotencyKey, scope: "global", taskIdentifier },
879+
childResidency: "NEW",
880+
},
881+
});
872882

873883
const counts = await countChildren(prisma14, prisma17, env, idempotencyKey, taskIdentifier);
874884
expect(counts.total).toBe(1);
875885
const cachedCount = [bRes, cRes].filter((r) => r.isCached).length;
876886
expect(cachedCount).toBe(1);
887+
// The cached loser must resolve to the RECREATED run (the other loser's fresh child), NOT the
888+
// cleared winner — a stale isCached still pointing at childWFriendly would otherwise slip past.
889+
const recreaterFriendly = bRes.isCached ? childCFriendly : childBFriendly;
890+
const cachedLoser = bRes.isCached ? bRes : cRes;
891+
if (cachedLoser.isCached === true) {
892+
expect(cachedLoser.run.friendlyId).toBe(recreaterFriendly);
893+
expect(cachedLoser.run.friendlyId).not.toBe(childWFriendly);
894+
}
877895
expect(bufferHarness.claimCalls).toBeGreaterThanOrEqual(3);
878896
}
879897
);

0 commit comments

Comments
 (0)