feat(backend): queue-time runtime/profile validation; job.runtime authoritative in worker

This commit is contained in:
Gabriel Brown
2026-07-11 10:58:05 -04:00
parent 70396feccc
commit c0f107c4cf
7 changed files with 250 additions and 32 deletions
@@ -1,4 +1,8 @@
import { describe, expect, test, vi } from 'vitest';
import { mkdtemp, readFile, rm, stat } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, test, vi } from 'vitest';
import type { NormalizedAgentEvent } from '../../src/agent-events';
import type { ExecStreamFn } from '../../src/runtime/agent-runtime';
@@ -51,6 +55,69 @@ const makeWorkspace = (): AdapterWorkspace => ({
runtime: 'claude',
});
const makeOAuthWorkspace = (
homeDir: string,
authType: 'anthropic_oauth_json' | 'opencode_auth_json',
): AdapterWorkspace => ({
claim: {
...claim,
aiProviderProfile: {
id: 'p',
name: 'Anthropic',
provider: 'anthropic',
authType,
secret: JSON.stringify({ access_token: 'oauth-token' }),
model: 'claude-sonnet-4',
reasoningEffort: 'medium',
},
} as unknown as Claim,
workdir: homeDir,
homeDir,
username: 'tester',
containerHome: '/home/tester',
containerRepo: '/home/tester/Code/spoon',
repoDir: path.join(homeDir, 'Code/spoon'),
boxName: 'spoon-box-tester',
redact: (value: string) => value,
runtime: 'claude',
});
const tempDirs: string[] = [];
afterEach(async () => {
while (tempDirs.length) {
const dir = tempDirs.pop();
if (dir) await rm(dir, { recursive: true, force: true });
}
});
describe('ClaudeCodeAdapter prepareAuth', () => {
test('writes credentials.json for an anthropic_oauth_json profile', async () => {
const { createClaudeAdapter } = await loadAdapter();
const homeDir = await mkdtemp(path.join(tmpdir(), 'spoon-claude-oauth-'));
tempDirs.push(homeDir);
const adapter = createClaudeAdapter();
await adapter.prepareAuth(makeOAuthWorkspace(homeDir, 'anthropic_oauth_json'));
const credentialsPath = path.join(homeDir, '.claude', '.credentials.json');
const contents = await readFile(credentialsPath, 'utf8');
expect(contents).toContain('oauth-token');
});
test('does not write credentials.json for an opencode_auth_json profile', async () => {
const { createClaudeAdapter } = await loadAdapter();
const homeDir = await mkdtemp(path.join(tmpdir(), 'spoon-claude-oauth-'));
tempDirs.push(homeDir);
const adapter = createClaudeAdapter();
await adapter.prepareAuth(makeOAuthWorkspace(homeDir, 'opencode_auth_json'));
const credentialsPath = path.join(homeDir, '.claude', '.credentials.json');
await expect(stat(credentialsPath)).rejects.toThrow();
});
});
describe('ClaudeCodeAdapter', () => {
test('streams normalized events and marks the turn via buildMarkedCommand', async () => {
const { createClaudeAdapter } = await loadAdapter();