/**
* The naming tool over a handle the run holds: registry join plus slug
* assignment, the pre-flight availability rules ported from the days when
* `run_pattern` carried a `register` option, and the boundary refusals — a
* token that is not a piece, a position inside one, another space's
* reference.
*/
import { afterEach, beforeEach, describe, it } from "@std/testing/bdd";
import { stub } from "@std/testing/mock";
import { expect } from "@std/expect";
import { join, toFileUrl } from "@std/path";
import { normalize } from "@std/path/posix";
import { PatternIndexClient } from "@commonfabric/pattern-index/client";
import { createSession, Identity } from "@commonfabric/identity";
import { table } from "@commonfabric/memory/sqlite/schema";
import type { SqliteDbRef } from "@commonfabric/memory/v2";
import {
assignSlug,
resolvePieceAddress,
resolveSlugTarget,
setSlugLink,
} from "@commonfabric/piece";
import { PiecesController } from "@commonfabric/piece/ops";
import {
computeEntryIdentity,
entityIdFrom,
getEntityId,
Runtime,
slugIdForSpace,
} from "@commonfabric/runner";
import {
createLLMFriendlyLink,
parseLLMFriendlyLink,
} from "@commonfabric/runner/shared";
import {
EmulatedStorageManager,
StorageManager,
} from "@commonfabric/runner/storage/cache.deno";
import { startServingMemoryServer } from "@commonfabric/runner/executor/serving-memory-server.deno";
import { patchableCell } from "../../runner/test/support/patchable-cell.ts";
import {
CfHarnessEngine,
type CreateHarnessEngineOptions,
} from "../src/engine.ts";
import {
HarnessInteractiveChatService,
type HarnessInteractivePromptLoopFactory,
} from "../src/interactive-chat-service.ts";
import { openSqliteHarnessChatSessionStore } from "../src/sqlite-session-store.ts";
import { resolveHandleToken } from "../src/handle-table.ts";
import type { ReadPieceSourceToolSuccessOutput } from "../src/tools/piece-source.ts";
import {
assignSlugTool,
type AssignSlugToolErrorOutput,
type AssignSlugToolSuccessOutput,
} from "../src/tools/assign-slug.ts";
import { createToolOutputId } from "../src/contracts/tool-result.ts";
import { urlToAppView } from "@commonfabric/navigation";
import type { HarnessToolContext } from "../src/tools/types.ts";
import type { RunPatternToolSuccessOutput } from "../src/tools/run-pattern.ts";
import type {
SandboxCommandRequest,
SandboxCommandResult,
SandboxRuntime,
SandboxRuntimeDescription,
SandboxShellRequest,
} from "../src/sandbox/types.ts";
import { openLegacySpace } from "./support/legacy-space.ts";
const signer = await Identity.fromPassphrase("cf-harness assign-slug tool");
const DOUBLING_PATTERN_SOURCE = [
"import { computed, pattern, UI } from 'commonfabric';",
"interface Input { n: number; }",
"interface Output { doubled: number; }",
"export default pattern(({ n }) => ({",
" doubled: computed(() => n * 2),",
" [UI]:
,\n",
"",
),
inputs: { n: 21 },
});
const result = await engine.invokeBuiltinTool("assign_slug", {
token: (created.output as RunPatternToolSuccessOutput).resultRef,
slug: "data-only-probe",
});
expect(result.output).toMatchObject({ status: "error" });
expect((result.output as AssignSlugToolErrorOutput).message).toContain(
"confirm a UI",
);
expect(await pieces.getRegisteredPieces()).toEqual([]);
});
it("refuses a pending result and names the same piece after its read settles", async () => {
await linkDefaultPattern();
const engine = createEngine();
const created = await engine.invokeBuiltinTool("run_pattern", {
sourceText: [
'import { pattern, UI } from "commonfabric";',
"export default pattern<{ pending: boolean }>(({ pending }) => ({",
" pending, [UI]:
Report
,",
"}));",
].join("\n"),
inputs: { pending: true },
});
const output = created.output as RunPatternToolSuccessOutput;
const assign = () =>
engine.invokeBuiltinTool("assign_slug", {
token: output.resultRef,
slug: "settled-report",
});
const refused = await assign();
expect(refused.output).toMatchObject({ status: "error" });
expect((refused.output as AssignSlugToolErrorOutput).message).toContain(
"pending",
);
expect(await pieces.getRegisteredPieces()).toEqual([]);
const piece = await pieces.get(output.pieceId);
await piece.input.set(false, ["pending"]);
await runtime.idle();
expect((await assign()).output).toMatchObject({
status: "ok",
slug: "settled-report",
});
expect((await pieces.getRegisteredPieces()).map((piece) => piece.id))
.toEqual([output.pieceId]);
});
it("registers the referenced piece and points the slug at it", async () => {
await linkDefaultPattern();
const engine = createEngine();
const created = await createPiece(engine);
const result = await engine.invokeBuiltinTool("assign_slug", {
token: created.resultRef,
slug: "doubling-report",
});
const output = result.output as AssignSlugToolSuccessOutput;
expect(output.status).toBe("ok");
expect(output.slug).toBe("doubling-report");
expect(output.pieceId).toBe(created.pieceId);
expect(created.patternPublication).toBeUndefined();
// The URL is the session's API URL, the space's DID, and the slug,
// even for a space this session opened by its legacy name.
expect(output.url).toBe(
`http://toolshed.test/${pieces.getSpace()}/doubling-report`,
);
expect(pieces.getSpaceName()).toBeDefined();
expect(output.url).not.toContain(`/${pieces.getSpaceName()}/`);
expect(output.space).toBe(pieces.getSpace());
expect(urlToAppView(new URL(output.url!))).toEqual({
spaceDid: pieces.getSpace(),
pieceSlug: "doubling-report",
});
const registered = await pieces.getRegisteredPieces();
expect(registered.map((piece) => piece.id)).toEqual([created.pieceId]);
// The slug resolves to the same piece, so the address a person opens
// names the piece the handle named rather than merely existing.
expect(await resolvePieceAddress(pieces, "doubling-report")).toBe(
created.pieceId,
);
});
it("joins the slug artifact to its published attempt across revision and an unrelated probe", async () => {
await linkDefaultPattern();
const artifactRoot = await Deno.makeTempDir();
const published: string[] = [];
const client = new PatternIndexClient({
baseUrl: "https://index.test",
signer,
fetchFn: (url, init) => {
const body = JSON.parse(String(init?.body));
if (String(url).endsWith("/publishPattern")) {
published.push(body.patternId);
return Promise.resolve(Response.json({
patternId: body.patternId,
created: true,
}));
}
return Promise.resolve(Response.json({ ok: true }));
},
});
try {
const publishingEngine = createEngine({
artifactRoot,
patternIndexClientFactory: () => Promise.resolve(client),
});
const created = await publishingEngine.invokeBuiltinTool(
"run_pattern",
{
sourceText: DOUBLING_PATTERN_SOURCE,
inputs: { n: 21 },
description: "Doubles a number",
},
);
const createdArtifact = JSON.parse(
await Deno.readTextFile(created.resultRef.artifactPath!),
) as RunPatternToolSuccessOutput;
expect(createdArtifact.status).toBe("ok");
const publishedId = computeEntryIdentity("/main.tsx", [{
name: "/main.tsx",
contents: DOUBLING_PATTERN_SOURCE,
}]);
expect(createdArtifact.patternPublication).toMatchObject({
patternId: publishedId,
status: "queued",
});
expect(published).toEqual([]);
await publishingEngine.flushPatternIndexLedger();
expect(published).toEqual([publishedId]);
// Separate engines share the fabric but not a publication ledger,
// as a child that builds a piece and a parent that names it do.
const namingEngine = createEngine({
artifactRoot,
patternIndexClientFactory: () => Promise.resolve(client),
});
const revisedSource = DOUBLING_PATTERN_SOURCE.replace("n * 2", "n * 3");
const revised = await namingEngine.invokeBuiltinTool("revise_piece", {
token: createdArtifact.resultRef,
sourceText: revisedSource,
});
expect(revised.output.status).toBe("ok");
const source =
(await namingEngine.invokeBuiltinTool("read_piece_source", {
token: createdArtifact.resultRef,
})).output as ReadPieceSourceToolSuccessOutput;
expect(source.files.map((file) => file.contents)).toContain(
revisedSource,
);
await namingEngine.flushPatternIndexLedger();
expect(published).toEqual([publishedId]);
const probe = await namingEngine.invokeBuiltinTool("run_pattern", {
sourceText: DOUBLING_PATTERN_SOURCE.replace("n * 2", "n * 7"),
inputs: { n: 1 },
description: "An unrelated probe",
});
await namingEngine.flushPatternIndexLedger();
const probeArtifact = JSON.parse(
await Deno.readTextFile(probe.resultRef.artifactPath!),
) as RunPatternToolSuccessOutput;
expect(probeArtifact.status).toBe("ok");
expect(published).toEqual([
publishedId,
probeArtifact.patternPublication?.patternId,
]);
expect(probeArtifact.patternPublication?.patternId).not.toBe(
publishedId,
);
for (const _ of ["first assignment", "same-piece assignment"]) {
const named = await namingEngine.invokeBuiltinTool("assign_slug", {
token: createdArtifact.resultRef,
slug: "doubling-report",
});
const slugArtifact = JSON.parse(
await Deno.readTextFile(named.resultRef.artifactPath!),
) as AssignSlugToolSuccessOutput;
expect(slugArtifact.status).toBe("ok");
expect(slugArtifact.slug).toBe("doubling-report");
const attempts = [createdArtifact, probeArtifact].filter(
(attempt) => attempt.pieceId === slugArtifact.pieceId,
);
expect(attempts.map((attempt) => attempt.outputId)).toEqual([
createdArtifact.outputId,
]);
expect(attempts[0].patternPublication?.patternId).toBe(publishedId);
}
expect(
JSON.parse(await Deno.readTextFile(created.resultRef.artifactPath!)),
)
.toEqual(createdArtifact);
} finally {
await Deno.remove(artifactRoot, { recursive: true });
}
});
it("names the piece under the next counter when the slug already names another piece", async () => {
// A second call naming the same slug would repoint an address a person
// already opens. The name is passed over rather than refused: the piece
// gets the word with a counter appended, and the receipt says so, which
// is where the caller reads the address it got.
await linkDefaultPattern();
const engine = createEngine();
const first = await createPiece(engine, 21);
const second = await createPiece(engine, 22);
const held = await engine.invokeBuiltinTool("assign_slug", {
token: first.resultRef,
slug: "doubling-report",
});
expect((held.output as AssignSlugToolSuccessOutput).status).toBe("ok");
const result = await engine.invokeBuiltinTool("assign_slug", {
token: second.resultRef,
slug: "doubling-report",
});
const output = result.output as AssignSlugToolSuccessOutput;
expect(output.status).toBe("ok");
expect(output.slug).toBe("doubling-report-2");
// The URL carries the name assigned, not the one asked for.
expect(output.url).toContain("http://toolshed.test/");
expect(output.url?.endsWith("/doubling-report-2")).toBe(true);
// The address still names the piece it named before, so the counter
// protected the name rather than taking it.
expect(await resolvePieceAddress(pieces, "doubling-report")).toBe(
first.pieceId,
);
expect(await resolvePieceAddress(pieces, "doubling-report-2")).toBe(
second.pieceId,
);
});
it("counts past every taken name, and answers ok for a counter already naming this piece", async () => {
// Three pieces asking for one word end up at the word, -2 and -3. The
// counter is derived from the requested word each time rather than
// remembered, so a fourth call from the third piece lands on the name
// it already holds and says so, the way the bare word does.
await linkDefaultPattern();
const engine = createEngine();
const pieces3 = [
await createPiece(engine, 21),
await createPiece(engine, 22),
await createPiece(engine, 23),
];
const slugs: string[] = [];
for (const piece of pieces3) {
const result = await engine.invokeBuiltinTool("assign_slug", {
token: piece.resultRef,
slug: "doubling-report",
});
slugs.push((result.output as AssignSlugToolSuccessOutput).slug);
}
expect(slugs).toEqual([
"doubling-report",
"doubling-report-2",
"doubling-report-3",
]);
expect(await resolvePieceAddress(pieces, "doubling-report-3")).toBe(
pieces3[2].pieceId,
);
const again = await engine.invokeBuiltinTool("assign_slug", {
token: pieces3[2].resultRef,
slug: "doubling-report",
});
expect((again.output as AssignSlugToolSuccessOutput).slug).toBe(
"doubling-report-3",
);
await expect(resolvePieceAddress(pieces, "doubling-report-4")).rejects
.toThrow();
const registered = await pieces.getRegisteredPieces();
expect(registered.map((piece) => piece.id)).toEqual(
pieces3.map((piece) => piece.pieceId),
);
});
it("passes over a slug that names a collection, leaving the address where it pointed", async () => {
// A slug pointing at a cell inside a piece names a collection, which is
// an address a person opens as much as a piece is. Reading that as an
// operational failure would report a positive statement about what the
// space holds as a "try again", and reading it as vacancy would repoint
// the name.
await linkDefaultPattern();
const engine = createEngine();
const held = await createPiece(engine, 21);
const taking = await createPiece(engine, 22);
const cell = pieces.runtime.getCellFromLink(
parseLLMFriendlyLink(held.resultRef, pieces.getSpace()),
);
await cell.sync();
await setSlugLink(pieces, "doubling-report", cell.key("doubled"));
const result = await engine.invokeBuiltinTool("assign_slug", {
token: taking.resultRef,
slug: "doubling-report",
});
const output = result.output as AssignSlugToolSuccessOutput;
expect(output.status).toBe("ok");
expect(output.slug).toBe("doubling-report-2");
// The name still points into the piece it pointed into, so the counter
// protected the address rather than taking it.
expect(await resolveSlugTarget(pieces, "doubling-report")).toEqual({
piece: held.pieceId,
pathInside: ["doubled"],
});
expect(await resolvePieceAddress(pieces, "doubling-report-2")).toBe(
taking.pieceId,
);
});
it("takes a name whose document points at no piece, which this tool calls free", async () => {
// The tool's availability rule competes only with pieces and
// collections, so a name redirecting to a plain document is free here
// even though the assignment underneath refuses a bound name by
// default. Carrying the binding this rule was judged against into the
// write is what lets the wider rule stand without spending the claim.
await linkDefaultPattern();
const engine = createEngine();
const created = await createPiece(engine);
const plain = pieces.runtime.getCell(
pieces.getSpace(),
{ space: pieces.getSpace(), random: "plain" },
);
await pieces.runtime.editWithRetry((tx) => {
plain.withTx(tx).set({ value: 1 });
});
await setSlugLink(pieces, "doubling-report", plain);
const result = await engine.invokeBuiltinTool("assign_slug", {
token: created.resultRef,
slug: "doubling-report",
});
const output = result.output as AssignSlugToolSuccessOutput;
expect(output.status).toBe("ok");
expect(await resolvePieceAddress(pieces, "doubling-report")).toBe(
created.pieceId,
);
});
it("gives a free name to one of two calls and the next counter to the other", async () => {
// Two calls for one free name, started together and settling one after
// the other. What this asserts is the outcome — exactly one takes the
// name and the other takes the counter — and nothing about the
// interleaving: the assertions hold whether the two overlapped or ran
// in sequence. Forcing over the read instead of carrying it into the
// write would let the loser overwrite the winner and report success,
// which is what the assertions catch.
//
// The racing path is the library's, pinned over two sessions where the
// losing replica is provably behind: `packages/piece/test/slug.test.ts`,
// under "the read a refusal claims on".
//
// The two tokens name different pieces, so the address each name ends
// up holding says which call won rather than being true either way.
await linkDefaultPattern();
const engine = createEngine();
const first = await createPiece(engine, 21);
const second = await createPiece(engine, 22);
expect(first.pieceId).not.toBe(second.pieceId);
const results = await Promise.all([
engine.invokeBuiltinTool("assign_slug", {
token: first.resultRef,
slug: "doubling-report",
}),
engine.invokeBuiltinTool("assign_slug", {
token: second.resultRef,
slug: "doubling-report",
}),
]);
const outputs = results.map((result) =>
result.output as AssignSlugToolSuccessOutput
);
expect(outputs.map((output) => output.status)).toEqual(["ok", "ok"]);
expect(outputs.map((output) => output.slug).sort()).toEqual([
"doubling-report",
"doubling-report-2",
]);
const winner = outputs[0].slug === "doubling-report" ? first : second;
const loser = winner === first ? second : first;
expect(await resolvePieceAddress(pieces, "doubling-report")).toBe(
winner.pieceId,
);
expect(await resolvePieceAddress(pieces, "doubling-report-2")).toBe(
loser.pieceId,
);
});
it("answers ok for a slug already pointing at the very piece the token names", async () => {
// The request is already true, so saying so beats refusing it — a
// retried call settles on the outcome instead of failing.
await linkDefaultPattern();
const engine = createEngine();
const created = await createPiece(engine);
const first = await engine.invokeBuiltinTool("assign_slug", {
token: created.resultRef,
slug: "doubling-report",
});
expect((first.output as AssignSlugToolSuccessOutput).status).toBe("ok");
const again = await engine.invokeBuiltinTool("assign_slug", {
token: created.resultRef,
slug: "doubling-report",
});
const output = again.output as AssignSlugToolSuccessOutput;
expect(output.status).toBe("ok");
expect(output.slug).toBe("doubling-report");
});
it("registers an unlisted piece whose slug already points at it", async () => {
// A slug can point at a piece the registry does not list — a
// pre-existing name, or a naming interrupted between its two steps.
// The idempotent answer still delivers the contract's other half.
await linkDefaultPattern();
const engine = createEngine();
const created = await createPiece(engine);
const cell = pieces.runtime.getCellFromLink(
// The resultRef names the piece cell itself.
parseLLMFriendlyLink(created.resultRef, pieces.getSpace()),
);
await cell.sync();
await assignSlug(pieces, cell, "doubling-report");
expect(await pieces.getRegisteredPieces()).toEqual([]);
const result = await engine.invokeBuiltinTool("assign_slug", {
token: created.resultRef,
slug: "doubling-report",
});
const output = result.output as AssignSlugToolSuccessOutput;
expect(output.status).toBe("ok");
const registered = await pieces.getRegisteredPieces();
expect(registered.map((piece) => piece.id)).toEqual([created.pieceId]);
});
it("initializes the space when an already-named piece has no registry to join", async () => {
const engine = createEngine();
const created = await createPiece(engine);
const cell = pieces.runtime.getCellFromLink(
parseLLMFriendlyLink(created.resultRef, pieces.getSpace()),
);
await cell.sync();
await assignSlug(pieces, cell, "doubling-report");
const result = await engine.invokeBuiltinTool("assign_slug", {
token: created.resultRef,
slug: "doubling-report",
});
const output = result.output as AssignSlugToolSuccessOutput;
expect(output.status).toBe("ok");
expect(await pieces.getDefaultPattern(false)).toBeDefined();
const registered = await pieces.getRegisteredPieces();
expect(registered.map((piece) => piece.id)).toEqual([created.pieceId]);
});
it("reports an initialization failure while listing an already-named piece", async () => {
const engine = createEngine();
const created = await createPiece(engine);
const cell = pieces.runtime.getCellFromLink(
parseLLMFriendlyLink(created.resultRef, pieces.getSpace()),
);
await cell.sync();
await assignSlug(pieces, cell, "doubling-report");
const originalEnsureDefaultPattern = pieces.ensureDefaultPattern;
pieces.ensureDefaultPattern = () =>
Promise.reject(new Error("space root unavailable"));
const result = await engine.invokeBuiltinTool("assign_slug", {
token: created.resultRef,
slug: "doubling-report",
});
pieces.ensureDefaultPattern = originalEnsureDefaultPattern;
const output = result.output as AssignSlugToolErrorOutput;
expect(output.status).toBe("error");
expect(output.message).toContain("failed while listing");
expect(output.message).toContain("space root unavailable");
});
it("answers ok on the bare word when the race it lost was to a call naming this same piece", async () => {
// Two calls for one piece under one word, one landing between the
// other's read and write. The loser's write is refused underneath, and
// what refused it is its own piece, so advancing to a counter would
// give the piece a second name it never asked for. The refused name is
// read again instead, and the read says the request is already true.
// The registry join sits between the read and the write, which is
// where the winning call is staged.
await linkDefaultPattern();
const engine = createEngine();
const created = await createPiece(engine);
const cell = pieces.runtime.getCellFromLink(
parseLLMFriendlyLink(created.resultRef, pieces.getSpace()),
);
await cell.sync();
const originalAdd = pieces.add.bind(pieces);
pieces.add = async (cells) => {
await originalAdd(cells);
await setSlugLink(pieces, "doubling-report", cell);
};
const result = await engine.invokeBuiltinTool("assign_slug", {
token: created.resultRef,
slug: "doubling-report",
});
pieces.add = originalAdd;
const output = result.output as AssignSlugToolSuccessOutput;
expect(output.status).toBe("ok");
expect(output.slug).toBe("doubling-report");
await expect(resolvePieceAddress(pieces, "doubling-report-2")).rejects
.toThrow();
});
it("reports a name released between the availability read and the write as one to retry", async () => {
// The tool judges a name free, and the name comes to point nowhere
// before the write lands. That is not the same outcome as a name
// somebody else took: nobody holds it, so the answer is to read it
// again rather than to move on to a counter. The registry join sits
// between the two, which is where the release is staged.
await linkDefaultPattern();
const engine = createEngine();
const created = await createPiece(engine);
// A name pointing at a plain document: free by this tool's rule, and a
// binding the claim carries, so the release below is a change to it.
const plain = pieces.runtime.getCell(
pieces.getSpace(),
{ space: pieces.getSpace(), random: "released-target" },
);
await pieces.runtime.editWithRetry((tx) => {
plain.withTx(tx).set({ value: 1 });
});
await setSlugLink(pieces, "doubling-report", plain);
const slugCell = pieces.runtime.getCellFromEntityId(
pieces.getSpace(),
entityIdFrom(slugIdForSpace(pieces.getSpace(), "doubling-report")),
);
const originalAdd = pieces.add.bind(pieces);
pieces.add = async (cells) => {
await originalAdd(cells);
// The join has landed and the availability answer is already read;
// the name now points at nothing.
await pieces.runtime.editWithRetry((tx) => {
slugCell.withTx(tx).setRawUntyped("not a redirect");
});
};
const result = await engine.invokeBuiltinTool("assign_slug", {
token: created.resultRef,
slug: "doubling-report",
});
pieces.add = originalAdd;
const output = result.output as AssignSlugToolErrorOutput;
expect(output.status).toBe("error");
expect(output.message).toContain("doubling-report");
expect(output.message).toContain("names nothing");
expect(output.message).toContain("Try the same call again");
// The registry join landed before the refusal, so the answer says the
// piece is listed rather than that nothing happened.
expect(output.message).toContain("the piece is listed");
expect(output.message).not.toContain("Nothing was assigned");
});
it("does not list the piece twice when retried after a failed assignment", async () => {
// A first call can join the registry and then fail at the slug write.
// The retry must settle the name without appending a second entry.
await linkDefaultPattern();
const engine = createEngine();
const created = await createPiece(engine);
const originalAdd = pieces.add.bind(pieces);
const originalGetCell = runtime.getCellFromEntityId.bind(runtime);
pieces.add = async (cells) => {
await originalAdd(cells);
// The join has landed; make the assignment that follows fail once.
// Through a call only the slug write makes, so the failure is the
// assignment's rather than the listing's — the tool reports those
// separately, because only one of them leaves a listed piece.
runtime.getCellFromEntityId = (() => {
runtime.getCellFromEntityId = originalGetCell;
throw new Error("slug assignment refused");
}) as Runtime["getCellFromEntityId"];
};
const first = await engine.invokeBuiltinTool("assign_slug", {
token: created.resultRef,
slug: "doubling-report",
});
pieces.add = originalAdd;
runtime.getCellFromEntityId = originalGetCell;
const failed = first.output as AssignSlugToolErrorOutput;
expect(failed.status).toBe("error");
expect(failed.message).toContain("failed while naming");
const retry = await engine.invokeBuiltinTool("assign_slug", {
token: created.resultRef,
slug: "doubling-report",
});
expect((retry.output as AssignSlugToolSuccessOutput).status).toBe("ok");
const registered = await pieces.getRegisteredPieces();
expect(registered.map((piece) => piece.id)).toEqual([created.pieceId]);
expect(await resolvePieceAddress(pieces, "doubling-report")).toBe(
created.pieceId,
);
});
it("reports a structured error when the referenced piece cannot be loaded", async () => {
// A storage or connection failure while loading the referent is the
// tool's answer to give, not an exception to escape with.
await linkDefaultPattern();
const engine = createEngine();
const created = await createPiece(engine);
const originalGetCellFromLink = pieces.runtime.getCellFromLink.bind(
pieces.runtime,
);
pieces.runtime.getCellFromLink = ((
...args: Parameters
) => {
const cell = patchableCell(originalGetCellFromLink(...args));
(cell as unknown as { sync: () => Promise }).sync = () =>
Promise.reject(new Error("storage unavailable"));
return cell;
// deno-lint-ignore no-explicit-any
}) as any;
const result = await engine.invokeBuiltinTool("assign_slug", {
token: created.resultRef,
slug: "doubling-report",
});
pieces.runtime.getCellFromLink = originalGetCellFromLink;
const output = result.output as AssignSlugToolErrorOutput;
expect(output.status).toBe("error");
expect(output.message).toContain("could not load the referenced piece");
expect(output.message).toContain("storage unavailable");
});
it("refuses when the slug's availability could not be established, assigning nothing", async () => {
// A resolution that fails operationally — storage error, sync that
// never landed — says nothing about what the slug holds. Reading it as
// vacancy would send the call on to take a name this side never
// established was free, which is what the availability check exists to
// prevent.
await linkDefaultPattern();
const engine = createEngine();
const created = await createPiece(engine);
// The slug document's own cell, so only its sync fails and every other
// cell the call reaches behaves normally.
const slugEntity = String(
entityIdFrom(slugIdForSpace(pieces.getSpace(), "doubling-report")),
);
const originalGetCell = runtime.getCellFromEntityId.bind(runtime);
let syncFailures = 0;
runtime.getCellFromEntityId = ((
...args: Parameters
) => {
const cell = patchableCell(originalGetCell(...args));
if (String(args[1]) !== slugEntity) {
return cell;
}
(cell as unknown as { sync: () => Promise }).sync = () => {
syncFailures += 1;
return Promise.reject(new Error("storage unavailable"));
};
return cell;
}) as Runtime["getCellFromEntityId"];
const result = await engine.invokeBuiltinTool("assign_slug", {
token: created.resultRef,
slug: "doubling-report",
});
runtime.getCellFromEntityId = originalGetCell;
// The injected failure really was the slug resolution's, so the
// refusal below answers it rather than something else going wrong.
expect(syncFailures).toBe(1);
const output = result.output as AssignSlugToolErrorOutput;
expect(output.status).toBe("error");
expect(output.message).toContain("could not establish");
expect(output.message).toContain("doubling-report");
expect(output.message).not.toContain("already names another piece");
expect(await pieces.getRegisteredPieces()).toEqual([]);
});
it("says the piece is listed when a name passed over after the join leaves the next one unanswerable", async () => {
// The registry join has committed, the bare word is then found held
// by another piece at the write, and the counter's first candidate
// cannot be read at all. The refusal reports what the call left: a
// listed piece with no name, not "nothing was assigned" — a caller
// told the latter would never look for the piece it listed.
await linkDefaultPattern();
const engine = createEngine();
const created = await createPiece(engine, 21);
const other = await createPiece(engine, 22);
const otherCell = pieces.runtime.getCellFromLink(
parseLLMFriendlyLink(other.resultRef, pieces.getSpace()),
);
await otherCell.sync();
// Only the counter's slug document fails to sync; the bare word and
// everything else the call reaches behave normally.
const counterEntity = String(
entityIdFrom(slugIdForSpace(pieces.getSpace(), "doubling-report-2")),
);
const originalGetCell = runtime.getCellFromEntityId.bind(runtime);
runtime.getCellFromEntityId = ((
...args: Parameters
) => {
const cell = patchableCell(originalGetCell(...args));
if (String(args[1]) !== counterEntity) {
return cell;
}
(cell as unknown as { sync: () => Promise }).sync = () =>
Promise.reject(new Error("storage unavailable"));
return cell;
}) as Runtime["getCellFromEntityId"];
const originalAdd = pieces.add.bind(pieces);
pieces.add = async (cells) => {
await originalAdd(cells);
// The join has landed; the bare word now belongs to another piece.
await setSlugLink(pieces, "doubling-report", otherCell);
};
const result = await engine.invokeBuiltinTool("assign_slug", {
token: created.resultRef,
slug: "doubling-report",
});
pieces.add = originalAdd;
runtime.getCellFromEntityId = originalGetCell;
const output = result.output as AssignSlugToolErrorOutput;
expect(output.status).toBe("error");
expect(output.message).toContain("could not establish");
expect(output.message).toContain("doubling-report-2");
expect(output.message).toContain("the piece is listed");
expect(output.message).not.toContain("Nothing was assigned");
expect(await resolvePieceAddress(pieces, "doubling-report")).toBe(
other.pieceId,
);
const registered = await pieces.getRegisteredPieces();
expect(registered.map((piece) => piece.id)).toEqual([created.pieceId]);
});
it("refuses a taken word too long for any counter to fit, naming nothing", async () => {
// The one collision that is refused: the requested word already sits
// at the slug length limit, so no counter can be appended and the tool
// has no free name to derive. Saying so beats inventing a shorter one.
await linkDefaultPattern();
const engine = createEngine();
const first = await createPiece(engine, 21);
const second = await createPiece(engine, 22);
const longest = "a".repeat(80);
const held = await engine.invokeBuiltinTool("assign_slug", {
token: first.resultRef,
slug: longest,
});
expect((held.output as AssignSlugToolSuccessOutput).slug).toBe(longest);
const result = await engine.invokeBuiltinTool("assign_slug", {
token: second.resultRef,
slug: longest,
});
const output = result.output as AssignSlugToolErrorOutput;
expect(output.status).toBe("error");
expect(output.message).toContain("no free slug can be derived");
expect(await resolvePieceAddress(pieces, longest)).toBe(first.pieceId);
const registered = await pieces.getRegisteredPieces();
expect(registered.map((piece) => piece.id)).toEqual([first.pieceId]);
});
it("returns a URL naming the space's DID when the session's space is configured by DID", async () => {
const spaceIdentity = await signer.derive(
`assign-slug-did-${crypto.randomUUID()}`,
);
const didPieces = new PiecesController(
createSession({
identity: signer,
spaceDid: spaceIdentity.did(),
}),
runtime,
);
await didPieces.synced();
expect(didPieces.getSpaceName()).toBeUndefined();
const defaultRoot = await didPieces.create(DEFAULT_PATTERN_SOURCE, {
input: { pieceRegistry: [] },
});
await didPieces.linkDefaultPattern(defaultRoot.getCell());
await runtime.idle();
await didPieces.synced();
const engine = new CfHarnessEngine({
sandboxRuntime: new FakeSandboxRuntime(),
runId: `assign-slug-test-${crypto.randomUUID()}`,
fabricSessionFactory: () => Promise.resolve({ pieces: didPieces }),
});
const created = await engine.invokeBuiltinTool("run_pattern", {
sourceText: DOUBLING_PATTERN_SOURCE,
inputs: { n: 21 },
});
const piece = created.output as RunPatternToolSuccessOutput;
expect(piece.status).toBe("ok");
const result = await engine.invokeBuiltinTool("assign_slug", {
token: piece.resultRef,
slug: "doubling-report",
});
const output = result.output as AssignSlugToolSuccessOutput;
expect(output.status).toBe("ok");
expect(output.slug).toBe("doubling-report");
expect(output.url).toBe(
`http://toolshed.test/${spaceIdentity.did()}/doubling-report`,
);
expect(output.space).toBe(spaceIdentity.did());
expect(await resolvePieceAddress(didPieces, "doubling-report")).toBe(
piece.pieceId,
);
});
it("returns an error for an unusable slug, assigning nothing", async () => {
await linkDefaultPattern();
const engine = createEngine();
const created = await createPiece(engine);
const result = await engine.invokeBuiltinTool("assign_slug", {
token: created.resultRef,
slug: "Not A Slug!",
});
const output = result.output as AssignSlugToolErrorOutput;
expect(output.status).toBe("error");
expect(output.message).toContain("slug is invalid");
expect(await pieces.getRegisteredPieces()).toEqual([]);
});
it("returns an error when the slug is not a string", async () => {
await linkDefaultPattern();
const engine = createEngine();
const created = await createPiece(engine);
const result = await engine.invokeBuiltinTool("assign_slug", {
token: created.resultRef,
slug: 7 as unknown as string,
});
const output = result.output as AssignSlugToolErrorOutput;
expect(output.status).toBe("error");
expect(output.message).toContain("string slug");
});
it("refuses a token naming a position inside a piece rather than the piece itself", async () => {
await linkDefaultPattern();
const engine = createEngine();
const created = await createPiece(engine);
const result = await engine.invokeBuiltinTool("assign_slug", {
token: `${created.resultRef}/doubled`,
slug: "doubling-report",
});
const output = result.output as AssignSlugToolErrorOutput;
expect(output.status).toBe("error");
expect(output.message).toContain("not a position inside one");
expect(await pieces.getRegisteredPieces()).toEqual([]);
});
it("refuses a token whose document is not a piece", async () => {
await linkDefaultPattern();
const engine = createEngine();
// A document that exists but carries no pattern identity: an ordinary
// data cell is not a piece and naming it would put a dead entry in the
// piece list.
const cell = runtime.getCell>(
pieces.getSpace(),
`assign-slug-plain-${crypto.randomUUID()}`,
);
const { error } = await runtime.editWithRetry((tx) => {
cell.withTx(tx).set({ n: 1 });
});
expect(error).toBeUndefined();
const link = cell.getAsNormalizedFullLink();
const result = await engine.invokeBuiltinTool("assign_slug", {
token: `/${link.id}`,
slug: "doubling-report",
});
const output = result.output as AssignSlugToolErrorOutput;
expect(output.status).toBe("error");
expect(output.message).toContain("does not refer to a piece");
expect(await pieces.getRegisteredPieces()).toEqual([]);
});
it("refuses a token naming a reference in another space", async () => {
await linkDefaultPattern();
const engine = createEngine();
const created = await createPiece(engine);
const otherSpace =
"did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
const bare = created.resultRef.replace(/^\//, "");
const result = await engine.invokeBuiltinTool("assign_slug", {
token: `/@${otherSpace}/${bare}`,
slug: "doubling-report",
});
const output = result.output as AssignSlugToolErrorOutput;
expect(output.status).toBe("error");
expect(output.message).toContain("own space");
});
it("returns an error when the run has no fabric session", async () => {
const engine = new CfHarnessEngine({
sandboxRuntime: new FakeSandboxRuntime(),
runId: `assign-slug-test-${crypto.randomUUID()}`,
});
const result = await engine.invokeBuiltinTool("assign_slug", {
token: "/of:fid1:abc",
slug: "doubling-report",
});
const output = result.output as AssignSlugToolErrorOutput;
expect(output.status).toBe("error");
expect(output.message).toContain("requires a fabric session");
});
it("returns an error for a missing token", async () => {
await linkDefaultPattern();
const engine = createEngine();
const result = await engine.invokeBuiltinTool("assign_slug", {
slug: "doubling-report",
});
const output = result.output as AssignSlugToolErrorOutput;
expect(output.status).toBe("error");
expect(output.message).toContain("requires a token");
});
it("initializes a fresh space before registering and naming the piece", async () => {
const engine = createEngine();
const created = await createPiece(engine);
expect(await pieces.getDefaultPattern(false)).toBeUndefined();
const result = await engine.invokeBuiltinTool("assign_slug", {
token: created.resultRef,
slug: "doubling-report",
});
const output = result.output as AssignSlugToolSuccessOutput;
expect(output.status).toBe("ok");
expect(await pieces.getDefaultPattern(false)).toBeDefined();
const registered = await pieces.getRegisteredPieces();
expect(registered.map((piece) => piece.id)).toEqual([created.pieceId]);
expect(await resolvePieceAddress(pieces, "doubling-report")).toBe(
created.pieceId,
);
});
it("reports the listing failure when the space root cannot be initialized, claiming no listing", async () => {
// The registry join is where this fails, so it is the listing that is
// reported and the piece is not listed. The refusals on the other side
// of the join say the piece IS listed, and a report that conflated the
// two would send a caller looking for a piece that is not there.
const engine = createEngine();
const created = await createPiece(engine);
const originalEnsureDefaultPattern = pieces.ensureDefaultPattern;
pieces.ensureDefaultPattern = () =>
Promise.reject(new Error("space root unavailable"));
const result = await engine.invokeBuiltinTool("assign_slug", {
token: created.resultRef,
slug: "doubling-report",
});
pieces.ensureDefaultPattern = originalEnsureDefaultPattern;
const output = result.output as AssignSlugToolErrorOutput;
expect(output.status).toBe("error");
expect(output.message).toContain("failed while listing");
expect(output.message).toContain("space root unavailable");
expect(output.message).not.toContain("the piece is listed");
expect(await pieces.getRegisteredPieces()).toEqual([]);
});
it("leaves an initialized space root untouched while assigning the slug", async () => {
await linkDefaultPattern();
const existingRoot = await pieces.getDefaultPattern(false);
expect(existingRoot).toBeDefined();
const existingRootId = getEntityId(existingRoot!);
const originalEnsureDefaultPattern = pieces.ensureDefaultPattern.bind(
pieces,
);
const originalEditWithRetry = runtime.editWithRetry.bind(runtime);
let ensureCalls = 0;
let rootWriteAttempts = 0;
pieces.ensureDefaultPattern = async () => {
ensureCalls++;
runtime.editWithRetry = ((fn, maxRetries) => {
rootWriteAttempts++;
return originalEditWithRetry(fn, maxRetries);
}) as typeof runtime.editWithRetry;
try {
return await originalEnsureDefaultPattern();
} finally {
runtime.editWithRetry = originalEditWithRetry;
}
};
const engine = createEngine();
const created = await createPiece(engine);
const result = await engine.invokeBuiltinTool("assign_slug", {
token: created.resultRef,
slug: "doubling-report",
});
pieces.ensureDefaultPattern = originalEnsureDefaultPattern;
const output = result.output as AssignSlugToolSuccessOutput;
expect(output.status).toBe("ok");
expect(ensureCalls).toBe(1);
expect(rootWriteAttempts).toBe(0);
const rootAfterAssignment = await pieces.getDefaultPattern(false);
expect(getEntityId(rootAfterAssignment!)).toEqual(existingRootId);
});
it("surfaces a rejected session construction as a structured error", async () => {
const engine = new CfHarnessEngine({
sandboxRuntime: new FakeSandboxRuntime(),
runId: `assign-slug-test-${crypto.randomUUID()}`,
fabricSessionFactory: () =>
Promise.reject(new Error("authorization denied")),
});
const result = await engine.invokeBuiltinTool("assign_slug", {
token: "/of:fid1:abc",
slug: "doubling-report",
});
const output = result.output as AssignSlugToolErrorOutput;
expect(output.status).toBe("error");
expect(output.message).toContain(
"could not establish the fabric session",
);
expect(output.message).toContain("authorization denied");
});
it("returns an error for a token that does not parse as a reference", async () => {
await linkDefaultPattern();
const engine = createEngine();
const result = await engine.invokeBuiltinTool("assign_slug", {
token: "not a reference at all",
slug: "doubling-report",
});
const output = result.output as AssignSlugToolErrorOutput;
expect(output.status).toBe("error");
expect(output.message).toContain("does not name a reference");
});
});
});