diff --git a/docs/error-contract.schema.json b/docs/error-contract.schema.json index 39a04a5baa..07a1207cdc 100644 --- a/docs/error-contract.schema.json +++ b/docs/error-contract.schema.json @@ -691,12 +691,8 @@ "type": "object", "properties": { "logical_path": { "type": "string" }, - "declared_sha256": { - "anyOf": [{ "type": "string" }, { "type": "null" }] - }, - "calculated_sha256": { - "anyOf": [{ "type": "string" }, { "type": "null" }] - }, + "declared_sha256": { "type": ["string", "null"] }, + "calculated_sha256": { "type": ["string", "null"] }, "unpacked": { "type": "boolean" } }, "required": [ diff --git a/licenses/npm-dependencies.txt b/licenses/npm-dependencies.txt index df798b062d..fad83666b5 100644 --- a/licenses/npm-dependencies.txt +++ b/licenses/npm-dependencies.txt @@ -4810,11 +4810,11 @@ yoctocolors@2.2.0 | development | MIT Package: https://www.npmjs.com/package/yoctocolors/v/2.2.0 Archive: https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz Integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg== -zod@4.4.3 | runtime | MIT +zod@4.6.6 | runtime | MIT Lockfile path: node_modules/zod - Package: https://www.npmjs.com/package/zod/v/4.4.3 - Archive: https://registry.npmjs.org/zod/-/zod-4.4.3.tgz - Integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ== + Package: https://www.npmjs.com/package/zod/v/4.6.6 + Archive: https://registry.npmjs.org/zod/-/zod-4.6.6.tgz + Integrity: sha512-4UmzQUF5fxvfcYHlbjBmejU+w+hbDnWFx4aS8kBx7nPBHcO2SqPbpAG8snbdg+Xse4pRefR98hgjFgIMCI/UhA== zwitch@2.0.4 | development | MIT Lockfile path: node_modules/zwitch Package: https://www.npmjs.com/package/zwitch/v/2.0.4 diff --git a/licenses/npm-runtime-notices.txt b/licenses/npm-runtime-notices.txt index 177220131c..e5e51f35fc 100644 --- a/licenses/npm-runtime-notices.txt +++ b/licenses/npm-runtime-notices.txt @@ -1,7 +1,7 @@ REA third-party npm runtime copyright, license and NOTICE texts Collected from integrity-verified locked npm archives. Release-specific supplements and upstream notice gaps are identified in their entries. External engines are separately obtained; see THIRD_PARTY_NOTICES.md. -Runtime dependency fingerprint (SHA-256): d0a0286bb8c158a670b019cdb56ee120542cb072bbecf3741c172b5f06b2301b +Runtime dependency fingerprint (SHA-256): 8f4447e00ab3adb9d6b6ab2850634d23945552fafd06999eb207df4652912ece ===== @babel/helper-string-parser@8.0.6 ===== Declared license: MIT @@ -7261,10 +7261,10 @@ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -===== zod@4.4.3 ===== +===== zod@4.6.6 ===== Declared license: MIT -Archive: https://registry.npmjs.org/zod/-/zod-4.4.3.tgz -Integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ== +Archive: https://registry.npmjs.org/zod/-/zod-4.6.6.tgz +Integrity: sha512-4UmzQUF5fxvfcYHlbjBmejU+w+hbDnWFx4aS8kBx7nPBHcO2SqPbpAG8snbdg+Xse4pRefR98hgjFgIMCI/UhA== Scope: npm runtime dependency, including optional platform packages. --- package/LICENSE --- diff --git a/package-lock.json b/package-lock.json index 7b00adf882..4c82cc98f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,7 @@ "write-file-atomic": "7.0.0", "ws": "8.21.0", "yaml": "2.9.0", - "zod": "4.4.3" + "zod": "4.6.6" }, "bin": { "rea": "scripts/rea.mjs", @@ -13916,9 +13916,9 @@ } }, "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.6.tgz", + "integrity": "sha512-4UmzQUF5fxvfcYHlbjBmejU+w+hbDnWFx4aS8kBx7nPBHcO2SqPbpAG8snbdg+Xse4pRefR98hgjFgIMCI/UhA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 4d81d78fd9..87017d2fbb 100644 --- a/package.json +++ b/package.json @@ -379,7 +379,7 @@ "write-file-atomic": "7.0.0", "ws": "8.21.0", "yaml": "2.9.0", - "zod": "4.4.3" + "zod": "4.6.6" }, "overrides": { "typed-rest-client": { diff --git a/scripts/lib/completion-ledger-generation.mts b/scripts/lib/completion-ledger-generation.mts index c360ecdb6b..6ad8bf7fab 100644 --- a/scripts/lib/completion-ledger-generation.mts +++ b/scripts/lib/completion-ledger-generation.mts @@ -2,6 +2,7 @@ import { z } from "zod"; import { digestCanonicalValue } from "../../src/domain/canonicalDigest.js"; import { IDENTIFIER_PATTERN } from "../../src/domain/stringPatterns.js"; +import { utcDateTimeSchema } from "../../src/domain/offsetDateTime.js"; import { compareAsciiStrings, isSortedUniqueAscii, @@ -44,7 +45,7 @@ const verifierRunSchema = z process_lineage: z.discriminatedUnion("status", [ z.strictObject({ status: z.literal("verified"), - observed_at: z.iso.datetime(), + observed_at: utcDateTimeSchema, launcher_pid: z.int().positive(), launcher_parent_pid: z.int().nonnegative(), process_group_id: z.int().positive(), @@ -58,7 +59,7 @@ const verifierRunSchema = z }), z.strictObject({ status: z.literal("unavailable"), - observed_at: z.iso.datetime(), + observed_at: utcDateTimeSchema, launcher_pid: z.int().positive(), launcher_parent_pid: z.int().nonnegative(), process_group_id: z.int().positive().nullable(), diff --git a/src/contracts/toolOutputSchemaPrimitives.ts b/src/contracts/toolOutputSchemaPrimitives.ts index a4374b60cf..fc412344f6 100644 --- a/src/contracts/toolOutputSchemaPrimitives.ts +++ b/src/contracts/toolOutputSchemaPrimitives.ts @@ -1,5 +1,6 @@ import { procedureInfoSchema } from "../domain/native/procedureInfo.js"; import { z } from "zod"; +import { utcDateTimeSchema } from "../domain/offsetDateTime.js"; import { functionDossierSchema } from "../domain/hopperValues.js"; import { analysisProfileSchema } from "../domain/analysisProfile.js"; @@ -179,13 +180,13 @@ const processCoordinates = { const processLineageObservation = z.discriminatedUnion("status", [ z.object({ status: z.literal("unavailable"), - observed_at: z.iso.datetime(), + observed_at: utcDateTimeSchema, ...processCoordinates, reason: z.string().min(1), }), z.object({ status: z.literal("verified"), - observed_at: z.iso.datetime(), + observed_at: utcDateTimeSchema, ...processCoordinates, launcher_parent_pid: z.int().min(0), descendants: z.array( diff --git a/src/domain/browserObservationSchemas.ts b/src/domain/browserObservationSchemas.ts index fad41246ba..7ce080e08d 100644 --- a/src/domain/browserObservationSchemas.ts +++ b/src/domain/browserObservationSchemas.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { utcDateTimeSchema } from "./offsetDateTime.js"; import { browserCompletenessSchema } from "./browserCompleteness.js"; import { @@ -266,8 +267,8 @@ export const webPageInspectionSchema = z.object({ browser: browserVersionSchema, target: browserTargetSchema, capture_window: z.object({ - started_at: z.iso.datetime(), - ended_at: z.iso.datetime(), + started_at: utcDateTimeSchema, + ended_at: utcDateTimeSchema, observation_ms: z.int().min(0), }), completeness: browserCompletenessSchema, @@ -293,12 +294,12 @@ export const webPageInspectionSchema = z.object({ network: z.object({ requests: z.array(browserNetworkRequestSchema), websocket_connections: z.array(browserWebSocketConnectionSchema), - coverage_started_at: z.iso.datetime(), + coverage_started_at: utcDateTimeSchema, prior_activity_available: z.literal(false), }), console: z.object({ events: z.array(browserConsoleEventSchema), - coverage_started_at: z.iso.datetime(), + coverage_started_at: utcDateTimeSchema, prior_activity_available: z.literal(false), }), workers: z.array(browserWorkerSchema), diff --git a/src/domain/browserScenarioCaptureValues.ts b/src/domain/browserScenarioCaptureValues.ts index 35e08b8ea8..04021df1ca 100644 --- a/src/domain/browserScenarioCaptureValues.ts +++ b/src/domain/browserScenarioCaptureValues.ts @@ -1,6 +1,7 @@ import { createHash } from "node:crypto"; import { z } from "zod"; +import { utcDateTimeSchema } from "./offsetDateTime.js"; import { emptyArraySchema } from "./emptyArraySchema.js"; import { @@ -148,8 +149,7 @@ const eventBase = { const eventUrl = sanitizedBrowserUrlSchema.nullable(); /** The browser's wall-clock request start, reported once the request ends. */ -const browserStartSchema = z.iso - .datetime() +const browserStartSchema = utcDateTimeSchema .optional() .describe( "Wall-clock time the browser started the request, as it reported at completion; absent when it reported none.", @@ -201,8 +201,7 @@ export const browserScenarioEventSchema = z kind: z.literal("request"), status: z.null(), failure: z.null(), - observed_at: z.iso - .datetime() + observed_at: utcDateTimeSchema .optional() .describe( "Wall-clock time the provider observed the request start; absent in captures made before it was recorded.", diff --git a/src/domain/browserSession.ts b/src/domain/browserSession.ts index c1bcbe6bd8..1978ba0dfe 100644 --- a/src/domain/browserSession.ts +++ b/src/domain/browserSession.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { utcDateTimeSchema } from "./offsetDateTime.js"; import { webRuntimeCommandTimeoutSchema } from "./browserDeadline.js"; import { @@ -60,8 +61,8 @@ export const webObservationSessionSchema = z.object({ final_url: z.string().nullable(), }), window: z.object({ - armed_at: z.iso.datetime(), - ended_at: z.iso.datetime(), + armed_at: utcDateTimeSchema, + ended_at: utcDateTimeSchema, requested_ms: z.int().min(1), end_reason: z.enum([ "window_elapsed", diff --git a/src/domain/javascript/electronObservation.ts b/src/domain/javascript/electronObservation.ts index a49a456ac4..15fd5b5c9f 100644 --- a/src/domain/javascript/electronObservation.ts +++ b/src/domain/javascript/electronObservation.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { utcDateTimeSchema } from "../offsetDateTime.js"; import { webRuntimeCommandTimeoutSchema } from "../browserDeadline.js"; import { browserCompletenessSchema } from "../browserCompleteness.js"; @@ -83,8 +84,8 @@ export const electronPageInspectionSchema = z.object({ browser: browserVersionSchema, target: electronTargetSchema, capture_window: z.object({ - started_at: z.iso.datetime(), - ended_at: z.iso.datetime(), + started_at: utcDateTimeSchema, + ended_at: utcDateTimeSchema, observation_ms: z.int().min(0), }), completeness: browserCompletenessSchema, diff --git a/src/domain/offsetDateTime.test.ts b/src/domain/offsetDateTime.test.ts index 783710a63c..60d65d7f72 100644 --- a/src/domain/offsetDateTime.test.ts +++ b/src/domain/offsetDateTime.test.ts @@ -4,11 +4,9 @@ import { z } from "zod"; import { offsetDateTimeSchema, readRecordedDateTime, + utcDateTimeSchema, } from "./offsetDateTime.js"; -/** The validator this schema replaces; it remains the oracle for the accepted set. */ -const replaced = z.string().datetime({ offset: true }); - const ACCEPTED: readonly (readonly [string, string])[] = [ ["a UTC timestamp", "2026-10-09T04:00:00Z"], ["a positive offset", "2026-10-09T04:00:00+02:00"], @@ -45,8 +43,6 @@ const REJECTED: readonly (readonly [string, string])[] = [ ["unrelated text", "not a timestamp"], ]; -const CASES = [...ACCEPTED, ...REJECTED]; - describe("offset datetime schema", () => { it.each(ACCEPTED)("accepts %s", (_label, value) => { expect(offsetDateTimeSchema.safeParse(value).success).toBe(true); @@ -55,29 +51,42 @@ describe("offset datetime schema", () => { it.each(REJECTED)("rejects %s", (_label, value) => { expect(offsetDateTimeSchema.safeParse(value).success).toBe(false); }); +}); - it("classifies every case exactly as the replaced Zod validator does", () => { - const divergences = CASES.filter( - ([, value]) => - offsetDateTimeSchema.safeParse(value).success !== - replaced.safeParse(value).success, - ).map(([label]) => label); - expect(divergences).toEqual([]); +describe("UTC datetime schema", () => { + it.each(ACCEPTED.filter(([, value]) => value.endsWith("Z")))( + "preserves %s without rewriting the source value", + (_label, value) => { + expect(utcDateTimeSchema.parse(value)).toBe(value); + }, + ); + + it.each([ + ...REJECTED, + ...ACCEPTED.filter(([, value]) => !value.endsWith("Z")), + [ + "omitted seconds with a numeric offset", + "2026-10-09T04:00+02:00", + ] as const, + ])("rejects %s", (_label, value) => { + expect(utcDateTimeSchema.safeParse(value).success).toBe(false); }); +}); +describe.each([ + ["offset", offsetDateTimeSchema], + ["UTC", utcDateTimeSchema], +] as const)("%s datetime advertisement", (_name, schema) => { it("emits a pattern that compiles in every mode a client may use", () => { - const emitted = z.toJSONSchema( - z.object({ observed_at: offsetDateTimeSchema }), - { target: "draft-2020-12" }, - ); + const emitted = z.toJSONSchema(z.object({ observed_at: schema }), { + target: "draft-2020-12", + }); const pattern = findPattern(emitted); expect(pattern).toBeDefined(); if (pattern === undefined) return; - // Annex B and `u` accept the pattern the replaced validator emitted; `v` does - // not, which is the reason this schema exists. + expect(() => new RegExp(pattern)).not.toThrow(); expect(() => new RegExp(pattern, "u")).not.toThrow(); expect(() => new RegExp(pattern, "v")).not.toThrow(); - expect(() => new RegExp(replacedSource(), "v")).toThrow(); }); }); @@ -222,14 +231,3 @@ const findPattern = (value: unknown): string | undefined => { } return undefined; }; - -/** The pattern the replaced Zod validator projects, recovered through the same path. */ -const replacedSource = (): string => { - const emitted = z.toJSONSchema(z.object({ observed_at: replaced }), { - target: "draft-2020-12", - }); - const pattern = findPattern(emitted); - if (pattern === undefined) - throw new Error("The replaced validator must project a pattern"); - return pattern; -}; diff --git a/src/domain/offsetDateTime.ts b/src/domain/offsetDateTime.ts index 801aeb3ca5..c1ba19f360 100644 --- a/src/domain/offsetDateTime.ts +++ b/src/domain/offsetDateTime.ts @@ -6,26 +6,31 @@ export interface OffsetDateTimeInstant { readonly fraction: string; } +const CALENDAR_DATE = String.raw`(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))`; +const CLOCK_TIME = String.raw`(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?`; +const UTC_OFFSET = String.raw`(?:Z|[+\x2d](?:[01]\d|2[0-3]):[0-5]\d)`; + /** - * ISO-8601 date-time that requires an explicit UTC offset. - * - * `z.string().datetime({ offset: true })` emits Zod's offset class as `[+-]`. - * Annex B and `u` mode accept that, but ECMA-262 `v` mode rejects the - * unescaped `-`, so any tool whose schema carries the pattern is unusable for a - * client that compiles advertised patterns — schema compilation happens before - * arguments are supplied, so the whole tools request fails rather than one call. - * - * The accepted set is identical to the Zod validator; only the offset class - * escapes `-`, as `\x2d`, because an escaped `-` inside a character class is - * removed by `oxlint --fix` as a useless escape. + * REA's ISO-8601 timestamps preserve optional seconds and arbitrary fractional + * precision, including inputs admitted before Zod 4.6.6 required seconds. + * Keep this grammar independent of the installed validator version. The + * escaped offset hyphen also compiles in ECMA-262 Unicode-set (`v`) mode. */ export const offsetDateTimeSchema = z .string() .regex( - /^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|(?:[+\x2d](?:[01]\d|2[0-3]):[0-5]\d)))$/u, + new RegExp(`^${CALENDAR_DATE}T${CLOCK_TIME}${UTC_OFFSET}$`, "u"), "Use an ISO-8601 date-time with an explicit UTC offset", ); +/** The same established timestamp grammar with the UTC `Z` designator only. */ +export const utcDateTimeSchema = z + .string() + .regex( + new RegExp(`^${CALENDAR_DATE}T${CLOCK_TIME}Z$`, "u"), + "Use an ISO-8601 date-time in UTC", + ); + /** Parse an already validated offset timestamp without losing sub-millisecond precision. */ export const parseOffsetDateTimeInstant = ( value: string, diff --git a/src/domain/process/processCapture.ts b/src/domain/process/processCapture.ts index 2eb2817db3..c859a7456e 100644 --- a/src/domain/process/processCapture.ts +++ b/src/domain/process/processCapture.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { utcDateTimeSchema } from "../offsetDateTime.js"; import { jsonValueSchema } from "../jsonValue.js"; import { normalizationSchema } from "./processScenario.js"; @@ -455,8 +456,8 @@ const processCaptureShapeSchema = z.strictObject({ platform: z.string().min(1), architecture: z.string().min(1), pty_backend: z.literal("node-pty"), - started_at: z.iso.datetime(), - completed_at: z.iso.datetime(), + started_at: utcDateTimeSchema, + completed_at: utcDateTimeSchema, scenario: z.record(z.string(), jsonValueSchema), comparison_contract: z.record(z.string(), jsonValueSchema), full_scenario_sha256: z.string().regex(/^[a-f0-9]{64}$/u), diff --git a/src/domain/webEventListeners.ts b/src/domain/webEventListeners.ts index e48bd8e14b..b5a155e9f0 100644 --- a/src/domain/webEventListeners.ts +++ b/src/domain/webEventListeners.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { utcDateTimeSchema } from "./offsetDateTime.js"; import { webRuntimeLocationSchema, webRuntimeScopeSchema, @@ -21,7 +22,7 @@ export type InspectWebEventListenersInput = z.infer< export const webEventListenersObservationSchema = z.object({ browser: browserVersionSchema, target: webRuntimeTargetSchema, - inspected_at: z.iso.datetime(), + inspected_at: utcDateTimeSchema, selected_node: z.object({ selector: z.string(), match: z.enum(["found", "not_found"]), diff --git a/src/domain/webExecution.ts b/src/domain/webExecution.ts index 632206c8bd..dc1d9fc6b9 100644 --- a/src/domain/webExecution.ts +++ b/src/domain/webExecution.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { utcDateTimeSchema } from "./offsetDateTime.js"; import { MAX_BROWSER_DEADLINE_MS } from "./browserDeadline.js"; import { jsonObjectSchema } from "./jsonValue.js"; import { @@ -34,8 +35,8 @@ export const webExecutionObservationSchema = z.object({ browser: browserVersionSchema, target: webRuntimeTargetSchema, window: z.object({ - armed_at: z.iso.datetime(), - ended_at: z.iso.datetime(), + armed_at: utcDateTimeSchema, + ended_at: utcDateTimeSchema, requested_ms: z.int().min(1), end_reason: z.enum([ "window_elapsed", diff --git a/src/domain/webScreenshot.ts b/src/domain/webScreenshot.ts index a0246b0764..5cdbd69d99 100644 --- a/src/domain/webScreenshot.ts +++ b/src/domain/webScreenshot.ts @@ -1,6 +1,7 @@ import { createHash } from "node:crypto"; import { z } from "zod"; +import { utcDateTimeSchema } from "./offsetDateTime.js"; import { webRuntimeCommandTimeoutSchema } from "./browserDeadline.js"; import { browserCompletenessSchema } from "./browserCompleteness.js"; @@ -52,7 +53,7 @@ export const webScreenshotSchema = z.object({ url: z.string(), origin: z.string(), }), - captured_at: z.iso.datetime(), + captured_at: utcDateTimeSchema, viewport: z.object({ width: z.int().min(1), height: z.int().min(1), diff --git a/src/kuna/KunaProjectCache.ts b/src/kuna/KunaProjectCache.ts index 83623335ba..4f46320330 100644 --- a/src/kuna/KunaProjectCache.ts +++ b/src/kuna/KunaProjectCache.ts @@ -20,6 +20,7 @@ import { join } from "node:path"; import canonicalize from "canonicalize"; import writeFileAtomic from "write-file-atomic"; import { z } from "zod"; +import { utcDateTimeSchema } from "../domain/offsetDateTime.js"; import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js"; import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js"; @@ -119,7 +120,7 @@ const manifestSchema = z.strictObject({ decompiled: z.int().nonnegative(), failed: z.int().nonnegative(), }), - created_at: z.string().datetime(), + created_at: utcDateTimeSchema, }); /** Immutable description of one published export, written once at publish. */ diff --git a/tests/boundary/cli/cliJsonStdinCli.test.ts b/tests/boundary/cli/cliJsonStdinCli.test.ts index 1b4150d1a8..6ffc692ec1 100644 --- a/tests/boundary/cli/cliJsonStdinCli.test.ts +++ b/tests/boundary/cli/cliJsonStdinCli.test.ts @@ -49,7 +49,7 @@ cliTest.for([false, true])( { env: { ...process.env, - NODE_OPTIONS: "--max-old-space-size=256", + NODE_OPTIONS: `--max-old-space-size=256 --import=${probe}`, REA_LOG_LEVEL: "silent", }, }, @@ -71,7 +71,32 @@ cliTest.for([false, true])( child.once("error", reject); child.once("close", () => resolve()); }); - child.stdin.end(`[${"[],".repeat(1_400_000)}[]]`); + await expect + .poll(() => /REA_TEST_STDIN_READ (\d+)/u.test(stderr), { + timeout: 10_000, + }) + .toBe(true); + const heapSizeLimit = Number( + /REA_TEST_STDIN_READ (\d+)/u.exec(stderr)?.[1], + ); + // Exceed the child's whole heap, independently of startup allocations or + // when V8 last collected. Reuse the production owner's expansion estimate. + let lower = 0, + upper = heapSizeLimit; + while (lower < upper) { + const middle = Math.floor((lower + upper) / 2); + if ( + nativeJsonParseRequiredBytes(middle * 3 + 4, snapshot) > heapSizeLimit + ) + upper = middle; + else lower = middle + 1; + } + const input = `[${"[],".repeat(lower)}[]]`; + expect(Buffer.byteLength(input)).toBeLessThanOrEqual(8 * 1024 * 1024); + expect( + nativeJsonParseRequiredBytes(Buffer.byteLength(input), snapshot), + ).toBeGreaterThan(heapSizeLimit); + child.stdin.end(input); expect(await waitForExit(child, 10_000), stderr).toBe(true); await closed; if (writeFailure !== undefined) diff --git a/tests/conformance/completionLedgerGeneration.test.ts b/tests/conformance/completionLedgerGeneration.test.ts index 3e3bd913e0..f50862575a 100644 --- a/tests/conformance/completionLedgerGeneration.test.ts +++ b/tests/conformance/completionLedgerGeneration.test.ts @@ -64,6 +64,43 @@ const firstClaim = (input: CompletionVerifierReport) => { }; describe("completion ledger generation", () => { + it.each(["verified", "unavailable"] as const)( + "preserves minute-precision UTC in %s verifier lineage", + (status) => { + const input = report(); + const observedAt = "2026-10-09T04:00Z"; + const processLineage = + status === "verified" + ? { + ...input.verifier_run.process_lineage, + status: "verified" as const, + observed_at: observedAt, + descendants: [], + } + : { + status: "unavailable" as const, + observed_at: observedAt, + launcher_pid: input.verifier_run.verifier_pid, + launcher_parent_pid: input.verifier_run.parent_pid, + process_group_id: null, + reason: "Lineage observation unavailable", + }; + const preserved = { + ...input, + verifier_run: { + ...input.verifier_run, + process_lineage: processLineage, + }, + }; + + expect( + completionVerifierReportSchema.parse(preserved).verifier_run + .process_lineage.observed_at, + ).toBe(observedAt); + expect(generate(preserved)).toEqual(generate(input)); + }, + ); + it("separates verifier outcomes while preserving immutable commitments", () => { const generated = generate(); diff --git a/tests/conformance/contracts/utcTimestampCompatibility.test.ts b/tests/conformance/contracts/utcTimestampCompatibility.test.ts new file mode 100644 index 0000000000..13cc6e06a0 --- /dev/null +++ b/tests/conformance/contracts/utcTimestampCompatibility.test.ts @@ -0,0 +1,44 @@ +import { Ajv2020 } from "ajv/dist/2020.js"; +import { describe, expect, it } from "vitest"; +import { z } from "zod"; + +import { webPageInspectionSchema } from "../../../src/domain/browserObservationSchemas.js"; +import { webObservationSessionSchema } from "../../../src/domain/browserSession.js"; +import { webScreenshotSchema } from "../../../src/domain/webScreenshot.js"; + +const fields = [ + ["screenshot capture", webScreenshotSchema.shape.captured_at], + [ + "page inspection start", + webPageInspectionSchema.shape.capture_window.shape.started_at, + ], + [ + "session observation end", + webObservationSessionSchema.shape.window.shape.ended_at, + ], +] as const; + +describe.each(fields)("%s timestamp compatibility", (_name, schema) => { + const advertised = z.toJSONSchema(schema, { target: "draft-2020-12" }); + const validate = new Ajv2020().compile(advertised); + + it.each([ + "2026-10-09T04:00Z", + "2026-10-09T04:00:00Z", + "2026-10-09T04:00:00.1234567890123456789Z", + ])("preserves the admitted source spelling %s", (value) => { + expect(schema.parse(value)).toBe(value); + expect(validate(value)).toBe(true); + }); + + it.each([ + "2026-10-09T04:00", + "2026-10-09T04:00+02:00", + "2026-10-09T04:00:00-03:00", + "2026-04-31T04:00Z", + "2026-10-09T04:00.5Z", + ])("rejects %s in both the canonical and advertised contracts", (value) => { + expect(schema.safeParse(value).success).toBe(false); + expect(validate(value)).toBe(false); + }); +}); diff --git a/tests/fixtures/cliJsonStdinProbe.mjs b/tests/fixtures/cliJsonStdinProbe.mjs index cb9a56af56..b81c3decc4 100644 --- a/tests/fixtures/cliJsonStdinProbe.mjs +++ b/tests/fixtures/cliJsonStdinProbe.mjs @@ -1,6 +1,10 @@ +import { getHeapStatistics } from "node:v8"; + // Observe the production stdin reader without supplying data or closing its pipe. const iterate = process.stdin[Symbol.asyncIterator]; process.stdin[Symbol.asyncIterator] = function (...args) { - process.stderr.write("REA_TEST_STDIN_READ\n"); + process.stderr.write( + `REA_TEST_STDIN_READ ${getHeapStatistics().heap_size_limit}\n`, + ); return iterate.apply(this, args); };