Skip to content

verify: an in-process handle on the booted stack — run a hook, flow, action or validation rule against the REAL engine and assert, so an app never fakes ctx.api again (epic hotcrm#1579, step 5a) #15951

Description

@os-zhuang

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)

  1. 「新增平台公开面」 — the test capability is platform-side.
  2. 「我认为平台的能力应该放在平台,但是平台该怎么设计提供这个能力,你需要完整的重新考虑」 — do not lift hotcrm's helpers into a package; redesign how the platform provides it.
  3. 「是不是 把执行能力并进 @objectstack/verify / os test 更合理?」 → the seat's design B′ (below) → 「同意」.
  4. 「应该还是刚才 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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions