Skip to content

Commit b15b2fc

Browse files
test(cli): migrate-meta e2e replays only the conversions step 17 still registers (#27132)
Part of #27114 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么:** 只改了 `packages/cli/test/migrate-meta.e2e.test.ts` 这一个夜间 e2e 测试文件。fixture 换成第 17 步仍注册的 12 个 conversion 各一处写法;#9418 那组用例改用仍会被拒并被转换的退役写法:`structuredOutput.format: 'regex'`(`agent-structured-output-refused-members-removed`)和 `dashboard.refreshInterval`;不用 `agent.lifecycle`,因为它的退役测试钉死该键在全树不得出现。 - **为什么改:** #26599 让 16.x 的 45 个 conversion 随退役套件老化离开第 17 步,`--from 16` 不再改写这些写法,快照里留下未知键,夜间 tiers 因此 6 个用例变红;PR 和合并队列不跑 e2e 层,所以只在夜间暴露。 - **风险与回滚:** 只动测试,不动产品代码,不发布任何东西。用例数不变(19 个),没有跳过、改名或换层。回滚就是 revert 本 PR。 - **先看:** ① 该文件里的 `PRE17_CONFIG` 和 `EXPECTED_CONVERSIONS`;② 新增的注册表钉子(`EXPECTED_CONVERSIONS` 必须等于 `MIGRATIONS_BY_MAJOR[17].conversionIds`);③ #9418 那组的 fixture 和 `os validate` 断言。 - **两分钟验证:** 先建好 cli 的依赖闭包,再跑 `OS_TEST_TIERS=nightly pnpm --filter @objectstack/cli exec vitest run test/migrate-meta.e2e.test.ts`,预期 19 个全部通过。 - **需要你做的:** 无,席位落地。 ## What changes #26599 (`dbf7e04adf`, ruling 6434038284 on #25767) removed the 45 conversions retired at 16.1.0 from step 17's `conversionIds`. It re-pointed the unit-tier cli pins but not this file, which runs only under `OS_TEST_TIERS=nightly`. The 2026-10-28 nightly (run 40638682260) went red on six of its cases. All six failures come from that narrowing: - The main fixture carried one shape per 16.x conversion family. Nine of the ten families it expected are no longer registered, and `--from 16` leaves those shapes as written. So the run attributes nothing to them, and the snapshot keeps `responsive`, `performance`, `aria` and `defaultSort` as unknown keys. That breaks `schemaValid`, the per-family attribution, the `--out` re-parse and idempotence. - The #9418 fixture carried `agent.knowledge` and `skill.triggerPhrases`. Their tombstones and conversions left the spec with the 16.x kits, so the load no longer converts them, and `os validate` answers with the strict unknown-key refusal that names the protocol upgrade guide, not the retired-key prescription. The fix is in this one file. No case is added, removed, renamed or skipped, so the count stays at 19. - **Main fixture:** the aged-out shapes are dropped (option (a) on the claim). The fixture now carries one shape for each of the 12 conversions step 17 still registers: the sharing-rule `full` → `edit` rename, the seven flow-node aliases and lifts (crud `object`, notify, wait, connector, map, subflow, script), `mongo` → `mongodb`, the converged retry policy (`retryDelayMs` → `backoffMs`), the page-header `description` → `subtitle` alias and the inline api action's object-form `params` → `bodyExtra`. The list was read from `registry.ts.template` step 17, not from memory, and every id was checked against `conversions/registry.ts`. The `required` / no-`storage` assertions (#16693), the canonical `delete_record` pass-through and the #4247 `maxRetries` TODO stay. - **Registry pin:** the attribution case first asserts that `EXPECTED_CONVERSIONS` equals `MIGRATIONS_BY_MAJOR[17].conversionIds` (imported from `@objectstack/spec/migrations`, as `migrate-meta-default-range-window.test.ts` does). The next family that leaves or joins the step then reds here by name, not as a missing rewrite. - **#9418 fixture:** re-pointed to retired spellings whose refusal and conversion still stand. `defineAgent` carries `structuredOutput: { format: 'regex' }`, a retired member refused at parse (`agent-structured-output-refused-members-removed`), and `Dashboard.create` from `@objectstack/spec/ui` carries `refreshInterval` (`dashboard-refresh-interval-to-refresh-interval-seconds`). Both are protocol 18, which `--from 16` replays into. `agent.lifecycle` is not used, because its retirement pins the key absent from the tree. There are still two per-artifact helpers from spec subpaths, so a tolerance scoped to `defineStack` alone still fails it. The `os validate` half now matches the refusal's own prescription, `--from 17`. ## Acceptance notes 1. **Option (a), not (b).** If an aged-out shape were kept, the replayed snapshot would be schema-invalid by design. That would break the `schemaValid`, re-parse and idempotence cases that the 12 surviving families need, and asserting the refusal instead would need a separate fixture and a new case. The ruled refusal itself, the unknown-key message naming the upgrade guide, was observed while reproducing this failure (the nightly log's `os validate` output). The spec owns the unit pins for it. 2. **Wider family coverage than before.** Before the narrowing, the file covered one step-17 family that still survives: sharing-rule. It now covers all 12 that step 17 registers, which restores the docblock's stated purpose: "one key from every v17 conversion family in a single pass". ## Tests At head `e3006f2bd` (the patch that moved the #9418 fixture off `agent.lifecycle`; addendum 6464184183 on #27114): `packages/spec` `test:repo` 81/81 files, 1324 tests (`agent-lifecycle-retirement.test.ts` 14/14); the nightly e2e 19/19; the cli unit project 343/343 files; derived gates 47 at exit 0, 1 NOT-MEASURED (`check:dual-build-cjs-loads`, dist absent). The readings below are the first head's. At head `f73c11c8` (this branch, merged with `origin/main` `6817bf1ea`): - `OS_TEST_TIERS=nightly pnpm --filter @objectstack/cli exec vitest run test/migrate-meta.e2e.test.ts`: before the fix, at `d90657b2`, `Tests 6 failed | 13 passed (19)`, the same six the nightly names. After: `Tests 19 passed (19)`, lock `VERDICT command-exit 0`. - `pnpm --filter @objectstack/cli exec vitest run --project unit`: 341 of 343 files and 4929 tests passed, 29 skipped. The two remaining files (`published-subpath-console.pin`, `published-subpath-hook-body.pin`) refused before any test because cli's own `dist/` was not built. After `pnpm --filter @objectstack/cli build` both passed (29 tests). ## Verification <details><summary>Derived gates (dispatch-gates --commands, 48 families)</summary> - 47 exited 0. - `node scripts/check-plugin-teardown-shape.mjs --self-test` first exited 3 (prerequisite: its pinned positive-control commit was outside the shallow clone). After fetching that commit, it exited 0 (48 cases). - `pnpm check:dual-build-cjs-loads` exited 3, `PREREQUISITE NOT MET`: it reads every package's `dist/`, and only the cli closure was built. Recorded as NOT MEASURED; CI's Lint and Repo Gates runs it. - `dispatch-gates --ran`: 48 derived, 47 run, 1 NOT-MEASURED (derived from the recorded exit 3), 0 UNRUN. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01QSeKYnN7X5PL1HWYjGfKJY)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3346da2 commit b15b2fc

1 file changed

Lines changed: 111 additions & 106 deletions

File tree

‎packages/cli/test/migrate-meta.e2e.test.ts‎

Lines changed: 111 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { createRequire } from 'node:module';
2424
import { fileURLToPath } from 'node:url';
2525
import { ObjectStackDefinitionSchema } from '@objectstack/spec';
2626
import { PROTOCOL_MAJOR, PROTOCOL_VERSION } from '@objectstack/spec/kernel';
27+
import { MIGRATIONS_BY_MAJOR } from '@objectstack/spec/migrations';
2728
import { childEnv } from './helpers/serve-process.js';
2829

2930
const execFileP = promisify(execFile);
@@ -33,10 +34,19 @@ const TSX = resolve(HERE, '../../../node_modules/.bin/tsx');
3334

3435
/**
3536
* A stack authored against protocol 16: every line marked `// 16:` is a shape
36-
* the v17 chain must rewrite, spanning each conversion family — renames
37-
* (action execute→target, sharing full→edit) and the #3896 close-out removals (rls.priority, the four
38-
* tool keys, flow active/template/outputSchema/fallbackNodeId, view/dashboard
39-
* inert keys, agent.knowledge, skill.triggerPhrases).
37+
* the v17 chain must rewrite, one per conversion step 17 still registers — the
38+
* sharing-rule `full`→`edit` rename, the flow-node config aliases and lifts,
39+
* the mongo driver alias, the converged retry policy, the page-header subtitle
40+
* alias and the inline action's object-form `params`.
41+
*
42+
* The protocol-16.x conversions (action execute→target, the #3896 close-out
43+
* removals: rls.priority, the four tool keys, flow active/template/
44+
* outputSchema/fallbackNodeId, view/dashboard inert keys, agent.knowledge,
45+
* skill.triggerPhrases) are deliberately NOT here. They aged out with their
46+
* retirement kits (#25767, ADR-0087 2026-10-26 amendment): `--from 16` no
47+
* longer rewrites those shapes, and the strict schema refuses them as unknown
48+
* keys pointing at the protocol upgrade guide, so carrying one would make the
49+
* replayed snapshot invalid by design rather than test the chain.
4050
*/
4151
const PRE17_CONFIG = `
4252
export default {
@@ -51,76 +61,43 @@ export default {
5161
notes: { type: 'textarea', label: 'Notes' },
5262
},
5363
}],
54-
actions: [{
55-
name: 'close_ticket',
56-
label: 'Close',
57-
type: 'script',
58-
execute: 'closeHandler', // 16: renamed to target
59-
shortcut: 'Ctrl+K', // 16: removed (never dispatched)
60-
bulkEnabled: true, // 16: removed (toolbar reads view bulkActions)
64+
datasources: [{
65+
name: 'events',
66+
label: 'Events',
67+
driver: 'mongo', // 16: mongo→mongodb
68+
config: { url: 'mongodb://mongo.internal:27017/events' },
6169
}],
6270
flows: [{
6371
name: 'e2e_flow',
6472
label: 'E2E Flow',
6573
type: 'autolaunched',
66-
active: false, // 16: removed (status is the lifecycle)
67-
template: true, // 16: removed (no reader)
68-
// 16: fallbackNodeId removed (fault edges own this). maxRetries is stated
69-
// because #4247 refuses a zero-attempt 'retry' — the count is the one v17
70-
// flow change the chain surfaces as a semantic TODO instead of rewriting.
71-
errorHandling: { strategy: 'retry', maxRetries: 2, fallbackNodeId: 'n9' },
74+
// 16: retryDelayMs→backoffMs. maxRetries is stated because #4247 refuses
75+
// a zero-attempt 'retry' — the count is the one v17 flow change the chain
76+
// surfaces as a semantic TODO instead of rewriting.
77+
errorHandling: { strategy: 'retry', maxRetries: 2, retryDelayMs: 500 },
7278
nodes: [
73-
{ id: 'n1', type: 'start', label: 'Start', outputSchema: { ok: { type: 'boolean' } } }, // 16: removed
79+
{ id: 'n1', type: 'start', label: 'Start' },
7480
{ id: 'n2', type: 'delete_record', label: 'Purge', config: { objectName: 'e2e_ticket', filter: { done: true } } }, // canonical since protocol 11 — must pass through untouched
81+
{ id: 'n3', type: 'get_record', label: 'Load', config: { object: 'e2e_ticket', filter: { done: false } } }, // 16: object→objectName
82+
{ id: 'n4', type: 'notify', label: 'Tell', config: { to: ['{record.owner}'], subject: 'Closed', body: 'Ticket closed.', channels: ['inbox'] } }, // 16: to/subject/body→recipients/title/message
83+
{ id: 'n5', type: 'wait', label: 'Pause', config: { duration: 'PT1M' } }, // 16: lifted into waitEventConfig
84+
{ id: 'n6', type: 'connector_action', label: 'Post', config: { connectorId: 'slack', actionId: 'chat.postMessage', input: { text: 'done' } } }, // 16: lifted into connectorConfig
85+
{ id: 'n7', type: 'map', label: 'Each', config: { collection: '{rows}', flow: 'e2e_child' } }, // 16: flow→flowName
86+
{ id: 'n8', type: 'subflow', label: 'Child', config: { flow: 'e2e_child' } }, // 16: flow→flowName
87+
{ id: 'n9', type: 'script', label: 'Score', config: { functionName: 'score_ticket', input: { id: '{recordId}' } } }, // 16: functionName/input→function/inputs
7588
],
7689
edges: [],
7790
}],
78-
views: [{
79-
object: 'e2e_ticket',
80-
list: { type: 'grid', columns: ['title'], responsive: { hiddenOn: ['xs'] }, performance: { lazyLoad: true } }, // 16: removed
81-
form: { type: 'simple', sections: [{ fields: ['title'] }], defaultSort: [{ field: 'title' }], aria: { label: 'Ticket' } }, // 16: removed
82-
}],
83-
dashboards: [{
84-
name: 'e2e_kpis',
85-
label: 'E2E KPIs',
86-
aria: { label: 'KPIs' }, // 16: removed
87-
performance: { prefetch: true },// 16: removed
88-
widgets: [],
89-
}],
90-
agents: [{
91-
name: 'e2e_agent',
92-
label: 'Agent',
93-
role: 'Helper',
94-
instructions: 'help',
95-
knowledge: { sources: ['faq'] }, // 16: removed (never scoped retrieval)
96-
}],
97-
skills: [{
98-
name: 'e2e_skill',
99-
label: 'Skill',
100-
tools: ['query_records'],
101-
triggerPhrases: ['do the thing'], // 16: removed (never matched)
102-
}],
103-
tools: [{
104-
name: 'e2e_tool',
105-
label: 'Tool',
106-
description: 'A tool',
107-
parameters: { type: 'object' },
108-
category: 'action', // 16: removed
109-
permissions: ['x.y'], // 16: removed (gated nothing)
110-
active: true, // 16: removed (withdrew nothing)
111-
builtIn: false, // 16: removed
112-
}],
113-
permissions: [{
114-
name: 'e2e_set',
115-
label: 'Set',
116-
objects: { e2e_ticket: { allowRead: true } },
117-
rowLevelSecurity: [{
118-
name: 'own_rows',
119-
object: 'e2e_ticket',
120-
operation: 'select',
121-
using: 'owner = current_user.id',
122-
enabled: true,
123-
priority: 10, // 16: removed (no conflict to order)
91+
pages: [{
92+
name: 'e2e_page',
93+
label: 'E2E Page',
94+
type: 'app',
95+
regions: [{
96+
name: 'header',
97+
components: [
98+
{ type: 'page:header', properties: { title: 'Tickets', description: 'All open tickets' } }, // 16: description→subtitle
99+
{ type: 'element:button', properties: { label: 'Submit', action: { type: 'api', target: '/api/v1/e2e/submit', method: 'POST', params: { note: 'hi' } } } }, // 16: object-form params→bodyExtra
100+
],
124101
}],
125102
}],
126103
sharingRules: [{
@@ -135,18 +112,25 @@ export default {
135112
};
136113
`;
137114

138-
/** Conversion ids the run MUST attribute at least one rewrite to. */
115+
/**
116+
* Conversion ids the run MUST attribute at least one rewrite to: step 17's
117+
* own `conversionIds`, written out so a family leaving the step reds here by
118+
* name (the registry pin in the case below) instead of thinning the fixture's
119+
* coverage in silence.
120+
*/
139121
const EXPECTED_CONVERSIONS = [
140-
'action-execute-to-target',
141-
'action-inert-keys-removed',
142-
'flow-inert-keys-removed',
143-
'view-inert-keys-removed',
144-
'dashboard-inert-keys-removed',
145-
'agent-knowledge-removed',
146-
'skill-trigger-phrases-removed',
147-
'tool-inert-authoring-keys-removed',
148-
'permission-rls-priority-removed',
149122
'sharing-rule-access-level-full-to-edit',
123+
'flow-node-crud-object-alias',
124+
'flow-node-notify-config-aliases',
125+
'flow-node-wait-event-config-lift',
126+
'flow-node-connector-config-lift',
127+
'flow-node-map-flow-alias',
128+
'flow-node-subflow-flow-alias',
129+
'flow-node-script-config-aliases',
130+
'datasource-driver-mongo-to-mongodb',
131+
'retry-policy-converged',
132+
'page-header-subtitle-alias',
133+
'inline-action-api-params-to-body-extra',
150134
];
151135

152136
let dir: string;
@@ -179,6 +163,11 @@ describe('os migrate meta --from 16 (e2e over the real CLI)', () => {
179163
});
180164

181165
it('attributes at least one rewrite to every expected v17 conversion family', () => {
166+
// The list IS step 17's: a conversion that ages out of the step (as the
167+
// 16.x ones did, #25767) or joins it fails here naming the drift, before
168+
// the per-id loop reads it as a missing rewrite.
169+
expect([...EXPECTED_CONVERSIONS].sort(), 'EXPECTED_CONVERSIONS must equal step 17\'s registered conversionIds')
170+
.toEqual([...(MIGRATIONS_BY_MAJOR[17]?.conversionIds ?? [])].sort());
182171
const ids = new Set(out.parsed.applied.map((a: any) => a.conversionId));
183172
for (const id of EXPECTED_CONVERSIONS) {
184173
expect(ids.has(id), `expected a rewrite from ${id}; got ${[...ids].join(', ')}`).toBe(true);
@@ -215,20 +204,26 @@ describe('os migrate meta --from 16 (e2e over the real CLI)', () => {
215204
expect(parsed.success, JSON.stringify(parsed.success ? '' : parsed.error.issues.slice(0, 3))).toBe(true);
216205

217206
// Spot-check one rewrite per family on the snapshot itself.
218-
expect(snap.actions[0].target).toBe('closeHandler');
219-
expect(snap.actions[0].execute).toBeUndefined();
220-
expect(snap.actions[0].shortcut).toBeUndefined();
221-
expect(snap.flows[0].active).toBeUndefined();
222-
expect(snap.flows[0].errorHandling.fallbackNodeId).toBeUndefined();
223-
expect(snap.flows[0].nodes[0].outputSchema).toBeUndefined();
224-
expect(snap.flows[0].nodes[1].config.filter, 'canonical key passes through untouched').toEqual({ done: true });
225-
expect(snap.views[0].list.responsive).toBeUndefined();
226-
expect(snap.views[0].form.defaultSort).toBeUndefined();
227-
expect(snap.dashboards[0].aria).toBeUndefined();
228-
expect(snap.agents[0].knowledge).toBeUndefined();
229-
expect(snap.skills[0].triggerPhrases).toBeUndefined();
230-
expect(snap.tools[0].category).toBeUndefined();
231-
expect(snap.permissions[0].rowLevelSecurity[0].priority).toBeUndefined();
207+
const [, purge, load, tell, pause, post, each, child, score] = snap.flows[0].nodes;
208+
expect(purge.config.filter, 'canonical key passes through untouched').toEqual({ done: true });
209+
expect(load.config.objectName).toBe('e2e_ticket');
210+
expect(load.config.object).toBeUndefined();
211+
expect(tell.config.recipients).toEqual(['{record.owner}']);
212+
expect(tell.config.to).toBeUndefined();
213+
expect(pause.waitEventConfig.timerDuration).toBe('PT1M');
214+
expect(post.connectorConfig.connectorId).toBe('slack');
215+
expect(each.config.flowName).toBe('e2e_child');
216+
expect(child.config.flowName).toBe('e2e_child');
217+
expect(score.config.function).toBe('score_ticket');
218+
expect(score.config.functionName).toBeUndefined();
219+
expect(snap.flows[0].errorHandling.backoffMs).toBe(500);
220+
expect(snap.flows[0].errorHandling.retryDelayMs).toBeUndefined();
221+
expect(snap.datasources[0].driver).toBe('mongodb');
222+
const [header, button] = snap.pages[0].regions[0].components;
223+
expect(header.properties.subtitle).toBe('All open tickets');
224+
expect(header.properties.description).toBeUndefined();
225+
expect(button.properties.action.bodyExtra).toEqual({ note: 'hi' });
226+
expect(button.properties.action.params).toBeUndefined();
232227
expect(snap.sharingRules[0].accessLevel).toBe('edit');
233228
// ⛔ ADR-0113, #16693: the chain does NOT write a column constraint for the
234229
// author. `required: true` crosses 16→17 as the write-time contract and
@@ -358,10 +353,19 @@ describe('os migrate meta --from 16 (e2e over the real CLI)', () => {
358353
* telling the author to run this command.
359354
*
360355
* So the fixture here is deliberately shaped like a real project and not like a
361-
* test: `defineStack` at the root, per-artifact helpers imported from a spec
362-
* SUBPATH (`@objectstack/spec/ai`, the spelling the example apps use), and the
363-
* retired keys authored inside them. A `defineStack`-only tolerance passes the
364-
* first and fails the last two.
356+
* test: `defineStack` at the root, per-artifact helpers imported from spec
357+
* SUBPATHS (`@objectstack/spec/ai`, `@objectstack/spec/ui`, the spellings the
358+
* example apps use), and the retired keys authored inside them. A
359+
* `defineStack`-only tolerance passes the first and fails the last two.
360+
*
361+
* The retired spellings are ones whose refusal and conversion still stand
362+
* (the `dashboard.refreshInterval` tombstone, and `regex`, a retired member of
363+
* `agent.structuredOutput.format` refused at parse with its prescription;
364+
* protocol 18, which `--from 16` replays into). The fixture used to carry
365+
* `agent.knowledge` and `skill.triggerPhrases`, whose 16.x kits aged out
366+
* (#25767): their tombstones and conversions left the spec, so neither is a
367+
* retired key any more, only an unknown one. `agent.lifecycle` is not used:
368+
* its retirement pins the key absent from the tree outside its own kit.
365369
*
366370
* The second test is the other half of the pin, and it is the one that keeps
367371
* this a restoration rather than a widening: `os validate` on the SAME fixture
@@ -379,7 +383,8 @@ describe('os migrate meta over a source carrying a retired key (#9418)', () => {
379383

380384
const RETIRED_KEY_CONFIG = `
381385
import { defineStack } from '@objectstack/spec';
382-
import { defineAgent, defineSkill } from '@objectstack/spec/ai';
386+
import { defineAgent } from '@objectstack/spec/ai';
387+
import { Dashboard } from '@objectstack/spec/ui';
383388
384389
export default defineStack({
385390
manifest: { id: 'com.example.retired-key-e2e', name: 'Retired Key E2E', version: '1.0.0', type: 'app', namespace: 'rk' },
@@ -393,13 +398,13 @@ export default defineStack({
393398
label: 'Agent',
394399
role: 'Helper',
395400
instructions: 'help',
396-
knowledge: { sources: ['faq'] }, // RETIRED in protocol 17 — the whole point
401+
structuredOutput: { format: 'regex' }, // RETIRED member in 17.7.0 — the whole point
397402
})],
398-
skills: [defineSkill({
399-
name: 'rk_skill',
400-
label: 'Skill',
401-
tools: ['query_records'],
402-
triggerPhrases: ['do the thing'], // RETIRED in protocol 17
403+
dashboards: [Dashboard.create({
404+
name: 'rk_ops',
405+
label: 'Ops',
406+
widgets: [],
407+
refreshInterval: 300, // RETIRED: renamed to refreshIntervalSeconds
403408
})],
404409
});
405410
`;
@@ -434,17 +439,17 @@ export default defineStack({
434439
expect(parsed.from).toBe(16);
435440
// …and through helpers imported from a spec subpath, which is where a
436441
// tolerance scoped to `defineStack` alone would still have refused.
437-
expect(appliedAt.get('agent-knowledge-removed')).toBe('agents[0].knowledge');
438-
expect(appliedAt.get('skill-trigger-phrases-removed')).toBe('skills[0].triggerPhrases');
442+
expect(appliedAt.get('agent-structured-output-refused-members-removed')).toBe('agents[0].structuredOutput.format');
443+
expect(appliedAt.get('dashboard-refresh-interval-to-refresh-interval-seconds')).toBe('dashboards[0].refreshIntervalSeconds');
439444
expect(parsed.schemaValid).toBe(true);
440445

441446
const snap = JSON.parse(readFileSync(join(rkDir, 'migrated.json'), 'utf-8'));
442-
expect(snap.agents[0].knowledge).toBeUndefined();
443-
expect(snap.skills[0].triggerPhrases).toBeUndefined();
447+
expect(snap.agents[0].structuredOutput).toBeUndefined();
448+
expect(snap.dashboards[0].refreshInterval).toBeUndefined();
444449
// The rest of the artifact survives the tolerant load intact — the codemod
445450
// removes what retired, not what it could not parse.
446451
expect(snap.agents[0].name).toBe('rk_agent');
447-
expect(snap.skills[0].tools).toEqual(['query_records']);
452+
expect(snap.dashboards[0].refreshIntervalSeconds).toBe(300);
448453
}, 180_000);
449454

450455
it('still refuses the same source everywhere else — `os validate` keeps the prescription', async () => {
@@ -458,8 +463,8 @@ export default defineStack({
458463
} catch (e: any) {
459464
refused = true;
460465
const output = `${String(e.stdout ?? '')}${String(e.stderr ?? '')}`;
461-
expect(output).toMatch(/`agent\.knowledge` was removed/);
462-
expect(output).toMatch(/os migrate meta --from 16/);
466+
expect(output).toMatch(/`regex` was removed/);
467+
expect(output).toMatch(/os migrate meta --from 17/);
463468
}
464469
expect(refused, '`os validate` must still reject a retired key').toBe(true);
465470
}, 180_000);

0 commit comments

Comments
 (0)