@cspeach/cli 0.6.4 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/dist/agent/intent-system-prompt.js +46 -0
  2. package/dist/agent/loop.js +180 -18
  3. package/dist/agent/parallel-write-guard.js +71 -0
  4. package/dist/agent/repair-partial.js +88 -1
  5. package/dist/agent/retry-cap.js +121 -0
  6. package/dist/agent/summarise-via-provider.js +51 -0
  7. package/dist/approvals/jwt.js +33 -12
  8. package/dist/commands/auto-compact.js +94 -0
  9. package/dist/commands/compact.js +265 -0
  10. package/dist/commands/cost.js +56 -0
  11. package/dist/commands/help.js +21 -14
  12. package/dist/config/loader.js +39 -0
  13. package/dist/cost/cost-log.js +113 -0
  14. package/dist/cost/pricing.js +91 -0
  15. package/dist/index.js +0 -0
  16. package/dist/one-shot.js +1 -1
  17. package/dist/renderer/markdown.js +7 -1
  18. package/dist/renderer/status-footer.js +37 -14
  19. package/dist/renderer/thinking-heartbeat.js +15 -3
  20. package/dist/renderer/tool-widget.js +6 -1
  21. package/dist/renderer/tty.js +27 -0
  22. package/dist/repl/cspeach-shell-detect.js +33 -0
  23. package/dist/repl/post-turn-status.js +68 -0
  24. package/dist/repl/slash-completer.js +2 -0
  25. package/dist/repl/slash-picker.js +2 -0
  26. package/dist/repl.js +371 -46
  27. package/dist/sap-errors/parse-adt-exception.js +149 -0
  28. package/dist/session/schema.js +2 -1
  29. package/dist/session/store.js +19 -0
  30. package/dist/tools/_filesystem-shared.js +9 -1
  31. package/dist/tools/ask-question.js +19 -0
  32. package/dist/tools/sap-read.js +56 -4
  33. package/dist/tools/sap-write.js +19 -0
  34. package/dist/tools/subagent/schedule_draft_create.js +137 -0
  35. package/dist/ui/alt-screen.js +120 -0
  36. package/dist/ui/app.js +39 -15
  37. package/dist/ui/ask-question-emitter.js +13 -0
  38. package/dist/ui/body.js +39 -23
  39. package/dist/ui/coaching-picker-classic.js +8 -1
  40. package/dist/ui/footer.js +6 -1
  41. package/dist/ui/header.js +19 -4
  42. package/dist/ui/sap-state-store.js +13 -1
  43. package/dist/ui/sidebar.js +5 -3
  44. package/dist/ui/status-emitter.js +19 -0
  45. package/dist/ui/status-row.js +30 -4
  46. package/dist/ui/widgets/ask-question-modal.js +132 -0
  47. package/dist/ui/widgets/coaching-picker.js +55 -8
  48. package/package.json +86 -82
  49. package/dist/agent/__tests__/crash-recover.integration.test.js +0 -254
  50. package/dist/agent/__tests__/loop-save-hook.test.js +0 -188
  51. package/dist/agent/__tests__/maybe-build-project-context.test.js +0 -247
  52. package/dist/agent/__tests__/project-context-injection.test.js +0 -161
  53. package/dist/agent/__tests__/repair-partial.test.js +0 -79
  54. package/dist/agent/__tests__/retry-key.test.js +0 -77
  55. package/dist/agent/__tests__/sap-connection-adapter.test.js +0 -132
  56. package/dist/agent/__tests__/skill-checkpoint.test.js +0 -154
  57. package/dist/agent/__tests__/turn-assistant-text.test.js +0 -73
  58. package/dist/agent/__tests__/turn-error-ux.test.js +0 -145
  59. package/dist/agent/__tests__/turn-stream.test.js +0 -100
  60. package/dist/agent/__tests__/turn-watchdog.test.js +0 -126
  61. package/dist/agent/providers/__tests__/ai-hub-provider.test.js +0 -30
  62. package/dist/agent/providers/__tests__/byok-provider.test.js +0 -32
  63. package/dist/agent/providers/__tests__/factory.test.js +0 -33
  64. package/dist/agent/providers/__tests__/local-provider.test.js +0 -55
  65. package/dist/approvals/__tests__/advisory-prompt.test.js +0 -57
  66. package/dist/approvals/__tests__/advisory-render.test.js +0 -54
  67. package/dist/auth/__tests__/auth-file.test.js +0 -42
  68. package/dist/auth/__tests__/me.test.js +0 -43
  69. package/dist/classifier/__tests__/client.test.js +0 -70
  70. package/dist/commands/__tests__/config-set-write-mode.test.js +0 -14
  71. package/dist/commands/__tests__/login.test.js +0 -152
  72. package/dist/commands/__tests__/logout.test.js +0 -39
  73. package/dist/commands/__tests__/project-context-impact.test.js +0 -173
  74. package/dist/commands/__tests__/spec-gap-status.test.js +0 -31
  75. package/dist/commands/__tests__/whoami.test.js +0 -91
  76. package/dist/config/__tests__/llm-config.test.js +0 -28
  77. package/dist/config/__tests__/shell-exec-config.test.js +0 -76
  78. package/dist/config/__tests__/write-mode.test.js +0 -36
  79. package/dist/doctor/__tests__/check-forge-rules.test.js +0 -134
  80. package/dist/doctor/__tests__/check-llm-mode.test.js +0 -21
  81. package/dist/doctor/__tests__/check-write-mode.test.js +0 -34
  82. package/dist/project-context/__tests__/conventions.test.js +0 -221
  83. package/dist/project-context/__tests__/detect.test.js +0 -177
  84. package/dist/project-context/__tests__/domain-abap-cloud.test.js +0 -119
  85. package/dist/project-context/__tests__/domain-abapgit.test.js +0 -161
  86. package/dist/project-context/__tests__/domain-cap.test.js +0 -168
  87. package/dist/project-context/__tests__/domain-fiori.test.js +0 -282
  88. package/dist/project-context/__tests__/git.test.js +0 -131
  89. package/dist/project-context/__tests__/index-files.test.js +0 -195
  90. package/dist/project-context/__tests__/index.test.js +0 -133
  91. package/dist/project-context/__tests__/render.test.js +0 -322
  92. package/dist/project-context/__tests__/types.test.js +0 -73
  93. package/dist/projects/__tests__/build.test.js +0 -52
  94. package/dist/projects/__tests__/canonicalize.test.js +0 -47
  95. package/dist/projects/__tests__/email-template.test.js +0 -71
  96. package/dist/projects/__tests__/expand-text-attachments.test.js +0 -105
  97. package/dist/projects/__tests__/extract-cca.test.js +0 -141
  98. package/dist/projects/__tests__/extract-design.test.js +0 -62
  99. package/dist/projects/__tests__/extract-estimate.test.js +0 -58
  100. package/dist/projects/__tests__/extract-modernize.test.js +0 -127
  101. package/dist/projects/__tests__/extract-spec-gap.test.js +0 -122
  102. package/dist/projects/__tests__/extract-test-coverage.test.js +0 -133
  103. package/dist/projects/__tests__/filename.test.js +0 -32
  104. package/dist/projects/__tests__/promote-command.test.js +0 -170
  105. package/dist/projects/__tests__/promote.test.js +0 -181
  106. package/dist/projects/__tests__/save-command.test.js +0 -216
  107. package/dist/projects/__tests__/save.test.js +0 -41
  108. package/dist/projects/__tests__/status.test.js +0 -208
  109. package/dist/projects/__tests__/types.test.js +0 -41
  110. package/dist/projects/__tests__/validate.test.js +0 -165
  111. package/dist/projects/__tests__/workspace.test.js +0 -337
  112. package/dist/repl/__tests__/current-transport.test.js +0 -43
  113. package/dist/repl/__tests__/diff-display.test.js +0 -59
  114. package/dist/repl/__tests__/file-picker.test.js +0 -97
  115. package/dist/repl/__tests__/rule8-detector.test.js +0 -114
  116. package/dist/repl/__tests__/safety-confirm.test.js +0 -130
  117. package/dist/repl/__tests__/safety-mode-state.test.js +0 -42
  118. package/dist/sap/__tests__/system-info.test.js +0 -344
  119. package/dist/session/__tests__/store-discovery.test.js +0 -114
  120. package/dist/session/__tests__/time-ago.test.js +0 -53
  121. package/dist/skills/__tests__/canonical.test.js +0 -20
  122. package/dist/skills/__tests__/manifest-client.test.js +0 -88
  123. package/dist/skills/__tests__/promotion-dispatch.test.js +0 -28
  124. package/dist/skills/__tests__/source-managed.test.js +0 -60
  125. package/dist/tools/__tests__/_background-shared.test.js +0 -69
  126. package/dist/tools/__tests__/_command-shared.test.js +0 -59
  127. package/dist/tools/__tests__/_filesystem-shared.test.js +0 -42
  128. package/dist/tools/__tests__/_flag.test.js +0 -66
  129. package/dist/tools/__tests__/_helpers.js +0 -17
  130. package/dist/tools/__tests__/_web-shared.test.js +0 -143
  131. package/dist/tools/__tests__/agent_run.test.js +0 -166
  132. package/dist/tools/__tests__/approval-advisory.test.js +0 -66
  133. package/dist/tools/__tests__/background_run.test.js +0 -113
  134. package/dist/tools/__tests__/convention_get.test.js +0 -53
  135. package/dist/tools/__tests__/file-edit.test.js +0 -72
  136. package/dist/tools/__tests__/file-read.test.js +0 -81
  137. package/dist/tools/__tests__/file-write.test.js +0 -68
  138. package/dist/tools/__tests__/glob.test.js +0 -128
  139. package/dist/tools/__tests__/grep.test.js +0 -71
  140. package/dist/tools/__tests__/monitor_emit.test.js +0 -96
  141. package/dist/tools/__tests__/playbook_get.test.js +0 -109
  142. package/dist/tools/__tests__/project_context_get.test.js +0 -71
  143. package/dist/tools/__tests__/registry-category.test.js +0 -60
  144. package/dist/tools/__tests__/sap-read-extras.test.js +0 -196
  145. package/dist/tools/__tests__/sap-write-extras.test.js +0 -573
  146. package/dist/tools/__tests__/schedule_create.test.js +0 -85
  147. package/dist/tools/__tests__/shell_exec.test.js +0 -135
  148. package/dist/tools/__tests__/transport-safety.test.js +0 -106
  149. package/dist/tools/__tests__/update-method-intercept.test.js +0 -128
  150. package/dist/tools/__tests__/web_fetch.test.js +0 -329
  151. package/dist/tools/__tests__/web_search.test.js +0 -215
  152. package/dist/tools/__tests__/write-mode.test.js +0 -32
  153. package/dist/ui/__tests__/login-banner.test.js +0 -69
@@ -1,134 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- // ---- Mocks must be declared BEFORE the module under test is imported ----
3
- vi.mock('../../config/loader.js', () => ({
4
- loadConfig: vi.fn(),
5
- }));
6
- vi.mock('../../sap/connection-manager.js', () => ({
7
- getConnection: vi.fn(),
8
- }));
9
- // Build a re-usable AdtClient mock factory so each test can configure its own
10
- // per-method behaviour without accidentally sharing state.
11
- const makeAdtMock = () => ({
12
- deleteObject: vi.fn().mockResolvedValue({ status: 200 }),
13
- createObject: vi.fn().mockResolvedValue({ status: 201 }),
14
- setSource: vi.fn().mockResolvedValue({ writeStatus: 200 }),
15
- syntaxCheck: vi.fn().mockResolvedValue({ hasErrors: false, hasWarnings: false, messages: [] }),
16
- });
17
- let adtMock = makeAdtMock();
18
- vi.mock('@cspeach/sap-client', () => {
19
- return {
20
- AdtClient: vi.fn().mockImplementation(() => adtMock),
21
- snapshots: {
22
- list: vi.fn(),
23
- },
24
- };
25
- });
26
- import { loadConfig } from '../../config/loader.js';
27
- import { getConnection } from '../../sap/connection-manager.js';
28
- import { AdtClient, snapshots } from '@cspeach/sap-client';
29
- import { checkForgeRules } from '../checks/forge-rules.js';
30
- // Typed helpers so we can call vi mock APIs without `as any` everywhere.
31
- const mockLoadConfig = loadConfig;
32
- const mockGetConnection = getConnection;
33
- const mockSnapshotsList = snapshots.list;
34
- function mkCfg(sapAliases) {
35
- const sap = {};
36
- for (const a of sapAliases) {
37
- sap[a] = { host: '10.0.0.1', port: 44300, client: '100', useSsl: true, sslVerify: false, username: 'DEV', authType: 'basic' };
38
- }
39
- return {
40
- proxy_url: 'https://x',
41
- default_model: 'claude-opus-4-7',
42
- effort: 'xhigh',
43
- telemetry: 'minimal',
44
- write_mode: 'approval-gated',
45
- llm: { mode: 'managed' },
46
- sap,
47
- classifier: { safe_mode: false },
48
- ui: { rendering: 'classic' },
49
- };
50
- }
51
- beforeEach(() => {
52
- adtMock = makeAdtMock();
53
- AdtClient.mockImplementation(() => adtMock);
54
- mockGetConnection.mockResolvedValue({} /* fake SapConnection */);
55
- vi.clearAllMocks();
56
- // Re-apply the AdtClient mock after clearAllMocks
57
- AdtClient.mockImplementation(() => adtMock);
58
- mockGetConnection.mockResolvedValue({});
59
- });
60
- describe('doctor check: forge-rules', () => {
61
- it('returns ok:true with skipped:undefined when Rule 7 and Rule 10 both pass', async () => {
62
- mockLoadConfig.mockResolvedValue(mkCfg(['S4H_DEV']));
63
- // Before write: 0 snapshots; after write: 1 snapshot
64
- mockSnapshotsList
65
- .mockResolvedValueOnce([]) // before setSource
66
- .mockResolvedValueOnce([{ id: 'snap1' }]); // after setSource
67
- const r = await checkForgeRules();
68
- expect(r.ok).toBe(true);
69
- expect(r.skipped).toBeUndefined();
70
- expect(r.message).toContain('Rule 7');
71
- expect(r.message).toContain('Rule 10');
72
- expect(r.message).toContain('Z_CSPEACH_DOCTOR_PROBE');
73
- });
74
- it('returns ok:false, skipped:true when createObject returns 401', async () => {
75
- mockLoadConfig.mockResolvedValue(mkCfg(['S4H_DEV']));
76
- adtMock.createObject.mockRejectedValue(new Error('HTTP 401 Unauthorized'));
77
- const r = await checkForgeRules();
78
- expect(r.ok).toBe(false);
79
- expect(r.skipped).toBe(true);
80
- expect(r.message).toMatch(/write probe not permitted/);
81
- expect(r.hint).toBeDefined();
82
- });
83
- it('returns ok:false, skipped:true when createObject returns 403', async () => {
84
- mockLoadConfig.mockResolvedValue(mkCfg(['S4H_DEV']));
85
- adtMock.createObject.mockRejectedValue(new Error('403 Forbidden — no create permission'));
86
- const r = await checkForgeRules();
87
- expect(r.ok).toBe(false);
88
- expect(r.skipped).toBe(true);
89
- expect(r.message).toMatch(/write probe not permitted/);
90
- });
91
- it('returns ok:false with Rule 7 message when snapshot count does not grow', async () => {
92
- mockLoadConfig.mockResolvedValue(mkCfg(['S4H_DEV']));
93
- // Both calls return the same list — count did not grow
94
- mockSnapshotsList.mockResolvedValue([{ id: 'snap_existing' }]);
95
- const r = await checkForgeRules();
96
- expect(r.ok).toBe(false);
97
- expect(r.skipped).toBeUndefined();
98
- expect(r.message).toMatch(/Rule 7/);
99
- expect(r.message).toMatch(/snapshot count did not increase/);
100
- });
101
- it('returns ok:false with Rule 10 message when syntax check reports errors', async () => {
102
- mockLoadConfig.mockResolvedValue(mkCfg(['S4H_DEV']));
103
- mockSnapshotsList
104
- .mockResolvedValueOnce([])
105
- .mockResolvedValueOnce([{ id: 'snap1' }]);
106
- adtMock.syntaxCheck.mockResolvedValue({
107
- hasErrors: true,
108
- hasWarnings: false,
109
- messages: [{ severity: 'E', line: 2, column: 1, text: 'Symbol not found' }],
110
- });
111
- const r = await checkForgeRules();
112
- expect(r.ok).toBe(false);
113
- expect(r.skipped).toBeUndefined();
114
- expect(r.message).toMatch(/Rule 10/);
115
- expect(r.message).toMatch(/Symbol not found/);
116
- });
117
- it('returns ok:false, skipped:true when no SAP system is configured', async () => {
118
- mockLoadConfig.mockResolvedValue(mkCfg([]));
119
- const r = await checkForgeRules();
120
- expect(r.ok).toBe(false);
121
- expect(r.skipped).toBe(true);
122
- expect(r.message).toMatch(/no SAP system configured/);
123
- expect(r.hint).toBeDefined();
124
- });
125
- it('always deletes the probe in the finally block even on Rule 7 failure', async () => {
126
- mockLoadConfig.mockResolvedValue(mkCfg(['S4H_DEV']));
127
- // Snapshot count does not grow → Rule 7 failure path
128
- mockSnapshotsList.mockResolvedValue([{ id: 'snap_existing' }]);
129
- await checkForgeRules();
130
- // deleteObject is called twice: once at top cleanup, once in finally
131
- expect(adtMock.deleteObject).toHaveBeenCalledWith('PROG', 'Z_CSPEACH_DOCTOR_PROBE');
132
- expect(adtMock.deleteObject.mock.calls.length).toBeGreaterThanOrEqual(1);
133
- });
134
- });
@@ -1,21 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- vi.mock('../../auth/api-key.js', () => ({
3
- getStore: vi.fn().mockResolvedValue({ get: vi.fn().mockResolvedValue('test-key') }),
4
- }));
5
- vi.mock('../../agent/providers/factory.js', () => ({
6
- createProviderForMode: vi.fn().mockReturnValue({
7
- name: 'managed',
8
- mode: 'managed',
9
- skillSource: { describe: () => 'mock source' },
10
- healthCheck: async () => ({ ok: true, message: 'healthy' }),
11
- }),
12
- }));
13
- import { checkLlmMode } from '../checks/llm-mode.js';
14
- describe('doctor check: llm-mode', () => {
15
- it('reports OK when provider healthCheck passes', async () => {
16
- const cfg = { llm: { mode: 'managed' }, proxy_url: 'https://x' };
17
- const r = await checkLlmMode(cfg);
18
- expect(r.ok).toBe(true);
19
- expect(r.message).toContain('managed');
20
- });
21
- });
@@ -1,34 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { checkWriteMode } from '../checks/write-mode.js';
3
- function mkCfg(mode) {
4
- return {
5
- proxy_url: 'https://x',
6
- default_model: 'claude-opus-4-7',
7
- effort: 'xhigh',
8
- telemetry: 'minimal',
9
- write_mode: mode,
10
- llm: { mode: 'managed' },
11
- sap: {},
12
- classifier: { safe_mode: false },
13
- ui: { rendering: 'classic' },
14
- };
15
- }
16
- describe('doctor check: write-mode', () => {
17
- it('reports approval-gated as OK', async () => {
18
- const r = await checkWriteMode(mkCfg('approval-gated'));
19
- expect(r.ok).toBe(true);
20
- expect(r.message).toContain('approval-gated');
21
- });
22
- it('reports advisory-only as OK with explanatory message', async () => {
23
- const r = await checkWriteMode(mkCfg('advisory-only'));
24
- expect(r.ok).toBe(true);
25
- expect(r.message).toContain('advisory-only');
26
- expect(r.message).toMatch(/no.*write/i);
27
- });
28
- it('reports auto with a caution hint', async () => {
29
- const r = await checkWriteMode(mkCfg('auto'));
30
- expect(r.ok).toBe(true);
31
- // The plan says `r.hint` — adapt if the CheckResult uses a different name (e.g. `detail`, `warning`, `note`)
32
- expect(r.hint ?? r.message).toMatch(/auto/i);
33
- });
34
- });
@@ -1,221 +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 { readConventions } from '../conventions.js';
6
- let root;
7
- beforeEach(async () => {
8
- root = await fs.mkdtemp(path.join(os.tmpdir(), 'cspeach-conv-'));
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('readConventions — defaults', () => {
19
- it('empty directory → DEFAULT_CONVENTIONS (null freeText, empty patterns, 2-space LF)', async () => {
20
- const c = await readConventions(root);
21
- expect(c.freeText).toBeNull();
22
- expect(c.namingPatterns.classes).toEqual([]);
23
- expect(c.namingPatterns.interfaces).toEqual([]);
24
- expect(c.namingPatterns.cdsViews).toEqual([]);
25
- expect(c.style.indentSize).toBe(2);
26
- expect(c.style.indentChar).toBe('space');
27
- expect(c.style.eol).toBe('lf');
28
- });
29
- it('non-existent directory → defaults (never throws)', async () => {
30
- const c = await readConventions(path.join(root, 'does-not-exist'));
31
- expect(c.freeText).toBeNull();
32
- });
33
- });
34
- describe('readConventions — CLAUDE.md', () => {
35
- it('reads CLAUDE.md verbatim into freeText with header', async () => {
36
- await writeFile('CLAUDE.md', '# Project conventions\n\nUse the ZCL_ prefix for all classes.\n');
37
- const c = await readConventions(root);
38
- expect(c.freeText).toContain('## CLAUDE.md');
39
- expect(c.freeText).toContain('Use the ZCL_ prefix');
40
- });
41
- it('handles a large CLAUDE.md without truncation at this layer', async () => {
42
- const big = '# Big\n' + 'a'.repeat(5000);
43
- await writeFile('CLAUDE.md', big);
44
- const c = await readConventions(root);
45
- expect(c.freeText?.length).toBeGreaterThan(4000);
46
- // Token-budget truncation happens at the render layer, not here.
47
- });
48
- });
49
- describe('readConventions — CSPEACH.md', () => {
50
- it('reads CSPEACH.md as override appended after CLAUDE.md', async () => {
51
- await writeFile('CLAUDE.md', 'Generic Claude conventions');
52
- await writeFile('CSPEACH.md', 'CSPeach-specific overrides');
53
- const c = await readConventions(root);
54
- expect(c.freeText).toContain('## CLAUDE.md');
55
- expect(c.freeText).toContain('Generic Claude conventions');
56
- expect(c.freeText).toContain('## CSPEACH.md');
57
- expect(c.freeText).toContain('CSPeach-specific overrides');
58
- expect(c.freeText.indexOf('## CLAUDE.md')).toBeLessThan(c.freeText.indexOf('## CSPEACH.md'));
59
- });
60
- it('CSPEACH.md alone (no CLAUDE.md) still produces freeText', async () => {
61
- await writeFile('CSPEACH.md', 'Solo CSPeach guidance');
62
- const c = await readConventions(root);
63
- expect(c.freeText).toContain('## CSPEACH.md');
64
- expect(c.freeText).toContain('Solo CSPeach guidance');
65
- expect(c.freeText).not.toContain('CLAUDE.md');
66
- });
67
- });
68
- describe('readConventions — .cspeach/conventions.toml', () => {
69
- it('parses namingPatterns from TOML', async () => {
70
- await writeFile('.cspeach/conventions.toml', `
71
- [namingPatterns]
72
- classes = ["ZCL_", "Z_"]
73
- interfaces = ["ZIF_"]
74
- cdsViews = ["ZI_", "ZC_"]
75
- `);
76
- const c = await readConventions(root);
77
- expect(c.namingPatterns.classes).toEqual(['ZCL_', 'Z_']);
78
- expect(c.namingPatterns.interfaces).toEqual(['ZIF_']);
79
- expect(c.namingPatterns.cdsViews).toEqual(['ZI_', 'ZC_']);
80
- });
81
- it('parses style from TOML', async () => {
82
- await writeFile('.cspeach/conventions.toml', `
83
- [style]
84
- indentSize = 4
85
- indentChar = "tab"
86
- eol = "crlf"
87
- `);
88
- const c = await readConventions(root);
89
- expect(c.style.indentSize).toBe(4);
90
- expect(c.style.indentChar).toBe('tab');
91
- expect(c.style.eol).toBe('crlf');
92
- });
93
- it('honours comments and blank lines', async () => {
94
- await writeFile('.cspeach/conventions.toml', `
95
- # Top-level comment
96
- [namingPatterns] # inline comment
97
- classes = ["ZCL_"]
98
-
99
- # blank line above
100
- interfaces = ["ZIF_"]
101
- `);
102
- const c = await readConventions(root);
103
- expect(c.namingPatterns.classes).toEqual(['ZCL_']);
104
- expect(c.namingPatterns.interfaces).toEqual(['ZIF_']);
105
- });
106
- it('malformed TOML falls back to defaults (does not throw)', async () => {
107
- await writeFile('.cspeach/conventions.toml', '!!!not a valid toml!!!');
108
- const c = await readConventions(root);
109
- // Defaults preserved; no exception.
110
- expect(c.namingPatterns.classes).toEqual([]);
111
- expect(c.style.indentSize).toBe(2);
112
- });
113
- it('partial TOML override leaves other fields at default', async () => {
114
- await writeFile('.cspeach/conventions.toml', `
115
- [style]
116
- indentSize = 8
117
- `);
118
- const c = await readConventions(root);
119
- expect(c.style.indentSize).toBe(8);
120
- // Other style fields default
121
- expect(c.style.indentChar).toBe('space');
122
- expect(c.style.eol).toBe('lf');
123
- });
124
- it('ignores unknown sections', async () => {
125
- await writeFile('.cspeach/conventions.toml', `
126
- [unknownSection]
127
- foo = "bar"
128
-
129
- [namingPatterns]
130
- classes = ["ZCL_"]
131
- `);
132
- const c = await readConventions(root);
133
- expect(c.namingPatterns.classes).toEqual(['ZCL_']);
134
- });
135
- it('rejects invalid status enum values silently', async () => {
136
- await writeFile('.cspeach/conventions.toml', `
137
- [style]
138
- indentChar = "purple"
139
- eol = "magenta"
140
- `);
141
- const c = await readConventions(root);
142
- // Invalid values rejected; falls back to defaults
143
- expect(c.style.indentChar).toBe('space');
144
- expect(c.style.eol).toBe('lf');
145
- });
146
- });
147
- describe('readConventions — style inference from existing files', () => {
148
- it('detects tab indentation from a sampled .ts file', async () => {
149
- await writeFile('src/foo.ts', '\tconst x = 1;\n\tconst y = 2;\n');
150
- const c = await readConventions(root);
151
- expect(c.style.indentChar).toBe('tab');
152
- expect(c.style.indentSize).toBe(1);
153
- });
154
- it('detects 4-space indentation from a sampled file', async () => {
155
- await writeFile('src/foo.ts', ' const x = 1;\n const y = 2;\n');
156
- const c = await readConventions(root);
157
- expect(c.style.indentChar).toBe('space');
158
- expect(c.style.indentSize).toBe(4);
159
- });
160
- it('skips JSDoc continuation lines (` *`) when measuring indent (regression)', async () => {
161
- // 2026-05-10 dogfooding finding: most CSPeach .ts files start with
162
- // a /** ... */ docblock. The earlier detector saw the ` * comment`
163
- // continuation as the first indented line and reported indentSize=1
164
- // — which the model dutifully echoed as "1-space indent" in the
165
- // first project_context turn.
166
- await writeFile('src/foo.ts', [
167
- '/**',
168
- ' * Top-of-file docblock.',
169
- ' * Continuation line with one-space indent.',
170
- ' */',
171
- '',
172
- 'export function bar() {',
173
- ' const x = 1;',
174
- '}',
175
- '',
176
- ].join('\n'));
177
- const c = await readConventions(root);
178
- expect(c.style.indentChar).toBe('space');
179
- expect(c.style.indentSize).toBe(2);
180
- });
181
- it('still detects tab indent even past a docblock', async () => {
182
- // Same docblock guard, but the actual code uses tabs.
183
- await writeFile('src/foo.ts', [
184
- '/**',
185
- ' * Docblock first.',
186
- ' */',
187
- 'export function bar() {',
188
- '\treturn 42;',
189
- '}',
190
- '',
191
- ].join('\n'));
192
- const c = await readConventions(root);
193
- expect(c.style.indentChar).toBe('tab');
194
- expect(c.style.indentSize).toBe(1);
195
- });
196
- it('detects CRLF line endings', async () => {
197
- await writeFile('src/foo.ts', 'const x = 1;\r\nconst y = 2;\r\n');
198
- const c = await readConventions(root);
199
- expect(c.style.eol).toBe('crlf');
200
- });
201
- it('skips node_modules + .git for inference', async () => {
202
- // Only files in noisy dirs → no inference, defaults retained.
203
- await writeFile('node_modules/junk.ts', '\tjunk();\n');
204
- await writeFile('.git/HEAD', 'ref: refs/heads/main\r\n');
205
- const c = await readConventions(root);
206
- expect(c.style.indentChar).toBe('space');
207
- expect(c.style.indentSize).toBe(2);
208
- });
209
- it('TOML overrides win over inferred style', async () => {
210
- // File suggests tabs, TOML says 4-space.
211
- await writeFile('src/foo.ts', '\tjunk();\n');
212
- await writeFile('.cspeach/conventions.toml', `
213
- [style]
214
- indentSize = 4
215
- indentChar = "space"
216
- `);
217
- const c = await readConventions(root);
218
- expect(c.style.indentChar).toBe('space');
219
- expect(c.style.indentSize).toBe(4);
220
- });
221
- });
@@ -1,177 +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 { detectProjectType } from '../detect.js';
6
- /**
7
- * Detection tests use real temp directories with synthetic file layouts.
8
- * Each test builds the layout, runs detection, asserts the type, cleans up.
9
- *
10
- * Pure read-only filesystem inspection — no SAP, no mocking required.
11
- */
12
- let root;
13
- beforeEach(async () => {
14
- root = await fs.mkdtemp(path.join(os.tmpdir(), 'cspeach-detect-'));
15
- });
16
- afterEach(async () => {
17
- await fs.rm(root, { recursive: true, force: true });
18
- });
19
- /* ── Helpers ───────────────────────────────────────────────────────── */
20
- async function writeFile(rel, content) {
21
- const full = path.join(root, rel);
22
- await fs.mkdir(path.dirname(full), { recursive: true });
23
- await fs.writeFile(full, content, 'utf-8');
24
- }
25
- async function makeDir(rel) {
26
- await fs.mkdir(path.join(root, rel), { recursive: true });
27
- }
28
- /* ── Tests ─────────────────────────────────────────────────────────── */
29
- describe('detectProjectType — base cases', () => {
30
- it('empty directory → unknown', async () => {
31
- expect(await detectProjectType(root)).toBe('unknown');
32
- });
33
- it('non-existent directory → null', async () => {
34
- expect(await detectProjectType(path.join(root, 'nope'))).toBeNull();
35
- });
36
- it('file (not directory) → null', async () => {
37
- await writeFile('a-file.txt', 'hi');
38
- expect(await detectProjectType(path.join(root, 'a-file.txt'))).toBeNull();
39
- });
40
- it('garbage path → null (never throws)', async () => {
41
- await expect(detectProjectType('\0\0\0')).resolves.toBeNull();
42
- });
43
- });
44
- describe('detectProjectType — mixed (mta.yaml)', () => {
45
- it('mta.yaml at root → mixed', async () => {
46
- await writeFile('mta.yaml', 'ID: my-app\nversion: 1.0.0\n');
47
- expect(await detectProjectType(root)).toBe('mixed');
48
- });
49
- it('mta.yaml beats any other signal (mixed has highest priority)', async () => {
50
- await writeFile('mta.yaml', 'ID: my-app\n');
51
- // Even if Fiori signals are also present:
52
- await writeFile('webapp/manifest.json', '{}');
53
- await writeFile('ui5.yaml', 'specVersion: "3.0"\n');
54
- expect(await detectProjectType(root)).toBe('mixed');
55
- });
56
- });
57
- describe('detectProjectType — CAP Node', () => {
58
- it('package.json with @sap/cds + srv/ + db/ → cap-node', async () => {
59
- await writeFile('package.json', JSON.stringify({ dependencies: { '@sap/cds': '^7.0.0' } }));
60
- await makeDir('srv');
61
- await makeDir('db');
62
- expect(await detectProjectType(root)).toBe('cap-node');
63
- });
64
- it('@sap/cds in devDependencies also counts', async () => {
65
- await writeFile('package.json', JSON.stringify({ devDependencies: { '@sap/cds': '^7.0.0' } }));
66
- await makeDir('srv');
67
- await makeDir('db');
68
- expect(await detectProjectType(root)).toBe('cap-node');
69
- });
70
- it('@sap/cds without srv/ or db/ → not CAP', async () => {
71
- await writeFile('package.json', JSON.stringify({ dependencies: { '@sap/cds': '^7.0.0' } }));
72
- // Only db/, no srv/
73
- await makeDir('db');
74
- expect(await detectProjectType(root)).toBe('unknown');
75
- });
76
- it('CAP detection wins over a webapp/ subdir (priority order)', async () => {
77
- await writeFile('package.json', JSON.stringify({ dependencies: { '@sap/cds': '^7.0.0' } }));
78
- await makeDir('srv');
79
- await makeDir('db');
80
- // Also add a Fiori-shaped subdir — CAP wins.
81
- await writeFile('app/myapp/webapp/manifest.json', '{}');
82
- expect(await detectProjectType(root)).toBe('cap-node');
83
- });
84
- });
85
- describe('detectProjectType — Fiori UI5', () => {
86
- it('webapp/manifest.json + ui5.yaml → fiori-ui5', async () => {
87
- await writeFile('webapp/manifest.json', '{}');
88
- await writeFile('ui5.yaml', 'specVersion: "3.0"\n');
89
- expect(await detectProjectType(root)).toBe('fiori-ui5');
90
- });
91
- it('webapp/manifest.json + package.json with @sap/ux-ui5-tooling → fiori-ui5', async () => {
92
- await writeFile('webapp/manifest.json', '{}');
93
- await writeFile('package.json', JSON.stringify({ devDependencies: { '@sap/ux-ui5-tooling': '^1.0.0' } }));
94
- expect(await detectProjectType(root)).toBe('fiori-ui5');
95
- });
96
- it('webapp/manifest.json alone (no ui5.yaml, no Fiori dep) → unknown', async () => {
97
- await writeFile('webapp/manifest.json', '{}');
98
- expect(await detectProjectType(root)).toBe('unknown');
99
- });
100
- it('ui5.yaml without webapp/manifest.json → unknown', async () => {
101
- await writeFile('ui5.yaml', 'specVersion: "3.0"\n');
102
- expect(await detectProjectType(root)).toBe('unknown');
103
- });
104
- });
105
- describe('detectProjectType — abapGit', () => {
106
- it('.abapgit.xml at root → abapgit', async () => {
107
- await writeFile('.abapgit.xml', '<?xml version="1.0" encoding="utf-8"?><abapGit></abapGit>');
108
- expect(await detectProjectType(root)).toBe('abapgit');
109
- });
110
- it('src/*.prog.abap without .abapgit.xml → abapgit', async () => {
111
- await writeFile('src/zhello.prog.abap', 'REPORT zhello.');
112
- expect(await detectProjectType(root)).toBe('abapgit');
113
- });
114
- it('src/*.clas.abap → abapgit', async () => {
115
- await writeFile('src/zcl_foo.clas.abap', 'CLASS zcl_foo DEFINITION. ENDCLASS.');
116
- expect(await detectProjectType(root)).toBe('abapgit');
117
- });
118
- it('src/ with non-abap files only → unknown', async () => {
119
- await writeFile('src/something.js', 'console.log("hi");');
120
- expect(await detectProjectType(root)).toBe('unknown');
121
- });
122
- it('classic ABAP files in nested package dir → abapgit (recursive walk)', async () => {
123
- await writeFile('src/zpkg_subdir/sub2/zhello.prog.abap', 'REPORT zhello.');
124
- expect(await detectProjectType(root)).toBe('abapgit');
125
- });
126
- });
127
- describe('detectProjectType — ABAP Cloud', () => {
128
- it('src/ with .bdef behavior definition → abap-cloud', async () => {
129
- await writeFile('src/zr_payments.bdef.asbdef', 'managed implementation ...');
130
- expect(await detectProjectType(root)).toBe('abap-cloud');
131
- });
132
- it('src/ with .srvd service definition → abap-cloud', async () => {
133
- await writeFile('src/zsd_payments.srvd.srvd', 'define service ...');
134
- expect(await detectProjectType(root)).toBe('abap-cloud');
135
- });
136
- it('src/ with .dcls access control → abap-cloud', async () => {
137
- await writeFile('src/zi_payments.dcls.asdcls', 'define role ...');
138
- expect(await detectProjectType(root)).toBe('abap-cloud');
139
- });
140
- it('mixed cloud + classic ABAP files → abap-cloud (cloud markers win)', async () => {
141
- await writeFile('src/zhello.prog.abap', 'REPORT zhello.');
142
- await writeFile('src/zr_root.bdef.asbdef', 'managed implementation ...');
143
- expect(await detectProjectType(root)).toBe('abap-cloud');
144
- });
145
- it('.abapgit.xml present + cloud markers → abap-cloud (cloud markers still win)', async () => {
146
- // abapGit working copies of cloud projects exist — the cloud marker
147
- // signals the project's character regardless of abapGit packaging.
148
- await writeFile('.abapgit.xml', '<abapGit/>');
149
- await writeFile('src/zr_root.bdef.asbdef', 'managed implementation ...');
150
- expect(await detectProjectType(root)).toBe('abap-cloud');
151
- });
152
- });
153
- describe('detectProjectType — priority + edge cases', () => {
154
- it('mta.yaml + CAP signals + Fiori signals → mixed (highest priority wins)', async () => {
155
- await writeFile('mta.yaml', 'ID: x\n');
156
- await writeFile('package.json', JSON.stringify({ dependencies: { '@sap/cds': '^7.0.0' } }));
157
- await makeDir('srv');
158
- await makeDir('db');
159
- await writeFile('webapp/manifest.json', '{}');
160
- expect(await detectProjectType(root)).toBe('mixed');
161
- });
162
- it('Fiori signals + abapGit signals → fiori-ui5 (Fiori has higher priority)', async () => {
163
- await writeFile('webapp/manifest.json', '{}');
164
- await writeFile('ui5.yaml', 'specVersion: "3.0"\n');
165
- await writeFile('.abapgit.xml', '<abapGit/>');
166
- expect(await detectProjectType(root)).toBe('fiori-ui5');
167
- });
168
- it('package.json without any SAP deps → not CAP / not Fiori', async () => {
169
- await writeFile('package.json', JSON.stringify({ dependencies: { react: '^18.0.0' } }));
170
- expect(await detectProjectType(root)).toBe('unknown');
171
- });
172
- it('malformed package.json does not crash detection', async () => {
173
- await writeFile('package.json', '{ this is not valid json');
174
- // Should still return unknown gracefully.
175
- expect(await detectProjectType(root)).toBe('unknown');
176
- });
177
- });