fix(qa): grant packet personas fixture access

This commit is contained in:
ajnart committed 2026-09-01 11:33:24 +02:00
1 parent d3e7cd45a3
commit b435230739
5 files changed
+473 -23

No files matched your search

+2
View File
@@ -20,6 +20,8 @@ The fixture command binds to an explicit loopback host on an automatically alloc
Each populated fixture manifest keeps the human-readable persona label in `personas[].name` and exposes the non-secret credentials username in `personas[].loginUsername`. Board route names are deterministic, unique, and validation-safe in `boards[].name`; shared board names exactly match the IDs in `coverage-manifest.json` (including `qa-scroll-lab`), while the human-readable title remains in `boards[].label` and the board page/meta titles. The login username is the deterministic lowercase persona handle; the password remains available only through `QA_PASSWORD_FILE`.
`scripts/release-v2-qa/permissions.mts` explicitly maps all 45 packet persona/board combinations to `none`, `view`, `modify`, or `full`. The seeder collapses repeated assignments to the highest access required by the campaign and writes only the resulting direct fixture-board grants; administrators and board owners rely on their existing implicit full access. The fixture manifest records both `packetBoardAccess` and the collapsed `expectedBoardAccess`. Live verification compares the exact direct rows and rejects unrelated grants, Nolan protected-board access, or Vivian edit access. Any access-contract change must update the packet mapping and its focused spec together.
Generated placements are validated before the seed transaction commits. Root items and containers must fit the active main/left/right lane after gutters, nested containers must fit their parent content grid without cycles, and every item must fit its selected root or container in every layout. `qa-grid-24` includes a dedicated unguttered 24-column custom layout that preserves exact width coverage `1–24`; its guttered Base layout and all narrower layouts keep lane-valid projected widths.
Use `report -- --init` to create missing agent reports. Use `report -- --refresh-placeholders` after changing manifest templates or rolling the checkout candidate. It refreshes only safe, untouched `not-reached` placeholders, reports refreshed and protected-skip counts, and preserves reports with execution or case results, widget results, findings, artifacts, measurements, limitations, or reproduction data byte-for-byte. Malformed reports are protected skips rather than overwrite candidates.
+252
View File
@@ -0,0 +1,252 @@
export const releaseV2QaBoardAccessValues = ["none", "view", "modify", "full"] as const;
export type ReleaseV2QaBoardAccess = (typeof releaseV2QaBoardAccessValues)[number];
export type ReleaseV2QaPacketBoardAccess = Record<string, Record<string, Record<string, ReleaseV2QaBoardAccess>>>;
export interface ReleaseV2QaCoverageAccessManifest {
boards: { id: string }[];
packets: { id: string; boards: string[]; personas: string[] }[];
personas: string[];
}
export const releaseV2QaPacketBoardAccess = {
"preflight-01": {},
"preflight-02": {
"Avery Admin": { "qa-grid-24": "full", "qa-permissions-public": "full" },
"Rowan Owner": { "qa-grid-24": "full", "qa-permissions-public": "full" },
"Vivian Viewer": { "qa-grid-24": "view", "qa-permissions-public": "view" },
"Nolan Outsider": { "qa-grid-24": "none", "qa-permissions-public": "none" },
},
"preflight-03": {},
"board-01": {
"Rowan Owner": { "qa-grid-24": "full" },
},
"board-02": {
"Eden Editor": { "qa-scroll-lab": "modify" },
},
"board-03": {
"Casey Chaos": { "qa-dense-collisions": "modify" },
},
"board-04": {
"Rowan Owner": { "qa-nested-containers": "full" },
},
"board-05": {
"Morgan Mobile": { "qa-layout-boundaries": "modify" },
},
"board-06": {
"Brooke Minimalist": { "qa-icons-bookmarks": "modify" },
},
"board-07": {
"Rowan Owner": { "qa-permissions-public": "full" },
"Eden Editor": { "qa-permissions-public": "modify" },
"Vivian Viewer": { "qa-permissions-public": "view" },
"Nolan Outsider": { "qa-permissions-public": "none" },
},
"board-08": {
"Avery Admin": { "qa-download-upload": "full" },
},
"board-09": {
"Kira Keyboard": { "qa-grid-24": "modify" },
},
"widgets-01": {
"Brooke Minimalist": { "qa-widgets-01": "modify" },
},
"widgets-02": {
"Cora Creator": { "qa-widgets-02": "modify" },
},
"widgets-03": {
"Cora Creator": { "qa-widgets-03": "modify" },
},
"widgets-04": {
"Ingrid Infra": { "qa-widgets-04": "modify" },
},
"widgets-05": {
"Ingrid Infra": { "qa-widgets-05": "modify" },
},
"widgets-06": {
"Ingrid Infra": { "qa-widgets-06": "modify" },
},
"widgets-07": {
"Ingrid Infra": { "qa-widgets-07": "modify" },
},
"widgets-08": {
"Ingrid Infra": { "qa-widgets-08": "modify" },
},
"widgets-09": {
"Maya Media": { "qa-widgets-09": "modify" },
},
"widgets-10": {
"Maya Media": { "qa-widgets-10": "modify" },
},
"widgets-11": {
"Ingrid Infra": { "qa-widgets-11": "modify" },
},
"widgets-12": {
"Ash Assistant": { "qa-widgets-12": "modify", "qa-custom-widget-assistant": "modify" },
"Cora Creator": { "qa-widgets-12": "modify", "qa-custom-widget-assistant": "modify" },
},
"core-v2-01": {
"Cora Creator": { "qa-custom-widget-assistant": "modify" },
},
"core-v2-02": {
"Ash Assistant": { "qa-custom-widget-assistant": "modify" },
},
"core-v2-03": {},
"core-v2-04": {
"Avery Admin": { "qa-permissions-public": "full" },
"Nolan Outsider": { "qa-permissions-public": "none" },
},
"core-v2-05": {
"Ingrid Infra": { "qa-widgets-07": "modify" },
},
"core-v2-06": {
"Kira Keyboard": { "qa-grid-24": "modify" },
},
"core-v2-07": {
"Vivian Viewer": { "qa-custom-widget-assistant": "view", "qa-permissions-public": "view" },
},
"core-v2-08": {
"Casey Chaos": { "qa-custom-widget-assistant": "modify", "qa-widgets-12": "modify" },
},
"whole-product-01": {
"Avery Admin": { "qa-grid-24": "full" },
"Nora Newcomer": { "qa-grid-24": "modify" },
},
"whole-product-02": {
"Rowan Owner": { "qa-grid-24": "full", "qa-custom-widget-assistant": "full" },
},
"whole-product-03": {
"Eden Editor": { "qa-scroll-lab": "modify", "qa-layout-boundaries": "modify" },
},
"whole-product-04": {
"Vivian Viewer": { "qa-permissions-public": "view" },
"Nolan Outsider": { "qa-permissions-public": "none" },
},
"whole-product-05": {
"Morgan Mobile": { "qa-layout-boundaries": "modify", "qa-widgets-01": "modify" },
},
"whole-product-06": {
"Maya Media": { "qa-widgets-04": "modify", "qa-widgets-05": "modify" },
},
"whole-product-07": {
"Ingrid Infra": {
"qa-widgets-07": "modify",
"qa-widgets-08": "modify",
"qa-widgets-09": "modify",
"qa-widgets-10": "modify",
"qa-widgets-11": "modify",
},
},
"whole-product-08": {
"Cora Creator": { "qa-custom-widget-assistant": "modify" },
"Ash Assistant": { "qa-custom-widget-assistant": "modify" },
},
"whole-product-09": {
"Kira Keyboard": {
"qa-grid-24": "full",
"qa-download-upload": "full",
"qa-custom-widget-assistant": "full",
},
"Casey Chaos": {
"qa-grid-24": "full",
"qa-download-upload": "full",
"qa-custom-widget-assistant": "full",
},
},
"performance-01": {
"Avery Admin": { "qa-grid-24": "full", "qa-widgets-12": "full" },
},
"performance-02": {
"Rowan Owner": { "qa-dense-collisions": "full", "qa-nested-containers": "full" },
},
"performance-03": {
"Ingrid Infra": { "qa-widgets-04": "modify", "qa-widgets-07": "modify", "qa-widgets-12": "modify" },
"Maya Media": { "qa-widgets-04": "modify", "qa-widgets-07": "modify", "qa-widgets-12": "modify" },
},
"performance-04": {
"Casey Chaos": {
"qa-grid-24": "modify",
"qa-custom-widget-assistant": "modify",
"qa-widgets-12": "modify",
},
},
} as const satisfies ReleaseV2QaPacketBoardAccess;
const accessRank: Record<ReleaseV2QaBoardAccess, number> = {
none: 0,
view: 1,
modify: 2,
full: 3,
};
export const getReleaseV2QaExpectedBoardAccess = (
packetAccess: ReleaseV2QaPacketBoardAccess,
): Record<string, Record<string, ReleaseV2QaBoardAccess>> => {
const expected: Record<string, Record<string, ReleaseV2QaBoardAccess>> = {};
for (const personaAccess of Object.values(packetAccess)) {
for (const [persona, boards] of Object.entries(personaAccess)) {
const expectedBoards = (expected[persona] ??= {});
for (const [board, access] of Object.entries(boards)) {
const current = expectedBoards[board];
if (!current || accessRank[access] > accessRank[current]) expectedBoards[board] = access;
}
}
}
return Object.fromEntries(
Object.entries(expected)
.toSorted(([left], [right]) => left.localeCompare(right))
.map(([persona, boards]) => [
persona,
Object.fromEntries(Object.entries(boards).toSorted(([left], [right]) => left.localeCompare(right))),
]),
);
};
const duplicateValues = (values: string[]) => values.filter((value, index) => values.indexOf(value) !== index);
export const validateReleaseV2QaPacketBoardAccess = (
manifest: ReleaseV2QaCoverageAccessManifest,
packetAccess: ReleaseV2QaPacketBoardAccess,
): string[] => {
const errors: string[] = [];
const packetIds = manifest.packets.map((packet) => packet.id);
const accessPacketIds = Object.keys(packetAccess);
for (const duplicate of new Set(duplicateValues(packetIds))) errors.push(`duplicate coverage packet ${duplicate}`);
for (const packetId of packetIds) {
if (!(packetId in packetAccess)) errors.push(`${packetId}: board access assignment is missing`);
}
for (const packetId of accessPacketIds) {
if (!packetIds.includes(packetId)) errors.push(`${packetId}: board access assignment is not a coverage packet`);
}
const knownPersonas = new Set(manifest.personas);
const knownBoards = new Set(manifest.boards.map((board) => board.id));
for (const packet of manifest.packets) {
const assignedAccess = packetAccess[packet.id] ?? {};
const expectedPersonas = packet.boards.length === 0 ? [] : packet.personas;
for (const persona of expectedPersonas) {
if (!(persona in assignedAccess)) errors.push(`${packet.id}: ${persona} board access assignment is missing`);
}
for (const persona of Object.keys(assignedAccess)) {
if (!knownPersonas.has(persona)) errors.push(`${packet.id}: unknown persona ${persona}`);
if (!packet.personas.includes(persona)) errors.push(`${packet.id}: unassigned persona ${persona}`);
const assignedBoards = assignedAccess[persona] ?? {};
for (const board of packet.boards) {
if (!(board in assignedBoards)) errors.push(`${packet.id}: ${persona} access to ${board} is missing`);
}
for (const [board, access] of Object.entries(assignedBoards)) {
if (!knownBoards.has(board)) errors.push(`${packet.id}: unknown board ${board}`);
if (!packet.boards.includes(board)) errors.push(`${packet.id}: unassigned board ${board}`);
if (!releaseV2QaBoardAccessValues.includes(access)) {
errors.push(`${packet.id}: invalid ${persona} access to ${board}: ${String(access)}`);
}
}
}
if (packet.boards.length === 0 && Object.keys(assignedAccess).length > 0) {
errors.push(`${packet.id}: board-less packet must not assign board access`);
}
}
return errors;
};
+70
View File
@@ -0,0 +1,70 @@
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
import {
getReleaseV2QaExpectedBoardAccess,
releaseV2QaPacketBoardAccess,
validateReleaseV2QaPacketBoardAccess,
} from "./permissions.mts";
interface CoverageManifest {
boards: { id: string }[];
packets: { id: string; boards: string[]; personas: string[] }[];
personas: string[];
}
const coverageManifest = JSON.parse(
await readFile(resolve(import.meta.dirname, "../../qa/release-v2/coverage-manifest.json"), "utf8"),
) as CoverageManifest;
describe("release-v2 QA packet board access", () => {
it("covers every assigned packet persona and board combination", () => {
expect(validateReleaseV2QaPacketBoardAccess(coverageManifest, releaseV2QaPacketBoardAccess)).toEqual([]);
expect(Object.keys(releaseV2QaPacketBoardAccess)).toHaveLength(45);
});
it("grants mutation access to specialized packet owners without unrelated boards", () => {
const access = getReleaseV2QaExpectedBoardAccess(releaseV2QaPacketBoardAccess);
expect(access["Casey Chaos"]).toEqual({
"qa-custom-widget-assistant": "full",
"qa-dense-collisions": "modify",
"qa-download-upload": "full",
"qa-grid-24": "full",
"qa-widgets-12": "modify",
});
expect(access["Morgan Mobile"]).toEqual({
"qa-layout-boundaries": "modify",
"qa-widgets-01": "modify",
});
expect(access["Brooke Minimalist"]).toEqual({
"qa-icons-bookmarks": "modify",
"qa-widgets-01": "modify",
});
expect(access["Cora Creator"]).toEqual({
"qa-custom-widget-assistant": "modify",
"qa-widgets-02": "modify",
"qa-widgets-03": "modify",
"qa-widgets-12": "modify",
});
expect(access["Ash Assistant"]).toEqual({
"qa-custom-widget-assistant": "modify",
"qa-widgets-12": "modify",
});
});
it("preserves owner, editor, viewer, outsider, and administrator invariants", () => {
const access = getReleaseV2QaExpectedBoardAccess(releaseV2QaPacketBoardAccess);
expect(Object.values(access["Avery Admin"] ?? {})).not.toContain("none");
expect(Object.values(access["Avery Admin"] ?? {})).not.toContain("view");
expect(Object.values(access["Avery Admin"] ?? {})).not.toContain("modify");
expect(access["Rowan Owner"]?.["qa-permissions-public"]).toBe("full");
expect(access["Eden Editor"]?.["qa-permissions-public"]).toBe("modify");
expect(Object.values(access["Vivian Viewer"] ?? {})).toEqual(expect.arrayContaining(["view"]));
expect(new Set(Object.values(access["Vivian Viewer"] ?? {}))).toEqual(new Set(["view"]));
expect(new Set(Object.values(access["Nolan Outsider"] ?? {}))).toEqual(new Set(["none"]));
});
});
+25 -11
View File
@@ -40,6 +40,12 @@ import {
import { boardNameSchema } from "../../packages/validation/src/board.ts";
import { findFixtureGeometryErrors } from "./geometry.mts";
import {
getReleaseV2QaExpectedBoardAccess,
releaseV2QaPacketBoardAccess,
validateReleaseV2QaPacketBoardAccess,
} from "./permissions.mts";
import type { ReleaseV2QaCoverageAccessManifest } from "./permissions.mts";
import {
assertReleaseV2QaProfileEnvironment,
releaseV2QaProfiles,
@@ -294,6 +300,7 @@ const disabledCustomDefinitionId = "qa-v2-custom-widget-disabled";
const missingSecretCustomDefinitionId = "qa-v2-custom-widget-missing-secret";
const customDefinitionIds = [customDefinitionId, disabledCustomDefinitionId, missingSecretCustomDefinitionId];
const iconRepositoryId = "qa-v2-icon-repository";
const expectedBoardAccess = getReleaseV2QaExpectedBoardAccess(releaseV2QaPacketBoardAccess);
const parseCli = (): CliOptions => {
const options: CliOptions = { reset: false };
@@ -1107,20 +1114,20 @@ const seedBoardsAsync = async (optionsByKind: Map<WidgetKind, string>) => {
await db.delete(boardUserPermissions).where(inArray(boardUserPermissions.boardId, qaBoardIds));
await db.delete(boardGroupPermissions).where(inArray(boardGroupPermissions.boardId, qaBoardIds));
const permissionRows: Array<{ boardId: string; userId: string; permission: BoardPermission }> = [];
for (const board of sharedBoards) {
permissionRows.push(
{ boardId: boardId(board.handle), userId: userId("rowan-owner"), permission: "full" },
{ boardId: boardId(board.handle), userId: userId("eden-editor"), permission: "modify" },
{ boardId: boardId(board.handle), userId: userId("vivian-viewer"), permission: "view" },
);
for (const persona of personas) {
if (["avery-admin", "rowan-owner", "eden-editor", "vivian-viewer", "nolan-outsider"].includes(persona.handle)) {
const personaByName = new Map(personas.map((persona) => [persona.name, persona]));
const boardByName = new Map(sharedBoards.map((board) => [boardName(board.handle), board]));
for (const [personaName, boardAccess] of Object.entries(expectedBoardAccess)) {
const persona = personaByName.get(personaName);
if (!persona) throw new Error(`Unknown QA persona in board access assignment: ${personaName}`);
for (const [name, permission] of Object.entries(boardAccess)) {
const board = boardByName.get(name);
if (!board) throw new Error(`Unknown QA board in access assignment: ${name}`);
if (permission === "none" || persona.permissions.includes("admin") || board.ownerHandle === persona.handle)
continue;
}
permissionRows.push({ boardId: boardId(board.handle), userId: userId(persona.handle), permission: "view" });
permissionRows.push({ boardId: boardId(board.handle), userId: userId(persona.handle), permission });
}
}
await db.insert(boardUserPermissions).values(permissionRows);
if (permissionRows.length > 0) await db.insert(boardUserPermissions).values(permissionRows);
for (const persona of personas) {
await db
@@ -1195,6 +1202,8 @@ const createManifest = (
"vivian-viewer": "view",
"nolan-outsider": "none",
},
packetBoardAccess: releaseV2QaPacketBoardAccess,
expectedBoardAccess: seeded ? expectedBoardAccess : {},
counts: {
personas: seeded ? personas.length : 0,
boards: boardEntries.length,
@@ -1235,6 +1244,11 @@ const createManifest = (
const main = async () => {
const options = parseCli();
validateQaBoardDefinitions();
const coverageManifest = JSON.parse(
await readFile(resolve(repoRoot, "qa/release-v2/coverage-manifest.json"), "utf8"),
) as ReleaseV2QaCoverageAccessManifest;
const accessErrors = validateReleaseV2QaPacketBoardAccess(coverageManifest, releaseV2QaPacketBoardAccess);
if (accessErrors.length > 0) throw new Error(`Invalid QA packet board access: ${accessErrors.join("; ")}`);
const galleryKinds = widgetGalleryGroups.flat();
if (
galleryKinds.length !== widgetKinds.length ||
+124 -12
View File
@@ -6,6 +6,16 @@ import { fileURLToPath } from "node:url";
import type { LayoutRole } from "../../packages/definitions/index.ts";
import { boardNameSchema } from "../../packages/validation/src/board.ts";
import { findFixtureGeometryErrors } from "./geometry.mts";
import {
getReleaseV2QaExpectedBoardAccess,
releaseV2QaPacketBoardAccess,
validateReleaseV2QaPacketBoardAccess,
} from "./permissions.mts";
import type {
ReleaseV2QaBoardAccess,
ReleaseV2QaCoverageAccessManifest,
ReleaseV2QaPacketBoardAccess,
} from "./permissions.mts";
import { resolveCheckoutCandidateSha } from "./provenance.mts";
import { collectHumanWidgetStatusErrors, normalizeReportMetadata } from "./report-integrity.mts";
import { assertSafeReportPath, readSafeReportFile, validateResolvedArtifactPath } from "./report-path-integrity.mts";
@@ -137,6 +147,8 @@ interface FixtureManifest {
boards: { id: string; handle: string; name: string; label: string; layouts: string[]; flags: { shared: boolean } }[];
layouts: { id: string; boardId: string; columnCount: number }[];
expectedBoardPermissions: Record<string, string>;
packetBoardAccess: ReleaseV2QaPacketBoardAccess;
expectedBoardAccess: Record<string, Record<string, ReleaseV2QaBoardAccess>>;
counts: Record<string, number>;
flags: {
demoMode: boolean;
@@ -201,6 +213,7 @@ const expectedProfileFlags: Record<string, string[]> = {
"onboarding-fresh": ["DEMO_MODE=false", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"],
degraded: ["DEMO_MODE=true", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true"],
};
const expectedBoardAccess = getReleaseV2QaExpectedBoardAccess(releaseV2QaPacketBoardAccess);
const errors: string[] = [];
const parseOptions = (): VerifyOptions => {
@@ -382,6 +395,12 @@ const validateFixtureManifest = async (
if (JSON.stringify(fixture.expectedBoardPermissions) !== JSON.stringify(expectedPermissions)) {
errors.push(`${label}: expected board permissions are incorrect`);
}
if (JSON.stringify(fixture.packetBoardAccess) !== JSON.stringify(releaseV2QaPacketBoardAccess)) {
errors.push(`${label}: packet board access differs from the campaign assignment`);
}
if (JSON.stringify(fixture.expectedBoardAccess) !== JSON.stringify(expectedBoardAccess)) {
errors.push(`${label}: effective assigned board access differs from the campaign assignment`);
}
} else {
const emptyFixtureCounts = [
"personas",
@@ -405,6 +424,9 @@ const validateFixtureManifest = async (
) {
errors.push(`${label}: onboarding-fresh must not contain populated QA fixtures`);
}
if (Object.keys(fixture.expectedBoardAccess ?? {}).length > 0) {
errors.push(`${label}: onboarding-fresh must not declare seeded board access`);
}
}
let fixtureOrigin: URL | undefined;
@@ -486,9 +508,12 @@ const validateSqliteState = async (runtime: RuntimeManifest, fixture: FixtureMan
}
}
const qaBoards = database.prepare(`select id, name from board where id like 'qa-v2-board-%'`).all() as {
const qaBoards = database
.prepare(`select id, name, creator_id as creatorId from board where id like 'qa-v2-board-%'`)
.all() as {
id: string;
name: string;
creatorId: string | null;
}[];
const boardNameById = new Map(fixture.boards.map((board) => [board.id, board.name]));
if (duplicateValues(qaBoards.map((board) => board.name)).length > 0) {
@@ -593,26 +618,107 @@ const validateSqliteState = async (runtime: RuntimeManifest, fixture: FixtureMan
errors.push(`${label}: widget gallery does not contain every canonical widget kind exactly once`);
}
for (const board of fixture.boards.filter((candidate) => candidate.flags.shared)) {
for (const [handle, permission] of Object.entries(fixture.expectedBoardPermissions)) {
const permissionCount = count(
`select count(*) as count from boardUserPermission where board_id = ? and user_id = ? and permission = ?`,
board.id,
`qa-v2-user-${handle}`,
permission,
);
const personaHandleByName = new Map(fixture.personas.map((persona) => [persona.name, persona.handle]));
const boardByName = new Map(fixture.boards.map((board) => [board.name, board]));
const creatorByBoardId = new Map(qaBoards.map((board) => [board.id, board.creatorId]));
const expectedDirectPermissions: string[] = [];
for (const [personaName, assignedBoards] of Object.entries(fixture.expectedBoardAccess)) {
const personaHandle = personaHandleByName.get(personaName);
if (!personaHandle) {
errors.push(`${label}: assigned board access references unknown persona ${personaName}`);
continue;
}
const currentUserId = `qa-v2-user-${personaHandle}`;
for (const [boardName, permission] of Object.entries(assignedBoards)) {
const board = boardByName.get(boardName);
if (!board) {
errors.push(`${label}: assigned board access references unknown board ${boardName}`);
continue;
}
const isAdmin = personaHandle === "avery-admin";
const isOwner = creatorByBoardId.get(board.id) === currentUserId;
if (permission === "none") {
const anyPermissionCount = count(
`select count(*) as count from boardUserPermission where board_id = ? and user_id = ?`,
board.id,
`qa-v2-user-${handle}`,
currentUserId,
);
if (anyPermissionCount !== 0) errors.push(`${label}: ${handle} unexpectedly has access to ${board.handle}`);
if (anyPermissionCount !== 0 || isAdmin || isOwner) {
errors.push(`${label}: ${personaHandle} unexpectedly has assigned access to ${board.handle}`);
}
continue;
}
if (permissionCount !== 1) errors.push(`${label}: ${handle} lacks ${permission} access to ${board.handle}`);
if (isAdmin || isOwner) {
if (permission !== "full") {
errors.push(
`${label}: ${personaHandle} has implicit full access but ${permission} was declared for ${board.handle}`,
);
}
continue;
}
expectedDirectPermissions.push(`${board.id}\t${currentUserId}\t${permission}`);
}
}
const actualDirectPermissions = database
.prepare(
`select board_id as boardId, user_id as userId, permission
from boardUserPermission
where board_id like 'qa-v2-board-%' and user_id like 'qa-v2-user-%'
order by board_id, user_id, permission`,
)
.all()
.map((row) => {
const permission = row as { boardId: string; userId: string; permission: string };
return `${permission.boardId}\t${permission.userId}\t${permission.permission}`;
});
if (JSON.stringify(actualDirectPermissions) !== JSON.stringify(expectedDirectPermissions.toSorted())) {
errors.push(`${label}: direct QA board permission rows differ from the packet access assignment`);
}
const averyAdminPermissionCount = count(
`select count(*) as count
from groupMember gm
join groupPermission gp on gp.group_id = gm.group_id
where gm.user_id = ? and gp.permission = 'admin'`,
"qa-v2-user-avery-admin",
);
if (averyAdminPermissionCount !== 1) errors.push(`${label}: Avery Admin lacks the admin fixture role`);
const nolanSharedBoardAccess = count(
`select count(*) as count
from boardUserPermission bup
join board b on b.id = bup.board_id
where bup.user_id = ? and b.id not like 'qa-v2-board-home-%'`,
"qa-v2-user-nolan-outsider",
);
const nolanGlobalBoardAccess = count(
`select count(*) as count
from groupMember gm
join groupPermission gp on gp.group_id = gm.group_id
where gm.user_id = ? and (gp.permission = 'admin' or gp.permission like 'board-%')`,
"qa-v2-user-nolan-outsider",
);
if (nolanSharedBoardAccess !== 0 || nolanGlobalBoardAccess !== 0) {
errors.push(`${label}: Nolan Outsider has protected fixture board access`);
}
const vivianEditAccess = count(
`select count(*) as count
from boardUserPermission
where user_id = ? and permission in ('modify', 'full')`,
"qa-v2-user-vivian-viewer",
);
const vivianGlobalEditAccess = count(
`select count(*) as count
from groupMember gm
join groupPermission gp on gp.group_id = gm.group_id
where gm.user_id = ? and gp.permission in ('admin', 'board-modify-all', 'board-full-all')`,
"qa-v2-user-vivian-viewer",
);
if (vivianEditAccess !== 0 || vivianGlobalEditAccess !== 0) {
errors.push(`${label}: Vivian Viewer has board edit access`);
}
}
} finally {
database.close();
@@ -795,6 +901,12 @@ const validateRequestedRuntime = async (
const main = async (): Promise<void> => {
const options = parseOptions();
const manifest = await readJson<Manifest>(join(qaRoot, "coverage-manifest.json"));
errors.push(
...validateReleaseV2QaPacketBoardAccess(
manifest as Manifest & ReleaseV2QaCoverageAccessManifest,
releaseV2QaPacketBoardAccess,
),
);
const ledgerPath = join(qaRoot, "ledger.json");
await assertSafeReportPath(qaRoot, ledgerPath, "release-v2 QA ledger path");
await assertSafeReportPath(qaRoot, join(qaRoot, "master-report.md"), "release-v2 QA master report path");