///
/**
* The SES pattern compartment endows no timers (part of the structural barrier
* pinned by security-timing.test.ts). Code that wants a delay in both a host
* context and a compartment must therefore guard its timer use: read
* `globalThis.setTimeout` (a member access that yields undefined in-sandbox,
* not a ReferenceError) and no-op when it is absent, so the wait degrades to an
* immediate return rather than throwing. These tests pin that the compartment
* omits the timers, that a RAW `setTimeout(...)` call throws inside it, and
* that the guard resolves immediately there.
*/
import { describe, it } from "@std/testing/bdd";
import { expect } from "@std/expect";
import {
ensureSESLockdown,
evaluateFunctionSourceInSES,
} from "../src/sandbox/ses-runtime.ts";
import { createModuleCompartmentGlobals } from "../src/sandbox/compartment-globals.ts";
// A backoff guarded by a member-access read of `setTimeout`, which yields
// `undefined` in a compartment rather than raising a ReferenceError.
const GUARDED_BACKOFF_SRC = `async function () {
const sleep = (ms) => {
if (ms <= 0) return Promise.resolve();
const timer = globalThis.setTimeout;
if (typeof timer !== "function") return Promise.resolve();
return new Promise((resolve) => { timer(resolve, ms); });
};
// A 30s backoff. In-sandbox this must resolve immediately (no timer), not throw.
await sleep(30000);
return "backoff-complete";
}`;
// The un-guarded form: a raw setTimeout call.
const RAW_BACKOFF_SRC = `async function () {
await new Promise((resolve) => setTimeout(resolve, 30000));
return "backoff-complete";
}`;
describe("sandbox timers (channel: no fine clock)", () => {
it("the module compartment endows no setTimeout/setInterval", () => {
ensureSESLockdown();
const globals = createModuleCompartmentGlobals();
expect("setTimeout" in globals).toBe(false);
expect("setInterval" in globals).toBe(false);
expect("queueMicrotask" in globals).toBe(false);
expect("requestAnimationFrame" in globals).toBe(false);
});
it("a raw setTimeout call throws inside the compartment (the hazard)", async () => {
ensureSESLockdown();
const globals = createModuleCompartmentGlobals();
const fn = evaluateFunctionSourceInSES(RAW_BACKOFF_SRC, {
lockdown: true,
globals,
}) as () => Promise;
// setTimeout is absent from the compartment (SES resolves the bare global to
// undefined), so calling it throws — the failure the guard exists to avoid.
await expect(fn()).rejects.toThrow(
/setTimeout is not (defined|a function)/,
);
});
it("the guarded backoff resolves immediately inside the compartment", async () => {
ensureSESLockdown();
const globals = createModuleCompartmentGlobals();
const fn = evaluateFunctionSourceInSES(GUARDED_BACKOFF_SRC, {
lockdown: true,
globals,
}) as () => Promise;
// No timer in the sandbox: the 30s backoff must complete without throwing
// and without actually waiting (the test itself would hang for 30s if it did).
const result = await fn();
expect(result).toBe("backoff-complete");
});
it("the same guard DOES wait when a timer is present (host behavior unchanged)", async () => {
// Outside the sandbox (this test's own Deno global has setTimeout), the guard
// takes the waiting branch. This runs under the package's fake clock, so the
// waiting branch's `setTimeout` is a frozen test-file timer: it does not
// resolve on its own, and `clock.tick` advances logical time to fire it.
const sleep = (ms: number): Promise => {
if (ms <= 0) return Promise.resolve();
const timer =
(globalThis as { setTimeout?: typeof setTimeout }).setTimeout;
if (typeof timer !== "function") return Promise.resolve();
return new Promise((resolve) => {
timer(resolve, ms);
});
};
let fired = false;
const p = sleep(10).then(() => {
fired = true;
});
expect(fired).toBe(false); // did not resolve synchronously — the waiting branch
await clock.tick(10);
await p;
expect(fired).toBe(true);
});
});