diff --git a/src/app/admin/audit/summarize.ts b/src/app/admin/audit/summarize.ts index e19f9c7..1f4a9ac 100644 --- a/src/app/admin/audit/summarize.ts +++ b/src/app/admin/audit/summarize.ts @@ -324,10 +324,19 @@ const PARTS: Record = { "access_list.watch_added": [accessListRef("name", "accessListId")], "access_list.watch_removed": [accessListRef("name", "accessListId")], "discord.unlinked": [scalar("reason")], + // Both writers stamp `partial` on every row (jobs/discord-roles.ts:143 and + // :369), so it has to be declared here or every single role-change row + // reports a `+1 more` that has nothing behind it. `flag` rather than + // `scalar`: the interesting state is the true one, and a row reading + // "partial false" would be noise on the overwhelming majority that are not. + // Last in the list because it qualifies the whole change rather than naming + // a part of it — and declared parts are never truncated, so trailing costs + // it no visibility. "discord.role_changed": [ roles("added", "removed"), tierLabelled("tier", "tier"), scalar("cause"), + flag("partial", "partial"), ], // `error` leads: this is the row an admin opens the audit log to read // (the deprovision half of `discord.role_sync_failed`'s failure), and the diff --git a/tests/audit-summarize.test.ts b/tests/audit-summarize.test.ts index c32fb70..360f8a9 100644 --- a/tests/audit-summarize.test.ts +++ b/tests/audit-summarize.test.ts @@ -224,6 +224,39 @@ describe("summarizeDetails, declared fields and role rendering", () => { ).toBe("—"); }); + // Both writers stamp `partial` on every row (jobs/discord-roles.ts:143 on the + // deprovision path, :369 on the main sweep), so leaving it undeclared made + // EVERY role-change row claim a hidden key it did not have. + it("says a role change was partial", () => { + expect( + summarizeDetails( + "discord.role_changed", + { added: ["300"], removed: [], partial: true }, + ROLE_NAMES, + ), + ).toBe("+alumni, partial"); + }); + + it("stays silent, not hidden, when a role change was complete", () => { + expect( + summarizeDetails( + "discord.role_changed", + { added: ["300"], removed: [], partial: false }, + ROLE_NAMES, + ), + ).toBe("+alumni"); + }); + + it("counts a genuinely undeclared key on a role change", () => { + expect( + summarizeDetails( + "discord.role_changed", + { added: ["300"], removed: [], partial: false, somethingNew: 1 }, + ROLE_NAMES, + ), + ).toBe("+alumni, +1 more"); + }); + it("surfaces the cause a tier change was written with", () => { expect( summarizeDetails("tier.changed", {