mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
test(server): plan 4 — concurrency pins for boundaries-upsert and token-mint races (3d carry)
Task 8b-2 item 2: R7's two remaining un-transacted-statement races flagged by the 3d Task 7 review (L4) but never pinned — RoadtripDayBoundariesService .save's belongs-check-then-upsert, and FeedsService.generateTripToken's read-then-write. Both tests pin today's actual outcome (an atomic ON CONFLICT upsert survives cleanly; the token mint has a last-write-wins loser) with the same concurrent-request-shape Promise.all/allSettled pattern roadtrip.service.test.ts's existing R7 vias pin uses. Not a fix.
This commit is contained in:
@@ -103,6 +103,31 @@ it('REST changes broadcast the complete state and exclude the saving socket', as
|
||||
expect(await controller.remove(String(tripA.id), 1)).toEqual({ boundaries: [] });
|
||||
});
|
||||
|
||||
// R7 (task-7-review.md L4 / 3d ledger's carry): `save`'s ownership check
|
||||
// (RB2 `belongs`) and its upsert (RB3) are two un-transacted statements, the
|
||||
// same class of race as `roadtrip.service.test.ts`'s R7 vias pin. Both
|
||||
// concurrent callers pass their own `belongs` check independently (neither
|
||||
// references the other's write), then race on `upsertBoundary`'s single
|
||||
// `ON CONFLICT (trip, day_number) DO UPDATE` — that statement is atomic, so
|
||||
// the row can never end up a hybrid of the two payloads; exactly one caller's
|
||||
// fraction survives. Pinning today's actual outcome, not a fix.
|
||||
it('R7: two concurrent saves for the same day_number both succeed; the upsert leaves one caller\'s row intact, never a hybrid (unserialized belongs-check-then-upsert)', async () => {
|
||||
const { service, tripA, fromId, toId } = setup();
|
||||
const boundaryA = { day_number: 1, from_assignment_id: fromId, to_assignment_id: toId, fraction: 0.25 };
|
||||
const boundaryB = { day_number: 1, from_assignment_id: fromId, to_assignment_id: toId, fraction: 0.75 };
|
||||
|
||||
const results = await Promise.allSettled([
|
||||
service.save(tripA.id, boundaryA),
|
||||
service.save(tripA.id, boundaryB),
|
||||
]);
|
||||
|
||||
expect(results.every((r) => r.status === 'fulfilled')).toBe(true);
|
||||
const rows = await service.list(tripA.id);
|
||||
// No duplicate row from the race, and no hybrid of the two fractions.
|
||||
expect(rows).toHaveLength(1);
|
||||
expect([0.25, 0.75]).toContain(rows[0].fraction);
|
||||
});
|
||||
|
||||
it('MCP checks demo, trip access and edit permission before saving', async () => {
|
||||
const s = setup();
|
||||
const ctx = { userId: 1 } as McpContext;
|
||||
|
||||
@@ -220,6 +220,35 @@ describe('trip feed token lifecycle', () => {
|
||||
// Proven directly at the repository too — the affected count IS the 0-row signal.
|
||||
expect(await tripsRepo.setFeedTokenIfReachable(trip.id, stranger.id, mintedToken)).toBe(0);
|
||||
});
|
||||
|
||||
// R7 (task-7-review.md L4 / 3d ledger's carry): FD1 (`getFeedTokenIfReachable`,
|
||||
// the "does one exist" check) and FD2 (`setFeedTokenIfReachable`, the write)
|
||||
// are two un-transacted statements. Two concurrent `generateTripToken` calls
|
||||
// for the same reachable trip with no existing token both read "none" at
|
||||
// FD1, so both mint a DISTINCT token, and both write — the last write wins
|
||||
// (FD2 is an unconditional UPDATE, not a conflict-checked upsert), so only
|
||||
// one of the two minted URLs ever resolves. Pinning today's actual outcome
|
||||
// (same class as `roadtrip.service.test.ts`'s R7 vias pin), not a fix.
|
||||
it('R7: two concurrent generateTripToken calls both mint, but only the last write survives — the other caller\'s URL never resolves (unserialized FD1-then-FD2)', async () => {
|
||||
const { user, trip } = seedTrip(); // no token yet
|
||||
|
||||
const [r1, r2] = await Promise.all([
|
||||
svc.generateTripToken(trip.id, user.id, BASE),
|
||||
svc.generateTripToken(trip.id, user.id, BASE),
|
||||
]);
|
||||
|
||||
const token1 = r1.feed_url.match(/trip\/([0-9a-f-]+)\.ics$/)![1];
|
||||
const token2 = r2.feed_url.match(/trip\/([0-9a-f-]+)\.ics$/)![1];
|
||||
// FD1 saw no token for either caller, so two distinct tokens were minted.
|
||||
expect(token1).not.toBe(token2);
|
||||
|
||||
const stored = (testDb.prepare('SELECT feed_token FROM trips WHERE id = ?').get(trip.id) as { feed_token: string | null }).feed_token;
|
||||
expect([token1, token2]).toContain(stored);
|
||||
const loser = stored === token1 ? token2 : token1;
|
||||
// The loser's URL is well-formed, but resolves nothing.
|
||||
expect(await svc.buildTripIcs(loser)).toBeNull();
|
||||
expect(await svc.buildTripIcs(stored!)).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ── User (all-trips) feed token ───────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user