File size: 4,306 Bytes
5f40163 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | import { ACP_PROVIDERS as CLIENT_ACP_PROVIDERS } from "@openhands/typescript-client";
import { describe, expect, it } from "vitest";
import {
ACP_MANAGED_SENTINEL,
ACP_PROVIDERS,
getAcpProviderSecrets,
SURFACED_ACP_PROVIDERS,
resolveEffectiveAcpModel,
} from "./acp-providers";
describe("ACP_PROVIDERS", () => {
it("passes every codex data field through from the pinned client registry", () => {
// No local override may sit between the registry and the picker: a
// hand-maintained model entry survives an upstream *removal*, so Canvas
// would keep offering an id the live ACP server has started rejecting.
const codex = ACP_PROVIDERS.find(({ key }) => key === "codex");
const client = CLIENT_ACP_PROVIDERS.codex;
expect(codex?.default_command).toEqual([...client.default_command]);
expect(codex?.available_models).toEqual(
client.available_models.map(({ id, label }) => ({ id, label })),
);
});
it("carries GPT-6 Astra, so the pin is new enough to launch it", () => {
// Canary for the pin's freshness, not a catalog Canvas maintains: Astra
// needs codex-acp >= 1.10.0, which only client >= 1.45.0 mirrors.
const codex = ACP_PROVIDERS.find(({ key }) => key === "codex");
expect(codex?.available_models?.map(({ id }) => id)).toContain(
"gpt-6-astra",
);
});
});
describe("resolveEffectiveAcpModel", () => {
it("surfaces the real claude-agent-acp 0.44+ 'default' model", () => {
// ``default`` ("Default (recommended)") is a real, selectable Claude model
// in the configOptions select — the server reports it as the current model.
// It must NOT be suppressed as a placeholder (regression: the chip would
// otherwise show no model for a session genuinely running on 'default').
expect(resolveEffectiveAcpModel({ runtimeId: "default" })).toBe("default");
expect(
resolveEffectiveAcpModel({ runtimeName: "Default (recommended)" }),
).toBe("Default (recommended)");
});
it("follows the runtime → configured → sdkLlm precedence", () => {
expect(
resolveEffectiveAcpModel({
runtimeName: "Sonnet",
runtimeId: "sonnet",
configured: "haiku",
}),
).toBe("Sonnet");
expect(resolveEffectiveAcpModel({ configured: "haiku" })).toBe("haiku");
expect(resolveEffectiveAcpModel({ sdkLlm: "gpt-5.5/medium" })).toBe(
"gpt-5.5/medium",
);
});
it("still suppresses the legacy acp-managed sentinel and blanks", () => {
expect(
resolveEffectiveAcpModel({ sdkLlm: ACP_MANAGED_SENTINEL }),
).toBeNull();
expect(resolveEffectiveAcpModel({ runtimeId: " " })).toBeNull();
expect(resolveEffectiveAcpModel({})).toBeNull();
});
it("falls back to providerDefault only when no concrete model resolves", () => {
expect(
resolveEffectiveAcpModel({
sdkLlm: ACP_MANAGED_SENTINEL,
providerDefault: "opus[1m]",
}),
).toBe("opus[1m]");
// A real 'default' wins over providerDefault — it is a concrete model.
expect(
resolveEffectiveAcpModel({
runtimeId: "default",
providerDefault: "opus[1m]",
}),
).toBe("default");
});
});
describe("surfaced ACP providers", () => {
// Everything the pinned client registry publishes that Canvas does not
// offer. Derived, so a harness added upstream is covered here without an
// edit — the point of declaring what we surface rather than what we hide.
const unsurfaced = Object.keys(CLIENT_ACP_PROVIDERS).filter(
(key) => !SURFACED_ACP_PROVIDERS.includes(key),
);
it("surfaces only Claude Code, Codex and Gemini CLI", () => {
expect([...SURFACED_ACP_PROVIDERS]).toEqual([
"claude-code",
"codex",
"gemini-cli",
]);
});
it("surfaces nothing the pinned client registry has dropped", () => {
// A rename or removal upstream must break loudly rather than leave a tile
// whose command and models resolve to nothing. An addition stays a no-op.
expect(
SURFACED_ACP_PROVIDERS.filter((key) => !(key in CLIENT_ACP_PROVIDERS)),
).toEqual([]);
});
it("offers no credential fields for a harness it does not surface", () => {
unsurfaced.forEach((key) => {
expect(getAcpProviderSecrets(key)).toEqual([]);
});
});
});
|