@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,81 +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 { fileReadHandler } from '../filesystem/file-read.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-file-read-')); });
|
|
10
|
-
afterEach(async () => { await fs.rm(root, { recursive: true, force: true }); });
|
|
11
|
-
describe('file_read handler', () => {
|
|
12
|
-
it('reads a file from the project and returns content with line numbers', async () => {
|
|
13
|
-
await fs.writeFile(path.join(root, 'foo.ts'), 'line one\nline two\nline three\n', 'utf-8');
|
|
14
|
-
const r = await fileReadHandler({ path: 'foo.ts' }, ctx());
|
|
15
|
-
expect(r.is_error).toBeFalsy();
|
|
16
|
-
expect(r.content).toContain('1\tline one');
|
|
17
|
-
expect(r.content).toContain('2\tline two');
|
|
18
|
-
expect(r.content).toContain('3\tline three');
|
|
19
|
-
});
|
|
20
|
-
it('refuses paths outside the project root', async () => {
|
|
21
|
-
const r = await fileReadHandler({ path: '../../etc/passwd' }, ctx());
|
|
22
|
-
expect(r.is_error).toBe(true);
|
|
23
|
-
expect(r.content).toMatch(/outside/i);
|
|
24
|
-
});
|
|
25
|
-
it('returns an error envelope when the file does not exist', async () => {
|
|
26
|
-
const r = await fileReadHandler({ path: 'nope.ts' }, ctx());
|
|
27
|
-
expect(r.is_error).toBe(true);
|
|
28
|
-
expect(r.content).toMatch(/not found|ENOENT/i);
|
|
29
|
-
});
|
|
30
|
-
it('truncates at 2000 lines and indicates truncation', async () => {
|
|
31
|
-
const lines = Array.from({ length: 3000 }, (_, i) => `line ${i + 1}`).join('\n');
|
|
32
|
-
await fs.writeFile(path.join(root, 'big.ts'), lines, 'utf-8');
|
|
33
|
-
const r = await fileReadHandler({ path: 'big.ts' }, ctx());
|
|
34
|
-
expect(r.content).toContain('1\tline 1');
|
|
35
|
-
expect(r.content).toContain('2000\tline 2000');
|
|
36
|
-
expect(r.content).not.toContain('2001\tline 2001');
|
|
37
|
-
expect(r.content).toMatch(/truncated.*1000.*more/i);
|
|
38
|
-
});
|
|
39
|
-
it('respects optional offset + limit args (read a window)', async () => {
|
|
40
|
-
const lines = Array.from({ length: 100 }, (_, i) => `L${i + 1}`).join('\n');
|
|
41
|
-
await fs.writeFile(path.join(root, 'mid.ts'), lines, 'utf-8');
|
|
42
|
-
const r = await fileReadHandler({ path: 'mid.ts', offset: 50, limit: 5 }, ctx());
|
|
43
|
-
expect(r.content).toContain('51\tL51');
|
|
44
|
-
expect(r.content).toContain('55\tL55');
|
|
45
|
-
expect(r.content).not.toContain('56\tL56');
|
|
46
|
-
});
|
|
47
|
-
it('rejects negative offset', async () => {
|
|
48
|
-
await fs.writeFile(path.join(root, 'foo.ts'), 'line\n', 'utf-8');
|
|
49
|
-
const r = await fileReadHandler({ path: 'foo.ts', offset: -1 }, ctx());
|
|
50
|
-
expect(r.is_error).toBe(true);
|
|
51
|
-
expect(r.content).toMatch(/offset.*non-negative/i);
|
|
52
|
-
});
|
|
53
|
-
it('rejects negative limit', async () => {
|
|
54
|
-
await fs.writeFile(path.join(root, 'foo.ts'), 'line\n', 'utf-8');
|
|
55
|
-
const r = await fileReadHandler({ path: 'foo.ts', limit: -5 }, ctx());
|
|
56
|
-
expect(r.is_error).toBe(true);
|
|
57
|
-
expect(r.content).toMatch(/limit.*positive/i);
|
|
58
|
-
});
|
|
59
|
-
it('refuses to read .cspeach/* (sensitive denylist)', async () => {
|
|
60
|
-
await fs.mkdir(path.join(root, '.cspeach'), { recursive: true });
|
|
61
|
-
await fs.writeFile(path.join(root, '.cspeach', 'auth.json'), '{"password":"secret"}', 'utf-8');
|
|
62
|
-
const r = await fileReadHandler({ path: '.cspeach/auth.json' }, ctx());
|
|
63
|
-
expect(r.is_error).toBe(true);
|
|
64
|
-
expect(r.content).toMatch(/sensitive path/i);
|
|
65
|
-
expect(r.content).not.toContain('secret'); // must not leak
|
|
66
|
-
});
|
|
67
|
-
it('refuses to read .env (denylist)', async () => {
|
|
68
|
-
await fs.writeFile(path.join(root, '.env'), 'SECRET=value', 'utf-8');
|
|
69
|
-
const r = await fileReadHandler({ path: '.env' }, ctx());
|
|
70
|
-
expect(r.is_error).toBe(true);
|
|
71
|
-
expect(r.content).toMatch(/sensitive path/i);
|
|
72
|
-
});
|
|
73
|
-
it('does NOT mis-number a trailing blank line on files ending in newline', async () => {
|
|
74
|
-
// "a\nb\n" split naively produces ['a', 'b', ''] — line 3 should NOT appear.
|
|
75
|
-
await fs.writeFile(path.join(root, 'trailer.ts'), 'a\nb\n', 'utf-8');
|
|
76
|
-
const r = await fileReadHandler({ path: 'trailer.ts' }, ctx());
|
|
77
|
-
expect(r.content).toContain('1\ta');
|
|
78
|
-
expect(r.content).toContain('2\tb');
|
|
79
|
-
expect(r.content).not.toContain('3\t');
|
|
80
|
-
});
|
|
81
|
-
});
|
|
@@ -1,68 +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 { fileWriteHandler } from '../filesystem/file-write.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-file-write-')); });
|
|
10
|
-
afterEach(async () => { await fs.rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }); });
|
|
11
|
-
describe('file_write handler', () => {
|
|
12
|
-
it('creates a new file with the given content', async () => {
|
|
13
|
-
const r = await fileWriteHandler({ path: 'newfile.ts', content: 'hello world\n' }, ctx());
|
|
14
|
-
expect(r.is_error).toBeFalsy();
|
|
15
|
-
expect(r.content).toMatch(/wrote.*newfile\.ts/i);
|
|
16
|
-
const written = await fs.readFile(path.join(root, 'newfile.ts'), 'utf-8');
|
|
17
|
-
expect(written).toBe('hello world\n');
|
|
18
|
-
});
|
|
19
|
-
it('overwrites an existing file', async () => {
|
|
20
|
-
await fs.writeFile(path.join(root, 'existing.ts'), 'old content\n', 'utf-8');
|
|
21
|
-
const r = await fileWriteHandler({ path: 'existing.ts', content: 'new content\n' }, ctx());
|
|
22
|
-
expect(r.is_error).toBeFalsy();
|
|
23
|
-
expect(r.content).toMatch(/wrote.*existing\.ts/i);
|
|
24
|
-
const written = await fs.readFile(path.join(root, 'existing.ts'), 'utf-8');
|
|
25
|
-
expect(written).toBe('new content\n');
|
|
26
|
-
});
|
|
27
|
-
it('creates parent directories that do not exist yet', async () => {
|
|
28
|
-
const r = await fileWriteHandler({ path: 'deep/nested/dir/file.ts', content: 'content\n' }, ctx());
|
|
29
|
-
expect(r.is_error).toBeFalsy();
|
|
30
|
-
expect(r.content).toMatch(/wrote.*deep\/nested\/dir\/file\.ts/i);
|
|
31
|
-
const written = await fs.readFile(path.join(root, 'deep', 'nested', 'dir', 'file.ts'), 'utf-8');
|
|
32
|
-
expect(written).toBe('content\n');
|
|
33
|
-
});
|
|
34
|
-
it('refuses path traversal (../foo)', async () => {
|
|
35
|
-
const r = await fileWriteHandler({ path: '../../etc/evil.txt', content: 'bad' }, ctx());
|
|
36
|
-
expect(r.is_error).toBe(true);
|
|
37
|
-
expect(r.content).toMatch(/outside/i);
|
|
38
|
-
});
|
|
39
|
-
it('refuses denylisted path (.cspeach/auth.json)', async () => {
|
|
40
|
-
await fs.mkdir(path.join(root, '.cspeach'), { recursive: true });
|
|
41
|
-
const r = await fileWriteHandler({ path: '.cspeach/auth.json', content: '{"token":"hacked"}' }, ctx());
|
|
42
|
-
expect(r.is_error).toBe(true);
|
|
43
|
-
expect(r.content).toMatch(/sensitive path/i);
|
|
44
|
-
// file must not have been created/modified with the new content
|
|
45
|
-
const exists = await fs.access(path.join(root, '.cspeach', 'auth.json')).then(() => true, () => false);
|
|
46
|
-
expect(exists).toBe(false);
|
|
47
|
-
});
|
|
48
|
-
it('refuses denylisted path (.git/config)', async () => {
|
|
49
|
-
await fs.mkdir(path.join(root, '.git'), { recursive: true });
|
|
50
|
-
await fs.writeFile(path.join(root, '.git', 'config'), '[core]\n', 'utf-8');
|
|
51
|
-
const r = await fileWriteHandler({ path: '.git/config', content: 'tampered\n' }, ctx());
|
|
52
|
-
expect(r.is_error).toBe(true);
|
|
53
|
-
expect(r.content).toMatch(/sensitive path/i);
|
|
54
|
-
// original file must be unchanged
|
|
55
|
-
const content = await fs.readFile(path.join(root, '.git', 'config'), 'utf-8');
|
|
56
|
-
expect(content).toBe('[core]\n');
|
|
57
|
-
});
|
|
58
|
-
it('atomic write — no leftover .tmp file on success', async () => {
|
|
59
|
-
const r = await fileWriteHandler({ path: 'atomic.ts', content: 'data\n' }, ctx());
|
|
60
|
-
expect(r.is_error).toBeFalsy();
|
|
61
|
-
// target file exists
|
|
62
|
-
const written = await fs.readFile(path.join(root, 'atomic.ts'), 'utf-8');
|
|
63
|
-
expect(written).toBe('data\n');
|
|
64
|
-
// no leftover .tmp
|
|
65
|
-
const tmpExists = await fs.access(path.join(root, 'atomic.ts.tmp')).then(() => true, () => false);
|
|
66
|
-
expect(tmpExists).toBe(false);
|
|
67
|
-
});
|
|
68
|
-
});
|
|
@@ -1,128 +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 { globHandler, globToRegex } from '../filesystem/glob.js';
|
|
6
|
-
import { makeCtx } from './_helpers.js';
|
|
7
|
-
let root;
|
|
8
|
-
const ctx = () => makeCtx(root);
|
|
9
|
-
beforeEach(async () => {
|
|
10
|
-
root = await fs.mkdtemp(path.join(os.tmpdir(), 'cspeach-glob-'));
|
|
11
|
-
});
|
|
12
|
-
afterEach(async () => {
|
|
13
|
-
await fs.rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
|
|
14
|
-
});
|
|
15
|
-
// ─── globToRegex unit tests ────────────────────────────────────────────────
|
|
16
|
-
describe('globToRegex', () => {
|
|
17
|
-
it('* matches any chars within a single path segment (not /)', () => {
|
|
18
|
-
const re = globToRegex('*.ts');
|
|
19
|
-
expect(re.test('foo.ts')).toBe(true);
|
|
20
|
-
expect(re.test('bar.tsx')).toBe(false);
|
|
21
|
-
expect(re.test('src/foo.ts')).toBe(false); // * must not cross /
|
|
22
|
-
expect(re.test('.ts')).toBe(true); // zero chars is fine
|
|
23
|
-
});
|
|
24
|
-
it('** matches across path segments including /', () => {
|
|
25
|
-
const re = globToRegex('src/**/*.ts');
|
|
26
|
-
expect(re.test('src/foo.ts')).toBe(true);
|
|
27
|
-
expect(re.test('src/a/b/c/foo.ts')).toBe(true);
|
|
28
|
-
expect(re.test('lib/foo.ts')).toBe(false);
|
|
29
|
-
});
|
|
30
|
-
it('? matches exactly one char within a segment (not /)', () => {
|
|
31
|
-
const re = globToRegex('?.ts');
|
|
32
|
-
expect(re.test('a.ts')).toBe(true);
|
|
33
|
-
expect(re.test('ab.ts')).toBe(false);
|
|
34
|
-
expect(re.test('/a.ts')).toBe(false); // ? must not cross /
|
|
35
|
-
});
|
|
36
|
-
it('literal dots and special regex chars are escaped', () => {
|
|
37
|
-
const re = globToRegex('package.json');
|
|
38
|
-
expect(re.test('package.json')).toBe(true);
|
|
39
|
-
expect(re.test('packageXjson')).toBe(false); // dot must not act as regex .
|
|
40
|
-
});
|
|
41
|
-
it('** at root matches files at any depth', () => {
|
|
42
|
-
const re = globToRegex('**/*.json');
|
|
43
|
-
expect(re.test('foo.json')).toBe(true);
|
|
44
|
-
expect(re.test('a/b/c/foo.json')).toBe(true);
|
|
45
|
-
expect(re.test('a/b/c/foo.ts')).toBe(false);
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
// ─── globHandler integration tests ────────────────────────────────────────
|
|
49
|
-
describe('glob handler', () => {
|
|
50
|
-
it('returns "no files match" when the project is empty', async () => {
|
|
51
|
-
const r = await globHandler({ pattern: '*.ts' }, ctx());
|
|
52
|
-
expect(r.is_error).toBeFalsy();
|
|
53
|
-
expect(r.content).toMatch(/no files match/i);
|
|
54
|
-
});
|
|
55
|
-
it('finds .ts files with a simple *.ts pattern', async () => {
|
|
56
|
-
await fs.writeFile(path.join(root, 'index.ts'), 'export {}', 'utf-8');
|
|
57
|
-
await fs.writeFile(path.join(root, 'README.md'), '# hi', 'utf-8');
|
|
58
|
-
const r = await globHandler({ pattern: '*.ts' }, ctx());
|
|
59
|
-
expect(r.is_error).toBeFalsy();
|
|
60
|
-
expect(r.content).toContain('index.ts');
|
|
61
|
-
expect(r.content).not.toContain('README.md');
|
|
62
|
-
});
|
|
63
|
-
it('finds nested .tsx files with **/*.tsx pattern', async () => {
|
|
64
|
-
await fs.mkdir(path.join(root, 'src', 'components'), { recursive: true });
|
|
65
|
-
await fs.writeFile(path.join(root, 'src', 'components', 'Button.tsx'), 'export {}', 'utf-8');
|
|
66
|
-
await fs.writeFile(path.join(root, 'src', 'components', 'Button.test.ts'), 'test', 'utf-8');
|
|
67
|
-
const r = await globHandler({ pattern: '**/*.tsx' }, ctx());
|
|
68
|
-
expect(r.is_error).toBeFalsy();
|
|
69
|
-
expect(r.content).toContain('src/components/Button.tsx');
|
|
70
|
-
expect(r.content).not.toContain('Button.test.ts');
|
|
71
|
-
});
|
|
72
|
-
it('sorts results by mtime descending (most recent first)', async () => {
|
|
73
|
-
// Write three files with deliberate delays via manipulated mtime
|
|
74
|
-
const fileA = path.join(root, 'a.ts');
|
|
75
|
-
const fileB = path.join(root, 'b.ts');
|
|
76
|
-
const fileC = path.join(root, 'c.ts');
|
|
77
|
-
await fs.writeFile(fileA, 'a', 'utf-8');
|
|
78
|
-
await fs.writeFile(fileB, 'b', 'utf-8');
|
|
79
|
-
await fs.writeFile(fileC, 'c', 'utf-8');
|
|
80
|
-
// Manually set mtimes: A=oldest, B=middle, C=newest
|
|
81
|
-
const now = Date.now();
|
|
82
|
-
await fs.utimes(fileA, new Date(now - 3000), new Date(now - 3000));
|
|
83
|
-
await fs.utimes(fileB, new Date(now - 2000), new Date(now - 2000));
|
|
84
|
-
await fs.utimes(fileC, new Date(now - 1000), new Date(now - 1000));
|
|
85
|
-
const r = await globHandler({ pattern: '*.ts' }, ctx());
|
|
86
|
-
expect(r.is_error).toBeFalsy();
|
|
87
|
-
const lines = r.content.split('\n');
|
|
88
|
-
const posA = lines.findIndex((l) => l.includes('a.ts'));
|
|
89
|
-
const posB = lines.findIndex((l) => l.includes('b.ts'));
|
|
90
|
-
const posC = lines.findIndex((l) => l.includes('c.ts'));
|
|
91
|
-
// c should appear before b which should appear before a
|
|
92
|
-
expect(posC).toBeLessThan(posB);
|
|
93
|
-
expect(posB).toBeLessThan(posA);
|
|
94
|
-
});
|
|
95
|
-
it('excludes denylisted paths (.cspeach/auth.json)', async () => {
|
|
96
|
-
await fs.mkdir(path.join(root, '.cspeach'), { recursive: true });
|
|
97
|
-
await fs.writeFile(path.join(root, '.cspeach', 'auth.json'), '{"token":"secret"}', 'utf-8');
|
|
98
|
-
await fs.writeFile(path.join(root, 'ok.json'), '{}', 'utf-8');
|
|
99
|
-
const r = await globHandler({ pattern: '**/*.json' }, ctx());
|
|
100
|
-
expect(r.is_error).toBeFalsy();
|
|
101
|
-
expect(r.content).not.toContain('auth.json');
|
|
102
|
-
expect(r.content).toContain('ok.json');
|
|
103
|
-
});
|
|
104
|
-
it('skips node_modules directory', async () => {
|
|
105
|
-
await fs.mkdir(path.join(root, 'node_modules', 'foo'), { recursive: true });
|
|
106
|
-
await fs.writeFile(path.join(root, 'node_modules', 'foo', 'index.ts'), 'module', 'utf-8');
|
|
107
|
-
await fs.writeFile(path.join(root, 'real.ts'), 'real', 'utf-8');
|
|
108
|
-
const r = await globHandler({ pattern: '**/*.ts' }, ctx());
|
|
109
|
-
expect(r.is_error).toBeFalsy();
|
|
110
|
-
expect(r.content).not.toContain('node_modules');
|
|
111
|
-
expect(r.content).toContain('real.ts');
|
|
112
|
-
});
|
|
113
|
-
it('refuses cwd that escapes the project root', async () => {
|
|
114
|
-
const r = await globHandler({ pattern: '*.ts', cwd: '../../etc' }, ctx());
|
|
115
|
-
expect(r.is_error).toBe(true);
|
|
116
|
-
expect(r.content).toMatch(/outside/i);
|
|
117
|
-
});
|
|
118
|
-
it('returns an error when search root does not exist', async () => {
|
|
119
|
-
const r = await globHandler({ pattern: '*.ts', cwd: 'nonexistent-dir' }, ctx());
|
|
120
|
-
expect(r.is_error).toBe(true);
|
|
121
|
-
expect(r.content).toMatch(/does not exist/i);
|
|
122
|
-
});
|
|
123
|
-
it('returns an error when pattern is missing or empty', async () => {
|
|
124
|
-
const r = await globHandler({ pattern: '' }, ctx());
|
|
125
|
-
expect(r.is_error).toBe(true);
|
|
126
|
-
expect(r.content).toMatch(/pattern.*required/i);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
@@ -1,71 +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 { grepHandler } from '../filesystem/grep.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-grep-')); });
|
|
10
|
-
afterEach(async () => { await fs.rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }); });
|
|
11
|
-
describe('grep handler', () => {
|
|
12
|
-
it('finds a literal string match', async () => {
|
|
13
|
-
await fs.writeFile(path.join(root, 'a.ts'), 'const x = 1;\nconst y = 2;\n', 'utf-8');
|
|
14
|
-
const r = await grepHandler({ pattern: 'const x' }, ctx());
|
|
15
|
-
expect(r.is_error).toBeFalsy();
|
|
16
|
-
expect(r.content).toMatch(/a\.ts:1:const x = 1;/);
|
|
17
|
-
});
|
|
18
|
-
it('matches a regex pattern', async () => {
|
|
19
|
-
await fs.writeFile(path.join(root, 'a.ts'), 'foo123\nbar456\n', 'utf-8');
|
|
20
|
-
const r = await grepHandler({ pattern: '\\d+' }, ctx());
|
|
21
|
-
expect(r.content).toContain('a.ts:1:foo123');
|
|
22
|
-
expect(r.content).toContain('a.ts:2:bar456');
|
|
23
|
-
});
|
|
24
|
-
it('is case-insensitive when case_insensitive=true', async () => {
|
|
25
|
-
await fs.writeFile(path.join(root, 'a.ts'), 'Hello\nHELLO\nhello\n', 'utf-8');
|
|
26
|
-
const r = await grepHandler({ pattern: 'hello', case_insensitive: true }, ctx());
|
|
27
|
-
expect(r.content.split('\n')).toHaveLength(3);
|
|
28
|
-
});
|
|
29
|
-
it('glob filter scopes which files get scanned', async () => {
|
|
30
|
-
await fs.writeFile(path.join(root, 'a.ts'), 'match\n', 'utf-8');
|
|
31
|
-
await fs.writeFile(path.join(root, 'b.md'), 'match\n', 'utf-8');
|
|
32
|
-
const r = await grepHandler({ pattern: 'match', glob: '**/*.ts' }, ctx());
|
|
33
|
-
expect(r.content).toContain('a.ts');
|
|
34
|
-
expect(r.content).not.toContain('b.md');
|
|
35
|
-
});
|
|
36
|
-
it('files_with_matches mode returns just filenames', async () => {
|
|
37
|
-
await fs.writeFile(path.join(root, 'a.ts'), 'foo\nfoo\n', 'utf-8');
|
|
38
|
-
await fs.writeFile(path.join(root, 'b.ts'), 'foo\n', 'utf-8');
|
|
39
|
-
const r = await grepHandler({ pattern: 'foo', output_mode: 'files_with_matches' }, ctx());
|
|
40
|
-
const lines = r.content.split('\n').sort();
|
|
41
|
-
expect(lines).toEqual(['a.ts', 'b.ts']);
|
|
42
|
-
});
|
|
43
|
-
it('count mode returns per-file counts', async () => {
|
|
44
|
-
await fs.writeFile(path.join(root, 'a.ts'), 'foo\nfoo\nfoo\n', 'utf-8');
|
|
45
|
-
await fs.writeFile(path.join(root, 'b.ts'), 'foo\n', 'utf-8');
|
|
46
|
-
const r = await grepHandler({ pattern: 'foo', output_mode: 'count' }, ctx());
|
|
47
|
-
expect(r.content).toContain('3\ta.ts');
|
|
48
|
-
expect(r.content).toContain('1\tb.ts');
|
|
49
|
-
});
|
|
50
|
-
it('returns error envelope on invalid regex', async () => {
|
|
51
|
-
const r = await grepHandler({ pattern: '[invalid' }, ctx());
|
|
52
|
-
expect(r.is_error).toBe(true);
|
|
53
|
-
expect(r.content).toMatch(/invalid regex/i);
|
|
54
|
-
});
|
|
55
|
-
it('does NOT scan denylisted paths (.cspeach/auth.json)', async () => {
|
|
56
|
-
await fs.mkdir(path.join(root, '.cspeach'), { recursive: true });
|
|
57
|
-
await fs.writeFile(path.join(root, '.cspeach', 'auth.json'), '{"password":"SECRET"}', 'utf-8');
|
|
58
|
-
await fs.writeFile(path.join(root, 'a.ts'), 'no match here\n', 'utf-8');
|
|
59
|
-
const r = await grepHandler({ pattern: 'SECRET' }, ctx());
|
|
60
|
-
// The denylist blocks .cspeach/auth.json — nothing matches, so we get the no-matches sentinel.
|
|
61
|
-
// The no-matches message echoes the pattern name; what must NOT appear is a content line
|
|
62
|
-
// referencing the denylist file.
|
|
63
|
-
expect(r.content).toMatch(/no matches/i);
|
|
64
|
-
expect(r.content).not.toContain('auth.json');
|
|
65
|
-
});
|
|
66
|
-
it('returns "no matches" when nothing hits', async () => {
|
|
67
|
-
await fs.writeFile(path.join(root, 'a.ts'), 'hello world\n', 'utf-8');
|
|
68
|
-
const r = await grepHandler({ pattern: 'xyz123' }, ctx());
|
|
69
|
-
expect(r.content).toMatch(/no matches/i);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { monitorEmitHandler } from '../subagent/monitor_emit.js';
|
|
3
|
-
import { registerBackground, recordEvent, __resetRegistryForTests, MAX_EVENTS_PER_BG, } from '../subagent/_background-shared.js';
|
|
4
|
-
import { makeCtx } from './_helpers.js';
|
|
5
|
-
const ctx = () => makeCtx(process.cwd());
|
|
6
|
-
beforeEach(() => { __resetRegistryForTests(); });
|
|
7
|
-
function fakeRegister(bg_id) {
|
|
8
|
-
registerBackground({
|
|
9
|
-
bg_id,
|
|
10
|
-
command: 'node',
|
|
11
|
-
argv: ['-e', 'noop'],
|
|
12
|
-
child: { kill: () => true, pid: 1 },
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
// monitor_emit's BG_ID_PATTERN enforces this exact shape — bg-<v4-uuid>.
|
|
16
|
-
// Tests register IDs that match the pattern so the handler validates them.
|
|
17
|
-
const BG_1 = 'bg-11111111-1111-1111-1111-111111111111';
|
|
18
|
-
const BG_2 = 'bg-22222222-2222-2222-2222-222222222222';
|
|
19
|
-
const BG_3 = 'bg-33333333-3333-3333-3333-333333333333';
|
|
20
|
-
const BG_4 = 'bg-44444444-4444-4444-4444-444444444444';
|
|
21
|
-
const BG_EVICT = 'bg-eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee';
|
|
22
|
-
const BG_CLOSED = 'bg-aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
|
|
23
|
-
describe('monitor_emit handler', () => {
|
|
24
|
-
it('returns recorded stdout events as JSON', async () => {
|
|
25
|
-
fakeRegister(BG_1);
|
|
26
|
-
recordEvent(BG_1, { kind: 'stdout', text: 'hello', ts: 1 });
|
|
27
|
-
recordEvent(BG_1, { kind: 'stdout', text: 'world', ts: 2 });
|
|
28
|
-
const r = await monitorEmitHandler({ bg_id: BG_1 }, ctx());
|
|
29
|
-
expect(r.is_error).toBeFalsy();
|
|
30
|
-
const parsed = JSON.parse(r.content);
|
|
31
|
-
expect(parsed.events).toHaveLength(2);
|
|
32
|
-
expect(parsed.events[0].text).toBe('hello');
|
|
33
|
-
expect(parsed.events[1].text).toBe('world');
|
|
34
|
-
expect(parsed.closed).toBe(false);
|
|
35
|
-
expect(parsed.dropped).toBe(0);
|
|
36
|
-
});
|
|
37
|
-
it('advances the cursor per call — subsequent calls only see new events', async () => {
|
|
38
|
-
fakeRegister(BG_2);
|
|
39
|
-
recordEvent(BG_2, { kind: 'stdout', text: 'first', ts: 1 });
|
|
40
|
-
const r1 = await monitorEmitHandler({ bg_id: BG_2 }, ctx());
|
|
41
|
-
expect(JSON.parse(r1.content).events).toHaveLength(1);
|
|
42
|
-
recordEvent(BG_2, { kind: 'stdout', text: 'second', ts: 2 });
|
|
43
|
-
const r2 = await monitorEmitHandler({ bg_id: BG_2 }, ctx());
|
|
44
|
-
const parsed2 = JSON.parse(r2.content);
|
|
45
|
-
expect(parsed2.events).toHaveLength(1);
|
|
46
|
-
expect(parsed2.events[0].text).toBe('second');
|
|
47
|
-
});
|
|
48
|
-
it('caps max per-call at 1000 even when buffer holds more', async () => {
|
|
49
|
-
fakeRegister(BG_3);
|
|
50
|
-
for (let i = 0; i < 1500; i++)
|
|
51
|
-
recordEvent(BG_3, { kind: 'stdout', text: `n${i}`, ts: i });
|
|
52
|
-
const r = await monitorEmitHandler({ bg_id: BG_3, max: 999999 }, ctx());
|
|
53
|
-
const parsed = JSON.parse(r.content);
|
|
54
|
-
expect(parsed.events.length).toBe(1000);
|
|
55
|
-
});
|
|
56
|
-
it('honours max arg (default 100)', async () => {
|
|
57
|
-
fakeRegister(BG_4);
|
|
58
|
-
for (let i = 0; i < 200; i++)
|
|
59
|
-
recordEvent(BG_4, { kind: 'stdout', text: `n${i}`, ts: i });
|
|
60
|
-
const rDefault = await monitorEmitHandler({ bg_id: BG_4 }, ctx());
|
|
61
|
-
expect(JSON.parse(rDefault.content).events.length).toBe(100);
|
|
62
|
-
const rExplicit = await monitorEmitHandler({ bg_id: BG_4, max: 50 }, ctx());
|
|
63
|
-
expect(JSON.parse(rExplicit.content).events.length).toBe(50);
|
|
64
|
-
});
|
|
65
|
-
it('returns structured error for an unknown bg_id (with valid shape)', async () => {
|
|
66
|
-
// Use a well-formed UUID-shape bg_id that hasn't been registered.
|
|
67
|
-
const r = await monitorEmitHandler({ bg_id: 'bg-99999999-9999-9999-9999-999999999999' }, ctx());
|
|
68
|
-
expect(r.is_error).toBe(true);
|
|
69
|
-
expect(r.content).toMatch(/not found|unknown/i);
|
|
70
|
-
});
|
|
71
|
-
it('surfaces dropped counter after FIFO eviction; counter resets on read', async () => {
|
|
72
|
-
fakeRegister(BG_EVICT);
|
|
73
|
-
for (let i = 0; i < MAX_EVENTS_PER_BG + 30; i++) {
|
|
74
|
-
recordEvent(BG_EVICT, { kind: 'stdout', text: `n${i}`, ts: i });
|
|
75
|
-
}
|
|
76
|
-
const r1 = await monitorEmitHandler({ bg_id: BG_EVICT, max: 1000 }, ctx());
|
|
77
|
-
expect(JSON.parse(r1.content).dropped).toBe(30);
|
|
78
|
-
const r2 = await monitorEmitHandler({ bg_id: BG_EVICT, max: 1000 }, ctx());
|
|
79
|
-
expect(JSON.parse(r2.content).dropped).toBe(0);
|
|
80
|
-
});
|
|
81
|
-
it('marks closed: true after the child emits an exit event', async () => {
|
|
82
|
-
fakeRegister(BG_CLOSED);
|
|
83
|
-
recordEvent(BG_CLOSED, { kind: 'stdout', text: 'final', ts: 1 });
|
|
84
|
-
recordEvent(BG_CLOSED, { kind: 'exit', text: 'code=0', ts: 2, exitCode: 0, signal: null });
|
|
85
|
-
const r = await monitorEmitHandler({ bg_id: BG_CLOSED }, ctx());
|
|
86
|
-
const parsed = JSON.parse(r.content);
|
|
87
|
-
expect(parsed.closed).toBe(true);
|
|
88
|
-
expect(parsed.exitCode).toBe(0);
|
|
89
|
-
expect(parsed.events.some((e) => e.kind === 'exit')).toBe(true);
|
|
90
|
-
});
|
|
91
|
-
it('rejects bg_id with invalid shape (defense-in-depth on registry-key injection)', async () => {
|
|
92
|
-
const r = await monitorEmitHandler({ bg_id: '../../etc/passwd' }, ctx());
|
|
93
|
-
expect(r.is_error).toBe(true);
|
|
94
|
-
expect(r.content).toMatch(/invalid|not found/i);
|
|
95
|
-
});
|
|
96
|
-
});
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import { playbookGetHandler, __resetPlaybookCacheForTests, __setPlaybookCacheTtlForTests } from '../project/playbook_get.js';
|
|
3
|
-
import { makeCtx } from './_helpers.js';
|
|
4
|
-
const FAKE_KEY = 'sk-ant-test-key-leak-guard-1234567890';
|
|
5
|
-
let origAnthropic;
|
|
6
|
-
let origCspeach;
|
|
7
|
-
let origBrave;
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
origAnthropic = process.env.ANTHROPIC_API_KEY;
|
|
10
|
-
origCspeach = process.env.CSPEACH_API_KEY;
|
|
11
|
-
origBrave = process.env.CSPEACH_BRAVE_SEARCH_KEY;
|
|
12
|
-
process.env.ANTHROPIC_API_KEY = FAKE_KEY;
|
|
13
|
-
process.env.CSPEACH_API_KEY = FAKE_KEY;
|
|
14
|
-
process.env.CSPEACH_BRAVE_SEARCH_KEY = FAKE_KEY;
|
|
15
|
-
__resetPlaybookCacheForTests();
|
|
16
|
-
__setPlaybookCacheTtlForTests(300_000);
|
|
17
|
-
});
|
|
18
|
-
afterEach(() => {
|
|
19
|
-
if (origAnthropic === undefined)
|
|
20
|
-
delete process.env.ANTHROPIC_API_KEY;
|
|
21
|
-
else
|
|
22
|
-
process.env.ANTHROPIC_API_KEY = origAnthropic;
|
|
23
|
-
if (origCspeach === undefined)
|
|
24
|
-
delete process.env.CSPEACH_API_KEY;
|
|
25
|
-
else
|
|
26
|
-
process.env.CSPEACH_API_KEY = origCspeach;
|
|
27
|
-
if (origBrave === undefined)
|
|
28
|
-
delete process.env.CSPEACH_BRAVE_SEARCH_KEY;
|
|
29
|
-
else
|
|
30
|
-
process.env.CSPEACH_BRAVE_SEARCH_KEY = origBrave;
|
|
31
|
-
__resetPlaybookCacheForTests();
|
|
32
|
-
__setPlaybookCacheTtlForTests(300_000);
|
|
33
|
-
vi.restoreAllMocks();
|
|
34
|
-
});
|
|
35
|
-
function makeSource(body, name = 'abap-review') {
|
|
36
|
-
const calls = [];
|
|
37
|
-
const src = {
|
|
38
|
-
async getSkillBody(n) {
|
|
39
|
-
calls.push(n);
|
|
40
|
-
return { name: n, body, sha256: 'fake-sha', signature: 'fake-sig', signedAt: '2026-05-12T00:00:00Z' };
|
|
41
|
-
},
|
|
42
|
-
async listAvailableSkills() { return [name]; },
|
|
43
|
-
describe() { return 'fake'; },
|
|
44
|
-
};
|
|
45
|
-
src.__calls = calls;
|
|
46
|
-
return src;
|
|
47
|
-
}
|
|
48
|
-
function makeFailingSource(err) {
|
|
49
|
-
return {
|
|
50
|
-
async getSkillBody() { throw err; },
|
|
51
|
-
async listAvailableSkills() { return []; },
|
|
52
|
-
describe() { return 'failing-fake'; },
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
describe('playbook_get', () => {
|
|
56
|
-
it('returns the body when ctx.skillSource resolves', async () => {
|
|
57
|
-
const src = makeSource('---\nname: abap-review\n---\n\nReview ABAP code.\n');
|
|
58
|
-
const ctx = makeCtx(process.cwd(), { skillSource: src });
|
|
59
|
-
const r = await playbookGetHandler({ name: 'abap-review' }, ctx);
|
|
60
|
-
expect(r.is_error).toBeFalsy();
|
|
61
|
-
expect(r.content).toContain('Review ABAP code.');
|
|
62
|
-
expect(src.__calls).toEqual(['abap-review']);
|
|
63
|
-
});
|
|
64
|
-
it('returns a structured error when ctx.skillSource is undefined', async () => {
|
|
65
|
-
const ctx = makeCtx(process.cwd());
|
|
66
|
-
const r = await playbookGetHandler({ name: 'abap-review' }, ctx);
|
|
67
|
-
expect(r.is_error).toBe(true);
|
|
68
|
-
expect(r.content).toMatch(/skillSource|not available|not wired/i);
|
|
69
|
-
});
|
|
70
|
-
it('rejects an empty name with a structured error', async () => {
|
|
71
|
-
const src = makeSource('body', 'abap-review');
|
|
72
|
-
const ctx = makeCtx(process.cwd(), { skillSource: src });
|
|
73
|
-
const r = await playbookGetHandler({ name: '' }, ctx);
|
|
74
|
-
expect(r.is_error).toBe(true);
|
|
75
|
-
expect(r.content).toMatch(/name.*required|empty/i);
|
|
76
|
-
});
|
|
77
|
-
it('rejects a malformed name with a structured error', async () => {
|
|
78
|
-
const src = makeSource('body', 'abap-review');
|
|
79
|
-
const ctx = makeCtx(process.cwd(), { skillSource: src });
|
|
80
|
-
const r = await playbookGetHandler({ name: '../etc/passwd' }, ctx);
|
|
81
|
-
expect(r.is_error).toBe(true);
|
|
82
|
-
expect(r.content).toMatch(/invalid|malformed|name/i);
|
|
83
|
-
});
|
|
84
|
-
it('caches the result — second call within TTL does not refetch', async () => {
|
|
85
|
-
const src = makeSource('cached body', 'abap-review');
|
|
86
|
-
const ctx = makeCtx(process.cwd(), { skillSource: src });
|
|
87
|
-
await playbookGetHandler({ name: 'abap-review' }, ctx);
|
|
88
|
-
await playbookGetHandler({ name: 'abap-review' }, ctx);
|
|
89
|
-
expect(src.__calls).toEqual(['abap-review']);
|
|
90
|
-
});
|
|
91
|
-
it('refetches when the TTL has expired', async () => {
|
|
92
|
-
const src = makeSource('body', 'abap-review');
|
|
93
|
-
const ctx = makeCtx(process.cwd(), { skillSource: src });
|
|
94
|
-
__setPlaybookCacheTtlForTests(1);
|
|
95
|
-
await playbookGetHandler({ name: 'abap-review' }, ctx);
|
|
96
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
97
|
-
await playbookGetHandler({ name: 'abap-review' }, ctx);
|
|
98
|
-
expect(src.__calls).toEqual(['abap-review', 'abap-review']);
|
|
99
|
-
});
|
|
100
|
-
it('scrubs API key values from error envelopes', async () => {
|
|
101
|
-
const err = new Error(`auth failed: bearer=${FAKE_KEY} request denied`);
|
|
102
|
-
const src = makeFailingSource(err);
|
|
103
|
-
const ctx = makeCtx(process.cwd(), { skillSource: src });
|
|
104
|
-
const r = await playbookGetHandler({ name: 'abap-review' }, ctx);
|
|
105
|
-
expect(r.is_error).toBe(true);
|
|
106
|
-
expect(r.content).not.toContain(FAKE_KEY);
|
|
107
|
-
expect(r.content).toMatch(/\*\*\*/);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import * as os from 'node:os';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
|
-
import { promises as fs } from 'node:fs';
|
|
5
|
-
import { projectContextGetHandler, __resetToolCacheForTests } from '../project/project_context_get.js';
|
|
6
|
-
import { makeCtx } from './_helpers.js';
|
|
7
|
-
import { __resetProjectContextCacheForTests } from '../../agent/maybe-build-project-context.js';
|
|
8
|
-
let tmpRoot;
|
|
9
|
-
beforeEach(async () => {
|
|
10
|
-
tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'cspeach-project-context-get-'));
|
|
11
|
-
__resetProjectContextCacheForTests();
|
|
12
|
-
__resetToolCacheForTests();
|
|
13
|
-
});
|
|
14
|
-
afterEach(async () => {
|
|
15
|
-
try {
|
|
16
|
-
await fs.rm(tmpRoot, { recursive: true, force: true });
|
|
17
|
-
}
|
|
18
|
-
catch { /* ignore */ }
|
|
19
|
-
__resetProjectContextCacheForTests();
|
|
20
|
-
__resetToolCacheForTests();
|
|
21
|
-
vi.restoreAllMocks();
|
|
22
|
-
});
|
|
23
|
-
describe('project_context_get', () => {
|
|
24
|
-
it('returns a rendered <project_context> block when cwd resolves', async () => {
|
|
25
|
-
await fs.writeFile(path.join(tmpRoot, 'CLAUDE.md'), '# Test project\n');
|
|
26
|
-
await fs.writeFile(path.join(tmpRoot, 'package.json'), JSON.stringify({ name: 'x', version: '1.0.0' }));
|
|
27
|
-
const r = await projectContextGetHandler({}, makeCtx(tmpRoot));
|
|
28
|
-
expect(r.is_error).toBeFalsy();
|
|
29
|
-
expect(r.content).toMatch(/<project_context/);
|
|
30
|
-
expect(r.content).toMatch(/<\/project_context>/);
|
|
31
|
-
});
|
|
32
|
-
it('returns a structured error when cwd is empty', async () => {
|
|
33
|
-
const r = await projectContextGetHandler({}, makeCtx(''));
|
|
34
|
-
expect(r.is_error).toBe(true);
|
|
35
|
-
expect(r.content).toMatch(/cwd/i);
|
|
36
|
-
});
|
|
37
|
-
it('uses the cached render on a default call (force_refresh false)', async () => {
|
|
38
|
-
await fs.writeFile(path.join(tmpRoot, 'CLAUDE.md'), '# Initial content\n');
|
|
39
|
-
const r1 = await projectContextGetHandler({}, makeCtx(tmpRoot));
|
|
40
|
-
expect(r1.is_error).toBeFalsy();
|
|
41
|
-
expect(r1.content).toMatch(/Initial content/);
|
|
42
|
-
await fs.writeFile(path.join(tmpRoot, 'CLAUDE.md'), '# Updated content\n');
|
|
43
|
-
const r2 = await projectContextGetHandler({}, makeCtx(tmpRoot));
|
|
44
|
-
expect(r2.is_error).toBeFalsy();
|
|
45
|
-
expect(r2.content).toMatch(/Initial content/);
|
|
46
|
-
expect(r2.content).not.toMatch(/Updated content/);
|
|
47
|
-
});
|
|
48
|
-
it('rebuilds when force_refresh: true is passed', async () => {
|
|
49
|
-
await fs.writeFile(path.join(tmpRoot, 'CLAUDE.md'), '# First content\n');
|
|
50
|
-
const r1 = await projectContextGetHandler({}, makeCtx(tmpRoot));
|
|
51
|
-
expect(r1.content).toMatch(/First content/);
|
|
52
|
-
await fs.writeFile(path.join(tmpRoot, 'CLAUDE.md'), '# Refreshed content\n');
|
|
53
|
-
const r2 = await projectContextGetHandler({ force_refresh: true }, makeCtx(tmpRoot));
|
|
54
|
-
expect(r2.is_error).toBeFalsy();
|
|
55
|
-
expect(r2.content).toMatch(/Refreshed content/);
|
|
56
|
-
expect(r2.content).not.toMatch(/First content/);
|
|
57
|
-
});
|
|
58
|
-
it('rendered block does not contain bearer or known key prefixes (sanity scrub)', async () => {
|
|
59
|
-
await fs.writeFile(path.join(tmpRoot, 'CLAUDE.md'), '# Plain project\n');
|
|
60
|
-
const r = await projectContextGetHandler({}, makeCtx(tmpRoot));
|
|
61
|
-
expect(r.is_error).toBeFalsy();
|
|
62
|
-
expect(r.content).not.toMatch(/Bearer\s+ey[A-Za-z0-9._-]+/i);
|
|
63
|
-
expect(r.content).not.toMatch(/sk-ant-[A-Za-z0-9_-]+/);
|
|
64
|
-
expect(r.content).not.toMatch(/ANTHROPIC_API_KEY/);
|
|
65
|
-
});
|
|
66
|
-
it('rejects force_refresh with a non-boolean value', async () => {
|
|
67
|
-
const r = await projectContextGetHandler({ force_refresh: 'yes' }, makeCtx(tmpRoot));
|
|
68
|
-
expect(r.is_error).toBe(true);
|
|
69
|
-
expect(r.content).toMatch(/force_refresh.*boolean/i);
|
|
70
|
-
});
|
|
71
|
-
});
|