@studio-foundation/cli 0.3.0-beta.1 → 0.3.0-beta.6
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/package.json +9 -5
- package/ARCHITECTURE.md +0 -40
- package/src/commands/api.ts +0 -126
- package/src/commands/config.ts +0 -433
- package/src/commands/init.ts +0 -879
- package/src/commands/install.ts +0 -23
- package/src/commands/integrations.ts +0 -332
- package/src/commands/list.ts +0 -197
- package/src/commands/logs.ts +0 -119
- package/src/commands/ollama.ts +0 -168
- package/src/commands/project.ts +0 -167
- package/src/commands/registry/audit.ts +0 -87
- package/src/commands/registry/index.ts +0 -63
- package/src/commands/registry/install.ts +0 -222
- package/src/commands/registry/publish.ts +0 -112
- package/src/commands/registry/remove.ts +0 -89
- package/src/commands/registry/search.ts +0 -93
- package/src/commands/registry/sync.ts +0 -24
- package/src/commands/registry/update.ts +0 -63
- package/src/commands/replay.ts +0 -559
- package/src/commands/run.ts +0 -454
- package/src/commands/status.ts +0 -163
- package/src/commands/template/index.ts +0 -59
- package/src/commands/template/validate.ts +0 -175
- package/src/commands/templates.ts +0 -99
- package/src/commands/tools.ts +0 -227
- package/src/commands/users.ts +0 -127
- package/src/commands/validate.ts +0 -46
- package/src/config.ts +0 -101
- package/src/index.ts +0 -201
- package/src/models-cache.ts +0 -127
- package/src/output/file-changes.ts +0 -97
- package/src/output/formatter.ts +0 -85
- package/src/output/formatters.ts +0 -303
- package/src/output/logger.ts +0 -18
- package/src/output/parallel-progress.ts +0 -103
- package/src/output/progress.ts +0 -414
- package/src/provider-validator.ts +0 -109
- package/src/registry/cache.ts +0 -46
- package/src/registry/client.ts +0 -84
- package/src/registry/lockfile.ts +0 -68
- package/src/registry/resolver.ts +0 -113
- package/src/registry/types.ts +0 -66
- package/src/run-logger.ts +0 -66
- package/src/run-store-factory.ts +0 -30
- package/src/studio-dir.ts +0 -28
- package/src/utils/input-wizard.ts +0 -73
- package/src/utils/placeholders.ts +0 -10
- package/tests/__stubs__/studio-runner.ts +0 -104
- package/tests/commands/api.test.ts +0 -110
- package/tests/commands/config.test.ts +0 -221
- package/tests/commands/init.test.ts +0 -919
- package/tests/commands/install.test.ts +0 -52
- package/tests/commands/integrations.test.ts +0 -158
- package/tests/commands/ollama.test.ts +0 -139
- package/tests/commands/project.test.ts +0 -179
- package/tests/commands/registry/audit.test.ts +0 -56
- package/tests/commands/registry/install.test.ts +0 -200
- package/tests/commands/registry/publish.test.ts +0 -56
- package/tests/commands/registry/remove.test.ts +0 -103
- package/tests/commands/registry/search.test.ts +0 -44
- package/tests/commands/registry/sync.test.ts +0 -37
- package/tests/commands/registry/update.test.ts +0 -62
- package/tests/commands/replay.test.ts +0 -283
- package/tests/commands/template/validate.test.ts +0 -150
- package/tests/commands/templates.test.ts +0 -42
- package/tests/commands/tools.test.ts +0 -106
- package/tests/config.test.ts +0 -142
- package/tests/formatter.test.ts +0 -158
- package/tests/integration/sigint.test.ts +0 -188
- package/tests/models-cache.test.ts +0 -250
- package/tests/output/file-changes.test.ts +0 -178
- package/tests/output/formatters.test.ts +0 -448
- package/tests/output/progress-spinner.test.ts +0 -232
- package/tests/output/progress-timer.test.ts +0 -230
- package/tests/provider-validator.test.ts +0 -182
- package/tests/registry/cache.test.ts +0 -66
- package/tests/registry/client.test.ts +0 -70
- package/tests/registry/lockfile.test.ts +0 -87
- package/tests/registry/resolver.test.ts +0 -122
- package/tests/run-logger-events.test.ts +0 -326
- package/tests/run-logger.test.ts +0 -64
- package/tests/run-store-factory.test.ts +0 -51
- package/tests/studio-dir.test.ts +0 -31
- package/tests/utils/input-wizard.test.ts +0 -153
- package/tests/utils/placeholders.test.ts +0 -36
- package/tsconfig.json +0 -24
- package/vitest.config.ts +0 -20
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
setCachedModels,
|
|
4
|
-
getCachedModels,
|
|
5
|
-
parseAndCacheModels,
|
|
6
|
-
getAvailableModels,
|
|
7
|
-
filterModels,
|
|
8
|
-
sortModels,
|
|
9
|
-
} from '../src/models-cache.js';
|
|
10
|
-
|
|
11
|
-
// ─── filterModels ────────────────────────────────────────────────────────────
|
|
12
|
-
|
|
13
|
-
describe('filterModels — anthropic', () => {
|
|
14
|
-
it('keeps claude-* models', () => {
|
|
15
|
-
const models = [
|
|
16
|
-
{ id: 'claude-sonnet-4-20250514' },
|
|
17
|
-
{ id: 'claude-haiku-4-20250514' },
|
|
18
|
-
];
|
|
19
|
-
expect(filterModels('anthropic', models)).toEqual([
|
|
20
|
-
'claude-sonnet-4-20250514',
|
|
21
|
-
'claude-haiku-4-20250514',
|
|
22
|
-
]);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('excludes claude-instant*', () => {
|
|
26
|
-
const models = [
|
|
27
|
-
{ id: 'claude-sonnet-4-20250514' },
|
|
28
|
-
{ id: 'claude-instant-1' },
|
|
29
|
-
];
|
|
30
|
-
expect(filterModels('anthropic', models)).not.toContain('claude-instant-1');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('excludes claude-2*', () => {
|
|
34
|
-
const models = [
|
|
35
|
-
{ id: 'claude-sonnet-4-20250514' },
|
|
36
|
-
{ id: 'claude-2.1' },
|
|
37
|
-
];
|
|
38
|
-
expect(filterModels('anthropic', models)).not.toContain('claude-2.1');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('excludes non-claude models', () => {
|
|
42
|
-
const models = [{ id: 'gpt-4o' }, { id: 'claude-sonnet-4-20250514' }];
|
|
43
|
-
expect(filterModels('anthropic', models)).toEqual(['claude-sonnet-4-20250514']);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
describe('filterModels — openai', () => {
|
|
48
|
-
it('keeps gpt-4* models', () => {
|
|
49
|
-
const models = [{ id: 'gpt-4o' }, { id: 'gpt-4-turbo' }];
|
|
50
|
-
const result = filterModels('openai', models);
|
|
51
|
-
expect(result).toContain('gpt-4o');
|
|
52
|
-
expect(result).toContain('gpt-4-turbo');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('keeps o1* and o3* models', () => {
|
|
56
|
-
const models = [{ id: 'o1-preview' }, { id: 'o3-mini' }, { id: 'gpt-3.5-turbo' }];
|
|
57
|
-
const result = filterModels('openai', models);
|
|
58
|
-
expect(result).toContain('o1-preview');
|
|
59
|
-
expect(result).toContain('o3-mini');
|
|
60
|
-
expect(result).not.toContain('gpt-3.5-turbo');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('excludes gpt-3.5* models', () => {
|
|
64
|
-
const models = [{ id: 'gpt-3.5-turbo' }, { id: 'gpt-4o' }];
|
|
65
|
-
expect(filterModels('openai', models)).toEqual(['gpt-4o']);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe('filterModels — unknown provider', () => {
|
|
70
|
-
it('keeps all models for unknown providers', () => {
|
|
71
|
-
const models = [{ id: 'model-a' }, { id: 'model-b' }];
|
|
72
|
-
expect(filterModels('future-provider', models)).toEqual(['model-a', 'model-b']);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// ─── sortModels ──────────────────────────────────────────────────────────────
|
|
77
|
-
|
|
78
|
-
describe('sortModels', () => {
|
|
79
|
-
it('sorts by created timestamp descending', () => {
|
|
80
|
-
const ids = ['old-model', 'new-model'];
|
|
81
|
-
const raw = [
|
|
82
|
-
{ id: 'old-model', created: 1000 },
|
|
83
|
-
{ id: 'new-model', created: 2000 },
|
|
84
|
-
];
|
|
85
|
-
expect(sortModels(ids, raw)).toEqual(['new-model', 'old-model']);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('sorts by created_at ISO string descending', () => {
|
|
89
|
-
const ids = ['model-a', 'model-b'];
|
|
90
|
-
const raw = [
|
|
91
|
-
{ id: 'model-a', created_at: '2024-01-01T00:00:00Z' },
|
|
92
|
-
{ id: 'model-b', created_at: '2025-01-01T00:00:00Z' },
|
|
93
|
-
];
|
|
94
|
-
expect(sortModels(ids, raw)).toEqual(['model-b', 'model-a']);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('falls back to lexicographic descending when no timestamp', () => {
|
|
98
|
-
const ids = ['claude-haiku', 'claude-sonnet', 'claude-opus'];
|
|
99
|
-
const raw = [{ id: 'claude-haiku' }, { id: 'claude-sonnet' }, { id: 'claude-opus' }];
|
|
100
|
-
const result = sortModels(ids, raw);
|
|
101
|
-
// lexicographic descending: sonnet > opus > haiku
|
|
102
|
-
expect(result[0]).toBe('claude-sonnet');
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// ─── setCachedModels / getCachedModels ───────────────────────────────────────
|
|
107
|
-
|
|
108
|
-
describe('setCachedModels / getCachedModels', () => {
|
|
109
|
-
it('returns null when no entry exists', () => {
|
|
110
|
-
expect(getCachedModels('anthropic', 'sk-ant-missing')).toBeNull();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('returns stored models on cache hit', () => {
|
|
114
|
-
const models = ['claude-sonnet-4-20250514', 'claude-haiku-4-20250514'];
|
|
115
|
-
setCachedModels('anthropic', 'sk-ant-test1234', models);
|
|
116
|
-
expect(getCachedModels('anthropic', 'sk-ant-test1234')).toEqual(models);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('uses first 8 chars of apiKey as part of cache key (different full keys, same prefix → same entry)', () => {
|
|
120
|
-
setCachedModels('anthropic', 'sk-ant-AAAAAAAA-different-suffix', ['model-a']);
|
|
121
|
-
// Same first 8 chars: 'sk-ant-A'
|
|
122
|
-
expect(getCachedModels('anthropic', 'sk-ant-AAAAAAAA-other-suffix')).toEqual(['model-a']);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('returns null after TTL expires', () => {
|
|
126
|
-
const realNow = Date.now;
|
|
127
|
-
try {
|
|
128
|
-
// Set cache entry
|
|
129
|
-
Date.now = () => 0;
|
|
130
|
-
setCachedModels('anthropic', 'sk-ant-expiry-test', ['model-x']);
|
|
131
|
-
// Advance time past 24h
|
|
132
|
-
Date.now = () => 25 * 60 * 60 * 1000;
|
|
133
|
-
expect(getCachedModels('anthropic', 'sk-ant-expiry-test')).toBeNull();
|
|
134
|
-
} finally {
|
|
135
|
-
Date.now = realNow;
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('different providers do not share cache entries', () => {
|
|
140
|
-
setCachedModels('anthropic', 'sk-ant-shared', ['claude-model']);
|
|
141
|
-
expect(getCachedModels('openai', 'sk-ant-shared')).toBeNull();
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// ─── parseAndCacheModels ─────────────────────────────────────────────────────
|
|
146
|
-
|
|
147
|
-
describe('parseAndCacheModels', () => {
|
|
148
|
-
it('parses Anthropic response format, filters, and caches', () => {
|
|
149
|
-
const json = {
|
|
150
|
-
data: [
|
|
151
|
-
{ id: 'claude-sonnet-4-20250514', created_at: '2025-05-14T00:00:00Z' },
|
|
152
|
-
{ id: 'claude-instant-1' }, // excluded
|
|
153
|
-
{ id: 'claude-2.1' }, // excluded
|
|
154
|
-
],
|
|
155
|
-
};
|
|
156
|
-
parseAndCacheModels('anthropic', 'sk-ant-parse-test', json);
|
|
157
|
-
const cached = getCachedModels('anthropic', 'sk-ant-parse-test');
|
|
158
|
-
expect(cached).toContain('claude-sonnet-4-20250514');
|
|
159
|
-
expect(cached).not.toContain('claude-instant-1');
|
|
160
|
-
expect(cached).not.toContain('claude-2.1');
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('parses OpenAI response format', () => {
|
|
164
|
-
const json = {
|
|
165
|
-
data: [
|
|
166
|
-
{ id: 'gpt-4o', created: 2000 },
|
|
167
|
-
{ id: 'gpt-3.5-turbo', created: 1000 }, // excluded
|
|
168
|
-
],
|
|
169
|
-
};
|
|
170
|
-
parseAndCacheModels('openai', 'sk-parse-openai', json);
|
|
171
|
-
const cached = getCachedModels('openai', 'sk-parse-openai');
|
|
172
|
-
expect(cached).toContain('gpt-4o');
|
|
173
|
-
expect(cached).not.toContain('gpt-3.5-turbo');
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('does not throw on malformed JSON', () => {
|
|
177
|
-
expect(() => parseAndCacheModels('anthropic', 'sk-ant-bad', null)).not.toThrow();
|
|
178
|
-
expect(() => parseAndCacheModels('anthropic', 'sk-ant-bad', { no_data: true })).not.toThrow();
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
// ─── getAvailableModels ──────────────────────────────────────────────────────
|
|
183
|
-
|
|
184
|
-
const mockFetch = vi.fn();
|
|
185
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
186
|
-
|
|
187
|
-
beforeEach(() => { mockFetch.mockReset(); });
|
|
188
|
-
|
|
189
|
-
describe('getAvailableModels', () => {
|
|
190
|
-
it('returns cached models without fetching', async () => {
|
|
191
|
-
const models = ['claude-sonnet-4-20250514'];
|
|
192
|
-
setCachedModels('anthropic', 'sk-ant-cached', models);
|
|
193
|
-
const result = await getAvailableModels('anthropic', 'sk-ant-cached');
|
|
194
|
-
expect(result).toEqual(models);
|
|
195
|
-
expect(mockFetch).not.toHaveBeenCalled();
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('fetches from Anthropic API on cache miss', async () => {
|
|
199
|
-
mockFetch.mockResolvedValueOnce({
|
|
200
|
-
ok: true,
|
|
201
|
-
json: async () => ({
|
|
202
|
-
data: [{ id: 'claude-sonnet-4-20250514' }, { id: 'claude-instant-1' }],
|
|
203
|
-
}),
|
|
204
|
-
});
|
|
205
|
-
const result = await getAvailableModels('anthropic', 'sk-ant-fetch-test');
|
|
206
|
-
expect(result).toContain('claude-sonnet-4-20250514');
|
|
207
|
-
expect(result).not.toContain('claude-instant-1');
|
|
208
|
-
expect(mockFetch).toHaveBeenCalledOnce();
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('fetches from OpenAI API on cache miss', async () => {
|
|
212
|
-
mockFetch.mockResolvedValueOnce({
|
|
213
|
-
ok: true,
|
|
214
|
-
json: async () => ({
|
|
215
|
-
data: [{ id: 'gpt-4o' }, { id: 'gpt-3.5-turbo' }],
|
|
216
|
-
}),
|
|
217
|
-
});
|
|
218
|
-
const result = await getAvailableModels('openai', 'sk-openai-fetch');
|
|
219
|
-
expect(result).toContain('gpt-4o');
|
|
220
|
-
expect(result).not.toContain('gpt-3.5-turbo');
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it('returns [] on fetch error (never throws)', async () => {
|
|
224
|
-
mockFetch.mockRejectedValueOnce(new Error('ECONNREFUSED'));
|
|
225
|
-
const result = await getAvailableModels('anthropic', 'sk-ant-network-error');
|
|
226
|
-
expect(result).toEqual([]);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('returns [] for google provider (not supported)', async () => {
|
|
230
|
-
const result = await getAvailableModels('google', 'AIzaKey');
|
|
231
|
-
expect(result).toEqual([]);
|
|
232
|
-
expect(mockFetch).not.toHaveBeenCalled();
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it('returns [] for local provider', async () => {
|
|
236
|
-
const result = await getAvailableModels('local', 'http://localhost:11434');
|
|
237
|
-
expect(result).toEqual([]);
|
|
238
|
-
expect(mockFetch).not.toHaveBeenCalled();
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('populates cache after fetch so second call is free', async () => {
|
|
242
|
-
mockFetch.mockResolvedValueOnce({
|
|
243
|
-
ok: true,
|
|
244
|
-
json: async () => ({ data: [{ id: 'gpt-4o' }] }),
|
|
245
|
-
});
|
|
246
|
-
await getAvailableModels('openai', 'sk-cache-after-fetch');
|
|
247
|
-
await getAvailableModels('openai', 'sk-cache-after-fetch'); // second call
|
|
248
|
-
expect(mockFetch).toHaveBeenCalledOnce(); // still only one fetch
|
|
249
|
-
});
|
|
250
|
-
});
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { FileChangeCollector, formatFileChanges, type FileChange } from '../../src/output/file-changes.js';
|
|
3
|
-
|
|
4
|
-
vi.mock('node:child_process', () => ({
|
|
5
|
-
execSync: vi.fn(),
|
|
6
|
-
}));
|
|
7
|
-
|
|
8
|
-
import { execSync } from 'node:child_process';
|
|
9
|
-
const mockExecSync = vi.mocked(execSync);
|
|
10
|
-
|
|
11
|
-
describe('FileChangeCollector', () => {
|
|
12
|
-
it('collects unique file paths from tool call events', () => {
|
|
13
|
-
const collector = new FileChangeCollector();
|
|
14
|
-
|
|
15
|
-
collector.onToolCallComplete({
|
|
16
|
-
tool: 'repo_manager-write_file',
|
|
17
|
-
result: { path: 'src/app.ts', written: true },
|
|
18
|
-
duration_ms: 10,
|
|
19
|
-
timestamp: Date.now(),
|
|
20
|
-
});
|
|
21
|
-
collector.onToolCallComplete({
|
|
22
|
-
tool: 'repo_manager-read_file',
|
|
23
|
-
result: { content: '...' },
|
|
24
|
-
duration_ms: 5,
|
|
25
|
-
timestamp: Date.now(),
|
|
26
|
-
});
|
|
27
|
-
collector.onToolCallComplete({
|
|
28
|
-
tool: 'repo_manager-write_file',
|
|
29
|
-
result: { path: 'src/app.ts', written: true },
|
|
30
|
-
duration_ms: 8,
|
|
31
|
-
timestamp: Date.now(),
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
expect(collector.getWrittenPaths()).toEqual(['src/app.ts']);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('returns empty array when no write calls', () => {
|
|
38
|
-
const collector = new FileChangeCollector();
|
|
39
|
-
expect(collector.getWrittenPaths()).toEqual([]);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('ignores failed tool calls', () => {
|
|
43
|
-
const collector = new FileChangeCollector();
|
|
44
|
-
|
|
45
|
-
collector.onToolCallComplete({
|
|
46
|
-
tool: 'repo_manager-write_file',
|
|
47
|
-
result: undefined,
|
|
48
|
-
error: 'Permission denied',
|
|
49
|
-
duration_ms: 10,
|
|
50
|
-
timestamp: Date.now(),
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
expect(collector.getWrittenPaths()).toEqual([]);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('collects multiple distinct paths in order', () => {
|
|
57
|
-
const collector = new FileChangeCollector();
|
|
58
|
-
|
|
59
|
-
collector.onToolCallComplete({
|
|
60
|
-
tool: 'repo_manager-write_file',
|
|
61
|
-
result: { path: 'src/b.ts', written: true },
|
|
62
|
-
duration_ms: 10,
|
|
63
|
-
timestamp: Date.now(),
|
|
64
|
-
});
|
|
65
|
-
collector.onToolCallComplete({
|
|
66
|
-
tool: 'repo_manager-write_file',
|
|
67
|
-
result: { path: 'src/a.ts', written: true },
|
|
68
|
-
duration_ms: 10,
|
|
69
|
-
timestamp: Date.now(),
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
expect(collector.getWrittenPaths()).toEqual(['src/b.ts', 'src/a.ts']);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe('FileChangeCollector.computeSummary', () => {
|
|
77
|
-
beforeEach(() => {
|
|
78
|
-
mockExecSync.mockReset();
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('returns null when no files were written', () => {
|
|
82
|
-
const collector = new FileChangeCollector();
|
|
83
|
-
const result = collector.computeSummary('/fake/repo');
|
|
84
|
-
expect(result).toBeNull();
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('parses git diff --numstat for modified files', () => {
|
|
88
|
-
mockExecSync.mockReturnValue('15\t3\tsrc/app.ts\n');
|
|
89
|
-
|
|
90
|
-
const collector = new FileChangeCollector();
|
|
91
|
-
collector.onToolCallComplete({
|
|
92
|
-
tool: 'repo_manager-write_file',
|
|
93
|
-
result: { path: 'src/app.ts', written: true },
|
|
94
|
-
duration_ms: 10,
|
|
95
|
-
timestamp: Date.now(),
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const summary = collector.computeSummary('/fake/repo');
|
|
99
|
-
expect(summary).toEqual([
|
|
100
|
-
{ path: 'src/app.ts', status: 'M', added: 15, removed: 3 },
|
|
101
|
-
]);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('marks files not in git diff as Added with line count', () => {
|
|
105
|
-
mockExecSync
|
|
106
|
-
.mockReturnValueOnce('') // git diff --numstat returns nothing
|
|
107
|
-
.mockReturnValueOnce('42\n'); // wc -l for the new file
|
|
108
|
-
|
|
109
|
-
const collector = new FileChangeCollector();
|
|
110
|
-
collector.onToolCallComplete({
|
|
111
|
-
tool: 'repo_manager-write_file',
|
|
112
|
-
result: { path: 'src/new.ts', written: true },
|
|
113
|
-
duration_ms: 10,
|
|
114
|
-
timestamp: Date.now(),
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
const summary = collector.computeSummary('/fake/repo');
|
|
118
|
-
expect(summary).toEqual([
|
|
119
|
-
{ path: 'src/new.ts', status: 'A', added: 42, removed: 0 },
|
|
120
|
-
]);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('returns null when git is not available', () => {
|
|
124
|
-
mockExecSync.mockImplementation(() => {
|
|
125
|
-
throw new Error('git not found');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
const collector = new FileChangeCollector();
|
|
129
|
-
collector.onToolCallComplete({
|
|
130
|
-
tool: 'repo_manager-write_file',
|
|
131
|
-
result: { path: 'src/app.ts', written: true },
|
|
132
|
-
duration_ms: 10,
|
|
133
|
-
timestamp: Date.now(),
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
const summary = collector.computeSummary('/fake/repo');
|
|
137
|
-
expect(summary).toBeNull();
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
describe('formatFileChanges', () => {
|
|
142
|
-
it('formats modified files with +/- counts', () => {
|
|
143
|
-
const changes: FileChange[] = [
|
|
144
|
-
{ path: 'src/app.ts', status: 'M', added: 15, removed: 3 },
|
|
145
|
-
];
|
|
146
|
-
const lines = formatFileChanges(changes);
|
|
147
|
-
expect(lines).toContain('Changes:');
|
|
148
|
-
expect(lines).toContain('M');
|
|
149
|
-
expect(lines).toContain('src/app.ts');
|
|
150
|
-
expect(lines).toContain('+15');
|
|
151
|
-
expect(lines).toContain('-3');
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('formats added files with line count', () => {
|
|
155
|
-
const changes: FileChange[] = [
|
|
156
|
-
{ path: 'src/new.ts', status: 'A', added: 42, removed: 0 },
|
|
157
|
-
];
|
|
158
|
-
const lines = formatFileChanges(changes);
|
|
159
|
-
expect(lines).toContain('A');
|
|
160
|
-
expect(lines).toContain('src/new.ts');
|
|
161
|
-
expect(lines).toContain('new file');
|
|
162
|
-
expect(lines).toContain('42');
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('formats mixed M and A files', () => {
|
|
166
|
-
const changes: FileChange[] = [
|
|
167
|
-
{ path: 'src/app.ts', status: 'M', added: 15, removed: 3 },
|
|
168
|
-
{ path: 'src/new.ts', status: 'A', added: 42, removed: 0 },
|
|
169
|
-
];
|
|
170
|
-
const lines = formatFileChanges(changes);
|
|
171
|
-
expect(lines).toContain('M');
|
|
172
|
-
expect(lines).toContain('A');
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('returns empty string for empty array', () => {
|
|
176
|
-
expect(formatFileChanges([])).toBe('');
|
|
177
|
-
});
|
|
178
|
-
});
|