@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
|
@@ -0,0 +1,154 @@
|
|
|
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
|
+
})
|
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
|
|