Skip to content

Commit 89dbc74

Browse files
fix(cli): os migrate value walks name the columns a disabled package's extension adds (#30696)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DCWRqxf2Z2WsHK3FdNXwnEU)_ Fixes #30626 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:`os migrate` 的五个值迁移命令(blank-single-values、blank-multi-values、option-codes、text-scalars、value-shapes)在报告里逐条列出因所属模块包被停用而没读到的列(对象.字段 + 包 id),`--json` 里同名 `withheldColumns`。 - 为什么改:模块包停用时,它扩展到别的对象上的字段从对外对象里隐去、但列和数据还在;这几个命令只看对外对象,于是跳过这些列,报告却像全扫过一样(实测:有待清理的空串,dry run 仍报「未发现」)。 - 风险与回滚:只加报告行和一个 JSON 字段,读写范围一行没变;回滚即 revert 本 PR。 - 先看:`packages/cli/src/utils/migrate-withheld-columns.ts`(判定:存储折叠减对外折叠,按停用扩展包归属);`blank-single-values.ts` 的 `walkBlankColumns` 返回处;`value-shapes.ts`(无列规则,列出全部被隐去字段)。 - 两分钟验证:`pnpm --filter @objectstack/cli exec vitest run src/commands/migrate/withheld-extension-columns.integration.test.ts`,预期 9 个用例全绿。 - 需要你做的:无,席位落地。 ## What changes - New `packages/cli/src/utils/migrate-withheld-columns.ts`: `withheldColumns(engine, objects, select?)` reads, per walked object, the extend contributors whose package `isPackageDisabled`, takes the walk's own column rule over `registry.getStorageObject(object)` minus the same rule over the served `engine.getObject(object)`, and attributes each remaining column to the disabled package whose extension declares it. `formatWithheldColumns` renders the list (nothing when empty). It lives in `utils/` because every `.ts` under `src/commands/**` is an oclif command (pattern strategy); the seat amended the claim's file surface for it. - `blank-single-values` and `blank-multi-values` (they share `walkBlankColumns`, so one change covers both), `option-codes`, `text-scalars`: the report gains `withheldColumns`, the human plan prints it, the command's engine view passes `registry`. - `value-shapes`: the scan is objectql's (`scanValueShapes`) and its column rule (`isScannableValueShapeField`) is not exported, so the command names every withheld field of the scanned objects (no `select`), in the plan and in the `--json` document. No `packages/objectql/**` change. **Behaviour picked: name the skipped columns, not warn-or-refuse.** Measured on `origin/main` (the integration fixture below with the module disabled): each walk reported `columns: []` over a row still holding a value it would rewrite, so the plan said "✓ No stored empty string found." (and the like) with no mention of the column. Refusing while a module that extends a target object is disabled would block the run for every other object and column it can cover, and the operator's remedy (enable, re-run) is the same either way; naming the columns keeps the run useful and makes the gap explicit. The withheld values cannot be rewritten in place while disabled (the card's out of scope: `engine.find` refuses a withheld field with `INVALID_FIELD`). Changed lines: 9 files, +321 / −1 (code 5 command files +58/−1, helper +89, tests +164, changeset +10). ## Tests - `src/commands/migrate/withheld-extension-columns.integration.test.ts` (integration tier: `new ObjectQL(` + sqlite `SqlDriver`): an app package owns `wx_account`, a module package adds `wx_tier` / `wx_level` (select), `wx_tags` (multiselect), `wx_note` (text) by extension; a row below the door holds a value for each walk to rewrite. Per walk: control (enabled: the column is read, nothing named) and the pin (disabled: `report.columns` empty, `withheldColumns` equals the walk's in-scope extension columns with the package id, the plan prints `wx_account.COLUMN (package com.qa.wx.loyalty)`). One more case for the value-shapes path (no column rule: all four fields named). - `src/utils/migrate-withheld-columns.test.ts` (unit): attribution, the column-rule narrowing, an engine without the registry surface, the empty format. <details><summary>Ablation (the four command files reverted to origin/main, helper and test kept; commit c2cdd7d)</summary> - Mutated leg: `grep -c withheldColumns` on the four files = 0 each; `Tests 8 failed | 1 passed (9)` (every walk case red, e.g. `expected undefined to deeply equal [ Array(2) ]`; the value-shapes helper case stays green as it does not go through a walk). Lock `VERDICT command-exit 1`. - Restore leg: `git checkout HEAD -- FILES`, `git diff HEAD` empty, each file's hash equals its HEAD blob. - Fixed tree: `Tests 9 passed (9)`, `VERDICT command-exit 0`. The CLI tests import `src/` directly, so no dist rebuild sits between the legs. </details> ## Acceptance notes - `value-shapes --apply` still records the `adr-0104-value-shapes` deployment flag on a full-scope run while withheld columns went unscanned; this PR only names them. Whether such a run should record no flag (as a `--object`-narrowed run does) is a posture question for the seat; carrier: none. - Other `os migrate` commands that read `engine.getObject` (`files-to-references`, `comment-reactions`, `activity-changed-fields`, the `audit-*` commands, `organization-ownership`) target platform objects or a scan in objectql; not measured for this blind spot here. carrier: none. ## Verification Local, on c2cdd7d (the implementation head; b94187d only merges 4 `origin/main` commits, none touching `packages/cli/src/commands/migrate/**` or `packages/cli/src/utils/migrate-*`): - `pnpm --filter @objectstack/cli typecheck` (includes `check:test-typecheck`): `VERDICT command-exit 0`. - `pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2`: `Test Files 390 passed (390)`, `Tests 5435 passed (5435)`, `VERDICT command-exit 0`. - Integration layer, the files this diff touches or feeds (new pin + `blank-single-values`, `blank-multi-values`, `option-codes`, `text-scalars` integration suites + the helper unit test): the four sibling integration suites and the helper unit test `5 passed`; the new pin, rerun after a fixture fix, `Tests 9 passed (9)`. The rest of the integration layer is declared to CI. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 60 families, each run with its exit code captured before any pipe; 56 exit 0 first pass, 4 exit 3 (no dist: `check:dual-build-cjs-loads`, `check:i18n`, `check:i18n-coverage`, `check:i18n-walk-parity`) cleared by a whole-tree `turbo run build` and then exit 0. `--ran`: "60 derived famil(ies) accounted for — 60 run, 0 NOT-MEASURED". - CI: the PR's check-runs are the record for every family CI runs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016WV4cwPmVRHj9NxSn1E2Xz Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1030a80 commit 89dbc74

9 files changed

Lines changed: 321 additions & 1 deletion
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): `os migrate` value walks name the columns they skip because the package that adds them is disabled
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`os migrate blank-single-values`, `blank-multi-values`, `option-codes`, `text-scalars` and `value-shapes` read each object as it is served. While a module package is disabled, the fields its `objectExtensions` add are withheld from that object (their columns and values are kept), so these runs could not read those columns and their reports read as if they had. Each report now lists every such column as `object.field (package ID)`, and the `--json` document carries the same list as `withheldColumns`. Enable the package and re-run to cover them. Nothing else changes: the runs still read and rewrite only what the served object declares.

‎packages/cli/src/commands/migrate/blank-multi-values.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { describeOccupancy, JOURNAL_MODE_SWITCH_NOTE } from '../../utils/sqlite-
2222
import { describeDriverSqliteFile } from '../../utils/connection-display.js';
2323
import { buildDataMigrationPlugins } from '../../utils/data-migration-plugins.js';
2424
import { refuseUndeclaredObjects } from '../../utils/migrate-object-scope.js';
25+
import { formatWithheldColumns } from '../../utils/migrate-withheld-columns.js';
2526
import {
2627
walkBlankColumns,
2728
type BlankSingleValuesEngine,
@@ -117,6 +118,7 @@ export function formatBlankMultiValuesReport(report: BlankSingleValuesReport): s
117118
if (report.truncated) {
118119
lines.push('⚠ Walk incomplete — rows past the cap or in an unreadable object were not read.');
119120
}
121+
lines.push(...formatWithheldColumns(report.withheldColumns));
120122
return lines;
121123
}
122124

@@ -295,6 +297,7 @@ export default class MigrateBlankMultiValues extends Command {
295297
find: (object, options) =>
296298
!apply && stack.tableAbsent?.(object) ? Promise.resolve([]) : engine.find(object, options),
297299
update: (object, data, options) => engine.update(object, data, options),
300+
registry: engine.registry,
298301
};
299302

300303
const report = await cleanBlankMultiValues(view, logger, {

‎packages/cli/src/commands/migrate/blank-single-values.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ import { describeOccupancy, JOURNAL_MODE_SWITCH_NOTE } from '../../utils/sqlite-
2424
import { describeDriverSqliteFile } from '../../utils/connection-display.js';
2525
import { buildDataMigrationPlugins } from '../../utils/data-migration-plugins.js';
2626
import { refuseUndeclaredObjects } from '../../utils/migrate-object-scope.js';
27+
import {
28+
formatWithheldColumns,
29+
withheldColumns,
30+
type WithheldColumn,
31+
type WithheldColumnsRegistry,
32+
} from '../../utils/migrate-withheld-columns.js';
2733

2834
async function confirm(question: string): Promise<boolean> {
2935
if (!process.stdin.isTTY) return false; // non-interactive → require --yes
@@ -76,6 +82,11 @@ export interface BlankSingleValuesReport {
7682
truncated: boolean;
7783
unreadableObjects: string[];
7884
failures: BlankSingleValueFailure[];
85+
/**
86+
* [#30626] In-scope columns this run did not read because the package that
87+
* adds them is disabled: re-run after enabling it.
88+
*/
89+
withheldColumns: WithheldColumn[];
7990
}
8091

8192
/** The engine surface the walk reads and writes through. */
@@ -84,6 +95,8 @@ export interface BlankSingleValuesEngine {
8495
getConfigs?(): Record<string, unknown>;
8596
find(object: string, options: Record<string, unknown>): Promise<Array<Record<string, unknown>>>;
8697
update(object: string, data: Record<string, unknown>, options?: any): Promise<unknown>;
98+
/** [#30626] Where the columns a disabled package's extension adds are read from. */
99+
registry?: WithheldColumnsRegistry;
87100
}
88101

89102
export interface BlankSingleValuesLogger {
@@ -265,6 +278,7 @@ export async function walkBlankColumns(
265278
truncated,
266279
unreadableObjects,
267280
failures,
281+
withheldColumns: withheldColumns(engine, candidates, spec.columnsOf),
268282
};
269283
}
270284

@@ -290,6 +304,7 @@ export function formatBlankSingleValuesReport(report: BlankSingleValuesReport):
290304
if (report.truncated) {
291305
lines.push('⚠ Walk incomplete — rows past the cap or in an unreadable object were not read.');
292306
}
307+
lines.push(...formatWithheldColumns(report.withheldColumns));
293308
return lines;
294309
}
295310

@@ -467,6 +482,7 @@ export default class MigrateBlankSingleValues extends Command {
467482
find: (object, options) =>
468483
!apply && stack.tableAbsent?.(object) ? Promise.resolve([]) : engine.find(object, options),
469484
update: (object, data, options) => engine.update(object, data, options),
485+
registry: engine.registry,
470486
};
471487

472488
const report = await cleanBlankSingleValues(view, logger, {

‎packages/cli/src/commands/migrate/option-codes.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ import { describeOccupancy, JOURNAL_MODE_SWITCH_NOTE } from '../../utils/sqlite-
2323
import { describeDriverSqliteFile } from '../../utils/connection-display.js';
2424
import { buildDataMigrationPlugins } from '../../utils/data-migration-plugins.js';
2525
import { refuseUndeclaredObjects } from '../../utils/migrate-object-scope.js';
26+
import {
27+
formatWithheldColumns,
28+
withheldColumns,
29+
type WithheldColumn,
30+
type WithheldColumnsRegistry,
31+
} from '../../utils/migrate-withheld-columns.js';
2632

2733
async function confirm(question: string): Promise<boolean> {
2834
if (!process.stdin.isTTY) return false; // non-interactive → require --yes
@@ -188,13 +194,20 @@ export interface OptionCodesReport {
188194
truncated: boolean;
189195
unreadableObjects: string[];
190196
failures: OptionCodeFailure[];
197+
/**
198+
* [#30626] In-scope columns this run did not read because the package that
199+
* adds them is disabled: re-run after enabling it.
200+
*/
201+
withheldColumns: WithheldColumn[];
191202
}
192203

193204
export interface OptionCodesEngine {
194205
getObject(name: string): any | undefined;
195206
getConfigs?(): Record<string, unknown>;
196207
find(object: string, options: Record<string, unknown>): Promise<Array<Record<string, unknown>>>;
197208
update(object: string, data: Record<string, unknown>, options?: any): Promise<unknown>;
209+
/** [#30626] Where the columns a disabled package's extension adds are read from. */
210+
registry?: WithheldColumnsRegistry;
198211
}
199212

200213
export interface OptionCodesLogger {
@@ -347,6 +360,7 @@ export async function migrateOptionCodes(
347360
truncated,
348361
unreadableObjects,
349362
failures,
363+
withheldColumns: withheldColumns(engine, candidates, optionCodeFields),
350364
};
351365
}
352366

@@ -389,6 +403,7 @@ export function formatOptionCodesReport(report: OptionCodesReport): string[] {
389403
if (report.truncated) {
390404
lines.push('⚠ Walk incomplete — rows past the cap or in an unreadable object were not read.');
391405
}
406+
lines.push(...formatWithheldColumns(report.withheldColumns));
392407
return lines;
393408
}
394409

@@ -562,6 +577,7 @@ export default class MigrateOptionCodes extends Command {
562577
find: (object, options) =>
563578
!apply && stack.tableAbsent?.(object) ? Promise.resolve([]) : engine.find(object, options),
564579
update: (object, data, options) => engine.update(object, data, options),
580+
registry: engine.registry,
565581
};
566582

567583
const report = await migrateOptionCodes(view, logger, {

‎packages/cli/src/commands/migrate/text-scalars.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ import { describeOccupancy, JOURNAL_MODE_SWITCH_NOTE } from '../../utils/sqlite-
2424
import { describeDriverSqliteFile } from '../../utils/connection-display.js';
2525
import { buildDataMigrationPlugins } from '../../utils/data-migration-plugins.js';
2626
import { refuseUndeclaredObjects } from '../../utils/migrate-object-scope.js';
27+
import {
28+
formatWithheldColumns,
29+
withheldColumns,
30+
type WithheldColumn,
31+
type WithheldColumnsRegistry,
32+
} from '../../utils/migrate-withheld-columns.js';
2733

2834
async function confirm(question: string): Promise<boolean> {
2935
if (!process.stdin.isTTY) return false; // non-interactive → require --yes
@@ -149,13 +155,20 @@ export interface TextScalarsReport {
149155
truncated: boolean;
150156
unreadableObjects: string[];
151157
failures: TextScalarFailure[];
158+
/**
159+
* [#30626] In-scope columns this run did not read because the package that
160+
* adds them is disabled: re-run after enabling it.
161+
*/
162+
withheldColumns: WithheldColumn[];
152163
}
153164

154165
export interface TextScalarsEngine {
155166
getObject(name: string): any | undefined;
156167
getConfigs?(): Record<string, unknown>;
157168
find(object: string, options: Record<string, unknown>): Promise<Array<Record<string, unknown>>>;
158169
update(object: string, data: Record<string, unknown>, options?: any): Promise<unknown>;
170+
/** [#30626] Where the columns a disabled package's extension adds are read from. */
171+
registry?: WithheldColumnsRegistry;
159172
}
160173

161174
export interface TextScalarsLogger {
@@ -282,6 +295,7 @@ export async function migrateTextScalars(
282295
truncated,
283296
unreadableObjects,
284297
failures,
298+
withheldColumns: withheldColumns(engine, candidates, textScalarFields),
285299
};
286300
}
287301

@@ -320,6 +334,7 @@ export function formatTextScalarsReport(report: TextScalarsReport): string[] {
320334
if (report.truncated) {
321335
lines.push('⚠ Walk incomplete — rows past the cap or in an unreadable object were not read.');
322336
}
337+
lines.push(...formatWithheldColumns(report.withheldColumns));
323338
return lines;
324339
}
325340

@@ -497,6 +512,7 @@ export default class MigrateTextScalars extends Command {
497512
find: (object, options) =>
498513
!apply && stack.tableAbsent?.(object) ? Promise.resolve([]) : engine.find(object, options),
499514
update: (object, data, options) => engine.update(object, data, options),
515+
registry: engine.registry,
500516
};
501517

502518
const report = await migrateTextScalars(view, logger, {

‎packages/cli/src/commands/migrate/value-shapes.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import { bootSchemaStack } from '../../utils/schema-migrate.js';
1919
import { buildDataMigrationPlugins } from '../../utils/data-migration-plugins.js';
2020
import { isNarrowedRun, narrowedFlagNote, refuseUndeclaredObjects } from '../../utils/migrate-object-scope.js';
21+
import { formatWithheldColumns, withheldColumns } from '../../utils/migrate-withheld-columns.js';
2122

2223
async function confirm(question: string): Promise<boolean> {
2324
if (!process.stdin.isTTY) return false; // non-interactive → require --yes
@@ -226,6 +227,10 @@ export default class MigrateValueShapes extends Command {
226227
maxRecordsPerObject: flags['max-records'],
227228
});
228229
const passed = valueShapeScanPassed(report);
230+
// [#30626] The scan reads the served fold, so a field a disabled
231+
// package's extension adds is not scanned. Every such field is named
232+
// (the scan's own column rule is the engine's, not exported).
233+
const withheld = withheldColumns(engine, flags.object);
229234
const notStoredLine = notStored.size > 0
230235
? `${notStored.size} scanned object(s) have no table in this database yet, so nothing is stored in ` +
231236
`them and they were not read: ${[...notStored].sort().join(', ')}.`
@@ -276,6 +281,7 @@ export default class MigrateValueShapes extends Command {
276281
// mistaken for a full one (the shape `os migrate duplicates` keeps).
277282
filter: narrowed ? { objects: flags.object } : null,
278283
scan: report,
284+
withheldColumns: withheld,
279285
gatePassed: passed,
280286
flag,
281287
duration: timer.elapsed(),
@@ -286,7 +292,7 @@ export default class MigrateValueShapes extends Command {
286292

287293
printInfo(`Database: ${chalk.white(stack.dbLabel)}`);
288294
console.log('');
289-
console.log(formatValueShapeScanReport(report).join('\n'));
295+
console.log([...formatValueShapeScanReport(report), ...formatWithheldColumns(withheld)].join('\n'));
290296
console.log('');
291297
if (notStoredLine) printInfo(notStoredLine);
292298

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#30626] The `os migrate` value walks on a real engine (`ObjectQL` + sqlite
5+
* `SqlDriver`) while a module package that extends the walked object is
6+
* disabled.
7+
*
8+
* The walks scope by the served fold, which withholds a disabled package's
9+
* extension fields (#30304) while the storage fold keeps their columns and
10+
* values. Pinned: each walk's report names every in-scope column it skipped
11+
* that way (object, field, package id) and its plan prints it; with the
12+
* package enabled the same column is read and nothing is named.
13+
*/
14+
15+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
16+
import { ObjectQL } from '@objectstack/objectql';
17+
import { SqlDriver } from '@objectstack/driver-sql';
18+
import { cleanBlankSingleValues, formatBlankSingleValuesReport } from './blank-single-values.js';
19+
import { cleanBlankMultiValues, formatBlankMultiValuesReport } from './blank-multi-values.js';
20+
import { migrateOptionCodes, formatOptionCodesReport } from './option-codes.js';
21+
import { migrateTextScalars, formatTextScalarsReport } from './text-scalars.js';
22+
import { withheldColumns, formatWithheldColumns } from '../../utils/migrate-withheld-columns.js';
23+
24+
const APP_PKG = 'com.qa.wx.app';
25+
const MODULE_PKG = 'com.qa.wx.loyalty';
26+
const OBJECT = 'wx_account';
27+
const OPTS = [{ value: 'gold', label: 'Gold' }, { value: 'silver', label: 'Silver' }];
28+
29+
const manifest = (id: string, type: string) =>
30+
({ id, name: id, version: '1.0.0', type, namespace: 'wx', ...(type === 'module' ? { dependencies: { [APP_PKG]: '^1.0.0' } } : {}) }) as any;
31+
const OWNER = {
32+
name: OBJECT, label: 'Account', systemFields: false,
33+
fields: {
34+
id: { name: 'id', type: 'text', primaryKey: true },
35+
name: { name: 'name', type: 'text' },
36+
},
37+
};
38+
/** A column for each walk to rewrite, each added only by the module's extension. */
39+
const EXTENSION = {
40+
name: OBJECT,
41+
fields: {
42+
wx_tier: { name: 'wx_tier', type: 'select', options: OPTS },
43+
wx_level: { name: 'wx_level', type: 'select', options: [{ value: '3', label: 'Three' }] },
44+
wx_tags: { name: 'wx_tags', type: 'multiselect', options: OPTS },
45+
wx_note: { name: 'wx_note', type: 'text' },
46+
},
47+
};
48+
49+
const silent = { info: () => {}, warn: () => {} };
50+
const live: ObjectQL[] = [];
51+
afterEach(async () => {
52+
while (live.length) {
53+
try { await live.pop()?.destroy(); } catch { /* noop */ }
54+
}
55+
});
56+
57+
async function boot() {
58+
const engine = new ObjectQL();
59+
live.push(engine);
60+
const driver = new SqlDriver({ client: 'better-sqlite3', connection: { filename: ':memory:' }, useNullAsDefault: true });
61+
engine.registerDriver(driver, true);
62+
await engine.init();
63+
engine.registry.installPackage(manifest(APP_PKG, 'app'), 'code');
64+
engine.registry.installPackage(manifest(MODULE_PKG, 'module'), 'code');
65+
engine.registry.registerObject(OWNER as any, APP_PKG, 'wx', 'own');
66+
engine.registry.registerObject(EXTENSION as any, MODULE_PKG, 'wx', 'extend');
67+
await engine.syncSchemas();
68+
// The legacy values each walk would rewrite, written below the engine's door.
69+
await driver.create(OBJECT, { id: 'r1', name: 'A', wx_tier: '', wx_level: 3, wx_tags: [], wx_note: 7 });
70+
return engine;
71+
}
72+
73+
/** `column`: the one this walk rewrites; `inScope`: every extension column its own rule covers. */
74+
const WALKS = [
75+
{ label: 'blank-single-values', column: 'wx_tier', inScope: ['wx_level', 'wx_tier'], run: cleanBlankSingleValues, format: formatBlankSingleValuesReport },
76+
{ label: 'blank-multi-values', column: 'wx_tags', inScope: ['wx_tags'], run: cleanBlankMultiValues, format: formatBlankMultiValuesReport },
77+
{ label: 'option-codes', column: 'wx_level', inScope: ['wx_level', 'wx_tags', 'wx_tier'], run: migrateOptionCodes, format: formatOptionCodesReport },
78+
{ label: 'text-scalars', column: 'wx_note', inScope: ['wx_note'], run: migrateTextScalars, format: formatTextScalarsReport },
79+
] as const;
80+
81+
describe('os migrate value walks: a disabled module\'s extension columns are named (#30626)', () => {
82+
let engine: ObjectQL;
83+
beforeEach(async () => { engine = await boot(); });
84+
85+
for (const walk of WALKS) {
86+
it(`${walk.label}: the control — enabled, the column is read and nothing is named`, async () => {
87+
const report: any = await walk.run(engine as any, silent, {});
88+
expect(report.columns.map((c: any) => `${c.object}.${c.field}`)).toContain(`${OBJECT}.${walk.column}`);
89+
expect(report.withheldColumns).toEqual([]);
90+
expect(walk.format(report).join('\n')).not.toContain(`(package ${MODULE_PKG})`);
91+
});
92+
93+
it(`${walk.label}: disabled, the plan names each skipped in-scope column with its package`, async () => {
94+
engine.registry.disablePackage(MODULE_PKG);
95+
const report: any = await walk.run(engine as any, silent, {});
96+
expect(report.columns).toEqual([]);
97+
expect(report.withheldColumns).toEqual(walk.inScope.map((field) => ({ object: OBJECT, field, packageId: MODULE_PKG })));
98+
expect(walk.format(report)).toContain(` • ${OBJECT}.${walk.column} (package ${MODULE_PKG})`);
99+
});
100+
}
101+
102+
it('value-shapes (no column rule): every withheld field of the object is named', async () => {
103+
engine.registry.disablePackage(MODULE_PKG);
104+
const named = withheldColumns(engine as any, undefined);
105+
expect(named.map((c) => c.field)).toEqual(['wx_level', 'wx_note', 'wx_tags', 'wx_tier']);
106+
expect(named.every((c) => c.object === OBJECT && c.packageId === MODULE_PKG)).toBe(true);
107+
expect(formatWithheldColumns(named)).toHaveLength(5);
108+
});
109+
});

0 commit comments

Comments
 (0)