mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 00:36:35 +00:00
Normalize parsed facts at their owning boundaries across PE exports and certificates, ELF section names, GPU bundles, IL2CPP generic types, dyld slice selection, source maps, environment replay, and JavaScript mapped arguments. Preserve partial facts and explicit unsupported cases; clarify the corresponding documentation. Validation: 831 affected tests passed with one existing skip across 23 files; build, typecheck/lint, documentation, formatting, and 2133-file test discovery passed. Format controls are synthetic; real Unity, GPU, and ARM-device execution is unverified. Fixes #6709 Fixes #6688 Fixes #6719 Fixes #6718 Fixes #6704 Fixes #6703 Fixes #6692 Fixes #6723 Fixes #6687 Fixes #6726 Fixes #6696 Fixes #6691 Fixes #6727
1253 lines
37 KiB
TypeScript
1253 lines
37 KiB
TypeScript
import { expect, it } from "vitest";
|
|
import {
|
|
analysisViewCaptureEvidence,
|
|
environmentManifestCaptureFixture,
|
|
environmentStateCaptureFixture,
|
|
timedCaptureEvidence,
|
|
type TimedCaptureRow,
|
|
} from "../../../tests/fixtures/analysisView.js";
|
|
import {
|
|
SCENARIO_EXCHANGES,
|
|
scenarioCaptureEvidence,
|
|
scenarioCaptureFixture,
|
|
} from "../../../tests/fixtures/browserScenarioCapture.js";
|
|
import type { Evidence } from "../evidence.js";
|
|
import type { JsonValue } from "../jsonValue.js";
|
|
import {
|
|
projectAnalysisView,
|
|
type AnalysisViewParent,
|
|
type AnalysisViewResult,
|
|
} from "./analysisView.js";
|
|
import { EnvironmentStateAccumulator } from "./EnvironmentStateAccumulator.js";
|
|
import {
|
|
at,
|
|
identityCandidates,
|
|
replaceAt,
|
|
type JsonObject,
|
|
} from "./environmentStateRules.js";
|
|
import { environmentStateFields } from "./environmentStateView.js";
|
|
|
|
type State = Extract<AnalysisViewResult, { kind: "environment-state" }>;
|
|
const ALL_RULES = [
|
|
"after-uid-literal",
|
|
"opaque-token",
|
|
"sibling-variance",
|
|
] as const;
|
|
|
|
const parentOf = (
|
|
evidence: Evidence = analysisViewCaptureEvidence(
|
|
environmentStateCaptureFixture(),
|
|
),
|
|
): AnalysisViewParent => ({
|
|
evidenceId: evidence.evidence_id,
|
|
operation: evidence.operation,
|
|
normalizedResult: evidence.normalized_result,
|
|
subject: evidence.subject,
|
|
limitations: [],
|
|
parameters: evidence.parameters,
|
|
});
|
|
|
|
const stateOf = (parent: AnalysisViewParent = parentOf()): State => {
|
|
const result = projectAnalysisView(parent, {
|
|
kind: "environment-state",
|
|
template_rules: [...ALL_RULES],
|
|
});
|
|
if (!result.ok || result.value.kind !== "environment-state")
|
|
throw new Error("Expected an environment state");
|
|
return result.value;
|
|
};
|
|
|
|
const captureOf = (rows: readonly TimedCaptureRow[]): AnalysisViewParent =>
|
|
parentOf(timedCaptureEvidence(rows));
|
|
|
|
const viewOf = (state: State, tool: string) =>
|
|
state.views.find((view) => view.tool === tool);
|
|
|
|
it("extracts the state the Grafana-shaped capture's reads observed", () => {
|
|
const state = stateOf();
|
|
expect(state.timeline).toBe("recorded-start-time");
|
|
expect(state.types).toContainEqual({
|
|
collection_template: "/api/folders",
|
|
id_field: "uid",
|
|
id_basis: "path-parameter",
|
|
init_before_ordinal: 20,
|
|
volatile_fields: [],
|
|
});
|
|
expect(state.types).toContainEqual({
|
|
collection_template: "/api/search",
|
|
id_field: "id",
|
|
id_basis: "field-name",
|
|
init_before_ordinal: null,
|
|
volatile_fields: [],
|
|
});
|
|
expect(
|
|
state.entities.filter((entity) => entity.type === "/api/folders"),
|
|
).toEqual([
|
|
{
|
|
type: "/api/folders",
|
|
collection: "/api/folders",
|
|
id: "fg1jo91e57e2ob",
|
|
fields: { id: 5, uid: "fg1jo91e57e2ob", title: "probe folder" },
|
|
view_fields: [],
|
|
record_ordinals: [12, 14],
|
|
},
|
|
{
|
|
type: "/api/folders",
|
|
collection: "/api/folders",
|
|
id: "general",
|
|
fields: { id: 0, uid: "general", title: "Dashboards" },
|
|
view_fields: [],
|
|
record_ordinals: [13],
|
|
},
|
|
]);
|
|
expect(viewOf(state, "get_folders")).toMatchObject({
|
|
shape: "list",
|
|
pointer: "",
|
|
membership: "recorded",
|
|
});
|
|
expect(viewOf(state, "get_folders_by_uid")).toMatchObject({
|
|
shape: "item",
|
|
envelope_ordinal: 12,
|
|
checked_record_ordinals: [12, 13],
|
|
});
|
|
expect(viewOf(state, "get_dashboards_by_uid")).toMatchObject({
|
|
shape: "item",
|
|
pointer: "/dashboard",
|
|
});
|
|
expect(viewOf(state, "get_annotations")).toEqual({
|
|
tool: "get_annotations",
|
|
shape: "recorded",
|
|
reason: "no-init-recording",
|
|
});
|
|
// The one team member listed proves no suffix-named identity, so the list is a document of its path.
|
|
expect(
|
|
state.documents.map((document) => [document.tool, document.path]),
|
|
).toEqual([
|
|
[
|
|
"get_datasources_health_by_uid",
|
|
"/api/datasources/uid/dg1jo96tern5sb/health",
|
|
],
|
|
["get_org", "/api/org"],
|
|
["get_teams_members_by_id", "/api/teams/1/members"],
|
|
["get_user", "/api/user"],
|
|
]);
|
|
expect(viewOf(state, "get_plugins_settings_by_plugin_id")).toEqual({
|
|
tool: "get_plugins_settings_by_plugin_id",
|
|
shape: "recorded",
|
|
reason: "path-varies",
|
|
});
|
|
expect(state.unknowns.map((unknown) => unknown.kind)).toEqual([
|
|
"authorization-model",
|
|
"response-semantics",
|
|
"state-transition",
|
|
]);
|
|
expect(
|
|
state.limitations.filter((limitation) =>
|
|
limitation.startsWith("Body shapes"),
|
|
),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("keeps collections with repeated path separators distinct", () => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
["GET", "/api/books", [{ id: 1, title: "Single separator" }]],
|
|
["GET", "/api//books", [{ id: 2, title: "Repeated separator" }]],
|
|
]),
|
|
);
|
|
expect(state.types.map((type) => type.collection_template)).toEqual(
|
|
expect.arrayContaining(["/api/books", "/api//books"]),
|
|
);
|
|
});
|
|
|
|
it("ends a type's state at the first write under its root, not at unrelated writes", () => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
["GET", "/api/items/1", { id: 1, title: "Granite Ledger" }],
|
|
["PUT", "/api/notes/9", { id: 9 }],
|
|
["GET", "/api/items/1", { id: 1, title: "Granite Ledger" }],
|
|
["PUT", "/api/items/1", { id: 1, title: "Saltmarsh Depot" }],
|
|
["GET", "/api/items/1", { id: 1, title: "Saltmarsh Depot" }],
|
|
]),
|
|
);
|
|
expect(state.types).toEqual([
|
|
{
|
|
collection_template: "/api/items",
|
|
id_field: "id",
|
|
id_basis: "path-parameter",
|
|
init_before_ordinal: 3,
|
|
volatile_fields: [],
|
|
},
|
|
]);
|
|
expect(state.entities).toMatchObject([
|
|
{ id: "1", fields: { title: "Granite Ledger" }, record_ordinals: [0, 2] },
|
|
]);
|
|
expect(state.views).toContainEqual(
|
|
expect.objectContaining({ shape: "item", checked_record_ordinals: [2] }),
|
|
);
|
|
});
|
|
|
|
it("keeps the latest value of a field that changed with no related write and names it volatile", () => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
["GET", "/api/items/1", { id: 1, title: "Lighthouse Rota", seen: 10 }],
|
|
["GET", "/api/items/1", { id: 1, title: "Lighthouse Rota", seen: 11 }],
|
|
["GET", "/api/items/2", { id: 2, title: "Blue Heron", seen: 3 }],
|
|
]),
|
|
);
|
|
expect(state.types[0]).toMatchObject({ volatile_fields: ["seen"] });
|
|
expect(state.entities[0]).toMatchObject({
|
|
id: "1",
|
|
fields: { seen: 11 },
|
|
});
|
|
expect(state.views).toContainEqual(
|
|
expect.objectContaining({
|
|
shape: "item",
|
|
checked_record_ordinals: [1, 2],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("leaves a type recorded when a value changed across a write under another root", () => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
["GET", "/api/search", [{ id: 1, title: "Granite Ledger" }]],
|
|
["POST", "/api/dashboards/db", { id: 1, title: "Saltmarsh Depot" }],
|
|
["GET", "/api/search", [{ id: 1, title: "Saltmarsh Depot" }]],
|
|
]),
|
|
);
|
|
expect(state.types).toEqual([]);
|
|
expect(state.entities).toEqual([]);
|
|
expect(viewOf(state, "get_search")).toEqual({
|
|
tool: "get_search",
|
|
shape: "recorded",
|
|
reason: "fields-differ",
|
|
});
|
|
});
|
|
|
|
it("leaves a type recorded when a value changed across a write already in flight", () => {
|
|
const exchange = ([method, path, seconds, time, title]: readonly [
|
|
string,
|
|
string,
|
|
number,
|
|
number,
|
|
string,
|
|
]) => ({
|
|
startedDateTime: new Date(
|
|
Date.parse("2026-10-17T18:00:00.000Z") + seconds * 1000,
|
|
).toISOString(),
|
|
time,
|
|
request: { method, url: `http://app.example${path}` },
|
|
response: {
|
|
status: 200,
|
|
content: {
|
|
mimeType: "application/json",
|
|
text: JSON.stringify([{ id: 1, title }]),
|
|
},
|
|
},
|
|
});
|
|
const state = stateOf(
|
|
parentOf(
|
|
analysisViewCaptureEvidence(
|
|
environmentManifestCaptureFixture([
|
|
exchange([
|
|
"POST",
|
|
"/api/dashboards/db",
|
|
0,
|
|
30_000,
|
|
"Saltmarsh Depot",
|
|
]),
|
|
exchange(["GET", "/api/search", 10, 5, "Granite Ledger"]),
|
|
exchange(["GET", "/api/search", 40, 5, "Saltmarsh Depot"]),
|
|
]),
|
|
),
|
|
),
|
|
);
|
|
expect(viewOf(state, "get_search")).toEqual({
|
|
tool: "get_search",
|
|
shape: "recorded",
|
|
reason: "fields-differ",
|
|
});
|
|
});
|
|
|
|
it("does not list the whole collection when a counter exceeds the number listed", () => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
[
|
|
"GET",
|
|
"/api/items",
|
|
{ total: 50, items: [{ id: 1, title: "Ada Finch" }] },
|
|
],
|
|
]),
|
|
);
|
|
expect(viewOf(state, "get_items")).toMatchObject({
|
|
shape: "list",
|
|
membership: "recorded",
|
|
counters: [],
|
|
});
|
|
});
|
|
|
|
it("lists the whole collection with its counter only when every recording did", () => {
|
|
const rows: TimedCaptureRow[] = [
|
|
[
|
|
"GET",
|
|
"/api/items",
|
|
{
|
|
total: 2,
|
|
items: [
|
|
{ id: 1, title: "Ada Finch" },
|
|
{ id: 2, title: "Tomas Vey" },
|
|
],
|
|
},
|
|
],
|
|
["GET", "/api/items/1", { title: "Ada Finch", id: 1, notes: "first" }],
|
|
["GET", "/api/items/2", { id: 2, notes: "second", title: "Tomas Vey" }],
|
|
];
|
|
const whole = stateOf(captureOf(rows));
|
|
expect(whole.views).toContainEqual(
|
|
expect.objectContaining({
|
|
shape: "list",
|
|
pointer: "/items",
|
|
fields: ["id", "title"],
|
|
membership: "all",
|
|
counters: ["total"],
|
|
}),
|
|
);
|
|
expect(whole.entities.map((entity) => entity.fields)).toEqual([
|
|
{ id: 1, title: "Ada Finch", notes: "first" },
|
|
{ id: 2, notes: "second", title: "Tomas Vey" },
|
|
]);
|
|
const filtered = stateOf(
|
|
captureOf([
|
|
...rows,
|
|
[
|
|
"GET",
|
|
"/api/items?title=Ada",
|
|
{ total: 1, items: [{ id: 1, title: "Ada Finch" }] },
|
|
],
|
|
]),
|
|
);
|
|
expect(filtered.views).toContainEqual(
|
|
expect.objectContaining({ shape: "list", membership: "recorded" }),
|
|
);
|
|
});
|
|
|
|
it("leaves recorded the views it cannot reproduce, with the reason", () => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
[
|
|
"GET",
|
|
"/api/boards/1",
|
|
{ meta: { viewer: "Ada Finch" }, board: { id: 1, name: "Rota" } },
|
|
],
|
|
[
|
|
"GET",
|
|
"/api/boards/2",
|
|
{ meta: { viewer: "Mina Okafor" }, board: { id: 2, name: "Depot" } },
|
|
],
|
|
["GET", "/api/ledgers/1", '{"id":1,"total":12345678901234567890}'],
|
|
["POST", "/api/boards/search", [{ id: 1, name: "Rota" }]],
|
|
]),
|
|
);
|
|
expect(
|
|
state.views.map((view) =>
|
|
view.shape === "recorded" ? [view.tool, view.reason] : view.tool,
|
|
),
|
|
).toEqual([
|
|
["post_boards_search", "query-body"],
|
|
["get_boards_by_id", "envelope-varies"],
|
|
["get_ledgers_by_id", "body-unusable"],
|
|
]);
|
|
expect(state.types).toEqual([]);
|
|
expect(state.entities).toEqual([]);
|
|
});
|
|
|
|
it("leaves a body too deep for the state's own nesting recorded instead of failing to seal", () => {
|
|
let deep: JsonValue = { theme: "dark" };
|
|
for (let level = 0; level < 995; level += 1) deep = { nested: deep };
|
|
const state = stateOf(
|
|
captureOf([
|
|
["GET", "/api/settings", deep],
|
|
["PUT", "/api/settings", { theme: "light" }],
|
|
]),
|
|
);
|
|
expect(state.documents).toEqual([]);
|
|
expect(viewOf(state, "get_settings")).toEqual({
|
|
tool: "get_settings",
|
|
shape: "recorded",
|
|
reason: "body-unusable",
|
|
});
|
|
});
|
|
|
|
it("holds a written path's non-entity read as a document", () => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
["GET", "/api/settings", { theme: "dark", density: "compact" }],
|
|
["PUT", "/api/settings", { theme: "light", density: "compact" }],
|
|
["GET", "/api/settings", { theme: "light", density: "compact" }],
|
|
]),
|
|
);
|
|
expect(state.documents).toEqual([
|
|
{
|
|
tool: "get_settings",
|
|
path: "/api/settings",
|
|
value: { theme: "dark", density: "compact" },
|
|
record_ordinal: 0,
|
|
},
|
|
]);
|
|
expect(viewOf(state, "get_settings")).toEqual({
|
|
tool: "get_settings",
|
|
shape: "document",
|
|
checked_record_ordinals: [0],
|
|
});
|
|
});
|
|
|
|
it("holds a read-only non-entity read as a document unless it varies by request line", () => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
["GET", "/api/config", { theme: "dark", locale: "en-GB" }],
|
|
[
|
|
"GET",
|
|
"/api/rates",
|
|
[
|
|
{ currency: "EUR", rate: 1.1 },
|
|
{ currency: "EUR", rate: 1.2 },
|
|
],
|
|
],
|
|
["GET", "/api/stats?range=day", { visits: 3 }],
|
|
["GET", "/api/stats?range=week", { visits: 9 }],
|
|
]),
|
|
);
|
|
expect(state.documents).toEqual([
|
|
{
|
|
tool: "get_config",
|
|
path: "/api/config",
|
|
value: { theme: "dark", locale: "en-GB" },
|
|
record_ordinal: 0,
|
|
},
|
|
{
|
|
tool: "get_rates",
|
|
path: "/api/rates",
|
|
value: [
|
|
{ currency: "EUR", rate: 1.1 },
|
|
{ currency: "EUR", rate: 1.2 },
|
|
],
|
|
record_ordinal: 1,
|
|
},
|
|
]);
|
|
expect(viewOf(state, "get_stats")).toEqual({
|
|
tool: "get_stats",
|
|
shape: "recorded",
|
|
reason: "query-varies",
|
|
});
|
|
expect(state.types).toEqual([]);
|
|
});
|
|
|
|
it("leaves a document recorded when rendering it exceeds the body limit", () => {
|
|
const small = stateOf(
|
|
captureOf([["GET", "/api/config", '{"values":[1e3]}']]),
|
|
);
|
|
expect(viewOf(small, "get_config")).toMatchObject({ shape: "document" });
|
|
expect(small.documents[0]?.value).toEqual({ values: [1000] });
|
|
// 60,000 `1e3` tokens are 240,012 recorded bytes and 300,012 once rendered as `1000`.
|
|
const values = Array.from({ length: 60_000 }, () => "1e3").join(",");
|
|
const body = `{"values":[${values}]}`;
|
|
expect(Buffer.byteLength(body, "utf8")).toBe(240_012);
|
|
const state = stateOf(captureOf([["GET", "/api/config", body]]));
|
|
expect(state.documents).toEqual([]);
|
|
expect(viewOf(state, "get_config")).toEqual({
|
|
tool: "get_config",
|
|
shape: "recorded",
|
|
reason: "rendered-oversize",
|
|
});
|
|
});
|
|
|
|
it("infers a suffix-named identity only when no identity name identifies the listed items", () => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
[
|
|
"GET",
|
|
"/api/memberships",
|
|
[
|
|
{ orgId: 7, userId: 1, role: "Admin" },
|
|
{ orgId: 8, userId: 1, role: "Viewer" },
|
|
],
|
|
],
|
|
[
|
|
"GET",
|
|
"/api/seats",
|
|
{
|
|
total: 2,
|
|
seats: [
|
|
{ parentId: 1, seat_id: "s-1" },
|
|
{ parentId: 1, seat_id: "s-2" },
|
|
],
|
|
},
|
|
],
|
|
[
|
|
"GET",
|
|
"/api/projects",
|
|
[
|
|
{ projectId: "a", id: 2 },
|
|
{ projectId: "b", id: 3 },
|
|
],
|
|
],
|
|
[
|
|
"GET",
|
|
"/api/grants",
|
|
[
|
|
{ userId: 1, orgId: 7 },
|
|
{ userId: 2, orgId: 8 },
|
|
],
|
|
],
|
|
["GET", "/api/members", []],
|
|
["GET", "/api/members", [{ memberId: "m-1" }, { memberId: "m-2" }]],
|
|
["GET", "/api/comments", [{ postId: 9, authorId: 3, text: "hi" }]],
|
|
]),
|
|
);
|
|
expect(
|
|
state.types.map(({ collection_template, id_field, id_basis }) => [
|
|
collection_template,
|
|
id_field,
|
|
id_basis,
|
|
]),
|
|
).toEqual([
|
|
// Both identify the grants; the earlier key in item order wins.
|
|
["/api/grants", "userId", "field-name"],
|
|
// The empty first recording constrains no field.
|
|
["/api/members", "memberId", "field-name"],
|
|
["/api/memberships", "orgId", "field-name"],
|
|
["/api/projects", "id", "field-name"],
|
|
["/api/seats", "seat_id", "field-name"],
|
|
]);
|
|
expect(viewOf(state, "get_seats")).toMatchObject({
|
|
shape: "list",
|
|
pointer: "/seats",
|
|
membership: "all",
|
|
counters: ["total"],
|
|
});
|
|
expect(viewOf(state, "get_members")).toMatchObject({ shape: "list" });
|
|
expect(
|
|
state.entities
|
|
.filter((entity) => entity.type === "/api/memberships")
|
|
.map((entity) => entity.id),
|
|
).toEqual(["7", "8"]);
|
|
// One listed item shows no field distinct, so its foreign keys identify nothing.
|
|
expect(viewOf(state, "get_comments")).toMatchObject({ shape: "document" });
|
|
});
|
|
|
|
it("chooses an envelope's array by identity name, then by a suffix-named field", () => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
[
|
|
"GET",
|
|
"/api/board",
|
|
{
|
|
labels: [{ text: "urgent" }, { text: "later" }],
|
|
cards: [
|
|
{ cardId: 1, text: "Granite Ledger" },
|
|
{ cardId: 2, text: "Blue Heron" },
|
|
],
|
|
},
|
|
],
|
|
[
|
|
"GET",
|
|
"/api/directory",
|
|
{
|
|
owners: [{ orgId: 1 }, { orgId: 2 }],
|
|
teams: [
|
|
{ id: 1, orgId: 1 },
|
|
{ id: 2, orgId: 1 },
|
|
],
|
|
},
|
|
],
|
|
]),
|
|
);
|
|
expect(
|
|
state.types.map(({ collection_template, id_field }) => [
|
|
collection_template,
|
|
id_field,
|
|
]),
|
|
).toEqual([
|
|
["/api/board", "cardId"],
|
|
["/api/directory", "id"],
|
|
]);
|
|
expect(viewOf(state, "get_board")).toMatchObject({
|
|
shape: "list",
|
|
pointer: "/cards",
|
|
});
|
|
expect(viewOf(state, "get_directory")).toMatchObject({
|
|
shape: "list",
|
|
pointer: "/teams",
|
|
});
|
|
});
|
|
|
|
it("locates an item entity wrapped in a one-element array and merges it with the list view", () => {
|
|
const first = "6ad47b5c1023b500010873e3";
|
|
const second = "6ad47b5c1023b500010873e4";
|
|
const state = stateOf(
|
|
captureOf([
|
|
[
|
|
"GET",
|
|
"/ghost/api/admin/posts/",
|
|
{
|
|
posts: [
|
|
{ id: first, title: "Granite Ledger", status: "draft" },
|
|
{ id: second, title: "Blue Heron", status: "published" },
|
|
],
|
|
meta: { pagination: { page: 1, pages: 1 } },
|
|
},
|
|
],
|
|
[
|
|
"GET",
|
|
`/ghost/api/admin/posts/${first}/`,
|
|
{
|
|
posts: [
|
|
{
|
|
id: first,
|
|
title: "Granite Ledger",
|
|
status: "draft",
|
|
html: "<p>Quarry count</p>",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
["GET", "/api/depots/5", [{ id: 5, name: "Saltmarsh Depot" }]],
|
|
["GET", "/api/rosters/9", [{ id: 31, roster: 9, name: "Ada Finch" }]],
|
|
]),
|
|
);
|
|
expect(viewOf(state, "get_rosters_by_id")).toEqual({
|
|
tool: "get_rosters_by_id",
|
|
shape: "recorded",
|
|
reason: "no-identity",
|
|
});
|
|
const posts = "/ghost/api/admin/posts";
|
|
expect(state.types).toContainEqual({
|
|
collection_template: posts,
|
|
id_field: "id",
|
|
id_basis: "path-parameter",
|
|
init_before_ordinal: null,
|
|
volatile_fields: [],
|
|
});
|
|
expect(state.views).toContainEqual(
|
|
expect.objectContaining({
|
|
shape: "item",
|
|
type: posts,
|
|
pointer: "/posts/0",
|
|
fields: ["id", "title", "status", "html"],
|
|
checked_record_ordinals: [1],
|
|
}),
|
|
);
|
|
expect(state.views).toContainEqual(
|
|
expect.objectContaining({
|
|
shape: "list",
|
|
type: posts,
|
|
pointer: "/posts",
|
|
membership: "all",
|
|
}),
|
|
);
|
|
expect(state.views).toContainEqual(
|
|
expect.objectContaining({
|
|
shape: "item",
|
|
type: "/api/depots",
|
|
pointer: "/0",
|
|
}),
|
|
);
|
|
expect(state.entities.filter((entity) => entity.type === posts)).toEqual([
|
|
{
|
|
type: posts,
|
|
collection: posts,
|
|
id: first,
|
|
fields: {
|
|
id: first,
|
|
title: "Granite Ledger",
|
|
status: "draft",
|
|
html: "<p>Quarry count</p>",
|
|
},
|
|
view_fields: [],
|
|
record_ordinals: [0, 1],
|
|
},
|
|
expect.objectContaining({ id: second, record_ordinals: [0] }),
|
|
]);
|
|
});
|
|
|
|
it("reads and replaces only the root, one member or the element of a one-element array", () => {
|
|
const body = { posts: [{ id: 1 }], meta: { posts: 1 } };
|
|
expect(at(body, "/posts/0")).toEqual({ id: 1 });
|
|
expect(replaceAt(body, "/posts/0", null)).toEqual({
|
|
posts: [null],
|
|
meta: { posts: 1 },
|
|
});
|
|
expect(at([{ id: 1 }], "/0")).toEqual({ id: 1 });
|
|
expect(replaceAt([{ id: 1 }], "/0", { id: 2 })).toEqual([{ id: 2 }]);
|
|
for (const pointer of ["/posts/1", "/meta/posts", "/posts/0/id"])
|
|
expect(at(body, pointer)).toBeUndefined();
|
|
expect(at({ posts: [{ id: 1 }, { id: 2 }] }, "/posts/0")).toBeUndefined();
|
|
expect(replaceAt(body, "/meta/posts", null)).toBe(body);
|
|
});
|
|
|
|
it("leaves an item path's non-entity read recorded", () => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
["GET", "/api/files/123", { text: "initial document" }],
|
|
["PUT", "/api/files/123", null],
|
|
]),
|
|
);
|
|
expect(state.documents).toEqual([]);
|
|
expect(viewOf(state, "get_files_by_id")).toMatchObject({ shape: "recorded" });
|
|
});
|
|
|
|
it("holds a deleted path's initial non-entity read as a document", () => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
["GET", "/api/settings", { text: "initial document" }],
|
|
["DELETE", "/api/settings", null],
|
|
]),
|
|
);
|
|
expect(state.documents).toEqual([
|
|
{
|
|
tool: "get_settings",
|
|
path: "/api/settings",
|
|
value: { text: "initial document" },
|
|
record_ordinal: 0,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("orders a scenario capture by request order and needs its response bodies", () => {
|
|
const state = stateOf(parentOf(scenarioCaptureEvidence()));
|
|
expect(state.timeline).toBe("record-order");
|
|
expect(state.types).toEqual([
|
|
{
|
|
collection_template: "/api/users",
|
|
id_field: "id",
|
|
id_basis: "path-parameter",
|
|
init_before_ordinal: null,
|
|
volatile_fields: [],
|
|
},
|
|
]);
|
|
expect(state.entities.map((entity) => entity.fields)).toEqual([
|
|
{ id: 2, login: "probe" },
|
|
{ id: 3, login: "viewer" },
|
|
]);
|
|
const headersOnly = scenarioCaptureEvidence(
|
|
scenarioCaptureFixture(SCENARIO_EXCHANGES, {
|
|
request_body: true,
|
|
response_body: false,
|
|
header_values: true,
|
|
}),
|
|
);
|
|
expect(
|
|
projectAnalysisView(parentOf(headersOnly), {
|
|
kind: "environment-state",
|
|
template_rules: [],
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
_tag: "AnalysisUnsupportedTargetError",
|
|
message: expect.stringContaining("response_body selected"),
|
|
},
|
|
});
|
|
});
|
|
|
|
it("refuses when extraction would exceed the projection budget", () => {
|
|
expect(
|
|
projectAnalysisView(
|
|
parentOf(),
|
|
{ kind: "environment-state", template_rules: [...ALL_RULES] },
|
|
{ state: new EnvironmentStateAccumulator(160 * 1024, [...ALL_RULES]) },
|
|
),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
_tag: "AnalysisResourceConstraintError",
|
|
resource: "memory",
|
|
message: expect.stringContaining("Environment-state"),
|
|
},
|
|
});
|
|
});
|
|
|
|
it("ends every type's state at a write whose URL was redacted, and says so", () => {
|
|
const parent = captureOf([
|
|
["GET", "/api/items/1", { id: 1, title: "Granite Ledger" }],
|
|
["PUT", "/api/items/1", { id: 1 }],
|
|
["GET", "/api/items/1", { id: 1, title: "Saltmarsh Depot" }],
|
|
]);
|
|
const result = structuredClone(parent.normalizedResult) as {
|
|
records: { reported: { request: { url: string | null } } }[];
|
|
};
|
|
// A scenario keeps a redacted URL as null, so the write's target is unknown.
|
|
result.records[1]!.reported.request.url = null;
|
|
const state = stateOf({ ...parent, normalizedResult: result });
|
|
expect(state.types).toMatchObject([{ init_before_ordinal: 1 }]);
|
|
expect(state.entities).toMatchObject([
|
|
{ fields: { title: "Granite Ledger" }, record_ordinals: [0] },
|
|
]);
|
|
expect(state.limitations).toContain(
|
|
"1 of the recorded writes had a redacted URL; each ends the initial state of every type, since its target is unknown.",
|
|
);
|
|
});
|
|
|
|
it("infers no identity for a list whose recordings held no items and holds it as a document", () => {
|
|
const state = stateOf(captureOf([["GET", "/api/widgets", []]]));
|
|
expect(state.types).toEqual([]);
|
|
expect(state.views).toEqual([
|
|
{ tool: "get_widgets", shape: "document", checked_record_ordinals: [0] },
|
|
]);
|
|
});
|
|
|
|
it("does not call a value volatile when it differs between request lines", () => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
["GET", "/api/items/1?lang=fr", { id: 1, name: "Bonjour" }],
|
|
["GET", "/api/items/1", { id: 1, name: "Hello" }],
|
|
]),
|
|
);
|
|
expect(state.types).toEqual([]);
|
|
expect(
|
|
state.views.map((view) =>
|
|
view.shape === "recorded" ? [view.tool, view.reason] : view.tool,
|
|
),
|
|
).toEqual([["get_items_by_id", "fields-differ"]]);
|
|
});
|
|
|
|
it("orders entities by their earliest sighting, so a complete list keeps all membership", () => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
[
|
|
"GET",
|
|
"/api/items",
|
|
[
|
|
{ id: 1, title: "Granite Ledger" },
|
|
{ id: 2, title: "Blue Heron" },
|
|
],
|
|
],
|
|
["GET", "/api/items/2", { id: 2, title: "Blue Heron" }],
|
|
["GET", "/api/items/1", { id: 1, title: "Granite Ledger" }],
|
|
]),
|
|
);
|
|
expect(state.entities.map((entity) => entity.id)).toEqual(["1", "2"]);
|
|
expect(state.views).toContainEqual(
|
|
expect.objectContaining({ tool: "get_items", membership: "all" }),
|
|
);
|
|
});
|
|
|
|
it("does not take a later recording that is itself an entity as the list", () => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
["GET", "/api/items", { items: [{ id: 1, title: "Granite Ledger" }] }],
|
|
[
|
|
"GET",
|
|
"/api/items",
|
|
{ id: 99, items: [{ id: 2, title: "Blue Heron" }] },
|
|
],
|
|
]),
|
|
);
|
|
// Without one list across its recordings, the read is held as a document, not entities.
|
|
expect(state.views).toContainEqual(
|
|
expect.objectContaining({ tool: "get_items", shape: "document" }),
|
|
);
|
|
expect(state.entities).toEqual([]);
|
|
});
|
|
|
|
it("leaves a type recorded when two of its tools read different values across a write", () => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
["GET", "/api/search", [{ id: 1, title: "Granite Ledger" }]],
|
|
["POST", "/api/dashboards/db", { id: 9 }],
|
|
["GET", "/api/search/1", { id: 1, title: "Saltmarsh Depot" }],
|
|
]),
|
|
);
|
|
expect(state.types).toEqual([]);
|
|
expect(state.entities).toEqual([]);
|
|
expect(
|
|
state.views.map((view) =>
|
|
view.shape === "recorded" ? [view.tool, view.reason] : view.tool,
|
|
),
|
|
).toEqual([
|
|
["get_search", "fields-differ"],
|
|
["get_search_by_id", "fields-differ"],
|
|
]);
|
|
});
|
|
|
|
it.each(["old", "new"])(
|
|
"retains field observations through sparse reads across a write (%s)",
|
|
(title) => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
["GET", "/api/search", [{ id: 1, title: "old" }]],
|
|
["POST", "/api/items", { id: 2 }],
|
|
["GET", "/api/search?fields=id", [{ id: 1 }]],
|
|
["GET", "/api/search/1", { id: 1, title }],
|
|
]),
|
|
);
|
|
if (title === "new") {
|
|
expect(state.types).toEqual([]);
|
|
expect(state.entities).toEqual([]);
|
|
expect(
|
|
state.views.every(
|
|
(view) =>
|
|
view.shape === "recorded" && view.reason === "fields-differ",
|
|
),
|
|
).toBe(true);
|
|
} else {
|
|
expect(viewOf(state, "get_search_by_id")).toMatchObject({
|
|
shape: "item",
|
|
});
|
|
expect(state.entities[0]?.fields.title).toBe("old");
|
|
}
|
|
},
|
|
);
|
|
|
|
it("proves the identity field against every item response", () => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
["GET", "/api/widgets/1", { left: 1, right: 1 }],
|
|
["GET", "/api/widgets/2", { right: 2, left: "other" }],
|
|
]),
|
|
);
|
|
expect(state.types).toMatchObject([
|
|
{ id_field: "right", id_basis: "path-parameter" },
|
|
]);
|
|
expect(state.entities.map((entity) => entity.id)).toEqual(["1", "2"]);
|
|
});
|
|
|
|
it.each(["/api", "/api/items/1/update"])(
|
|
"bounds a type by a write at a related root (%s)",
|
|
(path) => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
["GET", "/api/items/1", { id: 1, title: "initial" }],
|
|
["POST", path, {}],
|
|
["GET", "/api/items/1", { id: 1, title: "after" }],
|
|
]),
|
|
);
|
|
expect(state.types).toMatchObject([{ init_before_ordinal: 1 }]);
|
|
expect(state.entities).toMatchObject([{ fields: { title: "initial" } }]);
|
|
},
|
|
);
|
|
|
|
it("chooses the earliest related write in timeline order, across ancestor and descendant roots", () => {
|
|
const parent = captureOf([
|
|
["GET", "/api/items/1", { id: 1, title: "initial" }],
|
|
["POST", "/api/items/1/update", {}],
|
|
["POST", "/api", {}],
|
|
["GET", "/api/items/1", { id: 1, title: "after" }],
|
|
]);
|
|
const data = structuredClone(parent.normalizedResult) as {
|
|
records: { reported: { startedDateTime: string } }[];
|
|
};
|
|
const first = data.records[1]!.reported.startedDateTime;
|
|
data.records[1]!.reported.startedDateTime =
|
|
data.records[2]!.reported.startedDateTime;
|
|
data.records[2]!.reported.startedDateTime = first;
|
|
const state = stateOf({ ...parent, normalizedResult: data });
|
|
expect(state.types).toMatchObject([{ init_before_ordinal: 2 }]);
|
|
expect(state.entities).toMatchObject([{ fields: { title: "initial" } }]);
|
|
});
|
|
|
|
it("bounds a parameterized namespace by concrete writes before publishing new items", () => {
|
|
const result = projectAnalysisView(
|
|
captureOf([
|
|
["GET", "/api/v1/repos/alice/alpha/issues/1", { id: 1 }],
|
|
["GET", "/api/v1/repos/bob/beta/issues/2", { id: 2 }],
|
|
["GET", "/api/v1/repos/carol/gamma/issues/3", { id: 3 }],
|
|
["POST", "/api/v1/repos/dana/delta/issues", { id: 4 }],
|
|
["GET", "/api/v1/repos/dana/delta/issues/4", { id: 4 }],
|
|
]),
|
|
{
|
|
kind: "environment-state",
|
|
template_rules: [...ALL_RULES, "sibling-pair-variance"],
|
|
},
|
|
);
|
|
if (!result.ok || result.value.kind !== "environment-state")
|
|
throw new Error("Expected state");
|
|
expect(result.value.entities.map((entity) => entity.id)).toEqual([
|
|
"1",
|
|
"2",
|
|
"3",
|
|
]);
|
|
expect(result.value.types).toMatchObject([{ init_before_ordinal: 3 }]);
|
|
});
|
|
|
|
it.each(["totalcount", "itemcount"])(
|
|
"recognizes a compact compound item total (%s)",
|
|
(name) => {
|
|
const state = stateOf(
|
|
captureOf([["GET", "/api/items", { [name]: 50, items: [{ id: 1 }] }]]),
|
|
);
|
|
expect(viewOf(state, "get_items")).toMatchObject({
|
|
shape: "list",
|
|
membership: "recorded",
|
|
counters: [],
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
[[], [{ id: 1 }]],
|
|
[[{ id: 1 }], []],
|
|
[[{ id: 1 }], [{ id: 2 }]],
|
|
] as const)(
|
|
"keeps changed list membership across another root's write recorded (%j -> %j)",
|
|
(before, after) => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
["GET", "/api/search", [...before]],
|
|
["POST", "/api/items", { id: 2 }],
|
|
["GET", "/api/search", [...after]],
|
|
]),
|
|
);
|
|
expect(state.types).toEqual([]);
|
|
expect(state.entities).toEqual([]);
|
|
expect(viewOf(state, "get_search")).toEqual({
|
|
tool: "get_search",
|
|
shape: "recorded",
|
|
reason: "fields-differ",
|
|
});
|
|
},
|
|
);
|
|
|
|
it("accepts stable membership across another root's write and changes confined before it", () => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
["GET", "/api/search", []],
|
|
["GET", "/api/search", [{ id: 1 }]],
|
|
["POST", "/api/items", { id: 2 }],
|
|
["GET", "/api/search", [{ id: 1 }]],
|
|
]),
|
|
);
|
|
expect(viewOf(state, "get_search")).toMatchObject({ shape: "list" });
|
|
expect(state.entities.map((entity) => entity.id)).toEqual(["1"]);
|
|
});
|
|
|
|
it.each(["offset", "start", "_start"])(
|
|
"does not let an empty ?%s=1 page prove membership all",
|
|
(name) => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
["GET", "/api/orgs/1/books/1", { id: 1, name: "Alpha" }],
|
|
["GET", `/api/orgs/2/books?${name}=1`, []],
|
|
]),
|
|
);
|
|
expect(viewOf(state, "get_orgs_books_by_id")).toMatchObject({
|
|
shape: "list",
|
|
membership: "recorded",
|
|
});
|
|
},
|
|
);
|
|
|
|
it("does not let an empty ?page=2 page prove membership all", () => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
["GET", "/api/orgs/1/books/1", { id: 1, name: "Alpha" }],
|
|
["GET", "/api/orgs/2/books?page=2", []],
|
|
]),
|
|
);
|
|
expect(viewOf(state, "get_orgs_books_by_id")).toMatchObject({
|
|
shape: "list",
|
|
membership: "recorded",
|
|
});
|
|
});
|
|
|
|
it("does not let ?page=2&per_page=1 prove membership all when it lists the collection", () => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
[
|
|
"GET",
|
|
"/api/orgs/1/books?page=2&per_page=1",
|
|
[{ id: 1, name: "Alpha" }],
|
|
],
|
|
["GET", "/api/orgs/1/books/1", { id: 1, name: "Alpha" }],
|
|
]),
|
|
);
|
|
expect(viewOf(state, "get_orgs_books_by_id")).toMatchObject({
|
|
shape: "list",
|
|
membership: "recorded",
|
|
});
|
|
});
|
|
|
|
it("keeps page 1 membership recorded because the page base is unproven", () => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
["GET", "/api/orgs/1/books?page=1", [{ id: 1, name: "Alpha" }]],
|
|
["GET", "/api/orgs/1/books/1", { id: 1, name: "Alpha" }],
|
|
]),
|
|
);
|
|
expect(viewOf(state, "get_orgs_books_by_id")).toMatchObject({
|
|
shape: "list",
|
|
membership: "recorded",
|
|
});
|
|
});
|
|
|
|
it("keeps membership all when a zero offset lists the collection", () => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
["GET", "/api/orgs/1/books?offset=0", [{ id: 1, name: "Alpha" }]],
|
|
["GET", "/api/orgs/1/books/1", { id: 1, name: "Alpha" }],
|
|
]),
|
|
);
|
|
expect(viewOf(state, "get_orgs_books_by_id")).toMatchObject({
|
|
shape: "list",
|
|
membership: "all",
|
|
});
|
|
});
|
|
|
|
it("keeps membership checks specific to the full request line", () => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
["GET", "/api/search?page=1", [{ id: 1 }]],
|
|
["POST", "/api/items", { id: 2 }],
|
|
["GET", "/api/search?page=2", [{ id: 2 }]],
|
|
]),
|
|
);
|
|
expect(viewOf(state, "get_search")).toMatchObject({
|
|
shape: "list",
|
|
membership: "recorded",
|
|
});
|
|
});
|
|
|
|
it("does not treat a first observation as a membership change", () => {
|
|
const parent = captureOf([
|
|
["GET", "/api/search", [{ id: 1 }]],
|
|
["POST", "/api/items", { id: 2 }],
|
|
["GET", "/api/search", [{ id: 1 }]],
|
|
]);
|
|
const data = structuredClone(parent.normalizedResult) as {
|
|
records: { reported: { time: number } }[];
|
|
};
|
|
data.records[0]!.reported.time = 1500;
|
|
const state = stateOf({ ...parent, normalizedResult: data });
|
|
expect(viewOf(state, "get_search")).toMatchObject({ shape: "list" });
|
|
});
|
|
|
|
it.each([
|
|
[{ id: 1 }, { id: 1, title: "Created" }],
|
|
[{ id: 1, title: "Original" }, { id: 1 }],
|
|
])(
|
|
"keeps fields added or removed across another root's write recorded (%j -> %j)",
|
|
(before, after) => {
|
|
const state = stateOf(
|
|
captureOf([
|
|
["GET", "/api/search/1", before],
|
|
["POST", "/api/items", { id: 2 }],
|
|
["GET", "/api/search/1", after],
|
|
]),
|
|
);
|
|
expect(state.types).toEqual([]);
|
|
expect(state.entities).toEqual([]);
|
|
expect(viewOf(state, "get_search_by_id")).toMatchObject({
|
|
shape: "recorded",
|
|
reason: "fields-differ",
|
|
});
|
|
},
|
|
);
|
|
|
|
it("reads only item totals and next-page markers as signs of a partial list", () => {
|
|
const membership = (body: JsonValue) =>
|
|
stateOf(captureOf([["GET", "/api/items", body]])).views.map((view) =>
|
|
view.shape === "list" ? [view.membership, view.counters] : view.shape,
|
|
);
|
|
// Page and id numbers are not item totals, so a complete page stays whole.
|
|
expect(
|
|
membership({
|
|
page: 1,
|
|
total: 2,
|
|
total_pages: 1,
|
|
account_id: 77,
|
|
items: [{ id: 1 }, { id: 2 }],
|
|
}),
|
|
).toEqual([["all", ["total"]]]);
|
|
expect(membership({ has_more: true, items: [{ id: 1 }, { id: 2 }] })).toEqual(
|
|
[["recorded", []]],
|
|
);
|
|
expect(membership({ total: "50", items: [{ id: 1 }] })).toEqual([
|
|
["recorded", []],
|
|
]);
|
|
});
|
|
|
|
it("reads a sparse list once per candidate field when inferring identity", () => {
|
|
const fields = 2_000;
|
|
const wide: JsonObject = Object.fromEntries(
|
|
Array.from({ length: fields }, (_, index) => [
|
|
`f${String(index)}Id`,
|
|
index,
|
|
]),
|
|
);
|
|
let reads = 0;
|
|
const sparse = Array.from(
|
|
{ length: 5_000 },
|
|
() =>
|
|
new Proxy<JsonObject>(
|
|
{},
|
|
{
|
|
get: (target, key, receiver) => {
|
|
reads += 1;
|
|
return Reflect.get(target, key, receiver) as unknown;
|
|
},
|
|
},
|
|
),
|
|
);
|
|
expect(identityCandidates([wide, ...sparse])).toEqual([]);
|
|
// Each identity name and suffix field stops at the first sparse item.
|
|
expect(reads).toBeLessThanOrEqual(fields + 20);
|
|
});
|
|
|
|
it("keeps a document out of the state when its value changed across a write under another root", () => {
|
|
const changed = stateOf(
|
|
captureOf([
|
|
["GET", "/api/me", { credits: 10, plan: "free" }],
|
|
["POST", "/api/orders", { id: "o1" }],
|
|
["GET", "/api/me", { credits: 9, plan: "free" }],
|
|
]),
|
|
);
|
|
expect(changed.documents).toEqual([]);
|
|
expect(changed.views).toContainEqual({
|
|
tool: "get_me",
|
|
shape: "recorded",
|
|
reason: "fields-differ",
|
|
});
|
|
// A value the write left unchanged is still one initial state.
|
|
const unchanged = stateOf(
|
|
captureOf([
|
|
["GET", "/api/me", { credits: 10, plan: "free" }],
|
|
["POST", "/api/orders", { id: "o1" }],
|
|
["GET", "/api/me", { credits: 10, plan: "free" }],
|
|
]),
|
|
);
|
|
expect(unchanged.documents).toMatchObject([
|
|
{ tool: "get_me", value: { credits: 10, plan: "free" }, record_ordinal: 2 },
|
|
]);
|
|
});
|
|
|
|
it("refuses a view clock spelling wider than nine fraction digits", () => {
|
|
const item = stateOf().views.find((view) => view.shape === "item")!;
|
|
const spelled = (fraction: number) => [
|
|
{
|
|
...item,
|
|
clock_formats: {
|
|
created: { kind: "iso", separator: "T", fraction, zone: "Z" },
|
|
},
|
|
},
|
|
];
|
|
expect(environmentStateFields.views.safeParse(spelled(9)).success).toBe(true);
|
|
expect(environmentStateFields.views.safeParse(spelled(10)).success).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it.each(["+00:60", "+24:00", "+99:99"])(
|
|
"refuses a view clock spelling with invalid zone %s",
|
|
(zone) => {
|
|
const item = stateOf().views.find((view) => view.shape === "item")!;
|
|
const view = {
|
|
...item,
|
|
clock_formats: {
|
|
created: { kind: "iso", separator: "T", fraction: 0, zone },
|
|
},
|
|
};
|
|
expect(environmentStateFields.views.safeParse([view]).success).toBe(false);
|
|
},
|
|
);
|