Skip to content

Commit 1d0600b

Browse files
fix(runtime,cloud-connection): an install-local package binds its script-action bodies and body hooks; list_actions lists only what run_action can run (#21401)
Fixes #21321 Clause-②: yes (widening) An app installed with `os package install ./dist/objectstack.json` (install-local) now runs its `type: 'script'` action bodies and its body hooks exactly as the same artifact does under `os start --artifact`, on install, after a reinstall and after a restart. MCP `list_actions` now lists a script action only when `run_action` can run it. Triage's rulings on the card are implemented as written: route A, probe A, and the hook half folded in. ## What changed - **One binder, `@objectstack/runtime`.** The new module `packages/runtime/src/app-artifact-handlers.ts` exports `bindAppArtifactHandlers(ql, bundle, { appId, logger, source })` and `appArtifactHandlerOwner(appId)`, and the package index re-exports both. The function binds an artifact's action bodies through `ql.registerAction` and its hook bodies and bundle functions through `ql.bindHooks`, all under the owner `app:APPID`. It first removes the action handlers and hooks that owner bound before. On a first bind this does nothing. On a reinstall it keeps one handler per action and one binding per hook, and it unbinds an action or hook the new version dropped. The hook removal is explicit because `bindHooksToEngine` unregisters only when it is given a non-empty list. - **`AppPlugin.start`** calls the binder in place of its two inline blocks. The order (after `runtime.onEnable`), the log lines and the failure handling are the same as before. - **Install-local, `@objectstack/cloud-connection`.** The plugin calls the binder on `POST /api/v1/marketplace/install-local`, after `manifest.register` and `syncSchemas` and before translations and seeds. It calls it again on the `kernel:ready` rehydrate of each ledger entry, with appId set to the manifest id. The runtime is loaded lazily, like the plugin's other runtime helpers. A runtime without the export binds nothing and logs a `warn` that names the consequence. There is no fallback registration path. - **Probe for `list_actions`.** `registeredActionHandlerProbe` sits beside `executeRegisteredAction` in `action-execution.ts`. It reads the engine's public `listRegisteredActions()` once per listing and walks the same `actionHandlerObjectKeys` x `resolveActionHandlerKeys` order as the run door. `list_actions` uses it for the script branch: every action `invokeBusinessAction` sends to the handler registry, meaning neither a declarative update nor a flow. Those two branches keep their own checks. An engine without `listRegisteredActions` lists no script action. No engine `hasAction` was added. - Nothing changes in `packages/objectql`, `packages/metadata-protocol` or `packages/spec`. ## Measured with the real CLI, before and after The app has one object, one script action with an inline body and `ai.exposed`, and one `beforeInsert` body hook that appends `stamped` to `status`. The flow is `os build`, then an empty `os start` (`OS_CLOUD_URL=off`), then `os package install ./dist/objectstack.json`. Probes went over REST and over MCP Streamable HTTP with a minted API key. | phase | door | before (main f397608) | after (this branch) | |---|---|---|---| | after install | insert, hook | 201, `status: null` | 201, `status: "stamped"` | | | REST `POST /api/v1/actions/tasks_app_task/complete_task` | 404 `RESOURCE_NOT_FOUND` | 200 `{ok:true}`, row `done` | | | MCP `run_action` | isError: "No handler registered for action 'complete_task' on 'tasks_app_task'" | `{ok:true, result:{ok:true}}`, row `done` | | | MCP `list_actions` | lists `complete_task` (that run_action then refuses) | lists `complete_task` (that run_action runs) | | after reinstall | all four | same as after install | same as after install; hook fires once (`stamped`) | | after restart (same home) | all four | 404, "No handler registered", `status: null`; restart log `re-synced runtime-authored actions {"registered":0,...}` | 200, ok, `stamped`; restart log `[MarketplaceInstallLocal] Bound declarative actions {"appId":"com.example.tasksapp","actionCount":2}` | | control `os start --artifact` | all four | 200, ok, `stamped` | 200, ok, `stamped` | ## Pins (each measured red on unfixed code first) - `packages/cli/test/package-install-local-handlers.integration.test.ts` (integration tier, spawn) runs the real `os start` and `os package install` through the tsx source entry, across install, reinstall, restart and the `--artifact` control. Each phase checks four things: the hook fires once, the REST action runs, MCP `run_action` runs and `list_actions` lists the action, and a declared AI-exposed `ghost_task` (a `target` nothing registers) is not listed while `run_action` refuses it. Red on main: 13 failed, 4 passed (the control's three rows and harness health). - `packages/cloud-connection/src/marketplace-install-local-artifact-handlers.test.ts` uses the real runtime binder and a recording engine. It covers install, rehydrate, a reinstall leaving exactly one handler and binding, and a reinstall of a version without the action and hook unbinding both. Red on main: 4 of 4. - `packages/runtime/src/mcp-list-actions-handler-probe.test.ts` covers listing against run_action on one engine double: an unbound body action, a target-bound key, the object-less key, the flow control, and an engine that cannot list its handlers. Red on main: 3 failed, 3 passed (the controls). - `packages/runtime/src/app-artifact-handlers.test.ts` runs the binder on a real `ObjectQL` engine with the QuickJS sandbox: `executeAction` runs the body, `triggerHooks` runs the hook, re-binding keeps exactly one of each, a dropped action or hook is unbound, and other owners are left alone. ## Ablations (fix committed at 2e4e1ff; every leg through `scripts/ablation-replace.mjs`, restore proven blob == HEAD and `git status --porcelain` empty; dist legs rebuilt and checked with `scripts/ablation-dist-preflight.mjs` both ways) | leg | mutation | red | |---|---|---| | A | delete the install-route bind call | cc pin 3 of 4 (install, both reinstall cases); spawn pin 6 (after-install and after-reinstall hook, REST, MCP); restart and control stay green | | B | delete the rehydrate bind call | cc pin: rehydrate; spawn pin 3 (after-restart hook, REST, MCP) | | C1 | `ql.removeActionsByPackage(owner)` to `void 0` | binder pin and cc pin (via runtime dist): dropped action still bound | | C2 | `ql.unregisterHooksByPackage(owner)` to `void 0` | binder pin and cc pin (via dist): dropped hook still fires | | D | `packageId: owner` to `packageId: undefined` | binder pin 2 (re-bind runs the hook twice; dropped hook); cc pin 3 (via dist); spawn pin 1 (after-reinstall hook fires twice) | | E | remove the probe condition in `list_actions` | probe pin 3 (the defect and its two siblings); spawn pin 4 (`ghost_task` listed in every phase) | | F | AppPlugin call replaced by `void bindAppArtifactHandlers;` | spawn pin 3 (control hook, REST, MCP) | A first run of leg F deleted the call outright. That left the import unused, so the runtime DTS step failed with TS6133 after the JS had already been emitted. It was re-run with the type-clean replacement in the table, and that run is the one quoted. ## Tests and gates (at 2e4e1ff; main merged at db0cf22) - `@objectstack/runtime` `vitest run --project local` (2 shards): 305 files, 4341 passed, 11 skipped. - `@objectstack/cloud-connection` full: 31 files, 401 passed. - `@objectstack/cli` `--project unit` (3 shards): 246 files, 3489 passed. Integration project: only the new file was run locally (17 passed). The rest of the integration project is left to CI. - `typecheck` of runtime, cloud-connection and cli: exit 0, with each `check:test-typecheck` OK. - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 66 gate commands. All 66 were run with their exit codes recorded and all exited 0. `--ran` reconciled: 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN. - `pnpm lint` (the full `eslint . --no-inline-config`): exit 0. ## Acceptance notes - `AppPlugin` now clears `app:APPID` before it binds. A first boot is unchanged. If two `AppPlugin`s on one engine share an app id, the later one's set now replaces the earlier one's actions as well; before, it replaced only the hooks. - Seven existing install-local suites (`bundle`, `conflict`, `id-gate`, `list-posture`, `offline-degradation`, `posture-gate`, `storage-dir`) gained a module-top `import '@objectstack/runtime'`. An install or rehydrate now reaches the runtime's lazy import, and its first load inside a 5000ms `it` timed out `posture-gate` and `id-gate` (the clocked-window rule in `scripts/check-test-source-alias.mjs`). The suite now pays that load during collection. No baseline duration was measured. - The two `list_actions` engine fixtures in `http-dispatcher.test.ts` gained `listRegisteredActions()`, listing the keys their `executeAction` already answers. - The spawn pin runs in development through the tsx entry and signs in as the dev-admin seed. Using the production `bin/run.js` entry would add the file to `check:cli-test-child-env`'s pinned roster of six built-entry spawners, which needs an edit to that gate. - The spawn pin does not cover a reinstall that drops an action or hook. The binder and cc unit pins cover it (legs C1 and C2). - The `[AppPlugin|MarketplaceInstallLocal] Bound declarative actions` count is registrations, not distinct handlers. The same action collected from `actions[]` and `objects[].actions[]` counts 2 for one handler. This is unchanged and noted only. - #21322 (flows and permission sets on hot install) is not addressed here. It can reuse `bindAppArtifactHandlers`. --- _Generated by [Claude Code](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ca0dfb6 commit 1d0600b

19 files changed

Lines changed: 1277 additions & 104 deletions
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/runtime': minor
3+
'@objectstack/cloud-connection': patch
4+
---
5+
6+
An app installed with `os package install <artifact>` now runs its `type: 'script'` action bodies and its body hooks, and MCP `list_actions` lists a script action only when `run_action` can run it (#21321).
7+
8+
Clause-②: yes (widening)
9+
10+
- **`@objectstack/runtime`.** New export `bindAppArtifactHandlers(ql, bundle, { appId, logger, source? })`. It binds every action `body` of an artifact through `ql.registerAction`, and every hook `body` and bundle function through `ql.bindHooks`, all under the owner `app:<appId>`. `appArtifactHandlerOwner(appId)` returns that owner key. Each call first removes the action handlers and hooks the same owner bound before. A reinstall therefore leaves one handler per action, and an action or hook that the new version dropped stops running. `AppPlugin.start` now binds through this function, with the same log lines and the same results for a boot artifact.
11+
- **`@objectstack/runtime`, MCP `list_actions`.** A `script` action is listed only when the engine has a handler registered for it. The check reads `listRegisteredActions()` and uses the same object and key order as `run_action`. Before, a declared `target` or `body` was enough to be listed, so `list_actions` could list an action that `run_action` refused with "No handler registered". An engine without `listRegisteredActions` gets no script actions listed. Declarative update actions and `flow` actions are listed as before.
12+
- **`@objectstack/cloud-connection`.** The install-local plugin calls `bindAppArtifactHandlers` on `POST /api/v1/marketplace/install-local` and when it rehydrates its ledger at `kernel:ready`. Before, an installed package's script actions answered REST `404 RESOURCE_NOT_FOUND` and MCP "No handler registered", before and after a restart, and its body hooks never ran. The same artifact booted with `os start --artifact` was not affected.

‎packages/cli/test/package-install-local-handlers.integration.test.ts‎

Lines changed: 398 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* #21321 — an installed package's script-action bodies and body hooks are
5+
* BOUND, on the install route and on the `kernel:ready` rehydrate, through the
6+
* same runtime function `AppPlugin.start` uses for a boot artifact.
7+
*
8+
* The defect: `os package install <artifact>` registered the package's metadata
9+
* and nothing executable. Every door refused the installed `type: 'script'`
10+
* action (REST 404, MCP `run_action` "No handler registered") before and after
11+
* a restart, and its body hooks never fired — while `os start --artifact` of the
12+
* same file dispatched normally, because `AppPlugin.start` was the only binder.
13+
*
14+
* What this file pins, against the engine-level outcome rather than a call:
15+
*
16+
* - install: the action handler is registered under `app:<manifestId>` and
17+
* the body hook is handed to `bindHooks` under the same owner;
18+
* - rehydrate: a ledger entry written by an earlier process binds the same
19+
* way when a fresh plugin reaches `kernel:ready`;
20+
* - reinstall: still exactly one handler per action and one binding per hook;
21+
* - reinstall of a version that DROPPED its action and its hook: neither
22+
* stays bound — the owner's previous set is torn down first.
23+
*
24+
* The binder is the REAL `@objectstack/runtime` export (resolved through its
25+
* `exports`, i.e. its built `dist/`, like the plugin's own lazy import); the
26+
* engine is a recording double that models only what the binder touches —
27+
* including `bindHooksToEngine`'s own rule of unregistering a package's hooks
28+
* only when handed a NON-empty list, which is what the dropped-hook case reads.
29+
*/
30+
31+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
32+
import { mkdtempSync, rmSync } from 'node:fs';
33+
import { join } from 'node:path';
34+
import { tmpdir } from 'node:os';
35+
// The first load of the runtime's dist paid at module top, never inside a
36+
// clocked `it` (the clocked-window rule, `scripts/check-test-source-alias.mjs`):
37+
// the plugin reaches the same module through a dynamic `import()`.
38+
import '@objectstack/runtime';
39+
import { MarketplaceInstallLocalPlugin } from './marketplace-install-local-plugin.js';
40+
import { installerAuthService, withInstallerGrants } from './install-local-principal.fixtures.js';
41+
import { LocalManifestSource } from './local-manifest-source.js';
42+
43+
const APP_ID = 'com.example.tasksapp';
44+
const OWNER = `app:${APP_ID}`;
45+
46+
const ACTION = {
47+
name: 'complete_task',
48+
label: 'Complete Task',
49+
objectName: 'tasks_app_task',
50+
type: 'script',
51+
body: { language: 'js', capabilities: ['api.write'], source: 'return { ok: true };' },
52+
ai: { exposed: true, description: 'Mark a task as complete.' },
53+
};
54+
const HOOK = {
55+
name: 'tasks_app_stamp_status',
56+
object: 'tasks_app_task',
57+
events: ['beforeInsert'],
58+
body: { language: 'js', source: "ctx.input.status = 'stamped';" },
59+
};
60+
61+
/** The compiled-artifact shape `os build` writes and `os package install` sends. */
62+
function artifact(version: string, opts: { withHandlers: boolean }) {
63+
return {
64+
manifest: { id: APP_ID, namespace: 'tasks_app', version, type: 'app', name: 'Tasks App' },
65+
objects: [{
66+
name: 'tasks_app_task',
67+
label: 'Task',
68+
fields: { name: { type: 'text', label: 'Name' } },
69+
...(opts.withHandlers ? { actions: [ACTION] } : {}),
70+
}],
71+
...(opts.withHandlers ? { actions: [ACTION], hooks: [HOOK] } : {}),
72+
};
73+
}
74+
75+
/** The install route's normalization of a compiled bundle, as it lands in the ledger. */
76+
function flattened(bundle: ReturnType<typeof artifact>) {
77+
const { manifest: meta, ...sections } = bundle;
78+
return { ...meta, ...sections };
79+
}
80+
81+
/**
82+
* The engine surface the binder writes to, as state: an action Map keyed
83+
* `<object>:<name>` (the engine's own key) and one hook list.
84+
*/
85+
function recordingEngine() {
86+
const actions = new Map<string, { handler: unknown; package?: string }>();
87+
let hooks: Array<{ name: string; packageId?: string }> = [];
88+
const unregisterHooksByPackage = (packageId: string): number => {
89+
const before = hooks.length;
90+
hooks = hooks.filter((h) => h.packageId !== packageId);
91+
return before - hooks.length;
92+
};
93+
return {
94+
actions,
95+
hooksFor: (packageId: string) => hooks.filter((h) => h.packageId === packageId),
96+
engine: {
97+
syncSchemas: async () => undefined,
98+
registerAction: (object: string, name: string, handler: unknown, pkg?: string) => {
99+
actions.set(`${object}:${name}`, { handler, package: pkg });
100+
},
101+
removeActionsByPackage: (pkg: string) => {
102+
for (const [key, entry] of actions) if (entry.package === pkg) actions.delete(key);
103+
},
104+
listRegisteredActions: () =>
105+
[...actions].map(([key, entry]) => ({
106+
objectName: key.slice(0, key.indexOf(':')),
107+
actionName: key.slice(key.indexOf(':') + 1),
108+
...(entry.package ? { package: entry.package } : {}),
109+
})),
110+
unregisterHooksByPackage,
111+
// `bindHooksToEngine`'s own teardown fires only for a NON-empty list.
112+
bindHooks: (list: Array<{ name: string }> | undefined, opts?: { packageId?: string; bodyRunner?: unknown }) => {
113+
if (!Array.isArray(list) || list.length === 0) return;
114+
if (opts?.packageId) unregisterHooksByPackage(opts.packageId);
115+
expect(typeof opts?.bodyRunner, 'a body hook needs the sandbox runner').toBe('function');
116+
for (const h of list) hooks.push({ name: h.name, packageId: opts?.packageId });
117+
},
118+
},
119+
};
120+
}
121+
122+
type Handler = (c: any) => Promise<any>;
123+
124+
function makeRawApp() {
125+
const routes = new Map<string, Handler>();
126+
return {
127+
routes,
128+
get: (p: string, h: Handler) => routes.set(`GET ${p}`, h),
129+
post: (p: string, h: Handler) => routes.set(`POST ${p}`, h),
130+
delete: (p: string, h: Handler) => routes.set(`DELETE ${p}`, h),
131+
};
132+
}
133+
134+
function makeCtx(rawApp: any, services: Record<string, any>) {
135+
const hooks = new Map<string, any>();
136+
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
137+
return {
138+
logger,
139+
ctx: {
140+
hook: (e: string, h: any) => hooks.set(e, h),
141+
getService: (name: string) => {
142+
if (name === 'http-server') return { getRawApp: () => rawApp };
143+
const svc = services[name];
144+
if (svc === undefined) throw new Error(`no ${name}`);
145+
return svc;
146+
},
147+
logger,
148+
},
149+
fire: async () => { await hooks.get('kernel:ready')?.(); },
150+
};
151+
}
152+
153+
function makeC(body: any) {
154+
const json = vi.fn((payload: any, status?: number) => ({ payload, status: status ?? 200 }));
155+
return {
156+
req: {
157+
url: 'http://localhost:3000/api/v1/marketplace/install-local',
158+
raw: new Request('http://localhost:3000/x'),
159+
json: async () => body,
160+
param: () => undefined,
161+
},
162+
json,
163+
};
164+
}
165+
166+
let dir: string;
167+
beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'mil-handlers-')); });
168+
afterEach(() => { rmSync(dir, { recursive: true, force: true }); vi.restoreAllMocks(); });
169+
170+
async function bootPlugin(engine: Record<string, unknown>) {
171+
const rawApp = makeRawApp();
172+
const { ctx, fire, logger } = makeCtx(rawApp, {
173+
manifest: { register: vi.fn() },
174+
auth: installerAuthService(),
175+
objectql: withInstallerGrants(engine),
176+
});
177+
const plugin = new MarketplaceInstallLocalPlugin({ controlPlaneUrl: 'off', storageDir: dir });
178+
await plugin.start(ctx as any);
179+
await fire();
180+
const install = async (body: unknown) => {
181+
const res = await rawApp.routes.get('POST /api/v1/marketplace/install-local')!(makeC({ manifest: body }));
182+
expect(res.payload?.success, JSON.stringify(res.payload)).toBe(true);
183+
return res;
184+
};
185+
return { install, logger };
186+
}
187+
188+
describe('#21321: install-local binds an installed package’s handlers', () => {
189+
it('install — the script action is registered and the body hook is bound, both under app:<manifestId>', async () => {
190+
const rec = recordingEngine();
191+
const { install } = await bootPlugin(rec.engine);
192+
193+
await install(artifact('0.1.0', { withHandlers: true }));
194+
195+
const entry = rec.actions.get('tasks_app_task:complete_task');
196+
expect(entry, 'the installed script action has no handler — every door refuses it').toBeDefined();
197+
expect(entry!.package).toBe(OWNER);
198+
expect(typeof entry!.handler).toBe('function');
199+
expect(rec.hooksFor(OWNER).map((h) => h.name)).toEqual(['tasks_app_stamp_status']);
200+
});
201+
202+
it('rehydrate — a ledger entry from an earlier process is bound at kernel:ready', async () => {
203+
new LocalManifestSource(dir).write({
204+
packageId: APP_ID,
205+
versionId: 'local',
206+
manifestId: APP_ID,
207+
version: '0.1.0',
208+
// What the install route persists: the compiled bundle, flattened
209+
// (its `manifest` meta lifted to the top level beside the sections).
210+
manifest: flattened(artifact('0.1.0', { withHandlers: true })),
211+
installedAt: '2026-01-01T00:00:00.000Z',
212+
installedBy: 'admin',
213+
withSampleData: false,
214+
});
215+
const rec = recordingEngine();
216+
await bootPlugin(rec.engine);
217+
218+
expect(rec.actions.get('tasks_app_task:complete_task')?.package, 'a restart leaves the installed action unbound').toBe(OWNER);
219+
expect(rec.hooksFor(OWNER).map((h) => h.name)).toEqual(['tasks_app_stamp_status']);
220+
});
221+
222+
it('reinstall — still exactly one handler per action and one binding per hook', async () => {
223+
const rec = recordingEngine();
224+
const { install } = await bootPlugin(rec.engine);
225+
226+
await install(artifact('0.1.0', { withHandlers: true }));
227+
await install(artifact('0.1.0', { withHandlers: true }));
228+
229+
const owned = rec.engine.listRegisteredActions().filter((r) => r.package === OWNER);
230+
expect(owned).toEqual([{ objectName: 'tasks_app_task', actionName: 'complete_task', package: OWNER }]);
231+
expect(rec.hooksFor(OWNER)).toHaveLength(1);
232+
});
233+
234+
it('reinstall of a version that dropped its action and hook — neither stays bound', async () => {
235+
const rec = recordingEngine();
236+
const { install } = await bootPlugin(rec.engine);
237+
238+
await install(artifact('0.1.0', { withHandlers: true }));
239+
expect(rec.actions.size, 'precondition: the first version bound its action').toBe(1);
240+
241+
await install(artifact('0.2.0', { withHandlers: false }));
242+
243+
expect(rec.engine.listRegisteredActions().filter((r) => r.package === OWNER)).toEqual([]);
244+
expect(rec.hooksFor(OWNER), 'a hook the new version dropped must stop firing').toEqual([]);
245+
});
246+
});

‎packages/cloud-connection/src/marketplace-install-local-bundle.test.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
1212
import { mkdtempSync, rmSync } from 'node:fs';
1313
import { join } from 'node:path';
1414
import { tmpdir } from 'node:os';
15+
// [#21321] An install, and a rehydrate of a ledger entry, now bind the package's
16+
// handlers through `@objectstack/runtime` (a lazy `import()` inside the plugin).
17+
// Its first load is paid here, at module top — never inside a clocked `it`
18+
// (the clocked-window rule, `scripts/check-test-source-alias.mjs`).
19+
import '@objectstack/runtime';
1520
import { MarketplaceInstallLocalPlugin } from './marketplace-install-local-plugin.js';
1621
import { installerAuthService, withInstallerGrants } from './install-local-principal.fixtures.js';
1722

‎packages/cloud-connection/src/marketplace-install-local-conflict.test.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
1414
import { mkdtempSync, rmSync, readdirSync } from 'node:fs';
1515
import { join } from 'node:path';
1616
import { tmpdir } from 'node:os';
17+
// [#21321] An install, and a rehydrate of a ledger entry, now bind the package's
18+
// handlers through `@objectstack/runtime` (a lazy `import()` inside the plugin).
19+
// Its first load is paid here, at module top — never inside a clocked `it`
20+
// (the clocked-window rule, `scripts/check-test-source-alias.mjs`).
21+
import '@objectstack/runtime';
1722
import { MarketplaceInstallLocalPlugin } from './marketplace-install-local-plugin.js';
1823
import { installerAuthService, withInstallerGrants } from './install-local-principal.fixtures.js';
1924

‎packages/cloud-connection/src/marketplace-install-local-id-gate.test.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ import { join } from 'node:path';
3838
import { tmpdir } from 'node:os';
3939
import { manifestIdRefusal } from '@objectstack/spec/kernel';
4040
import { BaseResponseSchema, ApiErrorSchema, envelopeViolations } from '@objectstack/spec/api';
41+
// [#21321] An install, and a rehydrate of a ledger entry, now bind the package's
42+
// handlers through `@objectstack/runtime` (a lazy `import()` inside the plugin).
43+
// Its first load is paid here, at module top — never inside a clocked `it`
44+
// (the clocked-window rule, `scripts/check-test-source-alias.mjs`).
45+
import '@objectstack/runtime';
4146
import { MarketplaceInstallLocalPlugin } from './marketplace-install-local-plugin.js';
4247
import { LocalManifestSource } from './local-manifest-source.js';
4348
import { installerAuthService, withInstallerGrants } from './install-local-principal.fixtures.js';

‎packages/cloud-connection/src/marketplace-install-local-list-posture.test.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ import { mkdtempSync, rmSync } from 'node:fs';
6262
import { join } from 'node:path';
6363
import { tmpdir } from 'node:os';
6464

65+
// [#21321] An install, and a rehydrate of a ledger entry, now bind the package's
66+
// handlers through `@objectstack/runtime` (a lazy `import()` inside the plugin).
67+
// Its first load is paid here, at module top — never inside a clocked `it`
68+
// (the clocked-window rule, `scripts/check-test-source-alias.mjs`).
69+
import '@objectstack/runtime';
6570
import { MarketplaceInstallLocalPlugin } from './marketplace-install-local-plugin.js';
6671
import { LocalManifestSource } from './local-manifest-source.js';
6772

‎packages/cloud-connection/src/marketplace-install-local-offline-degradation.test.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ import { mkdtempSync, rmSync } from 'node:fs';
2828
import { join } from 'node:path';
2929
import { tmpdir } from 'node:os';
3030

31+
// [#21321] An install, and a rehydrate of a ledger entry, now bind the package's
32+
// handlers through `@objectstack/runtime` (a lazy `import()` inside the plugin).
33+
// Its first load is paid here, at module top — never inside a clocked `it`
34+
// (the clocked-window rule, `scripts/check-test-source-alias.mjs`).
35+
import '@objectstack/runtime';
3136
import { MarketplaceInstallLocalPlugin } from './marketplace-install-local-plugin.js';
3237
import { installerAuthService, withInstallerGrants } from './install-local-principal.fixtures.js';
3338

0 commit comments

Comments
 (0)