Repository navigation
Commit 9c431ba
fix(cli): os init -t plugin ships a starter test, so a fresh scaffold's npm test passes (#25196)
Part of #25175
Clause-②: no
Metadata-protocol: none
**This PR ships the `os init -t plugin` half of #25175. The `os create
plugin` half is not in it.** That half cannot land without editing three
public docs pages. Those pages are outside the file surface the dispatch
declared, and the dispatch says to stop on a breach of it. The section
"Why `os create plugin` is not here" below covers what that half needs
and which option it takes. When that half lands on this branch, the seat
changes the first line from "Part of" to the closing-keyword form.
## What changes
`os init NAME -t plugin` declared `"test": "vitest run"` and emitted no
test file, so a fresh project's `npm test` exited 1 with `No test files
found, exiting with code 1`. The template now emits one starter test,
`package.test.ts`, next to `objectstack.config.ts`:
- It imports the package's stack. That runs `defineStack()`, whose
strict parse is the protocol's own refusal path, so a definition another
stack would refuse fails the file before any test runs.
- It asserts that the stack declares itself a `plugin` and contains
every object that the `src/objects` barrel exports.
- Its header links the documented harness for engine-level tests
(`@objectstack/verify`, the "Plugin Testing" section of
`protocol/kernel/plugin-spec`).
The `test` script stays `vitest run` (run mode, which does not watch).
Files: `packages/cli/src/commands/init.ts` (`TEMPLATES.plugin.srcFiles`
only), the new pin
`packages/cli/test/scaffold-test-script-passes.test.ts`, and
`.changeset/25175-plugin-scaffold-test.md` (`@objectstack/cli` patch).
`SCAFFOLD_ALLOWED_PEER_VERSIONS`, `SCAFFOLD_NPM_OVERRIDES` and
`SCAFFOLD_VITEST_RANGE` are untouched.
## The route, and why it is honest
The card allows two routes: one meaningful starter test, or a test
script that does not fail on an empty suite. This PR takes the first.
- **Not `--passWithNoTests`.** It turns "nothing ran" into exit 0, which
is the very state the card describes, and it would also hide a misnamed
test file or a broken glob. The pin below counts the tests that ran, so
it fails on that route too.
- **Not a placeholder.** I checked that the emitted test can fail on a
real defect, on a real emission in a scratch project:
- with the config's `objects:` key emptied, it fails with
`AssertionError: expected [] to include 'probe_pkg_item'`;
- with an undeclared key in `manifest:`, the file fails to load with
`StackSchemaInvalidError: defineStack validation failed ... Unrecognized
key(s) on this package manifest`.
- **Not an engine boot.** The blank template's test boots the stack with
`@objectstack/verify`. That is the documented style, and I measured it
on a `-t plugin` emission: exit 0, `Test Files 1 passed`, the engine
refused an item saved without its required `name`, about 30 s of wall
in-repo. It was not chosen for three reasons:
- It needs `@objectstack/verify` declared as a devDependency. That
package is already in the install closure through
`@objectstack/cli@18.0.0`, so the declaration adds no package.
- It needs a `vitest.config.ts` for the boot's hook timeout, console
intercept and registry log level. That file would be a second copy of
the blank template's, free to drift from it.
- Its pin would run a stack boot in Test Core.
The shape chosen is the example the dispatch's H1 gives: the package
loads and declares what the scaffold wrote. The test asserts what the
package declares. It does not assert `required: true`, which would only
restate a declaration rather than show it is enforced.
## Measured end to end (npm 10.9.4, registry `@objectstack/*` 18.0.0,
vitest 4.1.12)
Scaffolded with this branch's `bin/run-dev.js`, then `npx -y npm@10
install` in a scratch directory with a scratch npm cache:
| `os init e2e-pkg -t plugin` | exit | reading |
|:---|:---:|:---|
| `npm test` | 0 | `Test Files 1 passed (1)`, `Tests 1 passed (1)`, 736
ms |
| `npm test` with `package.test.ts` removed (baseline) | 1 | `No test
files found, exiting with code 1` |
| `npm run typecheck` | 0 | |
| `npm run build` (`objectstack compile`) | 0 | `dist/objectstack.json`
|
| `npm run validate` | 0 | `Validation passed` |
| `npm run lint` | 0 | `All checks passed` |
## The pin, and its ablation
`test/scaffold-test-script-passes.test.ts` (`unit` tier):
- It takes every `os init` template whose emitted manifest declares a
`test` script (today `plugin`) and renders it through `init`'s own
renderers.
- It writes the project under the OS temp directory, outside the
repository. vitest looks for a config file in the project directory and
in every parent, so a project nested in `packages/cli` would run under
`packages/cli/vitest.config.ts`. I measured that it did. A case asserts
that no ancestor of the sandbox carries a vitest or vite config.
- It symlinks each declared dependency to the copy this package
resolves.
- It runs the emitted script with a JSON reporter added. It asserts exit
0, zero failed tests, at least one passed test, and that the files which
ran are exactly the shipped test files.
- **Control:** the same project with its shipped tests deleted must
fail. That is the defect's own signature.
Ablation (one-off, through `scripts/ablation-replace.mjs` with a proven
restore; `init.ts` is imported by relative source specifier, so no
`dist/` is on the path):
| mutation | result |
|:---|:---|
| emitted key `'package.test.ts'` renamed to `'package.ts'` | red: ``
`os init -t plugin` declares a test script and emits no test file:
expected 0 to be greater than 0 `` |
| shipped assertion `toBe('plugin')` changed to `toBe('app')` | red: ``
`vitest run` in a fresh `os init -t plugin` project: package.test.ts >
this package is a plugin that hands every object in src/objects to the
stack: AssertionError: expected 'plugin' to be 'app' `` |
| after each run | restored: blob equals HEAD `3dff72ee5d0a`, `git diff
HEAD` empty |
`scaffold-emission-typechecks.test.ts` already compiles every `srcFiles`
entry under the shipped `tsconfig.json`, so the new test's type layer is
covered there.
## Why `os create plugin` is not here
`create-plugin-docs-parity.test.ts` holds the file tree printed on
`content/docs/plugins/index.mdx`,
`content/docs/protocol/kernel/index.mdx` and
`content/docs/protocol/kernel/plugin-spec.mdx` equal to the emitted file
set. Any starter test file therefore needs a listing line on each of
those pages. They are outside the declared surface.
I measured the options on a real `os create plugin e2e` scaffold (npm
10.9.4). The baseline `npm test` exits 1 with `No test files found`, and
its `test` script is bare `vitest`, which watches in a terminal.
| placement | `npm test` | after `npm run build` | `typecheck` covers
the test |
|:---|:---|:---|:---|
| S1 `src/index.test.ts`, tsconfig unchanged | exit 0, 1 file | `tsc`
emits `dist/index.test.js` and `.d.ts`; vitest 4's default `exclude` is
only `node_modules` and `.git`, so `Test Files 2 passed (2)` | yes |
| S2 `src/index.test.ts` plus `tsconfig.build.json` (extends
`tsconfig.json`, excludes `src/**/*.test.ts`), build `tsc -p
tsconfig.build.json` | exit 0, 1 file | `dist/` holds `index.js` and
`index.d.ts` only; still 1 file | yes: a planted type error in the test
fails `typecheck` with TS2322 |
| S3 `test/index.test.ts` | exit 0 | clean | no: with `rootDir: 'src'`,
including it fails with TS6059, under `--noEmit` too |
| S4 `vitest run --passWithNoTests` | exit 0, nothing ran | | |
S2 is the shape I recommend. The test inside it can use `PluginSchema`
from `@objectstack/spec/kernel`, the contract both kernels run at
`use()`, and assert `name` and `init`, which the kernel loader requires.
The `test` script becomes `vitest run`, as in `-t plugin`.
That half touches `create.ts` (the plugin scaffold), the three docs
listings, this pin (extended to `os create` templates) and the
changeset.
## Verification (head `88a38f333`, after merging `origin/main`
`f7d740376`)
- `pnpm --filter '@objectstack/cli...' build`: exit 0.
- New pin plus every existing pin that sweeps the `os init` templates
(17 files: `scaffold-emission-typechecks`,
`init-template-comments-self-contained`,
`init-scaffold-authoring-rules`,
`init-scaffold-file-name-registry-parity`, `init`,
`scaffold-manifest-schema`, `scaffold-npm-overrides`,
`scaffold-object-declaration-shape`, `scaffold-workspace-consistency`,
`starter-field-consumers`, `init-manifest-id`,
`scaffold-ci-script-parity`, `create-objectstack-wiring-parity`,
`vitest-tiers-partition`, `create`, `create-plugin-docs-parity`): `Test
Files 17 passed (17)`, `Tests 268 passed (268)`.
- `@objectstack/cli` `unit` tier, full: `Test Files 324 passed (324)`,
`Tests 4775 passed (4775)`, lock verdict `command-exit 0`. The
`integration` tier is declared to CI, because the diff touches no spawn
entry and no integration-tier file.
- `pnpm --filter @objectstack/cli typecheck`: exit 0 (`tsc --noEmit`;
the test layer `check:test-typecheck` passes with no new error
signatures).
- Gates: all 69 families from `dispatch-gates.mjs --commands` (paths
derived from merge base `f7d740376`), plus
`check:template-version-sync`, exit 0. That includes
`check:scaffold-emission-policy`, `check:template-version-sync`,
`check:cli-test-child-env`, `check:cross-package-test-inputs`,
`check:test-source-alias`, `check:doc-authoring` and `check:nul-bytes`.
Two of them, `check:dual-build-cjs-loads` and `check:i18n-coverage`,
first exited 3 with `PREREQUISITE NOT MET`: packages outside the CLI's
closure had no `dist/`. Both exit 0 once those packages are built.
`--ran` reconciliation: 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN, and
the zero is derived from the recorded exit codes.
## Acceptance notes
- **The new pin's wall.** Its four tests take about 4.7 s. Importing
`init.ts` takes about 15 s, a cost every `os init` pin already pays.
Locally the file reads about 20 s against the 10 s per-file line, which
the timing report flags without failing anything.
- **`os init` without a NAME overwrites existing files.** It scaffolds
into the current directory and overwrites any existing file at a
template path. Only the empty wired barrels are written where no file
exists. `package.test.ts` joins the overwritten set, beside
`src/objects/index.ts` and the item object, which were already in it.
This is an observation from reading the code, not a measured reach.
Carrier: none.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent d0c56a2 commit 9c431ba
3 files changed
Lines changed: 284 additions & 0 deletions
File tree
- .changeset
- packages/cli
- src/commands
- test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1083 | 1083 | | |
1084 | 1084 | | |
1085 | 1085 | | |
| 1086 | + | |
| 1087 | + | |
| 1088 | + | |
| 1089 | + | |
| 1090 | + | |
| 1091 | + | |
| 1092 | + | |
| 1093 | + | |
| 1094 | + | |
| 1095 | + | |
| 1096 | + | |
| 1097 | + | |
| 1098 | + | |
| 1099 | + | |
| 1100 | + | |
| 1101 | + | |
| 1102 | + | |
| 1103 | + | |
| 1104 | + | |
| 1105 | + | |
| 1106 | + | |
| 1107 | + | |
| 1108 | + | |
| 1109 | + | |
| 1110 | + | |
| 1111 | + | |
| 1112 | + | |
| 1113 | + | |
| 1114 | + | |
| 1115 | + | |
| 1116 | + | |
| 1117 | + | |
| 1118 | + | |
| 1119 | + | |
| 1120 | + | |
| 1121 | + | |
| 1122 | + | |
| 1123 | + | |
| 1124 | + | |
1086 | 1125 | | |
1087 | 1126 | | |
1088 | 1127 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
0 commit comments