Skip to content

Commit c0e40e5

Browse files
os-zhuangclaude
andauthored
test(dogfood): runtime proof for ADR-0085 semantic roles over the served pipeline (#2528)
Adds showcase_semantic_zoo (canonical spellings) + showcase_semantic_zoo_legacy (deprecated spellings) as field_zoo-style fixtures, and a dogfood test that asserts against the real Hono app over HTTP: - highlightFields served verbatim + compactLayout transition mirror present (the assertion is deleted in the same PR that retires the alias); - deprecated compactLayout aliases onto highlightFields when served; - stageField:false arrives as a STRICT false (a falsy-dropping serializer or falsy-check renderer silently turns the stepper back on — the exact bug objectui#2168 fixed); - served metadata composes with the shared deriveFieldGroupLayout (§5): declared order, collapse passthrough, trailing ungrouped bucket; - the fixtures are real writable objects, not metadata ghosts. Red-proof: removing stageField:false from the fixture turns the strict-false assertion red (verified locally, then restored). Both packages are private — no changeset needed. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 5c15ccd commit c0e40e5

3 files changed

Lines changed: 198 additions & 0 deletions

File tree

examples/app-showcase/src/objects/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ export { PrivateNote } from './private-note.object.js';
1313
export { Announcement } from './announcement.object.js';
1414
export { Inquiry } from './inquiry.object.js';
1515
export { Contact } from './contact.object.js';
16+
export { SemanticZoo, SemanticZooLegacy } from './semantic-zoo.object.js';
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Semantic-role zoo — runtime regression fixtures for the ADR-0085 object
5+
* semantic roles (`highlightFields` / `stageField` / `fieldGroups.collapse`),
6+
* in the spirit of `showcase_field_zoo` (#2005): the roles are only
7+
* *static*-checked by the spec suite; these two objects prove the SERVED
8+
* pipeline (defineStack → artifact → register → REST serialization) neither
9+
* strips nor mangles them. Guarded by
10+
* `packages/dogfood/test/semantic-roles.dogfood.test.ts`.
11+
*
12+
* Two objects because the alias runs both directions:
13+
* - `SemanticZoo` authors the CANONICAL spellings (highlightFields,
14+
* stageField: 'status', collapse enum) — served meta must also carry the
15+
* deprecated `compactLayout` mirror for pre-11.7 renderers.
16+
* - `SemanticZooLegacy` authors the DEPRECATED spelling (compactLayout) and
17+
* `stageField: false` — served meta must carry the canonical
18+
* `highlightFields`, and `false` must survive (it is the only "stop
19+
* guessing" signal; a falsy-check regression turns the stepper back on).
20+
*/
21+
import { ObjectSchema, Field } from '@objectstack/spec/data';
22+
23+
export const SemanticZoo = ObjectSchema.create({
24+
name: 'showcase_semantic_zoo',
25+
label: 'Semantic Zoo',
26+
pluralLabel: 'Semantic Zoos',
27+
icon: 'flask-conical',
28+
description: 'ADR-0085 semantic-role runtime fixture (canonical spellings)',
29+
30+
fields: {
31+
name: Field.text({ label: 'Name', required: true }),
32+
status: Field.select({
33+
label: 'Status',
34+
options: [
35+
{ label: 'Draft', value: 'draft', default: true },
36+
{ label: 'Active', value: 'active' },
37+
{ label: 'Done', value: 'done' },
38+
],
39+
group: 'basics',
40+
}),
41+
amount: Field.number({ label: 'Amount', group: 'money' }),
42+
notes: Field.textarea({ label: 'Notes' }),
43+
},
44+
45+
highlightFields: ['name', 'status', 'amount'],
46+
stageField: 'status',
47+
fieldGroups: [
48+
{ key: 'basics', label: 'Basics' },
49+
{ key: 'money', label: 'Money', collapse: 'collapsed' },
50+
],
51+
});
52+
53+
export const SemanticZooLegacy = ObjectSchema.create({
54+
name: 'showcase_semantic_zoo_legacy',
55+
label: 'Semantic Zoo (Legacy)',
56+
pluralLabel: 'Semantic Zoo Legacies',
57+
icon: 'flask-round',
58+
description: 'ADR-0085 semantic-role runtime fixture (deprecated spellings)',
59+
60+
fields: {
61+
name: Field.text({ label: 'Name', required: true }),
62+
// Named `status` ON PURPOSE: the stepper heuristic would pick it up —
63+
// `stageField: false` below is what keeps it suppressed.
64+
status: Field.select({
65+
label: 'Status',
66+
options: [
67+
{ label: 'Red', value: 'red', default: true },
68+
{ label: 'Green', value: 'green' },
69+
],
70+
}),
71+
amount: Field.number({ label: 'Amount' }),
72+
},
73+
74+
// Deprecated alias on purpose — must surface as highlightFields when served.
75+
compactLayout: ['name', 'amount'],
76+
// This status is a color, not a lifecycle.
77+
stageField: false,
78+
});
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// ADR-0085 semantic roles — runtime proof over the SERVED pipeline.
4+
//
5+
// @proof: semantic-roles-served
6+
// The object semantic roles (`highlightFields` / `stageField` /
7+
// `fieldGroups.collapse`) are exhaustively unit-tested at parse time in
8+
// `@objectstack/spec`, but a parse-level green says nothing about the pipeline
9+
// a real renderer consumes: defineStack → artifact → registry → REST
10+
// serialization. That pipeline is exactly where the pre-ADR-0085 dialects
11+
// silently died (spec-authored `defaultExpanded` never reached the form;
12+
// `views.form.sections` never existed at all), and where a serializer
13+
// whitelist or a boot-cached merge can strip a key without any static check
14+
// noticing. These assertions run against the real Hono app over HTTP.
15+
//
16+
// Fixtures: `showcase_semantic_zoo` (canonical spellings) and
17+
// `showcase_semantic_zoo_legacy` (deprecated spellings) in
18+
// `examples/app-showcase/src/objects/semantic-zoo.object.ts`.
19+
20+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
21+
import showcaseStack from '@objectstack/example-showcase';
22+
import { bootStack, type VerifyStack } from '@objectstack/verify';
23+
import { deriveFieldGroupLayout } from '@objectstack/spec/data';
24+
25+
let stack: VerifyStack;
26+
let token: string;
27+
28+
async function servedObject(name: string): Promise<Record<string, any>> {
29+
const res = await stack.apiAs(token, 'GET', `/meta/objects/${name}`);
30+
expect(res.status).toBe(200);
31+
const body = await res.json();
32+
return (body as any).data ?? body;
33+
}
34+
35+
beforeAll(async () => {
36+
stack = await bootStack(showcaseStack);
37+
token = await stack.signIn();
38+
});
39+
40+
afterAll(async () => {
41+
await stack?.stop();
42+
});
43+
44+
describe('ADR-0085 semantic roles survive the served pipeline', () => {
45+
it('canonical spellings are served verbatim, with the compactLayout transition mirror', async () => {
46+
const def = await servedObject('showcase_semantic_zoo');
47+
48+
expect(def.highlightFields).toEqual(['name', 'status', 'amount']);
49+
// Transition mirror for pre-11.7 renderers (ObjectGrid & friends read the
50+
// old spelling). Deliberately asserted: when the deprecated alias is
51+
// retired, this expectation is DELETED IN THE SAME PR — a red here after
52+
// that removal means the retirement missed a consumer.
53+
expect(def.compactLayout).toEqual(['name', 'status', 'amount']);
54+
55+
expect(def.stageField).toBe('status');
56+
57+
expect(def.fieldGroups).toEqual([
58+
{ key: 'basics', label: 'Basics', collapse: 'none' },
59+
{ key: 'money', label: 'Money', collapse: 'collapsed' },
60+
]);
61+
expect(def.fields?.status?.group).toBe('basics');
62+
expect(def.fields?.amount?.group).toBe('money');
63+
});
64+
65+
it('deprecated spellings alias onto the canonical keys when served', async () => {
66+
const def = await servedObject('showcase_semantic_zoo_legacy');
67+
68+
// compactLayout (deprecated) must surface as highlightFields…
69+
expect(def.highlightFields).toEqual(['name', 'amount']);
70+
// …and stay readable under the old name (ADR-0079 alias pattern).
71+
expect(def.compactLayout).toEqual(['name', 'amount']);
72+
});
73+
74+
it('stageField:false survives serialization as a strict false', async () => {
75+
const def = await servedObject('showcase_semantic_zoo_legacy');
76+
77+
// `false` is the only "this status-shaped field is NOT a lifecycle" signal.
78+
// It has to arrive as a STRICT false: a serializer that drops falsy values
79+
// (or a renderer falsy-check) silently turns the stepper back on — the
80+
// exact bug PR objectui#2168 fixed. The fixture's `status` field is named
81+
// to trip the heuristic if this suppression ever leaks.
82+
expect(def.stageField).toBe(false);
83+
});
84+
85+
it('the served metadata composes with the shared fieldGroups derivation', async () => {
86+
const def = await servedObject('showcase_semantic_zoo');
87+
88+
// Every renderer (form / modal / detail) is a thin adapter over this one
89+
// function (ADR-0085 §5), so served-def × derivation IS the layout
90+
// contract: declared order, collapse passthrough, trailing untitled
91+
// bucket for ungrouped fields.
92+
const sections = deriveFieldGroupLayout(def);
93+
expect(sections).not.toBeNull();
94+
expect(sections!.map((s) => s.key)).toEqual(['basics', 'money', undefined]);
95+
expect(sections![0].fields).toContain('status');
96+
expect(sections![1]).toMatchObject({ key: 'money', collapse: 'collapsed', fields: ['amount'] });
97+
// Ungrouped trailing bucket keeps `notes` visible (never silently dropped).
98+
expect(sections![2].fields).toContain('notes');
99+
});
100+
101+
it('the fixture objects are real, writable objects (not metadata ghosts)', async () => {
102+
const created = await stack.apiAs(token, 'POST', '/data/showcase_semantic_zoo', {
103+
name: 'roles-roundtrip',
104+
status: 'active',
105+
amount: 7,
106+
});
107+
expect(created.status, `create failed: ${created.status}`).toBeLessThan(300);
108+
const createdBody = (await created.json()) as any;
109+
const id = createdBody.id ?? createdBody.record?.id ?? createdBody.data?.id;
110+
expect(id, 'no id returned from create').toBeTruthy();
111+
112+
const got = await stack.apiAs(token, 'GET', `/data/showcase_semantic_zoo/${id}`);
113+
expect(got.status).toBe(200);
114+
const gotBody = (await got.json()) as any;
115+
const rec = gotBody.record ?? gotBody.data ?? gotBody;
116+
expect(rec.status).toBe('active');
117+
expect(rec.amount).toBe(7);
118+
});
119+
});

0 commit comments

Comments
 (0)