#!/usr/bin/env node /** * cli:scaffold-tests * Responsabilité : Generate test files per architectural layer * Appelé par Claude Code via skill:development-testing * * Usage : * npx --prefer-offline tsx skills/development/testing/cli/scaffold-tests/index.ts \ * --spec '{"layer":"business","module":"hrm","appCode":"MyApp","entities":[...],"businessRules":[...],"projectPath":"/path"}' */ import { parseArgs } from 'node:util' import { mkdirSync, writeFileSync } from 'node:fs' import { dirname, resolve, join } from 'node:path' import { guardedRm } from '../../../../lib/guarded-rm.js' import { validate } from './validate.js' import { generate, legacyPaths } from './generate.js' import type { ScaffoldTestsInput } from './types.js' import { generateEnvelope, failGenerate, printEnvelope } from '../../../../lib/output.js' const COMMAND = 'scaffold-tests' function main(): void { const { values } = parseArgs({ options: { spec: { type: 'string' }, outdir: { type: 'string' }, dry_run: { type: 'boolean', default: false }, }, strict: true, }) if (!values.spec) { printEnvelope(failGenerate(COMMAND, ['--spec is required'])) process.exit(1) } let raw: unknown try { raw = JSON.parse(values.spec) } catch { printEnvelope(failGenerate(COMMAND, ['Invalid JSON in --spec'])) process.exit(1) } const validation = validate(raw) if (!validation.valid) { printEnvelope(failGenerate(COMMAND, validation.errors)) process.exit(1) } const spec = raw as ScaffoldTestsInput const files = generate(spec) if (values.dry_run) { printEnvelope(generateEnvelope(COMMAND, { data: { dryRun: true, fileCount: files.length, files: files.map(f => f.path) }, warnings: validation.warnings, })) process.exit(0) } const outdir = values.outdir ?? spec.projectPath // Idempotent relocate: delete the pre-classification test files before writing // the new Tests/// ones (a stale test references moved namespaces). // Guarded sweep (lib/guarded-rm): @customised files preserved, deletions traced. const legacySweep = guardedRm(legacyPaths(spec), { outdir }) const writtenPaths: string[] = [] for (const file of files) { const fullPath = resolve(join(outdir, file.path)) mkdirSync(dirname(fullPath), { recursive: true }) writeFileSync(fullPath, file.content, 'utf-8') writtenPaths.push(fullPath) } printEnvelope(generateEnvelope(COMMAND, { data: { layer: spec.layer, fileCount: writtenPaths.length, filesRemoved: legacySweep.removed }, filesCreated: writtenPaths, warnings: [...validation.warnings, ...legacySweep.preserved.map(p => `legacy path ${p} kept: marked @customised — delete manually if truly superseded.`)], nextSteps: [`Run tests: dotnet test --filter Category=${capitalize(spec.layer)}`], })) } function capitalize(s: string): string { return s.charAt(0).toUpperCase() + s.slice(1) } main()