Skip to content

Commit ea053b4

Browse files
fix(cli): os build leaves devPlugins out of the artifact; os validate --target stops reading it as code (#31276)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XR4fpPzKKft83b8NgK8NESr)_ Part of #31264 — this PR does items 1 and 3 and the cli half of item 2. The `RestrictionProfileSchema` docblock sentence (spec, `allowCode` rule) is left to the spec seat after this lands; the exact sentence is under "Spec docblock owed" below. Clause-②: yes (widening) — two accept-sets widen: `os validate --target` with `allowCode: false` now accepts a stack that declares `devPlugins`, and an unmanaged install (`os package install --install-mode unmanaged`, and the cloud-connection local installs) of a freshly built artifact of such a stack is no longer refused, because that artifact no longer carries the key. Leaving the key out of `objectstack.json` refuses nothing (measured below), so there is no narrowing arm. Metadata-protocol: none ## 维护者速读 - 改了什么:`os build` / `os compile` 写产物时去掉 `devPlugins`,和已经去掉的 `demoStaff` 同一处;`os validate --target` 在 `allowCode: false` 下不再把 `devPlugins` 当代码拒绝。 - 为什么改:`devPlugins` 只在本地 `--dev` 启动时从配置加载,产物里没有任何启动或安装会读它,但 `os package publish` / `os deploy` 会把产物原样送到目标环境,于是限制配置只能把它当代码拒掉。 - 风险与回滚:只是产物少一个开发用的键;`os dev` / `os serve --dev` 仍从配置加载它;旧产物带这个键照样能装。仓内没有读产物 `devPlugins` 的代码;cloud 仓本会话够不到,未能核验。回滚即还原本 PR。 - 先看:`packages/cli/src/commands/compile.ts`(`delete finalBundle.devPlugins`),`packages/cli/src/utils/restriction-profile.ts`(`NEVER_REACHES_TARGET`)。 - 两分钟验证:`pnpm --filter @objectstack/cli exec vitest run src/utils/restriction-profile.test.ts test/build-demo-staff-artifact.test.ts`,预期全绿。 - 需要你做的:无,席位落地。 ## What changes - `packages/cli/src/commands/compile.ts`: the artifact write deletes `devPlugins` right after `demoStaff`, at the one place `objectstack.json` is written. `os build` is an alias of this command; `os package publish` (`dist/objectstack.json` or its argument) and `os deploy` (`DEPLOY_ARTIFACT`, after `compileForDeploy`) both read that file rather than the config, so dropping it at the write covers them. - `packages/cli/src/utils/restriction-profile.ts`: `CODE_KEYS` (the keys `UNMANAGED_INSTALL_KEY_DISPOSITIONS` classifies `code`) now excludes a named set, `NEVER_REACHES_TARGET = {'devPlugins'}`, with the reason in its docblock and one line in the module header. The now-unreachable `devPlugins` branch of `codeItems` is removed. ⛔ `UNMANAGED_INSTALL_KEY_DISPOSITIONS` is unchanged: an unmanaged install still refuses `devPlugins` as code in a bundle that still carries the key (an artifact built before this change). A fresh build no longer carries it, so an unmanaged install of that artifact is no longer refused for it (the second widening on the Clause-② line). - `content/docs/getting-started/quick-start.mdx`: the one docs sentence that lists what `os build` leaves out of `objectstack.json` (it named `demoStaff`) now names `devPlugins` too. The `os compile` section of `deployment/cli.mdx` lists no dropped keys, so it is unchanged. - `.changeset/31264-build-drops-dev-plugins.md`: `@objectstack/cli` patch (both files ship in the cli `dist/`). ## Tests - `test/build-demo-staff-artifact.test.ts` (the existing artifact-drop pin for `demoStaff`): the fixture gains `devPlugins: ['@acme/legal-dev-only-plugin']`, the build runs once for both cases, and a new case asserts the key is absent from `objectstack.json` by key and by text, and absent from what `loadArtifactBundle` (the runtime's own loader) returns. Control: the stack's objects survive the same build; the `demoStaff` case still passes on the same build. - `src/utils/restriction-profile.test.ts`: under `allowCode: false`, a stack with `plugins` and `devPlugins` gets exactly one row, the `plugins` one. - `test/validate-target-restriction-profile.test.ts` (the command's door, in-process): the fixture stack gains a `devPlugins` entry, and the `allowCode: false` case keeps its exact four rows, so the door pins that the dev-only plugin is not refused; the no-target and all-open controls stay green with the key present. - Control for "`os dev --dev` still loads them": `stackBootPlugins` reads the config `serve` loads (`serve.ts`, `stackBootPlugins(config, flags.dev)`), untouched here; pinned by `src/utils/stack-collections.test.ts` and `src/utils/schema-migration-plugins.test.ts` (`--dev composes the config's devPlugins`), both in the cli unit tier run below. <details><summary>Red first on origin/main 455229e</summary> A stack with `devPlugins: ['@acme/dev-only-plugin']` and a `demoStaff` entry, CLI from source (`bin/run-dev.js`): - `os compile --json`: exit 0; `dist/objectstack.json` keys `manifest,objects,positions,devPlugins`, `devPlugins: ["@acme/dev-only-plugin"]`; `demoStaff` absent. - `os validate --target profile.json --json` (`allowCode: false`): exit 1, one row `{ rule: 'allowCode', where: "dev plugin '@acme/dev-only-plugin'", path: 'devPlugins[0]' }`. Same probe on the fix head: compile exit 0, keys `manifest,objects,positions`; validate exit 0, `valid: true`, no errors. </details> <details><summary>Ablation, one leg per behaviour</summary> `node scripts/ablation-replace.mjs`, each leg restored to blob == HEAD with `git diff HEAD` empty. Both suites import the subject from `src/` (in-process `Validate.run`, and `bin/run-dev.js` running the CLI from source), so no `dist/` leg applies. - Reader: `new Set(['devPlugins'])` → `new Set([])` (blob 5dde119 → 7c872933b460): 2 failed / 29 passed — the new reader case and the door's `allowCode: false` case. - Artifact write: `delete finalBundle.devPlugins;` deleted (blob b65a74f → 419934d0cafd): 1 failed / 1 passed — the `devPlugins` case red, the `demoStaff` case still green. </details> ## Cross-repo check `gh api -X GET search/code -f q='devPlugins org:objectstack-ai' --paginate`: 42 hits in `objectstack-ai/objectstack`, 3 in `objectstack-ai/hotcrm` — all three are `content/docs/administration/sandbox-and-releases{,.zh-Hans,.zh-Hant}.mdx`, a key list in docs prose, not a reader. **`objectstack-ai/cloud` is not reachable from this session** (`gh api repos/objectstack-ai/cloud` answers 403 "GitHub access to this repository is not enabled for this session"; `add_repo` answers "you don't have access"), so whether the cloud control plane reads `devPlugins` out of `sys_package_version.manifest_json` is **not measured**. In this repo no package outside `packages/cli` reads `devPlugins` off an artifact: `packages/spec` holds the schema (`stack.zod.ts`, optional), the dispositions table, the composition rule and the migration chain's relevance walk (`migrations/chain.ts`, which reads a listed `devPlugins` as "unknown" and with the key absent reads the stack's other keys); `packages/verify/src/harness.ts` only mentions it in a comment. **Open residual:** whether `objectstack-ai/cloud` reads `devPlugins` out of `sys_package_version.manifest_json` stays open on #31264, for a seat with cloud access to measure. ## Spec docblock owed (for the spec seat) On `RestrictionProfileSchema`'s `allowCode` docblock (`packages/spec/src/system/restriction-profile.zod.ts`), the sentence to add: > `devPlugins` is not judged by `allowCode`: it is loaded only by a local development boot, and `os build` leaves it out of the artifact a target installs, so it never reaches a target; `UNMANAGED_INSTALL_KEY_DISPOSITIONS` still classifies it as `code` for an unmanaged install. ## Acceptance notes - Clause-② arm, measured: the reader change widens the accept-set of `os validate --target` (a stack refused before is accepted). The artifact change narrows nothing: no door refuses anything new (every stack that compiled before still compiles), the stack schema still accepts `devPlugins` on an artifact (an artifact built before this change still installs and boots), and no reader in this repo loads the key from an artifact. Hence `yes (widening)`, matching the claim, with two widenings: the `--target` reader, and an unmanaged install of a fresh artifact (`package-unmanaged-install.ts` judges the bundle's keys against `UNMANAGED_INSTALL_KEY_DISPOSITIONS`, and the key is no longer in the bundle). No dogfood battery is owed (no `(narrowing)` arm, no `!`). - `os dev` and `os serve --dev` keep loading `devPlugins` from the config; `os dev --artifact` boots without a config and never loaded them, before or after. - The build summary's `Runtime:` row still counts `devPlugins`: `collectMetadataStats` reads the loaded config, not the artifact. ## Verification All on `2f7e23d754`, after `pnpm --filter '@objectstack/cli...' build`. Head `8f2196e851` adds one prose-only commit on top (the changeset body; `git diff --name-only 2f7e23d..HEAD` = `.changeset/31264-build-drops-dev-plugins.md`), so no code, test or gate input moved; `check:changeset-no-major`, `check:empty-changeset` and `check:adr-0087-registration` were re-run on the new head (below). - `pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 src/utils/restriction-profile.test.ts test/validate-target-restriction-profile.test.ts test/build-demo-staff-artifact.test.ts`: lock VERDICT command-exit 0, Test Files 3 passed (3), Tests 33 passed (33). - `pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2`: lock VERDICT command-exit 0, Test Files 401 passed (401), Tests 5664 passed (5664). The integration tier is declared to CI; the one spawn-tier file this diff edits (`build-demo-staff-artifact.test.ts`) ran green in the run above. - `pnpm --filter @objectstack/cli typecheck` (tsc + `check:test-typecheck`): exit 0, test layer OK, debt unchanged. - eslint (`--no-inline-config`) on the five touched TS files: exit 0. - `node scripts/pm/dispatch-gates.mjs --commands`: 93 derived families, every one run with its exit captured before any pipe; all exit 0. `check:skill-examples` and `check:i18n-coverage` first exited 3 (PREREQUISITE NOT MET: client SDK / example-config dist absent) and were re-run green after building those closures. `--ran`: 「93 derived famil(ies) accounted for — 93 run, 0 NOT-MEASURED」, exit 0. - `node scripts/check-nul-bytes.mjs`: exit 0. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (No spec edit.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes (eslint on the touched files; the repo run is CI's). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: not touched. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the 维护者速读 block is the repo's own convention). - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_01FEkQ5pBUHysxXskQoeR1p5)_ <!-- signed: domain:server#6 · os-project-manager · session_01FEkQ5p3T5YWhLjurajudnQ --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c0f88b9 commit ea053b4

7 files changed

Lines changed: 92 additions & 16 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): `os build` leaves `devPlugins` out of the artifact, and `os validate --target` no longer reads it as code
6+
7+
Clause-②: yes (widening) — two accept-sets widen: `os validate --target` with `allowCode: false` now accepts a stack that declares `devPlugins`, and an unmanaged install (`os package install --install-mode unmanaged`, and the cloud-connection local installs) of a freshly built artifact of such a stack is no longer refused, because that artifact no longer carries the key. Leaving the key out of `objectstack.json` refuses nothing (every stack that built before still builds, and the published stack schema still accepts an artifact that carries it).
8+
Metadata-protocol: none
9+
10+
`devPlugins` is the list a development boot (`os dev`, `os serve --dev`) adds to the plugins it loads, read off the config it loads. Nothing that boots or installs the artifact loads it, yet `os build` wrote it into `objectstack.json`, so `os package publish` and `os deploy` (which send that file) carried a development-only load list to the target. `os build` now drops it at the one place the artifact is written, as it already dropped `demoStaff`.
11+
12+
Because the key no longer reaches a target, a target environment's restriction profile has nothing to refuse: `os validate --target` with `allowCode: false` no longer reports a row per dev-only plugin. `plugins`, `functions`, `hooks` and the other code keys are judged as before, and `os dev` / `os serve --dev` still load `devPlugins` from the config.
13+
14+
The same goes for an unmanaged install: a freshly built artifact of a stack with `devPlugins` now installs unmanaged, where it was refused as carrying code. A bundle that still carries the key, such as an artifact built before this release, is still refused as code by an unmanaged install: the spec's classification of `devPlugins` is unchanged.

‎content/docs/getting-started/quick-start.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,8 @@ runtime-assembled manifests; `runtimeModule` is written by `objectstack build`);
237237
the stack rather than declare metadata items (`email` and `sms` carry no
238238
credentials — those stay in the environment or in Settings; `push` names each
239239
vendor secret by `credentialRef` and is read only when `requires` names `push`; `demoStaff` names the
240-
demo accounts a local `os dev` boot creates, and `os build` leaves it out of
241-
`objectstack.json`); and
240+
demo accounts a local `os dev` boot creates and `devPlugins` the plugins only a
241+
`--dev` boot loads, and `os build` leaves both out of `objectstack.json`); and
242242
`functions` and `onEnable` are code the runtime calls, not metadata it stores.
243243

244244
## How you verify

‎packages/cli/src/commands/compile.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1172,6 +1172,13 @@ export default class Compile extends Command {
11721172
// `devLogins` do; the module exports only `functions` and `meta`, so no
11731173
// boot can read the list out of it.)
11741174
delete finalBundle.demoStaff;
1175+
// `devPlugins` never reaches the artifact either, for the same reason:
1176+
// it is the list `os serve --dev` / `os dev` add to the boot, read off
1177+
// the config it loads (`stackBootPlugins`), and nothing that boots or
1178+
// installs the artifact loads it. Left in, `os package publish` and
1179+
// `os deploy` (which read this file) carried a development-only load
1180+
// list into a target, and a restriction profile had to read it as code.
1181+
delete finalBundle.devPlugins;
11751182
// [#18431] Docs read out of `src/<pkg>/docs/` attach to the body of the
11761183
// package that owns them — `packages[i].manifest`, ADR-0130 D4 option
11771184
// B — and ⛔ never to the top level, which is the maintainer's ruling

‎packages/cli/src/utils/restriction-profile.test.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ describe('judgeRestrictionProfile — one refusal per dimension', () => {
7979
expect(rowsOf(profile({ allowCode: false }), stack)).toEqual([]);
8080
});
8181

82+
it('allowCode: false — `devPlugins` never reaches a target, so it refuses nothing; `plugins` beside it still does', () => {
83+
const stack = { manifest: STACK.manifest, plugins: ['@acme/rp-plugin'], devPlugins: ['@acme/rp-dev-only-plugin'] };
84+
expect(rowsOf(profile({ allowCode: false }), stack)).toEqual([
85+
{ rule: 'allowCode', where: "plugin '@acme/rp-plugin'", path: 'plugins[0]' },
86+
]);
87+
});
88+
8289
it('allowCode: false — a code key on the manifest is refused under its manifest path', () => {
8390
const stack = { manifest: { ...STACK.manifest, main: 'dist/index.mjs' } };
8491
expect(rowsOf(profile({ allowCode: false }), stack)).toEqual([

‎packages/cli/src/utils/restriction-profile.ts‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
* so it is named here, as the profile's own docblock names it. A key that
3333
* carries nothing (`functions: {}`, `plugins: []`) refuses nothing — the
3434
* same `unmanagedInstallValueCarries` test that table's readers apply.
35+
* `devPlugins` is not read: `os build` leaves it out of the artifact, so it
36+
* never reaches a target (`NEVER_REACHES_TARGET` below).
3537
* - `allowScheduledAutomation` — a flow of `type: 'schedule'` and every
3638
* `jobs` entry (a job always carries a cron, interval or once `schedule`).
3739
* - `allowedFlowNodeTypes` — every node of every flow, at every region depth
@@ -148,9 +150,20 @@ export function loadRestrictionProfile(
148150
return { profile: parsed.data, file };
149151
}
150152

151-
/** The non-collection keys the spec classifies as code. */
153+
/**
154+
* The keys the spec classifies as code that never reach a target, so
155+
* `allowCode` does not read them. `devPlugins` is loaded only by a local
156+
* development boot (`os dev` / `os serve --dev`, off the config it loads), and
157+
* `os build` leaves it out of the artifact that `os package publish` and
158+
* `os deploy` send — so a target never runs it, whatever its profile says.
159+
* The spec table itself keeps `devPlugins: 'code'`: an unmanaged install still
160+
* refuses it, which is a different question from what a target runs.
161+
*/
162+
const NEVER_REACHES_TARGET: ReadonlySet<string> = new Set(['devPlugins']);
163+
164+
/** The non-collection keys the spec classifies as code, less the ones that never reach a target. */
152165
const CODE_KEYS: readonly string[] = Object.entries(UNMANAGED_INSTALL_KEY_DISPOSITIONS)
153-
.filter(([, disposition]) => disposition === 'code')
166+
.filter(([key, disposition]) => disposition === 'code' && !NEVER_REACHES_TARGET.has(key))
154167
.map(([key]) => key);
155168

156169
type Bag = Record<string, unknown>;
@@ -180,9 +193,8 @@ function codeItems(key: string, value: unknown): Array<{ label: string; at: stri
180193
}
181194
if (isBag(value)) return Object.keys(value).map((name) => ({ label: `function '${name}'`, at: `functions.${name}` }));
182195
}
183-
if ((key === 'plugins' || key === 'devPlugins') && Array.isArray(value)) {
184-
const noun = key === 'plugins' ? 'plugin' : 'dev plugin';
185-
return value.map((entry, i) => ({ label: `${noun} '${nameOf(entry, `${key}[${i}]`)}'`, at: `${key}[${i}]` }));
196+
if (key === 'plugins' && Array.isArray(value)) {
197+
return value.map((entry, i) => ({ label: `plugin '${nameOf(entry, `${key}[${i}]`)}'`, at: `${key}[${i}]` }));
186198
}
187199
return [{ label: `\`${key}\``, at: key }];
188200
}

‎packages/cli/test/build-demo-staff-artifact.test.ts‎

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,16 @@
1818
* there, as `data` and `devLogins` do), and what it exports is what a boot
1919
* can read.
2020
*
21+
* `devPlugins` is left out the same way: it is the list a development boot
22+
* (`os dev`, `os serve --dev`) adds off the config it loads, never read off the
23+
* artifact, and leaving it in carried a development-only load list through
24+
* `os package publish` / `os deploy` into a target. The same build measures
25+
* both keys.
26+
*
2127
* The control is the same build carrying the stack's other keys through: the
2228
* declared position the demo account names is in the artifact, and the
23-
* `functions` entry runs, so a green here is "this one key was left out", not
24-
* "the build wrote nothing".
29+
* `functions` entry runs, so a green here is "these two keys were left out",
30+
* not "the build wrote nothing".
2531
*
2632
* Runs the CLI from source (`bin/run-dev.js` under `node`), as the other build
2733
* suites do, so `compile.ts` is measured without a rebuild.
@@ -42,6 +48,7 @@ const CLI = resolve(HERE, '../bin/run-dev.js');
4248
const RUNTIME_MODULE = /^objectstack-runtime\.[a-f0-9]+\.mjs$/;
4349

4450
const STAFF_EMAIL = 'counsel1@legal.example';
51+
const DEV_PLUGIN = '@acme/legal-dev-only-plugin';
4552

4653
const CONFIG = `import { defineStack } from '@objectstack/spec';
4754
@@ -60,6 +67,7 @@ export default defineStack({
6067
functions: { legal_score: legalScore },
6168
positions: [{ name: 'legal_counsel', label: 'Legal Counsel' }],
6269
demoStaff: [{ name: 'Legal Counsel 1', email: '${STAFF_EMAIL}', position: 'legal_counsel' }],
70+
devPlugins: ['${DEV_PLUGIN}'],
6371
});
6472
`;
6573

@@ -91,15 +99,22 @@ function build(cwd: string): Promise<{ code: number; stdout: string; stderr: str
9199
});
92100
}
93101

94-
describe('`os build` leaves the demo accounts out of the artifact (#25777)', () => {
95-
it('writes no `demoStaff` into objectstack.json, and a deployment that loads the artifact reads none', async () => {
96-
const run = await build(dir);
102+
/** The one build both cases read: `os build --json` in the fixture directory. */
103+
let built: Promise<{ artifactPath: string; text: string; artifact: Record<string, unknown> }> | undefined;
104+
function buildOnce() {
105+
built ??= build(dir).then((run) => {
97106
expect(run.code, `os build --json failed:\n${run.stdout}\n${run.stderr}`).toBe(0);
98-
99107
const payload = JSON.parse(run.stdout) as { output?: string };
100108
const artifactPath = String(payload.output);
101109
const text = readFileSync(artifactPath, 'utf8');
102-
const artifact = JSON.parse(text) as Record<string, unknown>;
110+
return { artifactPath, text, artifact: JSON.parse(text) as Record<string, unknown> };
111+
});
112+
return built;
113+
}
114+
115+
describe('`os build` leaves the demo accounts out of the artifact (#25777)', () => {
116+
it('writes no `demoStaff` into objectstack.json, and a deployment that loads the artifact reads none', async () => {
117+
const { artifactPath, text, artifact } = await buildOnce();
103118

104119
expect(Object.keys(artifact)).not.toContain('demoStaff');
105120
expect(text).not.toContain(STAFF_EMAIL);
@@ -121,3 +136,19 @@ describe('`os build` leaves the demo accounts out of the artifact (#25777)', ()
121136
expect(await (functions as Record<string, (input: unknown) => unknown>).legal_score({})).toBe(7);
122137
}, 180_000);
123138
});
139+
140+
describe('`os build` leaves the dev-only plugins out of the artifact', () => {
141+
it('writes no `devPlugins` into objectstack.json, and a deployment that loads the artifact reads none', async () => {
142+
const { artifactPath, text, artifact } = await buildOnce();
143+
144+
expect(Object.keys(artifact)).not.toContain('devPlugins');
145+
expect(text).not.toContain(DEV_PLUGIN);
146+
147+
const loaded = (await loadArtifactBundle(artifactPath)) as Record<string, unknown> | null;
148+
expect(loaded, 'the runtime loader could not load the artifact').toBeTruthy();
149+
expect(Object.keys(loaded!)).not.toContain('devPlugins');
150+
151+
// Control: the stack's other keys survived the same build.
152+
expect((artifact.objects as Array<{ name: string }>).map((o) => o.name)).toEqual(['legal_matter']);
153+
}, 180_000);
154+
});

‎packages/cli/test/validate-target-restriction-profile.test.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,18 @@ const json = (r: Run): Record<string, any> => {
7070
}
7171
};
7272

73-
/** One item per dimension: a hook and its function, a scheduled flow, and a record flow with a script node. */
73+
/**
74+
* One item per dimension: a hook and its function, a scheduled flow, and a
75+
* record flow with a script node. Plus a dev-only plugin, which `os build`
76+
* leaves out of the artifact, so no target runs it and `allowCode` reads none.
77+
*/
7478
const STACK = `{
7579
manifest: { id: 'com.example.rpt', namespace: 'rpt', version: '1.0.0', name: 'Rpt', type: 'app', engines: { protocol: '^18' } },
7680
requires: ['automation', 'triggers'],
7781
objects: [{ name: 'rpt_doc', label: 'Doc', sharingModel: 'private', fields: { title: { type: 'text', label: 'Title' } } }],
7882
hooks: [{ name: 'rpt_audit', object: 'rpt_doc', events: ['beforeInsert'], handler: 'rpt_audit_fn' }],
7983
functions: { rpt_audit_fn: async () => {}, rpt_calc: () => 1 },
84+
devPlugins: ['@acme/rpt-dev-only-plugin'],
8085
flows: [
8186
{
8287
name: 'rpt_nightly', label: 'Nightly', type: 'schedule', status: 'active',
@@ -147,7 +152,7 @@ describe('os validate --target <restriction profile file>', () => {
147152
expect(json(r).valid).toBe(true);
148153
}, 60_000);
149154

150-
it('allowCode: false refuses the hook, both functions and the script node — each row naming allowCode once', async () => {
155+
it('allowCode: false refuses the hook, both functions and the script node — each row naming allowCode once; the dev-only plugin is not refused', async () => {
151156
const r = await run([config, '--json', '--target', profileFile('no-code', { ...OPEN, allowCode: false })]);
152157
expect(r.code, r.stdout).toBe(1);
153158
expect(json(r).valid).toBe(false);

0 commit comments

Comments
 (0)