/**
* Drives a release of a `system:` pattern through the reconciler onto a piece
* whose argument document holds fields only a named writer may change, each
* holding what that writer put there. The reconciler sets the release up with
* no argument, so the setup stages the stored document again, those fields
* among them, and the release commits only where that staging leaves them as
* they were.
*/
import { describe, it } from "@std/testing/bdd";
import { expect } from "@std/expect";
import { spy } from "@std/testing/mock";
import { Identity } from "@commonfabric/identity";
import { loadStoredCfcEnvelope } from "../src/cfc/prepare.ts";
import { cfcSchemaEntries } from "../src/cfc/schema-label-view.ts";
import { markRendererTrustedEvent } from "../src/cfc/ui-contract.ts";
import {
type Cell,
getPatternIdentityRef,
getPieceReconciliation,
type PieceReconciliation,
resolveEntryIdentity,
Runtime,
type RuntimeProgram,
systemPatternSource,
} from "../src/index.ts";
import { runtimePresets } from "../src/runtime-presets.ts";
import { StorageManager } from "../src/storage/cache.deno.ts";
const origin = systemPatternSource("system/guarded-room.tsx");
const route = "/api/patterns/system/guarded-room.tsx";
// `title` is written by `retitle` alone; `note` is guarded by nothing.
const WRITER_SOURCE = `///
import {
Default,
handler,
pattern,
Stream,
Writable,
type WriteAuthorizedBy,
} from "commonfabric";
const retitle = handler<{ text: string }, { title: Writable<{ text: string }> }>(
({ text }, { title }) => {
title.set({ text });
},
);
interface Input {
title?: Default<
WriteAuthorizedBy<{ text: string }, typeof retitle>,
{ text: "" }
>;
note?: Default;
}
interface Output {
title: { text: string };
note: string;
retitle: Stream<{ text: string }>;
}
export default pattern(({ title, note }) => ({
title,
note,
retitle: retitle({ title }),
}));
`;
// `about` is written by `describeRoom` alone, from its surface's trusted
// event, and labeled with its author; `badge` is written from its surface's
// trusted event by whatever handler receives one.
const CONTRACT_SOURCE = `///
import {
type AuthoredByCurrentUser,
Default,
handler,
pattern,
Stream,
type TrustedActionUiContract,
type TrustedActionWrite,
Writable,
} from "commonfabric";
const describeRoom = handler<
{ title: string },
{ about: Writable<{ title: string } | undefined> }
>(({ title }, { about }) => {
about.set({ title });
});
const pin = handler<{ text: string }, { badge: Writable<{ text: string }> }>(
({ text }, { badge }) => {
badge.set({ text });
},
);
interface Input {
about?: AuthoredByCurrentUser<
TrustedActionWrite<
{ title: string },
typeof describeRoom,
"Describe",
"GuardedRoom"
>
>;
badge?: Default<
TrustedActionUiContract<{ text: string }, "Pin", "GuardedRoom">,
{ text: "" }
>;
}
interface Output {
about?: { title: string };
badge: { text: string };
describeRoom: Stream<{ title: string }>;
pin: Stream<{ text: string }>;
}
export default pattern(({ about, badge }) => ({
about,
badge,
describeRoom: describeRoom({ about }),
pin: pin({ badge }),
}));
`;
// The shapes a FabriChat room's inputs take: a record whose type is a named
// definition, written by `describeRoom` alone from its surface's trusted
// event, and a list under a named record, grown by `join` alone.
const NAMED_SOURCE = `///
import {
type AuthoredByCurrentUser,
Default,
handler,
pattern,
Stream,
type TrustedActionWrite,
Writable,
type WriteAuthorizedBy,
} from "commonfabric";
interface About {
title: string;
}
const describeRoom = handler<
{ title: string },
{ about: Writable }
>(({ title }, { about }) => {
about.set({ title });
});
type StoredAbout = AuthoredByCurrentUser<
TrustedActionWrite
>;
const join = handler<{ name: string }, { roster: RosterCell }>(
({ name }, { roster }) => {
roster.key("items").push(name);
},
);
interface Roster {
items?: WriteAuthorizedBy;
}
type RosterCell = Writable>>;
interface Input {
about?: StoredAbout;
roster?: RosterCell;
}
interface Output {
about?: About;
roster: Roster;
describeRoom: Stream<{ title: string }>;
join: Stream<{ name: string }>;
}
export default pattern(({ about, roster }) => ({
about,
roster,
describeRoom: describeRoom({ about }),
join: join({ roster }),
}));
`;
// `NAMED_SOURCE` with `notes` beside its fields: links, each to a document of
// its own whose `tags` only `post` and `describeRoom` may write, as a
// FabriChat room's `messages` do.
const LINKED_SOURCE = `///
import {
type AuthoredByCurrentUser,
Default,
handler,
pattern,
Stream,
type TrustedActionWrite,
Writable,
type WriteAuthorizedBy,
type WritePolicyAnyOf,
} from "commonfabric";
interface About {
title: string;
}
const describeRoom = handler<
{ title: string },
{ about: Writable }
>(({ title }, { about }) => {
about.set({ title });
});
type StoredAbout = AuthoredByCurrentUser<
TrustedActionWrite
>;
interface Note {
text: string;
tags?: WritePolicyAnyOf,
WriteAuthorizedBy,
]>;
}
type NotesCell = Writable>;
const join = handler<{ name: string }, { roster: RosterCell }>(
({ name }, { roster }) => {
roster.key("items").push(name);
},
);
interface Roster {
items?: WriteAuthorizedBy;
}
type RosterCell = Writable>>;
const post = handler<{ text: string }, { notes: NotesCell }>(
({ text }, { notes }) => {
const note = notes.elementById(text);
note.set({ text, tags: ["new"] });
notes.addUnique(note);
},
);
interface Input {
about?: StoredAbout;
roster?: RosterCell;
notes?: NotesCell;
}
interface Output {
about?: About;
roster: Roster;
notes: Note[];
describeRoom: Stream<{ title: string }>;
join: Stream<{ name: string }>;
post: Stream<{ text: string }>;
}
export default pattern(({ about, roster, notes }) => ({
about,
roster,
notes,
describeRoom: describeRoom({ about }),
join: join({ roster }),
post: post({ notes }),
}));
`;
/** What a test reads off a room. */
type Room = Record;
/** What `reconcileRelease()` hands its probe. */
interface Released {
/** What the reconciler returned. */
outcome: string;
/** The room, read after the reconciliation. */
room: Cell;
/** The reconciliation the room records. */
recorded: PieceReconciliation | undefined;
/** The identity of the source the room was made from. */
previousIdentity: string;
/** The identity of the release. */
currentIdentity: string;
/** The runtime the room runs in. */
runtime: Runtime;
}
/** Sends `event` to the piece's `stream`, and waits for what it wrote. */
async function send(
runtime: Runtime,
room: Cell,
stream: string,
event: unknown,
): Promise {
(room.key(stream) as unknown as { send: (e: unknown) => void }).send(event);
await runtime.idle();
await room.pull();
await runtime.idle();
}
/**
* `fields` as an event from the room's own surface for `action`, which a
* field that surface's UI contract guards needs: the renderer-trusted mark
* and the surface's provenance.
*/
function trustedEvent(action: string, fields: T): T {
const event = {
...fields,
provenance: {
origin: "dom",
trusted: true,
ui: {
pattern: "GuardedRoom",
eventIntegrity: ["GuardedRoom"],
uiContractDataset: { uiAction: action },
},
},
};
markRendererTrustedEvent(event);
return event;
}
/**
* Creates a room from `previous`, served as the `system:` origin's source,
* and lets `prime` write its guarded fields. Then serves `current` instead,
* reconciles the room, and hands what came of it to `probe`.
*/
async function reconcileRelease(
name: string,
previous: string,
current: string,
prime: (runtime: Runtime, room: Cell) => Promise,
probe: (released: Released) => void | Promise,
): Promise {
const signer = await Identity.fromPassphrase(`guarded argument ${name}`);
const previousIdentity = await resolveEntryIdentity(
route,
() => Promise.resolve(previous),
);
const currentIdentity = await resolveEntryIdentity(
route,
() => Promise.resolve(current),
);
let served = { contents: previous, identity: previousIdentity };
const runtime = new Runtime(runtimePresets.unitTest({
apiUrl: new URL("https://room.test"),
storageManager: StorageManager.emulate({ as: signer }),
fetch: (input) => {
const url = new URL(input instanceof Request ? input.url : input);
if (url.pathname !== route) {
return Promise.resolve(new Response("not found", { status: 404 }));
}
return Promise.resolve(
new Response(
url.searchParams.has("identity") ? served.identity : served.contents,
),
);
},
}));
try {
const program: RuntimeProgram = {
main: route,
files: [{ name: route, contents: previous }],
};
const tx = runtime.edit();
const pattern = await runtime.patternManager.compilePattern(program, {
space: signer.did(),
tx,
});
const room = runtime.getCell(signer.did(), name);
runtime.runner.run(tx, pattern, {}, room, { sourceOrigin: origin });
runtime.prepareTxForCommit(tx);
expect((await tx.commit().settled).error).toBeUndefined();
await room.pull();
await runtime.idle();
await prime(runtime, room);
expect(getPatternIdentityRef(room)?.identity).toBe(previousIdentity);
served = { contents: current, identity: currentIdentity };
const outcome = await runtime.sourceReconciler.reconcile(room);
await runtime.runner.idlePointerMaintenance();
await room.pull();
await probe({
outcome,
room,
recorded: getPieceReconciliation(room),
previousIdentity,
currentIdentity,
runtime,
});
} finally {
await runtime.sourceReconciler.idle();
await runtime.patternManager.flushCompileCacheWrites();
await runtime.dispose();
}
}
/** `source` one byte apart, which gives every handler in it a new identity. */
const releaseBehind = (source: string): string =>
`${source}\n// a release behind\n`;
/** The claims the envelope the room's argument stores declares at `path`. */
function storedClaimsAt(
runtime: Runtime,
room: Cell,
path: string,
): unknown {
const tx = runtime.edit();
try {
const stored = loadStoredCfcEnvelope(
tx,
room.getArgumentCell()!.getAsNormalizedFullLink(),
);
expect(stored.status).toBe("loaded");
if (stored.status !== "loaded") return undefined;
const entry = cfcSchemaEntries(stored.schema).find((entry) =>
entry.path.join("/") === path
);
return (entry?.schema as { ifc?: unknown } | undefined)?.ifc;
} finally {
tx.abort();
}
}
/** The schema hash of the envelope the room's argument stores. */
function storedSchemaHash(runtime: Runtime, room: Cell): string {
const tx = runtime.edit();
try {
const stored = loadStoredCfcEnvelope(
tx,
room.getArgumentCell()!.getAsNormalizedFullLink(),
);
expect(stored.status).toBe("loaded");
return stored.status === "loaded" ? String(stored.metadata.schemaHash) : "";
} finally {
tx.abort();
}
}
describe("source-reconciler-protected-argument", () => {
describe("a field only its handler may write", () => {
const prime = async (runtime: Runtime, room: Cell) => {
await send(runtime, room, "retitle", { text: "lunch" });
expect(room.get().title).toEqual({ text: "lunch" });
};
it("adopts a release that leaves the field as it is, and keeps what it holds", async () => {
await reconcileRelease(
"writer-release",
releaseBehind(WRITER_SOURCE),
WRITER_SOURCE,
prime,
({ outcome, room, currentIdentity }) => {
expect(outcome).toBe("updated");
expect(getPatternIdentityRef(room)?.identity).toBe(currentIdentity);
expect(room.get().title).toEqual({ text: "lunch" });
},
);
});
it("refuses a release whose setup would change the field, records why, and keeps the field as it was", async () => {
// The release gives the field's record a member with a default, which
// the setup fills into what the field holds: a write there only
// `retitle` may make.
const current = WRITER_SOURCE.replace(
"WriteAuthorizedBy<{ text: string }, typeof retitle>",
'WriteAuthorizedBy<{ text: string; color?: Default }, typeof retitle>',
);
expect(current).not.toBe(WRITER_SOURCE);
await reconcileRelease(
"writer-changing-release",
WRITER_SOURCE,
current,
prime,
({ outcome, room, recorded, previousIdentity }) => {
expect(outcome).toBe("unavailable");
expect(recorded?.outcome).toBe("unreachable");
expect(recorded?.detail).toContain(
"writeAuthorizedBy requires a trusted verified binding identity " +
"at /title",
);
expect(getPatternIdentityRef(room)?.identity).toBe(previousIdentity);
expect(room.get().title).toEqual({ text: "lunch" });
},
);
});
});
describe("fields a UI contract guards", () => {
const prime = async (runtime: Runtime, room: Cell) => {
await send(
runtime,
room,
"describeRoom",
trustedEvent("Describe", { title: "lunch" }),
);
await send(runtime, room, "pin", trustedEvent("Pin", { text: "hi" }));
expect(room.get().about).toEqual({ title: "lunch" });
expect(room.get().badge).toEqual({ text: "hi" });
};
it("adopts a release that leaves the fields as they are, and keeps what they hold", async () => {
await reconcileRelease(
"contract-release",
releaseBehind(CONTRACT_SOURCE),
CONTRACT_SOURCE,
prime,
({ outcome, room, currentIdentity }) => {
expect(outcome).toBe("updated");
expect(getPatternIdentityRef(room)?.identity).toBe(currentIdentity);
expect(room.get().about).toEqual({ title: "lunch" });
expect(room.get().badge).toEqual({ text: "hi" });
},
);
});
it("refuses a release whose setup would change a field, records the missing trusted event, and keeps the field as it was", async () => {
// As for the writer's field above: the release fills a default into
// what `badge` holds, which only an event from its surface may write.
const current = CONTRACT_SOURCE.replace(
'TrustedActionUiContract<{ text: string }, "Pin", "GuardedRoom">',
'TrustedActionUiContract<{ text: string; color?: Default }, "Pin", "GuardedRoom">',
);
expect(current).not.toBe(CONTRACT_SOURCE);
await reconcileRelease(
"contract-changing-release",
CONTRACT_SOURCE,
current,
prime,
({ outcome, room, recorded, previousIdentity }) => {
expect(outcome).toBe("unavailable");
expect(recorded?.outcome).toBe("unreachable");
expect(recorded?.detail).toContain(
"missing trusted-event policy input for",
);
expect(recorded?.detail).toContain("at /badge");
expect(getPatternIdentityRef(room)?.identity).toBe(previousIdentity);
expect(room.get().badge).toEqual({ text: "hi" });
},
);
});
it("records a release that changes a field's trusted action as refused for good, and does not prepare it again", async () => {
// The envelope `about` stores fixes the reviewed action that may write
// it. The release's own differs, so the merge refuses it as a verdict
// naming the stored envelope, and the room keeps that refusal across a
// write under the schema it runs.
const current = CONTRACT_SOURCE.replace(
' typeof describeRoom,\n "Describe",',
' typeof describeRoom,\n "Rename",',
);
expect(current).not.toBe(CONTRACT_SOURCE);
await reconcileRelease(
"contract-action-release",
CONTRACT_SOURCE,
current,
prime,
async ({ outcome, room, recorded, previousIdentity, runtime }) => {
expect(outcome).toBe("rejected");
expect(recorded).toMatchObject({
outcome: "refused",
reason: "apply-failed",
});
expect(recorded?.detail).toContain("must remain stable at /about");
expect(getPatternIdentityRef(room)?.identity).toBe(previousIdentity);
await send(runtime, room, "pin", trustedEvent("Pin", { text: "yo" }));
using prepared = spy(runtime.runner, "syncStoredPieceCells");
expect(await runtime.sourceReconciler.reconcile(room)).toBe(
"rejected",
);
expect(prepared.calls).toHaveLength(0);
expect(room.get().badge).toEqual({ text: "yo" });
},
);
});
});
describe("fields whose types are named definitions", () => {
// Across a release, the merged envelope keeps the stored claims in force
// under another layout of definitions: it names the release's own
// definitions apart, and spells out in place what the stored one names
// by reference.
const prime = async (runtime: Runtime, room: Cell) => {
await send(
runtime,
room,
"describeRoom",
trustedEvent("Describe", { title: "lunch" }),
);
await send(runtime, room, "join", { name: "ada" });
expect(room.get().about).toEqual({ title: "lunch" });
expect(room.get().roster).toEqual({ items: ["ada"] });
};
it("adopts a release that leaves the fields as they are, and keeps what they hold", async () => {
await reconcileRelease(
"named-release",
releaseBehind(NAMED_SOURCE),
NAMED_SOURCE,
prime,
({ outcome, room, currentIdentity }) => {
expect(outcome).toBe("updated");
expect(getPatternIdentityRef(room)?.identity).toBe(currentIdentity);
expect(room.get().about).toEqual({ title: "lunch" });
expect(room.get().roster).toEqual({ items: ["ada"] });
},
);
});
it("refuses a release that adds a policy to a named definition, and keeps the field as it was", async () => {
// The release guards `Roster.items` with a UI contract as well, a
// policy the stored envelope does not declare there.
const current = NAMED_SOURCE.replace(
"items?: WriteAuthorizedBy;",
'items?: TrustedActionWrite;',
);
expect(current).not.toBe(NAMED_SOURCE);
await reconcileRelease(
"named-policy-release",
NAMED_SOURCE,
current,
prime,
({ outcome, room, recorded, previousIdentity }) => {
expect(outcome).toBe("unavailable");
expect(recorded?.outcome).toBe("unreachable");
expect(getPatternIdentityRef(room)?.identity).toBe(previousIdentity);
expect(room.get().roster).toEqual({ items: ["ada"] });
},
);
});
});
describe("fields beside a list of links", () => {
// A release re-describes the claims beneath a position holding links,
// which describe the documents linked there: each `Note`'s `tags` names
// the release's `post`. None of them governs this document.
const prime = async (runtime: Runtime, room: Cell) => {
await send(
runtime,
room,
"describeRoom",
trustedEvent("Describe", { title: "lunch" }),
);
await send(runtime, room, "join", { name: "ada" });
await send(runtime, room, "post", { text: "hello" });
expect(room.get().about).toEqual({ title: "lunch" });
expect(room.get().notes).toEqual([{ text: "hello", tags: ["new"] }]);
};
it("adopts a release that re-describes what lies beneath the links, and keeps what the fields hold", async () => {
await reconcileRelease(
"linked-release",
releaseBehind(LINKED_SOURCE),
LINKED_SOURCE,
prime,
({ outcome, room, currentIdentity }) => {
expect(outcome).toBe("updated");
expect(getPatternIdentityRef(room)?.identity).toBe(currentIdentity);
expect(room.get().about).toEqual({ title: "lunch" });
expect(room.get().notes).toEqual([{ text: "hello", tags: ["new"] }]);
},
);
});
it("keeps the stored envelope whole through a release that changes only the claims beneath the links", async () => {
// The release names no writer for each `Note`'s `tags`, so its
// envelope differs from the stored one beneath the links alone.
const current = LINKED_SOURCE.replace(
/tags\?: WritePolicyAnyOf<[^;]*;/,
"tags?: AuthoredByCurrentUser;",
);
expect(current).toContain("tags?: AuthoredByCurrentUser;");
let before = "";
await reconcileRelease(
"linked-claims-release",
LINKED_SOURCE,
current,
async (runtime, room) => {
await prime(runtime, room);
before = storedSchemaHash(runtime, room);
},
({ outcome, room, runtime, currentIdentity }) => {
expect(outcome).toBe("updated");
expect(getPatternIdentityRef(room)?.identity).toBe(currentIdentity);
expect(before).not.toBe("");
expect(storedSchemaHash(runtime, room)).toBe(before);
},
);
});
it("adopts a release that adds a guarded slot beside a change beneath the links, storing the slot's policy and the release's claims beneath the links", async () => {
// The release names no writer for each `Note`'s `tags` and adds
// `titles`, a list only `rename` may write. The envelope stored beside
// the new slot is the release's merged one, which re-describes what
// lies beneath the links, as a release's merged envelope does.
const current = LINKED_SOURCE
.replace(
/tags\?: WritePolicyAnyOf<[^;]*;/,
"tags?: AuthoredByCurrentUser;",
)
.replace(
"interface Input {",
`const rename = handler<{ title: string }, { titles: TitlesCell }>(
({ title }, { titles }) => {
const record = titles.elementById("title");
record.set({ title });
titles.addUnique(record);
},
);
type TitlesCell = Writable<
WriteAuthorizedBy[] | Default<[]>
>;
interface Input {
titles?: TitlesCell;`,
)
.replace(
"({ about, roster, notes }) => ({",
"({ about, roster, notes, titles }) => ({\n titles,\n rename: rename({ titles }),",
);
expect(current).toContain("tags?: AuthoredByCurrentUser;");
expect(current).toContain("rename: rename({ titles })");
await reconcileRelease(
"linked-added-slot-release",
LINKED_SOURCE,
current,
async (runtime, room) => {
await prime(runtime, room);
expect(storedClaimsAt(runtime, room, "notes/*/tags")).toMatchObject({
writePolicyAnyOf: expect.any(Array),
});
},
({ outcome, room, runtime, recorded, currentIdentity }) => {
expect(recorded?.detail).toBeUndefined();
expect(outcome).toBe("updated");
expect(getPatternIdentityRef(room)?.identity).toBe(currentIdentity);
expect(storedClaimsAt(runtime, room, "notes/*/tags")).toEqual({
addIntegrity: [{
kind: "authored-by",
subject: { __ctCurrentPrincipal: true },
}],
});
expect(storedClaimsAt(runtime, room, "titles/*")).toMatchObject({
writeAuthorizedBy: expect.anything(),
});
expect(room.get().titles).toEqual([]);
expect(room.get().notes).toEqual([{ text: "hello", tags: ["new"] }]);
},
);
});
it("refuses a release that adds a policy beside the links, and keeps the field as it was", async () => {
// `roster` holds no links, so a policy the release adds to
// `Roster.items` governs this document, links or no links beside it.
const current = LINKED_SOURCE.replace(
"items?: WriteAuthorizedBy;",
'items?: TrustedActionWrite;',
);
expect(current).not.toBe(LINKED_SOURCE);
await reconcileRelease(
"linked-policy-release",
LINKED_SOURCE,
current,
prime,
({ outcome, room, recorded, previousIdentity }) => {
expect(outcome).toBe("unavailable");
expect(recorded?.outcome).toBe("unreachable");
expect(getPatternIdentityRef(room)?.identity).toBe(previousIdentity);
expect(room.get().roster).toEqual({ items: ["ada"] });
},
);
});
it("refuses a release that adds a policy at the links themselves, and keeps the list as it was", async () => {
// A claim at the position holding the links governs which links this
// document may hold, so it is this document's to keep.
const current = LINKED_SOURCE.replace(
"type NotesCell = Writable>;",
"type NotesCell = Writable[] | Default<[]>>;",
);
expect(current).not.toBe(LINKED_SOURCE);
await reconcileRelease(
"linked-position-release",
LINKED_SOURCE,
current,
prime,
({ outcome, room, recorded, previousIdentity }) => {
expect(outcome).toBe("unavailable");
expect(recorded?.outcome).toBe("unreachable");
expect(getPatternIdentityRef(room)?.identity).toBe(previousIdentity);
expect(room.get().notes).toEqual([{ text: "hello", tags: ["new"] }]);
},
);
});
});
describe("a slot the release adds", () => {
// The stored document holds nothing at the slot, and the stored envelope
// declares nothing there, so the setup fills it with the release's
// default and the release's envelope, which declares the slot's policy,
// is stored.
const prime = async (runtime: Runtime, room: Cell) => {
await send(
runtime,
room,
"describeRoom",
trustedEvent("Describe", { title: "lunch" }),
);
await send(runtime, room, "join", { name: "ada" });
expect(room.get().about).toEqual({ title: "lunch" });
expect(room.get().roster).toEqual({ items: ["ada"] });
};
/**
* What the error of a commit writing `value` at `slot` of the room's
* argument says, through a cell with no schema, which only what the
* stored envelope declares there governs.
*/
const refusalOfDirectWrite = async (
runtime: Runtime,
room: Cell,
slot: string,
value: unknown,
): Promise => {
const argument = room.getArgumentCell()!;
expect(argument.schema).toBeUndefined();
const tx = runtime.edit();
argument.withTx(tx).key(slot).set(value);
return (await tx.commit().settled).error?.message;
};
// `NAMED_SOURCE` with `titles` beside its fields: a list whose entries only
// `rename` may write, defaulting to `defaultTitles`, as a FabriChat room's
// `titles` is.
const withList = (defaultTitles = "[]"): string => {
const source = NAMED_SOURCE
.replace(
"interface Input {",
`const rename = handler<{ title: string }, { titles: TitlesCell }>(
({ title }, { titles }) => {
const record = titles.elementById("title");
record.set({ title });
titles.addUnique(record);
},
);
type TitlesCell = Writable<
WriteAuthorizedBy[] | Default<${defaultTitles}>
>;
interface Input {
titles?: TitlesCell;`,
)
.replace(
" roster: Roster;\n",
" roster: Roster;\n titles: About[];\n",
)
.replace(
"({ about, roster }) => ({",
"({ about, roster, titles }) => ({\n titles,\n rename: rename({ titles }),",
)
.replace(
" join: Stream<{ name: string }>;\n",
" join: Stream<{ name: string }>;\n rename: Stream<{ title: string }>;\n",
);
expect(source).toContain("titles?: TitlesCell;");
expect(source).toContain("titles: About[];");
expect(source).toContain("rename: rename({ titles })");
expect(source).toContain("rename: Stream<");
return source;
};
// `NAMED_SOURCE` with `label` beside its fields: a record only `relabel`
// may write, defaulting to an empty title.
const withRecord = (): string => {
const source = NAMED_SOURCE
.replace(
"interface Input {",
`const relabel = handler<{ title: string }, { label: Writable }>(
({ title }, { label }) => {
label.set({ title });
},
);
interface Input {
label?: Default, { title: "" }>;`,
)
.replace(" roster: Roster;\n", " roster: Roster;\n label: About;\n")
.replace(
"({ about, roster }) => ({",
"({ about, roster, label }) => ({\n label,\n relabel: relabel({ label }),",
)
.replace(
" join: Stream<{ name: string }>;\n",
" join: Stream<{ name: string }>;\n relabel: Stream<{ title: string }>;\n",
);
expect(source).toContain("label?: Default<");
expect(source).toContain("label: About;");
expect(source).toContain("relabel: relabel({ label })");
expect(source).toContain("relabel: Stream<");
return source;
};
it("adopts a release that adds a list whose entries a writer guards, and keeps what the other fields hold", async () => {
await reconcileRelease(
"added-list-release",
NAMED_SOURCE,
withList(),
prime,
({ outcome, room, recorded, currentIdentity }) => {
expect(recorded?.detail).toBeUndefined();
expect(outcome).toBe("updated");
expect(getPatternIdentityRef(room)?.identity).toBe(currentIdentity);
expect(room.get().about).toEqual({ title: "lunch" });
expect(room.get().roster).toEqual({ items: ["ada"] });
expect(room.get().titles).toEqual([]);
},
);
});
it("stores the added list's policy, and its writers and the other fields' write after the release", async () => {
await reconcileRelease(
"added-list-writes",
NAMED_SOURCE,
withList(),
prime,
async ({ outcome, room, runtime }) => {
expect(outcome).toBe("updated");
expect(
await refusalOfDirectWrite(runtime, room, "titles", [{
title: "forged",
}]),
).toContain("writeAuthorizedBy");
expect(
await refusalOfDirectWrite(runtime, room, "about", {
title: "forged",
}),
).toContain("writeAuthorizedBy");
await send(runtime, room, "rename", { title: "tea" });
await send(runtime, room, "join", { name: "bo" });
await send(
runtime,
room,
"describeRoom",
trustedEvent("Describe", { title: "dinner" }),
);
expect(room.get().titles).toEqual([{ title: "tea" }]);
expect(room.get().roster).toEqual({ items: ["ada", "bo"] });
expect(room.get().about).toEqual({ title: "dinner" });
},
);
});
it("adopts a release that adds a record a writer guards, holding its default, and stores its policy", async () => {
await reconcileRelease(
"added-record-release",
NAMED_SOURCE,
withRecord(),
prime,
async ({ outcome, room, recorded, currentIdentity, runtime }) => {
expect(recorded?.detail).toBeUndefined();
expect(outcome).toBe("updated");
expect(getPatternIdentityRef(room)?.identity).toBe(currentIdentity);
expect(room.get().about).toEqual({ title: "lunch" });
expect(room.get().roster).toEqual({ items: ["ada"] });
expect(room.get().label).toEqual({ title: "" });
expect(
await refusalOfDirectWrite(runtime, room, "label", {
title: "forged",
}),
).toContain("writeAuthorizedBy");
await send(runtime, room, "relabel", { title: "tea" });
expect(room.get().label).toEqual({ title: "tea" });
},
);
});
it("refuses a release that adds a list whose default holds an entry, and keeps the fields as they were", async () => {
// An entry of the list is a value only `rename` may write, and no
// initialization reaches a list's entries.
await reconcileRelease(
"added-list-entry-release",
NAMED_SOURCE,
withList('[{ title: "first" }]'),
prime,
({ outcome, room, recorded, previousIdentity }) => {
expect(outcome).toBe("unavailable");
expect(recorded?.outcome).toBe("unreachable");
expect(recorded?.detail).toContain(
"writeAuthorizedBy requires a trusted verified binding identity " +
"at /titles/*",
);
expect(getPatternIdentityRef(room)?.identity).toBe(previousIdentity);
expect(room.get().about).toEqual({ title: "lunch" });
expect(room.get().titles).toBeUndefined();
},
);
});
it("refuses a release that adds a slot and changes a guarded field, and keeps the field as it was", async () => {
// The release gives `About` a member with a default, which the setup
// fills into what `about` holds: a write there only `describeRoom` may
// make, whatever the release adds beside it.
const current = withList().replace(
"interface About {\n title: string;\n}",
'interface About {\n title: string;\n color?: Default;\n}',
);
expect(current).toContain("color?: Default {
expect(outcome).toBe("unavailable");
expect(recorded?.outcome).toBe("unreachable");
expect(recorded?.detail).toContain("at /about");
expect(getPatternIdentityRef(room)?.identity).toBe(previousIdentity);
expect(room.get().about).toEqual({ title: "lunch" });
},
);
});
it("refuses a release that adds a guarded member to a guarded record, and keeps the record as it was", async () => {
// `about` holds no `note`, and the setup writes none, but the release
// changes what writing `about` requires, and the setup restates it.
const current = NAMED_SOURCE.replace(
"interface About {\n title: string;\n}",
"interface About {\n title: string;\n note?: WriteAuthorizedBy;\n}",
);
expect(current).toContain("note?: WriteAuthorizedBy {
expect(outcome).toBe("unavailable");
expect(recorded?.outcome).toBe("unreachable");
expect(recorded?.detail).toContain("at /about");
expect(getPatternIdentityRef(room)?.identity).toBe(previousIdentity);
expect(room.get().about).toEqual({ title: "lunch" });
},
);
});
it("refuses a release that adds a policy at a guarded slot the document does not hold yet, and keeps the fields as they were", async () => {
// The stored envelope already guards `label`, so a UI contract the
// release adds there joins a declared policy rather than a new field's.
const guarded = (policy: string) =>
withRecord()
.replace(
'label?: Default, { title: "" }>;',
`label?: ${policy};`,
)
.replace(" label: About;\n", " label?: About;\n")
.replace(
"{ label: Writable }",
"{ label: Writable }",
);
const previous = guarded("WriteAuthorizedBy");
const current = guarded(
'TrustedActionWrite',
);
expect(previous).toContain(
"label?: WriteAuthorizedBy;",
);
expect(current).toContain("label?: TrustedActionWrite }");
await reconcileRelease(
"unheld-policy-release",
previous,
current,
prime,
({ outcome, room, recorded, previousIdentity }) => {
expect(room.get().label).toBeUndefined();
expect(outcome).toBe("unavailable");
expect(recorded?.outcome).toBe("unreachable");
expect(getPatternIdentityRef(room)?.identity).toBe(previousIdentity);
expect(room.get().about).toEqual({ title: "lunch" });
},
);
});
it("refuses a release that adds a guarded list where the document holds `undefined`, and keeps the slot as it was", async () => {
// A slot holding `undefined` holds a value, which no initialization
// replaces and no new policy may adopt.
await reconcileRelease(
"undefined-slot-release",
NAMED_SOURCE,
withList(),
async (runtime, room) => {
await prime(runtime, room);
const argument = room.getArgumentCell()!;
const tx = runtime.edit();
argument.withTx(tx).key("titles").setRaw(undefined);
expect((await tx.commit().settled).error).toBeUndefined();
await argument.pull();
const stored = argument.getRaw() as Room;
expect(Object.hasOwn(stored, "titles")).toBe(true);
expect(stored.titles).toBeUndefined();
},
({ outcome, room, recorded, previousIdentity }) => {
expect(outcome).toBe("unavailable");
expect(recorded?.outcome).toBe("unreachable");
expect(getPatternIdentityRef(room)?.identity).toBe(previousIdentity);
const stored = room.getArgumentCell()!.getRaw() as Room;
expect(Object.hasOwn(stored, "titles")).toBe(true);
expect(stored.titles).toBeUndefined();
},
);
});
it("refuses a release that guards a slot the document already holds, and keeps the slot as it was", async () => {
// Before the release, `titles` is a list anyone may write, and holds an
// entry; guarding it would adopt what it holds.
const previous = withList().replace(
"WriteAuthorizedBy[]",
"About[]",
);
expect(previous).not.toContain("WriteAuthorizedBy {
await prime(runtime, room);
await send(runtime, room, "rename", { title: "tea" });
expect(room.get().titles).toEqual([{ title: "tea" }]);
},
({ outcome, room, recorded, previousIdentity }) => {
expect(outcome).toBe("unavailable");
expect(recorded?.outcome).toBe("unreachable");
expect(getPatternIdentityRef(room)?.identity).toBe(previousIdentity);
expect(room.get().titles).toEqual([{ title: "tea" }]);
},
);
});
});
});