From cfcde4735629831efc878bd73a150cd4ebeb8e73 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 23:33:09 +0000 Subject: [PATCH 1/5] feat(create-objectstack): the blank starter ships a test story on @objectstack/verify; plugin-spec docs teach bootStack The blank template gains a `test` script, a vitest config and one starter test (`note.test.ts`) that boots the scaffolded app's own stack with `bootStack` and asserts one business fact through the in-process handle. The Plugin Testing section of plugin-spec.mdx is rewritten around what ships, and the ghost `@objectstack/testing` package is gone from it. Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude --- .changeset/15952-blank-template-test-story.md | 17 ++ .github/workflows/scaffold-e2e.yml | 18 ++- content/docs/protocol/kernel/plugin-spec.mdx | 147 ++++++++++-------- .../src/templates/blank/README.md | 10 ++ .../src/templates/blank/note.test.ts | 54 +++++++ .../src/templates/blank/package.json | 7 +- .../src/templates/blank/tsconfig.json | 2 +- .../src/templates/blank/vitest.config.ts | 17 ++ packages/create-objectstack/vitest.config.ts | 10 +- 9 files changed, 214 insertions(+), 68 deletions(-) create mode 100644 .changeset/15952-blank-template-test-story.md create mode 100644 packages/create-objectstack/src/templates/blank/note.test.ts create mode 100644 packages/create-objectstack/src/templates/blank/vitest.config.ts diff --git a/.changeset/15952-blank-template-test-story.md b/.changeset/15952-blank-template-test-story.md new file mode 100644 index 0000000000..f1ac1e6e1e --- /dev/null +++ b/.changeset/15952-blank-template-test-story.md @@ -0,0 +1,17 @@ +--- +'create-objectstack': minor +--- + +The blank starter ships a test story: `pnpm test` boots the scaffolded app's own stack with `bootStack` from `@objectstack/verify` and asserts one business fact through its in-process handle + +Clause-②: no + +A project scaffolded with `npx create-objectstack` used to start with no `test` script and no way to assert its own behaviour, so the first test an author wrote was a hand-assembled kernel or a faked `ctx`. The blank template now carries: + +- **`note.test.ts`**, one starter test. It boots the app the project declares (its objects, its plugins, the services it requires) on the real engine, in-process, with an in-memory database and no server, and asserts that a note without a title is refused with the engine's own `VALIDATION_FAILED` error naming `title`, beside the same note with a title being saved. It addresses the object as `Note.name`, so it follows the namespace scaffolding writes. +- **`vitest.config.ts`**: a 60 s hook timeout for the boot, and vitest's console capture switched off, so a log line the stack writes while a worker shuts down cannot fail a green run. +- **`package.json`**: a `test` script (`vitest run`), and `@objectstack/verify` (at the project's `@objectstack/*` range) and `vitest` (`^4.0.0`, the range `objectstack init` and `objectstack create` declare) as devDependencies. +- **`tsconfig.json`** includes every root `*.ts` (`"*.ts"`, as `objectstack init` writes it), so `pnpm typecheck` checks the starter test and the vitest config. +- **`README.md`** lists `pnpm test` beside `validate`, `lint` and `typecheck`. + +The project's CI workflow is unchanged: its steps are the script set every scaffolder declares, and only this one ships a `test` script today. diff --git a/.github/workflows/scaffold-e2e.yml b/.github/workflows/scaffold-e2e.yml index 1097bade31..623d89da06 100644 --- a/.github/workflows/scaffold-e2e.yml +++ b/.github/workflows/scaffold-e2e.yml @@ -9,7 +9,7 @@ # # Job 1 (PRs touching the scaffolder / docker example) scaffolds with the # repo-built scaffolder and runs the generated project against the registry: -# install → validate → build → boot → health probes → docker build/run. That +# install → validate → build → test → boot → health probes → docker build/run. That # run scaffolds with `--skip-install` and installs the project itself, so it # exercises the scaffolder's `--skip-install` shape; a second, throwaway # project is scaffolded WITHOUT it purely to assert the shape a real user @@ -148,6 +148,16 @@ jobs: npm run validate npm run build + # The starter's own test (`note.test.ts`): it boots the generated + # project's stack in-process through @objectstack/verify, as installed + # from the registry, and asserts one business fact on the real engine. + # A scaffold whose `test` script is red out of the box teaches its user + # to ignore tests, so the starter ships it green or not at all. + - name: Test the generated project + run: | + cd "$RUNNER_TEMP/e2e-app" + npm test + # Two questions this step must answer about its own server, and used to # answer neither (#9779). Both belong here rather than in the CLI: `os # start` is already correct — the shell around it was not. @@ -486,7 +496,8 @@ jobs: # `build` is the gate every template must pass; `validate` is run only # when the scaffolded project defines it (the now-retired remote - # templates did not always ship it). + # templates did not always ship it), and `test` likewise: a published + # scaffolder older than the starter's `note.test.ts` declares none. - name: Gate the generated project run: | cd "$RUNNER_TEMP/canary-app" @@ -494,6 +505,9 @@ jobs: npm run validate fi npm run build + if node -e "process.exit(JSON.parse(require('fs').readFileSync('package.json','utf8')).scripts?.test ? 0 : 1)"; then + npm test + fi # Same two guards, same reasoning, as `Boot from the artifact and probe # health` in scaffold-local — see the measurement written out there. Kept diff --git a/content/docs/protocol/kernel/plugin-spec.mdx b/content/docs/protocol/kernel/plugin-spec.mdx index 8d4f735af0..4841478694 100644 --- a/content/docs/protocol/kernel/plugin-spec.mdx +++ b/content/docs/protocol/kernel/plugin-spec.mdx @@ -784,81 +784,104 @@ os package install ./dist/objectstack.json ## Plugin Testing - -The `@objectstack/testing` package and its `createTestContext()` / -`createTestEnvironment()` helpers shown below are **proposed** — there is no -such published package today. The snippets illustrate the intended test -ergonomics for the plugin spec, not a shipping API. - +Test a plugin the way it runs: boot a real stack that loads it, drive the +engine, and assert on what the engine did. `@objectstack/verify` ships the +harness for this, and unlike the manifest snippets above, the code in this +section is the shipped API. + +```bash +pnpm add -D @objectstack/verify vitest +``` -### Unit Tests +`bootStack(config, options?)` boots a configuration in-process: the real kernel +with ObjectQL, the bound hook chain, validation, the security middleware, +sharing and the REST dispatcher, on an in-memory database, with no server +process and no port. The stack it returns carries an **in-process handle**. +Each handle method passes the call to the door the kernel wired at boot (the +engine's own write, dry-run and read calls, or the runtime's `/automation` and +`/actions` routes driven without HTTP) and returns what that door returned. A +test never builds its own `ctx`, never orders hooks by hand and never copies a +permission check, so it keeps telling the truth when the engine changes. + +### Booting the stack ```typescript -// tests/triggers/account_validation.test.ts -import { createTestContext } from '@objectstack/testing'; -import accountValidation from '../../src/triggers/account_validation'; - -describe('Account Validation Trigger', () => { - it('should reject negative revenue', async () => { - const context = createTestContext(); - const record = { annual_revenue: -1000 }; - +// test/crm.test.ts +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { bootStack, type VerifyStack } from '@objectstack/verify'; +import config from '../objectstack.config'; + +describe('CRM plugin', () => { + let stack: VerifyStack; + let admin: string; + let rep: string; + + beforeAll(async () => { + stack = await bootStack(config); + admin = await stack.signIn(); // the dev admin the stack seeds + rep = await stack.signUp('rep@example.com'); // a fresh member: the default profile only + }); + + afterAll(async () => { + await stack.stop(); + }); + + it('refuses negative revenue', async () => { + // One real write: before* hooks, validation, the driver, after* hooks, + // and the permission check the caller is subject to. await expect( - accountValidation.execute({ record, context }) - ).rejects.toThrow('Annual revenue cannot be negative'); + stack.hooks.run('crm_account', 'insert', { name: 'Acme Corp', annual_revenue: -1000 }, { as: admin }), + ).rejects.toMatchObject({ code: 'VALIDATION_FAILED' }); }); - - it('should generate account number', async () => { - const context = createTestContext(); - const record = { name: 'Acme Corp' }; - - const result = await accountValidation.execute({ record, context }); - - expect(result.account_number).toMatch(/^ACC-\d{6}$/); + + it('numbers a new account', async () => { + const account = await stack.hooks.run('crm_account', 'insert', { name: 'Acme Corp' }, { as: admin }); + expect(account.account_number).toMatch(/^ACC-\d{6}$/); + }); + + it('keeps accounts away from a member without a grant', async () => { + await expect(stack.rows('crm_account', {}, { as: rep })) + .rejects.toMatchObject({ code: 'PERMISSION_DENIED', statusCode: 403 }); }); }); ``` -### Integration Tests +Assert on a refusal's `code` (and its status), never on its message alone. +A refusal from `hooks.run`, `validate` or `rows` is the engine's own error, +rethrown unchanged; one from `flows.*` or `actions.run` is the route's error +envelope (`code`, `status`, `details`), which `isVerifyRefusal(e)` recognises. -```typescript -// tests/integration/crm_workflow.test.ts -import { createTestEnvironment } from '@objectstack/testing'; +### What the handle drives -describe('CRM Workflow', () => { - let env; - - beforeAll(async () => { - env = await createTestEnvironment({ - plugins: ['@mycompany/crm'], - }); - }); - - it('should create account with contacts', async () => { - // Create account - const account = await env.objectQL.create('account', { - name: 'Test Corp', - industry: 'technology', - }); - - // Create contact - const contact = await env.objectQL.create('contact', { - first_name: 'John', - last_name: 'Doe', - account: account.id, - }); - - // Verify relationship - const accountWithContacts = await env.objectQL.findById('account', account.id, { - include: ['contacts'], - }); - - expect(accountWithContacts.contacts).toHaveLength(1); - expect(accountWithContacts.contacts[0].id).toBe(contact.id); - }); -}); +| Method | Door it drives | +|:---|:---| +| `hooks.run(object, operation, input, { as })` | One `'insert'`, `'update'` or `'delete'` through the engine, hooks, validation and permission checks included. `update` and `delete` address the row by `input.id`. | +| `validate(object, record, { as, mode? })` | The engine's validation pass without writing; resolves `{ valid, results }`. Hooks do not run. | +| `flows.run(name, params, { as })` / `flows.resume(run, input, { as })` | A flow through the runtime's automation route. Boot with `{ automation: true }` so the automation service is there to drive. | +| `actions.run(object, action, { as, recordId?, params? })` | A declared action through its route, param contract and permission gate included. | +| `seed(object, rows)` | Fixture rows written as the system, the way a stack's `data` is seeded: hooks and validation run, record-change flows do not. | +| `rows(object, where?, { as? })` | A read through the engine, as the system by default or as `as` under that caller's grants and row-level security. | +| `metadata` · `tenancy()` · `contextFor(token)` | The booted registry, the tenancy posture, and the execution context the platform resolves for a caller. | + +`as` is always a bearer token from `signIn()` or `signUp()` on the same stack. + +### A runtime plugin + +A plugin class (see [Runtime Contract](#runtime-contract)) boots with the stack +through `extraPlugins`, so its `init` and `start` run against the real kernel, +and the services it registers resolve from `stack.kernel`: + +```typescript +const stack = await bootStack(config, { extraPlugins: [new CRMPlugin()] }); +const crm = stack.kernel.getService('crm'); ``` +A pure helper with no engine behind it needs no harness: test it with plain +vitest. Many test files can share one boot through `bootStackOnce(config)`, +under vitest's `isolate: false`. A project scaffolded with +`npx create-objectstack` ships one test written this way (`note.test.ts`), and +the `@objectstack/verify` README documents every method and boot option. + ## Plugin Development Workflow ### 1. Scaffold Plugin diff --git a/packages/create-objectstack/src/templates/blank/README.md b/packages/create-objectstack/src/templates/blank/README.md index 3f248a0329..a2cacfa132 100644 --- a/packages/create-objectstack/src/templates/blank/README.md +++ b/packages/create-objectstack/src/templates/blank/README.md @@ -74,6 +74,8 @@ for OAuth, API keys, and which objects/actions become tools. stack, so `objectstack generate flow NAME` (or `view`, `action`, …) adds a file and one export line, and the item is loaded, and counted by `pnpm validate`, with no edit to the config +- `note.test.ts` — the starter test `pnpm test` runs, configured by + `vitest.config.ts` ## Connectors (default providers) @@ -112,6 +114,7 @@ pnpm validate # schema + CEL predicates + widget bindings (no artifact) pnpm lint # authoring rules validate does not run — e.g. a handler that # stopped lowering to a metadata-only body pnpm typecheck # TypeScript types against @objectstack/spec +pnpm test # your app's own tests, against the real engine ``` `pnpm validate` runs the same gates as `pnpm build` and catches mistakes that @@ -119,6 +122,13 @@ otherwise fail *silently at runtime* — e.g. a bare `done` (instead of `record.done`) in an action predicate that would hide the action on every record. See `AGENTS.md` for the full convention. +`pnpm test` checks what the app *does*. `note.test.ts` boots this app's stack +in-process with `bootStack` from `@objectstack/verify` (in-memory database, no +server) and asserts one business fact through its handle: a note without a +title is refused. Write your own the same way: `stack.hooks.run` makes a real +write through validation, permissions and hooks, and the same handle runs flows +and actions, seeds fixtures and reads rows as a given user. + ## Deploy The project ships container-ready — the `Dockerfile` builds your metadata into diff --git a/packages/create-objectstack/src/templates/blank/note.test.ts b/packages/create-objectstack/src/templates/blank/note.test.ts new file mode 100644 index 0000000000..e801ca9f51 --- /dev/null +++ b/packages/create-objectstack/src/templates/blank/note.test.ts @@ -0,0 +1,54 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { bootStack, type VerifyStack } from '@objectstack/verify'; +import config from './objectstack.config'; +import { Note } from './src/objects'; + +// Boots THIS app (its objects, its plugins, the services it requires) on the +// real ObjectStack engine, in this process, with an in-memory database and no +// server port, then asserts on what the engine does. Nothing is mocked: a +// write through `stack.hooks.run` takes the same path as a write over the REST +// API, through validation, the permission checks and every hook the app binds. +// +// The same handle runs a flow (`stack.flows.run`), an action +// (`stack.actions.run`), a validation pass without writing (`stack.validate`), +// seeds fixtures (`stack.seed`) and reads rows as a given user (`stack.rows`). +// The README of `@objectstack/verify` (in node_modules) documents each one. +// +// The object is addressed as `Note.name`, never as a typed-out string: +// scaffolding renamed it to carry this project's namespace, and adding your +// own objects works the same way. +describe('notes', () => { + let stack: VerifyStack; + let author: string; + + beforeAll(async () => { + stack = await bootStack(config); + // The development admin the stack seeds: a bearer token to act as. + author = await stack.signIn(); + }); + + afterAll(async () => { + await stack?.stop(); + }); + + it('cannot be saved without a title', async () => { + // `title` is declared `required` in src/objects/note.object.ts, and the + // engine refuses the write with its own error, naming the field. + await expect( + stack.hooks.run(Note.name, 'insert', { body: 'Agenda for Monday' }, { as: author }), + ).rejects.toMatchObject({ + code: 'VALIDATION_FAILED', + fields: [{ field: 'title', code: 'required' }], + }); + + // The same note with a title is saved, so the refusal above was the + // missing title and nothing else. + const note = await stack.hooks.run( + Note.name, + 'insert', + { title: 'Weekly sync', body: 'Agenda for Monday' }, + { as: author }, + ); + expect(note.title).toBe('Weekly sync'); + }); +}); diff --git a/packages/create-objectstack/src/templates/blank/package.json b/packages/create-objectstack/src/templates/blank/package.json index 48dca9cd5c..5d3efb4aeb 100644 --- a/packages/create-objectstack/src/templates/blank/package.json +++ b/packages/create-objectstack/src/templates/blank/package.json @@ -12,7 +12,8 @@ "build": "objectstack build", "validate": "objectstack validate", "lint": "objectstack lint", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "test": "vitest run" }, "dependencies": { "@objectstack/spec": "^17.0.0", @@ -24,6 +25,8 @@ }, "devDependencies": { "@objectstack/cli": "^17.0.0", - "typescript": "^5.3.0" + "@objectstack/verify": "^17.0.0", + "typescript": "^5.3.0", + "vitest": "^4.0.0" } } diff --git a/packages/create-objectstack/src/templates/blank/tsconfig.json b/packages/create-objectstack/src/templates/blank/tsconfig.json index 7c5f22bfb6..f529ef6f76 100644 --- a/packages/create-objectstack/src/templates/blank/tsconfig.json +++ b/packages/create-objectstack/src/templates/blank/tsconfig.json @@ -11,5 +11,5 @@ "noEmit": true, "isolatedModules": true }, - "include": ["objectstack.config.ts", "src/**/*"] + "include": ["*.ts", "src/**/*"] } diff --git a/packages/create-objectstack/src/templates/blank/vitest.config.ts b/packages/create-objectstack/src/templates/blank/vitest.config.ts new file mode 100644 index 0000000000..aa379415d5 --- /dev/null +++ b/packages/create-objectstack/src/templates/blank/vitest.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vitest/config'; + +// `pnpm test` runs every `*.test.ts` in this project. A test that boots the +// app (see note.test.ts) runs the real engine in this process, so two settings +// differ from vitest's defaults. +export default defineConfig({ + test: { + // Booting the stack takes a few seconds, longer on a busy CI runner, and + // the boot runs in a `beforeAll` hook: allow it more than the default 10s. + hookTimeout: 60_000, + // The stack logs while it boots and while it shuts down. A log line that + // arrives while vitest is closing a test worker can fail a run in which + // every test passed. Printing logs straight to the terminal, instead of + // through vitest's console capture, removes that failure. + disableConsoleIntercept: true, + }, +}); diff --git a/packages/create-objectstack/vitest.config.ts b/packages/create-objectstack/vitest.config.ts index 93fc82cc1c..d7ace906a7 100644 --- a/packages/create-objectstack/vitest.config.ts +++ b/packages/create-objectstack/vitest.config.ts @@ -1,6 +1,6 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { defineConfig } from 'vitest/config'; +import { configDefaults, defineConfig } from 'vitest/config'; const HERE = path.dirname(fileURLToPath(import.meta.url)); @@ -29,6 +29,14 @@ export default defineConfig({ // Enforced repo-wide by scripts/check-console-intercept-disarm.mjs. disableConsoleIntercept: true, include: ['test/**/*.test.ts', 'src/**/*.test.ts'], + // `src/templates/` is PAYLOAD: copied byte-for-byte into a scaffolded + // project and never compiled here (tsconfig.json excludes it too). The + // blank template ships its own `note.test.ts`, which boots THAT project's + // stack through `@objectstack/verify` — a dependency of the scaffolded + // project, not of this package. It runs where it lands: the Scaffold E2E + // workflow scaffolds a project, installs it from the registry and runs + // its `test` script. + exclude: [...configDefaults.exclude, 'src/templates/**'], environment: 'node', }, }); From 571f6b2407fbb5a94c7aae332288a55b93c72c23 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 00:11:46 +0000 Subject: [PATCH 2/5] feat(create-objectstack): the starter's vitest config quiets the engine's per-item registry log Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude --- .../create-objectstack/src/templates/blank/vitest.config.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/create-objectstack/src/templates/blank/vitest.config.ts b/packages/create-objectstack/src/templates/blank/vitest.config.ts index aa379415d5..21573f46e0 100644 --- a/packages/create-objectstack/src/templates/blank/vitest.config.ts +++ b/packages/create-objectstack/src/templates/blank/vitest.config.ts @@ -13,5 +13,9 @@ export default defineConfig({ // every test passed. Printing logs straight to the terminal, instead of // through vitest's console capture, removes that failure. disableConsoleIntercept: true, + // At its default level the engine logs one `[Registry]` line for every + // item it registers while booting, over a hundred for this starter alone. + // `warn` keeps the engine's warnings and errors and drops that chatter. + env: { OS_REGISTRY_LOG: 'warn' }, }, }); From 812d8dbf94b35c5530291989f4cdad5012627f97 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 00:14:38 +0000 Subject: [PATCH 3/5] docs(changeset): name the starter's registry-log setting and the CI workflow's script contract Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude --- .changeset/15952-blank-template-test-story.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/15952-blank-template-test-story.md b/.changeset/15952-blank-template-test-story.md index f1ac1e6e1e..6bfb8eb4b2 100644 --- a/.changeset/15952-blank-template-test-story.md +++ b/.changeset/15952-blank-template-test-story.md @@ -9,9 +9,9 @@ Clause-②: no A project scaffolded with `npx create-objectstack` used to start with no `test` script and no way to assert its own behaviour, so the first test an author wrote was a hand-assembled kernel or a faked `ctx`. The blank template now carries: - **`note.test.ts`**, one starter test. It boots the app the project declares (its objects, its plugins, the services it requires) on the real engine, in-process, with an in-memory database and no server, and asserts that a note without a title is refused with the engine's own `VALIDATION_FAILED` error naming `title`, beside the same note with a title being saved. It addresses the object as `Note.name`, so it follows the namespace scaffolding writes. -- **`vitest.config.ts`**: a 60 s hook timeout for the boot, and vitest's console capture switched off, so a log line the stack writes while a worker shuts down cannot fail a green run. +- **`vitest.config.ts`**: a 60 s hook timeout for the boot, vitest's console capture switched off, so a log line the stack writes while a worker shuts down cannot fail a green run, and `OS_REGISTRY_LOG: 'warn'`, which drops the engine's one-line-per-registered-item boot log (138 of the 341 lines the starter test printed). - **`package.json`**: a `test` script (`vitest run`), and `@objectstack/verify` (at the project's `@objectstack/*` range) and `vitest` (`^4.0.0`, the range `objectstack init` and `objectstack create` declare) as devDependencies. - **`tsconfig.json`** includes every root `*.ts` (`"*.ts"`, as `objectstack init` writes it), so `pnpm typecheck` checks the starter test and the vitest config. - **`README.md`** lists `pnpm test` beside `validate`, `lint` and `typecheck`. -The project's CI workflow is unchanged: its steps are the script set every scaffolder declares, and only this one ships a `test` script today. +The project's CI workflow is unchanged: its steps are the script set every scaffolder's project must declare, and `objectstack init -t app` declares no `test` script. From 9572bb790d9e6127ffc7fd659b3533c2099a8d2f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 00:49:50 +0000 Subject: [PATCH 4/5] revert(create-objectstack): hold the blank starter's test story off this PR; it lands docs-only The scaffold half (the blank template's test script, vitest config and starter test, create-objectstack's template exclude, the Scaffold E2E test step and the changeset) is held on claude/issue-15952-scaffold-held at 812d8dbf94: `npm install` of a vitest-4 project crashes on npm 10 upstream, and two repo gates count the template payload test as the scaffolder's own suite. This branch keeps the plugin-spec.mdx rewrite only. Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude --- .changeset/15952-blank-template-test-story.md | 17 ------ .github/workflows/scaffold-e2e.yml | 18 +------ .../src/templates/blank/README.md | 10 ---- .../src/templates/blank/note.test.ts | 54 ------------------- .../src/templates/blank/package.json | 7 +-- .../src/templates/blank/tsconfig.json | 2 +- .../src/templates/blank/vitest.config.ts | 21 -------- packages/create-objectstack/vitest.config.ts | 10 +--- 8 files changed, 6 insertions(+), 133 deletions(-) delete mode 100644 .changeset/15952-blank-template-test-story.md delete mode 100644 packages/create-objectstack/src/templates/blank/note.test.ts delete mode 100644 packages/create-objectstack/src/templates/blank/vitest.config.ts diff --git a/.changeset/15952-blank-template-test-story.md b/.changeset/15952-blank-template-test-story.md deleted file mode 100644 index 6bfb8eb4b2..0000000000 --- a/.changeset/15952-blank-template-test-story.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'create-objectstack': minor ---- - -The blank starter ships a test story: `pnpm test` boots the scaffolded app's own stack with `bootStack` from `@objectstack/verify` and asserts one business fact through its in-process handle - -Clause-②: no - -A project scaffolded with `npx create-objectstack` used to start with no `test` script and no way to assert its own behaviour, so the first test an author wrote was a hand-assembled kernel or a faked `ctx`. The blank template now carries: - -- **`note.test.ts`**, one starter test. It boots the app the project declares (its objects, its plugins, the services it requires) on the real engine, in-process, with an in-memory database and no server, and asserts that a note without a title is refused with the engine's own `VALIDATION_FAILED` error naming `title`, beside the same note with a title being saved. It addresses the object as `Note.name`, so it follows the namespace scaffolding writes. -- **`vitest.config.ts`**: a 60 s hook timeout for the boot, vitest's console capture switched off, so a log line the stack writes while a worker shuts down cannot fail a green run, and `OS_REGISTRY_LOG: 'warn'`, which drops the engine's one-line-per-registered-item boot log (138 of the 341 lines the starter test printed). -- **`package.json`**: a `test` script (`vitest run`), and `@objectstack/verify` (at the project's `@objectstack/*` range) and `vitest` (`^4.0.0`, the range `objectstack init` and `objectstack create` declare) as devDependencies. -- **`tsconfig.json`** includes every root `*.ts` (`"*.ts"`, as `objectstack init` writes it), so `pnpm typecheck` checks the starter test and the vitest config. -- **`README.md`** lists `pnpm test` beside `validate`, `lint` and `typecheck`. - -The project's CI workflow is unchanged: its steps are the script set every scaffolder's project must declare, and `objectstack init -t app` declares no `test` script. diff --git a/.github/workflows/scaffold-e2e.yml b/.github/workflows/scaffold-e2e.yml index 623d89da06..1097bade31 100644 --- a/.github/workflows/scaffold-e2e.yml +++ b/.github/workflows/scaffold-e2e.yml @@ -9,7 +9,7 @@ # # Job 1 (PRs touching the scaffolder / docker example) scaffolds with the # repo-built scaffolder and runs the generated project against the registry: -# install → validate → build → test → boot → health probes → docker build/run. That +# install → validate → build → boot → health probes → docker build/run. That # run scaffolds with `--skip-install` and installs the project itself, so it # exercises the scaffolder's `--skip-install` shape; a second, throwaway # project is scaffolded WITHOUT it purely to assert the shape a real user @@ -148,16 +148,6 @@ jobs: npm run validate npm run build - # The starter's own test (`note.test.ts`): it boots the generated - # project's stack in-process through @objectstack/verify, as installed - # from the registry, and asserts one business fact on the real engine. - # A scaffold whose `test` script is red out of the box teaches its user - # to ignore tests, so the starter ships it green or not at all. - - name: Test the generated project - run: | - cd "$RUNNER_TEMP/e2e-app" - npm test - # Two questions this step must answer about its own server, and used to # answer neither (#9779). Both belong here rather than in the CLI: `os # start` is already correct — the shell around it was not. @@ -496,8 +486,7 @@ jobs: # `build` is the gate every template must pass; `validate` is run only # when the scaffolded project defines it (the now-retired remote - # templates did not always ship it), and `test` likewise: a published - # scaffolder older than the starter's `note.test.ts` declares none. + # templates did not always ship it). - name: Gate the generated project run: | cd "$RUNNER_TEMP/canary-app" @@ -505,9 +494,6 @@ jobs: npm run validate fi npm run build - if node -e "process.exit(JSON.parse(require('fs').readFileSync('package.json','utf8')).scripts?.test ? 0 : 1)"; then - npm test - fi # Same two guards, same reasoning, as `Boot from the artifact and probe # health` in scaffold-local — see the measurement written out there. Kept diff --git a/packages/create-objectstack/src/templates/blank/README.md b/packages/create-objectstack/src/templates/blank/README.md index a2cacfa132..3f248a0329 100644 --- a/packages/create-objectstack/src/templates/blank/README.md +++ b/packages/create-objectstack/src/templates/blank/README.md @@ -74,8 +74,6 @@ for OAuth, API keys, and which objects/actions become tools. stack, so `objectstack generate flow NAME` (or `view`, `action`, …) adds a file and one export line, and the item is loaded, and counted by `pnpm validate`, with no edit to the config -- `note.test.ts` — the starter test `pnpm test` runs, configured by - `vitest.config.ts` ## Connectors (default providers) @@ -114,7 +112,6 @@ pnpm validate # schema + CEL predicates + widget bindings (no artifact) pnpm lint # authoring rules validate does not run — e.g. a handler that # stopped lowering to a metadata-only body pnpm typecheck # TypeScript types against @objectstack/spec -pnpm test # your app's own tests, against the real engine ``` `pnpm validate` runs the same gates as `pnpm build` and catches mistakes that @@ -122,13 +119,6 @@ otherwise fail *silently at runtime* — e.g. a bare `done` (instead of `record.done`) in an action predicate that would hide the action on every record. See `AGENTS.md` for the full convention. -`pnpm test` checks what the app *does*. `note.test.ts` boots this app's stack -in-process with `bootStack` from `@objectstack/verify` (in-memory database, no -server) and asserts one business fact through its handle: a note without a -title is refused. Write your own the same way: `stack.hooks.run` makes a real -write through validation, permissions and hooks, and the same handle runs flows -and actions, seeds fixtures and reads rows as a given user. - ## Deploy The project ships container-ready — the `Dockerfile` builds your metadata into diff --git a/packages/create-objectstack/src/templates/blank/note.test.ts b/packages/create-objectstack/src/templates/blank/note.test.ts deleted file mode 100644 index e801ca9f51..0000000000 --- a/packages/create-objectstack/src/templates/blank/note.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -import { bootStack, type VerifyStack } from '@objectstack/verify'; -import config from './objectstack.config'; -import { Note } from './src/objects'; - -// Boots THIS app (its objects, its plugins, the services it requires) on the -// real ObjectStack engine, in this process, with an in-memory database and no -// server port, then asserts on what the engine does. Nothing is mocked: a -// write through `stack.hooks.run` takes the same path as a write over the REST -// API, through validation, the permission checks and every hook the app binds. -// -// The same handle runs a flow (`stack.flows.run`), an action -// (`stack.actions.run`), a validation pass without writing (`stack.validate`), -// seeds fixtures (`stack.seed`) and reads rows as a given user (`stack.rows`). -// The README of `@objectstack/verify` (in node_modules) documents each one. -// -// The object is addressed as `Note.name`, never as a typed-out string: -// scaffolding renamed it to carry this project's namespace, and adding your -// own objects works the same way. -describe('notes', () => { - let stack: VerifyStack; - let author: string; - - beforeAll(async () => { - stack = await bootStack(config); - // The development admin the stack seeds: a bearer token to act as. - author = await stack.signIn(); - }); - - afterAll(async () => { - await stack?.stop(); - }); - - it('cannot be saved without a title', async () => { - // `title` is declared `required` in src/objects/note.object.ts, and the - // engine refuses the write with its own error, naming the field. - await expect( - stack.hooks.run(Note.name, 'insert', { body: 'Agenda for Monday' }, { as: author }), - ).rejects.toMatchObject({ - code: 'VALIDATION_FAILED', - fields: [{ field: 'title', code: 'required' }], - }); - - // The same note with a title is saved, so the refusal above was the - // missing title and nothing else. - const note = await stack.hooks.run( - Note.name, - 'insert', - { title: 'Weekly sync', body: 'Agenda for Monday' }, - { as: author }, - ); - expect(note.title).toBe('Weekly sync'); - }); -}); diff --git a/packages/create-objectstack/src/templates/blank/package.json b/packages/create-objectstack/src/templates/blank/package.json index 5d3efb4aeb..48dca9cd5c 100644 --- a/packages/create-objectstack/src/templates/blank/package.json +++ b/packages/create-objectstack/src/templates/blank/package.json @@ -12,8 +12,7 @@ "build": "objectstack build", "validate": "objectstack validate", "lint": "objectstack lint", - "typecheck": "tsc --noEmit", - "test": "vitest run" + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/spec": "^17.0.0", @@ -25,8 +24,6 @@ }, "devDependencies": { "@objectstack/cli": "^17.0.0", - "@objectstack/verify": "^17.0.0", - "typescript": "^5.3.0", - "vitest": "^4.0.0" + "typescript": "^5.3.0" } } diff --git a/packages/create-objectstack/src/templates/blank/tsconfig.json b/packages/create-objectstack/src/templates/blank/tsconfig.json index f529ef6f76..7c5f22bfb6 100644 --- a/packages/create-objectstack/src/templates/blank/tsconfig.json +++ b/packages/create-objectstack/src/templates/blank/tsconfig.json @@ -11,5 +11,5 @@ "noEmit": true, "isolatedModules": true }, - "include": ["*.ts", "src/**/*"] + "include": ["objectstack.config.ts", "src/**/*"] } diff --git a/packages/create-objectstack/src/templates/blank/vitest.config.ts b/packages/create-objectstack/src/templates/blank/vitest.config.ts deleted file mode 100644 index 21573f46e0..0000000000 --- a/packages/create-objectstack/src/templates/blank/vitest.config.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -// `pnpm test` runs every `*.test.ts` in this project. A test that boots the -// app (see note.test.ts) runs the real engine in this process, so two settings -// differ from vitest's defaults. -export default defineConfig({ - test: { - // Booting the stack takes a few seconds, longer on a busy CI runner, and - // the boot runs in a `beforeAll` hook: allow it more than the default 10s. - hookTimeout: 60_000, - // The stack logs while it boots and while it shuts down. A log line that - // arrives while vitest is closing a test worker can fail a run in which - // every test passed. Printing logs straight to the terminal, instead of - // through vitest's console capture, removes that failure. - disableConsoleIntercept: true, - // At its default level the engine logs one `[Registry]` line for every - // item it registers while booting, over a hundred for this starter alone. - // `warn` keeps the engine's warnings and errors and drops that chatter. - env: { OS_REGISTRY_LOG: 'warn' }, - }, -}); diff --git a/packages/create-objectstack/vitest.config.ts b/packages/create-objectstack/vitest.config.ts index d7ace906a7..93fc82cc1c 100644 --- a/packages/create-objectstack/vitest.config.ts +++ b/packages/create-objectstack/vitest.config.ts @@ -1,6 +1,6 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { configDefaults, defineConfig } from 'vitest/config'; +import { defineConfig } from 'vitest/config'; const HERE = path.dirname(fileURLToPath(import.meta.url)); @@ -29,14 +29,6 @@ export default defineConfig({ // Enforced repo-wide by scripts/check-console-intercept-disarm.mjs. disableConsoleIntercept: true, include: ['test/**/*.test.ts', 'src/**/*.test.ts'], - // `src/templates/` is PAYLOAD: copied byte-for-byte into a scaffolded - // project and never compiled here (tsconfig.json excludes it too). The - // blank template ships its own `note.test.ts`, which boots THAT project's - // stack through `@objectstack/verify` — a dependency of the scaffolded - // project, not of this package. It runs where it lands: the Scaffold E2E - // workflow scaffolds a project, installs it from the registry and runs - // its `test` script. - exclude: [...configDefaults.exclude, 'src/templates/**'], environment: 'node', }, }); From dfead30310f076c7ddfd4c1e88d12ce97113a471 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 00:59:27 +0000 Subject: [PATCH 5/5] docs(plugin-spec): the Plugin Testing section states only what ships; drop the starter-test clause The blank starter's note.test.ts is held off this PR, so the sentence naming it described nothing a scaffolded project carries today. Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude --- content/docs/protocol/kernel/plugin-spec.mdx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/content/docs/protocol/kernel/plugin-spec.mdx b/content/docs/protocol/kernel/plugin-spec.mdx index 4841478694..56fc06aa1a 100644 --- a/content/docs/protocol/kernel/plugin-spec.mdx +++ b/content/docs/protocol/kernel/plugin-spec.mdx @@ -878,9 +878,8 @@ const crm = stack.kernel.getService('crm'); A pure helper with no engine behind it needs no harness: test it with plain vitest. Many test files can share one boot through `bootStackOnce(config)`, -under vitest's `isolate: false`. A project scaffolded with -`npx create-objectstack` ships one test written this way (`note.test.ts`), and -the `@objectstack/verify` README documents every method and boot option. +under vitest's `isolate: false`. The `@objectstack/verify` README documents +every method and boot option. ## Plugin Development Workflow