Files
spoon/packages/backend/tests/unit/runtime-validation.test.ts

134 lines
3.9 KiB
TypeScript

import { convexTest } from 'convex-test';
import { describe, expect, test } from 'vitest';
import type { Id } from '../../convex/_generated/dataModel.js';
import { api } from '../../convex/_generated/api.js';
import schema from '../../convex/schema';
const modules = import.meta.glob('../../convex/**/*.*s');
const createUser = async (t: ReturnType<typeof convexTest>, email: string) =>
(await t.mutation(async (ctx) => {
return await ctx.db.insert('users', { email, name: email });
})) as Id<'users'>;
const authed = (t: ReturnType<typeof convexTest>, userId: string) =>
t.withIdentity({
subject: `${userId}|session`,
issuer: 'https://convex.test',
});
const githubSpoonInput = {
name: 'Editor Spoon',
provider: 'github' as const,
upstreamOwner: 'upstream',
upstreamRepo: 'editor',
upstreamDefaultBranch: 'main',
upstreamUrl: 'https://github.com/upstream/editor',
forkOwner: 'team',
forkRepo: 'editor-spoon',
forkUrl: 'https://github.com/team/editor-spoon',
visibility: 'private' as const,
maintenanceMode: 'watch' as const,
syncCadence: 'daily' as const,
productionRefStrategy: 'default_branch' as const,
};
const setup = async (
t: ReturnType<typeof convexTest>,
profile: {
provider: 'anthropic' | 'opencode_openai_login';
authType: 'api_key' | 'opencode_auth_json';
},
) => {
const ownerId = await createUser(t, 'owner@example.com');
const spoonId = await authed(t, ownerId).mutation(
api.spoons.createManual,
githubSpoonInput,
);
const threadId = await t.mutation(async (ctx) => {
const now = Date.now();
await ctx.db.insert('aiProviderProfiles', {
ownerId,
name: 'Primary provider',
provider: profile.provider,
authType: profile.authType,
encryptedSecret: 'encrypted-secret',
secretPreview: 'sk-...abcd',
defaultModel: 'claude-sonnet-4',
modelOptions: ['claude-sonnet-4'],
reasoningEffort: 'medium',
enabled: true,
createdAt: now,
updatedAt: now,
});
return await ctx.db.insert('threads', {
ownerId,
spoonId,
title: 'Runtime thread',
summary: 'do the work',
source: 'user_request',
status: 'open',
priority: 'normal',
createdAt: now,
updatedAt: now,
});
});
return { ownerId, spoonId, threadId };
};
describe('queue-time runtime validation', () => {
test('rejects a runtime the provider profile cannot drive', async () => {
const t = convexTest(schema, modules);
const { ownerId, threadId } = await setup(t, {
provider: 'anthropic',
authType: 'api_key',
});
await expect(
authed(t, ownerId).mutation(api.agentJobs.createForThread, {
threadId,
jobType: 'user_change',
runtime: 'codex',
}),
).rejects.toThrow(/cannot run the "codex" runtime/);
});
test('resolves a supported requested runtime onto the job', async () => {
const t = convexTest(schema, modules);
const { ownerId, threadId } = await setup(t, {
provider: 'anthropic',
authType: 'api_key',
});
const jobId = await authed(t, ownerId).mutation(
api.agentJobs.createForThread,
{
threadId,
jobType: 'user_change',
runtime: 'claude',
},
);
const job = await t.run(async (ctx) => await ctx.db.get(jobId));
expect(job?.runtime).toBe('claude');
});
test('defaults a ChatGPT-login profile job to the codex runtime', async () => {
const t = convexTest(schema, modules);
const { ownerId, threadId } = await setup(t, {
provider: 'opencode_openai_login',
authType: 'opencode_auth_json',
});
const jobId = await authed(t, ownerId).mutation(
api.agentJobs.createForThread,
{
threadId,
jobType: 'user_change',
},
);
const job = await t.run(async (ctx) => await ctx.db.get(jobId));
expect(job?.runtime).toBe('codex');
});
});