@owlmeans/server-planning 0.1.18-rc.13 → 0.1.18-rc.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -10,13 +10,16 @@ the `@owlmeans/server-planning/store` subpath.
|
|
|
10
10
|
## Installation
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
|
-
bun add @owlmeans/server-planning@^0.1.18-rc.
|
|
13
|
+
bun add @owlmeans/server-planning@^0.1.18-rc.14 @owlmeans/planning@^0.1.18-rc.12 ajv
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
## Concepts
|
|
17
17
|
|
|
18
|
-
- **Facade** — `
|
|
19
|
-
with no scope argument on any method.
|
|
18
|
+
- **Facade** — `ctx.service<PlanningHostService>(PLANNING_SERVICE).for(scope)`: every read and
|
|
19
|
+
write of one organization entity, with no scope argument on any method. `context.planning()` is
|
|
20
|
+
the same service, but only on a context that went through `appendPlanningService`; a service
|
|
21
|
+
registered as a plain one (`makePlanningService()`, a generated target's `services/planning.ts`)
|
|
22
|
+
has no `context.planning`.
|
|
20
23
|
- **Executor** — the only write path: normalize → idempotency → resolve → plugins' `before` →
|
|
21
24
|
validate → id → code → changes → seq CAS → append → project → receipt. Nothing is appended by a
|
|
22
25
|
refusal.
|
|
@@ -125,6 +128,13 @@ const cards = await planningFor(ctx, req, { channel: 'web' }).cards.list({ paren
|
|
|
125
128
|
|
|
126
129
|
## Common pitfalls
|
|
127
130
|
|
|
131
|
+
- A create's draft goes under `card` — `draft` is refused as `planning:malformed:create-without-draft`.
|
|
132
|
+
- `{ wait: true }` is the SECOND argument of `execute`; inside the execution it is ignored and the
|
|
133
|
+
receipt returns before the commit.
|
|
134
|
+
- A type or flow declaration has no `name` or `key`: the display name is `label`, the key is `type`
|
|
135
|
+
(a type) or `id` (a flow). `definitions.define` refuses anything else with `SchemaInvalid`; a code
|
|
136
|
+
plugin's registry does not check, so keep the literals typed.
|
|
137
|
+
|
|
128
138
|
- `after` hooks run where the transition is FOLDED, once per commit — register the same plugins in
|
|
129
139
|
every folding process, and never run hooks from a commit-bus subscription.
|
|
130
140
|
- The memory store is per-process heap: wrong for multiple processes or restarts.
|
|
@@ -153,7 +163,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
153
163
|
your project's skill store (`.agents/skills/`):
|
|
154
164
|
|
|
155
165
|
```sh
|
|
156
|
-
npx @owlmeans/agent-skills@^0.1.18-rc.
|
|
166
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.42
|
|
157
167
|
```
|
|
158
168
|
|
|
159
169
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/server-planning",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-09-
|
|
4
|
+
"version": "0.1.18-rc.14",
|
|
5
|
+
"generatedAt": "2026-09-29T08:01:58.111Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -8,7 +8,7 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/server-planning
|
|
9
9
|
|
|
10
10
|
**Layer:** Server
|
|
11
|
-
**Install:** `"@owlmeans/server-planning": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/server-planning": "^0.1.18-rc.14"` in `dependencies` (`ajv` is a peer)
|
|
12
12
|
|
|
13
13
|
The general implementation of `@owlmeans/planning`: the planning service (a plugin host), the
|
|
14
14
|
scoped facade, the executor every write goes through, the in-memory reference store, the commit
|
|
@@ -61,8 +61,68 @@ context.registerEntrypoints(servePlanningEntrypoints(planningProtocols, {
|
|
|
61
61
|
`ensurePlanningService(ctx).use(plugin)` works from `makeContext`, before `init()`.
|
|
62
62
|
- Call `appendPlanningService` BEFORE any `ensurePlanningService`: `ensure` registers a default
|
|
63
63
|
host when none exists, and a later `append` replaces it together with whatever was `use`d on it.
|
|
64
|
-
-
|
|
65
|
-
with no scope argument on any method.
|
|
64
|
+
- **The facade:** `ctx.service<PlanningHostService>(PLANNING_SERVICE).for({ entityId, profileId?,
|
|
65
|
+
channel?, actor? })` → a `PlanningFacade` with no scope argument on any method. That form works
|
|
66
|
+
however the service was registered. `ctx.planning()` is only a shortcut that `appendPlanningService`
|
|
67
|
+
/ `appendPostgresPlanning` install (typed `WithPlanningService`); a context that registered
|
|
68
|
+
`makePlanningService()` / `makePostgresPlanningService()` as a plain service — a generated target's
|
|
69
|
+
`services/planning.ts` — has no `ctx.planning` at all. In a request handler use
|
|
70
|
+
`planningFor(ctx, req)`: the entity comes from `requireEntityKey(req)`, never the token.
|
|
71
|
+
|
|
72
|
+
## Creating and reading a card
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { IntrinsicStatus, PLANNING_SERVICE, TransitionAction, WorkcardKind } from '@owlmeans/planning'
|
|
76
|
+
import type { PlanningHostService } from '@owlmeans/server-planning'
|
|
77
|
+
|
|
78
|
+
const planning = ctx.service<PlanningHostService>(PLANNING_SERVICE).for({ entityId, profileId })
|
|
79
|
+
|
|
80
|
+
const shed = await planning.execute({
|
|
81
|
+
action: TransitionAction.Create,
|
|
82
|
+
card: { kind: WorkcardKind.Project, type: 'shed:shed', title: 'Maple Street shed' },
|
|
83
|
+
}, { wait: true })
|
|
84
|
+
const drill = await planning.execute({
|
|
85
|
+
action: TransitionAction.Create,
|
|
86
|
+
card: { kind: WorkcardKind.Card, type: 'shed:tool', parent: shed.card!.id!, title: 'Cordless drill',
|
|
87
|
+
fields: { brand: 'Acme' } },
|
|
88
|
+
}, { wait: true }) // drill.card — the committed record, status 'available'
|
|
89
|
+
|
|
90
|
+
await planning.cards.get(drill.card!.id!)
|
|
91
|
+
await planning.cards.list({ parent: shed.card!.id!, type: 'shed:tool' })
|
|
92
|
+
await planning.execute({ action: TransitionAction.Transit, card: drill.card!.id!, transition: 'lend' }, { wait: true })
|
|
93
|
+
|
|
94
|
+
// A card type and a flow as DATA, scoped to this shed — `definitions` exists only where the store
|
|
95
|
+
// keeps schemas (planning-postgres; the memory store with `schemas: true`), and the shed's cards may
|
|
96
|
+
// use them when its project type says `scopedCardTypes: true` (or lists the type in `cardTypes`).
|
|
97
|
+
await planning.definitions!.define({
|
|
98
|
+
flows: [{
|
|
99
|
+
id: 'shed:repair', version: 1, label: 'Repair',
|
|
100
|
+
statuses: [
|
|
101
|
+
{ key: 'reported', intrinsic: IntrinsicStatus.Planned, initial: true, label: 'Reported' },
|
|
102
|
+
{ key: 'fixed', intrinsic: IntrinsicStatus.Closed, terminal: true, label: 'Fixed' },
|
|
103
|
+
],
|
|
104
|
+
transitions: [{ name: 'fix', from: ['reported'], to: 'fixed', label: 'Mark fixed', explicit: true }],
|
|
105
|
+
}],
|
|
106
|
+
types: [{
|
|
107
|
+
type: 'shed:repair', kind: WorkcardKind.Card, version: 1, label: 'Repair ticket',
|
|
108
|
+
fields: { type: 'object', properties: { tool: { type: 'string' } }, additionalProperties: false },
|
|
109
|
+
flows: ['shed:repair'], specifications: [],
|
|
110
|
+
}],
|
|
111
|
+
}, { project: shed.card!.id! })
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
| Wrong | What happens | Right |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| `execute({ action, draft: { … } })` | refused: `planning:malformed:create-without-draft` | the draft goes under `card` |
|
|
117
|
+
| `execute({ …, wait: true })` | in process the key is ignored: the receipt returns before the commit, with no `card` | `execute({ … }, { wait: true })` — only the wire body (`ExecuteRequest`) carries `wait` inside |
|
|
118
|
+
| `ctx.planning().for(…)` in a target | `ctx.planning is not a function` | `ctx.service<PlanningHostService>(PLANNING_SERVICE).for(…)` |
|
|
119
|
+
| `{ id, name: 'Repair', … }` on a flow, `{ key, name, … }` on a type | `define` refuses with `SchemaInvalid` (closed schemas); a code plugin's registry does not validate, so the key is silently dropped | a flow's key is `id`, a type's is `type`, the display name is `label` on both |
|
|
120
|
+
| `action: 'create'` | a type error — `TransitionAction` is an enum | `action: TransitionAction.Create` |
|
|
121
|
+
|
|
122
|
+
Every wrong form is a compile error against the typed facade (`PlanningFacade`,
|
|
123
|
+
`TransitionExecution`, `WorkcardTypeSchema`); it reaches run time only through a helper or a context
|
|
124
|
+
typed `any`, so never type one that way. `tests/create-example.spec.ts` runs this example and each
|
|
125
|
+
wrong form as written.
|
|
66
126
|
|
|
67
127
|
## The executor, step by step
|
|
68
128
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/server-planning",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.14",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "The general planning implementation — the planning service and its plugin/middleware registry, the transition executor, the in-memory store, the protocol handlers and the commit feed.",
|
|
6
6
|
"type": "module",
|
|
@@ -45,19 +45,19 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@owlmeans/auth": "^0.1.18-rc.36",
|
|
48
|
-
"@owlmeans/auth-common": "^0.1.18-rc.
|
|
48
|
+
"@owlmeans/auth-common": "^0.1.18-rc.41",
|
|
49
49
|
"@owlmeans/basic-ids": "^0.1.18-rc.34",
|
|
50
50
|
"@owlmeans/context": "^0.1.18-rc.32",
|
|
51
51
|
"@owlmeans/entrypoint": "^0.1.18-rc.37",
|
|
52
52
|
"@owlmeans/error": "^0.1.18-rc.36",
|
|
53
|
-
"@owlmeans/planning": "^0.1.18-rc.
|
|
54
|
-
"@owlmeans/queue": "^0.1.18-rc.
|
|
55
|
-
"@owlmeans/resource": "^0.1.18-rc.
|
|
53
|
+
"@owlmeans/planning": "^0.1.18-rc.12",
|
|
54
|
+
"@owlmeans/queue": "^0.1.18-rc.35",
|
|
55
|
+
"@owlmeans/resource": "^0.1.18-rc.37",
|
|
56
56
|
"@owlmeans/route": "^0.1.18-rc.33",
|
|
57
|
-
"@owlmeans/server-api": "^0.1.18-rc.
|
|
58
|
-
"@owlmeans/server-context": "^0.1.18-rc.
|
|
57
|
+
"@owlmeans/server-api": "^0.1.18-rc.45",
|
|
58
|
+
"@owlmeans/server-context": "^0.1.18-rc.38",
|
|
59
59
|
"@owlmeans/server-entrypoint": "^0.1.18-rc.36",
|
|
60
|
-
"@owlmeans/server-socket": "^0.1.18-rc.
|
|
60
|
+
"@owlmeans/server-socket": "^0.1.18-rc.48",
|
|
61
61
|
"@owlmeans/socket": "^0.1.18-rc.35"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@owlmeans/dep-config": "workspace:*",
|
|
69
|
-
"@owlmeans/test": "^0.1.18-rc.
|
|
69
|
+
"@owlmeans/test": "^0.1.18-rc.33",
|
|
70
70
|
"@types/bun": "^1.4.0",
|
|
71
71
|
"@types/node": "^26.1.0",
|
|
72
72
|
"ajv": "^8.17.1",
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { AppType, makeBasicContext } from '@owlmeans/context'
|
|
3
|
+
import type { BasicConfig } from '@owlmeans/context'
|
|
4
|
+
import {
|
|
5
|
+
CommitState, IntrinsicStatus, PLANNING_SERVICE, SchemaInvalid, TransitionAction, WorkcardKind,
|
|
6
|
+
} from '@owlmeans/planning'
|
|
7
|
+
import type {
|
|
8
|
+
PlanningPlugin, StatusFlowSchema, TransitionExecution, WithPlanningService, WorkcardTypeSchema,
|
|
9
|
+
} from '@owlmeans/planning'
|
|
10
|
+
import { appendPlanningService, makePlanningService } from '../src/service.js'
|
|
11
|
+
import { makeMemoryPlanningStore } from '../src/store/memory.js'
|
|
12
|
+
import type { MemoryPlanningStoreOptions } from '../src/store/types.js'
|
|
13
|
+
import type { PlanningHostService } from '../src/types.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The create example the `planning`, `server-planning` and `planning-postgres` skills show, run as
|
|
17
|
+
* written — and the wrong forms beside it, each answered the way the skills say.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const SHED: PlanningPlugin = {
|
|
21
|
+
name: 'shed-planning',
|
|
22
|
+
schemas: {
|
|
23
|
+
flows: [
|
|
24
|
+
{
|
|
25
|
+
id: 'shed:site', version: 1,
|
|
26
|
+
statuses: [
|
|
27
|
+
{ key: 'open', intrinsic: IntrinsicStatus.Planned, initial: true },
|
|
28
|
+
{ key: 'closed', intrinsic: IntrinsicStatus.Closed, terminal: true },
|
|
29
|
+
],
|
|
30
|
+
transitions: [{ name: 'close', from: ['open'], to: 'closed' }],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'shed:loan', version: 1,
|
|
34
|
+
statuses: [
|
|
35
|
+
{ key: 'available', intrinsic: IntrinsicStatus.Planned, initial: true, label: 'Available' },
|
|
36
|
+
{ key: 'lent', intrinsic: IntrinsicStatus.InProgress, label: 'Lent out' },
|
|
37
|
+
{ key: 'retired', intrinsic: IntrinsicStatus.Closed, terminal: true },
|
|
38
|
+
],
|
|
39
|
+
transitions: [
|
|
40
|
+
{ name: 'lend', from: ['available'], to: 'lent', label: 'Lend', explicit: true },
|
|
41
|
+
{ name: 'return', from: ['lent'], to: 'available', label: 'Return', explicit: true },
|
|
42
|
+
{ name: 'retire', from: '*', to: 'retired' },
|
|
43
|
+
],
|
|
44
|
+
label: 'Loan',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
types: [
|
|
48
|
+
{
|
|
49
|
+
type: 'shed:shed', kind: WorkcardKind.Project, version: 1, label: 'Tool shed',
|
|
50
|
+
fields: { type: 'object' }, flows: ['shed:site'], specifications: [],
|
|
51
|
+
cardTypes: ['shed:tool'], scopedCardTypes: true,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: 'shed:tool', kind: WorkcardKind.Card, version: 1, label: 'Tool',
|
|
55
|
+
fields: { type: 'object', properties: { brand: { type: 'string' } }, additionalProperties: false },
|
|
56
|
+
flows: ['shed:loan'], specifications: [],
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const cfg = (): BasicConfig => ({ ready: false, service: 'shed-tests', type: AppType.Backend, services: {} })
|
|
63
|
+
|
|
64
|
+
/** A target's shape: the service registered as a plain service, no `ctx.planning()`. */
|
|
65
|
+
const bootTarget = async (store: MemoryPlanningStoreOptions = {}) => {
|
|
66
|
+
const memory = makeMemoryPlanningStore({ schemas: true, ...store })
|
|
67
|
+
const context = makeBasicContext(cfg())
|
|
68
|
+
context.registerService(makePlanningService({ store: memory, plugins: [SHED] }))
|
|
69
|
+
context.configure()
|
|
70
|
+
await context.init()
|
|
71
|
+
|
|
72
|
+
return { context, memory }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const scope = { entityId: 'org-1', profileId: 'profile-1' }
|
|
76
|
+
|
|
77
|
+
describe('creating a card — the documented form', () => {
|
|
78
|
+
test('scoped facade → create a project and a card under it with { wait: true } → read back', async () => {
|
|
79
|
+
const { context } = await bootTarget()
|
|
80
|
+
const planning = context.service<PlanningHostService>(PLANNING_SERVICE).for(scope)
|
|
81
|
+
|
|
82
|
+
const shed = await planning.execute({
|
|
83
|
+
action: TransitionAction.Create,
|
|
84
|
+
card: { kind: WorkcardKind.Project, type: 'shed:shed', title: 'Maple Street shed' },
|
|
85
|
+
}, { wait: true })
|
|
86
|
+
const shedId = shed.card!.id!
|
|
87
|
+
|
|
88
|
+
const drill = await planning.execute({
|
|
89
|
+
action: TransitionAction.Create,
|
|
90
|
+
card: {
|
|
91
|
+
kind: WorkcardKind.Card, type: 'shed:tool', parent: shedId, title: 'Cordless drill',
|
|
92
|
+
fields: { brand: 'Acme' },
|
|
93
|
+
},
|
|
94
|
+
}, { wait: true })
|
|
95
|
+
|
|
96
|
+
expect(drill.transition.commit.state).toBe(CommitState.Committed)
|
|
97
|
+
expect(drill.card).toMatchObject({ title: 'Cordless drill', status: 'available', parent: shedId })
|
|
98
|
+
|
|
99
|
+
const again = await planning.cards.get(drill.card!.id!)
|
|
100
|
+
expect(again.fields).toEqual({ brand: 'Acme' })
|
|
101
|
+
const tools = await planning.cards.list({ parent: shedId, type: 'shed:tool' })
|
|
102
|
+
expect(tools.items.map(tool => tool.title)).toEqual(['Cordless drill'])
|
|
103
|
+
|
|
104
|
+
const lent = await planning.execute({
|
|
105
|
+
action: TransitionAction.Transit, card: drill.card!.id!, transition: 'lend',
|
|
106
|
+
}, { wait: true })
|
|
107
|
+
expect(lent.card?.status).toBe('lent')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('a data-defined type and flow with the exact keys, then a card of that type', async () => {
|
|
111
|
+
const { context } = await bootTarget()
|
|
112
|
+
const planning = context.service<PlanningHostService>(PLANNING_SERVICE).for(scope)
|
|
113
|
+
const shedId = (await planning.execute({
|
|
114
|
+
action: TransitionAction.Create,
|
|
115
|
+
card: { kind: WorkcardKind.Project, type: 'shed:shed', title: 'Maple Street shed' },
|
|
116
|
+
}, { wait: true })).card!.id!
|
|
117
|
+
|
|
118
|
+
const records = await planning.definitions!.define({
|
|
119
|
+
flows: [{
|
|
120
|
+
id: 'shed:repair', version: 1, label: 'Repair',
|
|
121
|
+
statuses: [
|
|
122
|
+
{ key: 'reported', intrinsic: IntrinsicStatus.Planned, initial: true, label: 'Reported' },
|
|
123
|
+
{ key: 'fixed', intrinsic: IntrinsicStatus.Closed, terminal: true, label: 'Fixed' },
|
|
124
|
+
],
|
|
125
|
+
transitions: [{ name: 'fix', from: ['reported'], to: 'fixed', label: 'Mark fixed', explicit: true }],
|
|
126
|
+
}],
|
|
127
|
+
types: [{
|
|
128
|
+
type: 'shed:repair', kind: WorkcardKind.Card, version: 1, label: 'Repair ticket',
|
|
129
|
+
fields: { type: 'object', properties: { tool: { type: 'string' } }, additionalProperties: false },
|
|
130
|
+
flows: ['shed:repair'], specifications: [],
|
|
131
|
+
}],
|
|
132
|
+
}, { project: shedId })
|
|
133
|
+
expect(records.map(record => `${record.kind}:${record.key}:${record.version}`))
|
|
134
|
+
.toEqual(['flow:shed:repair:1', 'type:shed:repair:1'])
|
|
135
|
+
|
|
136
|
+
const ticket = await planning.execute({
|
|
137
|
+
action: TransitionAction.Create,
|
|
138
|
+
card: { kind: WorkcardKind.Card, type: 'shed:repair', parent: shedId, title: 'Drill chuck slips' },
|
|
139
|
+
}, { wait: true })
|
|
140
|
+
expect(ticket.card?.status).toBe('reported')
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('creating a card — the wrong forms', () => {
|
|
145
|
+
test('`draft:` in place of `card:` is refused before anything is written', async () => {
|
|
146
|
+
const { context } = await bootTarget()
|
|
147
|
+
const planning = context.service<PlanningHostService>(PLANNING_SERVICE).for(scope)
|
|
148
|
+
const wrong = { action: TransitionAction.Create, draft: { kind: WorkcardKind.Project, type: 'shed:shed', title: 'x' } }
|
|
149
|
+
|
|
150
|
+
await expect(planning.execute(wrong as unknown as TransitionExecution, { wait: true }))
|
|
151
|
+
.rejects.toThrow('planning:malformed:create-without-draft')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('`wait: true` inside the execution is ignored in process — the receipt is back before the commit', async () => {
|
|
155
|
+
const { context, memory } = await bootTarget({ sync: false })
|
|
156
|
+
const planning = context.service<PlanningHostService>(PLANNING_SERVICE).for(scope)
|
|
157
|
+
const create: TransitionExecution = {
|
|
158
|
+
action: TransitionAction.Create,
|
|
159
|
+
card: { kind: WorkcardKind.Project, type: 'shed:shed', title: 'Maple Street shed' },
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const early = await planning.execute({ ...create, wait: true } as TransitionExecution)
|
|
163
|
+
expect(early.transition.commit.state).toBe(CommitState.Pending)
|
|
164
|
+
expect(early.card).toBeUndefined()
|
|
165
|
+
|
|
166
|
+
// In the second argument it holds until the commit lands — here, until the store folds.
|
|
167
|
+
const folding = setInterval(() => void memory.flush(), 5)
|
|
168
|
+
try {
|
|
169
|
+
const held = await planning.execute({
|
|
170
|
+
...create, card: { ...(create.card as object), title: 'Oak Lane shed' },
|
|
171
|
+
} as TransitionExecution, { wait: true })
|
|
172
|
+
expect(held.transition.commit.state).toBe(CommitState.Committed)
|
|
173
|
+
expect(held.card?.title).toBe('Oak Lane shed')
|
|
174
|
+
} finally {
|
|
175
|
+
clearInterval(folding)
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('`ctx.planning()` exists only where appendPlanningService ran; the service alias always works', async () => {
|
|
180
|
+
const { context } = await bootTarget()
|
|
181
|
+
expect((context as Partial<WithPlanningService>).planning).toBeUndefined()
|
|
182
|
+
expect(context.service<PlanningHostService>(PLANNING_SERVICE).for(scope).scope.entityId).toBe('org-1')
|
|
183
|
+
|
|
184
|
+
const appended = appendPlanningService(makeBasicContext(cfg()), { plugins: [SHED] })
|
|
185
|
+
appended.configure()
|
|
186
|
+
await appended.init()
|
|
187
|
+
expect(appended.planning()).toBe(appended.service<PlanningHostService>(PLANNING_SERVICE))
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('a declaration with `name` / `key` instead of `label` / `type` / `id` is SchemaInvalid', async () => {
|
|
191
|
+
const { context } = await bootTarget()
|
|
192
|
+
const planning = context.service<PlanningHostService>(PLANNING_SERVICE).for(scope)
|
|
193
|
+
const flow = SHED.schemas!.flows![1]
|
|
194
|
+
const type = SHED.schemas!.types![1] as WorkcardTypeSchema
|
|
195
|
+
|
|
196
|
+
const named = { ...flow, id: 'shed:named', name: 'Loan' } as unknown as StatusFlowSchema
|
|
197
|
+
await expect(planning.definitions!.define({ flows: [named] })).rejects.toBeInstanceOf(SchemaInvalid)
|
|
198
|
+
|
|
199
|
+
const keyed = { ...type, type: 'shed:keyed', key: 'shed:keyed', name: 'Tool' } as unknown as WorkcardTypeSchema
|
|
200
|
+
await expect(planning.definitions!.define({ types: [keyed] })).rejects.toBeInstanceOf(SchemaInvalid)
|
|
201
|
+
})
|
|
202
|
+
})
|