Files
wehub-resource-sync e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

239 lines
8.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { GorpClient } from "../src/GorpClient";
type State = {
count: number;
items: Record<string, { name: string }>;
};
type Command =
| { type: "addItem"; id: string; name: string }
| { type: "incCount" };
const initial = (): State => ({ count: 0, items: {} });
function makeClient() {
const replays: Array<{ command: Command; seq: number }> = [];
const mutator = (state: State, command: Command, seq: number) => {
replays.push({ command, seq });
if (command.type === "addItem") {
state.items[command.id] = { name: command.name };
} else {
state.count++;
}
};
const client = new GorpClient<State, Command>({
initialState: initial(),
mutator,
send: () => {},
});
return { client, replays };
}
describe("GorpClient — send / apply basics", () => {
it("send writes appear in getChangedKeys for the same frame", () => {
const { client } = makeClient();
client.send({ type: "addItem", id: "a", name: "alpha" });
expect(client.getChangedKeys(["items"])).toEqual(["a"]);
expect(client.state.items.a).toEqual({ name: "alpha" });
expect(client.pending).toHaveLength(1);
expect(client.firstPendingSeq).toBe(0);
});
it("apply with no ack advances committed without touching pending", () => {
const { client } = makeClient();
client.send({ type: "addItem", id: "a", name: "alpha" });
client.apply({
ops: [{ type: "set", path: ["count"], value: 7 }],
});
// Committed-side count surfaces; pending mutation still applied on top.
expect(client.state.count).toBe(7);
expect(client.state.items.a).toEqual({ name: "alpha" });
expect(client.pending).toHaveLength(1);
});
it("apply with ack covering all pending leaves optimistic == committed", () => {
const { client } = makeClient();
client.send({ type: "addItem", id: "opt-0", name: "alpha" });
client.apply({
ops: [{ type: "set", path: ["items", "real"], value: { name: "alpha" } }],
ack: 0,
});
expect(client.pending).toHaveLength(0);
expect(Object.keys(client.state.items)).toEqual(["real"]);
});
it("apply with ack covering some pending splices and replays the rest", () => {
const { client } = makeClient();
client.send({ type: "addItem", id: "a", name: "alpha" });
client.send({ type: "addItem", id: "b", name: "beta" });
client.apply({
ops: [
{ type: "set", path: ["items", "a"], value: { name: "alpha-server" } },
],
ack: 0,
});
expect(client.pending).toHaveLength(1);
expect(client.firstPendingSeq).toBe(1);
// a is now the server version; b stays optimistic.
expect(client.state.items).toEqual({
a: { name: "alpha-server" },
b: { name: "beta" },
});
});
it("idempotent re-ack: apply with ack < firstPendingSeq is a no-op splice", () => {
const { client } = makeClient();
client.send({ type: "addItem", id: "a", name: "alpha" });
client.send({ type: "addItem", id: "b", name: "beta" });
client.apply({ ops: [], ack: 0 });
// Server resends ack=0 for some reason after we already spliced cmd1.
client.apply({ ops: [], ack: 0 });
expect(client.pending).toHaveLength(1);
expect(client.firstPendingSeq).toBe(1);
});
});
describe("GorpClient — replay determinism", () => {
it("mutator receives the same seq across replays of the same command", () => {
const { client, replays } = makeClient();
client.send({ type: "addItem", id: "a", name: "alpha" });
// No-ack apply forces a rebuild → cmd1 is replayed.
client.apply({
ops: [{ type: "set", path: ["count"], value: 1 }],
});
client.send({ type: "addItem", id: "b", name: "beta" });
// Another no-ack apply → both are replayed.
client.apply({
ops: [{ type: "set", path: ["count"], value: 2 }],
});
const seqsForA = replays
.filter((r) => r.command.type === "addItem" && r.command.id === "a")
.map((r) => r.seq);
const seqsForB = replays
.filter((r) => r.command.type === "addItem" && r.command.id === "b")
.map((r) => r.seq);
expect(new Set(seqsForA)).toEqual(new Set([0]));
expect(new Set(seqsForB)).toEqual(new Set([1]));
expect(seqsForA.length).toBeGreaterThan(1); // confirm it was actually replayed
expect(seqsForB.length).toBeGreaterThan(1);
});
it("mutator producing different writes after committed advances surfaces both old and new paths in changes", () => {
type SmartState = { latestId: string | null; tags: Record<string, true> };
type SmartCommand = { type: "tag" };
const initialSmart = (): SmartState => ({
latestId: null,
tags: {},
});
// Mutator reads latestId from committed state and tags whichever id is
// current. Replay against a different latestId produces a different tag.
const mutator = (state: SmartState, _: SmartCommand) => {
if (state.latestId) state.tags[state.latestId] = true;
};
const client = new GorpClient<SmartState, SmartCommand>({
initialState: initialSmart(),
mutator,
send: () => {},
});
// Server says latestId is "first".
client.apply({
ops: [{ type: "set", path: ["latestId"], value: "first" }],
});
client.send({ type: "tag" });
expect(client.state.tags).toEqual({ first: true });
// Server advances latestId; pending is unchanged but its replay now
// produces a different write.
client.apply({
ops: [{ type: "set", path: ["latestId"], value: "second" }],
});
expect(client.state.tags).toEqual({ second: true });
// Both "first" (reverted) and "second" (newly set) should be visible
// as changes the consumer needs to re-read.
expect(new Set(client.getChangedKeys(["tags"]))).toEqual(
new Set(["first", "second"]),
);
});
});
describe("GorpClient — change-tracking edge cases", () => {
it("a pending mutation reverted by an ack appears as changed in the rebuild frame", () => {
const { client } = makeClient();
client.send({ type: "addItem", id: "opt-0", name: "alpha" });
// Server acks but commits a *different* id (rejected/remapped).
client.apply({
ops: [{ type: "set", path: ["items", "real"], value: { name: "alpha" } }],
ack: 0,
});
expect(new Set(client.getChangedKeys(["items"]))).toEqual(
new Set(["opt-0", "real"]),
);
// Consumer reading items["opt-0"] sees undefined → can drop the
// optimistic entry.
expect(client.state.items["opt-0"]).toBeUndefined();
expect(client.state.items.real).toEqual({ name: "alpha" });
});
it("apply with set [] (full snapshot) marks all changed keys via diffKeys", () => {
const { client } = makeClient();
client.apply({
ops: [{ type: "set", path: ["items", "a"], value: { name: "alpha" } }],
});
client.apply({
ops: [{ type: "set", path: ["items", "b"], value: { name: "beta" } }],
});
// Now full-snapshot replace: drops a, keeps b, adds c.
const snapshot: State = {
count: 99,
items: {
b: { name: "beta-new" },
c: { name: "gamma" },
},
};
client.apply({
ops: [{ type: "set", path: [], value: snapshot }],
});
// diffKeys returns new keys + deleted keys (in reverse).
expect(client.getChangedKeys(["items"])).toEqual(["b", "c", "a"]);
});
it("two send calls back-to-back open separate frames", () => {
const { client } = makeClient();
client.send({ type: "addItem", id: "a", name: "alpha" });
client.send({ type: "addItem", id: "b", name: "beta" });
// Second send's frame only sees b's writes, not a's.
expect(client.getChangedKeys(["items"])).toEqual(["b"]);
});
it("isChangedAt returns truthy for any path inside a marked subtree", () => {
const { client } = makeClient();
client.apply({
ops: [{ type: "set", path: [], value: initial() }],
});
// Root-level set marks the entire tree as changed.
expect(client.isChangedAt([])).toBe(true);
expect(client.isChangedAt(["items"])).toBe(true);
expect(client.isChangedAt(["items", "anything", "deep"])).toBe(true);
});
});
describe("GorpClient — pending queue accounting", () => {
it("firstPendingSeq advances correctly across send + ack", () => {
const { client } = makeClient();
expect(client.firstPendingSeq).toBe(0);
client.send({ type: "addItem", id: "a", name: "alpha" });
client.send({ type: "addItem", id: "b", name: "beta" });
client.send({ type: "addItem", id: "c", name: "gamma" });
expect(client.firstPendingSeq).toBe(0);
expect(client.pending).toHaveLength(3);
client.apply({ ops: [], ack: 0 });
expect(client.firstPendingSeq).toBe(1);
expect(client.pending).toHaveLength(2);
client.apply({ ops: [], ack: 2 });
expect(client.firstPendingSeq).toBe(3);
expect(client.pending).toHaveLength(0);
});
});