@owlmeans/planning 0.1.18-rc.2 → 0.1.18-rc.21
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 +69 -26
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/planning/SKILL.md +157 -45
- package/build/aliases/types.d.ts +49 -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 +27 -0
- package/build/aliases.js.map +1 -0
- package/build/consts.d.ts +23 -34
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +26 -25
- package/build/consts.js.map +1 -1
- package/build/entrypoints/types.d.ts +96 -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 +17 -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/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 +121 -143
- 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 +234 -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 +12 -0
- package/build/helpers/index.d.ts.map +1 -1
- package/build/helpers/index.js +2 -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/scoped/types.d.ts +59 -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 +24 -0
- package/build/helpers/scoped.d.ts.map +1 -0
- package/build/helpers/scoped.js +197 -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 +30 -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 +74 -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 +215 -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 +56 -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 +5 -4
- package/build/i18n.js.map +1 -1
- package/build/index.d.ts +4 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -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/schemas.d.ts +10 -2
- package/build/schemas.d.ts.map +1 -1
- package/build/schemas.js +87 -1
- package/build/schemas.js.map +1 -1
- package/build/types.d.ts +199 -148
- 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 +11 -11
- package/src/aliases/types.ts +38 -0
- package/src/aliases.ts +31 -0
- package/src/consts.ts +26 -25
- package/src/entrypoints/types.ts +68 -0
- package/src/entrypoints.ts +34 -14
- package/src/errors.ts +71 -0
- package/src/helpers/apply/consts.local.ts +10 -0
- package/src/helpers/apply/types.ts +32 -0
- package/src/helpers/apply.ts +135 -152
- 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 +267 -280
- package/src/helpers/code/types.ts +31 -0
- package/src/helpers/code.ts +52 -48
- package/src/helpers/index.ts +13 -0
- package/src/helpers/query/types.ts +26 -0
- package/src/helpers/query.ts +144 -314
- package/src/helpers/scoped/types.ts +64 -0
- package/src/helpers/scoped.ts +257 -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 +30 -0
- package/src/helpers/validate.ts +84 -77
- package/src/helpers/wire/types.ts +31 -0
- package/src/helpers/wire.ts +251 -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 +56 -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 +6 -4
- package/src/index.ts +4 -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/schemas.ts +98 -2
- package/src/types.ts +207 -126
- package/src/utils.ts +9 -0
- package/tests/apply.spec.ts +95 -31
- package/tests/code.spec.ts +9 -9
- package/tests/entrypoints.spec.ts +18 -2
- package/tests/error-status.spec.ts +6 -1
- package/tests/models.spec.ts +16 -15
- package/tests/query.spec.ts +13 -15
- package/tests/schemas.spec.ts +8 -8
- package/tests/scoped.spec.ts +154 -0
- package/tests/status.spec.ts +31 -22
package/tests/apply.spec.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import { IntrinsicStatus, TransitionAction, WorkcardKind } from '../src/consts.js'
|
|
3
3
|
import { IllegalTransition, PlanningError } from '../src/errors.js'
|
|
4
|
-
import {
|
|
5
|
-
import { assertMutable, computeChanges, isEmptyChange } from '../src/helpers/changes.js'
|
|
6
|
-
import type { Relationship, Specification, Workcard } from '../src/types.js'
|
|
4
|
+
import type { Relationship, Specification, Workcard, WorkcardChanges } from '../src/types.js'
|
|
7
5
|
import {
|
|
8
6
|
AT, ENTITY, LATER, PROJECT_TYPE, SPEC_TYPE, STORY_TYPE, TASK_TYPE, makeRegistry, transitionOf,
|
|
9
7
|
} from './fixtures.js'
|
|
8
|
+
import { applyHelper } from '../src/helpers/apply.js'
|
|
9
|
+
import { changesHelper } from '../src/helpers/changes.js'
|
|
10
10
|
|
|
11
11
|
const registry = makeRegistry()
|
|
12
12
|
|
|
13
13
|
const created = (): Workcard => {
|
|
14
|
-
const { changes } = computeChanges(undefined, {
|
|
14
|
+
const { changes } = changesHelper.computeChanges(undefined, {
|
|
15
15
|
action: TransitionAction.Create,
|
|
16
16
|
card: {
|
|
17
17
|
kind: WorkcardKind.Card, type: STORY_TYPE.type, parent: 'p1', title: 'As a user I sign in',
|
|
@@ -19,7 +19,7 @@ const created = (): Workcard => {
|
|
|
19
19
|
},
|
|
20
20
|
}, STORY_TYPE, registry, AT)
|
|
21
21
|
|
|
22
|
-
return applyTransition(undefined, transitionOf({ card: 'c1', seq: 1, action: TransitionAction.Create, changes }))!
|
|
22
|
+
return applyHelper.applyTransition(undefined, transitionOf({ card: 'c1', seq: 1, action: TransitionAction.Create, changes }))!
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
describe('applyTransition — the fold', () => {
|
|
@@ -38,7 +38,7 @@ describe('applyTransition — the fold', () => {
|
|
|
38
38
|
test('update merges fields, replaces the rest, clears unset — and never mutates its input', () => {
|
|
39
39
|
const card = created()
|
|
40
40
|
const before = structuredClone(card)
|
|
41
|
-
const next = applyTransition(card, transitionOf({
|
|
41
|
+
const next = applyHelper.applyTransition(card, transitionOf({
|
|
42
42
|
card: 'c1', seq: 2, action: TransitionAction.Update, at: LATER,
|
|
43
43
|
changes: { title: 'Renamed', fields: { warning: 'x' }, labels: ['ui'] },
|
|
44
44
|
unset: ['code', 'fields.primary'],
|
|
@@ -55,23 +55,23 @@ describe('applyTransition — the fold', () => {
|
|
|
55
55
|
test('an applied seq returns the card unchanged; a gap or a missing card is refused', () => {
|
|
56
56
|
const card = { ...created(), head: 3 }
|
|
57
57
|
|
|
58
|
-
expect(applyTransition(card, transitionOf({ card: 'c1', seq: 1, action: TransitionAction.Update }))).toBe(card)
|
|
59
|
-
expect(() => applyTransition(card, transitionOf({ card: 'c1', seq: 3, action: TransitionAction.Update })))
|
|
58
|
+
expect(applyHelper.applyTransition(card, transitionOf({ card: 'c1', seq: 1, action: TransitionAction.Update }))).toBe(card)
|
|
59
|
+
expect(() => applyHelper.applyTransition(card, transitionOf({ card: 'c1', seq: 3, action: TransitionAction.Update })))
|
|
60
60
|
.toThrow(PlanningError)
|
|
61
|
-
expect(() => applyTransition(undefined, transitionOf({ card: 'c1', seq: 2, action: TransitionAction.Update })))
|
|
61
|
+
expect(() => applyHelper.applyTransition(undefined, transitionOf({ card: 'c1', seq: 2, action: TransitionAction.Update })))
|
|
62
62
|
.toThrow('fold:out-of-order')
|
|
63
|
-
expect(applyTransition({ ...card, seq: 1 }, transitionOf({ card: 'c1', seq: 2, action: TransitionAction.Link, at: LATER })))
|
|
63
|
+
expect(applyHelper.applyTransition({ ...card, seq: 1 }, transitionOf({ card: 'c1', seq: 2, action: TransitionAction.Link, at: LATER })))
|
|
64
64
|
.toMatchObject({ seq: 2, head: 3, updatedAt: LATER, title: card.title })
|
|
65
65
|
})
|
|
66
66
|
|
|
67
67
|
test('delete answers null, and a revised document carries its new revision', () => {
|
|
68
68
|
const card = created()
|
|
69
|
-
expect(applyTransition(card, transitionOf({ card: 'c1', seq: 2, action: TransitionAction.Delete }))).toBeNull()
|
|
69
|
+
expect(applyHelper.applyTransition(card, transitionOf({ card: 'c1', seq: 2, action: TransitionAction.Delete }))).toBeNull()
|
|
70
70
|
|
|
71
71
|
const spec = { ...card, kind: WorkcardKind.Specification, category: 'design', format: 'json', body: '{}', revision: 1 } as Specification
|
|
72
|
-
const { changes } = computeChanges(spec, { card: 'c1', action: TransitionAction.Update, changes: { body: '{"a":1}' } },
|
|
72
|
+
const { changes } = changesHelper.computeChanges(spec, { card: 'c1', action: TransitionAction.Update, changes: { body: '{"a":1}' } },
|
|
73
73
|
SPEC_TYPE, registry, LATER, { slot: STORY_TYPE.specifications[0] })
|
|
74
|
-
const revised = applyTransition(spec, transitionOf({ card: 'c1', seq: 2, action: TransitionAction.Update, changes })) as Specification
|
|
74
|
+
const revised = applyHelper.applyTransition(spec, transitionOf({ card: 'c1', seq: 2, action: TransitionAction.Update, changes })) as Specification
|
|
75
75
|
|
|
76
76
|
expect([revised.revision, revised.body, revised.bodyChars]).toEqual([2, '{"a":1}', 7])
|
|
77
77
|
})
|
|
@@ -79,7 +79,7 @@ describe('applyTransition — the fold', () => {
|
|
|
79
79
|
|
|
80
80
|
describe('applyRelationship', () => {
|
|
81
81
|
test('a create folds its links, from the card unless a draft names another end', () => {
|
|
82
|
-
const links = applyRelationship([], transitionOf({
|
|
82
|
+
const links = applyHelper.applyRelationship([], transitionOf({
|
|
83
83
|
id: 't1', card: 'c2', seq: 1, action: TransitionAction.Create, project: 'p1',
|
|
84
84
|
links: [{ type: 'follows', to: 'c1' }, { type: 'follows', from: 'c0', to: 'c2' }],
|
|
85
85
|
}))
|
|
@@ -92,12 +92,12 @@ describe('applyRelationship', () => {
|
|
|
92
92
|
|
|
93
93
|
test('link is idempotent and unlink removes only the matching edge', () => {
|
|
94
94
|
const link = transitionOf({ card: 'c2', seq: 2, action: TransitionAction.Link, link: { type: 'follows', to: 'c1' } })
|
|
95
|
-
const once = applyRelationship([], link)
|
|
96
|
-
const twice = applyRelationship(once, link)
|
|
97
|
-
const other = applyRelationship(twice, { ...link, link: { type: 'blocks', to: 'c1' } })
|
|
95
|
+
const once = applyHelper.applyRelationship([], link)
|
|
96
|
+
const twice = applyHelper.applyRelationship(once, link)
|
|
97
|
+
const other = applyHelper.applyRelationship(twice, { ...link, link: { type: 'blocks', to: 'c1' } })
|
|
98
98
|
|
|
99
99
|
expect(twice).toHaveLength(1)
|
|
100
|
-
expect(applyRelationship(other, { ...link, action: TransitionAction.Unlink }).map(edge => edge.type))
|
|
100
|
+
expect(applyHelper.applyRelationship(other, { ...link, action: TransitionAction.Unlink }).map(edge => edge.type))
|
|
101
101
|
.toEqual(['blocks'])
|
|
102
102
|
})
|
|
103
103
|
|
|
@@ -105,7 +105,7 @@ describe('applyRelationship', () => {
|
|
|
105
105
|
const edge = (from: string, to: string): Relationship => ({ entityId: ENTITY, type: 'follows', from, to, createdAt: AT })
|
|
106
106
|
const links = [edge('c1', 'c2'), edge('c3', 'c1'), edge('c3', 'c4')]
|
|
107
107
|
|
|
108
|
-
expect(applyRelationship(links, transitionOf({ card: 'c1', seq: 5, action: TransitionAction.Delete })))
|
|
108
|
+
expect(applyHelper.applyRelationship(links, transitionOf({ card: 'c1', seq: 5, action: TransitionAction.Delete })))
|
|
109
109
|
.toEqual([edge('c3', 'c4')])
|
|
110
110
|
})
|
|
111
111
|
})
|
|
@@ -113,23 +113,23 @@ describe('applyRelationship', () => {
|
|
|
113
113
|
describe('computeChanges', () => {
|
|
114
114
|
test('an update records only what differs, and one that changes nothing is empty', () => {
|
|
115
115
|
const card = created()
|
|
116
|
-
const set = computeChanges(card, {
|
|
116
|
+
const set = changesHelper.computeChanges(card, {
|
|
117
117
|
card: 'c1', action: TransitionAction.Update,
|
|
118
118
|
changes: { title: card.title, description: 'New', fields: { area: 'user', primary: true } },
|
|
119
119
|
}, STORY_TYPE, registry, LATER)
|
|
120
120
|
|
|
121
121
|
expect(set).toEqual({ changes: { description: 'New', fields: { primary: true } }, unset: [] })
|
|
122
|
-
expect(isEmptyChange(computeChanges(card, {
|
|
122
|
+
expect(changesHelper.isEmptyChange(changesHelper.computeChanges(card, {
|
|
123
123
|
card: 'c1', action: TransitionAction.Update, changes: { title: card.title, code: null as never },
|
|
124
124
|
}, STORY_TYPE, registry, LATER))).toBe(false)
|
|
125
|
-
expect(isEmptyChange(computeChanges(card, {
|
|
125
|
+
expect(changesHelper.isEmptyChange(changesHelper.computeChanges(card, {
|
|
126
126
|
card: 'c1', action: TransitionAction.Update, changes: { title: card.title, description: null as never },
|
|
127
127
|
}, STORY_TYPE, registry, LATER))).toBe(true)
|
|
128
128
|
})
|
|
129
129
|
|
|
130
130
|
test('a transit moves the flow, mirrors status and intrinsic, and sets or clears closedAt', () => {
|
|
131
131
|
const card = { ...created(), status: 'in-progress', flows: { 'test:story': 'in-progress' }, intrinsic: IntrinsicStatus.InProgress }
|
|
132
|
-
const completed = computeChanges(card, { card: 'c1', action: TransitionAction.Transit, transition: 'complete' }, STORY_TYPE, registry, LATER)
|
|
132
|
+
const completed = changesHelper.computeChanges(card, { card: 'c1', action: TransitionAction.Transit, transition: 'complete' }, STORY_TYPE, registry, LATER)
|
|
133
133
|
|
|
134
134
|
expect(completed).toEqual({
|
|
135
135
|
changes: { flows: { 'test:story': 'completed' }, status: 'completed', intrinsic: IntrinsicStatus.Closed, closedAt: LATER },
|
|
@@ -137,23 +137,23 @@ describe('computeChanges', () => {
|
|
|
137
137
|
})
|
|
138
138
|
|
|
139
139
|
const closed = { ...card, status: 'completed', flows: { 'test:story': 'completed' }, intrinsic: IntrinsicStatus.Closed, closedAt: LATER }
|
|
140
|
-
expect(computeChanges(closed, { card: 'c1', action: TransitionAction.Transit, transition: 'reset' }, STORY_TYPE, registry, LATER).unset)
|
|
140
|
+
expect(changesHelper.computeChanges(closed, { card: 'c1', action: TransitionAction.Transit, transition: 'reset' }, STORY_TYPE, registry, LATER).unset)
|
|
141
141
|
.toEqual(['closedAt'])
|
|
142
|
-
expect(() => computeChanges(closed, { card: 'c1', action: TransitionAction.Transit, transition: 'start' }, STORY_TYPE, registry, LATER))
|
|
142
|
+
expect(() => changesHelper.computeChanges(closed, { card: 'c1', action: TransitionAction.Transit, transition: 'start' }, STORY_TYPE, registry, LATER))
|
|
143
143
|
.toThrow(IllegalTransition)
|
|
144
144
|
})
|
|
145
145
|
|
|
146
146
|
test('a non-primary transit under IntrinsicPolicy.All keeps the card open until every flow closes', () => {
|
|
147
147
|
const task = { ...created(), type: TASK_TYPE.type, status: 'completed', intrinsic: IntrinsicStatus.Planned,
|
|
148
148
|
flows: { 'test:story': 'completed', 'test:review': 'reviewing' } }
|
|
149
|
-
const set = computeChanges(task, { card: 'c1', action: TransitionAction.Transit, transition: 'approve', flow: 'test:review' },
|
|
149
|
+
const set = changesHelper.computeChanges(task, { card: 'c1', action: TransitionAction.Transit, transition: 'approve', flow: 'test:review' },
|
|
150
150
|
TASK_TYPE, registry, LATER)
|
|
151
151
|
|
|
152
152
|
expect(set.changes).toEqual({ flows: { 'test:review': 'approved' }, intrinsic: IntrinsicStatus.Closed, closedAt: LATER })
|
|
153
153
|
})
|
|
154
154
|
|
|
155
155
|
test('a create of a revisioned document starts at revision 1 with its body length', () => {
|
|
156
|
-
const { changes } = computeChanges(undefined, {
|
|
156
|
+
const { changes } = changesHelper.computeChanges(undefined, {
|
|
157
157
|
action: TransitionAction.Create,
|
|
158
158
|
card: { kind: WorkcardKind.Specification, type: SPEC_TYPE.type, parent: 'p1', title: 'scaffold', category: 'scaffold', body: '{}' },
|
|
159
159
|
}, SPEC_TYPE, registry, AT, { slot: PROJECT_TYPE.specifications[1] })
|
|
@@ -164,13 +164,77 @@ describe('computeChanges', () => {
|
|
|
164
164
|
|
|
165
165
|
describe('assertMutable', () => {
|
|
166
166
|
test('status moves only through transit, a fixed code stays fixed, derived fields are never supplied', () => {
|
|
167
|
-
expect(() => assertMutable({ card: 'c1', action: TransitionAction.Update, changes: { status: 'completed' } }, STORY_TYPE))
|
|
167
|
+
expect(() => changesHelper.assertMutable({ card: 'c1', action: TransitionAction.Update, changes: { status: 'completed' } }, STORY_TYPE))
|
|
168
168
|
.toThrow('immutable:status')
|
|
169
|
-
expect(() => assertMutable({ card: 'c1', action: TransitionAction.Update, changes: { code: 'US-X' } }, STORY_TYPE))
|
|
169
|
+
expect(() => changesHelper.assertMutable({ card: 'c1', action: TransitionAction.Update, changes: { code: 'US-X' } }, STORY_TYPE))
|
|
170
170
|
.toThrow('immutable:code')
|
|
171
|
-
expect(() => assertMutable({ card: 'c1', action: TransitionAction.Update, unset: ['title'] }, STORY_TYPE))
|
|
171
|
+
expect(() => changesHelper.assertMutable({ card: 'c1', action: TransitionAction.Update, unset: ['title'] }, STORY_TYPE))
|
|
172
172
|
.toThrow('immutable:title')
|
|
173
|
-
expect(() => assertMutable({ card: 'c1', action: TransitionAction.Update, changes: { code: 'free' } }, TASK_TYPE))
|
|
173
|
+
expect(() => changesHelper.assertMutable({ card: 'c1', action: TransitionAction.Update, changes: { code: 'free' } }, TASK_TYPE))
|
|
174
174
|
.not.toThrow()
|
|
175
175
|
})
|
|
176
|
+
|
|
177
|
+
test('provenance is never written after the create: createdBy and createdAt in changes or unset, on any action', () => {
|
|
178
|
+
const draft = { kind: WorkcardKind.Card, type: STORY_TYPE.type, parent: 'p1', title: 'Owned', createdBy: 'owner-1' }
|
|
179
|
+
const refused = [
|
|
180
|
+
{ card: 'c1', action: TransitionAction.Update, changes: { createdBy: 'intruder' } },
|
|
181
|
+
{ card: 'c1', action: TransitionAction.Update, changes: { createdBy: null } },
|
|
182
|
+
{ card: 'c1', action: TransitionAction.Update, unset: ['createdBy'] },
|
|
183
|
+
{ card: 'c1', action: TransitionAction.Transit, transition: 'start', changes: { createdBy: 'intruder' } },
|
|
184
|
+
{ card: 'c1', action: TransitionAction.Link, link: { type: 'follows', to: 'c2' }, unset: ['createdBy'] },
|
|
185
|
+
{ card: draft, action: TransitionAction.Create, changes: { createdBy: 'intruder' } },
|
|
186
|
+
] as Parameters<typeof changesHelper.assertMutable>[0][]
|
|
187
|
+
|
|
188
|
+
for (const exec of refused) {
|
|
189
|
+
expect(() => changesHelper.assertMutable(exec, STORY_TYPE)).toThrow('planning:immutable:createdBy')
|
|
190
|
+
}
|
|
191
|
+
expect(() => changesHelper.assertMutable({ card: 'c1', action: TransitionAction.Update, changes: { createdAt: LATER } as WorkcardChanges }, STORY_TYPE))
|
|
192
|
+
.toThrow('planning:immutable:createdAt')
|
|
193
|
+
expect(() => changesHelper.assertMutable({ card: 'c1', action: TransitionAction.Update, unset: ['createdAt'] }, STORY_TYPE))
|
|
194
|
+
.toThrow('planning:immutable:createdAt')
|
|
195
|
+
expect(() => changesHelper.assertMutable({ card: draft, action: TransitionAction.Create }, STORY_TYPE)).not.toThrow()
|
|
196
|
+
expect(() => changesHelper.assertMutable({ card: 'c1', action: TransitionAction.Update, changes: { createdBy: undefined, title: 'T' } }, STORY_TYPE))
|
|
197
|
+
.not.toThrow()
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
describe('createdBy — written by the create alone', () => {
|
|
202
|
+
const owned = (): Workcard => ({ ...created(), createdBy: 'owner-1' })
|
|
203
|
+
|
|
204
|
+
test('computeChanges takes it from the draft and never from changes or unset', () => {
|
|
205
|
+
const create = changesHelper.computeChanges(undefined, {
|
|
206
|
+
action: TransitionAction.Create,
|
|
207
|
+
card: { kind: WorkcardKind.Card, type: STORY_TYPE.type, parent: 'p1', title: 'Owned', createdBy: 'owner-1' },
|
|
208
|
+
changes: { createdBy: 'intruder' },
|
|
209
|
+
}, STORY_TYPE, registry, AT)
|
|
210
|
+
expect(create.changes.createdBy).toBe('owner-1')
|
|
211
|
+
|
|
212
|
+
const update = changesHelper.computeChanges(owned(), {
|
|
213
|
+
card: 'c1', action: TransitionAction.Update, changes: { createdBy: 'intruder', title: 'Renamed' }, unset: ['createdBy'],
|
|
214
|
+
}, STORY_TYPE, registry, LATER)
|
|
215
|
+
expect(update).toEqual({ changes: { title: 'Renamed' }, unset: [] })
|
|
216
|
+
|
|
217
|
+
const cleared = changesHelper.computeChanges(owned(), { card: 'c1', action: TransitionAction.Update, changes: { createdBy: null } as unknown as WorkcardChanges },
|
|
218
|
+
STORY_TYPE, registry, LATER)
|
|
219
|
+
expect(changesHelper.isEmptyChange(cleared)).toBe(true)
|
|
220
|
+
|
|
221
|
+
const transit = changesHelper.computeChanges(owned(), {
|
|
222
|
+
card: 'c1', action: TransitionAction.Transit, transition: 'start', changes: { createdBy: 'intruder' }, unset: ['createdBy'],
|
|
223
|
+
}, STORY_TYPE, registry, LATER)
|
|
224
|
+
expect(transit.changes.createdBy).toBeUndefined()
|
|
225
|
+
expect(transit.unset).not.toContain('createdBy')
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
test('the fold replays a legacy transition that named it, as written — the refusal is at admission', () => {
|
|
229
|
+
const card = owned()
|
|
230
|
+
const renamed = applyHelper.applyTransition(card, transitionOf({
|
|
231
|
+
card: 'c1', seq: 2, action: TransitionAction.Update, at: LATER, changes: { title: 'Legacy', createdBy: 'intruder' },
|
|
232
|
+
}))!
|
|
233
|
+
const cleared = applyHelper.applyTransition(renamed, transitionOf({ card: 'c1', seq: 3, action: TransitionAction.Update, unset: ['createdBy'] }))!
|
|
234
|
+
|
|
235
|
+
expect([renamed.seq, renamed.title, renamed.createdBy, renamed.createdAt]).toEqual([2, 'Legacy', 'intruder', AT])
|
|
236
|
+
expect([cleared.seq, cleared.title, cleared.createdBy]).toEqual([3, 'Legacy', undefined])
|
|
237
|
+
expect(applyHelper.applyTransition(cleared, transitionOf({ card: 'c1', seq: 4, action: TransitionAction.Update, changes: { title: 'After' } }))!
|
|
238
|
+
.title).toBe('After')
|
|
239
|
+
})
|
|
176
240
|
})
|
package/tests/code.spec.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import { CodeScope, CodeStyle } from '../src/consts.js'
|
|
3
3
|
import { CodeTaken } from '../src/errors.js'
|
|
4
|
-
import { codeScopeOf, mintCode } from '../src/helpers/code.js'
|
|
5
4
|
import { STORY_TYPE } from './fixtures.js'
|
|
5
|
+
import { codeHelper } from '../src/helpers/code.js'
|
|
6
6
|
|
|
7
7
|
const free = () => false
|
|
8
8
|
|
|
9
9
|
describe('mintCode', () => {
|
|
10
10
|
test('random: prefix plus an upper-cased Base58 part of the policy length', async () => {
|
|
11
|
-
const code = await mintCode(STORY_TYPE.code!, free)
|
|
11
|
+
const code = await codeHelper.mintCode(STORY_TYPE.code!, free)
|
|
12
12
|
|
|
13
13
|
expect(code).toMatch(/^US-[1-9A-Z]{5}$/)
|
|
14
14
|
})
|
|
@@ -17,24 +17,24 @@ describe('mintCode', () => {
|
|
|
17
17
|
const policy = { style: CodeStyle.Slug, uniqueWithin: CodeScope.Entity }
|
|
18
18
|
const taken = new Set(['shop-dashboard', 'shop-dashboard-2'])
|
|
19
19
|
|
|
20
|
-
expect(await mintCode(policy, free, 8, { seed: 'Shop Dashboard!' })).toBe('shop-dashboard')
|
|
21
|
-
expect(await mintCode(policy, code => taken.has(code), 8, { seed: 'Shop Dashboard!' })).toBe('shop-dashboard-3')
|
|
22
|
-
expect(await mintCode(policy, free)).toMatch(/^[a-z]+-[a-z]+$/)
|
|
20
|
+
expect(await codeHelper.mintCode(policy, free, 8, { seed: 'Shop Dashboard!' })).toBe('shop-dashboard')
|
|
21
|
+
expect(await codeHelper.mintCode(policy, code => taken.has(code), 8, { seed: 'Shop Dashboard!' })).toBe('shop-dashboard-3')
|
|
22
|
+
expect(await codeHelper.mintCode(policy, free)).toMatch(/^[a-z]+-[a-z]+$/)
|
|
23
23
|
})
|
|
24
24
|
|
|
25
25
|
test('sequential: counts on from the scope, zero-padded to the length', async () => {
|
|
26
26
|
const policy = { prefix: 'REQ-', style: CodeStyle.Sequential, length: 3, uniqueWithin: CodeScope.Parent }
|
|
27
27
|
|
|
28
|
-
expect(await mintCode(policy, free, 8, { count: 41 })).toBe('REQ-042')
|
|
29
|
-
expect(await mintCode(policy, async code => code === 'REQ-042', 8, { count: 41 })).toBe('REQ-043')
|
|
28
|
+
expect(await codeHelper.mintCode(policy, free, 8, { count: 41 })).toBe('REQ-042')
|
|
29
|
+
expect(await codeHelper.mintCode(policy, async code => code === 'REQ-042', 8, { count: 41 })).toBe('REQ-043')
|
|
30
30
|
})
|
|
31
31
|
|
|
32
32
|
test('refuses with CodeTaken after the attempt cap, asking exactly that many times', async () => {
|
|
33
33
|
let asked = 0
|
|
34
34
|
const always = async () => { asked++; return true }
|
|
35
35
|
|
|
36
|
-
await expect(mintCode(STORY_TYPE.code!, always, 3)).rejects.toThrow(CodeTaken)
|
|
36
|
+
await expect(codeHelper.mintCode(STORY_TYPE.code!, always, 3)).rejects.toThrow(CodeTaken)
|
|
37
37
|
expect(asked).toBe(3)
|
|
38
|
-
expect(codeScopeOf(STORY_TYPE.code!, { parent: 'p1' })).toEqual({ parent: 'p1' })
|
|
38
|
+
expect(codeHelper.codeScopeOf(STORY_TYPE.code!, { parent: 'p1' })).toEqual({ parent: 'p1' })
|
|
39
39
|
})
|
|
40
40
|
})
|
|
@@ -2,14 +2,15 @@ import { describe, expect, test } from 'bun:test'
|
|
|
2
2
|
import { protocols } from '@owlmeans/entrypoint'
|
|
3
3
|
import { RouteMethod, RouteProtocols } from '@owlmeans/route'
|
|
4
4
|
import { makePlanningProtocols } from '../src/entrypoints.js'
|
|
5
|
-
import { PLANNING_PATH
|
|
5
|
+
import { PLANNING_PATH } from '../src/consts.js'
|
|
6
|
+
import { planningAliasHelper } from '../src/aliases.js'
|
|
6
7
|
|
|
7
8
|
const BASE = 'app:api:planning'
|
|
8
9
|
|
|
9
10
|
describe('makePlanningProtocols', () => {
|
|
10
11
|
test('declares every leaf once, each alias derived from the base alias', () => {
|
|
11
12
|
const tree = makePlanningProtocols({ base: { alias: BASE }, guards: 'guard:default' })
|
|
12
|
-
const aliases = planningAliases(BASE)
|
|
13
|
+
const aliases = planningAliasHelper.planningAliases(BASE)
|
|
13
14
|
const declared = protocols(tree as never).map(protocol => protocol.alias)
|
|
14
15
|
|
|
15
16
|
expect(new Set(declared).size).toBe(declared.length)
|
|
@@ -46,6 +47,21 @@ describe('makePlanningProtocols', () => {
|
|
|
46
47
|
expect(tree.card.list.contract?.requestSchemas.query).toBeDefined()
|
|
47
48
|
})
|
|
48
49
|
|
|
50
|
+
test('definitions add the scoped schema list and the schema write; without them neither exists', () => {
|
|
51
|
+
const plain = makePlanningProtocols({ base: { alias: BASE }, guards: 'guard:default' })
|
|
52
|
+
const scoped = makePlanningProtocols({ base: { alias: BASE }, guards: 'guard:default', definitions: true })
|
|
53
|
+
const define = planningAliasHelper.planningDefinitionAliases(BASE).define
|
|
54
|
+
|
|
55
|
+
expect(plain.schema.define).toBeUndefined()
|
|
56
|
+
expect(plain.schema.list.contract?.requestSchemas.query).toBeUndefined()
|
|
57
|
+
expect(protocols(plain as never).some(protocol => protocol.alias === define)).toBe(false)
|
|
58
|
+
|
|
59
|
+
expect(scoped.schema.define?.alias).toBe(define)
|
|
60
|
+
expect(scoped.schema.define?.route.route).toMatchObject({ path: '/schemas', method: RouteMethod.POST, parent: BASE })
|
|
61
|
+
expect(scoped.schema.list.contract?.requestSchemas.query).toBeDefined()
|
|
62
|
+
expect(protocols(scoped as never).filter(protocol => protocol.alias === define)).toHaveLength(1)
|
|
63
|
+
})
|
|
64
|
+
|
|
49
65
|
test('the commit feed is a socket under socketBase when given, under the planning base otherwise', () => {
|
|
50
66
|
const own = makePlanningProtocols({ base: { alias: BASE }, guards: 'guard:default' })
|
|
51
67
|
const shared = makePlanningProtocols({ base: { alias: BASE }, guards: 'guard:default', socketBase: 'app:update' })
|
|
@@ -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
3
|
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'
|
|
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
|
|
@@ -28,14 +29,14 @@ const makeFacade = () => {
|
|
|
28
29
|
cards: {
|
|
29
30
|
get: async id => cards.get(id) ?? Promise.reject(new WorkcardNotFound(id)),
|
|
30
31
|
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,
|
|
32
|
+
list: async query => recordQueryHelper.applyQuery([...cards.values()], queryHelper.criteriaOf(query, facade.scope)),
|
|
33
|
+
count: async query => recordQueryHelper.applyQuery([...cards.values()], queryHelper.criteriaOf(query, facade.scope)).total,
|
|
33
34
|
summary: async (parents, query) =>
|
|
34
|
-
summaryOf(applyQuery([...cards.values()], criteriaOf(query, facade.scope)).items, parents),
|
|
35
|
+
queryHelper.summaryOf(recordQueryHelper.applyQuery([...cards.values()], queryHelper.criteriaOf(query, facade.scope)).items, parents),
|
|
35
36
|
},
|
|
36
37
|
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,
|
|
38
|
+
current: async (parent, category) => specificationHelper.currentSpecification([...cards.values()].filter(card => card.parent === parent), category),
|
|
39
|
+
list: async parent => recordQueryHelper.applyQuery([...cards.values()], { parent, kind: WorkcardKind.Specification }) as never,
|
|
39
40
|
get: async id => cards.get(id) as Specification,
|
|
40
41
|
revisions: async () => [],
|
|
41
42
|
},
|
|
@@ -53,18 +54,18 @@ const makeFacade = () => {
|
|
|
53
54
|
const type = schemas.type(card?.type ?? draft!.type)
|
|
54
55
|
const parent = cards.get(card?.parent ?? draft?.parent ?? '')
|
|
55
56
|
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 })
|
|
57
|
+
const slot = parent != null && category != null ? specificationHelper.slotOf(schemas.type(parent.type), category) : undefined
|
|
58
|
+
const set = changesHelper.computeChanges(card, exec, type, schemas, AT, { slot })
|
|
58
59
|
const id = card?.id ?? `card-${++next}`
|
|
59
60
|
const transition = {
|
|
60
61
|
id: `t-${++next}`, entityId: ENTITY, card: id, kind: card?.kind ?? draft!.kind, type: type.type,
|
|
61
62
|
seq: (card?.head ?? card?.seq ?? 0) + 1, action: exec.action, changes: set.changes, unset: set.unset,
|
|
62
63
|
actor: {}, at: AT, commit: { state: CommitState.Committed },
|
|
63
64
|
}
|
|
64
|
-
if (exec.action === TransitionAction.Update && isEmptyChange(set)) {
|
|
65
|
+
if (exec.action === TransitionAction.Update && changesHelper.isEmptyChange(set)) {
|
|
65
66
|
return { transition, card, committed: async () => card ?? null }
|
|
66
67
|
}
|
|
67
|
-
const folded = applyTransition(card, transition)
|
|
68
|
+
const folded = applyHelper.applyTransition(card, transition)
|
|
68
69
|
folded == null ? cards.delete(id) : cards.set(id, folded)
|
|
69
70
|
return { transition, card: folded, committed: async () => folded }
|
|
70
71
|
},
|
|
@@ -125,7 +126,7 @@ describe('write', () => {
|
|
|
125
126
|
const project = await seed({ kind: WorkcardKind.Project, type: PROJECT_TYPE.type, title: 'Shop' })
|
|
126
127
|
await seed({ kind: WorkcardKind.Card, type: STORY_TYPE.type, parent: project.id, title: 'Story', fields: { area: 'user', primary: false } })
|
|
127
128
|
|
|
128
|
-
const model = await facade.model(project) as unknown as
|
|
129
|
+
const model = await facade.model(project) as unknown as ProjectModel
|
|
129
130
|
|
|
130
131
|
expect(await model.summary()).toEqual({ total: 1, planned: 1, 'in-progress': 0, closed: 0 })
|
|
131
132
|
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', () => {
|