Sub-issue of the epic hotcrm#1579 (step 5a). Filed by the director seat (objectstack#12708, summon #15); pm:epic reserves it for the epic PM — ⛔ not pm:queue, domain seats do not take it. domain:* / type / priority are triage's.
Ruling (maintainer, 2026-09-05, live director chat, verbatim, in order)
- 「新增平台公开面」 — the test capability is platform-side.
- 「我认为平台的能力应该放在平台,但是平台该怎么设计提供这个能力,你需要完整的重新考虑」 — do not lift hotcrm's helpers into a package; redesign how the platform provides it.
- 「是不是 把执行能力并进 @objectstack/verify / os test 更合理?」 → the seat's design B′ (below) → 「同意」.
- 「应该还是刚才 hotcrm 专题卡的子卡片吧」 — filed as sub-issues of hotcrm#1579.
Governing text. hotcrm AGENTS.md § "Scope — a pure metadata application (2026-08-31 ruling)", rule 3: "Lint, validation, gates and diagnostics belong to the platform, uniformly … ⛔ Do not grow a gate farm." Protocol baseline (maintainer 2026-09-05): 「本项目以协议为基准。所以开发应该对其协议,协议有问题应该立卡修改协议」. ADR-0054 (prove-it-runs) is the ADR that created @objectstack/verify; this card extends it, it does not open a new territory.
Measured (objectstack origin/main 04679418; hotcrm origin/main 021db549; 2026-09-05T15:4xZ)
What exists. @objectstack/verify is published (packages/verify/package.json: 17.3.0, public). bootStack(config, options) (packages/verify/src/harness.ts) boots the real ObjectKernel + ObjectQLPlugin + Hono/REST + auth + SecurityPlugin(appSecurityPluginOptions(config)) + sharing + settings + analytics + PlatformObjectsPlugin on in-memory SQLite, request-injection, no socket. VerifyStack exposes kernel, api, raw, signIn, signUp, apiAs, stop (harness.ts:80-100). Two derived proof families: runCrudVerification, runRlsProofs (src/index.ts).
The gap. There is no in-process way to invoke a hook, flow, action or validation rule against the booted engine and read the result. An app can only write through HTTP and infer from persisted rows. QA.TestRunner (packages/core/src/qa/) has an HTTP adapter only; os test runs JSON Quality-Protocol suites over HTTP against a running server. createTestKernel / TestHarness / @objectstack/testing: not found — and content/docs/protocol/kernel/plugin-spec.mdx:758-765 documents @objectstack/testing's createTestContext() with a callout saying it is proposed, not published (step 5b's docs half).
What every consumer does instead.
- hotcrm
test/helpers/: hook-harness.ts (618 lines / 38 importers) delegates ctx.input to the real wrapDeclarativeHook but fakes ctx.api over arrays with a hand-written Mongo-ish matches(); flow-harness.ts (720 / 20) runs the real AutomationEngine but re-implements the driver and hook dispatch ([...hooks].sort((a,b) => priority)); action-sandbox.ts (407 / 17) runs the real QuickJSScriptRunner + actionBodyRunnerFactory over a stub engine that copies measured kernel rules; tenancy-probe.ts is a hand-written plugin registering a fake tenancy service. No permission check anywhere. The same repo already boots a real ObjectKernel in 10 tests and drives real ObjectQL over InMemoryDriver / SqliteWasmDriver in ~53.
examples/app-crm, app-todo, app-showcase: each hand-assembles kernel/ObjectQL/driver/plugins in its own tests; none depends on @objectstack/verify.
packages/qa/dogfood/test/fixtures/* (14 fixtures) + getSharedShowcase() (test/shared-showcase.ts:83, worker-scoped boot memo) — private, not exported.
packages/create-objectstack/src/templates/blank/package.json: no test script, no vitest — a new app is scaffolded with no testing story.
docs/audits/2026-09-hotcrm-handwritten-test-split.md:177-185: "4,069 lines are unambiguously absorbable … Every line exists because the platform ships no first-party test harness … A first-party harness deletes all of it."
The ask — design B′: verify takes the execution capability; no new package; os test untouched
Add to @objectstack/verify an in-process handle on the stack bootStack already boots. Every method is a thin facade over a service the kernel already wired at boot — the same engine path a REST write takes — with zero re-implemented semantics (no own ctx.api, no own hook ordering, no own state machine, no own permission model):
hooks.run(object, event, input, { user }) — runs the bound hook chain for that object/event through the real engine: real ordering, real ctx.api (= the booted ObjectQL), real permission context for user; returns the engine's verdict (mutated record / refusal error) so a test asserts on what the engine did.
flows.run(name, params, { user }) / flows.resume(runId, screenInput) — through the AutomationEngine registered at boot; returns the run handle the engine returns.
actions.run(object, action, { record, input, user }) — through the runtime's QuickJSScriptRunner + body runner factories as wired at boot.
validate(object, record, { user }) — the engine's validation-rule pass for one record.
seed(object, rows, { asSystem }) / rows(object, where?) — real ObjectQL writes/reads, so fixtures land where the app's code will find them.
metadata — the booted registry (objects, fields, profiles, locale packs), so an app never imports platform-object rosters by hand to learn what exists.
BootOptions.tenancy: 'single' | 'multi' — replaces the hand-written tenancy probe (--multi-tenant already exists on os verify; expose the option, do not invent a second one).
bootStackOnce(config, options) — the worker-scoped memo getSharedShowcase() already implements privately, promoted so 80+ test files can share one boot.
Design rules (binding on the implementer). (1) If a method needs a semantic the kernel does not expose, that is a kernel gap: file it, do not re-implement it in verify. (2) Same package, new exports; ⛔ no new package, ⛔ no change to os test (HTTP JSON suites are a different instrument). (3) The handle lives beside bootStack; it is not a second boot path. (4) Names above are the ask's shape, not a contract — the implementer proposes the final surface in the PR with the reasoning; the spec seat reads it as clause-②.
Acceptance
- Each method has a test in
packages/verify that proves it drives the real engine: an ablation that breaks the corresponding kernel service turns the method's test red (no method can pass against a stub).
- Parity pin: for one fixture hook,
hooks.run(...) and the same write through apiAs(...) produce the same persisted row and the same refusal on the negative case — the handle is the REST path minus HTTP, provably.
- The five hotcrm exemplars are portable with no helper:
test/hooks-runtime-sales.test.ts (hook), test/flow-quote.test.ts (flow run + resume), test/global-actions.test.ts (action body), test/sla-at-risk-live-work.test.ts (hook + predicate), test/unassigned-case-triage-reach.test.ts (security + tenancy). Show one ported in the PR as the proof of ergonomics (in packages/verify's own fixtures, not by editing hotcrm).
os verify behaviour unchanged; existing runCrudVerification / runRlsProofs tests unchanged.
- Changeset
@objectstack/verify minor. Clause-②: yes — new exported symbols on a published package; contract-review tier, spec seat reads the derived accept set.
- README / docs for
@objectstack/verify name the handle; the plugin-spec.mdx ghost is step 5b's.
Not in this card
Derived proof families beyond CRUD/RLS (step 5c, Blocked-by this card); the docs callout and the scaffold test story (step 5b); hotcrm's migration off its helpers (hotcrm card, Blocked-by this card published and pinned, per rule 2); any os test / QA-protocol change; any governed surface.
Refs: hotcrm#1579 (epic; census 5552607309; seat analysis comments) · docs/audits/2026-09-hotcrm-handwritten-test-split.md · ADR-0054 · objectstack#15935 (step 1) · objectstack#13848 (2026-08-31 rulings).
Sub-issue of the epic hotcrm#1579 (step 5a). Filed by the director seat (objectstack#12708, summon #15);
pm:epicreserves it for the epic PM — ⛔ notpm:queue, domain seats do not take it.domain:*/ type / priority are triage's.Ruling (maintainer, 2026-09-05, live director chat, verbatim, in order)
Governing text. hotcrm
AGENTS.md§ "Scope — a pure metadata application (2026-08-31 ruling)", rule 3: "Lint, validation, gates and diagnostics belong to the platform, uniformly … ⛔ Do not grow a gate farm." Protocol baseline (maintainer 2026-09-05): 「本项目以协议为基准。所以开发应该对其协议,协议有问题应该立卡修改协议」. ADR-0054 (prove-it-runs) is the ADR that created@objectstack/verify; this card extends it, it does not open a new territory.Measured (objectstack
origin/main04679418; hotcrmorigin/main021db549; 2026-09-05T15:4xZ)What exists.
@objectstack/verifyis published (packages/verify/package.json:17.3.0, public).bootStack(config, options)(packages/verify/src/harness.ts) boots the realObjectKernel+ObjectQLPlugin+ Hono/REST + auth +SecurityPlugin(appSecurityPluginOptions(config))+ sharing + settings + analytics +PlatformObjectsPluginon in-memory SQLite, request-injection, no socket.VerifyStackexposeskernel,api,raw,signIn,signUp,apiAs,stop(harness.ts:80-100). Two derived proof families:runCrudVerification,runRlsProofs(src/index.ts).The gap. There is no in-process way to invoke a hook, flow, action or validation rule against the booted engine and read the result. An app can only write through HTTP and infer from persisted rows.
QA.TestRunner(packages/core/src/qa/) has an HTTP adapter only;os testruns JSON Quality-Protocol suites over HTTP against a running server.createTestKernel/TestHarness/@objectstack/testing: not found — andcontent/docs/protocol/kernel/plugin-spec.mdx:758-765documents@objectstack/testing'screateTestContext()with a callout saying it is proposed, not published (step 5b's docs half).What every consumer does instead.
test/helpers/:hook-harness.ts(618 lines / 38 importers) delegatesctx.inputto the realwrapDeclarativeHookbut fakesctx.apiover arrays with a hand-written Mongo-ishmatches();flow-harness.ts(720 / 20) runs the realAutomationEnginebut re-implements the driver and hook dispatch ([...hooks].sort((a,b) => priority));action-sandbox.ts(407 / 17) runs the realQuickJSScriptRunner+actionBodyRunnerFactoryover a stub engine that copies measured kernel rules;tenancy-probe.tsis a hand-written plugin registering a faketenancyservice. No permission check anywhere. The same repo already boots a realObjectKernelin 10 tests and drives realObjectQLoverInMemoryDriver/SqliteWasmDriverin ~53.examples/app-crm,app-todo,app-showcase: each hand-assembles kernel/ObjectQL/driver/plugins in its own tests; none depends on@objectstack/verify.packages/qa/dogfood/test/fixtures/*(14 fixtures) +getSharedShowcase()(test/shared-showcase.ts:83, worker-scoped boot memo) — private, not exported.packages/create-objectstack/src/templates/blank/package.json: notestscript, no vitest — a new app is scaffolded with no testing story.docs/audits/2026-09-hotcrm-handwritten-test-split.md:177-185: "4,069 lines are unambiguously absorbable … Every line exists because the platform ships no first-party test harness … A first-party harness deletes all of it."The ask — design B′:
verifytakes the execution capability; no new package;os testuntouchedAdd to
@objectstack/verifyan in-process handle on the stackbootStackalready boots. Every method is a thin facade over a service the kernel already wired at boot — the same engine path a REST write takes — with zero re-implemented semantics (no ownctx.api, no own hook ordering, no own state machine, no own permission model):hooks.run(object, event, input, { user })— runs the bound hook chain for that object/event through the real engine: real ordering, realctx.api(= the booted ObjectQL), real permission context foruser; returns the engine's verdict (mutated record / refusal error) so a test asserts on what the engine did.flows.run(name, params, { user })/flows.resume(runId, screenInput)— through theAutomationEngineregistered at boot; returns the run handle the engine returns.actions.run(object, action, { record, input, user })— through the runtime'sQuickJSScriptRunner+ body runner factories as wired at boot.validate(object, record, { user })— the engine's validation-rule pass for one record.seed(object, rows, { asSystem })/rows(object, where?)— real ObjectQL writes/reads, so fixtures land where the app's code will find them.metadata— the booted registry (objects, fields, profiles, locale packs), so an app never imports platform-object rosters by hand to learn what exists.BootOptions.tenancy: 'single' | 'multi'— replaces the hand-written tenancy probe (--multi-tenantalready exists onos verify; expose the option, do not invent a second one).bootStackOnce(config, options)— the worker-scoped memogetSharedShowcase()already implements privately, promoted so 80+ test files can share one boot.Design rules (binding on the implementer). (1) If a method needs a semantic the kernel does not expose, that is a kernel gap: file it, do not re-implement it in
verify. (2) Same package, new exports; ⛔ no new package, ⛔ no change toos test(HTTP JSON suites are a different instrument). (3) The handle lives besidebootStack; it is not a second boot path. (4) Names above are the ask's shape, not a contract — the implementer proposes the final surface in the PR with the reasoning; the spec seat reads it as clause-②.Acceptance
packages/verifythat proves it drives the real engine: an ablation that breaks the corresponding kernel service turns the method's test red (no method can pass against a stub).hooks.run(...)and the same write throughapiAs(...)produce the same persisted row and the same refusal on the negative case — the handle is the REST path minus HTTP, provably.test/hooks-runtime-sales.test.ts(hook),test/flow-quote.test.ts(flow run + resume),test/global-actions.test.ts(action body),test/sla-at-risk-live-work.test.ts(hook + predicate),test/unassigned-case-triage-reach.test.ts(security + tenancy). Show one ported in the PR as the proof of ergonomics (inpackages/verify's own fixtures, not by editing hotcrm).os verifybehaviour unchanged; existingrunCrudVerification/runRlsProofstests unchanged.@objectstack/verifyminor.Clause-②: yes— new exported symbols on a published package; contract-review tier, spec seat reads the derived accept set.@objectstack/verifyname the handle; theplugin-spec.mdxghost is step 5b's.Not in this card
Derived proof families beyond CRUD/RLS (step 5c,
Blocked-bythis card); the docs callout and the scaffold test story (step 5b); hotcrm's migration off its helpers (hotcrm card,Blocked-bythis card published and pinned, per rule 2); anyos test/ QA-protocol change; any governed surface.Refs: hotcrm#1579 (epic; census 5552607309; seat analysis comments) ·
docs/audits/2026-09-hotcrm-handwritten-test-split.md· ADR-0054 · objectstack#15935 (step 1) · objectstack#13848 (2026-08-31 rulings).