style: apply biome formatting to merged changes
This commit is contained in:
@@ -1746,39 +1746,36 @@ describe("AuthStorage codex oauth ranking", () => {
|
||||
test.each([
|
||||
["gpt-5.6-terra", "free", "enterprise"],
|
||||
["gpt-5.6-terra-pro", "go", "pro"],
|
||||
])(
|
||||
"%s keeps a less-used %s account in ordinary ranking ahead of %s",
|
||||
async (modelId, lowUsagePlan, highUsagePlan) => {
|
||||
if (!authStorage) throw new Error("test setup failed");
|
||||
])("%s keeps a less-used %s account in ordinary ranking ahead of %s", async (modelId, lowUsagePlan, highUsagePlan) => {
|
||||
if (!authStorage) throw new Error("test setup failed");
|
||||
|
||||
await authStorage.set("openai-codex", [
|
||||
{ type: "oauth", ...createCredential("acct-low-usage", "low-usage@example.com") },
|
||||
{ type: "oauth", ...createCredential("acct-high-usage", "high-usage@example.com") },
|
||||
]);
|
||||
await authStorage.set("openai-codex", [
|
||||
{ type: "oauth", ...createCredential("acct-low-usage", "low-usage@example.com") },
|
||||
{ type: "oauth", ...createCredential("acct-high-usage", "high-usage@example.com") },
|
||||
]);
|
||||
|
||||
usageByAccount.set(
|
||||
"acct-low-usage",
|
||||
createCodexUsageReport({
|
||||
accountId: "acct-low-usage",
|
||||
primary: { usedFraction: 0.01, resetInMs: 30 * 60 * 1000 },
|
||||
secondary: { usedFraction: 0.01, resetInMs: 6 * 24 * 60 * 60 * 1000 },
|
||||
metadata: { planType: lowUsagePlan, email: "low-usage@example.com" },
|
||||
}),
|
||||
);
|
||||
usageByAccount.set(
|
||||
"acct-high-usage",
|
||||
createCodexUsageReport({
|
||||
accountId: "acct-high-usage",
|
||||
primary: { usedFraction: 0.8, resetInMs: 30 * 60 * 1000 },
|
||||
secondary: { usedFraction: 0.8, resetInMs: 6 * 24 * 60 * 60 * 1000 },
|
||||
metadata: { planType: highUsagePlan, email: "high-usage@example.com" },
|
||||
}),
|
||||
);
|
||||
usageByAccount.set(
|
||||
"acct-low-usage",
|
||||
createCodexUsageReport({
|
||||
accountId: "acct-low-usage",
|
||||
primary: { usedFraction: 0.01, resetInMs: 30 * 60 * 1000 },
|
||||
secondary: { usedFraction: 0.01, resetInMs: 6 * 24 * 60 * 60 * 1000 },
|
||||
metadata: { planType: lowUsagePlan, email: "low-usage@example.com" },
|
||||
}),
|
||||
);
|
||||
usageByAccount.set(
|
||||
"acct-high-usage",
|
||||
createCodexUsageReport({
|
||||
accountId: "acct-high-usage",
|
||||
primary: { usedFraction: 0.8, resetInMs: 30 * 60 * 1000 },
|
||||
secondary: { usedFraction: 0.8, resetInMs: 6 * 24 * 60 * 60 * 1000 },
|
||||
metadata: { planType: highUsagePlan, email: "high-usage@example.com" },
|
||||
}),
|
||||
);
|
||||
|
||||
const apiKey = await authStorage.getApiKey("openai-codex", undefined, { modelId });
|
||||
expect(apiKey).toBe("api-acct-low-usage");
|
||||
},
|
||||
);
|
||||
const apiKey = await authStorage.getApiKey("openai-codex", undefined, { modelId });
|
||||
expect(apiKey).toBe("api-acct-low-usage");
|
||||
});
|
||||
|
||||
test("keeps an eligible Codex session credential when usage headroom makes its sibling rank better", async () => {
|
||||
if (!authStorage) throw new Error("test setup failed");
|
||||
@@ -2365,65 +2362,62 @@ describe("AuthStorage codex oauth ranking", () => {
|
||||
test.each([
|
||||
["gpt-5.6-sol", 0.06, 0.09, true, false, 1, 1, false, true, "spark"],
|
||||
["gpt-5.3-codex-spark", 1, 1, false, true, 0.06, 0.09, true, false, "chat"],
|
||||
] as const)(
|
||||
"reports %s healthy after splitting a legacy shared block when only its meter has headroom",
|
||||
async (modelId, chatPrimary, chatSecondary, chatAllowed, chatLimitReached, sparkPrimary, sparkSecondary, sparkAllowed, sparkLimitReached, remainingBlockScope) => {
|
||||
if (!authStorage || !store?.listCredentialBlocks) throw new Error("test setup failed");
|
||||
await authStorage.set("openai-codex", [
|
||||
{ type: "oauth", ...createCredential("acct-legacy-meter", "legacy-meter@example.com") },
|
||||
]);
|
||||
const [row] = store.listAuthCredentials("openai-codex");
|
||||
if (!row) throw new Error("expected credential row");
|
||||
const blockedUntilMs = Date.now() + WEEK_MS;
|
||||
insertLegacyCodexSharedBlock(
|
||||
dbPath,
|
||||
row.id,
|
||||
blockedUntilMs,
|
||||
Math.floor((Date.now() - STALE_BLOCK_GUARD_MS) / 1000),
|
||||
);
|
||||
usageByAccount.set(
|
||||
"acct-legacy-meter",
|
||||
addSparkUsage(
|
||||
createCodexUsageReport({
|
||||
] as const)("reports %s healthy after splitting a legacy shared block when only its meter has headroom", async (modelId, chatPrimary, chatSecondary, chatAllowed, chatLimitReached, sparkPrimary, sparkSecondary, sparkAllowed, sparkLimitReached, remainingBlockScope) => {
|
||||
if (!authStorage || !store?.listCredentialBlocks) throw new Error("test setup failed");
|
||||
await authStorage.set("openai-codex", [
|
||||
{ type: "oauth", ...createCredential("acct-legacy-meter", "legacy-meter@example.com") },
|
||||
]);
|
||||
const [row] = store.listAuthCredentials("openai-codex");
|
||||
if (!row) throw new Error("expected credential row");
|
||||
const blockedUntilMs = Date.now() + WEEK_MS;
|
||||
insertLegacyCodexSharedBlock(
|
||||
dbPath,
|
||||
row.id,
|
||||
blockedUntilMs,
|
||||
Math.floor((Date.now() - STALE_BLOCK_GUARD_MS) / 1000),
|
||||
);
|
||||
usageByAccount.set(
|
||||
"acct-legacy-meter",
|
||||
addSparkUsage(
|
||||
createCodexUsageReport({
|
||||
accountId: "acct-legacy-meter",
|
||||
primary: { usedFraction: chatPrimary, resetInMs: FIVE_HOUR_MS },
|
||||
secondary: { usedFraction: chatSecondary, resetInMs: WEEK_MS },
|
||||
metadata: {
|
||||
allowed: chatAllowed,
|
||||
limitReached: chatLimitReached,
|
||||
planType: "pro",
|
||||
email: "legacy-meter@example.com",
|
||||
accountId: "acct-legacy-meter",
|
||||
primary: { usedFraction: chatPrimary, resetInMs: FIVE_HOUR_MS },
|
||||
secondary: { usedFraction: chatSecondary, resetInMs: WEEK_MS },
|
||||
metadata: {
|
||||
allowed: chatAllowed,
|
||||
limitReached: chatLimitReached,
|
||||
planType: "pro",
|
||||
email: "legacy-meter@example.com",
|
||||
accountId: "acct-legacy-meter",
|
||||
},
|
||||
}),
|
||||
sparkPrimary,
|
||||
sparkSecondary,
|
||||
{ allowed: sparkAllowed, limitReached: sparkLimitReached },
|
||||
),
|
||||
);
|
||||
|
||||
const health = await authStorage.getModelUsageHealth("openai-codex", {
|
||||
modelId,
|
||||
reserveFraction: 0.1,
|
||||
});
|
||||
|
||||
expect(health).toMatchObject({
|
||||
state: "healthy",
|
||||
accounts: [
|
||||
{
|
||||
credentialId: row.id,
|
||||
credentialType: "oauth",
|
||||
state: "healthy",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(health.accounts[0]?.remainingFraction).toBeCloseTo(0.91, 10);
|
||||
expect(store.listCredentialBlocks([row.id]).map(block => [block.blockScope, block.blockedUntilMs])).toEqual([
|
||||
[remainingBlockScope, blockedUntilMs],
|
||||
]);
|
||||
expect(readLegacyCodexSharedBlock(dbPath, row.id)).toBe(blockedUntilMs);
|
||||
},
|
||||
);
|
||||
}),
|
||||
sparkPrimary,
|
||||
sparkSecondary,
|
||||
{ allowed: sparkAllowed, limitReached: sparkLimitReached },
|
||||
),
|
||||
);
|
||||
|
||||
const health = await authStorage.getModelUsageHealth("openai-codex", {
|
||||
modelId,
|
||||
reserveFraction: 0.1,
|
||||
});
|
||||
|
||||
expect(health).toMatchObject({
|
||||
state: "healthy",
|
||||
accounts: [
|
||||
{
|
||||
credentialId: row.id,
|
||||
credentialType: "oauth",
|
||||
state: "healthy",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(health.accounts[0]?.remainingFraction).toBeCloseTo(0.91, 10);
|
||||
expect(store.listCredentialBlocks([row.id]).map(block => [block.blockScope, block.blockedUntilMs])).toEqual([
|
||||
[remainingBlockScope, blockedUntilMs],
|
||||
]);
|
||||
expect(readLegacyCodexSharedBlock(dbPath, row.id)).toBe(blockedUntilMs);
|
||||
});
|
||||
|
||||
test("deletes only the recovered persisted Codex meter block", async () => {
|
||||
if (!authStorage || !store?.upsertCredentialBlock || !store.getCredentialBlock) {
|
||||
|
||||
@@ -201,24 +201,25 @@ describe("openai-codex reasoning.context", () => {
|
||||
|
||||
// gpt-5.1-codex / gpt-5.3-codex / gpt-5.3-codex-spark reject `all_turns`
|
||||
// ("Unsupported value: 'all_turns' is not supported with this model").
|
||||
it.each(["gpt-5.1-codex", "gpt-5.3-codex", "gpt-5.3-codex-spark"])(
|
||||
"omits the all_turns default for pre-5.4 model %s",
|
||||
async modelId => {
|
||||
const model = createCodexModel(modelId);
|
||||
it.each([
|
||||
"gpt-5.1-codex",
|
||||
"gpt-5.3-codex",
|
||||
"gpt-5.3-codex-spark",
|
||||
])("omits the all_turns default for pre-5.4 model %s", async modelId => {
|
||||
const model = createCodexModel(modelId);
|
||||
|
||||
const defaulted = await transformRequestBody({ model: model.id }, model, { reasoningEffort: "medium" });
|
||||
expect(defaulted.reasoning).toBeDefined();
|
||||
expect(defaulted.reasoning?.context).toBeUndefined();
|
||||
expect("context" in (defaulted.reasoning ?? {})).toBe(false);
|
||||
const defaulted = await transformRequestBody({ model: model.id }, model, { reasoningEffort: "medium" });
|
||||
expect(defaulted.reasoning).toBeDefined();
|
||||
expect(defaulted.reasoning?.context).toBeUndefined();
|
||||
expect("context" in (defaulted.reasoning ?? {})).toBe(false);
|
||||
|
||||
// A supported override (current_turn/auto) is still honored.
|
||||
const overridden = await transformRequestBody({ model: model.id }, model, {
|
||||
reasoningEffort: "medium",
|
||||
reasoningContext: "current_turn",
|
||||
});
|
||||
expect(overridden.reasoning?.context).toBe("current_turn");
|
||||
},
|
||||
);
|
||||
// A supported override (current_turn/auto) is still honored.
|
||||
const overridden = await transformRequestBody({ model: model.id }, model, {
|
||||
reasoningEffort: "medium",
|
||||
reasoningContext: "current_turn",
|
||||
});
|
||||
expect(overridden.reasoning?.context).toBe("current_turn");
|
||||
});
|
||||
|
||||
it("suppresses an explicit all_turns override on a pre-5.4 model", async () => {
|
||||
const model = createCodexModel("gpt-5.3-codex-spark");
|
||||
@@ -254,23 +255,24 @@ describe("openai-codex reasoning.summary", () => {
|
||||
|
||||
// gpt-5.1-codex / gpt-5.3-codex / gpt-5.3-codex-spark reject `reasoning.summary`
|
||||
// ("Unsupported parameter: 'reasoning.summary' is not supported with this model").
|
||||
it.each(["gpt-5.1-codex", "gpt-5.3-codex", "gpt-5.3-codex-spark"])(
|
||||
"omits reasoning.summary for pre-5.4 model %s",
|
||||
async modelId => {
|
||||
const model = createCodexModel(modelId);
|
||||
it.each([
|
||||
"gpt-5.1-codex",
|
||||
"gpt-5.3-codex",
|
||||
"gpt-5.3-codex-spark",
|
||||
])("omits reasoning.summary for pre-5.4 model %s", async modelId => {
|
||||
const model = createCodexModel(modelId);
|
||||
|
||||
const defaulted = await transformRequestBody({ model: model.id }, model, { reasoningEffort: "medium" });
|
||||
expect(defaulted.reasoning).toBeDefined();
|
||||
expect("summary" in (defaulted.reasoning ?? {})).toBe(false);
|
||||
const defaulted = await transformRequestBody({ model: model.id }, model, { reasoningEffort: "medium" });
|
||||
expect(defaulted.reasoning).toBeDefined();
|
||||
expect("summary" in (defaulted.reasoning ?? {})).toBe(false);
|
||||
|
||||
// Even an explicit summary level is suppressed on unsupported ids.
|
||||
const forced = await transformRequestBody({ model: model.id }, model, {
|
||||
reasoningEffort: "medium",
|
||||
reasoningSummary: "detailed",
|
||||
});
|
||||
expect("summary" in (forced.reasoning ?? {})).toBe(false);
|
||||
},
|
||||
);
|
||||
// Even an explicit summary level is suppressed on unsupported ids.
|
||||
const forced = await transformRequestBody({ model: model.id }, model, {
|
||||
reasoningEffort: "medium",
|
||||
reasoningSummary: "detailed",
|
||||
});
|
||||
expect("summary" in (forced.reasoning ?? {})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("openai-codex Responses Lite input shaping", () => {
|
||||
@@ -375,21 +377,23 @@ describe("openai-codex Responses Lite input shaping", () => {
|
||||
expect(disabled.tools).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each(["gpt-5.3-codex-spark", "gpt-5.6-luna", "gpt-5.6-terra", "gpt-5.6-sol"])(
|
||||
"preserves a forced computer function through Lite for %s",
|
||||
async modelId => {
|
||||
const model = createCodexModel(modelId);
|
||||
const computer = { type: "function", name: "computer", parameters: { type: "object" } };
|
||||
const other = { type: "function", name: "read", parameters: { type: "object" } };
|
||||
const body = await transformRequestBody(
|
||||
{ model: model.id, tools: [computer, other], tool_choice: { type: "function", name: "computer" } },
|
||||
model,
|
||||
{ responsesLite: true },
|
||||
);
|
||||
expect(body.input?.[0]).toEqual({ type: "additional_tools", role: "developer", tools: [computer] });
|
||||
expect(body.tool_choice).toBe("required");
|
||||
},
|
||||
);
|
||||
it.each([
|
||||
"gpt-5.3-codex-spark",
|
||||
"gpt-5.6-luna",
|
||||
"gpt-5.6-terra",
|
||||
"gpt-5.6-sol",
|
||||
])("preserves a forced computer function through Lite for %s", async modelId => {
|
||||
const model = createCodexModel(modelId);
|
||||
const computer = { type: "function", name: "computer", parameters: { type: "object" } };
|
||||
const other = { type: "function", name: "read", parameters: { type: "object" } };
|
||||
const body = await transformRequestBody(
|
||||
{ model: model.id, tools: [computer, other], tool_choice: { type: "function", name: "computer" } },
|
||||
model,
|
||||
{ responsesLite: true },
|
||||
);
|
||||
expect(body.input?.[0]).toEqual({ type: "additional_tools", role: "developer", tools: [computer] });
|
||||
expect(body.tool_choice).toBe("required");
|
||||
});
|
||||
|
||||
it("moves instructions and tools into input items under lite", async () => {
|
||||
const model = createCodexModel("gpt-5.6-terra");
|
||||
|
||||
@@ -38,23 +38,23 @@ describe("opencode-zen/-go resolver routes MiniMax M3 to openai-completions (iss
|
||||
const npmAnthropic: ModelsDevModel = { provider: { npm: "@ai-sdk/anthropic" }, tool_call: true };
|
||||
|
||||
describe("opencode-zen", () => {
|
||||
test.each([["minimax-m3"], ["minimax-m3-free"]])(
|
||||
"%s resolves to openai-completions on /v1/chat/completions",
|
||||
modelId => {
|
||||
const resolved = zenDescriptor?.resolveApi?.(modelId, npmAnthropic);
|
||||
expect(resolved).toEqual({ api: "openai-completions", baseUrl: OPENCODE_ZEN_BASE });
|
||||
},
|
||||
);
|
||||
test.each([
|
||||
["minimax-m3"],
|
||||
["minimax-m3-free"],
|
||||
])("%s resolves to openai-completions on /v1/chat/completions", modelId => {
|
||||
const resolved = zenDescriptor?.resolveApi?.(modelId, npmAnthropic);
|
||||
expect(resolved).toEqual({ api: "openai-completions", baseUrl: OPENCODE_ZEN_BASE });
|
||||
});
|
||||
});
|
||||
|
||||
describe("opencode-go", () => {
|
||||
test.each([["minimax-m3"], ["minimax-m3-free"]])(
|
||||
"%s resolves to openai-completions on /v1/chat/completions",
|
||||
modelId => {
|
||||
const resolved = goDescriptor?.resolveApi?.(modelId, npmAnthropic);
|
||||
expect(resolved).toEqual({ api: "openai-completions", baseUrl: OPENCODE_GO_BASE });
|
||||
},
|
||||
);
|
||||
test.each([
|
||||
["minimax-m3"],
|
||||
["minimax-m3-free"],
|
||||
])("%s resolves to openai-completions on /v1/chat/completions", modelId => {
|
||||
const resolved = goDescriptor?.resolveApi?.(modelId, npmAnthropic);
|
||||
expect(resolved).toEqual({ api: "openai-completions", baseUrl: OPENCODE_GO_BASE });
|
||||
});
|
||||
});
|
||||
|
||||
test("opencode-zen /v1/models refresh routes a freshly-discovered M3 to openai-completions", async () => {
|
||||
|
||||
@@ -26,13 +26,14 @@ describe("opencode-go resolver routes 404-ing ids to openai-completions (issue #
|
||||
// would route them to /v1/messages on opencode.ai/zen/go which 404s.
|
||||
const npmAnthropic: ModelsDevModel = { provider: { npm: "@ai-sdk/anthropic" }, tool_call: true };
|
||||
|
||||
test.each([["minimax-m2.7"], ["qwen3.5-plus"], ["qwen3.6-plus"]])(
|
||||
"%s resolves to openai-completions on /v1/chat/completions",
|
||||
modelId => {
|
||||
const resolved = descriptor?.resolveApi?.(modelId, npmAnthropic);
|
||||
expect(resolved).toEqual({ api: "openai-completions", baseUrl: OPENCODE_GO_BASE });
|
||||
},
|
||||
);
|
||||
test.each([
|
||||
["minimax-m2.7"],
|
||||
["qwen3.5-plus"],
|
||||
["qwen3.6-plus"],
|
||||
])("%s resolves to openai-completions on /v1/chat/completions", modelId => {
|
||||
const resolved = descriptor?.resolveApi?.(modelId, npmAnthropic);
|
||||
expect(resolved).toEqual({ api: "openai-completions", baseUrl: OPENCODE_GO_BASE });
|
||||
});
|
||||
|
||||
test("minimax-m2.5 (control: works empirically) also resolves to openai-completions", () => {
|
||||
// models.dev currently lists minimax-m2.5 without an explicit provider.npm,
|
||||
|
||||
@@ -428,60 +428,61 @@ describe("LiteLLM provider discovery", () => {
|
||||
expect(models?.find(model => model.id === "params-tools")?.supportsTools).toBe(true);
|
||||
});
|
||||
|
||||
test.each([["all-team-models"], ["all-proxy-models"], ["no-default-models"]])(
|
||||
"falls back from %s placeholder to v2 model info",
|
||||
async sentinelModelId => {
|
||||
const calls: string[] = [];
|
||||
const fetchMock = vi.fn(async (input: string | URL | Request) => {
|
||||
const url = inputUrl(input);
|
||||
calls.push(url);
|
||||
if (url === MODELS_DEV_URL) {
|
||||
return Response.json({});
|
||||
}
|
||||
if (url === "http://primary:4000/model_group/info") {
|
||||
return Response.json({ data: [makeLiteLLMSentinelPlaceholder(sentinelModelId)] });
|
||||
}
|
||||
if (url === "http://primary:4000/v2/model/info") {
|
||||
return Response.json({
|
||||
data: [
|
||||
{
|
||||
model_name: "example-real-model",
|
||||
model_info: {
|
||||
max_input_tokens: 200_000,
|
||||
max_output_tokens: 12_000,
|
||||
supports_vision: false,
|
||||
supports_reasoning: true,
|
||||
},
|
||||
test.each([
|
||||
["all-team-models"],
|
||||
["all-proxy-models"],
|
||||
["no-default-models"],
|
||||
])("falls back from %s placeholder to v2 model info", async sentinelModelId => {
|
||||
const calls: string[] = [];
|
||||
const fetchMock = vi.fn(async (input: string | URL | Request) => {
|
||||
const url = inputUrl(input);
|
||||
calls.push(url);
|
||||
if (url === MODELS_DEV_URL) {
|
||||
return Response.json({});
|
||||
}
|
||||
if (url === "http://primary:4000/model_group/info") {
|
||||
return Response.json({ data: [makeLiteLLMSentinelPlaceholder(sentinelModelId)] });
|
||||
}
|
||||
if (url === "http://primary:4000/v2/model/info") {
|
||||
return Response.json({
|
||||
data: [
|
||||
{
|
||||
model_name: "example-real-model",
|
||||
model_info: {
|
||||
max_input_tokens: 200_000,
|
||||
max_output_tokens: 12_000,
|
||||
supports_vision: false,
|
||||
supports_reasoning: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (url === "http://primary:4000/v1/models") {
|
||||
throw new Error("/v1/models should not be called when v2 metadata succeeds");
|
||||
}
|
||||
throw new Error(`Unexpected URL: ${url}`);
|
||||
}) as FetchImpl;
|
||||
const options = litellmModelManagerOptions({
|
||||
apiKey: "sk-rich",
|
||||
baseUrl: "http://primary:4000/v1",
|
||||
fetch: fetchMock,
|
||||
});
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (url === "http://primary:4000/v1/models") {
|
||||
throw new Error("/v1/models should not be called when v2 metadata succeeds");
|
||||
}
|
||||
throw new Error(`Unexpected URL: ${url}`);
|
||||
}) as FetchImpl;
|
||||
const options = litellmModelManagerOptions({
|
||||
apiKey: "sk-rich",
|
||||
baseUrl: "http://primary:4000/v1",
|
||||
fetch: fetchMock,
|
||||
});
|
||||
|
||||
const models = await options.fetchDynamicModels?.();
|
||||
const models = await options.fetchDynamicModels?.();
|
||||
|
||||
expect(calls).toContain("http://primary:4000/model_group/info");
|
||||
expect(calls).toContain("http://primary:4000/v2/model/info");
|
||||
expect(calls).not.toContain("http://primary:4000/v1/models");
|
||||
expect(models?.map(model => model.id)).toEqual(["example-real-model"]);
|
||||
expect(models?.[0]).toMatchObject({
|
||||
id: "example-real-model",
|
||||
contextWindow: 200_000,
|
||||
maxTokens: 12_000,
|
||||
input: ["text"],
|
||||
reasoning: true,
|
||||
});
|
||||
},
|
||||
);
|
||||
expect(calls).toContain("http://primary:4000/model_group/info");
|
||||
expect(calls).toContain("http://primary:4000/v2/model/info");
|
||||
expect(calls).not.toContain("http://primary:4000/v1/models");
|
||||
expect(models?.map(model => model.id)).toEqual(["example-real-model"]);
|
||||
expect(models?.[0]).toMatchObject({
|
||||
id: "example-real-model",
|
||||
contextWindow: 200_000,
|
||||
maxTokens: 12_000,
|
||||
input: ["text"],
|
||||
reasoning: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("filters all-team-models placeholder from mixed model_group info", async () => {
|
||||
const calls: string[] = [];
|
||||
|
||||
@@ -4035,60 +4035,60 @@ describe("advisor", () => {
|
||||
expect(promptInputs[1]).toContain("keep me");
|
||||
});
|
||||
|
||||
it.each(["success", "error"] as const)(
|
||||
"releases blocked %s hooks so reset can run replacement work",
|
||||
async hookKind => {
|
||||
const hookStarted = Promise.withResolvers<void>();
|
||||
const releaseHook = Promise.withResolvers<void>();
|
||||
const replacementPromptStarted = Promise.withResolvers<void>();
|
||||
let promptCalls = 0;
|
||||
let hookCalls = 0;
|
||||
const blockHook = async () => {
|
||||
if (++hookCalls !== 1) return;
|
||||
hookStarted.resolve();
|
||||
await releaseHook.promise;
|
||||
};
|
||||
const agent: AdvisorAgent = {
|
||||
prompt: async () => {
|
||||
promptCalls++;
|
||||
if (promptCalls === 1 && hookKind === "error") throw new Error("provider failure");
|
||||
if (promptCalls === 2) replacementPromptStarted.resolve();
|
||||
},
|
||||
abort: () => {},
|
||||
reset: () => {},
|
||||
state: { messages: [] },
|
||||
};
|
||||
const runtime = new AdvisorRuntime(agent, {
|
||||
snapshotMessages: () => [],
|
||||
enqueueAdvice: () => {},
|
||||
...(hookKind === "success"
|
||||
? { onTurnSuccess: blockHook }
|
||||
: {
|
||||
onTurnError: async () => {
|
||||
await blockHook();
|
||||
return false;
|
||||
},
|
||||
}),
|
||||
});
|
||||
it.each([
|
||||
"success",
|
||||
"error",
|
||||
] as const)("releases blocked %s hooks so reset can run replacement work", async hookKind => {
|
||||
const hookStarted = Promise.withResolvers<void>();
|
||||
const releaseHook = Promise.withResolvers<void>();
|
||||
const replacementPromptStarted = Promise.withResolvers<void>();
|
||||
let promptCalls = 0;
|
||||
let hookCalls = 0;
|
||||
const blockHook = async () => {
|
||||
if (++hookCalls !== 1) return;
|
||||
hookStarted.resolve();
|
||||
await releaseHook.promise;
|
||||
};
|
||||
const agent: AdvisorAgent = {
|
||||
prompt: async () => {
|
||||
promptCalls++;
|
||||
if (promptCalls === 1 && hookKind === "error") throw new Error("provider failure");
|
||||
if (promptCalls === 2) replacementPromptStarted.resolve();
|
||||
},
|
||||
abort: () => {},
|
||||
reset: () => {},
|
||||
state: { messages: [] },
|
||||
};
|
||||
const runtime = new AdvisorRuntime(agent, {
|
||||
snapshotMessages: () => [],
|
||||
enqueueAdvice: () => {},
|
||||
...(hookKind === "success"
|
||||
? { onTurnSuccess: blockHook }
|
||||
: {
|
||||
onTurnError: async () => {
|
||||
await blockHook();
|
||||
return false;
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
runtime.onTurnEnd([{ role: "user", content: "old session", timestamp: 1 } as AgentMessage]);
|
||||
await hookStarted.promise;
|
||||
const pause = runtime.pauseForSessionTransition();
|
||||
const pausedQuickly = await Promise.race([pause.then(() => true), Bun.sleep(50).then(() => false)]);
|
||||
runtime.reset();
|
||||
runtime.onTurnEnd([{ role: "user", content: "replacement session", timestamp: 2 } as AgentMessage]);
|
||||
const replacementRan = await Promise.race([
|
||||
replacementPromptStarted.promise.then(() => true),
|
||||
Bun.sleep(50).then(() => false),
|
||||
]);
|
||||
releaseHook.resolve();
|
||||
await pause;
|
||||
runtime.dispose();
|
||||
runtime.onTurnEnd([{ role: "user", content: "old session", timestamp: 1 } as AgentMessage]);
|
||||
await hookStarted.promise;
|
||||
const pause = runtime.pauseForSessionTransition();
|
||||
const pausedQuickly = await Promise.race([pause.then(() => true), Bun.sleep(50).then(() => false)]);
|
||||
runtime.reset();
|
||||
runtime.onTurnEnd([{ role: "user", content: "replacement session", timestamp: 2 } as AgentMessage]);
|
||||
const replacementRan = await Promise.race([
|
||||
replacementPromptStarted.promise.then(() => true),
|
||||
Bun.sleep(50).then(() => false),
|
||||
]);
|
||||
releaseHook.resolve();
|
||||
await pause;
|
||||
runtime.dispose();
|
||||
|
||||
expect(pausedQuickly).toBe(true);
|
||||
expect(replacementRan).toBe(true);
|
||||
},
|
||||
);
|
||||
expect(pausedQuickly).toBe(true);
|
||||
expect(replacementRan).toBe(true);
|
||||
});
|
||||
it("aborts retry backoff before pausing for a session transition", async () => {
|
||||
const recoveryStarted = Promise.withResolvers<void>();
|
||||
const agent: AdvisorAgent = {
|
||||
|
||||
@@ -849,57 +849,56 @@ it("allow_always: caches decision and calls bridge only once for subsequent exec
|
||||
expect(bashTool.executeCalls).toBe(2);
|
||||
});
|
||||
|
||||
it.each(boundaryCases)(
|
||||
"%s permission decisions prompt again after a successful %s session boundary",
|
||||
async (decision, transition) => {
|
||||
const bashTool = makeFakeTool("bash");
|
||||
const bridge = makeBridge({ outcome: "selected", optionId: decision, kind: decision });
|
||||
const permissionSpy = spyOn(bridge, "requestPermission");
|
||||
session = await createSession([bashTool], bridge, {}, { persist: true });
|
||||
it.each(
|
||||
boundaryCases,
|
||||
)("%s permission decisions prompt again after a successful %s session boundary", async (decision, transition) => {
|
||||
const bashTool = makeFakeTool("bash");
|
||||
const bridge = makeBridge({ outcome: "selected", optionId: decision, kind: decision });
|
||||
const permissionSpy = spyOn(bridge, "requestPermission");
|
||||
session = await createSession([bashTool], bridge, {}, { persist: true });
|
||||
|
||||
await session.setActiveToolsByName(["bash"]);
|
||||
const wrappedBash = session.agent.state.tools.find(tool => tool.name === "bash");
|
||||
if (!wrappedBash) throw new Error("Expected wrapped bash tool");
|
||||
await session.setActiveToolsByName(["bash"]);
|
||||
const wrappedBash = session.agent.state.tools.find(tool => tool.name === "bash");
|
||||
if (!wrappedBash) throw new Error("Expected wrapped bash tool");
|
||||
|
||||
for (let callIndex = 0; callIndex < 2; callIndex++) {
|
||||
if (callIndex === 1) {
|
||||
if (transition === "new") {
|
||||
expect(await session.newSession()).toBe(true);
|
||||
} else {
|
||||
const targetId = `permission-target-${Bun.nanoseconds()}`;
|
||||
const targetPath = `${tempDir.path()}/${targetId}.jsonl`;
|
||||
await Bun.write(
|
||||
targetPath,
|
||||
`${JSON.stringify({
|
||||
type: "session",
|
||||
version: 3,
|
||||
id: targetId,
|
||||
timestamp: new Date().toISOString(),
|
||||
cwd: tempDir.path(),
|
||||
})}\n`,
|
||||
);
|
||||
expect(await session.switchSession(targetPath)).toBe(true);
|
||||
}
|
||||
}
|
||||
|
||||
const execution = wrappedBash.execute(
|
||||
`call-${callIndex}`,
|
||||
{ command: "echo boundary" },
|
||||
undefined,
|
||||
undefined as never,
|
||||
undefined as never,
|
||||
);
|
||||
if (decision === "reject_always") {
|
||||
await expect(execution).rejects.toThrow(/rejected by user/);
|
||||
for (let callIndex = 0; callIndex < 2; callIndex++) {
|
||||
if (callIndex === 1) {
|
||||
if (transition === "new") {
|
||||
expect(await session.newSession()).toBe(true);
|
||||
} else {
|
||||
await execution;
|
||||
const targetId = `permission-target-${Bun.nanoseconds()}`;
|
||||
const targetPath = `${tempDir.path()}/${targetId}.jsonl`;
|
||||
await Bun.write(
|
||||
targetPath,
|
||||
`${JSON.stringify({
|
||||
type: "session",
|
||||
version: 3,
|
||||
id: targetId,
|
||||
timestamp: new Date().toISOString(),
|
||||
cwd: tempDir.path(),
|
||||
})}\n`,
|
||||
);
|
||||
expect(await session.switchSession(targetPath)).toBe(true);
|
||||
}
|
||||
}
|
||||
|
||||
expect(permissionSpy).toHaveBeenCalledTimes(2);
|
||||
expect(bashTool.executeCalls).toBe(decision === "allow_always" ? 2 : 0);
|
||||
},
|
||||
);
|
||||
const execution = wrappedBash.execute(
|
||||
`call-${callIndex}`,
|
||||
{ command: "echo boundary" },
|
||||
undefined,
|
||||
undefined as never,
|
||||
undefined as never,
|
||||
);
|
||||
if (decision === "reject_always") {
|
||||
await expect(execution).rejects.toThrow(/rejected by user/);
|
||||
} else {
|
||||
await execution;
|
||||
}
|
||||
}
|
||||
|
||||
expect(permissionSpy).toHaveBeenCalledTimes(2);
|
||||
expect(bashTool.executeCalls).toBe(decision === "allow_always" ? 2 : 0);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 4. Read tool not gated: bridge never called even when bridge is set
|
||||
|
||||
@@ -203,67 +203,68 @@ describe("AgentSession bash session ownership", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["new", "switch", "branch"] as const)(
|
||||
"records a late bash result in its original session after %s",
|
||||
async transition => {
|
||||
const sessionDir = path.join(tempDir.path(), "sessions");
|
||||
const { completion, emitUserBash, extensionRunner } = createGatedBashRunner();
|
||||
createSession(SessionManager.create(tempDir.path(), sessionDir), extensionRunner);
|
||||
const oldSessionFile = await seedPersistedSession();
|
||||
const oldSessionId = session.sessionId;
|
||||
it.each([
|
||||
"new",
|
||||
"switch",
|
||||
"branch",
|
||||
] as const)("records a late bash result in its original session after %s", async transition => {
|
||||
const sessionDir = path.join(tempDir.path(), "sessions");
|
||||
const { completion, emitUserBash, extensionRunner } = createGatedBashRunner();
|
||||
createSession(SessionManager.create(tempDir.path(), sessionDir), extensionRunner);
|
||||
const oldSessionFile = await seedPersistedSession();
|
||||
const oldSessionId = session.sessionId;
|
||||
|
||||
const bashPromise = session.executeBash("old-session-command");
|
||||
expect(emitUserBash).toHaveBeenCalledTimes(1);
|
||||
const bashPromise = session.executeBash("old-session-command");
|
||||
expect(emitUserBash).toHaveBeenCalledTimes(1);
|
||||
|
||||
switch (transition) {
|
||||
case "new":
|
||||
await session.newSession();
|
||||
break;
|
||||
case "switch": {
|
||||
const targetManager = SessionManager.create(tempDir.path(), sessionDir);
|
||||
targetManager.appendMessage({ role: "user", content: "target", timestamp: Date.now() });
|
||||
targetManager.appendMessage(createAssistantMessage("target reply"));
|
||||
await targetManager.ensureOnDisk();
|
||||
const targetFile = targetManager.getSessionFile();
|
||||
if (!targetFile) throw new Error("Expected target session file");
|
||||
await targetManager.close();
|
||||
await session.switchSession(targetFile);
|
||||
break;
|
||||
}
|
||||
case "branch": {
|
||||
const userEntry = session.sessionManager
|
||||
.getEntries()
|
||||
.find(entry => entry.type === "message" && entry.message.role === "user");
|
||||
if (!userEntry) throw new Error("Expected user entry for branch");
|
||||
await session.branch(userEntry.id);
|
||||
break;
|
||||
}
|
||||
switch (transition) {
|
||||
case "new":
|
||||
await session.newSession();
|
||||
break;
|
||||
case "switch": {
|
||||
const targetManager = SessionManager.create(tempDir.path(), sessionDir);
|
||||
targetManager.appendMessage({ role: "user", content: "target", timestamp: Date.now() });
|
||||
targetManager.appendMessage(createAssistantMessage("target reply"));
|
||||
await targetManager.ensureOnDisk();
|
||||
const targetFile = targetManager.getSessionFile();
|
||||
if (!targetFile) throw new Error("Expected target session file");
|
||||
await targetManager.close();
|
||||
await session.switchSession(targetFile);
|
||||
break;
|
||||
}
|
||||
case "branch": {
|
||||
const userEntry = session.sessionManager
|
||||
.getEntries()
|
||||
.find(entry => entry.type === "message" && entry.message.role === "user");
|
||||
if (!userEntry) throw new Error("Expected user entry for branch");
|
||||
await session.branch(userEntry.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect(session.sessionId).not.toBe(oldSessionId);
|
||||
completion.resolve({ result: bashResult });
|
||||
await bashPromise;
|
||||
expect(session.sessionId).not.toBe(oldSessionId);
|
||||
completion.resolve({ result: bashResult });
|
||||
await bashPromise;
|
||||
|
||||
expect(
|
||||
session.messages.some(
|
||||
message => message.role === "bashExecution" && message.command === "old-session-command",
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
session.messages.some(
|
||||
message => message.role === "bashExecution" && message.command === "old-session-command",
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
const oldSession = await SessionManager.open(oldSessionFile, sessionDir, undefined, {
|
||||
initialCwd: tempDir.path(),
|
||||
suppressBreadcrumb: true,
|
||||
});
|
||||
additionalManagers.push(oldSession);
|
||||
const oldMessages = oldSession.getBranch().flatMap(entry => (entry.type === "message" ? [entry.message] : []));
|
||||
expect(oldMessages.slice(-3).map(message => message.role)).toEqual(["user", "assistant", "bashExecution"]);
|
||||
expect(oldMessages.at(-1)).toMatchObject({
|
||||
role: "bashExecution",
|
||||
command: "old-session-command",
|
||||
output: "old-output",
|
||||
});
|
||||
},
|
||||
);
|
||||
const oldSession = await SessionManager.open(oldSessionFile, sessionDir, undefined, {
|
||||
initialCwd: tempDir.path(),
|
||||
suppressBreadcrumb: true,
|
||||
});
|
||||
additionalManagers.push(oldSession);
|
||||
const oldMessages = oldSession.getBranch().flatMap(entry => (entry.type === "message" ? [entry.message] : []));
|
||||
expect(oldMessages.slice(-3).map(message => message.role)).toEqual(["user", "assistant", "bashExecution"]);
|
||||
expect(oldMessages.at(-1)).toMatchObject({
|
||||
role: "bashExecution",
|
||||
command: "old-session-command",
|
||||
output: "old-output",
|
||||
});
|
||||
});
|
||||
|
||||
it("stores minimized bash output with the originating session", async () => {
|
||||
const sessionDir = path.join(tempDir.path(), "sessions");
|
||||
|
||||
@@ -1878,13 +1878,14 @@ describe("InteractiveMode plan review rendering", () => {
|
||||
// `#approvePlan`'s `finally`. No aborted message_end is required to consume it,
|
||||
// so a stranded flag could otherwise silence the next unrelated abort. One
|
||||
// parametrized case per outcome keeps ok/cancelled/failed each covered.
|
||||
it.each(["ok", "cancelled", "failed"] as const)(
|
||||
"B1-B3: Approve and compact context + %s outcome → flag cleared by finally",
|
||||
async outcome => {
|
||||
await approveWithCompact(outcome);
|
||||
expect(session.isPlanInternalAbortPending).toBe(false);
|
||||
},
|
||||
);
|
||||
it.each([
|
||||
"ok",
|
||||
"cancelled",
|
||||
"failed",
|
||||
] as const)("B1-B3: Approve and compact context + %s outcome → flag cleared by finally", async outcome => {
|
||||
await approveWithCompact(outcome);
|
||||
expect(session.isPlanInternalAbortPending).toBe(false);
|
||||
});
|
||||
|
||||
it("B4: Approve and compact context + handleCompactCommand throws → showError surfaces the failure AND flag cleared by finally before the outer catch", async () => {
|
||||
// `handlePlanApproval` wraps `#approvePlan` in a try/catch
|
||||
|
||||
@@ -315,27 +315,27 @@ describe("MemoryProtocolHandler", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["memory://root/skills/**/../*.md", "memory://root/skills/**/%2e%2e/*.md"])(
|
||||
"rejects traversal in a memory glob suffix: %s",
|
||||
async pattern => {
|
||||
await withMemoryFixture(async ({ cwd }) => {
|
||||
await expect(createGlobTool(cwd).execute("memory-glob-traversal", { path: pattern })).rejects.toThrow(
|
||||
/traversal/i,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
it.each([
|
||||
"memory://root/skills/**/../*.md",
|
||||
"memory://root/skills/**/%2e%2e/*.md",
|
||||
])("rejects traversal in a memory glob suffix: %s", async pattern => {
|
||||
await withMemoryFixture(async ({ cwd }) => {
|
||||
await expect(createGlobTool(cwd).execute("memory-glob-traversal", { path: pattern })).rejects.toThrow(
|
||||
/traversal/i,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["memory://root/skills/**/demo%2fnested/*.md", "memory://root/skills/**/demo%5cnested/*.md"])(
|
||||
"rejects encoded separators in a memory glob suffix: %s",
|
||||
async pattern => {
|
||||
await withMemoryFixture(async ({ cwd }) => {
|
||||
await expect(createGlobTool(cwd).execute("memory-glob-separator", { path: pattern })).rejects.toThrow(
|
||||
/encoded path separator/i,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
it.each([
|
||||
"memory://root/skills/**/demo%2fnested/*.md",
|
||||
"memory://root/skills/**/demo%5cnested/*.md",
|
||||
])("rejects encoded separators in a memory glob suffix: %s", async pattern => {
|
||||
await withMemoryFixture(async ({ cwd }) => {
|
||||
await expect(createGlobTool(cwd).execute("memory-glob-separator", { path: pattern })).rejects.toThrow(
|
||||
/encoded path separator/i,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("throws clear error for missing files", async () => {
|
||||
await withMemoryFixture(async () => {
|
||||
|
||||
@@ -322,10 +322,7 @@ describe("RPC fast mode with unsupported Fireworks model and priority tier", ()
|
||||
|
||||
beforeEach(async () => {
|
||||
sessionDir = path.join(os.tmpdir(), `omp-rpc-fast-mode-test-${Snowflake.next()}`);
|
||||
await Bun.write(
|
||||
path.join(sessionDir, "config.yml"),
|
||||
["providers:", " fireworksTier: priority", ""].join("\n"),
|
||||
);
|
||||
await Bun.write(path.join(sessionDir, "config.yml"), ["providers:", " fireworksTier: priority", ""].join("\n"));
|
||||
client = new RpcClient({
|
||||
cliPath: path.join(import.meta.dir, "..", "src", "cli.ts"),
|
||||
cwd: path.join(import.meta.dir, ".."),
|
||||
|
||||
@@ -6,8 +6,8 @@ import { AsyncJobManager } from "@oh-my-pi/pi-coding-agent/async/job-manager";
|
||||
import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
|
||||
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import { createAgentSession, type ExtensionFactory } from "@oh-my-pi/pi-coding-agent/sdk";
|
||||
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
|
||||
import type { AsyncJobSnapshot } from "@oh-my-pi/pi-coding-agent/session/agent-session";
|
||||
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
|
||||
import { removeSyncWithRetries, Snowflake } from "@oh-my-pi/pi-utils";
|
||||
import { type } from "arktype";
|
||||
|
||||
@@ -40,10 +40,7 @@ describe("AsyncJobManager singleton across concurrent top-level sessions", () =>
|
||||
AsyncJobManager.resetForTests();
|
||||
});
|
||||
|
||||
async function spawnTopLevelSession(
|
||||
extraSettings?: Record<string, unknown>,
|
||||
extensions: ExtensionFactory[] = [],
|
||||
) {
|
||||
async function spawnTopLevelSession(extraSettings?: Record<string, unknown>, extensions: ExtensionFactory[] = []) {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `pi-sdk-async-singleton-${Snowflake.next()}-`));
|
||||
tempDirs.push(tempDir);
|
||||
const cwd = path.join(tempDir, `project-${Snowflake.next()}`);
|
||||
|
||||
@@ -97,19 +97,22 @@ describe("task async preflight", () => {
|
||||
spawns: "scout",
|
||||
expectation: "Cannot spawn 'task'",
|
||||
},
|
||||
])(
|
||||
"returns $name policy errors before registering an async job",
|
||||
async ({ name, params, settings, spawns, expectation }) => {
|
||||
mockDiscovery();
|
||||
const jobs = manager();
|
||||
const tool = await TaskTool.create(createSession({ manager: jobs, settings, spawns }));
|
||||
])("returns $name policy errors before registering an async job", async ({
|
||||
name,
|
||||
params,
|
||||
settings,
|
||||
spawns,
|
||||
expectation,
|
||||
}) => {
|
||||
mockDiscovery();
|
||||
const jobs = manager();
|
||||
const tool = await TaskTool.create(createSession({ manager: jobs, settings, spawns }));
|
||||
|
||||
const result = await tool.execute("preflight", params as TaskParams);
|
||||
const result = await tool.execute("preflight", params as TaskParams);
|
||||
|
||||
expect(textOf(result)).toContain(expectation);
|
||||
expect(jobs.getJob(name)).toBeUndefined();
|
||||
},
|
||||
);
|
||||
expect(textOf(result)).toContain(expectation);
|
||||
expect(jobs.getJob(name)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects an invalid async batch atomically before dispatching any item", async () => {
|
||||
mockDiscovery();
|
||||
|
||||
@@ -427,24 +427,25 @@ describe("title generator", () => {
|
||||
expect(title).toBe("Fix login button on mobile");
|
||||
});
|
||||
|
||||
it.each(["Here's a thinking process:", "Thinking process:", "Reasoning process:"])(
|
||||
"rejects a markerless prose thinking preamble: %s",
|
||||
async responseText => {
|
||||
const model = getModelFor("deepseek", "deepseek-v4-pro");
|
||||
vi.spyOn(ai, "completeSimple").mockResolvedValue({
|
||||
stopReason: "stop",
|
||||
content: [{ type: "text", text: responseText }],
|
||||
} as never);
|
||||
it.each([
|
||||
"Here's a thinking process:",
|
||||
"Thinking process:",
|
||||
"Reasoning process:",
|
||||
])("rejects a markerless prose thinking preamble: %s", async responseText => {
|
||||
const model = getModelFor("deepseek", "deepseek-v4-pro");
|
||||
vi.spyOn(ai, "completeSimple").mockResolvedValue({
|
||||
stopReason: "stop",
|
||||
content: [{ type: "text", text: responseText }],
|
||||
} as never);
|
||||
|
||||
const title = await generateSessionTitle(
|
||||
"the login button is broken on mobile",
|
||||
createRegistry(model),
|
||||
createSettings(model),
|
||||
);
|
||||
const title = await generateSessionTitle(
|
||||
"the login button is broken on mobile",
|
||||
createRegistry(model),
|
||||
createSettings(model),
|
||||
);
|
||||
|
||||
expect(title).toBeNull();
|
||||
},
|
||||
);
|
||||
expect(title).toBeNull();
|
||||
});
|
||||
|
||||
it("preserves a markerless title that mentions a <think> tag", async () => {
|
||||
const model = getModelFor("deepseek", "deepseek-v4-pro");
|
||||
|
||||
@@ -114,21 +114,27 @@ describe("default echo/printf redirect rule", () => {
|
||||
describe("default hub start rules", () => {
|
||||
const tools = ["hub"];
|
||||
|
||||
it.each(["bun run dev", "vite --host 0.0.0.0", "lldb ./app", "bun test --watch", "nohup server", "server &"])(
|
||||
"routes %s to hub start",
|
||||
command => {
|
||||
const result = checkBashInterception(command, tools, DEFAULT_BASH_INTERCEPTOR_RULES);
|
||||
expect(result.block).toBe(true);
|
||||
expect(result.suggestedTool).toBe("hub");
|
||||
},
|
||||
);
|
||||
it.each([
|
||||
"bun run dev",
|
||||
"vite --host 0.0.0.0",
|
||||
"lldb ./app",
|
||||
"bun test --watch",
|
||||
"nohup server",
|
||||
"server &",
|
||||
])("routes %s to hub start", command => {
|
||||
const result = checkBashInterception(command, tools, DEFAULT_BASH_INTERCEPTOR_RULES);
|
||||
expect(result.block).toBe(true);
|
||||
expect(result.suggestedTool).toBe("hub");
|
||||
});
|
||||
|
||||
it.each(["git diff -w", "docker compose up -d", "bun test", "printf 'server &'"])(
|
||||
"does not misclassify finite command %s",
|
||||
command => {
|
||||
expect(checkBashInterception(command, tools, DEFAULT_BASH_INTERCEPTOR_RULES).block).toBe(false);
|
||||
},
|
||||
);
|
||||
it.each([
|
||||
"git diff -w",
|
||||
"docker compose up -d",
|
||||
"bun test",
|
||||
"printf 'server &'",
|
||||
])("does not misclassify finite command %s", command => {
|
||||
expect(checkBashInterception(command, tools, DEFAULT_BASH_INTERCEPTOR_RULES).block).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("BashTool argument validation", () => {
|
||||
|
||||
Reference in New Issue
Block a user