Files
zpan/server/scripts/normalize-ids.integration.test.ts
T
Jasper Van b92df828ab ci: parallelize and isolate test suites (#556)
* ci: parallelize and isolate test suites

* ci: avoid unavailable Playwright video runtime

* ci: shard coverage and cache docker smoke

* ci: balance Playwright shards by test

* ci: smoke test the CLI container

* ci: enforce merged coverage thresholds

* ci: ratchet canonical coverage baseline

* ci: make coverage ratchet lossless

* ci: organize parallel gates by responsibility

* perf(ci): reduce total runner time

* perf(ci): balance runner cost and latency

* perf(ci): suppress passing test logs

* fix(test): make coverage sorting proof deterministic

* perf(docker): exclude test-only build inputs

* perf(ci): scope Docker smokes to packaging changes

* refactor(test): enforce fast test boundaries

* test: isolate coverage ownership

* perf(test): run backend projects concurrently

* perf(ci): separate test layers by runtime

* perf(test): separate integration boundaries

* perf(ci): prioritize test runners

* docs(ci): clarify package scheduling

* test: restore shared Cloudflare mocks

* fix(preview): isolate Cloudflare E2E build config

* fix(auth): bind preview sessions to request origin

* revert: remove ineffective preview auth workaround

* fix(auth): stop signing JWTs on session reads
2026-08-05 15:01:12 -04:00

1060 lines
46 KiB
TypeScript

import { mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import Database from 'better-sqlite3'
import { describe, expect, it } from 'vitest'
import { buildD1ApplySql, normalizeDatabase, runNormalizationCli } from '../../scripts/normalize-ids'
import { BASE62_PATTERN, IMAGE_TOKEN_PATTERN, SHARE_TOKEN_PATTERN } from '../../shared/ids'
function fixture(path = ':memory:'): Database.Database {
const db = new Database(path)
db.pragma('foreign_keys = OFF')
db.exec(`
CREATE TABLE user (id TEXT PRIMARY KEY);
CREATE TABLE organization (id TEXT PRIMARY KEY);
CREATE TABLE storages (id TEXT PRIMARY KEY, org_id TEXT);
CREATE TABLE matters (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
alias TEXT NOT NULL UNIQUE,
storage_id TEXT NOT NULL,
parent TEXT NOT NULL DEFAULT '',
object TEXT NOT NULL
);
CREATE TABLE shares (
id TEXT PRIMARY KEY,
token TEXT NOT NULL UNIQUE,
matter_id TEXT NOT NULL,
org_id TEXT NOT NULL,
creator_id TEXT NOT NULL
);
CREATE TABLE share_recipients (id TEXT PRIMARY KEY, share_id TEXT, recipient_user_id TEXT);
CREATE TABLE image_hostings (
id TEXT PRIMARY KEY,
token TEXT NOT NULL UNIQUE,
org_id TEXT NOT NULL,
storage_id TEXT NOT NULL,
storage_key TEXT NOT NULL
);
CREATE TABLE image_hosting_configs (org_id TEXT PRIMARY KEY, verification_token TEXT);
CREATE TABLE audit_events (
id TEXT PRIMARY KEY,
event_key TEXT UNIQUE,
org_id TEXT NOT NULL,
user_id TEXT,
target_type TEXT NOT NULL,
target_id TEXT,
actor_type TEXT,
actor_ref TEXT,
metadata TEXT
);
CREATE TABLE resource_changes (
sequence INTEGER PRIMARY KEY,
scope_type TEXT NOT NULL,
scope_id TEXT NOT NULL,
resource_type TEXT NOT NULL,
resource_id TEXT NOT NULL,
metadata TEXT
);
CREATE TABLE storage_usage_ledger (
id TEXT PRIMARY KEY,
event_key TEXT NOT NULL UNIQUE,
org_id TEXT NOT NULL,
storage_id TEXT NOT NULL,
resource_type TEXT NOT NULL,
resource_id TEXT NOT NULL
);
CREATE TABLE cloud_traffic_reports (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
source TEXT NOT NULL,
source_id TEXT NOT NULL,
event_id TEXT NOT NULL,
storage_id TEXT,
status TEXT NOT NULL
);
CREATE TABLE org_quota_entitlements (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
source TEXT NOT NULL,
source_id TEXT NOT NULL,
metadata TEXT
);
CREATE TABLE download_tasks (id TEXT PRIMARY KEY, events TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'completed');
CREATE TABLE notifications (
id TEXT PRIMARY KEY, user_id TEXT NOT NULL, ref_type TEXT, ref_id TEXT, metadata TEXT
);
CREATE TABLE apikey (id TEXT PRIMARY KEY, reference_id TEXT, key TEXT, metadata TEXT);
CREATE TABLE session (id TEXT PRIMARY KEY, user_id TEXT NOT NULL, token TEXT NOT NULL);
CREATE TABLE verification (id TEXT PRIMARY KEY);
CREATE TABLE deviceCode (
id TEXT PRIMARY KEY,
device_code TEXT NOT NULL,
user_code TEXT NOT NULL,
user_id TEXT,
client_id TEXT,
scope TEXT,
status TEXT NOT NULL,
expires_at INTEGER NOT NULL
);
CREATE TABLE oauthAccessToken (id TEXT PRIMARY KEY);
CREATE TABLE oauthRefreshToken (id TEXT PRIMARY KEY);
CREATE TABLE oauthPushedAuthorizationRequest (id TEXT PRIMARY KEY);
CREATE TABLE oauthClientAssertion (id TEXT PRIMARY KEY);
CREATE TABLE oauthClient (id TEXT PRIMARY KEY, client_id TEXT UNIQUE, user_id TEXT);
CREATE TABLE oauthResource (id TEXT PRIMARY KEY, identifier TEXT UNIQUE);
CREATE TABLE oauthClientResource (id TEXT PRIMARY KEY, client_id TEXT, resource_id TEXT);
CREATE TABLE oauthConsent (id TEXT PRIMARY KEY, client_id TEXT, user_id TEXT, authorization_details TEXT);
CREATE TABLE oauthClientRegistration (client_id TEXT PRIMARY KEY);
CREATE TABLE jwks (id TEXT PRIMARY KEY);
CREATE TABLE downloader_bootstrap_credentials (id TEXT PRIMARY KEY, user_id TEXT, token_hash TEXT);
CREATE TABLE downloaders (
id TEXT PRIMARY KEY, token_hash TEXT NOT NULL, token_jti TEXT NOT NULL UNIQUE, enabled INTEGER NOT NULL
);
CREATE TABLE object_upload_sessions (
id TEXT PRIMARY KEY, storage_key TEXT NOT NULL, created_by TEXT, status TEXT NOT NULL DEFAULT 'completed'
);
INSERT INTO user VALUES ('user-_legacy');
INSERT INTO organization VALUES ('-org_legacy-');
INSERT INTO storages VALUES ('user-_legacy', '-org_legacy-');
INSERT INTO matters VALUES (
'matter_old-', '-org_legacy-', '_alias-', 'user-_legacy', '',
'objects/-org_legacy-/matter_old-'
);
INSERT INTO shares VALUES ('share_old-', 'ds_direct-old', 'matter_old-', '-org_legacy-', 'user-_legacy');
INSERT INTO share_recipients VALUES ('recipient_old-', 'share_old-', 'user-_legacy');
INSERT INTO image_hostings VALUES (
'image_old-', 'ds_direct-old', '-org_legacy-', 'user-_legacy',
'ih/-org_legacy-/image_old-.png'
);
INSERT INTO image_hosting_configs VALUES ('-org_legacy-', 'verify_old-');
INSERT INTO audit_events VALUES (
'event:download_issued:matter_old-', NULL, '-org_legacy-', 'user-_legacy',
'file', 'matter_old-', 'user', 'user-_legacy',
'{"matterId":"matter_old-","storageId":"user-_legacy","nested":{"orgId":"-org_legacy-"},"sessionId":"upload_old-","sourceId":"matter_old-","customerId":"user-_legacy","eventId":"matter_old-"}'
);
INSERT INTO resource_changes VALUES (
1, 'organization', '-org_legacy-', 'share', 'share_old-',
'{"userId":"user-_legacy","storageId":"user-_legacy"}'
);
INSERT INTO storage_usage_ledger VALUES (
'opening:-org_legacy-:user-_legacy', 'opening:-org_legacy-:user-_legacy',
'-org_legacy-', 'user-_legacy', 'storage', 'user-_legacy'
);
INSERT INTO cloud_traffic_reports VALUES (
'traffic_row-', '-org_legacy-', 'direct_share', 'share_old-', 'traffic_share_old-', 'user-_legacy', 'reported'
);
INSERT INTO org_quota_entitlements VALUES (
'quota_row-', '-org_legacy-', 'free_plan', 'free_plan:-org_legacy-',
'{"targetOrgId":"-org_legacy-","grantedBy":"user-_legacy","cloudOrderId":"user-_legacy"}'
);
INSERT INTO download_tasks VALUES (
'task_old-', '[{"id":"initial:task_old-","type":"status_changed"}]', 'completed'
);
INSERT INTO notifications VALUES (
'notification_old-', 'user-_legacy', 'share', 'share_old-',
'{"shareId":"share_old-","token":"ds_direct-old","customerId":"user-_legacy"}'
);
INSERT INTO apikey VALUES (
'api_key_old-', 'user-_legacy', 'irreversible-hash',
'{"scope":{"mode":"workspace","orgId":"-org_legacy-"}}'
);
INSERT INTO session VALUES ('session_old-', 'user-_legacy', 'session-token');
INSERT INTO verification VALUES ('verification_old-');
INSERT INTO deviceCode VALUES (
'device_old-', 'device-code-value', 'USER-CODE', 'user-_legacy', 'static-client',
'openid offline_access', 'pending', 1999999999999
);
INSERT INTO oauthAccessToken VALUES ('access_old-');
INSERT INTO oauthRefreshToken VALUES ('refresh_old-');
INSERT INTO oauthPushedAuthorizationRequest VALUES ('par_old-');
INSERT INTO oauthClientAssertion VALUES ('assertion-old');
INSERT INTO oauthClient VALUES ('dynamic_client-', 'dynamic-client', 'user-_legacy');
INSERT INTO oauthClient VALUES ('static_client-', 'static-client', 'user-_legacy');
INSERT INTO oauthResource VALUES ('oauth_resource-', 'zpan-api');
INSERT INTO oauthClientResource VALUES ('dynamic_link-', 'dynamic-client', 'zpan-api');
INSERT INTO oauthClientResource VALUES ('static_link-', 'static-client', 'zpan-api');
INSERT INTO oauthConsent VALUES (
'consent_old-', 'static-client', 'user-_legacy',
'[{"type":"zpan_workspace","identifier":"-org_legacy-"}]'
);
INSERT INTO oauthClientRegistration VALUES ('dynamic-client');
INSERT INTO jwks VALUES ('protocol-key-id');
INSERT INTO downloader_bootstrap_credentials VALUES ('bootstrap_old-', 'user-_legacy', 'bootstrap-hash');
INSERT INTO downloaders VALUES ('downloader_old-', 'legacy-hash', 'legacy-jti', 1);
INSERT INTO downloaders VALUES ('SafeDownloader123', 'safe-legacy-hash', 'safe-legacy-jti', 1);
INSERT INTO object_upload_sessions VALUES (
'upload_old-', 'objects/-org_legacy-/matter_old-', 'downloader:downloader_old-', 'completed'
);
INSERT INTO object_upload_sessions VALUES (
'user_upload_old-', 'objects/-org_legacy-/matter-user_old-', 'user-_legacy', 'completed'
);
`)
return db
}
function value(db: Database.Database, sql: string): string {
return (db.prepare(sql).get() as { value: string }).value
}
function insertOrphanedAuditActors(db: Database.Database): void {
const insert = db.prepare(
`INSERT INTO audit_events
(id, event_key, org_id, user_id, target_type, target_id, actor_type, actor_ref, metadata)
VALUES (?, NULL, '-org_legacy-', ?, 'historical', NULL, ?, ?, NULL)`,
)
insert.run('HistoricalUserAudit', 'deleted-user_', 'user', 'deleted-user_')
insert.run('HistoricalApiKeyAudit', null, 'api_key', 'deleted-api-key_')
insert.run('HistoricalDownloaderAudit', null, 'downloader', 'deleted-downloader_')
insert.run('HistoricalTaskAudit', null, 'task-upload', 'deleted-task_')
db.exec(`
UPDATE audit_events
SET metadata = '{"matterId":"deleted-matter_"}'
WHERE id = 'HistoricalUserAudit';
INSERT INTO resource_changes
VALUES (2, 'organization', '-org_legacy-', 'matter', 'deleted-matter_', NULL);
`)
}
describe('ID normalization backfill', () => {
it('dry-runs without changing the source database', () => {
const db = fixture()
const summary = normalizeDatabase(db, false)
expect(summary.apply).toBe(false)
expect(summary.mappings.user).toBe(1)
expect(summary.invalidated.session).toBe(1)
expect(value(db, 'SELECT id AS value FROM user')).toBe('user-_legacy')
expect(db.prepare("SELECT 1 FROM sqlite_master WHERE name = '_zpan_id_normalization_map'").get()).toBeUndefined()
db.close()
})
it('atomically maps IDs and references, preserves API keys and device codes, and invalidates other credentials', () => {
const db = fixture()
const summary = normalizeDatabase(db, true)
expect(summary.jsonDocumentsUpdated).toBeGreaterThan(0)
const userId = value(db, 'SELECT id AS value FROM user')
const orgId = value(db, 'SELECT id AS value FROM organization')
const storageId = value(db, 'SELECT id AS value FROM storages')
const matterId = value(db, 'SELECT id AS value FROM matters')
const shareId = value(db, 'SELECT id AS value FROM shares')
const shareToken = value(db, 'SELECT token AS value FROM shares')
const imageToken = value(db, 'SELECT token AS value FROM image_hostings')
const taskId = value(db, 'SELECT id AS value FROM download_tasks')
const apiKeyId = value(db, 'SELECT id AS value FROM apikey')
const deviceCodeId = value(db, 'SELECT id AS value FROM deviceCode')
const downloaderId = value(
db,
"SELECT substr(created_by, 12) AS value FROM object_upload_sessions WHERE created_by LIKE 'downloader:%'",
)
const uploadSessionId = value(
db,
"SELECT id AS value FROM object_upload_sessions WHERE created_by LIKE 'downloader:%'",
)
for (const governedValue of [
userId,
orgId,
storageId,
matterId,
shareId,
shareToken,
imageToken,
taskId,
apiKeyId,
deviceCodeId,
downloaderId,
]) {
expect(governedValue).toMatch(BASE62_PATTERN)
}
expect(shareToken).toMatch(SHARE_TOKEN_PATTERN)
expect(imageToken).toMatch(IMAGE_TOKEN_PATTERN)
expect(shareToken).toHaveLength(12)
expect(imageToken).toHaveLength(12)
expect(shareToken).not.toBe(imageToken)
expect(value(db, 'SELECT org_id AS value FROM matters')).toBe(orgId)
expect(value(db, 'SELECT storage_id AS value FROM matters')).toBe(storageId)
expect(value(db, 'SELECT matter_id AS value FROM shares')).toBe(matterId)
expect(value(db, 'SELECT creator_id AS value FROM shares')).toBe(userId)
expect(value(db, 'SELECT share_id AS value FROM share_recipients')).toBe(shareId)
expect(value(db, 'SELECT target_id AS value FROM audit_events')).toBe(matterId)
expect(value(db, 'SELECT actor_ref AS value FROM audit_events')).toBe(userId)
expect(value(db, 'SELECT scope_id AS value FROM resource_changes')).toBe(orgId)
expect(value(db, 'SELECT resource_id AS value FROM resource_changes')).toBe(shareId)
expect(value(db, 'SELECT resource_id AS value FROM storage_usage_ledger')).toBe(storageId)
expect(value(db, 'SELECT source_id AS value FROM cloud_traffic_reports')).toBe(shareId)
expect(value(db, 'SELECT event_id AS value FROM cloud_traffic_reports')).toBe('traffic_share_old-')
expect(value(db, 'SELECT source_id AS value FROM org_quota_entitlements')).toBe(`free_plan:${orgId}`)
expect(value(db, 'SELECT ref_id AS value FROM notifications')).toBe(shareId)
expect(value(db, 'SELECT created_by AS value FROM object_upload_sessions')).toBe(`downloader:${downloaderId}`)
expect(
value(db, "SELECT created_by AS value FROM object_upload_sessions WHERE created_by NOT LIKE 'downloader:%'"),
).toBe(userId)
expect(summary.credentialsInvalidated.downloaders).toBe(2)
expect(
(
db.prepare('SELECT COUNT(*) AS count FROM downloaders WHERE enabled = 0 AND token_hash = ?').get('') as {
count: number
}
).count,
).toBe(2)
expect(
(
db.prepare("SELECT COUNT(*) AS count FROM downloaders WHERE token_jti GLOB '*[^A-Za-z0-9]*'").get() as {
count: number
}
).count,
).toBe(0)
const metadata = JSON.parse(value(db, 'SELECT metadata AS value FROM audit_events')) as Record<string, unknown>
expect(metadata).toMatchObject({ matterId, storageId, nested: { orgId } })
expect(metadata).toMatchObject({ sessionId: uploadSessionId, sourceId: matterId })
expect(metadata.customerId).toBe('user-_legacy')
expect(metadata.eventId).toBe('matter_old-')
expect(JSON.parse(value(db, 'SELECT metadata AS value FROM resource_changes'))).toEqual({ userId, storageId })
expect(JSON.parse(value(db, 'SELECT events AS value FROM download_tasks'))).toMatchObject([
{ id: `initial:${taskId}` },
])
expect(JSON.parse(value(db, 'SELECT metadata AS value FROM notifications'))).toMatchObject({
shareId,
token: shareToken,
customerId: 'user-_legacy',
})
expect(JSON.parse(value(db, 'SELECT metadata AS value FROM org_quota_entitlements'))).toMatchObject({
targetOrgId: orgId,
grantedBy: userId,
cloudOrderId: 'user-_legacy',
})
expect(value(db, 'SELECT event_key AS value FROM audit_events')).toContain(matterId)
expect(value(db, 'SELECT event_key AS value FROM storage_usage_ledger')).toBe(`opening:${orgId}:${storageId}`)
expect(value(db, 'SELECT id AS value FROM audit_events')).toMatch(BASE62_PATTERN)
expect(db.prepare('SELECT reference_id, key, metadata FROM apikey').get()).toEqual({
reference_id: userId,
key: 'irreversible-hash',
metadata: JSON.stringify({ scope: { mode: 'workspace', orgId } }),
})
expect(
db.prepare('SELECT device_code, user_code, user_id, client_id, scope, status, expires_at FROM deviceCode').get(),
).toEqual({
device_code: 'device-code-value',
user_code: 'USER-CODE',
user_id: userId,
client_id: 'static-client',
scope: 'openid offline_access',
status: 'pending',
expires_at: 1999999999999,
})
expect(summary.invalidated.apikey).toBeUndefined()
expect(summary.invalidated.deviceCode).toBeUndefined()
// Physical object keys are external storage references and intentionally do not change.
expect(value(db, 'SELECT object AS value FROM matters')).toBe('objects/-org_legacy-/matter_old-')
expect(value(db, 'SELECT storage_key AS value FROM image_hostings')).toBe('ih/-org_legacy-/image_old-.png')
expect(db.prepare('SELECT storage_key FROM object_upload_sessions ORDER BY storage_key').all()).toEqual([
{ storage_key: 'objects/-org_legacy-/matter-user_old-' },
{ storage_key: 'objects/-org_legacy-/matter_old-' },
])
for (const table of [
'session',
'verification',
'downloader_bootstrap_credentials',
'oauthAccessToken',
'oauthRefreshToken',
'oauthPushedAuthorizationRequest',
'oauthClientAssertion',
'oauthConsent',
'oauthClientRegistration',
'jwks',
]) {
expect((db.prepare(`SELECT COUNT(*) AS count FROM "${table}"`).get() as { count: number }).count).toBe(0)
}
expect(value(db, "SELECT client_id AS value FROM oauthClient WHERE client_id = 'static-client'")).toBe(
'static-client',
)
expect(db.prepare("SELECT 1 FROM oauthClient WHERE client_id = 'dynamic-client'").get()).toBeUndefined()
expect(db.prepare("SELECT 1 FROM oauthClientResource WHERE client_id = 'dynamic-client'").get()).toBeUndefined()
expect(db.prepare("SELECT 1 FROM oauthClientResource WHERE client_id = 'static-client'").get()).toBeTruthy()
expect(db.prepare('PRAGMA foreign_key_check').all()).toEqual([])
db.close()
})
it('normalizes orphaned historical audit actors without requiring deleted live rows', () => {
const planningCopy = fixture()
insertOrphanedAuditActors(planningCopy)
normalizeDatabase(planningCopy, true)
const historicalUserId = value(
planningCopy,
"SELECT user_id AS value FROM audit_events WHERE id = 'HistoricalUserAudit'",
)
expect(historicalUserId).toMatch(BASE62_PATTERN)
expect(value(planningCopy, "SELECT actor_ref AS value FROM audit_events WHERE id = 'HistoricalUserAudit'")).toBe(
historicalUserId,
)
for (const id of ['HistoricalApiKeyAudit', 'HistoricalDownloaderAudit', 'HistoricalTaskAudit']) {
expect(value(planningCopy, `SELECT actor_ref AS value FROM audit_events WHERE id = '${id}'`)).toMatch(
BASE62_PATTERN,
)
}
const historicalMatterId = (
JSON.parse(
value(planningCopy, "SELECT metadata AS value FROM audit_events WHERE id = 'HistoricalUserAudit'"),
) as { matterId: string }
).matterId
expect(historicalMatterId).toMatch(BASE62_PATTERN)
expect(value(planningCopy, 'SELECT resource_id AS value FROM resource_changes WHERE sequence = 2')).toBe(
historicalMatterId,
)
const plan = buildD1ApplySql(planningCopy)
const d1Copy = fixture()
insertOrphanedAuditActors(d1Copy)
d1Copy.exec(plan)
expect(
d1Copy
.prepare("SELECT id, user_id, actor_ref, metadata FROM audit_events WHERE id LIKE 'Historical%' ORDER BY id")
.all(),
).toEqual(
planningCopy
.prepare("SELECT id, user_id, actor_ref, metadata FROM audit_events WHERE id LIKE 'Historical%' ORDER BY id")
.all(),
)
expect(value(d1Copy, 'SELECT resource_id AS value FROM resource_changes WHERE sequence = 2')).toBe(
historicalMatterId,
)
expect(d1Copy.prepare('PRAGMA foreign_key_check').all()).toEqual([])
planningCopy.close()
d1Copy.close()
})
it('resolves legacy API-key task creators while preserving their normalized API keys', () => {
const addLegacyCreators = (db: Database.Database) => {
db.exec(`
ALTER TABLE download_tasks ADD COLUMN created_by_user_id TEXT;
UPDATE download_tasks SET created_by_user_id = 'api-key:api_key_old-';
INSERT INTO user VALUES ('legacy-api-user_');
INSERT INTO apikey VALUES ('orphan_key-', 'legacy-api-user_', 'orphan-hash', NULL);
INSERT INTO download_tasks (id, events, status, created_by_user_id)
VALUES ('OrphanCreatorTask', '[]', 'completed', 'api-key:orphan_key-');
`)
}
const planningCopy = fixture()
addLegacyCreators(planningCopy)
normalizeDatabase(planningCopy, true)
expect(
value(planningCopy, "SELECT created_by_user_id AS value FROM download_tasks WHERE id != 'OrphanCreatorTask'"),
).toBe(value(planningCopy, 'SELECT id AS value FROM user'))
expect(
value(planningCopy, "SELECT created_by_user_id AS value FROM download_tasks WHERE id = 'OrphanCreatorTask'"),
).toMatch(BASE62_PATTERN)
expect(value(planningCopy, "SELECT reference_id AS value FROM apikey WHERE key = 'orphan-hash'")).toBe(
value(planningCopy, "SELECT created_by_user_id AS value FROM download_tasks WHERE id = 'OrphanCreatorTask'"),
)
const plan = buildD1ApplySql(planningCopy)
const d1Copy = fixture()
addLegacyCreators(d1Copy)
d1Copy.exec(plan)
expect(d1Copy.prepare('SELECT id, created_by_user_id FROM download_tasks ORDER BY id').all()).toEqual(
planningCopy.prepare('SELECT id, created_by_user_id FROM download_tasks ORDER BY id').all(),
)
planningCopy.close()
d1Copy.close()
})
it('is idempotent after a completed apply', () => {
const db = fixture()
normalizeDatabase(db, true)
const firstUserId = value(db, 'SELECT id AS value FROM user')
const second = normalizeDatabase(db, true)
expect(Object.values(second.mappings).reduce((sum, count) => sum + count, 0)).toBe(0)
expect(value(db, 'SELECT id AS value FROM user')).toBe(firstUserId)
db.close()
})
it('treats a completed rerun as validation-only and preserves newly issued credentials', () => {
const db = fixture()
normalizeDatabase(db, true)
const matterId = value(db, 'SELECT id AS value FROM matters')
const orgId = value(db, 'SELECT id AS value FROM organization')
const userId = value(db, 'SELECT id AS value FROM user')
db.prepare('INSERT INTO shares VALUES (?, ?, ?, ?, ?)').run('NewShare123', 'sNewToken123', matterId, orgId, userId)
db.prepare('INSERT INTO session VALUES (?, ?, ?)').run('NewSession123', userId, 'NewSessionToken123')
const summary = normalizeDatabase(db, true)
expect(value(db, "SELECT token AS value FROM shares WHERE id = 'NewShare123'")).toBe('sNewToken123')
expect(value(db, "SELECT token AS value FROM session WHERE id = 'NewSession123'")).toBe('NewSessionToken123')
expect(summary.invalidated).toEqual({})
db.close()
})
it('accepts an active Cloud binding when every organization ID is already normalized', () => {
const db = fixture()
normalizeDatabase(db, true)
db.exec(`
CREATE TABLE license_bindings (id TEXT PRIMARY KEY, instance_id TEXT NOT NULL, status TEXT NOT NULL);
INSERT INTO license_bindings VALUES ('Binding123', 'SafeInstance123', 'active');
`)
expect(() => normalizeDatabase(db, true)).not.toThrow()
db.close()
})
it('rewrites typed ledger keys while preserving unknown polymorphic protocol values', () => {
const db = fixture()
db.exec(`
INSERT INTO storage_usage_ledger VALUES (
'matter_ledger-', 'matter:matter_old-', '-org_legacy-', 'user-_legacy', 'matter', 'matter_old-'
);
INSERT INTO storage_usage_ledger VALUES (
'image_ledger-', 'image:image_old-', '-org_legacy-', 'user-_legacy', 'image_hosting', 'image_old-'
);
INSERT INTO resource_changes VALUES (
2, 'protocol', 'urn:scope-with-punctuation', 'protocol', 'urn:resource-with-punctuation', '{}'
);
`)
normalizeDatabase(db, true)
const matterId = value(db, 'SELECT id AS value FROM matters')
const imageId = value(db, 'SELECT id AS value FROM image_hostings')
expect(value(db, "SELECT event_key AS value FROM storage_usage_ledger WHERE resource_type = 'matter'")).toBe(
`matter:${matterId}`,
)
expect(value(db, "SELECT event_key AS value FROM storage_usage_ledger WHERE resource_type = 'image_hosting'")).toBe(
`image:${imageId}`,
)
expect(value(db, 'SELECT scope_id AS value FROM resource_changes WHERE sequence = 2')).toBe(
'urn:scope-with-punctuation',
)
expect(value(db, 'SELECT resource_id AS value FROM resource_changes WHERE sequence = 2')).toBe(
'urn:resource-with-punctuation',
)
db.close()
})
it('rejects an empty required logical reference after completion', () => {
const db = fixture()
normalizeDatabase(db, true)
db.exec("UPDATE shares SET matter_id = ''")
expect(() => normalizeDatabase(db, true)).toThrow('invalid_value_remaining:shares.matter_id:1')
db.close()
})
it('rejects a stale typed JSON reference after completion without mutating it', () => {
const db = fixture()
normalizeDatabase(db, true)
db.exec(`UPDATE notifications SET metadata = '{"token":"ds_direct-old"}'`)
expect(() => normalizeDatabase(db, true)).toThrow('stale_embedded_reference:polymorphic=0,json=1,structured=0')
expect(value(db, 'SELECT metadata AS value FROM notifications')).toBe('{"token":"ds_direct-old"}')
db.close()
})
it('rejects a Base62 direct reference whose target does not exist', () => {
const db = fixture()
normalizeDatabase(db, true)
db.exec("UPDATE shares SET matter_id = 'MissingMatter123'")
expect(() => normalizeDatabase(db, true)).toThrow('dangling_reference:shares.matter_id->matters.id:1')
db.close()
})
it('rejects a Base62 typed JSON reference whose target does not exist', () => {
const db = fixture()
normalizeDatabase(db, true)
db.exec(`UPDATE notifications SET metadata = '{"shareId":"MissingShare123"}'`)
expect(() => normalizeDatabase(db, true)).toThrow('dangling_json_reference:notifications.metadata:1')
db.close()
})
it.each([
{
name: 'a newly persisted invalid API-key row ID',
mutate: (db: Database.Database) => db.exec("INSERT INTO apikey VALUES ('invalid-api-key', NULL, 'hash', NULL)"),
error: 'invalid_value_remaining:apikey.id:1',
},
{
name: 'an invalid known polymorphic reference',
mutate: (db: Database.Database) => db.exec("UPDATE notifications SET ref_id = 'invalid-share'"),
error: 'invalid_polymorphic_reference:notifications.ref_id:1',
},
{
name: 'an invalid structured upload-session creator',
mutate: (db: Database.Database) =>
db.exec(
"UPDATE object_upload_sessions SET created_by = 'downloader:invalid-id' WHERE id = (SELECT id FROM object_upload_sessions LIMIT 1)",
),
error: 'invalid_structured_reference:object_upload_sessions.created_by:1',
},
{
name: 'a share token with the image namespace prefix',
mutate: (db: Database.Database) => db.exec("UPDATE shares SET token = 'i00000000000'"),
error: 'invalid_value_remaining:shares.token:1',
},
{
name: 'an image token with the share namespace prefix',
mutate: (db: Database.Database) => db.exec("UPDATE image_hostings SET token = 's00000000000'"),
error: 'invalid_value_remaining:image_hostings.token:1',
},
])('rejects $name after completion', ({ mutate, error }) => {
const db = fixture()
normalizeDatabase(db, true)
mutate(db)
expect(() => normalizeDatabase(db, true)).toThrow(error)
db.close()
})
it('replays the exact mapping as a D1-compatible SQL plan', () => {
const planningCopy = fixture()
normalizeDatabase(planningCopy, true)
const expectedUserId = value(planningCopy, 'SELECT id AS value FROM user')
const expectedShareToken = value(planningCopy, 'SELECT token AS value FROM shares')
const plan = buildD1ApplySql(planningCopy)
expect(plan).not.toContain('WHERE rowid')
expect(plan).not.toContain('DELETE FROM "apikey"')
expect(plan).not.toContain('DELETE FROM "deviceCode"')
expect(plan).toContain('substr("token", 1, 1) != \'s\'')
expect(plan).toContain('substr("token", 1, 1) != \'i\'')
const d1Copy = fixture()
d1Copy.exec(plan)
expect(value(d1Copy, 'SELECT id AS value FROM user')).toBe(expectedUserId)
expect(value(d1Copy, 'SELECT token AS value FROM shares')).toBe(expectedShareToken)
expect(value(d1Copy, 'SELECT matter_id AS value FROM shares')).toBe(
value(planningCopy, 'SELECT matter_id AS value FROM shares'),
)
expect(JSON.parse(value(d1Copy, 'SELECT metadata AS value FROM audit_events'))).toEqual(
JSON.parse(value(planningCopy, 'SELECT metadata AS value FROM audit_events')),
)
expect(value(d1Copy, 'SELECT target_id AS value FROM audit_events')).toBe(
value(planningCopy, 'SELECT target_id AS value FROM audit_events'),
)
expect(value(d1Copy, 'SELECT resource_id AS value FROM resource_changes')).toBe(
value(planningCopy, 'SELECT resource_id AS value FROM resource_changes'),
)
expect(value(d1Copy, 'SELECT event_key AS value FROM storage_usage_ledger')).toBe(
value(planningCopy, 'SELECT event_key AS value FROM storage_usage_ledger'),
)
expect(value(d1Copy, 'SELECT source_id AS value FROM cloud_traffic_reports')).toBe(
value(planningCopy, 'SELECT source_id AS value FROM cloud_traffic_reports'),
)
expect(value(d1Copy, 'SELECT ref_id AS value FROM notifications')).toBe(
value(planningCopy, 'SELECT ref_id AS value FROM notifications'),
)
expect(value(d1Copy, 'SELECT events AS value FROM download_tasks')).toBe(
value(planningCopy, 'SELECT events AS value FROM download_tasks'),
)
expect(d1Copy.prepare('SELECT id, reference_id, key, metadata FROM apikey').get()).toEqual(
planningCopy.prepare('SELECT id, reference_id, key, metadata FROM apikey').get(),
)
expect(
d1Copy
.prepare('SELECT id, device_code, user_code, user_id, client_id, scope, status, expires_at FROM deviceCode')
.get(),
).toEqual(
planningCopy
.prepare('SELECT id, device_code, user_code, user_id, client_id, scope, status, expires_at FROM deviceCode')
.get(),
)
expect((d1Copy.prepare('SELECT COUNT(*) AS count FROM session').get() as { count: number }).count).toBe(0)
expect(d1Copy.prepare("SELECT 1 FROM oauthClient WHERE client_id = 'dynamic-client'").get()).toBeUndefined()
expect(d1Copy.prepare("SELECT 1 FROM oauthClient WHERE client_id = 'static-client'").get()).toBeTruthy()
expect(
(
d1Copy.prepare("SELECT COUNT(*) AS count FROM downloaders WHERE enabled = 0 AND token_hash = ''").get() as {
count: number
}
).count,
).toBe(2)
expect(
value(d1Copy, "SELECT created_by AS value FROM object_upload_sessions WHERE created_by NOT LIKE 'downloader:%'"),
).toBe(value(planningCopy, 'SELECT id AS value FROM user'))
expect(d1Copy.prepare('PRAGMA foreign_key_check').all()).toEqual([])
const migratedUserId = value(d1Copy, 'SELECT id AS value FROM user')
d1Copy.prepare('INSERT INTO session VALUES (?, ?, ?)').run('NewSession123', migratedUserId, 'NewToken123')
d1Copy.exec(`UPDATE notifications SET metadata = '{"fresh":true}'`)
d1Copy.exec(plan)
expect(value(d1Copy, "SELECT token AS value FROM session WHERE id = 'NewSession123'")).toBe('NewToken123')
expect(value(d1Copy, 'SELECT metadata AS value FROM notifications')).toBe('{"fresh":true}')
planningCopy.close()
d1Copy.close()
})
it('chunks large reviewed mappings below the D1 statement limit', () => {
const db = fixture()
const insert = db.prepare('INSERT INTO user (id) VALUES (?)')
db.transaction(() => {
for (let index = 0; index < 2_400; index += 1) insert.run(`legacy-user-${index}`)
})()
normalizeDatabase(db, true)
const plan = buildD1ApplySql(db)
expect(plan.match(/INSERT INTO _zpan_id_normalization_map/g)?.length).toBeGreaterThan(1)
expect(plan.match(/reviewed-mapping:\d+/g)?.length).toBeGreaterThan(1)
db.close()
})
it('fails before emitting a D1 plan with an oversized rewritten JSON literal', () => {
const db = fixture()
db.prepare('UPDATE notifications SET metadata = ?').run(
JSON.stringify({ shareId: 'share_old-', padding: 'x'.repeat(100_000) }),
)
normalizeDatabase(db, true)
expect(() => buildD1ApplySql(db)).toThrow('d1_plan_literal_too_large')
db.close()
})
it('batches exact JSON rewrites below the D1 command limit', () => {
const db = fixture()
const source = fixture()
for (const target of [db, source]) {
const insert = target.prepare(
"INSERT INTO notifications (id, user_id, ref_type, ref_id, metadata) VALUES (?, 'user-_legacy', 'share', 'share_old-', ?)",
)
target.transaction(() => {
for (let index = 0; index < 600; index += 1) {
insert.run(`SafeNotification${index}`, JSON.stringify({ shareId: 'share_old-' }))
}
})()
}
normalizeDatabase(db, true)
const plan = buildD1ApplySql(db)
expect(plan.match(/;\n/g)?.length).toBeLessThanOrEqual(1_000)
source.exec(plan)
expect((source.prepare('SELECT COUNT(*) AS count FROM notifications').get() as { count: number }).count).toBe(601)
expect(source.prepare('PRAGMA foreign_key_check').all()).toEqual([])
source.close()
db.close()
})
it('recovers by replaying the full D1 plan after an interrupted prefix', () => {
const planningCopy = fixture()
normalizeDatabase(planningCopy, true)
const plan = buildD1ApplySql(planningCopy)
const statements = plan
.replace(/^--.*$/gm, '')
.split(';')
.map((statement) => statement.trim())
.filter(Boolean)
const d1Copy = fixture()
d1Copy.exec(`${statements.slice(0, Math.floor(statements.length / 2)).join(';\n')};`)
d1Copy.exec(plan)
expect(value(d1Copy, 'SELECT id AS value FROM user')).toBe(value(planningCopy, 'SELECT id AS value FROM user'))
expect(value(d1Copy, 'SELECT token AS value FROM shares')).toBe(
value(planningCopy, 'SELECT token AS value FROM shares'),
)
expect(d1Copy.prepare('PRAGMA foreign_key_check').all()).toEqual([])
planningCopy.close()
d1Copy.close()
})
it('does not write the D1 completion marker when a mapped update silently misses its source row', () => {
const planningCopy = fixture()
normalizeDatabase(planningCopy, true)
const plan = buildD1ApplySql(planningCopy)
const d1Copy = fixture()
d1Copy.exec("UPDATE shares SET matter_id = 'UnrelatedBase62Matter'")
expect(() => d1Copy.exec(plan)).toThrow()
const stateExists = Boolean(
d1Copy
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = '_zpan_id_normalization_state'")
.get(),
)
expect(
stateExists
? d1Copy.prepare("SELECT 1 FROM _zpan_id_normalization_state WHERE key = 'completed_at'").get()
: undefined,
).toBeUndefined()
planningCopy.close()
d1Copy.close()
})
it('refuses to bless a pre-existing unversioned D1 completion marker', () => {
const planningCopy = fixture()
normalizeDatabase(planningCopy, true)
const plan = buildD1ApplySql(planningCopy)
const d1Copy = fixture()
d1Copy.exec(`
CREATE TABLE _zpan_id_normalization_state (key TEXT PRIMARY KEY, value TEXT NOT NULL);
INSERT INTO _zpan_id_normalization_state VALUES ('completed_at', 'unverified');
`)
expect(() => d1Copy.exec(plan)).toThrow()
expect(
d1Copy.prepare("SELECT 1 FROM _zpan_id_normalization_state WHERE key = 'validation_version'").get(),
).toBeUndefined()
planningCopy.close()
d1Copy.close()
})
it('rejects a conflicting pre-existing D1 mapping instead of using an unreviewed target', () => {
const planningCopy = fixture()
normalizeDatabase(planningCopy, true)
const plan = buildD1ApplySql(planningCopy)
const d1Copy = fixture()
d1Copy.exec(`
CREATE TABLE _zpan_id_normalization_map (
kind TEXT NOT NULL, old_value TEXT NOT NULL, new_value TEXT NOT NULL, created_at INTEGER NOT NULL,
PRIMARY KEY (kind, old_value), UNIQUE (kind, new_value)
);
INSERT INTO _zpan_id_normalization_map VALUES ('share_token', 'ds_direct-old', 'sDifferent01', 0);
`)
expect(() => d1Copy.exec(plan)).toThrow()
expect(
d1Copy.prepare("SELECT 1 FROM _zpan_id_normalization_state WHERE key = 'completed_at'").get(),
).toBeUndefined()
planningCopy.close()
d1Copy.close()
})
it('rejects a missed rotation of an already-Base62 public token', () => {
const planningCopy = fixture()
planningCopy.exec(`
UPDATE shares SET token = 'AlreadyBase62Token';
UPDATE notifications SET metadata = '{"shareId":"share_old-","token":"AlreadyBase62Token","customerId":"user-_legacy"}';
`)
normalizeDatabase(planningCopy, true)
const plan = buildD1ApplySql(planningCopy)
const d1Copy = fixture()
d1Copy.exec("UPDATE shares SET token = 'DriftedBase62Token'")
expect(() => d1Copy.exec(plan)).toThrow()
expect(
d1Copy.prepare("SELECT 1 FROM _zpan_id_normalization_state WHERE key = 'completed_at'").get(),
).toBeUndefined()
planningCopy.close()
d1Copy.close()
})
it('rejects a dangling typed JSON reference that drifted after the reviewed export', () => {
const prepareStableReference = (db: Database.Database) => {
db.exec(`
INSERT INTO shares VALUES (
'SafeShare123', 'SafeToken123', 'matter_old-', '-org_legacy-', 'user-_legacy'
);
INSERT INTO notifications VALUES (
'SafeNotification123', 'user-_legacy', NULL, NULL, '{"shareId":"SafeShare123"}'
);
`)
}
const planningCopy = fixture()
prepareStableReference(planningCopy)
normalizeDatabase(planningCopy, true)
const plan = buildD1ApplySql(planningCopy)
const d1Copy = fixture()
prepareStableReference(d1Copy)
d1Copy.exec(`UPDATE notifications SET metadata = '{"shareId":"MissingShare123"}' WHERE id = 'SafeNotification123'`)
expect(() => d1Copy.exec(plan)).toThrow()
expect(
d1Copy.prepare("SELECT 1 FROM _zpan_id_normalization_state WHERE key = 'completed_at'").get(),
).toBeUndefined()
planningCopy.close()
d1Copy.close()
})
it('rejects a same-count downloader replacement that escaped planned credential revocation', () => {
const planningCopy = fixture()
normalizeDatabase(planningCopy, true)
const plan = buildD1ApplySql(planningCopy)
const d1Copy = fixture()
d1Copy.exec(`
DELETE FROM downloaders WHERE id = 'SafeDownloader123';
INSERT INTO downloaders VALUES ('ReplacementDownloader123', '', 'ReplacementJti123', 0);
`)
expect(() => d1Copy.exec(plan)).toThrow()
expect(
d1Copy.prepare("SELECT 1 FROM _zpan_id_normalization_state WHERE key = 'completed_at'").get(),
).toBeUndefined()
planningCopy.close()
d1Copy.close()
})
it('rolls back every mutation after an interruption', () => {
const db = fixture()
db.exec(`
CREATE TRIGGER stop_normalization BEFORE UPDATE OF id ON matters
BEGIN SELECT RAISE(ABORT, 'simulated interruption'); END;
`)
expect(() => normalizeDatabase(db, true)).toThrow('simulated interruption')
expect(value(db, 'SELECT id AS value FROM user')).toBe('user-_legacy')
expect(value(db, 'SELECT id AS value FROM matters')).toBe('matter_old-')
expect(db.prepare("SELECT 1 FROM sqlite_master WHERE name = '_zpan_id_normalization_map'").get()).toBeUndefined()
db.close()
})
it('requires the generated audit event-key schema before mapping legacy audit IDs', () => {
const db = new Database(':memory:')
db.exec("CREATE TABLE audit_events (id TEXT PRIMARY KEY); INSERT INTO audit_events VALUES ('legacy-event-')")
expect(() => normalizeDatabase(db, true)).toThrow('schema_migration_required:0092_audit_event_key')
expect(db.prepare("SELECT 1 FROM sqlite_master WHERE name = '_zpan_id_normalization_map'").get()).toBeUndefined()
expect(value(db, 'SELECT id AS value FROM audit_events')).toBe('legacy-event-')
db.close()
})
it.each([
{
name: 'archive queue work',
prepare: (db: Database.Database) =>
db.exec(
"CREATE TABLE background_jobs (id TEXT PRIMARY KEY, status TEXT); INSERT INTO background_jobs VALUES ('Job123', 'queued')",
),
error: 'maintenance_state_not_drained:archive_jobs=1,object_upload_sessions=0,download_tasks=0',
},
{
name: 'an active multipart upload',
prepare: (db: Database.Database) =>
db.exec("UPDATE object_upload_sessions SET status = 'active' WHERE id = 'upload_old-'"),
error: 'maintenance_state_not_drained:archive_jobs=0,object_upload_sessions=1,download_tasks=0',
},
{
name: 'an active task-upload authorization',
prepare: (db: Database.Database) => db.exec("UPDATE download_tasks SET status = 'assigned'"),
error: 'maintenance_state_not_drained:archive_jobs=0,object_upload_sessions=0,download_tasks=1',
},
])('fails before mutation until $name is drained', ({ prepare, error }) => {
const db = fixture()
prepare(db)
expect(() => normalizeDatabase(db, true)).toThrow(error)
expect(value(db, 'SELECT id AS value FROM user')).toBe('user-_legacy')
expect(db.prepare("SELECT 1 FROM sqlite_master WHERE name = '_zpan_id_normalization_map'").get()).toBeUndefined()
db.close()
})
it('fails before mutation when an ID-derived avatar object needs external cleanup', () => {
const db = fixture()
db.exec("ALTER TABLE user ADD COLUMN image TEXT; UPDATE user SET image = '/api/avatar-blobs/user/user-_legacy?v=1'")
expect(() => normalizeDatabase(db, true)).toThrow('external_identity_image_reconciliation_required:1')
expect(value(db, 'SELECT id AS value FROM user')).toBe('user-_legacy')
db.close()
})
it('fails before mutation when an externally bound instance ID needs reconciliation', () => {
const db = fixture()
db.exec(`
CREATE TABLE system_options (key TEXT PRIMARY KEY, value TEXT NOT NULL);
CREATE TABLE license_bindings (id TEXT PRIMARY KEY, instance_id TEXT NOT NULL, status TEXT NOT NULL);
INSERT INTO system_options VALUES ('instance_id', 'instance_legacy');
INSERT INTO license_bindings VALUES ('binding1', 'instance_legacy', 'active');
`)
expect(() => normalizeDatabase(db, true)).toThrow('external_instance_id_reconciliation_required:2')
expect(value(db, 'SELECT id AS value FROM user')).toBe('user-_legacy')
expect(db.prepare("SELECT 1 FROM sqlite_master WHERE name = '_zpan_id_normalization_map'").get()).toBeUndefined()
db.close()
})
it('fails before mutation when an active Cloud binding owns an organization that would be remapped', () => {
const db = fixture()
db.exec(`
CREATE TABLE license_bindings (id TEXT PRIMARY KEY, instance_id TEXT NOT NULL, status TEXT NOT NULL);
INSERT INTO license_bindings VALUES ('binding1', 'SafeInstance123', 'active');
`)
expect(() => normalizeDatabase(db, true)).toThrow('external_organization_id_reconciliation_required:1')
expect(value(db, 'SELECT id AS value FROM organization')).toBe('-org_legacy-')
expect(db.prepare("SELECT 1 FROM sqlite_master WHERE name = '_zpan_id_normalization_map'").get()).toBeUndefined()
db.close()
})
it('fails before mutation when an unsynced external usage event would change identity', () => {
const db = fixture()
db.exec("UPDATE cloud_traffic_reports SET status = 'pending'")
expect(() => normalizeDatabase(db, true)).toThrow('external_usage_reconciliation_required:2')
expect(value(db, 'SELECT id AS value FROM user')).toBe('user-_legacy')
expect(db.prepare("SELECT 1 FROM sqlite_master WHERE name = '_zpan_id_normalization_map'").get()).toBeUndefined()
db.close()
})
it('fails before mutation for unresolved remote-download and x402 references', () => {
const db = fixture()
db.exec(`
CREATE TABLE remote_download_usage_reports (
id TEXT PRIMARY KEY, org_id TEXT, downloader_id TEXT, task_id TEXT, status TEXT
);
CREATE TABLE x402_capacity_purchase_intents (id TEXT PRIMARY KEY, org_id TEXT, status TEXT);
INSERT INTO remote_download_usage_reports VALUES (
'usage_old-', '-org_legacy-', 'downloader_old-', 'task_old-', 'pending'
);
INSERT INTO x402_capacity_purchase_intents VALUES ('purchase_old-', '-org_legacy-', 'pending');
`)
expect(() => normalizeDatabase(db, true)).toThrow('external_usage_reconciliation_required:2')
expect(value(db, 'SELECT id AS value FROM organization')).toBe('-org_legacy-')
db.close()
})
it('fails atomically when a governed JSON document is malformed', () => {
const db = fixture()
db.exec("UPDATE audit_events SET metadata = '{broken'")
expect(() => normalizeDatabase(db, true)).toThrow('invalid_json:audit_events.metadata')
expect(value(db, 'SELECT id AS value FROM user')).toBe('user-_legacy')
db.close()
})
it('handles an empty database', () => {
const db = new Database(':memory:')
db.exec('CREATE TABLE _cf_METADATA (key TEXT PRIMARY KEY, value TEXT)')
expect(normalizeDatabase(db, true)).toMatchObject({ apply: true, rowCountsVerified: 0 })
expect(db.prepare("SELECT value FROM _zpan_id_normalization_state WHERE key = 'completed_at'").get()).toBeTruthy()
db.close()
})
it('runs the CLI dry-run and protected apply workflow', async () => {
const directory = mkdtempSync(join(tmpdir(), 'zpan-normalize-cli-'))
const databasePath = join(directory, 'legacy.db')
const backupPath = join(directory, 'backup.db')
const planPath = join(directory, 'apply.sql')
fixture(databasePath).close()
const run = async (...args: string[]) => {
let stdout = ''
await runNormalizationCli(args, (value) => {
stdout += value
})
return stdout
}
try {
expect(JSON.parse(await run('--sqlite', databasePath))).toMatchObject({ apply: false })
expect(
JSON.parse(await run('--sqlite', databasePath, '--apply', '--backup', backupPath, '--emit-d1-sql', planPath)),
).toMatchObject({ apply: true })
expect(statSync(backupPath).mode & 0o777).toBe(0o600)
expect(statSync(planPath).mode & 0o777).toBe(0o600)
expect(readFileSync(planPath, 'utf8')).toContain('_zpan_id_normalization_map')
await expect(run('--sqlite', databasePath, '--apply')).rejects.toThrow('id_normalization_backup_required')
await expect(run('--sqlite', databasePath, '--emit-d1-sql', join(directory, 'invalid.sql'))).rejects.toThrow(
'd1_plan_requires_apply',
)
await expect(run('--sqlite', join(directory, 'missing.db'))).rejects.toThrow('sqlite_database_missing')
await expect(run()).rejects.toThrow('Usage: pnpm ids:normalize')
} finally {
rmSync(directory, { recursive: true, force: true })
}
})
})