@owlmeans/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 +122 -29
- package/agent-meta/manifest.json +131 -4
- package/agent-meta/skills/planning/SKILL.md +78 -171
- package/agent-meta/skills/planning/parts/collaboration.md +67 -0
- package/agent-meta/skills/planning/parts/errors.md +51 -0
- package/agent-meta/skills/planning/parts/fold.md +69 -0
- package/agent-meta/skills/planning/parts/mounting.md +64 -0
- package/agent-meta/skills/planning/parts/types.md +141 -0
- package/build/aliases/types.d.ts +72 -0
- package/build/aliases/types.d.ts.map +1 -0
- package/build/aliases/types.js +2 -0
- package/build/aliases/types.js.map +1 -0
- package/build/aliases.d.ts +4 -0
- package/build/aliases.d.ts.map +1 -0
- package/build/aliases.js +28 -0
- package/build/aliases.js.map +1 -0
- package/build/consts.d.ts +43 -34
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +49 -25
- package/build/consts.js.map +1 -1
- package/build/entrypoints/types.d.ts +105 -0
- package/build/entrypoints/types.d.ts.map +1 -0
- package/build/entrypoints/types.js +2 -0
- package/build/entrypoints/types.js.map +1 -0
- package/build/entrypoints.d.ts +5 -1
- package/build/entrypoints.d.ts.map +1 -1
- package/build/entrypoints.js +24 -6
- package/build/entrypoints.js.map +1 -1
- package/build/errors.d.ts +41 -0
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js +61 -0
- package/build/errors.js.map +1 -1
- package/build/helpers/app/consts.local.d.ts +5 -0
- package/build/helpers/app/consts.local.d.ts.map +1 -0
- package/build/helpers/app/consts.local.js +5 -0
- package/build/helpers/app/consts.local.js.map +1 -0
- package/build/helpers/app/types.d.ts +53 -0
- package/build/helpers/app/types.d.ts.map +1 -0
- package/build/helpers/app/types.js +2 -0
- package/build/helpers/app/types.js.map +1 -0
- package/build/helpers/app.d.ts +4 -0
- package/build/helpers/app.d.ts.map +1 -0
- package/build/helpers/app.js +139 -0
- package/build/helpers/app.js.map +1 -0
- package/build/helpers/apply/consts.local.d.ts +7 -0
- package/build/helpers/apply/consts.local.d.ts.map +1 -0
- package/build/helpers/apply/consts.local.js +9 -0
- package/build/helpers/apply/consts.local.js.map +1 -0
- package/build/helpers/apply/types.d.ts +32 -0
- package/build/helpers/apply/types.d.ts.map +1 -0
- package/build/helpers/apply/types.js +2 -0
- package/build/helpers/apply/types.js.map +1 -0
- package/build/helpers/apply.d.ts +5 -23
- package/build/helpers/apply.d.ts.map +1 -1
- package/build/helpers/apply.js +127 -144
- package/build/helpers/apply.js.map +1 -1
- package/build/helpers/card/types.d.ts +20 -0
- package/build/helpers/card/types.d.ts.map +1 -0
- package/build/helpers/card/types.js +2 -0
- package/build/helpers/card/types.js.map +1 -0
- package/build/helpers/card.d.ts +11 -7
- package/build/helpers/card.d.ts.map +1 -1
- package/build/helpers/card.js +41 -28
- package/build/helpers/card.js.map +1 -1
- package/build/helpers/changes/consts.local.d.ts +13 -0
- package/build/helpers/changes/consts.local.d.ts.map +1 -0
- package/build/helpers/changes/consts.local.js +13 -0
- package/build/helpers/changes/consts.local.js.map +1 -0
- package/build/helpers/changes/types.d.ts +58 -0
- package/build/helpers/changes/types.d.ts.map +1 -0
- package/build/helpers/changes/types.js +2 -0
- package/build/helpers/changes/types.js.map +1 -0
- package/build/helpers/changes.d.ts +11 -42
- package/build/helpers/changes.d.ts.map +1 -1
- package/build/helpers/changes.js +237 -251
- package/build/helpers/changes.js.map +1 -1
- package/build/helpers/code/types.d.ts +27 -0
- package/build/helpers/code/types.d.ts.map +1 -0
- package/build/helpers/code/types.js +2 -0
- package/build/helpers/code/types.js.map +1 -0
- package/build/helpers/code.d.ts +6 -17
- package/build/helpers/code.d.ts.map +1 -1
- package/build/helpers/code.js +39 -40
- package/build/helpers/code.js.map +1 -1
- package/build/helpers/index.d.ts +16 -0
- package/build/helpers/index.d.ts.map +1 -1
- package/build/helpers/index.js +4 -0
- package/build/helpers/index.js.map +1 -1
- package/build/helpers/query/types.d.ts +23 -0
- package/build/helpers/query/types.d.ts.map +1 -0
- package/build/helpers/query/types.js +2 -0
- package/build/helpers/query/types.js.map +1 -0
- package/build/helpers/query.d.ts +25 -17
- package/build/helpers/query.d.ts.map +1 -1
- package/build/helpers/query.js +108 -280
- package/build/helpers/query.js.map +1 -1
- package/build/helpers/reply/consts.d.ts +2 -0
- package/build/helpers/reply/consts.d.ts.map +1 -0
- package/build/helpers/reply/consts.js +2 -0
- package/build/helpers/reply/consts.js.map +1 -0
- package/build/helpers/reply/types.d.ts +10 -0
- package/build/helpers/reply/types.d.ts.map +1 -0
- package/build/helpers/reply/types.js +2 -0
- package/build/helpers/reply/types.js.map +1 -0
- package/build/helpers/reply.d.ts +5 -0
- package/build/helpers/reply.d.ts.map +1 -0
- package/build/helpers/reply.js +29 -0
- package/build/helpers/reply.js.map +1 -0
- package/build/helpers/scoped/types.d.ts +73 -0
- package/build/helpers/scoped/types.d.ts.map +1 -0
- package/build/helpers/scoped/types.js +2 -0
- package/build/helpers/scoped/types.js.map +1 -0
- package/build/helpers/scoped.d.ts +25 -0
- package/build/helpers/scoped.d.ts.map +1 -0
- package/build/helpers/scoped.js +223 -0
- package/build/helpers/scoped.js.map +1 -0
- package/build/helpers/specification/types.d.ts +24 -0
- package/build/helpers/specification/types.d.ts.map +1 -0
- package/build/helpers/specification/types.js +2 -0
- package/build/helpers/specification/types.js.map +1 -0
- package/build/helpers/specification.d.ts +9 -13
- package/build/helpers/specification.d.ts.map +1 -1
- package/build/helpers/specification.js +47 -44
- package/build/helpers/specification.js.map +1 -1
- package/build/helpers/status/types.d.ts +48 -0
- package/build/helpers/status/types.d.ts.map +1 -0
- package/build/helpers/status/types.js +2 -0
- package/build/helpers/status/types.js.map +1 -0
- package/build/helpers/status.d.ts +16 -29
- package/build/helpers/status.d.ts.map +1 -1
- package/build/helpers/status.js +77 -73
- package/build/helpers/status.js.map +1 -1
- package/build/helpers/types.local.d.ts +17 -0
- package/build/helpers/types.local.d.ts.map +1 -0
- package/build/helpers/types.local.js +2 -0
- package/build/helpers/types.local.js.map +1 -0
- package/build/helpers/validate/types.d.ts +31 -0
- package/build/helpers/validate/types.d.ts.map +1 -0
- package/build/helpers/validate/types.js +2 -0
- package/build/helpers/validate/types.js.map +1 -0
- package/build/helpers/validate.d.ts +8 -19
- package/build/helpers/validate.d.ts.map +1 -1
- package/build/helpers/validate.js +79 -79
- package/build/helpers/validate.js.map +1 -1
- package/build/helpers/wire/types.d.ts +28 -0
- package/build/helpers/wire/types.d.ts.map +1 -0
- package/build/helpers/wire/types.js +2 -0
- package/build/helpers/wire/types.js.map +1 -0
- package/build/helpers/wire.d.ts +4 -0
- package/build/helpers/wire.d.ts.map +1 -0
- package/build/helpers/wire.js +216 -0
- package/build/helpers/wire.js.map +1 -0
- package/build/i18n/be.json +5 -0
- package/build/i18n/de.json +5 -0
- package/build/i18n/en.json +5 -0
- package/build/i18n/es.json +5 -0
- package/build/i18n/fr.json +5 -0
- package/build/i18n/pl.json +5 -0
- package/build/i18n/ru.json +5 -0
- package/build/i18n/uk.json +5 -0
- package/build/i18n.js +3 -3
- package/build/i18n.js.map +1 -1
- package/build/index.d.ts +7 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -0
- package/build/index.js.map +1 -1
- package/build/models/index.d.ts +2 -3
- package/build/models/index.d.ts.map +1 -1
- package/build/models/index.js +1 -15
- package/build/models/index.js.map +1 -1
- package/build/models/model.d.ts +5 -0
- package/build/models/model.d.ts.map +1 -0
- package/build/models/model.js +16 -0
- package/build/models/model.js.map +1 -0
- package/build/models/project.d.ts +2 -1
- package/build/models/project.d.ts.map +1 -1
- package/build/models/project.js.map +1 -1
- package/build/models/specification.d.ts +2 -1
- package/build/models/specification.d.ts.map +1 -1
- package/build/models/specification.js.map +1 -1
- package/build/models/types.d.ts +69 -0
- package/build/models/types.d.ts.map +1 -0
- package/build/models/types.js +2 -0
- package/build/models/types.js.map +1 -0
- package/build/models/workcard.d.ts +2 -1
- package/build/models/workcard.d.ts.map +1 -1
- package/build/models/workcard.js +12 -12
- package/build/models/workcard.js.map +1 -1
- package/build/registry.d.ts +3 -2
- package/build/registry.d.ts.map +1 -1
- package/build/registry.js +51 -3
- package/build/registry.js.map +1 -1
- package/build/resources/mentions.d.ts +4 -0
- package/build/resources/mentions.d.ts.map +1 -0
- package/build/resources/mentions.js +15 -0
- package/build/resources/mentions.js.map +1 -0
- package/build/resources/protocol/types.d.ts +30 -0
- package/build/resources/protocol/types.d.ts.map +1 -0
- package/build/resources/protocol/types.js +2 -0
- package/build/resources/protocol/types.js.map +1 -0
- package/build/resources/protocol.d.ts +6 -0
- package/build/resources/protocol.d.ts.map +1 -0
- package/build/resources/protocol.js +31 -0
- package/build/resources/protocol.js.map +1 -0
- package/build/resources/types.d.ts +146 -0
- package/build/resources/types.d.ts.map +1 -0
- package/build/resources/types.js +2 -0
- package/build/resources/types.js.map +1 -0
- package/build/resources/types.local.d.ts +6 -0
- package/build/resources/types.local.d.ts.map +1 -0
- package/build/resources/types.local.js +2 -0
- package/build/resources/types.local.js.map +1 -0
- package/build/schemas.d.ts +21 -2
- package/build/schemas.d.ts.map +1 -1
- package/build/schemas.js +164 -2
- package/build/schemas.js.map +1 -1
- package/build/types.d.ts +330 -151
- package/build/types.d.ts.map +1 -1
- package/build/utils.d.ts +6 -0
- package/build/utils.d.ts.map +1 -0
- package/build/utils.js +6 -0
- package/build/utils.js.map +1 -0
- package/package.json +13 -13
- package/src/aliases/types.ts +41 -0
- package/src/aliases.ts +34 -0
- package/src/consts.ts +50 -25
- package/src/entrypoints/types.ts +77 -0
- package/src/entrypoints.ts +53 -24
- package/src/errors.ts +71 -0
- package/src/helpers/app/consts.local.ts +5 -0
- package/src/helpers/app/types.ts +54 -0
- package/src/helpers/app.ts +158 -0
- package/src/helpers/apply/consts.local.ts +10 -0
- package/src/helpers/apply/types.ts +32 -0
- package/src/helpers/apply.ts +141 -153
- package/src/helpers/card/types.ts +20 -0
- package/src/helpers/card.ts +59 -33
- package/src/helpers/changes/consts.local.ts +17 -0
- package/src/helpers/changes/types.ts +66 -0
- package/src/helpers/changes.ts +270 -280
- package/src/helpers/code/types.ts +31 -0
- package/src/helpers/code.ts +52 -48
- package/src/helpers/index.ts +17 -0
- package/src/helpers/query/types.ts +26 -0
- package/src/helpers/query.ts +144 -314
- package/src/helpers/reply/consts.ts +1 -0
- package/src/helpers/reply/types.ts +13 -0
- package/src/helpers/reply.ts +27 -0
- package/src/helpers/scoped/types.ts +78 -0
- package/src/helpers/scoped.ts +281 -0
- package/src/helpers/specification/types.ts +26 -0
- package/src/helpers/specification.ts +69 -51
- package/src/helpers/status/types.ts +46 -0
- package/src/helpers/status.ts +117 -93
- package/src/helpers/types.local.ts +18 -0
- package/src/helpers/validate/types.ts +31 -0
- package/src/helpers/validate.ts +89 -77
- package/src/helpers/wire/types.ts +31 -0
- package/src/helpers/wire.ts +252 -0
- package/src/i18n/be.json +5 -0
- package/src/i18n/de.json +5 -0
- package/src/i18n/en.json +5 -0
- package/src/i18n/es.json +5 -0
- package/src/i18n/fr.json +5 -0
- package/src/i18n/pl.json +5 -0
- package/src/i18n/ru.json +5 -0
- package/src/i18n/uk.json +5 -0
- package/src/i18n.ts +4 -3
- package/src/index.ts +8 -0
- package/src/models/index.ts +2 -17
- package/src/models/model.ts +18 -0
- package/src/models/project.ts +2 -1
- package/src/models/specification.ts +2 -1
- package/src/models/types.ts +70 -0
- package/src/models/workcard.ts +14 -16
- package/src/registry.ts +55 -4
- package/src/resources/mentions.ts +11 -0
- package/src/resources/protocol/types.ts +21 -0
- package/src/resources/protocol.ts +36 -0
- package/src/resources/types.local.ts +5 -0
- package/src/resources/types.ts +148 -0
- package/src/schemas.ts +179 -4
- package/src/types.ts +334 -130
- package/src/utils.ts +9 -0
- package/tests/app.spec.ts +188 -0
- package/tests/apply.spec.ts +95 -31
- package/tests/code.spec.ts +9 -9
- package/tests/csp.spec.ts +122 -0
- package/tests/entrypoints.spec.ts +18 -2
- package/tests/error-status.spec.ts +6 -1
- package/tests/models.spec.ts +24 -16
- package/tests/query.spec.ts +13 -15
- package/tests/schemas.spec.ts +8 -8
- package/tests/scoped.spec.ts +161 -0
- package/tests/status.spec.ts +31 -22
|
@@ -26,6 +26,11 @@ const DECLARED: Record<string, number | undefined> = {
|
|
|
26
26
|
LabelNotAllowed: 422,
|
|
27
27
|
SpecificationSlotUnknown: 422,
|
|
28
28
|
RelationshipRefused: 422,
|
|
29
|
+
SchemaConflict: 409,
|
|
30
|
+
SchemaInUse: 409,
|
|
31
|
+
SchemaSealed: 422,
|
|
32
|
+
SchemaInvalid: 422,
|
|
33
|
+
PlanningForbidden: 403,
|
|
29
34
|
CommitTimeout: undefined,
|
|
30
35
|
CommitFailed: undefined,
|
|
31
36
|
PlanningUnsupported: undefined,
|
|
@@ -40,7 +45,7 @@ describe('planning refusals — declared HTTP statuses', () => {
|
|
|
40
45
|
expect(classes.map(([name]) => name).sort()).toEqual(Object.keys(DECLARED).sort())
|
|
41
46
|
})
|
|
42
47
|
|
|
43
|
-
test('an absent card is 404, a card in another state 409, refused content 422; faults declare nothing', () => {
|
|
48
|
+
test('an absent card is 404, a missing grant 403, a card in another state 409, refused content 422; faults declare nothing', () => {
|
|
44
49
|
for (const [name, Class] of classes) {
|
|
45
50
|
expect([name, statusOf(new Class('x'))]).toEqual([name, DECLARED[name]])
|
|
46
51
|
}
|
package/tests/models.spec.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { applyQuery } from '@owlmeans/resource'
|
|
3
2
|
import { CommitState, TransitionAction, WorkcardKind } from '../src/consts.js'
|
|
4
|
-
import { WorkcardNotFound } from '../src/errors.js'
|
|
5
|
-
import { applyTransition } from '../src/helpers/apply.js'
|
|
6
|
-
import { computeChanges, isEmptyChange } from '../src/helpers/changes.js'
|
|
7
|
-
import { criteriaOf, summaryOf } from '../src/helpers/query.js'
|
|
8
|
-
import { currentSpecification, slotOf } from '../src/helpers/specification.js'
|
|
3
|
+
import { PlanningUnsupported, WorkcardNotFound } from '../src/errors.js'
|
|
9
4
|
import { makeProjectModel, makeWorkcardModel, modelOf } from '../src/models/index.js'
|
|
5
|
+
import type { ProjectModel } from '../src/models/types.js'
|
|
10
6
|
import type {
|
|
11
7
|
PlanningFacade, Project, Specification, TransitionExecution, Workcard, WorkcardDraft,
|
|
12
8
|
} from '../src/types.js'
|
|
13
9
|
import { AT, ENTITY, PROJECT_TYPE, STORY_TYPE, makeRegistry } from './fixtures.js'
|
|
10
|
+
import { applyHelper } from '../src/helpers/apply.js'
|
|
11
|
+
import { changesHelper } from '../src/helpers/changes.js'
|
|
12
|
+
import { queryHelper } from '../src/helpers/query.js'
|
|
13
|
+
import { specificationHelper } from '../src/helpers/specification.js'
|
|
14
|
+
import { recordQueryHelper } from '@owlmeans/resource'
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* The smallest facade the models can run against: maps, folded synchronously with the package's
|
|
@@ -21,21 +22,28 @@ const makeFacade = () => {
|
|
|
21
22
|
const cards = new Map<string, Workcard>()
|
|
22
23
|
const executed: TransitionExecution[] = []
|
|
23
24
|
let next = 0
|
|
25
|
+
const unsupported = async (): Promise<never> => { throw new PlanningUnsupported('model-fixture:resources') }
|
|
24
26
|
|
|
25
27
|
const facade: PlanningFacade = {
|
|
28
|
+
assignees: { get: unsupported, load: unsupported, list: unsupported, create: unsupported, update: unsupported, retire: unsupported },
|
|
29
|
+
teams: { get: unsupported, load: unsupported, list: unsupported, create: unsupported, update: unsupported, remove: unsupported,
|
|
30
|
+
members: unsupported, addMember: unsupported, removeMember: unsupported, projects: unsupported,
|
|
31
|
+
attach: unsupported, detach: unsupported, assignees: unsupported },
|
|
32
|
+
comments: { get: unsupported, load: unsupported, list: unsupported, create: unsupported, update: unsupported, remove: unsupported },
|
|
33
|
+
mentions: { get: unsupported, load: unsupported, list: unsupported, rebuild: unsupported },
|
|
26
34
|
scope: { entityId: ENTITY },
|
|
27
35
|
schemas,
|
|
28
36
|
cards: {
|
|
29
37
|
get: async id => cards.get(id) ?? Promise.reject(new WorkcardNotFound(id)),
|
|
30
38
|
load: async id => cards.get(id) ?? null,
|
|
31
|
-
list: async query => applyQuery([...cards.values()], criteriaOf(query, facade.scope)),
|
|
32
|
-
count: async query => applyQuery([...cards.values()], criteriaOf(query, facade.scope)).total,
|
|
39
|
+
list: async query => recordQueryHelper.applyQuery([...cards.values()], queryHelper.criteriaOf(query, facade.scope)),
|
|
40
|
+
count: async query => recordQueryHelper.applyQuery([...cards.values()], queryHelper.criteriaOf(query, facade.scope)).total,
|
|
33
41
|
summary: async (parents, query) =>
|
|
34
|
-
summaryOf(applyQuery([...cards.values()], criteriaOf(query, facade.scope)).items, parents),
|
|
42
|
+
queryHelper.summaryOf(recordQueryHelper.applyQuery([...cards.values()], queryHelper.criteriaOf(query, facade.scope)).items, parents),
|
|
35
43
|
},
|
|
36
44
|
specifications: {
|
|
37
|
-
current: async (parent, category) => currentSpecification([...cards.values()].filter(card => card.parent === parent), category),
|
|
38
|
-
list: async parent => applyQuery([...cards.values()], { parent, kind: WorkcardKind.Specification }) as never,
|
|
45
|
+
current: async (parent, category) => specificationHelper.currentSpecification([...cards.values()].filter(card => card.parent === parent), category),
|
|
46
|
+
list: async parent => recordQueryHelper.applyQuery([...cards.values()], { parent, kind: WorkcardKind.Specification }) as never,
|
|
39
47
|
get: async id => cards.get(id) as Specification,
|
|
40
48
|
revisions: async () => [],
|
|
41
49
|
},
|
|
@@ -53,18 +61,18 @@ const makeFacade = () => {
|
|
|
53
61
|
const type = schemas.type(card?.type ?? draft!.type)
|
|
54
62
|
const parent = cards.get(card?.parent ?? draft?.parent ?? '')
|
|
55
63
|
const category = (card as Specification | undefined)?.category ?? draft?.category
|
|
56
|
-
const slot = parent != null && category != null ? slotOf(schemas.type(parent.type), category) : undefined
|
|
57
|
-
const set = computeChanges(card, exec, type, schemas, AT, { slot })
|
|
64
|
+
const slot = parent != null && category != null ? specificationHelper.slotOf(schemas.type(parent.type), category) : undefined
|
|
65
|
+
const set = changesHelper.computeChanges(card, exec, type, schemas, AT, { slot })
|
|
58
66
|
const id = card?.id ?? `card-${++next}`
|
|
59
67
|
const transition = {
|
|
60
68
|
id: `t-${++next}`, entityId: ENTITY, card: id, kind: card?.kind ?? draft!.kind, type: type.type,
|
|
61
69
|
seq: (card?.head ?? card?.seq ?? 0) + 1, action: exec.action, changes: set.changes, unset: set.unset,
|
|
62
70
|
actor: {}, at: AT, commit: { state: CommitState.Committed },
|
|
63
71
|
}
|
|
64
|
-
if (exec.action === TransitionAction.Update && isEmptyChange(set)) {
|
|
72
|
+
if (exec.action === TransitionAction.Update && changesHelper.isEmptyChange(set)) {
|
|
65
73
|
return { transition, card, committed: async () => card ?? null }
|
|
66
74
|
}
|
|
67
|
-
const folded = applyTransition(card, transition)
|
|
75
|
+
const folded = applyHelper.applyTransition(card, transition)
|
|
68
76
|
folded == null ? cards.delete(id) : cards.set(id, folded)
|
|
69
77
|
return { transition, card: folded, committed: async () => folded }
|
|
70
78
|
},
|
|
@@ -125,7 +133,7 @@ describe('write', () => {
|
|
|
125
133
|
const project = await seed({ kind: WorkcardKind.Project, type: PROJECT_TYPE.type, title: 'Shop' })
|
|
126
134
|
await seed({ kind: WorkcardKind.Card, type: STORY_TYPE.type, parent: project.id, title: 'Story', fields: { area: 'user', primary: false } })
|
|
127
135
|
|
|
128
|
-
const model = await facade.model(project) as unknown as
|
|
136
|
+
const model = await facade.model(project) as unknown as ProjectModel
|
|
129
137
|
|
|
130
138
|
expect(await model.summary()).toEqual({ total: 1, planned: 1, 'in-progress': 0, closed: 0 })
|
|
131
139
|
expect((await model.cards()).total).toBe(1)
|
package/tests/query.spec.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { applyQuery } from '@owlmeans/resource'
|
|
3
2
|
import { IntrinsicStatus, WorkcardKind } from '../src/consts.js'
|
|
4
3
|
import { PlanningError } from '../src/errors.js'
|
|
5
|
-
import {
|
|
6
|
-
criteriaOf, decodeSummaryQuery, decodeWorkcardQuery, encodeSummaryQuery, encodeWorkcardQuery, listOptionsOf,
|
|
7
|
-
summaryOf,
|
|
8
|
-
} from '../src/helpers/query.js'
|
|
9
4
|
import type { Workcard, WorkcardQuery } from '../src/types.js'
|
|
10
5
|
import { AT, ENTITY } from './fixtures.js'
|
|
6
|
+
import { queryHelper } from '../src/helpers/query.js'
|
|
7
|
+
import { wireHelper } from '../src/helpers/wire.js'
|
|
8
|
+
import { recordQueryHelper } from '@owlmeans/resource'
|
|
11
9
|
|
|
12
10
|
const card = (id: string, extra: Partial<Workcard>): Workcard => ({
|
|
13
11
|
id, kind: WorkcardKind.Card, type: 'test:story', entityId: ENTITY, title: `Card ${id}`, parent: 'p1',
|
|
@@ -23,11 +21,11 @@ const CARDS = [
|
|
|
23
21
|
]
|
|
24
22
|
|
|
25
23
|
const ids = (query: WorkcardQuery): string[] =>
|
|
26
|
-
applyQuery(CARDS, criteriaOf(query, { entityId: ENTITY }), listOptionsOf(query)).items.map(item => item.id!)
|
|
24
|
+
recordQueryHelper.applyQuery(CARDS, queryHelper.criteriaOf(query, { entityId: ENTITY }), queryHelper.listOptionsOf(query)).items.map(item => item.id!)
|
|
27
25
|
|
|
28
26
|
describe('criteriaOf', () => {
|
|
29
27
|
test('translates the table, always scoped to the entity and omitting what is undefined', () => {
|
|
30
|
-
expect(criteriaOf({
|
|
28
|
+
expect(queryHelper.criteriaOf({
|
|
31
29
|
kind: WorkcardKind.Card, type: ['test:story', 'test:task'], parent: 'p1', within: 'p2', status: undefined,
|
|
32
30
|
labels: ['ui'], ids: ['a'], flow: { id: 'test:story', status: ['planned'] }, fields: { area: 'user' },
|
|
33
31
|
code: 'US-AAA', updatedSince: AT,
|
|
@@ -60,30 +58,30 @@ describe('wire queries', () => {
|
|
|
60
58
|
flow: { id: 'test:story', status: ['planned', 'failed'] }, fields: { area: 'user', primary: true },
|
|
61
59
|
q: 'sign in', page: 1, size: 20, sort: ['order', { field: 'updatedAt', order: 'desc' }],
|
|
62
60
|
}
|
|
63
|
-
const wire = encodeWorkcardQuery(query)
|
|
61
|
+
const wire = wireHelper.encodeWorkcardQuery(query)
|
|
64
62
|
|
|
65
63
|
expect(Object.values(wire).every(value => typeof value === 'string' || typeof value === 'number')).toBe(true)
|
|
66
64
|
expect(wire.sort).toBe('order,-updatedAt')
|
|
67
|
-
expect(criteriaOf(decodeWorkcardQuery(wire), { entityId: ENTITY })).toEqual(criteriaOf(query, { entityId: ENTITY }))
|
|
68
|
-
expect(listOptionsOf(decodeWorkcardQuery(wire))).toEqual({ page: 1, size: 20, sort: ['order', { field: 'updatedAt', order: 'desc' }] })
|
|
65
|
+
expect(queryHelper.criteriaOf(wireHelper.decodeWorkcardQuery(wire), { entityId: ENTITY })).toEqual(queryHelper.criteriaOf(query, { entityId: ENTITY }))
|
|
66
|
+
expect(queryHelper.listOptionsOf(wireHelper.decodeWorkcardQuery(wire))).toEqual({ page: 1, size: 20, sort: ['order', { field: 'updatedAt', order: 'desc' }] })
|
|
69
67
|
})
|
|
70
68
|
|
|
71
69
|
test('decode accepts what a transport may have split already, and coerces numbers', () => {
|
|
72
|
-
expect(decodeWorkcardQuery({ status: ['planned', 'failed'], size: '10', labels: 'ui' } as never))
|
|
70
|
+
expect(wireHelper.decodeWorkcardQuery({ status: ['planned', 'failed'], size: '10', labels: 'ui' } as never))
|
|
73
71
|
.toEqual({ status: ['planned', 'failed'], size: 10, labels: ['ui'] })
|
|
74
|
-
expect(decodeSummaryQuery(encodeSummaryQuery({ parents: ['p1', 'p2'], type: 'test:story' })))
|
|
72
|
+
expect(wireHelper.decodeSummaryQuery(wireHelper.encodeSummaryQuery({ parents: ['p1', 'p2'], type: 'test:story' })))
|
|
75
73
|
.toEqual({ parents: ['p1', 'p2'], type: 'test:story' })
|
|
76
74
|
})
|
|
77
75
|
|
|
78
76
|
test('a value that does not decode is a malformed query', () => {
|
|
79
|
-
expect(() => decodeWorkcardQuery({ fields: '{broken' })).toThrow(PlanningError)
|
|
80
|
-
expect(() => decodeWorkcardQuery({ size: 'many' } as never)).toThrow('malformed:query:size')
|
|
77
|
+
expect(() => wireHelper.decodeWorkcardQuery({ fields: '{broken' })).toThrow(PlanningError)
|
|
78
|
+
expect(() => wireHelper.decodeWorkcardQuery({ size: 'many' } as never)).toThrow('malformed:query:size')
|
|
81
79
|
})
|
|
82
80
|
})
|
|
83
81
|
|
|
84
82
|
describe('summaryOf', () => {
|
|
85
83
|
test('counts direct children per parent by intrinsic state, with no key for a parent without any', () => {
|
|
86
|
-
expect(summaryOf(CARDS.filter(item => item.entityId === ENTITY), ['p1', 'p3'])).toEqual({
|
|
84
|
+
expect(queryHelper.summaryOf(CARDS.filter(item => item.entityId === ENTITY), ['p1', 'p3'])).toEqual({
|
|
87
85
|
p1: { total: 2, planned: 1, 'in-progress': 0, closed: 1 },
|
|
88
86
|
})
|
|
89
87
|
})
|
package/tests/schemas.spec.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import * as schemas from '../src/schemas.js'
|
|
3
3
|
import { TransitionAction, WorkcardKind } from '../src/consts.js'
|
|
4
|
-
import { makeAjv,
|
|
5
|
-
import { applyTransition } from '../src/helpers/apply.js'
|
|
6
|
-
import { computeChanges } from '../src/helpers/changes.js'
|
|
4
|
+
import { makeAjv, validateHelper } from '../src/helpers/validate.js'
|
|
7
5
|
import { FieldsInvalid } from '../src/errors.js'
|
|
8
6
|
import { AT, STORY_TYPE, makeRegistry, transitionOf } from './fixtures.js'
|
|
7
|
+
import { applyHelper } from '../src/helpers/apply.js'
|
|
8
|
+
import { changesHelper } from '../src/helpers/changes.js'
|
|
9
9
|
|
|
10
10
|
type Node = Record<string, unknown>
|
|
11
11
|
|
|
@@ -53,15 +53,15 @@ describe('schemas', () => {
|
|
|
53
53
|
exported.forEach(([, schema]) => ajv.compile(schema as object))
|
|
54
54
|
|
|
55
55
|
const registry = makeRegistry()
|
|
56
|
-
const { changes } = computeChanges(undefined, {
|
|
56
|
+
const { changes } = changesHelper.computeChanges(undefined, {
|
|
57
57
|
action: TransitionAction.Create,
|
|
58
58
|
card: { kind: WorkcardKind.Card, type: STORY_TYPE.type, title: 'Story', fields: { area: 'user', primary: true } },
|
|
59
59
|
}, STORY_TYPE, registry, AT)
|
|
60
|
-
const card = applyTransition(undefined, transitionOf({ card: 'c1', seq: 1, action: TransitionAction.Create, changes }))!
|
|
60
|
+
const card = applyHelper.applyTransition(undefined, transitionOf({ card: 'c1', seq: 1, action: TransitionAction.Create, changes }))!
|
|
61
61
|
|
|
62
|
-
expect(() => validateCard(card, registry)).not.toThrow()
|
|
63
|
-
expect(() => validateCard({ ...card, fields: { area: 'moon', primary: true } }, registry)).toThrow(FieldsInvalid)
|
|
64
|
-
expect(() => validateCard({ ...card, fields: { 'a.b': 1 } }, registry)).toThrow('keys:a.b')
|
|
62
|
+
expect(() => validateHelper.validateCard(card, registry)).not.toThrow()
|
|
63
|
+
expect(() => validateHelper.validateCard({ ...card, fields: { area: 'moon', primary: true } }, registry)).toThrow(FieldsInvalid)
|
|
64
|
+
expect(() => validateHelper.validateCard({ ...card, fields: { 'a.b': 1 } }, registry)).toThrow('keys:a.b')
|
|
65
65
|
})
|
|
66
66
|
|
|
67
67
|
test('an execute request accepts a card id or a create draft, and refuses an unknown action', () => {
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
IntrinsicStatus, PlanningSchemaKind, SchemaOrigin, WorkcardKind,
|
|
4
|
+
} from '../src/consts.js'
|
|
5
|
+
import { PlanningUnsupported, SchemaInvalid, SchemaSealed } from '../src/errors.js'
|
|
6
|
+
import { makeSchemaRegistry } from '../src/registry.js'
|
|
7
|
+
import type {
|
|
8
|
+
ProjectTypeSchema, ScopedSchemaRecord, StatusFlowSchema, WorkcardTypeSchema,
|
|
9
|
+
} from '../src/types.js'
|
|
10
|
+
import { scopedSchemaHelper } from '../src/helpers/scoped.js'
|
|
11
|
+
|
|
12
|
+
/** A community garden: plots are tended through a season flow; a shared tool shed is sealed. */
|
|
13
|
+
const SEASON: StatusFlowSchema = {
|
|
14
|
+
id: 'garden:season',
|
|
15
|
+
version: 1,
|
|
16
|
+
overridable: true,
|
|
17
|
+
statuses: [
|
|
18
|
+
{ key: 'fallow', intrinsic: IntrinsicStatus.Planned, initial: true },
|
|
19
|
+
{ key: 'growing', intrinsic: IntrinsicStatus.InProgress },
|
|
20
|
+
{ key: 'harvested', intrinsic: IntrinsicStatus.Closed },
|
|
21
|
+
],
|
|
22
|
+
transitions: [
|
|
23
|
+
{ name: 'sow', from: ['fallow'], to: 'growing' },
|
|
24
|
+
{ name: 'harvest', from: ['growing'], to: 'harvested' },
|
|
25
|
+
],
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ROTA: StatusFlowSchema = {
|
|
29
|
+
id: 'garden:rota',
|
|
30
|
+
version: 1,
|
|
31
|
+
statuses: [{ key: 'open', intrinsic: IntrinsicStatus.Planned, initial: true }],
|
|
32
|
+
transitions: [],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const PLOT: WorkcardTypeSchema = {
|
|
36
|
+
type: 'garden:plot', kind: WorkcardKind.Card, version: 1, overridable: true,
|
|
37
|
+
fields: { type: 'object' }, flows: [SEASON.id], specifications: [],
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const SHED: WorkcardTypeSchema = {
|
|
41
|
+
type: 'garden:shed', kind: WorkcardKind.Card, version: 1,
|
|
42
|
+
fields: { type: 'object' }, flows: [ROTA.id], specifications: [],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const GARDEN: ProjectTypeSchema = {
|
|
46
|
+
type: 'garden:site', kind: WorkcardKind.Project, version: 1, fields: { type: 'object' },
|
|
47
|
+
flows: [ROTA.id], specifications: [], cardTypes: [PLOT.type], scopedCardTypes: true,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const code = makeSchemaRegistry({ flows: [SEASON, ROTA], types: [PLOT, SHED, GARDEN] })
|
|
51
|
+
|
|
52
|
+
const record = (patch: Partial<ScopedSchemaRecord> & Pick<ScopedSchemaRecord, 'kind' | 'key' | 'definition'>): ScopedSchemaRecord => ({
|
|
53
|
+
entityId: 'org-1', version: 1, createdAt: '2026-09-01T00:00:00.000Z', ...patch,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const BED: WorkcardTypeSchema = {
|
|
57
|
+
type: 'garden:bed', kind: WorkcardKind.Card, version: 1, fields: { type: 'object' }, flows: [SEASON.id], specifications: [],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('scoped schemas — layering', () => {
|
|
61
|
+
test('an organization record adds a type, a project record overrides it for that project only', () => {
|
|
62
|
+
const records = [
|
|
63
|
+
record({ kind: PlanningSchemaKind.Type, key: BED.type, definition: BED }),
|
|
64
|
+
record({ kind: PlanningSchemaKind.Type, key: BED.type, project: 'site-1', version: 3, definition: { ...BED, label: 'Raised bed' } }),
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
const shared = scopedSchemaHelper.scopedRegistryOf(scopedSchemaHelper.resolveScopedBundle(code.bundle(), records, { entityId: 'org-1', revision: 2 }))
|
|
68
|
+
const site = scopedSchemaHelper.scopedRegistryOf(scopedSchemaHelper.resolveScopedBundle(code.bundle(), records, { entityId: 'org-1', project: 'site-1' }))
|
|
69
|
+
const other = scopedSchemaHelper.scopedRegistryOf(scopedSchemaHelper.resolveScopedBundle(code.bundle(), records, { entityId: 'org-2' }))
|
|
70
|
+
|
|
71
|
+
expect(shared.type(BED.type).label).toBeUndefined()
|
|
72
|
+
expect(shared.originOf(PlanningSchemaKind.Type, BED.type)).toBe(SchemaOrigin.Entity)
|
|
73
|
+
expect(shared.revision).toBe(2)
|
|
74
|
+
expect(site.type(BED.type)).toMatchObject({ label: 'Raised bed', version: 3 })
|
|
75
|
+
expect(site.originOf(PlanningSchemaKind.Type, BED.type)).toBe(SchemaOrigin.Project)
|
|
76
|
+
expect(site.originOf(PlanningSchemaKind.Type, PLOT.type)).toBe(SchemaOrigin.Code)
|
|
77
|
+
expect(other.has(BED.type)).toBe(false)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('a sealed code key ignores a record; an overridable one takes it', () => {
|
|
81
|
+
const records = [
|
|
82
|
+
record({ kind: PlanningSchemaKind.Type, key: SHED.type, definition: { ...SHED, label: 'Hijacked' } }),
|
|
83
|
+
record({ kind: PlanningSchemaKind.Type, key: PLOT.type, definition: { ...PLOT, label: 'Allotment' } }),
|
|
84
|
+
record({ kind: PlanningSchemaKind.Type, key: GARDEN.type, definition: { ...BED, type: GARDEN.type } }),
|
|
85
|
+
]
|
|
86
|
+
const view = scopedSchemaHelper.scopedRegistryOf(scopedSchemaHelper.resolveScopedBundle(code.bundle(), records, { entityId: 'org-1' }))
|
|
87
|
+
|
|
88
|
+
expect(view.type(SHED.type).label).toBeUndefined()
|
|
89
|
+
expect(view.type(PLOT.type).label).toBe('Allotment')
|
|
90
|
+
expect(view.type(GARDEN.type).kind).toBe(WorkcardKind.Project)
|
|
91
|
+
expect(() => scopedSchemaHelper.assertOverridable(code, PlanningSchemaKind.Type, SHED.type)).toThrow(SchemaSealed)
|
|
92
|
+
expect(() => scopedSchemaHelper.assertOverridable(code, PlanningSchemaKind.Type, GARDEN.type)).toThrow(SchemaSealed)
|
|
93
|
+
expect(() => scopedSchemaHelper.assertOverridable(code, PlanningSchemaKind.Flow, ROTA.id)).toThrow(SchemaSealed)
|
|
94
|
+
expect(() => scopedSchemaHelper.assertOverridable(code, PlanningSchemaKind.Flow, SEASON.id)).not.toThrow()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('a retired record gives way to a live one below it, and resolves retired when none is left', () => {
|
|
98
|
+
const records = [
|
|
99
|
+
record({ kind: PlanningSchemaKind.Type, key: PLOT.type, retired: true, definition: { ...PLOT, label: 'Old' } }),
|
|
100
|
+
record({ kind: PlanningSchemaKind.Type, key: BED.type, retired: true, definition: BED }),
|
|
101
|
+
]
|
|
102
|
+
const view = scopedSchemaHelper.scopedRegistryOf(scopedSchemaHelper.resolveScopedBundle(code.bundle(), records, { entityId: 'org-1' }))
|
|
103
|
+
|
|
104
|
+
expect(view.type(PLOT.type).label).toBeUndefined()
|
|
105
|
+
expect(view.isRetired(PlanningSchemaKind.Type, PLOT.type)).toBe(false)
|
|
106
|
+
expect(view.type(BED.type).type).toBe(BED.type)
|
|
107
|
+
expect(view.isRetired(PlanningSchemaKind.Type, BED.type)).toBe(true)
|
|
108
|
+
expect(view.bundle().retired).toEqual({ types: [BED.type], flows: [] })
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('the view is read-only', () => {
|
|
112
|
+
const view = scopedSchemaHelper.scopedRegistryOf(scopedSchemaHelper.resolveScopedBundle(code.bundle(), [], { entityId: 'org-1' }))
|
|
113
|
+
|
|
114
|
+
expect(() => view.registerType(BED)).toThrow(PlanningUnsupported)
|
|
115
|
+
expect(() => view.load(code.bundle())).toThrow(PlanningUnsupported)
|
|
116
|
+
expect(view.primaryFlow(PLOT.type).id).toBe(SEASON.id)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('a flow retired with no live fallback is in use while a live type runs it', () => {
|
|
120
|
+
const flow: StatusFlowSchema = { ...ROTA, id: 'garden:watering' }
|
|
121
|
+
const using: WorkcardTypeSchema = { ...BED, type: 'garden:hose', flows: [flow.id] }
|
|
122
|
+
const retiredFlow = record({ kind: PlanningSchemaKind.Flow, key: flow.id, retired: true, definition: flow })
|
|
123
|
+
const live = scopedSchemaHelper.resolveScopedBundle(code.bundle(), [
|
|
124
|
+
retiredFlow, record({ kind: PlanningSchemaKind.Type, key: using.type, definition: using }),
|
|
125
|
+
], { entityId: 'org-1' })
|
|
126
|
+
const idle = scopedSchemaHelper.resolveScopedBundle(code.bundle(), [
|
|
127
|
+
retiredFlow, record({ kind: PlanningSchemaKind.Type, key: using.type, retired: true, definition: using }),
|
|
128
|
+
], { entityId: 'org-1' })
|
|
129
|
+
|
|
130
|
+
expect(scopedSchemaHelper.flowInUse(live, flow.id)).toBe(using.type)
|
|
131
|
+
expect(scopedSchemaHelper.flowInUse(idle, flow.id)).toBeUndefined()
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
describe('scoped schemas — closed-form checks', () => {
|
|
136
|
+
test('a flow needs unique statuses, declared targets and a from that is * or declared', () => {
|
|
137
|
+
expect(() => scopedSchemaHelper.assertFlowSchema(SEASON)).not.toThrow()
|
|
138
|
+
expect(() => scopedSchemaHelper.assertFlowSchema({ ...SEASON, statuses: [...SEASON.statuses, SEASON.statuses[0]] }))
|
|
139
|
+
.toThrow('status-repeated:fallow')
|
|
140
|
+
expect(() => scopedSchemaHelper.assertFlowSchema({ ...SEASON, transitions: [{ name: 'compost', from: '*', to: 'compost' }] }))
|
|
141
|
+
.toThrow('compost:to:compost')
|
|
142
|
+
expect(() => scopedSchemaHelper.assertFlowSchema({ ...SEASON, transitions: [{ name: 'sow', from: ['frozen'], to: 'growing' }] }))
|
|
143
|
+
.toThrow('sow:from:frozen')
|
|
144
|
+
expect(() => scopedSchemaHelper.assertFlowSchema({ ...SEASON, statuses: [] })).toThrow(SchemaInvalid)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('a type must be a card with a compiling fields schema and unique flows that resolve', () => {
|
|
148
|
+
expect(() => scopedSchemaHelper.assertTypeSchema(BED, code)).not.toThrow()
|
|
149
|
+
expect(() => scopedSchemaHelper.assertTypeSchema({ ...BED, kind: WorkcardKind.Specification }, code)).toThrow('kind:specification')
|
|
150
|
+
expect(() => scopedSchemaHelper.assertTypeSchema({ ...BED, fields: { type: 'nonsense' } }, code)).toThrow('fields:')
|
|
151
|
+
expect(() => scopedSchemaHelper.assertTypeSchema({ ...BED, flows: [SEASON.id, SEASON.id] }, code)).toThrow('flow-repeated')
|
|
152
|
+
expect(() => scopedSchemaHelper.assertTypeSchema({ ...BED, flows: ['garden:frost'] }, code)).toThrow('flow:garden:frost')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('a type and a flow may carry a tone and an icon as presentation hints; other keys stay refused', () => {
|
|
156
|
+
expect(() => scopedSchemaHelper.assertTypeSchema({ ...BED, tone: 'green', icon: 'sprout' }, code)).not.toThrow()
|
|
157
|
+
expect(() => scopedSchemaHelper.assertFlowSchema({ ...SEASON, tone: 'amber', icon: 'sun' })).not.toThrow()
|
|
158
|
+
expect(() => scopedSchemaHelper.assertTypeSchema({ ...BED, colour: 'green' } as WorkcardTypeSchema, code)).toThrow(SchemaInvalid)
|
|
159
|
+
expect(() => scopedSchemaHelper.assertFlowSchema({ ...SEASON, tone: 'x'.repeat(65) })).toThrow(SchemaInvalid)
|
|
160
|
+
})
|
|
161
|
+
})
|
package/tests/status.spec.ts
CHANGED
|
@@ -1,55 +1,64 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import { IntrinsicStatus } from '../src/consts.js'
|
|
3
3
|
import { UnknownStatusFlow, UnknownWorkcardType } from '../src/errors.js'
|
|
4
|
-
import {
|
|
5
|
-
canTransit, initialFlowsOf, initialStatusOf, intrinsicOf, resolveIntrinsic, ruleOf, transitionsFrom,
|
|
6
|
-
} from '../src/helpers/status.js'
|
|
7
4
|
import { PROJECT_TYPE, REVIEW_FLOW, STORY_FLOW, STORY_TYPE, TASK_TYPE, makeRegistry } from './fixtures.js'
|
|
5
|
+
import { statusHelper } from '../src/helpers/status.js'
|
|
8
6
|
|
|
9
7
|
const registry = makeRegistry()
|
|
10
8
|
|
|
11
9
|
describe('status flows', () => {
|
|
12
10
|
test('every status maps onto its intrinsic state; the initial one is the marked one', () => {
|
|
13
|
-
expect(intrinsicOf(STORY_FLOW, 'failed')).toBe(IntrinsicStatus.Planned)
|
|
14
|
-
expect(intrinsicOf(STORY_FLOW, 'completed')).toBe(IntrinsicStatus.Closed)
|
|
15
|
-
expect(intrinsicOf(STORY_FLOW, 'unknown')).toBeUndefined()
|
|
16
|
-
expect(initialStatusOf(REVIEW_FLOW)).toBe('pending')
|
|
11
|
+
expect(statusHelper.intrinsicOf(STORY_FLOW, 'failed')).toBe(IntrinsicStatus.Planned)
|
|
12
|
+
expect(statusHelper.intrinsicOf(STORY_FLOW, 'completed')).toBe(IntrinsicStatus.Closed)
|
|
13
|
+
expect(statusHelper.intrinsicOf(STORY_FLOW, 'unknown')).toBeUndefined()
|
|
14
|
+
expect(statusHelper.initialStatusOf(REVIEW_FLOW)).toBe('pending')
|
|
17
15
|
})
|
|
18
16
|
|
|
19
17
|
test('a repeated name picks the rule whose from matches, and a wildcard answers last', () => {
|
|
20
|
-
expect(ruleOf(STORY_FLOW, 'start', 'failed')?.to).toBe('in-progress')
|
|
21
|
-
expect(ruleOf(STORY_FLOW, 'start', 'completed')).toBeUndefined()
|
|
22
|
-
expect(ruleOf(REVIEW_FLOW, 'reopen', 'approved')?.to).toBe('reviewing')
|
|
23
|
-
expect(ruleOf(REVIEW_FLOW, 'reopen', 'reviewing')?.to).toBe('pending')
|
|
18
|
+
expect(statusHelper.ruleOf(STORY_FLOW, 'start', 'failed')?.to).toBe('in-progress')
|
|
19
|
+
expect(statusHelper.ruleOf(STORY_FLOW, 'start', 'completed')).toBeUndefined()
|
|
20
|
+
expect(statusHelper.ruleOf(REVIEW_FLOW, 'reopen', 'approved')?.to).toBe('reviewing')
|
|
21
|
+
expect(statusHelper.ruleOf(REVIEW_FLOW, 'reopen', 'reviewing')?.to).toBe('pending')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('a status the flow does not declare matches every rule of the name, the first declared answering', () => {
|
|
25
|
+
// A card whose flow changed under it: `abandoned` is no longer a status of the flow.
|
|
26
|
+
expect(statusHelper.ruleOf(STORY_FLOW, 'start', 'abandoned')?.to).toBe('in-progress')
|
|
27
|
+
expect(statusHelper.ruleOf(STORY_FLOW, 'complete', 'abandoned')?.to).toBe('completed')
|
|
28
|
+
expect(statusHelper.ruleOf(STORY_FLOW, 'reset', 'abandoned')?.to).toBe('planned')
|
|
29
|
+
expect(statusHelper.ruleOf(STORY_FLOW, 'nope', 'abandoned')).toBeUndefined()
|
|
30
|
+
// A declared status keeps its own rules only.
|
|
31
|
+
expect(statusHelper.ruleOf(STORY_FLOW, 'complete', 'planned')).toBeUndefined()
|
|
32
|
+
expect(statusHelper.transitionsFrom(STORY_FLOW, 'abandoned').map(rule => rule.name)).toEqual(['start', 'complete', 'fail', 'reset'])
|
|
24
33
|
})
|
|
25
34
|
|
|
26
35
|
test('canTransit and transitionsFrom read the same rules, one per name', () => {
|
|
27
|
-
expect(canTransit(STORY_FLOW, 'reset', 'planned')).toBe(true)
|
|
28
|
-
expect(canTransit(STORY_FLOW, 'complete', 'planned')).toBe(false)
|
|
29
|
-
expect(transitionsFrom(STORY_FLOW, 'in-progress').map(rule => rule.name)).toEqual(['complete', 'fail', 'reset'])
|
|
30
|
-
expect(transitionsFrom(STORY_FLOW, 'failed', { explicit: true }).map(rule => `${rule.name}→${rule.to}`))
|
|
36
|
+
expect(statusHelper.canTransit(STORY_FLOW, 'reset', 'planned')).toBe(true)
|
|
37
|
+
expect(statusHelper.canTransit(STORY_FLOW, 'complete', 'planned')).toBe(false)
|
|
38
|
+
expect(statusHelper.transitionsFrom(STORY_FLOW, 'in-progress').map(rule => rule.name)).toEqual(['complete', 'fail', 'reset'])
|
|
39
|
+
expect(statusHelper.transitionsFrom(STORY_FLOW, 'failed', { explicit: true }).map(rule => `${rule.name}→${rule.to}`))
|
|
31
40
|
.toEqual(['start→in-progress', 'reset→planned'])
|
|
32
41
|
})
|
|
33
42
|
})
|
|
34
43
|
|
|
35
44
|
describe('intrinsic resolution', () => {
|
|
36
45
|
test('primary policy reads the primary flow only', () => {
|
|
37
|
-
expect(resolveIntrinsic(STORY_TYPE, { 'test:story': 'in-progress' }, registry)).toBe(IntrinsicStatus.InProgress)
|
|
38
|
-
expect(resolveIntrinsic(PROJECT_TYPE, { 'test:story': 'completed' }, registry)).toBe(IntrinsicStatus.Closed)
|
|
46
|
+
expect(statusHelper.resolveIntrinsic(STORY_TYPE, { 'test:story': 'in-progress' }, registry)).toBe(IntrinsicStatus.InProgress)
|
|
47
|
+
expect(statusHelper.resolveIntrinsic(PROJECT_TYPE, { 'test:story': 'completed' }, registry)).toBe(IntrinsicStatus.Closed)
|
|
39
48
|
})
|
|
40
49
|
|
|
41
50
|
test('IntrinsicPolicy.All takes the least advanced flow', () => {
|
|
42
|
-
expect(resolveIntrinsic(TASK_TYPE, { 'test:story': 'completed', 'test:review': 'reviewing' }, registry))
|
|
51
|
+
expect(statusHelper.resolveIntrinsic(TASK_TYPE, { 'test:story': 'completed', 'test:review': 'reviewing' }, registry))
|
|
43
52
|
.toBe(IntrinsicStatus.InProgress)
|
|
44
|
-
expect(resolveIntrinsic(TASK_TYPE, { 'test:story': 'completed', 'test:review': 'approved' }, registry))
|
|
53
|
+
expect(statusHelper.resolveIntrinsic(TASK_TYPE, { 'test:story': 'completed', 'test:review': 'approved' }, registry))
|
|
45
54
|
.toBe(IntrinsicStatus.Closed)
|
|
46
|
-
expect(resolveIntrinsic(TASK_TYPE, { 'test:story': 'failed', 'test:review': 'approved' }, registry))
|
|
55
|
+
expect(statusHelper.resolveIntrinsic(TASK_TYPE, { 'test:story': 'failed', 'test:review': 'approved' }, registry))
|
|
47
56
|
.toBe(IntrinsicStatus.Planned)
|
|
48
57
|
})
|
|
49
58
|
|
|
50
59
|
test('initialFlowsOf starts every flow at its initial status, the primary at the draft status', () => {
|
|
51
|
-
expect(initialFlowsOf(TASK_TYPE, registry)).toEqual({ 'test:story': 'planned', 'test:review': 'pending' })
|
|
52
|
-
expect(initialFlowsOf(TASK_TYPE, registry, 'failed')).toEqual({ 'test:story': 'failed', 'test:review': 'pending' })
|
|
60
|
+
expect(statusHelper.initialFlowsOf(TASK_TYPE, registry)).toEqual({ 'test:story': 'planned', 'test:review': 'pending' })
|
|
61
|
+
expect(statusHelper.initialFlowsOf(TASK_TYPE, registry, 'failed')).toEqual({ 'test:story': 'failed', 'test:review': 'pending' })
|
|
53
62
|
})
|
|
54
63
|
})
|
|
55
64
|
|