Skip to content

Commit f32d795

Browse files
test(driver-sql): pin the NODE_ENV=production auto-migrate guard with a safe drift, beside a dev control (#24967)
Fixes #24903 Clause-②: no (test only: no product code, accept set or served shape moves) ## What changed One case in `packages/drivers/driver-sql/src/sql-driver-schema-drift.test.ts` is re-staged, and a development control is added beside it. Test only: no product file moves. - **Before:** "is force-disabled under NODE_ENV=production (warns, does not alter)" staged `relax_not_null`. ADR-0113 made that `needs_confirm`, and `reconcileAndWarnDrift` (`sql-driver.ts`) applies only `d.category === 'safe'`. So the column stayed NOT NULL with or without the production guard, and the case asserted no warning. - **After:** the case stages a `safe` drift: a `current_user` runtime token written as the column DEFAULT by a pre-#4560 build, which `detectManagedDrift` classes `drop_column_default` / `safe`. Under `NODE_ENV=production` with `autoMigrate: 'safe'` it asserts: - the DEFAULT is still on the column (`columnInfo().owner.defaultValue` still carries `current_user`); - the drift is still reported as `safe` / `drop_column_default`, so the guard is the only thing that held it back; - `logger.warn` received `[schema-drift] autoMigrate='safe' is ignored under NODE_ENV=production`, matched on that stable prefix. The em-dash tail is left unpinned. - **Control:** "applies the safe drop_column_default drift outside production (control for the guard below)". It is the same table and the same boot with `NODE_ENV=development`. The DEFAULT is removed, `detectManagedDrift()` answers `[]`, and no production warning is logged. The pair cannot pass vacuously again. - `NODE_ENV` is restored in `finally` in both cases. The logger is a per-driver `vi.fn()` object (the file's own `makeDriver`), so no global spy is left to restore. ## Why `drop_column_default` The `safe` ops `detectManagedDrift` emits on `main` (`schema-drift.ts`) are `drop_column_default`, `widen_varchar`, `replace_unique_index` and `create_index`. - `widen_varchar` is gated on `enforcesVarcharLength` (postgres / mysql only), so it never fires on this file's SQLite driver. - `create_index`: `syncTableIndexes` creates a missing declared index additively during `initObjects`, whatever `autoMigrate` says. The guard would not be the only reason it is absent. - `replace_unique_index` works, but it needs a tenant-scoped unique field, an organization column and an index read-back. It is already the stage of `sql-driver-index-drift.test.ts`'s production case. PR #24893 is also rewriting the NULL-safe key part it compares. - `drop_column_default` needs one column and one read-back, and stays in this file's `biz_unit` fixture idiom. The SQLite rebuild path applies it (`sql-driver-runtime-token-default.test.ts` measures that). **PR #24893** (draft) edits `schema-drift.ts`. Its patch changes no `category:` line, and it does not touch the runtime-token default branch. It does not change which ops are `safe`. ## The guard, as read `reconcileAndWarnDrift`: `autoOn = this.autoMigrate === 'safe' && this.schemaMode === 'managed'`, then `if (autoOn && this.isProductionEnv())`. `isProductionEnv()` is `(process.env.NODE_ENV ?? '').toLowerCase() === 'production'`, so the check is case-insensitive and reads nothing else. The warning goes out once per drifting table through `this.logger.warn`, not `console.warn`. The case spies on that logger. ## Verification (head `41b3fdf86`, `origin/main` merged at `19e5338cc`) - **Target file:** `pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2 src/sql-driver-schema-drift.test.ts`: 15 passed (was 14). - **Ablation:** `node scripts/ablation-replace.mjs --file packages/drivers/driver-sql/src/sql-driver.ts --anchor 'if (autoOn && this.isProductionEnv()) {' --replacement 'if (false /* ABLATED-24903 */) {' -- vitest run` on that file. - Anchor hit x1 to x0, and the replacement landed x0 to x1. Blob `b4393f25d41b` became `98a32996463a`. - The production case went red: `AssertionError: expected 'null' to contain 'current_user'`. The control stayed green: 1 failed / 14 passed. - Restored: blob after restore = blob at HEAD = `b4393f25d41b`, and `git diff HEAD` is empty. - The test imports `SqlDriver` from `../src/index.js`, so no `dist/` rebuild sits between the mutation and the run. - **Package checks:** `pnpm --filter @objectstack/driver-sql typecheck` exit 0, run before and after the merge. `tsc --listFiles` includes this test file. - **Full suite:** `pnpm --filter @objectstack/driver-sql test` exit 0 before and after the merge: 225 files passed and 12 skipped (the live-dialect suites), 3692 tests passed. - **Repo project:** `pnpm --filter @objectstack/core test:repo` exit 0: 6 files, 82 tests. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` gives 54 commands. All 54 were re-run on the final head with exit codes captured before any pipe: 54 of 54 exit 0 (26 direct node, 28 pnpm). - `check:dual-build-cjs-loads` and `check:lean-entry-closure` first exited 3 (PREREQUISITE NOT MET). They were re-run after `pnpm build` (76/76 tasks) and exited 0. - `--ran` reconciliation: 54 derived, 54 run, 0 NOT-MEASURED (a derived zero). - **Changeset:** none. The test file is outside `@objectstack/driver-sql`'s `files[]` (`dist`, `README.md`, `CHANGELOG.md`). The built `dist/` has zero hits for the test file name, and the positive control (the warning text) hits `dist/index.js`. `skip-changeset` is declared in the same create. ## Acceptance notes - **Pin sweep:** the warning text and the guard were swept across every package, `qa/`, `docs/qa/platform-checklist/` and `content/docs/`. - The only other pin of the driver-level guard is `sql-driver-index-drift.test.ts`, "is force-disabled under NODE_ENV=production — the index survives untouched". It stages `replace_unique_index` (`safe`) beside a dev self-heal sibling, so it is **not** vacuous. It does not assert the warning. Noted, not built. - The CLI / runtime pins (`dev-self-heal-host-parity`, `runtime/src/dev-auto-migrate`, `dev-standalone-self-heal`) pin a different layer: the host does not pass `autoMigrate` in production. They do not pin this driver branch. - `docs/qa/platform-checklist/areas/cli.json` names the warning in a live-boot step and carries this case as `knownGaps`. Re-adding the case to `cli.dev-automigrate-policy`'s `automated.ref` is `domain:devx`'s, and is not touched here. - Built by the `os-dev` agent for `domain:engine` seat 1, under claim 6297759904 on #24903. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- _Generated by [Claude Code](https://claude.ai/code/session_01XrcLWFkToZjHv2DJDAHhz4)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6440cf1 commit f32d795

1 file changed

Lines changed: 46 additions & 3 deletions

File tree

‎packages/drivers/driver-sql/src/sql-driver-schema-drift.test.ts‎

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,15 +199,58 @@ describe('SqlDriver managed-schema drift (#2186)', () => {
199199
expect(warnedDrift).toMatch(/os migrate/);
200200
});
201201

202+
// The production guard is pinned with a `safe` drift — the only category
203+
// auto-reconcile applies — so the guard is the sole reason it stays put:
204+
// a pre-#4560 build wrote the `current_user` token as the column DEFAULT,
205+
// and `drop_column_default` removes it. The control below proves the same
206+
// boot applies it outside production.
207+
const PRODUCTION_GUARD_WARNING = "[schema-drift] autoMigrate='safe' is ignored under NODE_ENV=production";
208+
const seedTokenDefaultTable = async () => {
209+
await knexInstance.schema.createTable('biz_unit', (t: any) => {
210+
t.string('id').primary();
211+
t.timestamp('created_at');
212+
t.timestamp('updated_at');
213+
t.string('owner').defaultTo('current_user');
214+
});
215+
};
216+
const tokenDefaultMeta = [{ name: 'biz_unit', fields: { owner: { type: 'string', defaultValue: 'current_user' } } }];
217+
const warned = (driver: SqlDriver): string =>
218+
((driver as any).logger.warn as ReturnType<typeof vi.fn>).mock.calls.map((c: any[]) => String(c[0])).join('\n');
219+
220+
it('applies the safe drop_column_default drift outside production (control for the guard below)', async () => {
221+
const prev = process.env.NODE_ENV;
222+
process.env.NODE_ENV = 'development';
223+
try {
224+
const driver = makeDriver({ autoMigrate: 'safe' });
225+
await seedTokenDefaultTable();
226+
await driver.initObjects(tokenDefaultMeta);
227+
228+
const info = await knexInstance('biz_unit').columnInfo();
229+
expect(info.owner.defaultValue ?? null).toBeNull();
230+
expect(await driver.detectManagedDrift()).toHaveLength(0);
231+
expect(warned(driver)).not.toContain(PRODUCTION_GUARD_WARNING);
232+
} finally {
233+
process.env.NODE_ENV = prev;
234+
}
235+
});
236+
202237
it('is force-disabled under NODE_ENV=production (warns, does not alter)', async () => {
203238
const prev = process.env.NODE_ENV;
204239
process.env.NODE_ENV = 'production';
205240
try {
206241
const driver = makeDriver({ autoMigrate: 'safe' });
207-
await seedLegacyTable(driver);
208-
await driver.initObjects(relaxedMeta);
242+
await seedTokenDefaultTable();
243+
await driver.initObjects(tokenDefaultMeta);
244+
245+
// Unchanged: the token DEFAULT the control above sees removed is still there...
209246
const info = await knexInstance('biz_unit').columnInfo();
210-
expect(info.region_code.nullable).toBe(false); // unchanged
247+
expect(String(info.owner.defaultValue)).toContain('current_user');
248+
// ...the drift is still the `safe` one auto-reconcile would have applied...
249+
const d = (await driver.detectManagedDrift()).find((x) => x.column === 'owner');
250+
expect(d?.category).toBe('safe');
251+
expect(d?.op.type).toBe('drop_column_default');
252+
// ...and the boot said why.
253+
expect(warned(driver)).toContain(PRODUCTION_GUARD_WARNING);
211254
} finally {
212255
process.env.NODE_ENV = prev;
213256
}

0 commit comments

Comments
 (0)