111 lines
3.9 KiB
TypeScript
111 lines
3.9 KiB
TypeScript
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([]);
|
|
});
|
|
});
|