/** * derive-pwa-spec — scan + derivation + write-mode roundtrip. */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import { execute } from '../execute.js' import { validate } from '../validate.js' let root: string beforeEach(() => { root = fs.mkdtempSync(path.join(os.tmpdir(), 'derive-pwa-')) }) afterEach(() => { fs.rmSync(root, { recursive: true, force: true }) }) function writePagespec(app: string, module: string, name: string, json: Record): void { const dir = path.join(root, '.smartstack', 'ba', app, module, 'pagespecs') fs.mkdirSync(dir, { recursive: true }) fs.writeFileSync( path.join(dir, `${name}.md`), `# ${name}\n\nSome prose.\n\n\`\`\`json\n${JSON.stringify(json, null, 2)}\n\`\`\`\n`, ) } function baseSpec(entity: string, view: string, extra: Record = {}): Record { return { screenCode: `SCR-${entity.toUpperCase()}-001`, appCode: 'client', module: 'hrm', section: 'employees', entity, view, permission: 'hrm.employees.read', specHash: 'sha256-x', ...extra, } } describe('derive-pwa-spec — derivation', () => { it('groups per module, expands form → create+edit, uniform meta → entity-level pwa', () => { writePagespec('CLIENT', 'HRM', 'Employee.list', baseSpec('Employee', 'list', { pwa: { support: 'adapted', offline: 'write' } })) writePagespec('CLIENT', 'HRM', 'Employee.form', baseSpec('Employee', 'form', { pwa: { support: 'adapted', offline: 'write' } })) const { report } = execute({ projectPath: root, baRoot: '.smartstack/ba', updates: [], write: false }) expect(report.modules).toHaveLength(1) const entity = report.modules[0].entities[0] expect(entity.entity).toBe('Employee') expect(entity.views.sort()).toEqual(['create', 'edit', 'list']) expect(entity.offline).toBe('write') expect(entity.pwa).toEqual({ support: 'adapted', offline: 'write' }) expect(entity.pwaByView).toBeUndefined() expect(report.requiresVersioned).toEqual([{ app: 'CLIENT', module: 'HRM', entity: 'Employee' }]) }) it('diverging metas → pwaByView; highest offline level wins for the entity', () => { writePagespec('CLIENT', 'HRM', 'Leave.list', baseSpec('Leave', 'list', { section: 'leaves', pwa: { support: 'adapted', offline: 'write' } })) writePagespec('CLIENT', 'HRM', 'Leave.form', baseSpec('Leave', 'form', { section: 'leaves', pwa: { support: 'desktop-only' } })) const { report } = execute({ projectPath: root, baRoot: '.smartstack/ba', updates: [], write: false }) const entity = report.modules[0].entities[0] expect(entity.pwa).toBeUndefined() expect(entity.pwaByView).toEqual({ list: { support: 'adapted', offline: 'write' }, create: { support: 'desktop-only' }, edit: { support: 'desktop-only' }, }) expect(entity.offline).toBe('write') }) it('pages without pwa land in uncovered and never in requiresVersioned', () => { writePagespec('CLIENT', 'CRM', 'Contact.list', baseSpec('Contact', 'list', { module: 'crm', section: 'contacts' })) const { report } = execute({ projectPath: root, baRoot: '.smartstack/ba', updates: [], write: false }) expect(report.uncovered).toEqual([{ app: 'CLIENT', module: 'CRM', pagespec: 'Contact.list' }]) expect(report.requiresVersioned).toEqual([]) expect(report.modules[0].entities[0].offline).toBe('none') }) }) describe('derive-pwa-spec — write mode', () => { it('sets and removes the pwa field in the machine block (report reflects it)', () => { writePagespec('CLIENT', 'HRM', 'Employee.list', baseSpec('Employee', 'list')) const { report } = execute({ projectPath: root, baRoot: '.smartstack/ba', write: true, updates: [{ app: 'CLIENT', module: 'HRM', pagespec: 'Employee.list', pwa: { support: 'adapted', offline: 'read' } }], }) expect(report.writeResults).toEqual([{ app: 'CLIENT', module: 'HRM', pagespec: 'Employee.list', action: 'set' }]) const md = fs.readFileSync(path.join(root, '.smartstack/ba/CLIENT/HRM/pagespecs/Employee.list.md'), 'utf8') expect(md).toContain('"pwa"') expect(md).toContain('"offline": "read"') expect(md).toContain('Some prose.') expect(report.modules[0].entities[0].pwa).toEqual({ support: 'adapted', offline: 'read' }) const removed = execute({ projectPath: root, baRoot: '.smartstack/ba', write: true, updates: [{ app: 'CLIENT', module: 'HRM', pagespec: 'Employee.list', pwa: null }], }) expect(removed.report.writeResults).toEqual([{ app: 'CLIENT', module: 'HRM', pagespec: 'Employee.list', action: 'removed' }]) expect(fs.readFileSync(path.join(root, '.smartstack/ba/CLIENT/HRM/pagespecs/Employee.list.md'), 'utf8')).not.toContain('"pwa"') }) it('missing update target is a warning, not a crash', () => { const { warnings } = execute({ projectPath: root, baRoot: '.smartstack/ba', write: true, updates: [{ app: 'X', module: 'Y', pagespec: 'Nope.list', pwa: { support: 'adapted' } }], }) expect(warnings.join('\n')).toMatch(/not found/) }) }) describe('derive-pwa-spec — validation', () => { it("rejects an update carrying support 'full'", () => { const res = validate({ projectPath: '/x', write: true, updates: [{ app: 'A', module: 'B', pagespec: 'E.list', pwa: { support: 'full' } }], }) expect(res.valid).toBe(false) }) it('warns when updates are provided without write: true', () => { const res = validate({ projectPath: '/x', updates: [{ app: 'A', module: 'B', pagespec: 'E.list', pwa: { support: 'adapted' } }], }) expect(res.valid).toBe(true) expect(res.warnings.join('\n')).toMatch(/report-only/) }) })