import { afterEach, beforeEach, describe, it } from "@std/testing/bdd";
import { expect } from "@std/expect";
import {
getPatternIdentityRef,
getPatternSetupIdentityRef,
isLink,
resolveEntryIdentity,
resolveSystemPatternSource,
Runtime,
} from "@commonfabric/runner";
import {
EmulatedStorageManager,
newLoopbackServer,
} from "@commonfabric/runner/storage/cache.deno";
import * as MemoryV2Server from "@commonfabric/memory/v2/server";
import { createSession, Identity } from "@commonfabric/identity";
import {
DEFAULT_APP_PATTERN_SOURCE,
PiecesController,
} from "../src/ops/pieces-controller.ts";
import { reconcilePieceSource } from "../src/ops/piece-origin.ts";
import { rawMetaWriteAuthorization } from "@commonfabric/runner/meta-seam";
// The route that ref expands to — what the toolshed serves, and what the
// worker names the module by when it compiles the pattern over HTTP.
const DEFAULT_APP_PATTERN_PATH = resolveSystemPatternSource(
DEFAULT_APP_PATTERN_SOURCE,
)!;
// Golden replay for non-home root state across an in-place update.
//
// The other swap tests prove that patternIdentity changes while the piece
// entity stays the same. This test seeds representative durable state under
// version N and updates the running root to version N+1. It verifies that the
// state remains intact and that the new code runs over it.
const signer = await Identity.fromPassphrase("default-app golden replay");
const newSharedServer = (): MemoryV2Server.Server => newLoopbackServer();
// Two versions of a default-app-shaped root with the same registry cause.
const ROOT_V1 = [
"import { pattern, computed, Writable } from 'commonfabric';",
"interface Profile { name: string; }",
"interface Input { label?: string; profile?: Profile; }",
"export default pattern(() => {",
" const pieceRegistry = new Writable([]);",
" return {",
" pieceRegistry,",
" summary: computed(() => `v1:` + pieceRegistry.get().length),",
" };",
"});",
"",
].join("\n");
const ROOT_V2 = [
"import { Default, pattern, computed, Writable } from 'commonfabric';",
"interface Profile { name: string; }",
"interface Input { label?: string; profile: Profile; count: number | Default<2>; }",
"export default pattern(({ profile }) => {",
" const pieceRegistry = new Writable([]);",
" return {",
" pieceRegistry,",
" summary: computed(() => `v2:` + pieceRegistry.get().length),",
" profileName: computed(() => profile.name),",
" };",
"});",
"",
].join("\n");
const SEEDED_PIECES = ["note:groceries", "note:standup", "notebook:trip"];
/** Content identity a toolshed would serve for `source`. */
function identityForSource(source: string): Promise {
return resolveEntryIdentity(
DEFAULT_APP_PATTERN_PATH,
(name) =>
name === DEFAULT_APP_PATTERN_PATH
? Promise.resolve(source)
: Promise.reject(new Error(`not found: ${name}`)),
);
}
interface StubControls {
setSource(source: string): void;
restore(): void;
}
function installFetchStub(): StubControls {
const original = globalThis.fetch;
let source = ROOT_V1;
globalThis.fetch = (async (input: string | URL | Request) => {
const href = typeof input === "string"
? input
: input instanceof URL
? input.href
: input.url;
const url = new URL(href);
if (url.pathname === DEFAULT_APP_PATTERN_PATH) {
if (url.searchParams.has("identity")) {
return new Response(await identityForSource(source), {
headers: { "content-type": "text/plain" },
});
}
return new Response(source, {
headers: { "content-type": "text/typescript-jsx" },
});
}
return new Response("not found", { status: 404 });
}) as typeof globalThis.fetch;
return {
setSource: (s) => (source = s),
restore: () => (globalThis.fetch = original),
};
}
describe("default-app golden replay (state survives an in-place roll-forward)", () => {
let stub: StubControls;
let server: MemoryV2Server.Server;
let storageManager: EmulatedStorageManager;
let runtime: Runtime;
let controller: PiecesController;
beforeEach(async () => {
stub = installFetchStub();
stub.setSource(ROOT_V1);
server = newSharedServer();
storageManager = EmulatedStorageManager.connectTo(server, { as: signer });
runtime = new Runtime({
apiUrl: new URL("http://toolshed.test"),
storageManager,
});
const session = createSession({
identity: signer,
spaceDid: await runtime.createSpace(),
});
controller = new PiecesController(session, runtime);
await controller.synced();
});
afterEach(async () => {
try {
await controller?.dispose();
} catch { /* already disposed */ }
await storageManager?.close();
await server?.close();
stub.restore();
});
it("preserves registry state without restoring removed pieces", async () => {
// N: instantiate the default-app-shaped root.
const piece = await controller.ensureDefaultPattern();
const root = piece.getCell();
const rootLinkBefore = JSON.stringify(root.getAsLink());
const idV1 = getPatternIdentityRef(root)?.identity;
expect(idV1).toBe(await identityForSource(ROOT_V1));
// Keep a live subscription on the root's reactive `summary`, the way a
// mounted shell would. The graph is pull-based; without a standing consumer
// the re-instantiated pattern would be built but never execute, and we would
// only be testing the meta swap (which the sibling tests already cover), not
// that the new code actually runs and re-reads the seeded state. The sink
// also delivers the RESOLVED computed value (a bare `.get()` on a computed
// result key yields the unresolved alias), which we latch here.
let summary: unknown;
const cancelSink = root.key("summary").sink((value) => {
summary = value;
});
// Seed representative state: add pieces to the running root, the way a
// user filling a fresh space would, and confirm they landed durably.
await runtime.editWithRetry((tx) => {
root.withTx(tx).key("pieceRegistry").set([...SEEDED_PIECES]);
});
await piece.setInput({ profile: { name: "warm" } });
await runtime.idle();
expect(root.key("pieceRegistry").get()).toEqual(SEEDED_PIECES);
// V1's reactive summary sees the seeded state.
expect(summary).toBe("v1:" + SEEDED_PIECES.length);
// N+1: the toolshed now serves a newer default-app (its `summary` logic
// changed). Roll forward in place.
stub.setSource(ROOT_V2);
expect(
await reconcilePieceSource(
runtime,
(await controller
.getDefaultPattern(false))!,
),
).toBe("updated");
// Let the pattern watcher observe the meta change and re-instantiate, then
// pull the root so the new instance actually executes (pull-based graph).
// A `system:` update grants the candidate the running pattern's writer
// authority, and the watcher reads that grant before it follows the
// pointer; `idle()` does not cover that read, the pointer maintenance
// settle does.
await runtime.idle();
await runtime.runner.idlePointerMaintenance();
const rolled = (await controller.getDefaultPattern(false))!;
await rolled.pull();
const registryRoot = rolled.asSchema(
{
type: "object",
properties: {
pieceRegistry: { type: "array", items: { type: "string" } },
},
} as const,
);
let pieceRegistry: unknown;
const cancelPieceRegistrySink = registryRoot.key("pieceRegistry").sink(
(value) => {
pieceRegistry = value;
},
);
await runtime.idle();
// Same piece entity — the root was rewritten in place, not re-minted.
expect(JSON.stringify(rolled.getAsLink())).toBe(rootLinkBefore);
// The identity advanced to V2.
const idV2 = getPatternIdentityRef(rolled)?.identity;
expect(idV2).toBe(await identityForSource(ROOT_V2));
expect(idV2).not.toBe(idV1);
// The new computation proves that V2 is running before we inspect the
// registry state.
expect(summary).toBe("v2:" + SEEDED_PIECES.length);
// The crux: the state seeded under V1 survived the swap, intact and in
// order. No crash, no loss.
expect(pieceRegistry).toEqual(SEEDED_PIECES);
// Emptying the registry later is intentional user state.
await runtime.editWithRetry((tx) => {
rolled.withTx(tx).key("pieceRegistry").set([]);
});
await runtime.idle();
expect(pieceRegistry).toEqual([]);
expect(summary).toBe("v2:0");
cancelPieceRegistrySink();
cancelSink();
});
it("preserves the registry before a cold root starts", async () => {
const piece = await controller.ensureDefaultPattern();
const root = piece.getCell();
const profilePattern = await runtime.patternManager.compilePattern(
{
main: "/cold-update-profile.tsx",
files: [{
name: "/cold-update-profile.tsx",
contents: [
"import { pattern } from 'commonfabric';",
"export default pattern(() => ({ name: 'Ada' }));",
"",
].join("\n"),
}],
},
{ space: controller.getSpace() },
);
const profile = await controller.runPersistent<{ name: string }>(
profilePattern,
{},
"cold update profile",
);
await runtime.editWithRetry((tx) => {
root.withTx(tx).key("pieceRegistry").set([...SEEDED_PIECES]);
});
await piece.setInput({ label: "durable", profile });
const storedProfile = (controller.getArgument(root).getRawUntyped() as {
profile?: unknown;
}).profile;
expect(isLink(storedProfile)).toBe(true);
await runtime.idle();
await controller.synced();
await controller.stopPiece(root);
stub.setSource(ROOT_V2);
const session = createSession({
identity: signer,
spaceDid: controller.getSpace(),
});
const readerStorage = EmulatedStorageManager.connectTo(server, {
as: signer,
});
const freshRuntime = new Runtime({
apiUrl: new URL("http://toolshed.test"),
storageManager: readerStorage,
});
const freshController = new PiecesController(session, freshRuntime);
let cancelPieceRegistrySink: (() => void) | undefined;
try {
await freshController.synced();
const profileLink = profile.getAsNormalizedFullLink();
const readerReplica = readerStorage.open(
controller.getSpace(),
) as unknown as {
get?: (uri: string, scope?: unknown) => unknown;
};
expect(
readerReplica.get?.(profileLink.id, profileLink.scope),
).toBeUndefined();
const coldRoot = await freshController.getDefaultPattern(false);
expect(coldRoot).toBeDefined();
expect(await reconcilePieceSource(freshRuntime, coldRoot!))
.toBe("updated");
expect(
readerReplica.get?.(profileLink.id, profileLink.scope),
).toBeDefined();
const updated = await freshController.ensureDefaultPattern();
const updatedRoot = updated.getCell().asSchema(
{
type: "object",
properties: {
pieceRegistry: { type: "array", items: { type: "string" } },
},
} as const,
);
let pieceRegistry: unknown;
cancelPieceRegistrySink = updatedRoot.key("pieceRegistry").sink(
(value) => {
pieceRegistry = value;
},
);
await freshRuntime.idle();
expect(getPatternIdentityRef(updatedRoot)?.identity).toBe(
await identityForSource(ROOT_V2),
);
expect(pieceRegistry).toEqual(SEEDED_PIECES);
const updatedArgument = freshController.getArgument(updatedRoot);
await updatedArgument.pull();
expect(updatedArgument.get()).toEqual({
label: "durable",
profile: { name: "Ada" },
count: 2,
});
} finally {
cancelPieceRegistrySink?.();
await freshController.dispose();
await readerStorage.close();
}
});
/**
* A root pinned to V2 whose stored setup was staged by V1, started as it is.
* Returns the V2 entry ref the root now points at.
*/
async function stageMetadataOnlyRollForward() {
const piece = await controller.ensureDefaultPattern();
const root = piece.getCell();
await piece.setInput({ profile: { name: "warm" } });
await runtime.idle();
await controller.stopPiece(root);
stub.setSource(ROOT_V2);
const currentPattern = await runtime.patternManager.compilePattern(
{
main: DEFAULT_APP_PATTERN_PATH,
files: [{ name: DEFAULT_APP_PATTERN_PATH, contents: ROOT_V2 }],
},
{ space: controller.getSpace() },
);
const currentRef = runtime.patternManager.getArtifactEntryRef(
currentPattern,
)!;
const { error } = await runtime.editWithRetry((tx) => {
root.withTx(tx).setMetaRaw(
"patternIdentity",
currentRef,
rawMetaWriteAuthorization,
);
});
expect(error).toBeUndefined();
const metadataOnlyRoot = (await controller.getDefaultPattern(false))!;
await controller.startPiece(metadataOnlyRoot);
return currentRef;
}
/**
* Run `body` with pattern loads for `identity` answered by `answer` instead
* of by the pattern manager.
*/
async function withLoadFor(
identity: string,
answer: () => Promise,
body: () => Promise,
) {
const manager = runtime.patternManager;
const load = manager.loadPatternByIdentity.bind(manager);
manager.loadPatternByIdentity = (...args: Parameters) =>
args[0] === identity ? answer() : load(...args);
try {
await body();
} finally {
manager.loadPatternByIdentity = load;
}
}
it("leaves the root alone when its pinned pattern will not load", async () => {
// The re-stage repairs a document whose stored setup an older version
// staged. With no pattern to stage from there is nothing to repair with,
// so the root is left exactly as it is rather than half-written.
const currentRef = await stageMetadataOnlyRollForward();
await withLoadFor(
currentRef.identity,
() => Promise.resolve(undefined),
async () => {
await controller.ensureDefaultPattern();
},
);
await runtime.idle();
const after = (await controller.getDefaultPattern(false))!;
expect(getPatternIdentityRef(after)).toEqual(currentRef);
expect(getPatternSetupIdentityRef(after)).not.toEqual(currentRef);
});
it("leaves the root alone when loading its pinned pattern throws", async () => {
const currentRef = await stageMetadataOnlyRollForward();
await withLoadFor(
currentRef.identity,
() => Promise.reject(new Error("the pattern index is unreadable")),
async () => {
await controller.ensureDefaultPattern();
},
);
await runtime.idle();
const after = (await controller.getDefaultPattern(false))!;
expect(getPatternIdentityRef(after)).toEqual(currentRef);
expect(getPatternSetupIdentityRef(after)).not.toEqual(currentRef);
});
it("repairs a metadata-only roll-forward to the current pattern", async () => {
const piece = await controller.ensureDefaultPattern();
const root = piece.getCell();
await piece.setInput({ profile: { name: "warm" } });
await runtime.editWithRetry((tx) => {
root.withTx(tx).key("pieceRegistry").set([...SEEDED_PIECES]);
});
await runtime.idle();
await controller.stopPiece(root);
stub.setSource(ROOT_V2);
const currentPattern = await runtime.patternManager.compilePattern(
{
main: DEFAULT_APP_PATTERN_PATH,
files: [{ name: DEFAULT_APP_PATTERN_PATH, contents: ROOT_V2 }],
},
{ space: controller.getSpace() },
);
const currentRef = runtime.patternManager.getArtifactEntryRef(
currentPattern,
)!;
expect(currentRef.identity).toBe(await identityForSource(ROOT_V2));
expect(
(currentPattern.resultSchema as { required?: string[] }).required,
).toContain("profileName");
// Reproduce the updater that advanced only patternIdentity. The persisted
// root still carries V1's stored schema and projection.
const metadataUpdate = await runtime.editWithRetry((tx) => {
root.withTx(tx).setMetaRaw(
"patternIdentity",
currentRef,
rawMetaWriteAuthorization,
);
});
expect(metadataUpdate.error).toBeUndefined();
const metadataOnlyRoot = (await controller.getDefaultPattern(false))!;
expect(getPatternIdentityRef(metadataOnlyRoot)).toEqual(currentRef);
expect(metadataOnlyRoot.getMetaRaw("schema")).not.toEqual(
currentPattern.resultSchema,
);
await controller.startPiece(metadataOnlyRoot);
expect(metadataOnlyRoot.key("profileName").getRaw()).toBeUndefined();
// Opening the root re-stages a document its pinned pattern did not set
// up, which is what repairs the projection. The repair writes through a
// transaction of its own, so read the root it hands back rather than the
// one captured before it: the older cell still describes the metadata the
// re-stage replaced.
const repaired = await controller.ensureDefaultPattern();
await runtime.idle();
expect(getPatternSetupIdentityRef(repaired.getCell())).toEqual(currentRef);
const repairedRoot = repaired.getCell().asSchema(
{
type: "object",
properties: {
pieceRegistry: { type: "array", items: { type: "string" } },
profileName: { type: "string" },
},
} as const,
);
let pieceRegistry: unknown;
const cancelPieceRegistrySink = repairedRoot.key("pieceRegistry").sink(
(value) => {
pieceRegistry = value;
},
);
await repairedRoot.pull();
await runtime.idle();
// `profileName` is V2's alone, and the projection carrying it is what the
// re-stage writes. `pieceRegistry` was seeded under V1 and survives a root
// that was never repaired, so it says nothing on its own.
expect(repairedRoot.key("profileName").get()).toBe("warm");
expect(pieceRegistry).toEqual(SEEDED_PIECES);
cancelPieceRegistrySink();
});
});