fix oauth route unit delete rollback (#450)
This commit is contained in:
@@ -3246,6 +3246,97 @@ describe('oauth routes', { timeout: 15_000 }, () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('restores oauth route unit route channels when delete rebuild keeps failing', async () => {
|
||||
const site = await db.insert(schema.sites).values({
|
||||
name: 'ChatGPT Codex OAuth',
|
||||
url: 'https://chatgpt.com/backend-api/codex',
|
||||
platform: 'codex',
|
||||
status: 'active',
|
||||
}).returning().get();
|
||||
|
||||
const first = await db.insert(schema.accounts).values({
|
||||
siteId: site.id,
|
||||
username: 'rollback-delete-double-fail-a@example.com',
|
||||
accessToken: 'rollback-delete-double-fail-access-a',
|
||||
status: 'active',
|
||||
oauthProvider: 'codex',
|
||||
oauthAccountKey: 'rollback-delete-double-fail-a',
|
||||
extraConfig: JSON.stringify({
|
||||
oauth: {
|
||||
provider: 'codex',
|
||||
accountId: 'rollback-delete-double-fail-a',
|
||||
accountKey: 'rollback-delete-double-fail-a',
|
||||
email: 'rollback-delete-double-fail-a@example.com',
|
||||
refreshToken: 'rollback-delete-double-fail-refresh-a',
|
||||
},
|
||||
}),
|
||||
}).returning().get();
|
||||
const second = await db.insert(schema.accounts).values({
|
||||
siteId: site.id,
|
||||
username: 'rollback-delete-double-fail-b@example.com',
|
||||
accessToken: 'rollback-delete-double-fail-access-b',
|
||||
status: 'active',
|
||||
oauthProvider: 'codex',
|
||||
oauthAccountKey: 'rollback-delete-double-fail-b',
|
||||
extraConfig: JSON.stringify({
|
||||
oauth: {
|
||||
provider: 'codex',
|
||||
accountId: 'rollback-delete-double-fail-b',
|
||||
accountKey: 'rollback-delete-double-fail-b',
|
||||
email: 'rollback-delete-double-fail-b@example.com',
|
||||
refreshToken: 'rollback-delete-double-fail-refresh-b',
|
||||
},
|
||||
}),
|
||||
}).returning().get();
|
||||
|
||||
await db.insert(schema.modelAvailability).values([
|
||||
{ accountId: first.id, modelName: 'gpt-5.4', available: true },
|
||||
{ accountId: second.id, modelName: 'gpt-5.4', available: true },
|
||||
]).run();
|
||||
|
||||
const routeRefreshWorkflow = await import('../../services/routeRefreshWorkflow.js');
|
||||
await routeRefreshWorkflow.rebuildRoutesOnly();
|
||||
|
||||
const createResponse = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/oauth/route-units',
|
||||
payload: {
|
||||
accountIds: [first.id, second.id],
|
||||
name: 'Rollback Delete Double Fail Pool',
|
||||
strategy: 'round_robin',
|
||||
},
|
||||
});
|
||||
expect(createResponse.statusCode).toBe(200);
|
||||
const routeUnitId = createResponse.json().routeUnit?.id as number;
|
||||
|
||||
const rebuildSpy = vi.spyOn(routeRefreshWorkflow, 'rebuildRoutesOnly');
|
||||
rebuildSpy.mockImplementation(async () => {
|
||||
throw new Error('route rebuild failed');
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `/api/oauth/route-units/${routeUnitId}`,
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(500);
|
||||
expect(response.json()).toMatchObject({
|
||||
message: 'route rebuild failed',
|
||||
});
|
||||
|
||||
const routeUnits = await db.select().from(schema.oauthRouteUnits).all();
|
||||
expect(routeUnits).toHaveLength(1);
|
||||
const members = await db.select().from(schema.oauthRouteUnitMembers).all();
|
||||
expect(members).toHaveLength(2);
|
||||
const routeChannels = await db.select().from(schema.routeChannels).all();
|
||||
expect(routeChannels).toHaveLength(1);
|
||||
expect(routeChannels[0]?.oauthRouteUnitId).toBe(routeUnitId);
|
||||
} finally {
|
||||
rebuildSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts normalized route unit strategy values from the HTTP payload', async () => {
|
||||
const site = await db.insert(schema.sites).values({
|
||||
name: 'ChatGPT Codex OAuth',
|
||||
|
||||
@@ -153,6 +153,7 @@ async function rollbackCreatedOauthRouteUnit(routeUnitId: number): Promise<void>
|
||||
async function restoreDeletedOauthRouteUnit(snapshot: {
|
||||
unit: typeof schema.oauthRouteUnits.$inferSelect;
|
||||
members: Array<typeof schema.oauthRouteUnitMembers.$inferSelect>;
|
||||
channels: Array<typeof schema.routeChannels.$inferSelect>;
|
||||
}): Promise<void> {
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.insert(schema.oauthRouteUnits).values({
|
||||
@@ -176,6 +177,31 @@ async function restoreDeletedOauthRouteUnit(snapshot: {
|
||||
updatedAt: member.updatedAt,
|
||||
}))).run();
|
||||
}
|
||||
|
||||
if (snapshot.channels.length > 0) {
|
||||
await tx.insert(schema.routeChannels).values(snapshot.channels.map((channel) => ({
|
||||
id: channel.id,
|
||||
routeId: channel.routeId,
|
||||
accountId: channel.accountId,
|
||||
tokenId: channel.tokenId,
|
||||
oauthRouteUnitId: channel.oauthRouteUnitId,
|
||||
sourceModel: channel.sourceModel,
|
||||
priority: channel.priority,
|
||||
weight: channel.weight,
|
||||
enabled: channel.enabled,
|
||||
manualOverride: channel.manualOverride,
|
||||
successCount: channel.successCount,
|
||||
failCount: channel.failCount,
|
||||
totalLatencyMs: channel.totalLatencyMs,
|
||||
totalCost: channel.totalCost,
|
||||
lastUsedAt: channel.lastUsedAt,
|
||||
lastSelectedAt: channel.lastSelectedAt,
|
||||
lastFailAt: channel.lastFailAt,
|
||||
consecutiveFailCount: channel.consecutiveFailCount,
|
||||
cooldownLevel: channel.cooldownLevel,
|
||||
cooldownUntil: channel.cooldownUntil,
|
||||
}))).run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -373,6 +399,9 @@ export async function deleteOauthRouteUnit(routeUnitId: number) {
|
||||
const existingMembers = await db.select().from(schema.oauthRouteUnitMembers)
|
||||
.where(eq(schema.oauthRouteUnitMembers.unitId, routeUnitId))
|
||||
.all();
|
||||
const existingChannels = await db.select().from(schema.routeChannels)
|
||||
.where(eq(schema.routeChannels.oauthRouteUnitId, routeUnitId))
|
||||
.all();
|
||||
|
||||
await db.delete(schema.routeChannels)
|
||||
.where(eq(schema.routeChannels.oauthRouteUnitId, routeUnitId))
|
||||
@@ -390,6 +419,7 @@ export async function deleteOauthRouteUnit(routeUnitId: number) {
|
||||
await restoreDeletedOauthRouteUnit({
|
||||
unit: existing,
|
||||
members: existingMembers,
|
||||
channels: existingChannels,
|
||||
});
|
||||
try {
|
||||
await routeRefreshWorkflow.rebuildRoutesOnly();
|
||||
|
||||
Reference in New Issue
Block a user