fix(environment): check recorded member and sequence values

This commit is contained in:
morluto committed 2026-10-28 04:49:11 +08:00
1 parent 310878cc06
commit af1bfc2ee8
3 files changed
+78 -6

No files matched your search

@@ -80,4 +80,60 @@ describe("writes whose calls send different members", () => {
expect(plan).toMatchObject({ served: true });
expect(plan.body?.defaults).toEqual([{ member: "owner", value: null }]);
});
it("recognizes a sent array when learning a later omitted-member default", () => {
const a = { id: 1, name: "A", tags: ["x"] };
const b = { id: 2, name: "B", tags: ["y"] };
const changedA = { ...a, name: "AA" };
const changedB = { ...b, name: "BB", tags: [] };
const rows: readonly TimedCaptureRow[] = [
["GET", "/api/tags", [a, b]],
["GET", "/api/tags/1", a],
["GET", "/api/tags/2", b],
["PUT", "/api/tags/1", changedA, 200, { name: "AA", tags: ["x"] }],
["GET", "/api/tags/1", changedA],
["PUT", "/api/tags/2", changedB, 200, { name: "BB" }],
["GET", "/api/tags/2", changedB],
["GET", "/api/tags", [changedA, changedB]],
];
expect(planOf(rows, "put_tags_by_id")).toMatchObject({ served: true });
});
it("withholds a sent sequence value contradicted by its recorded answer", () => {
const rows: readonly TimedCaptureRow[] = [
...NUMBERED_UNLESS_SENT_ROWS.map((row) =>
row[0] === "GET" && row[1] === "/api/bundles" && Array.isArray(row[2])
? ([
row[0],
row[1],
(row[2] as { id: number; name: string }[]).map(
({ id, name }) => ({ id, name }),
),
] as TimedCaptureRow)
: row,
),
[
"POST",
"/api/bundles",
{ id: 5, name: "Echo", order: 10 },
201,
{ name: "Echo", order: 9 },
],
[
"GET",
"/api/bundles",
[
{ id: 1, name: "Alpha" },
{ id: 2, name: "Bravo" },
{ id: 3, name: "Charlie" },
{ id: 4, name: "Delta" },
{ id: 5, name: "Echo" },
],
],
];
expect(planOf(rows, "post_bundles")).toMatchObject({
served: false,
reason: "unexplained-change",
});
});
});
@@ -105,6 +105,7 @@ const sameObserved = (left: ObservedValue, right: ObservedValue): boolean =>
export interface UpdateCall {
readonly shown: ReadonlyMap<string, ObservedValue>;
readonly sent: ReadonlyMap<string, JsonValue>;
readonly sentMembers: ReadonlySet<string>;
readonly kept: readonly string[];
}
@@ -337,7 +338,7 @@ export const defaultsDisagree = (
({ member, value }) =>
calls.earlier.some(
(call) =>
!call.sent.has(member) &&
!call.sentMembers.has(member) &&
call.shown.has(member) &&
!same(call.shown.get(member), observedValue(value)),
) ||
@@ -376,8 +377,22 @@ export const rememberCall = (
]),
);
for (const field of unstamped) shown.delete(field);
const facts = { shown, sent: scalars(call.sent), kept };
retain(64 + jsonTextCodeUnits([...facts.shown, ...facts.sent, kept]) * 2);
const facts = {
shown,
sent: scalars(call.sent),
sentMembers: new Set(Object.keys(call.sent)),
kept,
};
retain(
64 +
jsonTextCodeUnits([
...facts.shown,
...facts.sent,
[...facts.sentMembers],
kept,
]) *
2,
);
draft.updates.push(facts);
for (const field of steadyFields(draft.plan.rules, call.before, call.after))
draft.steady.add(field);
@@ -509,13 +509,14 @@ const followsSequences = (
draft: WriteDraft,
created: Pick<StateEntity, "type" | "collection" | "fields"> & {
readonly sent: JsonObject;
readonly observed: JsonObject;
},
): boolean =>
sequencesOf(draft, created.collection).every(({ rule, key }) => {
if (rule.sent === true && Object.hasOwn(created.sent, rule.field))
return same(
ownMember(created.fields, rule.field),
created.sent[rule.field],
return (
!Object.hasOwn(created.observed, rule.field) ||
same(ownMember(created.observed, rule.field), created.sent[rule.field])
);
const scope = [
...context.state.ofType(created.type),