@owlmeans/viable-common 0.0.19 → 0.0.20
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 +2 -2
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/agent-presentation/SKILL.md +1 -1
- package/agent-meta/skills/viable-common/SKILL.md +195 -10
- package/build/ba/consts.d.ts +2 -3
- package/build/ba/consts.d.ts.map +1 -1
- package/build/ba/consts.js +2 -3
- package/build/ba/consts.js.map +1 -1
- package/build/ba/helpers.d.ts +2 -1
- package/build/ba/helpers.d.ts.map +1 -1
- package/build/ba/helpers.js +3 -2
- package/build/ba/helpers.js.map +1 -1
- package/build/ba/types.d.ts +17 -2
- package/build/ba/types.d.ts.map +1 -1
- package/build/connect/consts.d.ts +5 -8
- package/build/connect/consts.d.ts.map +1 -1
- package/build/connect/consts.js +5 -8
- package/build/connect/consts.js.map +1 -1
- package/build/connect/entrypoints.d.ts +1 -43
- package/build/connect/entrypoints.d.ts.map +1 -1
- package/build/connect/entrypoints.js +1 -18
- package/build/connect/entrypoints.js.map +1 -1
- package/build/connect/index.d.ts +1 -0
- package/build/connect/index.d.ts.map +1 -1
- package/build/connect/index.js +1 -0
- package/build/connect/index.js.map +1 -1
- package/build/connect/jobs.d.ts +25 -0
- package/build/connect/jobs.d.ts.map +1 -0
- package/build/connect/jobs.js +29 -0
- package/build/connect/jobs.js.map +1 -0
- package/build/connect/references.d.ts +1 -40
- package/build/connect/references.d.ts.map +1 -1
- package/build/connect/references.js +0 -8
- package/build/connect/references.js.map +1 -1
- package/build/connect/schemas.d.ts +1 -7
- package/build/connect/schemas.d.ts.map +1 -1
- package/build/connect/schemas.js +1 -25
- package/build/connect/schemas.js.map +1 -1
- package/build/connect/types.d.ts +30 -45
- package/build/connect/types.d.ts.map +1 -1
- package/build/design/consts.d.ts +0 -2
- package/build/design/consts.d.ts.map +1 -1
- package/build/design/consts.js +0 -2
- package/build/design/consts.js.map +1 -1
- package/build/design/helpers.d.ts +6 -2
- package/build/design/helpers.d.ts.map +1 -1
- package/build/design/helpers.js +6 -2
- package/build/design/helpers.js.map +1 -1
- package/build/design/types.d.ts +12 -11
- package/build/design/types.d.ts.map +1 -1
- package/build/execution/types.d.ts +11 -0
- package/build/execution/types.d.ts.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/planning/consts.d.ts +141 -0
- package/build/planning/consts.d.ts.map +1 -0
- package/build/planning/consts.js +170 -0
- package/build/planning/consts.js.map +1 -0
- package/build/planning/errors.d.ts +64 -0
- package/build/planning/errors.d.ts.map +1 -0
- package/build/planning/errors.js +99 -0
- package/build/planning/errors.js.map +1 -0
- package/build/planning/flows.d.ts +34 -0
- package/build/planning/flows.d.ts.map +1 -0
- package/build/planning/flows.js +174 -0
- package/build/planning/flows.js.map +1 -0
- package/build/planning/helpers.d.ts +54 -0
- package/build/planning/helpers.d.ts.map +1 -0
- package/build/planning/helpers.js +101 -0
- package/build/planning/helpers.js.map +1 -0
- package/build/planning/index.d.ts +7 -0
- package/build/planning/index.d.ts.map +1 -0
- package/build/planning/index.js +6 -0
- package/build/planning/index.js.map +1 -0
- package/build/planning/schemas.d.ts +23 -0
- package/build/planning/schemas.d.ts.map +1 -0
- package/build/planning/schemas.js +65 -0
- package/build/planning/schemas.js.map +1 -0
- package/build/planning/types.d.ts +70 -0
- package/build/planning/types.d.ts.map +1 -0
- package/build/planning/types.js +2 -0
- package/build/planning/types.js.map +1 -0
- package/build/scaffold/schemas.d.ts.map +1 -1
- package/build/scaffold/schemas.js +7 -2
- package/build/scaffold/schemas.js.map +1 -1
- package/build/scaffold/types.d.ts +5 -1
- package/build/scaffold/types.d.ts.map +1 -1
- package/package.json +3 -1
- package/src/ba/consts.ts +2 -3
- package/src/ba/helpers.ts +3 -2
- package/src/ba/types.ts +17 -2
- package/src/connect/consts.ts +5 -8
- package/src/connect/entrypoints.ts +2 -41
- package/src/connect/index.ts +1 -0
- package/src/connect/jobs.ts +40 -0
- package/src/connect/references.ts +1 -22
- package/src/connect/schemas.ts +2 -29
- package/src/connect/types.ts +30 -51
- package/src/design/consts.ts +0 -3
- package/src/design/helpers.ts +6 -2
- package/src/design/types.ts +12 -11
- package/src/execution/types.ts +11 -0
- package/src/index.ts +1 -0
- package/src/planning/consts.ts +182 -0
- package/src/planning/errors.ts +117 -0
- package/src/planning/flows.ts +204 -0
- package/src/planning/helpers.ts +133 -0
- package/src/planning/index.ts +6 -0
- package/src/planning/schemas.ts +70 -0
- package/src/planning/types.ts +75 -0
- package/src/scaffold/schemas.ts +7 -2
- package/src/scaffold/types.ts +10 -1
- package/tests/branding.spec.ts +37 -0
- package/tests/connect-entrypoints.spec.ts +35 -3
- package/tests/design.spec.ts +3 -3
- package/tests/planning.spec.ts +356 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import Ajv from 'ajv'
|
|
3
|
+
import addFormats from 'ajv-formats'
|
|
4
|
+
import { ResilientError } from '@owlmeans/error'
|
|
5
|
+
import {
|
|
6
|
+
canTransit, CodeScope, CodeStyle, intrinsicOf, IntrinsicStatus, makeSchemaRegistry,
|
|
7
|
+
SpecificationFormat, transitionsFrom, WorkcardKind,
|
|
8
|
+
} from '@owlmeans/planning'
|
|
9
|
+
import type { Specification } from '@owlmeans/planning'
|
|
10
|
+
import { ProjectArea } from '../src/areas/consts.js'
|
|
11
|
+
import { ConnectingStoryKind, SpecCategory, StoryKind } from '../src/ba/consts.js'
|
|
12
|
+
import { mergeConnectingStories } from '../src/ba/helpers.js'
|
|
13
|
+
import { STORY_DESIGN_VERSION } from '../src/design/consts.js'
|
|
14
|
+
import { emptyStoryDesign } from '../src/design/helpers.js'
|
|
15
|
+
import { StoryActor } from '../src/design/runtime.js'
|
|
16
|
+
import type { StoryDesign } from '../src/design/types.js'
|
|
17
|
+
import {
|
|
18
|
+
isUserChannel, isViableProject, isViableStory, ProjectAgentOccupied, ProjectNotFound,
|
|
19
|
+
ProjectStoryMissconfigured, projectBriefOf, storyDraftOf, storyWriteInputOf, userStoryOf,
|
|
20
|
+
VIABLE_FLOW_SCHEMAS, VIABLE_PROJECT_FLOW, VIABLE_PROJECT_FLOW_SCHEMA, VIABLE_PROJECT_TYPE,
|
|
21
|
+
VIABLE_PROJECT_TYPE_SCHEMA, VIABLE_RESERVED_TYPE_SCHEMAS, VIABLE_SPEC_TYPE, VIABLE_STORY_FLOW,
|
|
22
|
+
VIABLE_STORY_FLOW_SCHEMA,
|
|
23
|
+
VIABLE_STORY_TYPE, VIABLE_STORY_TYPE_SCHEMA, VIABLE_TYPE_SCHEMAS, ViableChannel,
|
|
24
|
+
ViableProjectFieldsSchema, ViableProjectStatus, ViableProjectTransition, ViableRelationship,
|
|
25
|
+
ViableSpecCategory, ViableStoryFieldsSchema, ViableStoryStatus, ViableStoryTransition,
|
|
26
|
+
} from '../src/planning/index.js'
|
|
27
|
+
import type { ViableProjectCard, ViableStoryCard } from '../src/planning/index.js'
|
|
28
|
+
|
|
29
|
+
const at = '2026-09-16T10:00:00.000Z'
|
|
30
|
+
|
|
31
|
+
const storyCard = (extra: Partial<ViableStoryCard> = {}): ViableStoryCard => ({
|
|
32
|
+
id: 'story-card',
|
|
33
|
+
kind: WorkcardKind.Card,
|
|
34
|
+
type: VIABLE_STORY_TYPE,
|
|
35
|
+
entityId: 'entity',
|
|
36
|
+
code: 'US-ABC12',
|
|
37
|
+
title: 'A member books a slot.',
|
|
38
|
+
parent: 'project-card',
|
|
39
|
+
parents: ['project-card'],
|
|
40
|
+
status: ViableStoryStatus.Planned,
|
|
41
|
+
intrinsic: IntrinsicStatus.Planned,
|
|
42
|
+
flows: { [VIABLE_STORY_FLOW]: ViableStoryStatus.Planned },
|
|
43
|
+
labels: [],
|
|
44
|
+
order: 1,
|
|
45
|
+
fields: { area: ProjectArea.User, primary: true },
|
|
46
|
+
seq: 1,
|
|
47
|
+
createdAt: at,
|
|
48
|
+
...extra,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const projectCard = (extra: Partial<ViableProjectCard> = {}): ViableProjectCard => ({
|
|
52
|
+
id: 'project-card',
|
|
53
|
+
kind: WorkcardKind.Project,
|
|
54
|
+
type: VIABLE_PROJECT_TYPE,
|
|
55
|
+
entityId: 'entity',
|
|
56
|
+
code: 'slot-booker',
|
|
57
|
+
title: 'Slot Booker',
|
|
58
|
+
description: 'Book a slot.',
|
|
59
|
+
parents: [],
|
|
60
|
+
status: ViableProjectStatus.Draft,
|
|
61
|
+
intrinsic: IntrinsicStatus.Planned,
|
|
62
|
+
flows: { [VIABLE_PROJECT_FLOW]: ViableProjectStatus.Draft },
|
|
63
|
+
labels: [],
|
|
64
|
+
fields: { language: 'en' },
|
|
65
|
+
seq: 1,
|
|
66
|
+
createdAt: at,
|
|
67
|
+
...extra,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const spec = (category: string, body: string, revision?: number): Specification => ({
|
|
71
|
+
id: `${category}-${revision ?? 0}`,
|
|
72
|
+
kind: WorkcardKind.Specification,
|
|
73
|
+
type: VIABLE_SPEC_TYPE,
|
|
74
|
+
entityId: 'entity',
|
|
75
|
+
title: category,
|
|
76
|
+
parent: 'project-card',
|
|
77
|
+
parents: ['project-card'],
|
|
78
|
+
status: 'current',
|
|
79
|
+
intrinsic: IntrinsicStatus.Closed,
|
|
80
|
+
flows: {},
|
|
81
|
+
labels: [],
|
|
82
|
+
fields: {},
|
|
83
|
+
seq: 1,
|
|
84
|
+
createdAt: at,
|
|
85
|
+
category,
|
|
86
|
+
format: SpecificationFormat.Markdown,
|
|
87
|
+
body,
|
|
88
|
+
...(revision != null ? { revision } : {}),
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const designed = (): StoryDesign => emptyStoryDesign({
|
|
92
|
+
code: 'US-OLD00',
|
|
93
|
+
narrative: 'The narrative the design was written against.',
|
|
94
|
+
area: ProjectArea.Guest,
|
|
95
|
+
entities: [{ name: 'Booking', description: 'A reservation', dir: 'booking' }],
|
|
96
|
+
screens: [{
|
|
97
|
+
name: 'booking/list/overview',
|
|
98
|
+
definition: 'booking-overview',
|
|
99
|
+
path: 'sources/web/src/screens/booking-overview.tsx',
|
|
100
|
+
alias: 'booking-overview',
|
|
101
|
+
url: '/frontoffice/booking-overview',
|
|
102
|
+
parent: 'area-user',
|
|
103
|
+
adopted: false,
|
|
104
|
+
specs: { ux: 'the ux', ui: 'the ui' },
|
|
105
|
+
components: [],
|
|
106
|
+
}],
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('viable-common - the story flow', () => {
|
|
110
|
+
test('keeps the status keys every stored story, selector and frontmatter already carries', () => {
|
|
111
|
+
expect(Object.values(ViableStoryStatus)).toEqual(['planned', 'in-progress', 'completed', 'failed'])
|
|
112
|
+
expect(VIABLE_STORY_FLOW_SCHEMA.statuses.map(status => status.key))
|
|
113
|
+
.toEqual(['planned', 'in-progress', 'completed', 'failed'])
|
|
114
|
+
expect(VIABLE_STORY_FLOW_SCHEMA.statuses.filter(status => status.initial === true)
|
|
115
|
+
.map(status => status.key)).toEqual(['planned'])
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('maps a failed story onto PLANNED, so done is the closed count and a retry is a start', () => {
|
|
119
|
+
const flow = VIABLE_STORY_FLOW_SCHEMA
|
|
120
|
+
|
|
121
|
+
expect(intrinsicOf(flow, ViableStoryStatus.Planned)).toBe(IntrinsicStatus.Planned)
|
|
122
|
+
expect(intrinsicOf(flow, ViableStoryStatus.InProgress)).toBe(IntrinsicStatus.InProgress)
|
|
123
|
+
expect(intrinsicOf(flow, ViableStoryStatus.Completed)).toBe(IntrinsicStatus.Closed)
|
|
124
|
+
expect(intrinsicOf(flow, ViableStoryStatus.Failed)).toBe(IntrinsicStatus.Planned)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('starts from planned or failed only, and resets from anywhere', () => {
|
|
128
|
+
const flow = VIABLE_STORY_FLOW_SCHEMA
|
|
129
|
+
const { Planned, InProgress, Completed, Failed } = ViableStoryStatus
|
|
130
|
+
|
|
131
|
+
expect(canTransit(flow, ViableStoryTransition.Start, Planned)).toBe(true)
|
|
132
|
+
expect(canTransit(flow, ViableStoryTransition.Start, Failed)).toBe(true)
|
|
133
|
+
expect(canTransit(flow, ViableStoryTransition.Start, InProgress)).toBe(false)
|
|
134
|
+
expect(canTransit(flow, ViableStoryTransition.Start, Completed)).toBe(false)
|
|
135
|
+
expect(canTransit(flow, ViableStoryTransition.Complete, InProgress)).toBe(true)
|
|
136
|
+
expect(canTransit(flow, ViableStoryTransition.Fail, InProgress)).toBe(true)
|
|
137
|
+
expect(canTransit(flow, ViableStoryTransition.Fail, Planned)).toBe(false)
|
|
138
|
+
for (const status of Object.values(ViableStoryStatus)) {
|
|
139
|
+
expect(canTransit(flow, ViableStoryTransition.Reset, status)).toBe(true)
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('offers a completed story nothing but a reset', () => {
|
|
144
|
+
expect(transitionsFrom(VIABLE_STORY_FLOW_SCHEMA, ViableStoryStatus.Completed).map(rule => rule.name))
|
|
145
|
+
.toEqual([ViableStoryTransition.Reset])
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe('viable-common - the project flow', () => {
|
|
150
|
+
test('drafts, confirms, activates, and archives from anywhere', () => {
|
|
151
|
+
const flow = VIABLE_PROJECT_FLOW_SCHEMA
|
|
152
|
+
const { Draft, Confirmed, Active, Archived } = ViableProjectStatus
|
|
153
|
+
|
|
154
|
+
expect(flow.statuses.find(status => status.initial === true)?.key).toBe(Draft)
|
|
155
|
+
expect(intrinsicOf(flow, Draft)).toBe(IntrinsicStatus.Planned)
|
|
156
|
+
expect(intrinsicOf(flow, Confirmed)).toBe(IntrinsicStatus.InProgress)
|
|
157
|
+
expect(intrinsicOf(flow, Active)).toBe(IntrinsicStatus.InProgress)
|
|
158
|
+
expect(intrinsicOf(flow, Archived)).toBe(IntrinsicStatus.Closed)
|
|
159
|
+
expect(canTransit(flow, ViableProjectTransition.Confirm, Draft)).toBe(true)
|
|
160
|
+
expect(canTransit(flow, ViableProjectTransition.Confirm, Active)).toBe(false)
|
|
161
|
+
expect(canTransit(flow, ViableProjectTransition.Activate, Confirmed)).toBe(true)
|
|
162
|
+
expect(canTransit(flow, ViableProjectTransition.Archive, Active)).toBe(true)
|
|
163
|
+
expect(canTransit(flow, ViableProjectTransition.Reopen, Archived)).toBe(true)
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('viable-common - the viable types', () => {
|
|
168
|
+
test('register into a planning schema registry and survive the wire bundle a client boots from', () => {
|
|
169
|
+
const registry = makeSchemaRegistry({ types: VIABLE_TYPE_SCHEMAS, flows: VIABLE_FLOW_SCHEMAS })
|
|
170
|
+
const client = makeSchemaRegistry()
|
|
171
|
+
client.load(JSON.parse(JSON.stringify(registry.bundle())))
|
|
172
|
+
|
|
173
|
+
for (const type of VIABLE_TYPE_SCHEMAS) {
|
|
174
|
+
expect(() => client.validator(type.type), type.type).not.toThrow()
|
|
175
|
+
}
|
|
176
|
+
expect(client.primaryFlow(VIABLE_STORY_TYPE).id).toBe(VIABLE_STORY_FLOW)
|
|
177
|
+
expect(client.validator(VIABLE_STORY_TYPE)({ area: ProjectArea.Guest, primary: false })).toBe(true)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('declares every flow a type runs, and carries a tone for every status', () => {
|
|
181
|
+
const flows = new Set(VIABLE_FLOW_SCHEMAS.map(flow => flow.id))
|
|
182
|
+
|
|
183
|
+
for (const type of VIABLE_TYPE_SCHEMAS) {
|
|
184
|
+
expect(type.flows.length).toBeGreaterThan(0)
|
|
185
|
+
for (const id of type.flows) expect(flows.has(id), `${type.type} → ${id}`).toBe(true)
|
|
186
|
+
}
|
|
187
|
+
for (const flow of VIABLE_FLOW_SCHEMAS) {
|
|
188
|
+
for (const status of flow.statuses) expect(status.tone, `${flow.id}.${status.key}`).toBeDefined()
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test('slots the brief and the plan under a project, the design under a story, and no co-located spec', () => {
|
|
193
|
+
const project = VIABLE_PROJECT_TYPE_SCHEMA.specifications
|
|
194
|
+
const story = VIABLE_STORY_TYPE_SCHEMA.specifications
|
|
195
|
+
|
|
196
|
+
expect(project.map(slot => slot.category)).toEqual([
|
|
197
|
+
ViableSpecCategory.Specification, ViableSpecCategory.Vision, ViableSpecCategory.DesignSystem,
|
|
198
|
+
ViableSpecCategory.Scaffold,
|
|
199
|
+
])
|
|
200
|
+
expect(project.find(slot => slot.category === ViableSpecCategory.Specification)?.required).toBe(true)
|
|
201
|
+
expect(project.find(slot => slot.category === ViableSpecCategory.Scaffold))
|
|
202
|
+
.toEqual(expect.objectContaining({ format: SpecificationFormat.Json, revisioned: true, keepRevisions: 3 }))
|
|
203
|
+
expect(story).toEqual([expect.objectContaining({
|
|
204
|
+
category: ViableSpecCategory.Design, format: SpecificationFormat.Json, revisioned: true,
|
|
205
|
+
keepRevisions: 3, version: STORY_DESIGN_VERSION,
|
|
206
|
+
})])
|
|
207
|
+
|
|
208
|
+
const categories = VIABLE_TYPE_SCHEMAS.flatMap(type => type.specifications.map(slot => slot.category))
|
|
209
|
+
for (const coLocated of Object.values(SpecCategory)) expect(categories).not.toContain(coLocated)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('mints US- codes within the project and takes a project code as a slug within the entity', () => {
|
|
213
|
+
expect(VIABLE_STORY_TYPE_SCHEMA.code).toEqual({
|
|
214
|
+
prefix: 'US-', style: CodeStyle.Random, length: 5, uppercase: true, uniqueWithin: CodeScope.Parent,
|
|
215
|
+
})
|
|
216
|
+
expect(VIABLE_PROJECT_TYPE_SCHEMA.code).toEqual({ style: CodeStyle.Slug, uniqueWithin: CodeScope.Entity, mutable: true })
|
|
217
|
+
expect(VIABLE_STORY_TYPE_SCHEMA.code?.mutable).not.toBe(true)
|
|
218
|
+
expect(VIABLE_STORY_TYPE_SCHEMA.relationships?.map(link => link.name)).toEqual([
|
|
219
|
+
ViableRelationship.Follows, ViableRelationship.SharesWidget, ViableRelationship.SharesScreen,
|
|
220
|
+
])
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
test('reserves bug, improvement and requirement on the story flow without letting a project hold one', () => {
|
|
224
|
+
expect(VIABLE_RESERVED_TYPE_SCHEMAS.map(type => type.code?.prefix)).toEqual(['BUG-', 'IMP-', 'REQ-'])
|
|
225
|
+
for (const type of VIABLE_RESERVED_TYPE_SCHEMAS) {
|
|
226
|
+
expect(type.flows).toEqual([VIABLE_STORY_FLOW])
|
|
227
|
+
expect(VIABLE_PROJECT_TYPE_SCHEMA.cardTypes).not.toContain(type.type)
|
|
228
|
+
}
|
|
229
|
+
expect(VIABLE_PROJECT_TYPE_SCHEMA.cardTypes).toEqual([VIABLE_STORY_TYPE])
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
describe('viable-common - the viable field schemas', () => {
|
|
234
|
+
const ajv = addFormats(new Ajv({ allErrors: true, strict: false }))
|
|
235
|
+
const story = ajv.compile(ViableStoryFieldsSchema)
|
|
236
|
+
const project = ajv.compile(ViableProjectFieldsSchema)
|
|
237
|
+
|
|
238
|
+
test('a story accepts its area and flag, and an explicit null for an optional', () => {
|
|
239
|
+
expect(story({ area: ProjectArea.User, primary: false })).toBe(true)
|
|
240
|
+
expect(story({
|
|
241
|
+
area: ProjectArea.Admin, primary: true, actor: StoryActor.Agent, warning: '', kind: StoryKind.Connective,
|
|
242
|
+
})).toBe(true)
|
|
243
|
+
expect(story({ area: ProjectArea.User, primary: false, kind: null, actor: null })).toBe(true)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
test('a story refuses an undeclared field, a missing area, and a value outside a closed set', () => {
|
|
247
|
+
expect(story({ area: ProjectArea.User, primary: false, story: 'narrative' })).toBe(false)
|
|
248
|
+
expect(story({ primary: false })).toBe(false)
|
|
249
|
+
expect(story({ area: 'partner', primary: false })).toBe(false)
|
|
250
|
+
expect(story({ area: ProjectArea.User, primary: false, kind: 'queue' })).toBe(false)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
test('a project accepts none of its fields, all of them, and refuses a stray one', () => {
|
|
254
|
+
expect(project({})).toBe(true)
|
|
255
|
+
expect(project({
|
|
256
|
+
formerAliases: ['old-name'], language: 'en', blueprint: 'owlmeans', blueprintCase: 'web',
|
|
257
|
+
target: null, connectLlmMode: 'local', origin: { kind: 'github', repoUrl: 'https://x/y' },
|
|
258
|
+
})).toBe(true)
|
|
259
|
+
expect(project({ alias: 'slot-booker' })).toBe(false)
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
describe('viable-common - the viable card helpers', () => {
|
|
264
|
+
test('recognises a card by kind AND type', () => {
|
|
265
|
+
expect(isViableStory(storyCard())).toBe(true)
|
|
266
|
+
expect(isViableStory(storyCard({ type: 'notion:task' }))).toBe(false)
|
|
267
|
+
expect(isViableProject(projectCard())).toBe(true)
|
|
268
|
+
expect(isViableProject(storyCard())).toBe(false)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
test('userStoryOf lets the card win on narrative, code, area and actor, and takes screens from the design', () => {
|
|
272
|
+
const card = storyCard({ fields: { area: ProjectArea.User, primary: true, actor: StoryActor.Worker } })
|
|
273
|
+
const story = userStoryOf(card, designed())
|
|
274
|
+
|
|
275
|
+
expect(story.story).toBe(card.title)
|
|
276
|
+
expect(story.code).toBe('US-ABC12')
|
|
277
|
+
expect(story.area).toBe(ProjectArea.User)
|
|
278
|
+
expect(story.actor).toBe(StoryActor.Worker)
|
|
279
|
+
expect(story.screens.map(screen => screen.name)).toEqual(['booking/list/overview'])
|
|
280
|
+
expect(story.entities.map(entity => entity.name)).toEqual(['Booking'])
|
|
281
|
+
expect(userStoryOf(card).screens).toEqual([])
|
|
282
|
+
expect(storyDraftOf(card)).toEqual({ story: card.title, area: ProjectArea.User })
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
test('projectBriefOf reads the highest revision per part and answers an unwritten part empty', () => {
|
|
286
|
+
const brief = projectBriefOf(projectCard(), [
|
|
287
|
+
spec(ViableSpecCategory.Specification, 'first', 1),
|
|
288
|
+
spec(ViableSpecCategory.Specification, 'second', 2),
|
|
289
|
+
spec(ViableSpecCategory.Vision, 'the vision'),
|
|
290
|
+
])
|
|
291
|
+
|
|
292
|
+
expect(brief).toEqual({
|
|
293
|
+
name: 'Slot Booker', alias: 'slot-booker', description: 'Book a slot.', specification: 'second',
|
|
294
|
+
vision: 'the vision', designSystem: '', language: 'en',
|
|
295
|
+
})
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
test('storyWriteInputOf takes code, narrative and primary from the card, and refuses a card with no code', () => {
|
|
299
|
+
const content = {
|
|
300
|
+
userStory: userStoryOf(storyCard()), screenPaths: {}, componentPaths: {}, transitions: [],
|
|
301
|
+
section: 'Booking',
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
expect(storyWriteInputOf(storyCard(), content)).toEqual(expect.objectContaining({
|
|
305
|
+
code: 'US-ABC12', narrative: 'A member books a slot.', primary: true,
|
|
306
|
+
status: ViableStoryStatus.Planned, section: 'Booking',
|
|
307
|
+
}))
|
|
308
|
+
expect(storyWriteInputOf(storyCard(), { ...content, status: ViableStoryStatus.Completed }).status)
|
|
309
|
+
.toBe(ViableStoryStatus.Completed)
|
|
310
|
+
expect(() => storyWriteInputOf(storyCard({ code: undefined }), content))
|
|
311
|
+
.toThrow(ProjectStoryMissconfigured)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
test('only a person writes through the web and connect channels', () => {
|
|
315
|
+
expect(isUserChannel(ViableChannel.Web)).toBe(true)
|
|
316
|
+
expect(isUserChannel(ViableChannel.Connect)).toBe(true)
|
|
317
|
+
expect(isUserChannel(ViableChannel.Pipeline)).toBe(false)
|
|
318
|
+
expect(isUserChannel(undefined)).toBe(false)
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
describe('viable-common - the follows anchor', () => {
|
|
323
|
+
test('mergeConnectingStories carries the clamped anchor on each connective entry', () => {
|
|
324
|
+
const flow = [
|
|
325
|
+
{ story: 'One', area: ProjectArea.User },
|
|
326
|
+
{ story: 'Two', area: ProjectArea.User },
|
|
327
|
+
{ story: 'Three', area: ProjectArea.Admin },
|
|
328
|
+
]
|
|
329
|
+
const merged = mergeConnectingStories(flow, [
|
|
330
|
+
{ story: 'Queue', area: ProjectArea.Admin, kind: ConnectingStoryKind.Queue, after: 9 },
|
|
331
|
+
{ story: 'Tracker', area: ProjectArea.User, kind: ConnectingStoryKind.Tracker, after: 1 },
|
|
332
|
+
])
|
|
333
|
+
|
|
334
|
+
expect(merged.map(({ story, kind, after }) => ({ story, kind, after }))).toEqual([
|
|
335
|
+
{ story: 'One', kind: StoryKind.Flow, after: undefined },
|
|
336
|
+
{ story: 'Tracker', kind: StoryKind.Connective, after: 1 },
|
|
337
|
+
{ story: 'Two', kind: StoryKind.Flow, after: undefined },
|
|
338
|
+
{ story: 'Three', kind: StoryKind.Flow, after: undefined },
|
|
339
|
+
{ story: 'Queue', kind: StoryKind.Connective, after: 3 },
|
|
340
|
+
])
|
|
341
|
+
})
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
describe('viable-common - the project refusals', () => {
|
|
345
|
+
test('keep the type names and markers the browser and the connector match on', () => {
|
|
346
|
+
const occupied = ResilientError.ensure(new ProjectAgentOccupied('story').marshal())
|
|
347
|
+
const refused = ResilientError.ensure(new ProjectStoryMissconfigured('wrong-status').marshal())
|
|
348
|
+
|
|
349
|
+
expect(occupied).toBeInstanceOf(ProjectAgentOccupied)
|
|
350
|
+
expect(occupied.type).toBe('OccupiedAgentViableProjectResilientError')
|
|
351
|
+
expect(occupied.message).toContain('viable-project:agent:occupied:story')
|
|
352
|
+
expect(refused).toBeInstanceOf(ProjectStoryMissconfigured)
|
|
353
|
+
expect(refused.message).toContain('viable-project:story:missconfigured:wrong-status')
|
|
354
|
+
expect(new ProjectNotFound('x').type).toBe('NotFoundViableProjectResourceError')
|
|
355
|
+
})
|
|
356
|
+
})
|