@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,135 +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 { shellExecHandler } from '../shell/shell_exec.js';
|
|
6
|
-
import { makeCtx } from './_helpers.js';
|
|
7
|
-
let root;
|
|
8
|
-
const ctx = () => makeCtx(root);
|
|
9
|
-
beforeEach(async () => { root = await fs.mkdtemp(path.join(os.tmpdir(), 'cspeach-shell-')); });
|
|
10
|
-
afterEach(async () => { await fs.rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }); });
|
|
11
|
-
describe('shell_exec handler', () => {
|
|
12
|
-
it('runs a safelisted command and surfaces stdout + exit code', async () => {
|
|
13
|
-
const r = await shellExecHandler({ command: 'node', args: ['-e', 'process.stdout.write("hello")'] }, ctx());
|
|
14
|
-
expect(r.is_error).toBeFalsy();
|
|
15
|
-
expect(r.content).toMatch(/stdout:\s*hello/);
|
|
16
|
-
expect(r.content).toMatch(/exit code:\s*0/);
|
|
17
|
-
});
|
|
18
|
-
it('surfaces stderr separately from stdout', async () => {
|
|
19
|
-
const r = await shellExecHandler({ command: 'node', args: ['-e', 'process.stderr.write("err"); process.stdout.write("out")'] }, ctx());
|
|
20
|
-
expect(r.content).toMatch(/stdout:\s*out/);
|
|
21
|
-
expect(r.content).toMatch(/stderr:\s*err/);
|
|
22
|
-
});
|
|
23
|
-
it('reports non-zero exit code without marking is_error', async () => {
|
|
24
|
-
const r = await shellExecHandler({ command: 'node', args: ['-e', 'process.exit(42)'] }, ctx());
|
|
25
|
-
// Non-zero exit is a normal program outcome — the agent should see it
|
|
26
|
-
// and decide. is_error is reserved for "we refused / spawn failed".
|
|
27
|
-
expect(r.is_error).toBeFalsy();
|
|
28
|
-
expect(r.content).toMatch(/exit code:\s*42/);
|
|
29
|
-
});
|
|
30
|
-
it('shell metacharacters in args are passed literally — no shell parser', async () => {
|
|
31
|
-
// The string `; rm -rf /` becomes literal argv[1] to node, which echoes it.
|
|
32
|
-
// If a shell were interpreting, the `;` would split commands and the test
|
|
33
|
-
// output would be different (or the rm command would attempt to run).
|
|
34
|
-
const r = await shellExecHandler({ command: 'node', args: ['-e', 'process.stdout.write(process.argv[1])', '; rm -rf /'] }, ctx());
|
|
35
|
-
expect(r.content).toMatch(/stdout:\s*; rm -rf \//);
|
|
36
|
-
expect(r.content).toMatch(/exit code:\s*0/);
|
|
37
|
-
});
|
|
38
|
-
it('refuses an absolute-path command even if its basename is safelisted', async () => {
|
|
39
|
-
const fakeAbsolute = process.platform === 'win32' ? 'C:\\Windows\\System32\\cmd.exe' : '/bin/sh';
|
|
40
|
-
const r = await shellExecHandler({ command: fakeAbsolute, args: [] }, ctx());
|
|
41
|
-
expect(r.is_error).toBe(true);
|
|
42
|
-
expect(r.content).toMatch(/safelist|absolute path/i);
|
|
43
|
-
});
|
|
44
|
-
it('refuses a non-safelisted command', async () => {
|
|
45
|
-
const r = await shellExecHandler({ command: 'curl', args: ['https://example.com'] }, ctx());
|
|
46
|
-
expect(r.is_error).toBe(true);
|
|
47
|
-
expect(r.content).toMatch(/safelist/i);
|
|
48
|
-
expect(r.content).toMatch(/curl/);
|
|
49
|
-
});
|
|
50
|
-
it('refuses a cwd outside the project root', async () => {
|
|
51
|
-
const outsideAbsolute = process.platform === 'win32' ? 'C:\\' : '/';
|
|
52
|
-
const r = await shellExecHandler({ command: 'node', args: ['-e', 'process.stdout.write(process.cwd())'], cwd: outsideAbsolute }, ctx());
|
|
53
|
-
expect(r.is_error).toBe(true);
|
|
54
|
-
expect(r.content).toMatch(/outside|cwd/i);
|
|
55
|
-
});
|
|
56
|
-
it('does NOT pass auth-secret env vars (CSPEACH_*, ANTHROPIC_*, SAP_*) to the child', async () => {
|
|
57
|
-
process.env.CSPEACH_TOKEN_TEST = 'must-not-leak';
|
|
58
|
-
process.env.ANTHROPIC_API_KEY_TEST = 'must-not-leak';
|
|
59
|
-
try {
|
|
60
|
-
const r = await shellExecHandler({
|
|
61
|
-
command: 'node',
|
|
62
|
-
args: [
|
|
63
|
-
'-e',
|
|
64
|
-
'const k = ["CSPEACH_TOKEN_TEST","ANTHROPIC_API_KEY_TEST"].filter(x=>process.env[x]); process.stdout.write(k.join(","))',
|
|
65
|
-
],
|
|
66
|
-
}, ctx());
|
|
67
|
-
expect(r.content).toMatch(/stdout:\s*\n/); // empty stdout — neither var visible to child
|
|
68
|
-
expect(r.content).not.toContain('must-not-leak');
|
|
69
|
-
}
|
|
70
|
-
finally {
|
|
71
|
-
delete process.env.CSPEACH_TOKEN_TEST;
|
|
72
|
-
delete process.env.ANTHROPIC_API_KEY_TEST;
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
it('kills a runaway child after the wall-clock timeout', async () => {
|
|
76
|
-
const start = Date.now();
|
|
77
|
-
const r = await shellExecHandler({ command: 'node', args: ['-e', 'setInterval(()=>{},1000)'], timeout_ms: 300 }, ctx());
|
|
78
|
-
const elapsed = Date.now() - start;
|
|
79
|
-
expect(r.is_error).toBe(true);
|
|
80
|
-
expect(r.content).toMatch(/timeout|timed out/i);
|
|
81
|
-
expect(elapsed).toBeLessThan(3_000); // generous — must be well under the 30s default
|
|
82
|
-
});
|
|
83
|
-
it('truncates stdout when it exceeds the per-stream cap', async () => {
|
|
84
|
-
// 2 MB of A's — should be truncated at 1 MB with a marker.
|
|
85
|
-
const r = await shellExecHandler({
|
|
86
|
-
command: 'node',
|
|
87
|
-
args: ['-e', 'process.stdout.write("A".repeat(2_000_000))'],
|
|
88
|
-
timeout_ms: 10_000,
|
|
89
|
-
}, ctx());
|
|
90
|
-
expect(r.is_error).toBeFalsy();
|
|
91
|
-
expect(r.content).toMatch(/\[truncated/i);
|
|
92
|
-
// Content should not contain the full 2 MB — be conservative, check length cap.
|
|
93
|
-
expect(r.content.length).toBeLessThan(1_200_000);
|
|
94
|
-
});
|
|
95
|
-
it('reports a structured error when the safelisted binary is not on PATH', async () => {
|
|
96
|
-
const origPath = process.env.PATH;
|
|
97
|
-
process.env.PATH = root; // PATH points only at empty tempdir — nothing installed.
|
|
98
|
-
try {
|
|
99
|
-
const r = await shellExecHandler({ command: 'abaplint', args: ['--version'] }, ctx());
|
|
100
|
-
expect(r.is_error).toBe(true);
|
|
101
|
-
expect(r.content).toMatch(/not installed|not found/i);
|
|
102
|
-
expect(r.content).toMatch(/abaplint/);
|
|
103
|
-
}
|
|
104
|
-
finally {
|
|
105
|
-
if (origPath === undefined)
|
|
106
|
-
delete process.env.PATH;
|
|
107
|
-
else
|
|
108
|
-
process.env.PATH = origPath;
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
it('rejects a non-string cwd with a clean error message (not a leaked TypeError)', async () => {
|
|
112
|
-
const r = await shellExecHandler({ command: 'node', args: [], cwd: 42 }, ctx());
|
|
113
|
-
expect(r.is_error).toBe(true);
|
|
114
|
-
expect(r.content).toMatch(/cwd must be a string/i);
|
|
115
|
-
expect(r.content).not.toMatch(/typeerror|includes is not a function/i);
|
|
116
|
-
});
|
|
117
|
-
it('rejects a non-number timeout_ms with a clean error message', async () => {
|
|
118
|
-
const r = await shellExecHandler({ command: 'node', args: [], timeout_ms: 'fast' }, ctx());
|
|
119
|
-
expect(r.is_error).toBe(true);
|
|
120
|
-
expect(r.content).toMatch(/timeout_ms must be a number/i);
|
|
121
|
-
});
|
|
122
|
-
it('rejects an argv array longer than the cap (defense-in-depth against memory exhaustion)', async () => {
|
|
123
|
-
const huge = Array.from({ length: 1_001 }, () => 'x');
|
|
124
|
-
const r = await shellExecHandler({ command: 'node', args: huge }, ctx());
|
|
125
|
-
expect(r.is_error).toBe(true);
|
|
126
|
-
expect(r.content).toMatch(/too many args/i);
|
|
127
|
-
});
|
|
128
|
-
it('rejects an individual arg larger than the per-arg cap', async () => {
|
|
129
|
-
const tooBig = 'x'.repeat(100_001);
|
|
130
|
-
const r = await shellExecHandler({ command: 'node', args: [tooBig] }, ctx());
|
|
131
|
-
expect(r.is_error).toBe(true);
|
|
132
|
-
expect(r.content).toMatch(/exceeds max length/i);
|
|
133
|
-
expect(r.content).toMatch(/index 0/);
|
|
134
|
-
});
|
|
135
|
-
});
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* B.2 tests: sap_transport_release safety confirmation gate (Rule 9).
|
|
3
|
-
*
|
|
4
|
-
* Verifies that:
|
|
5
|
-
* - When safety mode is ON and user confirms, presentSafetyConfirmation is called
|
|
6
|
-
* with the correct SafetyAction shape (rule=RULE_9_TRANSPORT_RELEASE) and
|
|
7
|
-
* transportRelease proceeds.
|
|
8
|
-
* - When safety mode is ON and user declines, cancelled_by_user is returned and
|
|
9
|
-
* transportRelease is NOT called.
|
|
10
|
-
*/
|
|
11
|
-
import '../transport.js'; // triggers registration side-effect
|
|
12
|
-
import { getTool } from '../index.js';
|
|
13
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// Mock @cspeach/sap-client (auditLog used by transport.ts)
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
vi.mock('@cspeach/sap-client', async (importOriginal) => {
|
|
18
|
-
const actual = await importOriginal();
|
|
19
|
-
return {
|
|
20
|
-
...actual,
|
|
21
|
-
auditLog: vi.fn().mockResolvedValue(undefined),
|
|
22
|
-
};
|
|
23
|
-
});
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
// Mock verifyAndSpendApprovalId to bypass JWT check
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
const mockVerifyAndSpend = vi.hoisted(() => vi.fn());
|
|
28
|
-
vi.mock('../../approvals/jwt.js', () => ({
|
|
29
|
-
verifyAndSpendApprovalId: mockVerifyAndSpend,
|
|
30
|
-
mintApprovalId: vi.fn(),
|
|
31
|
-
}));
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// Mock safety-mode-state and safety-confirm
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
const mockIsSafetyConfirmEnabled = vi.hoisted(() => vi.fn());
|
|
36
|
-
vi.mock('../../repl/safety-mode-state.js', () => ({
|
|
37
|
-
isSafetyConfirmEnabled: mockIsSafetyConfirmEnabled,
|
|
38
|
-
}));
|
|
39
|
-
const mockPresentSafetyConfirmation = vi.hoisted(() => vi.fn());
|
|
40
|
-
vi.mock('../../repl/safety-confirm.js', () => ({
|
|
41
|
-
presentSafetyConfirmation: mockPresentSafetyConfirmation,
|
|
42
|
-
}));
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// Helper: build a minimal ToolContext stub
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
function makeCtx(adt) {
|
|
47
|
-
return {
|
|
48
|
-
adt: adt,
|
|
49
|
-
sapAlias: 'dev',
|
|
50
|
-
session: { toolCalls: [] },
|
|
51
|
-
cwd: '.',
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
beforeEach(() => {
|
|
55
|
-
vi.clearAllMocks();
|
|
56
|
-
// Default: approval always succeeds
|
|
57
|
-
mockVerifyAndSpend.mockResolvedValue({ ok: true, payload: {} });
|
|
58
|
-
// Default: safety mode OFF
|
|
59
|
-
mockIsSafetyConfirmEnabled.mockReturnValue(false);
|
|
60
|
-
});
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
// B.2 sap_transport_release — safety confirmation gate (Rule 9)
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
describe('sap_transport_release safety confirmation', () => {
|
|
65
|
-
it('calls presentSafetyConfirmation with correct SafetyAction and proceeds when confirmed', async () => {
|
|
66
|
-
mockIsSafetyConfirmEnabled.mockReturnValue(true);
|
|
67
|
-
mockPresentSafetyConfirmation.mockResolvedValue({ confirmed: true });
|
|
68
|
-
const mockTransportRelease = vi.fn().mockResolvedValue({
|
|
69
|
-
operation: 'release',
|
|
70
|
-
transportNumber: 'DEVK900123',
|
|
71
|
-
status: 'released',
|
|
72
|
-
});
|
|
73
|
-
const adt = { transportRelease: mockTransportRelease };
|
|
74
|
-
const tool = getTool('sap_transport_release');
|
|
75
|
-
const result = await tool.handler({ transport: 'DEVK900123', approval_id: 'dummy-jwt' }, makeCtx(adt));
|
|
76
|
-
// Safety confirmation must have been called with correct shape
|
|
77
|
-
expect(mockPresentSafetyConfirmation).toHaveBeenCalledOnce();
|
|
78
|
-
const action = mockPresentSafetyConfirmation.mock.calls[0][0];
|
|
79
|
-
expect(action.rule).toBe('RULE_9_TRANSPORT_RELEASE');
|
|
80
|
-
expect(action.op).toBe('transport_release');
|
|
81
|
-
expect(action.object).toEqual({ type: 'TRANSPORT', name: 'DEVK900123' });
|
|
82
|
-
expect(action.what_will_happen).toBeTruthy();
|
|
83
|
-
expect(action.rollback_available).toBe(false);
|
|
84
|
-
expect(action.transport).toBe('DEVK900123');
|
|
85
|
-
// transportRelease must have been called (confirmed)
|
|
86
|
-
expect(mockTransportRelease).toHaveBeenCalledOnce();
|
|
87
|
-
expect(mockTransportRelease).toHaveBeenCalledWith('DEVK900123');
|
|
88
|
-
const parsed = JSON.parse(result.content);
|
|
89
|
-
expect(parsed.released).toBe(true);
|
|
90
|
-
expect(result.is_error).toBeFalsy();
|
|
91
|
-
});
|
|
92
|
-
it('returns cancelled_by_user and does NOT call transportRelease when user declines', async () => {
|
|
93
|
-
mockIsSafetyConfirmEnabled.mockReturnValue(true);
|
|
94
|
-
mockPresentSafetyConfirmation.mockResolvedValue({ confirmed: false, reason: 'not ready' });
|
|
95
|
-
const mockTransportRelease = vi.fn();
|
|
96
|
-
const adt = { transportRelease: mockTransportRelease };
|
|
97
|
-
const tool = getTool('sap_transport_release');
|
|
98
|
-
const result = await tool.handler({ transport: 'DEVK900123', approval_id: 'dummy-jwt' }, makeCtx(adt));
|
|
99
|
-
expect(result.is_error).toBe(true);
|
|
100
|
-
const parsed = JSON.parse(result.content);
|
|
101
|
-
expect(parsed.error).toBe('cancelled_by_user');
|
|
102
|
-
expect(parsed.reason).toBe('not ready');
|
|
103
|
-
// Critical: transportRelease must NOT be called
|
|
104
|
-
expect(mockTransportRelease).not.toHaveBeenCalled();
|
|
105
|
-
});
|
|
106
|
-
});
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { interceptUpdateMethod, extractMethodBody, CancelledByUserError, } from '../update-method-intercept.js';
|
|
3
|
-
const SAMPLE_CLASS = `CLASS zcl_demo DEFINITION PUBLIC FINAL CREATE PUBLIC.
|
|
4
|
-
PUBLIC SECTION.
|
|
5
|
-
METHODS dispatch.
|
|
6
|
-
METHODS run_diff.
|
|
7
|
-
ENDCLASS.
|
|
8
|
-
|
|
9
|
-
CLASS zcl_demo IMPLEMENTATION.
|
|
10
|
-
|
|
11
|
-
METHOD dispatch.
|
|
12
|
-
DATA(lv_x) = 1.
|
|
13
|
-
lv_x = lv_x + 2.
|
|
14
|
-
" inline comment with " a quote
|
|
15
|
-
WRITE: lv_x.
|
|
16
|
-
ENDMETHOD.
|
|
17
|
-
|
|
18
|
-
METHOD run_diff.
|
|
19
|
-
RETURN.
|
|
20
|
-
ENDMETHOD.
|
|
21
|
-
|
|
22
|
-
ENDCLASS.`;
|
|
23
|
-
describe('extractMethodBody', () => {
|
|
24
|
-
it('returns the named method block including METHOD and ENDMETHOD', () => {
|
|
25
|
-
const body = extractMethodBody(SAMPLE_CLASS, 'dispatch');
|
|
26
|
-
expect(body).toMatch(/^\s*METHOD dispatch\.$/m);
|
|
27
|
-
expect(body).toMatch(/^\s*ENDMETHOD\.$/m);
|
|
28
|
-
expect(body).toContain('DATA(lv_x) = 1.');
|
|
29
|
-
// Should not bleed into run_diff's body.
|
|
30
|
-
expect(body).not.toContain('METHOD run_diff');
|
|
31
|
-
});
|
|
32
|
-
it('is case-insensitive for the method name', () => {
|
|
33
|
-
expect(extractMethodBody(SAMPLE_CLASS, 'DISPATCH')).toContain('DATA(lv_x)');
|
|
34
|
-
expect(extractMethodBody(SAMPLE_CLASS, 'Dispatch')).toContain('DATA(lv_x)');
|
|
35
|
-
});
|
|
36
|
-
it('returns empty string when method is not found', () => {
|
|
37
|
-
expect(extractMethodBody(SAMPLE_CLASS, 'nonexistent')).toBe('');
|
|
38
|
-
});
|
|
39
|
-
it('handles a method whose body contains an inline " comment', () => {
|
|
40
|
-
// The sample dispatch body has `" inline comment with " a quote` — the
|
|
41
|
-
// parser should NOT treat the `"` inside the comment as ending the body.
|
|
42
|
-
const body = extractMethodBody(SAMPLE_CLASS, 'dispatch');
|
|
43
|
-
expect(body).toContain('inline comment');
|
|
44
|
-
});
|
|
45
|
-
it('handles a method whose body contains string literals with keywords', () => {
|
|
46
|
-
const src = `CLASS x IMPLEMENTATION.
|
|
47
|
-
METHOD foo.
|
|
48
|
-
DATA(lv) = 'METHOD bar. ENDMETHOD.'.
|
|
49
|
-
DATA(lw) = \`ENDMETHOD pretend\`.
|
|
50
|
-
RETURN.
|
|
51
|
-
ENDMETHOD.
|
|
52
|
-
|
|
53
|
-
METHOD other.
|
|
54
|
-
WRITE 'hi'.
|
|
55
|
-
ENDMETHOD.
|
|
56
|
-
ENDCLASS.`;
|
|
57
|
-
const body = extractMethodBody(src, 'foo');
|
|
58
|
-
expect(body).toMatch(/RETURN\./);
|
|
59
|
-
// The parser must stop at the REAL ENDMETHOD (not the ones inside the
|
|
60
|
-
// string literals on lines 3 and 4), so the body should NOT bleed into
|
|
61
|
-
// METHOD other.
|
|
62
|
-
expect(body).not.toContain('METHOD other');
|
|
63
|
-
expect(body).not.toContain("WRITE 'hi'");
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
describe('interceptUpdateMethod', () => {
|
|
67
|
-
function makeClient(source) {
|
|
68
|
-
return {
|
|
69
|
-
getSource: vi.fn(async () => source),
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
it('returns the call when the hook confirms', async () => {
|
|
73
|
-
const client = makeClient(SAMPLE_CLASS);
|
|
74
|
-
const hook = vi.fn(async () => ({ confirmed: true }));
|
|
75
|
-
const call = {
|
|
76
|
-
className: 'ZCL_DEMO',
|
|
77
|
-
methodName: 'dispatch',
|
|
78
|
-
methodSource: 'METHOD dispatch.\n RETURN.\nENDMETHOD.',
|
|
79
|
-
transport: 'SXXK999999',
|
|
80
|
-
};
|
|
81
|
-
const result = await interceptUpdateMethod(client, call, hook);
|
|
82
|
-
expect(result).toBe(call);
|
|
83
|
-
expect(hook).toHaveBeenCalledOnce();
|
|
84
|
-
const passed = hook.mock.calls[0][0];
|
|
85
|
-
expect(passed.currentBody).toContain('DATA(lv_x) = 1.');
|
|
86
|
-
expect(passed.proposedBody).toBe(call.methodSource);
|
|
87
|
-
});
|
|
88
|
-
it('throws CancelledByUserError when the hook rejects', async () => {
|
|
89
|
-
const client = makeClient(SAMPLE_CLASS);
|
|
90
|
-
const hook = async () => ({ confirmed: false });
|
|
91
|
-
const call = {
|
|
92
|
-
className: 'ZCL_DEMO',
|
|
93
|
-
methodName: 'dispatch',
|
|
94
|
-
methodSource: 'METHOD dispatch.\nENDMETHOD.',
|
|
95
|
-
transport: 'SXXK999999',
|
|
96
|
-
};
|
|
97
|
-
await expect(interceptUpdateMethod(client, call, hook)).rejects.toThrow(CancelledByUserError);
|
|
98
|
-
});
|
|
99
|
-
it('passes empty currentBody when method does not exist on current class', async () => {
|
|
100
|
-
const client = makeClient(SAMPLE_CLASS);
|
|
101
|
-
const hook = vi.fn(async () => ({ confirmed: true }));
|
|
102
|
-
const call = {
|
|
103
|
-
className: 'ZCL_DEMO',
|
|
104
|
-
methodName: 'brand_new_method',
|
|
105
|
-
methodSource: 'METHOD brand_new_method.\n RETURN.\nENDMETHOD.',
|
|
106
|
-
transport: 'SXXK999999',
|
|
107
|
-
};
|
|
108
|
-
await interceptUpdateMethod(client, call, hook);
|
|
109
|
-
const passed = hook.mock.calls[0][0];
|
|
110
|
-
expect(passed.currentBody).toBe('');
|
|
111
|
-
expect(passed.proposedBody).toBe(call.methodSource);
|
|
112
|
-
});
|
|
113
|
-
it('propagates a getSource failure unchanged', async () => {
|
|
114
|
-
const client = {
|
|
115
|
-
getSource: async () => {
|
|
116
|
-
throw new Error('ADT 401');
|
|
117
|
-
},
|
|
118
|
-
};
|
|
119
|
-
const hook = async () => ({ confirmed: true });
|
|
120
|
-
const call = {
|
|
121
|
-
className: 'ZCL_DEMO',
|
|
122
|
-
methodName: 'dispatch',
|
|
123
|
-
methodSource: 'METHOD dispatch.\nENDMETHOD.',
|
|
124
|
-
transport: 'SXXK999999',
|
|
125
|
-
};
|
|
126
|
-
await expect(interceptUpdateMethod(client, call, hook)).rejects.toThrow('ADT 401');
|
|
127
|
-
});
|
|
128
|
-
});
|