Merge remote-tracking branch 'gitea/main'
# Conflicts: # prompts/owner-common-paired-room.md # src/channels/discord.ts # src/codex-warmup.ts # src/db/bootstrap.test.ts # src/db/migrations/index.ts # src/paired-execution-context-reviewer.ts # src/usage-primer.test.ts # src/usage-primer.ts
This commit is contained in:
@@ -6,8 +6,17 @@ import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { applyBaseSchema } from './base-schema.js';
|
||||
import { initializeDatabaseSchema } from './bootstrap.js';
|
||||
import { applyVersionedSchemaMigrations } from './migrations/index.js';
|
||||
import { applyLegacySchemaMigrations } from './schema.js';
|
||||
|
||||
function tableColumns(database: Database, table: string): string[] {
|
||||
return (
|
||||
database.prepare(`PRAGMA table_info(${table})`).all() as Array<{
|
||||
name: string;
|
||||
}>
|
||||
).map((column) => column.name);
|
||||
}
|
||||
|
||||
function getAppliedSchemaMigrations(
|
||||
database: Database,
|
||||
): Array<{ version: number; name: string }> {
|
||||
@@ -45,6 +54,8 @@ function getExpectedSchemaMigrations(): Array<{
|
||||
{ version: 18, name: 'paired_turn_output_attachments' },
|
||||
{ version: 19, name: 'turn_progress_text_recovery' },
|
||||
{ version: 20, name: 'arbiter_intervention_count' },
|
||||
{ version: 21, name: 'reviewer_failure_count' },
|
||||
{ version: 22, name: 'turn_progress_text_compat' },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -115,4 +126,55 @@ describe('initializeDatabaseSchema', () => {
|
||||
reopened.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('backfills turn_progress_text columns when version 15 was recorded under a different name', () => {
|
||||
const database = new Database(':memory:');
|
||||
|
||||
try {
|
||||
applyBaseSchema(database);
|
||||
|
||||
// Reproduce a deployment that first shipped reviewer_failure_count as a
|
||||
// local migration numbered 15, before turn_progress_text claimed that
|
||||
// version upstream. The runner skips by version number, so migration 015
|
||||
// (turn_progress_text) would otherwise never run on this database.
|
||||
database.exec(`
|
||||
CREATE TABLE schema_migrations (
|
||||
version INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
const collidedNames: Record<number, string> = {
|
||||
15: 'reviewer_failure_count',
|
||||
};
|
||||
for (let version = 1; version <= 15; version += 1) {
|
||||
database
|
||||
.prepare('INSERT INTO schema_migrations (version, name) VALUES (?, ?)')
|
||||
.run(version, collidedNames[version] ?? `legacy_${version}`);
|
||||
}
|
||||
// Precondition: the collided database is missing the progress columns.
|
||||
expect(tableColumns(database, 'paired_turns')).not.toContain(
|
||||
'progress_text',
|
||||
);
|
||||
|
||||
applyVersionedSchemaMigrations(database, { assistantName: 'Andy' });
|
||||
|
||||
const columns = tableColumns(database, 'paired_turns');
|
||||
expect(columns).toContain('progress_text');
|
||||
expect(columns).toContain('progress_updated_at');
|
||||
|
||||
// The pre-existing version-15 row is left untouched; the compat migration
|
||||
// (version 20) is what reconciles the schema.
|
||||
const version15 = database
|
||||
.prepare('SELECT name FROM schema_migrations WHERE version = 15')
|
||||
.get() as { name: string };
|
||||
expect(version15.name).toBe('reviewer_failure_count');
|
||||
const version20 = database
|
||||
.prepare('SELECT name FROM schema_migrations WHERE version = 20')
|
||||
.get() as { name: string } | null;
|
||||
expect(version20?.name).toBe('turn_progress_text_compat');
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
23
src/db/migrations/021_reviewer-failure-count.ts
Normal file
23
src/db/migrations/021_reviewer-failure-count.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { Database } from 'bun:sqlite';
|
||||
|
||||
import { tableHasColumn } from './helpers.js';
|
||||
import type { SchemaMigrationDefinition } from './types.js';
|
||||
|
||||
export const REVIEWER_FAILURE_COUNT_MIGRATION: SchemaMigrationDefinition = {
|
||||
version: 21,
|
||||
name: 'reviewer_failure_count',
|
||||
apply(database: Database) {
|
||||
if (!tableHasColumn(database, 'paired_tasks', 'reviewer_failure_count')) {
|
||||
database.exec(`
|
||||
ALTER TABLE paired_tasks
|
||||
ADD COLUMN reviewer_failure_count INTEGER NOT NULL DEFAULT 0
|
||||
`);
|
||||
}
|
||||
|
||||
database.exec(`
|
||||
UPDATE paired_tasks
|
||||
SET reviewer_failure_count = 0
|
||||
WHERE reviewer_failure_count IS NULL
|
||||
`);
|
||||
},
|
||||
};
|
||||
32
src/db/migrations/022_turn-progress-text-compat.ts
Normal file
32
src/db/migrations/022_turn-progress-text-compat.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { Database } from 'bun:sqlite';
|
||||
|
||||
import { tableHasColumn } from './helpers.js';
|
||||
import type { SchemaMigrationDefinition } from './types.js';
|
||||
|
||||
/**
|
||||
* Compatibility backfill for `turn_progress_text` (migration 015).
|
||||
*
|
||||
* Some deployments first shipped `reviewer_failure_count` as a local migration
|
||||
* numbered 15, so their `schema_migrations` table records version 15 under a
|
||||
* different name than the canonical `turn_progress_text` migration. The runner
|
||||
* skips migrations purely by version number, so on those databases the real
|
||||
* version-15 migration never runs and `paired_turns.progress_text` /
|
||||
* `progress_updated_at` are missing — yet the runtime reads them. This migration
|
||||
* re-adds the columns idempotently so collided databases converge with fresh
|
||||
* ones. On a fresh database migration 015 already created the columns, so the
|
||||
* `tableHasColumn` guards make this a no-op.
|
||||
*/
|
||||
export const TURN_PROGRESS_TEXT_COMPAT_MIGRATION: SchemaMigrationDefinition = {
|
||||
version: 22,
|
||||
name: 'turn_progress_text_compat',
|
||||
apply(database: Database) {
|
||||
if (!tableHasColumn(database, 'paired_turns', 'progress_text')) {
|
||||
database.exec(`ALTER TABLE paired_turns ADD COLUMN progress_text TEXT`);
|
||||
}
|
||||
if (!tableHasColumn(database, 'paired_turns', 'progress_updated_at')) {
|
||||
database.exec(
|
||||
`ALTER TABLE paired_turns ADD COLUMN progress_updated_at TEXT`,
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -20,6 +20,8 @@ import { SCHEDULED_TASK_ROOM_ROLE_MIGRATION } from './017_scheduled-task-room-ro
|
||||
import { PAIRED_TURN_OUTPUT_ATTACHMENTS_MIGRATION } from './018_paired-turn-output-attachments.js';
|
||||
import { TURN_PROGRESS_TEXT_RECOVERY_MIGRATION } from './019_turn-progress-text-recovery.js';
|
||||
import { ARBITER_INTERVENTION_COUNT_MIGRATION } from './020_arbiter-intervention-count.js';
|
||||
import { REVIEWER_FAILURE_COUNT_MIGRATION } from './021_reviewer-failure-count.js';
|
||||
import { TURN_PROGRESS_TEXT_COMPAT_MIGRATION } from './022_turn-progress-text-compat.js';
|
||||
import type {
|
||||
SchemaMigrationArgs,
|
||||
SchemaMigrationDefinition,
|
||||
@@ -48,6 +50,8 @@ const ORDERED_SCHEMA_MIGRATIONS: readonly SchemaMigrationDefinition[] = [
|
||||
PAIRED_TURN_OUTPUT_ATTACHMENTS_MIGRATION,
|
||||
TURN_PROGRESS_TEXT_RECOVERY_MIGRATION,
|
||||
ARBITER_INTERVENTION_COUNT_MIGRATION,
|
||||
REVIEWER_FAILURE_COUNT_MIGRATION,
|
||||
TURN_PROGRESS_TEXT_COMPAT_MIGRATION,
|
||||
];
|
||||
|
||||
function ensureSchemaMigrationsTable(database: Database): void {
|
||||
|
||||
@@ -50,6 +50,7 @@ export type PairedTaskUpdates = Partial<
|
||||
| 'review_requested_at'
|
||||
| 'round_trip_count'
|
||||
| 'owner_failure_count'
|
||||
| 'reviewer_failure_count'
|
||||
| 'owner_step_done_streak'
|
||||
| 'finalize_step_done_count'
|
||||
| 'task_done_then_user_reopen_count'
|
||||
@@ -177,6 +178,7 @@ export function createPairedTaskInDatabase(
|
||||
review_requested_at,
|
||||
round_trip_count,
|
||||
owner_failure_count,
|
||||
reviewer_failure_count,
|
||||
owner_step_done_streak,
|
||||
finalize_step_done_count,
|
||||
task_done_then_user_reopen_count,
|
||||
@@ -207,6 +209,7 @@ export function createPairedTaskInDatabase(
|
||||
task.review_requested_at,
|
||||
task.round_trip_count,
|
||||
task.owner_failure_count ?? 0,
|
||||
task.reviewer_failure_count ?? 0,
|
||||
task.owner_step_done_streak ?? 0,
|
||||
task.finalize_step_done_count ?? 0,
|
||||
task.task_done_then_user_reopen_count ?? 0,
|
||||
@@ -342,6 +345,10 @@ export function updatePairedTaskInDatabase(
|
||||
fields.push('owner_failure_count = ?');
|
||||
values.push(updates.owner_failure_count);
|
||||
}
|
||||
if (updates.reviewer_failure_count !== undefined) {
|
||||
fields.push('reviewer_failure_count = ?');
|
||||
values.push(updates.reviewer_failure_count);
|
||||
}
|
||||
if (updates.owner_step_done_streak !== undefined) {
|
||||
fields.push('owner_step_done_streak = ?');
|
||||
values.push(updates.owner_step_done_streak);
|
||||
@@ -425,6 +432,10 @@ export function updatePairedTaskIfUnchangedInDatabase(
|
||||
fields.push('owner_failure_count = ?');
|
||||
values.push(updates.owner_failure_count);
|
||||
}
|
||||
if (updates.reviewer_failure_count !== undefined) {
|
||||
fields.push('reviewer_failure_count = ?');
|
||||
values.push(updates.reviewer_failure_count);
|
||||
}
|
||||
if (updates.owner_step_done_streak !== undefined) {
|
||||
fields.push('owner_step_done_streak = ?');
|
||||
values.push(updates.owner_step_done_streak);
|
||||
|
||||
Reference in New Issue
Block a user