Skip to content

Commit 0c61850

Browse files
paulfitzclaude
andcommitted
Fix fuzz value distribution and column-type divergence gate
Two test-only fixes to the ActionSummary fuzz harness, both flagged while addressing the Copilot review of this branch. randomValue() had its probability thresholds out of order: the empty-string branch (r < 0.14) sat behind the encoded-list branch (r < 0.16) and was unreachable, so empty-string values were never generated and the Text-default and "0"-vs-0 coercion cases went untested. Reorder into monotonic bands. docSnapshot()'s same-final-state precondition compared only user-table row data, so it missed a column-type divergence that lives purely in metadata when the table ends with no surviving rows. The engine's formula-to-data type guess is batch-sensitive (Text vs Numeric), which let such scenarios slip past the gate and spuriously fail the oracle even though the summarizer was faithful. Fold column type/isFormula into the snapshot so these scenarios are skipped, as the harness already intends for type-divergent cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent bc4c426 commit 0c61850

1 file changed

Lines changed: 24 additions & 6 deletions

File tree

test/server/lib/ActionSummaryFuzz.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,11 @@ function pickInt(rng: Rng, lo: number, hi: number): number { return lo + Math.fl
6363
// type coercion.
6464
function randomValue(rng: Rng, tag: string): any {
6565
const r = rng();
66+
if (r < 0.08) { return ""; } // empty string (Text default)
6667
if (r < 0.16) { // encoded list (ChoiceList-style)
67-
return r < 0.08 ? ["L"] : // empty list (the list default)
68+
return r < 0.12 ? ["L"] : // empty list (the list default)
6869
["L", String(pickInt(rng, 1, 9)), pickInt(rng, 1, 9)];
6970
}
70-
if (r < 0.14) { return ""; } // empty string (Text default)
7171
if (r < 0.22) { return null; } // SQL NULL (a value, not absence)
7272
if (r < 0.30) { return 0; } // numeric default
7373
if (r < 0.38) { return pickInt(rng, 1, 9); } // small number
@@ -438,17 +438,35 @@ function softenSummary(sum: ActionSummary, other: ActionSummary): ActionSummary
438438
}
439439

440440
// A canonical snapshot of every user table's data (rows, columns, computed
441-
// values). Used to enforce the oracle's same-final-state precondition:
442-
// per-action and combined modes must reach the SAME final document state, or
443-
// comparing their summaries is meaningless. Type changes especially can
444-
// in principle make the two modes diverge; such scenarios are skipped.
441+
// values) plus each column's type/isFormula. Used to enforce the oracle's
442+
// same-final-state precondition: per-action and combined modes must reach the
443+
// SAME final document state, or comparing their summaries is meaningless. Type
444+
// changes especially can in principle make the two modes diverge; such
445+
// scenarios are skipped. The engine's formula->data type guess is batch-
446+
// sensitive, and that divergence is invisible in row data when the table ends
447+
// with no surviving rows -- so we fold column type/isFormula into the snapshot,
448+
// keyed by table+colId, to let the precondition skip those scenarios too.
445449
async function docSnapshot(doc: ActiveDoc, session: any): Promise<string> {
446450
const tables = (await doc.fetchTable(session, "_grist_Tables", true)).tableData as any;
447451
const tableIds: string[] = (tables[3].tableId || []).filter((t: any) => typeof t === "string");
448452
const out: { [t: string]: any } = {};
449453
for (const t of [...tableIds].sort()) {
450454
out[t] = canon((await doc.fetchTable(session, t, true)).tableData);
451455
}
456+
// Column type/isFormula, keyed by table+colId, so a metadata-only divergence
457+
// (no surviving rows to expose it) still trips the same-final-state gate.
458+
const refToTableId: { [ref: number]: string } = {};
459+
tables[2].forEach((ref: number, i: number) => { refToTableId[ref] = tables[3].tableId[i]; });
460+
const colData = (await doc.fetchTable(session, "_grist_Tables_column", true)).tableData as any;
461+
const cols = colData[3];
462+
const colMeta: { [key: string]: any } = {};
463+
colData[2].forEach((_ref: number, i: number) => {
464+
const tbl = refToTableId[cols.parentId[i]];
465+
if (tbl && tableIds.includes(tbl)) {
466+
colMeta[`${tbl}.${cols.colId[i]}`] = [cols.type[i], cols.isFormula[i]];
467+
}
468+
});
469+
out.__colMeta__ = canon(colMeta);
452470
return JSON.stringify(out);
453471
}
454472

0 commit comments

Comments
 (0)