@cspeach/cli 0.6.4 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +0 -0
- package/package.json +86 -82
- package/dist/agent/__tests__/crash-recover.integration.test.js +0 -254
- package/dist/agent/__tests__/loop-save-hook.test.js +0 -188
- package/dist/agent/__tests__/maybe-build-project-context.test.js +0 -247
- package/dist/agent/__tests__/project-context-injection.test.js +0 -161
- package/dist/agent/__tests__/repair-partial.test.js +0 -79
- package/dist/agent/__tests__/retry-key.test.js +0 -77
- package/dist/agent/__tests__/sap-connection-adapter.test.js +0 -132
- package/dist/agent/__tests__/skill-checkpoint.test.js +0 -154
- package/dist/agent/__tests__/turn-assistant-text.test.js +0 -73
- package/dist/agent/__tests__/turn-error-ux.test.js +0 -145
- package/dist/agent/__tests__/turn-stream.test.js +0 -100
- package/dist/agent/__tests__/turn-watchdog.test.js +0 -126
- package/dist/agent/providers/__tests__/ai-hub-provider.test.js +0 -30
- package/dist/agent/providers/__tests__/byok-provider.test.js +0 -32
- package/dist/agent/providers/__tests__/factory.test.js +0 -33
- package/dist/agent/providers/__tests__/local-provider.test.js +0 -55
- package/dist/approvals/__tests__/advisory-prompt.test.js +0 -57
- package/dist/approvals/__tests__/advisory-render.test.js +0 -54
- package/dist/auth/__tests__/auth-file.test.js +0 -42
- package/dist/auth/__tests__/me.test.js +0 -43
- package/dist/classifier/__tests__/client.test.js +0 -70
- package/dist/commands/__tests__/config-set-write-mode.test.js +0 -14
- package/dist/commands/__tests__/login.test.js +0 -152
- package/dist/commands/__tests__/logout.test.js +0 -39
- package/dist/commands/__tests__/project-context-impact.test.js +0 -173
- package/dist/commands/__tests__/spec-gap-status.test.js +0 -31
- package/dist/commands/__tests__/whoami.test.js +0 -91
- package/dist/config/__tests__/llm-config.test.js +0 -28
- package/dist/config/__tests__/shell-exec-config.test.js +0 -76
- package/dist/config/__tests__/write-mode.test.js +0 -36
- package/dist/doctor/__tests__/check-forge-rules.test.js +0 -134
- package/dist/doctor/__tests__/check-llm-mode.test.js +0 -21
- package/dist/doctor/__tests__/check-write-mode.test.js +0 -34
- package/dist/project-context/__tests__/conventions.test.js +0 -221
- package/dist/project-context/__tests__/detect.test.js +0 -177
- package/dist/project-context/__tests__/domain-abap-cloud.test.js +0 -119
- package/dist/project-context/__tests__/domain-abapgit.test.js +0 -161
- package/dist/project-context/__tests__/domain-cap.test.js +0 -168
- package/dist/project-context/__tests__/domain-fiori.test.js +0 -282
- package/dist/project-context/__tests__/git.test.js +0 -131
- package/dist/project-context/__tests__/index-files.test.js +0 -195
- package/dist/project-context/__tests__/index.test.js +0 -133
- package/dist/project-context/__tests__/render.test.js +0 -322
- package/dist/project-context/__tests__/types.test.js +0 -73
- package/dist/projects/__tests__/build.test.js +0 -52
- package/dist/projects/__tests__/canonicalize.test.js +0 -47
- package/dist/projects/__tests__/email-template.test.js +0 -71
- package/dist/projects/__tests__/expand-text-attachments.test.js +0 -105
- package/dist/projects/__tests__/extract-cca.test.js +0 -141
- package/dist/projects/__tests__/extract-design.test.js +0 -62
- package/dist/projects/__tests__/extract-estimate.test.js +0 -58
- package/dist/projects/__tests__/extract-modernize.test.js +0 -127
- package/dist/projects/__tests__/extract-spec-gap.test.js +0 -122
- package/dist/projects/__tests__/extract-test-coverage.test.js +0 -133
- package/dist/projects/__tests__/filename.test.js +0 -32
- package/dist/projects/__tests__/promote-command.test.js +0 -170
- package/dist/projects/__tests__/promote.test.js +0 -181
- package/dist/projects/__tests__/save-command.test.js +0 -216
- package/dist/projects/__tests__/save.test.js +0 -41
- package/dist/projects/__tests__/status.test.js +0 -208
- package/dist/projects/__tests__/types.test.js +0 -41
- package/dist/projects/__tests__/validate.test.js +0 -165
- package/dist/projects/__tests__/workspace.test.js +0 -337
- package/dist/repl/__tests__/current-transport.test.js +0 -43
- package/dist/repl/__tests__/diff-display.test.js +0 -59
- package/dist/repl/__tests__/file-picker.test.js +0 -97
- package/dist/repl/__tests__/rule8-detector.test.js +0 -114
- package/dist/repl/__tests__/safety-confirm.test.js +0 -130
- package/dist/repl/__tests__/safety-mode-state.test.js +0 -42
- package/dist/sap/__tests__/system-info.test.js +0 -344
- package/dist/session/__tests__/store-discovery.test.js +0 -114
- package/dist/session/__tests__/time-ago.test.js +0 -53
- package/dist/skills/__tests__/canonical.test.js +0 -20
- package/dist/skills/__tests__/manifest-client.test.js +0 -88
- package/dist/skills/__tests__/promotion-dispatch.test.js +0 -28
- package/dist/skills/__tests__/source-managed.test.js +0 -60
- package/dist/tools/__tests__/_background-shared.test.js +0 -69
- package/dist/tools/__tests__/_command-shared.test.js +0 -59
- package/dist/tools/__tests__/_filesystem-shared.test.js +0 -42
- package/dist/tools/__tests__/_flag.test.js +0 -66
- package/dist/tools/__tests__/_helpers.js +0 -17
- package/dist/tools/__tests__/_web-shared.test.js +0 -143
- package/dist/tools/__tests__/agent_run.test.js +0 -166
- package/dist/tools/__tests__/approval-advisory.test.js +0 -66
- package/dist/tools/__tests__/background_run.test.js +0 -113
- package/dist/tools/__tests__/convention_get.test.js +0 -53
- package/dist/tools/__tests__/file-edit.test.js +0 -72
- package/dist/tools/__tests__/file-read.test.js +0 -81
- package/dist/tools/__tests__/file-write.test.js +0 -68
- package/dist/tools/__tests__/glob.test.js +0 -128
- package/dist/tools/__tests__/grep.test.js +0 -71
- package/dist/tools/__tests__/monitor_emit.test.js +0 -96
- package/dist/tools/__tests__/playbook_get.test.js +0 -109
- package/dist/tools/__tests__/project_context_get.test.js +0 -71
- package/dist/tools/__tests__/registry-category.test.js +0 -60
- package/dist/tools/__tests__/sap-read-extras.test.js +0 -196
- package/dist/tools/__tests__/sap-write-extras.test.js +0 -573
- package/dist/tools/__tests__/schedule_create.test.js +0 -85
- package/dist/tools/__tests__/shell_exec.test.js +0 -135
- package/dist/tools/__tests__/transport-safety.test.js +0 -106
- package/dist/tools/__tests__/update-method-intercept.test.js +0 -128
- package/dist/tools/__tests__/web_fetch.test.js +0 -329
- package/dist/tools/__tests__/web_search.test.js +0 -215
- package/dist/tools/__tests__/write-mode.test.js +0 -32
- package/dist/ui/__tests__/login-banner.test.js +0 -69
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { loadWatchdogConfig, evaluateWatchdog, DEFAULT_WALL_BUDGET_MS, DEFAULT_TOKEN_BUDGET, } from '../turn-watchdog.js';
|
|
3
|
-
describe('loadWatchdogConfig', () => {
|
|
4
|
-
it('uses defaults when no env vars set', () => {
|
|
5
|
-
const cfg = loadWatchdogConfig({});
|
|
6
|
-
expect(cfg.enabled).toBe(true);
|
|
7
|
-
expect(cfg.wallBudgetMs).toBe(DEFAULT_WALL_BUDGET_MS);
|
|
8
|
-
expect(cfg.tokenBudget).toBe(DEFAULT_TOKEN_BUDGET);
|
|
9
|
-
});
|
|
10
|
-
it('honours CSPEACH_TURN_WATCHDOG=off', () => {
|
|
11
|
-
expect(loadWatchdogConfig({ CSPEACH_TURN_WATCHDOG: 'off' }).enabled).toBe(false);
|
|
12
|
-
expect(loadWatchdogConfig({ CSPEACH_TURN_WATCHDOG: 'OFF' }).enabled).toBe(false);
|
|
13
|
-
});
|
|
14
|
-
it('parses CSPEACH_TURN_WALL_BUDGET_MS override', () => {
|
|
15
|
-
const cfg = loadWatchdogConfig({ CSPEACH_TURN_WALL_BUDGET_MS: '60000' });
|
|
16
|
-
expect(cfg.wallBudgetMs).toBe(60_000);
|
|
17
|
-
});
|
|
18
|
-
it('parses CSPEACH_TURN_TOKEN_BUDGET override', () => {
|
|
19
|
-
const cfg = loadWatchdogConfig({ CSPEACH_TURN_TOKEN_BUDGET: '5000' });
|
|
20
|
-
expect(cfg.tokenBudget).toBe(5_000);
|
|
21
|
-
});
|
|
22
|
-
it('falls back to default on malformed env (rejects 0, negative, NaN)', () => {
|
|
23
|
-
expect(loadWatchdogConfig({ CSPEACH_TURN_WALL_BUDGET_MS: '0' }).wallBudgetMs).toBe(DEFAULT_WALL_BUDGET_MS);
|
|
24
|
-
expect(loadWatchdogConfig({ CSPEACH_TURN_WALL_BUDGET_MS: '-1' }).wallBudgetMs).toBe(DEFAULT_WALL_BUDGET_MS);
|
|
25
|
-
expect(loadWatchdogConfig({ CSPEACH_TURN_TOKEN_BUDGET: 'whatever' }).tokenBudget).toBe(DEFAULT_TOKEN_BUDGET);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
describe('evaluateWatchdog', () => {
|
|
29
|
-
const cfg = { enabled: true, wallBudgetMs: 20 * 60_000, tokenBudget: 100_000 };
|
|
30
|
-
const tStart = 1_000_000;
|
|
31
|
-
it('returns no-warn when both budgets unscathed', () => {
|
|
32
|
-
const d = evaluateWatchdog({
|
|
33
|
-
startedAt: tStart,
|
|
34
|
-
outputTokensThisTurn: 5_000,
|
|
35
|
-
config: cfg,
|
|
36
|
-
alreadyWarned: false,
|
|
37
|
-
}, tStart + 60_000); // 1 minute in
|
|
38
|
-
expect(d.shouldWarn).toBe(false);
|
|
39
|
-
expect(d.message).toBe('');
|
|
40
|
-
});
|
|
41
|
-
it('warns when wall-clock budget exceeded', () => {
|
|
42
|
-
const d = evaluateWatchdog({
|
|
43
|
-
startedAt: tStart,
|
|
44
|
-
outputTokensThisTurn: 1_000,
|
|
45
|
-
config: cfg,
|
|
46
|
-
alreadyWarned: false,
|
|
47
|
-
}, tStart + 21 * 60_000);
|
|
48
|
-
expect(d.shouldWarn).toBe(true);
|
|
49
|
-
expect(d.reason).toContain('21m elapsed');
|
|
50
|
-
expect(d.reason).toContain('limit 20m');
|
|
51
|
-
expect(d.message).toContain('CSPeach watchdog');
|
|
52
|
-
expect(d.message).toContain('checkpoint');
|
|
53
|
-
expect(d.message).toContain('summaris'); // 'summarise' or 'summarising'
|
|
54
|
-
});
|
|
55
|
-
it('warns when token budget exceeded', () => {
|
|
56
|
-
const d = evaluateWatchdog({
|
|
57
|
-
startedAt: tStart,
|
|
58
|
-
outputTokensThisTurn: 150_000,
|
|
59
|
-
config: cfg,
|
|
60
|
-
alreadyWarned: false,
|
|
61
|
-
}, tStart + 60_000);
|
|
62
|
-
expect(d.shouldWarn).toBe(true);
|
|
63
|
-
expect(d.reason).toContain('150K output tokens');
|
|
64
|
-
expect(d.reason).toContain('limit 100K');
|
|
65
|
-
});
|
|
66
|
-
it('reports both reasons when both budgets exceeded', () => {
|
|
67
|
-
const d = evaluateWatchdog({
|
|
68
|
-
startedAt: tStart,
|
|
69
|
-
outputTokensThisTurn: 200_000,
|
|
70
|
-
config: cfg,
|
|
71
|
-
alreadyWarned: false,
|
|
72
|
-
}, tStart + 30 * 60_000);
|
|
73
|
-
expect(d.shouldWarn).toBe(true);
|
|
74
|
-
expect(d.reason).toContain('30m elapsed');
|
|
75
|
-
expect(d.reason).toContain('200K output tokens');
|
|
76
|
-
});
|
|
77
|
-
it('does NOT warn twice in the same turn (alreadyWarned)', () => {
|
|
78
|
-
const d = evaluateWatchdog({
|
|
79
|
-
startedAt: tStart,
|
|
80
|
-
outputTokensThisTurn: 200_000,
|
|
81
|
-
config: cfg,
|
|
82
|
-
alreadyWarned: true,
|
|
83
|
-
}, tStart + 30 * 60_000);
|
|
84
|
-
expect(d.shouldWarn).toBe(false);
|
|
85
|
-
});
|
|
86
|
-
it('does NOT warn when watchdog disabled', () => {
|
|
87
|
-
const d = evaluateWatchdog({
|
|
88
|
-
startedAt: tStart,
|
|
89
|
-
outputTokensThisTurn: 999_999,
|
|
90
|
-
config: { ...cfg, enabled: false },
|
|
91
|
-
alreadyWarned: false,
|
|
92
|
-
}, tStart + 999 * 60_000);
|
|
93
|
-
expect(d.shouldWarn).toBe(false);
|
|
94
|
-
});
|
|
95
|
-
it('boundary: warns exactly at the threshold (>=)', () => {
|
|
96
|
-
const d = evaluateWatchdog({
|
|
97
|
-
startedAt: tStart,
|
|
98
|
-
outputTokensThisTurn: 100_000, // exactly at limit
|
|
99
|
-
config: cfg,
|
|
100
|
-
alreadyWarned: false,
|
|
101
|
-
}, tStart + 60_000);
|
|
102
|
-
expect(d.shouldWarn).toBe(true);
|
|
103
|
-
});
|
|
104
|
-
it('handles future startedAt (clamps to 0 elapsed) without crashing', () => {
|
|
105
|
-
const d = evaluateWatchdog({
|
|
106
|
-
startedAt: tStart + 1000,
|
|
107
|
-
outputTokensThisTurn: 5,
|
|
108
|
-
config: cfg,
|
|
109
|
-
alreadyWarned: false,
|
|
110
|
-
}, tStart);
|
|
111
|
-
expect(d.shouldWarn).toBe(false);
|
|
112
|
-
});
|
|
113
|
-
it('REGRESSION: 77-min ZDPR_PAYMENTS turn would have warned at 20min mark', () => {
|
|
114
|
-
// The lost session ran 77 minutes. With the watchdog active, the model
|
|
115
|
-
// would have been advised to wrap up at the 20-minute mark — well before
|
|
116
|
-
// the Anthropic stream timeout that ate the result.
|
|
117
|
-
const d = evaluateWatchdog({
|
|
118
|
-
startedAt: tStart,
|
|
119
|
-
outputTokensThisTurn: 50_000, // not yet token-busted
|
|
120
|
-
config: cfg,
|
|
121
|
-
alreadyWarned: false,
|
|
122
|
-
}, tStart + 20 * 60_000); // exactly at 20m
|
|
123
|
-
expect(d.shouldWarn).toBe(true);
|
|
124
|
-
expect(d.reason).toMatch(/elapsed/);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
vi.mock('keytar', () => ({
|
|
3
|
-
default: { getPassword: vi.fn(), setPassword: vi.fn() },
|
|
4
|
-
}));
|
|
5
|
-
describe('ai-hub provider', () => {
|
|
6
|
-
it('requires ai_hub_base_url in config', async () => {
|
|
7
|
-
const { createAiHubProvider } = await import('../ai-hub-provider.js');
|
|
8
|
-
const cfg = { llm: { mode: 'ai-hub' }, proxy_url: '' };
|
|
9
|
-
expect(() => createAiHubProvider(cfg)).toThrow(/ai_hub_base_url/);
|
|
10
|
-
});
|
|
11
|
-
it('creates provider with configured URL + model alias', async () => {
|
|
12
|
-
const { createAiHubProvider } = await import('../ai-hub-provider.js');
|
|
13
|
-
const cfg = {
|
|
14
|
-
llm: { mode: 'ai-hub', ai_hub_base_url: 'https://proxy.example.com/v1', ai_hub_model_alias: 'anthropic--claude-opus-4-6' },
|
|
15
|
-
proxy_url: '',
|
|
16
|
-
};
|
|
17
|
-
const p = createAiHubProvider(cfg);
|
|
18
|
-
expect(p.name).toBe('sap-ai-hub');
|
|
19
|
-
expect(p.mode).toBe('ai-hub');
|
|
20
|
-
});
|
|
21
|
-
it('skillSource uses manifest', async () => {
|
|
22
|
-
const { createAiHubProvider } = await import('../ai-hub-provider.js');
|
|
23
|
-
const cfg = {
|
|
24
|
-
llm: { mode: 'ai-hub', ai_hub_base_url: 'https://p', ai_hub_model_alias: 'x' },
|
|
25
|
-
proxy_url: '',
|
|
26
|
-
};
|
|
27
|
-
const p = createAiHubProvider(cfg);
|
|
28
|
-
expect(p.skillSource.describe()).toMatch(/manifest/i);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
vi.mock('keytar', () => ({
|
|
3
|
-
default: {
|
|
4
|
-
getPassword: vi.fn().mockResolvedValue('sk-ant-test-key-123'),
|
|
5
|
-
setPassword: vi.fn(),
|
|
6
|
-
},
|
|
7
|
-
}));
|
|
8
|
-
describe('byok provider', () => {
|
|
9
|
-
it('name + mode set correctly', async () => {
|
|
10
|
-
const { createByokProvider } = await import('../byok-provider.js');
|
|
11
|
-
const cfg = { llm: { mode: 'byok' }, proxy_url: '' };
|
|
12
|
-
const p = createByokProvider(cfg);
|
|
13
|
-
expect(p.name).toBe('anthropic-byok');
|
|
14
|
-
expect(p.mode).toBe('byok');
|
|
15
|
-
});
|
|
16
|
-
it('healthCheck fails when no key in keychain', async () => {
|
|
17
|
-
const keytar = (await import('keytar')).default;
|
|
18
|
-
keytar.getPassword.mockResolvedValueOnce(null);
|
|
19
|
-
const { createByokProvider } = await import('../byok-provider.js');
|
|
20
|
-
const cfg = { llm: { mode: 'byok' }, proxy_url: '' };
|
|
21
|
-
const p = createByokProvider(cfg);
|
|
22
|
-
const h = await p.healthCheck();
|
|
23
|
-
expect(h.ok).toBe(false);
|
|
24
|
-
expect(h.message).toMatch(/keychain|key/i);
|
|
25
|
-
});
|
|
26
|
-
it('skillSource uses manifest', async () => {
|
|
27
|
-
const { createByokProvider } = await import('../byok-provider.js');
|
|
28
|
-
const cfg = { llm: { mode: 'byok' }, proxy_url: '' };
|
|
29
|
-
const p = createByokProvider(cfg);
|
|
30
|
-
expect(p.skillSource.describe()).toMatch(/manifest/i);
|
|
31
|
-
});
|
|
32
|
-
});
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
vi.mock('keytar', () => ({ default: { getPassword: vi.fn().mockResolvedValue('k'), setPassword: vi.fn() } }));
|
|
3
|
-
describe('provider factory', () => {
|
|
4
|
-
it('returns managed provider for mode=managed', async () => {
|
|
5
|
-
const { createProviderForMode } = await import('../factory.js');
|
|
6
|
-
const cfg = { llm: { mode: 'managed' }, proxy_url: 'https://x' };
|
|
7
|
-
const p = createProviderForMode(cfg, async () => 'bearer');
|
|
8
|
-
expect(p.mode).toBe('managed');
|
|
9
|
-
});
|
|
10
|
-
it('returns byok provider for mode=byok', async () => {
|
|
11
|
-
const { createProviderForMode } = await import('../factory.js');
|
|
12
|
-
const cfg = { llm: { mode: 'byok' }, proxy_url: '' };
|
|
13
|
-
const p = createProviderForMode(cfg, async () => '');
|
|
14
|
-
expect(p.mode).toBe('byok');
|
|
15
|
-
});
|
|
16
|
-
it('returns ai-hub provider for mode=ai-hub', async () => {
|
|
17
|
-
const { createProviderForMode } = await import('../factory.js');
|
|
18
|
-
const cfg = { llm: { mode: 'ai-hub', ai_hub_base_url: 'https://p', ai_hub_model_alias: 'x' }, proxy_url: '' };
|
|
19
|
-
const p = createProviderForMode(cfg, async () => '');
|
|
20
|
-
expect(p.mode).toBe('ai-hub');
|
|
21
|
-
});
|
|
22
|
-
it('returns local provider for mode=local', async () => {
|
|
23
|
-
const { createProviderForMode } = await import('../factory.js');
|
|
24
|
-
const cfg = { llm: { mode: 'local', local_base_url: 'http://l', local_model: 'm' }, proxy_url: '' };
|
|
25
|
-
const p = createProviderForMode(cfg, async () => '');
|
|
26
|
-
expect(p.mode).toBe('local');
|
|
27
|
-
});
|
|
28
|
-
it('throws on invalid mode', async () => {
|
|
29
|
-
const { createProviderForMode } = await import('../factory.js');
|
|
30
|
-
const cfg = { llm: { mode: 'bogus' }, proxy_url: '' };
|
|
31
|
-
expect(() => createProviderForMode(cfg, async () => '')).toThrow(/mode/);
|
|
32
|
-
});
|
|
33
|
-
});
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
describe('local provider', () => {
|
|
3
|
-
it('requires local_base_url in config', async () => {
|
|
4
|
-
const { createLocalProvider } = await import('../local-provider.js');
|
|
5
|
-
const cfg = { llm: { mode: 'local' }, proxy_url: '' };
|
|
6
|
-
expect(() => createLocalProvider(cfg)).toThrow(/local_base_url/);
|
|
7
|
-
});
|
|
8
|
-
it('creates provider with configured URL + model', async () => {
|
|
9
|
-
const { createLocalProvider } = await import('../local-provider.js');
|
|
10
|
-
const cfg = {
|
|
11
|
-
llm: { mode: 'local', local_base_url: 'http://localhost:11434/v1', local_model: 'llama3.1:70b' },
|
|
12
|
-
proxy_url: '',
|
|
13
|
-
};
|
|
14
|
-
const p = createLocalProvider(cfg);
|
|
15
|
-
expect(p.name).toBe('local-openai-compat');
|
|
16
|
-
expect(p.mode).toBe('local');
|
|
17
|
-
});
|
|
18
|
-
it('skillSource uses bundled skills', async () => {
|
|
19
|
-
const { createLocalProvider } = await import('../local-provider.js');
|
|
20
|
-
const cfg = {
|
|
21
|
-
llm: { mode: 'local', local_base_url: 'http://l', local_model: 'm' },
|
|
22
|
-
proxy_url: '',
|
|
23
|
-
};
|
|
24
|
-
const p = createLocalProvider(cfg);
|
|
25
|
-
expect(p.skillSource.describe()).toMatch(/bundled/i);
|
|
26
|
-
});
|
|
27
|
-
it('createStream rejects params that include tools (Plan A scope: text-only)', async () => {
|
|
28
|
-
const { createLocalProvider } = await import('../local-provider.js');
|
|
29
|
-
const cfg = { llm: { mode: 'local', local_base_url: 'http://l', local_model: 'm' }, proxy_url: '' };
|
|
30
|
-
const p = createLocalProvider(cfg);
|
|
31
|
-
await expect(p.createStream({ model: 'm', max_tokens: 100, messages: [], tools: [{ name: 't', description: 'd', input_schema: {} }] }))
|
|
32
|
-
.rejects.toThrow(/tool calling.*not supported.*local mode/i);
|
|
33
|
-
});
|
|
34
|
-
it('adaptOpenAiStreamToAnthropic emits the canonical event sequence', async () => {
|
|
35
|
-
const { adaptOpenAiStreamToAnthropic } = await import('../local-provider.js');
|
|
36
|
-
async function* fakeOpenAIStream() {
|
|
37
|
-
yield { choices: [{ delta: { content: 'hello ' } }] };
|
|
38
|
-
yield { choices: [{ delta: { content: 'world' }, finish_reason: null }] };
|
|
39
|
-
yield { choices: [{ delta: {}, finish_reason: 'stop' }], usage: { prompt_tokens: 5, completion_tokens: 3 } };
|
|
40
|
-
}
|
|
41
|
-
const events = [];
|
|
42
|
-
for await (const ev of adaptOpenAiStreamToAnthropic(fakeOpenAIStream(), 'test-model'))
|
|
43
|
-
events.push(ev);
|
|
44
|
-
expect(events[0].type).toBe('message_start');
|
|
45
|
-
expect(events[0].message.model).toBe('test-model');
|
|
46
|
-
expect(events[1].type).toBe('content_block_start');
|
|
47
|
-
expect(events[1].index).toBe(0);
|
|
48
|
-
const deltas = events.filter(e => e.type === 'content_block_delta');
|
|
49
|
-
expect(deltas.map(d => d.delta.text).join('')).toBe('hello world');
|
|
50
|
-
expect(events[events.length - 3].type).toBe('content_block_stop');
|
|
51
|
-
expect(events[events.length - 2].type).toBe('message_delta');
|
|
52
|
-
expect(events[events.length - 2].usage.output_tokens).toBe(3);
|
|
53
|
-
expect(events[events.length - 1].type).toBe('message_stop');
|
|
54
|
-
});
|
|
55
|
-
});
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
// Mock clipboardy
|
|
3
|
-
vi.mock('clipboardy', () => ({
|
|
4
|
-
default: {
|
|
5
|
-
write: vi.fn().mockResolvedValue(undefined),
|
|
6
|
-
},
|
|
7
|
-
}));
|
|
8
|
-
// Mock readline/promises — feed keystrokes directly
|
|
9
|
-
const mockRead = vi.fn();
|
|
10
|
-
vi.mock('node:readline/promises', () => ({
|
|
11
|
-
default: {
|
|
12
|
-
createInterface: () => ({
|
|
13
|
-
question: mockRead,
|
|
14
|
-
close: vi.fn(),
|
|
15
|
-
}),
|
|
16
|
-
},
|
|
17
|
-
createInterface: () => ({
|
|
18
|
-
question: mockRead,
|
|
19
|
-
close: vi.fn(),
|
|
20
|
-
}),
|
|
21
|
-
}));
|
|
22
|
-
describe('promptAdvisory', () => {
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
mockRead.mockReset();
|
|
25
|
-
});
|
|
26
|
-
it('returns "applied" when user enters y', async () => {
|
|
27
|
-
mockRead.mockResolvedValueOnce('y');
|
|
28
|
-
const { promptAdvisory } = await import('../advisory-prompt.js');
|
|
29
|
-
const result = await promptAdvisory({ displayText: 'x', clipboardText: 'x' });
|
|
30
|
-
expect(result).toBe('applied');
|
|
31
|
-
});
|
|
32
|
-
it('returns "skipped" when user enters s', async () => {
|
|
33
|
-
mockRead.mockResolvedValueOnce('s');
|
|
34
|
-
const { promptAdvisory } = await import('../advisory-prompt.js');
|
|
35
|
-
expect(await promptAdvisory({ displayText: 'x', clipboardText: 'x' })).toBe('skipped');
|
|
36
|
-
});
|
|
37
|
-
it('returns "aborted" when user enters n', async () => {
|
|
38
|
-
mockRead.mockResolvedValueOnce('n');
|
|
39
|
-
const { promptAdvisory } = await import('../advisory-prompt.js');
|
|
40
|
-
expect(await promptAdvisory({ displayText: 'x', clipboardText: 'x' })).toBe('aborted');
|
|
41
|
-
});
|
|
42
|
-
it('copies to clipboard on c then re-prompts', async () => {
|
|
43
|
-
mockRead.mockResolvedValueOnce('c').mockResolvedValueOnce('y');
|
|
44
|
-
const clipboardy = (await import('clipboardy')).default;
|
|
45
|
-
const { promptAdvisory } = await import('../advisory-prompt.js');
|
|
46
|
-
const result = await promptAdvisory({ displayText: 'display', clipboardText: 'clip' });
|
|
47
|
-
expect(clipboardy.write).toHaveBeenCalledWith('clip');
|
|
48
|
-
expect(result).toBe('applied');
|
|
49
|
-
});
|
|
50
|
-
it('re-prompts on unrecognised input', async () => {
|
|
51
|
-
mockRead.mockResolvedValueOnce('q').mockResolvedValueOnce('y');
|
|
52
|
-
const { promptAdvisory } = await import('../advisory-prompt.js');
|
|
53
|
-
const result = await promptAdvisory({ displayText: 'x', clipboardText: 'x' });
|
|
54
|
-
expect(result).toBe('applied');
|
|
55
|
-
expect(mockRead).toHaveBeenCalledTimes(2);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { renderAdvisoryProposal } from '../advisory-render.js';
|
|
3
|
-
describe('renderAdvisoryProposal', () => {
|
|
4
|
-
const args = {
|
|
5
|
-
summary: 'Create ZCL_FOO with one method',
|
|
6
|
-
risk: 'low',
|
|
7
|
-
transport: 'DEVK900123',
|
|
8
|
-
changes: [
|
|
9
|
-
{
|
|
10
|
-
op: 'create',
|
|
11
|
-
object: 'ZCL_FOO',
|
|
12
|
-
type: 'CLAS',
|
|
13
|
-
diff: 'CLASS zcl_foo DEFINITION.\n PUBLIC SECTION.\n METHODS hello.\nENDCLASS.',
|
|
14
|
-
},
|
|
15
|
-
],
|
|
16
|
-
};
|
|
17
|
-
it('renders a header with the summary', () => {
|
|
18
|
-
const out = renderAdvisoryProposal(args);
|
|
19
|
-
expect(out.displayText).toContain('Create ZCL_FOO with one method');
|
|
20
|
-
});
|
|
21
|
-
it('renders each change with object + type + diff', () => {
|
|
22
|
-
const out = renderAdvisoryProposal(args);
|
|
23
|
-
expect(out.displayText).toContain('ZCL_FOO');
|
|
24
|
-
expect(out.displayText).toContain('CLAS');
|
|
25
|
-
expect(out.displayText).toContain('zcl_foo DEFINITION');
|
|
26
|
-
});
|
|
27
|
-
it('mentions the target transport', () => {
|
|
28
|
-
const out = renderAdvisoryProposal(args);
|
|
29
|
-
expect(out.displayText).toContain('DEVK900123');
|
|
30
|
-
});
|
|
31
|
-
it('provides a clipboard-friendly payload separate from display text', () => {
|
|
32
|
-
const out = renderAdvisoryProposal(args);
|
|
33
|
-
expect(out.clipboardText).toContain('zcl_foo DEFINITION');
|
|
34
|
-
// no ANSI escape codes — the ESC character (\x1b) should not appear
|
|
35
|
-
expect(out.clipboardText).not.toContain('\x1b[');
|
|
36
|
-
});
|
|
37
|
-
it('handles multiple changes', () => {
|
|
38
|
-
const multi = {
|
|
39
|
-
...args,
|
|
40
|
-
changes: [
|
|
41
|
-
args.changes[0],
|
|
42
|
-
{ op: 'activate', object: 'ZCL_FOO', type: 'CLAS' },
|
|
43
|
-
],
|
|
44
|
-
};
|
|
45
|
-
const out = renderAdvisoryProposal(multi);
|
|
46
|
-
expect(out.displayText).toContain('Change 1');
|
|
47
|
-
expect(out.displayText).toContain('Change 2');
|
|
48
|
-
});
|
|
49
|
-
it('warns that no SAP write will occur', () => {
|
|
50
|
-
const out = renderAdvisoryProposal(args);
|
|
51
|
-
expect(out.displayText.toLowerCase()).toContain('advisory');
|
|
52
|
-
expect(out.displayText.toLowerCase()).toMatch(/no\s+(sap\s+)?write/);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
@@ -1,42 +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 { readAuthFile, writeAuthFile, __setAuthFilePathForTests } from '../auth-file.js';
|
|
6
|
-
let dir;
|
|
7
|
-
let authPath;
|
|
8
|
-
beforeEach(async () => {
|
|
9
|
-
dir = await fs.mkdtemp(path.join(os.tmpdir(), 'cspeach-auth-'));
|
|
10
|
-
authPath = path.join(dir, 'auth.json');
|
|
11
|
-
__setAuthFilePathForTests(authPath);
|
|
12
|
-
});
|
|
13
|
-
afterEach(async () => {
|
|
14
|
-
__setAuthFilePathForTests(null);
|
|
15
|
-
await fs.rm(dir, { recursive: true, force: true });
|
|
16
|
-
});
|
|
17
|
-
describe('auth-file', () => {
|
|
18
|
-
it('readAuthFile returns null when file missing', async () => {
|
|
19
|
-
expect(await readAuthFile()).toBeNull();
|
|
20
|
-
});
|
|
21
|
-
it('writeAuthFile then readAuthFile round-trips', async () => {
|
|
22
|
-
await writeAuthFile({ apiKey: 'csp_live_abc', customerId: 'cust-1', byokAnthropicKey: null });
|
|
23
|
-
const r = await readAuthFile();
|
|
24
|
-
expect(r).toEqual({ apiKey: 'csp_live_abc', customerId: 'cust-1', byokAnthropicKey: null });
|
|
25
|
-
});
|
|
26
|
-
it('writeAuthFile sets 0600 permissions on POSIX', async () => {
|
|
27
|
-
if (process.platform === 'win32')
|
|
28
|
-
return; // chmod is a no-op on Windows ACLs
|
|
29
|
-
await writeAuthFile({ apiKey: 'csp_live_x', customerId: 'c', byokAnthropicKey: null });
|
|
30
|
-
const stat = await fs.stat(authPath);
|
|
31
|
-
expect(stat.mode & 0o777).toBe(0o600);
|
|
32
|
-
});
|
|
33
|
-
it('readAuthFile returns null on malformed JSON', async () => {
|
|
34
|
-
await fs.writeFile(authPath, 'not json', 'utf-8');
|
|
35
|
-
expect(await readAuthFile()).toBeNull();
|
|
36
|
-
});
|
|
37
|
-
it('writeAuthFile with byokAnthropicKey persists it', async () => {
|
|
38
|
-
await writeAuthFile({ apiKey: 'csp_live_x', customerId: 'c', byokAnthropicKey: 'sk-ant-byok-key' });
|
|
39
|
-
const r = await readAuthFile();
|
|
40
|
-
expect(r?.byokAnthropicKey).toBe('sk-ant-byok-key');
|
|
41
|
-
});
|
|
42
|
-
});
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
-
import { fetchMe } from '../me.js';
|
|
3
|
-
const { fetchMock } = vi.hoisted(() => ({ fetchMock: vi.fn() }));
|
|
4
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
5
|
-
beforeEach(() => { fetchMock.mockReset(); });
|
|
6
|
-
describe('fetchMe', () => {
|
|
7
|
-
it('returns parsed profile on 200', async () => {
|
|
8
|
-
fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({
|
|
9
|
-
email: 'c@a.com', plan: 'trial-managed', trial_mode: 'managed-capped',
|
|
10
|
-
trial_ends_at: '2099-01-01T00:00:00Z',
|
|
11
|
-
trial_token_cap_input: 200_000, trial_token_cap_output: 50_000,
|
|
12
|
-
usage_input_tokens: 47_000, usage_output_tokens: 8_000,
|
|
13
|
-
entitlements: { enabled_skills: ['abap-cca'], agent_mode: 'skill', enabled_features: [] },
|
|
14
|
-
}), { status: 200 }));
|
|
15
|
-
const r = await fetchMe({ proxyUrl: 'https://api.cspeach.dev', apiKey: 'csp_live_x' });
|
|
16
|
-
expect(r.ok).toBe(true);
|
|
17
|
-
if (r.ok) {
|
|
18
|
-
expect(r.profile.email).toBe('c@a.com');
|
|
19
|
-
expect(r.profile.usage_input_tokens).toBe(47_000);
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
it('returns 401 result without throwing', async () => {
|
|
23
|
-
fetchMock.mockResolvedValueOnce(new Response('{"error":"invalid_api_key"}', { status: 401 }));
|
|
24
|
-
const r = await fetchMe({ proxyUrl: 'https://api.cspeach.dev', apiKey: 'csp_live_bad' });
|
|
25
|
-
expect(r.ok).toBe(false);
|
|
26
|
-
if (!r.ok)
|
|
27
|
-
expect(r.status).toBe(401);
|
|
28
|
-
});
|
|
29
|
-
it('returns network result on fetch throw', async () => {
|
|
30
|
-
fetchMock.mockRejectedValueOnce(new Error('ECONNREFUSED'));
|
|
31
|
-
const r = await fetchMe({ proxyUrl: 'https://api.cspeach.dev', apiKey: 'csp_live_x' });
|
|
32
|
-
expect(r.ok).toBe(false);
|
|
33
|
-
if (!r.ok)
|
|
34
|
-
expect(r.status).toBe(0);
|
|
35
|
-
});
|
|
36
|
-
it('sends Authorization Bearer header', async () => {
|
|
37
|
-
fetchMock.mockResolvedValueOnce(new Response('{}', { status: 200 }));
|
|
38
|
-
await fetchMe({ proxyUrl: 'https://api.cspeach.dev', apiKey: 'csp_live_my_key' });
|
|
39
|
-
const call = fetchMock.mock.calls[0];
|
|
40
|
-
const opts = call[1];
|
|
41
|
-
expect(opts.headers.Authorization).toBe('Bearer csp_live_my_key');
|
|
42
|
-
});
|
|
43
|
-
});
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { classify } from '../client.js';
|
|
3
|
-
describe('client-side classifier', () => {
|
|
4
|
-
it('routes "generate a new class" to abap-generate', () => {
|
|
5
|
-
expect(classify('generate a new class ZCL_FOO')).toBe('abap-generate');
|
|
6
|
-
});
|
|
7
|
-
it('routes "review this code" to abap-review', () => {
|
|
8
|
-
expect(classify('review this abap code')).toBe('abap-review');
|
|
9
|
-
});
|
|
10
|
-
it('routes "explain this" to abap-explain', () => {
|
|
11
|
-
expect(classify('explain this abap code')).toBe('abap-explain');
|
|
12
|
-
});
|
|
13
|
-
it('returns undefined when no rule matches', () => {
|
|
14
|
-
expect(classify('sneeze a wombat')).toBeUndefined();
|
|
15
|
-
});
|
|
16
|
-
it('disambiguates test vs generate ("generate tests" routes to merged abap-test)', () => {
|
|
17
|
-
expect(classify('generate tests for ZCL_FOO')).toBe('abap-test');
|
|
18
|
-
expect(classify('generate a new class ZCL_FOO')).toBe('abap-generate');
|
|
19
|
-
});
|
|
20
|
-
it('disambiguates transport-analysis vs transport', () => {
|
|
21
|
-
expect(classify('transport analysis before release')).toBe('abap-transport-analysis');
|
|
22
|
-
expect(classify('transport create for ZCL_FOO')).toBe('abap-transport');
|
|
23
|
-
});
|
|
24
|
-
it('disambiguates upgrade-verify vs upgrade-fix vs upgrade-scan', () => {
|
|
25
|
-
expect(classify('verify upgrade remediation')).toBe('abap-upgrade-verify');
|
|
26
|
-
expect(classify('fix upgrade finding')).toBe('abap-upgrade-fix');
|
|
27
|
-
expect(classify('upgrade scan with readiness variant')).toBe('abap-upgrade-scan');
|
|
28
|
-
});
|
|
29
|
-
it('routes "extend ME21N" / "hook into MIGO" to abap-enhance (cluster F)', () => {
|
|
30
|
-
expect(classify('extend ME21N to validate vendors')).toBe('abap-enhance');
|
|
31
|
-
expect(classify('hook into MIGO to log goods receipts')).toBe('abap-enhance');
|
|
32
|
-
expect(classify('user exit for billing pricing')).toBe('abap-enhance');
|
|
33
|
-
expect(classify('extend SAP standard order processing')).toBe('abap-enhance');
|
|
34
|
-
});
|
|
35
|
-
it('routes "convert to Fiori" / "rebuild as RAP" to abap-modernize (cluster F)', () => {
|
|
36
|
-
expect(classify('convert this WRITE report to Fiori')).toBe('abap-modernize');
|
|
37
|
-
expect(classify('rebuild as RAP service')).toBe('abap-modernize');
|
|
38
|
-
expect(classify('convert FM to OData')).toBe('abap-modernize');
|
|
39
|
-
});
|
|
40
|
-
it('routes pipeline-framed dump prompts to abap-incident, not dump-analyze (cluster G)', () => {
|
|
41
|
-
expect(classify('ST22 to transport pipeline for the morning dump')).toBe('abap-incident');
|
|
42
|
-
expect(classify('dump to transport workflow')).toBe('abap-incident');
|
|
43
|
-
expect(classify('end to end dump fix and report')).toBe('abap-incident');
|
|
44
|
-
expect(classify('formal incident report after the dump')).toBe('abap-incident');
|
|
45
|
-
});
|
|
46
|
-
it('keeps single-pass dump prompts on abap-dump-analyze (cluster G regression check)', () => {
|
|
47
|
-
expect(classify('analyze this short dump')).toBe('abap-dump-analyze');
|
|
48
|
-
expect(classify('runtime error in ZSD_ORDER')).toBe('abap-dump-analyze');
|
|
49
|
-
});
|
|
50
|
-
it('routes pre-coding chain prompts to spec-gap / estimate (cluster H)', () => {
|
|
51
|
-
expect(classify('clarifying questions on this requirement')).toBe('abap-spec-gap');
|
|
52
|
-
expect(classify('gaps in the story we got')).toBe('abap-spec-gap');
|
|
53
|
-
expect(classify('size this ticket for me')).toBe('abap-estimate');
|
|
54
|
-
expect(classify('how many days to build this feature')).toBe('abap-estimate');
|
|
55
|
-
});
|
|
56
|
-
it('disambiguates rap / generate / data-model / segw (cluster I)', () => {
|
|
57
|
-
// rap precedes generate now — RAP-stack prompts win even when "generate" or "scaffold" is present
|
|
58
|
-
expect(classify('Generate a RAP service for sales orders')).toBe('abap-rap');
|
|
59
|
-
expect(classify('scaffold a CDS view + BDEF + service binding')).toBe('abap-rap');
|
|
60
|
-
// data-model catches "create a Z table" — used to fall through to generate
|
|
61
|
-
expect(classify('Create a Z_CUSTOMER table for the customer master')).toBe('abap-data-model');
|
|
62
|
-
expect(classify('I need a new domain Z_AMOUNT_TYPE with fixed values')).toBe('abap-data-model');
|
|
63
|
-
// generate keeps single-object prompts (no rap/data-model keywords)
|
|
64
|
-
expect(classify('generate a Z program to print open POs')).toBe('abap-generate');
|
|
65
|
-
expect(classify('create a new class ZCL_CUSTOMER')).toBe('abap-generate');
|
|
66
|
-
// segw catches DPC_EXT and ECC OData
|
|
67
|
-
expect(classify('Implement DPC_EXT for the GET_ENTITYSET method')).toBe('abap-segw');
|
|
68
|
-
expect(classify('build an OData service on ECC Gateway')).toBe('abap-segw');
|
|
69
|
-
});
|
|
70
|
-
});
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { validateValue } from '../config-set.js';
|
|
3
|
-
describe('config set write_mode', () => {
|
|
4
|
-
it('accepts auto, approval-gated, advisory-only', () => {
|
|
5
|
-
expect(validateValue('write_mode', 'auto')).toBe(true);
|
|
6
|
-
expect(validateValue('write_mode', 'approval-gated')).toBe(true);
|
|
7
|
-
expect(validateValue('write_mode', 'advisory-only')).toBe(true);
|
|
8
|
-
});
|
|
9
|
-
it('rejects unknown values', () => {
|
|
10
|
-
expect(validateValue('write_mode', 'yolo')).toBe(false);
|
|
11
|
-
expect(validateValue('write_mode', '')).toBe(false);
|
|
12
|
-
expect(validateValue('write_mode', 'approval_gated')).toBe(false); // underscore, not dash
|
|
13
|
-
});
|
|
14
|
-
});
|