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, email: string) => (await t.mutation(async (ctx) => { return await ctx.db.insert('users', { email, name: email }); })) as Id<'users'>; const authed = (t: ReturnType, 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, 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'); }); });