fix(environment): restrict epoch normalization to clock fields

Require a time-field token boundary before treating numeric values as epoch clocks, so identifiers such as media format codes remain literal. Clarify recognized field forms and clock offset and precision semantics in the fidelity guide.

Fixes #6551
This commit is contained in:
morluto
2026-10-23 17:59:18 +08:00
parent 18225b9bd6
commit 65168f77fb
4 changed files with 64 additions and 3 deletions
+3 -1
View File
@@ -755,8 +755,10 @@ recording replayed in its place and never from a recording whose body the probe
two collections can reuse a live id, and an equal pair stays in that collection. A live id in a later request's path, query or body is translated before the
environment answers, a path parameter by the collection the live path names (a nested collection keeps its parent values, each decoded once, and a literal route segment stays), except a query value that pages a list, such as `limit`
or `page`. Each call is checked against the served tool's input schema first, as the MCP server checks it. After mapping, two clock values compare by format only (ISO-8601 strings, and epoch numbers under
time-named fields), and two different values each side generated on its own compare equal: both UUIDs, or one length of
`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.
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
@@ -166,6 +166,21 @@ describe("compareAnswers", () => {
});
});
describe("clock field names", () => {
it("keeps numeric format identifiers literal and recognizes clock field forms", () => {
expect(
compare({ format: 1196444237 }, { format: 1448695129 }).differences,
).toMatchObject([{ category: "value", pointer: "/format" }]);
expect(
compare({ updated_at: 1760000000 }, { updated_at: 1760000999 })
.differences,
).toEqual([]);
expect(
compare({ createdAt: 1760000000 }, { createdAt: 1760000999 }).differences,
).toEqual([]);
});
});
describe("comparison body coverage", () => {
it("compares a live body over the replay limit by status only and counts it", () => {
const result = compareAnswers(
@@ -52,7 +52,10 @@ export interface Normalised {
/** At most this many differences are kept per step. */
export const STEP_DIFFERENCE_LIMIT = 20;
const VALUE_CHARS = 120;
const TIME_FIELD = /(at|time|date|timestamp|created|updated|modified)$/iu;
const TIME_FIELD =
/(?:^|[_-])(?:at|time|date|timestamp|created|updated|modified)$/iu;
const TIME_FIELD_CAMEL =
/(?:At|Time|Date|Timestamp|Created|Updated|Modified)$/u;
const ID_LIST_FIELD = /^(ids|uuids|guids)$/u;
const isObject = (value: JsonValue | undefined): value is JsonObject =>
@@ -76,7 +79,12 @@ const clockFormat = (
value: JsonValue,
key: string | undefined,
): string | undefined => {
if (typeof value === "number" && key !== undefined && !TIME_FIELD.test(key))
if (
typeof value === "number" &&
key !== undefined &&
!TIME_FIELD.test(key) &&
!TIME_FIELD_CAMEL.test(key)
)
return undefined;
const clock = readClock(value);
return clock === undefined ? undefined : formatKey(clock.format);
@@ -27,6 +27,42 @@ const compareRows = (
};
describe("environment comparison identifier semantics", () => {
it("compares numeric format identifiers literally while normalizing actual clock fields", () => {
const fourcc = (value: string) => Buffer.from(value).readUInt32LE(0);
const probe = compareRows(
[
[
"GET",
"/api/media",
{
format: fourcc("MJPG"),
size: 1,
updated_at: "2026-10-20T10:00:00Z",
},
],
],
[
[
"GET",
"/api/media",
{
format: fourcc("YUYV"),
size: 1,
updated_at: "2026-10-17T18:00:03Z",
},
],
],
);
expect(probe.results[0]?.equal).toBe(false);
expect(
probe.results[0]?.differences.map((difference) => [
difference.category,
difference.pointer,
]),
).toEqual([["value", "/format"]]);
expect(probe.normalised.clocks).toBe(1);
});
it("does not normalize measurements after learning an unrelated numeric ID", () => {
const probe = compareRows(
[