mirror of
https://github.com/morluto/rea.git
synced 2026-11-01 12:31:26 +00:00
The volatility check compared write start positions with the two reads, so a write already in flight when the earlier read started was missed and its effect was named volatile. A write now counts when its interval overlaps the span from the earlier read's start to the later read's end. The check also ran once per changed field and scanned every write. It now runs once per pair of observations, answered by a binary search over the writes in timeline order and the latest end among each prefix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
513 lines
15 KiB
TypeScript
513 lines
15 KiB
TypeScript
import { expect, it } from "vitest";
|
|
import {
|
|
analysisViewCaptureEvidence,
|
|
environmentManifestCaptureFixture,
|
|
environmentStateCaptureFixture,
|
|
} 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";
|
|
|
|
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;
|
|
};
|
|
|
|
type Row = readonly [
|
|
method: string,
|
|
path: string,
|
|
body?: JsonValue,
|
|
status?: number,
|
|
];
|
|
|
|
/** HAR entries one second apart on one origin; a string body is recorded as written. */
|
|
const captureOf = (rows: readonly Row[]): AnalysisViewParent =>
|
|
parentOf(
|
|
analysisViewCaptureEvidence(
|
|
environmentManifestCaptureFixture(
|
|
rows.map(([method, path, body = null, status = 200], index) => ({
|
|
startedDateTime: new Date(
|
|
Date.parse("2026-10-17T18:00:00.000Z") + index * 1000,
|
|
).toISOString(),
|
|
time: 5,
|
|
request: { method, url: `http://app.example${path}` },
|
|
response: {
|
|
status,
|
|
content: {
|
|
mimeType: "application/json",
|
|
text: typeof body === "string" ? body : JSON.stringify(body),
|
|
},
|
|
},
|
|
})),
|
|
),
|
|
),
|
|
);
|
|
|
|
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",
|
|
});
|
|
expect(state.documents).toEqual([]);
|
|
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("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: Row[] = [
|
|
[
|
|
"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("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("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", () => {
|
|
const state = stateOf(captureOf([["GET", "/api/widgets", []]]));
|
|
expect(state.types).toEqual([]);
|
|
expect(state.views).toEqual([
|
|
{ tool: "get_widgets", shape: "recorded", reason: "no-identity" },
|
|
]);
|
|
});
|
|
|
|
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" }] },
|
|
],
|
|
]),
|
|
);
|
|
expect(state.views).toContainEqual(
|
|
expect.objectContaining({ tool: "get_items", shape: "recorded" }),
|
|
);
|
|
expect(state.entities).toEqual([]);
|
|
});
|