Unit tests for clock, UUID, token, slug and delimited-token values, the field rules that explain created and updated entities, response shapes, insert rules and the shared apply functions; view tests for every plan reason, delete confirmation by a 400, 404 or null read, learned insert rules, unmodelled writes and observations from later reads; services tests for served creates, updates and deletes, refusals that leave the state unchanged, deterministic values across reset and load, missing entities, the catalog and paging-only list lines; and an MCP boundary test that serves a capture through the built CLI, applies a create and resets it. The capture fixture records request bodies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
243 lines
7.9 KiB
TypeScript
243 lines
7.9 KiB
TypeScript
import { expect, it } from "vitest";
|
|
import type { JsonValue } from "../jsonValue.js";
|
|
import {
|
|
containsDelimited,
|
|
delimitedOffsets,
|
|
formatClock,
|
|
integerOf,
|
|
isUuid,
|
|
readClock,
|
|
replaceDelimited,
|
|
seededToken,
|
|
seededUuid,
|
|
slugOf,
|
|
spelledLike,
|
|
type ClockFormat,
|
|
} from "./environmentStateWriteValues.js";
|
|
|
|
const WHOLE_SECOND = Date.UTC(2026, 2, 4, 5, 6, 7);
|
|
const WITH_MS = WHOLE_SECOND + 123;
|
|
const V4_UUID =
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
const UUID_EXEMPLAR = "123e4567-e89b-12d3-a456-426614174000";
|
|
|
|
const iso = (
|
|
separator: "T" | " ",
|
|
fraction: number,
|
|
zone: string,
|
|
): ClockFormat => ({ kind: "iso", separator, fraction, zone });
|
|
|
|
const seeds = (count: number): string[] =>
|
|
Array.from({ length: count }, (_, index) => `seed-${String(index)}`);
|
|
|
|
it.each<[string, number, ClockFormat]>([
|
|
["2026-03-04T05:06:07Z", WHOLE_SECOND, iso("T", 0, "Z")],
|
|
["2026-03-04 05:06:07.123Z", WITH_MS, iso(" ", 3, "Z")],
|
|
["2026-03-04T05:06:07.123456Z", WITH_MS, iso("T", 6, "Z")],
|
|
["2026-03-04T07:06:07.123+02:00", WITH_MS, iso("T", 3, "+02:00")],
|
|
["2026-03-03T23:36:07-0530", WHOLE_SECOND, iso("T", 0, "-0530")],
|
|
["2026-03-04T05:06:07", WHOLE_SECOND, iso("T", 0, "")],
|
|
["2024-02-29 00:00:00Z", Date.UTC(2024, 1, 29), iso(" ", 0, "Z")],
|
|
])("reads the ISO clock %s with its spelling", (text, ms, format) => {
|
|
expect(readClock(text)).toEqual({ ms, format });
|
|
});
|
|
|
|
it.each([
|
|
"2026-02-30T00:00:00Z",
|
|
"2026-02-29T00:00:00Z",
|
|
"2026-13-01T00:00:00Z",
|
|
"2026-03-04T24:00:00Z",
|
|
"2026-03-04T05:60:00Z",
|
|
"2026-03-04T05:06:60Z",
|
|
"2026-03-04",
|
|
"2026-03-04T05:06Z",
|
|
"2026-03-04T05:06:07+05",
|
|
"2026-03-04T05:06:07.1234567890Z",
|
|
"2026-03-04t05:06:07Z",
|
|
"next tuesday",
|
|
])("refuses %s as a clock", (text) => {
|
|
expect(readClock(text)).toBeUndefined();
|
|
});
|
|
|
|
it.each<[JsonValue | undefined, ReturnType<typeof readClock>]>([
|
|
[
|
|
WHOLE_SECOND / 1000,
|
|
{ ms: WHOLE_SECOND, format: { kind: "epoch", unit: "s" } },
|
|
],
|
|
[WITH_MS, { ms: WITH_MS, format: { kind: "epoch", unit: "ms" } }],
|
|
[999_999_999, undefined],
|
|
[99_999_999_999, undefined],
|
|
[10_000_000_000_000, undefined],
|
|
[WHOLE_SECOND / 1000 + 0.5, undefined],
|
|
[String(WHOLE_SECOND / 1000), undefined],
|
|
[null, undefined],
|
|
[undefined, undefined],
|
|
])("reads the epoch value %j", (value, expected) => {
|
|
expect(readClock(value)).toEqual(expected);
|
|
});
|
|
|
|
it.each<JsonValue>([
|
|
"2026-03-04T05:06:07Z",
|
|
"2026-03-04 05:06:07.123Z",
|
|
"2026-03-04T07:06:07.123+02:00",
|
|
"2026-03-03T23:36:07-0530",
|
|
"2026-03-04T05:06:07.1",
|
|
"2026-03-04T05:06:07.123000000",
|
|
WHOLE_SECOND / 1000,
|
|
WITH_MS,
|
|
])("formats %j back to its own spelling", (value) => {
|
|
const clock = readClock(value)!;
|
|
expect(formatClock(clock.ms, clock.format)).toBe(value);
|
|
});
|
|
|
|
it.each<[ClockFormat, JsonValue]>([
|
|
[iso("T", 6, "Z"), "2026-03-04T05:06:07.123000Z"],
|
|
[iso(" ", 9, ""), "2026-03-04 05:06:07.123000000"],
|
|
[iso("T", 1, "Z"), "2026-03-04T05:06:07.1Z"],
|
|
[iso("T", 0, "+02:00"), "2026-03-04T07:06:07+02:00"],
|
|
[iso("T", 3, "-0530"), "2026-03-03T23:36:07.123-0530"],
|
|
[{ kind: "epoch", unit: "s" }, WHOLE_SECOND / 1000],
|
|
[{ kind: "epoch", unit: "ms" }, WITH_MS],
|
|
])("formats an instant as %j", (format, expected) => {
|
|
expect(formatClock(WITH_MS, format)).toBe(expected);
|
|
});
|
|
|
|
it.each<[JsonValue | undefined, boolean]>([
|
|
[UUID_EXEMPLAR, true],
|
|
[UUID_EXEMPLAR.toUpperCase(), true],
|
|
[UUID_EXEMPLAR.replaceAll("-", ""), false],
|
|
[UUID_EXEMPLAR.slice(1), false],
|
|
[`g${UUID_EXEMPLAR.slice(1)}`, false],
|
|
[` ${UUID_EXEMPLAR}`, false],
|
|
[42, false],
|
|
[undefined, false],
|
|
])("isUuid(%j) is %s", (value, expected) => {
|
|
expect(isUuid(value)).toBe(expected);
|
|
});
|
|
|
|
it.each<[JsonValue | undefined, number | undefined]>([
|
|
[42, 42],
|
|
[-3, -3],
|
|
["42", 42],
|
|
["007", 7],
|
|
["123456789012345", 123_456_789_012_345],
|
|
["1234567890123456", undefined],
|
|
["-3", undefined],
|
|
["4.2", undefined],
|
|
[4.2, undefined],
|
|
[2 ** 53, undefined],
|
|
["", undefined],
|
|
[" 4", undefined],
|
|
[true, undefined],
|
|
[null, undefined],
|
|
[undefined, undefined],
|
|
])("integerOf(%j) is %j", (value, expected) => {
|
|
expect(integerOf(value)).toBe(expected);
|
|
});
|
|
|
|
it("spells an integer as the recorded value spells integers", () => {
|
|
expect(spelledLike("7", 8)).toBe("8");
|
|
expect(spelledLike(7, 8)).toBe(8);
|
|
expect(spelledLike(null, 8)).toBe(8);
|
|
});
|
|
|
|
it.each([
|
|
["Hello World", "hello-world"],
|
|
["Héllo, Wörld!", "hello-world"],
|
|
[" Crème brûlée -- recipe__v2 ", "creme-brulee-recipe-v2"],
|
|
["Fullwidth 2", "fullwidth-2"],
|
|
["already-a-slug", "already-a-slug"],
|
|
["--Edge--", "edge"],
|
|
["!!!", ""],
|
|
])("slugOf(%j) is %j", (text, slug) => {
|
|
expect(slugOf(text)).toBe(slug);
|
|
});
|
|
|
|
it.each<[string, string, number[]]>([
|
|
["/issues/2", "2", [8]],
|
|
["2026", "2", []],
|
|
["2 of 22, page 2", "2", [0, 14]],
|
|
["x2/2y", "2", []],
|
|
["é2", "2", []],
|
|
["https://x.test/items/abc123?ref=abc123", "abc123", [21, 32]],
|
|
["anything", "", []],
|
|
])("finds %j's delimited %j at %j", (text, token, offsets) => {
|
|
expect(delimitedOffsets(text, token)).toEqual(offsets);
|
|
});
|
|
|
|
it("replaces only delimited occurrences", () => {
|
|
expect(replaceDelimited("/issues/2 in 2026", "2", "9")).toBe(
|
|
"/issues/9 in 2026",
|
|
);
|
|
expect(replaceDelimited("a-2-b-2", "2", "10")).toBe("a-10-b-10");
|
|
expect(replaceDelimited("2026", "2", "9")).toBe("2026");
|
|
});
|
|
|
|
// Known bug: a rejected match skips its whole length, hiding an overlapping delimited one.
|
|
it("finds a delimited match that overlaps an undelimited one", () => {
|
|
expect(delimitedOffsets("11-1-1", "1-1")).toEqual([3]);
|
|
});
|
|
|
|
it("finds a delimited token in any string nested in objects and arrays", () => {
|
|
expect(
|
|
containsDelimited({ a: [1, { b: ["x", "see /issues/2"] }] }, "2"),
|
|
).toBe(true);
|
|
expect(containsDelimited("2", "2")).toBe(true);
|
|
expect(containsDelimited({ c: "2026", n: 2, d: null }, "2")).toBe(false);
|
|
expect(containsDelimited(["2026", true, [{ e: "x2" }]], "2")).toBe(false);
|
|
});
|
|
|
|
it("derives a deterministic version 4 UUID per seed", () => {
|
|
const first = seededUuid("seed-a", UUID_EXEMPLAR);
|
|
expect(first).toMatch(V4_UUID);
|
|
expect(seededUuid("seed-a", UUID_EXEMPLAR)).toBe(first);
|
|
const derived = seeds(64).map((seed) => seededUuid(seed, UUID_EXEMPLAR));
|
|
expect(new Set(derived).size).toBe(derived.length);
|
|
for (const uuid of derived) expect(uuid).toMatch(V4_UUID);
|
|
});
|
|
|
|
it("keeps upper case only when the recorded UUID is upper case", () => {
|
|
const upper = seededUuid("seed-a", UUID_EXEMPLAR.toUpperCase());
|
|
expect(upper).toBe(seededUuid("seed-a", UUID_EXEMPLAR).toUpperCase());
|
|
expect(upper).toMatch(/[A-F]/u);
|
|
const digits = "12345678-1234-4234-8234-123456789012";
|
|
expect(seededUuid("seed-a", digits)).toMatch(V4_UUID);
|
|
const mixed = "123e4567-E89B-12d3-a456-426614174000";
|
|
expect(seededUuid("seed-a", mixed)).toMatch(V4_UUID);
|
|
});
|
|
|
|
it.each<[string, RegExp]>([
|
|
["a1b2c3d4e5f6", /^[0-9a-f]{12}$/u],
|
|
["A1B2C3D4E5F6", /^[0-9A-F]{12}$/u],
|
|
["k9x2m7q4z1w8", /^[0-9a-z]{12}$/u],
|
|
["AbCdEfGhIjKl", /^[A-Za-z]{12}$/u],
|
|
["deadbeefcafe", /^[a-z]{12}$/u],
|
|
[
|
|
"tok_9Xa-7bQ.2",
|
|
/^[0-9A-Za-z]{3}_[0-9A-Za-z]{3}-[0-9A-Za-z]{3}\.[0-9A-Za-z]$/u,
|
|
],
|
|
["____", /^____$/u],
|
|
])("keeps the length and character classes of %j", (exemplar, shape) => {
|
|
for (const seed of seeds(16)) {
|
|
const token = seededToken(seed, exemplar);
|
|
expect(token).toHaveLength(exemplar.length);
|
|
expect(token).toMatch(shape);
|
|
expect(seededToken(seed, exemplar)).toBe(token);
|
|
}
|
|
});
|
|
|
|
it("draws tokens from the whole alphabet the recorded token uses", () => {
|
|
const characters = (exemplar: string): string =>
|
|
seeds(64)
|
|
.map((seed) => seededToken(seed, exemplar))
|
|
.join("");
|
|
expect(characters("a1b2c3d4e5f6")).toMatch(/^[0-9a-f]+$/u);
|
|
expect(characters("k9x2m7q4z1w8")).toMatch(/[g-z]/u);
|
|
const mixed = characters("AbCdEfGhIjKl");
|
|
expect(mixed).toMatch(/[a-z]/u);
|
|
expect(mixed).toMatch(/[A-Z]/u);
|
|
expect(seededToken("seed-a", "k9x2m7q4z1w8")).not.toBe(
|
|
seededToken("seed-b", "k9x2m7q4z1w8"),
|
|
);
|
|
});
|