refactor(worker): extract AgentRuntime interface, shared provider helpers, and adapter registry
This commit is contained in:
@@ -0,0 +1,49 @@
|
|||||||
|
import type { NormalizedAgentEvent } from '../agent-events';
|
||||||
|
import type { streamExecInContainer } from './docker';
|
||||||
|
import type { AdapterWorkspace } from './provider';
|
||||||
|
|
||||||
|
export type AgentRuntimeName = 'codex' | 'opencode' | 'claude';
|
||||||
|
|
||||||
|
export type ExecStreamFn = typeof streamExecInContainer;
|
||||||
|
|
||||||
|
export type TurnResult = {
|
||||||
|
// Assistant text the adapter captured out-of-band (Codex --output-last-message
|
||||||
|
// file, Claude `result` event). Empty/undefined when everything streamed via onEvent.
|
||||||
|
finalMessage?: string;
|
||||||
|
// Runtime session id to persist for continuity (resume/--session/--resume).
|
||||||
|
sessionId?: string;
|
||||||
|
// Non-empty when the runtime reported a hard failure the caller must surface.
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AgentRuntime = {
|
||||||
|
readonly name: AgentRuntimeName;
|
||||||
|
prepareAuth(workspace: AdapterWorkspace): Promise<void>;
|
||||||
|
runTurn(
|
||||||
|
workspace: AdapterWorkspace,
|
||||||
|
prompt: string,
|
||||||
|
onEvent: (event: NormalizedAgentEvent) => Promise<void>,
|
||||||
|
): Promise<TurnResult>;
|
||||||
|
abort(workspace: AdapterWorkspace): Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AdapterFactory = (deps?: {
|
||||||
|
execStream?: ExecStreamFn;
|
||||||
|
}) => AgentRuntime;
|
||||||
|
|
||||||
|
const factories = new Map<AgentRuntimeName, AdapterFactory>();
|
||||||
|
|
||||||
|
export const registerAdapter = (
|
||||||
|
name: AgentRuntimeName,
|
||||||
|
factory: AdapterFactory,
|
||||||
|
): void => {
|
||||||
|
factories.set(name, factory);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAdapter = (name: AgentRuntimeName): AgentRuntime => {
|
||||||
|
const factory = factories.get(name);
|
||||||
|
if (!factory) {
|
||||||
|
throw new Error(`No agent runtime adapter registered for "${name}".`);
|
||||||
|
}
|
||||||
|
return factory();
|
||||||
|
};
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import type { Id } from '@spoon/backend/convex/_generated/dataModel.js';
|
||||||
|
|
||||||
|
import type { AgentRuntimeName } from './agent-runtime';
|
||||||
|
|
||||||
|
export type Claim = {
|
||||||
|
job: {
|
||||||
|
_id: Id<'agentJobs'>;
|
||||||
|
prompt: string;
|
||||||
|
runtime?: 'codex' | 'opencode' | 'claude';
|
||||||
|
jobType?: 'user_change' | 'maintenance_review' | 'conflict_resolution';
|
||||||
|
envFilePath?: string;
|
||||||
|
materializeEnvFile?: boolean;
|
||||||
|
baseBranch: string;
|
||||||
|
workBranch: string;
|
||||||
|
forkOwner: string;
|
||||||
|
forkRepo: string;
|
||||||
|
upstreamOwner: string;
|
||||||
|
upstreamRepo: string;
|
||||||
|
};
|
||||||
|
spoon: { name: string };
|
||||||
|
openai: {
|
||||||
|
apiKey?: string;
|
||||||
|
model: string;
|
||||||
|
reasoningEffort: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
|
||||||
|
};
|
||||||
|
aiProviderProfile?: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
provider:
|
||||||
|
| 'openai'
|
||||||
|
| 'anthropic'
|
||||||
|
| 'google'
|
||||||
|
| 'openrouter'
|
||||||
|
| 'requesty'
|
||||||
|
| 'litellm'
|
||||||
|
| 'cloudflare_ai_gateway'
|
||||||
|
| 'custom_openai_compatible'
|
||||||
|
| 'opencode_openai_login';
|
||||||
|
authType: 'api_key' | 'opencode_auth_json' | 'none';
|
||||||
|
secret?: string;
|
||||||
|
baseUrl?: string;
|
||||||
|
model: string;
|
||||||
|
reasoningEffort: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
|
||||||
|
};
|
||||||
|
github: { installationId?: string };
|
||||||
|
agentSettings?: {
|
||||||
|
installCommand?: string;
|
||||||
|
checkCommand?: string;
|
||||||
|
testCommand?: string;
|
||||||
|
autoDetectCommands?: boolean;
|
||||||
|
} | null;
|
||||||
|
secrets: { name: string; value: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// The subset of the worker's active workspace that agent adapters read.
|
||||||
|
export type AdapterWorkspace = {
|
||||||
|
claim: Claim;
|
||||||
|
workdir: string;
|
||||||
|
homeDir: string;
|
||||||
|
username: string;
|
||||||
|
containerHome: string;
|
||||||
|
containerRepo: string;
|
||||||
|
repoDir: string;
|
||||||
|
boxName: string;
|
||||||
|
redact: (value: string) => string;
|
||||||
|
runtime: AgentRuntimeName;
|
||||||
|
codexSessionId?: string;
|
||||||
|
opencodeSessionId?: string;
|
||||||
|
claudeSessionId?: string;
|
||||||
|
turnMarker?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isCodexLoginProfile = (claim: Claim) =>
|
||||||
|
claim.aiProviderProfile?.provider === 'opencode_openai_login' ||
|
||||||
|
claim.aiProviderProfile?.authType === 'opencode_auth_json';
|
||||||
|
|
||||||
|
export const collectJsonStringValues = (value?: string): string[] => {
|
||||||
|
if (!value) return [];
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value) as unknown;
|
||||||
|
const values: string[] = [];
|
||||||
|
const visit = (item: unknown) => {
|
||||||
|
if (typeof item === 'string') {
|
||||||
|
if (item.length >= 12) values.push(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Array.isArray(item)) {
|
||||||
|
item.forEach(visit);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (item && typeof item === 'object') {
|
||||||
|
Object.values(item).forEach(visit);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
visit(parsed);
|
||||||
|
return values;
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const providerEnvironment = (
|
||||||
|
claim: Claim,
|
||||||
|
workspaceRoot?: string,
|
||||||
|
): Record<string, string> => {
|
||||||
|
if (isCodexLoginProfile(claim)) {
|
||||||
|
if (!workspaceRoot) {
|
||||||
|
throw new Error('Codex auth profiles require a prepared workspace.');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
CODEX_HOME: path.join(workspaceRoot, '.codex'),
|
||||||
|
HOME: workspaceRoot,
|
||||||
|
XDG_DATA_HOME: path.join(workspaceRoot, '.local', 'share'),
|
||||||
|
XDG_CONFIG_HOME: path.join(workspaceRoot, '.config'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const profile = claim.aiProviderProfile;
|
||||||
|
const secret = profile?.secret ?? claim.openai.apiKey;
|
||||||
|
if (!secret) {
|
||||||
|
throw new Error('No AI provider credential is configured for this job.');
|
||||||
|
}
|
||||||
|
const baseUrl: Record<string, string> = profile?.baseUrl
|
||||||
|
? { OPENAI_BASE_URL: profile.baseUrl }
|
||||||
|
: {};
|
||||||
|
if (!profile || profile.provider === 'openai') {
|
||||||
|
return { OPENAI_API_KEY: secret, ...baseUrl };
|
||||||
|
}
|
||||||
|
if (profile.provider === 'anthropic') return { ANTHROPIC_API_KEY: secret };
|
||||||
|
if (profile.provider === 'google') return { GOOGLE_API_KEY: secret };
|
||||||
|
if (profile.provider === 'openrouter') {
|
||||||
|
return { OPENROUTER_API_KEY: secret, ...baseUrl };
|
||||||
|
}
|
||||||
|
if (profile.provider === 'requesty') {
|
||||||
|
return { REQUESTY_API_KEY: secret, ...baseUrl };
|
||||||
|
}
|
||||||
|
if (profile.provider === 'cloudflare_ai_gateway') {
|
||||||
|
return { CLOUDFLARE_API_KEY: secret, ...baseUrl };
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
profile.provider === 'litellm' ||
|
||||||
|
profile.provider === 'custom_openai_compatible'
|
||||||
|
) {
|
||||||
|
return { OPENAI_API_KEY: secret, ...baseUrl };
|
||||||
|
}
|
||||||
|
throw new Error('Unsupported AI provider profile.');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const opencodeModel = (claim: Claim) => {
|
||||||
|
const profile = claim.aiProviderProfile;
|
||||||
|
const model = profile?.model ?? claim.openai.model;
|
||||||
|
if (model.includes('/')) return model;
|
||||||
|
if (!profile) return `openai/${model}`;
|
||||||
|
if (
|
||||||
|
profile.provider === 'custom_openai_compatible' ||
|
||||||
|
profile.provider === 'cloudflare_ai_gateway'
|
||||||
|
) {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
if (profile.provider === 'opencode_openai_login') return `openai/${model}`;
|
||||||
|
return `${profile.provider}/${model}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const codexModel = (claim: Claim) => {
|
||||||
|
const model = claim.aiProviderProfile?.model ?? claim.openai.model;
|
||||||
|
return model.includes('/') ? (model.split('/').at(-1) ?? model) : model;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const codexModelArgs = (claim: Claim) =>
|
||||||
|
isCodexLoginProfile(claim) ? [] : ['--model', codexModel(claim)];
|
||||||
+15
-163
@@ -16,6 +16,7 @@ import { api } from '@spoon/backend/convex/_generated/api.js';
|
|||||||
|
|
||||||
import type { NormalizedAgentEvent } from './agent-events';
|
import type { NormalizedAgentEvent } from './agent-events';
|
||||||
import type { OpenCodeSession } from './opencode-session';
|
import type { OpenCodeSession } from './opencode-session';
|
||||||
|
import type { AdapterWorkspace, Claim } from './runtime/provider';
|
||||||
import type { BoxHandle } from './user-container';
|
import type { BoxHandle } from './user-container';
|
||||||
import { normalizeCodexJsonLine } from './agent-events';
|
import { normalizeCodexJsonLine } from './agent-events';
|
||||||
import { abortManagedCodexProcess, managedCodexCommand } from './codex-process';
|
import { abortManagedCodexProcess, managedCodexCommand } from './codex-process';
|
||||||
@@ -45,82 +46,26 @@ import {
|
|||||||
stopWorkspaceContainer,
|
stopWorkspaceContainer,
|
||||||
streamExecInContainer,
|
streamExecInContainer,
|
||||||
} from './runtime/docker';
|
} from './runtime/docker';
|
||||||
|
import {
|
||||||
|
codexModelArgs,
|
||||||
|
collectJsonStringValues,
|
||||||
|
isCodexLoginProfile,
|
||||||
|
opencodeModel,
|
||||||
|
providerEnvironment,
|
||||||
|
} from './runtime/provider';
|
||||||
import { acquireUserBox, runningBoxUsernames } from './user-container';
|
import { acquireUserBox, runningBoxUsernames } from './user-container';
|
||||||
import { fetchUserEnvironment, materializeUserHome } from './user-environment';
|
import { fetchUserEnvironment, materializeUserHome } from './user-environment';
|
||||||
import { teardownWorkspace } from './workspace-teardown';
|
import { teardownWorkspace } from './workspace-teardown';
|
||||||
|
|
||||||
type Claim = {
|
type ActiveWorkspace = AdapterWorkspace & {
|
||||||
job: {
|
|
||||||
_id: Id<'agentJobs'>;
|
|
||||||
prompt: string;
|
|
||||||
runtime?: 'openai_direct' | 'opencode';
|
|
||||||
jobType?: 'user_change' | 'maintenance_review' | 'conflict_resolution';
|
|
||||||
envFilePath?: string;
|
|
||||||
materializeEnvFile?: boolean;
|
|
||||||
baseBranch: string;
|
|
||||||
workBranch: string;
|
|
||||||
forkOwner: string;
|
|
||||||
forkRepo: string;
|
|
||||||
upstreamOwner: string;
|
|
||||||
upstreamRepo: string;
|
|
||||||
};
|
|
||||||
spoon: { name: string };
|
|
||||||
openai: {
|
|
||||||
apiKey?: string;
|
|
||||||
model: string;
|
|
||||||
reasoningEffort: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
|
|
||||||
};
|
|
||||||
aiProviderProfile?: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
provider:
|
|
||||||
| 'openai'
|
|
||||||
| 'anthropic'
|
|
||||||
| 'google'
|
|
||||||
| 'openrouter'
|
|
||||||
| 'requesty'
|
|
||||||
| 'litellm'
|
|
||||||
| 'cloudflare_ai_gateway'
|
|
||||||
| 'custom_openai_compatible'
|
|
||||||
| 'opencode_openai_login';
|
|
||||||
authType: 'api_key' | 'opencode_auth_json' | 'none';
|
|
||||||
secret?: string;
|
|
||||||
baseUrl?: string;
|
|
||||||
model: string;
|
|
||||||
reasoningEffort: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
|
|
||||||
};
|
|
||||||
github: { installationId?: string };
|
|
||||||
agentSettings?: {
|
|
||||||
installCommand?: string;
|
|
||||||
checkCommand?: string;
|
|
||||||
testCommand?: string;
|
|
||||||
autoDetectCommands?: boolean;
|
|
||||||
} | null;
|
|
||||||
secrets: { name: string; value: string }[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type ActiveWorkspace = {
|
|
||||||
claim: Claim;
|
|
||||||
// Host path of the persistent per-user home (mounted at `containerHome`).
|
|
||||||
// Equal to `homeDir`; kept as `workdir` for the container mount source.
|
|
||||||
workdir: string;
|
|
||||||
homeDir: string;
|
|
||||||
username: string;
|
|
||||||
// In-container paths: HOME and the thread's checkout (~/Code/{spoon}/{branch}).
|
|
||||||
containerHome: string;
|
|
||||||
containerRepo: string;
|
|
||||||
repoDir: string;
|
|
||||||
// Phase 2: the per-user box container this thread execs into.
|
// Phase 2: the per-user box container this thread execs into.
|
||||||
boxName: string;
|
|
||||||
boxHandle: BoxHandle;
|
boxHandle: BoxHandle;
|
||||||
githubToken: string;
|
githubToken: string;
|
||||||
redact: (value: string) => string;
|
|
||||||
runtimeMode?: 'opencode_server' | 'codex_exec' | 'legacy_cli';
|
runtimeMode?: 'opencode_server' | 'codex_exec' | 'legacy_cli';
|
||||||
containerName?: string;
|
containerName?: string;
|
||||||
containerId?: string;
|
containerId?: string;
|
||||||
opencodePassword?: string;
|
opencodePassword?: string;
|
||||||
opencodeSession?: OpenCodeSession;
|
opencodeSession?: OpenCodeSession;
|
||||||
codexSessionId?: string;
|
|
||||||
codexPidFile?: string;
|
codexPidFile?: string;
|
||||||
agentTurnActive?: boolean;
|
agentTurnActive?: boolean;
|
||||||
cancelRequested?: boolean;
|
cancelRequested?: boolean;
|
||||||
@@ -381,104 +326,6 @@ const commandToShell = (command: string) => ['bash', '-lc', command];
|
|||||||
const workspaceContainerName = (jobId: string) =>
|
const workspaceContainerName = (jobId: string) =>
|
||||||
`spoon-agent-job-${jobId.replace(/[^a-zA-Z0-9_.-]/g, '-')}`;
|
`spoon-agent-job-${jobId.replace(/[^a-zA-Z0-9_.-]/g, '-')}`;
|
||||||
|
|
||||||
const isCodexLoginProfile = (claim: Claim) =>
|
|
||||||
claim.aiProviderProfile?.provider === 'opencode_openai_login' ||
|
|
||||||
claim.aiProviderProfile?.authType === 'opencode_auth_json';
|
|
||||||
|
|
||||||
const collectJsonStringValues = (value?: string): string[] => {
|
|
||||||
if (!value) return [];
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(value) as unknown;
|
|
||||||
const values: string[] = [];
|
|
||||||
const visit = (item: unknown) => {
|
|
||||||
if (typeof item === 'string') {
|
|
||||||
if (item.length >= 12) values.push(item);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Array.isArray(item)) {
|
|
||||||
item.forEach(visit);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (item && typeof item === 'object') {
|
|
||||||
Object.values(item).forEach(visit);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
visit(parsed);
|
|
||||||
return values;
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const providerEnvironment = (
|
|
||||||
claim: Claim,
|
|
||||||
workspaceRoot?: string,
|
|
||||||
): Record<string, string> => {
|
|
||||||
if (isCodexLoginProfile(claim)) {
|
|
||||||
if (!workspaceRoot) {
|
|
||||||
throw new Error('Codex auth profiles require a prepared workspace.');
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
CODEX_HOME: path.join(workspaceRoot, '.codex'),
|
|
||||||
HOME: workspaceRoot,
|
|
||||||
XDG_DATA_HOME: path.join(workspaceRoot, '.local', 'share'),
|
|
||||||
XDG_CONFIG_HOME: path.join(workspaceRoot, '.config'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const profile = claim.aiProviderProfile;
|
|
||||||
const secret = profile?.secret ?? claim.openai.apiKey;
|
|
||||||
if (!secret) {
|
|
||||||
throw new Error('No AI provider credential is configured for this job.');
|
|
||||||
}
|
|
||||||
const baseUrl: Record<string, string> = profile?.baseUrl
|
|
||||||
? { OPENAI_BASE_URL: profile.baseUrl }
|
|
||||||
: {};
|
|
||||||
if (!profile || profile.provider === 'openai') {
|
|
||||||
return { OPENAI_API_KEY: secret, ...baseUrl };
|
|
||||||
}
|
|
||||||
if (profile.provider === 'anthropic') return { ANTHROPIC_API_KEY: secret };
|
|
||||||
if (profile.provider === 'google') return { GOOGLE_API_KEY: secret };
|
|
||||||
if (profile.provider === 'openrouter') {
|
|
||||||
return { OPENROUTER_API_KEY: secret, ...baseUrl };
|
|
||||||
}
|
|
||||||
if (profile.provider === 'requesty') {
|
|
||||||
return { REQUESTY_API_KEY: secret, ...baseUrl };
|
|
||||||
}
|
|
||||||
if (profile.provider === 'cloudflare_ai_gateway') {
|
|
||||||
return { CLOUDFLARE_API_KEY: secret, ...baseUrl };
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
profile.provider === 'litellm' ||
|
|
||||||
profile.provider === 'custom_openai_compatible'
|
|
||||||
) {
|
|
||||||
return { OPENAI_API_KEY: secret, ...baseUrl };
|
|
||||||
}
|
|
||||||
throw new Error('Unsupported AI provider profile.');
|
|
||||||
};
|
|
||||||
|
|
||||||
const opencodeModel = (claim: Claim) => {
|
|
||||||
const profile = claim.aiProviderProfile;
|
|
||||||
const model = profile?.model ?? claim.openai.model;
|
|
||||||
if (model.includes('/')) return model;
|
|
||||||
if (!profile) return `openai/${model}`;
|
|
||||||
if (
|
|
||||||
profile.provider === 'custom_openai_compatible' ||
|
|
||||||
profile.provider === 'cloudflare_ai_gateway'
|
|
||||||
) {
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
if (profile.provider === 'opencode_openai_login') return `openai/${model}`;
|
|
||||||
return `${profile.provider}/${model}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const codexModel = (claim: Claim) => {
|
|
||||||
const model = claim.aiProviderProfile?.model ?? claim.openai.model;
|
|
||||||
return model.includes('/') ? (model.split('/').at(-1) ?? model) : model;
|
|
||||||
};
|
|
||||||
|
|
||||||
const codexModelArgs = (claim: Claim) =>
|
|
||||||
isCodexLoginProfile(claim) ? [] : ['--model', codexModel(claim)];
|
|
||||||
|
|
||||||
const writeJsonFile = async (filePath: string, content: string) => {
|
const writeJsonFile = async (filePath: string, content: string) => {
|
||||||
let normalized = content.trim();
|
let normalized = content.trim();
|
||||||
try {
|
try {
|
||||||
@@ -1432,6 +1279,8 @@ const runClaim = async (claim: Claim) => {
|
|||||||
boxHandle,
|
boxHandle,
|
||||||
githubToken,
|
githubToken,
|
||||||
redact,
|
redact,
|
||||||
|
// Placeholder until Task 3 resolves the runtime from the claim/profile.
|
||||||
|
runtime: 'codex',
|
||||||
};
|
};
|
||||||
if (userEnv) {
|
if (userEnv) {
|
||||||
await appendEvent(
|
await appendEvent(
|
||||||
@@ -2167,7 +2016,10 @@ export const startWorker = async () => {
|
|||||||
await sleep(env.pollMs);
|
await sleep(env.pollMs);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
await runClaim(claim);
|
// The Convex claim still carries the legacy `openai_direct` runtime
|
||||||
|
// literal, which `runClaim` rejects. Narrow to the shared `Claim` shape
|
||||||
|
// (widened to the codex/opencode/claude union) at this boundary.
|
||||||
|
await runClaim(claim as Claim);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
await sleep(env.pollMs);
|
await sleep(env.pollMs);
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import type { AgentRuntime } from '../../src/runtime/agent-runtime';
|
||||||
|
import type { Claim } from '../../src/runtime/provider';
|
||||||
|
import { getAdapter, registerAdapter } from '../../src/runtime/agent-runtime';
|
||||||
|
import { isCodexLoginProfile, opencodeModel } from '../../src/runtime/provider';
|
||||||
|
|
||||||
|
describe('agent runtime registry', () => {
|
||||||
|
test('registerAdapter/getAdapter resolves a registered runtime', () => {
|
||||||
|
const fake: AgentRuntime = {
|
||||||
|
name: 'codex',
|
||||||
|
prepareAuth: async () => {},
|
||||||
|
runTurn: async () => ({}),
|
||||||
|
abort: async () => {},
|
||||||
|
};
|
||||||
|
registerAdapter('codex', () => fake);
|
||||||
|
expect(getAdapter('codex').name).toBe('codex');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getAdapter throws for an unregistered runtime', () => {
|
||||||
|
expect(() => getAdapter('opencode')).toThrow(/No agent runtime adapter/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shared provider helpers re-exported from runtime/provider', () => {
|
||||||
|
test('isCodexLoginProfile detects opencode login profiles', () => {
|
||||||
|
expect(
|
||||||
|
isCodexLoginProfile({
|
||||||
|
aiProviderProfile: { provider: 'opencode_openai_login' },
|
||||||
|
} as Claim),
|
||||||
|
).toBe(true);
|
||||||
|
expect(isCodexLoginProfile({} as Claim)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('opencodeModel prefixes the provider', () => {
|
||||||
|
expect(
|
||||||
|
opencodeModel({
|
||||||
|
aiProviderProfile: { provider: 'anthropic', model: 'claude-x' },
|
||||||
|
} as Claim),
|
||||||
|
).toBe('anthropic/claude-x');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -156,6 +156,7 @@ describe('maintenance workspace cleanup', () => {
|
|||||||
boxHandle: { boxName: 'spoon-box-tester', release },
|
boxHandle: { boxName: 'spoon-box-tester', release },
|
||||||
githubToken: 'github-token',
|
githubToken: 'github-token',
|
||||||
redact: (value: string) => value,
|
redact: (value: string) => value,
|
||||||
|
runtime: 'codex',
|
||||||
containerName: 'spoon-agent-job-maintenance',
|
containerName: 'spoon-agent-job-maintenance',
|
||||||
opencodeSession: {
|
opencodeSession: {
|
||||||
client: {} as never,
|
client: {} as never,
|
||||||
|
|||||||
Reference in New Issue
Block a user