/** * scaffold-component — CLI entry contract. `index.ts` used to hand the RAW * spec to generate(): `views` (default list/detail/form), `routeMode`, * `fields[].required` (default true) and `filters[].control` never existed at * runtime — a field the BA left without `required` shipped as an OPTIONAL * form input, and a spec without `views` crashed with a stack trace. * * QC 2026-09-24 (lot 13): the lot-1 "parity" test compared generate(parse(x)) * to generate(parse(parse(x))) — schema output on both sides, so a schema * stripping a key the renderer reads could never be detected. The parity * block now holds two REAL properties (lib/__tests__/helpers/spec-parity): * no-strip against the RAW example, defaults-only against the schema's * DECLARED defaults — each proven to fire. */ import { describe, it, expect } from 'vitest' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { z } from 'zod' import { runCli } from '../../../../../../lib/__tests__/helpers/run-cli.js' import { specParity } from '../../../../../../lib/__tests__/helpers/spec-parity.js' import { validate } from '../validate.js' import { generate } from '../generate.js' import { ComponentFieldSchema, ScaffoldComponentInputSchema } from '../types.js' const INDEX = resolve(dirname(fileURLToPath(import.meta.url)), '../index.ts') /** The invocation documented in frontend/component/SKILL.md, WITHOUT `views` and WITHOUT `required`. */ const DOC_EXAMPLE = { module: 'hrm', appCode: 'myapp', entity: 'Employee', section: 'employees', fields: [{ name: 'firstName', type: 'string' }], projectPath: '/path', } describe('scaffold-component — the CLI entry runs the PARSED spec', () => { it('runs a spec without `views` in --dry_run (the raw spec crashed on `views.includes`)', () => { const r = runCli(INDEX, ['--spec', JSON.stringify(DOC_EXAMPLE), '--dry_run']) expect(r.envelope, r.stderr).not.toBeNull() expect(r.envelope.success, JSON.stringify(r.envelope.errors)).toBe(true) expect(r.status).toBe(0) const files: string[] = r.envelope.data.files expect(files.some(f => f.endsWith('EmployeesListPage.tsx'))).toBe(true) expect(files.some(f => f.endsWith('EmployeeDetailPage.tsx'))).toBe(true) expect(files.some(f => f.endsWith('EmployeeFormPage.tsx'))).toBe(true) }) it('validate() returns the parsed spec with the documented defaults applied', () => { const v = validate(DOC_EXAMPLE) expect(v.valid).toBe(true) expect(v.data?.views).toEqual(['list', 'detail', 'form']) expect(v.data?.routeMode).toBe('integration') expect(v.data?.fields[0]?.required).toBe(true) }) it('a field left without `required` renders as a REQUIRED form input once parsed', () => { const v = validate(DOC_EXAMPLE) const form = generate(v.data!).find(f => f.path.endsWith('EmployeeFormPage.tsx')) expect(form).toBeDefined() // form.ts emits the inline required-field validation only when a required editable field exists expect(form!.content).toContain('requiredMsg') }) }) describe('scaffold-component — parity RAW ↔ PARSED (real properties, not schema-vs-schema)', () => { const parsed = () => validate(DOC_EXAMPLE).data! it('(a) no-strip: every key path of the documented example survives the parse with its value', () => { const report = specParity(DOC_EXAMPLE, parsed(), ScaffoldComponentInputSchema) expect(report.rawPaths).toContain('fields[0].type') // the walk reaches the nested keys expect(report.stripped).toEqual([]) expect(report.changed).toEqual([]) }) it('(b) defaults-only: every key the parse adds is a default DECLARED at that path, with that value', () => { const report = specParity(DOC_EXAMPLE, parsed(), ScaffoldComponentInputSchema) expect(report.added.map(a => a.path)).toEqual(expect.arrayContaining(['views', 'routeMode', 'fields[0].required'])) expect(report.undeclared).toEqual([]) }) it('secondary: generate() reads nothing a JSON round-trip of the parsed spec would lose', () => { const p = parsed() expect(generate(p)).toEqual(generate(JSON.parse(JSON.stringify(p)))) }) it('FIRES (a): a strip-mode copy of the schema without `fields[].type` is reported at that path', () => { const stripping = z.object({ ...ScaffoldComponentInputSchema.shape, fields: z.array(ComponentFieldSchema.omit({ type: true })), }) const report = specParity(DOC_EXAMPLE, stripping.parse(DOC_EXAMPLE), stripping) expect(report.stripped).toEqual(['fields[0].type']) expect(report.changed).toEqual([]) }) it('FIRES (b): a key injected after the parse, or a default applied with another value, is reported', () => { expect(specParity(DOC_EXAMPLE, { ...parsed(), pluralName: 'Employees' }, ScaffoldComponentInputSchema).undeclared).toEqual(['pluralName']) expect(specParity(DOC_EXAMPLE, { ...parsed(), routeMode: 'screens' }, ScaffoldComponentInputSchema).undeclared).toEqual(['routeMode']) }) })