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