Initial Commit
This commit is contained in:
110
e2e/foundation-stack-saga/tests/twopc-postgres.spec.ts
Normal file
110
e2e/foundation-stack-saga/tests/twopc-postgres.spec.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Stage-6 closure smoke — 2PC against a real Postgres testcontainer.
|
||||
*
|
||||
* The container is started by `global/postgres-setup.ts` with
|
||||
* `max_prepared_transactions=8` so `PREPARE TRANSACTION` is accepted.
|
||||
* Specs drive the `api-2pc` coordinator through commit + abort paths
|
||||
* and assert that `pg_prepared_xacts` is empty after every round.
|
||||
*/
|
||||
|
||||
interface TwoPcVote {
|
||||
readonly participantId: string;
|
||||
readonly vote: string;
|
||||
}
|
||||
interface TwoPcOutcome {
|
||||
readonly txId: string;
|
||||
readonly state: "committed" | "aborted" | "in_doubt";
|
||||
readonly votes: ReadonlyArray<TwoPcVote>;
|
||||
readonly prepareDurationMs: number;
|
||||
readonly commitDurationMs: number;
|
||||
}
|
||||
|
||||
test.describe("foundation-stack-saga — 2PC against real Postgres", () => {
|
||||
test.beforeEach(async ({ request }) => {
|
||||
const xacts = await request.get("/2pc/prepared-xacts");
|
||||
expect(xacts.status()).toBe(200);
|
||||
const body = (await xacts.json()) as { count: number; gids: string[] };
|
||||
if (body.count > 0) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`pre-test pg_prepared_xacts non-empty: ${body.gids.join(", ")}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("1. commit path — both participants COMMIT PREPARED; rows visible", async ({
|
||||
request,
|
||||
}) => {
|
||||
const res = await request.post("/2pc/run", {
|
||||
data: { mode: "commit" },
|
||||
});
|
||||
expect(res.status()).toBe(200);
|
||||
const body = (await res.json()) as {
|
||||
outcome: TwoPcOutcome;
|
||||
rows: { a: number; b: number };
|
||||
};
|
||||
expect(body.outcome.state).toBe("committed");
|
||||
expect(body.outcome.votes.map((v) => v.vote)).toEqual([
|
||||
"commit",
|
||||
"commit",
|
||||
]);
|
||||
expect(body.rows.a).toBe(1);
|
||||
expect(body.rows.b).toBe(1);
|
||||
|
||||
const xacts = await request.get("/2pc/prepared-xacts");
|
||||
const xBody = (await xacts.json()) as { count: number };
|
||||
expect(xBody.count).toBe(0);
|
||||
});
|
||||
|
||||
test("2. abort path — one participant votes abort; both roll back; rows absent", async ({
|
||||
request,
|
||||
}) => {
|
||||
const res = await request.post("/2pc/run", {
|
||||
data: { mode: "abort" },
|
||||
});
|
||||
expect(res.status()).toBe(200);
|
||||
const body = (await res.json()) as {
|
||||
error?: string;
|
||||
rows: { a: number; b: number };
|
||||
};
|
||||
expect(body.error).toBe("TWOPC.VOTE_ABORT");
|
||||
expect(body.rows.a).toBe(0);
|
||||
expect(body.rows.b).toBe(0);
|
||||
|
||||
const xacts = await request.get("/2pc/prepared-xacts");
|
||||
const xBody = (await xacts.json()) as { count: number };
|
||||
expect(xBody.count).toBe(0);
|
||||
});
|
||||
|
||||
test("3. recovery — orphan prepared xact is rolled back; rows absent", async ({
|
||||
request,
|
||||
}) => {
|
||||
const res = await request.post("/2pc/recover-orphan");
|
||||
expect(res.status()).toBe(200);
|
||||
const body = (await res.json()) as {
|
||||
gid: string;
|
||||
rowsAfter: number;
|
||||
};
|
||||
expect(typeof body.gid).toBe("string");
|
||||
expect(body.rowsAfter).toBe(0);
|
||||
|
||||
const xacts = await request.get("/2pc/prepared-xacts");
|
||||
const xBody = (await xacts.json()) as { count: number };
|
||||
expect(xBody.count).toBe(0);
|
||||
});
|
||||
|
||||
test("4. commit + abort round-trip leaves pg_prepared_xacts empty", async ({
|
||||
request,
|
||||
}) => {
|
||||
const r1 = await request.post("/2pc/run", { data: { mode: "commit" } });
|
||||
expect(r1.status()).toBe(200);
|
||||
const r2 = await request.post("/2pc/run", { data: { mode: "abort" } });
|
||||
expect(r2.status()).toBe(200);
|
||||
const xacts = await request.get("/2pc/prepared-xacts");
|
||||
const xBody = (await xacts.json()) as { count: number; gids: string[] };
|
||||
expect(xBody.count).toBe(0);
|
||||
expect(xBody.gids).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user