Skip to content

Commit f14d7b5

Browse files
claude[bot]claude
andauthored
test(cli): pin both halves of Connect-an-Agent visibility over the wire (#17832)
Fixes #17647 Clause-②: no Commits the composed measurement #16746's delivering dev took once and deleted because it fell outside that card's declared file surface. This is a transcription of a measurement, not a redesign: the harness, the four readings and the argument for where it lives all come from #16746's `os-dev-report` (`5631274063`) and this card's triage ruling (`5642958095`). ## The four readings, as measured on this branch All three numbers reproduce the dispatch table exactly — nothing was adjusted to match. | | principal | route | measured | |:--|:--|:--|:--| | **M1** | permissionless | `GET /api/v1/meta/apps/account` | **200**, **12** nav ids, `grp_account_developer` = `nav_account_api_keys` · `nav_account_oauth_apps` · `nav_connect_agent` | | **M2** | **the same** principal | `GET /api/v1/meta/apps/setup` | **403**, `error.code` = `PERMISSION_DENIED`, `connect_agent` absent from the whole body | | **M3** | `setup.access` + `manage_platform_settings` | `GET /api/v1/meta/apps/setup` | **200**, **34** ids, `nav_connect_agent` present under `group_integrations` — the lit positive control | | **M4** | `setup.access` + `manage_platform_settings` | Setup entry count | **34**, byte-for-byte identical to a composition folding only the bundle's `app: 'setup'` half | Where 34 comes from, so the number is checkable rather than copied: `SETUP_APP` declares 9 nav groups; `SETUP_NAV_CONTRIBUTIONS` fills 7 of them with 25 entries and the MCP bundle adds 1 to `group_integrations`; `group_approvals` receives nothing and the filter collapses the empty group. 9 − 1 + 25 + 1 = 34. Account is 11 authored ids + the 1 contributed = 12. M1 and M2 are asserted in **one** `it()`, with M3 beside them, because the pairing is the pin: a test asserting only M1 would stay green if someone ungated Setup, and a 403 with no lit control proves the harness ran exactly as much as it proves the gate held. ## What is actually under test The real `SETUP_APP` / `ACCOUNT_APP` / `SETUP_NAV_CONTRIBUTIONS`, the real `CONNECT_AGENT_UI_BUNDLE`, the real `SchemaRegistry.applyNavContributions` fold and the real `RestServer` RBAC-by-route filter, driven through the registered `GET /api/v1/meta/:type/:name` handler with the stub-the-exec-context pattern from `packages/rest/src/meta-app-publish-gate.test.ts`. `packages/cli` is the only workspace package depending on all four at once — the same argument `packages/cli/scripts/check-app-nav-i18n.mjs` makes in its own header. The fold and the filter are ⛔ not reimplemented anywhere. Covered for free, per the card: `SETUP_APP.requiredPermissions = ['setup.access']` has no pin of its own today; M2 + M3 cover that the gate is still declared at all. ## Ablation — the pin goes RED on the failure the card names One leg, committed first, mutation proven on disk and in the built artifact before the colour was read. `packages/cli` resolves `@objectstack/platform-objects` through `exports` to its `dist/`, so each leg is edit → rebuild → preflight → run. - **MUTATE** — delete `requiredPermissions: ['setup.access']` from the real `SETUP_APP` (i.e. ungate Setup), with `label: 'Setup'` → `label: 'Setup ABLATED17647'` in the same object literal as the dist marker. `ablation-dist-preflight @objectstack/platform-objects ABLATED17647` → *marker present in 4 built files*; the gate literal is gone from `dist/index.js` (1 → 0). Pin run: **1 failed / 1 passed**, `AssertionError: expected 200 to be 403` on the M2 assertion. - **RESTORE** — `git checkout HEAD -- THE_MUTATED_PATH`, `git hash-object` equal to the HEAD blob `82fdad66e787b0459b43a4814257e6b916c7b0b2`, whole-tree `git status --porcelain` empty, rebuild, `--absent` preflight *marker absent from all 66 built files*, gate literal back to 1. Pin run: **2 passed**. ⚠️ Observed direction reported as observed, not as predicted: M4 stayed **green** under the ungating. That is correct — M4 measures the admin tree's invariance against the account-side half, which dropping the app-level gate does not move. The leg that reddens M4 would be a mutation of the bundle's contribution targets. The two edits and both legs are throwaway; nothing from the ablation is in this diff. ## Verification Union run against the final commit `524a9fe04`. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on the final 3-path diff → 48 families. Each run with output redirected to its own log and `$?` captured **before** any pipe, recorded as `command :: exit N`, then reconciled: *"Run reconciliation — 48 derived, 48 run, 0 NOT-MEASURED, 0 UNRUN"* with *"EXIT CODES — all 48 accounted families carry one"*. **All 48 exit 0.** - Two the tool does not name and this lane adds: `pnpm lint` repo-wide to completion (`eslint . --no-inline-config`, 94s on a shared box, **exit 0**) — so no narrowing is claimed and none of the three narrowing evidences is owed; and `node packages/cli/scripts/check-app-nav-i18n.mjs` **exit 0**, hit line *"OK (10 contributor(s), 54 merged `setup` nav id(s), 4 locale(s), every id labelled in every locale)"*, plus its `--self-test` exit 0. - Receiving package on the merged tree, one lock hold, `VERDICT command-exit 0`: `pnpm --filter @objectstack/cli typecheck` (*"check:test-typecheck: OK — 3 file(s) / 28 error(s)"*, the pre-existing ledgered population, **zero in the new file**) and `pnpm --filter @objectstack/cli exec vitest run --project unit` → **199 files / 2831 tests passed**. - Typecheck coverage proven rather than assumed: `tsc --noEmit --listFiles -p tsconfig.test.json` counts the new file **1** of 2275 program files. - `integration` tier declared to CI: this diff touches no integration-tier file, no spawn entry and no driver/kernel boot path. - `origin/main` merged per Multi-agent discipline §9/§10 (`packages/rest/src/index.ts` moved on the incoming side and this package's test program now reads rest's source); clean merge, **no os-regen deferral recorded**, install + closure rebuild + the whole verification above re-run on the merge result. ## Two consequences of the new file, both the gates' own prescriptions 1. `packages/cli/vitest.config.ts` — this is the first `new SchemaRegistry(` in `packages/cli` (measured: 0 occurrences at the base tree), so `check-registry-log-declared` now selects this package. A root-level `env` is inert under `projects`, so `env: { OS_REGISTRY_LOG: 'warn' }` is declared inside **each** project's own test block, verbatim as the gate prints it. 2. `packages/cli/tsconfig.test.json` — `check:type-source-resolution` flagged `@objectstack/rest` as a NEW dist-resolved type import for this package. Takes the gate's prescribed route, a star-less `paths` rule for a dep exposed through an **existing** program, rather than widening its shrink-only registry; `rootDir` already spans the repo root and rest publishes only `"."`. Both gates are green above. ## Changeset — deliberately skipped, with the reading Nothing published moves. `@objectstack/cli`'s `files[]` is `["dist", "README.md", "CHANGELOG.md"]` and `tsconfig.build.json` declares `include: ["src"]`, so `test/` is not compiled and not shipped. Built-output grep over the 508 files of `packages/cli/dist`: - negative — `connect-agent-both-halves-wire`, `grp_account_developer`, `composedRegistry`, `BOTH-HALVES`: **0 files each**; - positive control — `artifactPackagesOf` **2**, `nav-contribution-groups` **6**, `findNavGroupDiagnostics` **4**. So the zero is a reading, not an empty grep. `skip-changeset` applies. ## Serial constraints #17528 is in flight in this lane; its file set (fetched and diffed, not assumed) is `packages/cli/src/commands/lint.ts`, `test/fixtures/option-b-collection-zoo.ts`, `test/fixtures/option-b-reader-probe.ts`, `test/lint-handwritten-checks-package-fold.test.ts`, `test/option-b-reader-acceptance.pin.test.ts` and its changeset — **disjoint** from all three paths here, `packages/cli/vitest.config.ts` and `packages/cli/tsconfig.test.json` included. --- _Generated by [Claude Code](https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 54e8234 commit f14d7b5

3 files changed

Lines changed: 301 additions & 2 deletions

File tree

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// #17647 — the BOTH-HALVES WIRE pin for Connect-an-Agent visibility.
4+
//
5+
// ---------------------------------------------------------------------------
6+
// What this pins, and why one half of it would be worthless
7+
// ---------------------------------------------------------------------------
8+
// #16746's ruling (option A) promises two facts at once, and they are only a
9+
// promise TOGETHER:
10+
//
11+
// 1. a PERMISSIONLESS principal gets `200` on `GET /api/v1/meta/apps/account`
12+
// with `nav_connect_agent` present in `grp_account_developer`; and
13+
// 2. that SAME principal still gets `403` / `PERMISSION_DENIED` on
14+
// `GET /api/v1/meta/apps/setup`, with `connect_agent` absent from the body.
15+
//
16+
// A test asserting only (1) would stay green if someone ungated Setup — which
17+
// is not hypothetical: #16746's own history produced exactly that once, and
18+
// dropping Setup's two gates was measured to expose 14+ unrelated Setup
19+
// surfaces (Packages, Users, Organization, Business Units, Teams, Invitations,
20+
// Localization, Company, Branding, Feature Flags and their groups) to every
21+
// signed-in user. The PAIRING is the pin.
22+
//
23+
// ---------------------------------------------------------------------------
24+
// Why the positive control is not optional
25+
// ---------------------------------------------------------------------------
26+
// A `403` with no LIT control proves the harness ran exactly as much as it
27+
// proves the gate held — which is to say, neither. So the permitted principal's
28+
// reading sits in the same `it()` as the refusal: if the fold or the fixture
29+
// ever stops reaching the Connect-an-Agent card at all, the control goes red
30+
// instead of the refusal going quietly vacuous.
31+
//
32+
// ---------------------------------------------------------------------------
33+
// It lives in `packages/cli/test/` on purpose
34+
// ---------------------------------------------------------------------------
35+
// This is a COMPOSED fact: the real `SETUP_APP` / `ACCOUNT_APP` /
36+
// `SETUP_NAV_CONTRIBUTIONS` (`@objectstack/platform-objects`), the real
37+
// `CONNECT_AGENT_UI_BUNDLE` (`@objectstack/mcp`), the real `SchemaRegistry`
38+
// fold (`@objectstack/objectql`) and the real per-request RBAC-by-route filter
39+
// (`@objectstack/rest`). Read from `package.json`, `packages/cli` is the only
40+
// workspace package that depends on all four at once — the same argument
41+
// `packages/cli/scripts/check-app-nav-i18n.mjs` already makes in its own
42+
// header for living here ("`cli` is the composition root … a gate in
43+
// `platform-objects` could not import the plugins").
44+
//
45+
// ⛔ It is NOT beside the change in `packages/mcp`: that package declares no
46+
// dependency on `@objectstack/rest`, `@objectstack/objectql` or
47+
// `@objectstack/platform-objects`, and reimplementing the fold or the RBAC
48+
// filter there to keep the test local is precisely the divergence
49+
// `packages/cli/src/utils/nav-contribution-groups.ts` exists to refuse.
50+
//
51+
// ---------------------------------------------------------------------------
52+
// The harness is the established one
53+
// ---------------------------------------------------------------------------
54+
// Stub-the-exec-context, from `packages/rest/src/meta-app-publish-gate.test.ts`
55+
// and `meta-app-area-nav-gate.test.ts`: a real `RestServer` over a stubbed
56+
// protocol, `resolveExecCtx` replaced by a caller carrying a given
57+
// `systemPermissions` set, driven through the registered
58+
// `GET /api/v1/meta/:type/:name` route. Nothing about the gate is re-expressed
59+
// here — the assertions read the RESPONSE BODY, which is the only surface the
60+
// console and an AI client ever see.
61+
//
62+
// ⚠️ The two `requiresService` / `requiresObject` gates are deliberately NOT
63+
// exercised: the stubbed context carries no `__kernel` and no service provider,
64+
// so ADR-0057 D10's capability probe fails OPEN exactly as it does in the two
65+
// sibling harnesses. Permission gating is what is under test.
66+
67+
import { describe, expect, it, vi } from 'vitest';
68+
import { CONNECT_AGENT_UI_BUNDLE } from '@objectstack/mcp';
69+
import { SchemaRegistry } from '@objectstack/objectql';
70+
import { ACCOUNT_APP, SETUP_APP, SETUP_NAV_CONTRIBUTIONS } from '@objectstack/platform-objects/apps';
71+
import { RestServer } from '@objectstack/rest';
72+
73+
type AnyRec = Record<string, any>;
74+
75+
/** The two nav-contribution halves the MCP bundle ships, read by target app. */
76+
const bundleContributions = (): AnyRec[] =>
77+
(CONNECT_AGENT_UI_BUNDLE as AnyRec).navigationContributions as AnyRec[];
78+
79+
/**
80+
* Build the real composition.
81+
*
82+
* `structuredClone` on the two apps is deliberate and changes no content:
83+
* `registerItem` runs `applyProtection` on what it is handed, which WRITES the
84+
* `_lock` envelope onto the object (ADR-0010 §3.7). Handing it the imported
85+
* module constants would mutate them for every other file in this suite.
86+
*
87+
* @param includeAccountHalf when false, only the MCP bundle's `app: 'setup'`
88+
* contribution is folded — the counterfactual M4 reads Setup against.
89+
*/
90+
function composedRegistry(includeAccountHalf = true): SchemaRegistry {
91+
const registry = new SchemaRegistry({ multiTenant: false, collisionPolicy: 'error' });
92+
// The fold's own diagnostics are not under test here; keep the run quiet.
93+
(registry as AnyRec).logLevel = 'silent';
94+
95+
registry.registerApp(structuredClone(SETUP_APP), '@objectstack/platform-objects');
96+
registry.registerApp(structuredClone(ACCOUNT_APP), '@objectstack/platform-objects');
97+
98+
for (const contribution of SETUP_NAV_CONTRIBUTIONS) {
99+
registry.registerAppNavContribution(contribution as any, '@objectstack/platform-objects');
100+
}
101+
for (const contribution of bundleContributions()) {
102+
if (!includeAccountHalf && contribution.app !== 'setup') continue;
103+
registry.registerAppNavContribution(contribution as any, '@objectstack/mcp');
104+
}
105+
return registry;
106+
}
107+
108+
function createMockServer() {
109+
return {
110+
get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn(), patch: vi.fn(), use: vi.fn(),
111+
listen: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined),
112+
};
113+
}
114+
115+
function makeRes() {
116+
const res: AnyRec = { statusCode: 200, body: undefined };
117+
res.status = vi.fn((c: number) => { res.statusCode = c; return res; });
118+
res.json = vi.fn((b: unknown) => { res.body = b; return res; });
119+
res.header = vi.fn(); res.setHeader = vi.fn(); res.write = vi.fn(); res.end = vi.fn();
120+
return res;
121+
}
122+
123+
/**
124+
* A `RestServer` serving the composed apps to a caller holding `perms`.
125+
*
126+
* The protocol answers `getMetaItem` out of the registry, so the bytes under
127+
* test have gone through `applyNavContributions` — the real fold — exactly as
128+
* the serving path does (ADR-0029 D7: "the REST app endpoints read through the
129+
* protocol, not these helpers, so the merge must be reachable from there too").
130+
*/
131+
function setup(perms: string[], includeAccountHalf = true) {
132+
const registry = composedRegistry(includeAccountHalf);
133+
const protocol: AnyRec = {
134+
getDiscovery: vi.fn().mockResolvedValue({
135+
version: 'v0', routes: { data: '', metadata: '', ui: '', auth: '/auth' },
136+
}),
137+
getMetaTypes: vi.fn().mockResolvedValue([]),
138+
getMetaItems: vi.fn(async ({ type }: AnyRec) => {
139+
const t = String(type ?? '');
140+
return t === 'app' || t === 'apps' ? registry.getAllApps() : [];
141+
}),
142+
getMetaItem: vi.fn(async ({ name }: AnyRec) => {
143+
const item = registry.getApp(String(name));
144+
return item ? { type: 'app', name, item } : undefined;
145+
}),
146+
findData: vi.fn().mockResolvedValue([]),
147+
};
148+
const rest: AnyRec = new RestServer(
149+
createMockServer() as any, protocol as any, { api: { requireAuth: false } } as any,
150+
);
151+
// The RBAC filter only runs for a RESOLVED caller; stubbing the exec
152+
// context is this repo's established pattern for exercising it by route.
153+
rest.resolveExecCtx = async () => ({ userId: 'u1', systemPermissions: perms });
154+
rest.registerRoutes();
155+
return rest;
156+
}
157+
158+
/** `GET /api/v1/meta/apps/:name`, answered as `{ statusCode, body }`. */
159+
async function getApp(rest: AnyRec, name: string) {
160+
const route = rest.getRoutes().find(
161+
(r: AnyRec) => r.method === 'GET' && r.path === '/api/v1/meta/:type/:name',
162+
);
163+
if (!route) throw new Error('meta/:type/:name route not registered');
164+
const res = makeRes();
165+
await route.handler(
166+
{ method: 'GET', params: { type: 'apps', name }, query: {}, body: {}, headers: {} }, res,
167+
);
168+
return res;
169+
}
170+
171+
/** Every nav id in the served document, groups included, depth-first. */
172+
function navIds(app: AnyRec | undefined): string[] {
173+
const out: string[] = [];
174+
const walk = (items: AnyRec[] | undefined) => {
175+
for (const item of items ?? []) {
176+
if (item?.id) out.push(String(item.id));
177+
if (Array.isArray(item?.children)) walk(item.children);
178+
}
179+
};
180+
walk(app?.navigation);
181+
for (const area of app?.areas ?? []) walk(area?.navigation);
182+
return out;
183+
}
184+
185+
/** The children of one nav group, by id. */
186+
function groupChildren(app: AnyRec | undefined, groupId: string): string[] {
187+
let found: AnyRec | undefined;
188+
const walk = (items: AnyRec[] | undefined) => {
189+
for (const item of items ?? []) {
190+
if (item?.id === groupId) { found = item; return; }
191+
if (Array.isArray(item?.children)) walk(item.children);
192+
if (found) return;
193+
}
194+
};
195+
walk(app?.navigation);
196+
return (found?.children ?? []).map((c: AnyRec) => String(c?.id));
197+
}
198+
199+
/** The declared ADR-0112 refusal envelope, as the console reads it. */
200+
const refusal = (body: AnyRec | undefined) => ({
201+
code: body?.error?.code, message: body?.error?.message,
202+
});
203+
204+
describe('#17647 — Connect-an-Agent visibility: BOTH halves, over the wire', () => {
205+
it('a permissionless principal sees the Account entry AND is still refused Setup', async () => {
206+
// ONE principal, two routes — the pairing is the pin, so both readings
207+
// and the lit control are asserted here rather than in three `it()`s
208+
// that could pass one at a time.
209+
const rest = setup([]);
210+
211+
// ── M1 — permissionless on `GET /api/v1/meta/apps/account` ─────────
212+
const account = await getApp(rest, 'account');
213+
expect(account.statusCode).toBe(200);
214+
expect(account.body).toMatchObject({ type: 'app', name: 'account' });
215+
216+
// `ACCOUNT_APP` declares no `requiredPermissions` — deliberately, so
217+
// every authenticated user reaches their own security surface — and the
218+
// fold puts the contributed entry inside the group that already carries
219+
// `nav_account_api_keys`.
220+
expect(groupChildren(account.body?.item, 'grp_account_developer'))
221+
.toEqual(['nav_account_api_keys', 'nav_account_oauth_apps', 'nav_connect_agent']);
222+
// The whole served tree, counted: 11 authored ids + the one contributed.
223+
expect(navIds(account.body?.item)).toHaveLength(12);
224+
// …and the entry's TARGET rode along, so the menu item resolves.
225+
expect(JSON.stringify(account.body)).toContain('connect_agent');
226+
227+
// ── M2 — the SAME principal on `GET /api/v1/meta/apps/setup` ───────
228+
const setupDenied = await getApp(rest, 'setup');
229+
expect(setupDenied.statusCode).toBe(403);
230+
// ⛔ The status alone is not the assertion: a bare 403 cannot tell a
231+
// permission refusal from a route that was never reached. ADR-0112's
232+
// STANDARD catalog member for an authorization refusal, nested inside
233+
// `error` per `BaseResponseSchema`, is what objectui branches on.
234+
expect(refusal(setupDenied.body).code).toBe('PERMISSION_DENIED');
235+
expect(setupDenied.body?.success).toBe(false);
236+
expect(typeof refusal(setupDenied.body).message).toBe('string');
237+
238+
// Refused means refused, judged on the WIRE BYTES rather than on "not
239+
// in the nav group": neither the page name nor the nav id leaked.
240+
const deniedWire = JSON.stringify(setupDenied.body ?? {});
241+
expect(deniedWire).not.toContain('connect_agent');
242+
expect(setupDenied.body?.item).toBeUndefined();
243+
244+
// ── M3 — the LIT positive control, same composition ────────────────
245+
// Without this, M2's 403 proves the gate held exactly as much as it
246+
// proves the harness never reached the card.
247+
const permitted = setup(['setup.access', 'manage_platform_settings']);
248+
const setupAllowed = await getApp(permitted, 'setup');
249+
expect(setupAllowed.statusCode).toBe(200);
250+
expect(setupAllowed.body?.error).toBeUndefined();
251+
const allowedIds = navIds(setupAllowed.body?.item);
252+
expect(allowedIds).toContain('nav_connect_agent');
253+
expect(groupChildren(setupAllowed.body?.item, 'group_integrations'))
254+
.toContain('nav_connect_agent');
255+
// The Setup tree the platform admin is served: 9 authored groups less
256+
// `group_approvals`, which nothing contributes into and the filter
257+
// collapses, plus 25 contributed entries plus the MCP one.
258+
expect(allowedIds).toHaveLength(34);
259+
});
260+
261+
it('the Account half widened NOTHING on Setup — the admin tree is unchanged', async () => {
262+
// ── M4 — anti-widening control ─────────────────────────────────────
263+
// `SETUP_APP.requiredPermissions = ['setup.access']` has no pin of its
264+
// own anywhere in `packages/platform-objects`; this reading covers that
265+
// the gate is still DECLARED, and that adding the per-user half moved
266+
// nothing on the admin side. The counterfactual folds only the bundle's
267+
// `app: 'setup'` contribution.
268+
const withAccountHalf = await getApp(setup(['setup.access', 'manage_platform_settings']), 'setup');
269+
const withoutAccountHalf = await getApp(
270+
setup(['setup.access', 'manage_platform_settings'], false), 'setup',
271+
);
272+
273+
expect(withAccountHalf.statusCode).toBe(200);
274+
expect(withoutAccountHalf.statusCode).toBe(200);
275+
expect(navIds(withAccountHalf.body?.item)).toEqual(navIds(withoutAccountHalf.body?.item));
276+
expect(navIds(withAccountHalf.body?.item)).toHaveLength(34);
277+
278+
// The mechanism behind the invariance, asserted rather than assumed:
279+
// the bundle aims exactly one contribution at `setup` and one at
280+
// `account`, and the registry keys contributions by TARGET APP.
281+
expect(bundleContributions().map((c) => c.app)).toEqual(['setup', 'account']);
282+
});
283+
});

‎packages/cli/tsconfig.test.json‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@
126126
"moduleResolution": "bundler",
127127
"lib": ["ES2022", "DOM", "DOM.Iterable"],
128128
"types": ["node"],
129-
// [#15004, #15229] FOUR bare-name rules, no star, for the four workspace
129+
// [#15004, #15229] FIVE bare-name rules, no star, for the five workspace
130130
// deps the option-B acceptance pin reaches
131131
// (`test/option-b-reader-acceptance.pin.test.ts` and its two fixtures;
132132
// `@objectstack/verify` joined them with card 5/4's probe rows). Without them tsc resolves those specifiers through
@@ -160,6 +160,13 @@
160160
// package publishes only `"."`.
161161
"@objectstack/plugin-dev": ["../plugins/plugin-dev/src/index.ts"],
162162
"@objectstack/plugin-security": ["../plugins/plugin-security/src/index.ts"],
163+
// [#17647] The fifth, same reason and same no-star rule: the composed
164+
// Connect-an-Agent wire pin (`test/connect-agent-both-halves-wire.pin.test.ts`)
165+
// drives the REAL `RestServer` RBAC-by-route filter, so a type verdict
166+
// resolved through `exports` to rest's **dist** would be a verdict about
167+
// the last build rather than about the filter the pin is about. The
168+
// package publishes only `"."`.
169+
"@objectstack/rest": ["../rest/src/index.ts"],
163170
"@objectstack/runtime": ["../runtime/src/index.ts"],
164171
"@objectstack/verify": ["../verify/src/index.ts"]
165172
}

‎packages/cli/vitest.config.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -705,13 +705,18 @@ export default defineConfig({
705705
// "THE NIGHTLY TIERS" for the population both read. Both `extends: true`
706706
// so each project inherits the `resolve.alias` table and the
707707
// `server.deps.external` entry above; each repeats the console-intercept
708-
// disarm because the root-level one is inert under projects.
708+
// disarm — and, since #17647, the registry log level — because a
709+
// root-level value is inert under projects.
709710
projects: [
710711
{
711712
extends: true,
712713
test: {
713714
name: 'unit',
714715
disableConsoleIntercept: true,
716+
// #13517: quiet the registry's per-item registration chatter — the
717+
// engine's own `OS_REGISTRY_LOG` seam, not a change to its shipped
718+
// default. Enforced by scripts/check-registry-log-declared.mjs.
719+
env: { OS_REGISTRY_LOG: 'warn' },
715720
include: UNIT_FILES,
716721
},
717722
},
@@ -720,6 +725,10 @@ export default defineConfig({
720725
test: {
721726
name: 'integration',
722727
disableConsoleIntercept: true,
728+
// #13517: quiet the registry's per-item registration chatter — the
729+
// engine's own `OS_REGISTRY_LOG` seam, not a change to its shipped
730+
// default. Enforced by scripts/check-registry-log-declared.mjs.
731+
env: { OS_REGISTRY_LOG: 'warn' },
723732
include: INTEGRATION_FILES,
724733
},
725734
},

0 commit comments

Comments
 (0)