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
687 lines
22 KiB
TypeScript
687 lines
22 KiB
TypeScript
// Regression tests for Sol's phase 1a review (findings #1-#11) and the requested race tests.
|
|
import assert from "node:assert/strict";
|
|
import { Type } from "typebox";
|
|
import { Value } from "typebox/value";
|
|
import { describe, it } from "vitest";
|
|
import {
|
|
type Ctx,
|
|
createHost,
|
|
defineRegistry,
|
|
type Facet,
|
|
hostNamed,
|
|
isChordError,
|
|
local,
|
|
memoryLinkPair,
|
|
method,
|
|
type Ref,
|
|
remote,
|
|
} from "../src/index.ts";
|
|
import { fits } from "../src/schema.ts";
|
|
import { CountingLink } from "./fixtures/counting.ts";
|
|
|
|
const tick = () => new Promise<void>((resolve) => setImmediate(resolve));
|
|
const ticks = async (n = 10) => {
|
|
for (let i = 0; i < n; i++) await tick();
|
|
};
|
|
|
|
function latch() {
|
|
let open = () => {};
|
|
const promise = new Promise<void>((resolve) => {
|
|
open = resolve;
|
|
});
|
|
return { promise, open };
|
|
}
|
|
|
|
function emitter<E>(options: { failSubscribe?: boolean } = {}) {
|
|
const listeners = new Set<(event: E) => void>();
|
|
return {
|
|
emit: (event: E) => {
|
|
for (const fn of [...listeners]) fn(event);
|
|
},
|
|
subscribe: (fn: (event: E) => void) => {
|
|
if (options.failSubscribe) throw new Error("subscribe fails");
|
|
listeners.add(fn);
|
|
return () => listeners.delete(fn);
|
|
},
|
|
get size() {
|
|
return listeners.size;
|
|
},
|
|
};
|
|
}
|
|
|
|
const Item = local("t.item", {
|
|
version: 1,
|
|
input: Type.Object({ id: Type.String() }),
|
|
members: { version: method(undefined, Type.String()) },
|
|
});
|
|
|
|
describe("review: reload commit", () => {
|
|
it("#1 a failing listener subscription fails prepare; the code version and all objects stay unchanged", async () => {
|
|
const host = createHost({ self: { kind: "host" }, onError: () => {} });
|
|
const sources = { a: emitter<number>(), b: emitter<number>({ failSubscribe: true }), c: emitter<number>() };
|
|
const facet = (version: string, listen: boolean): Facet => ({
|
|
name: "item",
|
|
setup: (api) =>
|
|
api.serve(Item, ({ id }, _principal, self) => {
|
|
if (listen) self.on("events", sources[id as "a"], () => {});
|
|
return { version: () => version };
|
|
}),
|
|
});
|
|
host.install(facet("v1", false));
|
|
const refs = ["a", "b", "c"].map((id) => host.hold(Item, { id }));
|
|
await Promise.all(refs.map((ref) => ref.ready));
|
|
const codeVersion = host.codeVersion;
|
|
await assert.rejects(host.reload([facet("v2", true)]), /subscribe fails/);
|
|
assert.equal(host.codeVersion, codeVersion);
|
|
assert.deepEqual(await Promise.all(refs.map((ref) => ref.version())), ["v1", "v1", "v1"]);
|
|
assert.equal(sources.a.size, 0);
|
|
assert.equal(sources.c.size, 0);
|
|
});
|
|
|
|
it("#2 migration is validated against the new remote schemas, not the obsolete ones (dev mode)", async () => {
|
|
const host = createHost({ self: { kind: "host" }, dev: true, onError: () => {} });
|
|
const V1 = remote("t.counted", {
|
|
version: 1,
|
|
input: Type.Object({}),
|
|
host: hostNamed("host"),
|
|
variants: { main: { state: { count: Type.Object({ n: Type.Number() }) } } },
|
|
});
|
|
const V2 = remote("t.counted", {
|
|
version: 2,
|
|
input: Type.Object({}),
|
|
host: hostNamed("host"),
|
|
variants: { main: { state: { count: Type.Object({ total: Type.Number() }) } } },
|
|
});
|
|
host.install({
|
|
name: "counted",
|
|
setup: (api) =>
|
|
api.serve(
|
|
V1,
|
|
(_input, _principal, self) => ({
|
|
variant: "main",
|
|
count: self.state("count", Type.Object({ n: Type.Number() }), { n: 3 }),
|
|
}),
|
|
{ linger: 60_000 },
|
|
),
|
|
});
|
|
const first = host.hold(V1, {});
|
|
await first.ready;
|
|
first.release();
|
|
await host.reload([
|
|
{
|
|
name: "counted",
|
|
setup: (api) =>
|
|
api.serve(
|
|
V2,
|
|
(_input, _principal, self) => ({
|
|
variant: "main",
|
|
count: self.state(
|
|
"count",
|
|
Type.Object({ total: Type.Number() }),
|
|
{ total: 0 },
|
|
{ version: 2, migrate: (old) => ({ total: (old as { n: number }).n }) },
|
|
),
|
|
}),
|
|
{ linger: 60_000 },
|
|
),
|
|
},
|
|
]);
|
|
const second = host.hold(V2, {});
|
|
const values: unknown[] = [];
|
|
second.count.subscribe((value) => values.push(value));
|
|
await second.ready;
|
|
await ticks();
|
|
assert.deepEqual(second.count.value, { total: 3 });
|
|
});
|
|
|
|
it("#1 a commit that still fails midway fails the host terminally (no half-published code version serving work)", async () => {
|
|
const host = createHost({ self: { kind: "host" }, onError: () => {} });
|
|
host.install({ name: "item", setup: (api) => api.serve(Item, () => ({ version: () => "v1" })) });
|
|
const ref = host.hold(Item, { id: "a" });
|
|
await ref.ready;
|
|
// Everything fallible runs in prepare now, so break an install step directly (white box).
|
|
const objects = (host as unknown as { objects: Map<string, { startTasks(): void }> }).objects;
|
|
for (const object of objects.values()) {
|
|
object.startTasks = () => {
|
|
throw new Error("install step fails");
|
|
};
|
|
}
|
|
await assert.rejects(
|
|
host.reload([{ name: "item", setup: (api) => api.serve(Item, () => ({ version: () => "v2" })) }]),
|
|
/install step fails/,
|
|
);
|
|
await assert.rejects(
|
|
ref.version(),
|
|
(error) => isChordError(error, "unavailable") && error.reason === "host_failed",
|
|
);
|
|
});
|
|
|
|
it("reentrant observers calling another object during a commit see the whole new code version", async () => {
|
|
const host = createHost({ self: { kind: "host" }, onError: () => {} });
|
|
const Other = local("t.other", {
|
|
version: 1,
|
|
input: Type.Object({}),
|
|
members: { version: method(undefined, Type.String()) },
|
|
});
|
|
const facet = (version: string): Facet => ({
|
|
name: "pair",
|
|
setup: (api) => {
|
|
api.serve(Item, () => ({ version: () => version }));
|
|
api.serve(Other, () => ({ version: () => version }));
|
|
},
|
|
});
|
|
host.install(facet("v1"));
|
|
const item = host.hold(Item, { id: "a" });
|
|
const other = host.hold(Other, {});
|
|
await Promise.all([item.ready, other.ready]);
|
|
const seen: Promise<string>[] = [];
|
|
item.onShape(() => seen.push(other.version()));
|
|
await host.reload([facet("v2")]);
|
|
assert.deepEqual(await Promise.all(seen), ["v2"]);
|
|
});
|
|
});
|
|
|
|
describe("review: leases, invocations, activation", () => {
|
|
const Store = local("t.store", {
|
|
version: 1,
|
|
input: Type.Object({}),
|
|
members: { count: Type.Unknown() },
|
|
});
|
|
|
|
it("#4 a running listener holds a lease: the gate waits for it before migrating", async () => {
|
|
const host = createHost({ self: { kind: "host" }, onError: () => {} });
|
|
const events = emitter<number>();
|
|
const go = latch();
|
|
const seen: unknown[] = [];
|
|
host.install({
|
|
name: "store",
|
|
setup: (api) =>
|
|
api.serve(Store, (_input, _principal, self) => {
|
|
const count = self.state("count", Type.Object({ n: Type.Number() }), { n: 1 });
|
|
self.on("slow", events, async () => {
|
|
await go.promise;
|
|
seen.push(count.value);
|
|
});
|
|
return { count };
|
|
}),
|
|
});
|
|
const ref = host.hold(Store, {});
|
|
await ref.ready;
|
|
events.emit(1);
|
|
await tick();
|
|
const codeVersion = host.codeVersion;
|
|
const reload = host.reload(
|
|
[
|
|
{
|
|
name: "store",
|
|
setup: (api) =>
|
|
api.serve(Store, (_input, _principal, self) => ({
|
|
count: self.state(
|
|
"count",
|
|
Type.Object({ total: Type.Number() }),
|
|
{ total: 0 },
|
|
{ version: 2, migrate: (old) => ({ total: (old as { n: number }).n }) },
|
|
),
|
|
})),
|
|
},
|
|
],
|
|
{ gateDeadline: 2000 },
|
|
);
|
|
await ticks(20);
|
|
assert.equal(host.codeVersion, codeVersion, "the gate must wait for the listener");
|
|
go.open();
|
|
await reload;
|
|
assert.deepEqual(seen, [{ n: 1 }]);
|
|
assert.deepEqual(ref.count.value, { total: 1 });
|
|
});
|
|
|
|
it("#5 disposal waits for running methods before closing resources (released and forced)", async () => {
|
|
const Worker = local("t.busy", {
|
|
version: 1,
|
|
input: Type.Object({ id: Type.String() }),
|
|
members: { work: method(undefined, Type.Boolean()) },
|
|
});
|
|
const log: string[] = [];
|
|
let go = latch();
|
|
const host = createHost({ self: { kind: "host" }, onError: () => {} });
|
|
host.serve(Worker, ({ id }, _principal, self) => {
|
|
const connResource = self.resource("conn", {
|
|
version: 1,
|
|
open: () => ({ closed: false }),
|
|
close: (conn) => {
|
|
conn.closed = true;
|
|
log.push(`close ${id}`);
|
|
},
|
|
});
|
|
return {
|
|
work: async () => {
|
|
await go.promise;
|
|
log.push(`done ${id}`);
|
|
return connResource.value.closed;
|
|
},
|
|
};
|
|
});
|
|
const released = host.hold(Worker, { id: "r" });
|
|
const call = released.work();
|
|
await ticks();
|
|
released.release();
|
|
await ticks();
|
|
go.open();
|
|
assert.equal(await call, false);
|
|
await ticks();
|
|
assert.deepEqual(log, ["done r", "close r"]);
|
|
|
|
go = latch();
|
|
log.length = 0;
|
|
const forced = host.hold(Worker, { id: "f" }, { principal: { user: "u" } });
|
|
const forcedCall = forced.work();
|
|
await ticks();
|
|
const invalidated = host.invalidatePrincipal((principal) => (principal as { user?: string }).user === "u");
|
|
await ticks();
|
|
assert.equal(forced.status, "gone");
|
|
assert.deepEqual(log, []);
|
|
go.open();
|
|
await invalidated;
|
|
assert.equal(await forcedCall, false);
|
|
assert.deepEqual(log, ["done f", "close f"]);
|
|
});
|
|
|
|
it("#6 a registry conflict found while finalizing activation unwinds the loser completely", async () => {
|
|
const errors: unknown[] = [];
|
|
const host = createHost({ self: { kind: "host" }, onError: (error) => errors.push(error) });
|
|
const Routes = defineRegistry<string>("t.review-routes");
|
|
const log: string[] = [];
|
|
const types = ["t.eager-a", "t.eager-b"].map((name) => local(name, { version: 1, input: Type.Object({}) }));
|
|
for (const type of types) {
|
|
host.serve(
|
|
type,
|
|
(_input, _principal, self) => {
|
|
self.resource("r", {
|
|
version: 1,
|
|
open: () => void log.push(`open ${type.name}`),
|
|
close: () => void log.push(`close ${type.name}`),
|
|
});
|
|
self.register(Routes, { same: type.name });
|
|
return {};
|
|
},
|
|
{ lifetime: "host" },
|
|
);
|
|
}
|
|
await ticks(20);
|
|
assert.equal(log.filter((entry) => entry.startsWith("open")).length, 2);
|
|
assert.equal(log.filter((entry) => entry.startsWith("close")).length, 1);
|
|
assert.equal(host.registry(Routes).table().size, 1);
|
|
assert.ok(errors.some((error) => isChordError(error, "invalid") && error.reason === "registry_conflict"));
|
|
});
|
|
|
|
it("#6 releasing the last hold while listeners start up does not resurrect the object", async () => {
|
|
const host = createHost({ self: { kind: "host" }, onError: () => {} });
|
|
const log: string[] = [];
|
|
let ref: Ref<typeof Store> | undefined;
|
|
let runs = 0;
|
|
host.serve(Store, (_input, _principal, self) => {
|
|
runs++;
|
|
const count = self.state("count", Type.Number(), 0);
|
|
self.resource("r", {
|
|
version: 1,
|
|
open: () => void log.push("open"),
|
|
close: () => void log.push("close"),
|
|
});
|
|
// A readable delivers its current value as the listener starts: release the only hold right then.
|
|
self.on("drop", count, () => ref?.release());
|
|
return { count };
|
|
});
|
|
ref = host.hold(Store, {});
|
|
await ref.ready.catch(() => {});
|
|
await ticks(20);
|
|
assert.deepEqual(log, ["open", "close"]);
|
|
const again = host.hold(Store, {});
|
|
ref = undefined;
|
|
await again.ready;
|
|
assert.equal(runs, 2);
|
|
});
|
|
});
|
|
|
|
describe("review: prepare isolation and contracts", () => {
|
|
const Db = local("t.db", {
|
|
version: 1,
|
|
input: Type.Object({}),
|
|
members: { read: method(undefined, Type.Unknown()) },
|
|
});
|
|
|
|
it("#7 a prepare rerun cannot touch retained resources, even open ones", async () => {
|
|
const host = createHost({ self: { kind: "host" }, onError: () => {} });
|
|
const native = { mutated: false };
|
|
const facet = (touch: boolean): Facet => ({
|
|
name: "db",
|
|
setup: (api) =>
|
|
api.serve(Db, (_input, _principal, self) => {
|
|
const db = self.resource("db", { version: 1, open: () => native });
|
|
if (touch) db.value.mutated = true;
|
|
return { read: () => db.value };
|
|
}),
|
|
});
|
|
host.install(facet(false));
|
|
const ref = host.hold(Db, {});
|
|
await ref.ready;
|
|
await assert.rejects(host.reload([facet(true)]), (error) => isChordError(error, "not_ready"));
|
|
assert.equal(native.mutated, false);
|
|
assert.deepEqual(await ref.read(), { mutated: false });
|
|
});
|
|
|
|
it("#7 objects of types a reload adds start listeners and tasks only at commit", async () => {
|
|
const host = createHost({ self: { kind: "host" }, onError: () => {} });
|
|
const log: string[] = [];
|
|
const Fresh = local("t.fresh", { version: 1, input: Type.Object({}) });
|
|
host.install({ name: "item", setup: (api) => api.serve(Item, () => ({ version: () => "v1" })) });
|
|
await Promise.all(["a", "b"].map((id) => host.hold(Item, { id }).ready));
|
|
const plugins = (fail: boolean): Facet[] => [
|
|
{
|
|
name: "fresh",
|
|
setup: (api) =>
|
|
api.serve(Fresh, (_input, _principal, self) => {
|
|
self.task(async () => {
|
|
log.push("task started");
|
|
});
|
|
return {};
|
|
}),
|
|
},
|
|
{
|
|
name: "item",
|
|
setup: (api) =>
|
|
api.serve(Item, ({ id }, _principal, self) => {
|
|
self.get(Fresh, {});
|
|
if (fail && id === "b") throw new Error("b fails");
|
|
return { version: () => "v2" };
|
|
}),
|
|
},
|
|
];
|
|
await assert.rejects(host.reload(plugins(true)), /b fails/);
|
|
await ticks();
|
|
assert.deepEqual(log, []);
|
|
await host.reload(plugins(false));
|
|
await ticks();
|
|
assert.deepEqual(log, ["task started"]);
|
|
});
|
|
|
|
it("#8 a breaking provider change and its updated consumer reload in one batch", async () => {
|
|
const host = createHost({ self: { kind: "host" }, onError: () => {} });
|
|
const P1 = remote("t.provider", {
|
|
version: 1,
|
|
input: Type.Object({}),
|
|
host: hostNamed("host"),
|
|
variants: { main: { methods: { get: method(undefined, Type.String()) } } },
|
|
});
|
|
const P2 = remote("t.provider", {
|
|
version: 2,
|
|
input: Type.Object({}),
|
|
host: hostNamed("host"),
|
|
variants: { main: { methods: { fetch: method(undefined, Type.String()) } } },
|
|
});
|
|
const Consumer = local("t.consumer", {
|
|
version: 1,
|
|
input: Type.Object({}),
|
|
members: { ask: method(undefined, Type.String()) },
|
|
});
|
|
host.install({
|
|
name: "both",
|
|
setup: (api) => {
|
|
api.serve(P1, () => ({ variant: "main", get: () => "old" }));
|
|
api.serve(Consumer, (_input, _principal, self) => {
|
|
const provider = self.get(P1, {});
|
|
return { ask: (ctx: Ctx) => provider.get(ctx) };
|
|
});
|
|
},
|
|
});
|
|
const consumer = host.hold(Consumer, {});
|
|
assert.equal(await consumer.ask(), "old");
|
|
await host.reload([
|
|
{
|
|
name: "both",
|
|
setup: (api) => {
|
|
api.serve(P2, () => ({ variant: "main", fetch: () => "new" }));
|
|
api.serve(Consumer, (_input, _principal, self) => {
|
|
const provider = self.get(P2, {});
|
|
return { ask: (ctx: Ctx) => provider.fetch(ctx) };
|
|
});
|
|
},
|
|
},
|
|
]);
|
|
assert.equal(await consumer.ask(), "new");
|
|
});
|
|
|
|
it("#9 an exclusive resource whose close failed is restored on rollback, or marked unavailable", async () => {
|
|
for (const reopen of ["works", "fails"] as const) {
|
|
const host = createHost({ self: { kind: "host" }, onError: () => {} });
|
|
const log: string[] = [];
|
|
let opens = 0;
|
|
const facet = (rev: number): Facet => ({
|
|
name: "db",
|
|
setup: (api) =>
|
|
api.serve(Db, (_input, _principal, self) => {
|
|
const port = self.resource("port", {
|
|
version: rev,
|
|
exclusive: true,
|
|
open: () => {
|
|
if (rev === 2) throw new Error("new port fails");
|
|
if (++opens > 1 && reopen === "fails") throw new Error("reopen fails");
|
|
log.push(`open ${rev}`);
|
|
return { rev };
|
|
},
|
|
close: () => {
|
|
log.push(`close ${rev}`);
|
|
throw new Error("close fails");
|
|
},
|
|
});
|
|
return { read: () => port.value };
|
|
}),
|
|
});
|
|
host.install(facet(1));
|
|
const ref = host.hold(Db, {});
|
|
await ref.ready;
|
|
// The failing close aborts the reload at the gate; the old resource must not stay closed.
|
|
await assert.rejects(host.reload([facet(2)]), /close fails/);
|
|
if (reopen === "works") {
|
|
assert.deepEqual(log, ["open 1", "close 1", "open 1"]);
|
|
assert.deepEqual(await ref.read(), { rev: 1 });
|
|
} else {
|
|
await assert.rejects(ref.read(), (error) => isChordError(error, "unavailable"));
|
|
}
|
|
}
|
|
});
|
|
|
|
it("#10 retirement closes dropped resources before releasing dropped holds", async () => {
|
|
const host = createHost({ self: { kind: "host" }, onError: () => {} });
|
|
const log: string[] = [];
|
|
const Dep = local("t.dep", { version: 1, input: Type.Object({}) });
|
|
const facet = (withDecls: boolean): Facet => ({
|
|
name: "item",
|
|
setup: (api) => {
|
|
api.serve(Dep, (_input, _principal, self) => {
|
|
self.resource("r", { version: 1, open: () => {}, close: () => void log.push("dep closed") });
|
|
return {};
|
|
});
|
|
api.serve(Item, (_input, _principal, self) => {
|
|
if (withDecls) {
|
|
self.get(Dep, {});
|
|
// Slow close: releasing the hold first would let the dependency close before it finishes.
|
|
self.resource("own", {
|
|
version: 1,
|
|
open: () => {},
|
|
close: async () => {
|
|
await ticks(3);
|
|
log.push("own closed");
|
|
},
|
|
});
|
|
}
|
|
return { version: () => (withDecls ? "v1" : "v2") };
|
|
});
|
|
},
|
|
});
|
|
host.install(facet(true));
|
|
const ref = host.hold(Item, { id: "a" });
|
|
await ref.ready;
|
|
const go = latch();
|
|
const pinned = host.root(async (ctx) => {
|
|
await ref.version(ctx);
|
|
await go.promise;
|
|
});
|
|
await tick();
|
|
await host.reload([facet(false)]);
|
|
assert.deepEqual(log, []);
|
|
go.open();
|
|
await pinned;
|
|
await ticks(20);
|
|
assert.deepEqual(log, ["own closed", "dep closed"]);
|
|
});
|
|
|
|
it("#11 fits has no false positives for unmodelled keywords; counterexamples checked with Value.Check", () => {
|
|
const cases = [
|
|
[Type.String(), Type.String({ not: { const: "x" } }), "x"],
|
|
[Type.Array(Type.Number()), Type.Array(Type.Number(), { contains: Type.Literal(1) }), [2]],
|
|
[
|
|
Type.Object({ a: Type.String() }),
|
|
Type.Object({ a: Type.String() }, { additionalProperties: Type.Number() }),
|
|
{ a: "s", b: "t" },
|
|
],
|
|
[
|
|
Type.Object({ x: Type.String() }),
|
|
Type.Object({}, { patternProperties: { "^x$": Type.Number() } }),
|
|
{ x: "s" },
|
|
],
|
|
[Type.Number(), Type.Intersect([Type.Number(), Type.Number({ minimum: 1 })]), 0],
|
|
[
|
|
Type.Object({ a: Type.String() }, { additionalProperties: Type.Number() }),
|
|
Type.Object({ a: Type.String(), b: Type.Optional(Type.String()) }),
|
|
{ a: "", b: 1 },
|
|
],
|
|
] as const;
|
|
for (const [sub, sup, value] of cases) {
|
|
assert.ok(Value.Check(sub, value) && !Value.Check(sup, value), `bad counterexample ${JSON.stringify(value)}`);
|
|
assert.equal(fits(sub, sup), false, `fits(${JSON.stringify(sub)}, ${JSON.stringify(sup)})`);
|
|
}
|
|
// Additive changes still fit.
|
|
assert.ok(
|
|
fits(Type.Object({ a: Type.String() }), Type.Object({ a: Type.String(), b: Type.Optional(Type.Number()) })),
|
|
);
|
|
assert.ok(fits(Type.Object({ a: Type.String(), extra: Type.Number() }), Type.Object({ a: Type.String() })));
|
|
});
|
|
|
|
it("#11 arguments are stripped to what the consumer knows, then to what the provider knows", async () => {
|
|
const host = createHost({ self: { kind: "host" }, onError: () => {} });
|
|
const Strict = remote("t.strict", {
|
|
version: 1,
|
|
input: Type.Object({}),
|
|
host: hostNamed("host"),
|
|
variants: {
|
|
main: {
|
|
methods: {
|
|
echo: method(Type.Object({ a: Type.String() }, { additionalProperties: false }), Type.Unknown()),
|
|
},
|
|
},
|
|
},
|
|
});
|
|
host.serve(Strict, () => ({ variant: "main", echo: (args) => args }));
|
|
const ref = host.hold(Strict, {});
|
|
assert.deepEqual(await ref.echo({ a: "x", junk: 1 } as never), { a: "x" });
|
|
});
|
|
});
|
|
|
|
describe("review: ctx races", () => {
|
|
const Target = local("t.target", {
|
|
version: 1,
|
|
input: Type.Object({}),
|
|
members: { hit: method() },
|
|
});
|
|
|
|
it("an aborted task ctx cannot start fresh calls", async () => {
|
|
const host = createHost({ self: { kind: "host" }, onError: () => {} });
|
|
const Runner = local("t.runner", { version: 1, input: Type.Object({}) });
|
|
let hits = 0;
|
|
const go = latch();
|
|
let outcome: unknown;
|
|
host.serve(Target, () => ({
|
|
hit: () => {
|
|
hits++;
|
|
},
|
|
}));
|
|
host.serve(Runner, (_input, _principal, self) => {
|
|
const target = self.get(Target, {});
|
|
self.task(async (ctx) => {
|
|
await go.promise;
|
|
outcome = await target.hit(ctx).catch((error: unknown) => error);
|
|
});
|
|
return {};
|
|
});
|
|
const runner = host.hold(Runner, {});
|
|
await runner.ready;
|
|
const target = host.hold(Target, {});
|
|
await target.ready;
|
|
runner.release();
|
|
await ticks();
|
|
go.open();
|
|
await ticks(20);
|
|
assert.equal(hits, 0);
|
|
assert.ok(isChordError(outcome, "aborted"));
|
|
});
|
|
|
|
it("an expired ctx and a foreign ctx are refused", async () => {
|
|
const host = createHost({ self: { kind: "host" }, onError: () => {} });
|
|
const other = createHost({ self: { kind: "host" }, onError: () => {} });
|
|
host.serve(Target, () => ({ hit: () => {} }));
|
|
const target = host.hold(Target, {});
|
|
await target.ready;
|
|
const expired = await host.root((ctx) => ctx);
|
|
await assert.rejects(
|
|
target.hit(expired),
|
|
(error) => isChordError(error, "aborted") && error.reason === "expired",
|
|
);
|
|
const foreign = await other.root(async (ctx) => {
|
|
await assert.rejects(
|
|
target.hit(ctx),
|
|
(error) => isChordError(error, "invalid") && error.reason === "foreign_ctx",
|
|
);
|
|
return true;
|
|
});
|
|
assert.ok(foreign);
|
|
});
|
|
|
|
// #10959
|
|
it("a remote reference refuses a foreign ctx and a ctx not made by Chord, without calling", async () => {
|
|
const RemoteTarget = remote("t.remote-target", {
|
|
version: 1,
|
|
input: Type.Object({}),
|
|
host: hostNamed("server"),
|
|
variants: { main: { state: {}, methods: { hit: method() } } },
|
|
});
|
|
let hits = 0;
|
|
const server = createHost({ self: { kind: "host" }, name: "server", onError: () => {} });
|
|
server.serve(RemoteTarget, () => ({
|
|
variant: "main",
|
|
hit: () => {
|
|
hits++;
|
|
},
|
|
}));
|
|
const [down, up] = memoryLinkPair();
|
|
server.accept(down, { kind: "host", name: "worker", principal: { worker: true } });
|
|
const link = new CountingLink(up);
|
|
const worker = createHost({ self: { kind: "host" }, name: "worker", onError: () => {} });
|
|
worker.connect(link, { principal: { kind: "server" } });
|
|
const other = createHost({ self: { kind: "host" }, name: "other", onError: () => {} });
|
|
const target = worker.hold(RemoteTarget, {});
|
|
await target.ready;
|
|
const foreign = await other.root((ctx) => target.hit(ctx).catch((error: unknown) => error));
|
|
assert.ok(isChordError(foreign, "invalid") && foreign.reason === "foreign_ctx", String(foreign));
|
|
const forged = { rootId: "forged", codeVersion: 1, signal: new AbortController().signal } as unknown as Ctx;
|
|
await assert.rejects(
|
|
target.hit(forged),
|
|
(error) => isChordError(error, "invalid") && error.reason === "foreign_ctx",
|
|
);
|
|
await ticks();
|
|
assert.equal(hits, 0);
|
|
assert.equal(link.count("sent", "call"), 0);
|
|
await worker.root((ctx) => target.hit(ctx));
|
|
assert.equal(hits, 1);
|
|
});
|
|
});
|