Release v0.1.21 2026-07-19

This commit is contained in:
Warren H
2026-07-19 18:48:28 -04:00
parent 018e060cdd
commit 0e36ca1a41
30 changed files with 4701 additions and 120 deletions
+512 -3
View File
@@ -7,7 +7,8 @@ import { after, before, test } from "node:test";
import { createGameApiHandler } from "./game-api.mjs";
const dataDirectory = mkdtempSync(join(tmpdir(), "iwt-heal-api-"));
const api = createGameApiHandler({ dataDirectory });
let roguelikePvpNowMs = 1_000_000;
const api = createGameApiHandler({ dataDirectory, roguelikePvpNow: () => roguelikePvpNowMs });
const server = createServer((request, response) => {
void api.handle(request, response, () => {
response.statusCode = 404;
@@ -36,7 +37,7 @@ async function json(path, init = {}) {
function save(slotId, hunterName, bulldromeKills, highestRoguelikeRound, highestRogueTrialsEndlessKills, highestHockeyHealingReturns, longestHockeyHealingSecondsAtBest, hockeyHealingPvpWins = 0, hockeyHealingPvpLosses = 0, hockeyHealingPvpBossKills = 0, highestBlockbreakerBricks = 0, longestBlockbreakerSeconds = 0, highestBlockbreakerScore = 0, highestAetherAssaultScore = 0, highestAetherAssaultWaveAtBest = 0, longestAetherAssaultSecondsAtBest = 0) {
return {
schemaVersion: 6,
schemaVersion: 7,
slotId,
hunterName,
stats: { bossKills: { bulldrome: bulldromeKills }, highestRoguelikeRound, highestRogueTrialsEndlessKills, highestHockeyHealingReturns, longestHockeyHealingSecondsAtBest, hockeyHealingPvpWins, hockeyHealingPvpLosses, hockeyHealingPvpBossKills, highestBlockbreakerBricks, longestBlockbreakerSeconds, highestBlockbreakerScore, highestAetherAssaultScore, highestAetherAssaultWaveAtBest, longestAetherAssaultSecondsAtBest },
@@ -191,7 +192,7 @@ test("accounts, server saves, and top-five plus current rankings work end to end
assert.equal(legacyUpload.body.save.stats.highestBlockbreakerBricks, current.blockbreakerBricks);
assert.equal(legacyUpload.body.save.stats.longestBlockbreakerSeconds, current.blockbreakerSeconds);
assert.equal(legacyUpload.body.save.stats.highestBlockbreakerScore, current.blockbreakerScore);
assert.equal(legacyUpload.body.save.schemaVersion, 6);
assert.equal(legacyUpload.body.save.schemaVersion, 7);
assert.equal(legacyUpload.body.save.stats.highestAetherAssaultScore, current.aetherScore);
assert.equal(legacyUpload.body.save.stats.highestAetherAssaultWaveAtBest, current.aetherWave);
assert.equal(legacyUpload.body.save.stats.longestAetherAssaultSecondsAtBest, current.aetherDuration);
@@ -296,6 +297,514 @@ test("Healing Hockey PVP queue pairs players and relays match snapshots", async
assert.equal(freshExchange.response.status, 200);
});
test("Roguelike PVP isolates matchmaking, validates progress, hides drafts, and handles rematch lifecycle", async () => {
const registerPlayer = async (username) => {
const registration = await json("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password: `long-password-${username}` }),
});
assert.equal(registration.response.status, 201);
return registration.body.token;
};
const snapshot = (sequence, overrides = {}) => ({
sequence,
round: 1,
phase: "combat",
partyHp: [1, 0.9, 0.8, 0.7, 0.6],
bossHp: 350,
bossMaxHp: 500,
defeatedBosses: 0,
...overrides,
});
const alphaToken = await registerPlayer("rogue_pvp_alpha");
const betaToken = await registerPlayer("rogue_pvp_beta");
const invalidMode = await json("/api/roguelike-pvp/queue", {
method: "POST",
headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ mode: "hockey-healing-pvp", slotId: 1, hunterName: "Alpha", healerClassId: "priest" }),
});
assert.equal(invalidMode.response.status, 400);
const alphaQueue = await json("/api/roguelike-pvp/queue", {
method: "POST",
headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ mode: "roguelike-pvp", slotId: 1, hunterName: "Alpha", healerClassId: "druid" }),
});
assert.equal(alphaQueue.body.status, "waiting");
const betaQueue = await json("/api/roguelike-pvp/queue", {
method: "POST",
headers: { Authorization: `Bearer ${betaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ mode: "roguelike-pvp", slotId: 2, hunterName: "Beta", healerClassId: "shaman" }),
});
assert.equal(betaQueue.body.status, "matched");
assert.equal(betaQueue.body.match.mode, "roguelike-pvp");
assert.equal(betaQueue.body.match.role, "guest");
assert.equal(betaQueue.body.match.opponentName, "Alpha");
assert.equal(betaQueue.body.match.opponentHealerClassId, "druid");
assert.equal(betaQueue.body.match.countdownEndsAtMs, roguelikePvpNowMs + 5_000);
const alphaMatched = await json(`/api/roguelike-pvp/queue/${alphaQueue.body.ticketId}`, {
headers: { Authorization: `Bearer ${alphaToken}` },
});
assert.equal(alphaMatched.body.match.id, betaQueue.body.match.id);
assert.equal(alphaMatched.body.match.role, "host");
assert.equal(alphaMatched.body.match.opponentHealerClassId, "shaman");
assert.equal(alphaMatched.body.match.countdownEndsAtMs, betaQueue.body.match.countdownEndsAtMs);
const matchId = alphaMatched.body.match.id;
const hostState = await json(`/api/roguelike-pvp/matches/${matchId}/state`, {
method: "PUT",
headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ generation: 1, snapshot: snapshot(1) }),
});
assert.equal(hostState.response.status, 200);
assert.equal(hostState.body.status, "active");
assert.equal(hostState.body.opponentSnapshot, null);
const guestState = await json(`/api/roguelike-pvp/matches/${matchId}/state`, {
method: "PUT",
headers: { Authorization: `Bearer ${betaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ generation: 1, snapshot: snapshot(1, { bossHp: 280 }) }),
});
assert.deepEqual(guestState.body.opponentSnapshot, snapshot(1));
assert.deepEqual(guestState.body.hostSnapshot, snapshot(1));
const staleState = await json(`/api/roguelike-pvp/matches/${matchId}/state`, {
method: "PUT",
headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ generation: 1, snapshot: snapshot(1) }),
});
assert.equal(staleState.response.status, 409);
const invalidState = await json(`/api/roguelike-pvp/matches/${matchId}/state`, {
method: "PUT",
headers: { Authorization: `Bearer ${betaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ generation: 1, snapshot: snapshot(2, { partyHp: [1, 1] }) }),
});
assert.equal(invalidState.response.status, 400);
const roundOneDraftSnapshot = (sequence) => snapshot(sequence, {
phase: "draft",
bossHp: 0,
defeatedBosses: 2,
});
const hostRoundOneDraftState = await json(`/api/roguelike-pvp/matches/${matchId}/state`, {
method: "PUT",
headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ generation: 1, snapshot: roundOneDraftSnapshot(2) }),
});
assert.equal(hostRoundOneDraftState.response.status, 200);
const guestRoundOneDraftState = await json(`/api/roguelike-pvp/matches/${matchId}/state`, {
method: "PUT",
headers: { Authorization: `Bearer ${betaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ generation: 1, snapshot: roundOneDraftSnapshot(2) }),
});
assert.equal(guestRoundOneDraftState.response.status, 200);
const openedDraft = await json(`/api/roguelike-pvp/matches/${matchId}/drafts/1/open`, {
method: "POST",
headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ generation: 1 }),
});
assert.equal(openedDraft.body.status, "waiting");
assert.equal(openedDraft.body.deadlineAtMs, roguelikePvpNowMs + 15_000);
assert.equal(openedDraft.body.buffChoices.length, 3);
assert.equal(openedDraft.body.curseChoices.length, 3);
const futureDraft = await json(`/api/roguelike-pvp/matches/${matchId}/drafts/2/open`, {
method: "POST",
headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ generation: 1 }),
});
assert.equal(futureDraft.response.status, 409);
const nonexistentSelection = await json(`/api/roguelike-pvp/matches/${matchId}/drafts/1`, {
method: "PUT",
headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({
generation: 1,
selection: { buffId: "not-a-real-buff", curseId: openedDraft.body.curseChoices[0] },
}),
});
assert.equal(nonexistentSelection.response.status, 400);
const nonOfferedBuffId = [
"mend-echo", "mend-efficiency", "mend-cast-speed", "renew-spread",
"renew-duration", "renew-potency", "shield-echo", "shield-potency",
"shield-guard", "purify-renew", "purify-shield", "purify-chain",
"radiance-cooldown", "radiance-renew", "radiance-shield",
"barrier-cooldown", "barrier-duration", "barrier-regen",
].find((buffId) => !openedDraft.body.buffChoices.includes(buffId));
const nonOfferedSelection = await json(`/api/roguelike-pvp/matches/${matchId}/drafts/1`, {
method: "PUT",
headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({
generation: 1,
selection: { buffId: nonOfferedBuffId, curseId: openedDraft.body.curseChoices[0] },
}),
});
assert.equal(nonOfferedSelection.response.status, 400);
const alphaRoundOneSelection = {
buffId: openedDraft.body.buffChoices[0],
curseId: openedDraft.body.curseChoices[0],
};
const alphaDraft = await json(`/api/roguelike-pvp/matches/${matchId}/drafts/1`, {
method: "PUT",
headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({
generation: 1,
selection: alphaRoundOneSelection,
}),
});
assert.equal(alphaDraft.body.status, "waiting");
assert.equal(alphaDraft.body.submitted, true);
assert.equal("selection" in alphaDraft.body, false);
assert.equal("opponentSelection" in alphaDraft.body, false);
const betaDraftPoll = await json(`/api/roguelike-pvp/matches/${matchId}/drafts/1?generation=1`, {
headers: { Authorization: `Bearer ${betaToken}` },
});
assert.equal(betaDraftPoll.body.opponentSubmitted, true);
assert.equal("opponentSelection" in betaDraftPoll.body, false);
const betaRoundOneSelection = {
buffId: betaDraftPoll.body.buffChoices[0],
curseId: betaDraftPoll.body.curseChoices[0],
};
const betaDraft = await json(`/api/roguelike-pvp/matches/${matchId}/drafts/1`, {
method: "PUT",
headers: { Authorization: `Bearer ${betaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({
generation: 1,
selection: betaRoundOneSelection,
}),
});
assert.equal(betaDraft.body.status, "revealed");
assert.deepEqual(betaDraft.body.selection, {
...betaRoundOneSelection,
autoPicked: false,
});
assert.deepEqual(betaDraft.body.opponentSelection, {
...alphaRoundOneSelection,
autoPicked: false,
});
const changedBuffId = alphaRoundOneSelection.buffId === "mend-echo" ? "mend-efficiency" : "mend-echo";
const changedDraft = await json(`/api/roguelike-pvp/matches/${matchId}/drafts/1`, {
method: "PUT",
headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({
generation: 1,
selection: { buffId: changedBuffId, curseId: alphaRoundOneSelection.curseId },
}),
});
assert.equal(changedDraft.response.status, 409);
const roundTwoDraftSnapshot = (sequence) => snapshot(sequence, {
round: 2,
phase: "draft",
bossHp: 0,
defeatedBosses: 2,
});
await json(`/api/roguelike-pvp/matches/${matchId}/state`, {
method: "PUT",
headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ generation: 1, snapshot: roundTwoDraftSnapshot(3) }),
});
await json(`/api/roguelike-pvp/matches/${matchId}/state`, {
method: "PUT",
headers: { Authorization: `Bearer ${betaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ generation: 1, snapshot: roundTwoDraftSnapshot(3) }),
});
const openedRoundTwoDraft = await json(`/api/roguelike-pvp/matches/${matchId}/drafts/2/open`, {
method: "POST",
headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ generation: 1 }),
});
assert.equal(openedRoundTwoDraft.response.status, 200);
const betaRoundTwoDraft = await json(`/api/roguelike-pvp/matches/${matchId}/drafts/2?generation=1`, {
headers: { Authorization: `Bearer ${betaToken}` },
});
roguelikePvpNowMs += 15_000;
const lateManualDraft = await json(`/api/roguelike-pvp/matches/${matchId}/drafts/2`, {
method: "PUT",
headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({
generation: 1,
selection: {
buffId: openedRoundTwoDraft.body.buffChoices[0],
curseId: openedRoundTwoDraft.body.curseChoices[0],
},
}),
});
assert.equal(lateManualDraft.response.status, 409);
const alphaAutoDraft = await json(`/api/roguelike-pvp/matches/${matchId}/drafts/2`, {
method: "PUT",
headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({
generation: 1,
selection: {
buffId: openedRoundTwoDraft.body.buffChoices[0],
curseId: openedRoundTwoDraft.body.curseChoices[0],
autoPicked: true,
},
}),
});
assert.equal(alphaAutoDraft.body.deadlineExpired, true);
const betaAutoDraft = await json(`/api/roguelike-pvp/matches/${matchId}/drafts/2`, {
method: "PUT",
headers: { Authorization: `Bearer ${betaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({
generation: 1,
selection: {
buffId: betaRoundTwoDraft.body.buffChoices[0],
curseId: betaRoundTwoDraft.body.curseChoices[0],
autoPicked: true,
},
}),
});
assert.equal(betaAutoDraft.body.status, "revealed");
assert.equal(betaAutoDraft.body.opponentSelection.autoPicked, true);
const clientAuthoredWin = await json(`/api/roguelike-pvp/matches/${matchId}/state`, {
method: "PUT",
headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({
generation: 1,
snapshot: snapshot(4, { round: 3, phase: "won" }),
}),
});
assert.equal(clientAuthoredWin.response.status, 400);
const incompletePartyLoss = await json(`/api/roguelike-pvp/matches/${matchId}/state`, {
method: "PUT",
headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({
generation: 1,
snapshot: snapshot(4, { round: 3, phase: "lost", partyHp: [0, 0, 0, 0, 0.01] }),
}),
});
assert.equal(incompletePartyLoss.response.status, 400);
const hostPartyWipe = await json(`/api/roguelike-pvp/matches/${matchId}/state`, {
method: "PUT",
headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({
generation: 1,
snapshot: snapshot(4, { round: 3, phase: "lost", partyHp: [0, 0, 0, 0, 0] }),
}),
});
assert.equal(hostPartyWipe.body.status, "lost-by-forfeit");
assert.equal(hostPartyWipe.body.outcomeReason, "party-wipe");
// First accepted valid wipe is the stable simultaneous-wipe tie-break.
// A later opposing wipe cannot oscillate or reverse the frozen result.
const guestPartyWipeAfterOutcome = await json(`/api/roguelike-pvp/matches/${matchId}/state`, {
method: "PUT",
headers: { Authorization: `Bearer ${betaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({
generation: 1,
snapshot: snapshot(4, { round: 3, phase: "lost", partyHp: [0, 0, 0, 0, 0] }),
}),
});
assert.equal(guestPartyWipeAfterOutcome.body.status, "won-by-forfeit");
assert.equal(guestPartyWipeAfterOutcome.body.outcomeReason, "party-wipe");
const alphaRematch = await json(`/api/roguelike-pvp/matches/${matchId}/rematch`, {
method: "POST",
headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ generation: 1 }),
});
assert.equal(alphaRematch.body.status, "waiting");
const betaRematch = await json(`/api/roguelike-pvp/matches/${matchId}/rematch`, {
method: "POST",
headers: { Authorization: `Bearer ${betaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ generation: 1 }),
});
assert.equal(betaRematch.body.status, "matched");
assert.equal(betaRematch.body.match.generation, 2);
assert.equal(betaRematch.body.match.countdownEndsAtMs, roguelikePvpNowMs + 5_000);
const alphaRematchReady = await json(`/api/roguelike-pvp/matches/${matchId}/rematch`, {
method: "POST",
headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ generation: 1 }),
});
assert.equal(alphaRematchReady.body.match.seed, betaRematch.body.match.seed);
const staleGeneration = await json(`/api/roguelike-pvp/matches/${matchId}/state`, {
method: "PUT",
headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ generation: 1, snapshot: snapshot(1) }),
});
assert.equal(staleGeneration.response.status, 409);
await json(`/api/roguelike-pvp/matches/${matchId}/state`, {
method: "PUT",
headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ generation: 2, snapshot: snapshot(1) }),
});
roguelikePvpNowMs += 15_001;
const hostForfeitWin = await json(`/api/roguelike-pvp/matches/${matchId}/state`, {
method: "PUT",
headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ generation: 2, snapshot: snapshot(2) }),
});
assert.equal(hostForfeitWin.body.status, "won-by-forfeit");
assert.equal(hostForfeitWin.body.opponentConnection, "forfeited");
const guestForfeitLoss = await json(`/api/roguelike-pvp/matches/${matchId}/state`, {
method: "PUT",
headers: { Authorization: `Bearer ${betaToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ generation: 2, snapshot: snapshot(1) }),
});
assert.equal(guestForfeitLoss.body.status, "lost-by-forfeit");
roguelikePvpNowMs += 10 * 60_000 + 1;
const expiredMatch = await json(`/api/roguelike-pvp/queue/${alphaQueue.body.ticketId}`, {
headers: { Authorization: `Bearer ${alphaToken}` },
});
assert.equal(expiredMatch.response.status, 404);
});
test("Roguelike PVP draft deadline deterministically resolves missing submissions", async () => {
const registerPlayer = async (username) => {
const registration = await json("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password: `long-password-${username}` }),
});
assert.equal(registration.response.status, 201);
return registration.body.token;
};
const draftSnapshot = (sequence, round) => ({
sequence,
round,
phase: "draft",
partyHp: [1, 0.9, 0.8, 0.7, 0.6],
bossHp: 0,
bossMaxHp: 500,
defeatedBosses: 2,
});
const hostToken = await registerPlayer("rogue_deadline_host");
const guestToken = await registerPlayer("rogue_deadline_guest");
const hostQueue = await json("/api/roguelike-pvp/queue", {
method: "POST",
headers: { Authorization: `Bearer ${hostToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ mode: "roguelike-pvp", slotId: 1, hunterName: "Host", healerClassId: "priest" }),
});
const guestQueue = await json("/api/roguelike-pvp/queue", {
method: "POST",
headers: { Authorization: `Bearer ${guestToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ mode: "roguelike-pvp", slotId: 1, hunterName: "Guest", healerClassId: "druid" }),
});
assert.equal(hostQueue.body.status, "waiting");
assert.equal(guestQueue.body.status, "matched");
const matchId = guestQueue.body.match.id;
for (const [token, snapshot] of [
[hostToken, draftSnapshot(1, 1)],
[guestToken, draftSnapshot(1, 1)],
]) {
const state = await json(`/api/roguelike-pvp/matches/${matchId}/state`, {
method: "PUT",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({ generation: 1, snapshot }),
});
assert.equal(state.response.status, 200);
}
const hostRoundOne = await json(`/api/roguelike-pvp/matches/${matchId}/drafts/1/open`, {
method: "POST",
headers: { Authorization: `Bearer ${hostToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ generation: 1 }),
});
const guestRoundOne = await json(`/api/roguelike-pvp/matches/${matchId}/drafts/1?generation=1`, {
headers: { Authorization: `Bearer ${guestToken}` },
});
roguelikePvpNowMs = hostRoundOne.body.deadlineAtMs;
const expiredHostPoll = await json(`/api/roguelike-pvp/matches/${matchId}/drafts/1?generation=1`, {
headers: { Authorization: `Bearer ${hostToken}` },
});
assert.equal(expiredHostPoll.body.status, "revealed");
assert.equal(expiredHostPoll.body.deadlineExpired, true);
assert.deepEqual(expiredHostPoll.body.selection, {
buffId: hostRoundOne.body.buffChoices[0],
curseId: hostRoundOne.body.curseChoices[0],
autoPicked: true,
});
assert.deepEqual(expiredHostPoll.body.opponentSelection, {
buffId: guestRoundOne.body.buffChoices[0],
curseId: guestRoundOne.body.curseChoices[0],
autoPicked: true,
});
const mutateServerPick = await json(`/api/roguelike-pvp/matches/${matchId}/drafts/1`, {
method: "PUT",
headers: { Authorization: `Bearer ${hostToken}`, "Content-Type": "application/json" },
body: JSON.stringify({
generation: 1,
selection: {
buffId: hostRoundOne.body.buffChoices[1],
curseId: hostRoundOne.body.curseChoices[1],
autoPicked: true,
},
}),
});
assert.equal(mutateServerPick.response.status, 409);
const expiredGuestPoll = await json(`/api/roguelike-pvp/matches/${matchId}/drafts/1?generation=1`, {
headers: { Authorization: `Bearer ${guestToken}` },
});
assert.equal(expiredGuestPoll.body.status, "revealed");
for (const [token, snapshot] of [
[hostToken, draftSnapshot(2, 2)],
[guestToken, draftSnapshot(2, 2)],
]) {
const state = await json(`/api/roguelike-pvp/matches/${matchId}/state`, {
method: "PUT",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({ generation: 1, snapshot }),
});
assert.equal(state.response.status, 200);
}
const hostRoundTwo = await json(`/api/roguelike-pvp/matches/${matchId}/drafts/2/open`, {
method: "POST",
headers: { Authorization: `Bearer ${hostToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ generation: 1 }),
});
const hostManualSelection = {
buffId: hostRoundTwo.body.buffChoices[1],
curseId: hostRoundTwo.body.curseChoices[1],
};
const hostSubmission = await json(`/api/roguelike-pvp/matches/${matchId}/drafts/2`, {
method: "PUT",
headers: { Authorization: `Bearer ${hostToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ generation: 1, selection: hostManualSelection }),
});
assert.equal(hostSubmission.body.status, "waiting");
const guestRoundTwo = await json(`/api/roguelike-pvp/matches/${matchId}/drafts/2?generation=1`, {
headers: { Authorization: `Bearer ${guestToken}` },
});
roguelikePvpNowMs = hostRoundTwo.body.deadlineAtMs;
const guestAutoResolved = await json(`/api/roguelike-pvp/matches/${matchId}/drafts/2?generation=1`, {
headers: { Authorization: `Bearer ${guestToken}` },
});
assert.equal(guestAutoResolved.body.status, "revealed");
assert.deepEqual(guestAutoResolved.body.selection, {
buffId: guestRoundTwo.body.buffChoices[0],
curseId: guestRoundTwo.body.curseChoices[0],
autoPicked: true,
});
assert.deepEqual(guestAutoResolved.body.opponentSelection, {
...hostManualSelection,
autoPicked: false,
});
});
test("invalid credentials cannot access server saves", async () => {
const login = await json("/api/auth/login", {
method: "POST",