Skip to content

Commit 8ad872b

Browse files
os-litantclaude
andauthored
test(cli): sweep every os explain catalog entry against its spec schema (#15197)
The `SCHEMAS` catalog in `packages/cli/src/commands/explain.ts` is hand-maintained and derives from nothing; until now one field of one entry was guarded by an exact-token assertion, and one entry's example was pinned by parsing it against the real schema. This generalises that parse technique across the whole catalog, deriving the entry set from `SCHEMAS` itself so a future entry cannot be added without being classified. Entries whose example does not parse today land as `it.fails` xfails naming the card filed for each; rewriting a catalog entry is operator-facing output and a separate review question, so nothing is fixed here. The two entries with no schema to parse against assert that reason rather than being silently absent. Also corrects a stale claim in this file's own comments: it is no longer outside every tsc program. `packages/cli/tsconfig.test.json` includes `test/**/*`, and this file carries no row in `test-typecheck-debt.json`, so a diagnostic it gains is red on arrival. Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N Co-authored-by: Claude <noreply@anthropic.com>
1 parent b70a55d commit 8ad872b

1 file changed

Lines changed: 148 additions & 3 deletions

File tree

packages/cli/test/commands.test.ts

Lines changed: 148 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@ import Lint from '../src/commands/lint';
1313
import Diff from '../src/commands/diff';
1414
import Explain, { SCHEMAS } from '../src/commands/explain';
1515
import { FlowSchema } from '@objectstack/spec/automation';
16+
// The catalog sweep below resolves each entry's schema BY NAME, so it needs the
17+
// name surface rather than one binding. `@objectstack/spec`'s root exports none
18+
// of these Zod schemas (measured: 129 root exports, no `ObjectSchema` /
19+
// `FieldSchema` / … among them) — every one lives on a subpath, so the four
20+
// metadata-authoring subpaths the catalog draws on are named here. The named
21+
// `FlowSchema` import above stays: a value import fails loudly on a broken
22+
// export where a namespace property read would degrade to `undefined`, and the
23+
// sweep pays for its namespace form with an explicit resolvability assertion.
24+
import * as specAi from '@objectstack/spec/ai';
25+
import * as specAutomation from '@objectstack/spec/automation';
26+
import * as specData from '@objectstack/spec/data';
27+
import * as specUi from '@objectstack/spec/ui';
1628

1729
describe('CLI Commands (oclif)', () => {
1830
it('should have compile command', () => {
@@ -118,9 +130,13 @@ describe('os explain — schema catalog accuracy', () => {
118130
// drift — it re-derives the truth from the spec on every run, which is what
119131
// the hand-maintained catalog otherwise has no way to do.
120132
// The catalog's element shape, stated locally: `SchemaInfo` is not exported,
121-
// and these tests must stay honest even where `SCHEMAS` widens to `any`
122-
// (this file sits outside every tsc program — see the TEST_DEBT ledger — so
123-
// an implicit `any` here would silently stop checking anything).
133+
// and these tests must stay honest even where `SCHEMAS` widens to `any`.
134+
// ⚠️ The reason recorded here has CHANGED and the discipline has not. This
135+
// file no longer sits outside every tsc program: #14710 landed
136+
// `packages/cli/tsconfig.test.json`, whose `include: ["test/**/*"]` puts this
137+
// file in the program (`tsc --noEmit --listFiles -p tsconfig.test.json`
138+
// resolves it), and it carries NO row in `test-typecheck-debt.json` — so any
139+
// diagnostic it gains is red on arrival rather than silently unchecked.
124140
type CatalogField = { name: string; type: string };
125141
const flowFields = (kind: 'required' | 'optional'): CatalogField[] => SCHEMAS.flow[kind];
126142

@@ -161,3 +177,132 @@ describe('os explain — schema catalog accuracy', () => {
161177
expect(declared).toContain('edges');
162178
});
163179
});
180+
181+
// ── `os explain` — the WHOLE catalog, swept against the spec (#14811) ──────
182+
//
183+
// #14782 pinned one entry (`flow`) by parsing its `example` against the real
184+
// schema. This generalises that technique to every entry, and derives the entry
185+
// set from `SCHEMAS` itself: a hand-written list of entries is precisely the
186+
// place a future entry escapes through unnoticed, which is the same defect this
187+
// guard closes one level down. Add a catalog entry and this block goes RED
188+
// until the entry is classified.
189+
//
190+
// ⛔ It does NOT fix what it turns red. Rewriting a catalog entry rewrites
191+
// operator-facing output and is a separate change with a separate review
192+
// question, so entries whose example does not parse today land as `it.fails`
193+
// xfails naming the card filed for each. The day one is corrected its xfail
194+
// fails ("expected to fail but passed") — promote it to a plain `it` then.
195+
//
196+
// ⛔ And it does not skip silently. Two entries resolve to no schema at all;
197+
// they get tests that ASSERT that reason. A guard reporting green over the
198+
// entries it never looked at is this card's own defect, one layer up.
199+
describe('os explain — every catalog entry swept against its spec schema (#14811)', () => {
200+
type CatalogEntry = { name: string; example: string };
201+
const catalog = SCHEMAS as unknown as Record<string, CatalogEntry>;
202+
203+
// The searched name surface, stated rather than assumed: absence below means
204+
// absent from exactly these four subpaths. (`grep` over `packages/spec/src`
205+
// finds no `export const TriggerSchema` or `WorkflowSchema` anywhere at all.)
206+
const specSurface: Record<string, unknown> = {
207+
...specData,
208+
...specUi,
209+
...specAi,
210+
...specAutomation,
211+
};
212+
213+
type ParseResult = { success: boolean; error?: { issues: unknown[] } };
214+
type ZodLike = { safeParse: (value: unknown) => ParseResult };
215+
216+
// The catalog stores examples as authored source, so evaluate the literal —
217+
// the same technique as the `flow` pin above.
218+
const evaluate = (key: string): unknown =>
219+
new Function(`return (${catalog[key].example});`)() as unknown;
220+
221+
// Entries with one schema to parse against. `card` marks a known-broken one
222+
// and names where its errors are recorded; its absence means "must parse".
223+
const BOUND: Record<string, { schema: string; card?: number }> = {
224+
object: { schema: 'ObjectSchema', card: 15170 },
225+
field: { schema: 'FieldSchema' },
226+
view: { schema: 'ViewSchema', card: 15171 },
227+
flow: { schema: 'FlowSchema' },
228+
agent: { schema: 'AgentSchema', card: 15172 },
229+
app: { schema: 'AppSchema', card: 15173 },
230+
query: { schema: 'QuerySchema' },
231+
dashboard: { schema: 'DashboardSchema', card: 15174 },
232+
action: { schema: 'ActionSchema', card: 15175 },
233+
};
234+
235+
// Entries with NO single schema to parse against, and the reason each of the
236+
// two tests at the bottom asserts rather than merely states.
237+
const UNBOUND: Record<string, string> = {
238+
workflow:
239+
'there is no standalone Workflow authoring type (ADR-0019) — the entry is a '
240+
+ 'redirect and its example is commentary, not a literal',
241+
trigger:
242+
'no `TriggerSchema` exists in the spec, and the sample is not a Hook either (#15176)',
243+
};
244+
245+
it('classifies every entry in SCHEMAS — none is silently unswept', () => {
246+
const classified = [...Object.keys(BOUND), ...Object.keys(UNBOUND)].sort();
247+
expect(
248+
classified,
249+
'a new `os explain` catalog entry must be classified here: bind it to a spec '
250+
+ 'schema, or give it an UNBOUND reason plus a test that asserts that reason',
251+
).toEqual(Object.keys(catalog).sort());
252+
});
253+
254+
// Harness health, asserted separately from the xfails: `it.fails` is green on
255+
// ANY failure, so a broken subpath export or an unevaluable example would
256+
// otherwise keep six xfails passing while measuring nothing at all.
257+
it('resolves every bound entry to a real schema, and every bound example to an object', () => {
258+
for (const [key, bound] of Object.entries(BOUND)) {
259+
const schema = specSurface[bound.schema] as ZodLike | undefined;
260+
expect(typeof schema?.safeParse, `${bound.schema} (for os explain ${key})`).toBe('function');
261+
expect(typeof evaluate(key), `os explain ${key} example`).toBe('object');
262+
}
263+
});
264+
265+
for (const [key, bound] of Object.entries(BOUND)) {
266+
const parses = (): void => {
267+
const schema = specSurface[bound.schema] as ZodLike;
268+
const result = schema.safeParse(evaluate(key));
269+
expect(
270+
result.success,
271+
`os explain ${key}: its example must parse as ${bound.schema}. Issues: ${
272+
result.success ? '' : JSON.stringify(result.error?.issues, null, 2)
273+
}`,
274+
).toBe(true);
275+
};
276+
277+
if (bound.card === undefined) {
278+
it(`os explain ${key} — example parses as ${bound.schema}`, parses);
279+
} else {
280+
it.fails(
281+
`os explain ${key} — example does NOT parse as ${bound.schema} `
282+
+ `(known-broken, filed as #${bound.card}; promote to a plain assertion once fixed)`,
283+
parses,
284+
);
285+
}
286+
}
287+
288+
it(`os explain workflow — ${UNBOUND.workflow}`, () => {
289+
expect('WorkflowSchema' in specSurface).toBe(false);
290+
expect(catalog.workflow.name).toContain('no standalone type');
291+
// Its example is commentary about the live mechanisms, not a literal.
292+
// Asserted, so "nothing was parsed here" is a property of this file rather
293+
// than an omission a reader has to notice.
294+
expect(() => evaluate('workflow')).toThrow();
295+
});
296+
297+
it(`os explain trigger — ${UNBOUND.trigger}`, () => {
298+
expect('TriggerSchema' in specSurface).toBe(false);
299+
// …and it is not `HookSchema` under another name. Ruling the one real
300+
// candidate out is what makes the unbound classification a measurement
301+
// instead of an assumption: `event` is a strict-object ALIAS of `events`
302+
// (the same alias-as-a-documented-key failure the `flow` entry had), and a
303+
// hook's code slot is `handler`, so the entry's `flow` key is unrecognised.
304+
const hook = specSurface.HookSchema as ZodLike | undefined;
305+
expect(typeof hook?.safeParse, 'HookSchema — the candidate this rules out').toBe('function');
306+
expect(hook!.safeParse(evaluate('trigger')).success).toBe(false);
307+
});
308+
});

0 commit comments

Comments
 (0)