Fix failure

This commit is contained in:
Andras Schmelczer 2026-09-27 14:29:36 +01:00
commit 909096969a
6 changed files with 95 additions and 32 deletions

View file

@ -19,7 +19,9 @@ metadata, visible/durable disk images and CAS requests before cleanup.
## Workload and oracle ## Workload and oracle
Version 2 traces contain an editor-intent program and its compiled steps. Version 2 traces store the editor-intent program once. Replay compiles its steps
and full-state checkpoints, avoiding repeated file contents in large artifacts.
Older version 2 traces with compiled steps still replay from their program.
`model.ts` independently interprets that intent into complete expected file sets: `model.ts` independently interprets that intent into complete expected file sets:
exact bytes, paths and persistent identity keys. It imports no production merge exact bytes, paths and persistent identity keys. It imports no production merge
implementation and never adopts observed output as the expected state. Deleted implementation and never adopts observed output as the expected state. Deleted

View file

@ -6,8 +6,12 @@ import { Logger } from "sync-client";
import { ServerControl } from "../../deterministic-tests/src/server-control"; import { ServerControl } from "../../deterministic-tests/src/server-control";
import { ServerManager } from "../../deterministic-tests/src/server-manager"; import { ServerManager } from "../../deterministic-tests/src/server-manager";
import { TestRunner } from "../../deterministic-tests/src/test-runner"; import { TestRunner } from "../../deterministic-tests/src/test-runner";
import { generateTrace, traceTest, type Trace } from "./workload"; import {
import { compileProgram } from "./model"; generateTrace,
serializeTrace,
traceTest,
type Trace
} from "./workload";
import { minimizeProgram } from "./minimize"; import { minimizeProgram } from "./minimize";
function integer(value: string, zero = false): number { function integer(value: string, zero = false): number {
@ -44,8 +48,7 @@ async function main(): Promise<void> {
: fs.mkdtempSync(path.join(os.tmpdir(), "vault-link-fuzz-")); : fs.mkdtempSync(path.join(os.tmpdir(), "vault-link-fuzz-"));
fs.mkdirSync(artifacts, { recursive: true }); fs.mkdirSync(artifacts, { recursive: true });
const tracePath = path.join(artifacts, `trace-${trace.seed}.json`); const tracePath = path.join(artifacts, `trace-${trace.seed}.json`);
if (trace.version === 2) trace.steps = compileProgram(trace.program!); fs.writeFileSync(tracePath, serializeTrace(trace));
fs.writeFileSync(tracePath, JSON.stringify(trace, null, 2));
console.log(`Replay: node ${__filename} --replay ${tracePath}`); console.log(`Replay: node ${__filename} --replay ${tracePath}`);
const logger = new Logger(); const logger = new Logger();
logger.onLogEmitted.add((line) => console.log(line.message)); logger.onLogEmitted.add((line) => console.log(line.message));
@ -72,7 +75,7 @@ async function main(): Promise<void> {
JSON.stringify(result, null, 2) JSON.stringify(result, null, 2)
); );
if (!result.success) { if (!result.success) {
if (trace.program && !values["no-shrink"]) { if (trace.version === 2 && !values["no-shrink"]) {
try { try {
const reduced = await minimizeProgram( const reduced = await minimizeProgram(
trace.program, trace.program,
@ -82,8 +85,7 @@ async function main(): Promise<void> {
await server.start(); await server.start();
const candidate = { const candidate = {
...trace, ...trace,
program, program
steps: compileProgram(program)
}; };
const outcome = await new TestRunner( const outcome = await new TestRunner(
server, server,
@ -104,17 +106,13 @@ async function main(): Promise<void> {
); );
const minimized = { const minimized = {
...trace, ...trace,
program: reduced.program, program: reduced.program
steps: compileProgram(reduced.program)
}; };
const minimizedPath = path.join( const minimizedPath = path.join(
artifacts, artifacts,
`minimized-${trace.seed}.json` `minimized-${trace.seed}.json`
); );
fs.writeFileSync( fs.writeFileSync(minimizedPath, serializeTrace(minimized));
minimizedPath,
JSON.stringify(minimized, null, 2)
);
console.log( console.log(
`Reduced in ${reduced.attempts} attempts: node ${__filename} --replay ${minimizedPath}` `Reduced in ${reduced.attempts} attempts: node ${__filename} --replay ${minimizedPath}`
); );

View file

@ -1,9 +1,42 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { test } from "node:test"; import { test } from "node:test";
import { generateTrace, traceTest } from "./workload"; import { generateTrace, serializeTrace, traceTest } from "./workload";
import { compileProgram, type Program } from "./model"; import { compileProgram, type Program } from "./model";
import { minimizeProgram } from "./minimize"; import { minimizeProgram } from "./minimize";
test("seed 1329 at 500 iterations serializes a compact, complete replay", () => {
const trace = generateTrace(1329, 500);
const serialized = serializeTrace(trace);
assert.equal(serialized, JSON.stringify(trace, null, 2));
assert(
serialized.length < 10_000_000,
"Trace should store editor intent once"
);
const replay: unknown = JSON.parse(serialized);
assert.deepEqual(replay, trace);
const { steps } = traceTest(replay);
assert.equal(steps.length, 11883);
assert.equal(
steps.filter((step) => step.type === "assert-documents").length,
720
);
});
test("older version 2 traces discard compiled steps when saved and replay from intent", () => {
const trace = generateTrace(0, 1);
const expected = traceTest(trace);
const oldTrace = { ...trace, steps: expected.steps };
const serialized = serializeTrace(oldTrace);
assert.equal(serialized, serializeTrace(trace));
assert.deepEqual(traceTest(JSON.parse(serialized)), expected);
assert.equal(
oldTrace.steps,
expected.steps,
"Saving must not mutate the trace"
);
assert.deepEqual(traceTest({ ...oldTrace, steps: [] }), expected);
});
test("stateful programs replay primitive batches, crash boundaries, byte domains and conflict histories", () => { test("stateful programs replay primitive batches, crash boundaries, byte domains and conflict histories", () => {
const seen = new Set<string>(); const seen = new Set<string>();
let large = false, let large = false,
@ -19,9 +52,8 @@ test("stateful programs replay primitive batches, crash boundaries, byte domains
traceTest(trace), traceTest(trace),
traceTest(JSON.parse(JSON.stringify(trace))) traceTest(JSON.parse(JSON.stringify(trace)))
); );
assert(trace.program);
large ||= trace.program.initial.length >= 70; large ||= trace.program.initial.length >= 70;
for (const step of trace.steps) seen.add(step.type); for (const step of traceTest(trace).steps) seen.add(step.type);
for (const epoch of trace.program.epochs) { for (const epoch of trace.program.epochs) {
assert(epoch.actions.length); assert(epoch.actions.length);
for (let i = 0; i < epoch.actions.length; i++) { for (let i = 0; i < epoch.actions.length; i++) {
@ -123,7 +155,7 @@ test("shrinking retains the failing editor intent and regenerates a complete ind
test("shrinking rejects dependency-invalid histories and different failure mechanisms", async () => { test("shrinking rejects dependency-invalid histories and different failure mechanisms", async () => {
const trace = generateTrace(0, 1); const trace = generateTrace(0, 1);
const original = trace.program!; const original = trace.program;
const reduced = await minimizeProgram( const reduced = await minimizeProgram(
original, original,
"Byte mismatch at a.md", "Byte mismatch at a.md",

View file

@ -1,13 +1,12 @@
import { Random } from "../../test-support/random"; import { Random } from "../../test-support/random";
import { import {
compileProgram,
ReferenceModel, ReferenceModel,
type Content, type Content,
type Epoch, type Epoch,
type Mutation, type Mutation,
type Program type Program
} from "./model"; } from "./model";
import type { Trace } from "./workload"; import type { StatefulTrace } from "./workload";
const words = [ const words = [
"alpha", "alpha",
@ -42,7 +41,10 @@ export function randomContent(random: Random): Content {
/** Random primitive histories share a persistent model, including conflict /** Random primitive histories share a persistent model, including conflict
* copies. Only checkpoints settle the system; each epoch composes 2–12 edits. */ * copies. Only checkpoints settle the system; each epoch composes 2–12 edits. */
export function generateStatefulTrace(seed: number, iterations: number): Trace { export function generateStatefulTrace(
seed: number,
iterations: number
): StatefulTrace {
const random = new Random(seed); const random = new Random(seed);
const clients = 3; const clients = 3;
const model = new ReferenceModel(); const model = new ReferenceModel();
@ -475,7 +477,6 @@ export function generateStatefulTrace(seed: number, iterations: number): Trace {
seed, seed,
iterations, iterations,
clients, clients,
program, program
steps: compileProgram(program)
}; };
} }

View file

@ -1,14 +1,19 @@
import { test } from "node:test"; import { test } from "node:test";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { generateLegacyTrace as generateTrace, traceTest } from "./workload"; import {
generateLegacyTrace as generateTrace,
serializeTrace,
traceTest
} from "./workload";
test("concrete traces replay all choices and retain preservation assertions with delayed events", () => { test("concrete traces replay all choices and retain preservation assertions with delayed events", () => {
const trace = generateTrace(42, 12); const trace = generateTrace(42, 12);
assert.equal(serializeTrace(trace), JSON.stringify(trace, null, 2));
assert.deepEqual(trace, generateTrace(42, 12)); assert.deepEqual(trace, generateTrace(42, 12));
assert.notDeepEqual(trace, generateTrace(43, 12)); assert.notDeepEqual(trace, generateTrace(43, 12));
assert.deepEqual( assert.deepEqual(
traceTest(trace), traceTest(trace),
traceTest(JSON.parse(JSON.stringify(trace))) traceTest(JSON.parse(serializeTrace(trace)))
); );
assert.equal( assert.equal(
trace.steps.filter((step) => step.type === "assert-markers").length, trace.steps.filter((step) => step.type === "assert-markers").length,

View file

@ -7,23 +7,48 @@ import type {
} from "../../deterministic-tests/src/test-definition"; } from "../../deterministic-tests/src/test-definition";
import { Random } from "../../test-support/random"; import { Random } from "../../test-support/random";
export interface Trace { interface TraceMetadata {
version: 1 | 2;
program?: Program;
seed: number; seed: number;
iterations: number; iterations: number;
}
export interface LegacyTrace extends TraceMetadata {
version: 1;
/** Old traces omit this and retain their original two-client behavior. */ /** Old traces omit this and retain their original two-client behavior. */
clients?: number; clients?: number;
steps: TestStep[]; steps: TestStep[];
} }
export interface StatefulTrace extends TraceMetadata {
version: 2;
clients: number;
program: Program;
/** Older artifacts included these; replay always compiles the program. */
steps?: TestStep[];
}
export type Trace = LegacyTrace | StatefulTrace;
/** Full-state checkpoints can exceed V8's string limit on long campaigns.
* Persist their source program once, including when rewriting old artifacts. */
export function serializeTrace(trace: Trace): string {
return JSON.stringify(
trace.version === 2 ? { ...trace, steps: undefined } : trace,
null,
2
);
}
type Document = ExpectedDocument & { content: string }; type Document = ExpectedDocument & { content: string };
/** Concrete actions and independent expected states are serialized together. /** Concrete actions and independent expected states are serialized together.
* Every round checks the whole history, including earlier bytes and UUIDs. * Every round checks the whole history, including earlier bytes and UUIDs.
* A shuffled cycle covers nine actions; most revisit existing documents. * A shuffled cycle covers nine actions; most revisit existing documents.
*/ */
export function generateLegacyTrace(seed: number, iterations: number): Trace { export function generateLegacyTrace(
seed: number,
iterations: number
): LegacyTrace {
const random = new Random(seed); const random = new Random(seed);
const clients = 2 + random.int(2); const clients = 2 + random.int(2);
const steps: TestStep[] = []; const steps: TestStep[] = [];
@ -297,12 +322,13 @@ export function generateLegacyTrace(seed: number, iterations: number): Trace {
} }
export function traceTest(trace: Trace): TestDefinition { export function traceTest(trace: Trace): TestDefinition {
const program = trace.version === 2 ? trace.program : undefined;
if ( if (
![1, 2].includes(trace.version) || ![1, 2].includes(trace.version) ||
(trace.version === 1 && (trace.version === 1 &&
(!Array.isArray(trace.steps) || !trace.steps.length)) || (!Array.isArray(trace.steps) || !trace.steps.length)) ||
(trace.version === 2 && (trace.version === 2 &&
(!trace.program || trace.clients !== trace.program.clients)) || (!program || trace.clients !== program.clients)) ||
(trace.clients !== undefined && (trace.clients !== undefined &&
(!Number.isInteger(trace.clients) || trace.clients < 2)) (!Number.isInteger(trace.clients) || trace.clients < 2))
) )
@ -310,8 +336,7 @@ export function traceTest(trace: Trace): TestDefinition {
return { return {
clients: trace.clients ?? 2, clients: trace.clients ?? 2,
description: `Seed ${trace.seed}; ${trace.iterations} generated rounds`, description: `Seed ${trace.seed}; ${trace.iterations} generated rounds`,
steps: steps: trace.version === 2 ? compileProgram(trace.program) : trace.steps
trace.version === 2 ? compileProgram(trace.program!) : trace.steps
}; };
} }