mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 06:04:40 +00:00
fix(coding-agent,ai): finalize provider unregister lifecycle and dependency security updates fixes #1669
This commit is contained in:
parent
975de88eb1
commit
2f55890452
12 changed files with 904 additions and 832 deletions
|
|
@ -7,8 +7,9 @@ import * as os from "node:os";
|
|||
import * as path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { AuthStorage } from "../src/core/auth-storage.js";
|
||||
import { discoverAndLoadExtensions } from "../src/core/extensions/loader.js";
|
||||
import { createExtensionRuntime, discoverAndLoadExtensions } from "../src/core/extensions/loader.js";
|
||||
import { ExtensionRunner } from "../src/core/extensions/runner.js";
|
||||
import type { ExtensionActions, ExtensionContextActions, ProviderConfig } from "../src/core/extensions/types.js";
|
||||
import { DEFAULT_KEYBINDINGS, type KeyId } from "../src/core/keybindings.js";
|
||||
import { ModelRegistry } from "../src/core/model-registry.js";
|
||||
import { SessionManager } from "../src/core/session-manager.js";
|
||||
|
|
@ -32,6 +33,50 @@ describe("ExtensionRunner", () => {
|
|||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const providerModelConfig: ProviderConfig = {
|
||||
baseUrl: "https://provider.test/v1",
|
||||
apiKey: "PROVIDER_TEST_KEY",
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "instant-model",
|
||||
name: "Instant Model",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const extensionActions: ExtensionActions = {
|
||||
sendMessage: () => {},
|
||||
sendUserMessage: () => {},
|
||||
appendEntry: () => {},
|
||||
setSessionName: () => {},
|
||||
getSessionName: () => undefined,
|
||||
setLabel: () => {},
|
||||
getActiveTools: () => [],
|
||||
getAllTools: () => [],
|
||||
setActiveTools: () => {},
|
||||
getCommands: () => [],
|
||||
setModel: async () => false,
|
||||
getThinkingLevel: () => "off",
|
||||
setThinkingLevel: () => {},
|
||||
};
|
||||
|
||||
const extensionContextActions: ExtensionContextActions = {
|
||||
getModel: () => undefined,
|
||||
isIdle: () => true,
|
||||
abort: () => {},
|
||||
hasPendingMessages: () => false,
|
||||
shutdown: () => {},
|
||||
getContextUsage: () => undefined,
|
||||
compact: () => {},
|
||||
getSystemPrompt: () => "",
|
||||
};
|
||||
|
||||
describe("shortcut conflicts", () => {
|
||||
it("warns when extension shortcut conflicts with built-in", async () => {
|
||||
const extCode = `
|
||||
|
|
@ -491,6 +536,47 @@ describe("ExtensionRunner", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("provider registration", () => {
|
||||
it("pre-bind unregister removes all queued registrations for a provider", () => {
|
||||
const runtime = createExtensionRuntime();
|
||||
|
||||
runtime.registerProvider("queued-provider", providerModelConfig);
|
||||
runtime.registerProvider("queued-provider", {
|
||||
...providerModelConfig,
|
||||
models: [
|
||||
{
|
||||
id: "instant-model-2",
|
||||
name: "Instant Model 2",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(runtime.pendingProviderRegistrations).toHaveLength(2);
|
||||
|
||||
runtime.unregisterProvider("queued-provider");
|
||||
expect(runtime.pendingProviderRegistrations).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("post-bind register and unregister take effect immediately", () => {
|
||||
const runtime = createExtensionRuntime();
|
||||
const runner = new ExtensionRunner([], runtime, tempDir, sessionManager, modelRegistry);
|
||||
|
||||
runner.bindCore(extensionActions, extensionContextActions);
|
||||
expect(runtime.pendingProviderRegistrations).toHaveLength(0);
|
||||
|
||||
runtime.registerProvider("instant-provider", providerModelConfig);
|
||||
expect(runtime.pendingProviderRegistrations).toHaveLength(0);
|
||||
expect(modelRegistry.find("instant-provider", "instant-model")).toBeDefined();
|
||||
|
||||
runtime.unregisterProvider("instant-provider");
|
||||
expect(modelRegistry.find("instant-provider", "instant-model")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasHandlers", () => {
|
||||
it("returns true when handlers exist for event type", async () => {
|
||||
const extCode = `
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { OpenAICompletionsCompat } from "@mariozechner/pi-ai";
|
||||
import type { Api, Context, Model, OpenAICompletionsCompat } from "@mariozechner/pi-ai";
|
||||
import { getApiProvider, getOAuthProvider } from "@mariozechner/pi-ai";
|
||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import { AuthStorage } from "../src/core/auth-storage.js";
|
||||
import { clearApiKeyCache, ModelRegistry } from "../src/core/model-registry.js";
|
||||
|
|
@ -65,6 +66,23 @@ describe("ModelRegistry", () => {
|
|||
writeFileSync(modelsJsonPath, JSON.stringify({ providers }));
|
||||
}
|
||||
|
||||
const openAiModel: Model<Api> = {
|
||||
id: "test-openai-model",
|
||||
name: "Test OpenAI Model",
|
||||
api: "openai-completions",
|
||||
provider: "openai",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 4096,
|
||||
};
|
||||
|
||||
const emptyContext: Context = {
|
||||
messages: [],
|
||||
};
|
||||
|
||||
describe("baseUrl override (no custom models)", () => {
|
||||
test("overriding baseUrl keeps all built-in models", () => {
|
||||
writeRawModelsJson({
|
||||
|
|
@ -527,6 +545,61 @@ describe("ModelRegistry", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("dynamic provider lifecycle", () => {
|
||||
test("unregisterProvider removes custom OAuth provider and restores built-in OAuth provider", () => {
|
||||
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||
|
||||
registry.registerProvider("anthropic", {
|
||||
oauth: {
|
||||
name: "Custom Anthropic OAuth",
|
||||
login: async () => ({
|
||||
access: "custom-access-token",
|
||||
refresh: "custom-refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
}),
|
||||
refreshToken: async (credentials) => credentials,
|
||||
getApiKey: (credentials) => credentials.access,
|
||||
},
|
||||
});
|
||||
|
||||
expect(getOAuthProvider("anthropic")?.name).toBe("Custom Anthropic OAuth");
|
||||
|
||||
registry.unregisterProvider("anthropic");
|
||||
|
||||
expect(getOAuthProvider("anthropic")?.name).not.toBe("Custom Anthropic OAuth");
|
||||
});
|
||||
|
||||
test("unregisterProvider removes custom streamSimple override and restores built-in API stream handler", () => {
|
||||
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||
|
||||
registry.registerProvider("stream-override-provider", {
|
||||
api: "openai-completions",
|
||||
streamSimple: () => {
|
||||
throw new Error("custom streamSimple override");
|
||||
},
|
||||
});
|
||||
|
||||
let threwCustomOverride = false;
|
||||
try {
|
||||
getApiProvider("openai-completions")?.streamSimple(openAiModel, emptyContext);
|
||||
} catch (error) {
|
||||
threwCustomOverride = error instanceof Error && error.message === "custom streamSimple override";
|
||||
}
|
||||
expect(threwCustomOverride).toBe(true);
|
||||
|
||||
registry.unregisterProvider("stream-override-provider");
|
||||
|
||||
let threwCustomOverrideAfterUnregister = false;
|
||||
try {
|
||||
getApiProvider("openai-completions")?.streamSimple(openAiModel, emptyContext);
|
||||
} catch (error) {
|
||||
threwCustomOverrideAfterUnregister =
|
||||
error instanceof Error && error.message === "custom streamSimple override";
|
||||
}
|
||||
expect(threwCustomOverrideAfterUnregister).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("API key resolution", () => {
|
||||
/** Create provider config with custom apiKey */
|
||||
function providerWithApiKey(apiKey: string) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue