@cspeach/cli 0.6.4 → 0.6.5
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/dist/index.js +0 -0
- package/package.json +86 -82
- package/dist/agent/__tests__/crash-recover.integration.test.js +0 -254
- package/dist/agent/__tests__/loop-save-hook.test.js +0 -188
- package/dist/agent/__tests__/maybe-build-project-context.test.js +0 -247
- package/dist/agent/__tests__/project-context-injection.test.js +0 -161
- package/dist/agent/__tests__/repair-partial.test.js +0 -79
- package/dist/agent/__tests__/retry-key.test.js +0 -77
- package/dist/agent/__tests__/sap-connection-adapter.test.js +0 -132
- package/dist/agent/__tests__/skill-checkpoint.test.js +0 -154
- package/dist/agent/__tests__/turn-assistant-text.test.js +0 -73
- package/dist/agent/__tests__/turn-error-ux.test.js +0 -145
- package/dist/agent/__tests__/turn-stream.test.js +0 -100
- package/dist/agent/__tests__/turn-watchdog.test.js +0 -126
- package/dist/agent/providers/__tests__/ai-hub-provider.test.js +0 -30
- package/dist/agent/providers/__tests__/byok-provider.test.js +0 -32
- package/dist/agent/providers/__tests__/factory.test.js +0 -33
- package/dist/agent/providers/__tests__/local-provider.test.js +0 -55
- package/dist/approvals/__tests__/advisory-prompt.test.js +0 -57
- package/dist/approvals/__tests__/advisory-render.test.js +0 -54
- package/dist/auth/__tests__/auth-file.test.js +0 -42
- package/dist/auth/__tests__/me.test.js +0 -43
- package/dist/classifier/__tests__/client.test.js +0 -70
- package/dist/commands/__tests__/config-set-write-mode.test.js +0 -14
- package/dist/commands/__tests__/login.test.js +0 -152
- package/dist/commands/__tests__/logout.test.js +0 -39
- package/dist/commands/__tests__/project-context-impact.test.js +0 -173
- package/dist/commands/__tests__/spec-gap-status.test.js +0 -31
- package/dist/commands/__tests__/whoami.test.js +0 -91
- package/dist/config/__tests__/llm-config.test.js +0 -28
- package/dist/config/__tests__/shell-exec-config.test.js +0 -76
- package/dist/config/__tests__/write-mode.test.js +0 -36
- package/dist/doctor/__tests__/check-forge-rules.test.js +0 -134
- package/dist/doctor/__tests__/check-llm-mode.test.js +0 -21
- package/dist/doctor/__tests__/check-write-mode.test.js +0 -34
- package/dist/project-context/__tests__/conventions.test.js +0 -221
- package/dist/project-context/__tests__/detect.test.js +0 -177
- package/dist/project-context/__tests__/domain-abap-cloud.test.js +0 -119
- package/dist/project-context/__tests__/domain-abapgit.test.js +0 -161
- package/dist/project-context/__tests__/domain-cap.test.js +0 -168
- package/dist/project-context/__tests__/domain-fiori.test.js +0 -282
- package/dist/project-context/__tests__/git.test.js +0 -131
- package/dist/project-context/__tests__/index-files.test.js +0 -195
- package/dist/project-context/__tests__/index.test.js +0 -133
- package/dist/project-context/__tests__/render.test.js +0 -322
- package/dist/project-context/__tests__/types.test.js +0 -73
- package/dist/projects/__tests__/build.test.js +0 -52
- package/dist/projects/__tests__/canonicalize.test.js +0 -47
- package/dist/projects/__tests__/email-template.test.js +0 -71
- package/dist/projects/__tests__/expand-text-attachments.test.js +0 -105
- package/dist/projects/__tests__/extract-cca.test.js +0 -141
- package/dist/projects/__tests__/extract-design.test.js +0 -62
- package/dist/projects/__tests__/extract-estimate.test.js +0 -58
- package/dist/projects/__tests__/extract-modernize.test.js +0 -127
- package/dist/projects/__tests__/extract-spec-gap.test.js +0 -122
- package/dist/projects/__tests__/extract-test-coverage.test.js +0 -133
- package/dist/projects/__tests__/filename.test.js +0 -32
- package/dist/projects/__tests__/promote-command.test.js +0 -170
- package/dist/projects/__tests__/promote.test.js +0 -181
- package/dist/projects/__tests__/save-command.test.js +0 -216
- package/dist/projects/__tests__/save.test.js +0 -41
- package/dist/projects/__tests__/status.test.js +0 -208
- package/dist/projects/__tests__/types.test.js +0 -41
- package/dist/projects/__tests__/validate.test.js +0 -165
- package/dist/projects/__tests__/workspace.test.js +0 -337
- package/dist/repl/__tests__/current-transport.test.js +0 -43
- package/dist/repl/__tests__/diff-display.test.js +0 -59
- package/dist/repl/__tests__/file-picker.test.js +0 -97
- package/dist/repl/__tests__/rule8-detector.test.js +0 -114
- package/dist/repl/__tests__/safety-confirm.test.js +0 -130
- package/dist/repl/__tests__/safety-mode-state.test.js +0 -42
- package/dist/sap/__tests__/system-info.test.js +0 -344
- package/dist/session/__tests__/store-discovery.test.js +0 -114
- package/dist/session/__tests__/time-ago.test.js +0 -53
- package/dist/skills/__tests__/canonical.test.js +0 -20
- package/dist/skills/__tests__/manifest-client.test.js +0 -88
- package/dist/skills/__tests__/promotion-dispatch.test.js +0 -28
- package/dist/skills/__tests__/source-managed.test.js +0 -60
- package/dist/tools/__tests__/_background-shared.test.js +0 -69
- package/dist/tools/__tests__/_command-shared.test.js +0 -59
- package/dist/tools/__tests__/_filesystem-shared.test.js +0 -42
- package/dist/tools/__tests__/_flag.test.js +0 -66
- package/dist/tools/__tests__/_helpers.js +0 -17
- package/dist/tools/__tests__/_web-shared.test.js +0 -143
- package/dist/tools/__tests__/agent_run.test.js +0 -166
- package/dist/tools/__tests__/approval-advisory.test.js +0 -66
- package/dist/tools/__tests__/background_run.test.js +0 -113
- package/dist/tools/__tests__/convention_get.test.js +0 -53
- package/dist/tools/__tests__/file-edit.test.js +0 -72
- package/dist/tools/__tests__/file-read.test.js +0 -81
- package/dist/tools/__tests__/file-write.test.js +0 -68
- package/dist/tools/__tests__/glob.test.js +0 -128
- package/dist/tools/__tests__/grep.test.js +0 -71
- package/dist/tools/__tests__/monitor_emit.test.js +0 -96
- package/dist/tools/__tests__/playbook_get.test.js +0 -109
- package/dist/tools/__tests__/project_context_get.test.js +0 -71
- package/dist/tools/__tests__/registry-category.test.js +0 -60
- package/dist/tools/__tests__/sap-read-extras.test.js +0 -196
- package/dist/tools/__tests__/sap-write-extras.test.js +0 -573
- package/dist/tools/__tests__/schedule_create.test.js +0 -85
- package/dist/tools/__tests__/shell_exec.test.js +0 -135
- package/dist/tools/__tests__/transport-safety.test.js +0 -106
- package/dist/tools/__tests__/update-method-intercept.test.js +0 -128
- package/dist/tools/__tests__/web_fetch.test.js +0 -329
- package/dist/tools/__tests__/web_search.test.js +0 -215
- package/dist/tools/__tests__/write-mode.test.js +0 -32
- package/dist/ui/__tests__/login-banner.test.js +0 -69
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { extractSpecGap } from '../extract-spec-gap.js';
|
|
6
|
-
// ESM (`"type": "module"` in package.json): __dirname is NOT defined.
|
|
7
|
-
// Derive it via import.meta.url. See cspeach-cli/test/unit/skill-catalog.test.ts.
|
|
8
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const FIXTURE = readFileSync(join(__dirname, '..', '__fixtures__', 'spec-gap-skill-output.md'), 'utf8');
|
|
10
|
-
describe('extractSpecGap', () => {
|
|
11
|
-
it('parses TL;DR title', () => {
|
|
12
|
-
const r = extractSpecGap(FIXTURE);
|
|
13
|
-
expect(r.title).toBe('Customer Aging Report');
|
|
14
|
-
});
|
|
15
|
-
it('extracts at least 7 sections', () => {
|
|
16
|
-
const r = extractSpecGap(FIXTURE);
|
|
17
|
-
expect(r.content.sections.length).toBeGreaterThanOrEqual(7);
|
|
18
|
-
});
|
|
19
|
-
it('first section is "Missing Business Questions" (after TL;DR)', () => {
|
|
20
|
-
const r = extractSpecGap(FIXTURE);
|
|
21
|
-
expect(r.content.sections[0].name).toMatch(/Missing Business Questions/i);
|
|
22
|
-
});
|
|
23
|
-
it('extracts gaps with stable gap-001, gap-002 ids', () => {
|
|
24
|
-
const r = extractSpecGap(FIXTURE);
|
|
25
|
-
const allGaps = r.content.sections.flatMap((s) => s.gaps);
|
|
26
|
-
expect(allGaps[0].id).toBe('gap-001');
|
|
27
|
-
expect(allGaps[1].id).toBe('gap-002');
|
|
28
|
-
});
|
|
29
|
-
it('parses severity from leading [BLOCKER]/[IMPORTANT]/[NICE TO KNOW]', () => {
|
|
30
|
-
const r = extractSpecGap(FIXTURE);
|
|
31
|
-
const allGaps = r.content.sections.flatMap((s) => s.gaps);
|
|
32
|
-
const blockers = allGaps.filter((g) => g.severity === 'BLOCKER');
|
|
33
|
-
const important = allGaps.filter((g) => g.severity === 'IMPORTANT');
|
|
34
|
-
const nice = allGaps.filter((g) => g.severity === 'NICE TO KNOW');
|
|
35
|
-
expect(blockers.length).toBeGreaterThan(0);
|
|
36
|
-
expect(important.length).toBeGreaterThan(0);
|
|
37
|
-
expect(nice.length).toBeGreaterThan(0);
|
|
38
|
-
expect(blockers.length + important.length + nice.length).toBe(allGaps.length);
|
|
39
|
-
});
|
|
40
|
-
it('every gap gets a matching interaction.items entry with status="open"', () => {
|
|
41
|
-
const r = extractSpecGap(FIXTURE);
|
|
42
|
-
const allGaps = r.content.sections.flatMap((s) => s.gaps);
|
|
43
|
-
expect(r.items.length).toBe(allGaps.length);
|
|
44
|
-
expect(r.items.every((i) => i.status === 'open')).toBe(true);
|
|
45
|
-
expect(r.items.every((i) => i.answer === null)).toBe(true);
|
|
46
|
-
expect(r.items.every((i) => i.comments.length === 0)).toBe(true);
|
|
47
|
-
});
|
|
48
|
-
it('extracts inferredDomain from the "Inferred Domain:" line', () => {
|
|
49
|
-
const r = extractSpecGap(FIXTURE);
|
|
50
|
-
expect(r.content.inferredDomain.length).toBeGreaterThan(0);
|
|
51
|
-
});
|
|
52
|
-
it('throws on input without TL;DR + sections', () => {
|
|
53
|
-
expect(() => extractSpecGap('Hello world')).toThrow(/not a spec-gap output/i);
|
|
54
|
-
});
|
|
55
|
-
it('parses indented gap lines (real-world model output style)', () => {
|
|
56
|
-
const indented = [
|
|
57
|
-
'## Spec Gap Analysis — Test',
|
|
58
|
-
'',
|
|
59
|
-
'### Missing Business Questions',
|
|
60
|
-
'',
|
|
61
|
-
' 1. [BLOCKER] What defines "open"?',
|
|
62
|
-
' 2. [IMPORTANT] Currency rate?',
|
|
63
|
-
].join('\n');
|
|
64
|
-
const r = extractSpecGap(indented);
|
|
65
|
-
expect(r.content.sections).toHaveLength(1);
|
|
66
|
-
expect(r.content.sections[0].gaps).toHaveLength(2);
|
|
67
|
-
expect(r.content.sections[0].gaps[0].text).toBe('What defines "open"?');
|
|
68
|
-
});
|
|
69
|
-
it('parses tab-indented gap lines', () => {
|
|
70
|
-
const tabbed = [
|
|
71
|
-
'## Spec Gap Analysis — Test',
|
|
72
|
-
'',
|
|
73
|
-
'### Missing Business Questions',
|
|
74
|
-
'',
|
|
75
|
-
'\t1. [BLOCKER] What defines "open"?',
|
|
76
|
-
].join('\n');
|
|
77
|
-
const r = extractSpecGap(tabbed);
|
|
78
|
-
expect(r.content.sections[0].gaps).toHaveLength(1);
|
|
79
|
-
});
|
|
80
|
-
it('parses bold-wrapped severity (real-world variant: `1. **[BLOCKER]** Text`)', () => {
|
|
81
|
-
const bolded = [
|
|
82
|
-
'## Spec Gap Analysis — Test',
|
|
83
|
-
'',
|
|
84
|
-
'### Missing Business Questions',
|
|
85
|
-
'',
|
|
86
|
-
'1. **[BLOCKER]** What is the precise definition of "open"?',
|
|
87
|
-
'2. **[IMPORTANT]** Currency rate?',
|
|
88
|
-
'3. **[NICE TO KNOW]** Saved variants?',
|
|
89
|
-
].join('\n');
|
|
90
|
-
const r = extractSpecGap(bolded);
|
|
91
|
-
expect(r.content.sections).toHaveLength(1);
|
|
92
|
-
expect(r.content.sections[0].gaps).toHaveLength(3);
|
|
93
|
-
expect(r.content.sections[0].gaps[0].severity).toBe('BLOCKER');
|
|
94
|
-
expect(r.content.sections[0].gaps[1].severity).toBe('IMPORTANT');
|
|
95
|
-
expect(r.content.sections[0].gaps[2].severity).toBe('NICE TO KNOW');
|
|
96
|
-
expect(r.content.sections[0].gaps[0].text).toBe('What is the precise definition of "open"?');
|
|
97
|
-
});
|
|
98
|
-
it('parses indent + bold-wrapped severity together', () => {
|
|
99
|
-
const both = [
|
|
100
|
-
'## Spec Gap Analysis — Test',
|
|
101
|
-
'',
|
|
102
|
-
'### Missing Business Questions',
|
|
103
|
-
'',
|
|
104
|
-
' 1. **[BLOCKER]** Mixed-style line',
|
|
105
|
-
].join('\n');
|
|
106
|
-
const r = extractSpecGap(both);
|
|
107
|
-
expect(r.content.sections[0].gaps).toHaveLength(1);
|
|
108
|
-
expect(r.content.sections[0].gaps[0].severity).toBe('BLOCKER');
|
|
109
|
-
expect(r.content.sections[0].gaps[0].text).toBe('Mixed-style line');
|
|
110
|
-
});
|
|
111
|
-
it('tags sections with audience derived from name', () => {
|
|
112
|
-
const r = extractSpecGap(FIXTURE);
|
|
113
|
-
const audienceBySection = Object.fromEntries(r.content.sections.map((s) => [s.name, s.audience]));
|
|
114
|
-
expect(audienceBySection['Missing Business Questions']).toBe('business');
|
|
115
|
-
expect(audienceBySection['Missing Technical Questions']).toBe('technical');
|
|
116
|
-
expect(audienceBySection['Authorization Questions']).toBe('auth');
|
|
117
|
-
expect(audienceBySection['Edge Cases Not Covered']).toBe('business');
|
|
118
|
-
expect(audienceBySection['Transport and Release Concerns']).toBe('transport');
|
|
119
|
-
expect(audienceBySection['Clean Core / API Concerns']).toBe('clean-core');
|
|
120
|
-
expect(audienceBySection['Data Assumptions to Confirm']).toBe('data');
|
|
121
|
-
});
|
|
122
|
-
});
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { extractTestCoverage } from '../extract-test-coverage.js';
|
|
3
|
-
describe('extractTestCoverage', () => {
|
|
4
|
-
const FULL_FIXTURE = `# /abap-test — ZDPR_PAYMENTS test generation
|
|
5
|
-
|
|
6
|
-
<!-- csforge:test-coverage-manifest
|
|
7
|
-
artefact: test-coverage
|
|
8
|
-
title: ZDPR_PAYMENTS — test coverage
|
|
9
|
-
detail_path: .abapforge/tests/zdpr-payments/project.json
|
|
10
|
-
baseline_path: .cspeach/zdpr-payments-cca-assessment-3467-v1.cspeach.json
|
|
11
|
-
baseline_sha256: a7Qj8ibfLTj49kQRTn1DOE_UAExuoBgjhaXxf33+JPY=
|
|
12
|
-
transport: S4HK903401
|
|
13
|
-
test_classes_generated: 14
|
|
14
|
-
methods_added: 87
|
|
15
|
-
coverage_pct_before: 12
|
|
16
|
-
coverage_pct_after: 78
|
|
17
|
-
skipped: 0
|
|
18
|
-
failed: 0
|
|
19
|
-
tests:
|
|
20
|
-
item-001 | ZPAYMENTS | PROG | ZPAYMENTS_TEST | 6 | generated
|
|
21
|
-
item-002 | ZPAYMENTS_OLD | PROG | ZPAYMENTS_OLD_TEST | 4 | generated
|
|
22
|
-
item-003 | ZFGPAYMENTS | FUGR | ZFGPAYMENTS_TEST | 8 | generated
|
|
23
|
-
-->
|
|
24
|
-
`;
|
|
25
|
-
it('extracts title from manifest', () => {
|
|
26
|
-
const out = extractTestCoverage(FULL_FIXTURE);
|
|
27
|
-
expect(out.title).toBe('ZDPR_PAYMENTS — test coverage');
|
|
28
|
-
});
|
|
29
|
-
it('extracts detail + baseline refs', () => {
|
|
30
|
-
const out = extractTestCoverage(FULL_FIXTURE);
|
|
31
|
-
expect(out.content.detailPath).toBe('.abapforge/tests/zdpr-payments/project.json');
|
|
32
|
-
expect(out.content.baselineRef.path).toBe('.cspeach/zdpr-payments-cca-assessment-3467-v1.cspeach.json');
|
|
33
|
-
expect(out.content.baselineRef.sha256).toBe('a7Qj8ibfLTj49kQRTn1DOE_UAExuoBgjhaXxf33+JPY=');
|
|
34
|
-
});
|
|
35
|
-
it('extracts summary with coverage percentages', () => {
|
|
36
|
-
const out = extractTestCoverage(FULL_FIXTURE);
|
|
37
|
-
expect(out.content.summary).toEqual({
|
|
38
|
-
testClassesGenerated: 14,
|
|
39
|
-
methodsAdded: 87,
|
|
40
|
-
coveragePctBefore: 12,
|
|
41
|
-
coveragePctAfter: 78,
|
|
42
|
-
skipped: 0,
|
|
43
|
-
failed: 0,
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
it('parses coverage_pct=- as null (SCMP unavailable case)', () => {
|
|
47
|
-
const noCov = FULL_FIXTURE
|
|
48
|
-
.replace('coverage_pct_before: 12', 'coverage_pct_before: -')
|
|
49
|
-
.replace('coverage_pct_after: 78', 'coverage_pct_after: -');
|
|
50
|
-
const out = extractTestCoverage(noCov);
|
|
51
|
-
expect(out.content.summary.coveragePctBefore).toBeNull();
|
|
52
|
-
expect(out.content.summary.coveragePctAfter).toBeNull();
|
|
53
|
-
});
|
|
54
|
-
it('parses missing coverage fields as null', () => {
|
|
55
|
-
const minimal = `<!-- csforge:test-coverage-manifest
|
|
56
|
-
artefact: test-coverage
|
|
57
|
-
detail_path: x
|
|
58
|
-
baseline_path: y
|
|
59
|
-
test_classes_generated: 1
|
|
60
|
-
methods_added: 3
|
|
61
|
-
-->`;
|
|
62
|
-
const out = extractTestCoverage(minimal);
|
|
63
|
-
expect(out.content.summary.coveragePctBefore).toBeNull();
|
|
64
|
-
expect(out.content.summary.coveragePctAfter).toBeNull();
|
|
65
|
-
});
|
|
66
|
-
it('extracts tests table with method counts', () => {
|
|
67
|
-
const out = extractTestCoverage(FULL_FIXTURE);
|
|
68
|
-
expect(out.content.tests).toHaveLength(3);
|
|
69
|
-
expect(out.content.tests[0]).toEqual({
|
|
70
|
-
id: 'item-001',
|
|
71
|
-
targetObjectName: 'ZPAYMENTS',
|
|
72
|
-
targetObjectType: 'PROG',
|
|
73
|
-
testClassName: 'ZPAYMENTS_TEST',
|
|
74
|
-
methodCount: 6,
|
|
75
|
-
status: 'generated',
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
it('emits one InteractionItem per test entry with matching status', () => {
|
|
79
|
-
const out = extractTestCoverage(FULL_FIXTURE);
|
|
80
|
-
expect(out.items).toHaveLength(3);
|
|
81
|
-
for (const it of out.items) {
|
|
82
|
-
expect(it.status).toBe('generated');
|
|
83
|
-
expect(it.answer).toBeNull();
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
it('coerces unknown status to pending', () => {
|
|
87
|
-
const weird = `<!-- csforge:test-coverage-manifest
|
|
88
|
-
artefact: test-coverage
|
|
89
|
-
detail_path: x
|
|
90
|
-
baseline_path: y
|
|
91
|
-
tests:
|
|
92
|
-
item-001 | ZFOO | PROG | ZFOO_TEST | 2 | half-baked
|
|
93
|
-
-->`;
|
|
94
|
-
const out = extractTestCoverage(weird);
|
|
95
|
-
expect(out.content.tests[0]?.status).toBe('pending');
|
|
96
|
-
});
|
|
97
|
-
it('throws when artefact field disagrees', () => {
|
|
98
|
-
const wrong = `<!-- csforge:test-coverage-manifest
|
|
99
|
-
artefact: cca-assessment
|
|
100
|
-
detail_path: x
|
|
101
|
-
baseline_path: y
|
|
102
|
-
-->`;
|
|
103
|
-
expect(() => extractTestCoverage(wrong)).toThrow(/expected 'test-coverage'/);
|
|
104
|
-
});
|
|
105
|
-
it('throws when detail_path is missing', () => {
|
|
106
|
-
const noDetail = `<!-- csforge:test-coverage-manifest
|
|
107
|
-
artefact: test-coverage
|
|
108
|
-
baseline_path: y
|
|
109
|
-
-->`;
|
|
110
|
-
expect(() => extractTestCoverage(noDetail)).toThrow(/'detail_path'/);
|
|
111
|
-
});
|
|
112
|
-
it('throws when baseline_path is missing', () => {
|
|
113
|
-
const noBaseline = `<!-- csforge:test-coverage-manifest
|
|
114
|
-
artefact: test-coverage
|
|
115
|
-
detail_path: x
|
|
116
|
-
-->`;
|
|
117
|
-
expect(() => extractTestCoverage(noBaseline)).toThrow(/'baseline_path'/);
|
|
118
|
-
});
|
|
119
|
-
it('throws when no manifest block', () => {
|
|
120
|
-
expect(() => extractTestCoverage('# nothing')).toThrow(/No <!-- csforge:test-coverage-manifest --> block/);
|
|
121
|
-
});
|
|
122
|
-
it('handles non-numeric methodCount as 0', () => {
|
|
123
|
-
const weird = `<!-- csforge:test-coverage-manifest
|
|
124
|
-
artefact: test-coverage
|
|
125
|
-
detail_path: x
|
|
126
|
-
baseline_path: y
|
|
127
|
-
tests:
|
|
128
|
-
item-001 | ZFOO | PROG | ZFOO_TEST | notanumber | generated
|
|
129
|
-
-->`;
|
|
130
|
-
const out = extractTestCoverage(weird);
|
|
131
|
-
expect(out.content.tests[0]?.methodCount).toBe(0);
|
|
132
|
-
});
|
|
133
|
-
});
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { titleSlug, projectFilename, shortId } from '../filename.js';
|
|
3
|
-
describe('titleSlug', () => {
|
|
4
|
-
it('lowercases + hyphenates', () => {
|
|
5
|
-
expect(titleSlug('Customer Aging Report')).toBe('customer-aging-report');
|
|
6
|
-
});
|
|
7
|
-
it('strips punctuation', () => {
|
|
8
|
-
expect(titleSlug('FI-AR: Customer Aging!')).toBe('fi-ar-customer-aging');
|
|
9
|
-
});
|
|
10
|
-
it('collapses whitespace + trims', () => {
|
|
11
|
-
expect(titleSlug(' Multi space ')).toBe('multi-space');
|
|
12
|
-
});
|
|
13
|
-
it('handles unicode by transliterating common cases', () => {
|
|
14
|
-
expect(titleSlug('Müller Bericht')).toBe('muller-bericht');
|
|
15
|
-
});
|
|
16
|
-
it('caps length at 60 chars', () => {
|
|
17
|
-
const long = 'A'.repeat(200);
|
|
18
|
-
expect(titleSlug(long).length).toBeLessThanOrEqual(60);
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
describe('shortId', () => {
|
|
22
|
-
it('returns first 4 chars of UUID', () => {
|
|
23
|
-
expect(shortId('a1b2c3d4-1111-4111-8111-111111111111')).toBe('a1b2');
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
describe('projectFilename', () => {
|
|
27
|
-
it('builds canonical filename', () => {
|
|
28
|
-
expect(projectFilename({ title: 'Customer Aging Report', artefactType: 'spec-gap',
|
|
29
|
-
id: 'a1b2c3d4-1111-4111-8111-111111111111', version: 1 }))
|
|
30
|
-
.toBe('customer-aging-report-spec-gap-a1b2-v1.cspeach.json');
|
|
31
|
-
});
|
|
32
|
-
});
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { writeFileSync, mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { runPromoteCommand } from '../promote-command.js';
|
|
6
|
-
function envelopeFixture(overrides = {}) {
|
|
7
|
-
const base = {
|
|
8
|
-
schemaVersion: '1.0',
|
|
9
|
-
artefactType: 'spec-gap',
|
|
10
|
-
id: '11111111-2222-3333-4444-555555555555',
|
|
11
|
-
title: 'Customer Aging Report',
|
|
12
|
-
createdAt: '2026-05-04T12:00:00Z',
|
|
13
|
-
createdBy: { name: 'Laeeq', role: 'consultant' },
|
|
14
|
-
lastEditedAt: '2026-05-04T16:00:00Z',
|
|
15
|
-
lastEditedBy: { name: 'Sarah', role: 'BA' },
|
|
16
|
-
version: 3,
|
|
17
|
-
source: { skill: 'abap-spec-gap', skillVersion: '1.0', input: '', tokensUsed: 0, model: 'claude-opus-4-7' },
|
|
18
|
-
promotedFrom: null,
|
|
19
|
-
content: {
|
|
20
|
-
inputType: 'User Story', inferredDomain: 'FI',
|
|
21
|
-
sections: [{ name: 'G', audience: 'business', gaps: [
|
|
22
|
-
{ id: 'gap-001', severity: 'BLOCKER', text: 'Q1' },
|
|
23
|
-
{ id: 'gap-002', severity: 'IMPORTANT', text: 'Q2' },
|
|
24
|
-
] }],
|
|
25
|
-
},
|
|
26
|
-
interaction: { items: [
|
|
27
|
-
{ id: 'gap-001', status: 'answered', answer: 'A1', answeredBy: { name: 'S', role: 'BA' }, answeredAt: '2026-05-04T16:00:00Z', comments: [] },
|
|
28
|
-
{ id: 'gap-002', status: 'answered', answer: 'A2', answeredBy: { name: 'S', role: 'BA' }, answeredAt: '2026-05-04T16:00:00Z', comments: [] },
|
|
29
|
-
] },
|
|
30
|
-
history: [
|
|
31
|
-
{ at: '2026-05-04T12:00:00Z', by: { name: 'L', role: 'consultant' }, action: 'created' },
|
|
32
|
-
{ at: '2026-05-04T15:00:00Z', by: { name: 'Sarah', role: 'BA' }, action: 'edited', summary: '1 gap answered' },
|
|
33
|
-
{ at: '2026-05-04T16:00:00Z', by: { name: 'Sarah', role: 'BA' }, action: 'edited', summary: '1 more gap answered' },
|
|
34
|
-
],
|
|
35
|
-
};
|
|
36
|
-
return { ...base, ...overrides };
|
|
37
|
-
}
|
|
38
|
-
function writeFixture(path, env) {
|
|
39
|
-
writeFileSync(path, JSON.stringify(env, null, 2), 'utf8');
|
|
40
|
-
return path;
|
|
41
|
-
}
|
|
42
|
-
describe('runPromoteCommand', () => {
|
|
43
|
-
it('returns snapshot + extendedSkillInput when source is fully answered', async () => {
|
|
44
|
-
const dir = mkdtempSync(`${tmpdir()}/cspeach-promote-`);
|
|
45
|
-
try {
|
|
46
|
-
const path = writeFixture(join(dir, 'src.cspeach.json'), envelopeFixture());
|
|
47
|
-
const log = [];
|
|
48
|
-
const result = await runPromoteCommand({
|
|
49
|
-
sourcePath: path,
|
|
50
|
-
targetSkill: 'abap-design',
|
|
51
|
-
prompt: async () => 'y',
|
|
52
|
-
log: (...lines) => log.push(...lines),
|
|
53
|
-
});
|
|
54
|
-
expect(result).not.toBeNull();
|
|
55
|
-
expect(result.promotedFrom.artefactType).toBe('spec-gap');
|
|
56
|
-
expect(result.promotedFrom.version).toBe(3);
|
|
57
|
-
expect(result.extendedSkillInput).toContain('Promoted from spec-gap');
|
|
58
|
-
expect(result.extendedSkillInput).toContain('gap-001: Q1 → A1');
|
|
59
|
-
}
|
|
60
|
-
finally {
|
|
61
|
-
rmSync(dir, { recursive: true, force: true });
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
it('returns null and logs error when source artefactType mismatches', async () => {
|
|
65
|
-
const dir = mkdtempSync(`${tmpdir()}/cspeach-promote-`);
|
|
66
|
-
try {
|
|
67
|
-
const designFixture = {
|
|
68
|
-
...envelopeFixture(),
|
|
69
|
-
artefactType: 'design',
|
|
70
|
-
content: { objects: [
|
|
71
|
-
{ id: 'obj-001', name: 'ZCL_X', type: 'CLAS', package: 'P', dependsOn: [] },
|
|
72
|
-
] },
|
|
73
|
-
interaction: { items: [
|
|
74
|
-
{ id: 'obj-001', status: 'keep', answer: null, answeredBy: null, answeredAt: null, comments: [] },
|
|
75
|
-
] },
|
|
76
|
-
};
|
|
77
|
-
const path = writeFixture(join(dir, 'src.cspeach.json'), designFixture);
|
|
78
|
-
const log = [];
|
|
79
|
-
const result = await runPromoteCommand({
|
|
80
|
-
sourcePath: path,
|
|
81
|
-
targetSkill: 'abap-design',
|
|
82
|
-
prompt: async () => 'y',
|
|
83
|
-
log: (...lines) => log.push(...lines),
|
|
84
|
-
});
|
|
85
|
-
expect(result).toBeNull();
|
|
86
|
-
expect(log.join('\n')).toContain('expects a spec-gap source');
|
|
87
|
-
expect(log.join('\n')).toContain('got design');
|
|
88
|
-
}
|
|
89
|
-
finally {
|
|
90
|
-
rmSync(dir, { recursive: true, force: true });
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
it('prompts to force-promote when source has open items, returns null on "no"', async () => {
|
|
94
|
-
const dir = mkdtempSync(`${tmpdir()}/cspeach-promote-`);
|
|
95
|
-
try {
|
|
96
|
-
const partial = envelopeFixture({
|
|
97
|
-
interaction: { items: [
|
|
98
|
-
{ id: 'gap-001', status: 'open', answer: null, answeredBy: null, answeredAt: null, comments: [] },
|
|
99
|
-
{ id: 'gap-002', status: 'answered', answer: 'A2', answeredBy: { name: 'S', role: 'BA' }, answeredAt: '2026-05-04T16:00:00Z', comments: [] },
|
|
100
|
-
] },
|
|
101
|
-
});
|
|
102
|
-
const path = writeFixture(join(dir, 'src.cspeach.json'), partial);
|
|
103
|
-
const prompts = [];
|
|
104
|
-
const result = await runPromoteCommand({
|
|
105
|
-
sourcePath: path,
|
|
106
|
-
targetSkill: 'abap-design',
|
|
107
|
-
prompt: async (q) => { prompts.push(q); return 'n'; },
|
|
108
|
-
log: () => { },
|
|
109
|
-
});
|
|
110
|
-
expect(result).toBeNull();
|
|
111
|
-
expect(prompts.join('\n')).toMatch(/Promote anyway/i);
|
|
112
|
-
}
|
|
113
|
-
finally {
|
|
114
|
-
rmSync(dir, { recursive: true, force: true });
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
it('zero-answered source: prompts and respects "no"', async () => {
|
|
118
|
-
const dir = mkdtempSync(`${tmpdir()}/cspeach-promote-`);
|
|
119
|
-
try {
|
|
120
|
-
const empty = envelopeFixture({
|
|
121
|
-
interaction: { items: [
|
|
122
|
-
{ id: 'gap-001', status: 'open', answer: null, answeredBy: null, answeredAt: null, comments: [] },
|
|
123
|
-
{ id: 'gap-002', status: 'open', answer: null, answeredBy: null, answeredAt: null, comments: [] },
|
|
124
|
-
] },
|
|
125
|
-
});
|
|
126
|
-
const path = writeFixture(join(dir, 'src.cspeach.json'), empty);
|
|
127
|
-
const prompts = [];
|
|
128
|
-
const result = await runPromoteCommand({
|
|
129
|
-
sourcePath: path,
|
|
130
|
-
targetSkill: 'abap-design',
|
|
131
|
-
prompt: async (q) => { prompts.push(q); return ''; },
|
|
132
|
-
log: () => { },
|
|
133
|
-
});
|
|
134
|
-
expect(result).toBeNull();
|
|
135
|
-
expect(prompts.join('\n')).toMatch(/no answered items/i);
|
|
136
|
-
}
|
|
137
|
-
finally {
|
|
138
|
-
rmSync(dir, { recursive: true, force: true });
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
it('estimate target accepts a design source', async () => {
|
|
142
|
-
const dir = mkdtempSync(`${tmpdir()}/cspeach-promote-`);
|
|
143
|
-
try {
|
|
144
|
-
const designEnv = {
|
|
145
|
-
...envelopeFixture(),
|
|
146
|
-
artefactType: 'design',
|
|
147
|
-
content: { objects: [
|
|
148
|
-
{ id: 'obj-001', name: 'ZCL_X', type: 'CLAS', package: 'P', dependsOn: [] },
|
|
149
|
-
] },
|
|
150
|
-
interaction: { items: [
|
|
151
|
-
{ id: 'obj-001', status: 'keep', answer: null, answeredBy: null, answeredAt: null, comments: [] },
|
|
152
|
-
] },
|
|
153
|
-
};
|
|
154
|
-
const path = writeFixture(join(dir, 'src.cspeach.json'), designEnv);
|
|
155
|
-
const result = await runPromoteCommand({
|
|
156
|
-
sourcePath: path,
|
|
157
|
-
targetSkill: 'abap-estimate',
|
|
158
|
-
prompt: async () => 'y',
|
|
159
|
-
log: () => { },
|
|
160
|
-
});
|
|
161
|
-
expect(result).not.toBeNull();
|
|
162
|
-
expect(result.promotedFrom.artefactType).toBe('design');
|
|
163
|
-
expect(result.extendedSkillInput).toContain('Promoted from design');
|
|
164
|
-
expect(result.extendedSkillInput).toContain('obj-001: ZCL_X (CLAS) → keep');
|
|
165
|
-
}
|
|
166
|
-
finally {
|
|
167
|
-
rmSync(dir, { recursive: true, force: true });
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
});
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { buildPromotedFromSnapshot, validatePromotionSource } from '../promote.js';
|
|
3
|
-
const SPEC_GAP_SOURCE = {
|
|
4
|
-
schemaVersion: '1.0',
|
|
5
|
-
artefactType: 'spec-gap',
|
|
6
|
-
id: '11111111-2222-3333-4444-555555555555',
|
|
7
|
-
title: 'Customer Aging Report',
|
|
8
|
-
createdAt: '2026-05-04T12:00:00Z',
|
|
9
|
-
createdBy: { name: 'Laeeq', role: 'consultant' },
|
|
10
|
-
lastEditedAt: '2026-05-04T16:00:00Z',
|
|
11
|
-
lastEditedBy: { name: 'Sarah', role: 'BA' },
|
|
12
|
-
version: 3,
|
|
13
|
-
source: { skill: 'abap-spec-gap', skillVersion: '1.0', input: '', tokensUsed: 0, model: 'claude-opus-4-7' },
|
|
14
|
-
promotedFrom: null,
|
|
15
|
-
content: {
|
|
16
|
-
inputType: 'User Story', inferredDomain: 'FI',
|
|
17
|
-
sections: [
|
|
18
|
-
{ name: 'Missing Business Questions', audience: 'business', gaps: [
|
|
19
|
-
{ id: 'gap-001', severity: 'BLOCKER', text: 'What defines "open"?' },
|
|
20
|
-
{ id: 'gap-002', severity: 'IMPORTANT', text: 'Which currency rate?' },
|
|
21
|
-
] },
|
|
22
|
-
],
|
|
23
|
-
},
|
|
24
|
-
interaction: { items: [
|
|
25
|
-
{ id: 'gap-001', status: 'answered', answer: 'BSID open items only',
|
|
26
|
-
answeredBy: { name: 'Sarah', role: 'BA' }, answeredAt: '2026-05-04T16:00:00Z',
|
|
27
|
-
comments: [{ id: 'c1', by: { name: 'Sarah', role: 'BA' }, at: '2026-05-04T16:00:00Z', text: 'Confirmed with finance' }] },
|
|
28
|
-
{ id: 'gap-002', status: 'open', answer: null, answeredBy: null, answeredAt: null, comments: [] },
|
|
29
|
-
] },
|
|
30
|
-
history: [
|
|
31
|
-
{ at: '2026-05-04T12:00:00Z', by: { name: 'Laeeq', role: 'consultant' }, action: 'created' },
|
|
32
|
-
{ at: '2026-05-04T15:00:00Z', by: { name: 'Sarah', role: 'BA' }, action: 'edited', summary: '1 gap answered' },
|
|
33
|
-
{ at: '2026-05-04T16:00:00Z', by: { name: 'Sarah', role: 'BA' }, action: 'edited', summary: 'no changes' },
|
|
34
|
-
],
|
|
35
|
-
};
|
|
36
|
-
const DESIGN_SOURCE = {
|
|
37
|
-
...SPEC_GAP_SOURCE,
|
|
38
|
-
artefactType: 'design',
|
|
39
|
-
content: { objects: [
|
|
40
|
-
{ id: 'obj-001', name: 'ZCL_AGING', type: 'CLAS', package: 'ZAGING', dependsOn: [] },
|
|
41
|
-
{ id: 'obj-002', name: 'ZTBL_AGING', type: 'TABL', package: 'ZAGING', dependsOn: ['obj-001'] },
|
|
42
|
-
] },
|
|
43
|
-
interaction: { items: [
|
|
44
|
-
{ id: 'obj-001', status: 'keep', answer: null, answeredBy: null, answeredAt: null, comments: [] },
|
|
45
|
-
{ id: 'obj-002', status: 'redesign', answer: 'split into two', answeredBy: { name: 'A', role: 'arch' }, answeredAt: '2026-05-04T16:00:00Z', comments: [] },
|
|
46
|
-
] },
|
|
47
|
-
};
|
|
48
|
-
describe('buildPromotedFromSnapshot — spec-gap source', () => {
|
|
49
|
-
it('preserves id, title, artefactType, version', async () => {
|
|
50
|
-
const snap = await buildPromotedFromSnapshot(SPEC_GAP_SOURCE);
|
|
51
|
-
expect(snap.id).toBe(SPEC_GAP_SOURCE.id);
|
|
52
|
-
expect(snap.title).toBe('Customer Aging Report');
|
|
53
|
-
expect(snap.artefactType).toBe('spec-gap');
|
|
54
|
-
expect(snap.version).toBe(3);
|
|
55
|
-
});
|
|
56
|
-
it('emits ISO snapshottedAt', async () => {
|
|
57
|
-
const snap = await buildPromotedFromSnapshot(SPEC_GAP_SOURCE);
|
|
58
|
-
expect(snap.snapshottedAt).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
|
59
|
-
});
|
|
60
|
-
it('computes a 64-char hex sha256 over the canonicalised source', async () => {
|
|
61
|
-
const snap = await buildPromotedFromSnapshot(SPEC_GAP_SOURCE);
|
|
62
|
-
expect(snap.sha256).toMatch(/^[0-9a-f]{64}$/);
|
|
63
|
-
});
|
|
64
|
-
it('includes severity for spec-gap items, never comments', async () => {
|
|
65
|
-
const snap = await buildPromotedFromSnapshot(SPEC_GAP_SOURCE);
|
|
66
|
-
expect(snap.snapshot.items).toHaveLength(2);
|
|
67
|
-
expect(snap.snapshot.items[0]).toMatchObject({
|
|
68
|
-
id: 'gap-001',
|
|
69
|
-
text: 'What defines "open"?',
|
|
70
|
-
severity: 'BLOCKER',
|
|
71
|
-
status: 'answered',
|
|
72
|
-
answer: 'BSID open items only',
|
|
73
|
-
});
|
|
74
|
-
expect(snap.snapshot.items[1]).toMatchObject({
|
|
75
|
-
id: 'gap-002',
|
|
76
|
-
text: 'Which currency rate?',
|
|
77
|
-
severity: 'IMPORTANT',
|
|
78
|
-
status: 'open',
|
|
79
|
-
answer: null,
|
|
80
|
-
});
|
|
81
|
-
for (const it of snap.snapshot.items) {
|
|
82
|
-
expect(it).not.toHaveProperty('comments');
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
describe('buildPromotedFromSnapshot — design source', () => {
|
|
87
|
-
it('uses object name + type as text and omits severity', async () => {
|
|
88
|
-
const snap = await buildPromotedFromSnapshot(DESIGN_SOURCE);
|
|
89
|
-
expect(snap.snapshot.items).toHaveLength(2);
|
|
90
|
-
expect(snap.snapshot.items[0]).toMatchObject({
|
|
91
|
-
id: 'obj-001',
|
|
92
|
-
text: 'ZCL_AGING (CLAS)',
|
|
93
|
-
status: 'keep',
|
|
94
|
-
answer: null,
|
|
95
|
-
});
|
|
96
|
-
expect(snap.snapshot.items[0]).not.toHaveProperty('severity');
|
|
97
|
-
expect(snap.snapshot.items[1]).toMatchObject({
|
|
98
|
-
id: 'obj-002',
|
|
99
|
-
text: 'ZTBL_AGING (TABL)',
|
|
100
|
-
status: 'redesign',
|
|
101
|
-
answer: 'split into two',
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
describe('buildPromotedFromSnapshot — sha stability', () => {
|
|
106
|
-
it('returns the same sha for the same source content (timestamps stripped)', async () => {
|
|
107
|
-
const a = { ...SPEC_GAP_SOURCE, lastEditedAt: '2026-05-04T16:00:00Z',
|
|
108
|
-
history: [...SPEC_GAP_SOURCE.history.map((h) => ({ ...h, at: '2026-05-04T16:00:00Z' }))] };
|
|
109
|
-
const b = { ...SPEC_GAP_SOURCE, lastEditedAt: '2026-05-04T20:00:00Z',
|
|
110
|
-
history: [...SPEC_GAP_SOURCE.history.map((h) => ({ ...h, at: '2026-05-04T20:00:00Z' }))] };
|
|
111
|
-
const snapA = await buildPromotedFromSnapshot(a);
|
|
112
|
-
const snapB = await buildPromotedFromSnapshot(b);
|
|
113
|
-
expect(snapA.sha256).toBe(snapB.sha256);
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
describe('validatePromotionSource — predecessor type', () => {
|
|
117
|
-
it('hard-rejects when source artefactType does not match expected', () => {
|
|
118
|
-
const result = validatePromotionSource(SPEC_GAP_SOURCE, 'design');
|
|
119
|
-
expect(result.ok).toBe(false);
|
|
120
|
-
expect(result.error).toMatch(/expects a design source.*got spec-gap/);
|
|
121
|
-
});
|
|
122
|
-
it('accepts when source matches expected predecessor', () => {
|
|
123
|
-
const result = validatePromotionSource(SPEC_GAP_SOURCE, 'spec-gap');
|
|
124
|
-
expect(result.ok).toBe(true);
|
|
125
|
-
expect(result.error).toBeUndefined();
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
describe('validatePromotionSource — answered-state warnings', () => {
|
|
129
|
-
const fullyAnswered = {
|
|
130
|
-
...SPEC_GAP_SOURCE,
|
|
131
|
-
interaction: { items: SPEC_GAP_SOURCE.interaction.items.map((i) => ({
|
|
132
|
-
...i, status: 'answered', answer: 'A',
|
|
133
|
-
answeredBy: { name: 'X', role: 'BA' }, answeredAt: '2026-05-04T16:00:00Z',
|
|
134
|
-
})) },
|
|
135
|
-
};
|
|
136
|
-
const noneAnswered = {
|
|
137
|
-
...SPEC_GAP_SOURCE,
|
|
138
|
-
interaction: { items: SPEC_GAP_SOURCE.interaction.items.map((i) => ({
|
|
139
|
-
...i, status: 'open', answer: null, answeredBy: null, answeredAt: null,
|
|
140
|
-
})) },
|
|
141
|
-
};
|
|
142
|
-
it('emits no warning when source is fully answered', () => {
|
|
143
|
-
const r = validatePromotionSource(fullyAnswered, 'spec-gap');
|
|
144
|
-
expect(r.ok).toBe(true);
|
|
145
|
-
expect(r.warnings).toEqual([]);
|
|
146
|
-
});
|
|
147
|
-
it('emits ZERO_ANSWERED warning when no items are answered', () => {
|
|
148
|
-
const r = validatePromotionSource(noneAnswered, 'spec-gap');
|
|
149
|
-
expect(r.ok).toBe(true);
|
|
150
|
-
expect(r.warnings).toContainEqual({ kind: 'ZERO_ANSWERED', count: 2 });
|
|
151
|
-
});
|
|
152
|
-
it('emits PARTIAL_OPEN warning when some items still open', () => {
|
|
153
|
-
const r = validatePromotionSource(SPEC_GAP_SOURCE, 'spec-gap'); // 1 open, 1 answered
|
|
154
|
-
expect(r.ok).toBe(true);
|
|
155
|
-
expect(r.warnings).toContainEqual({ kind: 'PARTIAL_OPEN', openIds: ['gap-002'] });
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
describe('validatePromotionSource — multi-source (v0.7)', () => {
|
|
159
|
-
// The discriminated union ties artefactType ↔ content shape, but
|
|
160
|
-
// validatePromotionSource only inspects artefactType + interaction.items.
|
|
161
|
-
// Cast through unknown so tests can flip the artefactType label without
|
|
162
|
-
// having to construct a full upgrade-scan / cca-assessment content payload.
|
|
163
|
-
function withType(t) {
|
|
164
|
-
return { ...SPEC_GAP_SOURCE, artefactType: t };
|
|
165
|
-
}
|
|
166
|
-
it('accepts a source matching ANY type in the array', () => {
|
|
167
|
-
expect(validatePromotionSource(withType('upgrade-scan'), ['upgrade-scan', 'cca-assessment']).ok).toBe(true);
|
|
168
|
-
expect(validatePromotionSource(withType('cca-assessment'), ['upgrade-scan', 'cca-assessment']).ok).toBe(true);
|
|
169
|
-
});
|
|
170
|
-
it('rejects a source matching NONE of the array types', () => {
|
|
171
|
-
const r = validatePromotionSource(withType('spec-gap'), ['upgrade-scan', 'cca-assessment']);
|
|
172
|
-
expect(r.ok).toBe(false);
|
|
173
|
-
expect(r.error).toMatch(/expects one of \[upgrade-scan, cca-assessment\]/);
|
|
174
|
-
expect(r.error).toMatch(/got spec-gap/);
|
|
175
|
-
});
|
|
176
|
-
it('single-element array still produces "a <type>" phrasing in error', () => {
|
|
177
|
-
const r = validatePromotionSource(withType('spec-gap'), ['design']);
|
|
178
|
-
expect(r.ok).toBe(false);
|
|
179
|
-
expect(r.error).toMatch(/expects a design source/);
|
|
180
|
-
});
|
|
181
|
-
});
|