From bf8926ce7482104ca261b4ec4d3b2eaf7ba4d768 Mon Sep 17 00:00:00 2001 From: lidge-jun Date: Tue, 15 Sep 2026 11:15:04 +0900 Subject: [PATCH] test(lab): check that the sync activation window's callees are synchronous The existing guard scans the text between `Bun.serve` and `return server` for a body-level await. That window is 178 lines and calls ten free functions plus eleven receiver methods. If one of those callees becomes `async`, startServer no longer waits for it, the ordering the window exists to protect is gone, and the window text still has no `await` in it, so all four existing checks stay green. `activateLab` is the function that ordering is about. Making it async is a one-word change that the guard could not see. Two checks close that. "functions the window calls are synchronous" collects the body-level call sites in the window, splits them into free functions and receiver methods, resolves each free function through src/server/index.ts's imports and one level of re-export to the module that declares it, and asserts the declaration is not `async` and its body has no body-level await. Names it cannot resolve go in UNRESOLVED_CALLEES with a reason rather than being skipped, because a silent skip is how this kind of check rots. Receiver methods go in SYNC_WINDOW_RECEIVER_CALLS, and the collected set must match that list exactly, so a new `obj.method()` in the window fails the test and forces a review. Depth is one on purpose. Walking every function those callees invoke produces false positives on dynamic dispatch, and the regression this exists to catch lands at depth one. "the callee scan is not vacuous" pins the scanner against synthetic input and fails if the collector finds no free function at all, which is how a collapsed window would otherwise measure an empty string and pass. Driven red to prove it is not vacuous: declaring src/lib/lab-activation.ts's `activateLab` `async` leaves all four existing checks green and fails only the new one, with "activateLab in src/lib/lab-activation.ts: declared async". The declaration was restored; the suite is 19 pass / 0 fail. --- tests/lab/core-lab-boundary.test.ts | 559 +++++++++++++++++++++++++++- 1 file changed, 558 insertions(+), 1 deletion(-) diff --git a/tests/lab/core-lab-boundary.test.ts b/tests/lab/core-lab-boundary.test.ts index 57fcb0bc71e..b39a0afbf00 100644 --- a/tests/lab/core-lab-boundary.test.ts +++ b/tests/lab/core-lab-boundary.test.ts @@ -272,6 +272,435 @@ export function bodyLevelAwaitLines(region: string): number[] { return hits; } +const CALL_KEYWORDS = new Set([ + "if", "for", "while", "switch", "catch", "function", "async", "import", + "typeof", "return", "case", "new", "class", "interface", "else", "do", + "with", "of", "in", "as", "from", "void", "await", "yield", "delete", + "throw", "using", +]); + +function skipWs(code: string, i: number, dir: 1 | -1): number { + while (i >= 0 && i < code.length && /\s/.test(code[i]!)) i += dir; + return i; +} + +function readIdentBack(code: string, last: number): { name: string; start: number } | null { + if (last < 0 || last >= code.length || !/[\w$]/.test(code[last]!)) return null; + let start = last; + while (start > 0 && /[\w$]/.test(code[start - 1]!)) start--; + if (!/[A-Za-z_$]/.test(code[start]!)) return null; + return { name: code.slice(start, last + 1), start }; +} + +/** Skip a trailing `` immediately before a call, walking backward from `>`. */ +function skipTypeArgsBack(code: string, j: number): number { + if (j < 0 || code[j] !== ">") return j; + let angle = 0; + for (let k = j; k >= 0; k--) { + const ch = code[k]!; + if (ch === ">" && (k === 0 || code[k - 1] !== "=")) angle++; + else if (ch === "<") { + angle--; + if (angle === 0) return skipWs(code, k - 1, -1); + } + } + return j; +} + +/** + * Skip the expression of a concise arrow (`=> expr` without `{`) so a body-level + * `.then(x => helper())` cannot be misread as startServer calling `helper`. + * + * The window contains exactly that shape: `primeCodexPoolQuotas` sits in a + * concise `.then` callback. That callback runs after listen, so treating it as + * a window callee would either fail resolution (it is a destructured binding, + * not an import) or, worse, pin the wrong function's synchrony. + * + * Returns the index of the terminator (`)`, `,`, `;`) without consuming it. + */ +function skipConciseArrowBody(code: string, afterArrow: number): number { + let i = skipWs(code, afterArrow, 1); + if (i < code.length && code[i] === "{") return afterArrow; + let paren = 0; + let brace = 0; + let bracket = 0; + while (i < code.length) { + const ch = code[i]!; + if (paren === 0 && brace === 0 && bracket === 0 && (ch === ")" || ch === "," || ch === ";" || ch === "]")) { + return i; + } + if (ch === "(") paren++; + else if (ch === ")") { + if (paren === 0) return i; + paren--; + } else if (ch === "{") brace++; + else if (ch === "}") { + if (brace === 0) return i; + brace--; + } else if (ch === "[") bracket++; + else if (ch === "]") { + if (bracket === 0) return i; + bracket--; + } + i++; + } + return i; +} + +export type BodyLevelCalls = { + free: string[]; + receiver: string[]; +}; + +/** + * Body-level call sites in `region`, split into free function names and + * receiver expressions. Nested functions (including concise arrows) are + * skipped: those run later, the same distinction `bodyLevelAwaitLines` makes + * for `await`. + * + * Depth is one on purpose. Walking every function those callees invoke would + * treat dynamic dispatch (`.then`, `.map`, registry lookups) as startup + * callees and fail on helpers that never run during `startServer`. The actual + * regression — `activateLab` (or any other window callee) becoming `async` — + * is visible on the direct callee; a deeper walk would add false positives + * without catching more of that bug. + */ +export function collectBodyLevelCalls(region: string): BodyLevelCalls { + const code = blankCommentsAndStrings(region); + const free = new Set(); + const receiver = new Set(); + const stack: boolean[] = []; + let i = 0; + while (i < code.length) { + const ch = code[i]!; + if (ch === "{") { + stack.push(opensFunctionBody(code, i)); + i++; + continue; + } + if (ch === "}") { + stack.pop(); + i++; + continue; + } + if (ch === "=" && code[i + 1] === ">") { + const skipped = skipConciseArrowBody(code, i + 2); + if (skipped !== i + 2) { + i = skipped; + continue; + } + i += 2; + continue; + } + if (ch !== "(") { + i++; + continue; + } + if (stack.some(isFunctionBody => isFunctionBody)) { + i++; + continue; + } + const classified = classifyBodyLevelCall(code, i); + if (classified.kind === "free") free.add(classified.name); + else if (classified.kind === "receiver") receiver.add(classified.expr); + i++; + } + return { free: [...free], receiver: [...receiver] }; +} + +type ClassifiedCall = + | { kind: "skip" } + | { kind: "free"; name: string } + | { kind: "receiver"; expr: string }; + +function classifyBodyLevelCall(code: string, parenIndex: number): ClassifiedCall { + let j = skipWs(code, parenIndex - 1, -1); + // `foo?.(` optional-calls the binding, not a method. + if (j >= 1 && code[j] === "." && code[j - 1] === "?") j = skipWs(code, j - 2, -1); + if (j >= 0 && code[j] === ">") j = skipTypeArgsBack(code, j); + const ident = readIdentBack(code, j); + if (!ident) return { kind: "skip" }; + j = skipWs(code, ident.start - 1, -1); + // `...createResetCreditWhamClient(` is a spread of a call, not `obj.method(`. + // The last `.` of `...` would otherwise classify the callee as a receiver and + // hide it from declaration inspection — the exact way this scan goes vacuous. + const isSpread = j >= 2 && code[j] === "." && code[j - 1] === "." && code[j - 2] === "."; + const isMember = !isSpread && j >= 0 && (code[j] === "." || (j >= 1 && code[j] === "?" && code[j - 1] === ".")); + if (isMember) { + return { kind: "receiver", expr: formatReceiverFromWalk(code, ident) }; + } + const prev = readIdentBack(code, j); + // `new Foo(` is a constructor. Class constructors cannot be async; replacing + // this with `await Foo.create()` is already a body-level await and Guard 3's + // existing scan would catch it. Treating the class name as a free function + // would fail to find `function Foo` and rot into UNRESOLVED_CALLEES. + if (prev?.name === "new" || prev?.name === "function") return { kind: "skip" }; + if (CALL_KEYWORDS.has(ident.name)) return { kind: "skip" }; + return { kind: "free", name: ident.name }; +} + +function formatReceiverFromWalk(code: string, rightmost: { name: string; start: number }): string { + type Part = { name: string; optional: boolean }; + const chain: Part[] = [{ name: rightmost.name, optional: false }]; + let j = skipWs(code, rightmost.start - 1, -1); + while (j >= 0) { + let optional = false; + if (j >= 1 && code[j] === "." && code[j - 1] === "?") { + optional = true; + j = skipWs(code, j - 2, -1); + } else if (code[j] === ".") { + j = skipWs(code, j - 1, -1); + } else { + break; + } + if (j >= 0 && code[j] === ">") j = skipTypeArgsBack(code, j); + const ident = readIdentBack(code, j); + if (!ident) { + chain[0] = { ...chain[0], optional }; + return "(...)" + chain.map(p => (p.optional ? "?." : ".") + p.name).join("") + "()"; + } + chain[0] = { ...chain[0], optional }; + chain.unshift({ name: ident.name, optional: false }); + j = skipWs(code, ident.start - 1, -1); + } + const head = chain[0]!.name; + const tail = chain.slice(1).map(p => (p.optional ? "?." : ".") + p.name).join(""); + return head + tail + "()"; +} + +export type FunctionSyncInspection = { + found: boolean; + async: boolean; + awaitLines: number[]; +}; + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function matchPair(code: string, start: number, open: string, close: string): number { + let depth = 0; + for (let i = start; i < code.length; i++) { + const ch = code[i]!; + if (ch === "=" && code[i + 1] === ">") { i++; continue; } + if (ch === open) depth++; + else if (ch === close) { + depth--; + if (depth === 0) return i + 1; + } + } + return -1; +} + +function skipWsFwd(code: string, i: number): number { + return skipWs(code, i, 1); +} + +/** + * After `function name`, skip generics and the parameter list. + * Returns the index of the first character after the closing `)`. + */ +function skipParamList(code: string, i: number): number { + i = skipWsFwd(code, i); + if (code[i] === "<") { + const end = matchPair(code, i, "<", ">"); + if (end < 0) return -1; + i = skipWsFwd(code, end); + } + if (code[i] !== "(") return -1; + return matchPair(code, i, "(", ")"); +} + +/** + * Skip a TypeScript return type after `:`. The function body `{` is the `{` + * that appears at depth 0 once a primary type has already been consumed — so + * `): void {` and `): { inspect: () => T } {` both land on the body, not the + * object type. If we took the first `{` after `:`, `createResetCreditWhamClient` + * would be inspected as an empty object type and its real body (and any await + * added there) would go unseen. + */ +function skipReturnType(code: string, colonIndex: number): number { + let i = colonIndex + 1; + let paren = 0; + let bracket = 0; + let angle = 0; + let brace = 0; + let sawPrimary = false; + while (i < code.length) { + const ch = code[i]!; + if (/\s/.test(ch)) { i++; continue; } + if (ch === "=" && code[i + 1] === ">") { i += 2; sawPrimary = true; continue; } + const atTop = paren === 0 && bracket === 0 && angle === 0 && brace === 0; + if (ch === "{" && atTop) { + if (sawPrimary) return i; + brace++; + sawPrimary = true; + i++; + continue; + } + if (ch === "(") { paren++; sawPrimary = true; i++; continue; } + if (ch === ")") { if (paren === 0) return -1; paren--; i++; continue; } + if (ch === "[") { bracket++; sawPrimary = true; i++; continue; } + if (ch === "]") { if (bracket === 0) return -1; bracket--; i++; continue; } + if (ch === "{") { brace++; i++; continue; } + if (ch === "}") { if (brace === 0) return -1; brace--; i++; continue; } + if (ch === "<") { angle++; i++; continue; } + if (ch === ">") { if (angle > 0) angle--; i++; continue; } + if (/[A-Za-z_$]/.test(ch)) { + sawPrimary = true; + i++; + while (i < code.length && /[\w$]/.test(code[i]!)) i++; + continue; + } + i++; + } + return -1; +} + +function extractFunctionBody(code: string, afterName: number): string | null { + const afterParams = skipParamList(code, afterName); + if (afterParams < 0) return null; + let i = skipWsFwd(code, afterParams); + if (code[i] === ":") { + i = skipReturnType(code, i); + if (i < 0) return null; + } + i = skipWsFwd(code, i); + if (code[i] !== "{") return null; + const end = matchPair(code, i, "{", "}"); + if (end < 0) return null; + return code.slice(i, end); +} + +/** + * Text-level inspection of a named `function` declaration in `source`. + * + * This is deliberately not a parser. `export function f() { await p; }` is a + * syntax error without `async`, but it is also exactly the edit a hurried + * conversion to async forgets to finish — and the silent failure this guard + * exists to catch. A real parser would refuse the input; a text scan reports it. + */ +export function inspectFunctionDeclaration(source: string, name: string): FunctionSyncInspection { + const code = blankCommentsAndStrings(source); + const ident = escapeRegExp(name); + const fnRe = new RegExp("(export\\s+)?(async\\s+)?function\\s+" + ident + "\\b"); + const match = fnRe.exec(code); + if (!match || match.index === undefined) return { found: false, async: false, awaitLines: [] }; + const async = Boolean(match[2]); + const afterName = match.index + match[0].length; + const body = extractFunctionBody(code, afterName); + if (body === null) return { found: true, async, awaitLines: [] }; + return { found: true, async, awaitLines: bodyLevelAwaitLines(body) }; +} + +type SpecBinding = { local: string; exported: string }; + +function parseSpecList(inner: string): SpecBinding[] { + const bindings: SpecBinding[] = []; + for (const raw of inner.split(",")) { + const tokens = raw.trim().split(/\s+/).filter(Boolean); + if (tokens.length === 0) continue; + if (tokens[0] === "type") continue; + if (tokens.length >= 3 && tokens[1] === "as") { + bindings.push({ exported: tokens[0]!, local: tokens[2]! }); + continue; + } + if (tokens.length >= 1 && /^[A-Za-z_$]/.test(tokens[0]!)) { + bindings.push({ exported: tokens[0]!, local: tokens[0]! }); + } + } + return bindings; +} + +function namedImportsOf(source: string): Map { + // Do not blank strings first: that erases the specifier, so every import + // looks like `from " "` and resolveSpec returns null. A comment that + // happens to contain `import { foo } from "./bar"` is not a form this file uses. + const code = source; + const out = new Map(); + const re = /^\s*import\s+(?!type\b)(?:[^{;]+?,\s*)?\{([^}]+)\}\s*from\s*["']([^"']+)["']/gm; + let match: RegExpExecArray | null; + while ((match = re.exec(code)) !== null) { + const spec = match[2]!; + for (const binding of parseSpecList(match[1]!)) { + out.set(binding.local, { spec, exported: binding.exported }); + } + } + return out; +} + +function reexportOf(source: string, name: string): { spec: string; exported: string } | null { + const code = source; + const re = /^\s*export\s+\{([^}]+)\}\s*from\s*["']([^"']+)["']/gm; + let match: RegExpExecArray | null; + while ((match = re.exec(code)) !== null) { + for (const binding of parseSpecList(match[1]!)) { + if (binding.local === name) return { spec: match[2]!, exported: binding.exported }; + } + } + return null; +} + +export type ResolvedCallee = { + file: string; + inspection: FunctionSyncInspection; +}; + +function repoRel(file: string): string { + return file.slice(repoRoot.length + 1).replaceAll("\\", "/"); +} + +/** + * Resolve a free-function name used by `src/server/index.ts` to the module that + * declares it. Re-export hops are followed because two of the window's callees + * (`isCanonicalOpenAiForwardProvider`, `createResetCreditWhamClient`) and + * `getConfigDir` are imported through a barrel that only re-exports them. + * Stopping at the import target would report those names as missing and push + * them onto UNRESOLVED_CALLEES, which is how this scan would go vacuous. + * + * This is still depth 1 on the *call* graph: we inspect that declaration, we + * do not walk the functions it calls. + */ +export function resolveImportedCallee( + name: string, + fromFile: string, + fromSource: string, +): ResolvedCallee | null { + const imports = namedImportsOf(fromSource); + const imported = imports.get(name); + if (imported) { + const file = resolveSpec(imported.spec, fromFile); + if (!file) return null; + return resolveDeclarationFollowingReexports(file, imported.exported); + } + const local = inspectFunctionDeclaration(fromSource, name); + if (!local.found) return null; + return { file: fromFile, inspection: local }; +} + +function resolveDeclarationFollowingReexports(file: string, name: string): ResolvedCallee | null { + const visited = new Set(); + let currentFile = file; + let currentName = name; + for (let hop = 0; hop < 8; hop++) { + const key = `${currentFile}::${currentName}`; + if (visited.has(key)) return null; + visited.add(key); + if (!existsSync(currentFile)) return null; + const source = readFileSync(currentFile, "utf8"); + const inspection = inspectFunctionDeclaration(source, currentName); + if (inspection.found) return { file: currentFile, inspection }; + const next = reexportOf(source, currentName); + if (!next) return null; + const resolved = resolveSpec(next.spec, currentFile); + if (!resolved) return null; + currentFile = resolved; + currentName = next.exported; + } + return null; +} + + describe("core / Compatibility Lab boundary", () => { // Guard 1: the obvious case, a direct import. test.each(PROTECTED)("%s has no direct src/lab import", file => { @@ -354,6 +783,43 @@ describe("activation window stays synchronous", () => { const indexPath = resolve(repoRoot, "src/server/index.ts"); const source = readFileSync(indexPath, "utf8"); + + /** + * Receiver method calls in the activation window cannot be resolved to a + * `function` declaration from the call site: the receiver is an object, a + * builtin, or a call result. Pinning the exact set forces a review when a + * new `obj.method()` appears in the window — the alternative is silently + * skipping it, which is how `activateLab` becoming async would have a twin + * that this scan never sees. + */ + const SYNC_WINDOW_RECEIVER_CALLS: Record = { + "Bun.serve()": "Bun runtime API. serve() returns a Server synchronously; an async serve would be a Bun change, not ours. The existing body-level await scan would still catch `await Bun.serve()`.", + "server.stop()": "Server.stop on the just-created listener, invoked as `void server.stop(true)` in the loopback-bind rollback. The Promise is discarded, so even an async stop does not suspend startServer; `await server.stop()` is already a Guard-3 failure.", + "bound.stop()": "The same Server.stop on the loop variable of the management-ingress bind rollback. Same fire-and-forget shape as server.stop().", + "userCostOverlayReconciler?.stop()": "Instance method on the overlay reconciler. The local binding is typed `{ stop(): void } | null`; the call site cannot see the implementation in user-cost-overlay-reconciler.ts.", + "backgroundLifecycle?.releaseAfterFailedStart()": "Instance method from acquireServerBackgroundLifecycle in src/server/background-lifecycle.ts. Optional because the catch path can run before the lifecycle is assigned. That module owns the method's synchrony.", + "nativeMainLifecycle.release()": "Instance method on NativeMainStartupLifecycle. Called as `void nativeMainLifecycle.release()` so a Promise return would not suspend startServer; `await nativeMainLifecycle.release()` would already fail Guard 3.", + "server.stop.bind()": "Function.prototype.bind snapshotting the original stop before Object.defineProperty replaces it. bind itself is synchronous.", + "Object.defineProperty()": "Language builtin used to install the async stop wrapper. The wrapper's awaits run at shutdown, not during startServer. An `await Object.defineProperty(...)` would already fail Guard 3.", + "console.log()": "stdout. Cannot suspend startServer.", + "console.warn()": "stderr. Cannot suspend startServer.", + "(...).then()": "Promise.then on the fire-and-forget `import('../codex/plan-from-token')` chain. then() registers a callback and returns immediately; the callback is a nested function this scan skips. Awaiting the import would already fail Guard 3.", + "(...).catch()": "Promise.catch on that same dynamic-import chain. Same fire-and-forget: it cannot suspend startServer.", + "backgroundLifecycle.scheduleStartupRun()": "src/server/background-lifecycle.ts owns this object method. The call site cannot resolve the declaration statically; scheduleStartupRun is declared `(): void` and is documented as never blocking listen.", + }; + + /** + * Free identifiers the window calls whose declaration is not a named `function` + * this scan can inspect. Quietly skipping them would make the scan vacuous: + * a later edit that turns the binding into `const foo = async () => ...` and + * then `await foo()` is Guard 3, but `foo()` without await of an async + * function is the hole this list exists to keep visible. + */ + const UNRESOLVED_CALLEES: Record = { + unregisterQuotaAutoRefresh: "let-binding holding the return of registerCodexQuotaAutoRefreshWorker, optional-called on the bind-failure path. There is no `function unregisterQuotaAutoRefresh` to inspect; following the assignment would be depth 2.", + }; + + test("startServer is not async", () => { // An async startServer returns a Promise, so every caller treating the return value as a // live Server would break. The subtler cost is that it makes a body-level await legal, @@ -431,5 +897,96 @@ describe("activation window stays synchronous", () => { expect(region.includes("await backgroundLifecycle.release();")).toBe(true); expect(bodyLevelAwaitLines(region)).toEqual([]); }); -}); + test("functions the window calls are synchronous", () => { + // Guard 3's text scan of the window cannot see `activateLab` becoming + // `async`: the call site has no `await`, so the existing four tests stay + // green while startServer proceeds past a now-thenable activation and a + // policy route can evaluate before its evidence provider is registered. + // This test follows each body-level free call to its declaration (one hop, + // plus re-export aliases) and fails if that declaration is async or has a + // body-level await. Receiver methods go on SYNC_WINDOW_RECEIVER_CALLS + // instead of being skipped: a new `obj.method()` in the window must be + // reviewed rather than silently ignored. + const start = source.indexOf(SERVE_ANCHOR); + const end = source.indexOf(RETURN_ANCHOR, start); + expect(start).toBeGreaterThan(-1); + expect(end).toBeGreaterThan(start); + const region = source.slice(start, end); + const calls = collectBodyLevelCalls(region); + + expect([...calls.receiver].sort()).toEqual(Object.keys(SYNC_WINDOW_RECEIVER_CALLS).sort()); + + const unresolvedAllow = new Set(Object.keys(UNRESOLVED_CALLEES)); + const unresolvedFound: string[] = []; + const failures: string[] = []; + + for (const name of calls.free) { + if (unresolvedAllow.has(name)) { + unresolvedFound.push(name); + continue; + } + const got = resolveImportedCallee(name, indexPath, source); + if (!got) { + failures.push(`${name}: declaration not found`); + continue; + } + const rel = repoRel(got.file); + if (!got.inspection.found) failures.push(`${name} in ${rel}: declaration not found`); + if (got.inspection.async) failures.push(`${name} in ${rel}: declared async`); + if (got.inspection.awaitLines.length > 0) { + failures.push(`${name} in ${rel}: body-level await at relative ${got.inspection.awaitLines.join(",")}`); + } + } + + expect(unresolvedFound.sort()).toEqual([...unresolvedAllow].sort()); + expect(failures).toEqual([]); + }); + + test("the callee scan is not vacuous", () => { + // Same helpers the window test uses, on synthetic input, so a drift in the + // inspector cannot pass here and fail to catch `export async function` there. + expect(inspectFunctionDeclaration("export function f() { return 1; }", "f")).toEqual({ + found: true, + async: false, + awaitLines: [], + }); + expect(inspectFunctionDeclaration("export async function f() { return 1; }", "f")).toEqual({ + found: true, + async: true, + awaitLines: [], + }); + + // Illegal without `async`, but the helper is a text scan: this is the + // half-finished conversion (`function` left sync, `await` already added) + // that a parser would refuse and this guard still has to report. + const bodyAwait = "export function f() {\n const p = g();\n await p;\n}\n"; + expect(inspectFunctionDeclaration(bodyAwait, "f")).toEqual({ + found: true, + async: false, + awaitLines: [3], + }); + + const nested = "export function f() { void (async () => { await g(); }); }\n"; + expect(inspectFunctionDeclaration(nested, "f")).toEqual({ + found: true, + async: false, + awaitLines: [], + }); + + // Collector: a nested call is not a window callee, and a receiver is not a free function. + const collected = collectBodyLevelCalls("foo();\nobj.bar();\nvoid (async () => { nested(); });\n({ ...spreadCallee() });\n"); + expect(collected.free.sort()).toEqual(["foo", "spreadCallee"]); + expect(collected.receiver.sort()).toEqual(["obj.bar()"]); + + const start = source.indexOf(SERVE_ANCHOR); + const end = source.indexOf(RETURN_ANCHOR, start); + const region = source.slice(start, end); + const windowCalls = collectBodyLevelCalls(region); + // If the window collapsed to an empty string, or the collector stopped + // seeing calls, this test would still pass every sync assertion vacuously. + expect(windowCalls.free.length).toBeGreaterThan(0); + expect(windowCalls.free).toContain("activateLab"); + }); + +});