Merge pull request #217 from cita-777/codex/sqlite-site-announcements-migration-fixes
[codex] fix sqlite bootstrap and site announcement migration
This commit is contained in:
@@ -1,7 +1,27 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ensureRuntimeDatabaseReady } from './runtimeDatabaseBootstrap.js';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { runSqliteMigrations } from './db/migrate.js';
|
||||
import {
|
||||
__runtimeDatabaseBootstrapTestUtils,
|
||||
ensureRuntimeDatabaseReady,
|
||||
runSqliteRuntimeMigrations,
|
||||
} from './runtimeDatabaseBootstrap.js';
|
||||
|
||||
vi.mock('./db/migrate.js', () => ({
|
||||
runSqliteMigrations: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('runtimeDatabaseBootstrap', () => {
|
||||
beforeEach(() => {
|
||||
__runtimeDatabaseBootstrapTestUtils.resetSqliteMigrationsBootstrapped();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('runs sqlite migrations on the first runtime bootstrap call', async () => {
|
||||
await runSqliteRuntimeMigrations();
|
||||
|
||||
expect(runSqliteMigrations).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('runs sqlite runtime migrations when dialect is sqlite', async () => {
|
||||
const runSqliteRuntimeMigrations = vi.fn(async () => {});
|
||||
const ensureExternalRuntimeSchema = vi.fn(async () => {});
|
||||
|
||||
@@ -7,11 +7,10 @@ let sqliteMigrationsBootstrapped = false;
|
||||
|
||||
export async function runSqliteRuntimeMigrations(): Promise<void> {
|
||||
const migrateModule = await import('./db/migrate.js');
|
||||
if (sqliteMigrationsBootstrapped) {
|
||||
migrateModule.runSqliteMigrations();
|
||||
return;
|
||||
if (!sqliteMigrationsBootstrapped) {
|
||||
sqliteMigrationsBootstrapped = true;
|
||||
}
|
||||
sqliteMigrationsBootstrapped = true;
|
||||
migrateModule.runSqliteMigrations();
|
||||
}
|
||||
|
||||
type EnsureRuntimeDatabaseReadyInput = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import currentContract from '../db/generated/schemaContract.json' with { type: 'json' };
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
__databaseMigrationServiceTestUtils,
|
||||
maskConnectionString,
|
||||
@@ -10,6 +10,42 @@ function cloneContract<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
}
|
||||
|
||||
function createDbSchemaMock() {
|
||||
return {
|
||||
settings: { __table: 'settings' },
|
||||
sites: { __table: 'sites' },
|
||||
siteAnnouncements: { __table: 'siteAnnouncements' },
|
||||
siteDisabledModels: { __table: 'siteDisabledModels' },
|
||||
accounts: { __table: 'accounts' },
|
||||
accountTokens: { __table: 'accountTokens' },
|
||||
checkinLogs: { __table: 'checkinLogs' },
|
||||
modelAvailability: { __table: 'modelAvailability' },
|
||||
tokenModelAvailability: { __table: 'tokenModelAvailability' },
|
||||
tokenRoutes: { __table: 'tokenRoutes' },
|
||||
routeChannels: { __table: 'routeChannels' },
|
||||
routeGroupSources: { __table: 'routeGroupSources' },
|
||||
proxyLogs: { __table: 'proxyLogs' },
|
||||
proxyVideoTasks: { __table: 'proxyVideoTasks' },
|
||||
proxyFiles: { __table: 'proxyFiles' },
|
||||
downstreamApiKeys: { __table: 'downstreamApiKeys' },
|
||||
events: { __table: 'events' },
|
||||
};
|
||||
}
|
||||
|
||||
function createDbMock(rowsByTable: Record<string, unknown[]>) {
|
||||
return {
|
||||
select() {
|
||||
return {
|
||||
from(table: { __table: string }) {
|
||||
return {
|
||||
all: async () => rowsByTable[table.__table] ?? [],
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('databaseMigrationService', () => {
|
||||
it('accepts postgres migration input with normalized url', () => {
|
||||
const normalized = normalizeMigrationInput({
|
||||
@@ -179,6 +215,7 @@ describe('databaseMigrationService', () => {
|
||||
customHeaders: '{"x-site-scope":"internal"}',
|
||||
status: 'active',
|
||||
}],
|
||||
siteAnnouncements: [],
|
||||
siteDisabledModels: [],
|
||||
accounts: [],
|
||||
accountTokens: [],
|
||||
@@ -214,6 +251,7 @@ describe('databaseMigrationService', () => {
|
||||
timestamp: Date.now(),
|
||||
accounts: {
|
||||
sites: [],
|
||||
siteAnnouncements: [],
|
||||
siteDisabledModels: [{
|
||||
id: 3,
|
||||
siteId: 12,
|
||||
@@ -295,4 +333,136 @@ describe('databaseMigrationService', () => {
|
||||
expect(routeModeIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(tokenRouteStatement?.values[routeModeIndex]).toBe('explicit_group');
|
||||
});
|
||||
|
||||
it('includes site announcements in migration statements', () => {
|
||||
const statements = __databaseMigrationServiceTestUtils.buildStatements({
|
||||
version: 'test',
|
||||
timestamp: Date.now(),
|
||||
accounts: {
|
||||
sites: [],
|
||||
siteDisabledModels: [],
|
||||
accounts: [],
|
||||
accountTokens: [],
|
||||
checkinLogs: [],
|
||||
modelAvailability: [],
|
||||
tokenModelAvailability: [],
|
||||
tokenRoutes: [],
|
||||
routeChannels: [],
|
||||
routeGroupSources: [],
|
||||
proxyLogs: [],
|
||||
proxyVideoTasks: [],
|
||||
proxyFiles: [],
|
||||
downstreamApiKeys: [],
|
||||
events: [],
|
||||
siteAnnouncements: [{
|
||||
id: 11,
|
||||
siteId: 3,
|
||||
platform: 'openai',
|
||||
sourceKey: 'notice-1',
|
||||
title: '????',
|
||||
content: '????',
|
||||
level: 'warning',
|
||||
sourceUrl: 'https://example.com/notice',
|
||||
startsAt: '2026-03-20T00:00:00.000Z',
|
||||
endsAt: '2026-03-21T00:00:00.000Z',
|
||||
upstreamCreatedAt: '2026-03-19T00:00:00.000Z',
|
||||
upstreamUpdatedAt: '2026-03-20T00:00:00.000Z',
|
||||
firstSeenAt: '2026-03-20T00:00:00.000Z',
|
||||
lastSeenAt: '2026-03-20T01:00:00.000Z',
|
||||
readAt: null,
|
||||
dismissedAt: null,
|
||||
rawPayload: '{"id":"notice-1"}',
|
||||
}],
|
||||
},
|
||||
preferences: {
|
||||
settings: [],
|
||||
},
|
||||
} as any);
|
||||
|
||||
const statement = statements.find((item) => item.table === 'site_announcements');
|
||||
expect(statement).toBeDefined();
|
||||
expect(statement?.columns).toContain('source_key');
|
||||
expect(statement?.values[statement?.columns.indexOf('title') ?? -1]).toBe('????');
|
||||
});
|
||||
|
||||
it('includes site announcements in migration summary', async () => {
|
||||
vi.resetModules();
|
||||
|
||||
const rowsByTable = {
|
||||
settings: [],
|
||||
sites: [],
|
||||
siteAnnouncements: [{
|
||||
id: 11,
|
||||
siteId: 3,
|
||||
platform: 'openai',
|
||||
sourceKey: 'notice-1',
|
||||
title: '????',
|
||||
content: '????',
|
||||
level: 'warning',
|
||||
sourceUrl: 'https://example.com/notice',
|
||||
startsAt: '2026-03-20T00:00:00.000Z',
|
||||
endsAt: '2026-03-21T00:00:00.000Z',
|
||||
upstreamCreatedAt: '2026-03-19T00:00:00.000Z',
|
||||
upstreamUpdatedAt: '2026-03-20T00:00:00.000Z',
|
||||
firstSeenAt: '2026-03-20T00:00:00.000Z',
|
||||
lastSeenAt: '2026-03-20T01:00:00.000Z',
|
||||
readAt: null,
|
||||
dismissedAt: null,
|
||||
rawPayload: '{"id":"notice-1"}',
|
||||
}],
|
||||
siteDisabledModels: [],
|
||||
accounts: [],
|
||||
accountTokens: [],
|
||||
checkinLogs: [],
|
||||
modelAvailability: [],
|
||||
tokenModelAvailability: [],
|
||||
tokenRoutes: [],
|
||||
routeChannels: [],
|
||||
routeGroupSources: [],
|
||||
proxyLogs: [],
|
||||
proxyVideoTasks: [],
|
||||
proxyFiles: [],
|
||||
downstreamApiKeys: [],
|
||||
events: [],
|
||||
};
|
||||
|
||||
const client = {
|
||||
dialect: 'sqlite',
|
||||
connectionString: ':memory:',
|
||||
ssl: false,
|
||||
begin: vi.fn(async () => {}),
|
||||
commit: vi.fn(async () => {}),
|
||||
rollback: vi.fn(async () => {}),
|
||||
execute: vi.fn(async () => []),
|
||||
queryScalar: vi.fn(async () => 0),
|
||||
close: vi.fn(async () => {}),
|
||||
};
|
||||
|
||||
vi.doMock('../db/index.js', () => ({
|
||||
db: createDbMock(rowsByTable),
|
||||
schema: createDbSchemaMock(),
|
||||
}));
|
||||
vi.doMock('../db/runtimeSchemaBootstrap.js', () => ({
|
||||
createRuntimeSchemaClient: async () => client,
|
||||
ensureRuntimeDatabaseSchema: async () => {},
|
||||
}));
|
||||
|
||||
try {
|
||||
const { migrateCurrentDatabase } = await import('./databaseMigrationService.js');
|
||||
const summary = await migrateCurrentDatabase({
|
||||
dialect: 'sqlite',
|
||||
connectionString: ':memory:',
|
||||
overwrite: true,
|
||||
});
|
||||
|
||||
expect(summary.rows.siteAnnouncements).toBe(1);
|
||||
expect(client.begin).toHaveBeenCalledTimes(1);
|
||||
expect(client.commit).toHaveBeenCalledTimes(1);
|
||||
expect(client.close).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
vi.doUnmock('../db/index.js');
|
||||
vi.doUnmock('../db/runtimeSchemaBootstrap.js');
|
||||
vi.resetModules();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ type BackupSnapshot = {
|
||||
timestamp: number;
|
||||
accounts: {
|
||||
sites: Array<Record<string, unknown>>;
|
||||
siteAnnouncements: Array<Record<string, unknown>>;
|
||||
siteDisabledModels: Array<Record<string, unknown>>;
|
||||
accounts: Array<Record<string, unknown>>;
|
||||
accountTokens: Array<Record<string, unknown>>;
|
||||
@@ -56,6 +57,7 @@ export interface DatabaseMigrationSummary {
|
||||
timestamp: number;
|
||||
rows: {
|
||||
sites: number;
|
||||
siteAnnouncements: number;
|
||||
siteDisabledModels: number;
|
||||
accounts: number;
|
||||
accountTokens: number;
|
||||
@@ -212,6 +214,7 @@ async function toBackupSnapshot(): Promise<BackupSnapshot> {
|
||||
timestamp: Date.now(),
|
||||
accounts: {
|
||||
sites: await db.select().from(schema.sites).all() as Array<Record<string, unknown>>,
|
||||
siteAnnouncements: await db.select().from(schema.siteAnnouncements).all() as Array<Record<string, unknown>>,
|
||||
siteDisabledModels: await db.select().from(schema.siteDisabledModels).all() as Array<Record<string, unknown>>,
|
||||
accounts: await db.select().from(schema.accounts).all() as Array<Record<string, unknown>>,
|
||||
accountTokens: await db.select().from(schema.accountTokens).all() as Array<Record<string, unknown>>,
|
||||
@@ -257,6 +260,7 @@ async function clearTargetData(client: SqlClient): Promise<void> {
|
||||
'proxy_files',
|
||||
'account_tokens',
|
||||
'accounts',
|
||||
'site_announcements',
|
||||
'site_disabled_models',
|
||||
'token_routes',
|
||||
'sites',
|
||||
@@ -309,6 +313,50 @@ function buildStatements(snapshot: BackupSnapshot): InsertStatement[] {
|
||||
});
|
||||
}
|
||||
|
||||
for (const row of snapshot.accounts.siteAnnouncements || []) {
|
||||
statements.push({
|
||||
table: 'site_announcements',
|
||||
columns: [
|
||||
'id',
|
||||
'site_id',
|
||||
'platform',
|
||||
'source_key',
|
||||
'title',
|
||||
'content',
|
||||
'level',
|
||||
'source_url',
|
||||
'starts_at',
|
||||
'ends_at',
|
||||
'upstream_created_at',
|
||||
'upstream_updated_at',
|
||||
'first_seen_at',
|
||||
'last_seen_at',
|
||||
'read_at',
|
||||
'dismissed_at',
|
||||
'raw_payload',
|
||||
],
|
||||
values: [
|
||||
asNumber(row.id, 0),
|
||||
asNumber(row.siteId, 0),
|
||||
asNullableString(row.platform),
|
||||
asNullableString(row.sourceKey),
|
||||
asNullableString(row.title),
|
||||
asNullableString(row.content),
|
||||
asNullableString(row.level) ?? 'info',
|
||||
asNullableString(row.sourceUrl),
|
||||
asNullableString(row.startsAt),
|
||||
asNullableString(row.endsAt),
|
||||
asNullableString(row.upstreamCreatedAt),
|
||||
asNullableString(row.upstreamUpdatedAt),
|
||||
asNullableString(row.firstSeenAt),
|
||||
asNullableString(row.lastSeenAt),
|
||||
asNullableString(row.readAt),
|
||||
asNullableString(row.dismissedAt),
|
||||
asNullableString(row.rawPayload),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
for (const row of snapshot.accounts.accounts) {
|
||||
statements.push({
|
||||
table: 'accounts',
|
||||
@@ -617,6 +665,7 @@ async function syncPostgresSequences(client: SqlClient): Promise<void> {
|
||||
if (client.dialect !== 'postgres') return;
|
||||
const tables = [
|
||||
'sites',
|
||||
'site_announcements',
|
||||
'site_disabled_models',
|
||||
'accounts',
|
||||
'account_tokens',
|
||||
@@ -684,6 +733,7 @@ export async function migrateCurrentDatabase(input: DatabaseMigrationInput): Pro
|
||||
timestamp: snapshot.timestamp,
|
||||
rows: {
|
||||
sites: snapshot.accounts.sites.length,
|
||||
siteAnnouncements: snapshot.accounts.siteAnnouncements.length,
|
||||
siteDisabledModels: snapshot.accounts.siteDisabledModels.length,
|
||||
accounts: snapshot.accounts.accounts.length,
|
||||
accountTokens: snapshot.accounts.accountTokens.length,
|
||||
|
||||
Reference in New Issue
Block a user