Add account based multiplayer hosting and bug report retention

This commit is contained in:
2026-10-10 16:24:34 +05:00
parent 9af71c85c2
commit fc6da6756d
33 changed files with 1538 additions and 102 deletions

View File

@@ -44,6 +44,8 @@ test("reports exclude arbitrary URLs, secrets in page queries, malformed message
);
});
test("only exact staff prefixes publish replies or explicitly change glow", () => {
assert.deepEqual(supportCommand("#player-report-close"), { kind: "close" });
assert.equal(supportCommand("#player-report-close everyone"), null);
assert.deepEqual(supportCommand("#player-report-send Dear User\nThanks!"), {
kind: "reply",
text: "Dear User\nThanks!",

195
tests/bug-retention.test.ts Normal file
View File

@@ -0,0 +1,195 @@
import assert from "node:assert/strict";
import test from "node:test";
import { purgeClosedBugReport } from "../lib/bug-retention";
import { LinearSupportClient } from "../lib/linear-support-client";
import type { BugReportRecord } from "../shared/bug-reports";
const id = "00000000-0000-4000-8000-000000000001",
team = "00000000-0000-4000-8000-000000000002";
const closedAt = new Date(1000).toISOString(),
due = 1000 + 3 * 86400000;
const report = {
id,
state: "done",
closed_at: closedAt,
delete_after: new Date(due).toISOString(),
linear_done_synced: true,
} as BugReportRecord;
test("report retention never purges early, open reports, or an older closure generation", async () => {
for (const [record, now] of [
[report, due - 1],
[{ ...report, state: "open" }, due],
[{ ...report, closed_at: new Date(2000).toISOString() }, due],
[null, due],
] as const) {
await purgeClosedBugReport(id, closedAt, {
record: async () => record as BugReportRecord | null,
now: () => now,
linear: {
issue: async () => {
throw new Error("Must not query Linear");
},
remove: async () => assert.fail(),
},
begin: async () => {
assert.fail();
},
reopen: async () => assert.fail(),
removeFiles: async () => assert.fail(),
finish: async () => assert.fail(),
});
}
});
test("reopening a Linear issue cancels local cleanup and completed reports purge in a retry-safe order", async () => {
for (const state of ["started", "completed"]) {
const actions: string[] = [];
await purgeClosedBugReport(id, closedAt, {
record: async () => report,
now: () => due,
linear: {
issue: async () => ({
id,
identifier: "TEST-1",
team: { id: team },
state: { id: team, name: state, type: state },
}),
remove: async () => {
actions.push("linear");
},
},
begin: async () => {
actions.push("lock");
return true;
},
reopen: async () => {
actions.push("reopen");
},
removeFiles: async (paths) => {
assert.deepEqual(paths, [`${id}/input`, `${id}/screenshot.webp`]);
actions.push("storage");
},
finish: async () => {
actions.push("database");
},
});
assert.deepEqual(
actions,
state === "started"
? ["reopen"]
: ["lock", "linear", "storage", "database"],
);
}
});
test("failed external deletion preserves the local retry record", async () => {
for (const failure of ["linear", "storage"]) {
let finished = false;
await assert.rejects(
purgeClosedBugReport(id, closedAt, {
record: async () => ({ ...report, purge_started_at: closedAt }),
now: () => due,
linear: {
issue: async () => assert.fail(),
remove: async () => {
if (failure === "linear") throw new Error("retry");
},
},
begin: async () => true,
reopen: async () => assert.fail(),
removeFiles: async () => {
if (failure === "storage") throw new Error("retry");
},
finish: async () => {
finished = true;
},
}),
/retry/,
);
assert.equal(finished, false);
}
});
test("Linear Done uses the report team's Done state, validates acknowledgment and deletes idempotently", async () => {
let current = "started",
deleted = false;
const calls: string[] = [];
const client = new LinearSupportClient(
"private",
team,
undefined,
async (_url, init) => {
const { query, variables } = JSON.parse(String(init?.body));
calls.push(query.split("(")[0]);
if (query.startsWith("query SupportIssue"))
return Response.json({
data: {
issue: deleted
? null
: {
id,
identifier: "TEST-1",
team: { id: team },
state: {
id: current,
name: current === "done" ? "Done" : "In Progress",
type: current === "done" ? "completed" : "started",
},
},
},
});
if (query.startsWith("query SupportDoneState")) {
assert.equal(variables.team, team);
return Response.json({
data: {
team: {
states: {
nodes: [{ id: "done", name: "Done", type: "completed" }],
},
},
},
});
}
if (query.startsWith("mutation SupportDone")) {
assert.equal(variables.input.stateId, "done");
current = "done";
return Response.json({
data: {
issueUpdate: {
success: true,
issue: { id, state: { id: "done" } },
},
},
});
}
if (query.startsWith("mutation SupportDelete")) {
deleted = true;
return Response.json({ data: { issueDelete: { success: true } } });
}
throw new Error("Unexpected request");
},
);
await client.done(id);
await client.done(id);
await client.remove(id);
await client.remove(id);
assert.equal(calls.filter((c) => c === "mutation SupportDone").length, 1);
assert.equal(calls.filter((c) => c === "mutation SupportDelete").length, 1);
const wrongTeam = new LinearSupportClient(
"secret",
team,
undefined,
async () =>
Response.json({ data: { issue: { id, team: { id: "other" } } } }),
);
await assert.rejects(wrongTeam.remove(id), /team mismatch/);
const failed = new LinearSupportClient(
"secret",
team,
undefined,
async (_url, init) =>
Response.json({
data: JSON.parse(String(init?.body)).query.startsWith("query")
? { issue: { id, team: { id: team } } }
: { issueDelete: { success: false } },
}),
);
await assert.rejects(failed.remove(id), /not confirmed/);
});

View File

@@ -49,22 +49,33 @@ test("room limits include the owner and stay between 2 and 16", () => {
createRoomSchema.parse({
datasetId: map,
difficulty: "normal",
displayName: " Alex ",
playerLimit,
}).displayName,
"Alex",
}).playerLimit,
playerLimit,
);
for (const playerLimit of [1, 17, 2.5, "16", null])
assert.equal(
createRoomSchema.safeParse({
datasetId: map,
difficulty: "normal",
displayName: "Alex",
playerLimit,
}).success,
false,
);
});
test("party creation rejects a client-selected host identity", () => {
const room = { datasetId: map, difficulty: "normal", playerLimit: 4 };
assert.equal(createRoomSchema.safeParse(room).success, true);
for (const identity of [
{ displayName: "Impostor" },
{ accountId: map },
{ name: "Impostor" },
])
assert.equal(
createRoomSchema.safeParse({ ...room, ...identity }).success,
false,
);
});
test("multiplayer names and guesses reject hidden controls, invalid rounds, and non-finite positions", () => {
assert.equal(
joinRoomSchema.parse({ displayName: " Друг " }).displayName,