diff --git a/packages/cli/test/init-template-comments-self-contained.test.ts b/packages/cli/test/init-template-comments-self-contained.test.ts index 2f880b7c9a..1afe7f6ba7 100644 --- a/packages/cli/test/init-template-comments-self-contained.test.ts +++ b/packages/cli/test/init-template-comments-self-contained.test.ts @@ -1,8 +1,9 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. // -// Every comment that ships into a project scaffolded by `objectstack init` -// must be followable by the person reading it — someone who has that -// project and nothing else. +// Every comment that ships into a project this package scaffolds must be +// followable by the person reading it — someone who has that project and +// nothing else. This package has TWO scaffolders and both ship such text, +// so both are swept: `objectstack init` and `objectstack create`. // // ## The defect // @@ -16,19 +17,47 @@ // bundled template *files*; this is the OTHER scaffolder, which renders its // templates as in-source string literals instead. // +// ## Why the population spans BOTH scaffolders +// +// `packages/cli/src/commands/create.ts` exports a second, independent +// template map (`templates`) whose entries render text — an +// `objectstack.config.ts`, a plugin `src/index.ts`, a `README.md` — as +// in-source string literals and write them straight into the user's +// project. Same emitter shape, same population: text a scaffolded project +// actually receives. While this pin read only `init`'s map, its three +// assertions held for `init` only, and an edit to a `create` literal that +// cited an ADR or linked a docs page that later moved shipped to every +// `os create` user with every gate green. So the population is DERIVED +// from both maps — never written down — the way +// `scaffold-manifest-schema.test.ts` derives its own sweep: a template +// added to either map is swept the day it is added, with nobody +// remembering to extend this file. +// +// `create` has no `configContent` / `srcFiles` split. Every file it writes +// lives in one `files` map keyed by the path it lands at and is rendered by +// calling that entry, so this pin renders EVERY entry and serialises each +// one exactly the way `Create.run()` does (a string verbatim, anything else +// through `JSON.stringify(_, null, 2)`). Sweeping the whole map rather than +// a chosen subset is deliberate: a filter is a place a future file can +// escape through silently, which is the shape of the defect above. +// // ## Why the population is the RENDERED output, not the source file // // `init.ts` also carries its own ordinary source comments that legitimately // cite ADRs and issue numbers (e.g. the `printCreatedFilesSummary` doc // comment cites #10499) — those never ship, because they live outside the // `configContent` / `srcFiles` functions the command actually writes to -// disk. A pin that greps `init.ts` wholesale would match those too and -// report on the wrong population. So this pin does not read the source -// file at all: it calls the exact functions the `init` command calls -// (`template.configContent(...)`, `writeTemplateSrcFiles(...)`) and scans -// the files they actually write — the same real emitter -// `init-scaffold-authoring-rules.test.ts` uses, for the same reason (so -// neither test can drift from what `init` really does). +// disk. `create.ts` is the same: its `run()` body cites `packages/plugins` +// as a destination directory, which ships nowhere. A pin that grepped +// either source file wholesale would match those too and report on the +// wrong population. So this pin does not read the source files at all: it +// calls the exact functions each command calls +// (`template.configContent(...)`, `writeTemplateSrcFiles(...)`, and for +// `create` the entries of `template.files`) and scans the files they +// actually write — the same real emitters +// `init-scaffold-authoring-rules.test.ts` and +// `scaffold-manifest-schema.test.ts` use, for the same reason (so no test +// can drift from what the commands really do). // // ## Why this pin has TWO halves, and why the second is the load-bearing one // @@ -58,6 +87,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { TEMPLATES, sanitizeNamespace, writeTemplateSrcFiles } from '../src/commands/init.js'; +import { templates as createTemplates } from '../src/commands/create.js'; const HERE = path.dirname(fileURLToPath(import.meta.url)); const TMP_ROOT = path.resolve(HERE, '../tmp'); @@ -70,27 +100,60 @@ afterAll(() => { }); interface Rendered { - templateKey: string; + /** `: