@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.
Files changed (107) hide show
  1. package/dist/index.js +0 -0
  2. package/package.json +86 -82
  3. package/dist/agent/__tests__/crash-recover.integration.test.js +0 -254
  4. package/dist/agent/__tests__/loop-save-hook.test.js +0 -188
  5. package/dist/agent/__tests__/maybe-build-project-context.test.js +0 -247
  6. package/dist/agent/__tests__/project-context-injection.test.js +0 -161
  7. package/dist/agent/__tests__/repair-partial.test.js +0 -79
  8. package/dist/agent/__tests__/retry-key.test.js +0 -77
  9. package/dist/agent/__tests__/sap-connection-adapter.test.js +0 -132
  10. package/dist/agent/__tests__/skill-checkpoint.test.js +0 -154
  11. package/dist/agent/__tests__/turn-assistant-text.test.js +0 -73
  12. package/dist/agent/__tests__/turn-error-ux.test.js +0 -145
  13. package/dist/agent/__tests__/turn-stream.test.js +0 -100
  14. package/dist/agent/__tests__/turn-watchdog.test.js +0 -126
  15. package/dist/agent/providers/__tests__/ai-hub-provider.test.js +0 -30
  16. package/dist/agent/providers/__tests__/byok-provider.test.js +0 -32
  17. package/dist/agent/providers/__tests__/factory.test.js +0 -33
  18. package/dist/agent/providers/__tests__/local-provider.test.js +0 -55
  19. package/dist/approvals/__tests__/advisory-prompt.test.js +0 -57
  20. package/dist/approvals/__tests__/advisory-render.test.js +0 -54
  21. package/dist/auth/__tests__/auth-file.test.js +0 -42
  22. package/dist/auth/__tests__/me.test.js +0 -43
  23. package/dist/classifier/__tests__/client.test.js +0 -70
  24. package/dist/commands/__tests__/config-set-write-mode.test.js +0 -14
  25. package/dist/commands/__tests__/login.test.js +0 -152
  26. package/dist/commands/__tests__/logout.test.js +0 -39
  27. package/dist/commands/__tests__/project-context-impact.test.js +0 -173
  28. package/dist/commands/__tests__/spec-gap-status.test.js +0 -31
  29. package/dist/commands/__tests__/whoami.test.js +0 -91
  30. package/dist/config/__tests__/llm-config.test.js +0 -28
  31. package/dist/config/__tests__/shell-exec-config.test.js +0 -76
  32. package/dist/config/__tests__/write-mode.test.js +0 -36
  33. package/dist/doctor/__tests__/check-forge-rules.test.js +0 -134
  34. package/dist/doctor/__tests__/check-llm-mode.test.js +0 -21
  35. package/dist/doctor/__tests__/check-write-mode.test.js +0 -34
  36. package/dist/project-context/__tests__/conventions.test.js +0 -221
  37. package/dist/project-context/__tests__/detect.test.js +0 -177
  38. package/dist/project-context/__tests__/domain-abap-cloud.test.js +0 -119
  39. package/dist/project-context/__tests__/domain-abapgit.test.js +0 -161
  40. package/dist/project-context/__tests__/domain-cap.test.js +0 -168
  41. package/dist/project-context/__tests__/domain-fiori.test.js +0 -282
  42. package/dist/project-context/__tests__/git.test.js +0 -131
  43. package/dist/project-context/__tests__/index-files.test.js +0 -195
  44. package/dist/project-context/__tests__/index.test.js +0 -133
  45. package/dist/project-context/__tests__/render.test.js +0 -322
  46. package/dist/project-context/__tests__/types.test.js +0 -73
  47. package/dist/projects/__tests__/build.test.js +0 -52
  48. package/dist/projects/__tests__/canonicalize.test.js +0 -47
  49. package/dist/projects/__tests__/email-template.test.js +0 -71
  50. package/dist/projects/__tests__/expand-text-attachments.test.js +0 -105
  51. package/dist/projects/__tests__/extract-cca.test.js +0 -141
  52. package/dist/projects/__tests__/extract-design.test.js +0 -62
  53. package/dist/projects/__tests__/extract-estimate.test.js +0 -58
  54. package/dist/projects/__tests__/extract-modernize.test.js +0 -127
  55. package/dist/projects/__tests__/extract-spec-gap.test.js +0 -122
  56. package/dist/projects/__tests__/extract-test-coverage.test.js +0 -133
  57. package/dist/projects/__tests__/filename.test.js +0 -32
  58. package/dist/projects/__tests__/promote-command.test.js +0 -170
  59. package/dist/projects/__tests__/promote.test.js +0 -181
  60. package/dist/projects/__tests__/save-command.test.js +0 -216
  61. package/dist/projects/__tests__/save.test.js +0 -41
  62. package/dist/projects/__tests__/status.test.js +0 -208
  63. package/dist/projects/__tests__/types.test.js +0 -41
  64. package/dist/projects/__tests__/validate.test.js +0 -165
  65. package/dist/projects/__tests__/workspace.test.js +0 -337
  66. package/dist/repl/__tests__/current-transport.test.js +0 -43
  67. package/dist/repl/__tests__/diff-display.test.js +0 -59
  68. package/dist/repl/__tests__/file-picker.test.js +0 -97
  69. package/dist/repl/__tests__/rule8-detector.test.js +0 -114
  70. package/dist/repl/__tests__/safety-confirm.test.js +0 -130
  71. package/dist/repl/__tests__/safety-mode-state.test.js +0 -42
  72. package/dist/sap/__tests__/system-info.test.js +0 -344
  73. package/dist/session/__tests__/store-discovery.test.js +0 -114
  74. package/dist/session/__tests__/time-ago.test.js +0 -53
  75. package/dist/skills/__tests__/canonical.test.js +0 -20
  76. package/dist/skills/__tests__/manifest-client.test.js +0 -88
  77. package/dist/skills/__tests__/promotion-dispatch.test.js +0 -28
  78. package/dist/skills/__tests__/source-managed.test.js +0 -60
  79. package/dist/tools/__tests__/_background-shared.test.js +0 -69
  80. package/dist/tools/__tests__/_command-shared.test.js +0 -59
  81. package/dist/tools/__tests__/_filesystem-shared.test.js +0 -42
  82. package/dist/tools/__tests__/_flag.test.js +0 -66
  83. package/dist/tools/__tests__/_helpers.js +0 -17
  84. package/dist/tools/__tests__/_web-shared.test.js +0 -143
  85. package/dist/tools/__tests__/agent_run.test.js +0 -166
  86. package/dist/tools/__tests__/approval-advisory.test.js +0 -66
  87. package/dist/tools/__tests__/background_run.test.js +0 -113
  88. package/dist/tools/__tests__/convention_get.test.js +0 -53
  89. package/dist/tools/__tests__/file-edit.test.js +0 -72
  90. package/dist/tools/__tests__/file-read.test.js +0 -81
  91. package/dist/tools/__tests__/file-write.test.js +0 -68
  92. package/dist/tools/__tests__/glob.test.js +0 -128
  93. package/dist/tools/__tests__/grep.test.js +0 -71
  94. package/dist/tools/__tests__/monitor_emit.test.js +0 -96
  95. package/dist/tools/__tests__/playbook_get.test.js +0 -109
  96. package/dist/tools/__tests__/project_context_get.test.js +0 -71
  97. package/dist/tools/__tests__/registry-category.test.js +0 -60
  98. package/dist/tools/__tests__/sap-read-extras.test.js +0 -196
  99. package/dist/tools/__tests__/sap-write-extras.test.js +0 -573
  100. package/dist/tools/__tests__/schedule_create.test.js +0 -85
  101. package/dist/tools/__tests__/shell_exec.test.js +0 -135
  102. package/dist/tools/__tests__/transport-safety.test.js +0 -106
  103. package/dist/tools/__tests__/update-method-intercept.test.js +0 -128
  104. package/dist/tools/__tests__/web_fetch.test.js +0 -329
  105. package/dist/tools/__tests__/web_search.test.js +0 -215
  106. package/dist/tools/__tests__/write-mode.test.js +0 -32
  107. package/dist/ui/__tests__/login-banner.test.js +0 -69
@@ -1,119 +0,0 @@
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
- });
@@ -1,161 +0,0 @@
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
- });
@@ -1,168 +0,0 @@
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
- });