Skip to content

Commit 6bc8cc6

Browse files
fix(cli): os create plugin ships a starter test, so a fresh scaffold's npm test passes (#25236)
<!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4Uu4GrD7D5BC5v5WW4ryVhyCV)_ Fixes #25175 Clause-②: no Metadata-protocol: none This is the `os create plugin` half of #25175, the half left after #25196 landed the `os init -t plugin` half (`9c431bad3`). With it, both plugin scaffolds pass their own `npm test` out of the box. ## What changes A project made by `os create plugin NAME` declared `"test": "vitest"` and contained no test file, so its first `npm test` exited 1 with `No test files found, exiting with code 1`. The `plugin` template in `packages/cli/src/commands/create.ts` (both placements) now: - writes **`src/index.test.ts`**, a starter test that runs the checks the kernel runs on a plugin when it loads one, before any of its code: a `name`, an `init` function (the loader's `validatePluginStructure`), and the protocol's `PluginSchema` (`assertPluginContract`), through `@objectstack/spec/kernel`, which the manifest already declares. Its header links the `@objectstack/verify` harness ("A runtime plugin" on `protocol/kernel/plugin-spec`) for tests that need a booted kernel; - writes **`tsconfig.build.json`**: `extends: './tsconfig.json'`, `exclude: ['dist', 'node_modules', 'src/**/*.test.ts']`; - emits `build: 'tsc -p tsconfig.build.json'`, `dev: 'tsc -p tsconfig.build.json --watch'` and `test: 'vitest run'` (one pass that exits, as `os init` emits). `typecheck` stays `tsc --noEmit`, on `tsconfig.json`. The three public pages whose printed file tree `create-plugin-docs-parity.test.ts` holds equal to the emitted file set gain one line per new file: `content/docs/plugins/index.mdx`, `content/docs/protocol/kernel/index.mdx` and `content/docs/protocol/kernel/plugin-spec.mdx` (its CLI transcript, in emission order). Changeset: `.changeset/25175-create-plugin-scaffold-test.md` (`@objectstack/cli` patch), with the one-line fix for a project scaffolded by an earlier release. ## The route, and why it is honest The card allows a starter test or a script that does not fail on an empty suite. This takes the starter test. The previous dev's four measured placements (report on #25175) were re-measured on this branch: | placement | `npm test` | after `npm run build` | `typecheck` covers the test | |:---|:---|:---|:---| | S1 `src/index.test.ts`, build stays `tsc` | 1 file | `dist/index.test.js` + `.d.ts` emitted, the package ships its tests, and vitest collects the compiled copy too | yes | | **S2 (this PR)** `src/index.test.ts` + `tsconfig.build.json` | 1 file | `dist/` = `index.js`, `index.d.ts`; still 1 file | yes: a planted type error fails `typecheck` | | S3 `test/` outside `rootDir: 'src'` | 1 file | clean | no (TS6059 if included) | | S4 `vitest run --passWithNoTests` | exit 0, nothing ran | | | S2 is the only row that keeps `dist/` clean without hiding the test from the type checker. The pin below holds each of those properties, and the ablation shows each one can go red. The test is not a placeholder: emitted with `init` renamed to `start`, it fails `expected 'undefined' to be 'function'`; emitted with `version: 'v0.1.0'`, `PluginSchema` refuses it (ablation legs E and F). ## Measured end to end (npm 10.9.4, registry `@objectstack/spec` 18.0.0, vitest 4.1.12, typescript 5.9.3) `os create plugin e2e-pkg` from this branch's built CLI, outside the repository, then `npx -y npm@10 install`: | step | exit | reading | |:---|:---:|:---| | `npm test` | 0 | `Test Files 1 passed (1)`, `Tests 1 passed (1)` | | `npm run build` | 0 | `dist/` = `index.d.ts`, `index.js` | | `npm test` after the build | 0 | `Test Files 1 passed (1)` | | `npm run typecheck` | 0 | | | `npm test` with `src/index.test.ts` removed | 1 | `No test files found, exiting with code 1` | | baseline: `main`'s `os create plugin`, `npm test` | 1 | `No test files found, exiting with code 1` | ## The pin, and its ablation `packages/cli/test/scaffold-test-script-passes.test.ts` (`unit` tier) gains an `os create` block beside the `os init` one, sharing its sandbox (OS temp dir, no ancestor runner config, one symlink per declared dependency). Roster derived from `create`'s `templates` (default placement) whose manifest declares `test`: - the emitted `test` script exits 0 and the files that ran are exactly the shipped test files; - after the emitted `build` script, `dist/` holds `index.js` and no `*.test.*`, and `test` still runs exactly the shipped files; - the emitted `typecheck` passes, and fails once a type error is planted in the starter test; - control: the same project without its test files fails `test`. Ablation through `scripts/ablation-replace.mjs` on `create.ts` (imported by relative source specifier, no `dist/` on the path), each leg restored with `blob == HEAD` and `git diff HEAD` empty: | leg | mutation | result | |:---|:---|:---| | A | `build: 'tsc'` (S1) | red: `the build compiled the starter test into dist/: expected [ 'index.test.d.ts', 'index.test.js' ] to deeply equal []` | | B | `tsconfig.build.json` without the test exclude | red: same case, same reading | | C | `typecheck: 'tsc -p tsconfig.build.json --noEmit'` (test outside the checked program) | red: `a type error in the starter test passed typecheck` | | D | starter test emitted as `src/index.spec-off.ts` | red: 3 cases (no test file emitted; `vitest run` finds none) | | E | emitted plugin's `init` renamed to `start` | red: `expected 'undefined' to be 'function'` | | F | emitted plugin's version `v0.1.0` | red: `PluginSchema` issue list not empty | ## Verification (head `2aed9ddea`, after merging `origin/main` `03de83bf`; the two incoming commits touch none of these files) - New pin + `create-plugin-docs-parity` + `create`: `Test Files 3 passed (3)`, `Tests 32 passed (32)`. - `@objectstack/cli` `unit` tier, full (`vitest run --project unit --maxWorkers=2`): `Test Files 324 passed (324)`, `Tests 4784 passed (4784)`. The `integration` tier is declared to CI: the diff touches no spawn entry and no integration-tier file. - `pnpm --filter @objectstack/cli typecheck`: exit 0, `check:test-typecheck` OK (no new signature). ESLint on the two changed TS files: clean. - Gates: all 97 commands from `dispatch-gates.mjs --commands`, plus `check:template-version-sync` and `check:scaffold-emission-policy`, exit 0. Four first exited 3 on prerequisites outside this diff (`check-plugin-teardown-shape --self-test` needs a pinned commit a shallow clone lacks; `check:skill-examples`, `check:dual-build-cjs-loads` and `check:i18n-coverage` read `dist/` of packages outside the CLI closure) and exit 0 after `git fetch` of that commit and a full `pnpm build`. ## Acceptance notes - **The `--in-repo` placement** gets the same three changes; its sandbox run is not pinned, because its `tsconfig.json` extends this monorepo's root config and means nothing outside it. Its emission is covered by `create.test.ts` and the docs-parity pin reads the default placement only, as before. - **"Next steps"** printed by `os create` still say `pnpm install` / `pnpm build`; it does not mention `pnpm test`. Unchanged here, to keep the plugin-spec transcript the only docs edit beyond the listings. - **Process:** this card ran as a project card thread on the harness-pinned branch `claude/project-thread-4oxk47`, not `claude/issue-25175-*`; the claim on #25175 records that. --- _Generated by [Claude Code](https://claude.ai/code/session_01QkpbEcZUG5GHxLNpzuuR5y)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent e3190b9 commit 6bc8cc6

6 files changed

Lines changed: 217 additions & 7 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 create plugin` ships a starter test, so a new project's `npm test` passes
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
A project made by `os create plugin NAME` declared `"test": "vitest"` and contained no test file, so its first `npm test` exited 1 with `No test files found, exiting with code 1`. The template now writes `src/index.test.ts`, which runs the checks the kernel runs on a plugin when it loads one: a `name`, an `init` function, and the protocol's `PluginSchema`. It boots no kernel; its header links the `@objectstack/verify` harness for tests that need one. The `test` script is now `vitest run`, one pass that exits.
11+
12+
The starter test lives in `src/`, which `tsc` compiles, so the template also writes `tsconfig.build.json`: it extends `tsconfig.json` and excludes `src/**/*.test.ts`, and `build` and `dev` now run `tsc -p tsconfig.build.json`. `typecheck` still reads `tsconfig.json`, so a type error in a test still fails it, while `dist/` holds no compiled test and `npm test` after `npm run build` runs each test once.
13+
14+
**An existing project made by an earlier `os create plugin`** still has no test file. Add a `*.test.ts` file, for example a copy of the `src/index.test.ts` that a new `os create plugin NAME` writes. If you put it under `src/`, also copy `tsconfig.build.json` and point the `build` and `dev` scripts at it (`tsc -p tsconfig.build.json`), or `tsc` compiles the test into `dist/` and vitest runs it twice.

‎content/docs/plugins/index.mdx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,12 @@ os create plugin my-feature
9898
# plugin-my-feature/
9999
# ├── package.json
100100
# ├── tsconfig.json
101+
# ├── tsconfig.build.json
101102
# ├── README.md
102103
# ├── pnpm-workspace.yaml
103104
# └── src/
104-
# └── index.ts
105+
# ├── index.ts
106+
# └── index.test.ts
105107

106108
cd plugin-my-feature
107109
pnpm install

‎content/docs/protocol/kernel/index.mdx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,9 +398,11 @@ os create plugin slack-integration
398398
plugin-slack-integration/
399399
package.json # name, version, dependencies (@objectstack/spec, zod)
400400
tsconfig.json # self-contained, extends nothing outside the project
401+
tsconfig.build.json # what `pnpm build` compiles: tsconfig.json minus the tests
401402
pnpm-workspace.yaml # the pnpm build approvals a fresh install needs
402403
src/
403404
index.ts # default-export Plugin object (name, version, init, destroy)
405+
index.test.ts # starter test: the checks the kernel runs when it loads a plugin
404406
README.md
405407
```
406408

‎content/docs/protocol/kernel/plugin-spec.mdx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,7 +893,9 @@ os create plugin crm
893893

894894
✓ Created package.json
895895
✓ Created tsconfig.json
896+
✓ Created tsconfig.build.json
896897
✓ Created src/index.ts
898+
✓ Created src/index.test.ts
897899
✓ Created README.md
898900
✓ Created pnpm-workspace.yaml
899901

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

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -392,10 +392,15 @@ export const templates: Record<string, CreateTemplate> = {
392392
// Not a build-script allowlist (that is pnpm-workspace.yaml) — the
393393
// minimum pnpm that reads that file at all.
394394
...(standalone ? { engines: { pnpm: SCAFFOLD_PNPM_RANGE } } : {}),
395+
// `build` and `dev` compile through `tsconfig.build.json`, which
396+
// leaves the starter test out of `dist`; `typecheck` reads
397+
// `tsconfig.json`, which keeps it in. See `tsconfig.build.json` below.
398+
// `test` is `vitest run`, one pass that exits, as `os init` emits:
399+
// bare `vitest` watches in a terminal and never returns.
395400
scripts: {
396-
build: 'tsc',
397-
dev: 'tsc --watch',
398-
test: 'vitest',
401+
build: 'tsc -p tsconfig.build.json',
402+
dev: 'tsc -p tsconfig.build.json --watch',
403+
test: 'vitest run',
399404
typecheck: 'tsc --noEmit',
400405
},
401406
keywords: ['objectstack', 'plugin', name],
@@ -432,6 +437,20 @@ export const templates: Record<string, CreateTemplate> = {
432437
},
433438
include: ['src/**/*'],
434439
},
440+
// The config `build` and `dev` compile with (#25175). `tsconfig.json`
441+
// includes all of `src/`, the starter test with it, so `typecheck`
442+
// still catches a type error in a test. Built through that same config,
443+
// `tsc` also emitted `dist/index.test.js`, and vitest, whose default
444+
// exclude is only `node_modules` and `.git`, then collected the
445+
// compiled copy beside the source: `npm run build && npm test` ran the
446+
// suite twice, and the package shipped its tests. Excluding the tests
447+
// HERE, never in `tsconfig.json`, keeps both halves. ⛔ `exclude` in an
448+
// extending config replaces the base list rather than adding to it, so
449+
// `dist` and `node_modules` are restated.
450+
'tsconfig.build.json': () => ({
451+
extends: './tsconfig.json',
452+
exclude: ['dist', 'node_modules', 'src/**/*.test.ts'],
453+
}),
435454
'src/index.ts': (name: string) => `import type { Plugin } from '@objectstack/spec/contracts';
436455
437456
/**
@@ -453,6 +472,40 @@ export const ${sanitizeIdentifier(name)}Plugin: Plugin = {
453472
};
454473
455474
export default ${sanitizeIdentifier(name)}Plugin;
475+
`,
476+
// The starter test the `test` script runs (#25175). Without a test
477+
// file, `vitest run` exits 1 with "No test files found", so a fresh
478+
// project failed its own `npm test` — the defect `os init -t plugin`
479+
// had, closed there by `package.test.ts`. This one runs the checks the
480+
// kernel runs on a plugin at `use()`, before any of its code: the
481+
// loader's `name` and `init` presence checks, then the protocol's
482+
// `PluginSchema` (`assertPluginContract` in `@objectstack/core`),
483+
// through `@objectstack/spec`, which the manifest already declares. It
484+
// deliberately boots no kernel: that needs `@objectstack/verify`
485+
// declared, and the comment points the author at the documented
486+
// harness for that step instead. `test/scaffold-test-script-passes.test.ts`
487+
// runs it in a sandbox, after a build as well as before one.
488+
'src/index.test.ts': (name: string) => `import { describe, expect, it } from 'vitest';
489+
import { PluginSchema } from '@objectstack/spec/kernel';
490+
import plugin from './index';
491+
492+
// The kernel checks a plugin when it is handed one, before it runs any of its
493+
// code: a plugin with no name or no init is refused outright, and every other
494+
// key is judged against the protocol's PluginSchema. This file runs the same
495+
// checks, so a plugin the kernel would refuse fails here first.
496+
//
497+
// To test what the plugin does once it runs, such as the services its init
498+
// registers, boot it in a real stack with @objectstack/verify:
499+
// https://objectstack.ai/docs/protocol/kernel/plugin-spec#a-runtime-plugin
500+
describe('the ${name} plugin', () => {
501+
it('passes the checks the kernel runs when it loads a plugin', () => {
502+
expect(plugin.name).toBeTruthy();
503+
expect(typeof plugin.init).toBe('function');
504+
505+
const contract = PluginSchema.safeParse(plugin);
506+
expect(contract.error?.issues ?? []).toEqual([]);
507+
});
508+
});
456509
`,
457510
'README.md': (name: string) => {
458511
const packageName = pluginPackageName(placement, name);

‎packages/cli/test/scaffold-test-script-passes.test.ts‎

Lines changed: 140 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,34 @@
4242
*
4343
* Derived from `TEMPLATES`: every template whose rendered `package.json`
4444
* declares a `test` script. A template that grows one later is swept the day
45-
* it lands. `os create plugin` emits a `test` script too and is not swept
46-
* here yet; its starter test ships with the docs listings its file tree is
47-
* pinned against (`create-plugin-docs-parity.test.ts`).
45+
* it lands.
4846
*
4947
* The type layer of the emitted test is `scaffold-emission-typechecks.test.ts`'s
5048
* axis (it compiles every `srcFiles` entry under the shipped `tsconfig.json`),
5149
* so it is not restated here.
50+
*
51+
* ## `os create`
52+
*
53+
* `os create plugin` had the same defect (its script was bare `vitest`) and
54+
* ships `src/index.test.ts`. Its roster is derived from `create`'s
55+
* `templates`, in the default (standalone) placement: the `--in-repo`
56+
* placement's `tsconfig.json` extends this monorepo's root config, so it has no
57+
* meaning in a sandbox outside it. Two more cases than `os init` gets, because
58+
* `create`'s `build` is plain `tsc` and its starter test lives in `src/`, the
59+
* directory `tsc` compiles:
60+
*
61+
* - after the emitted `build` script, `test` still runs exactly the shipped
62+
* test files, and `dist/` holds no compiled test. Built through
63+
* `tsconfig.json`, `tsc` emitted `dist/index.test.js` and vitest collected
64+
* it beside the source: the suite ran twice and the package shipped its
65+
* tests. `tsconfig.build.json` is what keeps them out;
66+
* - the emitted `typecheck` script still type-checks the starter test: a type
67+
* error planted in it fails the script. Moving the test out of `src/`, or
68+
* excluding it in `tsconfig.json`, would have kept `dist/` clean by hiding
69+
* the test from the type checker.
70+
*
71+
* `create`'s whole emission lands nowhere `scaffold-emission-typechecks`
72+
* compiles, so that second case is this file's to hold.
5273
*/
5374

5475
import { afterAll, describe, expect, it } from 'vitest';
@@ -65,6 +86,7 @@ import {
6586
SCAFFOLD_TSCONFIG_INCLUDE_WITH_ROOT_CONFIG,
6687
writeTemplateSrcFiles,
6788
} from '../src/commands/init.js';
89+
import { DEFAULT_PLACEMENT, templates as createTemplates } from '../src/commands/create.js';
6890
import { childEnv } from './helpers/serve-process.js';
6991

7092
const HERE = path.dirname(fileURLToPath(import.meta.url));
@@ -149,6 +171,17 @@ function resolveBin(root: string, manifest: Manifest, bin: string): string {
149171
throw new Error(`no declared dependency of the emitted project provides the \`${bin}\` bin`);
150172
}
151173

174+
/** Spawn any other emitted script in the project; its exit code and output. */
175+
function runScript(root: string, manifest: Manifest, script: string): { code: number | null; output: string } {
176+
const [bin, ...args] = manifest.scripts![script].trim().split(/\s+/);
177+
const res = spawnSync(process.execPath, [resolveBin(root, manifest, bin), ...args], {
178+
cwd: root,
179+
encoding: 'utf-8',
180+
env: childEnv({ NO_COLOR: '1', CI: '1' }),
181+
});
182+
return { code: res.status, output: `${res.stdout ?? ''}${res.stderr ?? ''}` };
183+
}
184+
152185
/** Spawn the emitted `test` script, plus a JSON reporter, in the project. */
153186
function runTestScript(root: string, manifest: Manifest) {
154187
const [bin, ...args] = manifest.scripts!.test.trim().split(/\s+/);
@@ -231,3 +264,107 @@ describe('[#25175] an `os init` template with a `test` script passes it out of t
231264
expect(json?.numPassedTests ?? 0).toBe(0);
232265
}, 120_000);
233266
});
267+
268+
/** The `os create` templates whose emitted manifest declares a `test` script. */
269+
const CREATE_ROSTER = Object.keys(createTemplates).filter((key) => {
270+
const render = createTemplates[key].filesFor(DEFAULT_PLACEMENT)['package.json'];
271+
return typeof (render(PROJECT_NAME) as Manifest).scripts?.test === 'string';
272+
});
273+
274+
/** Materialise one `os create` template the way `Create.run()` writes it. */
275+
function emitCreate(templateKey: string): { root: string; manifest: Manifest; testFiles: string[] } {
276+
const root = fs.mkdtempSync(path.join(TMP_ROOT, `create-${templateKey}-`));
277+
for (const [rel, render] of Object.entries(createTemplates[templateKey].filesFor(DEFAULT_PLACEMENT))) {
278+
const content = render(PROJECT_NAME);
279+
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
280+
fs.writeFileSync(
281+
path.join(root, rel),
282+
typeof content === 'string' ? content : JSON.stringify(content, null, 2) + '\n',
283+
);
284+
}
285+
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')) as Manifest;
286+
linkDeclaredDependencies(root, manifest);
287+
const testFiles = Object.keys(createTemplates[templateKey].filesFor(DEFAULT_PLACEMENT)).filter((rel) =>
288+
/\.test\.[cm]?[jt]sx?$/.test(rel),
289+
);
290+
return { root, manifest, testFiles };
291+
}
292+
293+
/** Every file under `dir`, relative to it. */
294+
function listFiles(dir: string): string[] {
295+
if (!fs.existsSync(dir)) return [];
296+
return fs.readdirSync(dir, { recursive: true, withFileTypes: true })
297+
.filter((entry) => entry.isFile())
298+
.map((entry) => path.relative(dir, path.join(entry.parentPath, entry.name)))
299+
.sort();
300+
}
301+
302+
describe('[#25175] an `os create` template with a `test` script passes it out of the box', () => {
303+
it('the roster is populated, and still holds the template the defect was measured on', () => {
304+
expect(CREATE_ROSTER.length).toBeGreaterThan(0);
305+
expect(CREATE_ROSTER).toContain('plugin');
306+
});
307+
308+
it.each(CREATE_ROSTER)(
309+
'`os create %s`: the emitted `test` script runs the shipped tests and exits 0',
310+
(templateKey) => {
311+
const { root, manifest, testFiles } = emitCreate(templateKey);
312+
expect(testFiles.length, `\`os create ${templateKey}\` declares a test script and emits no test file`).toBeGreaterThan(0);
313+
314+
const { code, output, json } = runTestScript(root, manifest);
315+
316+
expect(code, `\`${manifest.scripts!.test}\` in a fresh \`os create ${templateKey}\` project:\n${output}`).toBe(0);
317+
expect(json, `the run wrote no JSON report:\n${output}`).not.toBeNull();
318+
expect(json!.numFailedTests).toBe(0);
319+
expect(json!.numPassedTests).toBeGreaterThan(0);
320+
expect(json!.testResults.map((r) => path.relative(root, r.name)).sort()).toEqual([...testFiles].sort());
321+
},
322+
120_000,
323+
);
324+
325+
it.each(CREATE_ROSTER)(
326+
'`os create %s`: after the emitted `build`, `dist/` holds no test and `test` runs the shipped tests once',
327+
(templateKey) => {
328+
const { root, manifest, testFiles } = emitCreate(templateKey);
329+
330+
const build = runScript(root, manifest, 'build');
331+
expect(build.code, `\`${manifest.scripts!.build}\`:\n${build.output}`).toBe(0);
332+
const dist = listFiles(path.join(root, 'dist'));
333+
// Calibration: a build that wrote nothing would pass the next line vacuously.
334+
expect(dist).toContain('index.js');
335+
expect(dist.filter((rel) => /\.test\./.test(rel)), 'the build compiled the starter test into dist/').toEqual([]);
336+
337+
const { code, output, json } = runTestScript(root, manifest);
338+
expect(code, output).toBe(0);
339+
expect(json!.testResults.map((r) => path.relative(root, r.name)).sort()).toEqual([...testFiles].sort());
340+
},
341+
120_000,
342+
);
343+
344+
it.each(CREATE_ROSTER)(
345+
'`os create %s`: the emitted `typecheck` script type-checks the starter test',
346+
(templateKey) => {
347+
const { root, manifest, testFiles } = emitCreate(templateKey);
348+
349+
const clean = runScript(root, manifest, 'typecheck');
350+
expect(clean.code, `\`${manifest.scripts!.typecheck}\` on the fresh scaffold:\n${clean.output}`).toBe(0);
351+
352+
const planted = path.join(root, testFiles[0]);
353+
fs.appendFileSync(planted, '\nexport const planted: number = \'not a number\';\n');
354+
const red = runScript(root, manifest, 'typecheck');
355+
expect(red.code, 'a type error in the starter test passed `typecheck`: the test is outside the checked program').not.toBe(0);
356+
expect(red.output).toContain(path.basename(testFiles[0]));
357+
},
358+
120_000,
359+
);
360+
361+
it('CONTROL: the same project without its shipped tests fails the `test` script (the defect)', () => {
362+
const { root, manifest, testFiles } = emitCreate(CREATE_ROSTER[0]);
363+
for (const rel of testFiles) fs.rmSync(path.join(root, rel));
364+
365+
const { code, json } = runTestScript(root, manifest);
366+
367+
expect(code, 'an empty suite must fail; a harness that reads 0 here measures nothing').not.toBe(0);
368+
expect(json?.numPassedTests ?? 0).toBe(0);
369+
}, 120_000);
370+
});

0 commit comments

Comments
 (0)