fix(hierarchy): include transfer snapshots in replay content comparison
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
Review round 1 blocker: sameEvent omitted transferFrom/transferTo, so a retried transfer re-routed to a different destination would silently replay instead of conflicting. Transfer source/destination are semantic event content per §5.2. Adds a witness varying the transfer destination on a duplicate idempotency key; also fixes a stale cross-reference in the db witness header.
This commit is contained in:
@@ -143,6 +143,27 @@ describe('hierarchy audit repository integration', (): void => {
|
|||||||
).rejects.toThrow(HierarchyAuditIdempotencyConflictError);
|
).rejects.toThrow(HierarchyAuditIdempotencyConflictError);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('throws on a duplicate idempotency key whose transfer destination differs', async () => {
|
||||||
|
const from = { kind: 'company' as const, id: randomUUID(), slug: 'src-co' };
|
||||||
|
const to = { kind: 'company' as const, id: randomUUID(), slug: 'dst-co' };
|
||||||
|
const first = input({
|
||||||
|
verb: 'transfer',
|
||||||
|
targetKind: 'estate',
|
||||||
|
transferFrom: from,
|
||||||
|
transferTo: to,
|
||||||
|
});
|
||||||
|
const original = await handle.db.transaction(async (tx) => repo.append(tx, first));
|
||||||
|
expect(original.replayed).toBe(false);
|
||||||
|
// Identical retry replays; a retry re-routed to a different destination must conflict.
|
||||||
|
const replay = await handle.db.transaction(async (tx) => repo.append(tx, first));
|
||||||
|
expect(replay.replayed).toBe(true);
|
||||||
|
await expect(
|
||||||
|
handle.db.transaction(async (tx) =>
|
||||||
|
repo.append(tx, { ...first, transferTo: { ...to, id: randomUUID() } }),
|
||||||
|
),
|
||||||
|
).rejects.toThrow(HierarchyAuditIdempotencyConflictError);
|
||||||
|
});
|
||||||
|
|
||||||
it('builds root-first parent chains and rejects unknown nodes', async () => {
|
it('builds root-first parent chains and rejects unknown nodes', async () => {
|
||||||
const companyId = randomUUID();
|
const companyId = randomUUID();
|
||||||
const estateId = randomUUID();
|
const estateId = randomUUID();
|
||||||
|
|||||||
@@ -165,7 +165,11 @@ function sameEvent(row: HierarchyAuditEventRow, input: AppendHierarchyEventInput
|
|||||||
row.targetId === input.targetId &&
|
row.targetId === input.targetId &&
|
||||||
row.correlationId === input.correlationId &&
|
row.correlationId === input.correlationId &&
|
||||||
(row.causationId ?? null) === (input.causationId ?? null) &&
|
(row.causationId ?? null) === (input.causationId ?? null) &&
|
||||||
canonicalJson(row.targetSnapshot) === canonicalJson(input.targetSnapshot)
|
canonicalJson(row.targetSnapshot) === canonicalJson(input.targetSnapshot) &&
|
||||||
|
// Transfer source/destination are semantic content (§5.2): a retry with a
|
||||||
|
// different destination must conflict, never silently replay.
|
||||||
|
canonicalJson(row.transferFrom ?? null) === canonicalJson(input.transferFrom ?? null) &&
|
||||||
|
canonicalJson(row.transferTo ?? null) === canonicalJson(input.transferTo ?? null)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
* table — events survive the deletion of their target), the causation
|
* table — events survive the deletion of their target), the causation
|
||||||
* self-FK, and the outbox's FK/uniqueness/status shape. The repository-level
|
* self-FK, and the outbox's FK/uniqueness/status shape. The repository-level
|
||||||
* half of §6.4 (same-transaction atomicity, rollback, replay) is witnessed in
|
* half of §6.4 (same-transaction atomicity, rollback, replay) is witnessed in
|
||||||
* apps/gateway/src/hierarchy/hierarchy-audit.witness.test.ts.
|
* apps/gateway/src/hierarchy/hierarchy-audit.integration.test.ts.
|
||||||
*
|
*
|
||||||
* Two legs run the same witness body:
|
* Two legs run the same witness body:
|
||||||
* - PGlite (WASM Postgres): always runs.
|
* - PGlite (WASM Postgres): always runs.
|
||||||
|
|||||||
Reference in New Issue
Block a user