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:
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)
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user