diff --git a/framework/runners/record-duplicate-lifecycle.ts b/framework/runners/record-duplicate-lifecycle.ts new file mode 100644 index 0000000..32db1d2 --- /dev/null +++ b/framework/runners/record-duplicate-lifecycle.ts @@ -0,0 +1,256 @@ +import { permanentDeleteTable } from "../../../utils/init-app"; +import { isExecuteDbIsolated } from "../env"; +import { measureAsync } from "../metrics"; +import { PerfRunDiagnosticError } from "../types"; +import type { + DuplicateRecordSeedBaseCaseConfig, + PerfCase, + PerfRunContext, + PerfRunnerKind, + PerfRunResult, +} from "../types"; +import { + assertDuplicateSourceReady, + assertRecordCount, + deleteRecordsInBatches, + prepareDuplicateSourceFixture, + type DuplicateRecordFixture, + type Measurement, + type SourceReadyVerification, +} from "./record-duplicate.shared"; + +// The lifecycle skeleton shared by selection-duplicate / record-duplicate-single. +// Before this driver, both runners hand-wrote the identical control flow: +// prepare(seed) -> seedReady -> the one measured duplicate operation -> verify -> +// build result (twice: catch + success) -> finally restore-back cleanup (delete +// the rows the duplicate created so the cached seed is left at its original row +// count, else drop the table). Only what actually varies between the two is +// declared by `RecordDuplicateSpec`; everything else lives here, once. +// +// Scope note: this driver is intentionally record-duplicate-family-shaped (both +// runners back onto record-duplicate.shared.ts and the `duplicateRecord` +// feature), not a universal runner driver. + +export type RecordDuplicateRunner = Extract< + PerfRunnerKind, + "selection-duplicate" | "record-duplicate-single" +>; + +export type RecordDuplicateHookArgs< + TConfig extends DuplicateRecordSeedBaseCaseConfig, +> = { + fixture: DuplicateRecordFixture; + config: TConfig; + perfCase: PerfCase; + context: PerfRunContext; +}; + +export type RecordDuplicateBuildArgs< + TConfig extends DuplicateRecordSeedBaseCaseConfig, + TPrimary, + TVerification, +> = { + config: TConfig; + fixture?: DuplicateRecordFixture; + prepareMeasurement?: Measurement; + sourceReadyMeasurement?: Measurement; + primaryMeasurement?: Measurement; + verifyMeasurement?: Measurement; + error?: unknown; +}; + +export type RecordDuplicateSpec< + TConfig extends DuplicateRecordSeedBaseCaseConfig, + TPrimary, + TVerification, +> = { + runner: RecordDuplicateRunner; + // Hash-input version + cache identity for the source fixture; must stay + // identical between seed mode and execute mode so the seed hash matches. + fixtureVersion: string; + // Human label used only in the restore-back warn message (not an artifact + // field), e.g. "single duplicate" / "selection duplicate". + seedLabel: string; + // The single MEASURED duplicate operation. The runner owns the measure + + // trace strategy (top-level stream trace vs per-iteration traces) and returns + // the measurement whose name becomes the primary phase name. + runPrimary: ( + args: RecordDuplicateHookArgs, + ) => Promise>; + // Verify the duplicated records and final row count through the real read path. + verify: ( + args: RecordDuplicateHookArgs & { primaryResult: TPrimary }, + ) => Promise; + // The record ids the measured operation created; deleted in restore-back + // cleanup so a reusable seed returns to its original row count. + getCreatedRecordIds: (primaryResult: TPrimary | undefined) => string[]; + buildResult: ( + args: RecordDuplicateBuildArgs, + ) => PerfRunResult; +}; + +const restoreOrDropFixture = async < + TConfig extends DuplicateRecordSeedBaseCaseConfig, +>( + baseId: string, + fixture: DuplicateRecordFixture | undefined, + config: TConfig, + createdRecordIds: string[], + seedLabel: string, +) => { + if (!fixture || isExecuteDbIsolated()) { + // CI execute jobs run on an isolated restored copy of the seed dump, so the + // mutated database is simply discarded after the job. + return; + } + + const dropTable = async () => { + try { + await permanentDeleteTable(baseId, fixture.tableId); + } catch (error) { + console.warn(`Failed to cleanup perf table ${fixture.tableId}`, error); + } + }; + + if (!fixture.reusableSeed) { + await dropTable(); + return; + } + + // The duplicate appended rows to a reusable seed. Delete just those rows so + // the cached fixture stays at its seeded row count; if that fails, drop it so + // the next run reseeds cleanly. + try { + if (createdRecordIds.length > 0) { + await deleteRecordsInBatches(fixture.tableId, createdRecordIds); + } + await assertRecordCount( + fixture, + config.rowCount, + config.verify.fullScanPageSize ?? 1_000, + ); + } catch (error) { + console.warn( + `Failed to restore cached ${seedLabel} seed ${fixture.tableId}; deleting it`, + error, + ); + await dropTable(); + } +}; + +export const runRecordDuplicateLifecycle = async < + TConfig extends DuplicateRecordSeedBaseCaseConfig, + TPrimary, + TVerification, +>( + perfCase: PerfCase, + context: PerfRunContext, + spec: RecordDuplicateSpec, +): Promise => { + // The registry dispatch guarantees this runner kind's case config matches the + // spec's TConfig; the generic widens the union so cast through unknown. + const config = perfCase.config as unknown as TConfig; + const baseId = globalThis.testConfig.baseId; + const tableName = `${config.tableNamePrefix}-${Date.now()}`; + let prepareMeasurement: Measurement | undefined; + let sourceReadyMeasurement: Measurement | undefined; + let primaryMeasurement: Measurement | undefined; + let verifyMeasurement: Measurement | undefined; + + try { + prepareMeasurement = await measureAsync("prepare", () => + prepareDuplicateSourceFixture({ + baseId, + tableName, + config, + perfCase, + runner: spec.runner, + fixtureVersion: spec.fixtureVersion, + }), + ); + const fixture = prepareMeasurement.result; + sourceReadyMeasurement = await measureAsync("seedReady", () => + assertDuplicateSourceReady(fixture, config), + ); + + const hookArgs: RecordDuplicateHookArgs = { + fixture, + config, + perfCase, + context, + }; + + try { + primaryMeasurement = await spec.runPrimary(hookArgs); + verifyMeasurement = await measureAsync("verify", () => + spec.verify({ ...hookArgs, primaryResult: primaryMeasurement!.result }), + ); + } catch (error) { + throw new PerfRunDiagnosticError( + error instanceof Error ? error.message : String(error), + spec.buildResult({ + config, + fixture, + prepareMeasurement, + sourceReadyMeasurement, + primaryMeasurement, + verifyMeasurement, + error, + }), + ); + } + + return spec.buildResult({ + config, + fixture, + prepareMeasurement, + sourceReadyMeasurement, + primaryMeasurement, + verifyMeasurement, + }); + } finally { + await restoreOrDropFixture( + baseId, + prepareMeasurement?.result, + config, + spec.getCreatedRecordIds(primaryMeasurement?.result), + spec.seedLabel, + ); + } +}; + +export const seedRecordDuplicateLifecycle = async < + TConfig extends DuplicateRecordSeedBaseCaseConfig, + TPrimary, + TVerification, +>( + perfCase: PerfCase, + _context: PerfRunContext, + spec: RecordDuplicateSpec, +): Promise => { + // The registry dispatch guarantees this runner kind's case config matches the + // spec's TConfig; the generic widens the union so cast through unknown. + const config = perfCase.config as unknown as TConfig; + const baseId = globalThis.testConfig.baseId; + const tableName = `${config.tableNamePrefix}-seed-${Date.now()}`; + const prepareMeasurement = await measureAsync("prepare", () => + prepareDuplicateSourceFixture({ + baseId, + tableName, + config, + perfCase, + runner: spec.runner, + fixtureVersion: spec.fixtureVersion, + }), + ); + const sourceReadyMeasurement = await measureAsync("seedReady", () => + assertDuplicateSourceReady(prepareMeasurement.result, config), + ); + + return spec.buildResult({ + config, + fixture: prepareMeasurement.result, + prepareMeasurement, + sourceReadyMeasurement, + }); +}; diff --git a/framework/runners/record-duplicate-single.runner.ts b/framework/runners/record-duplicate-single.runner.ts index ba62db0..db78006 100644 --- a/framework/runners/record-duplicate-single.runner.ts +++ b/framework/runners/record-duplicate-single.runner.ts @@ -1,6 +1,5 @@ import { duplicateRecord } from "@teable/openapi"; -import { permanentDeleteTable } from "../../../utils/init-app"; -import { getPrimaryThresholdMs, isExecuteDbIsolated } from "../env"; +import { getPrimaryThresholdMs } from "../env"; import { measureAsync, roundMetric, summarizeDurations } from "../metrics"; import { assertEngineRouting, @@ -14,18 +13,20 @@ import type { PerfRunResult, RecordDuplicateSingleCaseConfig, } from "../types"; -import { PerfRunDiagnosticError } from "../types"; import { assertDuplicatedRecordsMatchSource, assertDuplicateResponseMatchesSource, assertDuplicateSourceReady, assertRecordCount, - deleteRecordsInBatches, getSourceRecords, type DuplicateRecordFixture, type Measurement, - prepareDuplicateSourceFixture, } from "./record-duplicate.shared"; +import { + runRecordDuplicateLifecycle, + seedRecordDuplicateLifecycle, + type RecordDuplicateSpec, +} from "./record-duplicate-lifecycle"; type SingleDuplicateSample = { iteration: number; @@ -196,21 +197,6 @@ const verifySingleDuplicates = async ( }; }; -const cleanupDuplicatedRows = async ( - fixture: DuplicateRecordFixture, - config: RecordDuplicateSingleCaseConfig, - createdRecordIds: string[], -) => { - if (createdRecordIds.length > 0) { - await deleteRecordsInBatches(fixture.tableId, createdRecordIds); - } - return assertRecordCount( - fixture, - config.rowCount, - config.verify.fullScanPageSize ?? 1_000, - ); -}; - const buildRecordDuplicateSingleResult = ({ config, fixture, @@ -376,137 +362,34 @@ const buildRecordDuplicateSingleResult = ({ }, }); +const recordDuplicateSingleSpec: RecordDuplicateSpec< + RecordDuplicateSingleCaseConfig, + SingleDuplicatePrimaryResult, + SingleDuplicateVerification +> = { + runner: "record-duplicate-single", + fixtureVersion: RECORD_DUPLICATE_SINGLE_FIXTURE_VERSION, + seedLabel: "single duplicate", + // No top-level trace wrap: duplicateSingleRecords opens one trace step per + // sequential duplicate request, so the primary phase is just the loop timer. + runPrimary: ({ fixture, config, perfCase, context }) => + measureAsync("duplicateSingleLoop", () => + duplicateSingleRecords(fixture, config, perfCase, context), + ), + verify: ({ fixture, config, primaryResult }) => + verifySingleDuplicates(fixture, config, primaryResult.createdRecordIds), + getCreatedRecordIds: (primaryResult) => primaryResult?.createdRecordIds ?? [], + buildResult: buildRecordDuplicateSingleResult, +}; + export const runRecordDuplicateSingleCase = async ( perfCase: PerfCase, context: PerfRunContext, -): Promise => { - const config = perfCase.config as RecordDuplicateSingleCaseConfig; - const baseId = globalThis.testConfig.baseId; - const tableName = `${config.tableNamePrefix}-${Date.now()}`; - let prepareMeasurement: Measurement | undefined; - let sourceReadyMeasurement: - | Measurement>> - | undefined; - let primaryMeasurement: Measurement | undefined; - let verifyMeasurement: Measurement | undefined; - - try { - prepareMeasurement = await measureAsync("prepare", () => - prepareDuplicateSourceFixture({ - baseId, - tableName, - config, - perfCase, - runner: "record-duplicate-single", - fixtureVersion: RECORD_DUPLICATE_SINGLE_FIXTURE_VERSION, - }), - ); - sourceReadyMeasurement = await measureAsync("seedReady", () => - assertDuplicateSourceReady(prepareMeasurement!.result, config), - ); - - try { - primaryMeasurement = await measureAsync("duplicateSingleLoop", () => - duplicateSingleRecords( - prepareMeasurement!.result, - config, - perfCase, - context, - ), - ); - verifyMeasurement = await measureAsync("verify", () => - verifySingleDuplicates( - prepareMeasurement!.result, - config, - primaryMeasurement!.result.createdRecordIds, - ), - ); - } catch (error) { - throw new PerfRunDiagnosticError( - error instanceof Error ? error.message : String(error), - buildRecordDuplicateSingleResult({ - config, - fixture: prepareMeasurement.result, - prepareMeasurement, - sourceReadyMeasurement, - primaryMeasurement, - verifyMeasurement, - error, - }), - ); - } - - return buildRecordDuplicateSingleResult({ - config, - fixture: prepareMeasurement.result, - prepareMeasurement, - sourceReadyMeasurement, - primaryMeasurement, - verifyMeasurement, - }); - } finally { - const fixture = prepareMeasurement?.result; - if (fixture && !isExecuteDbIsolated() && fixture.reusableSeed) { - let restored = false; - try { - await cleanupDuplicatedRows( - fixture, - config, - primaryMeasurement?.result.createdRecordIds ?? [], - ); - restored = true; - } catch (error) { - console.warn( - `Failed to restore cached single duplicate seed ${fixture.tableId}; deleting it`, - error, - ); - } - - if (!restored) { - try { - await permanentDeleteTable(baseId, fixture.tableId); - } catch (error) { - console.warn( - `Failed to cleanup perf table ${fixture.tableId}`, - error, - ); - } - } - } else if (fixture && !isExecuteDbIsolated()) { - try { - await permanentDeleteTable(baseId, fixture.tableId); - } catch (error) { - console.warn(`Failed to cleanup perf table ${fixture.tableId}`, error); - } - } - } -}; +): Promise => + runRecordDuplicateLifecycle(perfCase, context, recordDuplicateSingleSpec); export const seedRecordDuplicateSingleCase = async ( perfCase: PerfCase, - _context: PerfRunContext, -): Promise => { - const config = perfCase.config as RecordDuplicateSingleCaseConfig; - const baseId = globalThis.testConfig.baseId; - const tableName = `${config.tableNamePrefix}-seed-${Date.now()}`; - const prepareMeasurement = await measureAsync("prepare", () => - prepareDuplicateSourceFixture({ - baseId, - tableName, - config, - perfCase, - runner: "record-duplicate-single", - fixtureVersion: RECORD_DUPLICATE_SINGLE_FIXTURE_VERSION, - }), - ); - const sourceReadyMeasurement = await measureAsync("seedReady", () => - assertDuplicateSourceReady(prepareMeasurement.result, config), - ); - - return buildRecordDuplicateSingleResult({ - config, - fixture: prepareMeasurement.result, - prepareMeasurement, - sourceReadyMeasurement, - }); -}; + context: PerfRunContext, +): Promise => + seedRecordDuplicateLifecycle(perfCase, context, recordDuplicateSingleSpec); diff --git a/framework/runners/selection-duplicate.runner.ts b/framework/runners/selection-duplicate.runner.ts index 35446e2..6fb5834 100644 --- a/framework/runners/selection-duplicate.runner.ts +++ b/framework/runners/selection-duplicate.runner.ts @@ -5,8 +5,7 @@ import type { IDuplicateSelectionStreamEvent, IDuplicateSelectionStreamProgressEvent, } from "@teable/openapi"; -import { permanentDeleteTable } from "../../../utils/init-app"; -import { getPrimaryThresholdMs, isExecuteDbIsolated } from "../env"; +import { getPrimaryThresholdMs } from "../env"; import { measureAsync } from "../metrics"; import { assertEngineRouting } from "../routing"; import { perfStreamSse } from "../sse"; @@ -17,16 +16,18 @@ import type { PerfRunResult, SelectionDuplicateCaseConfig, } from "../types"; -import { PerfRunDiagnosticError } from "../types"; import { assertDuplicatedRecordsMatchSource, assertDuplicateSourceReady, assertRecordCount, - deleteRecordsInBatches, type DuplicateRecordFixture, type Measurement, - prepareDuplicateSourceFixture, } from "./record-duplicate.shared"; +import { + runRecordDuplicateLifecycle, + seedRecordDuplicateLifecycle, + type RecordDuplicateSpec, +} from "./record-duplicate-lifecycle"; type SelectionDuplicateStreamResult = { totalCount: number; @@ -178,28 +179,12 @@ const verifySelectionDuplicate = async ( }; }; -const cleanupDuplicatedRows = async ( - baseId: string, - fixture: DuplicateRecordFixture, - config: SelectionDuplicateCaseConfig, - duplicatedRecordIds: string[], -) => { - if (duplicatedRecordIds.length > 0) { - await deleteRecordsInBatches(fixture.tableId, duplicatedRecordIds); - } - return assertRecordCount( - fixture, - config.rowCount, - config.verify.fullScanPageSize ?? 1_000, - ); -}; - const buildSelectionDuplicateResult = ({ config, fixture, prepareMeasurement, sourceReadyMeasurement, - duplicateMeasurement, + primaryMeasurement, verifyMeasurement, error, }: { @@ -209,7 +194,7 @@ const buildSelectionDuplicateResult = ({ sourceReadyMeasurement?: Measurement< Awaited> >; - duplicateMeasurement?: Measurement; + primaryMeasurement?: Measurement; verifyMeasurement?: Measurement; error?: unknown; }): PerfRunResult => ({ @@ -229,12 +214,12 @@ const buildSelectionDuplicateResult = ({ : {}), } : {}), - ...(duplicateMeasurement - ? { [config.threshold.metric]: duplicateMeasurement.durationMs } + ...(primaryMeasurement + ? { [config.threshold.metric]: primaryMeasurement.durationMs } : {}), ...(verifyMeasurement ? { verifyMs: verifyMeasurement.durationMs } : {}), }, - thresholds: duplicateMeasurement + thresholds: primaryMeasurement ? [ { metric: config.threshold.metric, @@ -260,11 +245,11 @@ const buildSelectionDuplicateResult = ({ }, ] : []), - ...(duplicateMeasurement + ...(primaryMeasurement ? [ { - name: duplicateMeasurement.name, - durationMs: duplicateMeasurement.durationMs, + name: primaryMeasurement.name, + durationMs: primaryMeasurement.durationMs, }, ] : []), @@ -321,17 +306,17 @@ const buildSelectionDuplicateResult = ({ : undefined, } : undefined, - duplicate: duplicateMeasurement?.result + duplicate: primaryMeasurement?.result ? { - totalCount: duplicateMeasurement.result.totalCount, - duplicatedCount: duplicateMeasurement.result.duplicatedCount, - duplicatedRecordIds: duplicateMeasurement.result.duplicatedRecordIds, - progressEventCount: duplicateMeasurement.result.progressEventCount, - status: duplicateMeasurement.result.status, - trace: duplicateMeasurement.result.trace, + totalCount: primaryMeasurement.result.totalCount, + duplicatedCount: primaryMeasurement.result.duplicatedCount, + duplicatedRecordIds: primaryMeasurement.result.duplicatedRecordIds, + progressEventCount: primaryMeasurement.result.progressEventCount, + status: primaryMeasurement.result.status, + trace: primaryMeasurement.result.trace, } : undefined, - routing: duplicateMeasurement?.result.routing, + routing: primaryMeasurement?.result.routing, verification: verifyMeasurement?.result ? { duplicatedIds: { @@ -356,149 +341,40 @@ const buildSelectionDuplicateResult = ({ }, }); +const selectionDuplicateSpec: RecordDuplicateSpec< + SelectionDuplicateCaseConfig, + SelectionDuplicateStreamResult, + SelectionDuplicateVerification +> = { + runner: "selection-duplicate", + fixtureVersion: SELECTION_DUPLICATE_FIXTURE_VERSION, + seedLabel: "selection duplicate", + // One top-level trace step wraps the whole duplicate-selection stream. + runPrimary: ({ fixture, config, perfCase, context }) => + withPerfTraceStep(context, perfCase, config.threshold.metric, () => + measureAsync(config.threshold.metric, () => + duplicateSelectionRange(fixture, config, perfCase, context), + ), + ), + verify: ({ fixture, config, primaryResult }) => + verifySelectionDuplicate( + fixture, + config, + primaryResult.duplicatedRecordIds, + ), + getCreatedRecordIds: (primaryResult) => + primaryResult?.duplicatedRecordIds ?? [], + buildResult: buildSelectionDuplicateResult, +}; + export const runSelectionDuplicateCase = async ( perfCase: PerfCase, context: PerfRunContext, -): Promise => { - const config = perfCase.config as SelectionDuplicateCaseConfig; - const baseId = globalThis.testConfig.baseId; - const tableName = `${config.tableNamePrefix}-${Date.now()}`; - let prepareMeasurement: Measurement | undefined; - let sourceReadyMeasurement: - | Measurement>> - | undefined; - let duplicateMeasurement: - | Measurement - | undefined; - let verifyMeasurement: - | Measurement - | undefined; - - try { - prepareMeasurement = await measureAsync("prepare", () => - prepareDuplicateSourceFixture({ - baseId, - tableName, - config, - perfCase, - runner: "selection-duplicate", - fixtureVersion: SELECTION_DUPLICATE_FIXTURE_VERSION, - }), - ); - sourceReadyMeasurement = await measureAsync("seedReady", () => - assertDuplicateSourceReady(prepareMeasurement!.result, config), - ); - - try { - duplicateMeasurement = await withPerfTraceStep( - context, - perfCase, - config.threshold.metric, - () => - measureAsync(config.threshold.metric, () => - duplicateSelectionRange( - prepareMeasurement!.result, - config, - perfCase, - context, - ), - ), - ); - - verifyMeasurement = await measureAsync("verify", () => - verifySelectionDuplicate( - prepareMeasurement!.result, - config, - duplicateMeasurement!.result.duplicatedRecordIds, - ), - ); - } catch (error) { - throw new PerfRunDiagnosticError( - error instanceof Error ? error.message : String(error), - buildSelectionDuplicateResult({ - config, - fixture: prepareMeasurement.result, - prepareMeasurement, - sourceReadyMeasurement, - duplicateMeasurement, - verifyMeasurement, - error, - }), - ); - } - - return buildSelectionDuplicateResult({ - config, - fixture: prepareMeasurement.result, - prepareMeasurement, - sourceReadyMeasurement, - duplicateMeasurement, - verifyMeasurement, - }); - } finally { - const fixture = prepareMeasurement?.result; - if (fixture && !isExecuteDbIsolated() && fixture.reusableSeed) { - let restored = false; - try { - await cleanupDuplicatedRows( - baseId, - fixture, - config, - duplicateMeasurement?.result.duplicatedRecordIds ?? [], - ); - restored = true; - } catch (error) { - console.warn( - `Failed to restore cached selection duplicate seed ${fixture.tableId}; deleting it`, - error, - ); - } - - if (!restored) { - try { - await permanentDeleteTable(baseId, fixture.tableId); - } catch (error) { - console.warn( - `Failed to cleanup perf table ${fixture.tableId}`, - error, - ); - } - } - } else if (fixture && !isExecuteDbIsolated()) { - try { - await permanentDeleteTable(baseId, fixture.tableId); - } catch (error) { - console.warn(`Failed to cleanup perf table ${fixture.tableId}`, error); - } - } - } -}; +): Promise => + runRecordDuplicateLifecycle(perfCase, context, selectionDuplicateSpec); export const seedSelectionDuplicateCase = async ( perfCase: PerfCase, - _context: PerfRunContext, -): Promise => { - const config = perfCase.config as SelectionDuplicateCaseConfig; - const baseId = globalThis.testConfig.baseId; - const tableName = `${config.tableNamePrefix}-seed-${Date.now()}`; - const prepareMeasurement = await measureAsync("prepare", () => - prepareDuplicateSourceFixture({ - baseId, - tableName, - config, - perfCase, - runner: "selection-duplicate", - fixtureVersion: SELECTION_DUPLICATE_FIXTURE_VERSION, - }), - ); - const sourceReadyMeasurement = await measureAsync("seedReady", () => - assertDuplicateSourceReady(prepareMeasurement.result, config), - ); - - return buildSelectionDuplicateResult({ - config, - fixture: prepareMeasurement.result, - prepareMeasurement, - sourceReadyMeasurement, - }); -}; + context: PerfRunContext, +): Promise => + seedRecordDuplicateLifecycle(perfCase, context, selectionDuplicateSpec); diff --git a/scripts/diff-artifacts.mjs b/scripts/diff-artifacts.mjs index 10e5a43..3ec13e6 100644 --- a/scripts/diff-artifacts.mjs +++ b/scripts/diff-artifacts.mjs @@ -60,6 +60,15 @@ const GENERATED_ID_KEYS = new Set([ "tableId", "trashId", "viewId", + // Generated record ids produced by the duplicate runners. Each run seeds a + // fresh table, so the duplicated/source record ids differ between two runs of + // unchanged code (confirmed by the record-duplicate baseline A vs B diff). + // Counts (requestCount/duplicatedCount/totalCount) stay visible; only the + // opaque id strings are masked, like the existing `recordId`. + "createdRecordIds", + "duplicatedRecordIds", + "sourceRecordId", + "duplicatedRecordId", ]); const GENERATED_NAME_KEYS = new Set(["foreignTableName", "tableName"]); @@ -127,6 +136,18 @@ const shouldMaskKey = (path, key) => { return true; } + // The duplicate runners echo the live request back in details.request: `path` + // embeds the freshly-seeded table id and `projection` is the list of generated + // field ids. Both differ between two runs of unchanged code (record-duplicate + // baseline A vs B). details.operation + details.request.method keep the + // endpoint identity visible. + if ( + pathEquals(path, ["details", "request"]) && + ["path", "projection"].includes(key) + ) { + return true; + } + if ( pathEquals(path, ["details", "import", "completion"]) && ["pollCount", "tableId"].includes(key) diff --git a/tasks/runner-migration-tracker.md b/tasks/runner-migration-tracker.md index 5d4ac50..5578269 100644 --- a/tasks/runner-migration-tracker.md +++ b/tasks/runner-migration-tracker.md @@ -6,22 +6,24 @@ that uses it, because migrating a runner means re-verifying all of its cases. Status as of 2026-06-19 on `main`. -**Migrated: 10 / 35 runner kinds · 12 / 55 cases.** +**Migrated: 12 / 35 runner kinds · 14 / 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) | +| 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) | ## Not migrated (⬜ legacy `*.runner.ts`) @@ -42,7 +44,6 @@ Status as of 2026-06-19 on `main`. | link-computed-propagation | 2 | lookup/dual-link-computed-first-link-4k, lookup/dual-link-computed-repoint-2k | | lookup-search-index | 2 | search/search-index-off-10k-20search-fields, search/search-index-on-10k-20search-fields | | record-create | 1 | record-create/mixed-1k-20fields-bulk-create | -| record-duplicate-single | 1 | record-duplicate/single-record-sequential-100 | | 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 | @@ -50,7 +51,6 @@ Status as of 2026-06-19 on `main`. | 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 | -| selection-duplicate | 1 | record-duplicate/grid-block-duplicate-1k | | table-create | 2 | table-create/10x-20f-no-records, table-create/1x-20f-1k-records | ## How migration proceeds