chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
1 parent
719370c8f7
commit
6081e3e325
1743 files changed
+205459
-131704
No files matched your search
@@ -0,0 +1,476 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { createModels } from "@earendil-works/pi-ai";
|
||||
import {
|
||||
AgentDoc,
|
||||
type Conversation,
|
||||
type ConversationId,
|
||||
configure,
|
||||
createRegistry,
|
||||
createSession,
|
||||
defineDoc,
|
||||
defineEntry,
|
||||
defineExtension,
|
||||
defineTask,
|
||||
type EntryRecord,
|
||||
Harness,
|
||||
LiveDoc,
|
||||
MemoryStorage,
|
||||
ProviderDoc,
|
||||
ROOT_CONVERSATION_ID,
|
||||
type ScanOrder,
|
||||
} from "@earendil-works/pi-durable";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { openNodeSqliteStorage } from "../src/storage/sqlite/node.ts";
|
||||
import { addTool, openHarness, tool, user } from "./harness-support.ts";
|
||||
import { ControlledStorage, context } from "./session-support.ts";
|
||||
|
||||
const directories = new Set<string>();
|
||||
const UUID_V7 = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
||||
|
||||
async function sqlitePath(): Promise<string> {
|
||||
const directory = await mkdtemp(join(tmpdir(), "pi-durable-harness-"));
|
||||
directories.add(directory);
|
||||
return join(directory, "session.sqlite");
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
for (const directory of directories) await rm(directory, { recursive: true, force: true });
|
||||
directories.clear();
|
||||
});
|
||||
|
||||
const NoteDoc = defineDoc<{ text: string }>({
|
||||
kind: "test.note",
|
||||
version: 1,
|
||||
scope: "conversation",
|
||||
history: "rewindable",
|
||||
fork: "asOf",
|
||||
initial: () => ({ text: "" }),
|
||||
});
|
||||
|
||||
const Message = defineEntry("message");
|
||||
|
||||
async function append(conversation: Conversation, text: string): Promise<EntryRecord> {
|
||||
return conversation.commit(
|
||||
(tx) => tx.appendEntry(conversation.id, { kind: "message", model: [user(text)] }),
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
async function allEntries(conversation: Conversation, order?: ScanOrder): Promise<string[]> {
|
||||
const texts: string[] = [];
|
||||
let cursor: Parameters<Conversation["entries"]>[2];
|
||||
do {
|
||||
const page = await conversation.entries(order === undefined ? {} : { order }, 2, cursor, context);
|
||||
for (const entry of page.items) texts.push((entry.model?.[0] as { content: string }).content);
|
||||
cursor = page.next;
|
||||
} while (cursor !== undefined);
|
||||
return texts;
|
||||
}
|
||||
|
||||
describe("Harness root and conversations", () => {
|
||||
it("creates the root lazily with its agent change and init in one commit", async () => {
|
||||
const storage = new ControlledStorage();
|
||||
const { harness } = await openHarness(storage, ["read", "bash"]);
|
||||
expect(storage.commits).toHaveLength(0);
|
||||
const root = await harness.root(context, {
|
||||
agent: { thinkingLevel: "high" },
|
||||
init: async (tx, id) => {
|
||||
// The agent change applied before init.
|
||||
expect((await tx.doc(AgentDoc, id)).thinkingLevel).toBe("high");
|
||||
(await tx.doc(NoteDoc, id)).text = "root note";
|
||||
},
|
||||
});
|
||||
expect(root.id).toBe(ROOT_CONVERSATION_ID);
|
||||
expect(storage.commits).toHaveLength(1);
|
||||
// Conversation, five built-in documents, and the init note.
|
||||
expect(storage.commits[0]!.map((write) => write.type)).toEqual([
|
||||
"conversation",
|
||||
"document.create",
|
||||
"document.create",
|
||||
"document.create",
|
||||
"document.create",
|
||||
"document.create",
|
||||
"document.create",
|
||||
]);
|
||||
expect(await harness.snapshot(LiveDoc, root.id, context)).toEqual({});
|
||||
expect(await harness.snapshot(ProviderDoc, root.id, context)).toEqual({
|
||||
sessionId: expect.stringMatching(UUID_V7),
|
||||
});
|
||||
expect(await harness.snapshot(AgentDoc, root.id, context)).toEqual({ thinkingLevel: "high" });
|
||||
const agent = await root.agent(context);
|
||||
expect(agent.thinkingLevel).toBe("high");
|
||||
expect(agent.tools.map((each) => each.name)).toEqual(["read", "bash"]);
|
||||
expect(await harness.snapshot(NoteDoc, root.id, context)).toEqual({ text: "root note" });
|
||||
|
||||
const again = await harness.root(context, { agent: { thinkingLevel: "low" }, init: () => expect.unreachable() });
|
||||
expect(again.id).toBe(root.id);
|
||||
expect(storage.commits).toHaveLength(1);
|
||||
await harness.close(context);
|
||||
});
|
||||
|
||||
it("keeps root and conversation identity and state across reopen", async () => {
|
||||
const path = await sqlitePath();
|
||||
let first = await openHarness(await openNodeSqliteStorage(path), ["read"]);
|
||||
const root = await first.harness.root(context);
|
||||
await root.configure({ model: { provider: "anthropic", modelId: "claude" }, cwd: "/repo" }, context);
|
||||
const entry = await append(root, "hello");
|
||||
const child = await first.harness.createConversation({ ownership: { kind: "ownerless" } }, context);
|
||||
const fork = await root.fork(entry.id, { ownership: { kind: "ownerless" } }, context);
|
||||
const providerSessionIds = await Promise.all(
|
||||
[root, child, fork].map(
|
||||
async (conversation) => (await first.harness.snapshot(ProviderDoc, conversation.id, context))?.sessionId,
|
||||
),
|
||||
);
|
||||
// Regression coverage for #10424: a fork must not inherit its parent's provider identity.
|
||||
for (const sessionId of providerSessionIds) expect(sessionId).toMatch(UUID_V7);
|
||||
expect(new Set(providerSessionIds).size).toBe(3);
|
||||
await first.harness.close(context);
|
||||
await expect(root.agent(context)).rejects.toThrow();
|
||||
|
||||
// The new process installs nothing: the stored choices survive, the tools do not resolve.
|
||||
first = await openHarness(await openNodeSqliteStorage(path), []);
|
||||
const reopened = await first.harness.root(context, { init: () => expect.unreachable() });
|
||||
expect(reopened.id).toBe(ROOT_CONVERSATION_ID);
|
||||
expect(await reopened.agent(context)).toMatchObject({
|
||||
model: { provider: "anthropic", modelId: "claude" },
|
||||
cwd: "/repo",
|
||||
tools: [],
|
||||
});
|
||||
expect(await allEntries(reopened)).toEqual(["hello"]);
|
||||
expect((await first.harness.conversation(child.id, context))?.id).toBe(child.id);
|
||||
await expect(
|
||||
Promise.all(
|
||||
[root.id, child.id, fork.id].map(
|
||||
async (id) => (await first.harness.snapshot(ProviderDoc, id, context))?.sessionId,
|
||||
),
|
||||
),
|
||||
).resolves.toEqual(providerSessionIds);
|
||||
const reopenedFork = await first.harness.conversation(fork.id, context);
|
||||
expect(await allEntries(reopenedFork!)).toEqual(["hello"]);
|
||||
expect((await reopenedFork!.agent(context)).model).toEqual({ provider: "anthropic", modelId: "claude" });
|
||||
expect(await first.harness.conversation(999 as ConversationId, context)).toBeUndefined();
|
||||
await first.harness.close(context);
|
||||
});
|
||||
|
||||
it("creates independent conversations atomically with init and rolls back failures", async () => {
|
||||
const storage = new ControlledStorage();
|
||||
const { harness, registry } = await openHarness(storage, ["read"]);
|
||||
const created = await harness.createConversation(
|
||||
{
|
||||
ownership: { kind: "ownerless" },
|
||||
init: async (tx, id) => {
|
||||
await tx.appendEntry(id, { kind: "message", model: [user("seed")] });
|
||||
},
|
||||
},
|
||||
context,
|
||||
);
|
||||
expect(storage.commits).toHaveLength(1);
|
||||
expect(await allEntries(created)).toEqual(["seed"]);
|
||||
|
||||
const before = storage.commits.length;
|
||||
await expect(
|
||||
harness.createConversation(
|
||||
{
|
||||
ownership: { kind: "ownerless" },
|
||||
agent: { thinkingLevel: "high" },
|
||||
init: () => {
|
||||
throw new Error("init failed");
|
||||
},
|
||||
},
|
||||
context,
|
||||
),
|
||||
).rejects.toThrow("init failed");
|
||||
expect(storage.commits).toHaveLength(before);
|
||||
|
||||
// A conversation on the default selection follows installs live.
|
||||
addTool(registry, tool("bash"));
|
||||
const names = async (conversation: Conversation) =>
|
||||
(await conversation.agent(context)).tools.map((each) => each.name);
|
||||
expect(await names(created)).toEqual(["read", "bash"]);
|
||||
await harness.close(context);
|
||||
});
|
||||
|
||||
it("forks at a concrete entry with the as-of agent and applies agent and init overrides", async () => {
|
||||
const { harness, registry } = await openHarness(new MemoryStorage(), ["read"]);
|
||||
const legacy = tool("legacy");
|
||||
addTool(registry, legacy);
|
||||
const root = await harness.root(context);
|
||||
await root.configure({ thinkingLevel: "low" }, context);
|
||||
const at = await append(root, "one");
|
||||
await root.configure({ thinkingLevel: "high", tools: [tool("read")] }, context);
|
||||
await append(root, "two");
|
||||
|
||||
const child = await root.fork(at.id, { ownership: { kind: "ownerless" } }, context);
|
||||
expect(await harness.snapshot(AgentDoc, child.id, context)).toEqual({ thinkingLevel: "low" });
|
||||
expect(await allEntries(child)).toEqual(["one"]);
|
||||
|
||||
const overridden = await root.fork(
|
||||
at.id,
|
||||
{
|
||||
ownership: { kind: "ownerless" },
|
||||
agent: { thinkingLevel: "minimal", tools: [legacy] },
|
||||
init: async (tx, id) => {
|
||||
expect((await tx.doc(AgentDoc, id)).thinkingLevel).toBe("minimal");
|
||||
},
|
||||
},
|
||||
context,
|
||||
);
|
||||
expect(await harness.snapshot(AgentDoc, overridden.id, context)).toEqual({
|
||||
thinkingLevel: "minimal",
|
||||
tools: ["legacy"],
|
||||
});
|
||||
expect(await harness.snapshot(AgentDoc, root.id, context)).toEqual({ thinkingLevel: "high", tools: ["read"] });
|
||||
|
||||
const unrelated = await harness.createConversation({ ownership: { kind: "ownerless" } }, context);
|
||||
await expect(root.fork(at.id, { ownership: { kind: "ownerless" } }, context)).resolves.toBeDefined();
|
||||
await expect(unrelated.fork(at.id, { ownership: { kind: "ownerless" } }, context)).rejects.toThrow(
|
||||
"is not visible",
|
||||
);
|
||||
await harness.close(context);
|
||||
});
|
||||
|
||||
it("paginates fork-aware history through deep ancestor caps and same-commit prefixes", async () => {
|
||||
const { harness } = await openHarness(new MemoryStorage());
|
||||
const root = await harness.root(context);
|
||||
await append(root, "r1");
|
||||
const [r2] = await root.commit(
|
||||
async (tx) => [
|
||||
await tx.appendEntry(root.id, { kind: "message", model: [user("r2")] }),
|
||||
await tx.appendEntry(root.id, { kind: "message", model: [user("r3")] }),
|
||||
],
|
||||
context,
|
||||
);
|
||||
const child = await root.fork(r2!.id, { ownership: { kind: "ownerless" } }, context);
|
||||
const c1 = await append(child, "c1");
|
||||
await append(child, "c2");
|
||||
const grandchild = await child.fork(c1.id, { ownership: { kind: "ownerless" } }, context);
|
||||
await append(grandchild, "g1");
|
||||
|
||||
expect(await allEntries(root)).toEqual(["r3", "r2", "r1"]);
|
||||
expect(await allEntries(child)).toEqual(["c2", "c1", "r2", "r1"]);
|
||||
expect(await allEntries(grandchild)).toEqual(["g1", "c1", "r2", "r1"]);
|
||||
// #10546
|
||||
expect(await allEntries(grandchild, "ascending")).toEqual(["r1", "r2", "c1", "g1"]);
|
||||
const bounded = await grandchild.entries(
|
||||
{ minEntryId: r2!.id, maxEntryId: c1.id, conversationId: root.id } as never,
|
||||
10,
|
||||
undefined,
|
||||
context,
|
||||
);
|
||||
expect(bounded.items.map((entry) => entry.id)).toEqual([c1.id, r2!.id]);
|
||||
expect(Message.is(bounded.items[0])).toBe(true);
|
||||
expect(Message.is(undefined)).toBe(false);
|
||||
await harness.close(context);
|
||||
});
|
||||
|
||||
it("binds commits and task creation to the conversation", async () => {
|
||||
const { harness } = await openHarness(new MemoryStorage());
|
||||
const conversation = await harness.createConversation({ ownership: { kind: "ownerless" } }, context);
|
||||
const task = defineTask<{ n: number }, { phase: "run" }, null>({
|
||||
name: "test.work",
|
||||
version: 1,
|
||||
initial: () => ({ phase: "run" }),
|
||||
phases: { run: async () => {} },
|
||||
abort: async () => {},
|
||||
});
|
||||
const taskId = await conversation.commit(
|
||||
(tx) => tx.createTask(task, { n: 1 }, { ownership: { kind: "conversation" } }),
|
||||
context,
|
||||
);
|
||||
const record = await harness.commit((tx) => tx.task(taskId), context);
|
||||
expect(record?.conversationId).toBe(conversation.id);
|
||||
await expect(
|
||||
harness.commit((tx) => tx.createTask(task, { n: 2 }, { ownership: { kind: "conversation" } }), context),
|
||||
).rejects.toThrow("requires options.conversationId");
|
||||
await harness.close(context);
|
||||
});
|
||||
|
||||
it("runs conversationCreated in every creating commit, after the built-ins and before agent and init", async () => {
|
||||
const seen: string[] = [];
|
||||
const harness = await Harness.open(
|
||||
new MemoryStorage(),
|
||||
{
|
||||
models: createModels(),
|
||||
registry: createRegistry(),
|
||||
conversationCreated: async (tx, conversation) => {
|
||||
const agent = await tx.doc(AgentDoc, conversation.id);
|
||||
seen.push(`${conversation.id}:${conversation.parent === undefined ? "new" : "fork"}:${agent.cwd}`);
|
||||
(await tx.doc(NoteDoc, conversation.id)).text ||= "created";
|
||||
if (agent.cwd === "/fail") throw new Error("no");
|
||||
},
|
||||
},
|
||||
context,
|
||||
);
|
||||
const root = await harness.root(context, {
|
||||
agent: { cwd: "/root" },
|
||||
init: async (tx, id) => void seen.push(`init:${(await tx.doc(NoteDoc, id)).text}`),
|
||||
});
|
||||
// A raw creation in a commit, as in a tool, and a fork, which already has the asOf copies.
|
||||
const raw = await harness.commit(
|
||||
async (tx) => (await tx.createConversation({ ownership: { kind: "ownerless" } })).id,
|
||||
context,
|
||||
);
|
||||
const entry = await append(root, "hello");
|
||||
const fork = await root.fork(entry.id, { ownership: { kind: "ownerless" } }, context);
|
||||
expect(seen).toEqual([
|
||||
`${root.id}:new:undefined`,
|
||||
"init:created",
|
||||
`${raw}:new:undefined`,
|
||||
`${fork.id}:fork:/root`,
|
||||
]);
|
||||
expect(await harness.snapshot(NoteDoc, raw, context)).toEqual({ text: "created" });
|
||||
// A throw fails the creating commit.
|
||||
await root.configure({ cwd: "/fail" }, context);
|
||||
await expect(root.fork(entry.id, { ownership: { kind: "ownerless" } }, context)).resolves.toBeDefined();
|
||||
const failing = await append(root, "after");
|
||||
await expect(root.fork(failing.id, { ownership: { kind: "ownerless" } }, context)).rejects.toThrow("no");
|
||||
await harness.close(context);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Harness agent", () => {
|
||||
it("replaces whole fields, clears them with null, and leaves undefined fields alone", async () => {
|
||||
const { harness, registry } = await openHarness(new MemoryStorage());
|
||||
const read = tool("read");
|
||||
const bash = tool("bash");
|
||||
const edit = tool("edit");
|
||||
registry.install(defineExtension({ name: "coding", tools: [read, bash, edit] }));
|
||||
const root = await harness.root(context);
|
||||
const stored = () => harness.snapshot(AgentDoc, root.id, context);
|
||||
const offered = async () => (await root.agent(context)).tools.map((each) => each.name);
|
||||
expect(await root.agent(context)).toMatchObject({ thinkingLevel: "off", tools: [read, bash, edit] });
|
||||
expect((await root.agent(context)).model).toBeUndefined();
|
||||
|
||||
await root.configure({ model: { provider: "openai", modelId: "gpt" }, thinkingLevel: "medium" }, context);
|
||||
await root.configure({ model: undefined, instructions: "Be terse." }, context);
|
||||
expect(await stored()).toEqual({
|
||||
model: { provider: "openai", modelId: "gpt" },
|
||||
thinkingLevel: "medium",
|
||||
instructions: "Be terse.",
|
||||
});
|
||||
await root.configure({ model: null, instructions: null }, context);
|
||||
expect(await stored()).toEqual({ thinkingLevel: "medium" });
|
||||
|
||||
await root.configure({ tools: { remove: [edit] } }, context);
|
||||
expect(await offered()).toEqual(["read", "bash"]);
|
||||
// A new filter replaces the old one: edit is offered again.
|
||||
await root.configure({ tools: { remove: [bash] } }, context);
|
||||
expect(await offered()).toEqual(["read", "edit"]);
|
||||
await root.configure({ tools: [edit, read] }, context);
|
||||
expect(await offered()).toEqual(["edit", "read"]);
|
||||
await root.configure({ tools: null }, context);
|
||||
expect(await offered()).toEqual(["read", "bash", "edit"]);
|
||||
// Names are stored without checking the registry.
|
||||
await root.configure({ tools: [tool("missing"), read] }, context);
|
||||
expect(await stored()).toMatchObject({ tools: ["missing", "read"] });
|
||||
expect(await offered()).toEqual(["read"]);
|
||||
await harness.close(context);
|
||||
});
|
||||
|
||||
it("gives conversations created through Tx their documents: empty, an owner copy, or the fork's as-of copy", async () => {
|
||||
const { harness } = await openHarness(new MemoryStorage(), ["read"]);
|
||||
const root = await harness.root(context, {
|
||||
agent: { model: { provider: "faux", modelId: "m" }, instructions: "Main role.", cwd: "/repo" },
|
||||
});
|
||||
const owner = defineTask<Record<string, never>, { phase: "never" }, null>({
|
||||
name: "test.owner",
|
||||
version: 1,
|
||||
initial: () => ({ phase: "never" }),
|
||||
phases: { never: async () => {} },
|
||||
abort: async () => {},
|
||||
});
|
||||
const ids = await root.commit(async (tx) => {
|
||||
const taskId = await tx.createTask(owner, {}, { ownership: { kind: "conversation" } });
|
||||
const plain = await tx.createConversation({ ownership: { kind: "ownerless" } });
|
||||
const owned = await tx.createConversation({ ownership: { kind: "task", taskId } });
|
||||
// The copy exists when createConversation() returns, so a configure() in the same callback overrides it.
|
||||
expect((await tx.doc(AgentDoc, owned.id)).cwd).toBe("/repo");
|
||||
await configure(tx, owned.id, { cwd: "/worktree" });
|
||||
return { taskId, plain: plain.id, owned: owned.id };
|
||||
}, context);
|
||||
expect(await harness.snapshot(AgentDoc, ids.plain, context)).toEqual({});
|
||||
expect(await harness.snapshot(LiveDoc, ids.plain, context)).toEqual({});
|
||||
expect(await harness.snapshot(ProviderDoc, ids.plain, context)).toEqual({
|
||||
sessionId: expect.stringMatching(UUID_V7),
|
||||
});
|
||||
expect((await harness.snapshot(ProviderDoc, ids.owned, context))?.sessionId).not.toBe(
|
||||
(await harness.snapshot(ProviderDoc, root.id, context))?.sessionId,
|
||||
);
|
||||
expect(await harness.snapshot(AgentDoc, ids.owned, context)).toEqual({
|
||||
model: { provider: "faux", modelId: "m" },
|
||||
instructions: "Main role.",
|
||||
cwd: "/worktree",
|
||||
});
|
||||
|
||||
// A later owner change does not reach the child.
|
||||
await root.configure({ thinkingLevel: "high" }, context);
|
||||
expect((await harness.snapshot(AgentDoc, ids.owned, context))?.thinkingLevel).toBeUndefined();
|
||||
|
||||
// A task-owned fork keeps its as-of copy of its fork parent, not its owner's agent.
|
||||
const at = await root.commit(async (tx) => (await tx.appendEntry(ids.plain, { kind: "note" })).id, context);
|
||||
const fork = await root.commit(
|
||||
async (tx) =>
|
||||
(await tx.forkConversation(ids.plain, at, { ownership: { kind: "task", taskId: ids.taskId } })).id,
|
||||
context,
|
||||
);
|
||||
expect(await harness.snapshot(AgentDoc, fork, context)).toEqual({});
|
||||
expect(await harness.snapshot(LiveDoc, fork, context)).toEqual({});
|
||||
expect((await harness.snapshot(ProviderDoc, fork, context))?.sessionId).not.toBe(
|
||||
(await harness.snapshot(ProviderDoc, ids.plain, context))?.sessionId,
|
||||
);
|
||||
await harness.close(context);
|
||||
});
|
||||
|
||||
it("reads an absent agent for conversations a plain Session created without writing", async () => {
|
||||
const storage = new ControlledStorage();
|
||||
const id = await createSession(storage).commit(
|
||||
async (tx) => (await tx.createConversation({ ownership: { kind: "ownerless" } })).id,
|
||||
context,
|
||||
);
|
||||
const { harness } = await openHarness(storage, ["read"]);
|
||||
const raw = await harness.conversation(id, context);
|
||||
expect(await harness.snapshot(LiveDoc, id, context)).toBeUndefined();
|
||||
const commits = storage.commits.length;
|
||||
expect(await raw!.agent(context)).toMatchObject({ thinkingLevel: "off", tools: [{ name: "read" }] });
|
||||
expect(storage.commits).toHaveLength(commits);
|
||||
await raw!.configure({ thinkingLevel: "low" }, context);
|
||||
expect(await harness.snapshot(AgentDoc, id, context)).toEqual({ thinkingLevel: "low" });
|
||||
await harness.close(context);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Harness lifecycle", () => {
|
||||
it("returns stateless handles and rejects operations after close", async () => {
|
||||
const { harness } = await openHarness(new MemoryStorage());
|
||||
const root = await harness.root(context);
|
||||
const again = await harness.root(context);
|
||||
expect(again).not.toBe(root);
|
||||
expect(again.id).toBe(root.id);
|
||||
await harness.close(context);
|
||||
await expect(harness.root(context)).rejects.toThrow("is closed");
|
||||
await expect(harness.createConversation({ ownership: { kind: "ownerless" } }, context)).rejects.toThrow(
|
||||
"is closed",
|
||||
);
|
||||
await expect(harness.conversation(root.id, context)).rejects.toThrow("is closed");
|
||||
});
|
||||
|
||||
it("forwards generic Session document APIs", async () => {
|
||||
const { harness } = await openHarness(new MemoryStorage());
|
||||
const root = await harness.root(context);
|
||||
const entry = await root.commit(async (tx) => {
|
||||
(await tx.doc(NoteDoc, root.id)).text = "first";
|
||||
return tx.appendEntry(root.id, { kind: "message", model: [user("m")] });
|
||||
}, context);
|
||||
await harness.commit(async (tx) => {
|
||||
(await tx.doc(NoteDoc, root.id)).text = "second";
|
||||
}, context);
|
||||
expect(await harness.snapshot(NoteDoc, root.id, context)).toEqual({ text: "second" });
|
||||
expect(await harness.snapshotAsOf(NoteDoc, root.id, entry.id, context)).toEqual({ text: "first" });
|
||||
const state = await harness.documentState(NoteDoc, root.id, context);
|
||||
expect(state?.value).toEqual({ text: "second" });
|
||||
state?.dispose();
|
||||
await harness.close(context);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user