@owlmeans/client-planning 0.1.18-rc.37 → 0.1.18-rc.38
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 +1 -1
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/client-planning/SKILL.md +7 -3
- package/agent-meta/skills/client-planning/parts/validation.md +4 -2
- package/build/client.d.ts +1 -1
- package/build/client.d.ts.map +1 -1
- package/build/client.js +17 -7
- package/build/client.js.map +1 -1
- package/package.json +3 -3
- package/src/client.ts +18 -9
- package/tests/app.spec.ts +72 -0
package/README.md
CHANGED
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 3,
|
|
3
3
|
"package": "@owlmeans/client-planning",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-10-
|
|
4
|
+
"version": "0.1.18-rc.38",
|
|
5
|
+
"generatedAt": "2026-10-24T21:07:16.901Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -8,7 +8,7 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/client-planning
|
|
9
9
|
|
|
10
10
|
**Layer:** Client
|
|
11
|
-
**Install:** `"@owlmeans/client-planning": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/client-planning": "^0.1.18-rc.38"` in `dependencies`
|
|
12
12
|
|
|
13
13
|
The client half of OwlMeans planning. It answers the `PlanningFacade` interface of
|
|
14
14
|
`@owlmeans/planning` over the protocol tree a server mounted with `@owlmeans/server-planning`, keeps
|
|
@@ -65,14 +65,18 @@ appendPlanningStores(context) // a browser mirror; a Node client usually
|
|
|
65
65
|
from the credential. The execution's own `actor` is never sent.
|
|
66
66
|
- A client runs no middleware and folds nothing: `store()`, `committed()` and a plugin carrying
|
|
67
67
|
`before`/`after`/`store`/`mintCode` answer `PlanningUnsupported`. `use()` accepts a schemas-only
|
|
68
|
-
plugin, layered over the server's bundle.
|
|
68
|
+
plugin, layered over the server's bundle; its `schemas.app` merges over the served declarations
|
|
69
|
+
(`bundle.app`) in plugin order, and a malformed one is refused (`SchemaInvalid`) before anything
|
|
70
|
+
changes.
|
|
69
71
|
- `timeout` (ms) bounds every call that is not a long poll; a long poll's deadline is its own hold
|
|
70
72
|
plus `LONG_POLL_GRACE`.
|
|
71
73
|
|
|
72
74
|
### The schema bundle
|
|
73
75
|
|
|
74
76
|
`model()` needs the flows and types (`can()`, `available()`, `statusOf()` read the registry with no
|
|
75
|
-
round trip). The bundle
|
|
77
|
+
round trip). The bundle also carries the application's declarations (`registry.app()`), so
|
|
78
|
+
`registry.fieldsSchema(type)` / `validator(type)` include its base card fields and a definition's
|
|
79
|
+
`app` bag reads back as written. The bundle is fetched in the background once the context is **Ready** and a failure
|
|
76
80
|
there is swallowed — a browser that is not signed in yet cannot read it, and a context must never
|
|
77
81
|
fail or wait over it. `model()` and `service.loadSchemas()` load it on first use regardless, and a
|
|
78
82
|
failed load is retried by the next caller. `schemas: false` skips the background load.
|
|
@@ -11,8 +11,10 @@ metadata:
|
|
|
11
11
|
|
|
12
12
|
## Browser field validation under CSP
|
|
13
13
|
|
|
14
|
-
Bind `makePlanningFieldValidator(type
|
|
15
|
-
organization or project bundle
|
|
14
|
+
Bind `makePlanningFieldValidator(registry.fieldsSchema(type))` to the registry of the resolved
|
|
15
|
+
organization or project bundle — the type's own `fields` plus the application's base card fields
|
|
16
|
+
(`planning` → types part); with no base fields it IS `type.fields`. Passing `type.fields` alone
|
|
17
|
+
skips the base fields the server still enforces. Its `validateFields(fields)` returns `{ valid, errors }`; errors
|
|
16
18
|
carry decoded `path`, `keyword`, `schemaPath` and a bounded value-free `message`. Construct one JSON
|
|
17
19
|
fields object, validate it and send that same object. No coercion, defaults or property removal
|
|
18
20
|
occurs, and the schema is left unchanged. Preserve nested money objects and calendar date strings.
|
package/build/client.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { Config, Context, PlanningClientOptions, PlanningClientService, Wit
|
|
|
6
6
|
* What only a server can do is refused rather than faked: a client runs no middleware and folds
|
|
7
7
|
* nothing, so `store()`, `committed()` and a plugin carrying hooks or a store answer
|
|
8
8
|
* `PlanningUnsupported`. A plugin carrying only schemas is accepted — it layers over the server's
|
|
9
|
-
* bundle.
|
|
9
|
+
* bundle (its `schemas.app` merged over the served declarations, in plugin order).
|
|
10
10
|
*/
|
|
11
11
|
export declare const makePlanningClientService: <C extends Config, T extends Context<C>>(context: T, options: PlanningClientOptions) => PlanningClientService;
|
|
12
12
|
/**
|
package/build/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,MAAM,EAAE,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,kBAAkB,EAClF,MAAM,YAAY,CAAA;AAEnB;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,WACrE,CAAC,WAAW,qBAAqB,KACzC,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,MAAM,EAAE,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,kBAAkB,EAClF,MAAM,YAAY,CAAA;AAEnB;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,WACrE,CAAC,WAAW,qBAAqB,KACzC,qBA6HF,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,WAChE,CAAC,WAAW,qBAAqB,KACzC,CAAC,GAAG,kBA0BN,CAAA"}
|
package/build/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { bindAll } from '@owlmeans/client-entrypoint';
|
|
2
2
|
import { createLazyService, MiddlewareStage, MiddlewareType } from '@owlmeans/context';
|
|
3
|
-
import { DEFAULT_PLUGIN_ORDER, makeSchemaRegistry, PLANNING_SERVICE, PlanningUnsupported, } from '@owlmeans/planning';
|
|
3
|
+
import { appSchemaHelper, DEFAULT_PLUGIN_ORDER, makeSchemaRegistry, PLANNING_SERVICE, PlanningUnsupported, } from '@owlmeans/planning';
|
|
4
4
|
import { makeRemoteCommitSource } from './commits.js';
|
|
5
5
|
import { makeRemoteDefinitions } from './definitions.js';
|
|
6
6
|
import { makeRemoteFacade } from './facade.js';
|
|
@@ -13,7 +13,7 @@ import { makePlanningClientLifecycle } from './lifecycle.js';
|
|
|
13
13
|
* What only a server can do is refused rather than faked: a client runs no middleware and folds
|
|
14
14
|
* nothing, so `store()`, `committed()` and a plugin carrying hooks or a store answer
|
|
15
15
|
* `PlanningUnsupported`. A plugin carrying only schemas is accepted — it layers over the server's
|
|
16
|
-
* bundle.
|
|
16
|
+
* bundle (its `schemas.app` merged over the served declarations, in plugin order).
|
|
17
17
|
*/
|
|
18
18
|
export const makePlanningClientService = (context, options) => {
|
|
19
19
|
const feeds = new Set();
|
|
@@ -33,11 +33,16 @@ export const makePlanningClientService = (context, options) => {
|
|
|
33
33
|
const commits = makeRemoteCommitSource(context, options.protocols, {
|
|
34
34
|
socket: options.socket, poll: options.poll, timeout: options.timeout, stores, lifecycle,
|
|
35
35
|
});
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
plugin.schemas?.
|
|
40
|
-
|
|
36
|
+
// The application's declarations the server served — what every client plugin's `app` merges over.
|
|
37
|
+
let served;
|
|
38
|
+
const layerPlugins = () => {
|
|
39
|
+
registry.registerApp(appSchemaHelper.merge([served, ...plugins.map(plugin => plugin.schemas?.app)]));
|
|
40
|
+
plugins.forEach(plugin => {
|
|
41
|
+
plugin.schemas?.assigneeTypes?.forEach(registry.registerAssigneeType);
|
|
42
|
+
plugin.schemas?.flows?.forEach(registry.registerFlow);
|
|
43
|
+
plugin.schemas?.types?.forEach(registry.registerType);
|
|
44
|
+
});
|
|
45
|
+
};
|
|
41
46
|
let loadedKey = scopeKey();
|
|
42
47
|
let loaded = false;
|
|
43
48
|
let loading = null;
|
|
@@ -57,6 +62,7 @@ export const makePlanningClientService = (context, options) => {
|
|
|
57
62
|
const bundle = await operation.wait(context.entrypoint(options.protocols.schema.list).call({ timeout: options.timeout, signal: operation.signal }));
|
|
58
63
|
operation.check();
|
|
59
64
|
registry.load(bundle);
|
|
65
|
+
served = registry.app();
|
|
60
66
|
layerPlugins();
|
|
61
67
|
loaded = true;
|
|
62
68
|
return registry;
|
|
@@ -87,6 +93,7 @@ export const makePlanningClientService = (context, options) => {
|
|
|
87
93
|
loading = null;
|
|
88
94
|
definitions?.invalidate();
|
|
89
95
|
registry.load({ version: 1, types: [], flows: [] });
|
|
96
|
+
served = undefined;
|
|
90
97
|
layerPlugins();
|
|
91
98
|
await Promise.all(stopping.map(async (feed) => await feed.stop()));
|
|
92
99
|
});
|
|
@@ -102,6 +109,9 @@ export const makePlanningClientService = (context, options) => {
|
|
|
102
109
|
if (plugin.before != null || plugin.after != null || plugin.store != null || plugin.mintCode != null) {
|
|
103
110
|
throw new PlanningUnsupported(`client:plugin:${plugin.name}`);
|
|
104
111
|
}
|
|
112
|
+
if (plugin.schemas?.app != null) {
|
|
113
|
+
appSchemaHelper.assertDeclarations(plugin.schemas.app);
|
|
114
|
+
}
|
|
105
115
|
const existing = plugins.findIndex(candidate => candidate.name === plugin.name);
|
|
106
116
|
if (existing >= 0) {
|
|
107
117
|
plugins.splice(existing, 1);
|
package/build/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAA;AACrD,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAEtF,OAAO,EACL,oBAAoB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,mBAAmB,
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/client-planning",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.38",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "The client half of OwlMeans planning — the remote planning facade, the state mirror and its commit fold, and the subscribe-then-poll commit wait. React-free: it runs in a browser and in a Node client alike.",
|
|
6
6
|
"type": "module",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"@owlmeans/context": "^0.1.18-rc.35",
|
|
31
31
|
"@owlmeans/entrypoint": "^0.1.18-rc.42",
|
|
32
32
|
"@owlmeans/error": "^0.1.18-rc.39",
|
|
33
|
-
"@owlmeans/planning": "^0.1.18-rc.
|
|
33
|
+
"@owlmeans/planning": "^0.1.18-rc.30",
|
|
34
34
|
"@owlmeans/resource": "^0.1.18-rc.41",
|
|
35
35
|
"@owlmeans/log": "^0.1.18-rc.4",
|
|
36
36
|
"@owlmeans/route": "^0.1.18-rc.36",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@owlmeans/dep-config": "workspace:*",
|
|
46
|
-
"@owlmeans/server-planning": "^0.1.18-rc.
|
|
46
|
+
"@owlmeans/server-planning": "^0.1.18-rc.36",
|
|
47
47
|
"@owlmeans/test": "^0.1.18-rc.38",
|
|
48
48
|
"@types/bun": "^1.4.2",
|
|
49
49
|
"ajv": "^8.20.0",
|
package/src/client.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { bindAll } from '@owlmeans/client-entrypoint'
|
|
|
2
2
|
import { createLazyService, MiddlewareStage, MiddlewareType } from '@owlmeans/context'
|
|
3
3
|
import type { EntrypointTree } from '@owlmeans/entrypoint'
|
|
4
4
|
import {
|
|
5
|
-
DEFAULT_PLUGIN_ORDER, makeSchemaRegistry, PLANNING_SERVICE, PlanningUnsupported,
|
|
5
|
+
appSchemaHelper, DEFAULT_PLUGIN_ORDER, makeSchemaRegistry, PLANNING_SERVICE, PlanningUnsupported,
|
|
6
6
|
} from '@owlmeans/planning'
|
|
7
|
-
import type { PlanningFacade, PlanningPlugin, PlanningSchemaRegistry } from '@owlmeans/planning'
|
|
7
|
+
import type { PlanningAppDeclarations, PlanningFacade, PlanningPlugin, PlanningSchemaRegistry } from '@owlmeans/planning'
|
|
8
8
|
import { makeRemoteCommitSource } from './commits.js'
|
|
9
9
|
import { makeRemoteDefinitions } from './definitions.js'
|
|
10
10
|
import { makeRemoteFacade } from './facade.js'
|
|
@@ -21,7 +21,7 @@ import type {
|
|
|
21
21
|
* What only a server can do is refused rather than faked: a client runs no middleware and folds
|
|
22
22
|
* nothing, so `store()`, `committed()` and a plugin carrying hooks or a store answer
|
|
23
23
|
* `PlanningUnsupported`. A plugin carrying only schemas is accepted — it layers over the server's
|
|
24
|
-
* bundle.
|
|
24
|
+
* bundle (its `schemas.app` merged over the served declarations, in plugin order).
|
|
25
25
|
*/
|
|
26
26
|
export const makePlanningClientService = <C extends Config, T extends Context<C>>(
|
|
27
27
|
context: T, options: PlanningClientOptions
|
|
@@ -43,11 +43,16 @@ export const makePlanningClientService = <C extends Config, T extends Context<C>
|
|
|
43
43
|
socket: options.socket, poll: options.poll, timeout: options.timeout, stores, lifecycle,
|
|
44
44
|
})
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
plugin.schemas?.
|
|
50
|
-
|
|
46
|
+
// The application's declarations the server served — what every client plugin's `app` merges over.
|
|
47
|
+
let served: PlanningAppDeclarations | undefined
|
|
48
|
+
const layerPlugins = (): void => {
|
|
49
|
+
registry.registerApp(appSchemaHelper.merge([served, ...plugins.map(plugin => plugin.schemas?.app)]))
|
|
50
|
+
plugins.forEach(plugin => {
|
|
51
|
+
plugin.schemas?.assigneeTypes?.forEach(registry.registerAssigneeType)
|
|
52
|
+
plugin.schemas?.flows?.forEach(registry.registerFlow)
|
|
53
|
+
plugin.schemas?.types?.forEach(registry.registerType)
|
|
54
|
+
})
|
|
55
|
+
}
|
|
51
56
|
|
|
52
57
|
let loadedKey = scopeKey()
|
|
53
58
|
let loaded = false
|
|
@@ -64,6 +69,7 @@ export const makePlanningClientService = <C extends Config, T extends Context<C>
|
|
|
64
69
|
const bundle = await operation.wait(context.entrypoint(options.protocols.schema.list).call({ timeout: options.timeout, signal: operation.signal }))
|
|
65
70
|
operation.check()
|
|
66
71
|
registry.load(bundle)
|
|
72
|
+
served = registry.app()
|
|
67
73
|
layerPlugins()
|
|
68
74
|
loaded = true
|
|
69
75
|
|
|
@@ -97,7 +103,7 @@ export const makePlanningClientService = <C extends Config, T extends Context<C>
|
|
|
97
103
|
const stopping = [...feeds]
|
|
98
104
|
feeds.clear()
|
|
99
105
|
loaded = false; loading = null; definitions?.invalidate()
|
|
100
|
-
registry.load({ version: 1, types: [], flows: [] }); layerPlugins()
|
|
106
|
+
registry.load({ version: 1, types: [], flows: [] }); served = undefined; layerPlugins()
|
|
101
107
|
await Promise.all(stopping.map(async feed => await feed.stop()))
|
|
102
108
|
})
|
|
103
109
|
|
|
@@ -116,6 +122,9 @@ export const makePlanningClientService = <C extends Config, T extends Context<C>
|
|
|
116
122
|
if (plugin.before != null || plugin.after != null || plugin.store != null || plugin.mintCode != null) {
|
|
117
123
|
throw new PlanningUnsupported(`client:plugin:${plugin.name}`)
|
|
118
124
|
}
|
|
125
|
+
if (plugin.schemas?.app != null) {
|
|
126
|
+
appSchemaHelper.assertDeclarations(plugin.schemas.app)
|
|
127
|
+
}
|
|
119
128
|
const existing = plugins.findIndex(candidate => candidate.name === plugin.name)
|
|
120
129
|
if (existing >= 0) {
|
|
121
130
|
plugins.splice(existing, 1)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { appSchemaHelper, FieldsInvalid, SchemaInvalid, TransitionAction, WorkcardKind } from '@owlmeans/planning'
|
|
3
|
+
import type { PlanningAppDeclarations, PlanningService, WorkcardTypeSchema } from '@owlmeans/planning'
|
|
4
|
+
import { makePlanningFieldValidator } from '../src/index.js'
|
|
5
|
+
import { makeSuite, STORY, STORY_FLOW } from './context.js'
|
|
6
|
+
import type { Suite } from './context.js'
|
|
7
|
+
|
|
8
|
+
/** A choir: every type says what it is for, every card may carry a part. */
|
|
9
|
+
const APP: PlanningAppDeclarations = {
|
|
10
|
+
type: { properties: { description: { type: 'string' } } },
|
|
11
|
+
card: { properties: { part: { type: 'string', enum: ['soprano', 'alto', 'tenor', 'bass'] } } },
|
|
12
|
+
}
|
|
13
|
+
/** What a service serves: the declarations merged (normalized with `type: 'object'`). */
|
|
14
|
+
const SERVED = appSchemaHelper.merge([APP])
|
|
15
|
+
|
|
16
|
+
const SECTIONAL: WorkcardTypeSchema = {
|
|
17
|
+
type: 'test:sectional', kind: WorkcardKind.Card, version: 1, flows: [STORY_FLOW], specifications: [],
|
|
18
|
+
fields: { type: 'object', additionalProperties: false },
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const declared = async (opts: Parameters<typeof makeSuite>[0] = {}): Promise<Suite> => {
|
|
22
|
+
const suite = await makeSuite(opts)
|
|
23
|
+
;(suite.server as unknown as { planning: () => PlanningService }).planning().use({ name: 'choir', schemas: { app: APP } })
|
|
24
|
+
|
|
25
|
+
return suite
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('@owlmeans/client-planning — application declarations', () => {
|
|
29
|
+
test('the served bundle carries them: the registry validates base fields, a form validator takes the same schema', async () => {
|
|
30
|
+
const suite = await declared()
|
|
31
|
+
const registry = await suite.client.planning().loadSchemas()
|
|
32
|
+
|
|
33
|
+
expect(registry.app()).toEqual(SERVED!)
|
|
34
|
+
expect(registry.validator(STORY)({ area: 'stage', part: 'alto' })).toBe(true)
|
|
35
|
+
expect(registry.validator(STORY)({ area: 'stage', part: 'drums' })).toBe(false)
|
|
36
|
+
const form = makePlanningFieldValidator(registry.fieldsSchema(STORY))
|
|
37
|
+
expect(form.validateFields({ area: 'stage', part: 'alto' }).valid).toBe(true)
|
|
38
|
+
expect(form.validateFields({ area: 'stage', part: 'drums' }).valid).toBe(false)
|
|
39
|
+
expect(form.validateFields({ area: 'stage', colour: 'red' }).valid).toBe(false)
|
|
40
|
+
|
|
41
|
+
const project = await suite.project('Spring concert')
|
|
42
|
+
await expect(suite.planning.execute({
|
|
43
|
+
card: { kind: WorkcardKind.Card, type: STORY, parent: project.id!, title: 'Warm-up', fields: { part: 'drums' } },
|
|
44
|
+
action: TransitionAction.Create,
|
|
45
|
+
}, { wait: true })).rejects.toBeInstanceOf(FieldsInvalid)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('a schemas-only client plugin merges over the served declarations; a malformed one is refused', async () => {
|
|
49
|
+
const suite = await declared()
|
|
50
|
+
const service = suite.client.planning()
|
|
51
|
+
await service.loadSchemas()
|
|
52
|
+
|
|
53
|
+
service.use({ name: 'stage', schemas: { app: { card: { properties: { row: { type: 'number' } } } } } })
|
|
54
|
+
expect(Object.keys(service.schemas.app()?.card?.properties ?? {})).toEqual(['part', 'row'])
|
|
55
|
+
expect(() => service.use({ name: 'broken', schemas: { app: { card: { properties: { 'a.b': {} } } } } })).toThrow(SchemaInvalid)
|
|
56
|
+
expect(service.plugins().map(plugin => plugin.name)).toEqual(['stage'])
|
|
57
|
+
service.use({ name: 'stage' })
|
|
58
|
+
expect(Object.keys(service.schemas.app()?.card?.properties ?? {})).toEqual(['part'])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('definitions carry a type\'s app bag over the wire; a refused bag crosses as SchemaInvalid', async () => {
|
|
62
|
+
const suite = await declared({ definitions: true })
|
|
63
|
+
const definitions = suite.planning.definitions!
|
|
64
|
+
|
|
65
|
+
await definitions.define({ types: [{ ...SECTIONAL, app: { description: 'Small groups' } }] })
|
|
66
|
+
const layer = await definitions.registry()
|
|
67
|
+
expect((layer.type(SECTIONAL.type) as WorkcardTypeSchema).app).toEqual({ description: 'Small groups' })
|
|
68
|
+
expect(layer.app()).toEqual(SERVED!)
|
|
69
|
+
expect(layer.validator(SECTIONAL.type)({ part: 'tenor' })).toBe(true)
|
|
70
|
+
await expect(definitions.putType({ ...SECTIONAL, version: 2, app: { conductor: 'me' } })).rejects.toBeInstanceOf(SchemaInvalid)
|
|
71
|
+
})
|
|
72
|
+
})
|