import { describe, it, expect } from 'vitest'; import { computeScopesToSuppress, entitledScopes, ALL_TIER_SCOPES, BASE_SCOPES, OPS_READ, OPS_TASKS, FINANCE_READ, FINANCE_ADMIN, OPS_GMAIL, OPS_CALENDAR, } from '../src/scopes.js'; describe('entitledScopes', () => { it('unions the scopes across a user’s groups', () => { // Lauren: assistant + finance (design.md §2.3 worked example). const got = entitledScopes(['sh-mcp-assistant', 'sh-mcp-finance']).sort(); expect(got).toEqual([OPS_CALENDAR, OPS_GMAIL, OPS_READ, OPS_TASKS, FINANCE_READ].sort()); // She does NOT get finance:admin. expect(got).not.toContain(FINANCE_ADMIN); }); it('ignores unknown groups (a stray group grants nothing)', () => { expect(entitledScopes(['sh-mcp-not-a-real-group'])).toEqual([]); expect(entitledScopes([])).toEqual([]); }); it('admin gets every tier scope', () => { expect(entitledScopes(['sh-mcp-admin']).sort()).toEqual([...ALL_TIER_SCOPES].sort()); }); }); describe('computeScopesToSuppress (suppress-only)', () => { it('suppresses exactly the tier scopes the groups do not grant', () => { // ops-only user: keeps ops:read, loses everything else. const suppress = computeScopesToSuppress(['sh-mcp-ops'], true); expect(suppress).toContain(OPS_TASKS); expect(suppress).toContain(FINANCE_READ); expect(suppress).toContain(FINANCE_ADMIN); expect(suppress).not.toContain(OPS_READ); }); it('finance group keeps finance:read but never finance:admin', () => { const suppress = computeScopesToSuppress(['sh-mcp-finance'], true); expect(suppress).not.toContain(FINANCE_READ); expect(suppress).not.toContain(OPS_READ); expect(suppress).toContain(FINANCE_ADMIN); expect(suppress).toContain(OPS_TASKS); }); it('admin suppresses nothing', () => { expect(computeScopesToSuppress(['sh-mcp-admin'], true)).toEqual([]); }); it('FAIL CLOSED: stale sync drops everyone to base ops:read regardless of groups', () => { // Even a finance admin is reduced to ops:read when sync is stale. const suppress = computeScopesToSuppress(['sh-mcp-admin'], false); const kept = ALL_TIER_SCOPES.filter((s) => !suppress.includes(s)); expect(kept).toEqual([...BASE_SCOPES]); expect(suppress).toContain(FINANCE_READ); expect(suppress).toContain(FINANCE_ADMIN); expect(suppress).toContain(OPS_TASKS); }); it('only ever returns scopes drawn from the issued tier-scope set (never invents one)', () => { for (const groups of [[], ['sh-mcp-ops'], ['sh-mcp-finance'], ['sh-mcp-admin']]) { for (const fresh of [true, false]) { const suppress = computeScopesToSuppress(groups, fresh); for (const s of suppress) expect(ALL_TIER_SCOPES).toContain(s); } } }); });