feat(backend): migration to retire openai_direct runtime

This commit is contained in:
Gabriel Brown
2026-07-11 11:05:12 -04:00
parent 0716a224ef
commit 6ec2e51312
2 changed files with 146 additions and 0 deletions
+32
View File
@@ -23,3 +23,35 @@ export const backfillAutoSyncDefaults = internalMutation({
return { updated };
},
});
// One-shot migration retiring the legacy `openai_direct` runtime literal:
// rewrites any surviving `agentJobs`/`spoonAgentSettings` rows to `opencode`.
// `openai_direct` stays in the schema unions as a legacy-read literal; nothing
// writes it anymore. Run once post-deploy via
// `npx convex run migrations:migrateOpenAiDirectRuntime`. Idempotent.
export const migrateOpenAiDirectRuntime = internalMutation({
args: {},
returns: v.object({ patched: v.number() }),
handler: async (ctx) => {
let patched = 0;
for (const job of await ctx.db.query('agentJobs').collect()) {
if (job.runtime === 'openai_direct') {
await ctx.db.patch(job._id, {
runtime: 'opencode',
updatedAt: Date.now(),
});
patched += 1;
}
}
for (const settings of await ctx.db.query('spoonAgentSettings').collect()) {
if (settings.runtime === 'openai_direct') {
await ctx.db.patch(settings._id, {
runtime: 'opencode',
updatedAt: Date.now(),
});
patched += 1;
}
}
return { patched };
},
});
@@ -0,0 +1,114 @@
import { convexTest } from 'convex-test';
import { describe, expect, test } from 'vitest';
import { internal } from '../../convex/_generated/api.js';
import schema from '../../convex/schema';
const modules = import.meta.glob('../../convex/**/*.*s');
describe('migrateOpenAiDirectRuntime', () => {
test('rewrites legacy openai_direct rows to opencode and leaves others', async () => {
const t = convexTest(schema, modules);
const now = Date.now();
const ids = await t.run(async (ctx) => {
const ownerId = await ctx.db.insert('users', {
email: 'owner@example.com',
name: 'Owner',
});
const spoonId = await ctx.db.insert('spoons', {
ownerId,
name: 'legacy-spoon',
provider: 'gitea',
upstreamOwner: 'upstream',
upstreamRepo: 'legacy-spoon',
upstreamDefaultBranch: 'main',
upstreamUrl: 'https://example.com/upstream/legacy-spoon',
forkOwner: 'owner',
forkRepo: 'legacy-spoon',
forkUrl: 'https://example.com/owner/legacy-spoon',
visibility: 'private',
maintenanceMode: 'watch',
syncCadence: 'daily',
productionRefStrategy: 'default_branch',
status: 'active',
createdAt: now,
updatedAt: now,
});
const agentRequestId = await ctx.db.insert('agentRequests', {
spoonId,
ownerId,
prompt: 'legacy request',
status: 'running',
createdAt: now,
updatedAt: now,
});
const settings = (runtime: 'openai_direct' | 'codex') => ({
spoonId,
ownerId,
enabled: true,
runtime,
branchPrefix: 'spoon/',
agentModel: 'gpt-5',
reasoningEffort: 'medium' as const,
maxJobDurationMs: 600000,
maxOutputBytes: 1000000,
createdAt: now,
updatedAt: now,
});
const job = (runtime: 'openai_direct' | 'codex') => ({
spoonId,
ownerId,
agentRequestId,
status: 'running' as const,
prompt: 'legacy job',
runtime,
baseBranch: 'main',
workBranch: 'spoon/legacy',
forkOwner: 'owner',
forkRepo: 'legacy-spoon',
forkUrl: 'https://example.com/owner/legacy-spoon',
upstreamOwner: 'upstream',
upstreamRepo: 'legacy-spoon',
selectedSecretIds: [],
model: 'gpt-5',
reasoningEffort: 'medium' as const,
createdAt: now,
updatedAt: now,
});
return {
legacySettings: await ctx.db.insert(
'spoonAgentSettings',
settings('openai_direct'),
),
legacyJob: await ctx.db.insert('agentJobs', job('openai_direct')),
codexSettings: await ctx.db.insert(
'spoonAgentSettings',
settings('codex'),
),
codexJob: await ctx.db.insert('agentJobs', job('codex')),
};
});
const result = await t.mutation(
internal.migrations.migrateOpenAiDirectRuntime,
{},
);
expect(result.patched).toBe(2);
const after = await t.run(async (ctx) => ({
legacySettings: await ctx.db.get(ids.legacySettings),
legacyJob: await ctx.db.get(ids.legacyJob),
codexSettings: await ctx.db.get(ids.codexSettings),
codexJob: await ctx.db.get(ids.codexJob),
}));
expect(after.legacySettings?.runtime).toBe('opencode');
expect(after.legacyJob?.runtime).toBe('opencode');
expect(after.codexSettings?.runtime).toBe('codex');
expect(after.codexJob?.runtime).toBe('codex');
});
});