chore: sync the frontend template from 4dc033c to 8157ad4
The product was materialized from the template at `4dc033c` and has stayed on it through 43 template commits, so it was missing all three rounds of adapter remediation — including files it never had, such as the shared `abortable-operation` primitive and the `exact-snapshot` decoder that later fixes are written against. Taking only the newest round was not possible for that reason: the delta is coherent only as a whole. The product had not touched `src/adapters` at all since materialization, so the 140-file delta applied with a three-way merge and no conflicts. `package.json` was the single overlap and merged cleanly: the product owns `name`, the template contributed `check:adapter-inventory`, `check:remediation-ledger` and the image-resolve-signal type fixture. All 24 product-owned files — README, index.html, CI workflow, i18n catalog, home page, generated schemas, evidence scripts, component and visual snapshots — are byte-identical to `main`. `template.lock.json` now pins the synced revision and tree. Verified in this repository, not inherited from the template: six type projects, lint, nine gates (adapter inventory, remediation ledger, registries, diagnostics, realtime boundaries, architecture, browser file/storage boundaries, optional recipes, documentation), the production build, and 2,054 of 2,073 tests. The 19 failures are all in `tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template records; four suites that failed once under parallel load pass in isolation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
002ba3624e
commit
4bff9ca151
@@ -214,6 +214,47 @@ describe("live/poll authoritative writer handoff", () => {
|
||||
void second;
|
||||
});
|
||||
|
||||
it("tracks a retired active writer after handoff queue overflow", async () => {
|
||||
let release: ((result: RealtimeResult<void>) => void) | undefined;
|
||||
const harness = createHarness({
|
||||
limits: {
|
||||
...limits,
|
||||
maxActiveQueueCount: 2,
|
||||
maxActiveQueueBytes: 10,
|
||||
},
|
||||
apply: vi.fn(async ({ value }) => {
|
||||
if (value === "first") {
|
||||
return await new Promise<RealtimeResult<void>>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
}
|
||||
return realtimeSuccess(undefined);
|
||||
}),
|
||||
});
|
||||
const writer = harness.coordinator.currentWriter()!;
|
||||
const first = writer.write("first", 5);
|
||||
const second = writer.write("second", 5);
|
||||
await flush();
|
||||
await expect(writer.write("overflow", 1)).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: { kind: "QUEUE_OVERFLOW" },
|
||||
});
|
||||
|
||||
// R-03. The fail-close dropped the active reference, but the writer is
|
||||
// still running, so close() must not claim quiescence.
|
||||
const closing = harness.coordinator.close();
|
||||
await flush();
|
||||
harness.clock.advance(100);
|
||||
await expect(closing).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: { kind: "IDLE_TIMEOUT", operation: "CLOSE" },
|
||||
});
|
||||
|
||||
release?.(realtimeSuccess(undefined));
|
||||
void first;
|
||||
void second;
|
||||
});
|
||||
|
||||
it("fences and aborts live, waits for quiescence, then recovers before activating poll", async () => {
|
||||
const liveEffect = deferred<RealtimeResult<void>>();
|
||||
const checkpoint = deferred<RealtimeResult<void>>();
|
||||
@@ -486,12 +527,87 @@ describe("live/poll authoritative writer handoff", () => {
|
||||
ok: false,
|
||||
error: { kind: "IDLE_TIMEOUT", operation: "CLOSE" },
|
||||
});
|
||||
await expect(harness.coordinator.close()).resolves.toMatchObject({
|
||||
// RT-RR-03. A second close re-runs rather than replaying a cached verdict.
|
||||
// The writer is still hung, so it still reports a timeout.
|
||||
const second = harness.coordinator.close();
|
||||
await flush();
|
||||
harness.clock.advance(100);
|
||||
await expect(second).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: { kind: "IDLE_TIMEOUT", operation: "CLOSE" },
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* RT-RR-03. Caching the first timeout forever meant a writer that later
|
||||
* settled could never be proved quiescent: every subsequent close replayed
|
||||
* the stale failure and the retained registry could never be pruned.
|
||||
*/
|
||||
it("converges to success once a retired writer finally settles", async () => {
|
||||
let release: ((value: RealtimeResult<void>) => void) | undefined;
|
||||
const harness = createHarness({
|
||||
apply: vi.fn(
|
||||
async () =>
|
||||
await new Promise<RealtimeResult<void>>((resolve) => {
|
||||
release = resolve;
|
||||
}),
|
||||
),
|
||||
});
|
||||
const live = harness.coordinator.currentWriter()!;
|
||||
void live.write("late-settle", 8);
|
||||
await flush();
|
||||
|
||||
const first = harness.coordinator.close();
|
||||
await flush();
|
||||
harness.clock.advance(100);
|
||||
await expect(first).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: { kind: "IDLE_TIMEOUT", operation: "CLOSE" },
|
||||
});
|
||||
|
||||
// The writer finishes after the first close gave up.
|
||||
release?.(realtimeSuccess(undefined));
|
||||
await flush();
|
||||
await flush();
|
||||
|
||||
const second = harness.coordinator.close();
|
||||
await flush();
|
||||
harness.clock.advance(100);
|
||||
await expect(second).resolves.toMatchObject({ ok: true });
|
||||
});
|
||||
|
||||
/**
|
||||
* RT-RR-04. Checkpoint work is an external authority call like a writer
|
||||
* tail. Racing it against a timeout bounded the public wait but left it out
|
||||
* of the retained registry, so `close()` could report quiescence while the
|
||||
* checkpoint was still running.
|
||||
*/
|
||||
it("does not report quiescence while a checkpoint is still running", async () => {
|
||||
let checkpointSignal: AbortSignal | undefined;
|
||||
const harness = createHarness({
|
||||
recover: vi.fn(async ({ signal }) => {
|
||||
checkpointSignal = signal;
|
||||
return await new Promise<RealtimeResult<void>>(() => {});
|
||||
}),
|
||||
});
|
||||
|
||||
const transition = harness.coordinator.switchToPoll();
|
||||
await flush();
|
||||
expect(checkpointSignal).toBeDefined();
|
||||
|
||||
const closing = harness.coordinator.close();
|
||||
await flush();
|
||||
harness.clock.advance(100);
|
||||
await expect(closing).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: { kind: "IDLE_TIMEOUT", operation: "CLOSE" },
|
||||
});
|
||||
|
||||
harness.clock.advance(100);
|
||||
await flush();
|
||||
await transition;
|
||||
});
|
||||
|
||||
it("rejects invalid initial authority and resource ceilings", () => {
|
||||
expect(() =>
|
||||
createLivePollHandoffCoordinator({
|
||||
|
||||
@@ -29,6 +29,91 @@ import {
|
||||
} from "./fixture.ts";
|
||||
|
||||
describe("transport-independent realtime stream coordinator", () => {
|
||||
it("keeps the stream DRAINING until a non-cooperative effect settles", async () => {
|
||||
const wedged = deferred<RealtimeResult<void>>();
|
||||
const harness = createHarness({
|
||||
apply: async () => wedged.promise,
|
||||
taskLimits: { effectTimeoutMs: 5, drainTimeoutMs: 5 },
|
||||
});
|
||||
await harness.initialize();
|
||||
|
||||
const applied = await harness.coordinator.accept(
|
||||
harness.event({
|
||||
eventId: "event-00000001",
|
||||
sequence: "1",
|
||||
resumeCursor: "cursor-00000001",
|
||||
}),
|
||||
);
|
||||
// Bounded for the caller, and explicitly non-retryable.
|
||||
expect(applied).toMatchObject({
|
||||
ok: false,
|
||||
error: { kind: "IDLE_TIMEOUT", operation: "APPLY", retryable: false },
|
||||
});
|
||||
expect(harness.coordinator.lifecycle(STREAM_ID)).toBe("DRAINING");
|
||||
|
||||
// DRAINING refuses new admission rather than queueing behind the wedge.
|
||||
await expect(
|
||||
harness.coordinator.accept(
|
||||
harness.event({
|
||||
eventId: "event-00000002",
|
||||
sequence: "2",
|
||||
resumeCursor: "cursor-00000002",
|
||||
}),
|
||||
),
|
||||
).resolves.toMatchObject({ ok: false, error: { kind: "CLOSED" } });
|
||||
|
||||
// close() cannot claim quiescence while the task is still running.
|
||||
await expect(harness.coordinator.close()).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: { kind: "IDLE_TIMEOUT", operation: "CLOSE" },
|
||||
});
|
||||
|
||||
// Only actual settlement ends DRAINING.
|
||||
wedged.resolve(realtimeSuccess(undefined));
|
||||
await expect(harness.coordinator.close()).resolves.toMatchObject({
|
||||
ok: true,
|
||||
});
|
||||
expect(harness.coordinator.lifecycle(STREAM_ID)).toBe("CLOSED");
|
||||
});
|
||||
|
||||
it("bounds non-cooperative recovery and rejects its late checkpoint", async () => {
|
||||
const wedged = deferred<RealtimeResult<RealtimeRecoveryCommit>>();
|
||||
let recoveries = 0;
|
||||
const harness = createHarness({
|
||||
recover: async () => {
|
||||
recoveries += 1;
|
||||
return recoveries === 1
|
||||
? realtimeSuccess(snapshotCommit("0"))
|
||||
: wedged.promise;
|
||||
},
|
||||
taskLimits: { recoveryTimeoutMs: 5, drainTimeoutMs: 5 },
|
||||
});
|
||||
await harness.initialize();
|
||||
|
||||
const recovered = await harness.coordinator.recover(
|
||||
STREAM_ID,
|
||||
"SEQUENCE_GAP",
|
||||
);
|
||||
expect(recovered).toMatchObject({
|
||||
ok: false,
|
||||
error: { kind: "IDLE_TIMEOUT", operation: "RECOVER", retryable: false },
|
||||
});
|
||||
expect(harness.coordinator.lifecycle(STREAM_ID)).toBe("DRAINING");
|
||||
|
||||
const beforeLateCommit = harness.coordinator.getResumeState(STREAM_ID);
|
||||
|
||||
// A late checkpoint from the abandoned attempt cannot commit.
|
||||
wedged.resolve(realtimeSuccess(snapshotCommit("9")));
|
||||
for (let flush = 0; flush < 10; flush += 1) await Promise.resolve();
|
||||
expect(harness.coordinator.getResumeState(STREAM_ID)).toEqual(
|
||||
beforeLateCommit,
|
||||
);
|
||||
// Settlement returns the stream to OPEN, marked STALE for an authoritative
|
||||
// recovery rather than silently trusting the abandoned attempt.
|
||||
expect(harness.coordinator.lifecycle(STREAM_ID)).toBe("OPEN");
|
||||
expect(harness.coordinator.inspect(STREAM_ID).freshness).toBe("STALE");
|
||||
});
|
||||
|
||||
it("applies one stream sequentially and commits each cursor after its effect", async () => {
|
||||
const first = deferred<RealtimeResult<void>>();
|
||||
const applied: string[] = [];
|
||||
@@ -858,6 +943,118 @@ describe("transport-independent realtime stream coordinator", () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* RT-RR-01 / RT-RR-02. A physical effect exists from the moment the coordinator
|
||||
* calls the authority, not from the moment its public wait expires. Registering
|
||||
* only on timeout let a `close()` that arrived first see an empty retained set
|
||||
* and report quiescence while the raw task was still running against the
|
||||
* authority.
|
||||
*/
|
||||
describe("realtime physical task ownership", () => {
|
||||
it("does not report quiescence while an apply is still running", async () => {
|
||||
const applyGate = deferred<RealtimeResult<void>>();
|
||||
const harness = createHarness({
|
||||
apply: async () => await applyGate.promise,
|
||||
taskLimits: { effectTimeoutMs: 10_000, drainTimeoutMs: 20 },
|
||||
});
|
||||
await harness.initialize();
|
||||
|
||||
const applying = harness.coordinator.accept(harness.event());
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
// close() arrives long before the effect deadline.
|
||||
const closed = await harness.coordinator.close();
|
||||
expect(closed.ok).toBe(false);
|
||||
expect(closed.ok ? null : closed.error.kind).toBe("IDLE_TIMEOUT");
|
||||
expect(harness.coordinator.lifecycle(STREAM_ID)).toBe("DRAINING");
|
||||
|
||||
applyGate.resolve(realtimeSuccess(undefined));
|
||||
await applying;
|
||||
});
|
||||
|
||||
it("reports quiescence once the raw task settles", async () => {
|
||||
const applyGate = deferred<RealtimeResult<void>>();
|
||||
const harness = createHarness({
|
||||
apply: async () => await applyGate.promise,
|
||||
taskLimits: { effectTimeoutMs: 10_000, drainTimeoutMs: 200 },
|
||||
});
|
||||
await harness.initialize();
|
||||
|
||||
const applying = harness.coordinator.accept(harness.event());
|
||||
await Promise.resolve();
|
||||
const closing = harness.coordinator.close();
|
||||
applyGate.resolve(realtimeSuccess(undefined));
|
||||
await applying;
|
||||
|
||||
expect(await closing).toMatchObject({ ok: true });
|
||||
});
|
||||
|
||||
it("does not start a queued event once the stream is DRAINING", async () => {
|
||||
const firstApply = deferred<RealtimeResult<void>>();
|
||||
let applyCalls = 0;
|
||||
const harness = createHarness({
|
||||
apply: async () => {
|
||||
applyCalls += 1;
|
||||
if (applyCalls === 1) return await firstApply.promise;
|
||||
return realtimeSuccess(undefined);
|
||||
},
|
||||
taskLimits: { effectTimeoutMs: 15, drainTimeoutMs: 50 },
|
||||
});
|
||||
await harness.initialize();
|
||||
|
||||
const first = harness.coordinator.accept(
|
||||
harness.event({ eventId: "event-0001", sequence: "1" }),
|
||||
);
|
||||
// Queued behind the first while it is still inside its deadline.
|
||||
const second = harness.coordinator.accept(
|
||||
harness.event({ eventId: "event-0002", sequence: "2" }),
|
||||
);
|
||||
|
||||
await first;
|
||||
expect(harness.coordinator.lifecycle(STREAM_ID)).toBe("DRAINING");
|
||||
|
||||
// The queued event is dropped at execution time, not applied and not
|
||||
// recovered: admission happened before DRAINING, execution happens inside
|
||||
// it, and the second decision is the one that counts.
|
||||
const secondResult = await second;
|
||||
expect(secondResult).toMatchObject({
|
||||
ok: true,
|
||||
value: { outcome: "DROPPED", reason: "CLOSED" },
|
||||
});
|
||||
expect(applyCalls).toBe(1);
|
||||
|
||||
firstApply.resolve(realtimeSuccess(undefined));
|
||||
});
|
||||
|
||||
it("discards the resume token when an effect is abandoned", async () => {
|
||||
const firstApply = deferred<RealtimeResult<void>>();
|
||||
let applyCalls = 0;
|
||||
const harness = createHarness({
|
||||
apply: async () => {
|
||||
applyCalls += 1;
|
||||
if (applyCalls === 1) return await firstApply.promise;
|
||||
return realtimeSuccess(undefined);
|
||||
},
|
||||
taskLimits: { effectTimeoutMs: 15, drainTimeoutMs: 50 },
|
||||
});
|
||||
await harness.initialize();
|
||||
expect(harness.coordinator.inspect(STREAM_ID).hasResumeState).toBe(true);
|
||||
|
||||
await harness.coordinator.accept(
|
||||
harness.event({ eventId: "event-0001", sequence: "1" }),
|
||||
);
|
||||
|
||||
// The abandoned effect may have applied part of its change, so the token it
|
||||
// was based on is no longer authoritative evidence.
|
||||
const inspection = harness.coordinator.inspect(STREAM_ID);
|
||||
expect(inspection.hasResumeState).toBe(false);
|
||||
expect(inspection.freshness).toBe("UNKNOWN");
|
||||
|
||||
firstApply.resolve(realtimeSuccess(undefined));
|
||||
});
|
||||
});
|
||||
|
||||
type HarnessOptions = Readonly<{
|
||||
registry?: RealtimePolicyRegistry;
|
||||
mappers?: typeof TEST_MAPPERS | Readonly<Record<string, typeof TEST_MAPPER>>;
|
||||
@@ -866,6 +1063,13 @@ type HarnessOptions = Readonly<{
|
||||
request: RealtimeRecoveryRequest,
|
||||
) => Promise<RealtimeResult<RealtimeRecoveryCommit>>;
|
||||
observe?: (observation: RealtimeObservation) => void;
|
||||
taskLimits?: Readonly<{
|
||||
effectTimeoutMs?: number;
|
||||
recoveryTimeoutMs?: number;
|
||||
drainTimeoutMs?: number;
|
||||
}>;
|
||||
scheduleTimeout?: (callback: () => void, delayMs: number) => unknown;
|
||||
clearScheduledTimeout?: (handle: unknown) => void;
|
||||
}>;
|
||||
|
||||
function createHarness(options: HarnessOptions = {}) {
|
||||
@@ -911,6 +1115,13 @@ function createHarness(options: HarnessOptions = {}) {
|
||||
},
|
||||
now: () => 10_000,
|
||||
observe: options.observe,
|
||||
...(options.taskLimits ? { taskLimits: options.taskLimits } : {}),
|
||||
...(options.scheduleTimeout
|
||||
? { scheduleTimeout: options.scheduleTimeout }
|
||||
: {}),
|
||||
...(options.clearScheduledTimeout
|
||||
? { clearScheduledTimeout: options.clearScheduledTimeout }
|
||||
: {}),
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -973,3 +1184,168 @@ function deferred<Value>() {
|
||||
});
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
/**
|
||||
* RT-01. The registry entry has to exist before the collaborator is called.
|
||||
* Registering after the invocation returned left a window in which an authority
|
||||
* that re-entered `close()` from inside its own callback saw an empty set, so
|
||||
* `close()` reported quiescence while its effect was still running.
|
||||
*/
|
||||
describe("RT-01 reentrant close cannot pass the registration window", () => {
|
||||
it("refuses quiescence when apply re-enters close during its invocation", async () => {
|
||||
const applyGate = deferred<RealtimeResult<void>>();
|
||||
let closeResult: RealtimeResult<void> | undefined;
|
||||
let harness: ReturnType<typeof createHarness> | undefined;
|
||||
harness = createHarness({
|
||||
apply: () => {
|
||||
// Re-entered from inside the invocation itself.
|
||||
void harness!.coordinator.close().then((result) => {
|
||||
closeResult = result;
|
||||
});
|
||||
return applyGate.promise;
|
||||
},
|
||||
taskLimits: { effectTimeoutMs: 10_000, drainTimeoutMs: 20 },
|
||||
});
|
||||
await harness.initialize();
|
||||
|
||||
const applying = harness.coordinator.accept(harness.event());
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(closeResult?.ok).toBe(false);
|
||||
expect(closeResult?.ok ? null : closeResult?.error.kind).toBe(
|
||||
"IDLE_TIMEOUT",
|
||||
);
|
||||
|
||||
applyGate.resolve(realtimeSuccess(undefined));
|
||||
await applying;
|
||||
// Once the raw task settled, a second close does converge.
|
||||
await expect(harness.coordinator.close()).resolves.toMatchObject({
|
||||
ok: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses quiescence when recovery re-enters close during its invocation", async () => {
|
||||
const recoveryGate = deferred<RealtimeResult<RealtimeRecoveryCommit>>();
|
||||
let closeResult: RealtimeResult<void> | undefined;
|
||||
let harness: ReturnType<typeof createHarness> | undefined;
|
||||
let recoveries = 0;
|
||||
harness = createHarness({
|
||||
recover: () => {
|
||||
recoveries += 1;
|
||||
if (recoveries === 1) {
|
||||
return Promise.resolve(realtimeSuccess(snapshotCommit("0")));
|
||||
}
|
||||
void harness!.coordinator.close().then((result) => {
|
||||
closeResult = result;
|
||||
});
|
||||
return recoveryGate.promise;
|
||||
},
|
||||
taskLimits: { recoveryTimeoutMs: 10_000, drainTimeoutMs: 20 },
|
||||
});
|
||||
await harness.initialize();
|
||||
|
||||
const recovering = harness.coordinator.recover(STREAM_ID, "SEQUENCE_GAP");
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(closeResult?.ok).toBe(false);
|
||||
expect(closeResult?.ok ? null : closeResult?.error.kind).toBe(
|
||||
"IDLE_TIMEOUT",
|
||||
);
|
||||
|
||||
recoveryGate.resolve(realtimeSuccess(snapshotCommit("0")));
|
||||
await recovering;
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* RT-02. A scheduler that cannot install a deadline leaves the wait unbounded.
|
||||
* Letting the exception escape turned a typed realtime result into a native
|
||||
* rejection and, through the caller's own catch, started a recovery that
|
||||
* overlapped the effect still running.
|
||||
*/
|
||||
describe("RT-02 a throwing scheduler stays inside the result contract", () => {
|
||||
/** Installs deadlines normally until the stream is initialized. */
|
||||
function breakableScheduler() {
|
||||
const state = { broken: false };
|
||||
return {
|
||||
state,
|
||||
scheduleTimeout: (callback: () => void, delayMs: number) => {
|
||||
if (state.broken) throw new TypeError("scheduleTimeout exploded");
|
||||
return setTimeout(callback, delayMs);
|
||||
},
|
||||
clearScheduledTimeout: (handle: unknown) => {
|
||||
clearTimeout(handle as ReturnType<typeof setTimeout>);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
it("does not start a recovery that overlaps a pending apply", async () => {
|
||||
const applyGate = deferred<RealtimeResult<void>>();
|
||||
const scheduler = breakableScheduler();
|
||||
let recoveriesAfterApply = 0;
|
||||
const harness = createHarness({
|
||||
apply: () => applyGate.promise,
|
||||
recover: async () => {
|
||||
recoveriesAfterApply += 1;
|
||||
return realtimeSuccess(snapshotCommit("0"));
|
||||
},
|
||||
taskLimits: { effectTimeoutMs: 10_000, drainTimeoutMs: 20 },
|
||||
scheduleTimeout: scheduler.scheduleTimeout,
|
||||
clearScheduledTimeout: scheduler.clearScheduledTimeout,
|
||||
});
|
||||
await harness.initialize();
|
||||
recoveriesAfterApply = 0;
|
||||
scheduler.state.broken = true;
|
||||
|
||||
const accepted = await harness.coordinator.accept(harness.event());
|
||||
|
||||
// The apply is still pending, so no recovery may have run beside it.
|
||||
expect(recoveriesAfterApply).toBe(0);
|
||||
expect(accepted.ok).toBe(false);
|
||||
expect(harness.coordinator.lifecycle(STREAM_ID)).toBe("DRAINING");
|
||||
|
||||
applyGate.resolve(realtimeSuccess(undefined));
|
||||
});
|
||||
|
||||
it("returns a typed close failure instead of rejecting natively", async () => {
|
||||
const applyGate = deferred<RealtimeResult<void>>();
|
||||
const scheduler = breakableScheduler();
|
||||
const harness = createHarness({
|
||||
apply: () => applyGate.promise,
|
||||
taskLimits: { effectTimeoutMs: 10_000, drainTimeoutMs: 20 },
|
||||
scheduleTimeout: scheduler.scheduleTimeout,
|
||||
clearScheduledTimeout: scheduler.clearScheduledTimeout,
|
||||
});
|
||||
await harness.initialize();
|
||||
scheduler.state.broken = true;
|
||||
|
||||
const applying = harness.coordinator.accept(harness.event());
|
||||
// Let the apply reach the authority before the fence arrives.
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
const closed = await harness.coordinator.close();
|
||||
|
||||
// A drain that could not be bounded is not proof of quiescence.
|
||||
expect(closed.ok).toBe(false);
|
||||
expect(closed.ok ? null : closed.error.kind).toBe("IDLE_TIMEOUT");
|
||||
|
||||
applyGate.resolve(realtimeSuccess(undefined));
|
||||
await applying;
|
||||
});
|
||||
|
||||
it("keeps the classified outcome when clearing a timer throws", async () => {
|
||||
const harness = createHarness({
|
||||
clearScheduledTimeout: () => {
|
||||
throw new TypeError("clearScheduledTimeout exploded");
|
||||
},
|
||||
});
|
||||
await harness.initialize();
|
||||
|
||||
await expect(
|
||||
harness.coordinator.accept(harness.event()),
|
||||
).resolves.toMatchObject({ ok: true });
|
||||
await expect(harness.coordinator.close()).resolves.toMatchObject({
|
||||
ok: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
REALTIME_WEBSOCKET_PROTOCOL,
|
||||
@@ -25,6 +25,43 @@ function encode(value: unknown): string {
|
||||
}
|
||||
|
||||
describe("realtime WebSocket protocol", () => {
|
||||
|
||||
it("rejects oversized text before allocating a full UTF-8 copy", () => {
|
||||
const encoderSpy = vi.spyOn(TextEncoder.prototype, "encode");
|
||||
try {
|
||||
const oversize = "a".repeat(64);
|
||||
expect(decodeWebSocketServerFrame(oversize, 8)).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "FRAME_TOO_LARGE" },
|
||||
});
|
||||
expect(encoderSpy).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
encoderSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("counts multibyte and lone-surrogate bytes like TextEncoder", () => {
|
||||
const samples = [
|
||||
"abc",
|
||||
"\u00e9\u00e9",
|
||||
"\u20ac\u20ac",
|
||||
"\u{1f600}",
|
||||
"a\ud800b",
|
||||
"\udc00",
|
||||
];
|
||||
for (const sample of samples) {
|
||||
const expected = new TextEncoder().encode(sample).byteLength;
|
||||
// At the exact budget the frame is admitted; one byte less rejects it.
|
||||
expect(
|
||||
decodeWebSocketServerFrame(sample, expected).ok ||
|
||||
decodeWebSocketServerFrame(sample, expected),
|
||||
).toBeTruthy();
|
||||
expect(decodeWebSocketServerFrame(sample, expected - 1)).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "FRAME_TOO_LARGE" },
|
||||
});
|
||||
}
|
||||
});
|
||||
it("decodes and freezes an exact WELCOME frame", () => {
|
||||
const result = decodeWebSocketServerFrame(
|
||||
encode({
|
||||
|
||||
Reference in New Issue
Block a user