/** * Drift lock: the RBAC action vocabulary is canonical in * `lib/permission-actions.ts` (12 values mirroring the platform * `PermissionAction` enum). Markdown carriers embed the list between * `` markers; code carriers must import the * canonical module instead of re-declaring the list. Edit ALL carriers or * this suite fails. * * The generated C# `ParseAction` switch is locked in * `scaffold-core-seed/__tests__/generate.test.ts` (it asserts the emitted * switch cases against PERMISSION_ACTION_ENUM on a real generated provider). */ import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' import { PERMISSION_ACTIONS, PERMISSION_ACTIONS_MARKER, PERMISSION_ACTION_ENUM, PERMISSION_DATA_ACTIONS, } from '../permission-actions.js' const skillsRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..') function readSkill(rel: string): string { return readFileSync(join(skillsRoot, rel), 'utf8') } function extractBlock(md: string, file: string): string { const re = new RegExp( `([\\s\\S]*?)`, ) const m = md.match(re) if (!m) throw new Error(`marker "${PERMISSION_ACTIONS_MARKER}" not found in ${file}`) return m[1] } /** Backticked single-word lowercase tokens — the action names of a block. */ function backtickedActions(text: string): string[] { return [...text.matchAll(/`([a-z]+)`/g)].map((m) => m[1]) } describe('permission-actions drift — markdown carriers', () => { it.each([ 'business-analyse/create-rbac/SKILL.md', 'business-analyse/audit-prd/SKILL.md', ])('%s carries exactly the 12 actions in its marked block', (file) => { const block = extractBlock(readSkill(file), file) expect(new Set(backtickedActions(block))).toEqual(new Set(PERMISSION_ACTIONS)) }) it('create-business-rules/levels/access-rules.md action row carries the 12', () => { const file = 'business-analyse/create-business-rules/levels/access-rules.md' const block = extractBlock(readSkill(file), file) // Region = the whole fields table (other rows carry scope/level tokens) — // anchor on the `| action |` row only. const actionRow = block .split('\n') .find((l) => l.trim().startsWith('| action |')) expect(actionRow, 'row starting with "| action |"').toBeDefined() expect(new Set(backtickedActions(actionRow!))).toEqual(new Set(PERMISSION_ACTIONS)) }) }) describe('permission-actions drift — code carriers', () => { it('scaffold-controller types.ts imports the canonical data-action enum (no local list)', () => { const src = readSkill('development/backend/controller/cli/scaffold-controller/types.ts') expect( /from\s+'(?:\.\.\/)+lib\/permission-actions\.js'/.test(src), 'types.ts must import lib/permission-actions.js', ).toBe(true) expect(src).toContain('PERMISSION_DATA_ACTIONS') // No hand-maintained copy of the 10-action list may reappear. expect(src).not.toMatch(/z\.enum\(\s*\[\s*'read'\s*,\s*'create'/) }) it('derive-seed-delta generate-sql.ts re-exports the canonical action → SQL map', async () => { const mod = await import( '../../development/backend/core-seed/cli/derive-seed-delta/generate-sql.js' ) expect(mod.ACTION_NAMES, 'generate-sql must export ACTION_NAMES').toBeDefined() expect(mod.ACTION_NAMES).toEqual(PERMISSION_ACTION_ENUM) }) it('the data-action enum is the 12 minus the structural pair', () => { expect(PERMISSION_DATA_ACTIONS).toHaveLength(10) expect(new Set([...PERMISSION_DATA_ACTIONS, 'access', 'lookup'])).toEqual( new Set(PERMISSION_ACTIONS), ) }) })