@owlmeans/server-planning 0.1.18-rc.9 → 0.1.18
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 +92 -19
- package/agent-meta/manifest.json +105 -4
- package/agent-meta/skills/server-planning/SKILL.md +89 -138
- package/agent-meta/skills/server-planning/parts/auxiliary.md +54 -0
- package/agent-meta/skills/server-planning/parts/executor.md +82 -0
- package/agent-meta/skills/server-planning/parts/schemas.md +26 -0
- package/agent-meta/skills/server-planning/parts/serving.md +88 -0
- package/agent-meta/skills/server-planning/parts/stores.md +91 -0
- package/build/actions/apply-schema-request.d.ts +8 -0
- package/build/actions/apply-schema-request.d.ts.map +1 -0
- package/build/actions/apply-schema-request.js +33 -0
- package/build/actions/apply-schema-request.js.map +1 -0
- package/build/actions/commit.d.ts +3 -3
- package/build/actions/commit.d.ts.map +1 -1
- package/build/actions/commit.js +5 -4
- package/build/actions/commit.js.map +1 -1
- package/build/actions/define-schemas.d.ts +13 -0
- package/build/actions/define-schemas.d.ts.map +1 -0
- package/build/actions/define-schemas.js +28 -0
- package/build/actions/define-schemas.js.map +1 -0
- package/build/actions/execute.d.ts +11 -10
- package/build/actions/execute.d.ts.map +1 -1
- package/build/actions/execute.js +17 -27
- package/build/actions/execute.js.map +1 -1
- package/build/actions/execution/types.d.ts +34 -0
- package/build/actions/execution/types.d.ts.map +1 -0
- package/build/actions/execution/types.js +2 -0
- package/build/actions/execution/types.js.map +1 -0
- package/build/actions/execution.d.ts +4 -0
- package/build/actions/execution.d.ts.map +1 -0
- package/build/actions/execution.js +88 -0
- package/build/actions/execution.js.map +1 -0
- package/build/actions/get-card.d.ts +6 -0
- package/build/actions/get-card.d.ts.map +1 -0
- package/build/actions/get-card.js +9 -0
- package/build/actions/get-card.js.map +1 -0
- package/build/actions/get-specification.d.ts +6 -0
- package/build/actions/get-specification.d.ts.map +1 -0
- package/build/actions/get-specification.js +9 -0
- package/build/actions/get-specification.js.map +1 -0
- package/build/actions/index.d.ts +12 -3
- package/build/actions/index.d.ts.map +1 -1
- package/build/actions/index.js +11 -3
- package/build/actions/index.js.map +1 -1
- package/build/actions/links.d.ts +3 -3
- package/build/actions/links.d.ts.map +1 -1
- package/build/actions/links.js +6 -5
- package/build/actions/links.js.map +1 -1
- package/build/actions/list-card-specifications.d.ts +6 -0
- package/build/actions/list-card-specifications.d.ts.map +1 -0
- package/build/actions/list-card-specifications.js +11 -0
- package/build/actions/list-card-specifications.js.map +1 -0
- package/build/actions/list-card-transitions.d.ts +6 -0
- package/build/actions/list-card-transitions.d.ts.map +1 -0
- package/build/actions/list-card-transitions.js +11 -0
- package/build/actions/list-card-transitions.js.map +1 -0
- package/build/actions/list-cards.d.ts +6 -0
- package/build/actions/list-cards.d.ts.map +1 -0
- package/build/actions/list-cards.js +10 -0
- package/build/actions/list-cards.js.map +1 -0
- package/build/actions/list-schemas.d.ts +13 -0
- package/build/actions/list-schemas.d.ts.map +1 -0
- package/build/actions/list-schemas.js +20 -0
- package/build/actions/list-schemas.js.map +1 -0
- package/build/actions/list-specification-revisions.d.ts +6 -0
- package/build/actions/list-specification-revisions.d.ts.map +1 -0
- package/build/actions/list-specification-revisions.js +10 -0
- package/build/actions/list-specification-revisions.js.map +1 -0
- package/build/actions/resources.d.ts +5 -0
- package/build/actions/resources.d.ts.map +1 -0
- package/build/actions/resources.js +111 -0
- package/build/actions/resources.js.map +1 -0
- package/build/actions/summarize-cards.d.ts +6 -0
- package/build/actions/summarize-cards.d.ts.map +1 -0
- package/build/actions/summarize-cards.js +11 -0
- package/build/actions/summarize-cards.js.map +1 -0
- package/build/actions/transitions.d.ts +3 -3
- package/build/actions/transitions.d.ts.map +1 -1
- package/build/actions/transitions.js +4 -3
- package/build/actions/transitions.js.map +1 -1
- package/build/actions/types.d.ts +6 -0
- package/build/actions/types.d.ts.map +1 -0
- package/build/actions/types.js +2 -0
- package/build/actions/types.js.map +1 -0
- package/build/actions/watch.d.ts +2 -2
- package/build/actions/watch.d.ts.map +1 -1
- package/build/actions/watch.js +8 -6
- package/build/actions/watch.js.map +1 -1
- package/build/conformance/assert/types.d.ts +19 -0
- package/build/conformance/assert/types.d.ts.map +1 -0
- package/build/conformance/assert/types.js +2 -0
- package/build/conformance/assert/types.js.map +1 -0
- package/build/conformance/assert.d.ts +15 -0
- package/build/conformance/assert.d.ts.map +1 -0
- package/build/conformance/assert.js +52 -0
- package/build/conformance/assert.js.map +1 -0
- package/build/conformance/cases.d.ts +7 -0
- package/build/conformance/cases.d.ts.map +1 -0
- package/build/conformance/cases.js +544 -0
- package/build/conformance/cases.js.map +1 -0
- package/build/conformance/clock.d.ts +6 -0
- package/build/conformance/clock.d.ts.map +1 -0
- package/build/conformance/clock.js +12 -0
- package/build/conformance/clock.js.map +1 -0
- package/build/conformance/consts.d.ts +30 -0
- package/build/conformance/consts.d.ts.map +1 -0
- package/build/conformance/consts.js +159 -0
- package/build/conformance/consts.js.map +1 -0
- package/build/conformance/errors.d.ts +5 -0
- package/build/conformance/errors.d.ts.map +1 -0
- package/build/conformance/errors.js +8 -0
- package/build/conformance/errors.js.map +1 -0
- package/build/conformance/fixtures/types.d.ts +13 -0
- package/build/conformance/fixtures/types.d.ts.map +1 -0
- package/build/conformance/fixtures/types.js +2 -0
- package/build/conformance/fixtures/types.js.map +1 -0
- package/build/conformance/fixtures.d.ts +14 -0
- package/build/conformance/fixtures.d.ts.map +1 -0
- package/build/conformance/fixtures.js +26 -0
- package/build/conformance/fixtures.js.map +1 -0
- package/build/conformance/index.d.ts +15 -0
- package/build/conformance/index.d.ts.map +1 -0
- package/build/conformance/index.js +12 -0
- package/build/conformance/index.js.map +1 -0
- package/build/conformance/resources.d.ts +4 -0
- package/build/conformance/resources.d.ts.map +1 -0
- package/build/conformance/resources.js +228 -0
- package/build/conformance/resources.js.map +1 -0
- package/build/conformance/types.d.ts +18 -0
- package/build/conformance/types.d.ts.map +1 -0
- package/build/conformance/types.js +2 -0
- package/build/conformance/types.js.map +1 -0
- package/build/consts.d.ts +5 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +5 -0
- package/build/consts.js.map +1 -1
- package/build/definitions.d.ts +18 -0
- package/build/definitions.d.ts.map +1 -0
- package/build/definitions.js +225 -0
- package/build/definitions.js.map +1 -0
- package/build/executor/changes/types.d.ts +36 -0
- package/build/executor/changes/types.d.ts.map +1 -0
- package/build/executor/changes/types.js +2 -0
- package/build/executor/changes/types.js.map +1 -0
- package/build/executor/changes.d.ts +3 -30
- package/build/executor/changes.d.ts.map +1 -1
- package/build/executor/changes.js +42 -51
- package/build/executor/changes.js.map +1 -1
- package/build/executor/code/types.d.ts +17 -0
- package/build/executor/code/types.d.ts.map +1 -0
- package/build/executor/code/types.js +2 -0
- package/build/executor/code/types.js.map +1 -0
- package/build/executor/code.d.ts +3 -14
- package/build/executor/code.d.ts.map +1 -1
- package/build/executor/code.js +50 -54
- package/build/executor/code.js.map +1 -1
- package/build/executor/consts.local.d.ts +5 -0
- package/build/executor/consts.local.d.ts.map +1 -0
- package/build/executor/consts.local.js +6 -0
- package/build/executor/consts.local.js.map +1 -0
- package/build/executor/creator/types.d.ts +33 -0
- package/build/executor/creator/types.d.ts.map +1 -0
- package/build/executor/creator/types.js +2 -0
- package/build/executor/creator/types.js.map +1 -0
- package/build/executor/creator.d.ts +11 -0
- package/build/executor/creator.d.ts.map +1 -0
- package/build/executor/creator.js +32 -0
- package/build/executor/creator.js.map +1 -0
- package/build/executor/resolve/types.d.ts +30 -0
- package/build/executor/resolve/types.d.ts.map +1 -0
- package/build/executor/resolve/types.js +2 -0
- package/build/executor/resolve/types.js.map +1 -0
- package/build/executor/resolve.d.ts +5 -34
- package/build/executor/resolve.d.ts.map +1 -1
- package/build/executor/resolve.js +169 -110
- package/build/executor/resolve.js.map +1 -1
- package/build/executor/types.d.ts +23 -0
- package/build/executor/types.d.ts.map +1 -0
- package/build/executor/types.js +2 -0
- package/build/executor/types.js.map +1 -0
- package/build/executor/validate.d.ts +6 -5
- package/build/executor/validate.d.ts.map +1 -1
- package/build/executor/validate.js +38 -27
- package/build/executor/validate.js.map +1 -1
- package/build/executor.d.ts +11 -7
- package/build/executor.d.ts.map +1 -1
- package/build/executor.js +27 -21
- package/build/executor.js.map +1 -1
- package/build/facade.d.ts +20 -2
- package/build/facade.d.ts.map +1 -1
- package/build/facade.js +144 -22
- package/build/facade.js.map +1 -1
- package/build/helper.d.ts +5 -9
- package/build/helper.d.ts.map +1 -1
- package/build/helper.js +12 -11
- package/build/helper.js.map +1 -1
- package/build/hierarchy/types.d.ts +20 -0
- package/build/hierarchy/types.d.ts.map +1 -0
- package/build/hierarchy/types.js +2 -0
- package/build/hierarchy/types.js.map +1 -0
- package/build/hierarchy.d.ts +5 -0
- package/build/hierarchy.d.ts.map +1 -0
- package/build/hierarchy.js +133 -0
- package/build/hierarchy.js.map +1 -0
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/invariants/types.d.ts +6 -0
- package/build/invariants/types.d.ts.map +1 -0
- package/build/invariants/types.js +2 -0
- package/build/invariants/types.js.map +1 -0
- package/build/invariants.d.ts +5 -0
- package/build/invariants.d.ts.map +1 -0
- package/build/invariants.js +126 -0
- package/build/invariants.js.map +1 -0
- package/build/projection.d.ts +4 -10
- package/build/projection.d.ts.map +1 -1
- package/build/projection.js +7 -5
- package/build/projection.js.map +1 -1
- package/build/queue/consts.d.ts +5 -0
- package/build/queue/consts.d.ts.map +1 -0
- package/build/queue/consts.js +5 -0
- package/build/queue/consts.js.map +1 -0
- package/build/queue/declare.d.ts +5 -0
- package/build/queue/declare.d.ts.map +1 -0
- package/build/queue/declare.js +5 -0
- package/build/queue/declare.js.map +1 -0
- package/build/queue/index.d.ts +3 -18
- package/build/queue/index.d.ts.map +1 -1
- package/build/queue/index.js +2 -7
- package/build/queue/index.js.map +1 -1
- package/build/queue/types.d.ts +13 -0
- package/build/queue/types.d.ts.map +1 -0
- package/build/queue/types.js +2 -0
- package/build/queue/types.js.map +1 -0
- package/build/registry.d.ts +5 -22
- package/build/registry.d.ts.map +1 -1
- package/build/registry.js +16 -10
- package/build/registry.js.map +1 -1
- package/build/resources.d.ts +5 -0
- package/build/resources.d.ts.map +1 -0
- package/build/resources.js +287 -0
- package/build/resources.js.map +1 -0
- package/build/service.d.ts +3 -16
- package/build/service.d.ts.map +1 -1
- package/build/service.js +12 -0
- package/build/service.js.map +1 -1
- package/build/store/commits.d.ts.map +1 -1
- package/build/store/commits.js +3 -1
- package/build/store/commits.js.map +1 -1
- package/build/store/composite.d.ts +1 -0
- package/build/store/composite.d.ts.map +1 -1
- package/build/store/composite.js +7 -2
- package/build/store/composite.js.map +1 -1
- package/build/store/consts.local.d.ts +2 -0
- package/build/store/consts.local.d.ts.map +1 -0
- package/build/store/consts.local.js +2 -0
- package/build/store/consts.local.js.map +1 -0
- package/build/store/fold/types.d.ts +43 -0
- package/build/store/fold/types.d.ts.map +1 -0
- package/build/store/fold/types.js +2 -0
- package/build/store/fold/types.js.map +1 -0
- package/build/store/fold.d.ts +8 -30
- package/build/store/fold.d.ts.map +1 -1
- package/build/store/fold.js +190 -198
- package/build/store/fold.js.map +1 -1
- package/build/store/index.d.ts +1 -0
- package/build/store/index.d.ts.map +1 -1
- package/build/store/index.js.map +1 -1
- package/build/store/memory.d.ts +12 -0
- package/build/store/memory.d.ts.map +1 -1
- package/build/store/memory.js +0 -0
- package/build/store/memory.js.map +1 -1
- package/build/store/types.d.ts +15 -1
- package/build/store/types.d.ts.map +1 -1
- package/build/store/types.local.d.ts +5 -0
- package/build/store/types.local.d.ts.map +1 -0
- package/build/store/types.local.js +2 -0
- package/build/store/types.local.js.map +1 -0
- package/build/types.d.ts +126 -2
- package/build/types.d.ts.map +1 -1
- package/build/types.local.d.ts +7 -0
- package/build/types.local.d.ts.map +1 -0
- package/build/types.local.js +2 -0
- package/build/types.local.js.map +1 -0
- package/build/utils/access/types.d.ts +30 -0
- package/build/utils/access/types.d.ts.map +1 -0
- package/build/utils/access/types.js +2 -0
- package/build/utils/access/types.js.map +1 -0
- package/build/utils/access.d.ts +4 -0
- package/build/utils/access.d.ts.map +1 -0
- package/build/utils/access.js +28 -0
- package/build/utils/access.js.map +1 -0
- package/build/utils/guard/types.d.ts +23 -0
- package/build/utils/guard/types.d.ts.map +1 -0
- package/build/utils/guard/types.js +2 -0
- package/build/utils/guard/types.js.map +1 -0
- package/build/utils/guard.d.ts +7 -10
- package/build/utils/guard.d.ts.map +1 -1
- package/build/utils/guard.js +36 -32
- package/build/utils/guard.js.map +1 -1
- package/build/utils/handler/types.d.ts +26 -0
- package/build/utils/handler/types.d.ts.map +1 -0
- package/build/utils/handler/types.js +2 -0
- package/build/utils/handler/types.js.map +1 -0
- package/build/utils/handler.d.ts +6 -0
- package/build/utils/handler.d.ts.map +1 -0
- package/build/utils/handler.js +21 -0
- package/build/utils/handler.js.map +1 -0
- package/build/utils/index.d.ts +8 -0
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js +3 -0
- package/build/utils/index.js.map +1 -1
- package/build/utils/request/types.d.ts +23 -0
- package/build/utils/request/types.d.ts.map +1 -0
- package/build/utils/request/types.js +2 -0
- package/build/utils/request/types.js.map +1 -0
- package/build/utils/request.d.ts +4 -0
- package/build/utils/request.d.ts.map +1 -0
- package/build/utils/request.js +27 -0
- package/build/utils/request.js.map +1 -0
- package/build/utils/scope.d.ts +17 -13
- package/build/utils/scope.d.ts.map +1 -1
- package/build/utils/scope.js +21 -35
- package/build/utils/scope.js.map +1 -1
- package/build/utils/types.d.ts +8 -0
- package/build/utils/types.d.ts.map +1 -0
- package/build/utils/types.js +2 -0
- package/build/utils/types.js.map +1 -0
- package/package.json +30 -22
- package/src/actions/apply-schema-request.ts +36 -0
- package/src/actions/commit.ts +7 -5
- package/src/actions/define-schemas.ts +34 -0
- package/src/actions/execute.ts +32 -34
- package/src/actions/execution/types.ts +34 -0
- package/src/actions/execution.ts +100 -0
- package/src/actions/get-card.ts +14 -0
- package/src/actions/get-specification.ts +14 -0
- package/src/actions/index.ts +13 -3
- package/src/actions/links.ts +8 -6
- package/src/actions/list-card-specifications.ts +15 -0
- package/src/actions/list-card-transitions.ts +15 -0
- package/src/actions/list-cards.ts +14 -0
- package/src/actions/list-schemas.ts +29 -0
- package/src/actions/list-specification-revisions.ts +15 -0
- package/src/actions/resources.ts +79 -0
- package/src/actions/summarize-cards.ts +15 -0
- package/src/actions/transitions.ts +6 -4
- package/src/actions/types.ts +5 -0
- package/src/actions/watch.ts +11 -7
- package/src/conformance/assert/types.ts +16 -0
- package/src/conformance/assert.ts +67 -0
- package/src/conformance/cases.ts +631 -0
- package/src/conformance/clock.ts +11 -0
- package/src/conformance/consts.ts +165 -0
- package/src/conformance/errors.ts +7 -0
- package/src/conformance/fixtures/types.ts +11 -0
- package/src/conformance/fixtures.ts +46 -0
- package/src/conformance/index.ts +14 -0
- package/src/conformance/resources.ts +227 -0
- package/src/conformance/types.ts +20 -0
- package/src/consts.ts +6 -0
- package/src/definitions.ts +264 -0
- package/src/executor/changes/types.ts +38 -0
- package/src/executor/changes.ts +49 -68
- package/src/executor/code/types.ts +23 -0
- package/src/executor/code.ts +62 -69
- package/src/executor/consts.local.ts +8 -0
- package/src/executor/creator/types.ts +33 -0
- package/src/executor/creator.ts +43 -0
- package/src/executor/resolve/types.ts +30 -0
- package/src/executor/resolve.ts +181 -139
- package/src/executor/types.ts +23 -0
- package/src/executor/validate.ts +40 -38
- package/src/executor.ts +33 -27
- package/src/facade.ts +132 -29
- package/src/helper.ts +12 -11
- package/src/hierarchy/types.ts +20 -0
- package/src/hierarchy.ts +113 -0
- package/src/index.ts +1 -0
- package/src/invariants/types.ts +6 -0
- package/src/invariants.ts +106 -0
- package/src/projection.ts +12 -18
- package/src/queue/consts.ts +5 -0
- package/src/queue/declare.ts +7 -0
- package/src/queue/index.ts +3 -25
- package/src/queue/types.ts +14 -0
- package/src/registry.ts +18 -36
- package/src/resources.ts +256 -0
- package/src/service.ts +16 -20
- package/src/store/commits.ts +5 -4
- package/src/store/composite.ts +7 -2
- package/src/store/consts.local.ts +1 -0
- package/src/store/fold/types.ts +44 -0
- package/src/store/fold.ts +214 -212
- package/src/store/index.ts +1 -0
- package/src/store/memory.ts +0 -0
- package/src/store/types.local.ts +3 -0
- package/src/store/types.ts +16 -1
- package/src/types.local.ts +7 -0
- package/src/types.ts +139 -6
- package/src/utils/access/types.ts +30 -0
- package/src/utils/access.ts +33 -0
- package/src/utils/guard/types.ts +21 -0
- package/src/utils/guard.ts +44 -31
- package/src/utils/handler/types.ts +28 -0
- package/src/utils/handler.ts +35 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/request/types.ts +23 -0
- package/src/utils/request.ts +40 -0
- package/src/utils/scope.ts +39 -42
- package/src/utils/types.ts +8 -0
- package/tests/access.spec.ts +160 -0
- package/tests/app.spec.ts +112 -0
- package/tests/conformance.spec.ts +47 -0
- package/tests/create-example.spec.ts +202 -0
- package/tests/creator.spec.ts +257 -0
- package/tests/entrypoints.spec.ts +5 -0
- package/tests/memory-store.spec.ts +52 -0
- package/tests/narrowing.spec.ts +94 -0
- package/tests/plugins.spec.ts +4 -3
- package/tests/writes.spec.ts +140 -0
- package/build/actions/cards.d.ts +0 -16
- package/build/actions/cards.d.ts.map +0 -1
- package/build/actions/cards.js +0 -32
- package/build/actions/cards.js.map +0 -1
- package/build/actions/schemas.d.ts +0 -6
- package/build/actions/schemas.d.ts.map +0 -1
- package/build/actions/schemas.js +0 -9
- package/build/actions/schemas.js.map +0 -1
- package/build/actions/specs.d.ts +0 -10
- package/build/actions/specs.d.ts.map +0 -1
- package/build/actions/specs.js +0 -14
- package/build/actions/specs.js.map +0 -1
- package/src/actions/cards.ts +0 -52
- package/src/actions/schemas.ts +0 -15
- package/src/actions/specs.ts +0 -23
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { AuthForbidden } from '@owlmeans/auth'
|
|
3
|
+
import {
|
|
4
|
+
CommitState, makePlanningProtocols, ParentNotFound, PlanningForbidden, PlanningSchemaKind, SchemaOrigin,
|
|
5
|
+
TransitionAction, WorkcardKind, WorkcardNotFound,
|
|
6
|
+
} from '@owlmeans/planning'
|
|
7
|
+
import type { CommitEvent, SchemaDefineReply, ScopedSchemaBundle } from '@owlmeans/planning'
|
|
8
|
+
import {
|
|
9
|
+
defineSchemas, executePlanning, getCard, getCommit, listCards, listLinks, listSchemas, summarizeCards,
|
|
10
|
+
} from '../src/actions/index.js'
|
|
11
|
+
import type { PlanningAccess, PlanningHandlerOptions } from '../src/types.js'
|
|
12
|
+
import {
|
|
13
|
+
createProject, createTask, ENTITY, invoke, makeTestPlanning, OTHER_ENTITY, PROJECT, session, TASK,
|
|
14
|
+
} from './context.js'
|
|
15
|
+
|
|
16
|
+
const tree = makePlanningProtocols({ base: { alias: 'test:planning' }, guards: 'test-guard', definitions: true })
|
|
17
|
+
|
|
18
|
+
const withAccess = (access: PlanningAccess | (() => PlanningAccess)): PlanningHandlerOptions => ({
|
|
19
|
+
access: async () => typeof access === 'function' ? access() : access,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('@owlmeans/server-planning — access resolver', () => {
|
|
23
|
+
test('the organization is the resolver\'s, never the token\'s, and a resolver throw is the answer', async () => {
|
|
24
|
+
const { context, facade } = await makeTestPlanning()
|
|
25
|
+
const theirs = await createProject(facade(OTHER_ENTITY), 'Theirs')
|
|
26
|
+
await createProject(facade(), 'Mine')
|
|
27
|
+
|
|
28
|
+
const listed = await invoke(listCards(tree.card.list, withAccess({ entityId: OTHER_ENTITY })), context, { ...session(ENTITY), query: {} })
|
|
29
|
+
expect(listed.items.map((card: { id: string }) => card.id)).toEqual([theirs.id])
|
|
30
|
+
|
|
31
|
+
const refusing: PlanningHandlerOptions = { access: async () => { throw new AuthForbidden('no-seat') } }
|
|
32
|
+
await expect(invoke(listCards(tree.card.list, refusing), context, { ...session(), query: {} })).rejects.toBeInstanceOf(AuthForbidden)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('projects narrow reads: an invisible project and its cards are absent, lists and summaries shrink', async () => {
|
|
36
|
+
const { context, facade } = await makeTestPlanning()
|
|
37
|
+
const visible = await createProject(facade(), 'Visible')
|
|
38
|
+
const hidden = await createProject(facade(), 'Hidden')
|
|
39
|
+
const seen = (await createTask(facade(), visible.id!, 'Seen')).card!
|
|
40
|
+
const unseen = (await createTask(facade(), hidden.id!, 'Unseen')).card!
|
|
41
|
+
await facade().execute({ card: unseen.id!, action: TransitionAction.Link, link: { type: 'blocks', to: seen.id! } }, { wait: true })
|
|
42
|
+
const opts = withAccess({ entityId: ENTITY, projects: [visible.id!] })
|
|
43
|
+
|
|
44
|
+
await expect(invoke(getCard(tree.card.get, opts), context, { ...session(), params: { id: unseen.id } })).rejects.toBeInstanceOf(WorkcardNotFound)
|
|
45
|
+
await expect(invoke(getCard(tree.card.get, opts), context, { ...session(), params: { id: hidden.id } })).rejects.toBeInstanceOf(WorkcardNotFound)
|
|
46
|
+
expect((await invoke(getCard(tree.card.get, opts), context, { ...session(), params: { id: seen.id } })).id).toBe(seen.id)
|
|
47
|
+
|
|
48
|
+
const listed = await invoke(listCards(tree.card.list, opts), context, { ...session(), query: { type: TASK } })
|
|
49
|
+
expect(listed.items.map((card: { id: string }) => card.id)).toEqual([seen.id])
|
|
50
|
+
const summary = await invoke(summarizeCards(tree.card.summary, opts), context, { ...session(), query: { parents: `${visible.id},${hidden.id}` } })
|
|
51
|
+
expect(Object.keys(summary)).toEqual([visible.id!])
|
|
52
|
+
expect((await invoke(listLinks(tree.link.list, opts), context, { ...session(), query: { to: seen.id } })).total).toBe(0)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('projects narrow writes: an invisible parent is ParentNotFound, an invisible card absent', async () => {
|
|
56
|
+
const { context, facade } = await makeTestPlanning()
|
|
57
|
+
const visible = await createProject(facade(), 'Visible')
|
|
58
|
+
const hidden = await createProject(facade(), 'Hidden')
|
|
59
|
+
const unseen = (await createTask(facade(), hidden.id!, 'Unseen')).card!
|
|
60
|
+
const execute = executePlanning(tree.execute, withAccess({ entityId: ENTITY, projects: [visible.id!] }))
|
|
61
|
+
|
|
62
|
+
await expect(invoke(execute, context, {
|
|
63
|
+
...session(), body: { card: { kind: WorkcardKind.Card, type: TASK, parent: hidden.id, title: 'Sneaky' }, action: TransitionAction.Create },
|
|
64
|
+
})).rejects.toBeInstanceOf(ParentNotFound)
|
|
65
|
+
await expect(invoke(execute, context, {
|
|
66
|
+
...session(), body: { card: unseen.id, action: TransitionAction.Update, changes: { title: 'Renamed' } },
|
|
67
|
+
})).rejects.toBeInstanceOf(WorkcardNotFound)
|
|
68
|
+
|
|
69
|
+
const made = await invoke(execute, context, {
|
|
70
|
+
...session(), body: { card: { kind: WorkcardKind.Card, type: TASK, parent: visible.id, title: 'Allowed' }, action: TransitionAction.Create, wait: true },
|
|
71
|
+
})
|
|
72
|
+
expect(made.card.title).toBe('Allowed')
|
|
73
|
+
expect((await facade().cards.get(unseen.id!)).title).toBe('Unseen')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('commit status and subscriptions stay inside the visible projects', async () => {
|
|
77
|
+
const { context, facade, service } = await makeTestPlanning()
|
|
78
|
+
const visible = await createProject(facade(), 'Visible')
|
|
79
|
+
const hidden = await createProject(facade(), 'Hidden')
|
|
80
|
+
const narrowed = service.for({ entityId: ENTITY, projects: [visible.id!] })
|
|
81
|
+
const heard: CommitEvent[] = []
|
|
82
|
+
const unsubscribe = await narrowed.commits.subscribe(event => { heard.push(event) })
|
|
83
|
+
|
|
84
|
+
const seen = await createTask(facade(), visible.id!, 'Seen')
|
|
85
|
+
const unseen = await createTask(facade(), hidden.id!, 'Unseen')
|
|
86
|
+
unsubscribe()
|
|
87
|
+
|
|
88
|
+
expect(heard.map(event => event.card)).toEqual([seen.card!.id!])
|
|
89
|
+
await expect(invoke(getCommit(tree.commit.get, withAccess({ entityId: ENTITY, projects: [visible.id!] })), context, {
|
|
90
|
+
...session(), params: { transition: unseen.transition.id }, query: {},
|
|
91
|
+
})).rejects.toBeInstanceOf(WorkcardNotFound)
|
|
92
|
+
expect((await narrowed.commits.status(seen.transition.id!)).state).toBe(CommitState.Committed)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('grants gate project creation, project deletion and schema definitions with a 403 refusal', async () => {
|
|
96
|
+
const { context, facade } = await makeTestPlanning({}, { schemas: true })
|
|
97
|
+
const parent = await createProject(facade(), 'Parent')
|
|
98
|
+
const other = await createProject(facade(), 'Other')
|
|
99
|
+
const nested = { kind: WorkcardKind.Project, type: PROJECT, parent: parent.id, title: 'Nested' }
|
|
100
|
+
const scoped = (grants: PlanningAccess['grants']) => withAccess({ entityId: ENTITY, grants })
|
|
101
|
+
|
|
102
|
+
await expect(invoke(executePlanning(tree.execute, scoped({})), context, {
|
|
103
|
+
...session(), body: { card: { kind: WorkcardKind.Project, type: PROJECT, title: 'Root' }, action: TransitionAction.Create },
|
|
104
|
+
})).rejects.toBeInstanceOf(PlanningForbidden)
|
|
105
|
+
expect(PlanningForbidden.httpStatus).toBe(403)
|
|
106
|
+
await expect(invoke(executePlanning(tree.execute, scoped({ createProjects: [parent.id!] })), context, {
|
|
107
|
+
...session(), body: { card: { kind: WorkcardKind.Project, type: PROJECT, title: 'Root' }, action: TransitionAction.Create },
|
|
108
|
+
})).rejects.toBeInstanceOf(PlanningForbidden)
|
|
109
|
+
const made = await invoke(executePlanning(tree.execute, scoped({ createProjects: [parent.id!] })), context, {
|
|
110
|
+
...session(), body: { card: nested, action: TransitionAction.Create, wait: true },
|
|
111
|
+
})
|
|
112
|
+
expect(made.card.parent).toBe(parent.id)
|
|
113
|
+
// A card is not a project: creating one needs no grant.
|
|
114
|
+
await invoke(executePlanning(tree.execute, scoped({})), context, {
|
|
115
|
+
...session(), body: { card: { kind: WorkcardKind.Card, type: TASK, parent: parent.id, title: 'Task' }, action: TransitionAction.Create },
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
await expect(invoke(executePlanning(tree.execute, scoped({ deleteProjects: [other.id!] })), context, {
|
|
119
|
+
...session(), body: { card: parent.id, action: TransitionAction.Delete },
|
|
120
|
+
})).rejects.toBeInstanceOf(PlanningForbidden)
|
|
121
|
+
await invoke(executePlanning(tree.execute, scoped({ deleteProjects: [other.id!] })), context, {
|
|
122
|
+
...session(), body: { card: other.id, action: TransitionAction.Delete, wait: true },
|
|
123
|
+
})
|
|
124
|
+
expect(await facade().cards.load(other.id!)).toBeNull()
|
|
125
|
+
|
|
126
|
+
const flow = { id: 'test:rota', version: 1, statuses: [{ key: 'due', intrinsic: 'planned' }], transitions: [] }
|
|
127
|
+
await expect(invoke(defineSchemas(tree.schema.define!, scoped({ defineSchemas: [parent.id!] })), context, {
|
|
128
|
+
...session(), body: { flows: [flow] },
|
|
129
|
+
})).rejects.toBeInstanceOf(PlanningForbidden)
|
|
130
|
+
const layer: SchemaDefineReply = await invoke(defineSchemas(tree.schema.define!, scoped({ defineSchemas: [parent.id!] })), context, {
|
|
131
|
+
...session(), body: { project: parent.id, flows: [flow] },
|
|
132
|
+
})
|
|
133
|
+
expect([layer.bundle.origins?.flows['test:rota'], layer.records.map(record => record.key)]).toEqual([SchemaOrigin.Project, ['test:rota']])
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
describe('@owlmeans/server-planning — schema handlers', () => {
|
|
138
|
+
test('schema.list answers the code bundle without a schema port, and the scoped layer with one', async () => {
|
|
139
|
+
const plain = await makeTestPlanning()
|
|
140
|
+
const bundle = await invoke(listSchemas(tree.schema.list), plain.context, { ...session(), query: {} })
|
|
141
|
+
expect(bundle).toEqual(plain.service.schemas.bundle())
|
|
142
|
+
|
|
143
|
+
const scoped = await makeTestPlanning({}, { schemas: true })
|
|
144
|
+
const project = await createProject(scoped.facade(), 'Layered')
|
|
145
|
+
const note = { type: 'test:memo', kind: WorkcardKind.Card, version: 1, fields: { type: 'object' }, flows: ['test:task'], specifications: [] }
|
|
146
|
+
await invoke(defineSchemas(tree.schema.define!), scoped.context, { ...session(), body: { project: project.id, types: [note] } })
|
|
147
|
+
|
|
148
|
+
const entityLayer: ScopedSchemaBundle = await invoke(listSchemas(tree.schema.list), scoped.context, { ...session(), query: {} })
|
|
149
|
+
const projectLayer: ScopedSchemaBundle = await invoke(listSchemas(tree.schema.list), scoped.context, { ...session(), query: { project: project.id } })
|
|
150
|
+
expect(entityLayer.types.some(type => type.type === 'test:memo')).toBe(false)
|
|
151
|
+
expect(projectLayer.origins?.types['test:memo']).toBe(SchemaOrigin.Project)
|
|
152
|
+
await expect(invoke(listSchemas(tree.schema.list), scoped.context, { ...session(OTHER_ENTITY), query: { project: project.id } }))
|
|
153
|
+
.rejects.toBeInstanceOf(WorkcardNotFound)
|
|
154
|
+
|
|
155
|
+
const retired: SchemaDefineReply = await invoke(defineSchemas(tree.schema.define!), scoped.context, {
|
|
156
|
+
...session(), body: { project: project.id, retire: [{ kind: PlanningSchemaKind.Type, key: 'test:memo' }] },
|
|
157
|
+
})
|
|
158
|
+
expect([retired.bundle.retired?.types, retired.records[0]?.retired]).toEqual([['test:memo'], true])
|
|
159
|
+
})
|
|
160
|
+
})
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
FieldsInvalid, IntrinsicStatus, makePlanningProtocols, SchemaInvalid, TransitionAction, WorkcardKind,
|
|
4
|
+
} from '@owlmeans/planning'
|
|
5
|
+
import type { PlanningAppDeclarations, SchemaDefineReply, ScopedSchemaBundle, StatusFlowSchema, WorkcardTypeSchema } from '@owlmeans/planning'
|
|
6
|
+
import { defineSchemas, listSchemas } from '../src/actions/index.js'
|
|
7
|
+
import { createProject, createTask, ENTITY, invoke, makeTestPlanning, session, TASK } from './context.js'
|
|
8
|
+
|
|
9
|
+
const tree = makePlanningProtocols({ base: { alias: 'test:planning' }, guards: 'test-guard', definitions: true })
|
|
10
|
+
|
|
11
|
+
const DESCRIBED: PlanningAppDeclarations = {
|
|
12
|
+
type: { properties: { description: { type: 'string' } } },
|
|
13
|
+
flow: { properties: { description: { type: 'string' } } },
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ESTIMATED: PlanningAppDeclarations = { card: { properties: { estimate: { type: 'number', minimum: 0 } } } }
|
|
17
|
+
|
|
18
|
+
const ROTA: StatusFlowSchema = {
|
|
19
|
+
id: 'test:rota', version: 1, statuses: [{ key: 'due', intrinsic: IntrinsicStatus.Planned, initial: true }], transitions: [],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const CHORE: WorkcardTypeSchema = {
|
|
23
|
+
type: 'test:chore', kind: WorkcardKind.Card, version: 1, flows: [ROTA.id], specifications: [],
|
|
24
|
+
fields: { type: 'object', additionalProperties: false },
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('@owlmeans/server-planning — application declarations', () => {
|
|
28
|
+
test('the service\'s own come first, then every plugin\'s in plugin order; a replaced plugin takes its own away', async () => {
|
|
29
|
+
const { service } = await makeTestPlanning({
|
|
30
|
+
schemas: { app: { card: { properties: { estimate: { type: 'string' } } } } },
|
|
31
|
+
plugins: [
|
|
32
|
+
{ name: 'late', order: 90, schemas: { app: { card: { properties: { estimate: { type: 'number' } }, required: ['estimate'] } } } },
|
|
33
|
+
{ name: 'early', order: 10, schemas: { app: { card: { properties: { estimate: { type: 'integer' }, hours: { type: 'number' } } } } } },
|
|
34
|
+
],
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
expect(service.schemas.app()?.card).toEqual({
|
|
38
|
+
type: 'object', properties: { estimate: { type: 'number' }, hours: { type: 'number' } }, required: ['estimate'],
|
|
39
|
+
})
|
|
40
|
+
service.use({ name: 'late' })
|
|
41
|
+
expect(service.schemas.app()?.card).toEqual({ type: 'object', properties: { estimate: { type: 'integer' }, hours: { type: 'number' } } })
|
|
42
|
+
expect(service.schemas.bundle().app?.card?.properties.estimate).toEqual({ type: 'integer' })
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('a malformed declaration refuses the plugin and leaves the registry as it was', async () => {
|
|
46
|
+
const { service } = await makeTestPlanning({ plugins: [{ name: 'app', schemas: { app: DESCRIBED } }] })
|
|
47
|
+
const before = service.plugins().map(plugin => plugin.name)
|
|
48
|
+
|
|
49
|
+
expect(() => service.use({ name: 'bad', schemas: { app: { card: { properties: {}, additionalProperties: false } as never } } }))
|
|
50
|
+
.toThrow(SchemaInvalid)
|
|
51
|
+
expect(service.plugins().map(plugin => plugin.name)).toEqual(before)
|
|
52
|
+
expect(service.schemas.app()).toEqual(service.schemas.bundle().app!)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('nothing declared: no `app` in the bundle, and an app bag on a data-defined type is refused', async () => {
|
|
56
|
+
const { service, facade } = await makeTestPlanning({}, { schemas: true })
|
|
57
|
+
|
|
58
|
+
expect('app' in service.schemas.bundle()).toBe(false)
|
|
59
|
+
expect('app' in await facade().definitions!.bundle()).toBe(false)
|
|
60
|
+
await expect(facade().definitions!.define({ flows: [ROTA], types: [{ ...CHORE, app: { description: 'x' } }] }))
|
|
61
|
+
.rejects.toThrow('type:test:chore:app:undeclared')
|
|
62
|
+
expect(await facade().definitions!.records()).toEqual([])
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('base card fields reach a code type with closed fields, on create and on update', async () => {
|
|
66
|
+
const { facade } = await makeTestPlanning({ plugins: [{ name: 'app', schemas: { app: ESTIMATED } }] })
|
|
67
|
+
const planning = facade()
|
|
68
|
+
const project = await createProject(planning)
|
|
69
|
+
|
|
70
|
+
const task = (await createTask(planning, project.id!, 'Estimated', { fields: { area: 'user', estimate: 3 } })).card!
|
|
71
|
+
expect(task.fields).toEqual({ area: 'user', estimate: 3 })
|
|
72
|
+
await expect(createTask(planning, project.id!, 'Negative', { fields: { estimate: -1 } })).rejects.toBeInstanceOf(FieldsInvalid)
|
|
73
|
+
await expect(createTask(planning, project.id!, 'Unknown', { fields: { colour: 'red' } })).rejects.toBeInstanceOf(FieldsInvalid)
|
|
74
|
+
await expect(planning.execute({ card: task.id!, action: TransitionAction.Update, changes: { fields: { estimate: 'soon' } } }, { wait: true }))
|
|
75
|
+
.rejects.toBeInstanceOf(FieldsInvalid)
|
|
76
|
+
expect((await planning.execute({ card: task.id!, action: TransitionAction.Update, changes: { fields: { estimate: 5 } } }, { wait: true })).card!.fields.estimate)
|
|
77
|
+
.toBe(5)
|
|
78
|
+
// A project is not a card type: its fields are its own type's alone.
|
|
79
|
+
expect(planning.schemas.fieldsSchema(project.type)).toBe(planning.schemas.type(project.type).fields)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('define, put and seed check the bags; the wire carries them both ways', async () => {
|
|
83
|
+
const { context, facade } = await makeTestPlanning({ plugins: [{ name: 'app', schemas: { app: { ...DESCRIBED, ...ESTIMATED } } }] }, { schemas: true })
|
|
84
|
+
const definitions = facade().definitions!
|
|
85
|
+
|
|
86
|
+
const reply: SchemaDefineReply = await invoke(defineSchemas(tree.schema.define!), context, {
|
|
87
|
+
...session(), body: { flows: [{ ...ROTA, app: { description: 'Weekly' } }], types: [{ ...CHORE, app: { description: 'Small jobs' } }] },
|
|
88
|
+
})
|
|
89
|
+
expect(reply.records.map(record => (record.definition as { app?: unknown }).app)).toEqual([{ description: 'Weekly' }, { description: 'Small jobs' }])
|
|
90
|
+
expect(reply.bundle.app?.type?.properties).toEqual({ description: { type: 'string' } })
|
|
91
|
+
const listed: ScopedSchemaBundle = await invoke(listSchemas(tree.schema.list), context, { ...session(), query: {} })
|
|
92
|
+
expect(listed.types.find(type => type.type === CHORE.type)?.app).toEqual({ description: 'Small jobs' })
|
|
93
|
+
|
|
94
|
+
await expect(invoke(defineSchemas(tree.schema.define!), context, {
|
|
95
|
+
...session(), body: { types: [{ ...CHORE, app: { description: 7 } }] },
|
|
96
|
+
})).rejects.toBeInstanceOf(SchemaInvalid)
|
|
97
|
+
await expect(definitions.putType({ ...CHORE, version: 2, app: { owner: 'me' } })).rejects.toThrow('type:test:chore:app:')
|
|
98
|
+
await expect(definitions.putFlow({ ...ROTA, version: 2, app: { owner: 'me' } })).rejects.toThrow('flow:test:rota:app:')
|
|
99
|
+
await expect(definitions.seed({ types: [{ ...CHORE, type: 'test:errand', app: { owner: 'me' } }] })).rejects.toBeInstanceOf(SchemaInvalid)
|
|
100
|
+
expect((await definitions.putType({ ...CHORE, version: 2, app: { description: 'Chores' } })).definition).toMatchObject({ app: { description: 'Chores' } })
|
|
101
|
+
expect((await definitions.registry()).validator(CHORE.type)({ estimate: 1 })).toBe(true)
|
|
102
|
+
expect((await definitions.registry()).validator(CHORE.type)({ estimate: -1 })).toBe(false)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('a code task type keeps answering for an organization with no records', async () => {
|
|
106
|
+
const { facade } = await makeTestPlanning({ plugins: [{ name: 'app', schemas: { app: ESTIMATED } }] }, { schemas: true })
|
|
107
|
+
const registry = await facade(ENTITY).definitions!.registry()
|
|
108
|
+
|
|
109
|
+
expect(registry.validator(TASK)({ area: 'guest', estimate: 2 })).toBe(true)
|
|
110
|
+
expect(registry.validator(TASK)({ area: 'guest', estimate: -2 })).toBe(false)
|
|
111
|
+
})
|
|
112
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, test } from 'bun:test'
|
|
2
|
+
import { AppType, makeBasicContext } from '@owlmeans/context'
|
|
3
|
+
import type { BasicConfig } from '@owlmeans/context'
|
|
4
|
+
import { appendPlanningService } from '../src/service.js'
|
|
5
|
+
import { makeMemoryPlanningStore } from '../src/store/memory.js'
|
|
6
|
+
import {
|
|
7
|
+
conformanceCasesFor, conformanceClock, planningConformance, planningConformancePlugin,
|
|
8
|
+
} from '../src/conformance/index.js'
|
|
9
|
+
import type { ConformanceSubject } from '../src/conformance/index.js'
|
|
10
|
+
|
|
11
|
+
/** The memory store, booted the way a backend boots it, answering every conformance case. */
|
|
12
|
+
const boot = async (schemas: boolean): Promise<ConformanceSubject> => {
|
|
13
|
+
const now = conformanceClock()
|
|
14
|
+
const store = makeMemoryPlanningStore({ now, schemas })
|
|
15
|
+
const cfg: BasicConfig = { ready: false, service: 'planning-conformance', type: AppType.Backend, services: {} }
|
|
16
|
+
const context = appendPlanningService(makeBasicContext(cfg), { store, now, plugins: [planningConformancePlugin] })
|
|
17
|
+
context.configure()
|
|
18
|
+
await context.init()
|
|
19
|
+
const service = context.planning()
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
service,
|
|
23
|
+
store,
|
|
24
|
+
facade: entityId => service.for({ entityId, profileId: 'conformance-librarian', channel: 'conformance' }),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('@owlmeans/server-planning — store conformance (memory, with data-defined schemas)', () => {
|
|
29
|
+
const probe = makeMemoryPlanningStore({ schemas: true })
|
|
30
|
+
for (const entry of conformanceCasesFor(probe)) {
|
|
31
|
+
test(entry.name, async () => { await entry.run(await boot(true)) })
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('@owlmeans/server-planning — store conformance (memory, code schemas only)', () => {
|
|
36
|
+
const probe = makeMemoryPlanningStore()
|
|
37
|
+
const cases = conformanceCasesFor(probe)
|
|
38
|
+
test('a store without the schema port qualifies for every case but the data-defined ones', () => {
|
|
39
|
+
const skipped = planningConformance.filter(entry => !cases.includes(entry))
|
|
40
|
+
if (skipped.some(entry => !(entry.needs ?? []).includes('schemas')) || skipped.length === 0) {
|
|
41
|
+
throw new Error('only the data-defined cases are skipped')
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
for (const entry of cases) {
|
|
45
|
+
test(entry.name, async () => { await entry.run(await boot(false)) })
|
|
46
|
+
}
|
|
47
|
+
})
|
|
@@ -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
|
+
})
|