@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.
- package/dist/agent/intent-system-prompt.js +46 -0
- package/dist/agent/loop.js +180 -18
- package/dist/agent/parallel-write-guard.js +71 -0
- package/dist/agent/repair-partial.js +88 -1
- package/dist/agent/retry-cap.js +121 -0
- package/dist/agent/summarise-via-provider.js +51 -0
- package/dist/approvals/jwt.js +33 -12
- package/dist/commands/auto-compact.js +94 -0
- package/dist/commands/compact.js +265 -0
- package/dist/commands/cost.js +56 -0
- package/dist/commands/help.js +21 -14
- package/dist/config/loader.js +39 -0
- package/dist/cost/cost-log.js +113 -0
- package/dist/cost/pricing.js +91 -0
- package/dist/index.js +0 -0
- package/dist/one-shot.js +1 -1
- package/dist/renderer/markdown.js +7 -1
- package/dist/renderer/status-footer.js +37 -14
- package/dist/renderer/thinking-heartbeat.js +15 -3
- package/dist/renderer/tool-widget.js +6 -1
- package/dist/renderer/tty.js +27 -0
- package/dist/repl/cspeach-shell-detect.js +33 -0
- package/dist/repl/post-turn-status.js +68 -0
- package/dist/repl/slash-completer.js +2 -0
- package/dist/repl/slash-picker.js +2 -0
- package/dist/repl.js +371 -46
- package/dist/sap-errors/parse-adt-exception.js +149 -0
- package/dist/session/schema.js +2 -1
- package/dist/session/store.js +19 -0
- package/dist/tools/_filesystem-shared.js +9 -1
- package/dist/tools/ask-question.js +19 -0
- package/dist/tools/sap-read.js +56 -4
- package/dist/tools/sap-write.js +19 -0
- package/dist/tools/subagent/schedule_draft_create.js +137 -0
- package/dist/ui/alt-screen.js +120 -0
- package/dist/ui/app.js +39 -15
- package/dist/ui/ask-question-emitter.js +13 -0
- package/dist/ui/body.js +39 -23
- package/dist/ui/coaching-picker-classic.js +8 -1
- package/dist/ui/footer.js +6 -1
- package/dist/ui/header.js +19 -4
- package/dist/ui/sap-state-store.js +13 -1
- package/dist/ui/sidebar.js +5 -3
- package/dist/ui/status-emitter.js +19 -0
- package/dist/ui/status-row.js +30 -4
- package/dist/ui/widgets/ask-question-modal.js +132 -0
- package/dist/ui/widgets/coaching-picker.js +55 -8
- package/package.json +86 -82
- package/dist/agent/__tests__/crash-recover.integration.test.js +0 -254
- package/dist/agent/__tests__/loop-save-hook.test.js +0 -188
- package/dist/agent/__tests__/maybe-build-project-context.test.js +0 -247
- package/dist/agent/__tests__/project-context-injection.test.js +0 -161
- package/dist/agent/__tests__/repair-partial.test.js +0 -79
- package/dist/agent/__tests__/retry-key.test.js +0 -77
- package/dist/agent/__tests__/sap-connection-adapter.test.js +0 -132
- package/dist/agent/__tests__/skill-checkpoint.test.js +0 -154
- package/dist/agent/__tests__/turn-assistant-text.test.js +0 -73
- package/dist/agent/__tests__/turn-error-ux.test.js +0 -145
- package/dist/agent/__tests__/turn-stream.test.js +0 -100
- package/dist/agent/__tests__/turn-watchdog.test.js +0 -126
- package/dist/agent/providers/__tests__/ai-hub-provider.test.js +0 -30
- package/dist/agent/providers/__tests__/byok-provider.test.js +0 -32
- package/dist/agent/providers/__tests__/factory.test.js +0 -33
- package/dist/agent/providers/__tests__/local-provider.test.js +0 -55
- package/dist/approvals/__tests__/advisory-prompt.test.js +0 -57
- package/dist/approvals/__tests__/advisory-render.test.js +0 -54
- package/dist/auth/__tests__/auth-file.test.js +0 -42
- package/dist/auth/__tests__/me.test.js +0 -43
- package/dist/classifier/__tests__/client.test.js +0 -70
- package/dist/commands/__tests__/config-set-write-mode.test.js +0 -14
- package/dist/commands/__tests__/login.test.js +0 -152
- package/dist/commands/__tests__/logout.test.js +0 -39
- package/dist/commands/__tests__/project-context-impact.test.js +0 -173
- package/dist/commands/__tests__/spec-gap-status.test.js +0 -31
- package/dist/commands/__tests__/whoami.test.js +0 -91
- package/dist/config/__tests__/llm-config.test.js +0 -28
- package/dist/config/__tests__/shell-exec-config.test.js +0 -76
- package/dist/config/__tests__/write-mode.test.js +0 -36
- package/dist/doctor/__tests__/check-forge-rules.test.js +0 -134
- package/dist/doctor/__tests__/check-llm-mode.test.js +0 -21
- package/dist/doctor/__tests__/check-write-mode.test.js +0 -34
- package/dist/project-context/__tests__/conventions.test.js +0 -221
- package/dist/project-context/__tests__/detect.test.js +0 -177
- package/dist/project-context/__tests__/domain-abap-cloud.test.js +0 -119
- package/dist/project-context/__tests__/domain-abapgit.test.js +0 -161
- package/dist/project-context/__tests__/domain-cap.test.js +0 -168
- package/dist/project-context/__tests__/domain-fiori.test.js +0 -282
- package/dist/project-context/__tests__/git.test.js +0 -131
- package/dist/project-context/__tests__/index-files.test.js +0 -195
- package/dist/project-context/__tests__/index.test.js +0 -133
- package/dist/project-context/__tests__/render.test.js +0 -322
- package/dist/project-context/__tests__/types.test.js +0 -73
- package/dist/projects/__tests__/build.test.js +0 -52
- package/dist/projects/__tests__/canonicalize.test.js +0 -47
- package/dist/projects/__tests__/email-template.test.js +0 -71
- package/dist/projects/__tests__/expand-text-attachments.test.js +0 -105
- package/dist/projects/__tests__/extract-cca.test.js +0 -141
- package/dist/projects/__tests__/extract-design.test.js +0 -62
- package/dist/projects/__tests__/extract-estimate.test.js +0 -58
- package/dist/projects/__tests__/extract-modernize.test.js +0 -127
- package/dist/projects/__tests__/extract-spec-gap.test.js +0 -122
- package/dist/projects/__tests__/extract-test-coverage.test.js +0 -133
- package/dist/projects/__tests__/filename.test.js +0 -32
- package/dist/projects/__tests__/promote-command.test.js +0 -170
- package/dist/projects/__tests__/promote.test.js +0 -181
- package/dist/projects/__tests__/save-command.test.js +0 -216
- package/dist/projects/__tests__/save.test.js +0 -41
- package/dist/projects/__tests__/status.test.js +0 -208
- package/dist/projects/__tests__/types.test.js +0 -41
- package/dist/projects/__tests__/validate.test.js +0 -165
- package/dist/projects/__tests__/workspace.test.js +0 -337
- package/dist/repl/__tests__/current-transport.test.js +0 -43
- package/dist/repl/__tests__/diff-display.test.js +0 -59
- package/dist/repl/__tests__/file-picker.test.js +0 -97
- package/dist/repl/__tests__/rule8-detector.test.js +0 -114
- package/dist/repl/__tests__/safety-confirm.test.js +0 -130
- package/dist/repl/__tests__/safety-mode-state.test.js +0 -42
- package/dist/sap/__tests__/system-info.test.js +0 -344
- package/dist/session/__tests__/store-discovery.test.js +0 -114
- package/dist/session/__tests__/time-ago.test.js +0 -53
- package/dist/skills/__tests__/canonical.test.js +0 -20
- package/dist/skills/__tests__/manifest-client.test.js +0 -88
- package/dist/skills/__tests__/promotion-dispatch.test.js +0 -28
- package/dist/skills/__tests__/source-managed.test.js +0 -60
- package/dist/tools/__tests__/_background-shared.test.js +0 -69
- package/dist/tools/__tests__/_command-shared.test.js +0 -59
- package/dist/tools/__tests__/_filesystem-shared.test.js +0 -42
- package/dist/tools/__tests__/_flag.test.js +0 -66
- package/dist/tools/__tests__/_helpers.js +0 -17
- package/dist/tools/__tests__/_web-shared.test.js +0 -143
- package/dist/tools/__tests__/agent_run.test.js +0 -166
- package/dist/tools/__tests__/approval-advisory.test.js +0 -66
- package/dist/tools/__tests__/background_run.test.js +0 -113
- package/dist/tools/__tests__/convention_get.test.js +0 -53
- package/dist/tools/__tests__/file-edit.test.js +0 -72
- package/dist/tools/__tests__/file-read.test.js +0 -81
- package/dist/tools/__tests__/file-write.test.js +0 -68
- package/dist/tools/__tests__/glob.test.js +0 -128
- package/dist/tools/__tests__/grep.test.js +0 -71
- package/dist/tools/__tests__/monitor_emit.test.js +0 -96
- package/dist/tools/__tests__/playbook_get.test.js +0 -109
- package/dist/tools/__tests__/project_context_get.test.js +0 -71
- package/dist/tools/__tests__/registry-category.test.js +0 -60
- package/dist/tools/__tests__/sap-read-extras.test.js +0 -196
- package/dist/tools/__tests__/sap-write-extras.test.js +0 -573
- package/dist/tools/__tests__/schedule_create.test.js +0 -85
- package/dist/tools/__tests__/shell_exec.test.js +0 -135
- package/dist/tools/__tests__/transport-safety.test.js +0 -106
- package/dist/tools/__tests__/update-method-intercept.test.js +0 -128
- package/dist/tools/__tests__/web_fetch.test.js +0 -329
- package/dist/tools/__tests__/web_search.test.js +0 -215
- package/dist/tools/__tests__/write-mode.test.js +0 -32
- package/dist/ui/__tests__/login-banner.test.js +0 -69
|
@@ -1,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
|
-
});
|