diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index d8aa9ec7d7..2ec5ac470b 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -42,6 +42,8 @@ jobs: - name: πŸ“₯ Download deps run: pnpm install --frozen-lockfile --filter trigger.dev... + # Prisma clients generate concurrently and share one engine binary in the + # pnpm store; the generate script retries the transient Windows EPERM race. - name: πŸ“€ Generate Prisma Client run: pnpm run generate diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index dc18972082..f5e0f0a671 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -1727,10 +1727,10 @@ const EnvironmentSchema = z RUN_REPLICATION_LEGACY_ORIGIN_GENERATION: z.coerce.number().int().default(0), RUN_REPLICATION_NEW_ORIGIN_GENERATION: z.coerce.number().int().default(1), - // Run-ops KSUID mint cutover β€” per-env, canary-first, OFF by default. - // Even when on, an env mints KSUID only if its per-org runOpsMintKsuid flag is - // "ksuid" AND isSplitEnabled() is true. Cache mirrors REALTIME_BACKEND_FLAG_CACHE_*. - RUN_OPS_MINT_KSUID_ENABLED: BoolEnv.default(false), + // Run-ops id mint cutover β€” per-env, canary-first, OFF by default. + // Even when on, an env mints run-ops ids only if its per-org runOpsMintKind flag is + // "runOpsId" AND isSplitEnabled() is true. Cache mirrors REALTIME_BACKEND_FLAG_CACHE_*. + RUN_OPS_MINT_ENABLED: BoolEnv.default(false), RUN_OPS_MINT_FLAG_CACHE_TTL_MS: z.coerce.number().int().default(30_000), RUN_OPS_MINT_FLAG_CACHE_MAX_ENTRIES: z.coerce.number().int().default(10_000), diff --git a/apps/webapp/app/presenters/v3/ApiBatchResultsPresenter.server.ts b/apps/webapp/app/presenters/v3/ApiBatchResultsPresenter.server.ts index 6d46074e72..0e04e909eb 100644 --- a/apps/webapp/app/presenters/v3/ApiBatchResultsPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/ApiBatchResultsPresenter.server.ts @@ -131,7 +131,7 @@ export class ApiBatchResultsPresenter extends BasePresenter { } // Split: resolve the batch row new-first then off the legacy READ REPLICA only (a batch id may - // be cuid or ksuid, and a cuid-shaped id can still have been backfilled onto NEW, so id-shape + // be cuid or run-ops id, and a cuid-shaped id can still have been backfilled onto NEW, so id-shape // residency is not authoritative for the row β€” the new-first-then-legacy probe is), then // hydrate every member run independently via the per-run read-through primitive. async #callSplit( diff --git a/apps/webapp/app/presenters/v3/ApiWaitpointPresenter.server.ts b/apps/webapp/app/presenters/v3/ApiWaitpointPresenter.server.ts index c42371f632..6619ab023a 100644 --- a/apps/webapp/app/presenters/v3/ApiWaitpointPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/ApiWaitpointPresenter.server.ts @@ -42,7 +42,7 @@ export class ApiWaitpointPresenter extends BasePresenter { // Public waitpoint retrieve. Split on: new run-ops client first, then the LEGACY // RUN-OPS READ REPLICA ONLY on a new-probe miss β€” never the legacy primary. // Split off (single-DB / self-host): one plain waitpoint.findFirst against the replica - // (passthrough). The waitpointId is the residency-classifiable KSUID id (the route + // (passthrough). The waitpointId is the residency-classifiable run-ops id (the route // pre-decodes the friendlyId via WaitpointId.toId). const hydrate = (client: PrismaReplicaClient) => client.waitpoint.findFirst({ diff --git a/apps/webapp/app/presenters/v3/BatchListPresenter.server.ts b/apps/webapp/app/presenters/v3/BatchListPresenter.server.ts index 8896b597fe..fd5a8481f1 100644 --- a/apps/webapp/app/presenters/v3/BatchListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/BatchListPresenter.server.ts @@ -101,7 +101,7 @@ export class BatchListPresenter extends BasePresenter { } } - // codepoint comparator (NEVER localeCompare): BatchTaskRun.id is ASCII (cuid or ksuid). + // codepoint comparator (NEVER localeCompare): BatchTaskRun.id is ASCII (cuid or run-ops id). const sign = direction === "forward" ? 1 : -1; // forward => DESC; backward => ASC return Array.from(byId.values()) .sort((a, b) => (a.id < b.id ? sign : a.id > b.id ? -sign : 0)) diff --git a/apps/webapp/app/presenters/v3/BatchPresenter.server.ts b/apps/webapp/app/presenters/v3/BatchPresenter.server.ts index e0eb3a24fd..d6af644be7 100644 --- a/apps/webapp/app/presenters/v3/BatchPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/BatchPresenter.server.ts @@ -79,7 +79,7 @@ export class BatchPresenter extends BasePresenter { const batchResult = await readThrough({ // The read-through key; here it is the batch friendlyId. A cuid-shaped batch friendlyId // classifies as LEGACY and the read-through probes both stores (new first, then legacy - // replica); a ksuid-shaped one (cut-over orgs) classifies as NEW and reads only the new + // replica); a run-ops-shaped one (cut-over orgs) classifies as NEW and reads only the new // store β€” either way the row is found on the DB that owns it. runId: batchId, environmentId, diff --git a/apps/webapp/app/routes/api.v1.runs.$runFriendlyId.input-streams.wait.ts b/apps/webapp/app/routes/api.v1.runs.$runFriendlyId.input-streams.wait.ts index c2efce0234..3c081b2280 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runFriendlyId.input-streams.wait.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runFriendlyId.input-streams.wait.ts @@ -78,7 +78,7 @@ const { action, loader } = createActionApiRoute( } } - // Create the waitpoint. Co-locate it with the owning run (run-ops split) so a ksuid + // Create the waitpoint. Co-locate it with the owning run (run-ops split) so a run-ops id // run's input-stream waitpoint lands on the run's DB and its block edge resolves. const result = await engine.createManualWaitpoint({ runId: run.id, diff --git a/apps/webapp/app/routes/api.v1.runs.$runFriendlyId.session-streams.wait.ts b/apps/webapp/app/routes/api.v1.runs.$runFriendlyId.session-streams.wait.ts index 0a34e6cddb..f6c58ab748 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runFriendlyId.session-streams.wait.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runFriendlyId.session-streams.wait.ts @@ -99,7 +99,7 @@ const { action, loader } = createActionApiRoute( } } - // Create the waitpoint. Co-locate it with the owning run (run-ops split) so a ksuid + // Create the waitpoint. Co-locate it with the owning run (run-ops split) so a run-ops id // run's session-stream waitpoint lands on the run's DB and its block edge resolves. const result = await engine.createManualWaitpoint({ runId: run.id, diff --git a/apps/webapp/app/routes/engine.v1.runs.$runFriendlyId.wait.duration.ts b/apps/webapp/app/routes/engine.v1.runs.$runFriendlyId.wait.duration.ts index 24f7f81ba0..c7a8c3c561 100644 --- a/apps/webapp/app/routes/engine.v1.runs.$runFriendlyId.wait.duration.ts +++ b/apps/webapp/app/routes/engine.v1.runs.$runFriendlyId.wait.duration.ts @@ -42,7 +42,7 @@ const { action } = createActionApiRoute( : undefined; const { waitpoint } = await engine.createDateTimeWaitpoint({ - // Co-locate the waitpoint with the run that blocks on it (run-ops split): a ksuid run lives + // Co-locate the waitpoint with the run that blocks on it (run-ops split): a run-ops run lives // on the dedicated DB, but the minted waitpoint id is always a cuid, so without the run id // the waitpoint would route to the control-plane DB and the block edge would never resolve. runId: run.id, diff --git a/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.test.ts b/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.test.ts index ee128224d1..39b806a0f7 100644 --- a/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.test.ts +++ b/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.test.ts @@ -16,9 +16,9 @@ function makeDeps(over: Partial): ResolveIdempoten fallbackClient: FALLBACK, newClient: NEW_CLIENT, legacyClient: LEGACY_CLIENT, - resolveMintKind: async () => "ksuid", + resolveMintKind: async () => "runOpsId", classify: (id) => { - if (id.length === 27) return "NEW"; + if (id.length === 26 && id[25] === "1") return "NEW"; if (id.length === 25) return "LEGACY"; throw new Error(`unclassifiable: ${id.length}`); }, @@ -38,10 +38,10 @@ describe("resolveIdempotencyDedupClient", () => { expect(client).toBe(FALLBACK); }); - it("routes a root run to the NEW client when the env mints ksuid", async () => { + it("routes a root run to the NEW client when the env mints run-ops ids", async () => { const client = await resolveIdempotencyDedupClient( { environmentForMint: env, parentRunFriendlyId: undefined }, - makeDeps({ resolveMintKind: async () => "ksuid" }) + makeDeps({ resolveMintKind: async () => "runOpsId" }) ); expect(client).toBe(NEW_CLIENT); }); @@ -54,10 +54,10 @@ describe("resolveIdempotencyDedupClient", () => { expect(client).toBe(LEGACY_CLIENT); }); - it("routes a child to the NEW client when the ksuid parent is NEW-resident", async () => { - const ksuidParent = RunId.toFriendlyId("a".repeat(27)); + it("routes a child to the NEW client when the run-ops parent is NEW-resident", async () => { + const runOpsParent = RunId.toFriendlyId("a".repeat(24) + "01"); const client = await resolveIdempotencyDedupClient( - { environmentForMint: env, parentRunFriendlyId: ksuidParent }, + { environmentForMint: env, parentRunFriendlyId: runOpsParent }, makeDeps({ resolveMintKind: async () => "cuid" }) // mint flag must NOT win for a child ); expect(client).toBe(NEW_CLIENT); @@ -67,7 +67,7 @@ describe("resolveIdempotencyDedupClient", () => { const cuidParent = RunId.toFriendlyId("b".repeat(25)); const client = await resolveIdempotencyDedupClient( { environmentForMint: env, parentRunFriendlyId: cuidParent }, - makeDeps({ resolveMintKind: async () => "ksuid" }) // mint flag must NOT win for a child + makeDeps({ resolveMintKind: async () => "runOpsId" }) // mint flag must NOT win for a child ); expect(client).toBe(LEGACY_CLIENT); }); diff --git a/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.ts b/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.ts index 38ef475584..86f1435654 100644 --- a/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.ts +++ b/apps/webapp/app/runEngine/concerns/idempotencyResidency.server.ts @@ -1,7 +1,7 @@ import { ownerEngine, RunId, type Residency } from "@trigger.dev/core/v3/isomorphic"; import type { PrismaClientOrTransaction } from "@trigger.dev/database"; -type MintKind = "cuid" | "ksuid"; +type MintKind = "cuid" | "runOpsId"; export type ResolveIdempotencyClientDeps = { isSplitEnabled: () => Promise; @@ -52,5 +52,5 @@ export async function resolveIdempotencyDedupClient( } const kind = await deps.resolveMintKind(args.environmentForMint); - return clientFor(kind === "ksuid" ? "NEW" : "LEGACY"); + return clientFor(kind === "runOpsId" ? "NEW" : "LEGACY"); } diff --git a/apps/webapp/app/runEngine/services/streamBatchItems.server.ts b/apps/webapp/app/runEngine/services/streamBatchItems.server.ts index 0011975d6d..05498066f9 100644 --- a/apps/webapp/app/runEngine/services/streamBatchItems.server.ts +++ b/apps/webapp/app/runEngine/services/streamBatchItems.server.ts @@ -129,7 +129,7 @@ export class StreamBatchItemsService extends WithRunEngine { const batchId = this.parseBatchFriendlyId(batchFriendlyId); // Validate batch exists and belongs to this environment. Routed by batch id so a - // ksuid (NEW-resident) batch is found on the owning DB; the env-ownership check that + // run-ops id (NEW-resident) batch is found on the owning DB; the env-ownership check that // was in the where clause is enforced app-side below. const batch = await this._engine.runStore.findBatchTaskRunById(batchId); diff --git a/apps/webapp/app/runEngine/services/triggerFailedTask.server.ts b/apps/webapp/app/runEngine/services/triggerFailedTask.server.ts index 811cefd350..ee420c835d 100644 --- a/apps/webapp/app/runEngine/services/triggerFailedTask.server.ts +++ b/apps/webapp/app/runEngine/services/triggerFailedTask.server.ts @@ -1,6 +1,6 @@ import type { RunEngine } from "@internal/run-engine"; import { TaskRunErrorCodes, type TaskRunError } from "@trigger.dev/core/v3"; -import { RunId, generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { RunId, generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import type { PrismaClientOrTransaction, RuntimeEnvironmentType, @@ -85,7 +85,7 @@ export class TriggerFailedTaskService { } // Mint a failed run's friendlyId. The id-kind decides which store the run is - // born in (cuid β†’ legacy store, ksuid β†’ new store); the whole subgraph of a + // born in (cuid β†’ legacy store, run-ops id β†’ new store); the whole subgraph of a // run must agree. Root failed runs mint by the environment's setting; child // failed runs inherit the parent's current store so they never split. private async mintFailedRunFriendlyId(args: { @@ -102,8 +102,8 @@ export class TriggerFailedTaskService { orgFeatureFlags: args.orgFeatureFlags, }); - return mintKind === "ksuid" - ? RunId.toFriendlyId(generateKsuidId()) + return mintKind === "runOpsId" + ? RunId.toFriendlyId(generateRunOpsId()) : RunId.generate().friendlyId; } diff --git a/apps/webapp/app/runEngine/services/triggerTask.server.ts b/apps/webapp/app/runEngine/services/triggerTask.server.ts index a86cb9e0ed..6a72ad34a1 100644 --- a/apps/webapp/app/runEngine/services/triggerTask.server.ts +++ b/apps/webapp/app/runEngine/services/triggerTask.server.ts @@ -14,7 +14,7 @@ import { TriggerTraceContext, } from "@trigger.dev/core/v3"; import { - generateKsuidId, + generateRunOpsId, parseTraceparent, RunId, serializeTraceparent, @@ -129,16 +129,21 @@ export class RunEngineTriggerTaskService { } // Mint a new run's friendlyId. The id-kind decides which store the run is born - // in (cuid β†’ legacy store, ksuid β†’ new store), so the whole subgraph of a run + // in (cuid β†’ legacy store, run-ops id β†’ new store), so the whole subgraph of a run // must agree. Two cases: // // - ROOT run (no parent): mint by the environment's cutover setting. // - CHILD run (has a parent): inherit the parent's residency by id-shape, so a - // parent and child never split across stores (ksuid parent β†’ ksuid child, + // parent and child never split across stores (run-ops parent β†’ run-ops child, // cuid parent β†’ cuid child). + // `region` is the caller-requested region (body.options.region). The id is + // minted before the worker queue is resolved (the idempotency concern needs + // the friendlyId first), so the stamped region char reflects the requested + // region β€” or the default char when the run targets the default region. private async mintRunFriendlyId( environment: AuthenticatedEnvironment, - parentRunFriendlyId?: string + parentRunFriendlyId?: string, + region?: string ): Promise { const mintKind = parentRunFriendlyId ? resolveInheritedMintKind(parentRunFriendlyId) @@ -148,8 +153,8 @@ export class RunEngineTriggerTaskService { orgFeatureFlags: environment.organization.featureFlags, }); - return mintKind === "ksuid" - ? RunId.toFriendlyId(generateKsuidId()) + return mintKind === "runOpsId" + ? RunId.toFriendlyId(generateRunOpsId(region)) : RunId.generate().friendlyId; } @@ -183,7 +188,11 @@ export class RunEngineTriggerTaskService { // parent is present, else the environment's setting. const runFriendlyId = options?.runFriendlyId ?? - (await this.mintRunFriendlyId(environment, body.options?.parentRunId)); + (await this.mintRunFriendlyId( + environment, + body.options?.parentRunId, + body.options?.region + )); const triggerRequest = { taskId, friendlyId: runFriendlyId, diff --git a/apps/webapp/app/services/realtime/sessions.server.ts b/apps/webapp/app/services/realtime/sessions.server.ts index 71170c322f..7f50450c3a 100644 --- a/apps/webapp/app/services/realtime/sessions.server.ts +++ b/apps/webapp/app/services/realtime/sessions.server.ts @@ -126,7 +126,7 @@ export function serializeSession(session: Session): SessionItem { * Skips the lookup when `currentRunId` is null. * * Resolves `currentRunId` -> `friendlyId` through `runStore.findRun` so a - * ksuid (NEW-DB) session run resolves from its owning store rather than the + * run-ops id (NEW-DB) session run resolves from its owning store rather than the * control-plane replica. Mirrors `sessionRunManager.server.ts`. * Tenant-scoped because `Session.currentRunId` is a no-FK pointer. */ diff --git a/apps/webapp/app/services/runsReplicationInstance.server.ts b/apps/webapp/app/services/runsReplicationInstance.server.ts index 30a757597f..ad048422ac 100644 --- a/apps/webapp/app/services/runsReplicationInstance.server.ts +++ b/apps/webapp/app/services/runsReplicationInstance.server.ts @@ -59,8 +59,8 @@ export function buildReplicationSources(args: { /** * The residency-split gate and the `#new`->ClickHouse replication gate are - * independent env vars. If split is on (ksuid runs are minted on the new DB) but the - * constructed sources[] has no `"new"` source, every ksuid run is silently missing from + * independent env vars. If split is on (run-ops runs are minted on the new DB) but the + * constructed sources[] has no `"new"` source, every run-ops run is silently missing from * ClickHouse β€” under-counting all CH-fronted usage/cost/metrics aggregates with no * Postgres fallback. Couple the gates at boot: this misconfiguration must fail loudly * rather than ship a fleet-wide under-count. @@ -69,7 +69,7 @@ export class SplitReplicationMisconfiguredError extends Error { constructor() { super( 'RUN_OPS_SPLIT_ENABLED is on but the runs-replication sources[] has no "new" source: ' + - "ksuid runs on the new DB would not replicate to ClickHouse, under-counting every " + + "run-ops runs on the new DB would not replicate to ClickHouse, under-counting every " + "ClickHouse-fronted aggregate. Enable the new replication source " + "(RUN_REPLICATION_NEW_ENABLED / RUN_OPS_DATABASE_URL) or turn the split off." ); diff --git a/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts b/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts index 65df2b4121..f57e37275c 100644 --- a/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts +++ b/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts @@ -194,7 +194,7 @@ export class ClickHouseRunsRepository implements IRunsRepository { // Preserve the ClickHouse keyset order (created_at desc, run_id desc) by re-ordering the // hydrated rows to match the input `runIds`. Sorting by raw `id` was only ~chronological - // when every id was a time-prefixed cuid; a mixed cuid/ksuid page sorts the two id-spaces + // when every id was a time-prefixed cuid; a mixed cuid/run-ops id page sorts the two id-spaces // into separate blocks, burying recent runs. Rows whose PG row is gone (e.g. past // retention) drop out, exactly as before. const byId = new Map(rows.map((r) => [r.id, r] as const)); diff --git a/apps/webapp/app/services/runsRepository/runsRepository.server.ts b/apps/webapp/app/services/runsRepository/runsRepository.server.ts index 2fadb8c710..fd095150ac 100644 --- a/apps/webapp/app/services/runsRepository/runsRepository.server.ts +++ b/apps/webapp/app/services/runsRepository/runsRepository.server.ts @@ -295,7 +295,7 @@ export async function convertRunListInputOptionsToFilterRunsOptions( }); convertedOptions.period = time.period ? (parseDuration(time.period) ?? undefined) : undefined; - // Cross-DB resolution: BatchTaskRun is a RUN-OPS table. A ksuid batch resident on the + // Cross-DB resolution: BatchTaskRun is a RUN-OPS table. A run-ops batch resident on the // dedicated run-ops DB must resolve via the store's NEW->LEGACY probe β€” a single control-plane // client would miss it and leave the friendlyId in the ClickHouse `batch_id` filter, matching // nothing. Split off / self-host: the store is a passthrough over the one client. diff --git a/apps/webapp/app/utils/friendlyId.test.ts b/apps/webapp/app/utils/friendlyId.test.ts index 4b2854f51c..e6c902c96b 100644 --- a/apps/webapp/app/utils/friendlyId.test.ts +++ b/apps/webapp/app/utils/friendlyId.test.ts @@ -2,30 +2,31 @@ import { describe, expect, it } from "vitest"; import { BatchId, generateFriendlyId, - generateKsuidId, + generateRunOpsId, RunId, } from "@trigger.dev/core/v3/isomorphic"; import { isValidFriendlyId, makeFriendlyIdValidator } from "./friendlyId"; describe("isValidFriendlyId", () => { it("accepts every id generation the real generators produce", () => { - // nanoid (legacy V1), cuid (run-engine), ksuid (run-ops split) + // nanoid (legacy V1), cuid (run-engine), run-ops v1 (run-ops split) expect(isValidFriendlyId(generateFriendlyId("run"), "run")).toBe(true); expect(isValidFriendlyId(RunId.generate().friendlyId, "run")).toBe(true); - expect(isValidFriendlyId(RunId.toFriendlyId(generateKsuidId()), "run")).toBe(true); + expect(isValidFriendlyId(RunId.toFriendlyId(generateRunOpsId()), "run")).toBe(true); expect(isValidFriendlyId(generateFriendlyId("batch"), "batch")).toBe(true); expect(isValidFriendlyId(BatchId.generate().friendlyId, "batch")).toBe(true); - expect(isValidFriendlyId(BatchId.toFriendlyId(generateKsuidId()), "batch")).toBe(true); + expect(isValidFriendlyId(BatchId.toFriendlyId(generateRunOpsId()), "batch")).toBe(true); }); - it("accepts each valid body length (21 nanoid, 25 cuid, 27 ksuid)", () => { + it("accepts each valid body length (21 nanoid, 25 cuid, 26 run-ops v1, 27 legacy base62)", () => { expect(isValidFriendlyId("run_" + "a".repeat(21), "run")).toBe(true); expect(isValidFriendlyId("run_" + "a".repeat(25), "run")).toBe(true); + expect(isValidFriendlyId("run_" + "a".repeat(26), "run")).toBe(true); expect(isValidFriendlyId("run_" + "a".repeat(27), "run")).toBe(true); }); - it("accepts mixed-case (uppercase) ksuid bodies", () => { + it("accepts mixed-case (uppercase) legacy base62 bodies", () => { expect(isValidFriendlyId("run_2ABCdefGHI0123456789jklMN", "run")).toBe(true); }); @@ -39,7 +40,7 @@ describe("isValidFriendlyId", () => { }); it("rejects body lengths that match no generator", () => { - for (const len of [0, 20, 22, 24, 26, 28]) { + for (const len of [0, 20, 22, 24, 28]) { expect(isValidFriendlyId("run_" + "a".repeat(len), "run")).toBe(false); } }); @@ -64,8 +65,8 @@ describe("makeFriendlyIdValidator", () => { it("returns undefined for a valid id of any generation", () => { expect(validateRunId(generateFriendlyId("run"))).toBeUndefined(); expect(validateRunId(RunId.generate().friendlyId)).toBeUndefined(); - expect(validateRunId(RunId.toFriendlyId(generateKsuidId()))).toBeUndefined(); - expect(validateBatchId(BatchId.toFriendlyId(generateKsuidId()))).toBeUndefined(); + expect(validateRunId(RunId.toFriendlyId(generateRunOpsId()))).toBeUndefined(); + expect(validateBatchId(BatchId.toFriendlyId(generateRunOpsId()))).toBeUndefined(); }); it("reports a wrong prefix distinctly from a wrong shape", () => { diff --git a/apps/webapp/app/utils/friendlyId.ts b/apps/webapp/app/utils/friendlyId.ts index bc26ef19b4..2500d685fa 100644 --- a/apps/webapp/app/utils/friendlyId.ts +++ b/apps/webapp/app/utils/friendlyId.ts @@ -1,22 +1,26 @@ -import { CUID_LENGTH, KSUID_LENGTH } from "@trigger.dev/core/v3/isomorphic"; +import { CUID_LENGTH, RUN_OPS_ID_LENGTH } from "@trigger.dev/core/v3/isomorphic"; -// The body after `_` is a base62 id; three generator lengths remain -// valid in existing data and must all be accepted: 21 (nanoid), 25 (cuid), -// 27 (ksuid). cuid/ksuid come from core so this tracks any future change. +// The body after `_` is an alphanumeric id; four generator lengths +// remain valid in existing data and must all be accepted: 21 (nanoid), +// 25 (cuid), 26 (run-ops v1 base32hex), 27 (pre-cutover base62, kept so old +// ids still pass filter validation). cuid/run-ops come from core so this +// tracks any future change. const NANOID_BODY_LENGTH = 21; +const LEGACY_BASE62_BODY_LENGTH = 27; const VALID_BODY_LENGTHS: ReadonlySet = new Set([ NANOID_BODY_LENGTH, CUID_LENGTH, - KSUID_LENGTH, + RUN_OPS_ID_LENGTH, + LEGACY_BASE62_BODY_LENGTH, ]); -const BASE62 = /^[0-9A-Za-z]+$/; +const ALPHANUMERIC = /^[0-9A-Za-z]+$/; export function isValidFriendlyId(value: string, prefix: string): boolean { const marker = `${prefix}_`; if (!value.startsWith(marker)) return false; const body = value.slice(marker.length); - return VALID_BODY_LENGTHS.has(body.length) && BASE62.test(body); + return VALID_BODY_LENGTHS.has(body.length) && ALPHANUMERIC.test(body); } export function makeFriendlyIdValidator(prefix: string, label: string) { diff --git a/apps/webapp/app/v3/featureFlags.ts b/apps/webapp/app/v3/featureFlags.ts index 4617179eda..637830aef0 100644 --- a/apps/webapp/app/v3/featureFlags.ts +++ b/apps/webapp/app/v3/featureFlags.ts @@ -18,7 +18,7 @@ export const FEATURE_FLAG = { computeMigrationPaidPercentage: "computeMigrationPaidPercentage", computeMigrationRequireTemplate: "computeMigrationRequireTemplate", devBranchesEnabled: "devBranchesEnabled", - runOpsMintKsuid: "runOpsMintKsuid", + runOpsMintKind: "runOpsMintKind", } as const; export const FeatureFlagCatalog = { @@ -51,9 +51,9 @@ export const FeatureFlagCatalog = { [FEATURE_FLAG.computeMigrationRequireTemplate]: z.boolean(), // Per-org access to development branches. Off unless enabled for the org. [FEATURE_FLAG.devBranchesEnabled]: z.coerce.boolean(), - // Per-org KSUID mint cutover. Defaults to "cuid"; only honored when - // RUN_OPS_MINT_KSUID_ENABLED is on AND isSplitEnabled() is true. - [FEATURE_FLAG.runOpsMintKsuid]: z.enum(["cuid", "ksuid"]), + // Per-org run-ops-id mint cutover. Defaults to "cuid"; only honored when + // RUN_OPS_MINT_ENABLED is on AND isSplitEnabled() is true. + [FEATURE_FLAG.runOpsMintKind]: z.enum(["cuid", "runOpsId"]), }; export type FeatureFlagKey = keyof typeof FeatureFlagCatalog; diff --git a/apps/webapp/app/v3/runEngineHandlersShared.server.ts b/apps/webapp/app/v3/runEngineHandlersShared.server.ts index 338bbd2f9f..3493a52ad2 100644 --- a/apps/webapp/app/v3/runEngineHandlersShared.server.ts +++ b/apps/webapp/app/v3/runEngineHandlersShared.server.ts @@ -71,7 +71,7 @@ export async function readRunForEventOrThrow( * Resolve which run-ops writer physically owns the `BatchTaskRun` row for * `batchId` by probing where the row lives, so the batch-completion txn commits * on a single run-ops DB. Length classification is INVALID here: a batch id may - * be a ksuid (cut-over orgs) or a cuid (and cuid-shaped ids can be backfilled + * be a run-ops id (cut-over orgs) or a cuid (and cuid-shaped ids can be backfilled * onto NEW), so id-shape does not reliably indicate the row's actual residency. * The existence probe is the correct signal. */ diff --git a/apps/webapp/app/v3/runOpsMigration/crossSeamGuard.server.ts b/apps/webapp/app/v3/runOpsMigration/crossSeamGuard.server.ts index 791a101e9e..ee836f57eb 100644 --- a/apps/webapp/app/v3/runOpsMigration/crossSeamGuard.server.ts +++ b/apps/webapp/app/v3/runOpsMigration/crossSeamGuard.server.ts @@ -56,7 +56,6 @@ export function selectStoreForWaitpoint( const classify = deps?.classify ?? ownerEngine; - // Loud on ambiguity: classify throws UnclassifiableRunId with the real id; never catch-and-default. const residency: RunOpsResidency = classify(input.waitpointId); const pinnedReason = applyPinningRules(input); diff --git a/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.test.ts b/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.test.ts index b552b4de73..9973be57d1 100644 --- a/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.test.ts +++ b/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.test.ts @@ -3,12 +3,12 @@ import { batchIdForMintKind, resolveBatchMintKind } from "./mintBatchFriendlyId. import { classifyKind } from "@trigger.dev/core/v3/isomorphic"; describe("batchIdForMintKind (pure)", () => { - it("ksuid -> 27-char classifiable NEW batch id (no 21-char ids)", () => { - const r = batchIdForMintKind("ksuid"); + it("'runOpsId' kind -> 26-char classifiable NEW batch id (no 21-char ids)", () => { + const r = batchIdForMintKind("runOpsId"); expect(r.friendlyId.startsWith("batch_")).toBe(true); - expect(r.id.length).toBe(27); - expect(classifyKind(r.id)).toBe("ksuid"); - expect(classifyKind(r.friendlyId)).toBe("ksuid"); + expect(r.id.length).toBe(26); + expect(classifyKind(r.id)).toBe("runOpsId"); + expect(classifyKind(r.friendlyId)).toBe("runOpsId"); }); it("cuid -> 25-char classifiable LEGACY batch id", () => { @@ -19,8 +19,8 @@ describe("batchIdForMintKind (pure)", () => { }); it("never mints a 21-char id", () => { - for (const kind of ["cuid", "ksuid"] as const) { - expect([25, 27]).toContain(batchIdForMintKind(kind).id.length); + for (const kind of ["cuid", "runOpsId"] as const) { + expect([25, 26]).toContain(batchIdForMintKind(kind).id.length); } }); }); @@ -29,12 +29,12 @@ describe("resolveBatchMintKind", () => { const environment = { organizationId: "org_1", id: "env_1", orgFeatureFlags: {} }; it("ROOT batch (no parent) resolves per-org kind via resolveRunIdMintKind", async () => { - const resolveRunIdMintKind = vi.fn().mockResolvedValue("ksuid"); + const resolveRunIdMintKind = vi.fn().mockResolvedValue("runOpsId"); const kind = await resolveBatchMintKind({ environment, deps: { resolveRunIdMintKind }, }); - expect(kind).toBe("ksuid"); + expect(kind).toBe("runOpsId"); expect(resolveRunIdMintKind).toHaveBeenCalledWith({ organizationId: "org_1", id: "env_1", @@ -51,8 +51,8 @@ describe("resolveBatchMintKind", () => { expect(kind).toBe("cuid"); }); - it("CHILD batch inherits a ksuid (NEW) parent by id-shape", async () => { - const parentRunFriendlyId = `run_${"a".repeat(27)}`; + it("CHILD batch inherits a run-ops (NEW) parent by id-shape", async () => { + const parentRunFriendlyId = `run_${"a".repeat(24) + "01"}`; const resolveRunIdMintKind = vi.fn(); const kind = await resolveBatchMintKind({ @@ -61,7 +61,7 @@ describe("resolveBatchMintKind", () => { deps: { resolveRunIdMintKind }, }); - expect(kind).toBe("ksuid"); + expect(kind).toBe("runOpsId"); expect(resolveRunIdMintKind).not.toHaveBeenCalled(); }); @@ -81,9 +81,9 @@ describe("resolveBatchMintKind", () => { // mint-on-FLIP invariant: a child follows its parent's store even after the org flag // flips the other way. The flag resolver must NEVER be consulted for a child. - it("FLIP cuid->ksuid: a cuid (LEGACY) parent still mints a cuid child though the flag now says ksuid", async () => { + it("FLIP 'cuid'->'runOpsId': a cuid (LEGACY) parent still mints a cuid child though the flag now says 'runOpsId'", async () => { const parentRunFriendlyId = `run_${"a".repeat(25)}`; - const resolveRunIdMintKind = vi.fn().mockResolvedValue("ksuid"); // flag flipped to ksuid + const resolveRunIdMintKind = vi.fn().mockResolvedValue("runOpsId"); // flag flipped to runOpsId const kind = await resolveBatchMintKind({ environment, parentRunFriendlyId, @@ -93,15 +93,15 @@ describe("resolveBatchMintKind", () => { expect(resolveRunIdMintKind).not.toHaveBeenCalled(); }); - it("FLIP ksuid->cuid: a ksuid (NEW) parent still mints a ksuid child though the flag now says cuid", async () => { - const parentRunFriendlyId = `run_${"a".repeat(27)}`; + it("FLIP 'runOpsId'->'cuid': a run-ops (NEW) parent still mints a run-ops child though the flag now says 'cuid'", async () => { + const parentRunFriendlyId = `run_${"a".repeat(24) + "01"}`; const resolveRunIdMintKind = vi.fn().mockResolvedValue("cuid"); // flag flipped back to cuid const kind = await resolveBatchMintKind({ environment, parentRunFriendlyId, deps: { resolveRunIdMintKind }, }); - expect(kind).toBe("ksuid"); + expect(kind).toBe("runOpsId"); expect(resolveRunIdMintKind).not.toHaveBeenCalled(); }); }); diff --git a/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.ts b/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.ts index 0503fc5b2c..e2d8511e3f 100644 --- a/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.ts +++ b/apps/webapp/app/v3/runOpsMigration/mintBatchFriendlyId.server.ts @@ -1,4 +1,4 @@ -import { BatchId, generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { BatchId, generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { resolveRunIdMintKind as defaultResolveRunIdMintKind, type RunIdMintKind, @@ -14,8 +14,8 @@ const defaultDeps: ResolveDeps = { }; export function batchIdForMintKind(kind: RunIdMintKind): { id: string; friendlyId: string } { - if (kind === "ksuid") { - const id = generateKsuidId(); + if (kind === "runOpsId") { + const id = generateRunOpsId(); return { id, friendlyId: BatchId.toFriendlyId(id) }; } return BatchId.generate(); diff --git a/apps/webapp/app/v3/runOpsMigration/readThrough.server.test.ts b/apps/webapp/app/v3/runOpsMigration/readThrough.server.test.ts index 1fe52189c8..f7f7c43a53 100644 --- a/apps/webapp/app/v3/runOpsMigration/readThrough.server.test.ts +++ b/apps/webapp/app/v3/runOpsMigration/readThrough.server.test.ts @@ -10,9 +10,9 @@ import { readThroughRun, type ReadThroughResult } from "./readThrough.server"; vi.setConfig({ testTimeout: 60_000 }); -// 25-char cuid body β†’ LEGACY residency. 27-char body β†’ NEW residency. +// 25-char cuid body β†’ LEGACY residency. 26-char v1 body (version "1" at index 25) β†’ NEW residency. const LEGACY_RUN_ID = "run_" + "a".repeat(25); -const NEW_RUN_ID = "run_" + "b".repeat(27); +const NEW_RUN_ID = "run_" + "b".repeat(24) + "01"; // Lightweight real read: a trivial `$queryRaw` that genuinely hits the given container. // `hit` controls whether the read "finds" the run, so we exercise routing without diff --git a/apps/webapp/app/v3/runOpsMigration/readThrough.server.ts b/apps/webapp/app/v3/runOpsMigration/readThrough.server.ts index 8b83b70d78..d173f3958b 100644 --- a/apps/webapp/app/v3/runOpsMigration/readThrough.server.ts +++ b/apps/webapp/app/v3/runOpsMigration/readThrough.server.ts @@ -4,7 +4,7 @@ * is false (single-DB passthrough). * * During the retention window, old run-ops rows are served off the legacy read replica. - * Residency is decided purely by id-shape: a ksuid (NEW) id reads new only, a cuid + * Residency is decided purely by id-shape: a run-ops id (NEW) id reads new only, a cuid * (LEGACY) id reads legacy only. An unclassifiable id falls back to a new-then-legacy * probe. After termination, past-retention runs return the normal not-found response. * Patterned on `mollifier/resolveRunForMutation.server.ts` (`?? default` DI), but with @@ -80,7 +80,7 @@ export async function readThroughRun( } } - // A ksuid id can only live on the new DB β€” skip the legacy replica entirely. + // A run-ops id can only live on the new DB β€” skip the legacy replica entirely. if (residency === "NEW") { const v = await input.readNew(newClient); return v != null ? { source: "new", value: v } : { source: "not-found" }; diff --git a/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.test.ts b/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.test.ts index 74baee1bb6..3f135793f8 100644 --- a/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.test.ts +++ b/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.test.ts @@ -1,12 +1,12 @@ import { describe, expect, it } from "vitest"; import { resolveInheritedMintKind } from "./resolveInheritedMintKind.server"; -const NEW_PARENT = `run_${"a".repeat(27)}`; // ksuid id-shape -> NEW +const NEW_PARENT = `run_${"a".repeat(24) + "01"}`; // run-ops id-shape -> NEW const LEGACY_PARENT = `run_${"b".repeat(25)}`; // cuid id-shape -> LEGACY describe("resolveInheritedMintKind (pure id-shape, shared across all mint paths)", () => { - it("inherits a ksuid (NEW) parent by id-shape -> ksuid", () => { - expect(resolveInheritedMintKind(NEW_PARENT)).toBe("ksuid"); + it("inherits a run-ops (NEW) parent by id-shape -> 'runOpsId' kind", () => { + expect(resolveInheritedMintKind(NEW_PARENT)).toBe("runOpsId"); }); it("inherits a cuid (LEGACY) parent by id-shape -> cuid", () => { diff --git a/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.ts b/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.ts index e43c3a8e33..6ec9583c94 100644 --- a/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.ts +++ b/apps/webapp/app/v3/runOpsMigration/resolveInheritedMintKind.server.ts @@ -4,7 +4,7 @@ import type { RunIdMintKind } from "./runOpsMintKind.server"; // Mint a child in the SAME physical store as its anchor (parent run / owning batch), // regardless of the org's current mint flag β€” keeps a subgraph co-resident across a // flip. With no migration/drain, residency is a pure id-shape check (zero hot-path -// I/O): a ksuid (NEW) parent mints ksuid children, a cuid (LEGACY) parent mints cuid. +// I/O): a run-ops (NEW) parent mints run-ops children, a cuid (LEGACY) parent mints cuid. export function resolveInheritedMintKind(parentRunFriendlyId: string): RunIdMintKind { - return ownerEngine(parentRunFriendlyId) === "NEW" ? "ksuid" : "cuid"; + return ownerEngine(parentRunFriendlyId) === "NEW" ? "runOpsId" : "cuid"; } diff --git a/apps/webapp/app/v3/runOpsMigration/runOpsCascadeCleanup.server.ts b/apps/webapp/app/v3/runOpsMigration/runOpsCascadeCleanup.server.ts index 2392d51618..6d08350f62 100644 --- a/apps/webapp/app/v3/runOpsMigration/runOpsCascadeCleanup.server.ts +++ b/apps/webapp/app/v3/runOpsMigration/runOpsCascadeCleanup.server.ts @@ -73,7 +73,7 @@ void _legacyWriterAssignable; * * Deletes route through the dedicated run-ops write clients (`runOpsNewPrismaClient` + * `runOpsLegacyPrisma`), NOT the control-plane `prisma`. The ordered delete pass runs against BOTH - * writers: a migrating env/project's run-ops rows split across the new (KSUID) and + * writers: a migrating env/project's run-ops rows split across the new (run-ops id) and * legacy (cuid) DBs per the per-env cutover + roll-new-forward rollback, and the * cloud DB that lost its physical FK has no cascade to clean the other writer's miss. In single-DB * both handles are reference-equal to the one collapsed client, so de-dup-by-reference runs the diff --git a/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.flipLatency.test.ts b/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.flipLatency.test.ts index fc346dad89..0465c8d1c8 100644 --- a/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.flipLatency.test.ts +++ b/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.flipLatency.test.ts @@ -31,7 +31,7 @@ describe("computeRunIdMintKind flip latency (mintCache TTL window β€” current be beforeEach(() => vi.useFakeTimers()); afterEach(() => vi.useRealTimers()); - it("returns the STALE cached kind within the TTL after the flag flips cuid->ksuid", async () => { + it("returns the STALE cached kind within the TTL after the flag flips 'cuid'->'runOpsId'", async () => { const cache = new BoundedTtlCache(TTL_MS, 100); let live: RunIdMintKind = "cuid"; const flag = makeCachedFlag(cache, () => live); @@ -39,12 +39,12 @@ describe("computeRunIdMintKind flip latency (mintCache TTL window β€” current be expect(await computeRunIdMintKind(env, deps)).toBe("cuid"); // populates the cache - live = "ksuid"; // admin flips the org flag + live = "runOpsId"; // admin flips the org flag vi.advanceTimersByTime(TTL_MS - 1); // still inside the window expect(await computeRunIdMintKind(env, deps)).toBe("cuid"); // STALE, as designed }); - it("returns the FRESH kind once the TTL expires after a cuid->ksuid flip", async () => { + it("returns the FRESH kind once the TTL expires after a 'cuid'->'runOpsId' flip", async () => { const cache = new BoundedTtlCache(TTL_MS, 100); let live: RunIdMintKind = "cuid"; const flag = makeCachedFlag(cache, () => live); @@ -52,22 +52,22 @@ describe("computeRunIdMintKind flip latency (mintCache TTL window β€” current be expect(await computeRunIdMintKind(env, deps)).toBe("cuid"); - live = "ksuid"; + live = "runOpsId"; vi.advanceTimersByTime(TTL_MS + 1); // past expiry -> entry evicted on read - expect(await computeRunIdMintKind(env, deps)).toBe("ksuid"); // re-reads the live flag + expect(await computeRunIdMintKind(env, deps)).toBe("runOpsId"); // re-reads the live flag }); - it("symmetric flip-back ksuid->cuid is also stale within TTL, fresh after", async () => { + it("symmetric flip-back 'runOpsId'->'cuid' is also stale within TTL, fresh after", async () => { const cache = new BoundedTtlCache(TTL_MS, 100); - let live: RunIdMintKind = "ksuid"; + let live: RunIdMintKind = "runOpsId"; const flag = makeCachedFlag(cache, () => live); const deps = { masterEnabled: true, splitEnabled: async () => true, flag }; - expect(await computeRunIdMintKind(env, deps)).toBe("ksuid"); + expect(await computeRunIdMintKind(env, deps)).toBe("runOpsId"); live = "cuid"; vi.advanceTimersByTime(TTL_MS - 1); - expect(await computeRunIdMintKind(env, deps)).toBe("ksuid"); // STALE + expect(await computeRunIdMintKind(env, deps)).toBe("runOpsId"); // STALE vi.advanceTimersByTime(2); // now past expiry expect(await computeRunIdMintKind(env, deps)).toBe("cuid"); // FRESH diff --git a/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.test.ts b/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.test.ts index 9d2e575fef..bdc8692f3b 100644 --- a/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.test.ts +++ b/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.test.ts @@ -12,8 +12,8 @@ describe("computeRunIdMintKind (pure)", () => { expect(flag).not.toHaveBeenCalled(); }); - it("mints cuid when split is OFF, even if master + per-org flag say ksuid", async () => { - const flag = vi.fn().mockResolvedValue("ksuid"); + it("mints cuid when split is OFF, even if master + per-org flag say 'runOpsId'", async () => { + const flag = vi.fn().mockResolvedValue("runOpsId"); const kind = await computeRunIdMintKind( { organizationId: "org_1", id: "env_1" }, { masterEnabled: true, splitEnabled: async () => false, flag } @@ -22,18 +22,18 @@ describe("computeRunIdMintKind (pure)", () => { expect(flag).not.toHaveBeenCalled(); // split-off short-circuits before any flag read }); - it("mints ksuid only when master on AND split on AND per-org flag = ksuid", async () => { - const flag = vi.fn().mockResolvedValue("ksuid"); + it("mints run-ops id only when master on AND split on AND per-org flag = 'runOpsId'", async () => { + const flag = vi.fn().mockResolvedValue("runOpsId"); const kind = await computeRunIdMintKind( { organizationId: "org_1", id: "env_1" }, { masterEnabled: true, splitEnabled: async () => true, flag } ); - expect(kind).toBe("ksuid"); + expect(kind).toBe("runOpsId"); }); it("passes the already-loaded org feature flags through to the flag fn (no extra DB read)", async () => { - const flag = vi.fn().mockResolvedValue("ksuid"); - const orgFeatureFlags = { runOpsMintKsuid: "ksuid" }; + const flag = vi.fn().mockResolvedValue("runOpsId"); + const orgFeatureFlags = { runOpsMintKind: "runOpsId" }; await computeRunIdMintKind( { organizationId: "org_1", id: "env_1", orgFeatureFlags }, { masterEnabled: true, splitEnabled: async () => true, flag } diff --git a/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.ts b/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.ts index c3751c993c..305b851bdc 100644 --- a/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.ts +++ b/apps/webapp/app/v3/runOpsMigration/runOpsMintKind.server.ts @@ -7,7 +7,7 @@ import { FEATURE_FLAG } from "~/v3/featureFlags"; import { makeFlag } from "~/v3/featureFlags.server"; import { isSplitEnabled } from "./splitMode.server"; -export type RunIdMintKind = "cuid" | "ksuid"; +export type RunIdMintKind = "cuid" | "runOpsId"; type MintKindDeps = { masterEnabled: boolean; @@ -51,10 +51,10 @@ export async function resolveRunIdMintKind(environment: { orgFeatureFlags?: unknown; }): Promise { return computeRunIdMintKind(environment, { - masterEnabled: env.RUN_OPS_MINT_KSUID_ENABLED, + masterEnabled: env.RUN_OPS_MINT_ENABLED, splitEnabled: isSplitEnabled, flag: async (orgId, orgFeatureFlags) => { - // The cache stores only "cuid"|"ksuid" (never undefined), so the cache's + // The cache stores only "cuid"|"runOpsId" (never undefined), so the cache's // "stored-undefined == miss" caveat never applies here. const cached = mintCache.get(orgId); if (cached !== undefined) return cached; @@ -73,7 +73,7 @@ export async function resolveRunIdMintKind(environment: { )?.featureFlags; const kind = await flagFn({ - key: FEATURE_FLAG.runOpsMintKsuid, + key: FEATURE_FLAG.runOpsMintKind, defaultValue: "cuid", overrides: (overrides as Record) ?? {}, }); diff --git a/apps/webapp/app/v3/runOpsMigration/splitMode.server.ts b/apps/webapp/app/v3/runOpsMigration/splitMode.server.ts index c1bf592e4a..955bd90b94 100644 --- a/apps/webapp/app/v3/runOpsMigration/splitMode.server.ts +++ b/apps/webapp/app/v3/runOpsMigration/splitMode.server.ts @@ -47,7 +47,7 @@ export type SplitRealtimeInterlockConfig = { /** * Boot-time realtime interlock (pure predicate). Split mode puts NEW-resident - * (ksuid) runs on the dedicated run-ops DB, but Electric replicates only from the + * (run-ops id) runs on the dedicated run-ops DB, but Electric replicates only from the * control-plane DB β€” with the native realtime backend OFF those runs are invisible * and every realtime subscription hangs. Refuse split unless native is on; split-off * is always allowed regardless of the realtime backend. diff --git a/apps/webapp/app/v3/runStore.server.test.ts b/apps/webapp/app/v3/runStore.server.test.ts index 065d8dbbc2..3f01ceeed0 100644 --- a/apps/webapp/app/v3/runStore.server.test.ts +++ b/apps/webapp/app/v3/runStore.server.test.ts @@ -6,9 +6,9 @@ import { buildRunStore } from "./runStore.server"; vi.setConfig({ testTimeout: 60_000 }); -// 25-char internal id -> cuid -> LEGACY; 27-char internal id -> ksuid -> NEW. +// 25-char internal id -> cuid -> LEGACY; v1 body (version "1" at index 25) -> NEW. const CUID_25 = "c".repeat(25); -const KSUID_27 = "k".repeat(27); +const NEW_ID_26 = "k".repeat(24) + "01"; async function seedEnvironment(prisma: PrismaClient, slugSuffix: string) { const organization = await prisma.organization.create({ @@ -80,18 +80,18 @@ function createRunInput(params: { }; } -describe("T24 β€” findRun resolves ksuid run on dedicated DB", () => { +describe("T24 β€” findRun resolves run-ops run on dedicated DB", () => { heteroRunOpsPostgresTest( - "split ON: findRun({friendlyId, runtimeEnvironmentId}, {select}) finds a ksuid run on the new store", + "split ON: findRun({friendlyId, runtimeEnvironmentId}, {select}) finds a run-ops run on the new store", async ({ prisma14, prisma17 }) => { - const ENV_ID = "env_t24_ksuid_probe"; + const ENV_ID = "env_t24_runops_probe"; const WORKER_ID = "worker_t24_lock"; await prisma17.taskRun.create({ data: { - id: KSUID_27, + id: NEW_ID_26, engine: "V2", status: "EXECUTING", - friendlyId: "run_t24_ksuid", + friendlyId: `run_${NEW_ID_26}`, runtimeEnvironmentId: ENV_ID, environmentType: "DEVELOPMENT", organizationId: "org_t24", @@ -118,13 +118,13 @@ describe("T24 β€” findRun resolves ksuid run on dedicated DB", () => { }); const run = await store.findRun( - { friendlyId: "run_t24_ksuid", runtimeEnvironmentId: ENV_ID }, + { friendlyId: `run_${NEW_ID_26}`, runtimeEnvironmentId: ENV_ID }, { select: { lockedToVersionId: true } } ); expect(run).not.toBeNull(); expect(run?.lockedToVersionId).toBe(WORKER_ID); - expect(await prisma14.taskRun.findUnique({ where: { id: KSUID_27 } })).toBeNull(); + expect(await prisma14.taskRun.findUnique({ where: { id: NEW_ID_26 } })).toBeNull(); } ); }); @@ -147,8 +147,8 @@ describe("buildRunStore", () => { expect(store).toBeInstanceOf(PostgresRunStore); const seed = await seedEnvironment(prisma14, "off"); - // A ksuid id (would route to NEW under split) must still land on the single DB. - const runId = KSUID_27; + // A run-ops id (would route to NEW under split) must still land on the single DB. + const runId = NEW_ID_26; await store.createRun( createRunInput({ runId, @@ -183,18 +183,18 @@ describe("buildRunStore", () => { const seedNew = await seedEnvironment(prisma17, "on_new"); const seedLegacy = await seedEnvironment(prisma14, "on_legacy"); - // ksuid -> NEW (PG17) + // run-ops id -> NEW (PG17) await store.createRun( createRunInput({ - runId: KSUID_27, + runId: NEW_ID_26, friendlyId: "run_new", organizationId: seedNew.organization.id, projectId: seedNew.project.id, runtimeEnvironmentId: seedNew.environment.id, }) ); - expect(await prisma17.taskRun.findUnique({ where: { id: KSUID_27 } })).not.toBeNull(); - expect(await prisma14.taskRun.findUnique({ where: { id: KSUID_27 } })).toBeNull(); + expect(await prisma17.taskRun.findUnique({ where: { id: NEW_ID_26 } })).not.toBeNull(); + expect(await prisma14.taskRun.findUnique({ where: { id: NEW_ID_26 } })).toBeNull(); // cuid -> LEGACY (PG14) await store.createRun( diff --git a/apps/webapp/app/v3/services/batchTriggerV3.server.ts b/apps/webapp/app/v3/services/batchTriggerV3.server.ts index 6277877896..bee0504a3d 100644 --- a/apps/webapp/app/v3/services/batchTriggerV3.server.ts +++ b/apps/webapp/app/v3/services/batchTriggerV3.server.ts @@ -12,7 +12,7 @@ import { Prisma, } from "@trigger.dev/database"; import type { RunStore } from "@internal/run-store"; -import { generateKsuidId, RunId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId, RunId } from "@trigger.dev/core/v3/isomorphic"; import { z } from "zod"; import type { PrismaClientOrTransaction } from "~/db.server"; import { prisma } from "~/db.server"; @@ -344,13 +344,14 @@ export class BatchTriggerV3Service extends BaseService { } // Mint a child run's friendlyId so it lands in the SAME physical store as its - // residency anchor. The caller passes the batch's friendlyId, so a ksuid - // (NEW) anchor yields a ksuid (NEW) child and a cuid anchor yields a cuid + // residency anchor. The caller passes the batch's friendlyId, so a run-ops id + // (NEW) anchor yields a run-ops id (NEW) child and a cuid anchor yields a cuid // (LEGACY) child. With no anchor it falls back to the env's cutover setting. // Mirrors RunEngineTriggerTaskService.mintRunFriendlyId. private async mintChildFriendlyId( environment: AuthenticatedEnvironment, - anchorFriendlyId?: string + anchorFriendlyId?: string, + region?: string ): Promise { const mintKind = anchorFriendlyId ? resolveInheritedMintKind(anchorFriendlyId) @@ -360,8 +361,8 @@ export class BatchTriggerV3Service extends BaseService { orgFeatureFlags: environment.organization.featureFlags, }); - return mintKind === "ksuid" - ? RunId.toFriendlyId(generateKsuidId()) + return mintKind === "runOpsId" + ? RunId.toFriendlyId(generateRunOpsId(region)) : RunId.generate().friendlyId; } @@ -379,7 +380,7 @@ export class BatchTriggerV3Service extends BaseService { if (body?.dependentAttempt) { return Promise.all( body.items.map(async (item) => ({ - id: await this.mintChildFriendlyId(environment, childAnchor), + id: await this.mintChildFriendlyId(environment, childAnchor, item.options?.region), isCached: false, idempotencyKey: undefined, taskIdentifier: item.task, @@ -441,7 +442,7 @@ export class BatchTriggerV3Service extends BaseService { expiredRunIds.add(cachedRun.friendlyId); return { - id: await this.mintChildFriendlyId(environment, childAnchor), + id: await this.mintChildFriendlyId(environment, childAnchor, item.options?.region), isCached: false, idempotencyKey: item.options?.idempotencyKey ?? undefined, taskIdentifier: item.task, @@ -457,7 +458,7 @@ export class BatchTriggerV3Service extends BaseService { } return { - id: await this.mintChildFriendlyId(environment, childAnchor), + id: await this.mintChildFriendlyId(environment, childAnchor, item.options?.region), isCached: false, idempotencyKey: item.options?.idempotencyKey ?? undefined, taskIdentifier: item.task, @@ -1001,7 +1002,7 @@ export async function tryCompleteBatchV3( batchId: string, tx: PrismaClientOrTransaction, scheduleResumeOnComplete: boolean, - // Threaded in so a ksuid (NEW-resident) batch + its items are read/written on the owning + // Threaded in so a run-ops id (NEW-resident) batch + its items are read/written on the owning // store, not the control-plane `tx`. Defaults to the singleton (single-DB = passthrough). runStore: RunStore = defaultRunStore ) { @@ -1061,7 +1062,7 @@ export async function completeBatchTaskRunItemV3( scheduleResumeOnComplete = false, taskRunAttemptId?: string, retryAttempt?: number, - // Threaded in so a ksuid (NEW-resident) batch's item lands on the owning store; route by + // Threaded in so a run-ops id (NEW-resident) batch's item lands on the owning store; route by // batchTaskRunId (items co-reside with their batch). Defaults to the singleton. runStore: RunStore = defaultRunStore ) { diff --git a/apps/webapp/app/v3/services/bulk/BulkActionV2.batchReadThrough.server.test.ts b/apps/webapp/app/v3/services/bulk/BulkActionV2.batchReadThrough.server.test.ts index df6b74753e..99d4cfd2dd 100644 --- a/apps/webapp/app/v3/services/bulk/BulkActionV2.batchReadThrough.server.test.ts +++ b/apps/webapp/app/v3/services/bulk/BulkActionV2.batchReadThrough.server.test.ts @@ -10,9 +10,9 @@ import { hydrateRunsAcrossSeam } from "./BulkActionV2.batchReadThrough.server"; vi.setConfig({ testTimeout: 60_000 }); -// 25-char cuid body β†’ LEGACY residency. 27-char body β†’ NEW residency. +// 25-char cuid body β†’ LEGACY residency. 26-char v1 body (version "1" at index 25) β†’ NEW residency. const LEGACY_RUN_ID = "run_" + "a".repeat(25); -const NEW_RUN_ID = "run_" + "b".repeat(27); +const NEW_RUN_ID = "run_" + "b".repeat(24) + "01"; type Row = { id: string }; diff --git a/apps/webapp/app/v3/services/createCheckpoint.server.ts b/apps/webapp/app/v3/services/createCheckpoint.server.ts index 43ff25f205..088001b826 100644 --- a/apps/webapp/app/v3/services/createCheckpoint.server.ts +++ b/apps/webapp/app/v3/services/createCheckpoint.server.ts @@ -146,7 +146,7 @@ export class CreateCheckpointService extends BaseService { break; } case "WAIT_FOR_BATCH": { - // Routed by friendlyId so a ksuid (NEW-resident) batch is found on the owning DB; + // Routed by friendlyId so a run-ops id (NEW-resident) batch is found on the owning DB; // env-scoped to the dependent attempt's run (a batch shares its dependent's env). const batchRun = await this.runStore.findBatchTaskRunByFriendlyId( reason.batchFriendlyId, @@ -361,7 +361,7 @@ export class CreateCheckpointService extends BaseService { }); await marqs?.cancelHeartbeat(attempt.taskRunId); - // Routed by friendlyId so a ksuid (NEW-resident) batch is found on the owning DB; + // Routed by friendlyId so a run-ops id (NEW-resident) batch is found on the owning DB; // env-scoped to the dependent attempt's run (a batch shares its dependent's env). const batchRun = await this.runStore.findBatchTaskRunByFriendlyId( reason.batchFriendlyId, diff --git a/apps/webapp/app/v3/services/executeTasksWaitingForDeploy.ts b/apps/webapp/app/v3/services/executeTasksWaitingForDeploy.ts index f8d6dcc655..4a123ff777 100644 --- a/apps/webapp/app/v3/services/executeTasksWaitingForDeploy.ts +++ b/apps/webapp/app/v3/services/executeTasksWaitingForDeploy.ts @@ -1,4 +1,4 @@ -import { isClassifiable, ownerEngine } from "@trigger.dev/core/v3/isomorphic"; +import { ownerEngine } from "@trigger.dev/core/v3/isomorphic"; import { env } from "~/env.server"; import { logger } from "~/services/logger.server"; import { marqs } from "~/v3/marqs/index.server"; @@ -72,13 +72,11 @@ export class ExecuteTasksWaitingForDeployService extends BaseService { // Defense-in-depth: the open-predicate findRuns fan-out can select runs from // either DB, but the status flip below is a single control-plane updateMany. A - // ksuid (NEW-resident) run can only reach WAITING_FOR_DEPLOY via a misconfiguration + // run-ops id (NEW-resident) run can only reach WAITING_FOR_DEPLOY via a misconfiguration // (it is a V1/cuid-only status β€” V2 uses PENDING_VERSION). Surface it loudly rather // than silently strand the run, and only mutate the LEGACY-resident runs the // control-plane client can actually reach. - const newResidentRuns = runsWaitingForDeploy.filter( - (run) => isClassifiable(run.id) && ownerEngine(run.id) === "NEW" - ); + const newResidentRuns = runsWaitingForDeploy.filter((run) => ownerEngine(run.id) === "NEW"); if (newResidentRuns.length) { logger.error( "WAITING_FOR_DEPLOY selected NEW-resident runs; skipping their control-plane status flip", diff --git a/apps/webapp/test/SpanPresenter.readthrough.test.ts b/apps/webapp/test/SpanPresenter.readthrough.test.ts index 6cf625e74f..2e6054f042 100644 --- a/apps/webapp/test/SpanPresenter.readthrough.test.ts +++ b/apps/webapp/test/SpanPresenter.readthrough.test.ts @@ -32,10 +32,10 @@ import { SpanPresenter } from "~/presenters/v3/SpanPresenter.server"; vi.setConfig({ testTimeout: 90_000 }); -// 25-char internal id β†’ cuid β†’ LEGACY; 27-char internal id β†’ ksuid β†’ NEW (the residency +// 25-char internal id β†’ cuid β†’ LEGACY; v1 internal id (26 chars, version "1" at index 25) β†’ NEW (the residency // classifier shared with the RoutingRunStore's default `ownerEngine`). const CUID_25 = "c".repeat(25); -const KSUID_27 = "k".repeat(27); +const NEW_ID_26 = "k".repeat(24) + "01"; type SeedContext = { organizationId: string; @@ -222,11 +222,12 @@ describe("SpanPresenter run-ops/control-plane partition (legacy + new)", () => { const ctxNew = await seedParents(prisma17, "partn"); await mirrorParents(prisma14, ctxNew, "partn"); // legacy run-ops + CP parents share ids - const runId = `run_${KSUID_27}`; // ksuid β†’ NEW residency - const childMigratedId = `run_a${KSUID_27.slice(1)}`; // also NEW + const runId = `run_${NEW_ID_26}`; // run-ops id β†’ NEW residency + const childMigratedId = `run_a${NEW_ID_26.slice(1)}`; // also NEW + const parentFriendlyId = `run_p${NEW_ID_26.slice(1)}`; // v1 body β†’ routes NEW by friendlyId await createRun(prisma17, ctxNew, { id: runId, - friendlyId: "run_parent", + friendlyId: parentFriendlyId, spanId: "span_parent", taskIdentifier: "parent-task", }); @@ -273,12 +274,12 @@ describe("SpanPresenter run-ops/control-plane partition (legacy + new)", () => { // (a) run hydrated through the run-ops store (NEW), byte-identical to the source row incl. // the run-ops self-relations. const run = await presenter.findRun({ - originalRunId: "run_parent", + originalRunId: parentFriendlyId, spanId: "span_parent", environmentId: ctxNew.environmentId, }); expect(run?.id).toBe(runId); - expect(run?.friendlyId).toBe("run_parent"); + expect(run?.friendlyId).toBe(parentFriendlyId); expect(run?.taskIdentifier).toBe("parent-task"); expect(run?.runTags).toEqual(["alpha", "beta"]); // Nested run-ops self-relation resolved on the same (NEW) store. @@ -286,7 +287,7 @@ describe("SpanPresenter run-ops/control-plane partition (legacy + new)", () => { // (b) the run does NOT exist on the CP DB β€” the run-ops read could only have come from the // run-ops store, never a CP join. - expect(await cp.taskRun.findFirst({ where: { friendlyId: "run_parent" } })).toBeNull(); + expect(await cp.taskRun.findFirst({ where: { friendlyId: parentFriendlyId } })).toBeNull(); // (c) the control-plane standalone reads resolve from the CP client. const region = await cp.workerInstanceGroup.findFirst({ where: { masterQueue: "main" } }); @@ -305,18 +306,18 @@ describe("SpanPresenter run-ops/control-plane partition (legacy + new)", () => { const ctx = await seedParents(prisma17, "kids"); await createRun(prisma17, ctx, { - id: `run_${KSUID_27}`, + id: `run_${NEW_ID_26}`, friendlyId: "run_parent2", spanId: "span_p2", }); await createRun(prisma17, ctx, { - id: `run_b${KSUID_27.slice(1)}`, + id: `run_b${NEW_ID_26.slice(1)}`, friendlyId: "run_kid_a", spanId: "span_kid_a", parentSpanId: "span_p2", }); await createRun(prisma17, ctx, { - id: `run_c${KSUID_27.slice(1)}`, + id: `run_c${NEW_ID_26.slice(1)}`, friendlyId: "run_kid_b", spanId: "span_kid_b", parentSpanId: "span_p2", @@ -384,7 +385,7 @@ describe("SpanPresenter run-ops/control-plane partition (legacy + new)", () => { async ({ prisma14, prisma17 }) => { const ctx = await seedParents(prisma17, "knownmig"); - const newRunId = `run_${KSUID_27}`; // ksuid β†’ NEW residency + const newRunId = `run_${NEW_ID_26}`; // run-ops id β†’ NEW residency await createRun(prisma17, ctx, { id: newRunId, friendlyId: "run_known_new", @@ -425,7 +426,7 @@ describe("SpanPresenter run-ops/control-plane partition (legacy + new)", () => { const cp = prisma14; const ctx = await seedParents(prisma14, "passthru"); - const runId = `run_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; await createRun(prisma14, ctx, { id: runId, friendlyId: "run_solo", @@ -433,7 +434,7 @@ describe("SpanPresenter run-ops/control-plane partition (legacy + new)", () => { taskIdentifier: "solo-task", }); await createRun(prisma14, ctx, { - id: `run_d${KSUID_27.slice(1)}`, + id: `run_d${NEW_ID_26.slice(1)}`, friendlyId: "run_solo_kid", spanId: "span_solo_kid", parentSpanId: "span_solo", @@ -493,7 +494,7 @@ describe("SpanPresenter run-ops/control-plane partition (legacy + new)", () => { await mirrorParents(prisma17, ctx, "e2e4"); const parentId = `run_${CUID_25}`; // cuid β†’ LEGACY (in-retention) - const childId = `run_${KSUID_27}`; // ksuid β†’ NEW (born-new) + const childId = `run_${NEW_ID_26}`; // run-ops id β†’ NEW (born-new) await createRun(prisma14, ctx, { id: parentId, @@ -535,7 +536,7 @@ describe("SpanPresenter run-ops/control-plane partition (legacy + new)", () => { // tree's FK self-relations stay single-DB. expect(parent?.rootTaskRun?.friendlyId).toBe("run_e2e_parent"); - // The child resolves from the NEW slot (routed by its ksuid id) and points back at the parent + // The child resolves from the NEW slot (routed by its run-ops id) and points back at the parent // span β€” the cross-the-line parent/child shape, with no cross-DB join. const child = await store.findRun( { id: childId }, diff --git a/apps/webapp/test/api.v1.waitpoints.tokens.test.ts b/apps/webapp/test/api.v1.waitpoints.tokens.test.ts index 0a05975c73..e5df494885 100644 --- a/apps/webapp/test/api.v1.waitpoints.tokens.test.ts +++ b/apps/webapp/test/api.v1.waitpoints.tokens.test.ts @@ -2,7 +2,7 @@ import { describe, expect, vi } from "vitest"; // Store-routed engine create/get seam + the residency-keyed id contract behind // the create route (not its HTTP action). A standalone MANUAL token is cuid β†’ -// LEGACY; NEW residency is reached only by co-locating the token with a ksuid run. +// LEGACY; NEW residency is reached only by co-locating the token with a run-ops run. import { RunEngine } from "@internal/run-engine"; import { setupAuthenticatedEnvironment } from "@internal/run-engine/tests"; @@ -18,7 +18,7 @@ import type { RunOpsPrismaClient } from "@internal/run-ops-database"; import { WaitpointId, RunId, - generateKsuidId, + generateRunOpsId, ownerEngine, CUID_LENGTH, } from "@trigger.dev/core/v3/isomorphic"; @@ -280,7 +280,7 @@ function buildCreateRunInput(params: { }; } -async function seedExecutingKsuidRun( +async function seedExecutingRunOpsRun( prisma14: PrismaClient, router: RoutingRunStore, runId: string, @@ -329,21 +329,21 @@ function makeRouter(prisma14: PrismaClient, prisma17: RunOpsPrismaClient) { return new RoutingRunStore({ new: newStore, legacy: legacyStore }); } -describe("waitpoint-token create engine seam β€” NEW residency via a ksuid run across the version boundary", () => { - // NEW residency comes from co-locating the token with a ksuid run; the token +describe("waitpoint-token create engine seam β€” NEW residency via a run-ops run across the version boundary", () => { + // NEW residency comes from co-locating the token with a run-ops run; the token // resolves only on its owning (#new) store across the PG14<->PG17 boundary, never #legacy. twoDbEngineTest( - "a ksuid run's token co-locates on #new and resolves only there, not on #legacy", + "a run-ops run's token co-locates on #new and resolves only there, not on #legacy", async ({ prisma14, prisma17, redisOptions }) => { const p14 = prisma14 as unknown as PrismaClient; const router = makeRouter(p14, prisma17); const engine = buildEngine({ prisma: prisma14, redisOptions, store: router }); try { - // A NEW-classified run id (explicit ksuid), mirroring the trigger-routing helper. - const runId = RunId.toFriendlyId(generateKsuidId()); + // A NEW-classified run id (explicit run-ops id), mirroring the trigger-routing helper. + const runId = RunId.toFriendlyId(generateRunOpsId()); expect(ownerEngine(runId)).toBe("NEW"); - const env = await seedExecutingKsuidRun(p14, router, runId, "wpnew"); + const env = await seedExecutingRunOpsRun(p14, router, runId, "wpnew"); const { waitpoint } = await engine.createManualWaitpoint({ runId, diff --git a/apps/webapp/test/apiBatchResultsPresenter.readroute.test.ts b/apps/webapp/test/apiBatchResultsPresenter.readroute.test.ts index 11c97dc270..cd1ac3210b 100644 --- a/apps/webapp/test/apiBatchResultsPresenter.readroute.test.ts +++ b/apps/webapp/test/apiBatchResultsPresenter.readroute.test.ts @@ -1,6 +1,6 @@ // Route-level regression for ApiBatchResultsPresenter: the /batches/:id/results route used to build // the presenter with no read-through deps, collapsing to a passthrough read off the control-plane -// replica only, which 404s a NEW-resident (ksuid) batch that lives on the dedicated run-ops DB. +// replica only, which 404s a NEW-resident (run-ops id) batch that lives on the dedicated run-ops DB. import { heteroPostgresTest } from "@internal/testcontainers"; import type { PrismaClient } from "@trigger.dev/database"; import { describe, expect, vi } from "vitest"; @@ -10,9 +10,9 @@ import { ApiBatchResultsPresenter } from "~/presenters/v3/ApiBatchResultsPresent vi.setConfig({ testTimeout: 60_000 }); -// 27-char body β†’ NEW residency (ksuid analog). 25-char body β†’ LEGACY residency (cuid analog). +// 26-char v1 body (version "1" at index 25) β†’ NEW residency. 25-char body β†’ LEGACY residency (cuid analog). function newRunId(c: string) { - return c.repeat(27); + return c.repeat(24) + "01"; } // A prisma handle that throws on any access β€” proves the split path never reads the passthrough diff --git a/apps/webapp/test/apiBatchResultsPresenter.readthrough.test.ts b/apps/webapp/test/apiBatchResultsPresenter.readthrough.test.ts index 4b0957767a..1efc3e1ed9 100644 --- a/apps/webapp/test/apiBatchResultsPresenter.readthrough.test.ts +++ b/apps/webapp/test/apiBatchResultsPresenter.readthrough.test.ts @@ -19,9 +19,9 @@ import { ApiBatchResultsPresenter } from "~/presenters/v3/ApiBatchResultsPresent vi.setConfig({ testTimeout: 60_000 }); -// 27-char body β†’ NEW residency (ksuid analog). 25-char body β†’ LEGACY residency (cuid analog). +// 26-char v1 body (version "1" at index 25) β†’ NEW residency. 25-char body β†’ LEGACY residency (cuid analog). function newRunId(c: string) { - return c.repeat(27); + return c.repeat(24) + "01"; } function legacyRunId(c: string) { return c.repeat(25); diff --git a/apps/webapp/test/apiRetrieveRunPresenter.readroute.test.ts b/apps/webapp/test/apiRetrieveRunPresenter.readroute.test.ts index 48e1bcc785..8d34782e7d 100644 --- a/apps/webapp/test/apiRetrieveRunPresenter.readroute.test.ts +++ b/apps/webapp/test/apiRetrieveRunPresenter.readroute.test.ts @@ -1,7 +1,7 @@ import { containerTest, heteroPostgresTest } from "@internal/testcontainers"; import { PostgresRunStore } from "@internal/run-store"; import type { Prisma, PrismaClient } from "@trigger.dev/database"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { beforeEach, describe, expect, vi } from "vitest"; // `resolveSchedule` reads the module-level `prisma` (control-plane handle). @@ -249,10 +249,10 @@ async function seedRunWithTree( suffix: string; } ) { - const parentId = generateKsuidId(); - const rootId = generateKsuidId(); - const runId = generateKsuidId(); - const childId = generateKsuidId(); + const parentId = generateRunOpsId(); + const rootId = generateRunOpsId(); + const runId = generateRunOpsId(); + const childId = generateRunOpsId(); await seedRun(prisma, { id: rootId, @@ -330,7 +330,7 @@ describe("ApiRetrieveRunPresenter.findRun store-routed read (single-DB invariant ); // Control-plane schedule on the SAME single client. - const scheduleId = generateKsuidId(); + const scheduleId = generateRunOpsId(); await prisma.taskSchedule.create({ data: { id: scheduleId, @@ -399,7 +399,7 @@ describe("ApiRetrieveRunPresenter.findRun store-routed read (single-DB invariant "cp-inv" ); - const scheduleId = generateKsuidId(); + const scheduleId = generateRunOpsId(); await prisma.taskSchedule.create({ data: { id: scheduleId, @@ -410,7 +410,7 @@ describe("ApiRetrieveRunPresenter.findRun store-routed read (single-DB invariant }, }); - const runId = generateKsuidId(); + const runId = generateRunOpsId(); await seedRun(prisma, { id: runId, friendlyId: `run_${runId}`, @@ -522,7 +522,7 @@ describe("ApiRetrieveRunPresenter.findRun cross-version read (PG14 + PG17)", () // The control-plane project the schedule hangs off lives on PG17. const cpEnv = await seedOrgProjectEnv(prisma17, "x-cp"); - const scheduleId = generateKsuidId(); + const scheduleId = generateRunOpsId(); await prisma17.taskSchedule.create({ data: { id: scheduleId, @@ -534,7 +534,7 @@ describe("ApiRetrieveRunPresenter.findRun cross-version read (PG14 + PG17)", () }, }); - const runId = generateKsuidId(); + const runId = generateRunOpsId(); await seedRun(prisma14, { id: runId, friendlyId: `run_${runId}`, @@ -576,7 +576,7 @@ describe("ApiRetrieveRunPresenter.findRun cross-version read (PG14 + PG17)", () // A run that exists on NEITHER store (terminated + past-retention, // observed at this layer as a miss on both underlying stores). - const goneFriendlyId = `run_${generateKsuidId()}`; + const goneFriendlyId = `run_${generateRunOpsId()}`; const fromNew = await readFoundRunViaStore( newStore, diff --git a/apps/webapp/test/apiRunResultPresenter.readthrough.test.ts b/apps/webapp/test/apiRunResultPresenter.readthrough.test.ts index 3b7571f4f5..530ef8234d 100644 --- a/apps/webapp/test/apiRunResultPresenter.readthrough.test.ts +++ b/apps/webapp/test/apiRunResultPresenter.readthrough.test.ts @@ -6,6 +6,7 @@ // boundaries (splitEnabled/isPastRetention) are injected. import { heteroPostgresTest } from "@internal/testcontainers"; import type { PrismaClient } from "@trigger.dev/database"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { customAlphabet } from "nanoid"; import { describe, expect, vi } from "vitest"; import { ApiRunResultPresenter } from "~/presenters/v3/ApiRunResultPresenter.server"; @@ -21,10 +22,11 @@ vi.setConfig({ testTimeout: 60_000 }); const idGenerator = customAlphabet("123456789abcdefghijkmnopqrstuvwxyz", 21); -// Residency by friendlyId length (after stripping `run_`): 27-char body β†’ NEW (ksuid analog), -// 25-char body β†’ LEGACY (cuid analog). ownerEngine classifies on the public friendly id. +// Residency by friendlyId shape (after stripping `run_`): a valid 26-char v1 body (version "1" at +// index 25, base32hex core) β†’ NEW; a 25-char body β†’ LEGACY (cuid analog). ownerEngine classifies on +// the public friendly id, so newFriendlyId uses the real generator to produce a NEW-classified body. function newFriendlyId(): string { - return "run_" + customAlphabet("abcdefghijklmnopqrstuvwxyz0123456789", 27)(); + return "run_" + generateRunOpsId(); } function legacyFriendlyId(): string { return "run_" + customAlphabet("abcdefghijklmnopqrstuvwxyz0123456789", 25)(); diff --git a/apps/webapp/test/apiWaitpointPresenter.readthrough.test.ts b/apps/webapp/test/apiWaitpointPresenter.readthrough.test.ts index 58d3827f55..44d6a783b9 100644 --- a/apps/webapp/test/apiWaitpointPresenter.readthrough.test.ts +++ b/apps/webapp/test/apiWaitpointPresenter.readthrough.test.ts @@ -9,14 +9,14 @@ import { } from "@internal/testcontainers"; import type { RunOpsPrismaClient } from "@internal/run-ops-database"; import type { PrismaClient, WaitpointType } from "@trigger.dev/database"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { describe, expect, vi } from "vitest"; import type { PrismaReplicaClient } from "~/db.server"; import { ApiWaitpointPresenter } from "~/presenters/v3/ApiWaitpointPresenter.server"; vi.setConfig({ testTimeout: 60_000 }); -// 25-char cuid body (length-disjoint from the 27-char KSUID) β†’ LEGACY residency. +// 25-char cuid body (no v1 version marker) β†’ LEGACY residency. function generateLegacyCuid() { const suffix = Array.from( { length: 24 }, @@ -111,10 +111,10 @@ const environmentArg = (env: { id: string; projectId: string }) => ({ describe("ApiWaitpointPresenter read-through (heterogeneous legacy + new Postgres)", () => { heteroPostgresTest( - "resolves on run-ops NEW (ksuid), legacy replica never touched", + "resolves on run-ops NEW (run-ops id), legacy replica never touched", async ({ prisma17, prisma14 }) => { - const id = generateKsuidId(); - expect(id.length).toBe(27); + const id = generateRunOpsId(); + expect(id.length).toBe(26); const { project, environment } = await seedOrgProjectEnv(prisma17, "new"); const seeded = await seedWaitpoint( @@ -139,7 +139,7 @@ describe("ApiWaitpointPresenter read-through (heterogeneous legacy + new Postgre expect(result.tags).toEqual(["x", "y", "z"]); expect(result.output).toBe(JSON.stringify({ n: 42 })); expect(result.type).toBe("MANUAL"); - // ksuid β†’ NEW: new store served the read, legacy never touched (fast-path). + // run-ops id β†’ NEW: new store served the read, legacy never touched (fast-path). expect(newClient.calls.length).toBe(1); expect(legacy.calls.length).toBe(0); } @@ -218,7 +218,7 @@ describe("ApiWaitpointPresenter read-through (heterogeneous legacy + new Postgre "cross-seam β€” new-resident served from NEW (legacy untouched); in-retention served from legacy", async ({ prisma17, prisma14 }) => { // New-resident waitpoint: lives on NEW, the new probe hits, legacy must never be touched. - const newId = generateKsuidId(); + const newId = generateRunOpsId(); const newEnv = await seedOrgProjectEnv(prisma17, "x2new"); await seedWaitpoint(prisma17, newId, { id: newEnv.environment.id, @@ -301,7 +301,7 @@ describe("ApiWaitpointPresenter passthrough (single-DB)", () => { postgresTest( "no read-through deps β†’ one plain replica read; legacy never touched", async ({ prisma }) => { - const id = generateKsuidId(); + const id = generateRunOpsId(); const { project, environment } = await seedOrgProjectEnv(prisma, "pt"); const seeded = await seedWaitpoint( prisma, diff --git a/apps/webapp/test/batchTriggerV3ResidencyInheritance.test.ts b/apps/webapp/test/batchTriggerV3ResidencyInheritance.test.ts index d5a4f0ccda..e57ddcb11b 100644 --- a/apps/webapp/test/batchTriggerV3ResidencyInheritance.test.ts +++ b/apps/webapp/test/batchTriggerV3ResidencyInheritance.test.ts @@ -13,18 +13,18 @@ vi.mock("~/db.server", () => ({ runOpsLegacyReplica: {}, })); -import { BatchId, generateKsuidId, ownerEngine, RunId } from "@trigger.dev/core/v3/isomorphic"; +import { BatchId, generateRunOpsId, ownerEngine, RunId } from "@trigger.dev/core/v3/isomorphic"; import type { AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { BatchTriggerV3Service } from "~/v3/services/batchTriggerV3.server"; vi.setConfig({ testTimeout: 60_000 }); const CUID_LEN = 25; -const KSUID_LEN = 27; +const RUN_OPS_ID_LEN = 26; // Minimal AuthenticatedEnvironment β€” only the fields the mint path reads // (organizationId, id, organization.featureFlags) need to be real. A root batch -// (no parentRunId) with no ksuid override mints cuid, which is the env-default +// (no parentRunId) with no run-ops id override mints cuid, which is the env-default // branch we assert on below. function fakeEnv(): AuthenticatedEnvironment { return { @@ -42,17 +42,17 @@ function buildService() { } describe("BatchTriggerV3Service child-residency inheritance", () => { - it("a ksuid parent yields ksuid (NEW) child friendlyIds", async () => { + it("a run-ops parent yields run-ops id (NEW) child friendlyIds", async () => { const service = buildService(); const parentFriendlyId = RunId.toFriendlyId( - // 27-char ksuid internal id β†’ NEW residency parent - "a".repeat(KSUID_LEN) + // v1 internal id (version "1" at index 25) β†’ NEW residency parent + "a".repeat(RUN_OPS_ID_LEN - 1) + "1" ); expect(ownerEngine(RunId.fromFriendlyId(parentFriendlyId))).toBe("NEW"); const childFriendlyId = await (service as any).mintChildFriendlyId(fakeEnv(), parentFriendlyId); - expect(RunId.fromFriendlyId(childFriendlyId).length).toBe(KSUID_LEN); + expect(RunId.fromFriendlyId(childFriendlyId).length).toBe(RUN_OPS_ID_LEN); expect(ownerEngine(RunId.fromFriendlyId(childFriendlyId))).toBe("NEW"); }); @@ -76,27 +76,27 @@ describe("BatchTriggerV3Service child-residency inheritance", () => { // A root batch's children are anchored to the batch's friendlyId, NOT to a // re-resolution of the per-org flag. Even with the env flag forced to "cuid" (a flip - // away from the batch's residency), a ksuid batch anchor yields ksuid children β€” so + // away from the batch's residency), a run-ops batch anchor yields run-ops children β€” so // batch + children stay co-resident and TaskRun.batchId never crosses the seam. - it("a ksuid batch anchor yields ksuid children even when the env flag resolves cuid", async () => { + it("a run-ops batch anchor yields run-ops children even when the env flag resolves cuid", async () => { const service = buildService(); // resolveMintKind forced to "cuid" - const batchFriendlyId = BatchId.toFriendlyId(generateKsuidId()); // ksuid (NEW) batch + const batchFriendlyId = BatchId.toFriendlyId(generateRunOpsId()); // run-ops id (NEW) batch expect(ownerEngine(batchFriendlyId)).toBe("NEW"); const childFriendlyId = await (service as any).mintChildFriendlyId(fakeEnv(), batchFriendlyId); - expect(RunId.fromFriendlyId(childFriendlyId).length).toBe(KSUID_LEN); + expect(RunId.fromFriendlyId(childFriendlyId).length).toBe(RUN_OPS_ID_LEN); expect(ownerEngine(RunId.fromFriendlyId(childFriendlyId))).toBe("NEW"); }); // The cuid mirror: a cuid batch anchor yields cuid children even if the flag flipped ON. - it("a cuid batch anchor yields cuid children even when the env flag resolves ksuid", async () => { + it("a cuid batch anchor yields cuid children even when the env flag resolves 'runOpsId'", async () => { const service = new BatchTriggerV3Service( undefined, undefined, {} as any, {} as any, - async () => "ksuid" // env flag flipped ON mid-batch + async () => "runOpsId" // env flag flipped ON mid-batch ); const batchFriendlyId = BatchId.generate().friendlyId; // cuid (LEGACY) batch expect(ownerEngine(batchFriendlyId)).toBe("LEGACY"); diff --git a/apps/webapp/test/batchTriggerV3StoreRouting.test.ts b/apps/webapp/test/batchTriggerV3StoreRouting.test.ts index 5e1f60d4de..39b7e2766a 100644 --- a/apps/webapp/test/batchTriggerV3StoreRouting.test.ts +++ b/apps/webapp/test/batchTriggerV3StoreRouting.test.ts @@ -1,7 +1,7 @@ import { heteroPostgresTest } from "@internal/testcontainers"; import { PostgresRunStore } from "@internal/run-store"; import { isUniqueConstraintError, type PrismaClient } from "@trigger.dev/database"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { describe, expect, vi } from "vitest"; vi.setConfig({ testTimeout: 60_000 }); @@ -50,7 +50,7 @@ async function seedRun( idempotencyKeyExpiresAt?: Date; } ) { - const runId = generateKsuidId(); + const runId = generateRunOpsId(); return prisma.taskRun.create({ data: { id: runId, @@ -74,7 +74,7 @@ async function seedRun( } async function seedBatch(prisma: PrismaClient, runtimeEnvironmentId: string, suffix: string) { - const batchId = generateKsuidId(); + const batchId = generateRunOpsId(); return prisma.batchTaskRun.create({ data: { id: batchId, diff --git a/apps/webapp/test/bulkActionV2ReadRouting.test.ts b/apps/webapp/test/bulkActionV2ReadRouting.test.ts index 314a6df6ca..3b9398e794 100644 --- a/apps/webapp/test/bulkActionV2ReadRouting.test.ts +++ b/apps/webapp/test/bulkActionV2ReadRouting.test.ts @@ -16,9 +16,9 @@ import { hydrateRunsAcrossSeam } from "~/v3/services/bulk/BulkActionV2.batchRead vi.setConfig({ testTimeout: 60_000 }); -// 27-char body β†’ NEW residency (ksuid analog). 25-char body β†’ LEGACY residency (cuid analog). +// 26-char v1 body (version "1" at index 25) β†’ NEW residency. 25-char body β†’ LEGACY residency (cuid analog). function newId(c: string) { - return "run_" + c.repeat(27); + return "run_" + c.repeat(24) + "01"; } function legacyId(c: string) { return "run_" + c.repeat(25); diff --git a/apps/webapp/test/cancelDevSessionRunsStoreRouting.test.ts b/apps/webapp/test/cancelDevSessionRunsStoreRouting.test.ts index ea29821fd1..9c91385071 100644 --- a/apps/webapp/test/cancelDevSessionRunsStoreRouting.test.ts +++ b/apps/webapp/test/cancelDevSessionRunsStoreRouting.test.ts @@ -3,14 +3,14 @@ // splitEnabled boundary and recording client wrappers are injected. import { heteroPostgresTest, postgresTest } from "@internal/testcontainers"; import type { PrismaClient } from "@trigger.dev/database"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { describe, expect, vi } from "vitest"; import type { PrismaReplicaClient } from "~/db.server"; import { CancelDevSessionRunsService } from "~/v3/services/cancelDevSessionRuns.server"; vi.setConfig({ testTimeout: 60_000 }); -// 25-char cuid body (length-disjoint from the 27-char KSUID) β†’ LEGACY residency. +// 25-char cuid body (no v1 version marker) β†’ LEGACY residency. function generateLegacyCuid() { const suffix = Array.from( { length: 24 }, @@ -90,10 +90,10 @@ function recording(client: PrismaClient, opts: { forbidden?: boolean } = {}) { describe("CancelDevSessionRunsService store routing (hetero)", () => { heteroPostgresTest( - "a NEW run (ksuid) resolves on the new store via read-through, by friendlyId and by id", + "a NEW run (run-ops id) resolves on the new store via read-through, by friendlyId and by id", async ({ prisma17, prisma14 }) => { - const id = generateKsuidId(); - expect(id.length).toBe(27); + const id = generateRunOpsId(); + expect(id.length).toBe(26); const friendlyId = `run_${id}`; const { project, organization, runtimeEnvironment } = await seedOrgProjectEnv( @@ -127,7 +127,7 @@ describe("CancelDevSessionRunsService store routing (hetero)", () => { cancelledAt: new Date(), reason: "test", }); - // ksuid β†’ NEW: new store served the read, legacy never touched. + // run-ops id β†’ NEW: new store served the read, legacy never touched. expect(newClient.calls.length).toBe(1); expect(legacy.calls.length).toBe(0); } @@ -204,7 +204,7 @@ describe("CancelDevSessionRunsService passthrough (single-DB)", () => { postgresTest( "with no read-through deps, the run is read from the single DB and session reads stay on it", async ({ prisma }) => { - const id = generateKsuidId(); + const id = generateRunOpsId(); const friendlyId = `run_${id}`; const { project, organization, runtimeEnvironment } = await seedOrgProjectEnv(prisma, "pt"); diff --git a/apps/webapp/test/crossSeamGuard.proof.test.ts b/apps/webapp/test/crossSeamGuard.proof.test.ts index ec59649dd4..2ed27ed7b6 100644 --- a/apps/webapp/test/crossSeamGuard.proof.test.ts +++ b/apps/webapp/test/crossSeamGuard.proof.test.ts @@ -13,7 +13,7 @@ import { } from "~/v3/runOpsMigration/unblockRouteCatalog"; import { WaitpointId } from "@trigger.dev/core/v3/isomorphic"; -const NEW_WP = WaitpointId.toFriendlyId("0".repeat(27)); // 27-char internal body β†’ NEW +const NEW_WP = WaitpointId.toFriendlyId("0".repeat(24) + "01"); // v1 internal body β†’ NEW const LEGACY_WP = WaitpointId.toFriendlyId("c".repeat(25)); // 25-char internal body β†’ LEGACY describe("cross-seam guard β€” exhaustive per-route store selection", () => { @@ -163,7 +163,7 @@ describe("cross-seam guard β€” PG14+PG17 hetero-fixture proof", () => { heteroPostgresTest( "exhaustive routes resolve to the physically-correct store on PG14+PG17", async ({ prisma14, prisma17 }) => { - const newWp = WaitpointId.toFriendlyId("0".repeat(27)); // NEW β†’ PG17 + const newWp = WaitpointId.toFriendlyId("0".repeat(24) + "01"); // NEW β†’ PG17 const legacyWp = WaitpointId.toFriendlyId("c".repeat(25)); // LEGACY β†’ PG14 // Distinct parent chains per residency; each Waitpoint lives on its own DB diff --git a/apps/webapp/test/engine/triggerFailedTask.test.ts b/apps/webapp/test/engine/triggerFailedTask.test.ts index ab6951a570..498312956a 100644 --- a/apps/webapp/test/engine/triggerFailedTask.test.ts +++ b/apps/webapp/test/engine/triggerFailedTask.test.ts @@ -4,7 +4,7 @@ import { RunEngine } from "@internal/run-engine"; import { setupAuthenticatedEnvironment, setupBackgroundWorker } from "@internal/run-engine/tests"; import { containerTest } from "@internal/testcontainers"; import { trace } from "@opentelemetry/api"; -import { RunId, classifyKind, generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { RunId, classifyKind, generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { TriggerFailedTaskService } from "../../app/runEngine/services/triggerFailedTask.server"; import { EventRepository } from "../../app/v3/eventRepository/eventRepository.server"; @@ -86,15 +86,15 @@ describe("TriggerFailedTaskService β€” failed run residency", () => { ); containerTest( - "failed child of a NEW (ksuid) parent mints ksuid (call)", + "failed child of a NEW (run-ops id) parent mints run-ops id (call)", async ({ prisma, redisOptions }) => { const engine = makeEngine(prisma, redisOptions); const environment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); const taskIdentifier = "failed-residency-task"; await setupBackgroundWorker(engine, environment, taskIdentifier); - const parentFriendlyId = RunId.toFriendlyId(generateKsuidId()); - expect(classifyKind(parentFriendlyId)).toBe("ksuid"); + const parentFriendlyId = RunId.toFriendlyId(generateRunOpsId()); + expect(classifyKind(parentFriendlyId)).toBe("runOpsId"); await engine.trigger( { friendlyId: parentFriendlyId, @@ -122,7 +122,7 @@ describe("TriggerFailedTaskService β€” failed run residency", () => { parentRunId: parentFriendlyId, }); - expect(classifyKind(friendlyId!)).toBe("ksuid"); + expect(classifyKind(friendlyId!)).toBe("runOpsId"); // The failed run write must land (persistence) and link to the resolved parent. const persisted = await prisma.taskRun.findFirst({ where: { friendlyId: friendlyId! } }); @@ -182,14 +182,14 @@ describe("TriggerFailedTaskService β€” failed run residency", () => { ); containerTest( - "failed child of a NEW parent mints ksuid (callWithoutTraceEvents)", + "failed child of a NEW parent mints run-ops id (callWithoutTraceEvents)", async ({ prisma, redisOptions }) => { const engine = makeEngine(prisma, redisOptions); const environment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); const taskIdentifier = "failed-residency-task"; await setupBackgroundWorker(engine, environment, taskIdentifier); - const parentFriendlyId = RunId.toFriendlyId(generateKsuidId()); + const parentFriendlyId = RunId.toFriendlyId(generateRunOpsId()); await engine.trigger( { friendlyId: parentFriendlyId, @@ -220,7 +220,7 @@ describe("TriggerFailedTaskService β€” failed run residency", () => { parentRunId: parentFriendlyId, }); - expect(classifyKind(friendlyId!)).toBe("ksuid"); + expect(classifyKind(friendlyId!)).toBe("runOpsId"); await engine.quit(); } @@ -236,9 +236,9 @@ describe("TriggerFailedTaskService β€” failed run residency", () => { const service = makeService(prisma, engine); - // A well-formed ksuid parent friendlyId that was NEVER triggered β†’ no row. + // A well-formed run-ops parent friendlyId that was NEVER triggered β†’ no row. // Exercises the missing-parent fallback in callWithoutTraceEvents. - const absentParentFriendlyId = RunId.toFriendlyId(generateKsuidId()); + const absentParentFriendlyId = RunId.toFriendlyId(generateRunOpsId()); const friendlyId = await service.callWithoutTraceEvents({ environmentId: environment.id, diff --git a/apps/webapp/test/engine/triggerTask.test.ts b/apps/webapp/test/engine/triggerTask.test.ts index ba1452abce..4a3fc76260 100644 --- a/apps/webapp/test/engine/triggerTask.test.ts +++ b/apps/webapp/test/engine/triggerTask.test.ts @@ -30,7 +30,7 @@ import { RunId, classifyKind, generateInternalId, - generateKsuidId, + generateRunOpsId, } from "@trigger.dev/core/v3/isomorphic"; import type { TaskRun } from "@trigger.dev/database"; import { Redis } from "ioredis"; @@ -2385,21 +2385,19 @@ describe("RunEngineTriggerTaskService β€” child run residency inheritance", () = ); containerTest( - "child of a NEW (ksuid) parent is minted ksuid (born NEW)", + "child of a NEW (run-ops id) parent is minted run-ops id (born NEW)", async ({ prisma, redisOptions }) => { const { engine, authenticatedEnvironment, taskIdentifier, triggerTaskService } = await setupResidencyService(prisma, redisOptions); - // Construct a NEW-resident parent directly by minting a ksuid friendlyId + // Construct a NEW-resident parent directly by minting a run-ops id friendlyId // and creating its run row, so the child inherits NEW by id-shape alone - // (no marker needed). We trigger the parent with an explicit ksuid id via + // (no marker needed). We trigger the parent with an explicit run-ops id via // the runFriendlyId option so the row physically exists for the parent // lookup the child path performs. - const parentFriendlyId = RunId.toFriendlyId( - // 27-char ksuid β†’ classifies NEW - (await import("@trigger.dev/core/v3/isomorphic")).generateKsuidId() - ); - expect(classifyKind(parentFriendlyId)).toBe("ksuid"); + // v1 id (version "1" at index 25) β†’ classifies NEW + const parentFriendlyId = RunId.toFriendlyId(generateRunOpsId()); + expect(classifyKind(parentFriendlyId)).toBe("runOpsId"); const parent = await triggerTaskService.call({ taskId: taskIdentifier, @@ -2415,7 +2413,7 @@ describe("RunEngineTriggerTaskService β€” child run residency inheritance", () = body: { payload: { test: "child" }, options: { parentRunId: parentFriendlyId } }, }); - expect(classifyKind(child!.run.friendlyId)).toBe("ksuid"); + expect(classifyKind(child!.run.friendlyId)).toBe("runOpsId"); await engine.quit(); } @@ -2427,11 +2425,11 @@ describe("RunEngineTriggerTaskService β€” child run residency inheritance", () = const { engine, authenticatedEnvironment, taskIdentifier, triggerTaskService } = await setupResidencyService(prisma, redisOptions); - // Explicit cuid id for the run, and a ksuid/NEW parent id. + // Explicit cuid id for the run, and a run-ops id/NEW parent id. const explicitFriendlyId = RunId.toFriendlyId(generateInternalId()); - const parentFriendlyId = RunId.toFriendlyId(generateKsuidId()); + const parentFriendlyId = RunId.toFriendlyId(generateRunOpsId()); expect(classifyKind(explicitFriendlyId)).toBe("cuid"); - expect(classifyKind(parentFriendlyId)).toBe("ksuid"); + expect(classifyKind(parentFriendlyId)).toBe("runOpsId"); const result = await triggerTaskService.call({ taskId: taskIdentifier, @@ -2440,7 +2438,7 @@ describe("RunEngineTriggerTaskService β€” child run residency inheritance", () = options: { runFriendlyId: explicitFriendlyId }, }); - // Caller-supplied id wins verbatim β€” NOT re-minted to ksuid despite the NEW parent. + // Caller-supplied id wins verbatim β€” NOT re-minted to run-ops id despite the NEW parent. expect(result!.run.friendlyId).toBe(explicitFriendlyId); await engine.quit(); diff --git a/apps/webapp/test/idempotencyDedupResidency.test.ts b/apps/webapp/test/idempotencyDedupResidency.test.ts index d8ab8d934c..38cfc2abf3 100644 --- a/apps/webapp/test/idempotencyDedupResidency.test.ts +++ b/apps/webapp/test/idempotencyDedupResidency.test.ts @@ -1,6 +1,6 @@ import { heteroPostgresTest } from "@internal/testcontainers"; import type { PrismaClient } from "@trigger.dev/database"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { describe, expect, vi } from "vitest"; // Stub so the runStore singleton doesn't eagerly connect at import. @@ -79,7 +79,7 @@ async function seedRun( status?: "PENDING" | "EXECUTING" | "COMPLETED_SUCCESSFULLY" | "COMPLETED_WITH_ERRORS"; } ) { - const runId = generateKsuidId(); + const runId = generateRunOpsId(); return prisma.taskRun.create({ data: { id: runId, diff --git a/apps/webapp/test/idempotencyKeyConcernLegacyAuthority.test.ts b/apps/webapp/test/idempotencyKeyConcernLegacyAuthority.test.ts index 5434567d42..f4de27e6f2 100644 --- a/apps/webapp/test/idempotencyKeyConcernLegacyAuthority.test.ts +++ b/apps/webapp/test/idempotencyKeyConcernLegacyAuthority.test.ts @@ -1,6 +1,6 @@ import { heteroPostgresTest } from "@internal/testcontainers"; import type { PrismaClient } from "@trigger.dev/database"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { describe, expect, vi } from "vitest"; // Stub `~/db.server` so the `runStore` singleton doesn't eagerly connect at @@ -97,7 +97,7 @@ async function seedRun( idempotencyKeyExpiresAt?: Date; } ) { - const runId = generateKsuidId(); + const runId = generateRunOpsId(); return prisma.taskRun.create({ data: { id: runId, diff --git a/apps/webapp/test/performTaskRunAlertsStoreRouting.test.ts b/apps/webapp/test/performTaskRunAlertsStoreRouting.test.ts index 707a3546cf..c6db85d521 100644 --- a/apps/webapp/test/performTaskRunAlertsStoreRouting.test.ts +++ b/apps/webapp/test/performTaskRunAlertsStoreRouting.test.ts @@ -1,6 +1,6 @@ // Real heterogeneous legacy + new Postgres proof for the alert-hydration TaskRun read. // The DB is never mocked. A test-only RunStore wraps two real PostgresRunStore -// instances and routes findRun by id residency (ksuid β†’ NEW, cuid β†’ LEGACY), +// instances and routes findRun by id residency (run-ops id β†’ NEW, cuid β†’ LEGACY), // mirroring the sibling routing suite. The ProjectAlertChannel read must stay control-plane. // // The alert env-type read (parentEnvironment?.type ?? type) is resolved via the app @@ -10,7 +10,7 @@ import { heteroPostgresTest, postgresTest } from "@internal/testcontainers"; import { PostgresRunStore } from "@internal/run-store"; import type { ReadClient, RunStore } from "@internal/run-store"; import type { Prisma, PrismaClient } from "@trigger.dev/database"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId, ownerEngine } from "@trigger.dev/core/v3/isomorphic"; import { describe, expect } from "vitest"; import { ControlPlaneCache } from "~/v3/runOpsMigration/controlPlaneCache.server"; import { ControlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server"; @@ -41,7 +41,7 @@ class RoutingRunStore implements RunStore { } #resolveById(runId: string): PostgresRunStore { - return runId.length === 27 ? this.#newStore : this.#legacyStore; + return ownerEngine(runId) === "NEW" ? this.#newStore : this.#legacyStore; } #idFromWhere(where: Prisma.TaskRunWhereInput): string | undefined { @@ -220,7 +220,7 @@ describe("PerformTaskRunAlertsService store routing (hetero)", () => { heteroPostgresTest( "env type resolves via the control-plane resolver (distinct DB) while the run resolves on the run-ops store", async ({ prisma17, prisma14 }) => { - const id = generateKsuidId(); + const id = generateRunOpsId(); const friendlyId = `run_${id}`; // Cloud shape: run-ops = the new DB (cross-seam FKs dropped), control-plane = the legacy DB. @@ -312,7 +312,7 @@ describe("PerformTaskRunAlertsService passthrough (single-DB)", () => { postgresTest( "with the default store, run read + alert-channel read both resolve on the single DB", async ({ prisma }) => { - const id = generateKsuidId(); + const id = generateRunOpsId(); const friendlyId = `run_${id}`; const { project, organization, runtimeEnvironment } = await seedProject(prisma, "pt"); diff --git a/apps/webapp/test/realtime/runReaderReadThrough.test.ts b/apps/webapp/test/realtime/runReaderReadThrough.test.ts index 88c4e11e27..cd23d954fe 100644 --- a/apps/webapp/test/realtime/runReaderReadThrough.test.ts +++ b/apps/webapp/test/realtime/runReaderReadThrough.test.ts @@ -16,15 +16,15 @@ import { RunHydrator } from "~/services/realtime/runReader.server"; // (which live in the hydrator, not the store) are unaffected by the seam. // // The heterogeneous fixture gives real legacy + new Postgres containers; NO DB is mocked. The ONLY -// non-DB fake is the residency selector that the routing-shaped store uses (`ownerEngine`: ksuid -> +// non-DB fake is the residency selector that the routing-shaped store uses (`ownerEngine`: run-ops id -> // NEW, cuid -> LEGACY), exactly the substrate the RoutingRunStore ships. Run ids are 25 chars (cuid -// -> LEGACY) or 27 chars (ksuid -> NEW) so the classifier routes them deterministically. +// -> LEGACY) or v1-shaped (26 chars, version "1" at index 25 -> NEW) so the classifier routes them deterministically. -// 25-char internal id -> cuid -> LEGACY; 27-char internal id -> ksuid -> NEW. The +// 25-char internal id -> cuid -> LEGACY; v1 internal id (26 chars, version "1" at index 25) -> NEW. The // classifier strips a leading `_`, so these ids must carry NO underscore (a bare // alphanumeric body of the exact length). function newId(label: string): string { - return ("k" + label.replace(/[^a-z0-9]/gi, "")).padEnd(27, "0").slice(0, 27); + return ("k" + label.replace(/[^0-9a-v]/g, "")).padEnd(24, "0").slice(0, 24) + "01"; } function legacyId(label: string): string { return ("c" + label.replace(/[^a-z0-9]/gi, "")).padEnd(25, "0").slice(0, 25); @@ -310,7 +310,7 @@ describe("RunHydrator read-route through the runStore seam (legacy + new)", () = } ); - // Terminal-metadata read-seam: a NEW-resident (ksuid) run's final metadata is hydrated through + // Terminal-metadata read-seam: a NEW-resident (run-ops id) run's final metadata is hydrated through // the owning (NEW) store, not off a generic legacy replica. Asserts read-seam ROUTING for the // terminal read; it is not a hard ordering/consistency guarantee about when the terminal marker // and the row's terminal columns converge. diff --git a/apps/webapp/test/resetIdempotencyKeyLegacyAuthority.test.ts b/apps/webapp/test/resetIdempotencyKeyLegacyAuthority.test.ts index 04c442ca18..df1264022a 100644 --- a/apps/webapp/test/resetIdempotencyKeyLegacyAuthority.test.ts +++ b/apps/webapp/test/resetIdempotencyKeyLegacyAuthority.test.ts @@ -1,6 +1,6 @@ import { heteroPostgresTest } from "@internal/testcontainers"; import type { PrismaClient } from "@trigger.dev/database"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { describe, expect, vi } from "vitest"; // Stub these so the default singletons don't eagerly connect at import. The @@ -76,7 +76,7 @@ async function seedRun( idempotencyKeyExpiresAt?: Date; } ) { - const runId = generateKsuidId(); + const runId = generateRunOpsId(); return prisma.taskRun.create({ data: { id: runId, diff --git a/apps/webapp/test/resolveWaitpointThroughReadThrough.readthrough.test.ts b/apps/webapp/test/resolveWaitpointThroughReadThrough.readthrough.test.ts index 3b3ec41789..4bceb95152 100644 --- a/apps/webapp/test/resolveWaitpointThroughReadThrough.readthrough.test.ts +++ b/apps/webapp/test/resolveWaitpointThroughReadThrough.readthrough.test.ts @@ -1,14 +1,14 @@ import { heteroRunOpsPostgresTest, postgresTest } from "@internal/testcontainers"; import type { RunOpsPrismaClient } from "@internal/run-ops-database"; import type { PrismaClient } from "@trigger.dev/database"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { describe, expect, vi } from "vitest"; import type { PrismaReplicaClient } from "~/db.server"; import { resolveWaitpointThroughReadThrough } from "~/runEngine/concerns/resolveWaitpointThroughReadThrough.server"; vi.setConfig({ testTimeout: 60_000 }); -// 25-char cuid (length-disjoint from the 27-char KSUID) -> LEGACY residency. +// 25-char cuid (no v1 version marker) -> LEGACY residency. function generateLegacyCuid() { const suffix = Array.from( { length: 24 }, @@ -84,15 +84,15 @@ const read = (waitpointId: string, environmentId: string) => (client: PrismaRepl describe("resolveWaitpointThroughReadThrough (hetero PG14 legacy + dedicated run-ops PG17)", () => { heteroRunOpsPostgresTest( - "ksuid waitpoint resolves on the dedicated run-ops client; legacy replica never touched", + "run-ops waitpoint resolves on the dedicated run-ops client; legacy replica never touched", async ({ prisma17, prisma14 }) => { - const id = generateKsuidId(); - expect(id.length).toBe(27); + const id = generateRunOpsId(); + expect(id.length).toBe(26); // The dedicated run-ops DB has no control-plane tables; the waitpoint's // environment/project FKs are synthetic scalar ids. - const environmentId = generateKsuidId(); - const projectId = generateKsuidId(); + const environmentId = generateRunOpsId(); + const projectId = generateRunOpsId(); const seeded = await seedWaitpoint(prisma17, id, { id: environmentId, projectId }); const newClient = recording(prisma17); @@ -156,10 +156,10 @@ describe("resolveWaitpointThroughReadThrough (hetero PG14 legacy + dedicated run async ({ prisma17, prisma14 }) => { // The bare wait route passes NO `deps`; the `defaults` DI seam models old vs new // fallback against containers, avoiding the real db.server topology. - const id = generateKsuidId(); - expect(id.length).toBe(27); - const environmentId = generateKsuidId(); - const projectId = generateKsuidId(); + const id = generateRunOpsId(); + expect(id.length).toBe(26); + const environmentId = generateRunOpsId(); + const projectId = generateRunOpsId(); const seeded = await seedWaitpoint(prisma17, id, { id: environmentId, projectId }); // FAIL-BEFORE: old default pinned newClient to control-plane ($replica β‰ˆ prisma14) β†’ miss. @@ -215,7 +215,7 @@ describe("resolveWaitpointThroughReadThrough (hetero PG14 legacy + dedicated run postgresTest( "passthrough (single-DB): one plain read; legacy never invoked", async ({ prisma }) => { - const id = generateKsuidId(); + const id = generateRunOpsId(); const { project, environment } = await seedOrgProjectEnv(prisma, "pt"); const seeded = await seedWaitpoint(prisma, id, { id: environment.id, diff --git a/apps/webapp/test/runDetailLoaders.controlPlane.readthrough.test.ts b/apps/webapp/test/runDetailLoaders.controlPlane.readthrough.test.ts index d2bf3d6e19..76898104f0 100644 --- a/apps/webapp/test/runDetailLoaders.controlPlane.readthrough.test.ts +++ b/apps/webapp/test/runDetailLoaders.controlPlane.readthrough.test.ts @@ -50,7 +50,10 @@ async function seedAll(prisma: PrismaClient) { // The run lives on the dedicated run-ops client; its control-plane FKs are synthetic scalar ids // pointing at rows that exist only on PG14 (the dedicated DB has no such tables). -async function seedKsuidRun(prisma17: RunOpsPrismaClient, cp: Awaited>) { +async function seedRunOpsRun( + prisma17: RunOpsPrismaClient, + cp: Awaited> +) { const k = n++; return prisma17.taskRun.create({ data: { @@ -91,11 +94,11 @@ function wire(prisma14: PrismaClient, prisma17: RunOpsPrismaClient) { describe("run-detail loaders cross-DB read-through (dedicated run-ops client)", () => { heteroRunOpsPostgresTest( - "ksuid run resolves: friendlyId read on the dedicated run-ops DB + membership/env auth on PG14 (resources.runs.$runParam shape)", + "run-ops run resolves: friendlyId read on the dedicated run-ops DB + membership/env auth on PG14 (resources.runs.$runParam shape)", async ({ prisma14, prisma17 }) => { const cp14 = prisma14 as unknown as PrismaClient; const cp = await seedAll(cp14); - const run = await seedKsuidRun(prisma17, cp); + const run = await seedRunOpsRun(prisma17, cp); const { runStore, resolver } = wire(cp14, prisma17); const found = await runStore.findRun( @@ -141,7 +144,7 @@ describe("run-detail loaders cross-DB read-through (dedicated run-ops client)", async ({ prisma14, prisma17 }) => { const cp14 = prisma14 as unknown as PrismaClient; const cp = await seedAll(cp14); - const run = await seedKsuidRun(prisma17, cp); + const run = await seedRunOpsRun(prisma17, cp); const { runStore } = wire(cp14, prisma17); const found = await runStore.findRun( @@ -166,7 +169,7 @@ describe("run-detail loaders cross-DB read-through (dedicated run-ops client)", async ({ prisma14, prisma17 }) => { const cp14 = prisma14 as unknown as PrismaClient; const cp = await seedAll(cp14); - const run = await seedKsuidRun(prisma17, cp); + const run = await seedRunOpsRun(prisma17, cp); const { runStore, resolver } = wire(cp14, prisma17); const found = await runStore.findRun( diff --git a/apps/webapp/test/runOpsCrossSeamGuard.test.ts b/apps/webapp/test/runOpsCrossSeamGuard.test.ts index 5f232c0869..2f62cf0172 100644 --- a/apps/webapp/test/runOpsCrossSeamGuard.test.ts +++ b/apps/webapp/test/runOpsCrossSeamGuard.test.ts @@ -3,12 +3,10 @@ import { computeStoreForCompletion, selectStoreForWaitpoint, } from "~/v3/runOpsMigration/crossSeamGuard.server"; -import { UnclassifiableRunId } from "@trigger.dev/core/v3/isomorphic"; - // Real sample ids exercising the genuine run-id residency classifier (no stub). -const NEW = "waitpoint_" + "a".repeat(27); // 27-char ksuid body -> NEW +const NEW = "waitpoint_" + "a".repeat(24) + "01"; // v1 body (version "1" at index 25) -> NEW const LEGACY = "waitpoint_" + "a".repeat(25); // 25-char cuid body -> LEGACY -const AMBIGUOUS = "waitpoint_" + "a".repeat(10); // neither length -> throws +const UNRECOGNIZED = "waitpoint_" + "a".repeat(10); // no version marker -> LEGACY describe("selectStoreForWaitpoint β€” happy-path residency routing", () => { it("MANUAL completion of a NEW waitpoint selects the new store", () => { @@ -92,11 +90,11 @@ describe("selectStoreForWaitpoint β€” legacy pins", () => { }); }); -describe("selectStoreForWaitpoint β€” ambiguity and unknown routes are loud", () => { - it("rethrows UnclassifiableRunId for an ambiguous-length id (never silently routes)", () => { - expect(() => selectStoreForWaitpoint({ waitpointId: AMBIGUOUS, routeKind: "MANUAL" })).toThrow( - UnclassifiableRunId - ); +describe("selectStoreForWaitpoint β€” unrecognized shapes and unknown routes", () => { + it("routes an id without the v1 version marker to legacy (classification is total)", () => { + const d = selectStoreForWaitpoint({ waitpointId: UNRECOGNIZED, routeKind: "MANUAL" }); + expect(d.store).toBe("legacy"); + expect(d.residency).toBe("LEGACY"); }); it("throws when an unknown routeKind is supplied", () => { @@ -111,7 +109,7 @@ describe("computeStoreForCompletion β€” single-DB no-op + flag wrapper", () => { it("returns the single store without classifying when split is OFF", () => { const calls: string[] = []; const d = computeStoreForCompletion( - { waitpointId: AMBIGUOUS, routeKind: "MANUAL" }, + { waitpointId: UNRECOGNIZED, routeKind: "MANUAL" }, { splitEnabled: false, classify: (id) => { diff --git a/apps/webapp/test/runOpsMintCutover.test.ts b/apps/webapp/test/runOpsMintCutover.test.ts index d838a382e9..0fd9e40fff 100644 --- a/apps/webapp/test/runOpsMintCutover.test.ts +++ b/apps/webapp/test/runOpsMintCutover.test.ts @@ -1,14 +1,14 @@ -// Per-env KSUID mint cutover integration proof. +// Per-env run-ops-id mint cutover integration proof. // // NEVER mocks the DB: the mint decision runs through the pure core `computeRunIdMintKind` // wired to a REAL `makeFlag(prisma)` that reads the REAL `Organization.featureFlags` / // `FeatureFlag` rows in a testcontainers Postgres. Only the two boundary knobs // are injected β€” `masterEnabled` and the `splitEnabled` boot-boolean β€” never a -// mocked DB. The KSUID/cuid format + residency are then proven through the SAME isomorphic -// helpers the real trigger path uses (`generateKsuidId` / `RunId.toFriendlyId` / +// mocked DB. The run-ops-id/cuid format + residency are then proven through the SAME isomorphic +// helpers the real trigger path uses (`generateRunOpsId` / `RunId.toFriendlyId` / // `RunId.fromFriendlyId` / `ownerEngine`). import type { PrismaClient } from "@trigger.dev/database"; -import { generateKsuidId, ownerEngine, RunId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId, ownerEngine, RunId } from "@trigger.dev/core/v3/isomorphic"; import { postgresTest } from "@internal/testcontainers"; import { describe, expect, vi } from "vitest"; import { @@ -27,14 +27,14 @@ vi.setConfig({ testTimeout: 60_000 }); // The real trigger-path mint helper, copied verbatim from triggerTask.server.ts so the // test exercises the exact id format a cut-over env produces. -function mintRunKsuidFriendlyId(): string { - return RunId.toFriendlyId(generateKsuidId()); +function mintRunOpsFriendlyId(): string { + return RunId.toFriendlyId(generateRunOpsId()); } -// Mirrors the real trigger path: resolve the kind, then mint either a KSUID friendlyId or +// Mirrors the real trigger path: resolve the kind, then mint either a run-ops friendlyId or // the default cuid one (RunId.generate()). function mintRunFriendlyId(kind: RunIdMintKind): string { - return kind === "ksuid" ? mintRunKsuidFriendlyId() : RunId.generate().friendlyId; + return kind === "runOpsId" ? mintRunOpsFriendlyId() : RunId.generate().friendlyId; } async function seedOrgEnv(prisma: PrismaClient, mintFlag?: RunIdMintKind) { @@ -48,7 +48,7 @@ async function seedOrgEnv(prisma: PrismaClient, mintFlag?: RunIdMintKind) { if (mintFlag) { await prisma.organization.update({ where: { id: organization.id }, - data: { featureFlags: { [FEATURE_FLAG.runOpsMintKsuid]: mintFlag } }, + data: { featureFlags: { [FEATURE_FLAG.runOpsMintKind]: mintFlag } }, }); } return { organization, environment }; @@ -70,18 +70,18 @@ function realFlag(prisma: PrismaClient) { }) )?.featureFlags; return flagFn({ - key: FEATURE_FLAG.runOpsMintKsuid, + key: FEATURE_FLAG.runOpsMintKind, defaultValue: "cuid", overrides: (overrides as Record) ?? {}, }); }; } -describe("per-env KSUID mint cutover", () => { +describe("per-env run-ops-id mint cutover", () => { postgresTest( - "canary org mints KSUID/NEW; non-canary org mints cuid/LEGACY", + "canary org mints run-ops/NEW; non-canary org mints cuid/LEGACY", async ({ prisma }) => { - const a = await seedOrgEnv(prisma, "ksuid"); // canary + const a = await seedOrgEnv(prisma, "runOpsId"); // canary const b = await seedOrgEnv(prisma); // not cut over const flag = realFlag(prisma); @@ -96,13 +96,13 @@ describe("per-env KSUID mint cutover", () => { deps ); - expect(kindA).toBe("ksuid"); + expect(kindA).toBe("runOpsId"); expect(kindB).toBe("cuid"); const friendlyA = mintRunFriendlyId(kindA); const friendlyB = mintRunFriendlyId(kindB); - expect(RunId.fromFriendlyId(friendlyA).length).toBe(27); + expect(RunId.fromFriendlyId(friendlyA).length).toBe(26); expect(ownerEngine(RunId.fromFriendlyId(friendlyA))).toBe("NEW"); expect(RunId.fromFriendlyId(friendlyB).length).toBe(25); @@ -111,9 +111,9 @@ describe("per-env KSUID mint cutover", () => { ); postgresTest( - "split OFF mints cuid even for a flagged-ksuid org (split gate dominates)", + "split OFF mints cuid even for a 'runOpsId'-flagged org (split gate dominates)", async ({ prisma }) => { - const a = await seedOrgEnv(prisma, "ksuid"); + const a = await seedOrgEnv(prisma, "runOpsId"); const flag = vi.fn(realFlag(prisma)); const kind = await computeRunIdMintKind( @@ -127,25 +127,25 @@ describe("per-env KSUID mint cutover", () => { ); postgresTest( - "drain-new-forward (D8): flipping back to cuid stops new KSUID mints without reverting existing", + "drain-new-forward (D8): flipping back to cuid stops new run-ops mints without reverting existing", async ({ prisma }) => { - const a = await seedOrgEnv(prisma, "ksuid"); + const a = await seedOrgEnv(prisma, "runOpsId"); const flag = realFlag(prisma); const deps = { masterEnabled: true, splitEnabled: async () => true, flag }; - // First run is born KSUID/NEW while cut over. + // First run is born run-ops/NEW while cut over. const firstKind = await computeRunIdMintKind( { organizationId: a.organization.id, id: a.environment.id }, deps ); const firstFriendly = mintRunFriendlyId(firstKind); - expect(firstKind).toBe("ksuid"); + expect(firstKind).toBe("runOpsId"); expect(ownerEngine(RunId.fromFriendlyId(firstFriendly))).toBe("NEW"); // Roll the org back to cuid (drain-new-forward β€” set the flag to "cuid"). await prisma.organization.update({ where: { id: a.organization.id }, - data: { featureFlags: { [FEATURE_FLAG.runOpsMintKsuid]: "cuid" } }, + data: { featureFlags: { [FEATURE_FLAG.runOpsMintKind]: "cuid" } }, }); // The NEXT run mints cuid again (the env-bound resolver's TTL cache is not used here, @@ -158,8 +158,8 @@ describe("per-env KSUID mint cutover", () => { expect(nextKind).toBe("cuid"); expect(ownerEngine(RunId.fromFriendlyId(nextFriendly))).toBe("LEGACY"); - // The already-minted KSUID run is untouched β€” drain-new-forward never reverts it. - expect(RunId.fromFriendlyId(firstFriendly).length).toBe(27); + // The already-minted run-ops run is untouched β€” drain-new-forward never reverts it. + expect(RunId.fromFriendlyId(firstFriendly).length).toBe(26); expect(ownerEngine(RunId.fromFriendlyId(firstFriendly))).toBe("NEW"); } ); @@ -168,7 +168,7 @@ describe("per-env KSUID mint cutover", () => { "parent and child re-resolve independently from their own org flag", async ({ prisma }) => { // Parent lives in a cut-over org; child is triggered into a NON-cut-over org. - const parentOrg = await seedOrgEnv(prisma, "ksuid"); + const parentOrg = await seedOrgEnv(prisma, "runOpsId"); const childOrg = await seedOrgEnv(prisma); // not cut over const flag = realFlag(prisma); const deps = { masterEnabled: true, splitEnabled: async () => true, flag }; @@ -184,9 +184,9 @@ describe("per-env KSUID mint cutover", () => { // Observed behavior: the mint decision is resolved per the run's OWN org/env flag β€” // it does NOT inherit the parent's residency. A child in a non-cut-over org mints cuid - // even when its parent was born KSUID. If children must inherit, that inheritance + // even when its parent was born run-ops. If children must inherit, that inheritance // belongs to the child-trigger path, not this resolver. - expect(parentKind).toBe("ksuid"); + expect(parentKind).toBe("runOpsId"); expect(childKind).toBe("cuid"); } ); diff --git a/apps/webapp/test/runPresenterReadRoute.test.ts b/apps/webapp/test/runPresenterReadRoute.test.ts index e3733ff8f2..bf3a92ca30 100644 --- a/apps/webapp/test/runPresenterReadRoute.test.ts +++ b/apps/webapp/test/runPresenterReadRoute.test.ts @@ -6,7 +6,7 @@ // `user.findFirst` admin read β€” keeping the test off ClickHouse. import { postgresTest } from "@internal/testcontainers"; import type { PrismaClient } from "@trigger.dev/database"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import { describe, expect, vi } from "vitest"; vi.setConfig({ testTimeout: 60_000 }); @@ -172,7 +172,7 @@ describe("RunPresenter run read seam (single-DB, real PG)", () => { suffix ); - const id = generateKsuidId(); + const id = generateRunOpsId(); const friendlyId = `run_${id}`; const run = await seedRun( prisma, @@ -216,7 +216,7 @@ describe("RunPresenter run read seam (single-DB, real PG)", () => { suffix ); - const id = generateKsuidId(); + const id = generateRunOpsId(); const friendlyId = `run_${id}`; await seedRun( prisma, @@ -270,7 +270,7 @@ describe("RunPresenter run read seam (single-DB, real PG)", () => { const suffix = uniqueSuffix("notfound"); const { user, project, runtimeEnvironment } = await seedOrgProjectEnvMember(prisma, suffix); - const missingFriendlyId = `run_${generateKsuidId()}`; + const missingFriendlyId = `run_${generateRunOpsId()}`; const presenter = new RunPresenter(prisma); await expect( @@ -294,7 +294,7 @@ describe("RunPresenter run read seam (single-DB, real PG)", () => { suffix ); - const id = generateKsuidId(); + const id = generateRunOpsId(); const friendlyId = `run_${id}`; await seedRun( prisma, diff --git a/apps/webapp/test/runsReplicationInstance.test.ts b/apps/webapp/test/runsReplicationInstance.test.ts index 9dc2dd6a10..ea109c5d3b 100644 --- a/apps/webapp/test/runsReplicationInstance.test.ts +++ b/apps/webapp/test/runsReplicationInstance.test.ts @@ -153,7 +153,7 @@ describe("assertReplicationCoversSplit (boot gate-coupling)", () => { }; it('throws when split is on but sources[] has no "new" source (the silent under-count)', () => { - // Split on, but the new replication source is forced off β€” ksuid runs would not + // Split on, but the new replication source is forced off β€” run-ops runs would not // reach ClickHouse. This is the exact misconfiguration the boot gate must refuse to boot with. const sources = buildReplicationSources({ ...baseArgs, diff --git a/apps/webapp/test/runsRepository.readthrough.test.ts b/apps/webapp/test/runsRepository.readthrough.test.ts index fd3f342f9c..0294450699 100644 --- a/apps/webapp/test/runsRepository.readthrough.test.ts +++ b/apps/webapp/test/runsRepository.readthrough.test.ts @@ -352,11 +352,11 @@ describe("RunsRepository read-through id-set hydrate (PG14 legacy + PG17 new)", } ); - // Full-keyset walk over interleaved cuid + ksuid ids: hydration must preserve the ClickHouse + // Full-keyset walk over interleaved cuid + run-ops ids: hydration must preserve the ClickHouse // (created_at DESC, run_id DESC) order across the id-space seam. A hydrate that reverts to lexical // `id desc` splits the two id-spaces into separate blocks, so it would fail this walk. replicationContainerTest( - "paginating the full keyset enumerates every interleaved cuid/ksuid id once, in CH keyset order, with no empty page", + "paginating the full keyset enumerates every interleaved cuid/run-ops id once, in CH keyset order, with no empty page", async ({ clickhouseContainer, redisOptions, postgresContainer, prisma }) => { const { clickhouse } = await setupClickhouseReplication({ prisma, @@ -367,21 +367,21 @@ describe("RunsRepository read-through id-set hydrate (PG14 legacy + PG17 new)", const ctx = await seedParents(prisma, "keysetwalk"); - // cuid-shaped ids (25 chars, "c" prefix) and ksuid-shaped ids (27 chars, "2" prefix). Lexical + // cuid-shaped ids (25 chars, "c" prefix) and v1-shaped ids (26 chars, "2" prefix, version "1"). Lexical // `id desc` groups all "c" ids ahead of all "2" ids; the created_at order below interleaves // them, so the two orders genuinely differ across the seam. const cuid = (n: number) => `c${String(n).padStart(24, "0")}`; - const ksuid = (n: number) => `2${String(n).padStart(26, "0")}`; + const runOpsId = (n: number) => `2${String(n).padStart(23, "0")}01`; - // created_at DESC order (index 0 = most recent) interleaves the id-spaces: ksuid, cuid, - // ksuid, cuid, ksuid, cuid. + // created_at DESC order (index 0 = most recent) interleaves the id-spaces: run-ops id, cuid, + // run-ops id, cuid, run-ops id, cuid. const now = Date.now(); const seeds = [ - { id: ksuid(6), friendlyId: "run_k6", createdAt: new Date(now - 0 * 60_000) }, + { id: runOpsId(6), friendlyId: "run_k6", createdAt: new Date(now - 0 * 60_000) }, { id: cuid(5), friendlyId: "run_c5", createdAt: new Date(now - 1 * 60_000) }, - { id: ksuid(4), friendlyId: "run_k4", createdAt: new Date(now - 2 * 60_000) }, + { id: runOpsId(4), friendlyId: "run_k4", createdAt: new Date(now - 2 * 60_000) }, { id: cuid(3), friendlyId: "run_c3", createdAt: new Date(now - 3 * 60_000) }, - { id: ksuid(2), friendlyId: "run_k2", createdAt: new Date(now - 4 * 60_000) }, + { id: runOpsId(2), friendlyId: "run_k2", createdAt: new Date(now - 4 * 60_000) }, { id: cuid(1), friendlyId: "run_c1", createdAt: new Date(now - 5 * 60_000) }, ]; for (const s of seeds) { diff --git a/apps/webapp/test/sessions.readthrough.test.ts b/apps/webapp/test/sessions.readthrough.test.ts index 6496baeb16..d5d5358565 100644 --- a/apps/webapp/test/sessions.readthrough.test.ts +++ b/apps/webapp/test/sessions.readthrough.test.ts @@ -11,7 +11,7 @@ vi.mock("~/db.server", () => ({ import { heteroRunOpsPostgresTest, postgresTest } from "@internal/testcontainers"; import { buildRunStore } from "~/v3/runStore.server"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import type { RunOpsPrismaClient } from "@internal/run-ops-database"; import type { PrismaClient } from "@trigger.dev/database"; import { @@ -91,7 +91,7 @@ async function createLegacyRun( } /** - * Create a NEW (dedicated run-ops) TaskRun with a ksuid id β€” classifies NEW and + * Create a NEW (dedicated run-ops) TaskRun with a run-ops id β€” classifies NEW and * lives only on the run-ops DB. Scalar tenant columns only (the subset schema is * FK-free, so no org/project/env rows are required here). */ @@ -250,16 +250,16 @@ describe("sessions serializer currentRunId resolution", () => { ); // --- Split single-run across two physical DBs (the production-shaped break) --- - // ksuid (NEW-DB) session run must serialize a non-null friendlyId, and a cuid + // run-ops id (NEW-DB) session run must serialize a non-null friendlyId, and a cuid // (LEGACY) run must still resolve β€” proving the asymmetry is gone. heteroRunOpsPostgresTest( - "split single-run resolves a NEW-ksuid run from the run-ops DB and a LEGACY-cuid run from control-plane", + "split single-run resolves a NEW-run-ops run from the run-ops DB and a LEGACY-cuid run from control-plane", async ({ prisma14, prisma17 }) => { const ctx = await seedParents(prisma14, "split-single"); const newRun = await createNewRun(prisma17, ctx, { friendlyId: "run_new", - id: generateKsuidId(), + id: generateRunOpsId(), }); const legacyRun = await createLegacyRun(prisma14, ctx, { friendlyId: "run_legacy" }); @@ -299,7 +299,7 @@ describe("sessions serializer currentRunId resolution", () => { const newRun = await createNewRun(prisma17, ctx, { friendlyId: "run_bnew", - id: generateKsuidId(), + id: generateRunOpsId(), }); const legacyRun = await createLegacyRun(prisma14, ctx, { friendlyId: "run_blegacy" }); const crossEnvRun = await createLegacyRun(prisma14, otherCtx, { friendlyId: "run_bcross" }); diff --git a/apps/webapp/test/updateMetadataStoreRoutingHetero.test.ts b/apps/webapp/test/updateMetadataStoreRoutingHetero.test.ts index eded6bccf9..2bfe3d0571 100644 --- a/apps/webapp/test/updateMetadataStoreRoutingHetero.test.ts +++ b/apps/webapp/test/updateMetadataStoreRoutingHetero.test.ts @@ -3,7 +3,7 @@ import { PostgresRunStore } from "@internal/run-store"; import type { ReadClient, RunStore } from "@internal/run-store"; import type { Prisma, PrismaClient } from "@trigger.dev/database"; import { parsePacket } from "@trigger.dev/core/v3"; -import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { generateRunOpsId, ownerEngine } from "@trigger.dev/core/v3/isomorphic"; import { setTimeout } from "timers/promises"; import { describe, expect } from "vitest"; import { UpdateMetadataService } from "~/services/metadata/updateMetadata.server"; @@ -22,7 +22,7 @@ vi.setConfig({ testTimeout: 60_000 }); * store by id length, then calls the inner store WITHOUT forwarding the outer tx * (passes undefined), so the inner PostgresRunStore uses its own prisma17/prisma14. * - * Classification contract (length-disjoint): 27-char id => KSUID => NEW store; + * Classification contract (version char): a v1 id (26 chars, version "1" at index 25) => NEW store; * 25-char cuid => LEGACY store. */ class RoutingRunStore implements RunStore { @@ -34,13 +34,13 @@ class RoutingRunStore implements RunStore { this.#legacyStore = legacyStore; } - // Resolve by run-id length: 27 => NEW (KSUID), otherwise LEGACY (25-char cuid). + // Resolve by the version char: a v1 body => NEW, otherwise LEGACY (25-char cuid). #resolveById(runId: string): PostgresRunStore { - return runId.length === 27 ? this.#newStore : this.#legacyStore; + return ownerEngine(runId) === "NEW" ? this.#newStore : this.#legacyStore; } // Extract a classifiable run id from a `where`. Prefers `where.id`; if only a - // friendlyId is present we cannot classify by length, so the caller falls back + // friendlyId is present the stub does not classify, so the caller falls back // to read-through (try NEW, then LEGACY). #idFromWhere(where: Prisma.TaskRunWhereInput): string | undefined { const id = (where as { id?: unknown }).id; @@ -56,7 +56,7 @@ class RoutingRunStore implements RunStore { ): Promise { const id = this.#idFromWhere(where); if (id !== undefined) { - // Classifiable by id length β€” route to the owning store, dropping the + // Classifiable by id shape β€” route to the owning store, dropping the // forwarded client so the inner store uses its OWN prisma. return (this.#resolveById(id).findRun as any)(where, argsOrClient); } @@ -222,7 +222,7 @@ function buildRoutingStore(prisma17: PrismaClient, prisma14: PrismaClient) { return new RoutingRunStore(newStore, legacyStore); } -// 25-char cuid-format id (starts with 'c'), length-disjoint from the 27-char KSUID. +// 25-char cuid-format id (starts with "c"), no v1 version marker. function generateLegacyCuid() { const suffix = Array.from( { length: 24 }, @@ -259,10 +259,10 @@ async function seedOrgProjectEnv(prisma: PrismaClient, suffix: string) { describe("UpdateMetadataService store routing (hetero)", () => { heteroPostgresTest( - "routes read+CAS to the owning (NEW/PG17) store for a KSUID run", + "routes read+CAS to the owning (NEW/PG17) store for a run-ops run", async ({ prisma17, prisma14 }) => { - const runId = generateKsuidId(); - expect(runId.length).toBe(27); + const runId = generateRunOpsId(); + expect(runId.length).toBe(26); const { project, organization, runtimeEnvironment } = await seedOrgProjectEnv( prisma17, @@ -326,8 +326,8 @@ describe("UpdateMetadataService store routing (hetero)", () => { heteroPostgresTest( "preserves CAS under concurrent writers on a NEW-DB (PG17) run", async ({ prisma17, prisma14 }) => { - const runId = generateKsuidId(); - expect(runId.length).toBe(27); + const runId = generateRunOpsId(); + expect(runId.length).toBe(26); const { project, organization, runtimeEnvironment } = await seedOrgProjectEnv( prisma17, diff --git a/apps/webapp/test/v3/runOpsMigration/runOpsCascadeCleanup.server.test.ts b/apps/webapp/test/v3/runOpsMigration/runOpsCascadeCleanup.server.test.ts index d925049695..d4ea1eba6a 100644 --- a/apps/webapp/test/v3/runOpsMigration/runOpsCascadeCleanup.server.test.ts +++ b/apps/webapp/test/v3/runOpsMigration/runOpsCascadeCleanup.server.test.ts @@ -554,7 +554,7 @@ describe("RunOpsCascadeCleanupService", () => { ); // The two-writer split β€” an env whose rows straddle both DBs (cuid runs on the LEGACY DB, - // ksuid runs on the NEW DB) is fully cleaned by one call; a single-writer service leaks orphans. + // run-ops runs on the NEW DB) is fully cleaned by one call; a single-writer service leaks orphans. heteroPostgresTest( "two-writer fan-out cleans a split env on both DBs; single-writer leaves orphans", async ({ prisma14, prisma17 }) => { diff --git a/apps/webapp/test/waitpointCallback.controlPlane.test.ts b/apps/webapp/test/waitpointCallback.controlPlane.test.ts index 54dc125be6..8fdac12743 100644 --- a/apps/webapp/test/waitpointCallback.controlPlane.test.ts +++ b/apps/webapp/test/waitpointCallback.controlPlane.test.ts @@ -37,7 +37,7 @@ vi.mock("~/db.server", async () => { runOpsNewPrisma: lazyProxy(replicaHolder, "replicaHolder.client"), runOpsNewReplica: lazyProxy(replicaHolder, "replicaHolder.client"), runOpsLegacyReplica: lazyProxy(replicaHolder, "replicaHolder.client"), - // The route's read-through helper reads this off `~/db.server`; split-on routes ksuid (NEW) + // The route's read-through helper reads this off `~/db.server`; split-on routes run-ops id (NEW) // ids to the `runOpsNewReplica` proxy, which points at the seeded container. runOpsSplitReadEnabled: true, sqlDatabaseSchema: Prisma.sql([`public`]), diff --git a/apps/webapp/test/waitpointPresenter.readthrough.test.ts b/apps/webapp/test/waitpointPresenter.readthrough.test.ts index 4cb5809233..a60562a950 100644 --- a/apps/webapp/test/waitpointPresenter.readthrough.test.ts +++ b/apps/webapp/test/waitpointPresenter.readthrough.test.ts @@ -378,7 +378,7 @@ describe("WaitpointPresenter connected-runs hydrate routed through read-through describe("WaitpointPresenter bare-ctor production default activates readThroughRun", () => { heteroPostgresTest( - "ksuid waitpoint on the new DB resolves via readThroughRun production defaults", + "run-ops waitpoint on the new DB resolves via readThroughRun production defaults", async ({ prisma14, prisma17 }) => { const ctx = await seedParents(prisma17, "proddefault"); const seeded = await seedWaitpoint(prisma17, ctx, "waitpoint_proddefault", { diff --git a/internal-packages/database/package.json b/internal-packages/database/package.json index ec0bc950b8..560cf229dd 100644 --- a/internal-packages/database/package.json +++ b/internal-packages/database/package.json @@ -15,7 +15,7 @@ }, "scripts": { "clean": "rimraf dist", - "generate": "prisma generate", + "generate": "node ../../scripts/retry-prisma-generate.mjs", "db:migrate:dev:create": "prisma migrate dev --create-only", "db:migrate:deploy": "prisma migrate deploy", "db:push": "prisma db push", diff --git a/internal-packages/database/prisma/schema.prisma b/internal-packages/database/prisma/schema.prisma index 6220fc265a..1dc7788f3f 100644 --- a/internal-packages/database/prisma/schema.prisma +++ b/internal-packages/database/prisma/schema.prisma @@ -2365,7 +2365,7 @@ model ProjectAlert { type ProjectAlertType - // Run-ops split: these reference run-subgraph rows that live on the dedicated run-ops DB for ksuid + // Run-ops split: these reference run-subgraph rows that live on the dedicated run-ops DB for run-ops id // runs, so the cross-DB FK can't hold. Scalar-only; the run is resolved via runStore.findRun. taskRunAttemptId String? diff --git a/internal-packages/run-engine/src/engine/errors.ts b/internal-packages/run-engine/src/engine/errors.ts index 170a9dec5b..f5ca48160a 100644 --- a/internal-packages/run-engine/src/engine/errors.ts +++ b/internal-packages/run-engine/src/engine/errors.ts @@ -118,7 +118,7 @@ export class UnclassifiableWaitpointId extends Error { readonly cause?: unknown; constructor(waitpointId: string, options?: { cause?: unknown }) { super( - `Unclassifiable waitpointId for completion: length ${waitpointId.length} matches neither cuid nor ksuid β€” waitpointId=${JSON.stringify( + `Unclassifiable waitpointId for completion: length ${waitpointId.length} matches neither cuid nor run-ops id β€” waitpointId=${JSON.stringify( waitpointId )}` ); diff --git a/internal-packages/run-engine/src/engine/systems/debounceSystem.test.ts b/internal-packages/run-engine/src/engine/systems/debounceSystem.test.ts index f2aa7557fc..342dee4d41 100644 --- a/internal-packages/run-engine/src/engine/systems/debounceSystem.test.ts +++ b/internal-packages/run-engine/src/engine/systems/debounceSystem.test.ts @@ -211,7 +211,7 @@ describe("debounceSystem store routing (single-DB passthrough)", () => { // Even on the tx path the snapshot read routes through the store. // getLatestExecutionSnapshot always passes this.$.runStore, so the read is routed to the - // OWNING DB (correct for split mode β€” a ksuid run's snapshot lives on the dedicated DB, not the + // OWNING DB (correct for split mode β€” a run-ops run's snapshot lives on the dedicated DB, not the // caller's control-plane tx). Driving the locked reschedule inside a tx must still increment the // counting store's snapshot-read counter. containerTest( diff --git a/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.test.ts b/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.test.ts index c52291876c..aeb361019e 100644 --- a/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.test.ts +++ b/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.test.ts @@ -345,7 +345,7 @@ describe("executionSnapshotSystem store routing (cross-version read-through)", ( } ); - // Routing keys off runId, the SnapshotId is a cuid (not a 27-char ksuid), and no + // Routing keys off runId, the SnapshotId is a cuid (not a v1 run-ops id), and no // residency classifier is consulted for the snapshot id (D5). heteroPostgresTest( "snapshots route by owning run id; SnapshotId stays cuid", @@ -356,7 +356,7 @@ describe("executionSnapshotSystem store routing (cross-version read-through)", ( const rNew = "run_new_f"; const { snapshot } = await seedRunWithSnapshot(prisma17 as any, newStore, "new_f", rNew); - // cuid is 25 chars (c + 24); a ksuid friendly id is 27 chars. The snapshot id is a cuid. + // cuid is 25 chars (c + 24); a v1 run-ops body is 26 chars ending in "1". The snapshot id is a cuid. expect(snapshot.id.length).toBe(25); const router = new TwoStoreSnapshotRouter(newStore, legacyStore, [rNew]); diff --git a/internal-packages/run-engine/src/engine/systems/waitpointSystem.test.ts b/internal-packages/run-engine/src/engine/systems/waitpointSystem.test.ts index 87ad9dd352..050b3e77fb 100644 --- a/internal-packages/run-engine/src/engine/systems/waitpointSystem.test.ts +++ b/internal-packages/run-engine/src/engine/systems/waitpointSystem.test.ts @@ -1396,7 +1396,7 @@ describe("WaitpointSystem completion fan-out + residency store-selection guard", { timeout: 15_000, interval: 100 } ); - // (c) the load-bearing no-op: an unclassifiable id (length 26) must NOT throw + // (c) the load-bearing no-op: an unrecognized id (26 "a"s, no version marker) must NOT throw // UnclassifiableWaitpointId under the default single store β€” the classifier is // never consulted. It finds no PENDING row, the re-read fails, and the ordinary // "Waitpoint not found" surfaces instead. @@ -1415,7 +1415,7 @@ describe("WaitpointSystem completion fan-out + residency store-selection guard", // ----- Group 5: cross-seam two-store + loud-ambiguity + pinning ----- - // Cross-seam completion applied to the OWNING store: a ksuid waitpoint resides on the dedicated + // Cross-seam completion applied to the OWNING store: a run-ops waitpoint resides on the dedicated // run-ops (NEW) DB, a cuid waitpoint on the legacy/control-plane DB. Driving the completion at the // store seam (forWaitpointCompletion -> updateManyWaitpoints, as the engine does) must apply each // completion to its owning store only. @@ -1429,17 +1429,17 @@ describe("WaitpointSystem completion fan-out + residency store-selection guard", const envLegacy = await seedHeteroEnvironment(prisma14, "csl"); const envNew = await seedHeteroEnvironment(prisma17, "csn"); - // 27-char body => ksuid => NEW (dedicated run-ops DB); 25-char body => cuid => LEGACY. - const ksuidId = "waitpoint_" + "a".repeat(27); + // v1 body (26 chars, version "1" at index 25) => run-ops id => NEW (dedicated run-ops DB); 25-char body => cuid => LEGACY. + const runOpsId = "waitpoint_" + "a".repeat(24) + "01"; const cuidId = "waitpoint_" + "b".repeat(25); await prisma17.waitpoint.create({ data: { - id: ksuidId, + id: runOpsId, friendlyId: "waitpoint_ks", type: "MANUAL", status: "PENDING", - idempotencyKey: `idem_${ksuidId}`, + idempotencyKey: `idem_${runOpsId}`, userProvidedIdempotencyKey: false, projectId: envNew.projectId, environmentId: envNew.id, @@ -1459,9 +1459,9 @@ describe("WaitpointSystem completion fan-out + residency store-selection guard", }); const completedAt = new Date(); - const ownerKsuid = await router.forWaitpointCompletion(ksuidId, { routeKind: "MANUAL" }); - await ownerKsuid.updateManyWaitpoints({ - where: { id: ksuidId, status: "PENDING" }, + const runOpsOwner = await router.forWaitpointCompletion(runOpsId, { routeKind: "MANUAL" }); + await runOpsOwner.updateManyWaitpoints({ + where: { id: runOpsId, status: "PENDING" }, data: { status: "COMPLETED", completedAt }, }); const ownerCuid = await router.forWaitpointCompletion(cuidId, { routeKind: "MANUAL" }); @@ -1470,11 +1470,11 @@ describe("WaitpointSystem completion fan-out + residency store-selection guard", data: { status: "COMPLETED", completedAt }, }); - // ksuid completed on the dedicated run-ops (NEW) DB only. - expect((await prisma17.waitpoint.findUniqueOrThrow({ where: { id: ksuidId } })).status).toBe( + // run-ops id completed on the dedicated run-ops (NEW) DB only. + expect((await prisma17.waitpoint.findUniqueOrThrow({ where: { id: runOpsId } })).status).toBe( "COMPLETED" ); - expect(await prisma14.waitpoint.findUnique({ where: { id: ksuidId } })).toBeNull(); + expect(await prisma14.waitpoint.findUnique({ where: { id: runOpsId } })).toBeNull(); // cuid completed on the legacy DB only. expect((await prisma14.waitpoint.findUniqueOrThrow({ where: { id: cuidId } })).status).toBe( "COMPLETED" @@ -1484,7 +1484,7 @@ describe("WaitpointSystem completion fan-out + residency store-selection guard", ); // Ambiguity resolution: forWaitpointCompletion safe-classifies an id matching neither cuid nor - // ksuid to LEGACY, then probes both DBs. With no row anywhere it resolves to the LEGACY fallback + // run-ops id to LEGACY, then probes both DBs. With no row anywhere it resolves to the LEGACY fallback // rather than throwing β€” the loud-failure contract lives at the engine seam (completeWaitpoint // re-reads and surfaces "Waitpoint not found"). The residency probe made this method async. heteroPostgresTest( @@ -1500,7 +1500,7 @@ describe("WaitpointSystem completion fan-out + residency store-selection guard", } ); - // Pinning proof: a cross-tree-idempotency completion of a ksuid + // Pinning proof: a cross-tree-idempotency completion of a run-ops id // (NEW residency) waitpoint pins to the LEGACY store. heteroPostgresTest( "cross-seam cross-tree-idempotency completion pins to legacy", @@ -1510,8 +1510,8 @@ describe("WaitpointSystem completion fan-out + residency store-selection guard", const router = new RoutingRunStore({ new: newStore, legacy }); // pin is DRIVEN via explicit context at the store seam; the engine completeWaitpoint entry cannot derive it β€” the organic cross-tree-idempotency pin is applied at the webapp idempotency caller. - const ksuidId = "waitpoint_" + "a".repeat(27); - const handle = await router.forWaitpointCompletion(ksuidId, { + const runOpsId = "waitpoint_" + "a".repeat(24) + "01"; + const handle = await router.forWaitpointCompletion(runOpsId, { routeKind: "IDEMPOTENCY_REUSE", isCrossTreeIdempotency: true, }); diff --git a/internal-packages/run-engine/src/engine/systems/waitpointSystem.ts b/internal-packages/run-engine/src/engine/systems/waitpointSystem.ts index 6687cdb907..344a0c1d8c 100644 --- a/internal-packages/run-engine/src/engine/systems/waitpointSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/waitpointSystem.ts @@ -229,7 +229,7 @@ export class WaitpointSystem { // Co-location invariant: a DATETIME wait waitpoint lives on the same run-ops DB as the run that // blocks on it (so the block edge's local `Waitpoint` join resolves and completion/resume stay // local). The minted waitpoint id is always a cuid, so without `coLocateWithRunId` the upsert - // would always route to LEGACY and a ksuid run on NEW would hang. The (env,idempotencyKey) dedup + // would always route to LEGACY and a run-ops run on NEW would hang. The (env,idempotencyKey) dedup // is within the owning run/tree (co-resident on one DB), so the dedup probe + rotation target the // SAME store. With no run id (a standalone token has no owning run yet) the lookup falls back to // a cross-DB NEW-then-LEGACY scan and the upsert routes by id-shape. A caller-supplied tx pins a diff --git a/internal-packages/run-engine/src/engine/tests/blockEdgeResidency.test.ts b/internal-packages/run-engine/src/engine/tests/blockEdgeResidency.test.ts index f4d64a844e..e930b1cfd5 100644 --- a/internal-packages/run-engine/src/engine/tests/blockEdgeResidency.test.ts +++ b/internal-packages/run-engine/src/engine/tests/blockEdgeResidency.test.ts @@ -1,6 +1,6 @@ // Block-edge write goes to the wrong DB so the parent never suspends. Two-physical-DB topology with // the real dedicated run-ops schema on #new (prisma17). RED before the fix: the control-plane tx -// threaded by RunEngine.trigger forces the raw CTE to join `Waitpoint` on #legacy, where the ksuid +// threaded by RunEngine.trigger forces the raw CTE to join `Waitpoint` on #legacy, where the run-ops id // waitpoint does not exist, so 0 edges are written and the parent stays EXECUTING. GREEN after: the // block path always routes through the store, landing the edge + WaitpointRunConnection on #new and // suspending the parent. (Snapshot reads/writes route by run id regardless of tx.) @@ -29,9 +29,9 @@ const twoDbEngineTest = heteroRunOpsPostgresTest.extend<{ redisOptions, }); -// ksuid (27-char internal id) β†’ classified NEW β†’ routed to the run-ops (#new) store. -const KSUID_A = "k".repeat(27); -const KSUID_B = "m".repeat(27); +// run-ops id (v1 internal id, version "1" at index 25) β†’ classified NEW β†’ routed to the run-ops (#new) store. +const RUN_OPS_A = "k".repeat(24) + "01"; +const RUN_OPS_B = "m".repeat(24) + "01"; function baseEngineOptions(redisOptions: any, prisma: any) { return { @@ -128,9 +128,9 @@ function buildCreateRunInput(params: { }; } -// Seed an EXECUTING ksuid parent run on #new (prisma17) via the routed store, then a ksuid PENDING +// Seed an EXECUTING run-ops parent run on #new (prisma17) via the routed store, then a run-ops id PENDING // RUN waitpoint co-resident on #new. Returns the env + ids the block path needs. -async function seedExecutingKsuidParent( +async function seedExecutingRunOpsParent( prisma14: PrismaClient, prisma17: RunOpsPrismaClient, router: RoutingRunStore, @@ -166,7 +166,7 @@ async function seedExecutingKsuidParent( prisma14 ); - // The associated waitpoint lives on #new (co-resident with the ksuid run). + // The associated waitpoint lives on #new (co-resident with the run-ops run). await prisma17.waitpoint.create({ data: { id: waitpointId, @@ -198,11 +198,11 @@ function makeRouter(prisma14: PrismaClient, prisma17: RunOpsPrismaClient) { } describe("RunEngine block-edge residency (two physical DBs, dedicated #new)", () => { - // RED before fix / GREEN after: a ksuid parent blocked by a #new-resident waitpoint, with the + // RED before fix / GREEN after: a run-ops parent blocked by a #new-resident waitpoint, with the // control-plane tx threaded exactly as RunEngine.trigger does, ends EXECUTING_WITH_WAITPOINTS with // the edge + WaitpointRunConnection physically on #new. twoDbEngineTest( - "blockRunWithWaitpoint suspends a ksuid parent with the edge on #new (control-plane tx threaded)", + "blockRunWithWaitpoint suspends a run-ops parent with the edge on #new (control-plane tx threaded)", async ({ prisma14, prisma17, redisOptions }) => { const router = makeRouter(prisma14 as unknown as PrismaClient, prisma17); const engine = new RunEngine({ @@ -211,9 +211,9 @@ describe("RunEngine block-edge residency (two physical DBs, dedicated #new)", () }); try { - const parentRunId = `run_${KSUID_A}`; - const waitpointId = `waitpoint_${KSUID_A}`; - const env = await seedExecutingKsuidParent( + const parentRunId = `run_${RUN_OPS_A}`; + const waitpointId = `waitpoint_${RUN_OPS_A}`; + const env = await seedExecutingRunOpsParent( prisma14 as unknown as PrismaClient, prisma17, router, @@ -267,9 +267,9 @@ describe("RunEngine block-edge residency (two physical DBs, dedicated #new)", () }); try { - const parentRunId = `run_${KSUID_B}`; - const waitpointId = `waitpoint_${KSUID_B}`; - const env = await seedExecutingKsuidParent( + const parentRunId = `run_${RUN_OPS_B}`; + const waitpointId = `waitpoint_${RUN_OPS_B}`; + const env = await seedExecutingRunOpsParent( prisma14 as unknown as PrismaClient, prisma17, router, @@ -283,7 +283,7 @@ describe("RunEngine block-edge residency (two physical DBs, dedicated #new)", () runId: parentRunId, waitpoints: waitpointId, projectId: env.project.id, - batch: { id: `batch_${KSUID_B}`, index: 0 }, + batch: { id: `batch_${RUN_OPS_B}`, index: 0 }, tx: prisma14 as unknown as PrismaClient, }); diff --git a/internal-packages/run-engine/src/engine/tests/completeWaitpointReadResidency.test.ts b/internal-packages/run-engine/src/engine/tests/completeWaitpointReadResidency.test.ts index dc1d7e3509..251d556d96 100644 --- a/internal-packages/run-engine/src/engine/tests/completeWaitpointReadResidency.test.ts +++ b/internal-packages/run-engine/src/engine/tests/completeWaitpointReadResidency.test.ts @@ -3,7 +3,7 @@ // control-plane client. Two-physical-DB topology with the real dedicated run-ops schema on // #new (prisma17), modelled on the block-edge residency test. // -// RED before the fix: completeWaitpoint resolved the #new store (where the ksuid RUN waitpoint +// RED before the fix: completeWaitpoint resolved the #new store (where the run-ops id RUN waitpoint // lives), marked it COMPLETED there, then re-read it via `store.findWaitpoint({where:{id}}, this.$.prisma)`. // A resolved PostgresRunStore HONORS the passed client, so the re-read hit the control-plane DB // (#legacy / prisma14), found nothing, and threw "Waitpoint not found" BEFORE enqueueing @@ -35,10 +35,10 @@ const twoDbEngineTest = heteroRunOpsPostgresTest.extend<{ redisOptions, }); -// ksuid (27-char internal id) β†’ classified NEW β†’ routed to the run-ops (#new) store. -const KSUID_A = "n".repeat(27); -// A second ksuid run for the cross-DB (NEW-run β†’ LEGACY-token) case. -const KSUID_X = "x".repeat(27); +// run-ops id (v1 internal id, version "1" at index 25) β†’ classified NEW β†’ routed to the run-ops (#new) store. +const RUN_OPS_A = "n".repeat(24) + "01"; +// A second run-ops run for the cross-DB (NEW-run β†’ LEGACY-token) case. +const RUN_OPS_X = "k".repeat(24) + "01"; // cuid (25-char) β†’ classified LEGACY β†’ a standalone token resident on #legacy (prisma14). const CUID_25 = "c".repeat(25); @@ -134,9 +134,9 @@ function buildCreateRunInput(params: { }; } -// Seed an EXECUTING ksuid parent on #new (prisma17) via the routed store, plus a ksuid PENDING RUN +// Seed an EXECUTING run-ops parent on #new (prisma17) via the routed store, plus a run-ops id PENDING RUN // waitpoint co-resident on #new. Returns the env + ids the block/complete path needs. -async function seedExecutingKsuidParent( +async function seedExecutingRunOpsParent( prisma14: PrismaClient, prisma17: RunOpsPrismaClient, router: RoutingRunStore, @@ -170,7 +170,7 @@ async function seedExecutingKsuidParent( prisma14 ); - // The RUN waitpoint lives on #new, co-resident with the ksuid run, and is completed-by that run. + // The RUN waitpoint lives on #new, co-resident with the run-ops run, and is completed-by that run. await prisma17.waitpoint.create({ data: { id: waitpointId, @@ -188,10 +188,10 @@ async function seedExecutingKsuidParent( return env; } -// Seed an EXECUTING ksuid parent on #new (prisma17) AND a standalone MANUAL token resident on +// Seed an EXECUTING run-ops parent on #new (prisma17) AND a standalone MANUAL token resident on // #legacy (prisma14, cuid) β€” the tolerated NEW-run β†’ LEGACY-token cross-DB direction (standalone // tokens are minted on LEGACY). The token is NOT created on #new. Returns both envs + ids. -async function seedKsuidParentAndLegacyToken( +async function seedRunOpsParentAndLegacyToken( prisma14: PrismaClient, prisma17: RunOpsPrismaClient, router: RoutingRunStore, @@ -267,9 +267,9 @@ describe("RunEngine completeWaitpoint re-read residency (two physical DBs, dedic }); try { - const parentRunId = `run_${KSUID_A}`; - const waitpointId = `waitpoint_${KSUID_A}`; - const env = await seedExecutingKsuidParent( + const parentRunId = `run_${RUN_OPS_A}`; + const waitpointId = `waitpoint_${RUN_OPS_A}`; + const env = await seedExecutingRunOpsParent( prisma14 as unknown as PrismaClient, prisma17, router, @@ -335,7 +335,7 @@ describe("RunEngine completeWaitpoint re-read residency (two physical DBs, dedic } ); - // End-to-end cross-DB gate: a ksuid run on #new blocked on a standalone MANUAL token resident on + // End-to-end cross-DB gate: a run-ops run on #new blocked on a standalone MANUAL token resident on // #legacy (the tolerated NEW-run β†’ LEGACY-token direction β€” standalone tokens are minted on // LEGACY). RED before the writer fix: blockRunWithWaitpointEdges' dedicated branch joined // `FROM "Waitpoint" w`, which matched 0 rows on #new (the token is on #legacy) β†’ 0 edges β†’ the run @@ -344,7 +344,7 @@ describe("RunEngine completeWaitpoint re-read residency (two physical DBs, dedic // token (the completion fan-out discovers the #new edge and resolves its COMPLETED status across // both DBs) resumes the NEW run. twoDbEngineTest( - "completeWaitpoint on a LEGACY-resident token unblocks a ksuid run whose edge lives on #new", + "completeWaitpoint on a LEGACY-resident token unblocks a run-ops run whose edge lives on #new", async ({ prisma14, prisma17, redisOptions }) => { const router = makeRouter(prisma14 as unknown as PrismaClient, prisma17); const engine = new RunEngine({ @@ -353,9 +353,9 @@ describe("RunEngine completeWaitpoint re-read residency (two physical DBs, dedic }); try { - const parentRunId = `run_${KSUID_X}`; // ksuid run β†’ #new + const parentRunId = `run_${RUN_OPS_X}`; // run-ops run β†’ #new const waitpointId = `waitpoint_${CUID_25}`; // cuid standalone token β†’ #legacy - const env = await seedKsuidParentAndLegacyToken( + const env = await seedRunOpsParentAndLegacyToken( prisma14 as unknown as PrismaClient, prisma17, router, @@ -374,7 +374,7 @@ describe("RunEngine completeWaitpoint re-read residency (two physical DBs, dedic tx: prisma14 as unknown as PrismaClient, }); - // The block edge is physically on #new; #legacy holds none for the ksuid run (safety invariant). + // The block edge is physically on #new; #legacy holds none for the run-ops run (safety invariant). expect(await prisma17.taskRunWaitpoint.count({ where: { taskRunId: parentRunId } })).toBe( 1 ); diff --git a/internal-packages/run-engine/src/engine/tests/datetimeWaitpointColocation.test.ts b/internal-packages/run-engine/src/engine/tests/datetimeWaitpointColocation.test.ts index 0468016188..ac47e75296 100644 --- a/internal-packages/run-engine/src/engine/tests/datetimeWaitpointColocation.test.ts +++ b/internal-packages/run-engine/src/engine/tests/datetimeWaitpointColocation.test.ts @@ -1,9 +1,9 @@ // DATETIME / MANUAL waitpoint co-location with the owning run (run-ops split). // // The bug: `wait.for`/`wait.until` (DATETIME) and wait-token (MANUAL) waitpoints over the ~5s -// checkpoint threshold hang a ksuid run forever. `createDateTimeWaitpoint`/`createManualWaitpoint` +// checkpoint threshold hang a run-ops run forever. `createDateTimeWaitpoint`/`createManualWaitpoint` // mint an ALWAYS-cuid WaitpointId, and the routing store routed the upsert by that id β†’ #legacy, -// even though the owning ksuid run lives on #new. `blockRunWithWaitpoint` then writes its block edge +// even though the owning run-ops run lives on #new. `blockRunWithWaitpoint` then writes its block edge // on #new (routed by run id), but the CTE joins `Waitpoint` LOCALLY on #new β€” where the // waitpoint does not exist β€” so it writes 0 edges and the run is never actually blocked nor resumed. // @@ -35,11 +35,11 @@ const twoDbEngineTest = heteroRunOpsPostgresTest.extend<{ redisOptions, }); -// ksuid (27-char internal id) β†’ classified NEW β†’ routed to the run-ops (#new) store. -const KSUID_A = "k".repeat(27); -const KSUID_B = "m".repeat(27); -const KSUID_C = "n".repeat(27); -const KSUID_D = "p".repeat(27); +// run-ops id (v1 internal id, version "1" at index 25) β†’ classified NEW β†’ routed to the run-ops (#new) store. +const RUN_OPS_A = "k".repeat(24) + "01"; +const RUN_OPS_B = "m".repeat(24) + "01"; +const RUN_OPS_C = "n".repeat(24) + "01"; +const RUN_OPS_D = "p".repeat(24) + "01"; function baseEngineOptions(redisOptions: any, prisma: any) { return { @@ -133,8 +133,8 @@ function buildCreateRunInput(params: { }; } -// Seed an EXECUTING ksuid run on #new (prisma17) via the routed store. Returns the env + run id. -async function seedExecutingKsuidRun( +// Seed an EXECUTING run-ops run on #new (prisma17) via the routed store. Returns the env + run id. +async function seedExecutingRunOpsRun( prisma14: PrismaClient, router: RoutingRunStore, runId: string, @@ -184,7 +184,7 @@ function makeRouter(prisma14: PrismaClient, prisma17: RunOpsPrismaClient) { } describe("DATETIME/MANUAL waitpoint co-location with the owning run (two physical DBs)", () => { - // RED before fix: the DATETIME waitpoint created for a ksuid run lands on #legacy (routed by its + // RED before fix: the DATETIME waitpoint created for a run-ops run lands on #legacy (routed by its // own cuid id), so the block edge (on #new) finds no local waitpoint and the run never blocks/resumes. // GREEN after: the waitpoint co-locates on #new, the edge resolves, and the run resumes once the // datetime waitpoint completes via the engine's finishWaitpoint timer. @@ -196,8 +196,8 @@ describe("DATETIME/MANUAL waitpoint co-location with the owning run (two physica const engine = new RunEngine({ store: router, ...baseEngineOptions(redisOptions, prisma14) }); try { - const runId = `run_${KSUID_A}`; - const env = await seedExecutingKsuidRun(p14, router, runId, "dta"); + const runId = `run_${RUN_OPS_A}`; + const env = await seedExecutingRunOpsRun(p14, router, runId, "dta"); // ~600ms out so the finishWaitpoint timer fires within the test window. const date = new Date(Date.now() + 600); @@ -255,8 +255,8 @@ describe("DATETIME/MANUAL waitpoint co-location with the owning run (two physica const engine = new RunEngine({ store: router, ...baseEngineOptions(redisOptions, prisma14) }); try { - const runId = `run_${KSUID_B}`; - const env = await seedExecutingKsuidRun(p14, router, runId, "mna"); + const runId = `run_${RUN_OPS_B}`; + const env = await seedExecutingRunOpsRun(p14, router, runId, "mna"); const { waitpoint } = await engine.createManualWaitpoint({ runId, @@ -310,8 +310,8 @@ describe("DATETIME/MANUAL waitpoint co-location with the owning run (two physica const engine = new RunEngine({ store: router, ...baseEngineOptions(redisOptions, prisma14) }); try { - const runId = `run_${KSUID_C}`; - const env = await seedExecutingKsuidRun(p14, router, runId, "idem"); + const runId = `run_${RUN_OPS_C}`; + const env = await seedExecutingRunOpsRun(p14, router, runId, "idem"); const idempotencyKey = "dedup-key-1"; const date = new Date(Date.now() + 60_000); @@ -364,8 +364,8 @@ describe("DATETIME/MANUAL waitpoint co-location with the owning run (two physica const engine = new RunEngine({ store: router, ...baseEngineOptions(redisOptions, prisma14) }); try { - const runId = `run_${KSUID_D}`; - const env = await seedExecutingKsuidRun(p14, router, runId, "idemm"); + const runId = `run_${RUN_OPS_D}`; + const env = await seedExecutingRunOpsRun(p14, router, runId, "idemm"); const idempotencyKey = "dedup-key-2"; const first = await engine.createManualWaitpoint({ diff --git a/internal-packages/run-engine/src/engine/tests/lifecycleRouter.test.ts b/internal-packages/run-engine/src/engine/tests/lifecycleRouter.test.ts index 5bcbfac8ce..cd24ba1255 100644 --- a/internal-packages/run-engine/src/engine/tests/lifecycleRouter.test.ts +++ b/internal-packages/run-engine/src/engine/tests/lifecycleRouter.test.ts @@ -452,14 +452,22 @@ describe("RunEngine lifecycle read routing (single-DB)", () => { // Read-through / cross-version proofs (PG14 legacy <-> PG17 run-ops). These test // the routing layer the engine's threaded reads delegate to: a real RoutingRunStore // over two real PostgresRunStores on two real containers (NEVER mocked). A new run -// (ksuid id, born on PG17) resolves from the run-ops store; an old in-retention run +// (run-ops id, born on PG17) resolves from the run-ops store; an old in-retention run // (cuid id, on PG14) reads THROUGH the legacy store's read-only (replica) client. // --------------------------------------------------------------------------- -// A cuid-length (25-char) internal id β†’ classifies LEGACY; a ksuid-length (27-char) +// A cuid-length (25-char) internal id β†’ classifies LEGACY; a v1-shaped (26-char, version "1") // internal id β†’ classifies NEW. The `run_` prefix is stripped before classification. const legacyRunId = (suffix: string) => `run_${suffix.padEnd(25, "0").slice(0, 25)}`; -const newRunId = (suffix: string) => `run_${suffix.padEnd(27, "0").slice(0, 27)}`; +// Map each suffix char into the base32hex alphabet by code point (not a lossy outlierβ†’"0" replace, +// which collapsed suffixes differing only in out-of-range chars): 24-char core + region + version. +const BASE32HEX = "0123456789abcdefghijklmnopqrstuv"; +const newRunId = (suffix: string) => + `run_${[...suffix] + .map((ch) => BASE32HEX[ch.charCodeAt(0) % 32]) + .join("") + .padEnd(24, "0") + .slice(0, 24)}01`; async function seedRunWithSnapshot( prisma: PrismaClient, @@ -527,7 +535,7 @@ async function seedRunWithSnapshot( } describe("RunEngine lifecycle read-through routing (PG14/PG17)", () => { - // A NEW run (ksuid id) seeded only on the run-ops (PG17/new) store resolves + // A NEW run (run-ops id) seeded only on the run-ops (PG17/new) store resolves // its latest snapshot from that store, and the legacy store is never touched. heteroPostgresTest( "a new run resolves its latest snapshot from the run-ops store", diff --git a/internal-packages/run-engine/src/engine/tests/triggerCreateRouting.test.ts b/internal-packages/run-engine/src/engine/tests/triggerCreateRouting.test.ts index f27ef6b857..34353cb44e 100644 --- a/internal-packages/run-engine/src/engine/tests/triggerCreateRouting.test.ts +++ b/internal-packages/run-engine/src/engine/tests/triggerCreateRouting.test.ts @@ -7,7 +7,7 @@ import { type CreateFailedRunInput, type CreateRunInput, } from "@internal/run-store"; -import { RunId, ownerEngine, generateKsuidId } from "@trigger.dev/core/v3/isomorphic"; +import { RunId, ownerEngine, generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import type { PrismaClientOrTransaction } from "@trigger.dev/database"; import { expect } from "vitest"; import { RunEngine } from "../index.js"; @@ -100,8 +100,8 @@ function freshRunId() { return RunId.generate().friendlyId; } -function freshKsuidRunId() { - return RunId.toFriendlyId(generateKsuidId()); +function freshRunOpsRunId() { + return RunId.toFriendlyId(generateRunOpsId()); } const baseTriggerParams = (friendlyId: string, environment: any, taskIdentifier: string) => ({ @@ -381,7 +381,7 @@ describe("RunEngine trigger/create routing", () => { } ); - // Split/two-store proof: with the ksuid mint enabled, a NEW-minted run id is + // Split/two-store proof: with the run-ops id mint enabled, a NEW-minted run id is // classified NEW and a RoutingRunStore writes it to the run-ops (NEW) store, // never the LEGACY store. Proves a new run is born on the run-ops store. containerTest( @@ -405,7 +405,7 @@ describe("RunEngine trigger/create routing", () => { const taskIdentifier = "test-task"; await setupBackgroundWorker(engine, environment, taskIdentifier); - const friendlyId = freshKsuidRunId(); + const friendlyId = freshRunOpsRunId(); // Sanity: this id classifies NEW so RoutingRunStore must pick newStore. expect(ownerEngine(friendlyId)).toBe("NEW"); @@ -426,7 +426,7 @@ describe("RunEngine trigger/create routing", () => { // A child triggered with the parent's residency persists to the // SAME store the parent was written to (routing-by-run-id). Both parent and - // child mint NEW (ksuid) ids β†’ both land on newStore. + // child mint NEW (run-ops id) ids β†’ both land on newStore. containerTest("child inherits the parent's residency store", async ({ prisma, redisOptions }) => { const environment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); const newStore = new CountingRunStore({ prisma, readOnlyPrisma: prisma, label: "new" }); @@ -448,7 +448,7 @@ describe("RunEngine trigger/create routing", () => { await setupBackgroundWorker(engine, environment, [parentTask, childTask]); const parentRun = await engine.trigger( - baseTriggerParams(freshKsuidRunId(), environment, parentTask) + baseTriggerParams(freshRunOpsRunId(), environment, parentTask) ); await engine.dequeueFromWorkerQueue({ consumerId: "test", workerQueue: "main" }); @@ -459,7 +459,7 @@ describe("RunEngine trigger/create routing", () => { }); const childRun = await engine.trigger({ - ...baseTriggerParams(freshKsuidRunId(), environment, childTask), + ...baseTriggerParams(freshRunOpsRunId(), environment, childTask), resumeParentOnCompletion: true, parentTaskRunId: parentRun.id, rootTaskRunId: parentRun.id, diff --git a/internal-packages/run-ops-database/package.json b/internal-packages/run-ops-database/package.json index cbeb0279b0..7bfe0fe271 100644 --- a/internal-packages/run-ops-database/package.json +++ b/internal-packages/run-ops-database/package.json @@ -25,7 +25,7 @@ }, "scripts": { "clean": "rimraf dist", - "generate": "prisma generate", + "generate": "node ../../scripts/retry-prisma-generate.mjs", "db:migrate:dev:create": "prisma migrate dev --create-only", "db:migrate:deploy": "node scripts/migrate.mjs deploy", "db:migrate:status": "node scripts/migrate.mjs status", diff --git a/internal-packages/run-store/src/PostgresRunStore.batchProbeReadClient.test.ts b/internal-packages/run-store/src/PostgresRunStore.batchProbeReadClient.test.ts index ec6ec5cd3e..54e76feba4 100644 --- a/internal-packages/run-store/src/PostgresRunStore.batchProbeReadClient.test.ts +++ b/internal-packages/run-store/src/PostgresRunStore.batchProbeReadClient.test.ts @@ -1,13 +1,13 @@ // REDβ†’GREEN repro for the run-ops split BASELINE BLOCKER: // RoutingRunStore cross-DB PROBE reads forward the caller's control-plane `client` into the #new // sub-store probe, so #new queries the CONTROL-PLANE DB instead of its own (5434) and never finds a -// ksuid-resident batch/attempt β†’ returns null. Live effect: batchSystem.#tryCompleteBatch calls +// run-ops id-resident batch/attempt β†’ returns null. Live effect: batchSystem.#tryCompleteBatch calls // `runStore.findBatchTaskRunById(batchId, undefined, this.$.prisma)` β†’ null β†’ "batch doesn't exist" // β†’ the batch waitpoint is never completed β†’ every `batchTriggerAndWait` parent hangs forever. // // `heteroRunOpsPostgresTest` gives a REAL split topology: prisma17 = real RunOpsPrismaClient over the // dedicated subset schema (#new / 5434), prisma14 = full legacy schema on a SEPARATE physical PG -// container (#legacy / control-plane). NEVER mocked. The repro seeds a ksuid batch (and a ksuid +// container (#legacy / control-plane). NEVER mocked. The repro seeds a run-ops batch (and a run-ops id // attempt) on #new and probes via the router passing the LEGACY client as the read client β€” exactly // as the live caller does. RED before the fix (router forwards the client β†’ #new reads control-plane // β†’ null); GREEN after (router drops the client β†’ #new reads its own DB β†’ finds the row). @@ -23,9 +23,9 @@ import type { RunStoreSchemaVariant } from "./types.js"; type AnyClient = PrismaClient | RunOpsPrismaClient; // ownerEngine classifies by internal-id LENGTH (runOpsResidency.ts): 25 chars β†’ cuid β†’ LEGACY, -// 27 chars β†’ ksuid β†’ NEW. +// a v1 body (26 chars, version "1" at index 25) β†’ NEW. const CUID_25 = "c".repeat(25); // β†’ LEGACY (#legacy / prisma14, full schema) -const KSUID_27 = "k".repeat(27); // β†’ NEW (#new / prisma17, dedicated subset schema) +const NEW_ID_26 = "k".repeat(24) + "01"; // β†’ NEW (#new / prisma17, dedicated subset schema) async function seedEnvironment( prisma: AnyClient, @@ -96,13 +96,13 @@ describe("run-ops split β€” cross-DB probe reads must NOT forward the caller's c // findBatchTaskRunById β€” the live batchTriggerAndWait hang: #tryCompleteBatch probes with the // control-plane client, which the router forwarded into #new β†’ #new read the wrong DB β†’ null. heteroRunOpsPostgresTest( - "findBatchTaskRunById FINDS a ksuid batch on #new even when probed with the LEGACY (control-plane) client", + "findBatchTaskRunById FINDS a run-ops batch on #new even when probed with the LEGACY (control-plane) client", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "batchprobe_new"); - const batchId = `batch_${KSUID_27}`; // ksuid β†’ #new + const batchId = `batch_${NEW_ID_26}`; // run-ops id β†’ #new - // Seed the batch directly on #new (5434), exactly where a runEngine-routed ksuid batch lives. + // Seed the batch directly on #new (5434), exactly where a runEngine-routed run-ops batch lives. await prisma17.batchTaskRun.create({ data: { id: batchId, @@ -151,11 +151,11 @@ describe("run-ops split β€” cross-DB probe reads must NOT forward the caller's c // findBatchTaskRunByFriendlyId β€” same anti-pattern (env-scoped friendlyId probe). heteroRunOpsPostgresTest( - "findBatchTaskRunByFriendlyId FINDS a ksuid batch on #new despite the LEGACY client", + "findBatchTaskRunByFriendlyId FINDS a run-ops batch on #new despite the LEGACY client", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "batchfid_new"); - const batchId = `batch_${KSUID_27}`; + const batchId = `batch_${NEW_ID_26}`; const friendlyId = "batch_fid_new"; await prisma17.batchTaskRun.create({ @@ -180,11 +180,11 @@ describe("run-ops split β€” cross-DB probe reads must NOT forward the caller's c // findBatchTaskRunByIdempotencyKey β€” same anti-pattern (env + idempotency-key probe). heteroRunOpsPostgresTest( - "findBatchTaskRunByIdempotencyKey FINDS a ksuid batch on #new despite the LEGACY client", + "findBatchTaskRunByIdempotencyKey FINDS a run-ops batch on #new despite the LEGACY client", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "batchidem_new"); - const batchId = `batch_${KSUID_27}`; + const batchId = `batch_${NEW_ID_26}`; const idempotencyKey = "idem_batch_new"; await prisma17.batchTaskRun.create({ @@ -209,14 +209,14 @@ describe("run-ops split β€” cross-DB probe reads must NOT forward the caller's c ); // findTaskRunAttempt β€” same anti-pattern. A classifiable taskRunId routes to the owning store - // (#new for a ksuid run) but the control-plane client was still forwarded into it. + // (#new for a run-ops run) but the control-plane client was still forwarded into it. heteroRunOpsPostgresTest( - "findTaskRunAttempt FINDS a ksuid attempt on #new even when probed with the LEGACY client", + "findTaskRunAttempt FINDS a run-ops id attempt on #new even when probed with the LEGACY client", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "attempt_new"); - const runId = `run_${KSUID_27}`; // ksuid run β†’ #new - const attemptId = `attempt_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; // run-ops run β†’ #new + const attemptId = `attempt_${NEW_ID_26}`; // The attempt's owning run lives on #new (the FK is co-resident on the dedicated schema). await prisma17.taskRun.create({ @@ -247,10 +247,10 @@ describe("run-ops split β€” cross-DB probe reads must NOT forward the caller's c number: 1, friendlyId: "attempt_fid_new", taskRunId: runId, - backgroundWorkerId: `bw_${KSUID_27}`, - backgroundWorkerTaskId: `bwt_${KSUID_27}`, + backgroundWorkerId: `bw_${NEW_ID_26}`, + backgroundWorkerTaskId: `bwt_${NEW_ID_26}`, runtimeEnvironmentId: env.environment.id, - queueId: `queue_${KSUID_27}`, + queueId: `queue_${NEW_ID_26}`, status: "PENDING", }, }); @@ -274,7 +274,7 @@ describe("run-ops split β€” cross-DB probe reads must NOT forward the caller's c // Single-DB config: both slots point at the same dedicated store (split effectively OFF). const router = new RoutingRunStore({ new: newStore, legacy: newStore }); const env = await seedEnvironment(prisma17, "dedicated", "splitoff_new"); - const batchId = `batch_${KSUID_27}`; + const batchId = `batch_${NEW_ID_26}`; await prisma17.batchTaskRun.create({ data: { diff --git a/internal-packages/run-store/src/PostgresRunStore.controlPlaneAlertFk.test.ts b/internal-packages/run-store/src/PostgresRunStore.controlPlaneAlertFk.test.ts index 7dfffb4ee6..12a438870a 100644 --- a/internal-packages/run-store/src/PostgresRunStore.controlPlaneAlertFk.test.ts +++ b/internal-packages/run-store/src/PostgresRunStore.controlPlaneAlertFk.test.ts @@ -1,5 +1,5 @@ -// ProjectAlert.taskRunId/taskRunAttemptId FKs point INTO the run subgraph. A ksuid run lives ONLY -// on the dedicated run-ops DB (prisma17), so `projectAlert.create({ taskRunId: })` on +// ProjectAlert.taskRunId/taskRunAttemptId FKs point INTO the run subgraph. A run-ops run lives ONLY +// on the dedicated run-ops DB (prisma17), so `projectAlert.create({ taskRunId: })` on // control-plane (prisma14) violates the FK and the alert is silently dropped. After the FK drop + // @relation removal the create succeeds; the read path resolves the run via runStore.findRun. // Asserts the create succeeds: it fails with an FK violation before the fix and succeeds after. @@ -9,8 +9,8 @@ import type { PrismaClient } from "@trigger.dev/database"; import type { RunOpsPrismaClient } from "@internal/run-ops-database"; import { describe, expect } from "vitest"; -// 27-char internal id β†’ ksuid β†’ NEW (lives only on the dedicated run-ops DB). -const KSUID_27 = "k".repeat(27); +// v1 internal id (26 chars, version "1" at index 25) β†’ NEW (lives only on the dedicated run-ops DB). +const NEW_ID_26 = "k".repeat(24) + "01"; async function seedControlPlaneAlertPrereqs(prisma: PrismaClient, suffix: string) { const organization = await prisma.organization.create({ @@ -50,9 +50,9 @@ async function seedControlPlaneAlertPrereqs(prisma: PrismaClient, suffix: string describe("ProjectAlert control-plane β†’ run-subgraph FK reconciliation", () => { heteroRunOpsPostgresTest( - "creating a TASK_RUN alert with a ksuid taskRunId (run only on the run-ops DB) succeeds on control-plane", + "creating a TASK_RUN alert with a run-ops id taskRunId (run only on the run-ops DB) succeeds on control-plane", async ({ prisma14, prisma17 }) => { - const suffix = "alert-ksuid"; + const suffix = "alert-runops"; const { project, environment, channel } = await seedControlPlaneAlertPrereqs( prisma14, suffix @@ -61,7 +61,7 @@ describe("ProjectAlert control-plane β†’ run-subgraph FK reconciliation", () => // The run exists ONLY on the dedicated run-ops DB (prisma17), never on control-plane. await (prisma17 as RunOpsPrismaClient).taskRun.create({ data: { - id: KSUID_27, + id: NEW_ID_26, friendlyId: `run_${suffix}`, engine: "V2", status: "COMPLETED_WITH_ERRORS", @@ -78,7 +78,7 @@ describe("ProjectAlert control-plane β†’ run-subgraph FK reconciliation", () => }, }); - // Control-plane has no TaskRun row for KSUID_27. With the FK present this throws P2003; + // Control-plane has no TaskRun row for NEW_ID_26. With the FK present this throws P2003; // after the FK is dropped + the @relation removed it succeeds. const alert = await prisma14.projectAlert.create({ data: { @@ -88,32 +88,32 @@ describe("ProjectAlert control-plane β†’ run-subgraph FK reconciliation", () => environmentId: environment.id, status: "PENDING", type: "TASK_RUN", - taskRunId: KSUID_27, + taskRunId: NEW_ID_26, }, }); - expect(alert.taskRunId).toBe(KSUID_27); + expect(alert.taskRunId).toBe(NEW_ID_26); // The scalar round-trips and can be re-read off the control-plane row (the read path resolves // the actual run via runStore.findRun against the run-ops DB). const reread = await prisma14.projectAlert.findUniqueOrThrow({ where: { id: alert.id } }); - expect(reread.taskRunId).toBe(KSUID_27); + expect(reread.taskRunId).toBe(NEW_ID_26); }, 120_000 ); heteroRunOpsPostgresTest( - "creating a TASK_RUN_ATTEMPT alert with a ksuid taskRunAttemptId (attempt only on the run-ops DB) succeeds on control-plane", + "creating a TASK_RUN_ATTEMPT alert with a run-ops id taskRunAttemptId (attempt only on the run-ops DB) succeeds on control-plane", async ({ prisma14 }) => { - const suffix = "alert-ksuid-attempt"; + const suffix = "alert-run-ops id-attempt"; const { project, environment, channel } = await seedControlPlaneAlertPrereqs( prisma14, suffix ); - // A ksuid attempt id with no matching control-plane TaskRunAttempt row. With the FK present + // A run-ops id attempt id with no matching control-plane TaskRunAttempt row. With the FK present // this throws P2003; after the FK is dropped it succeeds. - const attemptId = "a".repeat(27); + const attemptId = "a".repeat(24) + "01"; const alert = await prisma14.projectAlert.create({ data: { friendlyId: `alert_${suffix}`, diff --git a/internal-packages/run-store/src/PostgresRunStore.dedicatedRepro.test.ts b/internal-packages/run-store/src/PostgresRunStore.dedicatedRepro.test.ts index e0cc3e217b..17f1e20e60 100644 --- a/internal-packages/run-store/src/PostgresRunStore.dedicatedRepro.test.ts +++ b/internal-packages/run-store/src/PostgresRunStore.dedicatedRepro.test.ts @@ -4,7 +4,7 @@ // dedicated subset schema (`heteroRunOpsPostgresTest.prisma17` is a real `RunOpsPrismaClient` over // the @internal/run-ops-database SUBSET schema) and the full legacy schema on a SEPARATE physical // PG container (`prisma14`). An earlier harness masked every one of these by backing the "#new" -// store with the FULL legacy schema and globally minting ksuid, so the split never ran against the +// store with the FULL legacy schema and globally minting run-ops id, so the split never ran against the // dedicated schema. // // Each case either asserts the fixed behavior directly or, for a still-open item, wraps the broken @@ -22,9 +22,9 @@ import type { CreateRunInput, RunStoreSchemaVariant } from "./types.js"; type AnyClient = PrismaClient | RunOpsPrismaClient; // ownerEngine classifies by internal-id LENGTH (runOpsResidency.ts): 25 chars β†’ cuid β†’ LEGACY, -// 27 chars β†’ ksuid β†’ NEW. A `run_`-prefixed friendly id strips the first underscore before length. +// a v1 body (version "1" at index 25) β†’ run-ops id β†’ NEW. A `run_`-prefixed friendly id strips the first underscore first. const CUID_25 = "c".repeat(25); // β†’ LEGACY (#legacy / control-plane DB, full schema) -const KSUID_27 = "k".repeat(27); // β†’ NEW (#new / dedicated run-ops DB, subset schema) +const NEW_ID_26 = "k".repeat(24) + "01"; // β†’ NEW (#new / dedicated run-ops DB, subset schema) // On the dedicated subset there are no Organization/Project/RuntimeEnvironment models (the run-ops // rows carry FK-free scalar ids), so we mint synthetic owning ids. On legacy we seed the real rows @@ -158,7 +158,7 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche const store = makeDedicatedStore(prisma17); const rows = await store.findManyTaskRunWaitpoints({ - where: { taskRunId: `run_${KSUID_27}` }, + where: { taskRunId: `run_${NEW_ID_26}` }, select: { id: true, batchId: true, @@ -179,8 +179,8 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche async ({ prisma17 }) => { const store = makeDedicatedStore(prisma17); const env = await seedEnvironment(prisma17, "dedicated", "gap4hyd_new"); - const runId = `run_${KSUID_27}`; - const waitpointId = `waitpoint_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; + const waitpointId = `waitpoint_${NEW_ID_26}`; await prisma17.waitpoint.create({ data: { id: waitpointId, @@ -217,7 +217,7 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche const store = makeLegacyStore(prisma14); const rows = await store.findManyTaskRunWaitpoints({ - where: { taskRunId: `run_${KSUID_27}` }, + where: { taskRunId: `run_${NEW_ID_26}` }, select: { id: true, waitpoint: { select: { id: true, status: true } }, @@ -236,7 +236,7 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche const { router } = makeSplitRouter(prisma14, prisma17); const rows = await router.findManyTaskRunWaitpoints({ - where: { taskRunId: `run_${KSUID_27}` }, + where: { taskRunId: `run_${NEW_ID_26}` }, select: { id: true, waitpoint: { select: { id: true, status: true, type: true, completedAfter: true } }, @@ -248,21 +248,21 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche // =========================================================================================== // Cross-DB waitpoint hydration through the router. - // A ksuid run (on #new) blocked by a waitpoint that lives on the OTHER DB (#legacy). The block + // A run-ops run (on #new) blocked by a waitpoint that lives on the OTHER DB (#legacy). The block // edge co-resides with the run on #new; the token is on #legacy. A single store hydrates the // edge's `waitpoint` from its own client β†’ null β†’ the run hangs / loses output. The // router must re-resolve the token across BOTH DBs. // =========================================================================================== - // Co-resident control (the ksuid happy path): a ksuid run blocked by a ksuid waitpoint, + // Co-resident control (the run-ops id happy path): a run-ops run blocked by a run-ops waitpoint, // both on #new, hydrates through the router with the real status/output. heteroRunOpsPostgresTest( - "cross-DB: a ksuid run blocked by a CO-RESIDENT ksuid waitpoint hydrates the real status via the router", + "cross-DB: a run-ops run blocked by a CO-RESIDENT run-ops waitpoint hydrates the real status via the router", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "cores_new"); - const runId = `run_${KSUID_27}`; - const waitpointId = `waitpoint_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; + const waitpointId = `waitpoint_${NEW_ID_26}`; await prisma17.waitpoint.create({ data: { id: waitpointId, @@ -297,19 +297,19 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche ); // The cross-DB topology. The block edge is on #new (co-resident with the - // ksuid run), the completing token is on #legacy. The router resolves the token across both DBs + // run-ops run), the completing token is on #legacy. The router resolves the token across both DBs // and returns its REAL status and OUTPUT (the wrong-result guard) β€” a single store would // hydrate null here and strand the run. heteroRunOpsPostgresTest( - "cross-DB: a ksuid run completed by a waitpoint on the OTHER DB hydrates the real status + output via the router", + "cross-DB: a run-ops run completed by a waitpoint on the OTHER DB hydrates the real status + output via the router", async ({ prisma14, prisma17 }) => { const { router, newStore } = makeSplitRouter(prisma14, prisma17); const newEnv = await seedEnvironment(prisma17, "dedicated", "xdb_new"); const legEnv = await seedEnvironment(prisma14, "legacy", "xdb_leg"); - const runId = `run_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; const waitpointId = `waitpoint_${CUID_25}`; // cuid β†’ lives on #legacy - // The completing token lives on #legacy (cuid MANUAL token blocking a ksuid run). + // The completing token lives on #legacy (cuid MANUAL token blocking a run-ops run). await prisma14.waitpoint.create({ data: { id: waitpointId, @@ -323,7 +323,7 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche environmentId: legEnv.environment.id, }, }); - // The block edge co-resides with the ksuid RUN on #new. + // The block edge co-resides with the run-ops id RUN on #new. await prisma17.taskRunWaitpoint.create({ data: { taskRunId: runId, waitpointId, projectId: newEnv.project.id }, }); @@ -361,8 +361,8 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const newEnv = await seedEnvironment(prisma17, "dedicated", "phantom_new"); - const runId = `run_${KSUID_27}`; - const waitpointId = `waitpoint_${"p".repeat(27)}`; // ksuid-shaped, but never created anywhere + const runId = `run_${NEW_ID_26}`; + const waitpointId = `waitpoint_${"p".repeat(24) + "01"}`; // run-ops-shaped, but never created anywhere await prisma17.taskRunWaitpoint.create({ data: { taskRunId: runId, waitpointId, projectId: newEnv.project.id }, @@ -471,13 +471,13 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche } ); - // Control: a ksuid run's checkpoint, routed by its owning run id, co-resides on #new. + // Control: a run-ops run's checkpoint, routed by its owning run id, co-resides on #new. heteroRunOpsPostgresTest( - "control: a ksuid run's checkpoint co-resides on #new with its snapshot", + "control: a run-ops run's checkpoint co-resides on #new with its snapshot", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "gap2k_new"); - const runId = `run_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; await router.createRun( buildCreateRunInput({ runId, @@ -491,9 +491,9 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche const checkpoint = await router.createTaskRunCheckpoint( { data: { - friendlyId: `checkpoint_${KSUID_27}`, + friendlyId: `checkpoint_${NEW_ID_26}`, type: "DOCKER", - location: "s3://bucket/ksuid-run-checkpoint", + location: "s3://bucket/run-ops id-run-checkpoint", projectId: env.project.id, runtimeEnvironmentId: env.environment.id, }, @@ -506,7 +506,7 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche const snapshot = await router.createExecutionSnapshot({ run: { id: runId, status: "EXECUTING", attemptNumber: 1 }, - snapshot: { executionStatus: "SUSPENDED", description: "ksuid suspended" }, + snapshot: { executionStatus: "SUSPENDED", description: "run-ops suspended" }, checkpointId: checkpoint.id, environmentId: env.environment.id, environmentType: "DEVELOPMENT", @@ -637,15 +637,15 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche } ); - // Control: a KSUID run (on #new / dedicated) IS visible through the router β€” proving the read gap + // Control: a run-ops run (on #new / dedicated) IS visible through the router β€” proving the read gap // is residency-specific (only the cuid/#legacy cohort would be dropped), not a blanket failure. heteroRunOpsPostgresTest( - "control: a ksuid run's #new snapshot IS found through the router", + "control: a run-ops run's #new snapshot IS found through the router", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "gap5c_new"); - const runId = `run_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; await router.createRun( buildCreateRunInput({ runId, @@ -657,7 +657,7 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche ); const created = await router.createExecutionSnapshot({ run: { id: runId, status: "EXECUTING", attemptNumber: 1 }, - snapshot: { executionStatus: "EXECUTING", description: "ksuid run executing" }, + snapshot: { executionStatus: "EXECUTING", description: "run-ops run executing" }, environmentId: env.environment.id, environmentType: "DEVELOPMENT", projectId: env.project.id, @@ -696,10 +696,10 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche heteroRunOpsPostgresTest( "mechanism: the block-edge CTE writes ZERO edges when the waitpoint is on the other DB", async ({ prisma14, prisma17 }) => { - // A ksuid parent run + its associated waitpoint live on #new (prisma17 / dedicated). + // A run-ops parent run + its associated waitpoint live on #new (prisma17 / dedicated). const newEnv = await seedEnvironment(prisma17, "dedicated", "gap3_new"); - const parentRunId = `run_${KSUID_27}`; - const waitpointId = `waitpoint_${KSUID_27}`; + const parentRunId = `run_${NEW_ID_26}`; + const waitpointId = `waitpoint_${NEW_ID_26}`; await prisma17.taskRun.create({ data: { id: parentRunId, @@ -826,20 +826,20 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche // =========================================================================================== // Lazy RUN-waitpoint residency split. // `getOrCreateRunWaitpoint` creates the lazy RUN waitpoint via `createWaitpoint` - // carrying `completedByTaskRunId: runId`. Production never mints ksuid waitpoint ids, so routing by - // the waitpoint's OWN id-shape would land it on #legacy while a ksuid run is on #new β†’ run-completion + // carrying `completedByTaskRunId: runId`. Production never mints run-ops waitpoint ids, so routing by + // the waitpoint's OWN id-shape would land it on #legacy while a run-ops run is on #new β†’ run-completion // hydrate (associatedWaitpoint by completedByTaskRunId on the run's DB) misses it β†’ parent hangs. // Fix: route the create by the OWNING run id (completedByTaskRunId) so it co-resides with the run. // =========================================================================================== - // A ksuid run's lazy RUN-waitpoint with a CUID-shaped waitpoint id (production-like: ksuid + // A run-ops run's lazy RUN-waitpoint with a CUID-shaped waitpoint id (production-like: run-ops id // waitpoint minting is off) co-resides on #new with the run, NOT on #legacy by its own id-shape. heteroRunOpsPostgresTest( - "a ksuid run's lazy RUN-waitpoint co-resides on #new (routed by completedByTaskRunId)", + "a run-ops run's lazy RUN-waitpoint co-resides on #new (routed by completedByTaskRunId)", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "gap6_new"); - const runId = `run_${KSUID_27}`; // ksuid run β†’ #new + const runId = `run_${NEW_ID_26}`; // run-ops run β†’ #new const waitpointId = `waitpoint_${CUID_25}`; // cuid waitpoint id β†’ would route to #legacy by id-shape await router.createRun( @@ -867,7 +867,7 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche }, }); - // Co-resides with the ksuid run on #new (NOT stranded on #legacy by the cuid id-shape). + // Co-resides with the run-ops run on #new (NOT stranded on #legacy by the cuid id-shape). const onNew = await prisma17.waitpoint.findUnique({ where: { id: waitpointId } }); expect(onNew).not.toBeNull(); const onLegacy = await prisma14.waitpoint.findUnique({ where: { id: waitpointId } }); @@ -887,7 +887,7 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma14, "legacy", "gap6c_leg"); const runId = `run_${CUID_25}`; // cuid run β†’ #legacy - const waitpointId = `waitpoint_${KSUID_27}`; // ksuid waitpoint id β†’ would route to #new by id-shape + const waitpointId = `waitpoint_${NEW_ID_26}`; // run-ops waitpoint id β†’ would route to #new by id-shape await router.createRun( buildCreateRunInput({ @@ -922,13 +922,13 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche // =========================================================================================== // Snapshot resume payload must not lose a cross-DB waitpoint's OUTPUT. // `findLatestExecutionSnapshot` hydrates `completedWaitpoints` from the - // snapshot's own (run's) client. A ksuid run resumed by a waitpoint that completed on the OTHER DB + // snapshot's own (run's) client. A run-ops run resumed by a waitpoint that completed on the OTHER DB // (cuid token) would get the token hydrated to a stale/absent row β†’ its OUTPUT silently vanishes from // the resume payload (a wrong-result, not just a wrong dashboard). Fix: the router re-resolves the // snapshot's completed waitpoints across BOTH DBs. // =========================================================================================== - // A ksuid run's latest snapshot lists a completed waitpoint that lives on #legacy + // A run-ops run's latest snapshot lists a completed waitpoint that lives on #legacy // (cross-DB). The single #new store hydrates it null; the router recovers its real OUTPUT. heteroRunOpsPostgresTest( "findLatestExecutionSnapshot recovers a cross-DB completed waitpoint's OUTPUT via the router", @@ -936,7 +936,7 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche const { router, newStore } = makeSplitRouter(prisma14, prisma17); const newEnv = await seedEnvironment(prisma17, "dedicated", "cg1_new"); const legEnv = await seedEnvironment(prisma14, "legacy", "cg1_leg"); - const runId = `run_${KSUID_27}`; // ksuid run β†’ #new + const runId = `run_${NEW_ID_26}`; // run-ops run β†’ #new const waitpointId = `waitpoint_${CUID_25}`; // cuid token β†’ completed on #legacy await router.createRun( @@ -964,7 +964,7 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche }, }); - // The latest snapshot (on #new, co-resident with the ksuid run) lists the cross-DB token as a + // The latest snapshot (on #new, co-resident with the run-ops run) lists the cross-DB token as a // completed waitpoint via the CompletedWaitpoint join. await router.createExecutionSnapshot({ run: { id: runId, status: "EXECUTING", attemptNumber: 1 }, @@ -994,7 +994,7 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche // =========================================================================================== // Block-edge WRITER must not require a LOCAL waitpoint row. // The design routes the block edge to the RUN's DB and mints standalone tokens on LEGACY, so a - // ksuid run on #new can legitimately block on a cuid token resident on #legacy (the one tolerated + // run-ops run on #new can legitimately block on a cuid token resident on #legacy (the one tolerated // cross-DB direction β€” the #new `TaskRunWaitpoint` is FK-free precisely for this). If // `blockRunWithWaitpointEdges`'s dedicated branch sourced the edge rows from // `FROM "Waitpoint" w WHERE w.id = ANY(...)`, then when the token is NOT on the run's own DB the @@ -1004,19 +1004,19 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche // the #new DB is FK-free on these columns. // =========================================================================================== - // A ksuid run on #new blocking on a cuid token resident on + // A run-ops run on #new blocking on a cuid token resident on // #legacy writes the block edge (TaskRunWaitpoint + WaitpointRunConnection) on #new, NOT requiring - // the waitpoint row to be local. The #legacy DB holds NO edge for the ksuid run. + // the waitpoint row to be local. The #legacy DB holds NO edge for the run-ops run. heteroRunOpsPostgresTest( "a NEW run blocking on a LEGACY-resident token writes the edge on NEW (no local waitpoint required)", async ({ prisma14, prisma17 }) => { const { router, newStore } = makeSplitRouter(prisma14, prisma17); const newEnv = await seedEnvironment(prisma17, "dedicated", "gap3b_new"); const legEnv = await seedEnvironment(prisma14, "legacy", "gap3b_leg"); - const runId = `run_${KSUID_27}`; // ksuid run β†’ #new + const runId = `run_${NEW_ID_26}`; // run-ops run β†’ #new const waitpointId = `waitpoint_${CUID_25}`; // cuid standalone token β†’ resides on #legacy - // The ksuid run lives on #new. + // The run-ops run lives on #new. await prisma17.taskRun.create({ data: { id: runId, @@ -1069,7 +1069,7 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche where: { taskRunId: runId }, }); expect(connectionsOnNew).toBe(1); - // The #legacy DB holds NO edge for the ksuid run (the safety invariant: no cross-ref on LEGACY). + // The #legacy DB holds NO edge for the run-ops run (the safety invariant: no cross-ref on LEGACY). const edgesOnLegacy = await prisma14.taskRunWaitpoint.count({ where: { taskRunId: runId } }); expect(edgesOnLegacy).toBe(0); @@ -1084,7 +1084,7 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche // Single-store cross-check: the #new store ALSO writes the edge directly (proving the fix is in // the store writer, not only the router routing). - const runId2 = `run_${"m".repeat(27)}`; // a second ksuid run on #new + const runId2 = `run_${"m".repeat(24) + "01"}`; // a second run-ops run on #new await prisma17.taskRun.create({ data: { id: runId2, @@ -1117,15 +1117,15 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche } ); - // Co-resident control: a ksuid run blocking on a CO-RESIDENT ksuid token still writes the + // Co-resident control: a run-ops run blocking on a CO-RESIDENT run-ops token still writes the // edge on #new (proving the fix didn't break the co-resident case the old join handled). heteroRunOpsPostgresTest( "control: a NEW run blocking on a CO-RESIDENT NEW token writes the edge on NEW", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "gap3bco_new"); - const runId = `run_${KSUID_27}`; - const waitpointId = `waitpoint_${KSUID_27}`; // ksuid token β†’ co-resident on #new + const runId = `run_${NEW_ID_26}`; + const waitpointId = `waitpoint_${NEW_ID_26}`; // run-ops token β†’ co-resident on #new await prisma17.taskRun.create({ data: { @@ -1185,7 +1185,7 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche const { router } = makeSplitRouter(prisma14, prisma17); const newEnv = await seedEnvironment(prisma17, "dedicated", "gap3bidem_new"); const legEnv = await seedEnvironment(prisma14, "legacy", "gap3bidem_leg"); - const runId = `run_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; const waitpointId = `waitpoint_${CUID_25}`; // cuid token β†’ #legacy await prisma17.taskRun.create({ @@ -1251,8 +1251,8 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "cg1c_new"); - const runId = `run_${KSUID_27}`; - const waitpointId = `waitpoint_${KSUID_27}`; // co-resident on #new + const runId = `run_${NEW_ID_26}`; + const waitpointId = `waitpoint_${NEW_ID_26}`; // co-resident on #new await router.createRun( buildCreateRunInput({ @@ -1301,14 +1301,14 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche // waitpoint's blocking edge, run connection and completing snapshot all CO-LOCATE WITH THE RUN // (blockRunWithWaitpointEdges routes by runId; the CompletedWaitpoint + WaitpointRunConnection // join rows are written on the run's DB). A cuid token blocking - // a ksuid run therefore has every group-A TARGET on the OTHER DB β†’ the single-client hydrator finds + // a run-ops run therefore has every group-A TARGET on the OTHER DB β†’ the single-client hydrator finds // nothing β†’ engine.getWaitpoint (include blockingTaskRunsβ†’taskRun) silently returns an // empty `blockingTaskRuns`. Fix: the router (RoutingRunStore.findWaitpoint/findManyWaitpoints) strips // these relation keys from the per-leg query and re-resolves the targets across BOTH DBs, mirroring // findManyTaskRunWaitpoints' edge fan-out. // =========================================================================================== - // A cuid token on #legacy blocking a ksuid run on #new. The block edge + run connection live + // A cuid token on #legacy blocking a run-ops run on #new. The block edge + run connection live // on #new (the run's DB). getWaitpoint's include{ blockingTaskRuns: { select: { taskRun } } } must // surface the cross-DB blocked run. Single-store guard proves the #legacy hydrator alone misses it. heteroRunOpsPostgresTest( @@ -1317,7 +1317,7 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche const { router, legacyStore } = makeSplitRouter(prisma14, prisma17); const newEnv = await seedEnvironment(prisma17, "dedicated", "gap13bt_new"); const legEnv = await seedEnvironment(prisma14, "legacy", "gap13bt_leg"); - const runId = `run_${KSUID_27}`; // ksuid run β†’ #new + const runId = `run_${NEW_ID_26}`; // run-ops run β†’ #new const waitpointId = `waitpoint_${CUID_25}`; // cuid token β†’ #legacy await router.createRun( @@ -1380,14 +1380,14 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche ); // Sibling: connectedRuns. The WaitpointRunConnection join is co-resident with the run (#new), - // so a cuid token's connectedRuns must be re-resolved across BOTH DBs to surface the ksuid run. + // so a cuid token's connectedRuns must be re-resolved across BOTH DBs to surface the run-ops run. heteroRunOpsPostgresTest( "findWaitpoint include connectedRuns surfaces a cross-DB connected run via the router", async ({ prisma14, prisma17 }) => { const { router, legacyStore } = makeSplitRouter(prisma14, prisma17); const newEnv = await seedEnvironment(prisma17, "dedicated", "gap13cr_new"); const legEnv = await seedEnvironment(prisma14, "legacy", "gap13cr_leg"); - const runId = `run_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; const waitpointId = `waitpoint_${CUID_25}`; await router.createRun( @@ -1446,7 +1446,7 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche const { router, legacyStore } = makeSplitRouter(prisma14, prisma17); const newEnv = await seedEnvironment(prisma17, "dedicated", "gap13cs_new"); const legEnv = await seedEnvironment(prisma14, "legacy", "gap13cs_leg"); - const runId = `run_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; const waitpointId = `waitpoint_${CUID_25}`; await router.createRun( @@ -1471,7 +1471,7 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche environmentId: legEnv.environment.id, }, }); - // The snapshot (on #new, co-resident with the ksuid run) records the cross-DB token as completed + // The snapshot (on #new, co-resident with the run-ops run) records the cross-DB token as completed // via the CompletedWaitpoint join. const snapshot = await router.createExecutionSnapshot({ run: { id: runId, status: "EXECUTING", attemptNumber: 1 }, @@ -1510,8 +1510,8 @@ describe("run-ops split β€” store-level behavior against the REAL dedicated sche async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "gap13ctl_new"); - const runId = `run_${KSUID_27}`; - const waitpointId = `waitpoint_${KSUID_27}`; // co-resident on #new + const runId = `run_${NEW_ID_26}`; + const waitpointId = `waitpoint_${NEW_ID_26}`; // co-resident on #new await router.createRun( buildCreateRunInput({ diff --git a/internal-packages/run-store/src/PostgresRunStore.writeAtomicity.test.ts b/internal-packages/run-store/src/PostgresRunStore.writeAtomicity.test.ts index 2d43fc9083..031d575a3e 100644 --- a/internal-packages/run-store/src/PostgresRunStore.writeAtomicity.test.ts +++ b/internal-packages/run-store/src/PostgresRunStore.writeAtomicity.test.ts @@ -2,7 +2,7 @@ // // Under the run-ops split, several engine operations that were atomic-by-`prisma.$transaction` in // single-DB make TWO distinct RunStore writes (e.g. startAttempt + createExecutionSnapshot, or -// promotePendingVersionRuns + createExecutionSnapshot). When the run is ksuid (#new), `RoutingRunStore` +// promotePendingVersionRuns + createExecutionSnapshot). When the run is run-ops id (#new), `RoutingRunStore` // routes each write to the NEW store but DROPS the caller's control-plane `tx` β€” so the two writes // execute as independent auto-commit statements on the NEW DB, OUTSIDE any shared transaction. A crash // between them leaves partial state (a run EXECUTING with no matching snapshot; promoted-but-no-snapshot). @@ -26,9 +26,9 @@ import type { CreateRunInput, RunStore, RunStoreSchemaVariant } from "./types.js type AnyClient = PrismaClient | RunOpsPrismaClient; -// ownerEngine classifies by internal-id LENGTH: 25 chars β†’ cuid β†’ LEGACY, 27 chars β†’ ksuid β†’ NEW. +// ownerEngine classifies by the version char: no marker β†’ cuid β†’ LEGACY, v1 body β†’ run-ops id β†’ NEW. const CUID_25 = "c".repeat(25); // β†’ LEGACY (#legacy / control-plane DB, full schema) -const KSUID_27 = "k".repeat(27); // β†’ NEW (#new / dedicated run-ops DB, subset schema) +const NEW_ID_26 = "k".repeat(24) + "01"; // β†’ NEW (#new / dedicated run-ops DB, subset schema) async function seedEnvironment( prisma: AnyClient, @@ -136,14 +136,14 @@ function makeSplitRouter(prisma14: PrismaClient, prisma17: RunOpsPrismaClient) { }; } -// Seed a ksuid run on #new (its create nests the initial RUN_CREATED snapshot) and return its ids. -async function seedKsuidRun( +// Seed a run-ops run on #new (its create nests the initial RUN_CREATED snapshot) and return its ids. +async function seedRunOpsRun( router: RunStore, prisma17: RunOpsPrismaClient, suffix: string ): Promise<{ runId: string; env: { project: { id: string }; environment: { id: string } } }> { const env = await seedEnvironment(prisma17, "dedicated", suffix); - const runId = `run_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; await router.createRun( buildCreateRunInput({ runId, @@ -175,14 +175,14 @@ function snapshotInput( describe("cross-DB write atomicity (startAttempt + createExecutionSnapshot)", () => { // --------------------------------------------------------------------------------------------- // RED demonstration: the BROKEN behaviour. Two separate routed writes (as the engine made them - // before the fix) on a ksuid run leave PARTIAL state on a mid-pair failure β€” the run is EXECUTING + // before the fix) on a run-ops run leave PARTIAL state on a mid-pair failure β€” the run is EXECUTING // but no EXECUTING snapshot exists. This is the regression vs single-DB. // --------------------------------------------------------------------------------------------- heteroRunOpsPostgresTest( "BROKEN baseline: two un-wrapped routed writes persist partial state on a mid-pair failure", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); - const { runId, env } = await seedKsuidRun(router, prisma17, "broken_atomic"); + const { runId, env } = await seedRunOpsRun(router, prisma17, "broken_atomic"); // Simulate the OLD engine pattern: startAttempt then a failure BEFORE createExecutionSnapshot, // each as an independent routed (auto-commit) write β€” no shared transaction. @@ -214,10 +214,10 @@ describe("cross-DB write atomicity (startAttempt + createExecutionSnapshot)", () // BETWEEN the two writes rolls the FIRST write back β€” no partial state. // --------------------------------------------------------------------------------------------- heteroRunOpsPostgresTest( - "runInTransaction rolls back startAttempt when a failure is injected before the snapshot write (ksuid β†’ #new)", + "runInTransaction rolls back startAttempt when a failure is injected before the snapshot write (run-ops id β†’ #new)", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); - const { runId, env } = await seedKsuidRun(router, prisma17, "rollback_new"); + const { runId, env } = await seedRunOpsRun(router, prisma17, "rollback_new"); await expect( router.runInTransaction(runId, async (store, tx) => { @@ -246,10 +246,10 @@ describe("cross-DB write atomicity (startAttempt + createExecutionSnapshot)", () ); heteroRunOpsPostgresTest( - "runInTransaction commits BOTH writes atomically on success (ksuid β†’ #new)", + "runInTransaction commits BOTH writes atomically on success (run-ops id β†’ #new)", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); - const { runId, env } = await seedKsuidRun(router, prisma17, "commit_new"); + const { runId, env } = await seedRunOpsRun(router, prisma17, "commit_new"); const result = await router.runInTransaction(runId, async (store, tx) => { const run = await store.startAttempt( diff --git a/internal-packages/run-store/src/batchCompletionResidency.test.ts b/internal-packages/run-store/src/batchCompletionResidency.test.ts index dc9730b74d..e8b42893ad 100644 --- a/internal-packages/run-store/src/batchCompletionResidency.test.ts +++ b/internal-packages/run-store/src/batchCompletionResidency.test.ts @@ -1,7 +1,7 @@ // REGRESSION suite for the run-ops split "control-plane tx/client forwarded into a NEW-resident // store" bug class on the BatchTaskRun write/probe path. When the router resolves the owning store // to #new but forwards the caller's control-plane handle, #new issues its statement against the -// CONTROL-PLANE DB where the ksuid row does not exist β†’ "No record was found" (update), wrong-DB row +// CONTROL-PLANE DB where the run-ops id row does not exist β†’ "No record was found" (update), wrong-DB row // (create), or wrong count. Covers updateBatchTaskRun (commit 62ae880af), createBatchTaskRun and // countBatchTaskRunItems (this sweep). `heteroRunOpsPostgresTest` is the REAL two-DB split topology // (prisma17 = dedicated #new, prisma14 = legacy #legacy); NEVER mocked. @@ -17,9 +17,9 @@ import type { RunStoreSchemaVariant } from "./types.js"; type AnyClient = PrismaClient | RunOpsPrismaClient; // ownerEngine classifies by internal-id LENGTH (runOpsResidency.ts): 25 chars β†’ cuid β†’ LEGACY, -// 27 chars β†’ ksuid β†’ NEW. +// a v1 body (26 chars, version "1" at index 25) β†’ NEW. const CUID_25 = "c".repeat(25); // β†’ LEGACY (#legacy / prisma14, full schema) -const KSUID_27 = "k".repeat(27); // β†’ NEW (#new / prisma17, dedicated subset schema) +const NEW_ID_26 = "k".repeat(24) + "01"; // β†’ NEW (#new / prisma17, dedicated subset schema) async function seedEnvironment( prisma: AnyClient, @@ -118,16 +118,16 @@ describe("run-ops split β€” BatchTaskRun writes/probes must NOT forward the cont // updateBatchTaskRun (commit 62ae880af) β€” the batch-completion residency regression. // =========================================================================================== - // The live `batchSystem.#tryCompleteBatch` shape: a ksuid batch on #new is updated to COMPLETED + // The live `batchSystem.#tryCompleteBatch` shape: a run-ops batch on #new is updated to COMPLETED // while the control-plane client is passed as `tx`. RED on the pre-62ae880af code (the router // forwarded tx β†’ #new ran the UPDATE on the control-plane DB β†’ "No record was found for an // update"); GREEN now (tx dropped for NEW β†’ the row updates on #new's own DB). heteroRunOpsPostgresTest( - "updateBatchTaskRun marks a ksuid batch on #new COMPLETED even when the control-plane client is passed as tx", + "updateBatchTaskRun marks a run-ops batch on #new COMPLETED even when the control-plane client is passed as tx", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "updbatch_new"); - const batchId = `batch_${KSUID_27}`; // ksuid β†’ #new + const batchId = `batch_${NEW_ID_26}`; // run-ops id β†’ #new await prisma17.batchTaskRun.create({ data: { @@ -185,15 +185,15 @@ describe("run-ops split β€” BatchTaskRun writes/probes must NOT forward the cont // createBatchTaskRun (this sweep) β€” same anti-pattern on the create path. // =========================================================================================== - // A ksuid batch routed to #new with a forwarded control-plane tx must still be created on #new's + // A run-ops batch routed to #new with a forwarded control-plane tx must still be created on #new's // OWN DB, not the control-plane DB (which would strand the batch away from its co-resident child // runs/items). Forwarding tx unconditionally would land the row on prisma14. heteroRunOpsPostgresTest( - "createBatchTaskRun lands a ksuid batch on #new even when the control-plane client is passed as tx", + "createBatchTaskRun lands a run-ops batch on #new even when the control-plane client is passed as tx", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "crbatch_new"); - const batchId = `batch_${KSUID_27}`; // ksuid β†’ #new + const batchId = `batch_${NEW_ID_26}`; // run-ops id β†’ #new const created = await router.createBatchTaskRun( { @@ -206,7 +206,7 @@ describe("run-ops split β€” BatchTaskRun writes/probes must NOT forward the cont ); expect(created.id).toBe(batchId); - // Resident on #new (its own DB), absent from #legacy β€” co-located with its ksuid child runs. + // Resident on #new (its own DB), absent from #legacy β€” co-located with its run-ops child runs. const onNew = await prisma17.batchTaskRun.findUnique({ where: { id: batchId } }); expect(onNew).not.toBeNull(); const onLegacy = await prisma14.batchTaskRun.findUnique({ where: { id: batchId } }); @@ -243,14 +243,14 @@ describe("run-ops split β€” BatchTaskRun writes/probes must NOT forward the cont // countBatchTaskRunItems (this sweep) β€” same anti-pattern on a routed probe read. // =========================================================================================== - // A ksuid batch's items live on #new; counting them with the control-plane client forwarded would + // A run-ops batch's items live on #new; counting them with the control-plane client forwarded would // count on the wrong DB (β†’ 0). The routed store must read its OWN DB and return the real count. heteroRunOpsPostgresTest( - "countBatchTaskRunItems counts a ksuid batch's items on #new even when the control-plane client is passed", + "countBatchTaskRunItems counts a run-ops batch's items on #new even when the control-plane client is passed", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "cntitems_new"); - const batchId = `batch_${KSUID_27}`; // ksuid β†’ #new + const batchId = `batch_${NEW_ID_26}`; // run-ops id β†’ #new await prisma17.batchTaskRun.create({ data: { @@ -261,8 +261,8 @@ describe("run-ops split β€” BatchTaskRun writes/probes must NOT forward the cont status: "PROCESSING", }, }); - const runA = `run_${KSUID_27.slice(0, -3)}cra`; - const runB = `run_${KSUID_27.slice(0, -3)}crb`; + const runA = `run_${NEW_ID_26.slice(0, -3)}ra1`; + const runB = `run_${NEW_ID_26.slice(0, -3)}rb1`; await seedDedicatedRun(prisma17, env.environment.id, runA); await seedDedicatedRun(prisma17, env.environment.id, runB); await prisma17.batchTaskRunItem.create({ diff --git a/internal-packages/run-store/src/runOpsStore.flipWindowDuplicate.test.ts b/internal-packages/run-store/src/runOpsStore.flipWindowDuplicate.test.ts index 7ee5250665..373984bc34 100644 --- a/internal-packages/run-store/src/runOpsStore.flipWindowDuplicate.test.ts +++ b/internal-packages/run-store/src/runOpsStore.flipWindowDuplicate.test.ts @@ -3,7 +3,7 @@ // // During the flip window, two CONCURRENT same-(env, idempotencyKey, taskIdentifier) ROOT triggers can // land on instances with DIVERGENT cached mint-kinds: the stale instance mints a cuid run on #legacy, -// the flipped instance a ksuid run on #new. The dedup probe (probe-before-mint) only catches an +// the flipped instance a run-ops run on #new. The dedup probe (probe-before-mint) only catches an // ALREADY-COMMITTED run; two truly-simultaneous mints both miss, then both create. The per-DB unique // constraint on (runtimeEnvironmentId, idempotencyKey, taskIdentifier) is PER PHYSICAL DB, so it // cannot reject the second insert that lands on the OTHER DB. This test proves both creates SUCCEED @@ -22,9 +22,10 @@ import type { CreateRunInput, RunStoreSchemaVariant } from "./types.js"; type AnyClient = PrismaClient | RunOpsPrismaClient; // ownerEngine classifies by internal-id length (no internal underscore): 25 -> cuid -> LEGACY, -// 27 -> ksuid -> NEW. +// 27 -> run-ops id -> NEW. const cuidLegacy = (seed: string) => (seed + "c".repeat(25)).slice(0, 25); -const ksuidNew = (seed: string) => (seed + "k".repeat(27)).slice(0, 27); +const runOpsNew = (seed: string) => + (seed.replace(/[^0-9a-v]/g, "0") + "k".repeat(24)).slice(0, 24) + "01"; async function seedEnvironment( prisma: AnyClient, @@ -137,7 +138,7 @@ describe("RoutingRunStore β€” mint-on-flip bounded concurrent-window cross-DB du const taskIdentifier = "my-task"; const staleCuidRunId = cuidLegacy("rfl"); // stale instance mints cuid -> #legacy - const flippedKsuidRunId = ksuidNew("rfn"); // flipped instance mints ksuid -> #new + const flippedRunOpsRunId = runOpsNew("rfn"); // flipped instance mints run-ops id -> #new // Both concurrent mints commit. The second does NOT throw a unique violation: the constraint is // PER-DB and these land on different physical DBs. @@ -154,7 +155,7 @@ describe("RoutingRunStore β€” mint-on-flip bounded concurrent-window cross-DB du ); await router.createRun( buildCreateRunInput({ - runId: flippedKsuidRunId, + runId: flippedRunOpsRunId, friendlyId: "run_flip_new", organizationId: seed.organization.id, projectId: seed.project.id, @@ -166,7 +167,9 @@ describe("RoutingRunStore β€” mint-on-flip bounded concurrent-window cross-DB du // The duplicate is REAL: a row for the same key physically exists on BOTH DBs. expect(await prisma14.taskRun.findFirst({ where: { id: staleCuidRunId } })).not.toBeNull(); - expect(await prisma17.taskRun.findFirst({ where: { id: flippedKsuidRunId } })).not.toBeNull(); + expect( + await prisma17.taskRun.findFirst({ where: { id: flippedRunOpsRunId } }) + ).not.toBeNull(); // The duplicate is BOUNDED: subsequent reads via the id-less probe collapse to exactly ONE run, // deterministically the NEW one (NEW-first fan-out) β€” the same tie-break the cross-DB dedup @@ -177,7 +180,7 @@ describe("RoutingRunStore β€” mint-on-flip bounded concurrent-window cross-DB du taskIdentifier, })) as Record | null; expect(found).not.toBeNull(); - expect(found!.id).toBe(flippedKsuidRunId); + expect(found!.id).toBe(flippedRunOpsRunId); } ); }); diff --git a/internal-packages/run-store/src/runOpsStore.forWaitpointCompletion.test.ts b/internal-packages/run-store/src/runOpsStore.forWaitpointCompletion.test.ts index f8a4b6fc15..eff2a71cd9 100644 --- a/internal-packages/run-store/src/runOpsStore.forWaitpointCompletion.test.ts +++ b/internal-packages/run-store/src/runOpsStore.forWaitpointCompletion.test.ts @@ -5,7 +5,7 @@ import type { RunStore } from "./types.js"; // forWaitpointCompletion is async: it picks a preferred store from the id-shape + pins, then // PROBES findWaitpoint to resolve where the token ACTUALLY lives (drain can relocate a cuid -// waitpoint onto NEW, or a ksuid token can be pinned LEGACY), falling back to the other store. +// waitpoint onto NEW, or a run-ops token can be pinned LEGACY), falling back to the other store. // So the slots here are fakes whose only behaviour is "do I hold this waitpoint id?". function fakeStore(slot: string, heldIds: Set): RunStore { return { @@ -17,7 +17,7 @@ function fakeStore(slot: string, heldIds: Set): RunStore { } as unknown as RunStore; } -const KSUID_ID = "waitpoint_" + "a".repeat(27); +const RUN_OPS_ID = "waitpoint_" + "a".repeat(24) + "01"; const CUID_ID = "waitpoint_" + "a".repeat(25); const UNCLASSIFIABLE_ID = "waitpoint_" + "a".repeat(26); @@ -28,7 +28,7 @@ function buildRouter(opts?: { newHolds?: string[]; legacyHolds?: string[] }): { newStore: RunStore; legacyStore: RunStore; } { - const all = [KSUID_ID, CUID_ID, UNCLASSIFIABLE_ID]; + const all = [RUN_OPS_ID, CUID_ID, UNCLASSIFIABLE_ID]; const newStore = fakeStore("new", new Set(opts?.newHolds ?? all)); const legacyStore = fakeStore("legacy", new Set(opts?.legacyHolds ?? all)); return { @@ -39,9 +39,9 @@ function buildRouter(opts?: { newHolds?: string[]; legacyHolds?: string[] }): { } describe("RoutingRunStore.forWaitpointCompletion", () => { - it("resolves a ksuid waitpointId with no pins to the NEW slot", async () => { + it("resolves a run-ops waitpointId with no pins to the NEW slot", async () => { const { router, newStore } = buildRouter(); - expect(await router.forWaitpointCompletion(KSUID_ID, { routeKind: "MANUAL" })).toBe(newStore); + expect(await router.forWaitpointCompletion(RUN_OPS_ID, { routeKind: "MANUAL" })).toBe(newStore); }); it("resolves a cuid waitpointId with no pins to the LEGACY slot", async () => { @@ -49,30 +49,30 @@ describe("RoutingRunStore.forWaitpointCompletion", () => { expect(await router.forWaitpointCompletion(CUID_ID, { routeKind: "MANUAL" })).toBe(legacyStore); }); - it("pins to LEGACY when isCrossTreeIdempotency is true, even for a ksuid id", async () => { + it("pins to LEGACY when isCrossTreeIdempotency is true, even for a run-ops id", async () => { const { router, legacyStore } = buildRouter(); expect( - await router.forWaitpointCompletion(KSUID_ID, { + await router.forWaitpointCompletion(RUN_OPS_ID, { routeKind: "IDEMPOTENCY_REUSE", isCrossTreeIdempotency: true, }) ).toBe(legacyStore); }); - it("pins to LEGACY when treeOwnerResidency is LEGACY, even for a ksuid id", async () => { + it("pins to LEGACY when treeOwnerResidency is LEGACY, even for a run-ops id", async () => { const { router, legacyStore } = buildRouter(); expect( - await router.forWaitpointCompletion(KSUID_ID, { + await router.forWaitpointCompletion(RUN_OPS_ID, { routeKind: "MANUAL", treeOwnerResidency: "LEGACY", }) ).toBe(legacyStore); }); - it("pins to LEGACY when hasLegacyParent is true, even for a ksuid id", async () => { + it("pins to LEGACY when hasLegacyParent is true, even for a run-ops id", async () => { const { router, legacyStore } = buildRouter(); expect( - await router.forWaitpointCompletion(KSUID_ID, { + await router.forWaitpointCompletion(RUN_OPS_ID, { routeKind: "RUN", hasLegacyParent: true, }) @@ -80,10 +80,10 @@ describe("RoutingRunStore.forWaitpointCompletion", () => { }); it("falls back to the OTHER store when the preferred store does not hold the token", async () => { - // ksuid id prefers NEW, but the token actually lives on LEGACY (drain/relocation): the + // run-ops id prefers NEW, but the token actually lives on LEGACY (drain/relocation): the // probe must fall through to LEGACY rather than route by id-shape alone and miss it. - const { router, legacyStore } = buildRouter({ newHolds: [], legacyHolds: [KSUID_ID] }); - expect(await router.forWaitpointCompletion(KSUID_ID, { routeKind: "MANUAL" })).toBe( + const { router, legacyStore } = buildRouter({ newHolds: [], legacyHolds: [RUN_OPS_ID] }); + expect(await router.forWaitpointCompletion(RUN_OPS_ID, { routeKind: "MANUAL" })).toBe( legacyStore ); }); diff --git a/internal-packages/run-store/src/runOpsStore.idempotencyDedup.test.ts b/internal-packages/run-store/src/runOpsStore.idempotencyDedup.test.ts index 8e523660b7..3546ea197c 100644 --- a/internal-packages/run-store/src/runOpsStore.idempotencyDedup.test.ts +++ b/internal-packages/run-store/src/runOpsStore.idempotencyDedup.test.ts @@ -4,7 +4,7 @@ // `runStore.findRun({ runtimeEnvironmentId, idempotencyKey, taskIdentifier }, // { include: { associatedWaitpoint: true } }, dedupClient)` // (apps/webapp/app/runEngine/concerns/idempotencyKeys.server.ts). The existing run may live -// on EITHER physical DB (a cuid run on #legacy minted before the org flipped to ksuid; a ksuid run on +// on EITHER physical DB (a cuid run on #legacy minted before the org flipped to run-ops id; a run-ops run on // #new after). The PG unique key is PER-DB and cannot enforce cross-DB uniqueness, so dedup must be // correct at the routing layer. RoutingRunStore.findRun drops the caller // dedupClient and, for an id-less where, fans out NEWβ†’LEGACY (#findRunUnrouted). @@ -27,13 +27,13 @@ import type { type AnyClient = PrismaClient | RunOpsPrismaClient; // ownerEngine classifies by internal-id LENGTH after stripping a single leading `_`: -// 25 chars β†’ cuid β†’ LEGACY, 27 chars β†’ ksuid β†’ NEW. So a classifiable id +// 25 chars β†’ cuid β†’ LEGACY, a v1 body (version "1" at index 25) β†’ run-ops id β†’ NEW. So a classifiable id // must carry NO internal underscore. These mint a distinct id of the right length from a short seed. function cuidLegacy(seed: string): string { return (seed + "c".repeat(25)).slice(0, 25); // 25 chars, no underscore β†’ LEGACY } -function ksuidNew(seed: string): string { - return (seed + "k".repeat(27)).slice(0, 27); // 27 chars, no underscore β†’ NEW +function runOpsNew(seed: string): string { + return (seed.replace(/[^0-9a-v]/g, "0") + "k".repeat(24)).slice(0, 24) + "01"; } // On the dedicated subset there are no Organization/Project/RuntimeEnvironment models (the run-ops @@ -240,16 +240,16 @@ describe("RoutingRunStore β€” cross-DB idempotency dedup probe", () => { } ); - // the matching run + its associated waitpoint live on #new (ksuid, dedicated subset). The + // the matching run + its associated waitpoint live on #new (run-ops id, dedicated subset). The // probe hits the NEW leg first; the SCALAR-ONLY store must strip the `associatedWaitpoint` relation // and re-hydrate it from `Waitpoint.completedByTaskRunId`. heteroRunOpsPostgresTest( - "a ksuid run on #new is found by the id-less probe with associatedWaitpoint hydrated from scalar", + "a run-ops run on #new is found by the id-less probe with associatedWaitpoint hydrated from scalar", async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "cg2_b"); - const runId = ksuidNew("rbn"); // 27 chars β†’ NEW home - const waitpointId = ksuidNew("wbn"); + const runId = runOpsNew("rbn"); // v1 body β†’ NEW home + const waitpointId = runOpsNew("wbn"); const idempotencyKey = "cg2-key-b"; const taskIdentifier = "my-task"; @@ -271,7 +271,7 @@ describe("RoutingRunStore β€” cross-DB idempotency dedup probe", () => { }) ); - // It must NOT have landed on #legacy (the ksuid id routes to NEW). + // It must NOT have landed on #legacy (the run-ops id routes to NEW). expect(await prisma14.taskRun.findFirst({ where: { id: runId } })).toBeNull(); expect(await prisma17.taskRun.findFirst({ where: { id: runId } })).not.toBeNull(); @@ -293,7 +293,7 @@ describe("RoutingRunStore β€” cross-DB idempotency dedup probe", () => { // duplicate-guard contract: a run with the SAME (env, idempotencyKey, taskIdentifier) // exists on BOTH DBs. The per-DB unique constraint allows one row each (it cannot enforce cross-DB - // uniqueness); the probe MUST still resolve to exactly ONE run, deterministically the NEW (ksuid) + // uniqueness); the probe MUST still resolve to exactly ONE run, deterministically the NEW (run-ops id) // one per #findRunUnrouted (NEW-first). The duplicate itself is prevented upstream by // probe-before-mint plus the per-DB unique constraint; this locks the read tie-break contract. heteroRunOpsPostgresTest( @@ -307,9 +307,9 @@ describe("RoutingRunStore β€” cross-DB idempotency dedup probe", () => { const taskIdentifier = "my-task"; const legacyRunId = cuidLegacy("rcl"); // cuid β†’ LEGACY - const newRunId = ksuidNew("rcn"); // ksuid β†’ NEW + const newRunId = runOpsNew("rcn"); // run-ops id β†’ NEW const legacyWaitpointId = cuidLegacy("wcl"); - const newWaitpointId = ksuidNew("wcn"); + const newWaitpointId = runOpsNew("wcn"); await router.createRun( buildCreateRunInput({ @@ -392,7 +392,7 @@ describe("RoutingRunStore β€” cross-DB idempotency dedup probe", () => { async ({ prisma14, prisma17 }) => { const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedEnvironment(prisma17, "dedicated", "cg2_sa_n"); - const runId = ksuidNew("rsan"); // ksuid β†’ NEW + const runId = runOpsNew("rsan"); // run-ops id β†’ NEW const idempotencyKey = "cg2-key-sa-n"; await router.createRun( diff --git a/internal-packages/run-store/src/runOpsStore.mixedResidency.test.ts b/internal-packages/run-store/src/runOpsStore.mixedResidency.test.ts index f3b5d16465..ca2027d572 100644 --- a/internal-packages/run-store/src/runOpsStore.mixedResidency.test.ts +++ b/internal-packages/run-store/src/runOpsStore.mixedResidency.test.ts @@ -1,10 +1,10 @@ // MIXED-RESIDENCY MATRIX β€” systematic LOCK that every RoutingRunStore fan-out / partition / merge / -// dedup method behaves correctly when cuid (#legacy) AND ksuid (#new) data COEXIST in the SAME call, +// dedup method behaves correctly when cuid (#legacy) AND run-ops id (#new) data COEXIST in the SAME call, // against the REAL two-physical-DB split (heteroRunOpsPostgresTest: prisma14 = full/legacy on PG14, // prisma17 = RunOpsPrismaClient / dedicated subset on PG17). NEVER mocked. // // Existing tests exercise these methods one residency at a time or for a single specific bug. This -// file is the cross-residency matrix: each case seeds BOTH a cuid row on #legacy AND a ksuid row on +// file is the cross-residency matrix: each case seeds BOTH a cuid row on #legacy AND a run-ops id row on // #new in one environment, then drives the wired router and asserts the merge/partition is correct. // The matrix MUST go RED if a fan-out leg is dropped or a NEW-wins dedup regresses (see the reverted // mutation probes recorded in the task report). @@ -20,13 +20,13 @@ import type { CreateRunInput, RunStoreSchemaVariant } from "./types.js"; type AnyClient = PrismaClient | RunOpsPrismaClient; // ownerEngine classifies by internal-id LENGTH after stripping a leading `_` -// (runOpsResidency.ts): 25 chars (no internal underscore) β†’ cuid β†’ LEGACY, 27 chars β†’ ksuid β†’ NEW. +// (runOpsResidency.ts): 25 chars (no internal underscore) β†’ cuid β†’ LEGACY, a v1 body (version "1" at index 25) β†’ run-ops id β†’ NEW. // These mint a distinct classifiable id of the right length from a short seed. function cuidLegacy(seed: string): string { return (seed + "c".repeat(25)).slice(0, 25); // 25 chars β†’ LEGACY (#legacy / prisma14) } -function ksuidNew(seed: string): string { - return (seed + "k".repeat(27)).slice(0, 27); // 27 chars β†’ NEW (#new / prisma17) +function runOpsNew(seed: string): string { + return (seed.replace(/[^0-9a-v]/g, "0") + "k".repeat(24)).slice(0, 24) + "01"; } // On the dedicated subset there are no Organization/Project/RuntimeEnvironment models (run-ops rows @@ -194,11 +194,11 @@ async function seedSharedEnv(prisma14: PrismaClient, suffix: string) { }; } -describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new coexisting)", () => { +describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + run-ops id #new coexisting)", () => { // ── Case 1: findRuns by a MIXED bounded id-set (#findRunsByIdSet, runOpsStore.ts:294) ── - // A list-hydrate id set spans cuid (legacy) + ksuid (new) ids plus a ksuid id absent from legacy. + // A list-hydrate id set spans cuid (legacy) + run-ops id (new) ids plus a run-ops id absent from legacy. // Both resident runs returned; take/skip applied GLOBALLY post-merge; orderBy honored; the absent - // ksuid short-circuits (never probed on LEGACY, :309). + // run-ops id short-circuits (never probed on LEGACY, :309). heteroRunOpsPostgresTest( "case 1: findRuns by a mixed id-set returns both DBs' runs, ordered, take/skip global", async ({ prisma14, prisma17 }) => { @@ -206,8 +206,8 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new const env = await seedSharedEnv(prisma14, "m1"); const legacyId = cuidLegacy("m1l"); // cuid β†’ #legacy - const newId = ksuidNew("m1n"); // ksuid β†’ #new - const ghostKsuid = ksuidNew("m1g"); // ksuid, NEVER created β†’ tests the LEGACY short-circuit + const newId = runOpsNew("m1n"); // run-ops id β†’ #new + const ghostRunOpsId = runOpsNew("m1g"); // run-ops id, NEVER created β†’ tests the LEGACY short-circuit await router.createRun( buildCreateRunInput({ @@ -234,7 +234,7 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new // Full merge, ordered by createdAt asc β†’ newId (Jan 1) before legacyId (Jan 2). const all = await router.findRuns({ - where: { id: { in: [legacyId, newId, ghostKsuid] } }, + where: { id: { in: [legacyId, newId, ghostRunOpsId] } }, select: { id: true, createdAt: true }, orderBy: { createdAt: "asc" }, }); @@ -243,7 +243,7 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new // take=1 after the merge β†’ only the first (newId). Proves take is applied GLOBALLY, not per-leg // (a per-leg take=1 would return one row from EACH DB β†’ both ids). const firstOnly = await router.findRuns({ - where: { id: { in: [legacyId, newId, ghostKsuid] } }, + where: { id: { in: [legacyId, newId, ghostRunOpsId] } }, select: { id: true }, orderBy: { createdAt: "asc" }, take: 1, @@ -252,7 +252,7 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new // skip=1 take=1 β†’ the second (legacyId). const second = await router.findRuns({ - where: { id: { in: [legacyId, newId, ghostKsuid] } }, + where: { id: { in: [legacyId, newId, ghostRunOpsId] } }, select: { id: true }, orderBy: { createdAt: "asc" }, skip: 1, @@ -319,7 +319,7 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new const env = await seedSharedEnv(prisma14, "m2"); const legacyId = cuidLegacy("m2l"); - const newId = ksuidNew("m2n"); + const newId = runOpsNew("m2n"); await router.createRun( buildCreateRunInput({ runId: legacyId, @@ -342,7 +342,7 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new ); await router.createRun( buildCreateRunInput({ - runId: ksuidNew("m2np"), + runId: runOpsNew("m2np"), friendlyId: "run_m2_new_pending", status: "PENDING", ...env, @@ -359,7 +359,7 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new ); // ── Case 3: expireRunsBatch with a MIXED id list (runOpsStore.ts:474) ── - // Partitions ksuidβ†’NEW / cuidβ†’LEGACY; each leg called only when non-empty; counts summed; each row + // Partitions run-ops idβ†’NEW / cuidβ†’LEGACY; each leg called only when non-empty; counts summed; each row // updated on its OWN DB only. heteroRunOpsPostgresTest( "case 3: expireRunsBatch partitions a mixed id list per-DB and sums the count", @@ -368,7 +368,7 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new const env = await seedSharedEnv(prisma14, "m3"); const legacyId = cuidLegacy("m3l"); - const newId = ksuidNew("m3n"); + const newId = runOpsNew("m3n"); await router.createRun( buildCreateRunInput({ runId: legacyId, friendlyId: "run_m3_legacy", ...env }) ); @@ -400,7 +400,7 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new const env = await seedSharedEnv(prisma14, "m4"); const legacyId = cuidLegacy("m4l"); - const newId = ksuidNew("m4n"); + const newId = runOpsNew("m4n"); await router.createRun( buildCreateRunInput({ runId: legacyId, @@ -441,8 +441,8 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new const env = await seedSharedEnv(prisma14, "m5"); const legacyWp = cuidLegacy("m5l"); // PENDING on #legacy - const newWp = ksuidNew("m5n"); // PENDING on #new - const completedWp = ksuidNew("m5c"); // COMPLETED on #new β†’ must NOT be counted + const newWp = runOpsNew("m5n"); // PENDING on #new + const completedWp = runOpsNew("m5c"); // COMPLETED on #new β†’ must NOT be counted await seedPendingWaitpoint(prisma14, { id: legacyWp, friendlyId: "wp_m5_legacy", @@ -477,7 +477,7 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new const env = await seedSharedEnv(prisma14, "m6"); const legacyWp = cuidLegacy("m6l"); - const newWp = ksuidNew("m6n"); + const newWp = runOpsNew("m6n"); await seedPendingWaitpoint(prisma14, { id: legacyWp, friendlyId: "wp_m6_legacy", @@ -499,7 +499,7 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new // ── Case 8: findExecutionSnapshot / findManyExecutionSnapshots OPEN (no runId) where ── // A by-snapshot-id-only lookup (snapshot ids are non-classifiable cuids) must fan out NEWβ†’LEGACY // (findExecutionSnapshot, :675) / merge both (findManyExecutionSnapshots, :688). Seed a snapshot on - // EACH DB (one ksuid run on #new, one cuid run on #legacy) and read with a no-runId where. + // EACH DB (one run-ops run on #new, one cuid run on #legacy) and read with a no-runId where. heteroRunOpsPostgresTest( "case 8: findExecutionSnapshot/findManyExecutionSnapshots with an open where reach both DBs", async ({ prisma14, prisma17 }) => { @@ -507,7 +507,7 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new const env = await seedSharedEnv(prisma14, "m8"); const legacyRun = cuidLegacy("m8l"); - const newRun = ksuidNew("m8n"); + const newRun = runOpsNew("m8n"); await router.createRun( buildCreateRunInput({ runId: legacyRun, friendlyId: "run_m8_legacy", ...env }) ); @@ -548,7 +548,7 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new ); // ── Case 9a: findRun with an UNCLASSIFIABLE where (spanId) on a #legacy run (#findRunUnrouted, :213) ── - // A ksuid run on #new and a cuid run on #legacy each carry a distinct spanId. A spanId where can't + // A run-ops run on #new and a cuid run on #legacy each carry a distinct spanId. A spanId where can't // be id-classified β†’ fan out NEW-first then LEGACY. The legacy-resident run must be found. heteroRunOpsPostgresTest( "case 9a: findRun by spanId fans out and finds a #legacy run (NEW miss β†’ LEGACY hit)", @@ -557,7 +557,7 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new const env = await seedSharedEnv(prisma14, "m9a"); const legacyRun = cuidLegacy("m9al"); - const newRun = ksuidNew("m9an"); + const newRun = runOpsNew("m9an"); await router.createRun( buildCreateRunInput({ runId: legacyRun, @@ -599,7 +599,7 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new const env = await seedSharedEnv(prisma14, "m9b"); const legacyRun = cuidLegacy("m9bl"); - const newRun = ksuidNew("m9bn"); + const newRun = runOpsNew("m9bn"); await router.createRun( buildCreateRunInput({ runId: legacyRun, @@ -633,7 +633,7 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new // ── Case 7: findManyTaskRunWaitpoints with edges whose relations STRADDLE DBs (runOpsStore.ts:876) ── // An edge co-locates with its RUN, but its `waitpoint`/`taskRun` relations can live on the OTHER DB - // (a cuid token blocking a ksuid run, and vice versa). The per-leg scalar query is stripped of the + // (a cuid token blocking a run-ops run, and vice versa). The per-leg scalar query is stripped of the // relation keys; the router re-hydrates `waitpoint`/`taskRun` across BOTH DBs. Exercises BOTH // straddle directions in one read by querying both edges via { taskRunId: { in } }. heteroRunOpsPostgresTest( @@ -642,8 +642,8 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedSharedEnv(prisma14, "m7"); - // Direction A: ksuid run on #new, blocked on a cuid token that lives ONLY on #legacy. Edge on #new. - const newRun = ksuidNew("m7nr"); + // Direction A: run-ops run on #new, blocked on a cuid token that lives ONLY on #legacy. Edge on #new. + const newRun = runOpsNew("m7nr"); const legacyToken = cuidLegacy("m7lt"); await router.createRun( buildCreateRunInput({ runId: newRun, friendlyId: "run_m7_new", ...env }) @@ -660,12 +660,12 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new `INSERT INTO "TaskRunWaitpoint" ("id","taskRunId","waitpointId","projectId","createdAt","updatedAt") VALUES (gen_random_uuid(),'${newRun}','${legacyToken}','${env.projectId}',NOW(),NOW())` ); - // Direction B: cuid run on #legacy, blocked on a ksuid token mirrored onto BOTH DBs (drain + // Direction B: cuid run on #legacy, blocked on a run-ops token mirrored onto BOTH DBs (drain // window). The #legacy copy is a STALE placeholder (PENDING) that satisfies the legacy edge FK; // the AUTHORITATIVE #new copy is COMPLETED. Edge on #legacy. Hydration re-resolves cross-DB and // NEW-wins the dedup β†’ the edge's waitpoint must read the #new (COMPLETED) copy, not the local mirror. const legacyRun = cuidLegacy("m7lr"); - const newToken = ksuidNew("m7nt"); + const newToken = runOpsNew("m7nt"); await router.createRun( buildCreateRunInput({ runId: legacyRun, friendlyId: "run_m7_legacy", ...env }) ); @@ -733,8 +733,8 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new const { router } = makeSplitRouter(prisma14, prisma17); const env = await seedSharedEnv(prisma14, "m7b"); - const newRun = ksuidNew("m7br"); - const ghostToken = ksuidNew("m7bg"); // never created on either DB + const newRun = runOpsNew("m7br"); + const ghostToken = runOpsNew("m7bg"); // never created on either DB await router.createRun( buildCreateRunInput({ runId: newRun, friendlyId: "run_m7b_new", ...env }) ); @@ -752,8 +752,8 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new ); // ── Case 10: findBatchTaskRunById / findBatchTaskRunByFriendlyId NEW-then-LEGACY probe (:1124,:1137) ── - // A batch resident on #legacy AND a ksuid-id batch landed on #new (the control-plane window mints - // cuid ids, but a ksuid batch resides on #new) are BOTH found via the probe, regardless of id-shape. + // A batch resident on #legacy AND a run-ops-id batch landed on #new (the control-plane window mints + // cuid ids, but a run-ops batch resides on #new) are BOTH found via the probe, regardless of id-shape. heteroRunOpsPostgresTest( "case 10: findBatchTaskRunById/byFriendlyId probe NEW then LEGACY and find batches on either DB", async ({ prisma14, prisma17 }) => { @@ -761,7 +761,7 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new const env = await seedSharedEnv(prisma14, "m10"); const legacyBatch = cuidLegacy("m10l"); // cuid β†’ #legacy - const newBatch = ksuidNew("m10n"); // ksuid β†’ #new + const newBatch = runOpsNew("m10n"); // run-ops id β†’ #new await prisma14.batchTaskRun.create({ data: { id: legacyBatch, @@ -805,7 +805,7 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new const env = await seedSharedEnv(prisma14, "m11a"); const legacyWp = cuidLegacy("m11al"); - const newWp = ksuidNew("m11an"); + const newWp = runOpsNew("m11an"); await seedPendingWaitpoint(prisma14, { id: legacyWp, friendlyId: "wp_m11a_legacy", @@ -846,7 +846,7 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new // ONE logical run id whose edges happen to exist on BOTH DBs (the straddle the fan-out guards). // The edge is FK-free on #new (unnest path) and FK-bound on #legacy, so seed a co-resident // waitpoint + run on #legacy for its edge, and write the #new edge directly. - const runId = ksuidNew("m11br"); + const runId = runOpsNew("m11br"); const legacyToken = cuidLegacy("m11bt"); await router.createRun(buildCreateRunInput({ runId, friendlyId: "run_m11b", ...env })); // #legacy needs the run + token present for the FK-bound edge insert. @@ -881,8 +881,8 @@ describe("RoutingRunStore β€” mixed-residency matrix (cuid #legacy + ksuid #new await prisma14.$executeRawUnsafe( `INSERT INTO "TaskRunWaitpoint" ("id","taskRunId","waitpointId","projectId","createdAt","updatedAt") VALUES (gen_random_uuid(),'${runId}','${legacyToken}','${env.projectId}',NOW(),NOW())` ); - // #new edge (FK-free) pointing at a ksuid token absent locally β€” drain straddle. - const newToken = ksuidNew("m11bn"); + // #new edge (FK-free) pointing at a run-ops token absent locally β€” drain straddle. + const newToken = runOpsNew("m11bn"); await prisma17.$executeRawUnsafe( `INSERT INTO "TaskRunWaitpoint" ("id","taskRunId","waitpointId","projectId","createdAt","updatedAt") VALUES (gen_random_uuid(),'${runId}','${newToken}','${env.projectId}',NOW(),NOW())` ); diff --git a/internal-packages/run-store/src/runOpsStore.readAfterWrite.test.ts b/internal-packages/run-store/src/runOpsStore.readAfterWrite.test.ts index a32ac9a12d..a08408455e 100644 --- a/internal-packages/run-store/src/runOpsStore.readAfterWrite.test.ts +++ b/internal-packages/run-store/src/runOpsStore.readAfterWrite.test.ts @@ -26,9 +26,9 @@ import { RoutingRunStore } from "./runOpsStore.js"; type AnyClient = PrismaClient | RunOpsPrismaClient; -// ownerEngine classifies by internal-id LENGTH: 25 chars β†’ cuid β†’ LEGACY, 27 β†’ ksuid β†’ NEW. +// ownerEngine classifies by internal-id LENGTH: 25 chars β†’ cuid β†’ LEGACY, 27 β†’ run-ops id β†’ NEW. const CUID_25 = "c".repeat(25); // β†’ LEGACY (#legacy / prisma14, full schema) -const KSUID_27 = "k".repeat(27); // β†’ NEW (#new / prisma17, dedicated subset schema) +const NEW_ID_26 = "k".repeat(24) + "01"; // β†’ NEW (#new / prisma17, dedicated subset schema) // A recording "replica" that has NOT yet caught up: its taskRun reads always come back empty and // record that they ran, so a replica-routed read misses the just-written row. Everything else @@ -203,12 +203,12 @@ describe("run-ops split β€” read-after-write reads the OWNING store's WRITER, no } ); - // (b) NEW-resident (ksuid) run: born on the NEW DB (5434). The NEW replica lags. Passing the NEW + // (b) NEW-resident (run-ops id) run: born on the NEW DB (5434). The NEW replica lags. Passing the NEW // WRITER as the read-your-writes client must resolve the run via the NEW writer, NOT its replica β€” // and (proving the constraint that motivated the original client-drop) the control-plane writer is // never leaked into the NEW query: each store reads its OWN writer. heteroRunOpsPostgresTest( - "NEW ksuid: read-after-write via the NEW WRITER finds the fresh run despite NEW replica lag", + "NEW run-ops id: read-after-write via the NEW WRITER finds the fresh run despite NEW replica lag", async ({ prisma14, prisma17 }) => { const newReplica = laggingReplica(prisma17); const newStore = new PostgresRunStore({ @@ -224,7 +224,7 @@ describe("run-ops split β€” read-after-write reads the OWNING store's WRITER, no const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); const seed = seedEnvironmentDedicated("raw_new"); - const runId = `run_${KSUID_27}`; // ksuid β†’ NEW + const runId = `run_${NEW_ID_26}`; // run-ops id β†’ NEW await prisma17.taskRun.create({ data: taskRunData({ id: runId, @@ -259,7 +259,7 @@ describe("run-ops split β€” read-after-write reads the OWNING store's WRITER, no expect(newReplica2.wasHit()).toBe(false); // Even passing the LEGACY (control-plane) WRITER as the read-your-writes signal resolves the - // ksuid run: the router routes by residency to the NEW store's OWN writer, never forwarding the + // run-ops run: the router routes by residency to the NEW store's OWN writer, never forwarding the // control-plane client into the NEW DB. (This is the exact live shape β€” sessions/trigger pass // the control-plane `prisma`, and the run may be NEW-resident under split-ON.) const newReplica3 = laggingReplica(prisma17); diff --git a/internal-packages/run-store/src/runOpsStore.routedReadPrimary.test.ts b/internal-packages/run-store/src/runOpsStore.routedReadPrimary.test.ts index 4e9b191de5..8d2333cf81 100644 --- a/internal-packages/run-store/src/runOpsStore.routedReadPrimary.test.ts +++ b/internal-packages/run-store/src/runOpsStore.routedReadPrimary.test.ts @@ -12,6 +12,7 @@ // primary-routed read finds the row, with no replica==primary aliasing to mask the drop. import { heteroPostgresTest } from "@internal/testcontainers"; +import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic"; import type { PrismaClient } from "@trigger.dev/database"; import { describe, expect } from "vitest"; import { PostgresRunStore } from "./PostgresRunStore.js"; @@ -19,9 +20,10 @@ import { RoutingRunStore } from "./runOpsStore.js"; import { markReadReplicaClient } from "./readReplicaClient.js"; import type { CreateRunInput } from "./types.js"; -// ownerEngine classifies by internal-id LENGTH: 25 chars β†’ cuid β†’ LEGACY, 27 β†’ ksuid β†’ NEW. +// ownerEngine classifies by the version char: a 25-char cuid β†’ LEGACY; a valid run-ops v1 body +// (26 chars: base32hex core + region char + version "1") β†’ NEW. const CUID_25 = "c".repeat(25); -const KSUID_27 = "k".repeat(27); +const RUN_OPS_ID_BODY = generateRunOpsId(); // Router topology where the OWNING store (the one the test's run ids route to) writes to `writer` // but reads by default from `lagging` β€” a physically separate, never-written DB. The other store @@ -189,16 +191,16 @@ describe("run-ops split β€” routed reads honor a caller-passed client via the ow } ); - // NEW (ksuid) routing arm. The caller's client here is the CONTROL-PLANE writer β€” the wrong + // NEW (run-ops id) routing arm. The caller's client here is the CONTROL-PLANE writer β€” the wrong // physical DB for a NEW-resident run β€” so this also pins that the client is never forwarded // verbatim: the read must resolve on the owning NEW store's OWN primary. heteroPostgresTest( - "NEW ksuid: a control-plane client routes the snapshot read to the NEW store's OWN primary", + "NEW run-ops id: a control-plane client routes the snapshot read to the NEW store's OWN primary", async ({ prisma14, prisma17 }) => { // Owning (NEW) store writes to prisma14; the control-plane/other store is prisma17. const { router } = splitTopology("NEW", prisma14, prisma17); const seed = await seedEnvironment(prisma14, "snap_new"); - const runId = `run_${KSUID_27}`; + const runId = `run_${RUN_OPS_ID_BODY}`; await router.createRun( buildCreateRunInput({ runId, diff --git a/internal-packages/run-store/src/runOpsStore.snapshots.test.ts b/internal-packages/run-store/src/runOpsStore.snapshots.test.ts index 564afa0813..6ee9ca0211 100644 --- a/internal-packages/run-store/src/runOpsStore.snapshots.test.ts +++ b/internal-packages/run-store/src/runOpsStore.snapshots.test.ts @@ -60,9 +60,9 @@ async function seedEnvironment( return { organization, project, environment }; } -// ownerEngine classifies by internal-id LENGTH after stripping a single leading `_`: 27 chars -// β†’ ksuid β†’ NEW (#new / dedicated run-ops DB subset), 25 chars β†’ cuid β†’ LEGACY (#legacy / full schema). -const KSUID_27 = "k".repeat(27); // β†’ NEW residency, exercises the dedicated store +// ownerEngine classifies by the version char after stripping a single leading `_`: a v1 body +// β†’ run-ops id β†’ NEW (#new / dedicated run-ops DB subset), 25 chars β†’ cuid β†’ LEGACY (#legacy / full schema). +const NEW_ID_26 = "k".repeat(24) + "01"; // β†’ NEW residency, exercises the dedicated store const CUID_25 = "c".repeat(25); // β†’ LEGACY residency, exercises the full-schema store function buildCreateRunInput(params: { @@ -201,7 +201,7 @@ describe("RunStore run-ops persistence β€” snapshots", () => { }; const legacyRunId = `run_${CUID_25}`; // β†’ #legacy (full schema) - const newRunId = `run_${KSUID_27}`; // β†’ #new (dedicated subset) + const newRunId = `run_${NEW_ID_26}`; // β†’ #new (dedicated subset) const seed14 = await seed(prisma14, "legacy", legacyRunId, "sa14"); const seed17 = await seed(prisma17, "dedicated", newRunId, "sa17"); @@ -292,7 +292,7 @@ describe("RunStore run-ops persistence β€” snapshots", () => { }; const r14 = await run(prisma14, "legacy", `run_${CUID_25}`, "sb14"); - const r17 = await run(prisma17, "dedicated", `run_${KSUID_27}`, "sb17"); + const r17 = await run(prisma17, "dedicated", `run_${NEW_ID_26}`, "sb17"); // The join links the snapshot to both waitpoints (set-equal) on both stores. expect([...r14.joinIds].sort()).toEqual([r14.w1, r14.w2].sort()); @@ -344,7 +344,7 @@ describe("RunStore run-ops persistence β€” snapshots", () => { }; await seed(prisma14, "legacy", `run_${CUID_25}`, "sc14"); - await seed(prisma17, "dedicated", `run_${KSUID_27}`, "sc17"); + await seed(prisma17, "dedicated", `run_${NEW_ID_26}`, "sc17"); const orderedDescriptions = async (client: AnyClient) => { const rows = await (client as PrismaClient).$queryRawUnsafe<{ description: string }[]>( diff --git a/internal-packages/run-store/src/runOpsStore.test.ts b/internal-packages/run-store/src/runOpsStore.test.ts index 6d65616dea..e64bf9f28c 100644 --- a/internal-packages/run-store/src/runOpsStore.test.ts +++ b/internal-packages/run-store/src/runOpsStore.test.ts @@ -6,9 +6,9 @@ import { PostgresRunStore } from "./PostgresRunStore.js"; import { RoutingRunStore } from "./runOpsStore.js"; import type { CreateRunInput } from "./types.js"; -// 25-char internal id β†’ cuid β†’ LEGACY; 27-char internal id β†’ ksuid β†’ NEW. +// 25-char internal id β†’ cuid β†’ LEGACY; v1 internal id (26 chars, version "1" at index 25) β†’ NEW. const CUID_25 = "c".repeat(25); -const KSUID_27 = "k".repeat(27); +const NEW_ID_26 = "k".repeat(24) + "01"; async function seedEnvironment(prisma: PrismaClient, slugSuffix: string) { const organization = await prisma.organization.create({ @@ -229,7 +229,7 @@ describe("RoutingRunStore (TaskRun-core)", () => { const seed17 = await seedEnvironment(prisma17, "c17"); // (i) createRun lands on NEW, never on LEGACY. - const bornId = "run_born_on_new"; + const bornId = `run_${"b".repeat(24)}01`; await router.createRun( buildCreateRunInput({ runId: bornId, @@ -243,10 +243,10 @@ describe("RoutingRunStore (TaskRun-core)", () => { expect(await prisma17.taskRun.findUnique({ where: { id: bornId } })).not.toBeNull(); expect(await prisma14.taskRun.findUnique({ where: { id: bornId } })).toBeNull(); - // (ii) seed a cuid-length (LEGACY) row on the legacy DB and a ksuid-length (NEW) row on + // (ii) seed a cuid-length (LEGACY) row on the legacy DB and a run-ops id-length (NEW) row on // the new DB, then prove residency selection via ownerEngine length classification. const legacyRunId = `run_${CUID_25}`; - const newRunId = `run_${KSUID_27}`; + const newRunId = `run_${NEW_ID_26}`; await legacyStore.createRun( buildCreateRunInput({ runId: legacyRunId, @@ -355,9 +355,9 @@ describe("RoutingRunStore (TaskRun-core)", () => { const seed = await seedEnvironment(prisma14, "d14"); - // Use a ksuid-length (NEW-residency) id to exercise the route; in single-DB both + // Use a run-ops id-length (NEW-residency) id to exercise the route; in single-DB both // slots are the same store, so the round-trip must succeed on the one client. - const runId = `run_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; await router.createRun( buildCreateRunInput({ runId, @@ -484,7 +484,7 @@ describe("BatchTaskRun group", () => { const seed17 = await seedEnvironment(prisma17, "batchb17"); - const batchId = "batch_born_on_new"; + const batchId = `batch_${"b".repeat(24)}01`; await router.createBatchTaskRun( batchCreateData({ id: batchId, @@ -536,19 +536,19 @@ describe("BatchTaskRun group", () => { ); heteroPostgresTest( - "findBatchTaskRunById routes ksuidβ†’NEW and cuidβ†’LEGACY", + "findBatchTaskRunById routes run-ops idβ†’NEW and cuidβ†’LEGACY", async ({ prisma14, prisma17 }) => { const legacyStore = new PostgresRunStore({ prisma: prisma14, readOnlyPrisma: prisma14 }); const newStore = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 }); const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); const seed14 = await seedEnvironment(prisma14, "p8a_cuid14"); - const seed17 = await seedEnvironment(prisma17, "p8a_ksuid17"); + const seed17 = await seedEnvironment(prisma17, "p8a_runops17"); await newStore.createBatchTaskRun( batchCreateData({ - id: KSUID_27, - friendlyId: "batch_ksuid_p8a", + id: NEW_ID_26, + friendlyId: "batch_runops_p8a", runtimeEnvironmentId: seed17.environment.id, runCount: 1, }) @@ -562,13 +562,13 @@ describe("BatchTaskRun group", () => { }) ); - expect((await router.findBatchTaskRunById(KSUID_27))?.id).toBe(KSUID_27); + expect((await router.findBatchTaskRunById(NEW_ID_26))?.id).toBe(NEW_ID_26); expect((await router.findBatchTaskRunById(CUID_25))?.id).toBe(CUID_25); } ); heteroPostgresTest( - "updateBatchTaskRun routes cuidβ†’LEGACY and ksuidβ†’NEW", + "updateBatchTaskRun routes cuidβ†’LEGACY and run-ops idβ†’NEW", async ({ prisma14, prisma17 }) => { const legacyStore = new PostgresRunStore({ prisma: prisma14, readOnlyPrisma: prisma14 }); const newStore = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 }); @@ -577,13 +577,13 @@ describe("BatchTaskRun group", () => { const seed14 = await seedEnvironment(prisma14, "p8a_upd14"); const seed17 = await seedEnvironment(prisma17, "p8a_upd17"); - const ksuidBatchId = `${KSUID_27.slice(0, -1)}u`; + const runOpsBatchId = `${NEW_ID_26.slice(0, -2)}u1`; const cuidBatchId = `${CUID_25.slice(0, -1)}u`; await newStore.createBatchTaskRun( batchCreateData({ - id: ksuidBatchId, - friendlyId: "batch_ksuid_upd", + id: runOpsBatchId, + friendlyId: "batch_runops_upd", runtimeEnvironmentId: seed17.environment.id, runCount: 2, }) @@ -598,12 +598,12 @@ describe("BatchTaskRun group", () => { ); const updNew = await router.updateBatchTaskRun({ - where: { id: ksuidBatchId }, + where: { id: runOpsBatchId }, data: { processingJobsCount: { increment: 1 } }, select: { processingJobsCount: true, runCount: true }, }); expect(updNew).toEqual({ processingJobsCount: 1, runCount: 2 }); - expect(await prisma14.batchTaskRun.findUnique({ where: { id: ksuidBatchId } })).toBeNull(); + expect(await prisma14.batchTaskRun.findUnique({ where: { id: runOpsBatchId } })).toBeNull(); const updLegacy = await router.updateBatchTaskRun({ where: { id: cuidBatchId }, @@ -623,18 +623,18 @@ describe("BatchTaskRun group", () => { const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); const seed17 = await seedEnvironment(prisma17, "p8a_inc17"); - const ksuidBatchId = `${KSUID_27.slice(0, -2)}in`; + const runOpsBatchId = `${NEW_ID_26.slice(0, -2)}i1`; await newStore.createBatchTaskRun( batchCreateData({ - id: ksuidBatchId, + id: runOpsBatchId, friendlyId: "batch_inc_p8a", runtimeEnvironmentId: seed17.environment.id, runCount: 1, }) ); - const runId = `${KSUID_27.slice(0, -3)}run`; + const runId = `${NEW_ID_26.slice(0, -3)}ru1`; await prisma17.taskRun.create({ data: { id: runId, @@ -659,10 +659,10 @@ describe("BatchTaskRun group", () => { }, }); await prisma17.batchTaskRunItem.create({ - data: { batchTaskRunId: ksuidBatchId, taskRunId: runId, status: "PENDING" }, + data: { batchTaskRunId: runOpsBatchId, taskRunId: runId, status: "PENDING" }, }); - const withItems = await router.findBatchTaskRunById(ksuidBatchId, { + const withItems = await router.findBatchTaskRunById(runOpsBatchId, { include: { items: true }, }); expect(withItems?.items).toBeDefined(); @@ -672,30 +672,30 @@ describe("BatchTaskRun group", () => { ); heteroPostgresTest( - "createBatchTaskRun routes ksuidβ†’NEW and cuidβ†’LEGACY", + "createBatchTaskRun routes run-ops idβ†’NEW and cuidβ†’LEGACY", async ({ prisma14, prisma17 }) => { const legacyStore = new PostgresRunStore({ prisma: prisma14, readOnlyPrisma: prisma14 }); const newStore = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 }); const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); const seed14 = await seedEnvironment(prisma14, "p8c_cuid14"); - const seed17 = await seedEnvironment(prisma17, "p8c_ksuid17"); + const seed17 = await seedEnvironment(prisma17, "p8c_runops17"); - const ksuidBatchId = `${KSUID_27.slice(0, -2)}c1`; + const runOpsBatchId = `${NEW_ID_26.slice(0, -2)}c1`; const cuidBatchId = `${CUID_25.slice(0, -2)}c1`; await router.createBatchTaskRun( batchCreateData({ - id: ksuidBatchId, - friendlyId: "batch_p8c_ksuid", + id: runOpsBatchId, + friendlyId: "batch_p8c_runops", runtimeEnvironmentId: seed17.environment.id, runCount: 1, }) ); expect( - await prisma17.batchTaskRun.findUnique({ where: { id: ksuidBatchId } }) + await prisma17.batchTaskRun.findUnique({ where: { id: runOpsBatchId } }) ).not.toBeNull(); - expect(await prisma14.batchTaskRun.findUnique({ where: { id: ksuidBatchId } })).toBeNull(); + expect(await prisma14.batchTaskRun.findUnique({ where: { id: runOpsBatchId } })).toBeNull(); await router.createBatchTaskRun( batchCreateData({ @@ -710,10 +710,10 @@ describe("BatchTaskRun group", () => { } ); - // Probe: a ksuid-id batch physically resident on LEGACY (written by batchTriggerV3 raw - // to the control-plane) must be found; strict id-routing (ksuidβ†’NEW only) would miss it. + // Probe: a run-ops-id batch physically resident on LEGACY (written by batchTriggerV3 raw + // to the control-plane) must be found; strict id-routing (run-ops idβ†’NEW only) would miss it. heteroPostgresTest( - "findBatchTaskRunById probe finds ksuid-id batch resident on LEGACY (cross-residency)", + "findBatchTaskRunById probe finds run-ops-id batch resident on LEGACY (cross-residency)", async ({ prisma14, prisma17 }) => { const legacyStore = new PostgresRunStore({ prisma: prisma14, readOnlyPrisma: prisma14 }); const newStore = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 }); @@ -722,36 +722,38 @@ describe("BatchTaskRun group", () => { const seed14 = await seedEnvironment(prisma14, "p8c_probe14"); const seed17 = await seedEnvironment(prisma17, "p8c_probe17"); - const ksuidOnLegacy = `${KSUID_27.slice(0, -2)}pl`; + const runOpsIdOnLegacy = `${NEW_ID_26.slice(0, -2)}l1`; await legacyStore.createBatchTaskRun( batchCreateData({ - id: ksuidOnLegacy, + id: runOpsIdOnLegacy, friendlyId: "batch_p8c_probe_legacy", runtimeEnvironmentId: seed14.environment.id, runCount: 1, }) ); expect( - await prisma14.batchTaskRun.findUnique({ where: { id: ksuidOnLegacy } }) + await prisma14.batchTaskRun.findUnique({ where: { id: runOpsIdOnLegacy } }) ).not.toBeNull(); - expect(await prisma17.batchTaskRun.findUnique({ where: { id: ksuidOnLegacy } })).toBeNull(); + expect( + await prisma17.batchTaskRun.findUnique({ where: { id: runOpsIdOnLegacy } }) + ).toBeNull(); - expect((await router.findBatchTaskRunById(ksuidOnLegacy))?.id).toBe(ksuidOnLegacy); + expect((await router.findBatchTaskRunById(runOpsIdOnLegacy))?.id).toBe(runOpsIdOnLegacy); - const ksuidOnNew = `${KSUID_27.slice(0, -2)}pn`; + const runOpsIdOnNew = `${NEW_ID_26.slice(0, -2)}n1`; await newStore.createBatchTaskRun( batchCreateData({ - id: ksuidOnNew, + id: runOpsIdOnNew, friendlyId: "batch_p8c_probe_new", runtimeEnvironmentId: seed17.environment.id, runCount: 1, }) ); - expect((await router.findBatchTaskRunById(ksuidOnNew))?.id).toBe(ksuidOnNew); + expect((await router.findBatchTaskRunById(runOpsIdOnNew))?.id).toBe(runOpsIdOnNew); } ); - // A BATCH-completion waitpoint (cuid own-id, `completedByBatchId` = ksuid batch on NEW) must be + // A BATCH-completion waitpoint (cuid own-id, `completedByBatchId` = run-ops batch on NEW) must be // born on NEW alongside its batch. On the control-plane DB (prisma14) the Waitpointβ†’BatchTaskRun // FK is enforced, so routing by the waitpoint's own cuid id-shape would land it on LEGACY and // FK-violate against the absent batch. The dedicated run-ops schema carries `completedByBatchId` as a scalar. @@ -765,8 +767,8 @@ describe("BatchTaskRun group", () => { }); const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); - // The ksuid batch lives on NEW only β€” never on the control-plane DB. - const batchId = `${KSUID_27.slice(0, -2)}bw`; + // The run-ops batch lives on NEW only β€” never on the control-plane DB. + const batchId = `${NEW_ID_26.slice(0, -2)}b1`; await prisma17.batchTaskRun.create({ data: { id: batchId, @@ -814,7 +816,7 @@ describe("BatchTaskRun group", () => { describe("RoutingRunStore cross-DB client + friendlyId routing (regression)", () => { // A create routed to NEW must land on NEW even when the caller forwards the LEGACY // client as `tx` (the webapp passes its control-plane client there). If the router - // forwarded it, the ksuid run would be written through the legacy connection. + // forwarded it, the run-ops run would be written through the legacy connection. heteroPostgresTest( "createRun ignores a forwarded wrong-DB tx and lands the run on its owning store", async ({ prisma14, prisma17 }) => { @@ -823,11 +825,11 @@ describe("RoutingRunStore cross-DB client + friendlyId routing (regression)", () const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); const seed17 = await seedEnvironment(prisma17, "txnew17"); - const newRunId = KSUID_27; + const newRunId = NEW_ID_26; await router.createRun( buildCreateRunInput({ runId: newRunId, - friendlyId: `run_${KSUID_27}`, + friendlyId: `run_${NEW_ID_26}`, taskIdentifier: "tx-task", organizationId: seed17.organization.id, projectId: seed17.project.id, @@ -854,7 +856,7 @@ describe("RoutingRunStore cross-DB client + friendlyId routing (regression)", () const seed17 = await seedEnvironment(prisma17, "fid17"); const legacyFriendly = `run_${CUID_25}`; - const newFriendly = `run_${KSUID_27}`; + const newFriendly = `run_${NEW_ID_26}`; await legacyStore.createRun( buildCreateRunInput({ runId: CUID_25, @@ -867,7 +869,7 @@ describe("RoutingRunStore cross-DB client + friendlyId routing (regression)", () ); await newStore.createRun( buildCreateRunInput({ - runId: KSUID_27, + runId: NEW_ID_26, friendlyId: newFriendly, taskIdentifier: "new-task", organizationId: seed17.organization.id, @@ -877,7 +879,7 @@ describe("RoutingRunStore cross-DB client + friendlyId routing (regression)", () ); expect((await router.findRun({ friendlyId: legacyFriendly }))?.id).toBe(CUID_25); - expect((await router.findRun({ friendlyId: newFriendly }))?.id).toBe(KSUID_27); + expect((await router.findRun({ friendlyId: newFriendly }))?.id).toBe(NEW_ID_26); } ); @@ -894,8 +896,8 @@ describe("RoutingRunStore cross-DB client + friendlyId routing (regression)", () await newStore.createRun( buildCreateRunInput({ - runId: KSUID_27, - friendlyId: `run_${KSUID_27}`, + runId: NEW_ID_26, + friendlyId: `run_${NEW_ID_26}`, taskIdentifier: "write-task", organizationId: seed17.organization.id, projectId: seed17.project.id, @@ -904,7 +906,7 @@ describe("RoutingRunStore cross-DB client + friendlyId routing (regression)", () ); const result = await router.updateMetadata( - KSUID_27, + NEW_ID_26, { metadata: '{"x":1}', metadataVersion: { increment: 1 }, updatedAt: new Date() }, {}, // Forwarded LEGACY client β€” must be ignored. @@ -913,7 +915,7 @@ describe("RoutingRunStore cross-DB client + friendlyId routing (regression)", () expect(result.count).toBe(1); const row = await prisma17.taskRun.findUnique({ - where: { id: KSUID_27 }, + where: { id: NEW_ID_26 }, select: { metadata: true }, }); expect(row?.metadata).toBe('{"x":1}'); @@ -923,9 +925,10 @@ describe("RoutingRunStore cross-DB client + friendlyId routing (regression)", () describe("RoutingRunStore.findRuns split-mode fan-out + drain", () => { // Internal-id convention (matches the file): `run_` + a 25-char body (cuid β†’ LEGACY) or - // a 27-char body (ksuid β†’ NEW). The classifier strips `run_` then keys on body length. + // a v1 body (run-ops id β†’ NEW). The classifier strips `run_` then keys on the version char. const legacyId = (suffix: string) => `run_${"c".repeat(25 - suffix.length)}${suffix}`; - const newId = (suffix: string) => `run_${"k".repeat(27 - suffix.length)}${suffix}`; + const newId = (suffix: string) => + `run_${(suffix.replace(/[^0-9a-v]/g, "0") + "k".repeat(24)).slice(0, 24)}01`; async function createRunOn( store: PostgresRunStore, @@ -1290,7 +1293,7 @@ describe("RoutingRunStore.findRuns split-mode fan-out + drain", () => { } ); - // A waitpoint must be born on the same DB as its run (cuid β†’ LEGACY, ksuid β†’ NEW) so that + // A waitpoint must be born on the same DB as its run (cuid β†’ LEGACY, run-ops id β†’ NEW) so that // completion and the blocking edge β€” which already routes by run id β€” line up. A cuid // waitpoint landing on NEW is the regression that strands a non-opted org's wait forever. heteroPostgresTest( @@ -1317,10 +1320,10 @@ describe("RoutingRunStore.findRuns split-mode fan-out + drain", () => { expect(await prisma14.waitpoint.findUnique({ where: { id: cuidWp } })).not.toBeNull(); expect(await prisma17.waitpoint.findUnique({ where: { id: cuidWp } })).toBeNull(); - const ksuidWp = `waitpoint_${KSUID_27}`; + const runOpsWp = `waitpoint_${NEW_ID_26}`; await router.createWaitpoint({ data: { - id: ksuidWp, + id: runOpsWp, friendlyId: "waitpoint_co_k", type: "MANUAL", idempotencyKey: "co-key-k", @@ -1329,8 +1332,8 @@ describe("RoutingRunStore.findRuns split-mode fan-out + drain", () => { environmentId: seed17.environment.id, }, }); - expect(await prisma17.waitpoint.findUnique({ where: { id: ksuidWp } })).not.toBeNull(); - expect(await prisma14.waitpoint.findUnique({ where: { id: ksuidWp } })).toBeNull(); + expect(await prisma17.waitpoint.findUnique({ where: { id: runOpsWp } })).not.toBeNull(); + expect(await prisma14.waitpoint.findUnique({ where: { id: runOpsWp } })).toBeNull(); } ); }); @@ -1339,7 +1342,8 @@ describe("RoutingRunStore.findRuns split-mode fan-out + drain", () => { // prisma17 is RunOpsPrismaClient (subset schema, no control-plane tables). describe("RoutingRunStore.findRuns cross-DB fan-out over distinct schemas", () => { const legacyId = (suffix: string) => `run_${"c".repeat(25 - suffix.length)}${suffix}`; - const newId = (suffix: string) => `run_${"k".repeat(27 - suffix.length)}${suffix}`; + const newId = (suffix: string) => + `run_${(suffix.replace(/[^0-9a-v]/g, "0") + "k".repeat(24)).slice(0, 24)}01`; heteroRunOpsPostgresTest( "id-set fans out across NEW (RunOpsPrismaClient) and LEGACY (PrismaClient) distinct schemas", @@ -1367,10 +1371,10 @@ describe("RoutingRunStore.findRuns cross-DB fan-out over distinct schemas", () = // NEW side has no control-plane tables and no associatedWaitpoint relation; // seed the TaskRun row directly with synthetic scalar ids. - const ksuidId = newId("t10"); + const runOpsId = newId("t10"); await prisma17.taskRun.create({ data: { - id: ksuidId, + id: runOpsId, engine: "V2", status: "PENDING", friendlyId: "run_t10_new", @@ -1395,11 +1399,11 @@ describe("RoutingRunStore.findRuns cross-DB fan-out over distinct schemas", () = }); const rows = (await router.findRuns({ - where: { id: { in: [cuidId, ksuidId] } }, + where: { id: { in: [cuidId, runOpsId] } }, select: { id: true }, })) as Array<{ id: string }>; - expect(rows.map((r) => r.id).sort()).toEqual([cuidId, ksuidId].sort()); + expect(rows.map((r) => r.id).sort()).toEqual([cuidId, runOpsId].sort()); }, 120_000 ); @@ -1407,7 +1411,8 @@ describe("RoutingRunStore.findRuns cross-DB fan-out over distinct schemas", () = describe("RoutingRunStore write-path fan-outs", () => { const legacyId = (suffix: string) => `run_${"c".repeat(25 - suffix.length)}${suffix}`; - const newId = (suffix: string) => `run_${"k".repeat(27 - suffix.length)}${suffix}`; + const newId = (suffix: string) => + `run_${(suffix.replace(/[^0-9a-v]/g, "0") + "k".repeat(24)).slice(0, 24)}01`; async function createRunWithKey( store: PostgresRunStore, @@ -1529,7 +1534,7 @@ describe("RoutingRunStore write-path fan-outs", () => { } ); - // expireRunsBatch with mixed ksuid+cuid ids partitions across both DBs and sums. + // expireRunsBatch with mixed run-ops id+cuid ids partitions across both DBs and sums. heteroPostgresTest( "expireRunsBatch with mixed ids partitions across NEW+LEGACY and sums count", async ({ prisma14, prisma17 }) => { @@ -1578,9 +1583,9 @@ describe("RoutingRunStore write-path fan-outs", () => { } ); - // all-ksuid batch goes only to NEW; LEGACY store is not called with an empty list. + // all-run-ops batch goes only to NEW; LEGACY store is not called with an empty list. heteroPostgresTest( - "expireRunsBatch all-ksuid batch skips LEGACY (no empty IN query)", + "expireRunsBatch all-run-ops batch skips LEGACY (no empty IN query)", async ({ prisma14, prisma17 }) => { const legacyStore = new PostgresRunStore({ prisma: prisma14, readOnlyPrisma: prisma14 }); let legacyCalled = false; @@ -1604,7 +1609,7 @@ describe("RoutingRunStore write-path fan-outs", () => { buildCreateRunInput({ runId: nId, friendlyId: "run_ks_n", - taskIdentifier: "ksuid-only-task", + taskIdentifier: "runops-only-task", organizationId: seed17.organization.id, projectId: seed17.project.id, runtimeEnvironmentId: seed17.environment.id, @@ -1625,7 +1630,8 @@ describe("RoutingRunStore write-path fan-outs", () => { describe("RoutingRunStore.findTaskRunAttempt residency routing", () => { const legacyRunId = (suffix: string) => `run_${"c".repeat(25 - suffix.length)}${suffix}`; - const newRunId = (suffix: string) => `run_${"k".repeat(27 - suffix.length)}${suffix}`; + const newRunId = (suffix: string) => + `run_${(suffix.replace(/[^0-9a-v]/g, "0") + "k".repeat(24)).slice(0, 24)}01`; async function seedAttempt( prisma: PrismaClient, @@ -1691,13 +1697,13 @@ describe("RoutingRunStore.findTaskRunAttempt residency routing", () => { ); heteroPostgresTest( - "a ksuid (NEW) run's attempt still resolves via findTaskRunAttempt", + "a run-ops id (NEW) run's attempt still resolves via findTaskRunAttempt", async ({ prisma14, prisma17 }) => { const legacyStore = new PostgresRunStore({ prisma: prisma14, readOnlyPrisma: prisma14 }); const newStore = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 }); const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); - const seed17 = await seedEnvironment(prisma17, "t9a_ksuid17"); + const seed17 = await seedEnvironment(prisma17, "t9a_runops17"); const runId = newRunId("t9a2"); await newStore.createRun( buildCreateRunInput({ @@ -1710,7 +1716,7 @@ describe("RoutingRunStore.findTaskRunAttempt residency routing", () => { }) ); - const attemptId = "attempt_t9a_ksuid1"; + const attemptId = "attempt_t9a_runops1"; await seedAttempt(prisma17, { attemptId, friendlyId: "attempt_t9a_k1", @@ -1894,7 +1900,7 @@ describe("findBatchTaskRunByFriendlyId probe", () => { ); }); -// Batch residency: the four new accessors must route by batch id so a ksuid +// Batch residency: the four new accessors must route by batch id so a run-ops id // batch + its items live on NEW with its child runs, and fall back to fan-out where there // is no classifiable id (idempotency probe; status-only updateMany). describe("RoutingRunStore batch-residency accessors", () => { @@ -1960,10 +1966,10 @@ describe("RoutingRunStore batch-residency accessors", () => { const seed14 = await seedEnvironment(prisma14, "optA_idem14"); - // ksuid batch with an idempotency key on NEW (dedicated, scalar env id) + // run-ops batch with an idempotency key on NEW (dedicated, scalar env id) await newStore.createBatchTaskRun( batchData({ - id: `${KSUID_27.slice(0, -2)}i1`, + id: `${NEW_ID_26.slice(0, -2)}i1`, friendlyId: "batch_idem_new", runtimeEnvironmentId: ENV_NEW, idempotencyKey: "key-new", @@ -1991,7 +1997,7 @@ describe("RoutingRunStore batch-residency accessors", () => { } ); - // updateManyBatchTaskRun: routes by where.id (ksuidβ†’NEW, cuidβ†’LEGACY); fans out + sums when unrouted. + // updateManyBatchTaskRun: routes by where.id (run-ops idβ†’NEW, cuidβ†’LEGACY); fans out + sums when unrouted. heteroRunOpsPostgresTest( "updateManyBatchTaskRun routes by where.id and fans out otherwise", async ({ prisma14, prisma17 }) => { @@ -2001,10 +2007,10 @@ describe("RoutingRunStore batch-residency accessors", () => { const seed14 = await seedEnvironment(prisma14, "optA_um14"); - const ksuidBatchId = `${KSUID_27.slice(0, -2)}m1`; + const runOpsBatchId = `${NEW_ID_26.slice(0, -2)}m1`; const cuidBatchId = `${CUID_25.slice(0, -2)}m1`; await newStore.createBatchTaskRun( - batchData({ id: ksuidBatchId, friendlyId: "batch_um_new", runtimeEnvironmentId: ENV_NEW }) + batchData({ id: runOpsBatchId, friendlyId: "batch_um_new", runtimeEnvironmentId: ENV_NEW }) ); await legacyStore.createBatchTaskRun( batchData({ @@ -2014,14 +2020,14 @@ describe("RoutingRunStore batch-residency accessors", () => { }) ); - // where.id ksuid β†’ NEW only + // where.id run-ops id β†’ NEW only const upNew = await router.updateManyBatchTaskRun({ - where: { id: ksuidBatchId }, + where: { id: runOpsBatchId }, data: { status: "COMPLETED" }, }); expect(upNew.count).toBe(1); expect( - (await prisma17.batchTaskRun.findUnique({ where: { id: ksuidBatchId } }))?.status + (await prisma17.batchTaskRun.findUnique({ where: { id: runOpsBatchId } }))?.status ).toBe("COMPLETED"); // where.id cuid β†’ LEGACY only @@ -2051,25 +2057,25 @@ describe("RoutingRunStore batch-residency accessors", () => { const newStore = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 }); const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); - const ksuidBatchId = `${KSUID_27.slice(0, -2)}c1`; + const runOpsBatchId = `${NEW_ID_26.slice(0, -2)}c1`; await newStore.createBatchTaskRun( - batchData({ id: ksuidBatchId, friendlyId: "batch_cnt_new", runtimeEnvironmentId: ENV_NEW }) + batchData({ id: runOpsBatchId, friendlyId: "batch_cnt_new", runtimeEnvironmentId: ENV_NEW }) ); - const runA = `${KSUID_27.slice(0, -3)}cra`; - const runB = `${KSUID_27.slice(0, -3)}crb`; + const runA = `${NEW_ID_26.slice(0, -3)}ra1`; + const runB = `${NEW_ID_26.slice(0, -3)}rb1`; await seedDedicatedRun(prisma17, ENV_NEW, runA); await seedDedicatedRun(prisma17, ENV_NEW, runB); await prisma17.batchTaskRunItem.create({ - data: { batchTaskRunId: ksuidBatchId, taskRunId: runA, status: "COMPLETED" }, + data: { batchTaskRunId: runOpsBatchId, taskRunId: runA, status: "COMPLETED" }, }); await prisma17.batchTaskRunItem.create({ - data: { batchTaskRunId: ksuidBatchId, taskRunId: runB, status: "PENDING" }, + data: { batchTaskRunId: runOpsBatchId, taskRunId: runB, status: "PENDING" }, }); - expect(await router.countBatchTaskRunItems({ batchTaskRunId: ksuidBatchId })).toBe(2); + expect(await router.countBatchTaskRunItems({ batchTaskRunId: runOpsBatchId })).toBe(2); expect( - await router.countBatchTaskRunItems({ batchTaskRunId: ksuidBatchId, status: "COMPLETED" }) + await router.countBatchTaskRunItems({ batchTaskRunId: runOpsBatchId, status: "COMPLETED" }) ).toBe(1); } ); @@ -2082,24 +2088,24 @@ describe("RoutingRunStore batch-residency accessors", () => { const newStore = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 }); const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); - const ksuidBatchId = `${KSUID_27.slice(0, -2)}u1`; + const runOpsBatchId = `${NEW_ID_26.slice(0, -2)}u1`; await newStore.createBatchTaskRun( - batchData({ id: ksuidBatchId, friendlyId: "batch_ui_new", runtimeEnvironmentId: ENV_NEW }) + batchData({ id: runOpsBatchId, friendlyId: "batch_ui_new", runtimeEnvironmentId: ENV_NEW }) ); - const runX = `${KSUID_27.slice(0, -3)}uix`; + const runX = `${NEW_ID_26.slice(0, -3)}ux1`; await seedDedicatedRun(prisma17, ENV_NEW, runX); await prisma17.batchTaskRunItem.create({ - data: { batchTaskRunId: ksuidBatchId, taskRunId: runX, status: "PENDING" }, + data: { batchTaskRunId: runOpsBatchId, taskRunId: runX, status: "PENDING" }, }); const res = await router.updateManyBatchTaskRunItems({ - where: { batchTaskRunId: ksuidBatchId, taskRunId: runX }, + where: { batchTaskRunId: runOpsBatchId, taskRunId: runX }, data: { status: "COMPLETED" }, }); expect(res.count).toBe(1); const item = await prisma17.batchTaskRunItem.findFirst({ - where: { batchTaskRunId: ksuidBatchId, taskRunId: runX }, + where: { batchTaskRunId: runOpsBatchId, taskRunId: runX }, }); expect(item?.status).toBe("COMPLETED"); } @@ -2112,7 +2118,7 @@ describe("RoutingRunStore batch-residency accessors", () => { const store = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 }); const router = new RoutingRunStore({ new: store, legacy: store }); - const batchId = `${KSUID_27.slice(0, -2)}s1`; + const batchId = `${NEW_ID_26.slice(0, -2)}s1`; await router.createBatchTaskRun( batchData({ id: batchId, @@ -2126,7 +2132,7 @@ describe("RoutingRunStore batch-residency accessors", () => { batchId ); - const runId = `${KSUID_27.slice(0, -3)}srn`; + const runId = `${NEW_ID_26.slice(0, -3)}sr1`; await seedDedicatedRun(prisma17, ENV_NEW, runId); await prisma17.batchTaskRunItem.create({ data: { batchTaskRunId: batchId, taskRunId: runId, status: "PENDING" }, diff --git a/internal-packages/run-store/src/runOpsStore.ts b/internal-packages/run-store/src/runOpsStore.ts index 583cbdbb79..9b7f453837 100644 --- a/internal-packages/run-store/src/runOpsStore.ts +++ b/internal-packages/run-store/src/runOpsStore.ts @@ -32,7 +32,7 @@ import { isReadReplicaClient } from "./readReplicaClient.js"; * Run-ops routing substrate for the TaskRun-core method group. Implements {@link RunStore} * by selecting between a NEW store (the dedicated run-ops DB, where new runs are born) and * a LEGACY store (the control-plane DB) via the residency classifier (`ownerEngine`: - * ksuidβ†’NEW, cuidβ†’LEGACY). In single-DB both stores are the same, so routing is a no-op + * run-ops idβ†’NEW, cuidβ†’LEGACY). In single-DB both stores are the same, so routing is a no-op * passthrough. Inert until the injecting seam wires it in under `isSplitEnabled()`; reads no * flag here. The TaskRun-core methods (create/find/findRuns + updateMetadata/clearIdempotencyKey) * route by residency; all other methods are mechanical residency-routing delegates. @@ -95,8 +95,9 @@ export class RoutingRunStore implements RunStore { return this.#classify(id) === "NEW" ? this.#new : this.#legacy; } - // Best-effort route; falls back to NEW (the steady-state home) when the id is absent - // or unclassifiable. + // Best-effort route; falls back to NEW (the steady-state home) when the id is absent. + // Classification is total (any id without the v1 version marker is LEGACY), so the + // catch below only guards injected classifiers that still throw. #routeOrNew(id: string | undefined): RunStore { if (typeof id !== "string") { return this.#new; @@ -108,7 +109,7 @@ export class RoutingRunStore implements RunStore { } } - // WRITE routing is pure id-shape (cuid β†’ LEGACY, ksuid β†’ NEW). A LEGACY-classified id is + // WRITE routing is pure id-shape (cuid β†’ LEGACY, run-ops id β†’ NEW). A LEGACY-classified id is // always LEGACY-resident; no marker check exists. Kept async so the many // `await this.#routeForWrite(...)` call sites need no edits (awaiting a resolved store is // a no-op). @@ -133,7 +134,7 @@ export class RoutingRunStore implements RunStore { return this.#routeOrNew(runId).runInTransaction(runId, fn); } - // A waitpoint WRITE co-locates with its run by id-shape (cuid β†’ LEGACY, ksuid β†’ NEW, + // A waitpoint WRITE co-locates with its run by id-shape (cuid β†’ LEGACY, run-ops id β†’ NEW, // unclassifiable β†’ LEGACY), mirroring how `blockRunWithWaitpointEdges` routes the edge by // run id. `tx` is forwarded only to LEGACY (same physical DB as the control-plane tx); // for NEW it's dropped so the row lands on NEW's own client. @@ -177,7 +178,7 @@ export class RoutingRunStore implements RunStore { // --------------------------------------------------------------------------- // TaskRun-core: Create β€” a run is born on the store named by its MINTED id-kind: - // cuid β†’ LEGACY, ksuid β†’ NEW, unclassifiable β†’ NEW. The mint layer encodes + // cuid β†’ LEGACY, run-ops id β†’ NEW, unclassifiable β†’ NEW. The mint layer encodes // inherited residency into the id-kind, so create-by-id-shape is correct; // a brand-new run has no redirect marker. // @@ -332,7 +333,7 @@ export class RoutingRunStore implements RunStore { } // Bounded id-set (the list hydrate + engine sweeps). Query NEW for the whole set first - // (it holds ksuid runs); probe LEGACY only for the ids NEW missed that could still live + // (it holds run-ops runs); probe LEGACY only for the ids NEW missed that could still live // there (cuid). The two id sets are disjoint by construction, so the merge needs no dedupe. async #findRunsByIdSet( args: FindRunsArgs, @@ -353,7 +354,7 @@ export class RoutingRunStore implements RunStore { const toLegacy: string[] = []; for (const id of ids) { if (foundIds.has(id)) continue; - if (this.#classifySafe(id) === "NEW") continue; // ksuid: cannot live on LEGACY + if (this.#classifySafe(id) === "NEW") continue; // run-ops id: cannot live on LEGACY toLegacy.push(id); } @@ -513,7 +514,7 @@ export class RoutingRunStore implements RunStore { data: { error: TaskRunError; now: Date }, tx?: PrismaClientOrTransaction ): Promise { - // Partition by id-shape: ksuid β†’ NEW, everything else β†’ LEGACY. Call each store + // Partition by id-shape: run-ops id β†’ NEW, everything else β†’ LEGACY. Call each store // only when its partition is non-empty (avoids an empty IN () clause). Sum counts. const newIds = runIds.filter((id) => this.#classifySafe(id) === "NEW"); const legacyIds = runIds.filter((id) => this.#classifySafe(id) !== "NEW"); @@ -747,7 +748,7 @@ export class RoutingRunStore implements RunStore { // completed the run; the owning store can only hydrate the ones that live on its own DB. When every // join id is already present we leave the array untouched (byte-identical for single-DB / the // co-resident steady state β€” no extra fan-out write); only genuinely-missing ids are resolved - // cross-DB and appended, so a cuid token completing a ksuid run keeps its OUTPUT on the resume. + // cross-DB and appended, so a cuid token completing a run-ops run keeps its OUTPUT on the resume. async #reresolveCompletedWaitpointsCrossDb( snapshot: Record, owningStore: RunStore, @@ -1197,7 +1198,7 @@ export class RoutingRunStore implements RunStore { // strand that run forever. Dedup is a no-op in steady state; it guards the copyβ†’fence window. // // The edge's `waitpoint`/`taskRun` relations can also straddle DBs (a cuid MANUAL/DATETIME token - // blocking a ksuid run; a drain-relocated token). A single store hydrates them from its own + // blocking a run-ops run; a drain-relocated token). A single store hydrates them from its own // client only β†’ a cross-DB target resolves to null β†’ the run hangs or its resume // output is silently dropped. So the router strips those relation keys from the per-leg // query (scalar edges only) and re-resolves them across BOTH stores here. @@ -1341,16 +1342,16 @@ export class RoutingRunStore implements RunStore { } // --------------------------------------------------------------------------- - // BatchTaskRun (run-ops). Route by id-shape: ksuidβ†’NEW, cuidβ†’LEGACY. + // BatchTaskRun (run-ops). Route by id-shape: run-ops idβ†’NEW, cuidβ†’LEGACY. // --------------------------------------------------------------------------- async createBatchTaskRun( data: CreateBatchTaskRunData, tx?: PrismaClientOrTransaction ): Promise { - // Route by the batch's classifiable internal id: ksuidβ†’NEW, cuidβ†’LEGACY. + // Route by the batch's classifiable internal id: run-ops idβ†’NEW, cuidβ†’LEGACY. // Never forward a control-plane tx to NEW (the create would land in the wrong DB, stranding the - // ksuid batch + its co-resident child runs/items); forward tx only to LEGACY (same physical DB + // run-ops batch + its co-resident child runs/items); forward tx only to LEGACY (same physical DB // as the tx). Mirrors #routeWaitpointWrite / updateBatchTaskRun. const store = await this.#routeOrNewForWrite(data.id); return store.createBatchTaskRun(data, store === this.#legacy ? tx : undefined); @@ -1374,7 +1375,7 @@ export class RoutingRunStore implements RunStore { // Batches can be written to either DB by different create paths (runEngine routes by id; // batchTriggerV3 writes raw to the control-plane), so probe NEW first then LEGACY rather - // than strict id-routing, which would miss a ksuid-id batch resident on the control-plane. + // than strict id-routing, which would miss a run-ops-id batch resident on the control-plane. async findBatchTaskRunById( id: string, args?: { include?: T }, @@ -1420,7 +1421,7 @@ export class RoutingRunStore implements RunStore { } // --------------------------------------------------------------------------- - // Batch residency β€” route every batch op by the batch id so a ksuid + // Batch residency β€” route every batch op by the batch id so a run-ops id // batch + its items co-reside on NEW with its child runs (the TaskRun.batchId and // BatchTaskRunItem.batchTaskRunId FKs resolve locally). // --------------------------------------------------------------------------- @@ -1471,7 +1472,7 @@ export class RoutingRunStore implements RunStore { where: { batchTaskRunId: string; status?: BatchTaskRunItemStatus }, client?: ReadClient ): Promise { - // Never forward the caller's client verbatim (a ksuid batch routes to NEW, so a forwarded + // Never forward the caller's client verbatim (a run-ops batch routes to NEW, so a forwarded // control-plane client would count items on the wrong DB β†’ 0/wrong count); its presence // resolves to the owning store's OWN primary. const store = this.#routeOrNew(where.batchTaskRunId); diff --git a/internal-packages/run-store/src/runOpsStore.waitpoints.test.ts b/internal-packages/run-store/src/runOpsStore.waitpoints.test.ts index 9017d8c85b..3f9b345e43 100644 --- a/internal-packages/run-store/src/runOpsStore.waitpoints.test.ts +++ b/internal-packages/run-store/src/runOpsStore.waitpoints.test.ts @@ -19,9 +19,9 @@ import type { CreateRunInput, RunStoreSchemaVariant } from "./types.js"; type AnyClient = PrismaClient | RunOpsPrismaClient; -// ownerEngine classifies by internal-id LENGTH after stripping a single leading `_`: 27 chars -// β†’ ksuid β†’ NEW (#new / dedicated subset), 25 chars β†’ cuid β†’ LEGACY (#legacy / full schema). -const KSUID_27 = "k".repeat(27); +// ownerEngine classifies by the version char after stripping a single leading `_`: a v1 body +// β†’ run-ops id β†’ NEW (#new / dedicated subset), 25 chars β†’ cuid β†’ LEGACY (#legacy / full schema). +const NEW_ID_26 = "k".repeat(24) + "01"; const CUID_25 = "c".repeat(25); // On the dedicated subset there are no Organization/Project/RuntimeEnvironment models (the run-ops @@ -234,7 +234,7 @@ describe("RunStore run-ops persistence β€” waitpoints", () => { }; const wp14 = await run(prisma14, "legacy", `run_${CUID_25}`, "wa14"); - const wp17 = await run(prisma17, "dedicated", `run_${KSUID_27}`, "wa17"); + const wp17 = await run(prisma17, "dedicated", `run_${NEW_ID_26}`, "wa17"); expect(wp14).not.toBeNull(); expect(wp17).not.toBeNull(); @@ -324,7 +324,7 @@ describe("RunStore run-ops persistence β€” waitpoints", () => { { prisma: prisma17, schemaVariant: "dedicated" as const, - runId: `run_${KSUID_27}`, + runId: `run_${NEW_ID_26}`, suffix: "wb17", }, ]) { @@ -439,9 +439,9 @@ describe("RunStore run-ops persistence β€” waitpoints", () => { const env = await seedEnvironment(prisma14, "legacy", "wd14"); - // KSUID_27-length id β†’ NEW residency, exercising the route; both slots are the same store so + // NEW_ID_26-length id β†’ NEW residency, exercising the route; both slots are the same store so // it still lands on prisma14. - const runId = `run_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; await router.createRun( buildCreateRunInput({ runId, @@ -490,7 +490,7 @@ describe("RunStore run-ops persistence β€” waitpoints", () => { } ); - // the silent-hang case, against the REAL split. A NEW (ksuid) run is blocked on + // the silent-hang case, against the REAL split. A NEW (run-ops id) run is blocked on // a LEGACY (cuid) token, so its block edge lives on #new (co-located with the run) while the token's // id-shape says LEGACY. Completing that token must FAN OUT the waitpointId edge read across both DBs // and find the edge on #new β€” routing by the token's id-shape (LEGACY) returns zero edges and the @@ -505,7 +505,7 @@ describe("RunStore run-ops persistence β€” waitpoints", () => { // The NEW run + its (synthetic) env live on the dedicated #new subset (prisma17). const env17 = await seedEnvironment(prisma17, "dedicated", "we17"); - const runId = `run_${KSUID_27}`; // ksuid β†’ NEW residency + const runId = `run_${NEW_ID_26}`; // run-ops id β†’ NEW residency await router.createRun( buildCreateRunInput({ runId, @@ -568,7 +568,7 @@ describe("RunStore run-ops persistence β€” waitpoints", () => { const router = new RoutingRunStore({ new: newStore, legacy: legacyStore }); const env17 = await seedEnvironment(prisma17, "dedicated", "wf17"); - const runId = `run_${KSUID_27}`; + const runId = `run_${NEW_ID_26}`; await router.createRun( buildCreateRunInput({ runId, diff --git a/internal-packages/run-store/src/types.ts b/internal-packages/run-store/src/types.ts index ce5e141de0..b1729347c4 100644 --- a/internal-packages/run-store/src/types.ts +++ b/internal-packages/run-store/src/types.ts @@ -310,7 +310,7 @@ export interface RunStore { /** * Run a co-resident multi-write unit atomically on the store that OWNS `runId`. The callback gets * the owning `RunStore` plus a `tx` opened on THAT store's OWN client; passing `tx` to the inner - * writes lands them all in ONE transaction on the owning DB (NEW for a ksuid run, LEGACY for a cuid + * writes lands them all in ONE transaction on the owning DB (NEW for a run-ops run, LEGACY for a cuid * run), so a failure between two writes rolls BOTH back. NOT a cross-DB transaction: `tx` is the * owning store's own client (never the control-plane tx), and every write MUST target the same run / * its co-resident subgraph. Callers MUST use the supplied `store` + `tx`, not the outer router diff --git a/packages/core/src/v3/isomorphic/friendlyId.test.ts b/packages/core/src/v3/isomorphic/friendlyId.test.ts index 9885cd14b8..439de1e8c8 100644 --- a/packages/core/src/v3/isomorphic/friendlyId.test.ts +++ b/packages/core/src/v3/isomorphic/friendlyId.test.ts @@ -4,15 +4,19 @@ import { WaitpointId, SnapshotId, QueueId, - generateKsuidId, - decodeKsuid, - KSUID_PAYLOAD_BYTES, + RUN_OPS_ID_LENGTH, + RUN_OPS_ID_REGION_INDEX, + RUN_OPS_ID_VERSION, + RUN_OPS_ID_VERSION_INDEX, + base32hexDecode, + base32hexEncode, + generateRunOpsId, + parseRunId, } from "./friendlyId.js"; const CUID_LEN = 25; -const KSUID_LEN = 27; -describe("RunId + WaitpointId mint cuid by default; ksuid via generateKsuidId", () => { +describe("RunId + WaitpointId mint cuid by default; run-ops v1 via generateRunOpsId", () => { it("default: run + waitpoint mint cuid (25) and round-trip", () => { for (const util of [RunId, WaitpointId]) { const { id, friendlyId } = util.generate(); @@ -24,11 +28,11 @@ describe("RunId + WaitpointId mint cuid by default; ksuid via generateKsuidId", } }); - it("explicit ksuid: a run/waitpoint friendlyId over generateKsuidId() is 27-char and round-trips", () => { + it("explicit run-ops id: a run/waitpoint friendlyId over generateRunOpsId() is 26-char and round-trips", () => { for (const util of [RunId, WaitpointId]) { - const id = generateKsuidId(); + const id = generateRunOpsId(); const friendlyId = util.toFriendlyId(id); - expect(id.length).toBe(KSUID_LEN); + expect(id.length).toBe(RUN_OPS_ID_LENGTH); expect(util.fromFriendlyId(friendlyId)).toBe(id); expect(util.toId(friendlyId)).toBe(id); expect(util.toId(id)).toBe(id); @@ -39,80 +43,161 @@ describe("RunId + WaitpointId mint cuid by default; ksuid via generateKsuidId", expect(SnapshotId.generate().id.length).toBe(CUID_LEN); expect(QueueId.generate().id.length).toBe(CUID_LEN); }); +}); - it("disjoint lengths: 27 (ksuid) vs 25 (cuid) β€” the classifier margin", () => { - expect(generateKsuidId().length).not.toBe(SnapshotId.generate().id.length); +describe("base32hex codec (lowercase RFC 4648 Β§7)", () => { + // Independent reference: interpret the bytes as one big-endian integer and + // emit base-32 digits. Only exact multiples of 5 bytes (40 bits) are used, so + // there is never a partial trailing group to disagree on. + const ALPHA = "0123456789abcdefghijklmnopqrstuv"; + function referenceEncode(bytes: Uint8Array): string { + let n = 0n; + for (const b of bytes) n = (n << 8n) | BigInt(b); + const chars = (bytes.length * 8) / 5; + let out = ""; + for (let i = 0; i < chars; i++) { + out = ALPHA[Number(n & 31n)] + out; + n >>= 5n; + } + return out; + } + + it("matches the big-integer reference bit-for-bit (property, 5/10/15/20-byte inputs)", () => { + for (let iter = 0; iter < 2_000; iter++) { + for (const len of [5, 10, 15, 20]) { + const bytes = new Uint8Array(len); + crypto.getRandomValues(bytes); + const encoded = base32hexEncode(bytes); + expect(encoded).toBe(referenceEncode(bytes)); + expect(Array.from(base32hexDecode(encoded))).toEqual(Array.from(bytes)); + } + } }); - it("generateKsuidId() is directly callable and yields 27 chars", () => { - expect(generateKsuidId().length).toBe(KSUID_LEN); + it("hand-verified vectors", () => { + expect(base32hexEncode(new Uint8Array(5))).toBe("00000000"); + expect(base32hexEncode(new Uint8Array(5).fill(0xff))).toBe("vvvvvvvv"); + expect(base32hexEncode(new Uint8Array([0, 0, 0, 0, 1]))).toBe("00000001"); + // 0x20 0 0 0 0 = 2^37; 2^37 / 32^7 = 4 β†’ leading digit "4" + expect(base32hexEncode(new Uint8Array([0x20, 0, 0, 0, 0]))).toBe("40000000"); + }); + + it("decode rejects characters outside the lowercase base32hex alphabet", () => { + for (const bad of ["w", "x", "z", "A", "V", "-", "_", " "]) { + expect(() => base32hexDecode(`0000000${bad}`)).toThrow(/invalid/i); + } }); }); -describe("generateKsuidId is a genuine KSUID (decodable timestamp, time-ordered)", () => { +describe("generateRunOpsId β€” run-ops v1 id spec", () => { afterEach(() => vi.useRealTimers()); - it("is exactly 27 base62 chars", () => { - expect(generateKsuidId()).toMatch(/^[0-9A-Za-z]{27}$/); + it("emits <24-char base32hex core> β€” 26 chars total", () => { + const id = generateRunOpsId(); + expect(id.length).toBe(RUN_OPS_ID_LENGTH); + expect(id).toMatch(/^[0-9a-v]{24}[a-z0-9]1$/); + expect(id[RUN_OPS_ID_VERSION_INDEX]).toBe(RUN_OPS_ID_VERSION); + }); + + it("only ever uses lowercase [a-z0-9] and NEVER '-' (DNS-1123 / pod-name invariant)", () => { + for (let i = 0; i < 5_000; i++) { + const id = generateRunOpsId(); + expect(id).toMatch(/^[a-z0-9]+$/); + expect(id).not.toContain("-"); + } }); - it("carries a decodable timestamp within a few seconds of now", () => { - const before = Math.floor(Date.now() / 1000); - const { timestampSeconds: ts } = decodeKsuid(generateKsuidId()); - expect(ts).toBeGreaterThanOrEqual(before - 2); - expect(ts).toBeLessThanOrEqual(Math.floor(Date.now() / 1000) + 2); + it("stamps the region char from REGION_CODES, defaulting to '0' when unknown/absent", () => { + expect(generateRunOpsId("us-east-1")[RUN_OPS_ID_REGION_INDEX]).toBe("e"); + expect(generateRunOpsId("us-west-2")[RUN_OPS_ID_REGION_INDEX]).toBe("w"); + expect(generateRunOpsId("eu-central-1")[RUN_OPS_ID_REGION_INDEX]).toBe("c"); + expect(generateRunOpsId("mars-north-1")[RUN_OPS_ID_REGION_INDEX]).toBe("0"); + expect(generateRunOpsId()[RUN_OPS_ID_REGION_INDEX]).toBe("0"); }); - it("is k-sortable: ids from later seconds sort lexicographically after earlier ones", () => { + it("sorts lexicographically in creation order at ms resolution (A@t, C@t+3ms, B@t+1s β†’ A,C,B)", () => { vi.useFakeTimers(); - const ids: string[] = []; - for (const t of ["2026-01-01T00:00:00Z", "2026-01-01T00:05:00Z", "2026-09-01T12:00:00Z"]) { - vi.setSystemTime(new Date(t)); - ids.push(generateKsuidId()); + const t = new Date("2026-07-04T12:00:00.000Z").getTime(); + vi.setSystemTime(t); + const a = generateRunOpsId(); + vi.setSystemTime(t + 1000); + const b = generateRunOpsId(); + vi.setSystemTime(t + 3); + const c = generateRunOpsId(); + expect([b, c, a].sort()).toEqual([a, c, b]); + }); + + it("decode recovers the exact ms timestamp", () => { + vi.useFakeTimers(); + const t = new Date("2026-07-04T12:34:56.789Z"); + vi.setSystemTime(t); + const parsed = parseRunId(`run_${generateRunOpsId("us-east-1")}`); + expect(parsed.format).toBe("b32hex"); + if (parsed.format === "b32hex") { + expect(parsed.timestamp.getTime()).toBe(t.getTime()); } - expect([...ids].sort()).toEqual(ids); }); - it("is unique across many mints in the same second", () => { - const n = 1000; - expect(new Set(Array.from({ length: n }, () => generateKsuidId())).size).toBe(n); + it("is unique across many mints in the same ms (72 bits of CSPRNG)", () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-07-04T00:00:00.000Z")); + const n = 2_000; + expect(new Set(Array.from({ length: n }, () => generateRunOpsId())).size).toBe(n); }); }); -describe("KSUID payload encode/decode (foundation primitive)", () => { - it("round-trips a full 16-byte payload exactly", () => { - const payload = new Uint8Array(KSUID_PAYLOAD_BYTES).map((_, i) => (i * 17 + 1) & 0xff); - const { payload: decoded } = decodeKsuid(generateKsuidId(payload)); - expect(Array.from(decoded)).toEqual(Array.from(payload)); +describe("parseRunId β€” version-char discrimination (not length)", () => { + it("parses a v1 friendly id as partitioned with region + version", () => { + const parsed = parseRunId(`run_${generateRunOpsId("us-west-2")}`); + expect(parsed).toMatchObject({ + format: "b32hex", + table: "partitioned", + region: "w", + version: "1", + }); }); - it("preserves a partial payload prefix and keeps the remainder for entropy", () => { - const meta = new Uint8Array([9, 8, 7, 6]); - const { payload } = decodeKsuid(generateKsuidId(meta)); - expect(Array.from(payload.slice(0, 4))).toEqual([9, 8, 7, 6]); - expect(payload.length).toBe(KSUID_PAYLOAD_BYTES); + it("classifies a cuid friendly id legacy", () => { + expect(parseRunId(RunId.generate().friendlyId)).toEqual({ + format: "legacy", + table: "legacy", + }); }); - it("still carries a decodable timestamp when a payload is embedded", () => { - const before = Math.floor(Date.now() / 1000); - const { timestampSeconds } = decodeKsuid(generateKsuidId(new Uint8Array([1, 2, 3]))); - expect(timestampSeconds).toBeGreaterThanOrEqual(before - 2); - expect(timestampSeconds).toBeLessThanOrEqual(Math.floor(Date.now() / 1000) + 2); + it("classifies a nanoid-bodied friendly id and a run_-less id legacy", () => { + expect(parseRunId("run_123456789abcdefghijkm").format).toBe("legacy"); // 21-char nanoid body + expect(parseRunId(generateRunOpsId()).format).toBe("legacy"); // bare body, no run_ prefix + expect(parseRunId("waitpoint_" + generateRunOpsId()).format).toBe("legacy"); // wrong prefix }); - it("stays 27 chars with a full payload and decodes through a friendlyId prefix", () => { - const id = generateKsuidId(new Uint8Array(KSUID_PAYLOAD_BYTES).fill(0xab)); - expect(id).toMatch(/^[0-9A-Za-z]{27}$/); - expect(Array.from(decodeKsuid(`run_${id}`).payload)).toEqual( - new Array(KSUID_PAYLOAD_BYTES).fill(0xab) - ); + it("falls back to legacy on a malformed v1 (bad alphabet / wrong version char)", () => { + expect(parseRunId(`run_${"A".repeat(25)}1`).format).toBe("legacy"); // uppercase core + expect(parseRunId(`run_${"a".repeat(25)}2`).format).toBe("legacy"); // wrong version + expect(parseRunId(`run_${"a".repeat(24)}-1`).format).toBe("legacy"); // region char not [a-z0-9] + expect(parseRunId(`run_${"a".repeat(27)}`).format).toBe("legacy"); // old 27-char shape }); +}); - it("throws if the payload exceeds the 16-byte budget", () => { - expect(() => generateKsuidId(new Uint8Array(KSUID_PAYLOAD_BYTES + 1))).toThrow(); +describe("firekeeper pod-name round-trip (runner-[-attempt-N] β†’ run_)", () => { + // Mirrors firekeeper's runIDFromPodName: strip "runner-", cut before the first + // hyphen, prepend "run_". Works because a v1 id is all-lowercase [0-9a-v] and + // NEVER contains "-" β€” the hyphens all belong to the pod-name delimiters. + function firekeeperRunIdFromPodName(name: string): string { + const rest = name.slice("runner-".length); + const hyphen = rest.indexOf("-"); + return `run_${hyphen === -1 ? rest : rest.slice(0, hyphen)}`; + } + + it("recovers the exact id (incl. region + version chars) from first-attempt and retry pods", () => { + const id = generateRunOpsId("us-east-1"); + expect(firekeeperRunIdFromPodName(`runner-${id}`)).toBe(`run_${id}`); + expect(firekeeperRunIdFromPodName(`runner-${id}-attempt-2`)).toBe(`run_${id}`); + expect(parseRunId(firekeeperRunIdFromPodName(`runner-${id}-attempt-2`)).format).toBe("b32hex"); }); - it("decodeKsuid rejects a body that is not 27 base62 chars", () => { - expect(() => decodeKsuid("run_tooShort")).toThrow(); + it("the recovered id is a valid DNS-1123 label body (k8s accepts runner-)", () => { + const podName = `runner-${generateRunOpsId("eu-central-1")}`; + expect(podName).toMatch(/^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$/); + expect(podName.length).toBeLessThanOrEqual(63); }); }); diff --git a/packages/core/src/v3/isomorphic/friendlyId.ts b/packages/core/src/v3/isomorphic/friendlyId.ts index bb0e8a2acc..4a466dfd8e 100644 --- a/packages/core/src/v3/isomorphic/friendlyId.ts +++ b/packages/core/src/v3/isomorphic/friendlyId.ts @@ -7,13 +7,42 @@ export function generateFriendlyId(prefix: string, size?: number) { return `${prefix}_${idGenerator(size)}`; } -// KSUID epoch (2014-05-13T16:53:20Z) β€” seconds offset applied to the unix timestamp. -const KSUID_EPOCH = 1_400_000_000; -const KSUID_TIMESTAMP_BYTES = 4; -export const KSUID_PAYLOAD_BYTES = 16; -const KSUID_TOTAL_BYTES = KSUID_TIMESTAMP_BYTES + KSUID_PAYLOAD_BYTES; -export const KSUID_STRING_LENGTH = 27; -const BASE62_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; +// Run-ops v1 id: `<24-char base32hex core>` β€” 26 chars. +// Core = 6-byte big-endian unix ms timestamp + 9 bytes CSPRNG. Invariants: +// - alphabet is lowercase [a-z0-9] (base32hex is [0-9a-v]): DNS-1123 safe for +// k8s pod names, and byte order == lexicographic order, so ids sort in mint +// order at ms resolution; +// - the id NEVER contains "-" β€” that delimiter belongs to pod-name suffixes +// (`runner--attempt-N`), so the id round-trips through a pod name by +// cutting at the first hyphen; +// - the `run_` (friendly) and `runner-` (pod) prefixes are part of the spec: +// they guarantee the k8s name starts with a letter even though a base32hex +// core can start with a digit. +const RUN_OPS_ID_ALPHABET = "0123456789abcdefghijklmnopqrstuv"; // base32hex, lowercase (RFC 4648 Β§7) +export const RUN_OPS_ID_LENGTH = 26; +export const RUN_OPS_ID_REGION_INDEX = 24; +export const RUN_OPS_ID_VERSION_INDEX = 25; +export const RUN_OPS_ID_VERSION = "1"; +const RUN_OPS_ID_CORE_BYTES = 15; // 6 timestamp + 9 random β†’ exactly 24 base32hex chars +const RUN_OPS_ID_CORE_LENGTH = 24; +const RUN_OPS_ID_TIMESTAMP_BYTES = 6; + +/** Region char stamped when the region is unknown or unmapped at mint. */ +export const DEFAULT_REGION_CHAR = "0"; +// The region char is a raw positional char (readable via charAt before any +// decoding), NOT part of the base32hex core β€” so it may use the full DNS-safe +// lowercase [a-z0-9] range (e.g. "w" for us-west-2, which is outside [0-9a-v]). +const REGION_CHAR_PATTERN = /^[a-z0-9]$/; +/** One lowercase [a-z0-9] char per supported region, at RUN_OPS_ID_REGION_INDEX. */ +export const REGION_CODES: Readonly> = { + "us-east-1": "e", + "us-west-2": "w", + "eu-central-1": "c", +}; + +export function regionCharForRegion(region: string | undefined): string { + return (region && REGION_CODES[region]) || DEFAULT_REGION_CHAR; +} // globalThis.crypto is absent on Node 18.20 (a supported engine) without a flag, so fall back to // node:crypto's webcrypto, loaded only when the global is missing to stay isomorphic. @@ -40,116 +69,111 @@ function loadNodeWebCrypto(): Crypto | undefined { } // Resolve the crypto source lazily on first use (memoized), so merely importing this -// widely-used module never throws when crypto is unavailable β€” only minting a KSUID would. +// widely-used module never throws when crypto is unavailable β€” only minting an id would. let cachedGetRandomValues: RandomFiller | undefined; const getRandomValues: RandomFiller = (array) => (cachedGetRandomValues ??= resolveGetRandomValues())(array); -/** Encode raw bytes as base62 (big-endian), left-padded to the given length. */ -function base62Encode(bytes: Uint8Array, length: number): string { - const digits = Array.from(bytes); - let result = ""; - - while (digits.length > 0) { - let remainder = 0; - const quotient: number[] = []; - - for (let i = 0; i < digits.length; i++) { - const acc = (digits[i] ?? 0) + remainder * 256; - const q = Math.floor(acc / 62); - remainder = acc % 62; - - if (quotient.length > 0 || q > 0) { - quotient.push(q); - } +/** Lowercase base32hex (RFC 4648 Β§7): 5 bits per char, order-preserving, no padding. */ +export function base32hexEncode(bytes: Uint8Array): string { + let out = ""; + let buf = 0; + let bits = 0; + for (const b of bytes) { + buf = (buf << 8) | b; + bits += 8; + while (bits >= 5) { + out += RUN_OPS_ID_ALPHABET[(buf >> (bits - 5)) & 31]; + bits -= 5; } - - result = BASE62_ALPHABET.charAt(remainder) + result; - digits.length = 0; - digits.push(...quotient); } + return out; +} - return result.padStart(length, BASE62_ALPHABET.charAt(0)); +/** Inverse of base32hexEncode. Throws on characters outside the lowercase alphabet. */ +export function base32hexDecode(s: string): Uint8Array { + const out: number[] = []; + let buf = 0; + let bits = 0; + for (const c of s) { + const v = RUN_OPS_ID_ALPHABET.indexOf(c); + if (v === -1) { + throw new Error(`invalid run id char: ${c}`); + } + buf = (buf << 5) | v; + bits += 5; + if (bits >= 8) { + out.push((buf >> (bits - 8)) & 0xff); + bits -= 8; + } + } + return Uint8Array.from(out); } /** - * 27-char, base62, time-ordered KSUID body (length-disjoint from the 25-char cuid): a 4-byte - * timestamp (seconds since the KSUID epoch) + a 16-byte payload; ids from different seconds - * sort in mint order. Payload defaults to CSPRNG entropy; callers may supply up to - * KSUID_PAYLOAD_BYTES metadata bytes (written first, remainder stays random for uniqueness). + * Mint a run-ops v1 id body (26 chars, no prefix): 24-char base32hex core + * (6-byte ms timestamp + 9 CSPRNG bytes) + region char + version char "1". + * The trailing version char at RUN_OPS_ID_VERSION_INDEX is the residency + * discriminator β€” see runOpsResidency.ts. */ -export function generateKsuidId(payload?: Uint8Array): string { - const bytes = new Uint8Array(KSUID_TOTAL_BYTES); - - const timestamp = Math.floor(Date.now() / 1000) - KSUID_EPOCH; - bytes[0] = (timestamp >>> 24) & 0xff; - bytes[1] = (timestamp >>> 16) & 0xff; - bytes[2] = (timestamp >>> 8) & 0xff; - bytes[3] = timestamp & 0xff; - - if (payload && payload.length > KSUID_PAYLOAD_BYTES) { - throw new Error( - `KSUID payload must be at most ${KSUID_PAYLOAD_BYTES} bytes (got ${payload.length})` - ); - } - const reserved = payload?.length ?? 0; - if (payload && reserved > 0) { - bytes.set(payload, KSUID_TIMESTAMP_BYTES); - } - if (reserved < KSUID_PAYLOAD_BYTES) { - getRandomValues(bytes.subarray(KSUID_TIMESTAMP_BYTES + reserved)); +export function generateRunOpsId(region?: string): string { + const core = new Uint8Array(RUN_OPS_ID_CORE_BYTES); + + let ms = Date.now(); + for (let i = RUN_OPS_ID_TIMESTAMP_BYTES - 1; i >= 0; i--) { + core[i] = ms % 256; + ms = Math.floor(ms / 256); } + getRandomValues(core.subarray(RUN_OPS_ID_TIMESTAMP_BYTES)); - return base62Encode(bytes, KSUID_STRING_LENGTH); + return `${base32hexEncode(core)}${regionCharForRegion(region)}${RUN_OPS_ID_VERSION}`; } -/** Decoded parts of a KSUID body: its mint timestamp and 16-byte payload. */ -export type DecodedKsuid = { - timestampSeconds: number; - timestamp: Date; - payload: Uint8Array; -}; +export type ParsedRunId = + | { format: "b32hex"; table: "partitioned"; timestamp: Date; region: string; version: string } + | { format: "legacy"; table: "legacy" }; + +const LEGACY_RUN_ID: ParsedRunId = { format: "legacy", table: "legacy" }; /** - * Decode a KSUID body (or a `prefix_` friendly id) into its timestamp + 16-byte payload. - * The inverse of generateKsuidId's layout. Throws if the body is not 27 base62 chars. + * Parse a v1 id body (no prefix). Returns undefined unless the body is exactly + * 26 chars with version "1" at index 25 and every char inside the base32hex + * alphabet β€” anything else (cuid, nanoid, pre-cutover 27-char base62, malformed + * v1) is a legacy shape. */ -export function decodeKsuid(idOrFriendlyId: string): DecodedKsuid { - const underscore = idOrFriendlyId.indexOf("_"); - const body = underscore === -1 ? idOrFriendlyId : idOrFriendlyId.slice(underscore + 1); - if (body.length !== KSUID_STRING_LENGTH) { - throw new Error( - `Not a KSUID body: expected ${KSUID_STRING_LENGTH} base62 chars, got ${body.length}` - ); +export function parseRunOpsIdBody( + body: string +): { timestamp: Date; region: string; version: string } | undefined { + if (body.length !== RUN_OPS_ID_LENGTH) return undefined; + if (body[RUN_OPS_ID_VERSION_INDEX] !== RUN_OPS_ID_VERSION) return undefined; + const region = body[RUN_OPS_ID_REGION_INDEX] ?? ""; + if (!REGION_CHAR_PATTERN.test(region)) return undefined; + + let core: Uint8Array; + try { + core = base32hexDecode(body.slice(0, RUN_OPS_ID_CORE_LENGTH)); + } catch { + return undefined; } - let n = BigInt(0); - for (const ch of body) { - const digit = BASE62_ALPHABET.indexOf(ch); - if (digit < 0) { - throw new Error(`Invalid base62 character in KSUID body: ${ch}`); - } - n = n * BigInt(62) + BigInt(digit); + let ms = 0; + for (let i = 0; i < RUN_OPS_ID_TIMESTAMP_BYTES; i++) { + ms = ms * 256 + (core[i] ?? 0); } - const bytes = new Uint8Array(KSUID_TOTAL_BYTES); - for (let i = KSUID_TOTAL_BYTES - 1; i >= 0; i--) { - bytes[i] = Number(n & BigInt(0xff)); - n >>= BigInt(8); - } + return { timestamp: new Date(ms), region, version: RUN_OPS_ID_VERSION }; +} + +/** True if the (prefixless) id body is a well-formed run-ops v1 id. */ +export function isRunOpsIdBody(body: string): boolean { + return parseRunOpsIdBody(body) !== undefined; +} - const timestampSeconds = - (bytes[0] ?? 0) * 0x1000000 + - (bytes[1] ?? 0) * 0x10000 + - (bytes[2] ?? 0) * 0x100 + - (bytes[3] ?? 0) + - KSUID_EPOCH; - - return { - timestampSeconds, - timestamp: new Date(timestampSeconds * 1000), - payload: bytes.slice(KSUID_TIMESTAMP_BYTES), - }; +/** Parse a `run_`-prefixed friendly id; anything not a well-formed v1 id is legacy. */ +export function parseRunId(id: string): ParsedRunId { + if (!id.startsWith("run_")) return LEGACY_RUN_ID; + const parsed = parseRunOpsIdBody(id.slice(4)); + return parsed ? { format: "b32hex", table: "partitioned", ...parsed } : LEGACY_RUN_ID; } export function generateInternalId(): string { diff --git a/packages/core/src/v3/isomorphic/runOpsResidency.test.ts b/packages/core/src/v3/isomorphic/runOpsResidency.test.ts index 9ed13b05be..ceb6d9b535 100644 --- a/packages/core/src/v3/isomorphic/runOpsResidency.test.ts +++ b/packages/core/src/v3/isomorphic/runOpsResidency.test.ts @@ -1,17 +1,11 @@ import { describe, expect, it } from "vitest"; -import { RunId, WaitpointId, SnapshotId, generateKsuidId } from "./friendlyId.js"; -import { - ownerEngine, - classifyResidency, - classifyKind, - isClassifiable, - UnclassifiableRunId, -} from "./runOpsResidency.js"; +import { RunId, WaitpointId, BatchId, SnapshotId, generateRunOpsId } from "./friendlyId.js"; +import { ownerEngine, classifyResidency, classifyKind, isClassifiable } from "./runOpsResidency.js"; const SAMPLES = 50_000; // property-scale; CI-fast. (Bump locally toward "millions" for deeper coverage.) -describe("ownerEngine β€” residency classifier", () => { - it("cuid-length ids (default mint) classify LEGACY, friendly + internal", () => { +describe("ownerEngine β€” residency classifier (version char at fixed position, not length)", () => { + it("cuid ids (default mint) classify LEGACY, friendly + internal", () => { for (const util of [RunId, WaitpointId]) { const { id, friendlyId } = util.generate(); expect(ownerEngine(id)).toBe("LEGACY"); @@ -22,38 +16,43 @@ describe("ownerEngine β€” residency classifier", () => { } }); - it("ksuid-length ids (explicit generateKsuidId) classify NEW, friendly + internal", () => { - for (const util of [RunId, WaitpointId]) { - const id = generateKsuidId(); + it("run-ops v1 ids (generateRunOpsId) classify NEW, friendly + internal, across id-shape co-located entities", () => { + for (const util of [RunId, WaitpointId, BatchId]) { + const id = generateRunOpsId("us-east-1"); const friendlyId = util.toFriendlyId(id); expect(ownerEngine(id)).toBe("NEW"); expect(ownerEngine(friendlyId)).toBe("NEW"); expect(classifyResidency(id)).toBe("NEW"); - expect(classifyKind(id)).toBe("ksuid"); + expect(classifyKind(id)).toBe("runOpsId"); } }); - it("disjointness: no cuid sample is ever NEW, no ksuid sample is ever LEGACY", () => { - for (let i = 0; i < SAMPLES; i++) { - expect(ownerEngine(RunId.generate().id)).toBe("LEGACY"); - expect(ownerEngine(generateKsuidId())).toBe("NEW"); - } + it("discriminates on the version char, not length: 26+'1' β†’ NEW, 26+'2' β†’ LEGACY", () => { + const v1 = "a".repeat(24) + "e1"; + expect(ownerEngine(v1)).toBe("NEW"); + expect(ownerEngine("a".repeat(24) + "e2")).toBe("LEGACY"); + expect(ownerEngine("a".repeat(26))).toBe("LEGACY"); // 26 chars but no version marker }); - it("throws UnclassifiableRunId on malformed lengths (24, 26, 28, empty)", () => { - for (const bad of ["", "x".repeat(24), "x".repeat(26), "x".repeat(28), "x".repeat(40)]) { - expect(() => ownerEngine(bad)).toThrow(UnclassifiableRunId); - expect(isClassifiable(bad)).toBe(false); + it("malformed v1 shapes fall back to LEGACY (never throw)", () => { + for (const bad of [ + "", + "x".repeat(24) + "01", // 'x' outside base32hex + "A".repeat(25) + "1", // uppercase + "a".repeat(24) + "-1", // hyphen region char + "a".repeat(27), // pre-cutover 27-char shape β†’ LEGACY under the version rule + "run_" + "b".repeat(27), // 27-char base62 pre-cutover friendly id β†’ LEGACY + "x".repeat(40), + ]) { + expect(ownerEngine(bad)).toBe("LEGACY"); + expect(isClassifiable(bad)).toBe(true); // classification is total now } }); - it("error carries the offending value + length for diagnostics", () => { - try { - ownerEngine("x".repeat(26)); - throw new Error("should have thrown"); - } catch (e) { - expect(e).toBeInstanceOf(UnclassifiableRunId); - expect((e as UnclassifiableRunId).message).toContain("26"); + it("disjointness: no cuid sample is ever NEW, no v1 sample is ever LEGACY", () => { + for (let i = 0; i < SAMPLES; i++) { + expect(ownerEngine(RunId.generate().id)).toBe("LEGACY"); + expect(ownerEngine(generateRunOpsId())).toBe("NEW"); } }); diff --git a/packages/core/src/v3/isomorphic/runOpsResidency.ts b/packages/core/src/v3/isomorphic/runOpsResidency.ts index edecec5ee7..f0b1e3db33 100644 --- a/packages/core/src/v3/isomorphic/runOpsResidency.ts +++ b/packages/core/src/v3/isomorphic/runOpsResidency.ts @@ -1,26 +1,29 @@ -import { KSUID_STRING_LENGTH } from "./friendlyId.js"; +import { isRunOpsIdBody } from "./friendlyId.js"; /** The two run-ops stores a run/waitpoint can reside in. */ export type Residency = "LEGACY" | "NEW"; -/** Underlying id format. cuid β†’ LEGACY store, ksuid β†’ NEW store. */ -export type ResidencyKind = "cuid" | "ksuid"; +/** + * Underlying id lineage. "runOpsId" is the label for the NEW-store mint path + * β€” a base32hex run-ops v1 id (see friendlyId.ts). It is the value persisted in + * the runOpsMintKind feature flag. "cuid" is every legacy shape (cuid, nanoid, + * and the pre-cutover 27-char base62 format). + */ +export type ResidencyKind = "cuid" | "runOpsId"; -/** @bugsnag/cuid emits 25-char ids (cuid path, flag OFF). */ +/** @bugsnag/cuid emits 25-char ids (legacy mint path, flag OFF). */ export const CUID_LENGTH = 25; -/** KSUID / nanoid-27 emits 27-char ids (ksuid path, flag ON). */ -export const KSUID_LENGTH = KSUID_STRING_LENGTH; -/** Thrown when an id length matches neither the cuid nor the ksuid margin. */ +/** + * Kept for API compatibility: the default classifier no longer throws (every + * non-v1 shape is legacy), but injected classifiers may still raise it and + * callers still catch it. + */ export class UnclassifiableRunId extends Error { readonly value: string; readonly valueLength: number; constructor(value: string) { - super( - `Unclassifiable run-ops id: length ${value.length} matches neither cuid (${CUID_LENGTH}) nor ksuid (${KSUID_LENGTH}) β€” value=${JSON.stringify( - value - )}` - ); + super(`Unclassifiable run-ops id: value=${JSON.stringify(value)} (length ${value.length})`); this.name = "UnclassifiableRunId"; this.value = value; this.valueLength = value.length; @@ -38,23 +41,26 @@ function internalForm(id: string): string { return underscore === -1 ? id : id.slice(underscore + 1); } -/** Returns the underlying id FORMAT (cuid|ksuid), or throws if unclassifiable. */ +/** + * Returns the id lineage by the version-char rule: a well-formed run-ops v1 + * body (26 chars, version "1" at index 25, base32hex alphabet) is "runOpsId" + * (NEW store); everything else β€” including malformed v1 shapes β€” is "cuid" + * (legacy). Total: never throws. Transition: pre-cutover 27-char base62 ids (the old + * NEW-mint format) now classify LEGACY, so ship this with the base32hex generator only once + * any 27-char NEW-resident runs are drained/disposable β€” no live run is misrouted mid-cutover. + */ export function classifyKind(id: string): ResidencyKind { - const internal = internalForm(id); - if (internal.length === CUID_LENGTH) return "cuid"; - if (internal.length === KSUID_LENGTH) return "ksuid"; - throw new UnclassifiableRunId(id); + return isRunOpsIdBody(internalForm(id)) ? "runOpsId" : "cuid"; } -/** Non-throwing predicate: is this id length one we can classify? */ -export function isClassifiable(id: string): boolean { - const len = internalForm(id).length; - return len === CUID_LENGTH || len === KSUID_LENGTH; +/** Classification is total now; kept for API compatibility. */ +export function isClassifiable(_id: string): boolean { + return true; } -/** Map an id to its owning run-ops store by length. Throws on ambiguity. */ +/** Map an id to its owning run-ops store by the version-char rule. */ export function classifyResidency(id: string): Residency { - return classifyKind(id) === "ksuid" ? "NEW" : "LEGACY"; + return classifyKind(id) === "runOpsId" ? "NEW" : "LEGACY"; } /** Primary public name (RoutingRunStore / cross-seam guard). */ diff --git a/scripts/retry-prisma-generate.mjs b/scripts/retry-prisma-generate.mjs new file mode 100644 index 0000000000..f20fa4616c --- /dev/null +++ b/scripts/retry-prisma-generate.mjs @@ -0,0 +1,53 @@ +// Retry wrapper around `prisma generate`. Our two prisma clients pin the same +// prisma version and so share one package instance in the pnpm store; when +// `turbo run generate` runs them concurrently both race to write the shared +// query-engine binary, and on Windows the loser fails with `EPERM ... rename`. +// Retrying lets it succeed once the engine file is present and unlocked. On +// non-Windows the first attempt succeeds, so this is a zero-cost no-op. +import { spawnSync } from "node:child_process"; + +const MAX_ATTEMPTS = 5; +const BASE_DELAY_MS = 500; + +// Transient, retryable filesystem contention on the shared engine binary. +const TRANSIENT = + /\b(EPERM|EBUSY|EACCES)\b|operation not permitted|resource busy or locked|being used by another process/i; + +const passthroughArgs = process.argv.slice(2); + +function sleepSync(ms) { + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); +} + +let lastStatus = 1; + +for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + const result = spawnSync("prisma", ["generate", ...passthroughArgs], { + shell: true, + encoding: "utf8", + }); + + process.stdout.write(result.stdout ?? ""); + process.stderr.write(result.stderr ?? ""); + + if (result.status === 0) { + process.exit(0); + } + + lastStatus = result.status ?? 1; + + const output = `${result.stdout ?? ""}${result.stderr ?? ""}`; + const isRetryable = TRANSIENT.test(output); + + if (!isRetryable || attempt === MAX_ATTEMPTS) { + break; + } + + const delay = BASE_DELAY_MS * attempt; + console.error( + `prisma generate hit a transient filesystem error (attempt ${attempt}/${MAX_ATTEMPTS}); retrying in ${delay}ms...` + ); + sleepSync(delay); +} + +process.exit(lastStatus);