Skip to content

Commit fbb065f

Browse files
feat(verify): the handle fronts the automation engine's condition evaluator (#22553)
Part of #22301 Clause-②: yes (widening) Item 8 of the card ("`automation.evaluateCondition` is not fronted"): the verify handle gains a door to the automation engine's own condition evaluator. Items 5, 6 and 7 and item 1's remaining composition gap stay open on the card; this PR touches none of them. ## Premise, read on `origin/main` `86bf9ed7d5` before any edit - `AutomationEngine.evaluateCondition` exists at `packages/services/service-automation/src/engine.ts:12225`. It takes a condition (a string, or an envelope `{ dialect?, source?, ast? }`) and a `Map` of variables, and returns a boolean. - It is not on the slot's contract: `IAutomationService` (`packages/spec/src/contracts/automation-service.ts:739`) has no `evaluateCondition` member (grep: 0 hits in that file). - The kernel's `automation` service is the engine instance itself. `AutomationServicePlugin` registers `this.engine` under `'automation'` (`plugin.ts:975`), and that is the only producer of the slot in `packages/` (grep: 1 hit). - The handle had `contextFor`, `hooks.run`, `hooks.updateWhere`, `validate`, `flows.run`, `flows.resume`, `actions.run`, `seed`, `rows`, `metadata` and `tenancy`. None reaches the evaluator (`evaluateCondition` in `packages/verify`: 0 hits). - hotcrm, read-only from a sparse clone at `f0afcbd`: `conditionHolds` (`test/helpers/verify-stack.ts:184`-`:188`) calls `stack.kernel.getService('automation').evaluateCondition(…)` by hand. It wraps a string condition as `{ dialect: 'cel', source }` first. It has 28 call sites in 5 test files. ## What lands - **`stack.automation.evaluateCondition(condition, variables)`** resolves the booted kernel's `automation` service at call time (`kernel.getServiceAsync`, the lookup the handle's engine doors use). It calls the service's own `evaluateCondition` and resolves with its boolean, unchanged. There is no second evaluator and no re-derived dialect rule, and no caller is resolved, because the evaluator takes none. - **`condition`** has the engine's own parameter type, read off the method (`Parameters` of `AutomationEngine['evaluateCondition']`), so the door accepts exactly the shapes the engine accepts. It is handed over as given. The engine's own flow sites hand the evaluator an envelope: the start gate wraps a string `condition` as `{ dialect: 'cel', source }`, the `decision` executor wraps each branch `expression` the same way, and an edge's condition already is one. A bare string gets the engine's sniff (CEL unless it carries a `{var}` hole), the reading a screen field's `visibleWhen` gets. The JSDoc says which to pass. - **`variables`** is a plain object. Each own key becomes one entry of the `Map` the engine takes. - **Faults are the engine's.** A condition the engine cannot evaluate rejects with the engine's own error, never `false`. No catch is added. - **No automation service on the stack:** the door rejects with the kernel's own branded `SERVICE_NOT_REGISTERED` error, whose `serviceName` is `'automation'` (`isServiceNotRegisteredError` from `@objectstack/core`). No error code is minted in `@objectstack/verify`, so its error-code ledger row is unchanged and nothing in `packages/spec` changes. The remedy (`automation: true`, or the app's `requires: ['automation']`) is in the JSDoc and the README. - **The name.** `automation` is the kernel slot the door resolves, and `evaluateCondition` is the engine method it calls, so the door reads as the call it makes, the way `tenancy()` is named after its slot. The `flows` group was the other candidate, but its doors are dispatcher routes run as a caller, and this one is an engine call with no caller. - The door is named in `handle.ts`'s door roster, in `VerifyStack`'s member list (`harness.ts`, docblock only) and in the README's handle section, API list and the `automation` boot option. - `.changeset/22301-verify-evaluate-condition-door.md`: `@objectstack/verify` `minor`, `Clause-②: yes (widening)`. - **`packages/qa/dogfood/test/rls-runner.test.ts` (+1, patch round at `1582733139`).** Its hand-built `VerifyStack` literal now stubs `automation: undefined as never` beside the other handle members it does not model. Without it, `@objectstack/dogfood#typecheck` failed with TS2741 on `79e9053e50`. A typed grep across `packages/**`, `examples/**`, `apps/**`, `skills/**` and `content/docs` found no other hand-built `VerifyStack` / `VerifyHandle` literal. ## Evidence, at HEAD `79e9053e50` - **The pins:** `packages/verify/src/handle.automation-door.test.ts`, 6 / 6. Two boots: one whose app declares `requires: ['automation']`, and one without the service. 1. A spy on the booted service instance sees one call with the same envelope object and a `Map` whose entries equal the variables, and its return value is the door's answer. 2. The truth table over record-shaped variables (`record`, `previous`) is true once and false twice, equal to the engine called directly. 3. An envelope and a bare CEL string answer alike. 4. A bare `{amount} > 100` keeps the template dialect (true), and the spy sees the string as given. The same text in a CEL envelope is the brace trap: the door rejects with the same message the engine throws directly. 5. A CEL parse error, and a reference to an unbound variable, reject with the engine's own error: the same message as the direct call, not a `VerifyRefusal`, no `code` added. 6. Without the service, the door rejects with the kernel's `SERVICE_NOT_REGISTERED` error (`code`, `serviceName: 'automation'`, the brand predicate), with the same message the kernel gives when asked directly. - **Two ablations, on the committed head, each restored blob-equal to HEAD** (`node scripts/ablation-replace.mjs`, anchor hit 1 -> 0, blob changed, `git diff HEAD` empty after restore). The subject resolves to `src/` (relative imports, no package `exports`), so no rebuild is involved. - A: the door answered around the engine (a stand-in that never calls it). Predicted 5 red and 1 green; observed 5 red, 1 green (the no-service pin, which the kernel answers first). - B: the door wrapped a string as a CEL envelope, as the hotcrm helper does. Predicted only the dialect pin red; observed only that pin red (`condition failed to evaluate as CEL: Expected COLON, got RBRACE`). - **The type is the engine's.** A one-off probe passing `42` as the condition reds `tsc` with TS2345, naming the engine's parameter type. The probe was removed and the tree is clean. - **`@objectstack/verify` in full:** 26 files / 209 tests pass, run at `0aac167d63`. The one later commit changes a docblock only. Typecheck exit 0, with the new test file in the test program (1 of 26 test files listed by `--listFiles`). - **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `79e9053e50` derives 64 families. 63 ran, each exit 0. `pnpm check:dual-build-cjs-loads` is NOT MEASURED: it needs a whole-workspace build, which this dispatch excludes, so CI runs it. `--ran` reconciles 64 of 64, with 0 UNRUN. From the artifact-roster block, 49 of its 52 commands ran, each exit 0. That includes `check:error-code-provenance`: 337 stamp sites, each listed or waived, and nothing new under `@objectstack/verify`. The other three are the PR-context guards. They run against this PR once it exists, and their readings go in the card report. **Patch round, at `1582733139`:** - the dogfood closure build was 63/63; `pnpm --filter @objectstack/dogfood typecheck` and `pnpm --filter @objectstack/verify typecheck` exit 0; the door file is 6/6. - Control leg: deleting the new line turns the dogfood typecheck red with CI's exact TS2741, and the restore was blob-equal. - Gates: 68 derived, 67 run and 1 NOT MEASURED (`dual-build-cjs-loads`), 0 UNRUN; the rosters were 51; CI showed 32 success and 3 skipped. ## Acceptance notes Noted here, not filed: - The door types the `automation` slot as the engine class, because `evaluateCondition` is not on `IAutomationService`. This is the same reason `engine` types the `objectql` slot as `ObjectQL`. Another provider under that slot would answer a `TypeError` at this door. There is one producer of the slot in `packages/` today, so this is an observation with no reach. - For "no automation service", the two automation surfaces on the handle answer differently, by design. `flows.*` is a route door, and gets the route's envelope for an empty slot (the dispatcher's `501`, read from `packages/runtime/src/domains/automation.ts:2374`, not measured on a boot here). This door is an engine door, and gets the kernel's in-process error. That is the split the handle already documents between its route doors and its engine doors. ## For the `repo:hotcrm` seat ("done when") `conditionHolds(stack, condition, vars)` can move to `await stack.automation.evaluateCondition(condition, vars)`. Two porting differences apply: - the helper's string wrap is the start gate's and the decision executor's call-site behaviour, not the evaluator's. To keep modelling those sites, pass the envelope (`{ dialect: 'cel', source }`), as the helper did; - the door resolves a promise. The `filter` call site at `account-name-normalized-match.test.ts:600` needs `Promise.all`. --- _Generated by [Claude Code](https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 20e7d52 commit fbb065f

6 files changed

Lines changed: 295 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/verify': minor
3+
---
4+
5+
feat(verify): the handle gains a door to the automation engine's condition evaluator
6+
7+
Clause-②: yes (widening)
8+
9+
An app's tests can evaluate a flow condition on the booted stack's own evaluator through the in-process handle, so a suite no longer calls the booted kernel's `automation` service by hand for a condition's truth table.
10+
11+
- **`stack.automation.evaluateCondition(condition, variables)`** calls the booted `automation` service's `evaluateCondition`, the method a flow's start gate, its edges and its `decision` branches are decided by. It resolves with the engine's boolean, unchanged. It is for a truth table over variable shapes no write produces.
12+
- **`condition`** takes the shapes the engine takes, and is handed over as given: an envelope (`{ dialect: 'cel', source }`) or a string, which the engine reads as CEL unless it carries a `{var}` hole (the legacy template dialect). The engine's own flow sites hand the evaluator an envelope (a start node's string `condition` and a `decision` branch's `expression` are wrapped as `{ dialect: 'cel', source }`, and an edge's condition already is one), so pass the envelope to model any of them.
13+
- **`variables`** is a plain object. Each own key becomes one entry of the `Map` the engine takes: `{ record, previous }` binds `record` and `previous`.
14+
- **Refusals are not the handle's.** A condition the engine cannot evaluate rejects with the engine's own error, never `false`. On a stack with no automation service it rejects with the kernel's own `SERVICE_NOT_REGISTERED` error, whose `serviceName` is `'automation'` (`isServiceNotRegisteredError` from `@objectstack/core` answers `true`). Boot with `automation: true`, or have the app declare `requires: ['automation']`. No error code is added.

‎packages/qa/dogfood/test/rls-runner.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ function fakeStack(opts: FakeOpts): VerifyStack {
133133
validate: undefined as never,
134134
flows: undefined as never,
135135
actions: undefined as never,
136+
automation: undefined as never,
136137
seed: undefined as never,
137138
rows: undefined as never,
138139
metadata: undefined as never,

‎packages/verify/README.md‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ await stack.stop();
7777
Every `VerifyStack` carries it; nothing extra to boot. Each method is a thin
7878
facade over a door the kernel wired at boot — the ObjectQL engine's own write,
7979
dry-run and read calls, the runtime's `/automation` and `/actions` routes
80-
driven in-process, the `SchemaRegistry`, the `tenancy` service — with **zero
80+
driven in-process, the automation engine's condition evaluator, the
81+
`SchemaRegistry`, the `tenancy` service — with **zero
8182
re-implemented semantics**: the handle assembles no execution context, orders
8283
no hooks, evaluates no permission. What the engine does is what you assert on.
8384

@@ -120,6 +121,13 @@ await stack.flows.resume(run, { quoteName: 'Q-1', discount: 10 }, { as: rep });
120121
const out = await stack.actions.run('crm_opportunity', 'apply_discount',
121122
{ as: rep, recordId: deal.id, params: { discount: 10 } });
122123

124+
// A flow condition's truth table, on the engine's own evaluator, over
125+
// variable shapes no write produces. A start gate's, an edge's and a
126+
// decision branch's condition reach it as a CEL envelope, so pass one.
127+
expect(await stack.automation.evaluateCondition(
128+
{ dialect: 'cel', source: 'record.stage != previous.stage' },
129+
{ record: { stage: 'closed_won' }, previous: { stage: 'proposal' } })).toBe(true);
130+
123131
// Fixtures and reads through the real engine.
124132
const [acc] = await stack.seed('crm_account', [{ name: 'Globex' }]);
125133
const mine = await stack.rows('crm_opportunity', { crm_account: acc.id }, { as: rep });
@@ -140,8 +148,11 @@ await stack.stop();
140148
`hooks.updateWhere`) run as the system principal, deliberately and by name.
141149
- A refusal from `flows.*` / `actions.run` is the route's ADR-0112 envelope
142150
(`VerifyRefusal`: `code`, `status`, `details`; `isVerifyRefusal(e)`); a
143-
refusal from `hooks.run` / `hooks.updateWhere` / `validate` / `rows` is the
144-
engine's own error. The two update doors refuse a malformed call themselves,
151+
refusal from `hooks.run` / `hooks.updateWhere` / `validate` / `rows` /
152+
`automation.evaluateCondition` is the engine's own error (a condition the
153+
engine cannot evaluate rejects, never answers `false`; on a stack with no
154+
automation service, `automation.evaluateCondition` rejects with the kernel's
155+
own `SERVICE_NOT_REGISTERED` error naming `automation`). The two update doors refuse a malformed call themselves,
145156
before the engine is touched, with `INVALID_REQUEST` / `400` (`{ system: true }`
146157
on an insert or delete, a caller named twice, or a `hooks.updateWhere` the
147158
engine would write by id). Assert on `code` (and `status` / `statusCode`),
@@ -202,7 +213,8 @@ run" must never read like "nothing to find".
202213
## API
203214

204215
- `bootStack(config, opts?)` → `VerifyStack` (`api` / `raw` / `signIn` / `signUp` / `apiAs` / `stop`, plus the handle:
205-
`hooks.run` / `hooks.updateWhere` / `validate` / `flows.run` / `flows.resume` / `actions.run` / `seed` / `rows` / `metadata` / `tenancy` / `contextFor`).
216+
`hooks.run` / `hooks.updateWhere` / `validate` / `flows.run` / `flows.resume` / `actions.run` /
217+
`automation.evaluateCondition` / `seed` / `rows` / `metadata` / `tenancy` / `contextFor`).
206218
- `bootStackOnce(config, opts?)` → the same, memoised per `(config, opts)` identity for the process.
207219
- `deriveCrudCases(config)` → the auto-derived round-trip cases (write one, read one, assert) for every object.
208220
- `runCrudVerification(stack, token, config)` → `VerifyReport`; `formatReport(report)` for a log summary.
@@ -211,7 +223,9 @@ run" must never read like "nothing to find".
211223
`bootStack` options: `admin`, `authSecret`, `security` (a custom `SecurityPlugin`
212224
for owner-scoped fixtures), `multiTenant` (also what decides the posture
213225
`tenancy()` reports), `automation` (register the automation service so
214-
`flows.*` has something to drive), `orgContext`, `databaseFile`, `extraPlugins`.
226+
`flows.*` and `automation.evaluateCondition` have something to drive; an app
227+
that declares `requires: ['automation']` gets it without this), `orgContext`,
228+
`databaseFile`, `extraPlugins`.
215229

216230
## Known limitations
217231

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* #22301 item 8: the handle's door to the automation engine's predicate
5+
* evaluator, `stack.automation.evaluateCondition(condition, variables)`.
6+
*
7+
* On 17.7.0 the handle had no such door, so an app's suite that tested a
8+
* flow condition's truth table over variable shapes no write produces reached
9+
* `kernel.getService('automation').evaluateCondition(…)` by hand (hotcrm's
10+
* `conditionHolds`). The door is that same call, made by the handle: the
11+
* booted kernel's own `automation` service, the condition as given, the
12+
* variables as the `Map` the engine takes, and the engine's answer back.
13+
*
14+
* So every claim below is read off the engine itself: a spy on the booted
15+
* service instance (it was asked, with exactly these arguments, and its
16+
* answer is the door's), the engine called directly beside the door (same
17+
* verdict, same fault), and the engine's own dialect decision, which a door
18+
* that rewrote the condition would change. The no-automation boot answers
19+
* with the kernel's own rejection, compared against the kernel asked
20+
* directly.
21+
*/
22+
23+
import { describe, it, expect, beforeAll, afterAll, afterEach, vi } from 'vitest';
24+
25+
import { isServiceNotRegisteredError, SERVICE_NOT_REGISTERED_CODE } from '@objectstack/core';
26+
import { defineStack } from '@objectstack/spec';
27+
import { ObjectSchema, Field } from '@objectstack/spec/data';
28+
29+
import { bootStack, type VerifyStack } from './harness.js';
30+
import { isVerifyRefusal } from './handle.js';
31+
32+
// Booting the full in-process stack runs well past vitest's 5s default.
33+
const BOOT_TIMEOUT = 120_000;
34+
35+
/** The engine's evaluator, as the booted `automation` service exposes it. */
36+
interface Evaluator {
37+
evaluateCondition(condition: unknown, variables: Map<string, unknown>): boolean;
38+
}
39+
40+
/**
41+
* The fixture, built anew per boot: one neutral object, and the automation
42+
* capability declared (`requires`) or not. Two configurations, so the two
43+
* boots are two stacks rather than a refused second boot of one.
44+
*/
45+
function fixture(withAutomation: boolean) {
46+
return defineStack({
47+
manifest: {
48+
id: `com.objectstack.verify.automation-door${withAutomation ? '' : '-none'}`,
49+
namespace: 'acd',
50+
version: '0.0.0',
51+
type: 'app',
52+
name: 'Verify Automation Door Fixture',
53+
description: 'One neutral object; the automation capability declared or not.',
54+
},
55+
...(withAutomation ? { requires: ['automation'] } : {}),
56+
objects: [
57+
ObjectSchema.create({
58+
name: 'acd_deal',
59+
label: 'Deal',
60+
pluralLabel: 'Deals',
61+
fields: {
62+
name: Field.text({ label: 'Name', required: true }),
63+
stage: Field.text({ label: 'Stage' }),
64+
amount: Field.number({ label: 'Amount' }),
65+
},
66+
}),
67+
],
68+
} as never);
69+
}
70+
71+
/** A record-change predicate over `record` and `previous`, as a flow's start gate holds it. */
72+
const MOVED_AND_LARGE = 'record.amount >= 1000 && record.stage != previous.stage';
73+
const movedLarge = { record: { id: 'd1', amount: 5000, stage: 'won' }, previous: { id: 'd1', amount: 5000, stage: 'open' } };
74+
const unmoved = { record: { id: 'd1', amount: 5000, stage: 'won' }, previous: { id: 'd1', amount: 5000, stage: 'won' } };
75+
const small = { record: { id: 'd1', amount: 10, stage: 'won' }, previous: { id: 'd1', amount: 10, stage: 'open' } };
76+
77+
/** What the door, or the engine asked directly, answered: a verdict or a fault. */
78+
async function answer<T>(run: () => T | Promise<T>): Promise<{ value?: T; fault?: unknown }> {
79+
try {
80+
return { value: await run() };
81+
} catch (fault) {
82+
return { fault };
83+
}
84+
}
85+
86+
describe('stack.automation.evaluateCondition (#22301 item 8)', () => {
87+
let stack: VerifyStack;
88+
let bare: VerifyStack;
89+
let engine: Evaluator;
90+
91+
beforeAll(async () => {
92+
stack = await bootStack(fixture(true));
93+
bare = await bootStack(fixture(false));
94+
engine = await stack.kernel.getServiceAsync<Evaluator>('automation');
95+
}, BOOT_TIMEOUT * 2);
96+
97+
afterAll(async () => {
98+
await stack?.stop();
99+
await bare?.stop();
100+
});
101+
102+
afterEach(() => {
103+
vi.restoreAllMocks();
104+
});
105+
106+
/** The door and the engine, asked the same question; the engine's Map built here, independently. */
107+
async function both(condition: unknown, variables: Record<string, unknown>) {
108+
const door = await answer(() =>
109+
stack.automation.evaluateCondition(condition as Parameters<VerifyStack['automation']['evaluateCondition']>[0], variables),
110+
);
111+
const direct = await answer(() => engine.evaluateCondition(condition, new Map(Object.entries(variables))));
112+
return { door, direct };
113+
}
114+
115+
it('asks the booted automation service, with the condition as given and the variables as its Map, and answers its boolean', async () => {
116+
const spy = vi.spyOn(engine, 'evaluateCondition');
117+
const condition = { dialect: 'cel', source: MOVED_AND_LARGE };
118+
119+
const verdict = await stack.automation.evaluateCondition(condition, movedLarge);
120+
121+
expect(spy).toHaveBeenCalledTimes(1);
122+
const [given, variables] = spy.mock.calls[0];
123+
// The same envelope object, not a rebuilt one.
124+
expect(given).toBe(condition);
125+
expect(variables).toBeInstanceOf(Map);
126+
expect([...variables.entries()]).toEqual(Object.entries(movedLarge));
127+
expect(spy.mock.results[0]).toEqual({ type: 'return', value: true });
128+
expect(verdict).toBe(true);
129+
});
130+
131+
it('answers the truth table over record-shaped variables: true and false, as the engine does', async () => {
132+
const condition = { dialect: 'cel', source: MOVED_AND_LARGE };
133+
const rows: Array<[Record<string, unknown>, boolean]> = [
134+
[movedLarge, true],
135+
// A pre-image the engine would have read as unchanged, and an amount under the bar.
136+
[unmoved, false],
137+
[small, false],
138+
];
139+
for (const [variables, expected] of rows) {
140+
const { door, direct } = await both(condition, variables);
141+
expect(door).toEqual({ value: expected });
142+
expect(direct).toEqual({ value: expected });
143+
}
144+
});
145+
146+
it('takes an envelope and a bare CEL string alike', async () => {
147+
for (const [variables, expected] of [[movedLarge, true], [unmoved, false]] as const) {
148+
expect(await stack.automation.evaluateCondition({ dialect: 'cel', source: MOVED_AND_LARGE }, variables)).toBe(expected);
149+
expect(await stack.automation.evaluateCondition(MOVED_AND_LARGE, variables)).toBe(expected);
150+
}
151+
});
152+
153+
it('never rewrites the condition: a bare `{var}` string keeps the template dialect the engine gives it', async () => {
154+
const holed = '{amount} > 100';
155+
const spy = vi.spyOn(engine, 'evaluateCondition');
156+
157+
// The engine reads a bare string with a `{…}` hole as the template dialect.
158+
expect(await stack.automation.evaluateCondition(holed, { amount: 5000 })).toBe(true);
159+
expect(spy.mock.calls[0][0]).toBe(holed);
160+
161+
// The same text in a CEL envelope is the brace trap, and the engine refuses it.
162+
const { door, direct } = await both({ dialect: 'cel', source: holed }, { amount: 5000 });
163+
expect(direct.fault).toBeInstanceOf(Error);
164+
expect(door.fault).toBeInstanceOf(Error);
165+
expect((door.fault as Error).message).toBe((direct.fault as Error).message);
166+
});
167+
168+
it("rejects with the engine's own error when the condition faults, never `false`", async () => {
169+
// A CEL parse error, and a reference to a variable the set does not bind.
170+
const faulting: Array<[unknown, Record<string, unknown>]> = [
171+
[{ dialect: 'cel', source: 'record.amount >=' }, movedLarge],
172+
[{ dialect: 'cel', source: 'record.amount >= 1000' }, {}],
173+
];
174+
for (const [condition, variables] of faulting) {
175+
const { door, direct } = await both(condition, variables);
176+
expect(direct.fault).toBeInstanceOf(Error);
177+
expect(door.value).toBeUndefined();
178+
expect(door.fault).toBeInstanceOf(Error);
179+
// Handed back as the engine threw it: its message, and no refusal envelope added.
180+
expect((door.fault as Error).message).toBe((direct.fault as Error).message);
181+
expect(isVerifyRefusal(door.fault)).toBe(false);
182+
expect((door.fault as { code?: unknown }).code).toBeUndefined();
183+
}
184+
});
185+
186+
it("rejects with the kernel's own SERVICE_NOT_REGISTERED error, naming 'automation', on a stack without the service", async () => {
187+
const kernelSaid = await answer(() => bare.kernel.getServiceAsync<Evaluator>('automation'));
188+
expect(isServiceNotRegisteredError(kernelSaid.fault)).toBe(true);
189+
190+
const door = await answer(() => bare.automation.evaluateCondition({ dialect: 'cel', source: 'true' }, {}));
191+
expect(door.value).toBeUndefined();
192+
expect(isServiceNotRegisteredError(door.fault)).toBe(true);
193+
const fault = door.fault as Error & { code?: unknown; serviceName?: unknown };
194+
expect(fault.code).toBe(SERVICE_NOT_REGISTERED_CODE);
195+
expect(fault.serviceName).toBe('automation');
196+
expect(fault.message).toBe((kernelSaid.fault as Error).message);
197+
});
198+
});

0 commit comments

Comments
 (0)