From b8f2bf5fc855398d6d3f2de790ccea3ae4e4e8d6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 19 Aug 2026 17:43:38 +0000 Subject: [PATCH 1/2] fix(storage): drop leftover service inventory purge after production is empty Production APP_DB has zero kind=service user_storage_buckets rows. Remove the estimate-backfill purge hook, tighten the live CHECK in migration 0017, and garden the leftover-service docs. Closes #1565. Co-authored-by: me --- .../contributing/architecture/data-storage.md | 19 ++- .../runtime-worker-migration-runbook.md | 13 +- .../0025-no-package-services-primitive.md | 32 ++--- ...0017-drop-retired-service-storage-kind.sql | 43 +++++++ .../estimate-backfill.node.test.ts | 28 ----- .../src/storage-buckets/estimate-backfill.ts | 14 --- .../estimate-backfill.workers.test.ts | 73 ----------- ...purge-retired-service-storage.node.test.ts | 119 ------------------ .../purge-retired-service-storage.ts | 69 ---------- .../worker/src/storage-buckets/service.ts | 7 +- .../storage-buckets/service.workers.test.ts | 14 +++ .../worker/src/storage-buckets/test-schema.ts | 6 +- tools/migration-ledger.json | 4 + 13 files changed, 99 insertions(+), 342 deletions(-) create mode 100644 packages/worker/migrations/0017-drop-retired-service-storage-kind.sql delete mode 100644 packages/worker/src/storage-buckets/purge-retired-service-storage.node.test.ts delete mode 100644 packages/worker/src/storage-buckets/purge-retired-service-storage.ts diff --git a/docs/contributing/architecture/data-storage.md b/docs/contributing/architecture/data-storage.md index 15bf7f0da9..15c30755f1 100644 --- a/docs/contributing/architecture/data-storage.md +++ b/docs/contributing/architecture/data-storage.md @@ -568,17 +568,14 @@ Storage split: workspaces for the check-only `storage_bytes` baseline. Rows use `kind` to dispatch `getEstimatedBytes` to the correct Durable Object, cache the latest `databaseSize` in `estimated_bytes`, and feed the same bounded -`storage_bucket_estimate_backfill` lane. Leftover retired `kind = 'service'` -rows stay inventoried until that lane clears the matching StorageRunner Durable -Objects and then deletes the rows, so account export, deletion, and DR keep a -user→storage_id map. Estimate reads treat those leftover rows as StorageRunner -(`unknown`). Index-backed repo-session reconciliation pages -`repo_session_due_owners` with a per-tick owner budget and a platform-owned -`repo_session_storage_bucket_cursor` so a sweep cannot walk the whole fleet. -Repo sessions register on open, opportunistically refresh after workspace -mutations, and remove the inventory row on discard, purge, scheduled cleanup, -source deletion, or account deletion. This estimate component is composed with -the authoritative UserMeter D1 payload bytes; it is not reserved into UserMeter. +`storage_bucket_estimate_backfill` lane. Index-backed repo-session +reconciliation pages `repo_session_due_owners` with a per-tick owner budget and +a platform-owned `repo_session_storage_bucket_cursor` so a sweep cannot walk the +whole fleet. Repo sessions register on open, opportunistically refresh after +workspace mutations, and remove the inventory row on discard, purge, scheduled +cleanup, source deletion, or account deletion. This estimate component is +composed with the authoritative UserMeter D1 payload bytes; it is not reserved +into UserMeter. ## Durable Objects (`UserMeter`) diff --git a/docs/contributing/architecture/runtime-worker-migration-runbook.md b/docs/contributing/architecture/runtime-worker-migration-runbook.md index 9a817a1c29..a07cfb2271 100644 --- a/docs/contributing/architecture/runtime-worker-migration-runbook.md +++ b/docs/contributing/architecture/runtime-worker-migration-runbook.md @@ -174,11 +174,14 @@ rejects a same-deploy `deleted_classes` migration while that binding still exists (error 10061). Existing objects also require the script to keep exporting the class until `deleted_classes` runs (error 10064). Export a stub, drop the binding, then add the `deleted_classes` migration and its -`tools/ci/do-deletion-allowlist.json` entry on a later deploy. - -`PackageServiceInstance` uses that sequence: production `kody-runtime` applies -tag `v2` after a stub-export deploy dropped the transferred binding. Preview -applies `v1` `new_sqlite_classes` then `v2` `deleted_classes` on first deploy. +`tools/ci/do-deletion-allowlist.json` entry on a later deploy. Preview +`deleted_classes` requires a previous script version that exported the class +(error 10074). A first preview deploy can apply the create and delete tags +together so they elide. + +`PackageServiceInstance` is gone from production `kody-runtime` (tag `v2`; no +stub export). Preview applies `v1` `new_sqlite_classes` then `v2` +`deleted_classes` on first deploy so create and delete elide. ## Rollback diff --git a/docs/contributing/decisions/0025-no-package-services-primitive.md b/docs/contributing/decisions/0025-no-package-services-primitive.md index c30f8eb5b5..c155c5fbf1 100644 --- a/docs/contributing/decisions/0025-no-package-services-primitive.md +++ b/docs/contributing/decisions/0025-no-package-services-primitive.md @@ -30,18 +30,20 @@ subscriptions, and package-owned storage. Integrations that require a continuously connected daemon need an external deployment and communicate with Kody through supported integration surfaces. -Leftover `kind = 'service'` `user_storage_buckets` rows stay until the -`storage_bucket_estimate_backfill` lane clears the matching StorageRunner -Durable Objects and then deletes the inventory. Account export and deletion keep -discovering those storage ids until that purge succeeds. - -Deleting `PackageServiceInstance` on production `kody-runtime` is two deploys. -The class arrived through a `transferred_classes` migration, so Cloudflare still -has a remote binding and existing objects after the source binding is gone. A -same-deploy `deleted_classes` migration fails with error 10061. Dropping the -binding without exporting the class fails with error 10064. Export a stub, drop -the remote binding, then apply runtime-worker tag `v2` `deleted_classes` and -remove the stub. #1559 completed the stub-and-drop-binding deploy. The -`tools/ci/do-deletion-allowlist.json` entry for tag `v2` is the contract for the -class-delete follow-up. Preview `deleted_classes` requires a previous script -version that exported the class (error 10074). +Leftover `kind = 'service'` `user_storage_buckets` rows stayed until the +`storage_bucket_estimate_backfill` lane cleared the matching StorageRunner +Durable Objects and then deleted the inventory. Production `APP_DB` had zero +leftover rows on 2026-08-19. #1565 removes the purge lane and tightens the live +CHECK so `'service'` cannot return. + +Deleting `PackageServiceInstance` on production `kody-runtime` required two +deploys after #1552. The class arrived through a `transferred_classes` +migration, so Cloudflare still had a remote binding and existing objects after +the source binding was gone. A same-deploy `deleted_classes` migration failed +with error 10061 (#1552 deploy). Dropping the binding without exporting the +class failed with error 10064 (#1558). #1559 exported a stub and dropped the +remote binding. #1560 applied runtime-worker tag `v2` `deleted_classes` and +removed the stub. Preview `deleted_classes` requires a previous script version +that exported the class (error 10074); a first preview deploy applies `v1` +`new_sqlite_classes` and `v2` `deleted_classes` together so create and delete +elide. diff --git a/packages/worker/migrations/0017-drop-retired-service-storage-kind.sql b/packages/worker/migrations/0017-drop-retired-service-storage-kind.sql new file mode 100644 index 0000000000..9bd8ef54c9 --- /dev/null +++ b/packages/worker/migrations/0017-drop-retired-service-storage-kind.sql @@ -0,0 +1,43 @@ +-- Production APP_DB has zero leftover kind='service' inventory (confirmed +-- 2026-08-19). Tighten the CHECK so the retired kind cannot return. Fail +-- closed if a leftover row appears: the INSERT into the next table rejects +-- 'service'. + +CREATE TABLE user_storage_buckets_next ( + user_id TEXT NOT NULL, + storage_id TEXT NOT NULL, + kind TEXT NOT NULL CHECK ( + kind IN ('job', 'package', 'execute', 'repo_session', 'unknown') + ), + created_at TEXT NOT NULL, + last_seen_at TEXT NOT NULL, + estimated_bytes INTEGER, + estimated_bytes_updated_at TEXT, + PRIMARY KEY (user_id, storage_id) +); + +INSERT INTO user_storage_buckets_next ( + user_id, + storage_id, + kind, + created_at, + last_seen_at, + estimated_bytes, + estimated_bytes_updated_at +) +SELECT + user_id, + storage_id, + kind, + created_at, + last_seen_at, + estimated_bytes, + estimated_bytes_updated_at +FROM user_storage_buckets; + +DROP TABLE user_storage_buckets; + +ALTER TABLE user_storage_buckets_next RENAME TO user_storage_buckets; + +CREATE INDEX idx_user_storage_buckets_user +ON user_storage_buckets(user_id); diff --git a/packages/worker/src/storage-buckets/estimate-backfill.node.test.ts b/packages/worker/src/storage-buckets/estimate-backfill.node.test.ts index fbade7d2da..9ef7f7b617 100644 --- a/packages/worker/src/storage-buckets/estimate-backfill.node.test.ts +++ b/packages/worker/src/storage-buckets/estimate-backfill.node.test.ts @@ -6,11 +6,6 @@ const mockModule = vi.hoisted(() => ({ listStorageBucketsMissingEstimates: vi.fn(), registerMissingRepoSessionStorageBuckets: vi.fn(async () => 0), updateStorageBucketEstimate: vi.fn(async () => true), - purgeRetiredServiceStorageBuckets: vi.fn(async () => ({ - scanned: 0, - purged: 0, - failed: 0, - })), })) vi.mock('#worker/storage-runner.ts', () => ({ @@ -18,11 +13,6 @@ vi.mock('#worker/storage-runner.ts', () => ({ mockModule.readInventoriedStorageBucketEstimatedBytes(...args), })) -vi.mock('./purge-retired-service-storage.ts', () => ({ - purgeRetiredServiceStorageBuckets: (...args: Array) => - mockModule.purgeRetiredServiceStorageBuckets(...args), -})) - vi.mock('./service.ts', () => ({ listStorageBucketsMissingEstimates: (...args: Array) => mockModule.listStorageBucketsMissingEstimates(...args), @@ -61,10 +51,6 @@ test('backfill tolerates per-bucket probe failures and keeps sweeping peers', as updated: 2, failed: 1, }) - expect(mockModule.purgeRetiredServiceStorageBuckets).toHaveBeenCalledWith({ - env, - limit: 24, - }) // The failing bucket is logged and left NULL for a later sweep; the two // healthy buckets are persisted. @@ -88,18 +74,4 @@ test('backfill tolerates per-bucket probe failures and keeps sweeping peers', as estimatedBytes: 2048, }), ) - - mockModule.purgeRetiredServiceStorageBuckets.mockRejectedValueOnce( - new Error('purge boom'), - ) - mockModule.listStorageBucketsMissingEstimates.mockResolvedValue([]) - await expect(backfillStorageBucketEstimates({ env })).resolves.toEqual({ - scanned: 0, - updated: 0, - failed: 0, - }) - expect(consoleWarn).toHaveBeenCalledWith( - 'retired-service-storage-purge-failed', - expect.any(Error), - ) }) diff --git a/packages/worker/src/storage-buckets/estimate-backfill.ts b/packages/worker/src/storage-buckets/estimate-backfill.ts index 51784cb774..5b038cf09d 100644 --- a/packages/worker/src/storage-buckets/estimate-backfill.ts +++ b/packages/worker/src/storage-buckets/estimate-backfill.ts @@ -1,4 +1,3 @@ -import { purgeRetiredServiceStorageBuckets } from './purge-retired-service-storage.ts' import { listStorageBucketsMissingEstimates, registerMissingRepoSessionStorageBuckets, @@ -18,10 +17,6 @@ import { readInventoriedStorageBucketEstimatedBytes } from '#worker/storage-runn * retried on a later sweep; it never blocks the rest of the batch. The lane * uses a short retry policy (the write path carries the strong one) because * sweep retries are free. - * - * The same tick also clears leftover retired `kind = 'service'` StorageRunner - * Durable Objects and then deletes those inventory rows. D1 cannot purge - * Durable Objects, so migration 0016 keeps the rows until this succeeds. */ export const storageBucketEstimateBackfillBatchSize = 24 @@ -34,15 +29,6 @@ export async function backfillStorageBucketEstimates(input: { batchSize?: number }): Promise<{ scanned: number; updated: number; failed: number }> { const batchSize = input.batchSize ?? storageBucketEstimateBackfillBatchSize - // Leftover kind='service' inventory is the only map to retired - // StorageRunner DOs. Clear those objects, then drop the rows. Failures - // stay inventoried so export and account deletion still find them. - await purgeRetiredServiceStorageBuckets({ - env: input.env, - limit: batchSize, - }).catch((error: unknown) => { - console.warn('retired-service-storage-purge-failed', error) - }) // Recover active repo sessions whose awaited open-time registration lost // its D1 write (registration is deliberately non-blocking for the // session). Race-safe: the statement filters on active status, so it can diff --git a/packages/worker/src/storage-buckets/estimate-backfill.workers.test.ts b/packages/worker/src/storage-buckets/estimate-backfill.workers.test.ts index 576c52c7b4..bc3e3914e8 100644 --- a/packages/worker/src/storage-buckets/estimate-backfill.workers.test.ts +++ b/packages/worker/src/storage-buckets/estimate-backfill.workers.test.ts @@ -8,9 +8,7 @@ import { repoSessionRpc } from '#worker/repo/repo-session-rpc.ts' import { backfillStorageBucketEstimates } from './estimate-backfill.ts' import { clearStorageBucketRegistrationDedupeForTests, - flushStorageBucketRegistrationsForTests, listUserStorageBucketEstimates, - listUserStorageBucketIds, registerStorageBucket, } from './service.ts' import { ensureUserStorageBucketsTestSchema } from './test-schema.ts' @@ -125,74 +123,3 @@ test( }) }, ) - -test( - 'backfill clears leftover service StorageRunner objects before dropping inventory', - { timeout: testTimeout }, - async () => { - await ensureUserStorageBucketsTestSchema(env.APP_DB) - clearStorageBucketRegistrationDedupeForTests() - const userId = `usb-service-purge-${crypto.randomUUID()}` - const liveBucket = `package:${crypto.randomUUID()}` - const leftoverBucket = `service:${crypto.randomUUID()}` - const now = new Date().toISOString() - await env.APP_DB.prepare( - `INSERT INTO user_storage_buckets ( - user_id, storage_id, kind, created_at, last_seen_at, - estimated_bytes - ) VALUES (?, ?, 'service', ?, ?, NULL), (?, ?, 'package', ?, ?, ?)`, - ) - .bind( - userId, - leftoverBucket, - now, - now, - userId, - liveBucket, - now, - now, - emptyStorageRunnerEstimatedBytes, - ) - .run() - await storageRunnerRpc({ - env, - userId, - storageId: leftoverBucket, - }).setValue({ key: 'leftover', value: { kept: true } }) - await flushStorageBucketRegistrationsForTests() - await expect( - storageRunnerRpc({ - env, - userId, - storageId: leftoverBucket, - }).getValue({ key: 'leftover' }), - ).resolves.toEqual({ - key: 'leftover', - value: { kept: true }, - }) - await expect(listUserStorageBucketIds({ env, userId })).resolves.toEqual( - [liveBucket, leftoverBucket].sort(), - ) - - await backfillStorageBucketEstimates({ env }) - await expect(listUserStorageBucketIds({ env, userId })).resolves.toEqual([ - liveBucket, - ]) - await expect( - storageRunnerRpc({ - env, - userId, - storageId: leftoverBucket, - }).getValue({ key: 'leftover' }), - ).resolves.toEqual({ key: 'leftover', value: null }) - await expect( - listUserStorageBucketEstimates({ env, userId }), - ).resolves.toEqual([ - { - storageId: liveBucket, - kind: 'package', - estimatedBytes: emptyStorageRunnerEstimatedBytes, - }, - ]) - }, -) diff --git a/packages/worker/src/storage-buckets/purge-retired-service-storage.node.test.ts b/packages/worker/src/storage-buckets/purge-retired-service-storage.node.test.ts deleted file mode 100644 index 2a23d2ac71..0000000000 --- a/packages/worker/src/storage-buckets/purge-retired-service-storage.node.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { DatabaseSync } from 'node:sqlite' -import { expect, test, vi } from 'vitest' -import { applyAllMigrations } from '#worker/test-support/apply-all-migrations.ts' -import { consoleWarn } from '#worker/test-support/console-spies.ts' -import { createD1FromSqlite } from '#worker/test-support/create-d1-from-sqlite.ts' -import { - listPlatformStorageBuckets, - listStorageBucketsMissingEstimates, - listUserStorageBucketEstimates, - listUserStorageBucketIds, -} from './service.ts' - -const storageRunnerMock = vi.hoisted(() => ({ - clearStorage: vi.fn(async () => ({ ok: true as const })), -})) - -vi.mock('#worker/storage-runner.ts', () => ({ - storageRunnerRpc: () => ({ - clearStorage: (...args: Array) => - storageRunnerMock.clearStorage(...args), - }), -})) - -const { purgeRetiredServiceStorageBuckets } = - await import('./purge-retired-service-storage.ts') - -function createDb() { - const sqlite = new DatabaseSync(':memory:') - applyAllMigrations(sqlite, new URL('../../migrations/', import.meta.url)) - return createD1FromSqlite(sqlite) -} - -async function insertBucket( - db: D1Database, - row: { userId: string; storageId: string; kind: string }, -) { - const now = '2026-08-19T00:00:00.000Z' - await db - .prepare( - `INSERT INTO user_storage_buckets ( - user_id, storage_id, kind, created_at, last_seen_at - ) VALUES (?, ?, ?, ?, ?)`, - ) - .bind(row.userId, row.storageId, row.kind, now, now) - .run() -} - -test('migration 0016 keeps leftover service inventory until StorageRunner clear succeeds', async () => { - consoleWarn.mockImplementation(() => {}) - const db = createDb() - const env = { APP_DB: db } as Env - await insertBucket(db, { - userId: 'user-a', - storageId: 'service:gateway', - kind: 'service', - }) - await insertBucket(db, { - userId: 'user-a', - storageId: 'package:keep', - kind: 'package', - }) - await insertBucket(db, { - userId: 'user-b', - storageId: 'service:other', - kind: 'service', - }) - - await expect( - listUserStorageBucketIds({ env, userId: 'user-a' }), - ).resolves.toEqual(['package:keep', 'service:gateway']) - await expect(listPlatformStorageBuckets({ db })).resolves.toEqual([ - { userId: 'user-a', storageId: 'package:keep' }, - { userId: 'user-a', storageId: 'service:gateway' }, - { userId: 'user-b', storageId: 'service:other' }, - ]) - await expect( - listUserStorageBucketEstimates({ env, userId: 'user-a' }), - ).resolves.toEqual([ - { storageId: 'package:keep', kind: 'package', estimatedBytes: null }, - { storageId: 'service:gateway', kind: 'unknown', estimatedBytes: null }, - ]) - await expect( - listStorageBucketsMissingEstimates({ db, limit: 10 }), - ).resolves.toEqual([ - { userId: 'user-a', storageId: 'package:keep', kind: 'package' }, - ]) - - storageRunnerMock.clearStorage.mockImplementationOnce(async () => { - throw new Error('storage runner unavailable') - }) - await expect( - purgeRetiredServiceStorageBuckets({ env, limit: 1 }), - ).resolves.toEqual({ scanned: 1, purged: 0, failed: 1 }) - expect(consoleWarn).toHaveBeenCalledWith( - 'retired-service-storage-purge-row-failed', - 'service:gateway', - expect.any(Error), - ) - await expect( - listUserStorageBucketIds({ env, userId: 'user-a' }), - ).resolves.toEqual(['package:keep', 'service:gateway']) - - storageRunnerMock.clearStorage.mockResolvedValue({ ok: true }) - await expect(purgeRetiredServiceStorageBuckets({ env })).resolves.toEqual({ - scanned: 2, - purged: 2, - failed: 0, - }) - expect(storageRunnerMock.clearStorage).toHaveBeenCalledTimes(3) - await expect( - listUserStorageBucketIds({ env, userId: 'user-a' }), - ).resolves.toEqual(['package:keep']) - await expect(listPlatformStorageBuckets({ db })).resolves.toEqual([ - { userId: 'user-a', storageId: 'package:keep' }, - ]) - await expect( - listUserStorageBucketIds({ env, userId: 'user-b' }), - ).resolves.toEqual([]) -}) diff --git a/packages/worker/src/storage-buckets/purge-retired-service-storage.ts b/packages/worker/src/storage-buckets/purge-retired-service-storage.ts deleted file mode 100644 index 85839f5775..0000000000 --- a/packages/worker/src/storage-buckets/purge-retired-service-storage.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { storageRunnerRpc } from '#worker/storage-runner.ts' -import { deleteStorageBucketInventory } from './service.ts' - -/** - * Retired package-service inventory kind. The live `StorageBucketKind` union - * no longer includes it; leftover rows stay until this purge clears the - * matching StorageRunner Durable Objects. - */ -const retiredServiceStorageBucketKind = 'service' - -export const retiredServiceStoragePurgeBatchSize = 24 - -export async function listRetiredServiceStorageBuckets(input: { - db: D1Database - limit: number -}): Promise> { - const result = await input.db - .prepare( - `SELECT user_id AS userId, storage_id AS storageId - FROM user_storage_buckets - WHERE kind = ? - ORDER BY user_id ASC, storage_id ASC - LIMIT ?`, - ) - .bind(retiredServiceStorageBucketKind, input.limit) - .all<{ userId: string; storageId: string }>() - return result.results ?? [] -} - -/** - * Clear leftover package-service StorageRunner Durable Objects, then delete - * their inventory rows. D1 cannot delete Durable Objects, so migration 0016 - * keeps these rows as the only user→storage_id map for export, deletion, and - * DR until this purge succeeds. A failed clear leaves the row in place. - */ -export async function purgeRetiredServiceStorageBuckets(input: { - env: Env - limit?: number -}): Promise<{ scanned: number; purged: number; failed: number }> { - const rows = await listRetiredServiceStorageBuckets({ - db: input.env.APP_DB, - limit: input.limit ?? retiredServiceStoragePurgeBatchSize, - }) - let purged = 0 - let failed = 0 - for (const row of rows) { - try { - await storageRunnerRpc({ - env: input.env, - userId: row.userId, - storageId: row.storageId, - }).clearStorage() - await deleteStorageBucketInventory({ - db: input.env.APP_DB, - userId: row.userId, - storageId: row.storageId, - }) - purged += 1 - } catch (error) { - failed += 1 - console.warn( - 'retired-service-storage-purge-row-failed', - row.storageId, - error, - ) - } - } - return { scanned: rows.length, purged, failed } -} diff --git a/packages/worker/src/storage-buckets/service.ts b/packages/worker/src/storage-buckets/service.ts index 23842bcbd1..333cbbe65d 100644 --- a/packages/worker/src/storage-buckets/service.ts +++ b/packages/worker/src/storage-buckets/service.ts @@ -334,7 +334,7 @@ export async function listStorageBucketsMissingEstimates(input: { .prepare( `SELECT user_id AS userId, storage_id AS storageId, kind FROM user_storage_buckets - WHERE estimated_bytes IS NULL AND kind <> 'service' + WHERE estimated_bytes IS NULL ORDER BY last_seen_at DESC, user_id ASC, storage_id ASC LIMIT ?`, ) @@ -525,9 +525,8 @@ export function storageBucketKindFromStorageId( /** * Map an inventoried `kind` onto the live StorageRunner / RepoSession - * dispatch union. Leftover retired `service` rows stay in D1 until their - * Durable Objects are cleared; estimate reads treat them as StorageRunner - * (`unknown`) so entitlement and export probes do not throw. + * dispatch union. Unexpected values become StorageRunner (`unknown`) so + * entitlement and export probes do not throw. */ function normalizeInventoriedStorageBucketKind( kind: string, diff --git a/packages/worker/src/storage-buckets/service.workers.test.ts b/packages/worker/src/storage-buckets/service.workers.test.ts index 99c48c27a0..304d8119e5 100644 --- a/packages/worker/src/storage-buckets/service.workers.test.ts +++ b/packages/worker/src/storage-buckets/service.workers.test.ts @@ -40,6 +40,20 @@ function catalogSessionRow( } } +test('user_storage_buckets CHECK rejects the retired service kind', async () => { + await ensureUserStorageBucketsTestSchema(env.APP_DB) + const now = new Date().toISOString() + await expect( + env.APP_DB.prepare( + `INSERT INTO user_storage_buckets ( + user_id, storage_id, kind, created_at, last_seen_at + ) VALUES (?, ?, 'service', ?, ?)`, + ) + .bind(`usb-service-${crypto.randomUUID()}`, 'service:retired', now, now) + .run(), + ).rejects.toThrow(/CHECK/i) +}) + test('registerStorageBucket upserts and list helpers scope correctly on real D1', async () => { await ensureUserStorageBucketsTestSchema(env.APP_DB) clearStorageBucketRegistrationDedupeForTests() diff --git a/packages/worker/src/storage-buckets/test-schema.ts b/packages/worker/src/storage-buckets/test-schema.ts index 906a6c0645..061cb8080b 100644 --- a/packages/worker/src/storage-buckets/test-schema.ts +++ b/packages/worker/src/storage-buckets/test-schema.ts @@ -1,9 +1,7 @@ /** * Non-destructive schema for `user_storage_buckets` in workers-unit tests, * where the D1 database starts empty and each suite provisions the tables it - * needs. Mirrors the squashed baseline plus - * `0003-repo-session-storage-buckets.sql` (CHECK still allows retired - * `service` so leftover inventory rows can be seeded and purged). + * needs. Mirrors the live `user_storage_buckets` schema (through 0017). */ export async function ensureUserStorageBucketsTestSchema(db: D1Database) { await db @@ -11,7 +9,7 @@ export async function ensureUserStorageBucketsTestSchema(db: D1Database) { `CREATE TABLE IF NOT EXISTS user_storage_buckets ( user_id TEXT NOT NULL, storage_id TEXT NOT NULL, - kind TEXT NOT NULL CHECK (kind IN ('job', 'package', 'service', 'execute', 'repo_session', 'unknown')), + kind TEXT NOT NULL CHECK (kind IN ('job', 'package', 'execute', 'repo_session', 'unknown')), created_at TEXT NOT NULL, last_seen_at TEXT NOT NULL, estimated_bytes INTEGER, diff --git a/tools/migration-ledger.json b/tools/migration-ledger.json index 66138fd397..91e6814ca1 100644 --- a/tools/migration-ledger.json +++ b/tools/migration-ledger.json @@ -68,6 +68,10 @@ "filename": "0016-drop-package-service-states.sql", "sha256": "9b05cc28701cdbf10a89464958c6709e9c8011a43dd154dc6653a23a6b2e86ed" }, + { + "filename": "0017-drop-retired-service-storage-kind.sql", + "sha256": "a7036229d5821900c3a45da2e4841d3439ea7d219f874a208462a10a29de26c5" + }, { "filename": "0017-package-owned-invocation-tokens.sql", "sha256": "772f7ef88f28778f4055ef04e431da1b9ff450cce7c48d94603271fed6d50885" From d710e41ba1e5247b98e3807792959433b6e48db5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 19 Aug 2026 17:45:05 +0000 Subject: [PATCH 2/2] fix(storage): renumber retired-service CHECK migration to 0018 main already used 0017 for package-owned invocation tokens. Co-authored-by: me --- ...ind.sql => 0018-drop-retired-service-storage-kind.sql} | 0 packages/worker/src/storage-buckets/test-schema.ts | 2 +- tools/migration-ledger.json | 8 ++++---- 3 files changed, 5 insertions(+), 5 deletions(-) rename packages/worker/migrations/{0017-drop-retired-service-storage-kind.sql => 0018-drop-retired-service-storage-kind.sql} (100%) diff --git a/packages/worker/migrations/0017-drop-retired-service-storage-kind.sql b/packages/worker/migrations/0018-drop-retired-service-storage-kind.sql similarity index 100% rename from packages/worker/migrations/0017-drop-retired-service-storage-kind.sql rename to packages/worker/migrations/0018-drop-retired-service-storage-kind.sql diff --git a/packages/worker/src/storage-buckets/test-schema.ts b/packages/worker/src/storage-buckets/test-schema.ts index 061cb8080b..79c199e4c2 100644 --- a/packages/worker/src/storage-buckets/test-schema.ts +++ b/packages/worker/src/storage-buckets/test-schema.ts @@ -1,7 +1,7 @@ /** * Non-destructive schema for `user_storage_buckets` in workers-unit tests, * where the D1 database starts empty and each suite provisions the tables it - * needs. Mirrors the live `user_storage_buckets` schema (through 0017). + * needs. Mirrors the live `user_storage_buckets` schema (through 0018). */ export async function ensureUserStorageBucketsTestSchema(db: D1Database) { await db diff --git a/tools/migration-ledger.json b/tools/migration-ledger.json index 91e6814ca1..42beb60236 100644 --- a/tools/migration-ledger.json +++ b/tools/migration-ledger.json @@ -68,13 +68,13 @@ "filename": "0016-drop-package-service-states.sql", "sha256": "9b05cc28701cdbf10a89464958c6709e9c8011a43dd154dc6653a23a6b2e86ed" }, - { - "filename": "0017-drop-retired-service-storage-kind.sql", - "sha256": "a7036229d5821900c3a45da2e4841d3439ea7d219f874a208462a10a29de26c5" - }, { "filename": "0017-package-owned-invocation-tokens.sql", "sha256": "772f7ef88f28778f4055ef04e431da1b9ff450cce7c48d94603271fed6d50885" + }, + { + "filename": "0018-drop-retired-service-storage-kind.sql", + "sha256": "a7036229d5821900c3a45da2e4841d3439ea7d219f874a208462a10a29de26c5" } ] }