@pennyfarthing/core 7.7.0 → 7.8.0
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 +1 -1
- package/package.json +1 -1
- package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/doctor.js +114 -0
- package/packages/core/dist/cli/commands/doctor.js.map +1 -1
- package/pennyfarthing-dist/agents/sm-setup.md +37 -2
- package/pennyfarthing-dist/agents/sm.md +68 -22
- package/pennyfarthing-dist/agents/workflow-status-check.md +11 -1
- package/pennyfarthing-dist/commands/git-cleanup.md +43 -308
- package/pennyfarthing-dist/commands/solo.md +31 -0
- package/pennyfarthing-dist/guides/patterns/approval-gates-pattern.md +1 -1
- package/pennyfarthing-dist/personas/themes/gilligans-island.yaml +83 -83
- package/pennyfarthing-dist/personas/themes/the-expanse.yaml +11 -11
- package/pennyfarthing-dist/scripts/core/check-context.sh +3 -0
- package/pennyfarthing-dist/scripts/core/handoff-marker.sh +13 -2
- package/pennyfarthing-dist/scripts/core/prime.sh +3 -157
- package/pennyfarthing-dist/scripts/core/run.sh +9 -0
- package/pennyfarthing-dist/scripts/hooks/__pycache__/question_reflector_check.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +117 -20
- package/pennyfarthing-dist/scripts/jira/README.md +10 -7
- package/pennyfarthing-dist/scripts/misc/add-short-names.sh +13 -0
- package/pennyfarthing-dist/scripts/misc/add_short_names.py +226 -0
- package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +6 -5
- package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +319 -0
- package/pennyfarthing-dist/scripts/sprint/import-epic-to-future.sh +6 -5
- package/pennyfarthing-dist/scripts/sprint/import_epic_to_future.py +270 -0
- package/pennyfarthing-dist/scripts/test/ensure-swebench-data.sh +59 -0
- package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.sh +8 -6
- package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +402 -0
- package/pennyfarthing-dist/scripts/workflow/check.sh +3 -476
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +61 -0
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +13 -0
- package/pennyfarthing-dist/skills/judge/SKILL.md +57 -0
- package/pennyfarthing-dist/skills/sprint/scripts/sync-epic-jira.sh +4 -22
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-01-analyze.md +83 -0
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-02-categorize.md +116 -0
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-03-execute.md +210 -0
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-04-verify.md +88 -0
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +71 -0
- package/pennyfarthing-dist/workflows/git-cleanup.yaml +59 -0
- package/pennyfarthing-dist/scripts/hooks/question-reflector-check.mjs +0 -393
- package/pennyfarthing-dist/scripts/hooks/tests/question-reflector.test.mjs +0 -545
- package/pennyfarthing-dist/scripts/jira/jira-bidirectional-sync.mjs +0 -327
- package/pennyfarthing-dist/scripts/jira/jira-bidirectional-sync.test.mjs +0 -503
- package/pennyfarthing-dist/scripts/jira/jira-lib.mjs +0 -443
- package/pennyfarthing-dist/scripts/jira/jira-sync-story.mjs +0 -208
- package/pennyfarthing-dist/scripts/jira/jira-sync.mjs +0 -198
- package/pennyfarthing-dist/scripts/misc/add-short-names.mjs +0 -264
- package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.mjs +0 -474
- package/pennyfarthing-dist/scripts/sprint/import-epic-to-future.mjs +0 -377
- package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.js +0 -492
- /package/pennyfarthing-dist/guides/{AGENT-COORDINATION.md → agent-coordination.md} +0 -0
- /package/pennyfarthing-dist/guides/{HOOKS.md → hooks.md} +0 -0
- /package/pennyfarthing-dist/guides/{PROMPT-PATTERNS.md → prompt-patterns.md} +0 -0
- /package/pennyfarthing-dist/guides/{SESSION-ARTIFACTS.md → session-artifacts.md} +0 -0
- /package/pennyfarthing-dist/guides/{XML-TAGS.md → xml-tags.md} +0 -0
|
@@ -1,503 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* jira-bidirectional-sync.test.mjs - Tests for bidirectional Jira sync
|
|
4
|
-
*
|
|
5
|
-
* Story: MSSCI-11842
|
|
6
|
-
* TDD Phase: RED
|
|
7
|
-
*
|
|
8
|
-
* Run with: node --test pennyfarthing-dist/scripts/utils/jira/jira-bidirectional-sync.test.mjs
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
|
|
12
|
-
import assert from 'node:assert';
|
|
13
|
-
|
|
14
|
-
// The module under test (will be created by Dev)
|
|
15
|
-
// import { ... } from './jira-bidirectional-sync.mjs';
|
|
16
|
-
|
|
17
|
-
// For now, import from jira-lib.mjs to test helper functions
|
|
18
|
-
import {
|
|
19
|
-
mapStatusToJira,
|
|
20
|
-
mapJiraToStatus,
|
|
21
|
-
extractJiraKey
|
|
22
|
-
} from './jira-lib.mjs';
|
|
23
|
-
|
|
24
|
-
// =============================================================================
|
|
25
|
-
// Test Fixtures
|
|
26
|
-
// =============================================================================
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Sample YAML story data (as parsed from sprint YAML)
|
|
30
|
-
*/
|
|
31
|
-
const sampleYamlStories = [
|
|
32
|
-
{
|
|
33
|
-
id: 'MSSCI-11842',
|
|
34
|
-
title: 'Bidirectional sync script',
|
|
35
|
-
status: 'in_progress',
|
|
36
|
-
points: 4,
|
|
37
|
-
priority: 'P2'
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
id: 'MSSCI-11843',
|
|
41
|
-
title: 'Document Jira auto-creation',
|
|
42
|
-
status: 'backlog',
|
|
43
|
-
points: 2,
|
|
44
|
-
priority: 'P2'
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
id: 'MSSCI-11844',
|
|
48
|
-
title: 'YAML-only story',
|
|
49
|
-
status: 'backlog',
|
|
50
|
-
points: 3,
|
|
51
|
-
priority: 'P3'
|
|
52
|
-
}
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Sample Jira story data (as returned from Jira API)
|
|
57
|
-
*/
|
|
58
|
-
const sampleJiraStories = [
|
|
59
|
-
{
|
|
60
|
-
key: 'MSSCI-11842',
|
|
61
|
-
fields: {
|
|
62
|
-
summary: 'Bidirectional sync script',
|
|
63
|
-
status: { name: 'In Progress' },
|
|
64
|
-
customfield_10031: 4, // Story points
|
|
65
|
-
priority: { name: 'Medium' }
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
key: 'MSSCI-11843',
|
|
70
|
-
fields: {
|
|
71
|
-
summary: 'Document Jira auto-creation',
|
|
72
|
-
status: { name: 'Done' }, // Different from YAML!
|
|
73
|
-
customfield_10031: 2,
|
|
74
|
-
priority: { name: 'Medium' }
|
|
75
|
-
}
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
key: 'MSSCI-11850',
|
|
79
|
-
fields: {
|
|
80
|
-
summary: 'Jira-only story',
|
|
81
|
-
status: { name: 'To Do' },
|
|
82
|
-
customfield_10031: 5,
|
|
83
|
-
priority: { name: 'High' }
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
];
|
|
87
|
-
|
|
88
|
-
// =============================================================================
|
|
89
|
-
// AC1: Script syncs status changes both directions
|
|
90
|
-
// =============================================================================
|
|
91
|
-
|
|
92
|
-
describe('AC1: Bidirectional status sync', () => {
|
|
93
|
-
|
|
94
|
-
describe('YAML → Jira direction', () => {
|
|
95
|
-
|
|
96
|
-
it('should detect when YAML status differs from Jira status', async () => {
|
|
97
|
-
// Import the function that will be implemented
|
|
98
|
-
const { generateSyncPlan } = await import('./jira-bidirectional-sync.mjs');
|
|
99
|
-
|
|
100
|
-
const plan = generateSyncPlan(sampleYamlStories, sampleJiraStories, {
|
|
101
|
-
syncStatus: true
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// MSSCI-11843: YAML=backlog, Jira=Done
|
|
105
|
-
const story11843 = plan.changes.find(c => c.key === 'MSSCI-11843');
|
|
106
|
-
assert.ok(story11843, 'Should detect status difference for MSSCI-11843');
|
|
107
|
-
assert.strictEqual(story11843.yamlStatus, 'backlog');
|
|
108
|
-
assert.strictEqual(story11843.jiraStatus, 'Done');
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('should generate Jira update action when YAML is source of truth', async () => {
|
|
112
|
-
const { generateSyncPlan } = await import('./jira-bidirectional-sync.mjs');
|
|
113
|
-
|
|
114
|
-
const plan = generateSyncPlan(sampleYamlStories, sampleJiraStories, {
|
|
115
|
-
syncStatus: true,
|
|
116
|
-
direction: 'yaml-to-jira'
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
const story11843 = plan.changes.find(c => c.key === 'MSSCI-11843');
|
|
120
|
-
assert.ok(story11843);
|
|
121
|
-
assert.strictEqual(story11843.action, 'update-jira');
|
|
122
|
-
assert.strictEqual(story11843.targetStatus, 'To Do'); // backlog maps to To Do
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
describe('Jira → YAML direction', () => {
|
|
128
|
-
|
|
129
|
-
it('should generate YAML update action when Jira is source of truth', async () => {
|
|
130
|
-
const { generateSyncPlan } = await import('./jira-bidirectional-sync.mjs');
|
|
131
|
-
|
|
132
|
-
const plan = generateSyncPlan(sampleYamlStories, sampleJiraStories, {
|
|
133
|
-
syncStatus: true,
|
|
134
|
-
direction: 'jira-to-yaml'
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
const story11843 = plan.changes.find(c => c.key === 'MSSCI-11843');
|
|
138
|
-
assert.ok(story11843);
|
|
139
|
-
assert.strictEqual(story11843.action, 'update-yaml');
|
|
140
|
-
assert.strictEqual(story11843.targetStatus, 'done'); // Done maps to done
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('should correctly map Jira status to YAML status', () => {
|
|
144
|
-
// Test existing helper function
|
|
145
|
-
assert.strictEqual(mapJiraToStatus('To Do'), 'backlog');
|
|
146
|
-
assert.strictEqual(mapJiraToStatus('In Progress'), 'in-progress');
|
|
147
|
-
assert.strictEqual(mapJiraToStatus('Done'), 'done');
|
|
148
|
-
assert.strictEqual(mapJiraToStatus('In Review'), 'review');
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
describe('Bidirectional conflict detection', () => {
|
|
154
|
-
|
|
155
|
-
it('should detect conflicts when both systems changed', async () => {
|
|
156
|
-
const { generateSyncPlan } = await import('./jira-bidirectional-sync.mjs');
|
|
157
|
-
|
|
158
|
-
// Simulate scenario where timestamps indicate both changed
|
|
159
|
-
const plan = generateSyncPlan(sampleYamlStories, sampleJiraStories, {
|
|
160
|
-
syncStatus: true,
|
|
161
|
-
direction: 'bidirectional',
|
|
162
|
-
lastSyncTime: new Date('2026-01-17T00:00:00Z')
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
const conflicts = plan.conflicts || [];
|
|
166
|
-
// Should identify potential conflicts for manual resolution
|
|
167
|
-
assert.ok(Array.isArray(conflicts));
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('should use Jira as default winner on conflict', async () => {
|
|
171
|
-
const { generateSyncPlan } = await import('./jira-bidirectional-sync.mjs');
|
|
172
|
-
|
|
173
|
-
const plan = generateSyncPlan(sampleYamlStories, sampleJiraStories, {
|
|
174
|
-
syncStatus: true,
|
|
175
|
-
direction: 'bidirectional'
|
|
176
|
-
// No yamlWins flag, so Jira should win
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// Default: Jira wins conflicts
|
|
180
|
-
const story11843 = plan.changes.find(c => c.key === 'MSSCI-11843');
|
|
181
|
-
if (story11843) {
|
|
182
|
-
assert.strictEqual(story11843.action, 'update-yaml');
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('should use YAML as winner when --yaml-wins flag is set', async () => {
|
|
187
|
-
const { generateSyncPlan } = await import('./jira-bidirectional-sync.mjs');
|
|
188
|
-
|
|
189
|
-
const plan = generateSyncPlan(sampleYamlStories, sampleJiraStories, {
|
|
190
|
-
syncStatus: true,
|
|
191
|
-
direction: 'bidirectional',
|
|
192
|
-
yamlWins: true
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
const story11843 = plan.changes.find(c => c.key === 'MSSCI-11843');
|
|
196
|
-
if (story11843) {
|
|
197
|
-
assert.strictEqual(story11843.action, 'update-jira');
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
// =============================================================================
|
|
206
|
-
// AC2: Points updated in Jira match sprint YAML
|
|
207
|
-
// =============================================================================
|
|
208
|
-
|
|
209
|
-
describe('AC2: Story points sync', () => {
|
|
210
|
-
|
|
211
|
-
it('should detect when YAML points differ from Jira points', async () => {
|
|
212
|
-
const { generateSyncPlan } = await import('./jira-bidirectional-sync.mjs');
|
|
213
|
-
|
|
214
|
-
// Create test data with mismatched points
|
|
215
|
-
const yamlWithDifferentPoints = [
|
|
216
|
-
{ id: 'MSSCI-11842', status: 'in_progress', points: 5 } // YAML says 5
|
|
217
|
-
];
|
|
218
|
-
const jiraWithDifferentPoints = [
|
|
219
|
-
{ key: 'MSSCI-11842', fields: { status: { name: 'In Progress' }, customfield_10031: 4 } } // Jira says 4
|
|
220
|
-
];
|
|
221
|
-
|
|
222
|
-
const plan = generateSyncPlan(yamlWithDifferentPoints, jiraWithDifferentPoints, {
|
|
223
|
-
syncPoints: true
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
const pointsChange = plan.changes.find(c => c.key === 'MSSCI-11842' && c.field === 'points');
|
|
227
|
-
assert.ok(pointsChange, 'Should detect points difference');
|
|
228
|
-
assert.strictEqual(pointsChange.yamlPoints, 5);
|
|
229
|
-
assert.strictEqual(pointsChange.jiraPoints, 4);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it('should sync points from YAML to Jira', async () => {
|
|
233
|
-
const { generateSyncPlan } = await import('./jira-bidirectional-sync.mjs');
|
|
234
|
-
|
|
235
|
-
const yamlStories = [{ id: 'MSSCI-11842', status: 'backlog', points: 8 }];
|
|
236
|
-
const jiraStories = [{ key: 'MSSCI-11842', fields: { status: { name: 'To Do' }, customfield_10031: 5 } }];
|
|
237
|
-
|
|
238
|
-
const plan = generateSyncPlan(yamlStories, jiraStories, {
|
|
239
|
-
syncPoints: true,
|
|
240
|
-
direction: 'yaml-to-jira'
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
const change = plan.changes.find(c => c.field === 'points');
|
|
244
|
-
assert.ok(change);
|
|
245
|
-
assert.strictEqual(change.action, 'update-jira');
|
|
246
|
-
assert.strictEqual(change.targetPoints, 8);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it('should sync points from Jira to YAML', async () => {
|
|
250
|
-
const { generateSyncPlan } = await import('./jira-bidirectional-sync.mjs');
|
|
251
|
-
|
|
252
|
-
const yamlStories = [{ id: 'MSSCI-11842', status: 'backlog', points: 3 }];
|
|
253
|
-
const jiraStories = [{ key: 'MSSCI-11842', fields: { status: { name: 'To Do' }, customfield_10031: 5 } }];
|
|
254
|
-
|
|
255
|
-
const plan = generateSyncPlan(yamlStories, jiraStories, {
|
|
256
|
-
syncPoints: true,
|
|
257
|
-
direction: 'jira-to-yaml'
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
const change = plan.changes.find(c => c.field === 'points');
|
|
261
|
-
assert.ok(change);
|
|
262
|
-
assert.strictEqual(change.action, 'update-yaml');
|
|
263
|
-
assert.strictEqual(change.targetPoints, 5);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it('should not generate change when points match', async () => {
|
|
267
|
-
const { generateSyncPlan } = await import('./jira-bidirectional-sync.mjs');
|
|
268
|
-
|
|
269
|
-
const yamlStories = [{ id: 'MSSCI-11842', status: 'backlog', points: 4 }];
|
|
270
|
-
const jiraStories = [{ key: 'MSSCI-11842', fields: { status: { name: 'To Do' }, customfield_10031: 4 } }];
|
|
271
|
-
|
|
272
|
-
const plan = generateSyncPlan(yamlStories, jiraStories, {
|
|
273
|
-
syncPoints: true
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
const pointsChange = plan.changes.find(c => c.key === 'MSSCI-11842' && c.field === 'points');
|
|
277
|
-
assert.ok(!pointsChange, 'Should not generate change when points match');
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
// =============================================================================
|
|
283
|
-
// AC3: New stories in either system detected
|
|
284
|
-
// =============================================================================
|
|
285
|
-
|
|
286
|
-
describe('AC3: Detect new stories in either system', () => {
|
|
287
|
-
|
|
288
|
-
describe('YAML-only stories', () => {
|
|
289
|
-
|
|
290
|
-
it('should detect stories in YAML but not in Jira', async () => {
|
|
291
|
-
const { generateSyncPlan } = await import('./jira-bidirectional-sync.mjs');
|
|
292
|
-
|
|
293
|
-
const plan = generateSyncPlan(sampleYamlStories, sampleJiraStories, {});
|
|
294
|
-
|
|
295
|
-
// MSSCI-11844 is in YAML but not in Jira fixtures
|
|
296
|
-
assert.ok(plan.yamlOnly, 'Plan should have yamlOnly array');
|
|
297
|
-
assert.ok(plan.yamlOnly.includes('MSSCI-11844'), 'Should detect MSSCI-11844 as YAML-only');
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
it('should report YAML-only stories for potential Jira creation', async () => {
|
|
301
|
-
const { generateSyncPlan } = await import('./jira-bidirectional-sync.mjs');
|
|
302
|
-
|
|
303
|
-
const plan = generateSyncPlan(sampleYamlStories, sampleJiraStories, {
|
|
304
|
-
reportMissing: true
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
const yamlOnlyReport = plan.reports?.yamlOnly || [];
|
|
308
|
-
assert.ok(yamlOnlyReport.length > 0, 'Should report YAML-only stories');
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
describe('Jira-only stories', () => {
|
|
314
|
-
|
|
315
|
-
it('should detect stories in Jira but not in YAML', async () => {
|
|
316
|
-
const { generateSyncPlan } = await import('./jira-bidirectional-sync.mjs');
|
|
317
|
-
|
|
318
|
-
const plan = generateSyncPlan(sampleYamlStories, sampleJiraStories, {});
|
|
319
|
-
|
|
320
|
-
// MSSCI-11850 is in Jira but not in YAML fixtures
|
|
321
|
-
assert.ok(plan.jiraOnly, 'Plan should have jiraOnly array');
|
|
322
|
-
assert.ok(plan.jiraOnly.includes('MSSCI-11850'), 'Should detect MSSCI-11850 as Jira-only');
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
it('should report Jira-only stories for potential YAML import', async () => {
|
|
326
|
-
const { generateSyncPlan } = await import('./jira-bidirectional-sync.mjs');
|
|
327
|
-
|
|
328
|
-
const plan = generateSyncPlan(sampleYamlStories, sampleJiraStories, {
|
|
329
|
-
reportMissing: true
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
const jiraOnlyReport = plan.reports?.jiraOnly || [];
|
|
333
|
-
assert.ok(jiraOnlyReport.length > 0, 'Should report Jira-only stories');
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
describe('Stories in both systems', () => {
|
|
339
|
-
|
|
340
|
-
it('should identify stories present in both systems', async () => {
|
|
341
|
-
const { generateSyncPlan } = await import('./jira-bidirectional-sync.mjs');
|
|
342
|
-
|
|
343
|
-
const plan = generateSyncPlan(sampleYamlStories, sampleJiraStories, {});
|
|
344
|
-
|
|
345
|
-
assert.ok(plan.both, 'Plan should have both array');
|
|
346
|
-
// MSSCI-11842 and MSSCI-11843 are in both
|
|
347
|
-
assert.ok(plan.both.includes('MSSCI-11842'));
|
|
348
|
-
assert.ok(plan.both.includes('MSSCI-11843'));
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
// =============================================================================
|
|
356
|
-
// AC4: Dry-run mode shows changes before applying
|
|
357
|
-
// =============================================================================
|
|
358
|
-
|
|
359
|
-
describe('AC4: Dry-run mode', () => {
|
|
360
|
-
|
|
361
|
-
it('should generate plan without executing changes in dry-run mode', async () => {
|
|
362
|
-
const { executeSyncPlan, generateSyncPlan } = await import('./jira-bidirectional-sync.mjs');
|
|
363
|
-
|
|
364
|
-
const plan = generateSyncPlan(sampleYamlStories, sampleJiraStories, {
|
|
365
|
-
syncStatus: true,
|
|
366
|
-
syncPoints: true
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
// Execute with dry-run
|
|
370
|
-
const result = await executeSyncPlan(plan, { dryRun: true });
|
|
371
|
-
|
|
372
|
-
assert.ok(result.dryRun, 'Result should indicate dry-run mode');
|
|
373
|
-
assert.ok(result.wouldApply, 'Result should list what would be applied');
|
|
374
|
-
assert.strictEqual(result.applied, 0, 'Should not apply any changes');
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
it('should display planned changes in human-readable format', async () => {
|
|
378
|
-
const { formatSyncPlan } = await import('./jira-bidirectional-sync.mjs');
|
|
379
|
-
|
|
380
|
-
const plan = {
|
|
381
|
-
changes: [
|
|
382
|
-
{ key: 'MSSCI-11843', field: 'status', action: 'update-yaml', targetStatus: 'done' }
|
|
383
|
-
],
|
|
384
|
-
yamlOnly: ['MSSCI-11844'],
|
|
385
|
-
jiraOnly: ['MSSCI-11850'],
|
|
386
|
-
both: ['MSSCI-11842', 'MSSCI-11843']
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
const output = formatSyncPlan(plan);
|
|
390
|
-
|
|
391
|
-
assert.ok(typeof output === 'string', 'Should return string output');
|
|
392
|
-
assert.ok(output.includes('MSSCI-11843'), 'Should include story keys');
|
|
393
|
-
assert.ok(output.includes('status'), 'Should include field names');
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
it('should not modify YAML file in dry-run mode', async () => {
|
|
397
|
-
const { executeSyncPlan, generateSyncPlan } = await import('./jira-bidirectional-sync.mjs');
|
|
398
|
-
|
|
399
|
-
// This test verifies no file writes happen
|
|
400
|
-
const plan = generateSyncPlan(
|
|
401
|
-
[{ id: 'MSSCI-11842', status: 'backlog', points: 4 }],
|
|
402
|
-
[{ key: 'MSSCI-11842', fields: { status: { name: 'Done' }, customfield_10031: 4 } }],
|
|
403
|
-
{ syncStatus: true, direction: 'jira-to-yaml' }
|
|
404
|
-
);
|
|
405
|
-
|
|
406
|
-
const result = await executeSyncPlan(plan, { dryRun: true });
|
|
407
|
-
|
|
408
|
-
assert.ok(result.dryRun);
|
|
409
|
-
assert.strictEqual(result.yamlModified, false, 'YAML should not be modified');
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
it('should not call Jira API in dry-run mode', async () => {
|
|
413
|
-
const { executeSyncPlan, generateSyncPlan } = await import('./jira-bidirectional-sync.mjs');
|
|
414
|
-
|
|
415
|
-
const plan = generateSyncPlan(
|
|
416
|
-
[{ id: 'MSSCI-11842', status: 'done', points: 4 }],
|
|
417
|
-
[{ key: 'MSSCI-11842', fields: { status: { name: 'To Do' }, customfield_10031: 4 } }],
|
|
418
|
-
{ syncStatus: true, direction: 'yaml-to-jira' }
|
|
419
|
-
);
|
|
420
|
-
|
|
421
|
-
const result = await executeSyncPlan(plan, { dryRun: true });
|
|
422
|
-
|
|
423
|
-
assert.ok(result.dryRun);
|
|
424
|
-
assert.strictEqual(result.jiraApiCalls, 0, 'Should not make Jira API calls');
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
// =============================================================================
|
|
430
|
-
// Integration: CLI argument parsing
|
|
431
|
-
// =============================================================================
|
|
432
|
-
|
|
433
|
-
describe('CLI argument parsing', () => {
|
|
434
|
-
|
|
435
|
-
it('should parse --dry-run flag', async () => {
|
|
436
|
-
const { parseCliArgs } = await import('./jira-bidirectional-sync.mjs');
|
|
437
|
-
|
|
438
|
-
const args = parseCliArgs(['--dry-run']);
|
|
439
|
-
assert.strictEqual(args.dryRun, true);
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
it('should parse --yaml-wins flag', async () => {
|
|
443
|
-
const { parseCliArgs } = await import('./jira-bidirectional-sync.mjs');
|
|
444
|
-
|
|
445
|
-
const args = parseCliArgs(['--yaml-wins']);
|
|
446
|
-
assert.strictEqual(args.yamlWins, true);
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
it('should parse --status and --points flags', async () => {
|
|
450
|
-
const { parseCliArgs } = await import('./jira-bidirectional-sync.mjs');
|
|
451
|
-
|
|
452
|
-
const args = parseCliArgs(['--status', '--points']);
|
|
453
|
-
assert.strictEqual(args.syncStatus, true);
|
|
454
|
-
assert.strictEqual(args.syncPoints, true);
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
it('should parse --all flag as status + points', async () => {
|
|
458
|
-
const { parseCliArgs } = await import('./jira-bidirectional-sync.mjs');
|
|
459
|
-
|
|
460
|
-
const args = parseCliArgs(['--all']);
|
|
461
|
-
assert.strictEqual(args.syncStatus, true);
|
|
462
|
-
assert.strictEqual(args.syncPoints, true);
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
it('should parse --sprint <id> option', async () => {
|
|
466
|
-
const { parseCliArgs } = await import('./jira-bidirectional-sync.mjs');
|
|
467
|
-
|
|
468
|
-
const args = parseCliArgs(['--sprint', '275']);
|
|
469
|
-
assert.strictEqual(args.sprintId, '275');
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
// =============================================================================
|
|
475
|
-
// Helper function tests (using existing jira-lib.mjs)
|
|
476
|
-
// =============================================================================
|
|
477
|
-
|
|
478
|
-
describe('Helper functions from jira-lib.mjs', () => {
|
|
479
|
-
|
|
480
|
-
it('mapStatusToJira converts Pennyfarthing status to Jira', () => {
|
|
481
|
-
assert.strictEqual(mapStatusToJira('backlog'), 'To Do');
|
|
482
|
-
assert.strictEqual(mapStatusToJira('in_progress'), 'In Progress');
|
|
483
|
-
assert.strictEqual(mapStatusToJira('in-progress'), 'In Progress');
|
|
484
|
-
assert.strictEqual(mapStatusToJira('done'), 'Done');
|
|
485
|
-
assert.strictEqual(mapStatusToJira('review'), 'In Review');
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
it('mapJiraToStatus converts Jira status to Pennyfarthing', () => {
|
|
489
|
-
assert.strictEqual(mapJiraToStatus('To Do'), 'backlog');
|
|
490
|
-
assert.strictEqual(mapJiraToStatus('In Progress'), 'in-progress');
|
|
491
|
-
assert.strictEqual(mapJiraToStatus('Done'), 'done');
|
|
492
|
-
assert.strictEqual(mapJiraToStatus('In Review'), 'review');
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
it('extractJiraKey extracts key from URL or returns as-is', () => {
|
|
496
|
-
assert.strictEqual(extractJiraKey('MSSCI-11842'), 'MSSCI-11842');
|
|
497
|
-
assert.strictEqual(
|
|
498
|
-
extractJiraKey('https://1898andco.atlassian.net/browse/MSSCI-11842'),
|
|
499
|
-
'MSSCI-11842'
|
|
500
|
-
);
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
});
|