Skip to content

Commit b440182

Browse files
committed
test(cli): pin the package-owned readers on a two-package config, and enumerate every top-level read
- normalized-call-sites.test.ts gains a second table: every read of a package-owned key (the key set derived in stack-collections.ts) off a stack-named receiver in src, classified resolved (with evidence present in the code) or top-level on purpose (with the reason). - serve-package-declared-capabilities.test.ts boots os serve on two-package configs: requires, the requires narrowing, analyticsCubes, the flow count and tiers, with the one-package control. - package-owned-readers.test.ts and package-owned-command-readers.test.ts pin os generate, os doctor, os diff and os migrate meta against their one-package controls; the migrate plan provider case joins its existing file. Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 2b4c001 commit b440182

5 files changed

Lines changed: 840 additions & 3 deletions

‎packages/cli/src/utils/schema-migrate.requires-providers.integration.test.ts‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
55
import { tmpdir } from 'node:os';
66
import { join } from 'node:path';
77
import { bootSchemaStack } from './schema-migrate.js';
8+
import { linkSpec } from '../../test/helpers/define-stack-fixture.js';
89

910
/**
1011
* #21732 — `os migrate plan` / `apply` boot a config whose plugins hard-depend
@@ -93,3 +94,79 @@ describe('os migrate plan/apply resolve the requires-supplied provider a plugin
9394
}
9495
}, 60_000);
9596
});
97+
98+
/**
99+
* #22288 — the same boot when `requires` is declared by ONE PACKAGE of a
100+
* multi-package config, not at its top level.
101+
*
102+
* `composeStacks([a, b], { manifest: 'preserve' })` carries `requires` only in
103+
* the body of the package that declared it. The composition read the top level
104+
* alone, so the token was never searched and the kernel refused the order —
105+
* measured through the door on `6729e107`: `os migrate plan` exited 1 with
106+
* `Dependency 'com.objectstack.service-automation' not found for plugin …`,
107+
* while the same definitions in one `defineStack` planned and exited 0. It now
108+
* reads the tokens by `os serve`'s rule (`stackDeclaredCapabilities`).
109+
*/
110+
describe('os migrate plan/apply resolve a provider a PACKAGE requires (#22288)', () => {
111+
let dir: string;
112+
const savedEnv: Record<string, string | undefined> = {};
113+
114+
beforeAll(() => {
115+
dir = mkdtempSync(join(tmpdir(), 'os-22288-'));
116+
writeFileSync(
117+
join(dir, 'objectstack.config.ts'),
118+
[
119+
"import { defineStack, composeStacks } from '@objectstack/spec';",
120+
'class DependentConnector {',
121+
" name = 'com.example.os22288.connector';",
122+
" dependencies = ['com.objectstack.service-automation'];",
123+
' async init(ctx: any) {',
124+
" ctx.getService('automation').registerConnectorProvider('os22288', () => ({}));",
125+
' }',
126+
" async start() { throw new Error('a host start() must not run under os migrate'); }",
127+
'}',
128+
// The SERVICE package declares the capability; the APP package carries
129+
// the plugin that hard-depends on its provider.
130+
'const svc = defineStack({',
131+
" manifest: { id: 'com.example.os22288.svc', name: 'svc', namespace: 'osa', version: '0.0.0', type: 'module' },",
132+
" requires: ['automation'],",
133+
" objects: [{ name: 'osa_thing', label: 'Thing', fields: { title: { type: 'text', label: 'Title' } } }],",
134+
'} as any);',
135+
'const app = defineStack({',
136+
" manifest: { id: 'com.example.os22288.app', name: 'app', namespace: 'osb', version: '0.0.0', type: 'app' },",
137+
" objects: [{ name: 'osb_other', label: 'Other', fields: { title: { type: 'text', label: 'Title' } } }],",
138+
' plugins: [new DependentConnector()],',
139+
'} as any);',
140+
"export default composeStacks([svc, app], { manifest: 'preserve' });",
141+
'',
142+
].join('\n'),
143+
);
144+
linkSpec(dir);
145+
savedEnv.OS_ARTIFACT_PATH = process.env.OS_ARTIFACT_PATH;
146+
process.env.OS_ARTIFACT_PATH = join(dir, 'dist', 'objectstack.json');
147+
});
148+
149+
afterAll(() => {
150+
if (savedEnv.OS_ARTIFACT_PATH === undefined) delete process.env.OS_ARTIFACT_PATH;
151+
else process.env.OS_ARTIFACT_PATH = savedEnv.OS_ARTIFACT_PATH;
152+
try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
153+
});
154+
155+
it('boots and orders the dependent plugin after the provider the package declared', async () => {
156+
const boot = await bootSchemaStack({
157+
jsonOutput: false,
158+
databaseUrl: `file:${join(dir, 'plan.db')}`,
159+
deferSchemaDdl: true,
160+
composeHostStack: true,
161+
projectRoot: dir,
162+
});
163+
try {
164+
expect(boot.kernel.getService('automation').getConnectorProvider('os22288')).toBeTypeOf('function');
165+
expect(boot.composition.notes.join(' ')).toContain(
166+
"Composed AutomationServicePlugin for `requires: ['automation']`",
167+
);
168+
} finally {
169+
await boot.shutdown();
170+
}
171+
}, 60_000);
172+
});

0 commit comments

Comments
 (0)