Skip to content

Commit 6080831

Browse files
committed
test(cli): judge a binding scaffold beside the object os g object writes
The scaffold-validates harness judged `os g view` in a stack that declared no object, so once the author-time rules resolve a view container's `object` the harness's own omission read as the template's defect. Every `namesObject` generator is now validated beside the object scaffold for the same name, materialized through the same loader. Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0d60f88 commit 6080831

1 file changed

Lines changed: 57 additions & 8 deletions

File tree

‎packages/cli/test/generate-scaffold-validates.test.ts‎

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -132,31 +132,64 @@ afterAll(() => {
132132
fs.rmSync(TMP_ROOT, { recursive: true, force: true });
133133
});
134134

135-
/** A legal, minimal host stack. Only the collection under test is populated. */
136-
const hostStack = (collection: string, artifact: unknown) => ({
135+
/**
136+
* A legal, minimal host stack: the collection under test, plus the objects a
137+
* binding scaffold needs present (see {@link boundObjects}).
138+
*/
139+
const hostStack = (collection: string, artifact: unknown, objects: readonly unknown[] = []) => ({
137140
manifest: {
138141
id: 'com.example.scaffold',
139142
name: 'scaffold',
140143
version: '1.0.0',
141144
type: 'app' as const,
142145
namespace: 'scaffold',
143146
},
147+
...(objects.length > 0 ? { objects: [...objects] } : {}),
144148
[collection]: [artifact],
145149
});
146150

151+
/** Materialize one scaffold through the loader `os validate` uses (see the header). */
152+
async function loadScaffold(fileStem: string, source: string): Promise<unknown> {
153+
const file = path.join(TMP_ROOT, `${fileStem}.scaffold.ts`);
154+
fs.writeFileSync(file, source, 'utf8');
155+
const { mod } = await bundleRequire({ filepath: file, external: BUNDLE_REQUIRE_EXTERNALS });
156+
return (mod as { default?: unknown }).default ?? mod;
157+
}
158+
159+
/**
160+
* The object a BINDING scaffold names, as `os g object` writes it for the same
161+
* name — the precondition the author's own project supplies.
162+
*
163+
* A generator flagged `namesObject` (other than `object` itself) writes a
164+
* binding to `objectNameFor(STEM)`: a view container's `object`, an action's or
165+
* a flow start node's `objectName`, an app nav entry's `objectName`. The
166+
* templates are written to COMPOSE — `os g object NAME` then `os g view NAME`
167+
* — so each binding names exactly the object the object scaffold declares.
168+
* Validating a binding scaffold in a stack WITHOUT that object judged it against
169+
* an empty object set, and once the author-time rules resolved a view
170+
* container's `object` (`object-reference-unknown` at `views[0].object`), the
171+
* harness's own omission read as the scaffold's defect. So the object is
172+
* scaffolded here, through the same loader, and carried beside the artifact —
173+
* ⛔ never special-cased in a rule, and ⛔ never the scaffold under test edited
174+
* to fit the harness.
175+
*/
176+
async function boundObjects(type: string): Promise<unknown[]> {
177+
const target = GENERATOR_SCAFFOLD_TARGETS.find((t) => t.type === type);
178+
if (!target?.namesObject || type === 'object') return [];
179+
const objectTarget = GENERATOR_SCAFFOLD_TARGETS.find((t) => t.type === 'object');
180+
if (!objectTarget) throw new Error('the `object` generator must exist to seed a binding scaffold');
181+
return [await loadScaffold('bound-object', objectTarget.generate(STEM))];
182+
}
183+
147184
/**
148185
* Load a scaffold the way `os validate` loads authored TypeScript, then run
149186
* the two steps `Validate.run()` runs on it.
150187
*/
151188
async function validateScaffold(type: string, source: string) {
152-
const file = path.join(TMP_ROOT, `${type}.scaffold.ts`);
153-
fs.writeFileSync(file, source, 'utf8');
154-
155-
const { mod } = await bundleRequire({ filepath: file, external: BUNDLE_REQUIRE_EXTERNALS });
156-
const artifact = (mod as { default?: unknown }).default ?? mod;
189+
const artifact = await loadScaffold(type, source);
157190

158191
const normalized = normalizeStackInput(
159-
hostStack(singularToPlural(type), artifact) as Record<string, unknown>,
192+
hostStack(singularToPlural(type), artifact, await boundObjects(type)) as Record<string, unknown>,
160193
) as Record<string, unknown>;
161194

162195
const unknownKeys = [
@@ -203,6 +236,22 @@ describe('[#14087] every `os generate` scaffold passes `os validate`', () => {
203236
expect(Object.keys(KNOWN_UNVALIDATED_SCAFFOLDS)).not.toContain('flow');
204237
});
205238

239+
it('a binding scaffold is judged beside the object `os g object` writes for the same name', async () => {
240+
// The precondition `boundObjects` supplies is only honest while the view's
241+
// binding and the object's name are the SAME spelling. Pinned directly, so
242+
// a template drifting one side of the pair turns this red rather than
243+
// quietly handing the view scaffold an object it does not bind.
244+
const [object] = (await boundObjects('view')) as { name?: unknown }[];
245+
const view = GENERATOR_SCAFFOLD_TARGETS.find((t) => t.type === 'view');
246+
expect(view, 'the view generator must exist').toBeDefined();
247+
const container = (await loadScaffold('view-binding', view!.generate(STEM))) as { object?: unknown };
248+
expect(object?.name).toBe(STEM);
249+
expect(container.object).toBe(object?.name);
250+
// …and a non-binding generator is judged with no object carried at all.
251+
expect(await boundObjects('dashboard')).toEqual([]);
252+
expect(await boundObjects('object')).toEqual([]);
253+
});
254+
206255
const clean = GENERATOR_SCAFFOLD_TARGETS.filter((t) => !(t.type in KNOWN_UNVALIDATED_SCAFFOLDS));
207256
const known = GENERATOR_SCAFFOLD_TARGETS.filter((t) => t.type in KNOWN_UNVALIDATED_SCAFFOLDS);
208257

0 commit comments

Comments
 (0)