From 0bbd635ab811b6d6c05cb73188225dbce2d31aa1 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:50:11 +0800 Subject: [PATCH] fix(environment): preserve clock years and exact probe chronology Reuse the shared offset-instant owner for calendar validation and exact probe ordering. Preserve first-century years, arbitrary recorded fraction digits in the supported offset profile, ordinal ties and legacy native-Date fallback. Keep original clock spelling when conversion leaves the supported four-digit year range, and refuse unrepresentable generated clocks before committing state. Validation: focused clock/comparison and public CLI regressions cover leap years, offset equivalence, precise DELETE/GET order, spelling classification and refusal atomicity. Replay/state compatibility checks pass. Numeric state intervals and generated-clock progression retain their documented millisecond limits. Fixes #6835 Fixes #6643 --- docs/agent-environments.md | 11 +- .../environment/environmentCompare.test.ts | 114 ++++++++++++--- .../environmentCompareDiff.test.ts | 15 ++ .../environment/environmentCompareProbe.ts | 35 ++++- .../environmentStateWriteAnswer.test.ts | 19 ++- .../environmentStateWriteAnswer.ts | 10 +- .../environmentStateWriteValues.test.ts | 42 ++++++ .../environmentStateWriteValues.ts | 74 +++++----- .../cli/environmentCompareCli.test.ts | 135 +++++++++++++++++- 9 files changed, 383 insertions(+), 72 deletions(-) diff --git a/docs/agent-environments.md b/docs/agent-environments.md index e1d0689554..59106bb20a 100644 --- a/docs/agent-environments.md +++ b/docs/agent-environments.md @@ -774,7 +774,16 @@ or `page`. Each call is checked against the served tool's input schema first, as `at`, `time`, `date`, `timestamp`, `created`, `updated` or `modified` field names, including snake-case and camel-case forms), and two different values each side generated on its own compare equal: both UUIDs, or one length of eight or more with the same character classes and a digit. The report counts each kind of normalised value. ISO clock offsets are accepted only within RFC 3339's `00`–`23` hour and `00`–`59` minute ranges. -Timestamp ordering retains submillisecond precision when the source records it. +Probe ordering retains every recorded fractional-second digit in ISO timestamps +with a four-digit year, `T`, and a `Z` or `±HH:MM` suffix. Equal instants use ordinal order. Other native-Date +spellings keep their existing millisecond interpretation; an absent or unreadable +start time makes the whole probe use record order. Original capture timestamps +remain unchanged. Clock-format comparison and respelling recognize four-digit +ISO years `0000`–`0099` as well as later years, with normal calendar validation. +Respelling preserves the original value when the target zone would require a year +outside `0000`–`9999`; generated writes refuse a clock outside that format. +Capture-state extraction still uses millisecond coordinates for request intervals +and their ordering, and generated-clock progression compares milliseconds. Comparison keeps response representations distinct: JSON values compare structurally, text compares as text, and a retained base64 body compares by bytes (including with UTF-8 text carrying those same bytes), never by its base64 diff --git a/src/application/environment/environmentCompare.test.ts b/src/application/environment/environmentCompare.test.ts index fd8853fe44..d00523dc2f 100644 --- a/src/application/environment/environmentCompare.test.ts +++ b/src/application/environment/environmentCompare.test.ts @@ -36,7 +36,7 @@ import { parameterScopes, } from "./environmentCompareMatch.js"; import type { EnvironmentManifestTool } from "./environmentSource.js"; -import { isCut } from "./environmentCompareProbe.js"; +import { isCut, probeRequests } from "./environmentCompareProbe.js"; import { parseCaptureEvidence } from "./environmentSource.js"; const fixture = environmentStateServeFixture(timedCaptureEvidence(BOOK_ROWS)); @@ -644,29 +644,99 @@ describe("browser probes", () => { }); }); -describe("what a probe did not observe", () => { - const at = (second: number): string => - new Date( - Date.parse("2026-10-20T09:00:00.000Z") + second * 1000, - ).toISOString(); - const exchange = ( - second: number, - method: string, - url: string | null, - status: number, - ) => ({ - startedDateTime: at(second), - request: { method, url }, - response: { status, content: { mimeType: "application/json" } }, +const at = (second: number): string => + new Date( + Date.parse("2026-10-20T09:00:00.000Z") + second * 1000, + ).toISOString(); +const exchange = ( + second: number, + method: string, + url: string | null, + status: number, +) => ({ + startedDateTime: at(second), + request: { method, url }, + response: { status, content: { mimeType: "application/json" } }, +}); +const run = (value: unknown) => + compareEnvironment({ + source, + captureValue: fixture.capture, + probes: [{ label: "probe-1", value }], + operation: "environment-compare", }); - const run = (value: unknown) => - compareEnvironment({ - source, - captureValue: fixture.capture, - probes: [{ label: "probe-1", value }], - operation: "environment-compare", - }); +describe("probe request chronology", () => { + it.each([ + ["2026-10-20T09:00:00.0002Z", "2026-10-20T09:00:00.0001Z"], + ["2026-10-20T09:00:00.0000000002Z", "2026-10-20T09:00:00.0000000001Z"], + ["0001-01-01T00:00:00.0002Z", "0001-01-01T01:00:00.0001+01:00"], + ])( + "replays an earlier delete before a later submillisecond read (%s)", + (later, earlier) => { + const report = run( + analysisViewCaptureEvidence( + environmentManifestCaptureFixture([ + { + ...exchange(1, "GET", "http://app.example/api/books/2", 404), + startedDateTime: later, + response: { + status: 404, + content: { + mimeType: "application/json", + text: JSON.stringify(BOOK_NOT_FOUND), + }, + }, + }, + { + ...exchange(0, "DELETE", "http://app.example/api/books/2", 204), + startedDateTime: earlier, + }, + ]), + ), + ); + if (!report.ok) throw report.error; + expect( + report.value.probes[0]?.results.map(({ ordinal, env_status }) => [ + ordinal, + env_status, + ]), + ).toEqual([ + [1, 204], + [0, 404], + ]); + expect(report.value.summary).toMatchObject({ + steps: 2, + equal: 2, + agreement: 1, + }); + }, + ); + + it.each([ + ["2026-10-20T09:00:00.000100Z", "2026-10-20T10:00:00.0001+01:00"], + ["Tue, 20 Oct 2026 09:00:00 GMT", "2026-10-20T09:00:00Z"], + ])( + "keeps ordinal order for equal instants and native-Date spelling compatibility (%s)", + (left, right) => { + const capture = environmentManifestCaptureFixture([ + { + ...exchange(0, "GET", "http://app.example/api/books/1", 200), + startedDateTime: left, + }, + { + ...exchange(1, "GET", "http://app.example/api/books/2", 200), + startedDateTime: right, + }, + ]); + expect( + probeRequests(capture.records).steps.map(({ ordinal }) => ordinal), + ).toEqual([0, 1]); + }, + ); +}); + +describe("what a probe did not observe", () => { it("compares an unrecorded body by status only and lists the records it could not compare", () => { const report = run( analysisViewCaptureEvidence( diff --git a/src/application/environment/environmentCompareDiff.test.ts b/src/application/environment/environmentCompareDiff.test.ts index 315a0d4c90..0d5b160adc 100644 --- a/src/application/environment/environmentCompareDiff.test.ts +++ b/src/application/environment/environmentCompareDiff.test.ts @@ -16,6 +16,21 @@ const compare = ( map, ); +describe("early-year clock formats", () => { + it.each(["0000", "0001", "0099", "0100"])( + "classifies a year-%s clock spelling difference by format", + (year) => { + const result = compare( + { created_at: `${year}-01-01T00:00:00Z` }, + { created_at: `${year}-01-01 00:00:00.000Z` }, + ); + expect( + result.differences.map(({ category, pointer }) => [category, pointer]), + ).toEqual([["clock-format", "/created_at"]]); + }, + ); +}); + describe("compareAnswers", () => { it("agrees on mapped ids, clocks of one format and generated values, and counts each", () => { const map = new IdMap(); diff --git a/src/application/environment/environmentCompareProbe.ts b/src/application/environment/environmentCompareProbe.ts index e35d9af933..4b6cc90711 100644 --- a/src/application/environment/environmentCompareProbe.ts +++ b/src/application/environment/environmentCompareProbe.ts @@ -12,6 +12,13 @@ import { } from "../../domain/analysisView/webNetworkRouteTemplates.js"; import { parseExactJson } from "../../domain/exactJsonNumber.js"; import type { JsonValue } from "../../domain/jsonValue.js"; +import { + compareOffsetDateTimeInstants, + offsetDateTimeInstantFromEpochMilliseconds, + offsetDateTimeSchema, + parseOffsetDateTimeInstant, + type OffsetDateTimeInstant, +} from "../../domain/offsetDateTime.js"; import type { WebNetworkCaptureRecord } from "../../domain/webNetworkCapture.js"; import type { Answer } from "./environmentCompareDiff.js"; import type { ProbeRequest } from "./environmentCompareMatch.js"; @@ -46,10 +53,20 @@ const postDataSchema = z.object({ const methodSchema = z.object({ request: z.object({ method: z.string() }) }); -const startedMs = (record: WebNetworkCaptureRecord): number | undefined => { +const startedInstant = ( + record: WebNetworkCaptureRecord, +): OffsetDateTimeInstant | undefined => { const started = startedSchema.safeParse(record.reported); - const ms = started.success ? Date.parse(started.data.startedDateTime) : NaN; - return Number.isFinite(ms) ? ms : undefined; + if (!started.success) return undefined; + const timestamp = offsetDateTimeSchema.safeParse( + started.data.startedDateTime, + ); + if (timestamp.success) return parseOffsetDateTimeInstant(timestamp.data); + // Preserve native-Date admission for legacy spellings, at its original precision. + const ms = Date.parse(started.data.startedDateTime); + return Number.isFinite(ms) + ? offsetDateTimeInstantFromEpochMilliseconds(ms) + : undefined; }; /** Whether a probe makes calls that carry a request body: a POST, PUT or PATCH. */ @@ -118,13 +135,17 @@ export const probeRequests = ( const unobserved: UnobservedRecord[] = []; // Without a start time for every record the probe keeps its recorded order, as state extraction does. const starts = new Map( - records.map((record) => [record.ordinal, startedMs(record)]), + records.map((record) => [record.ordinal, startedInstant(record)]), ); - const timed = [...starts.values()].every((ms) => ms !== undefined); + const timed = [...starts.values()].every((instant) => instant !== undefined); const ordered = [...records].sort( (left, right) => - (timed ? starts.get(left.ordinal)! - starts.get(right.ordinal)! : 0) || - left.ordinal - right.ordinal, + (timed + ? compareOffsetDateTimeInstants( + starts.get(left.ordinal)!, + starts.get(right.ordinal)!, + ) + : 0) || left.ordinal - right.ordinal, ); const lines = new Map( records.map((record) => [ diff --git a/src/application/environment/environmentStateWriteAnswer.test.ts b/src/application/environment/environmentStateWriteAnswer.test.ts index 3f0007db34..6dc0ec7da1 100644 --- a/src/application/environment/environmentStateWriteAnswer.test.ts +++ b/src/application/environment/environmentStateWriteAnswer.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { analysisViewCaptureEvidence, @@ -183,6 +183,23 @@ describe("confirmed write response bodies", () => { ); }); +it("refuses an unrepresentable generated clock without changing the held state", () => { + const books = servedFrom(); + const pending = books.store.pendingWrite(); + const pendingWrite = books.store.pendingWrite.bind(books.store); + vi.spyOn(books.store, "pendingWrite").mockImplementation(() => ({ + ...pendingWrite(), + clockMs: Date.parse("0000-01-01T00:00:00+01:00"), + })); + const before = books.snapshot(); + const refusal = books.refusal("post_books", CREATE_NEW_BOOK); + expect(JSON.stringify(refusal)).toContain( + "outside its recorded four-digit year format", + ); + expect(books.snapshot()).toBe(before); + expect(books.store.pendingWrite().count).toBe(pending.count); +}); + describe("served creates", () => { it("refuses an exhausted generated-value collision search without changing state", () => { const existing = { diff --git a/src/application/environment/environmentStateWriteAnswer.ts b/src/application/environment/environmentStateWriteAnswer.ts index 114d9e8903..620452e9f9 100644 --- a/src/application/environment/environmentStateWriteAnswer.ts +++ b/src/application/environment/environmentStateWriteAnswer.ts @@ -201,8 +201,14 @@ const generator = ( const seed = (attempt: number) => `${store.state.evidenceId}:${request.tool.name}:${rule.field}:${String(write.count)}:${String(attempt)}`; switch (rule.rule) { - case "clock": - return ok(formatClock(write.clockMs, rule.format)); + case "clock": { + const value = formatClock(write.clockMs, rule.format); + return value === undefined + ? err( + `The generated clock for ${rule.field} is outside its recorded four-digit year format.`, + ) + : ok(value); + } case "sequence": return ok( spelledLike( diff --git a/src/domain/analysisView/environmentStateWriteValues.test.ts b/src/domain/analysisView/environmentStateWriteValues.test.ts index dc4ef3c564..275b937715 100644 --- a/src/domain/analysisView/environmentStateWriteValues.test.ts +++ b/src/domain/analysisView/environmentStateWriteValues.test.ts @@ -33,6 +33,27 @@ const seeds = (count: number): string[] => Array.from({ length: count }, (_, index) => `seed-${String(index)}`); it.each<[string, number, ClockFormat, number?]>([ + [ + "0000-02-29T00:00:00.000123456Z", + Date.parse("0000-02-29T00:00:00Z"), + iso("T", 9, "Z"), + 123456, + ], + [ + "0001-01-01T00:00:00Z", + Date.parse("0001-01-01T00:00:00Z"), + iso("T", 0, "Z"), + ], + [ + "0099-01-01T00:00:00Z", + Date.parse("0099-01-01T00:00:00Z"), + iso("T", 0, "Z"), + ], + [ + "0100-01-01T00:00:00Z", + Date.parse("0100-01-01T00:00:00Z"), + iso("T", 0, "Z"), + ], ["2026-03-04T05:06:07Z", WHOLE_SECOND, iso("T", 0, "Z")], ["2026-03-04 05:06:07.123Z", WITH_MS, iso(" ", 3, "Z")], ["2026-03-04T05:06:07.123456Z", WITH_MS, iso("T", 6, "Z"), 456000], @@ -53,6 +74,8 @@ it.each<[string, number, ClockFormat, number?]>([ ); it.each([ + "0001-02-29T00:00:00Z", + "0100-02-29T00:00:00Z", "2026-02-30T00:00:00Z", "2026-02-29T00:00:00Z", "2026-13-01T00:00:00Z", @@ -72,6 +95,25 @@ it.each([ expect(readClock(text)).toBeUndefined(); }); +it.each(["0000", "0001", "0099", "0100"])( + "respells year %s without a numeric-year century adjustment", + (year) => { + expect(respellClock(`${year}-01-01T00:00:00Z`, iso(" ", 3, "Z"))).toBe( + `${year}-01-01 00:00:00.000Z`, + ); + }, +); + +it.each(["0000-01-01T00:00:00+01:00", "9999-12-31T23:59:59-01:00"])( + "keeps %s when conversion cannot fit the four-digit year format", + (value) => { + const clock = readClock(value)!; + expect(respellClock(value, iso("T", 0, "Z"))).toBe(value); + expect(formatClock(clock.ms, iso("T", 0, "Z"))).toBeUndefined(); + expect(formatClock(clock.ms, clock.format)).toBe(value); + }, +); + it.each<[JsonValue | undefined, ReturnType]>([ [ WHOLE_SECOND / 1000, diff --git a/src/domain/analysisView/environmentStateWriteValues.ts b/src/domain/analysisView/environmentStateWriteValues.ts index 6048e64917..d9f9691057 100644 --- a/src/domain/analysisView/environmentStateWriteValues.ts +++ b/src/domain/analysisView/environmentStateWriteValues.ts @@ -1,5 +1,9 @@ import { createHash } from "node:crypto"; import type { JsonValue } from "../jsonValue.js"; +import { + offsetDateTimeSchema, + parseOffsetDateTimeInstant, +} from "../offsetDateTime.js"; /** How a recorded clock value is spelled: ISO-8601 with its separator, fraction digits and zone, or an epoch number. */ export type ClockFormat = @@ -19,7 +23,7 @@ interface ClockReading { } const ISO_INSTANT = - /^(\d{4})-(\d{2})-(\d{2})([T ])(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}:?\d{2})?$/u; + /^\d{4}-\d{2}-\d{2}([T ])\d{2}:\d{2}:\d{2}(?:\.(\d{1,9}))?(Z|[+-]\d{2}:?\d{2})?$/u; const MINUTE_MS = 60_000; /** Minutes east of UTC that a zone suffix names; none and `Z` are UTC. */ @@ -49,34 +53,22 @@ const zoneMinutesForFormat = (zone: string): number => { const readIsoClock = (value: string): ClockReading | undefined => { const match = ISO_INSTANT.exec(value); if (match === null) return undefined; - const [year, month, day, separator, hour, minute, second] = match - .slice(1, 8) - .map((part) => (part === "T" || part === " " ? part : Number(part))); - const fraction = match[8] ?? ""; - const zone = match[9] ?? ""; - const offsetMinutes = zoneMinutes(zone); - if (offsetMinutes === undefined) return undefined; - const local = Date.UTC( - year as number, - (month as number) - 1, - day as number, - hour as number, - minute as number, - second as number, - Number(`${fraction}000`.slice(0, 3)), + const [, separator, fraction = "", zone = ""] = match; + // Clock spelling also permits a space, compact offsets and implicit UTC. + // Normalize those spellings before the shared owner validates the calendar. + const offset = + zone === "" + ? "Z" + : zone === "Z" || zone.includes(":") + ? zone + : `${zone.slice(0, 3)}:${zone.slice(3)}`; + const timestamp = offsetDateTimeSchema.safeParse( + `${value.slice(0, 10)}T${value.slice(11, 19)}${fraction === "" ? "" : `.${fraction}`}${offset}`, ); - // Date.UTC rolls invalid fields over; the round trip refuses them. - const rolled = new Date(local); - if ( - rolled.getUTCFullYear() !== year || - rolled.getUTCMonth() !== (month as number) - 1 || - rolled.getUTCDate() !== day || - rolled.getUTCHours() !== hour || - rolled.getUTCMinutes() !== minute - ) - return undefined; + if (!timestamp.success) return undefined; + const instant = parseOffsetDateTimeInstant(timestamp.data); return { - ms: local - offsetMinutes * MINUTE_MS, + ms: instant.epochSecond * 1000 + Number(`${fraction}000`.slice(0, 3)), subMillisecond: Number(fraction.padEnd(9, "0").slice(3)), format: { kind: "iso", @@ -118,11 +110,17 @@ const isoText = ( separator: string, digits: string, zone: string, -): string => - `${pad(at.getUTCFullYear(), 4)}-${pad(at.getUTCMonth() + 1, 2)}-${pad(at.getUTCDate(), 2)}${separator}${pad(at.getUTCHours(), 2)}:${pad(at.getUTCMinutes(), 2)}:${pad(at.getUTCSeconds(), 2)}${digits === "" ? "" : `.${digits}`}${zone}`; +): string | undefined => { + const year = at.getUTCFullYear(); + if (!Number.isInteger(year) || year < 0 || year > 9999) return undefined; + return `${pad(year, 4)}-${pad(at.getUTCMonth() + 1, 2)}-${pad(at.getUTCDate(), 2)}${separator}${pad(at.getUTCHours(), 2)}:${pad(at.getUTCMinutes(), 2)}:${pad(at.getUTCSeconds(), 2)}${digits === "" ? "" : `.${digits}`}${zone}`; +}; -/** An instant spelled in a recorded clock format; fraction digits past milliseconds are zeros. */ -export const formatClock = (ms: number, format: ClockFormat): JsonValue => { +/** An instant in a recorded format; undefined outside its four-digit ISO year range. Fraction digits past milliseconds are zeros. */ +export const formatClock = ( + ms: number, + format: ClockFormat, +): JsonValue | undefined => { if (format.kind === "epoch") return format.unit === "s" ? Math.floor(ms / 1000) : ms; const at = new Date(ms + zoneMinutesForFormat(format.zone) * MINUTE_MS); @@ -144,7 +142,7 @@ export const respellClock = ( ): JsonValue => { const clock = readClock(value); if (clock === undefined) return value; - if (format.kind === "epoch") return formatClock(clock.ms, format); + if (format.kind === "epoch") return formatClock(clock.ms, format) ?? value; const millis = ((clock.ms % 1000) + 1000) % 1000; // An epoch value's fraction is its milliseconds, without trailing zeros. const digits = @@ -157,11 +155,13 @@ export const respellClock = ( const at = new Date( clock.ms - millis + zoneMinutesForFormat(format.zone) * MINUTE_MS, ); - return isoText( - at, - format.separator, - digits.padEnd(format.fraction, "0"), - format.zone, + return ( + isoText( + at, + format.separator, + digits.padEnd(format.fraction, "0"), + format.zone, + ) ?? value ); }; diff --git a/tests/boundary/cli/environmentCompareCli.test.ts b/tests/boundary/cli/environmentCompareCli.test.ts index 9d95a210f2..49cfe168bd 100644 --- a/tests/boundary/cli/environmentCompareCli.test.ts +++ b/tests/boundary/cli/environmentCompareCli.test.ts @@ -1,13 +1,18 @@ -import { writeFile } from "node:fs/promises"; +import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { describe, expect } from "vitest"; -import { timedCaptureEvidence } from "../../fixtures/analysisView.js"; +import { + analysisViewCaptureEvidence, + environmentManifestCaptureFixture, + timedCaptureEvidence, +} from "../../fixtures/analysisView.js"; import { environmentStateServeFixture } from "../../fixtures/environmentServe.js"; import { BOOK_NOT_FOUND, BOOK_ROWS, + FIRST_BOOK, book, } from "../../fixtures/environmentStateWrites.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; @@ -34,7 +39,133 @@ const writeCompareFiles = async (root: string) => { return paths; }; +/** The exact chronology and clock-format probes share one unchanged source capture. */ +const writeExactClockFiles = async ( + root: string, +): Promise> => { + const capture = timedCaptureEvidence([ + ["GET", "/api/books", [FIRST_BOOK]], + ["GET", "/api/books/1", FIRST_BOOK], + ["DELETE", "/api/books/1", "", 204], + ["GET", "/api/books/1", BOOK_NOT_FOUND, 404], + ]); + const fixture = environmentStateServeFixture(capture); + const ordered = analysisViewCaptureEvidence( + environmentManifestCaptureFixture([ + { + startedDateTime: "0001-01-01T00:00:00.0000000002Z", + request: { method: "GET", url: "http://app.example/api/books/1" }, + response: { + status: 404, + content: { + mimeType: "application/json", + text: JSON.stringify(BOOK_NOT_FOUND), + }, + }, + }, + { + startedDateTime: "0001-01-01T01:00:00.0000000001+01:00", + request: { + method: "DELETE", + url: "http://app.example/api/books/1", + }, + response: { + status: 204, + content: { mimeType: "application/json", text: "" }, + }, + }, + ]), + ); + const clocks = timedCaptureEvidence([ + [ + "GET", + "/api/books/1", + { ...FIRST_BOOK, created_at: "0001-01-01 00:00:00.000Z" }, + ], + ]); + const originals = new Map(); + for (const [name, value] of [ + ["manifest.json", fixture.manifest], + ["capture.json", capture], + ["order.json", ordered], + ["clocks.json", clocks], + ] as const) { + const path = join(root, name); + const text = JSON.stringify(value); + originals.set(path, text); + await writeFile(path, text); + } + return originals; +}; + describe("rea environment-compare", () => { + cliTest( + "preserves exact probe chronology and first-century clock-format differences", + async ({ cli }) => { + const root = await createTestTempDirectory( + "rea-environment-exact-clock-", + ); + const originals = await writeExactClockFiles(root); + const result = await cli.run({ + arguments: [ + "environment-compare", + join(root, "manifest.json"), + "--evidence", + join(root, "capture.json"), + "--probe", + join(root, "order.json"), + "--probe", + join(root, "clocks.json"), + "--json", + ], + }); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.json).toMatchObject({ + status: "complete", + environment: { state: "served" }, + summary: { probes: 2, steps: 3, equal: 2 }, + probes: [ + { + label: "order.json", + results: [ + { + ordinal: 1, + method: "DELETE", + live_status: 204, + env_status: 204, + equal: true, + }, + { + ordinal: 0, + method: "GET", + live_status: 404, + env_status: 404, + equal: true, + }, + ], + }, + { + label: "clocks.json", + results: [ + { + categories: ["clock-format"], + differences: [ + { + category: "clock-format", + pointer: "/created_at", + live: '"0001-01-01 00:00:00.000Z"', + }, + ], + }, + ], + }, + ], + }); + for (const [path, text] of originals) + expect(await readFile(path, "utf8")).toBe(text); + }, + ); + cliTest( "compares the served environment with a probe of the re-seeded application and prints the report", async ({ cli }) => {