Files
openclaw/extensions/anthropic/cli-migration.test.ts
Peter Steinberger bb46b79d3c refactor: internalize OpenClaw agent runtime (#85341)
* refactor: extract agent core package

Introduce packages/agent-core as the OpenClaw-owned home for reusable agent loop, harness, session, prompt, and runtime dependency contracts.

* refactor: extract shared llm runtime

Move provider model registries, stream wrappers, OAuth helpers, and LLM utilities into src/llm with plugin-sdk barrels instead of depending on the old embedded runtime layout.

* refactor: remove pi runtime internals

Rename remaining Pi-shaped agent surfaces to OpenClaw agent runtime names, delete obsolete Pi docs and package graph checks, and add the third-party notice for incorporated code.

* refactor: tighten agent session runtime

Make agent-core/runtime dependencies explicit, consolidate compaction and session transcript helpers, and move model/session helpers behind OpenClaw-owned contracts.

* refactor: remove static model and pi auth paths

Drop static model catalogs and Pi auth bridges, move model/provider facts to manifest-owned runtime contracts, and harden internal embedded-agent utilities.

* refactor: remove legacy provider compat paths

* docs: remove agent parity notes

* fix: skip provider wildcard metadata parsing

* refactor: share session extension sdk loading

* refactor: inline acpx proxy error formatter

* refactor: fold edit recovery into edit tool

* fix: accept extension batch separator

* test: align startup provider plugin expectations

* fix: restore provider-scoped release discovery

* test: align static asset packaging expectations

* fix: run static provider catalogs during scoped discovery

* fix: add provider entry catalogs for scoped live discovery

* fix: load lightweight provider catalog entries

* fix: refresh provider-scoped plugin metadata

* fix: keep provider catalog entries on release live path

* fix: keep static manifest models in release live checks

* fix: harden release model discovery

* fix: reduce OpenAI live cache probe reasoning

* fix: disable OpenAI cache probe reasoning

* ci: extend OpenAI gateway live timeout

* fix: extend live gateway model budget

* fix: stabilize release validation regressions

* fix: honor provider aliases in model rows

* fix: stabilize release validation lanes

* fix: stabilize release memory qa

* ci: stabilize release validation lanes

* ci: prefer ipv4 for live docker node calls

* fix: restore shared tool-call stream wrapper

* ci: remove legacy pi test shard alias

* fix: clean up embedded agent test drift

* fix: stabilize runtime alias status

* fix: clean up embedded agent ci drift

* fix: restore release ci invariants

* fix: clean up post-rebase runtime drift

* fix: restore release ci checks

* fix: restore release ci after rebase

* fix: remove stale pi runtime path

* test: align compaction runtime expectations

* test: update plugin prerelease expectations

* fix: handle claude live tool approvals

* fix: stabilize release validation gates

* fix: finish agent runtime import

* test: finish post-rebase agent runtime mocks

* fix: keep codex compaction native

* fix: stabilize codex app-server hook tests

* test: isolate codex diagnostic active run

* test: remove codex diagnostic completion race

# Conflicts:
#	extensions/codex/src/app-server/run-attempt.test.ts

* ci: fix full release manifest performance run id

* refactor: narrow llm plugin sdk boundary

* chore: drop generated google boundary stamps

* fix: repair rebase fallout

* fix: clean up rebased runtime references

* fix: decode codex jwt payloads as base64url

* fix: preserve shipped pi runtime alias

* fix: add scoped sdk virtual modules

* fix: decode llm codex oauth jwt as base64url

* fix: avoid stale vertex adc negative cache

* fix: harden tool arg decoding and codeql path

* fix: keep vertex adc negative checks live

* refactor: consolidate codex jwt and edit helpers

* fix: await codex oauth node runtime imports

* fix: preserve sdk tool and notice contracts

* fix: preserve shipped compat config boundaries

* fix: align codex oauth callback host

* fix: terminate agent-core loop streams on failure

* fix: keep codex oauth callback alive during fallback

* ci: include session tools in critical codeql scans

* fix: keep Cloudflare Anthropic provider auth header

* docs: redirect legacy pi runtime pages

* fix: honor bundled web provider compat discovery

* fix: protect session output spill files

* fix: keep legacy agent dir env blocked

* fix: contain auto-discovered skill symlinks

* fix: harden agent core sdk proxy surfaces

* fix: restore approval reaction sdk compat

* fix: keep live docker runs bounded

* fix: keep codex oauth redirect host aligned

* fix: resolve post-rebase agent runtime drift

* fix: redact anthropic oauth parse failures

* fix: preserve responses strict tool shaping

* fix: repair agent runtime rebase cleanup

* docs: redirect retired parity pages

* fix: bound auto-discovered resources to roots

* fix: repair post-rebase agent test drift

* fix: preserve bundled provider allowlist migration

* fix: preserve manifest-owned provider aliases

* fix: declare photon image dependency

* fix: keep provider headers out of proxy body

* fix: preserve shipped env aliases

* fix: refresh control ui i18n generated state

* fix: quote read fallback paths

* fix: preview edits through configured backend

* test: satisfy core test typecheck

* fix: preserve ZAI usage auth fallback

* test: repair codex diagnostic test

* fix: repair agent runtime rebase drift

* test: finish embedded runner import rename

* fix: repair agent runtime rebase integrations

* test: align compaction oauth fallback expectations

* fix: allow sdk-auth session models

* fix: update doctor tool schema import

* fix: preserve bedrock plugin region

* fix: stream harmony-like prose immediately

* ci: include session runtime in codeql shards

* fix: repair latest rebase integrations

* fix: honor explicit codex websocket transport

* fix: keep openai-compatible credentials provider-scoped

* fix: refresh sdk api baseline after rebase

* fix: route cli runtime aliases through openclaw harness

* test: rename stale harness mock expectation

* test: rename embedded agent overflow calls

* test: clean embedded auth test wording

* test: use openclaw stream types in deepinfra cache test

* fix: refresh sdk api baseline on latest main

* fix: honor bundled discovery compat allowlists

* fix: refresh sdk api baseline after latest rebase

* fix: remove stale rebase imports

* test: rename stale model catalog mock

* test: mock renamed doctor runtime modules

* fix: map canonical kimi env auth

* fix: use internal model registry in bench script

* fix: migrate deepinfra provider catalog entry

* fix: enforce builtin tool suppression

* fix: route compaction auth and proxy payloads safely

* refactor: prune unused llm registry leftovers

* test: update codex hooks session import

* test: fix model picker ci coverage

* test: align model picker auth mock types
2026-05-27 19:24:04 +01:00

498 lines
16 KiB
TypeScript

import type {
ProviderAuthContext,
ProviderAuthMethodNonInteractiveContext,
} from "openclaw/plugin-sdk/plugin-entry";
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
const { readClaudeCliCredentialsForSetup, readClaudeCliCredentialsForSetupNonInteractive } =
vi.hoisted(() => ({
readClaudeCliCredentialsForSetup: vi.fn(),
readClaudeCliCredentialsForSetupNonInteractive: vi.fn(),
}));
vi.mock("./cli-auth-seam.js", async (importActual) => {
const actual = await importActual<typeof import("./cli-auth-seam.js")>();
return {
...actual,
readClaudeCliCredentialsForSetup,
readClaudeCliCredentialsForSetupNonInteractive,
};
});
const { buildAnthropicCliMigrationResult, hasClaudeCliAuth } = await import("./cli-migration.js");
const { resolveKnownAnthropicModelRef } = await import("./claude-model-refs.js");
const { createTestWizardPrompter, registerSingleProviderPlugin } =
await import("openclaw/plugin-sdk/plugin-test-runtime");
const { default: anthropicPlugin } = await import("./index.js");
beforeEach(() => {
readClaudeCliCredentialsForSetup.mockReset();
readClaudeCliCredentialsForSetupNonInteractive.mockReset();
});
afterAll(() => {
vi.doUnmock("./cli-auth-seam.js");
vi.resetModules();
});
describe("anthropic Claude model refs", () => {
it("upgrades retired refs without rewriting future canonical refs", () => {
expect(resolveKnownAnthropicModelRef("anthropic/claude-opus-4-5")).toBe(
"anthropic/claude-opus-4-7",
);
expect(resolveKnownAnthropicModelRef("anthropic/claude-opus-4-5@anthropic:work")).toBe(
"anthropic/claude-opus-4-7@anthropic:work",
);
expect(resolveKnownAnthropicModelRef("anthropic/claude-sonnet-4-20250514")).toBe(
"anthropic/claude-sonnet-4-6",
);
expect(resolveKnownAnthropicModelRef("anthropic/claude-opus-5-0")).toBe(
"anthropic/claude-opus-5-0",
);
expect(resolveKnownAnthropicModelRef("anthropic/claude-opus-4-10")).toBe(
"anthropic/claude-opus-4-10",
);
expect(resolveKnownAnthropicModelRef("anthropic/claude-sonnet-4-7")).toBe(
"anthropic/claude-sonnet-4-7",
);
});
});
async function resolveAnthropicCliAuthMethod() {
const provider = await registerSingleProviderPlugin(anthropicPlugin);
const method = provider.auth.find((entry) => entry.id === "cli");
if (!method) {
throw new Error("anthropic cli auth method missing");
}
return method;
}
function createProviderAuthContext(
config: ProviderAuthContext["config"] = {},
): ProviderAuthContext {
return {
config,
opts: {},
env: {},
agentDir: "/tmp/openclaw/agents/main",
workspaceDir: "/tmp/openclaw/workspace",
prompter: createTestWizardPrompter(),
runtime: {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
},
allowSecretRefPrompt: false,
isRemote: false,
openUrl: vi.fn(),
oauth: {
createVpsAwareHandlers: vi.fn(),
},
};
}
function createProviderAuthMethodNonInteractiveContext(
config: ProviderAuthMethodNonInteractiveContext["config"] = {},
): ProviderAuthMethodNonInteractiveContext {
return {
authChoice: "anthropic-cli",
config,
baseConfig: config,
opts: {},
runtime: {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
},
agentDir: "/tmp/openclaw/agents/main",
workspaceDir: "/tmp/openclaw/workspace",
resolveApiKey: vi.fn(async () => null),
toApiKeyCredential: vi.fn(() => null),
};
}
describe("anthropic cli migration", () => {
it("detects local Claude CLI auth", () => {
readClaudeCliCredentialsForSetup.mockReturnValue({ type: "oauth" });
expect(hasClaudeCliAuth()).toBe(true);
});
it("uses the non-interactive Claude auth probe without keychain prompts", () => {
readClaudeCliCredentialsForSetup.mockReset();
readClaudeCliCredentialsForSetupNonInteractive.mockReset();
readClaudeCliCredentialsForSetup.mockReturnValue(null);
readClaudeCliCredentialsForSetupNonInteractive.mockReturnValue({ type: "oauth" });
expect(hasClaudeCliAuth({ allowKeychainPrompt: false })).toBe(true);
expect(readClaudeCliCredentialsForSetup).not.toHaveBeenCalled();
expect(readClaudeCliCredentialsForSetupNonInteractive).toHaveBeenCalledTimes(1);
});
it("keeps anthropic defaults and selects the claude-cli runtime", () => {
const result = buildAnthropicCliMigrationResult({
agents: {
defaults: {
model: {
primary: "anthropic/claude-opus-4-7",
fallbacks: ["anthropic/claude-opus-4-6", "openai/gpt-5.2"],
},
models: {
"anthropic/claude-opus-4-7": { alias: "Opus" },
"anthropic/claude-opus-4-6": { alias: "Opus" },
"openai/gpt-5.2": {},
},
},
},
});
expect(result.profiles).toStrictEqual([]);
expect(result.defaultModel).toBe("anthropic/claude-opus-4-7");
expect(result.configPatch).toEqual({
agents: {
defaults: {
model: {
primary: "anthropic/claude-opus-4-7",
fallbacks: ["anthropic/claude-opus-4-6", "openai/gpt-5.2"],
},
models: {
"anthropic/claude-opus-4-7": {
alias: "Opus",
agentRuntime: { id: "claude-cli" },
},
"anthropic/claude-sonnet-4-6": { agentRuntime: { id: "claude-cli" } },
"anthropic/claude-opus-4-6": {
alias: "Opus",
agentRuntime: { id: "claude-cli" },
},
"openai/gpt-5.2": {},
},
},
},
});
});
it("routes provider-qualified shorthand refs through Claude CLI without dropping the raw ref", () => {
const result = buildAnthropicCliMigrationResult({
agents: {
defaults: {
model: {
primary: "anthropic/opus-4.7",
fallbacks: ["anthropic/sonnet-4.6", "openai/gpt-5.2"],
},
models: {
"anthropic/opus-4.7": { alias: "Opus shorthand" },
"anthropic/sonnet-4.6": { alias: "Sonnet shorthand" },
},
},
},
});
const defaults = result.configPatch?.agents?.defaults;
expect(defaults?.model).toEqual({
primary: "anthropic/claude-opus-4-7",
fallbacks: ["anthropic/claude-sonnet-4-6", "openai/gpt-5.2"],
});
expect(defaults?.models?.["anthropic/opus-4.7"]).toEqual({
alias: "Opus shorthand",
agentRuntime: { id: "claude-cli" },
});
expect(defaults?.models?.["anthropic/claude-opus-4-7"]).toEqual({
alias: "Opus shorthand",
agentRuntime: { id: "claude-cli" },
});
expect(defaults?.models?.["anthropic/sonnet-4.6"]).toEqual({
alias: "Sonnet shorthand",
agentRuntime: { id: "claude-cli" },
});
expect(defaults?.models?.["anthropic/claude-sonnet-4-6"]).toEqual({
alias: "Sonnet shorthand",
agentRuntime: { id: "claude-cli" },
});
});
it("keeps unknown Anthropic refs raw while still selecting Claude CLI", () => {
const result = buildAnthropicCliMigrationResult({
agents: {
defaults: {
model: { primary: "anthropic/opus-5.0" },
models: {
"anthropic/opus-5.0": { alias: "Future Opus" },
},
},
},
});
const defaults = result.configPatch?.agents?.defaults;
expect(result.defaultModel).toBe("anthropic/opus-5.0");
expect(defaults?.model).toBeUndefined();
expect(defaults?.models?.["anthropic/opus-5.0"]).toEqual({
alias: "Future Opus",
agentRuntime: { id: "claude-cli" },
});
expect(defaults?.models?.["anthropic/claude-opus-5-0"]).toBeUndefined();
});
it("adds a Claude CLI default when no anthropic default is present", () => {
const result = buildAnthropicCliMigrationResult({
agents: {
defaults: {
model: { primary: "openai/gpt-5.2" },
models: {
"openai/gpt-5.2": {},
},
},
},
});
expect(result.defaultModel).toBe("anthropic/claude-opus-4-7");
expect(result.configPatch).toEqual({
agents: {
defaults: {
models: {
"openai/gpt-5.2": {},
"anthropic/claude-opus-4-7": { agentRuntime: { id: "claude-cli" } },
"anthropic/claude-sonnet-4-6": { agentRuntime: { id: "claude-cli" } },
"anthropic/claude-opus-4-6": { agentRuntime: { id: "claude-cli" } },
},
},
},
});
});
it("does not treat bare non-Claude model refs as Anthropic", () => {
const result = buildAnthropicCliMigrationResult({
agents: {
defaults: {
model: { primary: "gpt-5.2" },
models: {
"openai/gpt-5.2": {},
},
},
},
});
expect(result.defaultModel).toBe("anthropic/claude-opus-4-7");
expect(result.configPatch?.agents?.defaults?.model).toBeUndefined();
expect(result.configPatch?.agents?.defaults?.models?.["anthropic/gpt-5.2"]).toBeUndefined();
});
it("backfills the Claude CLI allowlist when older configs only stored sonnet", () => {
const result = buildAnthropicCliMigrationResult({
agents: {
defaults: {
model: { primary: "claude-cli/claude-opus-4-7" },
models: {
"claude-cli/claude-opus-4-7": {},
},
},
},
});
expect(result.configPatch).toEqual({
agents: {
defaults: {
model: { primary: "anthropic/claude-opus-4-7" },
models: {
"anthropic/claude-opus-4-7": { agentRuntime: { id: "claude-cli" } },
"anthropic/claude-sonnet-4-6": { agentRuntime: { id: "claude-cli" } },
"anthropic/claude-opus-4-6": { agentRuntime: { id: "claude-cli" } },
},
},
},
});
});
it("preserves explicit model runtime policy while filling missing Claude CLI policies", () => {
const result = buildAnthropicCliMigrationResult({
agents: {
defaults: {
model: {
primary: "anthropic/claude-opus-4-7",
fallbacks: ["anthropic/claude-sonnet-4-6"],
},
models: {
"anthropic/claude-opus-4-7": {
alias: "Opus",
agentRuntime: { id: "openclaw" },
},
"anthropic/claude-sonnet-4-6": {
alias: "Sonnet",
agentRuntime: { id: "auto" },
},
},
},
},
});
const defaults = result.configPatch?.agents?.defaults;
if (!defaults) {
throw new Error("Expected Claude CLI migration to return default agent config");
}
expect(defaults.models?.["anthropic/claude-opus-4-7"]).toEqual({
alias: "Opus",
agentRuntime: { id: "openclaw" },
});
expect(defaults.models?.["anthropic/claude-sonnet-4-6"]).toEqual({
alias: "Sonnet",
agentRuntime: { id: "claude-cli" },
});
});
it("registered cli auth tells users to run claude auth login when local auth is missing", async () => {
readClaudeCliCredentialsForSetup.mockReturnValue(null);
const method = await resolveAnthropicCliAuthMethod();
await expect(method.run(createProviderAuthContext())).rejects.toThrow(
[
"Claude CLI is not authenticated on this host.",
"Run claude auth login first, then re-run this setup.",
].join("\n"),
);
});
it("registered cli auth returns the same migration result as the builder", async () => {
const credential = {
type: "oauth",
provider: "anthropic",
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 60_000,
} as const;
readClaudeCliCredentialsForSetup.mockReturnValue(credential);
const method = await resolveAnthropicCliAuthMethod();
const config = {
agents: {
defaults: {
model: {
primary: "anthropic/claude-opus-4-7",
fallbacks: ["anthropic/claude-opus-4-6", "openai/gpt-5.2"],
},
models: {
"anthropic/claude-opus-4-7": { alias: "Opus" },
"anthropic/claude-opus-4-6": { alias: "Opus" },
"openai/gpt-5.2": {},
},
},
},
};
await expect(method.run(createProviderAuthContext(config))).resolves.toEqual(
buildAnthropicCliMigrationResult(config, credential),
);
});
it("stores a claude-cli oauth profile when Claude CLI credentials are available", () => {
const result = buildAnthropicCliMigrationResult(
{},
{
type: "oauth",
provider: "anthropic",
access: "access-token",
refresh: "refresh-token",
expires: 123,
},
);
expect(result.profiles).toEqual([
{
profileId: "anthropic:claude-cli",
credential: {
type: "oauth",
provider: "claude-cli",
access: "access-token",
refresh: "refresh-token",
expires: 123,
},
},
]);
});
it("stores a claude-cli token profile when Claude CLI only exposes a bearer token", () => {
const result = buildAnthropicCliMigrationResult(
{},
{
type: "token",
provider: "anthropic",
token: "bearer-token",
expires: 123,
},
);
expect(result.profiles).toEqual([
{
profileId: "anthropic:claude-cli",
credential: {
type: "token",
provider: "claude-cli",
token: "bearer-token",
expires: 123,
},
},
]);
});
it("registered non-interactive cli auth keeps anthropic fallbacks and selects claude-cli runtime", async () => {
readClaudeCliCredentialsForSetupNonInteractive.mockReturnValue({
type: "oauth",
provider: "anthropic",
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 60_000,
});
const method = await resolveAnthropicCliAuthMethod();
const config = {
agents: {
defaults: {
model: {
primary: "anthropic/claude-opus-4-7",
fallbacks: ["anthropic/claude-opus-4-6", "openai/gpt-5.2"],
},
models: {
"anthropic/claude-opus-4-7": { alias: "Opus" },
"anthropic/claude-opus-4-6": { alias: "Opus" },
"openai/gpt-5.2": {},
},
},
},
};
const result = await method.runNonInteractive?.(
createProviderAuthMethodNonInteractiveContext(config),
);
const defaults = result?.agents?.defaults as
| {
model?: { primary?: string; fallbacks?: string[] };
models?: Record<string, unknown>;
}
| undefined;
expect(defaults?.model?.primary).toBe("anthropic/claude-opus-4-7");
expect(defaults?.model?.fallbacks).toEqual(["anthropic/claude-opus-4-6", "openai/gpt-5.2"]);
expect(defaults?.models?.["anthropic/claude-opus-4-7"]).toEqual({
alias: "Opus",
agentRuntime: { id: "claude-cli" },
});
expect(defaults?.models?.["anthropic/claude-opus-4-6"]).toEqual({
alias: "Opus",
agentRuntime: { id: "claude-cli" },
});
expect(defaults?.models?.["openai/gpt-5.2"]).toEqual({});
});
it("registered non-interactive cli auth reports missing local auth and exits cleanly", async () => {
readClaudeCliCredentialsForSetupNonInteractive.mockReturnValue(null);
const method = await resolveAnthropicCliAuthMethod();
const ctx = createProviderAuthMethodNonInteractiveContext();
await expect(method.runNonInteractive?.(ctx)).resolves.toBeNull();
expect(ctx.runtime.error).toHaveBeenCalledWith(
[
'Auth choice "anthropic-cli" requires Claude CLI auth on this host.',
"Run claude auth login first.",
].join("\n"),
);
expect(ctx.runtime.exit).toHaveBeenCalledWith(1);
});
});