feat(catalog): added Gemini Flash model definitions and routing

- Added Gemini 3.7 Flash model definitions and reasoning effort routing configurations.
- Implemented geminiLevelFlashFamily in variant-collapse.ts to manage 3.6+ flash thinking levels.
- Added test coverage for Gemini 3.7 Flash variant collapse and discovery routing.
This commit is contained in:
can1357
2026-08-13 19:35:41 +02:00
parent b279db1790
commit 3ce33d436b
6 changed files with 186 additions and 623 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Added
- Added dynamic Antigravity and Gemini CLI discovery support for Gemini 3.7 Flash, with low/medium/high thinking-level routing.
### Changed
- Updated model metadata, context windows, pricing, and configurations in the catalog
+104 -20
View File
@@ -12007,7 +12007,8 @@
],
"supportsDisplay": true
},
"supportsComputerUse": false
"supportsComputerUse": false,
"supportsComputerUseConfig": false
},
"claude-opus-4-0": {
"id": "claude-opus-4-0",
@@ -18704,7 +18705,8 @@
"contextWindow": 200000,
"maxTokens": 64000,
"cursorMaxMode": false,
"supportsComputerUse": false
"supportsComputerUse": false,
"supportsComputerUseConfig": false
},
"gemini-3.7-flash-low": {
"id": "gemini-3.7-flash-low",
@@ -18726,7 +18728,8 @@
"contextWindow": 200000,
"maxTokens": 64000,
"cursorMaxMode": false,
"supportsComputerUse": false
"supportsComputerUse": false,
"supportsComputerUseConfig": false
},
"gemini-3.7-flash-medium": {
"id": "gemini-3.7-flash-medium",
@@ -18748,7 +18751,8 @@
"contextWindow": 200000,
"maxTokens": 64000,
"cursorMaxMode": false,
"supportsComputerUse": false
"supportsComputerUse": false,
"supportsComputerUseConfig": false
},
"glm-5.2-high": {
"id": "glm-5.2-high",
@@ -25111,6 +25115,36 @@
"requiresEffort": true
}
},
"gemini-3.7-flash": {
"id": "gemini-3.7-flash",
"name": "Gemini 3.7 Flash",
"api": "google-generative-ai",
"provider": "google",
"baseUrl": "https://generativelanguage.googleapis.com/v1beta",
"reasoning": true,
"input": [
"text",
"image"
],
"cost": {
"input": 0.75,
"output": 3.75,
"cacheRead": 0.075,
"cacheWrite": 0
},
"contextWindow": 1048576,
"maxTokens": 65536,
"thinking": {
"mode": "google-level",
"efforts": [
"minimal",
"low",
"medium",
"high"
],
"requiresEffort": true
}
},
"gemini-flash-latest": {
"id": "gemini-flash-latest",
"name": "Gemini Flash Latest",
@@ -26792,6 +26826,36 @@
"requiresEffort": true
}
},
"gemini-3.7-flash": {
"id": "gemini-3.7-flash",
"name": "Gemini 3.7 Flash",
"api": "google-vertex",
"provider": "google-vertex",
"baseUrl": "https://{location}-aiplatform.googleapis.com",
"reasoning": true,
"input": [
"text",
"image"
],
"cost": {
"input": 0.75,
"output": 3.75,
"cacheRead": 0.075,
"cacheWrite": 0
},
"contextWindow": 1048576,
"maxTokens": 65536,
"thinking": {
"mode": "google-level",
"efforts": [
"minimal",
"low",
"medium",
"high"
],
"requiresEffort": true
}
},
"gemini-flash-latest": {
"id": "gemini-flash-latest",
"name": "Gemini Flash Latest",
@@ -32855,7 +32919,8 @@
"xhigh"
]
},
"supportsComputerUse": false
"supportsComputerUse": false,
"supportsComputerUseConfig": false
},
"inclusionai/ring-2.6-1t": {
"id": "inclusionai/ring-2.6-1t",
@@ -51761,7 +51826,8 @@
},
"contextWindow": 262144,
"maxTokens": 32768,
"supportsComputerUse": false
"supportsComputerUse": false,
"supportsComputerUseConfig": false
},
"inclusionai/ling-3.0-tiny:thinking": {
"id": "inclusionai/ling-3.0-tiny:thinking",
@@ -51791,7 +51857,8 @@
"xhigh"
]
},
"supportsComputerUse": false
"supportsComputerUse": false,
"supportsComputerUseConfig": false
},
"inclusionai/ring-2.6-1t": {
"id": "inclusionai/ring-2.6-1t",
@@ -61824,7 +61891,8 @@
],
"requiresEffort": true
},
"supportsComputerUse": false
"supportsComputerUse": false,
"supportsComputerUseConfig": false
},
"TEE/muse-glimmer-30b": {
"id": "TEE/muse-glimmer-30b",
@@ -77936,10 +78004,10 @@
"image"
],
"cost": {
"input": 1.5,
"output": 7.5,
"cacheRead": 0.15,
"cacheWrite": 0.0833333333333333
"input": 0.375,
"output": 1.875,
"cacheRead": 0.0375,
"cacheWrite": 0.0416666666666667
},
"contextWindow": 1048576,
"maxTokens": 65536,
@@ -81248,7 +81316,8 @@
],
"requiresEffort": true
},
"supportsComputerUse": false
"supportsComputerUse": false,
"supportsComputerUseConfig": false
},
"google/gemini-3.7-flash:batch": {
"id": "google/gemini-3.7-flash:batch",
@@ -81279,7 +81348,8 @@
],
"requiresEffort": true
},
"supportsComputerUse": false
"supportsComputerUse": false,
"supportsComputerUseConfig": false
},
"google/gemma-3-12b-it": {
"id": "google/gemma-3-12b-it",
@@ -90818,7 +90888,8 @@
},
"requiresEffort": true
},
"supportsComputerUse": false
"supportsComputerUse": false,
"supportsComputerUseConfig": false
},
"hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4": {
"id": "hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4",
@@ -90848,7 +90919,8 @@
"xhigh"
]
},
"supportsComputerUse": false
"supportsComputerUse": false,
"supportsComputerUseConfig": false
},
"hf:openai/gpt-oss-120b": {
"id": "hf:openai/gpt-oss-120b",
@@ -90876,7 +90948,8 @@
"high"
]
},
"supportsComputerUse": false
"supportsComputerUse": false,
"supportsComputerUseConfig": false
},
"hf:Qwen/Qwen3.6-27B": {
"id": "hf:Qwen/Qwen3.6-27B",
@@ -90906,7 +90979,8 @@
"high"
]
},
"supportsComputerUse": false
"supportsComputerUse": false,
"supportsComputerUseConfig": false
},
"hf:zai-org/GLM-4.7-Flash": {
"id": "hf:zai-org/GLM-4.7-Flash",
@@ -90936,7 +91010,8 @@
"xhigh"
]
},
"supportsComputerUse": false
"supportsComputerUse": false,
"supportsComputerUseConfig": false
},
"hf:zai-org/GLM-5.2": {
"id": "hf:zai-org/GLM-5.2",
@@ -90966,7 +91041,8 @@
"max"
]
},
"supportsComputerUse": false
"supportsComputerUse": false,
"supportsComputerUseConfig": false
},
"syn:large:text": {
"id": "syn:large:text",
@@ -104236,6 +104312,7 @@
"contextWindow": 2000000,
"maxTokens": 2000000,
"supportsComputerUse": false,
"supportsComputerUseConfig": false,
"compat": {
"reasoningEffortMap": {
"minimal": "low"
@@ -104267,6 +104344,7 @@
"contextWindow": 2000000,
"maxTokens": 2000000,
"supportsComputerUse": false,
"supportsComputerUseConfig": false,
"compat": {
"reasoningEffortMap": {
"minimal": "low"
@@ -104310,6 +104388,7 @@
}
},
"supportsComputerUse": false,
"supportsComputerUseConfig": false,
"compat": {
"reasoningEffortMap": {
"minimal": "low"
@@ -104354,6 +104433,7 @@
}
},
"supportsComputerUse": false,
"supportsComputerUseConfig": false,
"compat": {
"reasoningEffortMap": {
"minimal": "low"
@@ -104398,6 +104478,7 @@
}
},
"supportsComputerUse": false,
"supportsComputerUseConfig": false,
"compat": {
"reasoningEffortMap": {
"minimal": "low"
@@ -104457,6 +104538,7 @@
"contextWindow": 512000,
"maxTokens": 512000,
"supportsComputerUse": false,
"supportsComputerUseConfig": false,
"compat": {
"reasoningEffortMap": {
"minimal": "low"
@@ -104488,6 +104570,7 @@
"contextWindow": 256000,
"maxTokens": 256000,
"supportsComputerUse": false,
"supportsComputerUseConfig": false,
"compat": {
"reasoningEffortMap": {
"minimal": "low"
@@ -104518,6 +104601,7 @@
"contextWindow": 200000,
"maxTokens": 200000,
"supportsComputerUse": false,
"supportsComputerUseConfig": false,
"compat": {
"reasoningEffortMap": {
"minimal": "low"
+34 -17
View File
@@ -265,22 +265,32 @@ function geminiFlashFamily(mode: "budget" | "google-level"): EffortVariantFamily
};
}
const GEMINI_36_FLASH_FAMILY: EffortVariantFamily = {
id: "gemini-3.6-flash",
name: "Gemini 3.6 Flash",
members: ["gemini-3.6-flash-low", "gemini-3.6-flash-medium", "gemini-3.6-flash-high", "gemini-3.6-flash-tiered"],
routing: {
[Effort.Minimal]: "gemini-3.6-flash-low",
[Effort.Low]: "gemini-3.6-flash-low",
[Effort.Medium]: "gemini-3.6-flash-medium",
[Effort.High]: "gemini-3.6-flash-high",
},
thinking: {
mode: "google-level",
efforts: GEMINI_3_FLASH_FAMILY_EFFORTS,
requiresEffort: true,
},
};
/**
* Gemini 3.6+ Flash exposes one mandatory-reasoning wire id per thinking
* level. Some generations retain additional discovery-only aliases.
*/
function geminiLevelFlashFamily(version: "3.6" | "3.7", ...additionalMembers: string[]): EffortVariantFamily {
const id = `gemini-${version}-flash`;
return {
id,
name: `Gemini ${version} Flash`,
members: [`${id}-low`, `${id}-medium`, `${id}-high`, ...additionalMembers],
routing: {
[Effort.Minimal]: `${id}-low`,
[Effort.Low]: `${id}-low`,
[Effort.Medium]: `${id}-medium`,
[Effort.High]: `${id}-high`,
},
thinking: {
mode: "google-level",
efforts: GEMINI_3_FLASH_FAMILY_EFFORTS,
requiresEffort: true,
},
};
}
const GEMINI_36_FLASH_FAMILY = geminiLevelFlashFamily("3.6", "gemini-3.6-flash-tiered");
const GEMINI_37_FLASH_FAMILY = geminiLevelFlashFamily("3.7");
function geminiProFamily(mode: "budget" | "google-level"): EffortVariantFamily {
const budget = mode === "budget";
@@ -364,13 +374,20 @@ const SHARED_CCA_FAMILIES: readonly EffortVariantFamily[] = [
/** `google-antigravity` Gemini families, using each generation's native transport. */
export const ANTIGRAVITY_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
families: [GEMINI_36_FLASH_FAMILY, geminiFlashFamily("budget"), geminiProFamily("budget"), ...SHARED_CCA_FAMILIES],
families: [
GEMINI_36_FLASH_FAMILY,
GEMINI_37_FLASH_FAMILY,
geminiFlashFamily("budget"),
geminiProFamily("budget"),
...SHARED_CCA_FAMILIES,
],
};
/** `google-gemini-cli` Gemini families on the official CLI's level transport. */
export const GEMINI_CLI_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
families: [
GEMINI_36_FLASH_FAMILY,
GEMINI_37_FLASH_FAMILY,
geminiFlashFamily("google-level"),
geminiProFamily("google-level"),
...SHARED_CCA_FAMILIES,
+44 -1
View File
@@ -814,6 +814,27 @@ describe("antigravity discovery collapsing", () => {
supportsImages: true,
thinkingBudget: 10_000,
},
"gemini-3.7-flash-low": {
displayName: "Gemini 3.7 Flash Low",
supportsThinking: true,
supportsImages: true,
maxTokens: 1_048_576,
maxOutputTokens: 65_536,
},
"gemini-3.7-flash-medium": {
displayName: "Gemini 3.7 Flash Medium",
supportsThinking: true,
supportsImages: true,
maxTokens: 1_048_576,
maxOutputTokens: 65_536,
},
"gemini-3.7-flash-high": {
displayName: "Gemini 3.7 Flash High",
supportsThinking: true,
supportsImages: true,
maxTokens: 1_048_576,
maxOutputTokens: 65_536,
},
"claude-sonnet-4-6": { displayName: "Claude Sonnet 4.6", supportsThinking: true, supportsImages: true },
"claude-sonnet-4-6-thinking": {
displayName: "Claude Sonnet 4.6 Thinking",
@@ -835,7 +856,12 @@ describe("antigravity discovery collapsing", () => {
it("returns collapsed logical entries and keeps the denylist", async () => {
const models = await fetchAntigravityDiscoveryModels({ token: "t", endpoint: "https://cca.test", fetcher });
expect(models?.map(m => m.id).sort()).toEqual(["claude-sonnet-4-6", "gemini-2.5-flash", "gemini-3.5-flash"]);
expect(models?.map(m => m.id).sort()).toEqual([
"claude-sonnet-4-6",
"gemini-2.5-flash",
"gemini-3.5-flash",
"gemini-3.7-flash",
]);
const flash = models?.find(m => m.id === "gemini-3.5-flash");
expect(flash?.requestModelId).toBe("gemini-3.5-flash-extra-low");
expect(flash?.thinking?.effortRouting?.[Effort.High]).toBe("gemini-3-flash-agent");
@@ -845,6 +871,19 @@ describe("antigravity discovery collapsing", () => {
const flash25 = models?.find(m => m.id === "gemini-2.5-flash");
expect(flash25?.thinking?.effortRouting?.[Effort.High]).toBe("gemini-2.5-flash-thinking");
expect(flash25?.thinking?.effortRouting?.off).toBe("gemini-2.5-flash");
const flash37 = models?.find(m => m.id === "gemini-3.7-flash");
expect(flash37?.requestModelId).toBe("gemini-3.7-flash-low");
expect(flash37?.thinking).toEqual({
mode: "google-level",
efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High],
requiresEffort: true,
effortRouting: {
minimal: "gemini-3.7-flash-low",
low: "gemini-3.7-flash-low",
medium: "gemini-3.7-flash-medium",
high: "gemini-3.7-flash-high",
},
});
});
it("keeps collapsed routing through the gemini-cli re-provision", async () => {
@@ -860,6 +899,10 @@ describe("antigravity discovery collapsing", () => {
expect(flash?.baseUrl).toBe("https://cca.test");
expect(flash?.requestModelId).toBe("gemini-3.5-flash-extra-low");
expect(flash?.thinking?.effortRouting?.off).toBe("gemini-3.5-flash-extra-low");
const flash37 = models?.find(m => m.id === "gemini-3.7-flash");
expect(flash37?.requestModelId).toBe("gemini-3.7-flash-low");
expect(flash37?.thinking?.requiresEffort).toBe(true);
expect(flash37?.thinking?.effortRouting?.[Effort.High]).toBe("gemini-3.7-flash-high");
});
it("uses the primary daily endpoint by default", async () => {
@@ -1,522 +0,0 @@
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import * as fs from "node:fs";
import * as path from "node:path";
import { type } from "@oh-my-pi/omptype";
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
import { TempDir } from "@oh-my-pi/pi-utils";
import { Settings } from "../../src/config/settings";
import {
disposeAllVmContexts,
setJsEvalWorkerThreadForTests,
setWorkerCloseTimeoutMsForTests,
} from "../../src/eval/js/context-manager";
import { executeJs } from "../../src/eval/js/executor";
import type { ToolSession } from "../../src/tools";
const originalWorker = globalThis.Worker;
interface FakeWorkerStats {
closeRequests: number;
terminateCalls: number;
}
interface FakeWorkerBehavior {
exitOnClose: boolean;
settleRuns: boolean;
errorOnStart?: boolean;
/**
* Reproduces `WorkerCore#runOne` for a floated bridge call: start a tool call
* and report the run finished in the same turn, without awaiting the call.
*/
floatingToolCall?: string;
}
function makeSession(cwd: string, ...tools: AgentTool[]): ToolSession {
return {
cwd,
hasUI: false,
settings: Settings.isolated({
"async.enabled": false,
"task.isolation.mode": "none",
"task.enableLsp": true,
}),
taskDepth: 0,
enableLsp: true,
getSessionFile: () => null,
getSessionSpawns: () => "*",
getActiveModelString: () => "p/active",
getModelString: () => "p/fallback",
getArtifactsDir: () => null,
getSessionId: () => "test-session",
getEvalSessionId: () => "test-eval-session",
getToolByName: name => tools.find(tool => tool.name === name),
};
}
async function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
let timeout: NodeJS.Timeout | undefined;
try {
return await Promise.race([
promise,
new Promise<never>((_, reject) => {
timeout = setTimeout(() => reject(new Error(`${label} timed out`)), ms);
}),
]);
} finally {
if (timeout) clearTimeout(timeout);
}
}
async function waitForRealWorkerExitAfterClose(cwd: string): Promise<void> {
const worker = new originalWorker(new URL("../../src/eval/js/worker-entry.ts", import.meta.url).href, {
type: "module",
});
const ready = Promise.withResolvers<void>();
const runComplete = Promise.withResolvers<void>();
const closedAck = Promise.withResolvers<void>();
const workerClosed = Promise.withResolvers<void>();
const runId = `keep-alive:${crypto.randomUUID()}`;
const snapshot = { cwd, sessionId: `worker-exit:${crypto.randomUUID()}` };
worker.addEventListener("message", event => {
const msg = event.data as { type?: string; runId?: string; ok?: boolean };
if (msg.type === "ready") ready.resolve();
else if (msg.type === "result" && msg.runId === runId && msg.ok) runComplete.resolve();
else if (msg.type === "closed") closedAck.resolve();
});
worker.addEventListener("close", () => workerClosed.resolve());
try {
worker.postMessage({ type: "init", snapshot });
await withTimeout(ready.promise, 1_000, "worker ready");
worker.postMessage({
type: "run",
runId,
code: "globalThis.__keepAlive = setInterval(() => {}, 1000);\nundefined;",
filename: "keep-alive.js",
snapshot,
});
await withTimeout(runComplete.promise, 1_000, "worker run");
worker.postMessage({ type: "close" });
await withTimeout(closedAck.promise, 1_000, "worker closed ack");
await withTimeout(workerClosed.promise, 1_000, "worker close event");
} finally {
worker.terminate();
}
}
function installFakeWorker(stats: FakeWorkerStats, behavior: FakeWorkerBehavior): void {
class FakeWorker {
#messageListeners = new Set<(event: MessageEvent) => void>();
#closeListeners = new Set<(event: Event) => void>();
#errorListeners = new Set<(event: Event) => void>();
#readyQueued = false;
#exited = false;
postMessage(message: unknown): void {
if (!message || typeof message !== "object") return;
const typed = message as { type?: string; runId?: string };
if (typed.type === "run" && typed.runId && behavior.floatingToolCall) {
const runId = typed.runId;
queueMicrotask(() => {
this.#emitMessage({
type: "tool-call",
runId,
id: `tc-${runId}`,
name: behavior.floatingToolCall,
args: {},
});
this.#emitMessage({ type: "result", runId, ok: true });
});
return;
}
if (typed.type === "run" && typed.runId && behavior.settleRuns) {
queueMicrotask(() => this.#emitMessage({ type: "result", runId: typed.runId, ok: true }));
return;
}
if (typed.type === "close") {
stats.closeRequests++;
queueMicrotask(() => {
this.#emitMessage({ type: "closed" });
if (behavior.exitOnClose) this.#emitClose();
});
}
}
addEventListener(type: string, listener: (event: MessageEvent | Event) => void): void {
if (type === "close") {
this.#closeListeners.add(listener as (event: Event) => void);
return;
}
if (type === "error") {
this.#errorListeners.add(listener as (event: Event) => void);
return;
}
if (type !== "message") return;
this.#messageListeners.add(listener as (event: MessageEvent) => void);
if (!this.#readyQueued) {
this.#readyQueued = true;
queueMicrotask(() => {
if (behavior.errorOnStart) this.#emitError();
else this.#emitMessage({ type: "ready" });
});
}
}
removeEventListener(type: string, listener: (event: MessageEvent | Event) => void): void {
if (type === "close") {
this.#closeListeners.delete(listener as (event: Event) => void);
return;
}
if (type === "error") {
this.#errorListeners.delete(listener as (event: Event) => void);
return;
}
if (type !== "message") return;
this.#messageListeners.delete(listener as (event: MessageEvent) => void);
}
terminate(): void {
stats.terminateCalls++;
this.#emitClose();
}
#emitMessage(data: unknown): void {
const event = new MessageEvent("message", { data });
for (const listener of this.#messageListeners) listener(event);
}
#emitClose(): void {
if (this.#exited) return;
this.#exited = true;
const event = new Event("close");
for (const listener of this.#closeListeners) listener(event);
}
#emitError(): void {
const event = new ErrorEvent("error", {
message: "fake worker failed to start",
error: new Error("fake worker failed to start"),
});
for (const listener of this.#errorListeners) listener(event);
}
}
Object.defineProperty(globalThis, "Worker", {
configurable: true,
writable: true,
value: FakeWorker as unknown as typeof Worker,
});
}
describe("JavaScript eval worker lifecycle", () => {
let restoreCloseTimeoutMs = 0;
let restoreWorkerThread = false;
beforeEach(() => {
restoreWorkerThread = setJsEvalWorkerThreadForTests(true);
// Shrink the graceful-close grace period so the "close acked but the worker
// never exits -> force terminate" contract is proven without a real 1s wait.
restoreCloseTimeoutMs = setWorkerCloseTimeoutMsForTests(1);
});
afterEach(async () => {
// Dispose while the shrunk timeout is still active so a hung worker's afterEach
// close also force-terminates instantly, then restore the production default.
await disposeAllVmContexts();
setWorkerCloseTimeoutMsForTests(restoreCloseTimeoutMs);
Object.defineProperty(globalThis, "Worker", {
configurable: true,
writable: true,
value: originalWorker,
});
setJsEvalWorkerThreadForTests(restoreWorkerThread);
});
it("exits a real worker on graceful close even with ref'ed user handles", async () => {
using tempDir = TempDir.createSync("@omp-js-worker-real-close-");
await waitForRealWorkerExitAfterClose(tempDir.path());
});
it("waits for the worker to close on reset instead of force-terminating it", async () => {
using tempDir = TempDir.createSync("@omp-js-worker-close-");
const stats: FakeWorkerStats = { closeRequests: 0, terminateCalls: 0 };
installFakeWorker(stats, { exitOnClose: true, settleRuns: true });
const session = makeSession(tempDir.path());
const sessionId = `js-close:${crypto.randomUUID()}`;
const first = await executeJs("globalThis.marker = 1;", { cwd: tempDir.path(), sessionId, session });
expect(first.exitCode).toBe(0);
const second = await executeJs("globalThis.marker = 2;", {
cwd: tempDir.path(),
sessionId,
session,
reset: true,
});
expect(second.exitCode).toBe(0);
expect(stats.closeRequests).toBe(1);
expect(stats.terminateCalls).toBe(0);
});
it("terminates when close is acknowledged but the worker does not exit", async () => {
using tempDir = TempDir.createSync("@omp-js-worker-close-hung-");
const stats: FakeWorkerStats = { closeRequests: 0, terminateCalls: 0 };
installFakeWorker(stats, { exitOnClose: false, settleRuns: true });
const session = makeSession(tempDir.path());
const sessionId = `js-close-hung:${crypto.randomUUID()}`;
const first = await executeJs("globalThis.marker = 1;", { cwd: tempDir.path(), sessionId, session });
expect(first.exitCode).toBe(0);
const second = await executeJs("globalThis.marker = 2;", {
cwd: tempDir.path(),
sessionId,
session,
reset: true,
});
expect(second.exitCode).toBe(0);
expect(stats.closeRequests).toBe(1);
expect(stats.terminateCalls).toBe(1);
});
it("force-terminates instead of closing when an in-flight run is aborted", async () => {
using tempDir = TempDir.createSync("@omp-js-worker-abort-");
const stats: FakeWorkerStats = { closeRequests: 0, terminateCalls: 0 };
installFakeWorker(stats, { exitOnClose: true, settleRuns: false });
const session = makeSession(tempDir.path());
const sessionId = `js-abort:${crypto.randomUUID()}`;
const controller = new AbortController();
const resultPromise = executeJs("globalThis.neverFinishes = true;", {
cwd: tempDir.path(),
sessionId,
session,
signal: controller.signal,
});
setTimeout(() => controller.abort(new DOMException("Execution aborted", "AbortError")), 0);
const result = await resultPromise;
expect(result.cancelled).toBe(true);
expect(stats.closeRequests).toBe(0);
expect(stats.terminateCalls).toBe(1);
});
it("falls back to a Bun Worker when the subprocess cannot spawn", async () => {
using tempDir = TempDir.createSync("@omp-js-spawn-fallback-");
// Exercise the production ladder (process -> worker -> inline), not the
// worker-thread test seam the surrounding describe enables.
setJsEvalWorkerThreadForTests(false);
const stats: FakeWorkerStats = { closeRequests: 0, terminateCalls: 0 };
installFakeWorker(stats, { exitOnClose: true, settleRuns: true });
const originalSpawn = Bun.spawn;
let spawnAttempts = 0;
Bun.spawn = ((): never => {
spawnAttempts++;
throw new Error("subprocess spawn unavailable");
}) as unknown as typeof Bun.spawn;
try {
const session = makeSession(tempDir.path());
const sessionId = `js-spawn-fallback:${crypto.randomUUID()}`;
// The fake Worker settles runs without executing the cell, so an empty
// output proves the middle rung handled it — the inline fallback would
// have actually evaluated the expression and printed 42.
const result = await executeJs("return String(6 * 7);", { cwd: tempDir.path(), sessionId, session });
expect(result.exitCode).toBe(0);
expect(result.output.trim()).toBe("");
expect(spawnAttempts).toBe(1);
} finally {
Bun.spawn = originalSpawn;
}
});
it("falls back to a Bun Worker when the subprocess fails during initialization", async () => {
using tempDir = TempDir.createSync("@omp-js-init-fallback-");
// Exercise the production ladder (process -> worker -> inline), not the
// worker-thread test seam the surrounding describe enables.
setJsEvalWorkerThreadForTests(false);
const stats: FakeWorkerStats = { closeRequests: 0, terminateCalls: 0 };
installFakeWorker(stats, { exitOnClose: true, settleRuns: true });
const originalSpawn = Bun.spawn;
let spawnAttempts = 0;
Bun.spawn = ((options: unknown) => {
spawnAttempts++;
const spawnOptions = options as {
onExit?: (proc: unknown, exitCode: number | null, signalCode: string | null) => void;
};
const fakeProcess = {
send: () => undefined,
kill: () => undefined,
unref: () => undefined,
};
queueMicrotask(() => spawnOptions.onExit?.(fakeProcess, 1, null));
return fakeProcess;
}) as unknown as typeof Bun.spawn;
try {
const session = makeSession(tempDir.path());
const sessionId = `js-init-fallback:${crypto.randomUUID()}`;
// The fake Worker settles runs without executing the cell, so empty
// output proves the middle rung handled the retry. Inline execution
// would evaluate the expression and print 42.
const result = await executeJs("return String(6 * 7);", { cwd: tempDir.path(), sessionId, session });
expect(result.exitCode).toBe(0);
expect(result.output.trim()).toBe("");
expect(spawnAttempts).toBe(1);
} finally {
Bun.spawn = originalSpawn;
}
});
it("falls back to the inline worker when the spawned worker errors during startup", async () => {
using tempDir = TempDir.createSync("@omp-js-worker-error-");
const stats: FakeWorkerStats = { closeRequests: 0, terminateCalls: 0 };
installFakeWorker(stats, { exitOnClose: true, settleRuns: true, errorOnStart: true });
const session = makeSession(tempDir.path());
const sessionId = `js-worker-error:${crypto.randomUUID()}`;
// The spawned worker emits an `error` event instead of `ready`. Without fail-fast
// error handling the handshake would stall until WORKER_INIT_TIMEOUT_MS (15s); with
// it, the handshake rejects at once and the inline worker runs the cell.
const result = await executeJs("return String(6 * 7);", { cwd: tempDir.path(), sessionId, session });
expect(result.exitCode).toBe(0);
expect(result.output.trim()).toBe("42");
// The errored primary worker is torn down before the inline retry takes over.
expect(stats.terminateCalls).toBe(1);
});
it("holds a finished run until its floated tool call drains", async () => {
// Regression, and the sharpest form of the runaway `agent()` fan-out — no
// cancellation required. `WorkerCore#runOne` reports a finished run without
// awaiting outstanding tool calls, so `agent(...)` with no `await` used to
// settle the cell immediately; `runOnce` then dropped the run's abort
// listener and pending entry, leaving the subagent running with nothing
// able to cancel it. A cell must own every bridge call it starts.
using tempDir = TempDir.createSync("@omp-js-worker-float-");
const stats: FakeWorkerStats = { closeRequests: 0, terminateCalls: 0 };
installFakeWorker(stats, { exitOnClose: true, settleRuns: false, floatingToolCall: "park" });
const started = Promise.withResolvers<void>();
const release = Promise.withResolvers<void>();
let toolReturned = false;
const park: AgentTool = {
name: "park",
label: "Park",
description: "Parks until released",
parameters: type({}),
execute: async () => {
started.resolve();
await release.promise;
toolReturned = true;
return { content: [{ type: "text", text: "parked" }] };
},
};
const session = makeSession(tempDir.path(), park);
let settled = false;
const cell = executeJs('tool.park({});\nreturn "floated";', {
cwd: tempDir.path(),
sessionId: `js-float:${crypto.randomUUID()}`,
session,
}).finally(() => {
settled = true;
});
await started.promise;
// The fake worker already reported the run finished, in the same microtask
// that started the call. Draining the microtask queue is exact here: the
// harness is queueMicrotask-driven, with no IPC or timers in the path.
for (let i = 0; i < 50; i++) await Promise.resolve();
expect(settled).toBe(false);
expect(toolReturned).toBe(false);
release.resolve();
const result = await cell;
expect(toolReturned).toBe(true);
expect(result.exitCode).toBe(0);
});
});
describe.skipIf(process.platform === "win32")("JavaScript eval process isolation", () => {
afterEach(async () => {
await disposeAllVmContexts();
});
it("preserves process isolation, cwd, state, and rejection recovery in one kernel", async () => {
using tempDir = TempDir.createSync("@omp-js-process-isolation-");
const session = makeSession(tempDir.path());
const evalSessionId = `js-isolation:${crypto.randomUUID()}`;
const result = await executeJs(
[
`const child = Bun.spawn(["/bin/sh", "-c", 'pgid=$(ps -o pgid= -p $$); printf "%s %s\\n" "$pgid" "$PPID"'], { stdout: "pipe" });`,
"return await new Response(child.stdout).text();",
].join("\n"),
{ cwd: tempDir.path(), sessionId: evalSessionId, session },
);
const [processGroupId, parentProcessId] = result.output.trim().split(/\s+/).map(Number);
expect(parentProcessId).not.toBe(process.pid);
expect(processGroupId).toBe(parentProcessId);
const cwd = await executeJs("return process.cwd();", {
cwd: tempDir.path(),
sessionId: evalSessionId,
session,
});
// process.chdir resolves symlinks (macOS tempdirs live under /var ->
// /private/var), so compare physical paths.
expect(cwd.output.trim()).toBe(fs.realpathSync(tempDir.path()));
await executeJs("var saved = 41; function increment(value) { return value + 1; }", {
cwd: tempDir.path(),
sessionId: evalSessionId,
session,
});
const reused = await executeJs("return increment(saved);", {
cwd: tempDir.path(),
sessionId: evalSessionId,
session,
});
expect(reused.output.trim()).toBe("42");
const handled = await executeJs('await Promise.reject("handled rejection").catch(() => undefined); return 42;', {
cwd: tempDir.path(),
sessionId: evalSessionId,
session,
});
expect(handled.exitCode).toBe(0);
expect(handled.output.trim()).toBe("42");
const rejected = await executeJs('var savedAfterRejection = 41; Promise.reject("stackless rejection");', {
cwd: tempDir.path(),
sessionId: evalSessionId,
session,
});
expect(rejected.exitCode).toBe(1);
expect(rejected.output).toContain("Unhandled rejection (missing await?): stackless rejection");
const recovered = await executeJs("return savedAfterRejection + 1;", {
cwd: tempDir.path(),
sessionId: evalSessionId,
session,
});
expect(recovered.exitCode).toBe(0);
expect(recovered.output.trim()).toBe("42");
});
it("still runs cells when the session cwd does not exist", async () => {
using tempDir = TempDir.createSync("@omp-js-process-cwd-missing-");
const missingCwd = path.join(tempDir.path(), "deleted");
const session = makeSession(missingCwd);
const result = await executeJs("return String(6 * 7);", {
cwd: missingCwd,
sessionId: `js-cwd-missing:${crypto.randomUUID()}`,
session,
});
expect(result.exitCode).toBe(0);
expect(result.output.trim()).toBe("42");
});
});
@@ -1,63 +0,0 @@
import { afterEach, describe, expect, it, vi } from "bun:test";
import { convertBufferWithMarkit } from "@oh-my-pi/pi-coding-agent/utils/markit";
import { logger } from "@oh-my-pi/pi-utils";
function warningPdf(): Uint8Array {
const objects: string[] = [];
function add(body: string): void {
objects.push(body);
}
const pageText = "/P <</MCID 0>> BDC\nBT /F1 24 Tf 72 720 Td (Tagged PDF repro text) Tj ET\nEMC\n";
add("<< /Type /Catalog /Pages 2 0 R /MarkInfo << /Marked true >> /StructTreeRoot 8 0 R >>");
add("<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
add(
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R /StructParents 0 /Annots [9 0 R] >>",
);
add("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>");
add(`<< /Length ${pageText.length} >>\nstream\n${pageText}endstream`);
add("<< /Nums [0 [7 0 R]] >>");
add("<< /Type /StructElem /S /P /P 8 0 R /Pg 3 0 R /K 99 >>");
add("<< /Type /StructTreeRoot /K [7 0 R] /ParentTree 6 0 R /ParentTreeNextKey 1 >>");
add("<< /Type /Annot /Subtype /Screen /Rect [72 650 200 700] /T (movie) >>");
let pdf = "%PDF-1.7\n";
const offsets = [0];
for (let i = 0; i < objects.length; i++) {
offsets.push(Buffer.byteLength(pdf));
pdf += `${i + 1} 0 obj\n${objects[i]}\nendobj\n`;
}
const xref = Buffer.byteLength(pdf);
pdf += `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`;
for (let i = 1; i < offsets.length; i++) {
pdf += `${String(offsets[i]).padStart(10, "0")} 00000 n \n`;
}
pdf += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`;
return new TextEncoder().encode(pdf);
}
describe("markit MuPDF warnings", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("routes recoverable PDF warnings to the file logger", async () => {
const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
const debug = vi.spyOn(logger, "debug").mockImplementation(() => undefined);
const result = await convertBufferWithMarkit(warningPdf(), ".pdf", undefined, { useCache: false });
expect(result.ok).toBe(true);
expect(result.content).toContain("Tagged PDF repro text");
expect(consoleError).not.toHaveBeenCalled();
expect(
debug.mock.calls.some(([message, metadata]) => {
if (message !== "mupdf wasm output" || typeof metadata !== "object" || metadata === null) return false;
if (!("stream" in metadata) || metadata.stream !== "stderr") return false;
return "message" in metadata && String(metadata.message).includes("Screen annotations");
}),
).toBe(true);
});
});