Files
rea/src/application/environment/environmentCompareDiff.test.ts
T

390 lines
12 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { JsonValue } from "../../domain/jsonValue.js";
import { compareAnswers } from "./environmentCompareDiff.js";
import { IdMap, learnIds } from "./environmentCompareIds.js";
const compare = (
live: unknown,
env: unknown,
map = new IdMap(),
status: [number, number] = [200, 200],
) =>
compareAnswers(
{ status: status[0], body: { kind: "json", value: live as JsonValue } },
{ status: status[1], body: { kind: "json", value: env as JsonValue } },
map,
);
describe("compareAnswers", () => {
it("normalizes mapped ids and clocks while comparing unproven generated-looking values literally", () => {
const map = new IdMap();
map.learn(101, 1);
const result = compare(
{
id: 101,
created_at: "2026-10-20T10:00:00Z",
token: "3f2a9c8d7e",
uuid: "6f1c2b9e-2a3d-4f5e-8a9b-0c1d2e3f4a5b",
},
{
id: 1,
created_at: "2026-10-17T18:00:03Z",
token: "9b8e7d6c5a",
uuid: "0a1b2c3d-4e5f-4a6b-9c7d-8e9f0a1b2c3d",
},
map,
);
expect(
result.differences.map(({ category, pointer }) => [category, pointer]),
).toEqual([
["value", "/token"],
["value", "/uuid"],
]);
expect(result.normalised).toEqual({
ids: 1,
clocks: 1,
generated: 0,
large_bodies: 0,
unobserved_bodies: 0,
});
});
it("normalizes string generation only at fields identified by an executed write plan", () => {
const result = compareAnswers(
{
status: 201,
body: { kind: "json", value: { token: "live-token", sku: "product2" } },
},
{
status: 201,
body: { kind: "json", value: { token: "env-token", sku: "product1" } },
},
new IdMap(),
{ generatedFields: new Set(["token"]) },
);
expect(result.differences).toMatchObject([
{ category: "value", pointer: "/sku" },
]);
expect(result.normalised.generated).toBe(1);
});
it("categorises status, missing and extra fields, values and clock formats", () => {
const result = compare(
{ name: "A", gone: 1, at: "2026-10-20T10:00:00.123+00:00" },
{ name: "B", added: true, at: "2026-10-20T10:00:00Z" },
new IdMap(),
[409, 204],
);
expect(result.differences.map((d) => [d.category, d.pointer])).toEqual([
["status", ""],
["value", "/name"],
["missing-field", "/gone"],
["clock-format", "/at"],
["extra-field", "/added"],
]);
});
it("reports array length and order and compares paired items", () => {
const result = compare(
[
{ name: "B", n: 2 },
{ name: "A", n: 1 },
{ name: "C", n: 3 },
],
[
{ name: "A", n: 1 },
{ name: "B", n: 5 },
],
);
expect(result.differences.map((d) => [d.category, d.pointer])).toEqual([
["length", ""],
["order", ""],
["value", "/0/n"],
]);
});
it("treats an epoch number as a clock only under a time-named field", () => {
expect(
compare({ updated: 1760000000 }, { updated: 1760000999 }).differences,
).toEqual([]);
expect(
compare({ size: 1760000000 }, { size: 1760000999 }).differences.map(
(d) => d.category,
),
).toEqual(["value"]);
});
it("compares an object keyed by mapped ids under the environment's keys", () => {
const map = new IdMap();
map.learn("liveTable", "envTable");
expect(
compare(
{ tables: { liveTable: { name: "T" } } },
{ tables: { envTable: { name: "T" } } },
map,
).differences,
).toEqual([]);
});
it("compares ordinary fields by name, even one named like a learned id", () => {
const map = new IdMap();
map.learn("books", "tags");
expect(
compare({ books: 1, tags: 2 }, { books: 1, tags: 2 }, map).differences,
).toEqual([]);
// A real rename shows, rather than being taken for a mapped key.
expect(
compare({ books: 1 }, { tags: 1 }, map).differences.map((d) => [
d.category,
d.pointer,
]),
).toEqual([
["missing-field", "/books"],
["extra-field", "/tags"],
]);
});
it("normalizes known string references but leaves ordinary numeric and string values literal", () => {
const map = new IdMap();
map.learn(101, 1);
learnIds({ ref: "live-ref" }, { ref: "env-ref" }, map, {
heldIds: new Set(["env-ref"]),
});
expect(
compare(
{ ref: "live-ref", price: 101, label: "live-ref" },
{ ref: "env-ref", price: 1, label: "env-ref" },
map,
).differences.map((difference) => difference.pointer),
).toEqual(["/price", "/label"]);
map.learn("live-key", "env-key");
expect(
compare({ ids: ["live-key"] }, { ids: ["env-key"] }, map).differences,
).toEqual([]);
});
it("still compares an object keyed by ids under the environment's keys when some ids are the same on both sides", () => {
const map = new IdMap();
map.learn("101", "2");
expect(
compare(
{ counts: { "1": 3, "101": 5 } },
{ counts: { "1": 3, "2": 5 } },
map,
).differences,
).toEqual([]);
});
it("reports scalar reordering and keeps unnamed epoch-shaped array measurements literal", () => {
expect(
compare(
{ access: ["a", "b", "c"] },
{ access: ["c", "a", "b"] },
).differences.map((d) => [d.category, d.pointer]),
).toEqual([["order", "/access"]]);
expect(
compare({ stats: [[1792450800, 3]] }, { stats: [[1792285200, 3]] })
.differences,
).toMatchObject([{ category: "value", pointer: "/stats/0/0" }]);
});
});
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(
{ status: 200, body: { kind: "cut", text: '{"data":[' } },
{ status: 200, body: { kind: "none" } },
new IdMap(),
);
expect(result.differences).toEqual([]);
expect(result.normalised.large_bodies).toBe(1);
});
it("compares a live body the probe did not record by status only and counts it", () => {
const unrecorded = {
status: 200,
body: { kind: "none" as const },
unobserved: true,
};
const result = compareAnswers(
unrecorded,
{ status: 200, body: { kind: "json", value: { id: 1 } } },
new IdMap(),
);
expect(result.differences).toEqual([]);
expect(result.normalised.unobserved_bodies).toBe(1);
expect(
compareAnswers(
unrecorded,
{ status: 404, body: { kind: "none" } },
new IdMap(),
).differences,
).toEqual([{ category: "status", pointer: "", live: "200", env: "404" }]);
});
});
describe("scalar sets of mapped ids", () => {
it("differ only in order once live ids are mapped", () => {
const map = new IdMap();
map.learn(101, 1);
map.learn(102, 2);
const compared = compareAnswers(
{ status: 200, body: { kind: "json", value: { ids: [101, 102] } } },
{ status: 200, body: { kind: "json", value: { ids: [2, 1] } } },
map,
);
expect(compared.differences).toEqual([
{ category: "order", pointer: "/ids" },
]);
expect(compared.normalised.ids).toBe(2);
});
});
describe("ids two collections reuse", () => {
it("compares an entity's own id within the call's collection and any other id by the probe's mapping", () => {
const map = new IdMap();
map.learn(101, 1, "/api/books");
map.learn(101, 2, "/api/authors");
const answer = (body: JsonValue) => ({
status: 200,
body: { kind: "json" as const, value: body },
});
expect(
compareAnswers(answer({ id: 101 }), answer({ id: 2 }), map, {
scope: "/api/authors",
}).differences,
).toEqual([]);
expect(
compareAnswers(answer({ book_id: 101 }), answer({ book_id: 2 }), map)
.differences,
).toEqual([
{ category: "value", pointer: "/book_id", live: "101", env: "2" },
]);
});
});
describe("identity responses two collections reuse", () => {
it("compares a create's bare id and created member within the call's collection", () => {
const books = new IdMap();
books.learn(103, 1, "/api/books");
learnIds(103, 3, books, {
heldIds: new Set(),
scope: "/api/authors",
identity: true,
});
const created = new IdMap();
created.learn(103, 1, "/api/books");
learnIds({ created: 103 }, { created: 3 }, created, {
heldIds: new Set(),
scope: "/api/authors",
entityAt: "/created",
identity: true,
});
const answer = (body: JsonValue) => ({
status: 201,
body: { kind: "json" as const, value: body },
});
expect(books.env(103)).toBe(1);
expect(books.env(103, "/api/authors")).toBe(3);
expect(created.env(103, "/api/authors")).toBe(3);
expect(
compareAnswers(answer(103), answer(3), books, {
scope: "/api/authors",
identity: true,
}).differences,
).toEqual([]);
expect(
compareAnswers(
answer({ created: 103 }),
answer({ created: 3 }),
created,
{
scope: "/api/authors",
at: "/created",
identity: true,
},
).differences,
).toEqual([]);
});
});
describe("wrapped answers", () => {
it("compares the identity of an entity inside an envelope within the call's collection", () => {
const map = new IdMap();
map.learn(101, 1, "/api/books");
learnIds(
{ data: [{ id: 101, name: "Bob" }] },
{ data: [{ id: 2, name: "Bob" }] },
map,
{ heldIds: new Set(), scope: "/api/authors", entityAt: "/data" },
);
expect(map.env(101, "/api/authors")).toBe(2);
expect(
compareAnswers(
{
status: 200,
body: { kind: "json", value: { data: { id: 101, name: "Bob" } } },
},
{
status: 200,
body: { kind: "json", value: { data: { id: 2, name: "Bob" } } },
},
map,
{ scope: "/api/authors", at: "/data" },
).differences,
).toEqual([]);
});
});
describe("raw ids a mapping gives to other entities", () => {
const answer = (body: JsonValue) => ({
status: 200,
body: { kind: "json" as const, value: body },
});
it("reports an entity's own identity equal on both sides that its collection's mapping gives to another entity", () => {
const map = new IdMap();
map.learn(101, 1, "/api/books");
map.learn(102, 101, "/api/books");
const books = { scope: "/api/books" };
expect(
compareAnswers(answer({ id: 101 }), answer({ id: 101 }), map, books)
.differences,
).toEqual([{ category: "value", pointer: "/id", live: "101", env: "101" }]);
expect(
compareAnswers(answer({ id: 102 }), answer({ id: 101 }), map, books)
.differences,
).toEqual([]);
});
it("leaves an equal reference equal, as the probe's mapping cannot tell which collection it names", () => {
const map = new IdMap();
// Another collection's create reassigned live 2, while the seeded project 2 is the same on both sides.
map.learn(2, 3);
map.learn(5, 2);
expect(
compareAnswers(
answer({ project_id: "2" }),
answer({ project_id: "2" }),
map,
).differences,
).toEqual([]);
});
});