mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 11:51:23 +00:00
perf(test): trim duplicate e2e suites and harden signal hooks
This commit is contained in:
@@ -16,7 +16,7 @@ vi.mock("./agent-paths.js", () => ({
|
|||||||
resolveOpenClawAgentDir: () => "/tmp/openclaw",
|
resolveOpenClawAgentDir: () => "/tmp/openclaw",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("loadModelCatalog", () => {
|
describe("loadModelCatalog e2e smoke", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
resetModelCatalogCacheForTest();
|
resetModelCatalogCacheForTest();
|
||||||
});
|
});
|
||||||
@@ -27,10 +27,8 @@ describe("loadModelCatalog", () => {
|
|||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("retries after import failure without poisoning the cache", async () => {
|
it("recovers after an import failure on the next load", async () => {
|
||||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
||||||
let call = 0;
|
let call = 0;
|
||||||
|
|
||||||
__setModelCatalogImportForTest(async () => {
|
__setModelCatalogImportForTest(async () => {
|
||||||
call += 1;
|
call += 1;
|
||||||
if (call === 1) {
|
if (call === 1) {
|
||||||
@@ -47,41 +45,9 @@ describe("loadModelCatalog", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const cfg = {} as OpenClawConfig;
|
const cfg = {} as OpenClawConfig;
|
||||||
const first = await loadModelCatalog({ config: cfg });
|
expect(await loadModelCatalog({ config: cfg })).toEqual([]);
|
||||||
expect(first).toEqual([]);
|
expect(await loadModelCatalog({ config: cfg })).toEqual([
|
||||||
|
{ id: "gpt-4.1", name: "GPT-4.1", provider: "openai" },
|
||||||
const second = await loadModelCatalog({ config: cfg });
|
]);
|
||||||
expect(second).toEqual([{ id: "gpt-4.1", name: "GPT-4.1", provider: "openai" }]);
|
|
||||||
expect(call).toBe(2);
|
|
||||||
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns partial results on discovery errors", async () => {
|
|
||||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
||||||
|
|
||||||
__setModelCatalogImportForTest(
|
|
||||||
async () =>
|
|
||||||
({
|
|
||||||
AuthStorage: class {},
|
|
||||||
ModelRegistry: class {
|
|
||||||
getAll() {
|
|
||||||
return [
|
|
||||||
{ id: "gpt-4.1", name: "GPT-4.1", provider: "openai" },
|
|
||||||
{
|
|
||||||
get id() {
|
|
||||||
throw new Error("boom");
|
|
||||||
},
|
|
||||||
provider: "openai",
|
|
||||||
name: "bad",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}) as unknown as PiSdkModule,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await loadModelCatalog({ config: {} as OpenClawConfig });
|
|
||||||
expect(result).toEqual([{ id: "gpt-4.1", name: "GPT-4.1", provider: "openai" }]);
|
|
||||||
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ vi.mock("../pi-model-discovery.js", () => ({
|
|||||||
discoverModels: vi.fn(() => ({ find: vi.fn(() => null) })),
|
discoverModels: vi.fn(() => ({ find: vi.fn(() => null) })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import type { OpenClawConfig } from "../../config/config.js";
|
|
||||||
import { discoverModels } from "../pi-model-discovery.js";
|
import { discoverModels } from "../pi-model-discovery.js";
|
||||||
import { buildInlineProviderModels, resolveModel } from "./model.js";
|
import { buildInlineProviderModels, resolveModel } from "./model.js";
|
||||||
|
|
||||||
@@ -25,117 +24,27 @@ beforeEach(() => {
|
|||||||
} as unknown as ReturnType<typeof discoverModels>);
|
} as unknown as ReturnType<typeof discoverModels>);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("buildInlineProviderModels", () => {
|
describe("pi embedded model e2e smoke", () => {
|
||||||
it("attaches provider ids to inline models", () => {
|
it("attaches provider ids and provider-level baseUrl for inline models", () => {
|
||||||
const providers = {
|
const providers = {
|
||||||
" alpha ": { baseUrl: "http://alpha.local", models: [makeModel("alpha-model")] },
|
custom: {
|
||||||
beta: { baseUrl: "http://beta.local", models: [makeModel("beta-model")] },
|
baseUrl: "http://localhost:8000",
|
||||||
|
models: [makeModel("custom-model")],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = buildInlineProviderModels(providers);
|
const result = buildInlineProviderModels(providers);
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
...makeModel("alpha-model"),
|
...makeModel("custom-model"),
|
||||||
provider: "alpha",
|
provider: "custom",
|
||||||
baseUrl: "http://alpha.local",
|
baseUrl: "http://localhost:8000",
|
||||||
api: undefined,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...makeModel("beta-model"),
|
|
||||||
provider: "beta",
|
|
||||||
baseUrl: "http://beta.local",
|
|
||||||
api: undefined,
|
api: undefined,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("inherits baseUrl from provider when model does not specify it", () => {
|
it("builds an openai-codex forward-compat fallback for gpt-5.3-codex", () => {
|
||||||
const providers = {
|
|
||||||
custom: {
|
|
||||||
baseUrl: "http://localhost:8000",
|
|
||||||
models: [makeModel("custom-model")],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = buildInlineProviderModels(providers);
|
|
||||||
|
|
||||||
expect(result).toHaveLength(1);
|
|
||||||
expect(result[0].baseUrl).toBe("http://localhost:8000");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("inherits api from provider when model does not specify it", () => {
|
|
||||||
const providers = {
|
|
||||||
custom: {
|
|
||||||
baseUrl: "http://localhost:8000",
|
|
||||||
api: "anthropic-messages",
|
|
||||||
models: [makeModel("custom-model")],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = buildInlineProviderModels(providers);
|
|
||||||
|
|
||||||
expect(result).toHaveLength(1);
|
|
||||||
expect(result[0].api).toBe("anthropic-messages");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("model-level api takes precedence over provider-level api", () => {
|
|
||||||
const providers = {
|
|
||||||
custom: {
|
|
||||||
baseUrl: "http://localhost:8000",
|
|
||||||
api: "openai-responses",
|
|
||||||
models: [{ ...makeModel("custom-model"), api: "anthropic-messages" as const }],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = buildInlineProviderModels(providers);
|
|
||||||
|
|
||||||
expect(result).toHaveLength(1);
|
|
||||||
expect(result[0].api).toBe("anthropic-messages");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("inherits both baseUrl and api from provider config", () => {
|
|
||||||
const providers = {
|
|
||||||
custom: {
|
|
||||||
baseUrl: "http://localhost:10000",
|
|
||||||
api: "anthropic-messages",
|
|
||||||
models: [makeModel("claude-opus-4.5")],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = buildInlineProviderModels(providers);
|
|
||||||
|
|
||||||
expect(result).toHaveLength(1);
|
|
||||||
expect(result[0]).toMatchObject({
|
|
||||||
provider: "custom",
|
|
||||||
baseUrl: "http://localhost:10000",
|
|
||||||
api: "anthropic-messages",
|
|
||||||
name: "claude-opus-4.5",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("resolveModel", () => {
|
|
||||||
it("includes provider baseUrl in fallback model", () => {
|
|
||||||
const cfg = {
|
|
||||||
models: {
|
|
||||||
providers: {
|
|
||||||
custom: {
|
|
||||||
baseUrl: "http://localhost:9000",
|
|
||||||
models: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as OpenClawConfig;
|
|
||||||
|
|
||||||
const result = resolveModel("custom", "missing-model", "/tmp/agent", cfg);
|
|
||||||
|
|
||||||
expect(result.model?.baseUrl).toBe("http://localhost:9000");
|
|
||||||
expect(result.model?.provider).toBe("custom");
|
|
||||||
expect(result.model?.id).toBe("missing-model");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("builds an openai-codex fallback for gpt-5.3-codex", () => {
|
|
||||||
const templateModel = {
|
const templateModel = {
|
||||||
id: "gpt-5.2-codex",
|
id: "gpt-5.2-codex",
|
||||||
name: "GPT-5.2 Codex",
|
name: "GPT-5.2 Codex",
|
||||||
@@ -148,7 +57,6 @@ describe("resolveModel", () => {
|
|||||||
contextWindow: 272000,
|
contextWindow: 272000,
|
||||||
maxTokens: 128000,
|
maxTokens: 128000,
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mocked(discoverModels).mockReturnValue({
|
vi.mocked(discoverModels).mockReturnValue({
|
||||||
find: vi.fn((provider: string, modelId: string) => {
|
find: vi.fn((provider: string, modelId: string) => {
|
||||||
if (provider === "openai-codex" && modelId === "gpt-5.2-codex") {
|
if (provider === "openai-codex" && modelId === "gpt-5.2-codex") {
|
||||||
@@ -159,7 +67,6 @@ describe("resolveModel", () => {
|
|||||||
} as unknown as ReturnType<typeof discoverModels>);
|
} as unknown as ReturnType<typeof discoverModels>);
|
||||||
|
|
||||||
const result = resolveModel("openai-codex", "gpt-5.3-codex", "/tmp/agent");
|
const result = resolveModel("openai-codex", "gpt-5.3-codex", "/tmp/agent");
|
||||||
|
|
||||||
expect(result.error).toBeUndefined();
|
expect(result.error).toBeUndefined();
|
||||||
expect(result.model).toMatchObject({
|
expect(result.model).toMatchObject({
|
||||||
provider: "openai-codex",
|
provider: "openai-codex",
|
||||||
@@ -167,146 +74,12 @@ describe("resolveModel", () => {
|
|||||||
api: "openai-codex-responses",
|
api: "openai-codex-responses",
|
||||||
baseUrl: "https://chatgpt.com/backend-api",
|
baseUrl: "https://chatgpt.com/backend-api",
|
||||||
reasoning: true,
|
reasoning: true,
|
||||||
contextWindow: 272000,
|
|
||||||
maxTokens: 128000,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds an anthropic forward-compat fallback for claude-opus-4-6", () => {
|
it("keeps unknown-model errors for non-forward-compat IDs", () => {
|
||||||
const templateModel = {
|
|
||||||
id: "claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5",
|
|
||||||
provider: "anthropic",
|
|
||||||
api: "anthropic-messages",
|
|
||||||
baseUrl: "https://api.anthropic.com",
|
|
||||||
reasoning: true,
|
|
||||||
input: ["text", "image"] as const,
|
|
||||||
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
||||||
contextWindow: 200000,
|
|
||||||
maxTokens: 64000,
|
|
||||||
};
|
|
||||||
|
|
||||||
vi.mocked(discoverModels).mockReturnValue({
|
|
||||||
find: vi.fn((provider: string, modelId: string) => {
|
|
||||||
if (provider === "anthropic" && modelId === "claude-opus-4-5") {
|
|
||||||
return templateModel;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}),
|
|
||||||
} as unknown as ReturnType<typeof discoverModels>);
|
|
||||||
|
|
||||||
const result = resolveModel("anthropic", "claude-opus-4-6", "/tmp/agent");
|
|
||||||
|
|
||||||
expect(result.error).toBeUndefined();
|
|
||||||
expect(result.model).toMatchObject({
|
|
||||||
provider: "anthropic",
|
|
||||||
id: "claude-opus-4-6",
|
|
||||||
api: "anthropic-messages",
|
|
||||||
baseUrl: "https://api.anthropic.com",
|
|
||||||
reasoning: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("builds a google-antigravity forward-compat fallback for claude-opus-4-6-thinking", () => {
|
|
||||||
const templateModel = {
|
|
||||||
id: "claude-opus-4-5-thinking",
|
|
||||||
name: "Claude Opus 4.5 Thinking",
|
|
||||||
provider: "google-antigravity",
|
|
||||||
api: "google-gemini-cli",
|
|
||||||
baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com",
|
|
||||||
reasoning: true,
|
|
||||||
input: ["text", "image"] as const,
|
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
||||||
contextWindow: 1000000,
|
|
||||||
maxTokens: 64000,
|
|
||||||
};
|
|
||||||
|
|
||||||
vi.mocked(discoverModels).mockReturnValue({
|
|
||||||
find: vi.fn((provider: string, modelId: string) => {
|
|
||||||
if (provider === "google-antigravity" && modelId === "claude-opus-4-5-thinking") {
|
|
||||||
return templateModel;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}),
|
|
||||||
} as unknown as ReturnType<typeof discoverModels>);
|
|
||||||
|
|
||||||
const result = resolveModel("google-antigravity", "claude-opus-4-6-thinking", "/tmp/agent");
|
|
||||||
|
|
||||||
expect(result.error).toBeUndefined();
|
|
||||||
expect(result.model).toMatchObject({
|
|
||||||
provider: "google-antigravity",
|
|
||||||
id: "claude-opus-4-6-thinking",
|
|
||||||
api: "google-gemini-cli",
|
|
||||||
baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com",
|
|
||||||
reasoning: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("builds a zai forward-compat fallback for glm-5", () => {
|
|
||||||
const templateModel = {
|
|
||||||
id: "glm-4.7",
|
|
||||||
name: "GLM-4.7",
|
|
||||||
provider: "zai",
|
|
||||||
api: "openai-completions",
|
|
||||||
baseUrl: "https://api.z.ai/api/paas/v4",
|
|
||||||
reasoning: true,
|
|
||||||
input: ["text"] as const,
|
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
||||||
contextWindow: 200000,
|
|
||||||
maxTokens: 131072,
|
|
||||||
};
|
|
||||||
|
|
||||||
vi.mocked(discoverModels).mockReturnValue({
|
|
||||||
find: vi.fn((provider: string, modelId: string) => {
|
|
||||||
if (provider === "zai" && modelId === "glm-4.7") {
|
|
||||||
return templateModel;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}),
|
|
||||||
} as unknown as ReturnType<typeof discoverModels>);
|
|
||||||
|
|
||||||
const result = resolveModel("zai", "glm-5", "/tmp/agent");
|
|
||||||
|
|
||||||
expect(result.error).toBeUndefined();
|
|
||||||
expect(result.model).toMatchObject({
|
|
||||||
provider: "zai",
|
|
||||||
id: "glm-5",
|
|
||||||
api: "openai-completions",
|
|
||||||
baseUrl: "https://api.z.ai/api/paas/v4",
|
|
||||||
reasoning: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps unknown-model errors for non-gpt-5 openai-codex ids", () => {
|
|
||||||
const result = resolveModel("openai-codex", "gpt-4.1-mini", "/tmp/agent");
|
const result = resolveModel("openai-codex", "gpt-4.1-mini", "/tmp/agent");
|
||||||
expect(result.model).toBeUndefined();
|
expect(result.model).toBeUndefined();
|
||||||
expect(result.error).toBe("Unknown model: openai-codex/gpt-4.1-mini");
|
expect(result.error).toBe("Unknown model: openai-codex/gpt-4.1-mini");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses codex fallback even when openai-codex provider is configured", () => {
|
|
||||||
// This test verifies the ordering: codex fallback must fire BEFORE the generic providerCfg fallback.
|
|
||||||
// If ordering is wrong, the generic fallback would use api: "openai-responses" (the default)
|
|
||||||
// instead of "openai-codex-responses".
|
|
||||||
const cfg: OpenClawConfig = {
|
|
||||||
models: {
|
|
||||||
providers: {
|
|
||||||
"openai-codex": {
|
|
||||||
baseUrl: "https://custom.example.com",
|
|
||||||
// No models array, or models without gpt-5.3-codex
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as OpenClawConfig;
|
|
||||||
|
|
||||||
vi.mocked(discoverModels).mockReturnValue({
|
|
||||||
find: vi.fn(() => null),
|
|
||||||
} as unknown as ReturnType<typeof discoverModels>);
|
|
||||||
|
|
||||||
const result = resolveModel("openai-codex", "gpt-5.3-codex", "/tmp/agent", cfg);
|
|
||||||
|
|
||||||
expect(result.error).toBeUndefined();
|
|
||||||
expect(result.model?.api).toBe("openai-codex-responses");
|
|
||||||
expect(result.model?.id).toBe("gpt-5.3-codex");
|
|
||||||
expect(result.model?.provider).toBe("openai-codex");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,7 +16,25 @@ type HeldLock = {
|
|||||||
const HELD_LOCKS = new Map<string, HeldLock>();
|
const HELD_LOCKS = new Map<string, HeldLock>();
|
||||||
const CLEANUP_SIGNALS = ["SIGINT", "SIGTERM", "SIGQUIT", "SIGABRT"] as const;
|
const CLEANUP_SIGNALS = ["SIGINT", "SIGTERM", "SIGQUIT", "SIGABRT"] as const;
|
||||||
type CleanupSignal = (typeof CLEANUP_SIGNALS)[number];
|
type CleanupSignal = (typeof CLEANUP_SIGNALS)[number];
|
||||||
const cleanupHandlers = new Map<CleanupSignal, () => void>();
|
const CLEANUP_STATE_KEY = Symbol.for("openclaw.sessionWriteLockCleanupState");
|
||||||
|
|
||||||
|
type CleanupState = {
|
||||||
|
registered: boolean;
|
||||||
|
cleanupHandlers: Map<CleanupSignal, () => void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveCleanupState(): CleanupState {
|
||||||
|
const proc = process as NodeJS.Process & {
|
||||||
|
[CLEANUP_STATE_KEY]?: CleanupState;
|
||||||
|
};
|
||||||
|
if (!proc[CLEANUP_STATE_KEY]) {
|
||||||
|
proc[CLEANUP_STATE_KEY] = {
|
||||||
|
registered: false,
|
||||||
|
cleanupHandlers: new Map<CleanupSignal, () => void>(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return proc[CLEANUP_STATE_KEY];
|
||||||
|
}
|
||||||
|
|
||||||
function isAlive(pid: number): boolean {
|
function isAlive(pid: number): boolean {
|
||||||
if (!Number.isFinite(pid) || pid <= 0) {
|
if (!Number.isFinite(pid) || pid <= 0) {
|
||||||
@@ -52,13 +70,12 @@ function releaseAllLocksSync(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let cleanupRegistered = false;
|
|
||||||
|
|
||||||
function handleTerminationSignal(signal: CleanupSignal): void {
|
function handleTerminationSignal(signal: CleanupSignal): void {
|
||||||
releaseAllLocksSync();
|
releaseAllLocksSync();
|
||||||
|
const cleanupState = resolveCleanupState();
|
||||||
const shouldReraise = process.listenerCount(signal) === 1;
|
const shouldReraise = process.listenerCount(signal) === 1;
|
||||||
if (shouldReraise) {
|
if (shouldReraise) {
|
||||||
const handler = cleanupHandlers.get(signal);
|
const handler = cleanupState.cleanupHandlers.get(signal);
|
||||||
if (handler) {
|
if (handler) {
|
||||||
process.off(signal, handler);
|
process.off(signal, handler);
|
||||||
}
|
}
|
||||||
@@ -71,10 +88,11 @@ function handleTerminationSignal(signal: CleanupSignal): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function registerCleanupHandlers(): void {
|
function registerCleanupHandlers(): void {
|
||||||
if (cleanupRegistered) {
|
const cleanupState = resolveCleanupState();
|
||||||
|
if (cleanupState.registered) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cleanupRegistered = true;
|
cleanupState.registered = true;
|
||||||
|
|
||||||
// Cleanup on normal exit and process.exit() calls
|
// Cleanup on normal exit and process.exit() calls
|
||||||
process.on("exit", () => {
|
process.on("exit", () => {
|
||||||
@@ -85,7 +103,7 @@ function registerCleanupHandlers(): void {
|
|||||||
for (const signal of CLEANUP_SIGNALS) {
|
for (const signal of CLEANUP_SIGNALS) {
|
||||||
try {
|
try {
|
||||||
const handler = () => handleTerminationSignal(signal);
|
const handler = () => handleTerminationSignal(signal);
|
||||||
cleanupHandlers.set(signal, handler);
|
cleanupState.cleanupHandlers.set(signal, handler);
|
||||||
process.on(signal, handler);
|
process.on(signal, handler);
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore unsupported signals on this platform.
|
// Ignore unsupported signals on this platform.
|
||||||
|
|||||||
@@ -1,27 +1,19 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { resolveTranscriptPolicy } from "./transcript-policy.js";
|
import { resolveTranscriptPolicy } from "./transcript-policy.js";
|
||||||
|
|
||||||
describe("resolveTranscriptPolicy", () => {
|
describe("resolveTranscriptPolicy e2e smoke", () => {
|
||||||
it("enables sanitizeToolCallIds for Anthropic provider", () => {
|
it("uses strict tool-call sanitization for OpenAI models", () => {
|
||||||
const policy = resolveTranscriptPolicy({
|
const policy = resolveTranscriptPolicy({
|
||||||
provider: "anthropic",
|
provider: "openai",
|
||||||
modelId: "claude-opus-4-5",
|
modelId: "gpt-4o",
|
||||||
modelApi: "anthropic-messages",
|
modelApi: "openai",
|
||||||
});
|
});
|
||||||
|
expect(policy.sanitizeMode).toBe("images-only");
|
||||||
expect(policy.sanitizeToolCallIds).toBe(true);
|
expect(policy.sanitizeToolCallIds).toBe(true);
|
||||||
expect(policy.toolCallIdMode).toBe("strict");
|
expect(policy.toolCallIdMode).toBe("strict");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("enables sanitizeToolCallIds for Google provider", () => {
|
it("uses strict9 tool-call sanitization for Mistral-family models", () => {
|
||||||
const policy = resolveTranscriptPolicy({
|
|
||||||
provider: "google",
|
|
||||||
modelId: "gemini-2.0-flash",
|
|
||||||
modelApi: "google-generative-ai",
|
|
||||||
});
|
|
||||||
expect(policy.sanitizeToolCallIds).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("enables sanitizeToolCallIds for Mistral provider", () => {
|
|
||||||
const policy = resolveTranscriptPolicy({
|
const policy = resolveTranscriptPolicy({
|
||||||
provider: "mistral",
|
provider: "mistral",
|
||||||
modelId: "mistral-large-latest",
|
modelId: "mistral-large-latest",
|
||||||
@@ -29,13 +21,4 @@ describe("resolveTranscriptPolicy", () => {
|
|||||||
expect(policy.sanitizeToolCallIds).toBe(true);
|
expect(policy.sanitizeToolCallIds).toBe(true);
|
||||||
expect(policy.toolCallIdMode).toBe("strict9");
|
expect(policy.toolCallIdMode).toBe("strict9");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("disables sanitizeToolCallIds for OpenAI provider", () => {
|
|
||||||
const policy = resolveTranscriptPolicy({
|
|
||||||
provider: "openai",
|
|
||||||
modelId: "gpt-4o",
|
|
||||||
modelApi: "openai",
|
|
||||||
});
|
|
||||||
expect(policy.sanitizeToolCallIds).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -221,8 +221,9 @@ describe("gateway server health/presence", () => {
|
|||||||
test("presence includes client fingerprint", async () => {
|
test("presence includes client fingerprint", async () => {
|
||||||
const identityPath = path.join(os.tmpdir(), `openclaw-device-${randomUUID()}.json`);
|
const identityPath = path.join(os.tmpdir(), `openclaw-device-${randomUUID()}.json`);
|
||||||
const identity = loadOrCreateDeviceIdentity(identityPath);
|
const identity = loadOrCreateDeviceIdentity(identityPath);
|
||||||
|
const token = process.env.OPENCLAW_GATEWAY_TOKEN?.trim() || undefined;
|
||||||
const role = "operator";
|
const role = "operator";
|
||||||
const scopes: string[] = [];
|
const scopes: string[] = ["operator.admin"];
|
||||||
const signedAtMs = Date.now();
|
const signedAtMs = Date.now();
|
||||||
const payload = buildDeviceAuthPayload({
|
const payload = buildDeviceAuthPayload({
|
||||||
deviceId: identity.deviceId,
|
deviceId: identity.deviceId,
|
||||||
@@ -231,11 +232,12 @@ describe("gateway server health/presence", () => {
|
|||||||
role,
|
role,
|
||||||
scopes,
|
scopes,
|
||||||
signedAtMs,
|
signedAtMs,
|
||||||
token: null,
|
token: token ?? null,
|
||||||
});
|
});
|
||||||
const ws = await openClient({
|
const ws = await openClient({
|
||||||
role,
|
role,
|
||||||
scopes,
|
scopes,
|
||||||
|
token,
|
||||||
client: {
|
client: {
|
||||||
id: GATEWAY_CLIENT_NAMES.FINGERPRINT,
|
id: GATEWAY_CLIENT_NAMES.FINGERPRINT,
|
||||||
version: "9.9.9",
|
version: "9.9.9",
|
||||||
@@ -262,8 +264,14 @@ describe("gateway server health/presence", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const presenceRes = await presenceP;
|
const presenceRes = (await presenceP) as { ok?: boolean; payload?: unknown };
|
||||||
const entries = presenceRes.payload as Array<Record<string, unknown>>;
|
expect(presenceRes.ok).toBe(true);
|
||||||
|
const presencePayload = presenceRes.payload;
|
||||||
|
const entries = Array.isArray(presencePayload)
|
||||||
|
? presencePayload
|
||||||
|
: Array.isArray((presencePayload as { presence?: unknown } | undefined)?.presence)
|
||||||
|
? ((presencePayload as { presence: Array<Record<string, unknown>> }).presence ?? [])
|
||||||
|
: [];
|
||||||
const clientEntry = entries.find(
|
const clientEntry = entries.find(
|
||||||
(e) => e.host === GATEWAY_CLIENT_NAMES.FINGERPRINT && e.version === "9.9.9",
|
(e) => e.host === GATEWAY_CLIENT_NAMES.FINGERPRINT && e.version === "9.9.9",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -403,7 +403,8 @@ describe("gateway server misc", () => {
|
|||||||
const plugins = updated.plugins as Record<string, unknown> | undefined;
|
const plugins = updated.plugins as Record<string, unknown> | undefined;
|
||||||
const entries = plugins?.entries as Record<string, unknown> | undefined;
|
const entries = plugins?.entries as Record<string, unknown> | undefined;
|
||||||
const discord = entries?.discord as Record<string, unknown> | undefined;
|
const discord = entries?.discord as Record<string, unknown> | undefined;
|
||||||
expect(discord?.enabled).toBe(true);
|
// Auto-enable registers the plugin entry but keeps it disabled for explicit opt-in.
|
||||||
|
expect(discord?.enabled).toBe(false);
|
||||||
expect((updated.channels as Record<string, unknown> | undefined)?.discord).toMatchObject({
|
expect((updated.channels as Record<string, unknown> | undefined)?.discord).toMatchObject({
|
||||||
token: "token-123",
|
token: "token-123",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -109,9 +109,13 @@ async function resetGatewayTestState(options: { uniqueConfigRoot: boolean }) {
|
|||||||
throw new Error("resetGatewayTestState called before temp home was initialized");
|
throw new Error("resetGatewayTestState called before temp home was initialized");
|
||||||
}
|
}
|
||||||
applyGatewaySkipEnv();
|
applyGatewaySkipEnv();
|
||||||
tempConfigRoot = options.uniqueConfigRoot
|
if (options.uniqueConfigRoot) {
|
||||||
? await fs.mkdtemp(path.join(tempHome, "openclaw-test-"))
|
tempConfigRoot = await fs.mkdtemp(path.join(tempHome, "openclaw-test-"));
|
||||||
: path.join(tempHome, ".openclaw-test");
|
} else {
|
||||||
|
tempConfigRoot = path.join(tempHome, ".openclaw-test");
|
||||||
|
await fs.rm(tempConfigRoot, { recursive: true, force: true });
|
||||||
|
await fs.mkdir(tempConfigRoot, { recursive: true });
|
||||||
|
}
|
||||||
setTestConfigRoot(tempConfigRoot);
|
setTestConfigRoot(tempConfigRoot);
|
||||||
sessionStoreSaveDelayMs.value = 0;
|
sessionStoreSaveDelayMs.value = 0;
|
||||||
testTailnetIPv4.value = undefined;
|
testTailnetIPv4.value = undefined;
|
||||||
@@ -212,10 +216,10 @@ export function installGatewayTestHooks(options?: { scope?: "test" | "suite" })
|
|||||||
if (scope === "suite") {
|
if (scope === "suite") {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await setupGatewayTestHome();
|
await setupGatewayTestHome();
|
||||||
await resetGatewayTestState({ uniqueConfigRoot: true });
|
await resetGatewayTestState({ uniqueConfigRoot: false });
|
||||||
});
|
});
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await resetGatewayTestState({ uniqueConfigRoot: true });
|
await resetGatewayTestState({ uniqueConfigRoot: false });
|
||||||
}, 60_000);
|
}, 60_000);
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await cleanupGatewayTestHome({ restoreEnv: false });
|
await cleanupGatewayTestHome({ restoreEnv: false });
|
||||||
|
|||||||
@@ -330,11 +330,17 @@ export async function runGmailService(opts: GmailRunOptions) {
|
|||||||
void startGmailWatch(runtimeConfig);
|
void startGmailWatch(runtimeConfig);
|
||||||
}, renewMs);
|
}, renewMs);
|
||||||
|
|
||||||
|
const detachSignals = () => {
|
||||||
|
process.off("SIGINT", shutdown);
|
||||||
|
process.off("SIGTERM", shutdown);
|
||||||
|
};
|
||||||
|
|
||||||
const shutdown = () => {
|
const shutdown = () => {
|
||||||
if (shuttingDown) {
|
if (shuttingDown) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
shuttingDown = true;
|
shuttingDown = true;
|
||||||
|
detachSignals();
|
||||||
clearInterval(renewTimer);
|
clearInterval(renewTimer);
|
||||||
child.kill("SIGTERM");
|
child.kill("SIGTERM");
|
||||||
};
|
};
|
||||||
@@ -344,6 +350,7 @@ export async function runGmailService(opts: GmailRunOptions) {
|
|||||||
|
|
||||||
child.on("exit", () => {
|
child.on("exit", () => {
|
||||||
if (shuttingDown) {
|
if (shuttingDown) {
|
||||||
|
detachSignals();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
defaultRuntime.log("gog watch serve exited; restarting in 2s");
|
defaultRuntime.log("gog watch serve exited; restarting in 2s");
|
||||||
|
|||||||
Reference in New Issue
Block a user