@cspeach/cli 0.6.3 → 0.6.4
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/agent/__tests__/crash-recover.integration.test.js +254 -0
- package/dist/agent/__tests__/loop-save-hook.test.js +188 -0
- package/dist/agent/__tests__/maybe-build-project-context.test.js +247 -0
- package/dist/agent/__tests__/project-context-injection.test.js +161 -0
- package/dist/agent/__tests__/repair-partial.test.js +79 -0
- package/dist/agent/__tests__/retry-key.test.js +77 -0
- package/dist/agent/__tests__/sap-connection-adapter.test.js +132 -0
- package/dist/agent/__tests__/skill-checkpoint.test.js +154 -0
- package/dist/agent/__tests__/turn-assistant-text.test.js +73 -0
- package/dist/agent/__tests__/turn-error-ux.test.js +145 -0
- package/dist/agent/__tests__/turn-stream.test.js +100 -0
- package/dist/agent/__tests__/turn-watchdog.test.js +126 -0
- package/dist/agent/providers/__tests__/ai-hub-provider.test.js +30 -0
- package/dist/agent/providers/__tests__/byok-provider.test.js +32 -0
- package/dist/agent/providers/__tests__/factory.test.js +33 -0
- package/dist/agent/providers/__tests__/local-provider.test.js +55 -0
- package/dist/approvals/__tests__/advisory-prompt.test.js +57 -0
- package/dist/approvals/__tests__/advisory-render.test.js +54 -0
- package/dist/auth/__tests__/auth-file.test.js +42 -0
- package/dist/auth/__tests__/me.test.js +43 -0
- package/dist/classifier/__tests__/client.test.js +70 -0
- package/dist/commands/__tests__/config-set-write-mode.test.js +14 -0
- package/dist/commands/__tests__/login.test.js +152 -0
- package/dist/commands/__tests__/logout.test.js +39 -0
- package/dist/commands/__tests__/project-context-impact.test.js +173 -0
- package/dist/commands/__tests__/spec-gap-status.test.js +31 -0
- package/dist/commands/__tests__/whoami.test.js +91 -0
- package/dist/config/__tests__/llm-config.test.js +28 -0
- package/dist/config/__tests__/shell-exec-config.test.js +76 -0
- package/dist/config/__tests__/write-mode.test.js +36 -0
- package/dist/doctor/__tests__/check-forge-rules.test.js +134 -0
- package/dist/doctor/__tests__/check-llm-mode.test.js +21 -0
- package/dist/doctor/__tests__/check-write-mode.test.js +34 -0
- package/dist/index.js +0 -0
- package/dist/project-context/__tests__/conventions.test.js +221 -0
- package/dist/project-context/__tests__/detect.test.js +177 -0
- package/dist/project-context/__tests__/domain-abap-cloud.test.js +119 -0
- package/dist/project-context/__tests__/domain-abapgit.test.js +161 -0
- package/dist/project-context/__tests__/domain-cap.test.js +168 -0
- package/dist/project-context/__tests__/domain-fiori.test.js +282 -0
- package/dist/project-context/__tests__/git.test.js +131 -0
- package/dist/project-context/__tests__/index-files.test.js +195 -0
- package/dist/project-context/__tests__/index.test.js +133 -0
- package/dist/project-context/__tests__/render.test.js +322 -0
- package/dist/project-context/__tests__/types.test.js +73 -0
- package/dist/projects/__tests__/build.test.js +52 -0
- package/dist/projects/__tests__/canonicalize.test.js +47 -0
- package/dist/projects/__tests__/email-template.test.js +71 -0
- package/dist/projects/__tests__/expand-text-attachments.test.js +105 -0
- package/dist/projects/__tests__/extract-cca.test.js +141 -0
- package/dist/projects/__tests__/extract-design.test.js +62 -0
- package/dist/projects/__tests__/extract-estimate.test.js +58 -0
- package/dist/projects/__tests__/extract-modernize.test.js +127 -0
- package/dist/projects/__tests__/extract-spec-gap.test.js +122 -0
- package/dist/projects/__tests__/extract-test-coverage.test.js +133 -0
- package/dist/projects/__tests__/filename.test.js +32 -0
- package/dist/projects/__tests__/promote-command.test.js +170 -0
- package/dist/projects/__tests__/promote.test.js +181 -0
- package/dist/projects/__tests__/save-command.test.js +216 -0
- package/dist/projects/__tests__/save.test.js +41 -0
- package/dist/projects/__tests__/status.test.js +208 -0
- package/dist/projects/__tests__/types.test.js +41 -0
- package/dist/projects/__tests__/validate.test.js +165 -0
- package/dist/projects/__tests__/workspace.test.js +337 -0
- package/dist/repl/__tests__/current-transport.test.js +43 -0
- package/dist/repl/__tests__/diff-display.test.js +59 -0
- package/dist/repl/__tests__/file-picker.test.js +97 -0
- package/dist/repl/__tests__/rule8-detector.test.js +114 -0
- package/dist/repl/__tests__/safety-confirm.test.js +130 -0
- package/dist/repl/__tests__/safety-mode-state.test.js +42 -0
- package/dist/sap/__tests__/system-info.test.js +344 -0
- package/dist/session/__tests__/store-discovery.test.js +114 -0
- package/dist/session/__tests__/time-ago.test.js +53 -0
- package/dist/skills/__tests__/canonical.test.js +20 -0
- package/dist/skills/__tests__/manifest-client.test.js +88 -0
- package/dist/skills/__tests__/promotion-dispatch.test.js +28 -0
- package/dist/skills/__tests__/source-managed.test.js +60 -0
- package/dist/tools/__tests__/_background-shared.test.js +69 -0
- package/dist/tools/__tests__/_command-shared.test.js +59 -0
- package/dist/tools/__tests__/_filesystem-shared.test.js +42 -0
- package/dist/tools/__tests__/_flag.test.js +66 -0
- package/dist/tools/__tests__/_helpers.js +17 -0
- package/dist/tools/__tests__/_web-shared.test.js +143 -0
- package/dist/tools/__tests__/agent_run.test.js +166 -0
- package/dist/tools/__tests__/approval-advisory.test.js +66 -0
- package/dist/tools/__tests__/background_run.test.js +113 -0
- package/dist/tools/__tests__/convention_get.test.js +53 -0
- package/dist/tools/__tests__/file-edit.test.js +72 -0
- package/dist/tools/__tests__/file-read.test.js +81 -0
- package/dist/tools/__tests__/file-write.test.js +68 -0
- package/dist/tools/__tests__/glob.test.js +128 -0
- package/dist/tools/__tests__/grep.test.js +71 -0
- package/dist/tools/__tests__/monitor_emit.test.js +96 -0
- package/dist/tools/__tests__/playbook_get.test.js +109 -0
- package/dist/tools/__tests__/project_context_get.test.js +71 -0
- package/dist/tools/__tests__/registry-category.test.js +60 -0
- package/dist/tools/__tests__/sap-read-extras.test.js +196 -0
- package/dist/tools/__tests__/sap-write-extras.test.js +573 -0
- package/dist/tools/__tests__/schedule_create.test.js +85 -0
- package/dist/tools/__tests__/shell_exec.test.js +135 -0
- package/dist/tools/__tests__/transport-safety.test.js +106 -0
- package/dist/tools/__tests__/update-method-intercept.test.js +128 -0
- package/dist/tools/__tests__/web_fetch.test.js +329 -0
- package/dist/tools/__tests__/web_search.test.js +215 -0
- package/dist/tools/__tests__/write-mode.test.js +32 -0
- package/dist/ui/__tests__/login-banner.test.js +69 -0
- package/package.json +82 -83
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { extractAbapCloud } from '../domain/abap-cloud.js';
|
|
6
|
+
let root;
|
|
7
|
+
beforeEach(async () => {
|
|
8
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'cspeach-abap-cloud-'));
|
|
9
|
+
});
|
|
10
|
+
afterEach(async () => {
|
|
11
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
12
|
+
});
|
|
13
|
+
async function writeFile(rel, content = '') {
|
|
14
|
+
const full = path.join(root, rel);
|
|
15
|
+
await fs.mkdir(path.dirname(full), { recursive: true });
|
|
16
|
+
await fs.writeFile(full, content, 'utf-8');
|
|
17
|
+
}
|
|
18
|
+
describe('extractAbapCloud — empty / missing', () => {
|
|
19
|
+
it('returns empty objects + empty behaviorDefinitions when src/ is absent', async () => {
|
|
20
|
+
const result = await extractAbapCloud(root);
|
|
21
|
+
expect(result).toEqual({
|
|
22
|
+
type: 'abap-cloud',
|
|
23
|
+
objects: [],
|
|
24
|
+
behaviorDefinitions: [],
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
it('returns empty arrays for a src/ with no recognized abap files', async () => {
|
|
28
|
+
await writeFile('src/notes.txt');
|
|
29
|
+
const result = await extractAbapCloud(root);
|
|
30
|
+
if (result.type !== 'abap-cloud')
|
|
31
|
+
throw new Error('wrong variant');
|
|
32
|
+
expect(result.objects).toEqual([]);
|
|
33
|
+
expect(result.behaviorDefinitions).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('extractAbapCloud — happy path', () => {
|
|
37
|
+
it('promotes behavior definitions into a dedicated list', async () => {
|
|
38
|
+
await writeFile('src/zr_order.bdef.asbdef', '');
|
|
39
|
+
await writeFile('src/zr_invoice.bdef.asbdef', '');
|
|
40
|
+
await writeFile('src/zcl_order_helper.clas.abap', '');
|
|
41
|
+
const result = await extractAbapCloud(root);
|
|
42
|
+
if (result.type !== 'abap-cloud')
|
|
43
|
+
throw new Error('wrong variant');
|
|
44
|
+
expect(result.behaviorDefinitions.sort()).toEqual(['ZR_INVOICE', 'ZR_ORDER']);
|
|
45
|
+
});
|
|
46
|
+
it('still surfaces non-BDEF RAP objects in objects[]', async () => {
|
|
47
|
+
await writeFile('src/zsd_order.srvd.srvd', '');
|
|
48
|
+
await writeFile('src/zsd_order.srvb.srvb', '');
|
|
49
|
+
await writeFile('src/zr_order.dcls.asdcls', '');
|
|
50
|
+
await writeFile('src/zc_order.ddlx.asddlx', '');
|
|
51
|
+
const result = await extractAbapCloud(root);
|
|
52
|
+
if (result.type !== 'abap-cloud')
|
|
53
|
+
throw new Error('wrong variant');
|
|
54
|
+
const types = result.objects.map((o) => o.type).sort();
|
|
55
|
+
expect(types).toEqual(['DCLS', 'DDLX', 'SRVB', 'SRVD']);
|
|
56
|
+
// None of these are BDEFs.
|
|
57
|
+
expect(result.behaviorDefinitions).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
it('infers package from folder for cloud objects too', async () => {
|
|
60
|
+
await writeFile('src/zsd/zsd_order/zr_order.bdef.asbdef', '');
|
|
61
|
+
const result = await extractAbapCloud(root);
|
|
62
|
+
if (result.type !== 'abap-cloud')
|
|
63
|
+
throw new Error('wrong variant');
|
|
64
|
+
expect(result.objects[0]).toMatchObject({
|
|
65
|
+
name: 'ZR_ORDER',
|
|
66
|
+
type: 'BDEF',
|
|
67
|
+
package: 'ZSD_ORDER',
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('extractAbapCloud — realistic RAP stack', () => {
|
|
72
|
+
it('handles a complete RAP root entity stack', async () => {
|
|
73
|
+
await writeFile('src/zsd/zsd_orders/ztorder.tabl.xml', '');
|
|
74
|
+
await writeFile('src/zsd/zsd_orders/zi_order.ddls.asddls', '');
|
|
75
|
+
await writeFile('src/zsd/zsd_orders/zc_order.ddls.asddls', '');
|
|
76
|
+
await writeFile('src/zsd/zsd_orders/zc_order.ddlx.asddlx', '');
|
|
77
|
+
await writeFile('src/zsd/zsd_orders/zr_order.bdef.asbdef', '');
|
|
78
|
+
await writeFile('src/zsd/zsd_orders/zbp_zr_order.clas.abap', '');
|
|
79
|
+
await writeFile('src/zsd/zsd_orders/zsd_order.srvd.srvd', '');
|
|
80
|
+
await writeFile('src/zsd/zsd_orders/zsd_order_o4.srvb.srvb', '');
|
|
81
|
+
await writeFile('src/zsd/zsd_orders/zr_order.dcls.asdcls', '');
|
|
82
|
+
const result = await extractAbapCloud(root);
|
|
83
|
+
if (result.type !== 'abap-cloud')
|
|
84
|
+
throw new Error('wrong variant');
|
|
85
|
+
// 9 distinct objects, all in package ZSD_ORDERS.
|
|
86
|
+
expect(result.objects).toHaveLength(9);
|
|
87
|
+
expect(result.objects.every((o) => o.package === 'ZSD_ORDERS')).toBe(true);
|
|
88
|
+
expect(result.behaviorDefinitions).toEqual(['ZR_ORDER']);
|
|
89
|
+
});
|
|
90
|
+
it('returns behavior definitions in the same order they appear in objects[]', async () => {
|
|
91
|
+
// objects[] is sorted by name then type — behaviorDefinitions inherits that.
|
|
92
|
+
await writeFile('src/zr_omega.bdef.asbdef', '');
|
|
93
|
+
await writeFile('src/zr_alpha.bdef.asbdef', '');
|
|
94
|
+
await writeFile('src/zr_middle.bdef.asbdef', '');
|
|
95
|
+
const result = await extractAbapCloud(root);
|
|
96
|
+
if (result.type !== 'abap-cloud')
|
|
97
|
+
throw new Error('wrong variant');
|
|
98
|
+
expect(result.behaviorDefinitions).toEqual(['ZR_ALPHA', 'ZR_MIDDLE', 'ZR_OMEGA']);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe('extractAbapCloud — fault tolerance', () => {
|
|
102
|
+
it('does not throw on missing cwd', async () => {
|
|
103
|
+
const result = await extractAbapCloud(path.join(root, 'nope'));
|
|
104
|
+
expect(result).toEqual({
|
|
105
|
+
type: 'abap-cloud',
|
|
106
|
+
objects: [],
|
|
107
|
+
behaviorDefinitions: [],
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
it('does not throw when src is a file', async () => {
|
|
111
|
+
await writeFile('src', 'not a dir');
|
|
112
|
+
const result = await extractAbapCloud(root);
|
|
113
|
+
expect(result).toEqual({
|
|
114
|
+
type: 'abap-cloud',
|
|
115
|
+
objects: [],
|
|
116
|
+
behaviorDefinitions: [],
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { extractAbapGit, parseAbapFilename } from '../domain/abapgit.js';
|
|
6
|
+
let root;
|
|
7
|
+
beforeEach(async () => {
|
|
8
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'cspeach-abapgit-'));
|
|
9
|
+
});
|
|
10
|
+
afterEach(async () => {
|
|
11
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
12
|
+
});
|
|
13
|
+
async function writeFile(rel, content = '') {
|
|
14
|
+
const full = path.join(root, rel);
|
|
15
|
+
await fs.mkdir(path.dirname(full), { recursive: true });
|
|
16
|
+
await fs.writeFile(full, content, 'utf-8');
|
|
17
|
+
}
|
|
18
|
+
describe('parseAbapFilename', () => {
|
|
19
|
+
it('parses program filename', () => {
|
|
20
|
+
expect(parseAbapFilename('zhello.prog.abap')).toEqual({ name: 'ZHELLO', type: 'PROG' });
|
|
21
|
+
});
|
|
22
|
+
it('parses class filename', () => {
|
|
23
|
+
expect(parseAbapFilename('zcl_foo.clas.abap')).toEqual({ name: 'ZCL_FOO', type: 'CLAS' });
|
|
24
|
+
});
|
|
25
|
+
it('parses interface filename', () => {
|
|
26
|
+
expect(parseAbapFilename('zif_bar.intf.abap')).toEqual({ name: 'ZIF_BAR', type: 'INTF' });
|
|
27
|
+
});
|
|
28
|
+
it('parses table xml filename', () => {
|
|
29
|
+
expect(parseAbapFilename('ztab.tabl.xml')).toEqual({ name: 'ZTAB', type: 'TABL' });
|
|
30
|
+
});
|
|
31
|
+
it('parses CDS DDLS filename', () => {
|
|
32
|
+
expect(parseAbapFilename('zi_view.ddls.asddls')).toEqual({ name: 'ZI_VIEW', type: 'DDLS' });
|
|
33
|
+
});
|
|
34
|
+
it('returns null for non-conforming filenames', () => {
|
|
35
|
+
expect(parseAbapFilename('README.md')).toBeNull();
|
|
36
|
+
expect(parseAbapFilename('.gitignore')).toBeNull();
|
|
37
|
+
expect(parseAbapFilename('package.json')).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
it('returns null when type token is unknown', () => {
|
|
40
|
+
// .foo. is not a recognized abapGit type segment.
|
|
41
|
+
expect(parseAbapFilename('zsomething.foo.abap')).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
it('uppercases lowercase object names', () => {
|
|
44
|
+
expect(parseAbapFilename('mixed_Case.clas.abap')).toEqual({
|
|
45
|
+
name: 'MIXED_CASE',
|
|
46
|
+
type: 'CLAS',
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe('extractAbapGit — empty / missing src', () => {
|
|
51
|
+
it('returns empty objects when src/ is absent', async () => {
|
|
52
|
+
const result = await extractAbapGit(root);
|
|
53
|
+
expect(result).toEqual({ type: 'abapgit', objects: [] });
|
|
54
|
+
});
|
|
55
|
+
it('returns empty objects when src/ has no abap files', async () => {
|
|
56
|
+
await writeFile('src/notes.txt', 'just a note');
|
|
57
|
+
await writeFile('src/README.md', '# repo');
|
|
58
|
+
const result = await extractAbapGit(root);
|
|
59
|
+
expect(result.type).toBe('abapgit');
|
|
60
|
+
if (result.type === 'abapgit') {
|
|
61
|
+
expect(result.objects).toEqual([]);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('extractAbapGit — happy path', () => {
|
|
66
|
+
it('collects mixed object types from a flat src/', async () => {
|
|
67
|
+
await writeFile('src/zhello.prog.abap', 'REPORT zhello.');
|
|
68
|
+
await writeFile('src/zcl_foo.clas.abap', 'CLASS zcl_foo DEFINITION.');
|
|
69
|
+
await writeFile('src/zif_bar.intf.abap', 'INTERFACE zif_bar.');
|
|
70
|
+
await writeFile('src/ztab.tabl.xml', '<xml/>');
|
|
71
|
+
const result = await extractAbapGit(root);
|
|
72
|
+
if (result.type !== 'abapgit')
|
|
73
|
+
throw new Error('wrong variant');
|
|
74
|
+
const names = result.objects.map((o) => `${o.name}:${o.type}`).sort();
|
|
75
|
+
expect(names).toEqual([
|
|
76
|
+
'ZCL_FOO:CLAS',
|
|
77
|
+
'ZHELLO:PROG',
|
|
78
|
+
'ZIF_BAR:INTF',
|
|
79
|
+
'ZTAB:TABL',
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
it('infers package from the immediate parent folder, uppercased', async () => {
|
|
83
|
+
await writeFile('src/zfi/zfi_payments/zcl_payment.clas.abap', '');
|
|
84
|
+
const result = await extractAbapGit(root);
|
|
85
|
+
if (result.type !== 'abapgit')
|
|
86
|
+
throw new Error('wrong variant');
|
|
87
|
+
expect(result.objects).toHaveLength(1);
|
|
88
|
+
expect(result.objects[0]).toMatchObject({
|
|
89
|
+
name: 'ZCL_PAYMENT',
|
|
90
|
+
type: 'CLAS',
|
|
91
|
+
package: 'ZFI_PAYMENTS',
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
it('uses forward-slash relative paths regardless of platform', async () => {
|
|
95
|
+
await writeFile('src/zfi/zhello.prog.abap', '');
|
|
96
|
+
const result = await extractAbapGit(root);
|
|
97
|
+
if (result.type !== 'abapgit')
|
|
98
|
+
throw new Error('wrong variant');
|
|
99
|
+
expect(result.objects[0].path).toBe('src/zfi/zhello.prog.abap');
|
|
100
|
+
});
|
|
101
|
+
it('dedupes multi-file objects (e.g. class + locals)', async () => {
|
|
102
|
+
// abapGit emits multiple files per class — main + locals_def + locals_imp + tests.
|
|
103
|
+
// All share the same (name, type) pair; we keep only the first.
|
|
104
|
+
await writeFile('src/zcl_foo.clas.abap', '');
|
|
105
|
+
await writeFile('src/zcl_foo.clas.locals_def.abap', '');
|
|
106
|
+
await writeFile('src/zcl_foo.clas.locals_imp.abap', '');
|
|
107
|
+
await writeFile('src/zcl_foo.clas.testclasses.abap', '');
|
|
108
|
+
const result = await extractAbapGit(root);
|
|
109
|
+
if (result.type !== 'abapgit')
|
|
110
|
+
throw new Error('wrong variant');
|
|
111
|
+
// The dedup key is name+type, but the secondary files like
|
|
112
|
+
// .clas.locals_def.abap have type 'LOCALS_DEF' (unknown) and are
|
|
113
|
+
// skipped at parse time. Only the main .clas.abap survives.
|
|
114
|
+
const clasEntries = result.objects.filter((o) => o.name === 'ZCL_FOO' && o.type === 'CLAS');
|
|
115
|
+
expect(clasEntries).toHaveLength(1);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe('extractAbapGit — realistic package fixture', () => {
|
|
119
|
+
it('handles a small multi-package repo', async () => {
|
|
120
|
+
await writeFile('src/package.devc.xml', '<xml/>');
|
|
121
|
+
await writeFile('src/zfi_root.devc.xml', '<xml/>');
|
|
122
|
+
await writeFile('src/zfi/zfi_payments.devc.xml', '<xml/>');
|
|
123
|
+
await writeFile('src/zfi/zfi_payments/zcl_payment.clas.abap', '');
|
|
124
|
+
await writeFile('src/zfi/zfi_payments/zif_payment.intf.abap', '');
|
|
125
|
+
await writeFile('src/zfi/zfi_payments/zpayment.tabl.xml', '');
|
|
126
|
+
await writeFile('src/zhr/zhr_employee.clas.abap', '');
|
|
127
|
+
await writeFile('src/README.md', '# repo');
|
|
128
|
+
const result = await extractAbapGit(root);
|
|
129
|
+
if (result.type !== 'abapgit')
|
|
130
|
+
throw new Error('wrong variant');
|
|
131
|
+
const byKey = new Map(result.objects.map((o) => [`${o.name}:${o.type}`, o]));
|
|
132
|
+
expect(byKey.size).toBe(7);
|
|
133
|
+
expect(byKey.get('ZCL_PAYMENT:CLAS')?.package).toBe('ZFI_PAYMENTS');
|
|
134
|
+
expect(byKey.get('ZIF_PAYMENT:INTF')?.package).toBe('ZFI_PAYMENTS');
|
|
135
|
+
expect(byKey.get('ZHR_EMPLOYEE:CLAS')?.package).toBe('ZHR');
|
|
136
|
+
});
|
|
137
|
+
it('returns objects sorted by name then type for stable ordering', async () => {
|
|
138
|
+
await writeFile('src/zbeta.prog.abap', '');
|
|
139
|
+
await writeFile('src/zalpha.prog.abap', '');
|
|
140
|
+
await writeFile('src/zalpha.tabl.xml', '');
|
|
141
|
+
const result = await extractAbapGit(root);
|
|
142
|
+
if (result.type !== 'abapgit')
|
|
143
|
+
throw new Error('wrong variant');
|
|
144
|
+
expect(result.objects.map((o) => `${o.name}:${o.type}`)).toEqual([
|
|
145
|
+
'ZALPHA:PROG',
|
|
146
|
+
'ZALPHA:TABL',
|
|
147
|
+
'ZBETA:PROG',
|
|
148
|
+
]);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
describe('extractAbapGit — fault tolerance', () => {
|
|
152
|
+
it('does not throw when src is a file rather than a directory', async () => {
|
|
153
|
+
await writeFile('src', 'not a dir');
|
|
154
|
+
const result = await extractAbapGit(root);
|
|
155
|
+
expect(result).toEqual({ type: 'abapgit', objects: [] });
|
|
156
|
+
});
|
|
157
|
+
it('does not throw when cwd does not exist', async () => {
|
|
158
|
+
const result = await extractAbapGit(path.join(root, 'nope'));
|
|
159
|
+
expect(result).toEqual({ type: 'abapgit', objects: [] });
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { extractCap } from '../domain/cap.js';
|
|
6
|
+
let root;
|
|
7
|
+
beforeEach(async () => {
|
|
8
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'cspeach-cap-'));
|
|
9
|
+
});
|
|
10
|
+
afterEach(async () => {
|
|
11
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
12
|
+
});
|
|
13
|
+
async function writeFile(rel, content = '') {
|
|
14
|
+
const full = path.join(root, rel);
|
|
15
|
+
await fs.mkdir(path.dirname(full), { recursive: true });
|
|
16
|
+
await fs.writeFile(full, content, 'utf-8');
|
|
17
|
+
}
|
|
18
|
+
describe('extractCap — empty / missing', () => {
|
|
19
|
+
it('returns empty arrays when neither db/ nor srv/ exists', async () => {
|
|
20
|
+
const result = await extractCap(root);
|
|
21
|
+
expect(result).toEqual({ type: 'cap-node', services: [], entities: [] });
|
|
22
|
+
});
|
|
23
|
+
it('returns empty arrays when db/ + srv/ exist but contain no .cds', async () => {
|
|
24
|
+
await writeFile('db/notes.txt');
|
|
25
|
+
await writeFile('srv/readme.md');
|
|
26
|
+
const result = await extractCap(root);
|
|
27
|
+
if (result.type !== 'cap-node')
|
|
28
|
+
throw new Error('wrong variant');
|
|
29
|
+
expect(result.services).toEqual([]);
|
|
30
|
+
expect(result.entities).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('extractCap — happy path', () => {
|
|
34
|
+
it('extracts service names from srv/*.cds', async () => {
|
|
35
|
+
await writeFile('srv/cat-service.cds', `using my.bookshop from '../db/schema';
|
|
36
|
+
|
|
37
|
+
service CatalogService {
|
|
38
|
+
entity Books as projection on bookshop.Books;
|
|
39
|
+
entity Authors as projection on bookshop.Authors;
|
|
40
|
+
}`);
|
|
41
|
+
const result = await extractCap(root);
|
|
42
|
+
if (result.type !== 'cap-node')
|
|
43
|
+
throw new Error('wrong variant');
|
|
44
|
+
expect(result.services.map((s) => s.name)).toEqual(['CatalogService']);
|
|
45
|
+
const cat = result.services[0];
|
|
46
|
+
expect(cat.path).toBe('srv/cat-service.cds');
|
|
47
|
+
expect(cat.entities.sort()).toEqual(['Authors', 'Books']);
|
|
48
|
+
});
|
|
49
|
+
it('extracts entity names from db/*.cds', async () => {
|
|
50
|
+
await writeFile('db/schema.cds', `namespace my.bookshop;
|
|
51
|
+
|
|
52
|
+
entity Books {
|
|
53
|
+
key ID : Integer;
|
|
54
|
+
title : String;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
entity Authors {
|
|
58
|
+
key ID : Integer;
|
|
59
|
+
name : String;
|
|
60
|
+
}`);
|
|
61
|
+
const result = await extractCap(root);
|
|
62
|
+
if (result.type !== 'cap-node')
|
|
63
|
+
throw new Error('wrong variant');
|
|
64
|
+
expect(result.entities.map((e) => e.name).sort()).toEqual(['Authors', 'Books']);
|
|
65
|
+
expect(result.entities.every((e) => e.path === 'db/schema.cds')).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
it('flags @cds.persistence.exists entities as external-table sourced', async () => {
|
|
68
|
+
await writeFile('db/external.cds', `@cds.persistence.exists
|
|
69
|
+
entity LegacyMaterial {
|
|
70
|
+
key Matnr : String;
|
|
71
|
+
}`);
|
|
72
|
+
const result = await extractCap(root);
|
|
73
|
+
if (result.type !== 'cap-node')
|
|
74
|
+
throw new Error('wrong variant');
|
|
75
|
+
expect(result.entities[0]).toMatchObject({
|
|
76
|
+
name: 'LegacyMaterial',
|
|
77
|
+
source: 'external-table',
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
it('handles multiple services declared across multiple files', async () => {
|
|
81
|
+
await writeFile('srv/admin.cds', 'service AdminService { entity Logs; }');
|
|
82
|
+
await writeFile('srv/public.cds', 'service PublicService { entity Catalog; }');
|
|
83
|
+
const result = await extractCap(root);
|
|
84
|
+
if (result.type !== 'cap-node')
|
|
85
|
+
throw new Error('wrong variant');
|
|
86
|
+
expect(result.services.map((s) => s.name)).toEqual(['AdminService', 'PublicService']);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe('extractCap — comment + dedup handling', () => {
|
|
90
|
+
it('ignores entity declarations inside line comments', async () => {
|
|
91
|
+
await writeFile('db/schema.cds', `// entity Disabled { ... }
|
|
92
|
+
entity RealOne { key ID : Integer; }`);
|
|
93
|
+
const result = await extractCap(root);
|
|
94
|
+
if (result.type !== 'cap-node')
|
|
95
|
+
throw new Error('wrong variant');
|
|
96
|
+
expect(result.entities.map((e) => e.name)).toEqual(['RealOne']);
|
|
97
|
+
});
|
|
98
|
+
it('dedupes a service-file entity referenced multiple times', async () => {
|
|
99
|
+
// CAP allows redeclaration / re-projection — we want one entry per name.
|
|
100
|
+
await writeFile('srv/dup.cds', `service S { entity Item; }
|
|
101
|
+
extend service S with { entity Item; }`);
|
|
102
|
+
const result = await extractCap(root);
|
|
103
|
+
if (result.type !== 'cap-node')
|
|
104
|
+
throw new Error('wrong variant');
|
|
105
|
+
expect(result.services[0].entities).toEqual(['Item']);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe('extractCap — realistic bookshop fixture', () => {
|
|
109
|
+
it('extracts a small bookshop project', async () => {
|
|
110
|
+
await writeFile('db/schema.cds', `namespace sap.capire.bookshop;
|
|
111
|
+
|
|
112
|
+
entity Books {
|
|
113
|
+
key ID : Integer;
|
|
114
|
+
title : localized String;
|
|
115
|
+
author : Association to Authors;
|
|
116
|
+
stock : Integer;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
entity Authors {
|
|
120
|
+
key ID : Integer;
|
|
121
|
+
name : String;
|
|
122
|
+
books : Composition of many Books on books.author = $self;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
entity Genres {
|
|
126
|
+
key ID : Integer;
|
|
127
|
+
name : String;
|
|
128
|
+
}`);
|
|
129
|
+
await writeFile('srv/cat-service.cds', `using sap.capire.bookshop as my from '../db/schema';
|
|
130
|
+
|
|
131
|
+
service CatalogService @(path:'/browse') {
|
|
132
|
+
@readonly entity Books as projection on my.Books;
|
|
133
|
+
@readonly entity Authors as projection on my.Authors;
|
|
134
|
+
}`);
|
|
135
|
+
await writeFile('srv/admin-service.cds', `using sap.capire.bookshop as my from '../db/schema';
|
|
136
|
+
|
|
137
|
+
service AdminService {
|
|
138
|
+
entity Books as projection on my.Books;
|
|
139
|
+
entity Authors as projection on my.Authors;
|
|
140
|
+
entity Genres as projection on my.Genres;
|
|
141
|
+
}`);
|
|
142
|
+
const result = await extractCap(root);
|
|
143
|
+
if (result.type !== 'cap-node')
|
|
144
|
+
throw new Error('wrong variant');
|
|
145
|
+
expect(result.services.map((s) => s.name).sort()).toEqual([
|
|
146
|
+
'AdminService',
|
|
147
|
+
'CatalogService',
|
|
148
|
+
]);
|
|
149
|
+
const admin = result.services.find((s) => s.name === 'AdminService');
|
|
150
|
+
expect(admin.entities.sort()).toEqual(['Authors', 'Books', 'Genres']);
|
|
151
|
+
const dbEntities = result.entities.filter((e) => e.path === 'db/schema.cds');
|
|
152
|
+
expect(dbEntities.map((e) => e.name).sort()).toEqual(['Authors', 'Books', 'Genres']);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
describe('extractCap — fault tolerance', () => {
|
|
156
|
+
it('does not throw on missing cwd', async () => {
|
|
157
|
+
const result = await extractCap(path.join(root, 'nope'));
|
|
158
|
+
expect(result).toEqual({ type: 'cap-node', services: [], entities: [] });
|
|
159
|
+
});
|
|
160
|
+
it('skips files larger than the safety cap (best-effort, no crash)', async () => {
|
|
161
|
+
// Write a 1.5 MB .cds file with a real entity declaration; we should
|
|
162
|
+
// skip it (size cap) and still return cleanly.
|
|
163
|
+
const padding = 'x'.repeat(1_500_000);
|
|
164
|
+
await writeFile('db/huge.cds', `entity Real { key ID : Integer; } // ${padding}`);
|
|
165
|
+
const result = await extractCap(root);
|
|
166
|
+
expect(result).toEqual({ type: 'cap-node', services: [], entities: [] });
|
|
167
|
+
});
|
|
168
|
+
});
|