fix(environment): keep invalid recorded dates unknown

Distinguish invalid supported timestamp spellings from unrecognized legacy formats at the recorded-date owner. Probe replay retains ordinal order for invalid dates without changing the existing legacy fallback.

Validated date parsing, state ordering and comparison workflows; the prior implementation reorders the invalid-date regression.
This commit is contained in:
morluto committed 2026-10-27 03:29:11 +08:00
1 parent 4e86bd0c1c
commit dd7a2da5bb
4 files changed
+80 -44

No files matched your search

@@ -841,28 +841,37 @@ const registerProbeCoverageCases = () => {
};
const registerProbeOrderingAndCaptureCases = () => {
it("keeps the recorded order when a record has no start time, as state extraction does", () => {
const { startedDateTime: _undated, ...read } = {
...exchange(1, "GET", "http://app.example/api/books/2", 404),
response: {
status: 404,
content: {
mimeType: "application/json",
text: JSON.stringify(BOOK_NOT_FOUND),
it.each([undefined, "2026-02-30T09:00:00Z"])(
"keeps replay in recorded order when a start is missing or invalid: %s",
(startedDateTime) => {
const { startedDateTime: _undated, ...read } = {
...exchange(1, "GET", "http://app.example/api/books/2", 404),
response: {
status: 404,
content: {
mimeType: "application/json",
text: JSON.stringify(BOOK_NOT_FOUND),
},
},
},
};
const report = run(
analysisViewCaptureEvidence(
environmentManifestCaptureFixture([
exchange(0, "DELETE", "http://app.example/api/books/2", 204),
read,
]),
),
);
if (!report.ok) throw report.error;
expect(report.value.summary).toMatchObject({ steps: 2, equal: 2 });
});
};
const report = run(
analysisViewCaptureEvidence(
environmentManifestCaptureFixture([
exchange(0, "DELETE", "http://app.example/api/books/2", 204),
{
...read,
...(startedDateTime === undefined ? {} : { startedDateTime }),
},
]),
),
);
if (!report.ok) throw report.error;
expect(report.value.summary).toMatchObject({ steps: 2, equal: 2 });
expect(
report.value.probes[0]?.results.map(({ ordinal }) => ordinal),
).toEqual([0, 1]);
},
);
it.each([
["2026-10-20T09:00:00.0002Z", "2026-10-20T09:00:00.0001Z"],
@@ -19,7 +19,7 @@ import type { JsonValue } from "../../domain/jsonValue.js";
import {
compareOffsetDateTimeInstants,
offsetDateTimeInstantFromEpochMilliseconds,
readRecordedDateTime,
inspectRecordedDateTime,
type OffsetDateTimeInstant,
} from "../../domain/offsetDateTime.js";
import type { WebNetworkCaptureRecord } from "../../domain/webNetworkCapture.js";
@@ -55,8 +55,9 @@ const startedInstant = (
): OffsetDateTimeInstant | undefined => {
const started = startedSchema.safeParse(record.reported);
if (!started.success) return undefined;
const reading = readRecordedDateTime(started.data.startedDateTime);
if (reading !== undefined) return reading.instant;
const reading = inspectRecordedDateTime(started.data.startedDateTime);
if (reading.kind === "valid") return reading.reading.instant;
if (reading.kind === "invalid") return undefined;
// Preserve native-Date admission for legacy spellings, at its original precision.
const ms = Date.parse(started.data.startedDateTime);
return Number.isFinite(ms)
+13
View File
@@ -2,11 +2,24 @@ import { describe, expect, it } from "vitest";
import { z } from "zod";
import {
inspectRecordedDateTime,
offsetDateTimeSchema,
readRecordedDateTime,
utcDateTimeSchema,
} from "./offsetDateTime.js";
it.each([
["2026-02-30T09:00:00Z", "invalid"],
["1900-02-29 09:00:00", "invalid"],
["2026-10-20T24:00:00Z", "invalid"],
["2026-10-20T09:00:00+24:00", "invalid"],
["2026-10-20 09:00:00.0000000001", "valid"],
["Tue, 20 Oct 2026 09:00:00 GMT", "unrecognized"],
["2026-10-20", "unrecognized"],
])("classifies recorded spelling %s as %s", (text, kind) => {
expect(inspectRecordedDateTime(text).kind).toBe(kind);
});
const ACCEPTED: readonly (readonly [string, string])[] = [
["a UTC timestamp", "2026-10-09T04:00:00Z"],
["a positive offset", "2026-10-09T04:00:00+02:00"],
+33 -20
View File
@@ -55,25 +55,27 @@ export const parseOffsetDateTimeInstant = (
};
};
/** Parse recorded ISO spelling once, retaining its exact instant and formatting facts. */
export const readRecordedDateTime = (
export interface RecordedDateTimeReading {
readonly instant: OffsetDateTimeInstant;
readonly spelling: {
readonly separator: "T" | " ";
readonly secondsPresent: boolean;
readonly fractionDigits: number;
readonly zone: string;
};
}
/** Distinguish invalid supported spellings from formats this reader does not interpret. */
export const inspectRecordedDateTime = (
value: string,
):
| {
readonly instant: OffsetDateTimeInstant;
readonly spelling: {
readonly separator: "T" | " ";
readonly secondsPresent: boolean;
readonly fractionDigits: number;
readonly zone: string;
};
}
| undefined => {
| { readonly kind: "valid"; readonly reading: RecordedDateTimeReading }
| { readonly kind: "invalid" | "unrecognized" } => {
const match =
/^(\d{4}-\d{2}-\d{2})([T ])(\d{2}:\d{2})(?::(\d{2})(?:\.(\d+))?)?(Z|[+-]\d{2}:?\d{2})?$/u.exec(
value,
);
if (match === null) return undefined;
if (match === null) return { kind: "unrecognized" };
const [, date, separator, time, seconds, fraction = "", zone = ""] = match;
const offset =
zone === ""
@@ -84,18 +86,29 @@ export const readRecordedDateTime = (
const normalized = offsetDateTimeSchema.safeParse(
`${date}T${time}${seconds === undefined ? "" : `:${seconds}`}${fraction === "" ? "" : `.${fraction}`}${offset}`,
);
if (!normalized.success) return undefined;
if (!normalized.success) return { kind: "invalid" };
return {
instant: parseOffsetDateTimeInstant(normalized.data),
spelling: {
separator: separator as "T" | " ",
secondsPresent: seconds !== undefined,
fractionDigits: fraction.length,
zone,
kind: "valid",
reading: {
instant: parseOffsetDateTimeInstant(normalized.data),
spelling: {
separator: separator as "T" | " ",
secondsPresent: seconds !== undefined,
fractionDigits: fraction.length,
zone,
},
},
};
};
/** Parse recorded ISO spelling once, retaining its exact instant and formatting facts. */
export const readRecordedDateTime = (
value: string,
): RecordedDateTimeReading | undefined => {
const inspected = inspectRecordedDateTime(value);
return inspected.kind === "valid" ? inspected.reading : undefined;
};
/** Convert a Date.now-style integer into the same exact comparison shape. */
export const offsetDateTimeInstantFromEpochMilliseconds = (
epochMilliseconds: number,