Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
280 changes: 280 additions & 0 deletions documentation/server-components/frames-consistency-contract.md

Large diffs are not rendered by default.

67 changes: 67 additions & 0 deletions packages/web/test/consistency/generic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Generic hydration harness

The frames-free arm of the consistency contract
(`documentation/server-components/frames-consistency-contract.md`, §"Generic
hydration — classification and pins"). Same idea as `../harness` — a
fast-check schedule generator plus an oracle of the contract's laws — over a
PLAIN Solid 2 page: `hydrate()` of a `renderToStream` document with two
sibling streamed `<Loading>` boundaries reading module-level state
(`test/harness/generic-hydration.tsx`). No frames, slots or records.

## Run

```sh
# the pins (always on): reds as test.fails, controls and holds as test
npx vitest run --config vite.config.hydrate.mjs test/consistency/generic

# the campaign (opt-in), same knobs as ../harness
CONSISTENCY_FUZZ=1 CONSISTENCY_SEED=3289 CONSISTENCY_CASES=500 \
npx vitest run --config vite.config.hydrate.mjs test/consistency/generic
CONSISTENCY_FUZZ=1 CONSISTENCY_SEED=91501 CONSISTENCY_CASES=500 CONSISTENCY_IGNORE=C1,C9,C19,E ...
CONSISTENCY_FUZZ=1 CONSISTENCY_MODE=shrink CONSISTENCY_IGNORE=C19 ...

# replaying: GENERIC_DEBUG=1 prints the page and the sources after every step
GENERIC_DEBUG=1 npx vitest run --config vite.config.hydrate.mjs test/consistency/generic/replay.spec.tsx -t "GH4" --silent=false
```

The page's server render is an artifact: `test/harness/__artifacts__/
generic-hydration-{ab,ba}.json` (`{ shell, chunks }`, one per fragment
order), written by `test/server/generic-hydration.gen.spec.tsx` under the
server config. Regenerate it after changing the shared page:

```sh
npx vitest run --config vite.config.server.mjs test/server/generic-hydration.gen.spec.tsx
```

## Scenario (`scenario.ts`)

`[order] :: H C0 W C1 t P C2 Ea X` — `H` hydrate, `Cn` the stream's n-th
chunk parses (markup appended, scripts run; wire order kept), `W` a client
write to the module-level signal (`setPath("/b")`), `P` a push to the
module-level store list, `Ea`/`Eb` a click on a boundary's button (queued by
the bootstrap-shaped capture if its range is not claimed yet), `t` a settle
(20ms — an in-flight client async re-run lands), `m` a microtask, `X` dispose.
`W`, `P` and `X` are normalized after `H` (a write before `hydrate()` is an
app mismatch, outside the contract).

## Laws (`run.tsx`)

| when | id | law |
| --------- | --- | --------------------------------------------------------------- |
| immediate | G | `no-runtime-error` |
| immediate | C1 | `no-key-miss`, `no-unclaimed`, `node-identity`, `no-duplicate` |
| immediate | C9 | `no-fallback-over-settled` (a client fallback after the reveal) |
| immediate | C3 | `in-progress-until-done` |
| immediate | C14 | `dispose-no-invoke`, `dispose-no-dom` |
| settled | C12 | `fragment-parity` |
| settled | C19 | `claim-shows-signal` / `-memo` / `-async-memo` / `-store-list` |
| end | C3 | `done-counts-holds` |
| end | C2 | `every-range-live`, `every-range-reactive` |
| end | E | `queued-click-replays-once` |

## Limitations

One page shape; the server's chunking is fixed per order; `_hydrationDone`
is a worker latch (every case after the first runs post-done — a reveal
before `hydrate()` is held and replayed at registration); `readyState` is
not mocked.
99 changes: 99 additions & 0 deletions packages/web/test/consistency/generic/campaign.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* @vitest-environment jsdom
*
* The opt-in property campaign of the GENERIC (frames-free) hydration
* harness. Off by default; run with
*
* CONSISTENCY_FUZZ=1 [CONSISTENCY_SEED=3289] [CONSISTENCY_CASES=100]
* [CONSISTENCY_IGNORE=C19] [CONSISTENCY_MODE=survey|shrink]
* npx vitest run --config vite.config.hydrate.mjs test/consistency/generic
*
* Same knobs and modes as test/consistency/harness (survey tallies findings
* by invariant and law; shrink stops on the first finding outside
* CONSISTENCY_IGNORE and prints the reduced scenario as JSON for
* replay.spec.tsx).
*/
import { describe, expect, test } from "vitest";
import fc from "fast-check";
import { runScenario } from "./run.js";
import { describeScenario, scenarioArb, type Scenario } from "./scenario.js";
import { loadArtifact } from "./support.js";

const FUZZ = !!process.env.CONSISTENCY_FUZZ;
const SEED = Number(process.env.CONSISTENCY_SEED ?? 3289);
const CASES = Number(process.env.CONSISTENCY_CASES ?? 100);
const IGNORE = new Set((process.env.CONSISTENCY_IGNORE ?? "").split(",").filter(Boolean));
const MODE = process.env.CONSISTENCY_MODE ?? "survey";

const out = (s: string) => process.stdout.write(s + "\n");
const arb = scenarioArb(order => loadArtifact(order).chunks.length);

describe("generic hydration harness", () => {
test("smoke: the canonical schedule (hydrate, then the chunks) has no finding", async () => {
const art = loadArtifact("ab");
const result = await runScenario({
order: "ab",
chunks: art.chunks.length,
events: [
{ t: "hydrate" },
...art.chunks.map((_, i) => ({ t: "chunk" as const, i })),
{ t: "tick" }
]
});
expect(result.findings).toEqual([]);
});
});

describe.skipIf(!FUZZ)("generic hydration harness campaign", () => {
test(
`seed ${SEED}, ${CASES} cases, mode ${MODE}, ignoring [${[...IGNORE].join(",")}]`,
{ timeout: 0 },
async () => {
if (MODE === "shrink") {
try {
await fc.assert(
fc.asyncProperty(arb, async scenario => {
const result = await runScenario(scenario);
const real = result.findings.filter(f => !IGNORE.has(f.id));
if (real.length) {
const f = real[0];
throw new Error(
`${f.id} ${f.law} @${f.step}: ${f.detail}\n${result.description}\n${JSON.stringify(scenario)}`
);
}
}),
{ seed: SEED, numRuns: CASES, endOnFailure: true }
);
out(`campaign: seed ${SEED}, ${CASES} cases, no finding outside [${[...IGNORE]}]`);
} catch (e: any) {
out(String(e && e.message));
throw e;
}
return;
}
const scenarios = fc.sample(arb, { seed: SEED, numRuns: CASES });
const tally = new Map<string, { count: number; first: Scenario; firstDetail: string }>();
let failing = 0;
for (const scenario of scenarios) {
const result = await runScenario(scenario);
const real = result.findings.filter(f => !IGNORE.has(f.id));
if (real.length) failing++;
const seen = new Set<string>();
for (const f of real) {
const key = `${f.id} ${f.law}`;
if (seen.has(key)) continue;
seen.add(key);
const t = tally.get(key);
if (t) t.count++;
else tally.set(key, { count: 1, first: scenario, firstDetail: f.detail });
}
}
out(`campaign survey: seed ${SEED}, ${CASES} cases, ${failing} with findings`);
for (const [key, t] of [...tally].sort((a, b) => b[1].count - a[1].count)) {
out(` ${key}: ${t.count} cases — e.g. ${t.firstDetail}`);
out(` ${describeScenario(t.first)}`);
out(` ${JSON.stringify(t.first)}`);
}
}
);
});
Loading