From beeea424861e1d9a4f53757edb1245a3b83b5595 Mon Sep 17 00:00:00 2001 From: HynLcc Date: Fri, 19 Jun 2026 21:00:17 +0800 Subject: [PATCH] Migrate record-reorder onto the record-mutation lifecycle driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third record-mutation member. record-reorder rides the existing record-mutation-lifecycle driver unchanged (useRecordWindow: true): the runner keeps only its diff logic (seed-cache order fixture, the measured block-reorder + routing + position verification bundle, result assembly, restore-order-or-drop cleanup) and wires into the driver via a spec. A small buildResult adapter maps the driver's primaryMeasurement onto the runner's existing reorderMeasurement param so the artifact assembly is untouched. G1 mask additions (scripts/diff-artifacts.mjs): the record-reorder baseline A vs B diff surfaced run-to-run noise the comparator did not yet mask — this runner is newly G1-diffed. All are genuinely volatile on unchanged code, none semantic: - details.verification.firstRecordId / anchorRecordId / movedRecordIds: generated record ids (added to GENERATED_ID_KEYS, like the existing recordId / createdRecordIds). - details.prepare.seedBatchDurations: raw per-batch seed timings (masked like every other *Ms / maxSeedBatchMs duration). The semantic reorder proof stays visible and asserted: a perturbation of verification.checkedPositions[].expectedOriginalRowNumber still fails the diff. Verification: pnpm check green; baseline A/B and A↔candidate pass on v1+v2; negative tests fail for phases[0].name AND checkedPositions expectedRowNumber; masked movedRecordIds tamper correctly passes; the updated comparator still passes for the already-migrated record-create / record-update artifacts. Tracker: record-reorder Not migrated -> Migrated. Counts 14->15 / 35 runner kinds, 16->17 / 55 cases. Co-Authored-By: Claude Opus 4.8 --- framework/runners/record-reorder.runner.ts | 229 ++++++++++----------- scripts/diff-artifacts.mjs | 21 ++ tasks/runner-migration-tracker.md | 36 ++-- 3 files changed, 144 insertions(+), 142 deletions(-) diff --git a/framework/runners/record-reorder.runner.ts b/framework/runners/record-reorder.runner.ts index b2de147..7e70b47 100644 --- a/framework/runners/record-reorder.runner.ts +++ b/framework/runners/record-reorder.runner.ts @@ -22,7 +22,6 @@ import { type SeedCacheInfo, } from "../seed-cache"; import { withPerfTraceStep } from "../trace-collector"; -import { PerfRunDiagnosticError } from "../types"; import type { MetricThreshold, PerfCase, @@ -33,16 +32,15 @@ import type { RecordUndoRedoBaseCaseConfig, } from "../types"; import { - buildRecordWindowId, undoRedoMixed20Fields, withRecordWindowId, + type Measurement, } from "./record-undo-redo.shared"; - -type Measurement = { - name: string; - durationMs: number; - result: T; -}; +import { + runRecordMutationLifecycle, + seedRecordMutationLifecycle, + type RecordMutationLifecycleSpec, +} from "./record-mutation-lifecycle"; type ReorderField = RecordReorderCaseConfig["fields"][number] & { id: string; @@ -1083,139 +1081,122 @@ const buildResult = ({ }, }); -export const runRecordReorderCase = async ( +// The single measured operation, run inside the driver's record window: +// trace-wrapped block reorder -> routing assertion -> post-reorder position +// verification, all bundled into one reorder measurement whose duration is the +// primary metric. +const runReorderMeasuredOperation = async ( perfCase: PerfCase, context: PerfRunContext, -): Promise => { - const config = perfCase.config as RecordReorderCaseConfig; - const baseId = globalThis.testConfig.baseId; - const tableName = `${config.tableNamePrefix}-${Date.now()}`; - const windowId = buildRecordWindowId(context, perfCase); - let prepareMeasurement: Measurement | undefined; - let seedReadyMeasurement: - | Measurement>> - | undefined; - - try { - prepareMeasurement = await measureAsync("prepare", () => - prepareFixture(baseId, tableName, config, perfCase), - ); - const fixture = prepareMeasurement.result; - seedReadyMeasurement = await measureAsync("seedReady", () => - assertSeedReady(fixture, config), - ); - let reorderMeasurement: Measurement | undefined; + config: RecordReorderCaseConfig, + fixture: ReorderFixture, +): Promise> => { + const requestMeasurement = await measureAsync(config.threshold.metric, () => + executeReorder(context, perfCase, fixture, config), + ); + let reorderMeasurement: Measurement = { + ...requestMeasurement, + result: { + ...requestMeasurement.result, + requestMs: requestMeasurement.durationMs, + routing: assertEngineRouting( + context, + requestMeasurement.result.responseHeaders, + { + operation: "updateRecordOrders", + }, + ), + }, + }; + const verification = await verifyReorder(fixture, config); + reorderMeasurement = { + ...reorderMeasurement, + result: { + ...reorderMeasurement.result, + ...verification, + }, + }; + return reorderMeasurement; +}; +// The measured reorder moves the reusable seed rows, so a shared (non-isolated) +// execute DB is restored to the original order inside the same record window — +// or the table dropped if restore fails. The non-reusable case just drops the +// table. Isolated CI execute DBs are discarded after the job. +const cleanupReorderFixture = async ({ + baseId, + fixture, + config, + windowId, +}: { + baseId: string; + fixture: ReorderFixture | undefined; + config: RecordReorderCaseConfig; + windowId: string; +}) => { + if (fixture?.tableId && fixture.reusableSeed && !isExecuteDbIsolated()) { try { - await withRecordWindowId(windowId, async () => { - const requestMeasurement = await measureAsync( - config.threshold.metric, - () => executeReorder(context, perfCase, fixture, config), - ); - reorderMeasurement = { - ...requestMeasurement, - result: { - ...requestMeasurement.result, - requestMs: requestMeasurement.durationMs, - routing: assertEngineRouting( - context, - requestMeasurement.result.responseHeaders, - { - operation: "updateRecordOrders", - }, - ), - }, - }; - const verification = await verifyReorder(fixture, config); - reorderMeasurement = { - ...reorderMeasurement, - result: { - ...reorderMeasurement.result, - ...verification, - }, - }; - }); + await withRecordWindowId(windowId, () => + restoreOriginalOrder(fixture, config), + ); } catch (error) { - throw new PerfRunDiagnosticError( - error instanceof Error ? error.message : String(error), - buildResult({ - config, - windowId, - fixture, - prepareMeasurement, - seedReadyMeasurement, - reorderMeasurement, - error, - }), + console.warn( + `Failed to restore cached record reorder seed ${fixture.tableId}; deleting it`, + error, ); - } - - return buildResult({ - config, - windowId, - fixture, - prepareMeasurement, - seedReadyMeasurement, - reorderMeasurement, - }); - } finally { - const fixture = prepareMeasurement?.result; - if (fixture?.tableId && fixture.reusableSeed && !isExecuteDbIsolated()) { try { - await withRecordWindowId(windowId, () => - restoreOriginalOrder(fixture, config), - ); - } catch (error) { + await permanentDeleteTable(baseId, fixture.tableId); + } catch (cleanupError) { console.warn( - `Failed to restore cached record reorder seed ${fixture.tableId}; deleting it`, - error, + `Failed to cleanup perf table ${fixture.tableId}`, + cleanupError, ); - try { - await permanentDeleteTable(baseId, fixture.tableId); - } catch (cleanupError) { - console.warn( - `Failed to cleanup perf table ${fixture.tableId}`, - cleanupError, - ); - } - } - } else if ( - fixture?.tableId && - !fixture.reusableSeed && - !isExecuteDbIsolated() - ) { - try { - await permanentDeleteTable(baseId, fixture.tableId); - } catch (error) { - console.warn(`Failed to cleanup perf table ${fixture.tableId}`, error); } } + } else if ( + fixture?.tableId && + !fixture.reusableSeed && + !isExecuteDbIsolated() + ) { + try { + await permanentDeleteTable(baseId, fixture.tableId); + } catch (error) { + console.warn(`Failed to cleanup perf table ${fixture.tableId}`, error); + } } }; -export const seedRecordReorderCase = async ( - perfCase: PerfCase, - _context: PerfRunContext, -): Promise => { - const config = perfCase.config as RecordReorderCaseConfig; - const baseId = globalThis.testConfig.baseId; - const tableName = `${config.tableNamePrefix}-seed-${Date.now()}`; - const prepareMeasurement = await measureAsync("prepare", () => +const recordReorderLifecycleSpec: RecordMutationLifecycleSpec< + RecordReorderCaseConfig, + ReorderFixture, + Awaited>, + ReorderOperationResult +> = { + // Group the reorder write under one record window id (mirrors the legacy + // runner; the same window scopes the restore in cleanup). + useRecordWindow: true, + prepareFixture: ({ baseId, tableName, config, perfCase }) => prepareFixture(baseId, tableName, config, perfCase), - ); - const seedReadyMeasurement = await measureAsync("seedReady", () => - assertSeedReady(prepareMeasurement.result, config), - ); - - return buildResult({ - config, - windowId: `seed-${_context.runId}-${perfCase.id}`, - fixture: prepareMeasurement.result, - prepareMeasurement, - seedReadyMeasurement, - }); + assertSeedReady: ({ fixture, config }) => assertSeedReady(fixture, config), + runMeasuredOperation: ({ perfCase, context, config, fixture }) => + runReorderMeasuredOperation(perfCase, context, config, fixture), + buildResult: ({ primaryMeasurement, ...rest }) => + buildResult({ ...rest, reorderMeasurement: primaryMeasurement }), + cleanup: cleanupReorderFixture, }; +export const runRecordReorderCase = async ( + perfCase: PerfCase, + context: PerfRunContext, +): Promise => + runRecordMutationLifecycle(perfCase, context, recordReorderLifecycleSpec); + +export const seedRecordReorderCase = async ( + perfCase: PerfCase, + context: PerfRunContext, +): Promise => + seedRecordMutationLifecycle(perfCase, context, recordReorderLifecycleSpec); + export const recordReorderMixed10kBaseConfig = { baseId: "seed-base" as const, rowCount: 10_000, diff --git a/scripts/diff-artifacts.mjs b/scripts/diff-artifacts.mjs index 3ec13e6..4bc3e1b 100644 --- a/scripts/diff-artifacts.mjs +++ b/scripts/diff-artifacts.mjs @@ -69,6 +69,16 @@ const GENERATED_ID_KEYS = new Set([ "duplicatedRecordIds", "sourceRecordId", "duplicatedRecordId", + // Generated record ids in the record-reorder verification evidence. Each run + // seeds a fresh table, so the moved block's record ids and the + // first/anchor record id differ between two runs of unchanged code (confirmed + // by the record-reorder baseline A vs B diff). The semantic reorder proof — + // checkedPositions[].expectedOriginalRowNumber / viewOffset and + // verifiedSamples[].expected — stays visible; only the opaque id strings are + // masked, like the existing `recordId`. + "firstRecordId", + "anchorRecordId", + "movedRecordIds", ]); const GENERATED_NAME_KEYS = new Set(["foreignTableName", "tableName"]); @@ -185,6 +195,17 @@ const shouldMaskKey = (path, key) => { return true; } + // record-reorder serializes its raw per-batch seed timings as an array under + // details.prepare. Like every other *Ms / maxSeedBatchMs duration, these vary + // run-to-run on unchanged code (confirmed by the record-reorder baseline A vs + // B diff); the batch count stays visible elsewhere. + if ( + pathEquals(path, ["details", "prepare"]) && + key === "seedBatchDurations" + ) { + return true; + } + return false; }; diff --git a/tasks/runner-migration-tracker.md b/tasks/runner-migration-tracker.md index 81d2e68..cc0d410 100644 --- a/tasks/runner-migration-tracker.md +++ b/tasks/runner-migration-tracker.md @@ -6,26 +6,27 @@ that uses it, because migrating a runner means re-verifying all of its cases. Status as of 2026-06-19 on `main`. -**Migrated: 14 / 35 runner kinds · 16 / 55 cases.** +**Migrated: 15 / 35 runner kinds · 17 / 55 cases.** ## Migrated (✅ on the driver) -| Runner kind | Driver / where | Cases | Verified | -| ----------------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------- | --------------------- | -| csv-import | `csv-import-lifecycle.ts` | 3 csv-import cases | ✅ v1+v2 pass (local) | -| field-delete | `field-delete-lifecycle.ts` | field-delete/mixed-10k-delete-19-fields | ✅ v1+v2 pass (local) | -| record-delete | `record-replay-lifecycle.ts` (no setup) | record-delete/delete-1k | ✅ v1+v2 pass (local) | -| record-delete-link | `table-link-lifecycle.ts` (single fixture) | record-delete/link-trash-1k | ✅ v1+v2 pass (local) | -| record-undo | `record-replay-lifecycle.ts` (delete setup) | record-undo/delete-1k | ✅ v1+v2 pass (local) | -| record-redo | `record-replay-lifecycle.ts` (delete+undo setup) | record-redo/delete-1k | ✅ v1+v2 pass (local) | -| table-delete | `table-lifecycle.ts` (sampled archive + restore-back) | table-delete/10k-20f | ✅ v1+v2 pass (local) | -| table-restore | `table-lifecycle.ts` (archive setup + sampled restore) | table-restore/10k-20f | ✅ v1+v2 pass (local) | -| table-delete-link | `table-link-lifecycle.ts` (sampled foreign-table delete) | table-delete/10k-20f-link-detach | ✅ v1+v2 pass (local) | -| table-restore-link | `table-link-lifecycle.ts` (sampled table restore) | table-restore/10k-20f-link-1k | ✅ v1+v2 pass (local) | -| selection-duplicate | `record-duplicate-lifecycle.ts` (stream block duplicate) | record-duplicate/grid-block-duplicate-1k | ✅ v1+v2 pass (local) | -| record-duplicate-single | `record-duplicate-lifecycle.ts` (sequential single dup) | record-duplicate/single-record-sequential-100 | ✅ v1+v2 pass (local) | -| record-update | `record-mutation-lifecycle.ts` (bulk update over seeded rows, record window + restore-or-delete) | record-update/mixed-1k-20fields-bulk-update | ✅ v1+v2 pass (local) | -| record-create | `record-mutation-lifecycle.ts` (bulk insert into empty seed, no window + delete-created-or-drop) | record-create/mixed-1k-20fields-bulk-create | ✅ v1+v2 pass (local) | +| Runner kind | Driver / where | Cases | Verified | +| ----------------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------- | --------------------- | +| csv-import | `csv-import-lifecycle.ts` | 3 csv-import cases | ✅ v1+v2 pass (local) | +| field-delete | `field-delete-lifecycle.ts` | field-delete/mixed-10k-delete-19-fields | ✅ v1+v2 pass (local) | +| record-delete | `record-replay-lifecycle.ts` (no setup) | record-delete/delete-1k | ✅ v1+v2 pass (local) | +| record-delete-link | `table-link-lifecycle.ts` (single fixture) | record-delete/link-trash-1k | ✅ v1+v2 pass (local) | +| record-undo | `record-replay-lifecycle.ts` (delete setup) | record-undo/delete-1k | ✅ v1+v2 pass (local) | +| record-redo | `record-replay-lifecycle.ts` (delete+undo setup) | record-redo/delete-1k | ✅ v1+v2 pass (local) | +| table-delete | `table-lifecycle.ts` (sampled archive + restore-back) | table-delete/10k-20f | ✅ v1+v2 pass (local) | +| table-restore | `table-lifecycle.ts` (archive setup + sampled restore) | table-restore/10k-20f | ✅ v1+v2 pass (local) | +| table-delete-link | `table-link-lifecycle.ts` (sampled foreign-table delete) | table-delete/10k-20f-link-detach | ✅ v1+v2 pass (local) | +| table-restore-link | `table-link-lifecycle.ts` (sampled table restore) | table-restore/10k-20f-link-1k | ✅ v1+v2 pass (local) | +| selection-duplicate | `record-duplicate-lifecycle.ts` (stream block duplicate) | record-duplicate/grid-block-duplicate-1k | ✅ v1+v2 pass (local) | +| record-duplicate-single | `record-duplicate-lifecycle.ts` (sequential single dup) | record-duplicate/single-record-sequential-100 | ✅ v1+v2 pass (local) | +| record-update | `record-mutation-lifecycle.ts` (bulk update over seeded rows, record window + restore-or-delete) | record-update/mixed-1k-20fields-bulk-update | ✅ v1+v2 pass (local) | +| record-create | `record-mutation-lifecycle.ts` (bulk insert into empty seed, no window + delete-created-or-drop) | record-create/mixed-1k-20fields-bulk-create | ✅ v1+v2 pass (local) | +| record-reorder | `record-mutation-lifecycle.ts` (block reorder over seeded rows, record window + restore-order-or-drop) | record-reorder/10k-move-last-1k-to-front | ✅ v1+v2 pass (local) | ## Not migrated (⬜ legacy `*.runner.ts`) @@ -47,7 +48,6 @@ Status as of 2026-06-19 on `main`. | lookup-search-index | 2 | search/search-index-off-10k-20search-fields, search/search-index-on-10k-20search-fields | | record-paste | 4 | record-paste/flat-10k-4fields-copy-paste, record-paste/flat-10k-20fields-copy-paste, record-paste/mixed-10k-20fields-complex-copy-paste, selection-paste/10k-expand-rows-and-fields-stream | | record-read | 2 | record-read/10k-50fields-10x1k-pages, record-read/10k-50fields-filter-sort-groupby-overhead | -| record-reorder | 1 | record-reorder/10k-move-last-1k-to-front | | record-update-attachment | 1 | record-update/attachment-insert-100 | | record-update-link | 1 | record-update/1k-link-cells-bulk-update | | selection-clear | 1 | selection-clear/flat-1k-20fields-cell-clear-stream |