Skip to content

Commit 6150d2a

Browse files
committed
feat(verify): a refused fixture row is retried with every synthesized field, and the report names the shape (#15953 slice 5, WIP)
Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 769f3fb commit 6150d2a

4 files changed

Lines changed: 52 additions & 14 deletions

File tree

‎content/docs/deployment/cli.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2027,7 +2027,8 @@ os verify --dashboard-windows # Also prove each dashboard date
20272027
on every preset's first and last day and the day either side (never after
20282028
today), stamped at UTC noon, owned by the caller and holding every column
20292029
the widget's own filter, the dataset's filter and the measure's filter
2030-
compare, and read back. The widget is then asked through
2030+
compare (the smallest record first, every synthesized field if the seed
2031+
door refuses it), and read back. The widget is then asked through
20312032
`POST /analytics/dataset/query` as the signed-in admin with no window and
20322033
under each of the 13 presets, before and after the fixtures. A preset is
20332034
handed as the spec's `{date-macro}` window for it (`{ $gte: '{7_days_ago}' }`

‎packages/verify/README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,9 @@ through the seed door on each preset's first and last day and the day either
166166
side (never after today), each stamped at UTC noon (a `date` field takes the
167167
day), owned by the caller, with every column the widget's filter, the
168168
dataset's filter and the measure's filter compare held to a value they select,
169-
and read back before anything is judged. Every count is a difference (after
169+
and read back before anything is judged. A row is the smallest record the CRUD
170+
derivation offers (its required fields); if the seed door refuses that (an app
171+
rule that requires a field only in some state), every field it synthesizes. Every count is a difference (after
170172
the fixtures minus before, under the same window), so the app's own rows
171173
cancel out, and the widget with no window must count every fixture row before
172174
any window is judged. A count measure is probed (a `count` of a named field

‎packages/verify/src/dashboard-window.test.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,19 @@ const ticket = {
4747

4848
const source = {
4949
manifest: { id: 'com.example.dashboard-window', namespace: 'wnd', version: '0.0.1', type: 'app', name: 'Dashboard Window Fixture' },
50+
hooks: [
51+
{
52+
// An app rule requiring a field only in some state: a closed ticket needs its effort.
53+
name: 'wnd_closed_needs_effort',
54+
object: 'wnd_ticket',
55+
events: ['beforeInsert'],
56+
handler: async (ctx: { input: Record<string, unknown> }) => {
57+
if (ctx.input.is_closed === true && ctx.input.effort == null) {
58+
throw Object.assign(new Error('Effort is required on a closed ticket'), { code: 'VALIDATION_FAILED', status: 400 });
59+
}
60+
},
61+
},
62+
],
5063
objects: [
5164
{ name: 'wnd_agent', label: 'Agent', fields: { name: { type: 'text', label: 'Name', required: true } } },
5265
ticket,
@@ -197,6 +210,9 @@ describe('runDashboardWindowProofs — the fixture app', () => {
197210
}
198211
}
199212
expect(widgetOf(report, 'open_tickets').fieldType).toBe('datetime');
213+
// The closed tickets meet the app's own rule only with every synthesized field.
214+
expect(widgetOf(report, 'closed_tickets').measures[0]!.fixture).toBe('every-field');
215+
expect(widgetOf(report, 'open_tickets').measures[0]!.fixture).toBe('smallest');
200216
expect(widgetOf(report, 'due_window').fieldType).toBe('date');
201217
expect(widgetOf(report, 'all_tickets').fieldType).toBe('datetime');
202218
});

‎packages/verify/src/dashboard-window.ts‎

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ export interface DashboardWindowMeasureResult {
193193
excludedKind?: DashboardWindowExclusion;
194194
/** The fixture days written (UTC calendar days). */
195195
days?: string[];
196+
/** `smallest` — the required fields only; `every-field` — the seed door refused that, so every field the CRUD derivation synthesizes. */
197+
fixture?: 'smallest' | 'every-field';
196198
/** The control leg: the widget with no window must count every fixture row. */
197199
unwindowed?: { expected: number; answered?: number; answer?: DashboardWindowAnswer };
198200
presets: DashboardWindowPresetProbe[];
@@ -515,18 +517,19 @@ function uniqueFields(objectDef: any): Set<string> {
515517
}
516518

517519
/**
518-
* The smallest record the CRUD derivation can offer (the shape the sibling
519-
* families use): its values for the fields the object REQUIRES, so every
520-
* optional field takes the app's own default; each `date` / `datetime` one day
521-
* after the one declared before it, and each unique-constrained value made
522-
* distinct by `n`.
520+
* A record the CRUD derivation can offer: by default the smallest (the shape
521+
* the sibling families use) — its values for the fields the object REQUIRES,
522+
* so every optional field takes the app's own default; with `full`, every
523+
* field it synthesizes (what a rule that requires a field only in some state
524+
* asks for). Each `date` / `datetime` is one day after the one declared before
525+
* it, and each unique-constrained value is made distinct by `n`.
523526
*/
524-
function fixtureBody(body: Record<string, unknown>, objectDef: any, n: number): Record<string, unknown> {
527+
function fixtureBody(body: Record<string, unknown>, objectDef: any, n: number, full = false): Record<string, unknown> {
525528
const unique = uniqueFields(objectDef);
526529
const out: Record<string, unknown> = {};
527530
let dates = 0;
528531
for (const [name, def] of Object.entries<any>(objectDef?.fields ?? {})) {
529-
if (def?.required !== true || !(name in body)) continue;
532+
if ((!full && def?.required !== true) || !(name in body)) continue;
530533
let value = body[name];
531534
if ((def?.type === 'date' || def?.type === 'datetime') && typeof value === 'string') value = laterBy(value, dates++);
532535
if (unique.has(name) && (typeof value === 'string' || typeof value === 'number')) value = vary(value, n);
@@ -860,24 +863,40 @@ export async function runDashboardWindowProofs(
860863
const days = fixtureDays(presets, day0);
861864
const stamp = (day: string) => (fieldType === 'date' ? day : `${day}T12:00:00.000Z`);
862865
const written: Array<{ day: string; id: string }> = [];
866+
// The smallest record first; if the seed door refuses it (an app rule that
867+
// requires a field in the state the held columns put the row in), every
868+
// field the CRUD derivation synthesizes, and that shape for the rest.
869+
let full = false;
863870
for (const day of days) {
864-
const row: Record<string, unknown> = {
865-
...fixtureBody(body, objectDefs.get(c.object!), ++counter),
871+
const row = (wide: boolean): Record<string, unknown> => ({
872+
...fixtureBody(body, objectDefs.get(c.object!), ++counter, wide),
866873
...(ownerColumn ? { owner_id: principal } : {}),
867874
...Object.fromEntries(held),
868875
[c.field!]: stamp(day),
876+
});
877+
const seedOne = async (wide: boolean): Promise<string> => {
878+
const [made] = await stack.seed(c.object!, [row(wide)]);
879+
if (made?.id == null) throw new Error(`the seed of ${c.object} returned no id`);
880+
return String(made.id);
869881
};
870882
try {
871-
const [made] = await stack.seed(c.object!, [row]);
872-
if (made?.id == null) throw new Error(`the seed of ${c.object} returned no id`);
873-
written.push({ day, id: String(made.id) });
883+
let id: string;
884+
try {
885+
id = await seedOne(full);
886+
} catch (first) {
887+
if (full || written.length > 0) throw first;
888+
full = true;
889+
id = await seedOne(true);
890+
}
891+
written.push({ day, id });
874892
} catch (e) {
875893
r.days = days;
876894
notRun(`the fixture row for ${day} was refused by the seed door: ${clip((e as Error)?.message, 160)}`);
877895
return;
878896
}
879897
}
880898
r.days = days;
899+
r.fixture = full ? 'every-field' : 'smallest';
881900
// Read back: the stamp and every held column must read as written.
882901
for (const { day, id } of written) {
883902
const [got] = await stack.rows(c.object!, { id });

0 commit comments

Comments
 (0)