@studio-foundation/cli 0.3.0-beta.1 → 0.3.0-beta.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.
Files changed (91) hide show
  1. package/package.json +8 -5
  2. package/ARCHITECTURE.md +0 -40
  3. package/src/commands/api.ts +0 -126
  4. package/src/commands/config.ts +0 -433
  5. package/src/commands/init.ts +0 -879
  6. package/src/commands/install.ts +0 -23
  7. package/src/commands/integrations.ts +0 -332
  8. package/src/commands/list.ts +0 -197
  9. package/src/commands/logs.ts +0 -119
  10. package/src/commands/ollama.ts +0 -168
  11. package/src/commands/project.ts +0 -167
  12. package/src/commands/registry/audit.ts +0 -87
  13. package/src/commands/registry/index.ts +0 -63
  14. package/src/commands/registry/install.ts +0 -222
  15. package/src/commands/registry/publish.ts +0 -112
  16. package/src/commands/registry/remove.ts +0 -89
  17. package/src/commands/registry/search.ts +0 -93
  18. package/src/commands/registry/sync.ts +0 -24
  19. package/src/commands/registry/update.ts +0 -63
  20. package/src/commands/replay.ts +0 -559
  21. package/src/commands/run.ts +0 -454
  22. package/src/commands/status.ts +0 -163
  23. package/src/commands/template/index.ts +0 -59
  24. package/src/commands/template/validate.ts +0 -175
  25. package/src/commands/templates.ts +0 -99
  26. package/src/commands/tools.ts +0 -227
  27. package/src/commands/users.ts +0 -127
  28. package/src/commands/validate.ts +0 -46
  29. package/src/config.ts +0 -101
  30. package/src/index.ts +0 -201
  31. package/src/models-cache.ts +0 -127
  32. package/src/output/file-changes.ts +0 -97
  33. package/src/output/formatter.ts +0 -85
  34. package/src/output/formatters.ts +0 -303
  35. package/src/output/logger.ts +0 -18
  36. package/src/output/parallel-progress.ts +0 -103
  37. package/src/output/progress.ts +0 -414
  38. package/src/provider-validator.ts +0 -109
  39. package/src/registry/cache.ts +0 -46
  40. package/src/registry/client.ts +0 -84
  41. package/src/registry/lockfile.ts +0 -68
  42. package/src/registry/resolver.ts +0 -113
  43. package/src/registry/types.ts +0 -66
  44. package/src/run-logger.ts +0 -66
  45. package/src/run-store-factory.ts +0 -30
  46. package/src/studio-dir.ts +0 -28
  47. package/src/utils/input-wizard.ts +0 -73
  48. package/src/utils/placeholders.ts +0 -10
  49. package/templates/.studiorc.yaml +0 -16
  50. package/templates/projects/blank/metadata.json +0 -7
  51. package/templates/studio-config.yaml +0 -14
  52. package/tests/__stubs__/studio-runner.ts +0 -104
  53. package/tests/commands/api.test.ts +0 -110
  54. package/tests/commands/config.test.ts +0 -221
  55. package/tests/commands/init.test.ts +0 -919
  56. package/tests/commands/install.test.ts +0 -52
  57. package/tests/commands/integrations.test.ts +0 -158
  58. package/tests/commands/ollama.test.ts +0 -139
  59. package/tests/commands/project.test.ts +0 -179
  60. package/tests/commands/registry/audit.test.ts +0 -56
  61. package/tests/commands/registry/install.test.ts +0 -200
  62. package/tests/commands/registry/publish.test.ts +0 -56
  63. package/tests/commands/registry/remove.test.ts +0 -103
  64. package/tests/commands/registry/search.test.ts +0 -44
  65. package/tests/commands/registry/sync.test.ts +0 -37
  66. package/tests/commands/registry/update.test.ts +0 -62
  67. package/tests/commands/replay.test.ts +0 -283
  68. package/tests/commands/template/validate.test.ts +0 -150
  69. package/tests/commands/templates.test.ts +0 -42
  70. package/tests/commands/tools.test.ts +0 -106
  71. package/tests/config.test.ts +0 -142
  72. package/tests/formatter.test.ts +0 -158
  73. package/tests/integration/sigint.test.ts +0 -188
  74. package/tests/models-cache.test.ts +0 -250
  75. package/tests/output/file-changes.test.ts +0 -178
  76. package/tests/output/formatters.test.ts +0 -448
  77. package/tests/output/progress-spinner.test.ts +0 -232
  78. package/tests/output/progress-timer.test.ts +0 -230
  79. package/tests/provider-validator.test.ts +0 -182
  80. package/tests/registry/cache.test.ts +0 -66
  81. package/tests/registry/client.test.ts +0 -70
  82. package/tests/registry/lockfile.test.ts +0 -87
  83. package/tests/registry/resolver.test.ts +0 -122
  84. package/tests/run-logger-events.test.ts +0 -326
  85. package/tests/run-logger.test.ts +0 -64
  86. package/tests/run-store-factory.test.ts +0 -51
  87. package/tests/studio-dir.test.ts +0 -31
  88. package/tests/utils/input-wizard.test.ts +0 -153
  89. package/tests/utils/placeholders.test.ts +0 -36
  90. package/tsconfig.json +0 -24
  91. package/vitest.config.ts +0 -20
@@ -1,122 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import type { RegistryIndex, PackageMetadata, Lockfile } from '../../src/registry/types.js';
3
-
4
- // Build a minimal index entry
5
- function indexEntry(name: string, type: string) {
6
- return { name, type, version: '1.0.0', description: '', author: '', license: 'MIT', tags: [], studio_version: null, downloads: 0 };
7
- }
8
-
9
- function meta(name: string, deps?: PackageMetadata['dependencies']): PackageMetadata {
10
- return { ...indexEntry(name, 'tool'), dependencies: deps };
11
- }
12
-
13
- const EMPTY_LOCKFILE: Lockfile = { installed: {} };
14
-
15
- const MOCK_FETCH = async (name: string): Promise<PackageMetadata> => meta(name);
16
-
17
- describe('resolveDependencies', () => {
18
- it('returns empty graph when package has no dependencies', async () => {
19
- const { resolveDependencies } = await import('../../src/registry/resolver.js');
20
- const index: RegistryIndex = { generated_at: '', version: '1', packages: [] };
21
- const result = await resolveDependencies('my-pkg', meta('my-pkg'), index, EMPTY_LOCKFILE, MOCK_FETCH);
22
- expect(result.required).toEqual([]);
23
- expect(result.recommended).toEqual([]);
24
- });
25
-
26
- it('resolves required tools from first-level deps', async () => {
27
- const { resolveDependencies } = await import('../../src/registry/resolver.js');
28
- const index: RegistryIndex = {
29
- generated_at: '', version: '1',
30
- packages: [indexEntry('repo-manager', 'tool'), indexEntry('shell', 'tool')],
31
- };
32
- const pkgMeta = meta('software-full', {
33
- tools: { required: ['repo-manager', 'shell'] },
34
- });
35
- const result = await resolveDependencies('software-full', pkgMeta, index, EMPTY_LOCKFILE, MOCK_FETCH);
36
- expect(result.required.map(d => d.name)).toEqual(expect.arrayContaining(['repo-manager', 'shell']));
37
- expect(result.required).toHaveLength(2);
38
- expect(result.recommended).toEqual([]);
39
- });
40
-
41
- it('returns recommended at first level without recursion', async () => {
42
- const { resolveDependencies } = await import('../../src/registry/resolver.js');
43
- const index: RegistryIndex = {
44
- generated_at: '', version: '1',
45
- packages: [indexEntry('code-conventions', 'skill')],
46
- };
47
- const pkgMeta = meta('software-full', {
48
- skills: { recommended: ['code-conventions'] },
49
- });
50
- const result = await resolveDependencies('software-full', pkgMeta, index, EMPTY_LOCKFILE, MOCK_FETCH);
51
- expect(result.recommended.map(d => d.name)).toEqual(['code-conventions']);
52
- expect(result.required).toEqual([]);
53
- });
54
-
55
- it('resolves required deps recursively (A requires B which requires C)', async () => {
56
- const { resolveDependencies } = await import('../../src/registry/resolver.js');
57
- const index: RegistryIndex = {
58
- generated_at: '', version: '1',
59
- packages: [
60
- indexEntry('B', 'tool'),
61
- indexEntry('C', 'tool'),
62
- ],
63
- };
64
- const fetchMeta = async (name: string): Promise<PackageMetadata> => {
65
- if (name === 'B') return meta('B', { tools: { required: ['C'] } });
66
- return meta(name);
67
- };
68
- const pkgMeta = meta('A', { tools: { required: ['B'] } });
69
- const result = await resolveDependencies('A', pkgMeta, index, EMPTY_LOCKFILE, fetchMeta);
70
- expect(result.required.map(d => d.name)).toEqual(expect.arrayContaining(['B', 'C']));
71
- expect(result.required).toHaveLength(2);
72
- });
73
-
74
- it('deduplicates when two paths lead to the same dep', async () => {
75
- const { resolveDependencies } = await import('../../src/registry/resolver.js');
76
- const index: RegistryIndex = {
77
- generated_at: '', version: '1',
78
- packages: [indexEntry('B', 'tool'), indexEntry('C', 'tool'), indexEntry('D', 'tool')],
79
- };
80
- // A requires B and C, both require D
81
- const fetchMeta = async (name: string): Promise<PackageMetadata> => {
82
- if (name === 'B') return meta('B', { tools: { required: ['D'] } });
83
- if (name === 'C') return meta('C', { tools: { required: ['D'] } });
84
- return meta(name);
85
- };
86
- const pkgMeta = meta('A', { tools: { required: ['B', 'C'] } });
87
- const result = await resolveDependencies('A', pkgMeta, index, EMPTY_LOCKFILE, fetchMeta);
88
- const names = result.required.map(d => d.name);
89
- expect(names.filter(n => n === 'D')).toHaveLength(1); // D appears only once
90
- });
91
-
92
- it('throws on circular dependency', async () => {
93
- const { resolveDependencies } = await import('../../src/registry/resolver.js');
94
- const index: RegistryIndex = {
95
- generated_at: '', version: '1',
96
- packages: [indexEntry('B', 'tool')],
97
- };
98
- // A requires B, B requires A — cycle
99
- const fetchMeta = async (name: string): Promise<PackageMetadata> => {
100
- if (name === 'B') return meta('B', { tools: { required: ['A'] } });
101
- return meta(name);
102
- };
103
- const pkgMeta = meta('A', { tools: { required: ['B'] } });
104
- await expect(resolveDependencies('A', pkgMeta, index, EMPTY_LOCKFILE, fetchMeta))
105
- .rejects.toThrow(/circular/i);
106
- });
107
-
108
- it('includes already-installed packages in required list (caller handles required_by update)', async () => {
109
- const { resolveDependencies } = await import('../../src/registry/resolver.js');
110
- const index: RegistryIndex = {
111
- generated_at: '', version: '1',
112
- packages: [indexEntry('repo-manager', 'tool')],
113
- };
114
- const lockfile: Lockfile = {
115
- installed: { 'repo-manager': { version: '1.0.0', type: 'tool', installed_at: '2026-02-28', sha256: 'abc' } },
116
- };
117
- const pkgMeta = meta('software-full', { tools: { required: ['repo-manager'] } });
118
- const result = await resolveDependencies('software-full', pkgMeta, index, lockfile, MOCK_FETCH);
119
- // Already installed — should still appear in required list (caller handles required_by update)
120
- expect(result.required.filter(d => d.name === 'repo-manager')).toHaveLength(1);
121
- });
122
- });
@@ -1,326 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import type { EngineEvents } from '@studio-foundation/engine';
3
-
4
- interface LogEntry {
5
- ts: string;
6
- event: string;
7
- [key: string]: unknown;
8
- }
9
-
10
- function createCapturingLogger() {
11
- const entries: LogEntry[] = [];
12
- return {
13
- logger: {
14
- start: vi.fn(),
15
- log: vi.fn((payload: Record<string, unknown>) => {
16
- entries.push({ ts: new Date().toISOString(), ...payload } as LogEntry);
17
- }),
18
- close: vi.fn().mockResolvedValue(undefined),
19
- getLogPath: () => '/tmp/test.jsonl',
20
- },
21
- entries,
22
- };
23
- }
24
-
25
- describe('mergeEvents — pipeline_start', () => {
26
- it('logs full input object (no truncation)', async () => {
27
- const { mergeEvents } = await import('../src/commands/run.js');
28
- const { logger, entries } = createCapturingLogger();
29
- const noopEvents: EngineEvents = {};
30
-
31
- const largeInput = {
32
- brief_summary: 'A'.repeat(500),
33
- nested: { deep: { value: 'B'.repeat(300) } },
34
- };
35
-
36
- const events = mergeEvents(noopEvents, logger, 'test-pipeline', largeInput);
37
- events.onPipelineStart!({ pipeline_name: 'test-pipeline', run_id: 'abc12345-def6-7890' });
38
-
39
- expect(logger.start).toHaveBeenCalledWith('abc12345-def6-7890', 'test-pipeline');
40
- expect(entries).toHaveLength(1);
41
- const entry = entries[0];
42
- expect(entry.event).toBe('pipeline_start');
43
- expect(entry.input).toEqual(largeInput);
44
- // input_summary should not exist anymore
45
- expect(entry).not.toHaveProperty('input_summary');
46
- });
47
-
48
- it('logs full input string (no truncation)', async () => {
49
- const { mergeEvents } = await import('../src/commands/run.js');
50
- const { logger, entries } = createCapturingLogger();
51
- const noopEvents: EngineEvents = {};
52
- const largeString = 'X'.repeat(1000);
53
-
54
- const events = mergeEvents(noopEvents, logger, 'pipe', largeString);
55
- events.onPipelineStart!({ pipeline_name: 'pipe', run_id: 'run-id-12345678' });
56
-
57
- const entry = entries[0];
58
- expect(entry.input).toBe(largeString);
59
- });
60
- });
61
-
62
- describe('mergeEvents — pipeline_complete', () => {
63
- it('logs pipeline_name from event', async () => {
64
- const { mergeEvents } = await import('../src/commands/run.js');
65
- const { logger, entries } = createCapturingLogger();
66
- const noopEvents: EngineEvents = {};
67
-
68
- const events = mergeEvents(noopEvents, logger, 'feature-builder', 'test input');
69
- // Must call onPipelineStart first to initialize logger
70
- events.onPipelineStart!({ pipeline_name: 'feature-builder', run_id: 'run-12345678' });
71
- events.onPipelineComplete!({
72
- pipeline_name: 'feature-builder',
73
- run_id: 'run-12345678',
74
- status: 'success',
75
- duration_ms: 12345,
76
- total_tokens: 5000,
77
- total_tool_calls: 7,
78
- });
79
-
80
- const entry = entries.find(e => e.event === 'pipeline_complete')!;
81
- expect(entry.pipeline_name).toBe('feature-builder');
82
- expect(entry.status).toBe('success');
83
- expect(entry.duration_ms).toBe(12345);
84
- expect(entry.total_tokens).toBe(5000);
85
- expect(entry.total_tool_calls).toBe(7);
86
- });
87
- });
88
-
89
- describe('mergeEvents — stage_complete', () => {
90
- it('logs full output object (not truncated summary)', async () => {
91
- const { mergeEvents } = await import('../src/commands/run.js');
92
- const { logger, entries } = createCapturingLogger();
93
- const noopEvents: EngineEvents = {};
94
-
95
- const events = mergeEvents(noopEvents, logger, 'pipe', 'input');
96
- events.onPipelineStart!({ pipeline_name: 'pipe', run_id: 'run-12345678' });
97
-
98
- const largeOutput = {
99
- summary: 'A'.repeat(500),
100
- files_changed: ['src/a.ts', 'src/b.ts'],
101
- details: { nested: 'B'.repeat(300) },
102
- };
103
-
104
- events.onStageComplete!({
105
- stage_name: 'code-generation',
106
- stage_index: 0,
107
- total_stages: 3,
108
- status: 'success',
109
- attempts: 1,
110
- duration_ms: 5000,
111
- output: largeOutput,
112
- output_summary: '3 fields: summary, files_changed, details',
113
- tool_calls: [
114
- { name: 'repo_manager-write_file', arguments_summary: 'src/a.ts' },
115
- { name: 'repo_manager-write_file', arguments_summary: 'src/b.ts' },
116
- ],
117
- token_usage: { prompt_tokens: 1000, completion_tokens: 500, total_tokens: 1500 },
118
- });
119
-
120
- const entry = entries.find(e => e.event === 'stage_complete')!;
121
- // Full output, not truncated
122
- expect(entry.output).toEqual(largeOutput);
123
- // output_summary and output_fields should NOT be logged
124
- expect(entry).not.toHaveProperty('output_summary');
125
- expect(entry).not.toHaveProperty('output_fields');
126
- // tool_calls is the full array, not a count
127
- expect(entry.tool_calls).toEqual([
128
- { name: 'repo_manager-write_file', arguments_summary: 'src/a.ts' },
129
- { name: 'repo_manager-write_file', arguments_summary: 'src/b.ts' },
130
- ]);
131
- });
132
-
133
- it('handles stage_complete with no output', async () => {
134
- const { mergeEvents } = await import('../src/commands/run.js');
135
- const { logger, entries } = createCapturingLogger();
136
- const noopEvents: EngineEvents = {};
137
-
138
- const events = mergeEvents(noopEvents, logger, 'pipe', 'input');
139
- events.onPipelineStart!({ pipeline_name: 'pipe', run_id: 'run-12345678' });
140
-
141
- events.onStageComplete!({
142
- stage_name: 'analysis',
143
- stage_index: 0,
144
- total_stages: 1,
145
- status: 'failed',
146
- attempts: 3,
147
- duration_ms: 10000,
148
- });
149
-
150
- const entry = entries.find(e => e.event === 'stage_complete')!;
151
- expect(entry.output).toBeUndefined();
152
- expect(entry.tool_calls).toBeUndefined();
153
- });
154
- });
155
-
156
- describe('mergeEvents — stage_retry', () => {
157
- it('logs all failures, real max_attempts, and diagnostic fields', async () => {
158
- const { mergeEvents } = await import('../src/commands/run.js');
159
- const { logger, entries } = createCapturingLogger();
160
- const noopEvents: EngineEvents = {};
161
-
162
- const events = mergeEvents(noopEvents, logger, 'pipe', 'input');
163
- events.onPipelineStart!({ pipeline_name: 'pipe', run_id: 'run-12345678' });
164
-
165
- events.onTaskRetry!({
166
- stage: 'code-generation',
167
- attempt: 2,
168
- max_attempts: 3,
169
- failures: [
170
- 'missing required field: summary',
171
- 'tool_calls.minimum: expected 1, got 0',
172
- ],
173
- agent_output_raw: '{"partial": "data"}',
174
- tool_calls_count: 0,
175
- });
176
-
177
- const entry = entries.find(e => e.event === 'stage_retry')!;
178
- expect(entry.stage).toBe('code-generation');
179
- expect(entry.attempt).toBe(2);
180
- expect(entry.max_attempts).toBe(3);
181
- expect(entry.failures).toEqual([
182
- 'missing required field: summary',
183
- 'tool_calls.minimum: expected 1, got 0',
184
- ]);
185
- expect(entry.agent_output_raw).toBe('{"partial": "data"}');
186
- expect(entry.tool_calls_count).toBe(0);
187
- });
188
- });
189
-
190
- describe('mergeEvents — tool call events', () => {
191
- it('logs tool_call_start with tool name and params', async () => {
192
- const { mergeEvents } = await import('../src/commands/run.js');
193
- const { logger, entries } = createCapturingLogger();
194
- const noopEvents: EngineEvents = {};
195
-
196
- const events = mergeEvents(noopEvents, logger, 'pipe', 'input');
197
- events.onPipelineStart!({ pipeline_name: 'pipe', run_id: 'run-12345678' });
198
-
199
- events.onToolCallStart!({
200
- tool: 'repo_manager-write_file',
201
- params: { path: 'src/index.ts', content: 'console.log("hello")' },
202
- timestamp: Date.now(),
203
- });
204
-
205
- const entry = entries.find(e => e.event === 'tool_call_start')!;
206
- expect(entry.tool).toBe('repo_manager-write_file');
207
- expect(entry.params).toEqual({ path: 'src/index.ts', content: 'console.log("hello")' });
208
- });
209
-
210
- it('logs tool_call_complete with result and duration', async () => {
211
- const { mergeEvents } = await import('../src/commands/run.js');
212
- const { logger, entries } = createCapturingLogger();
213
- const noopEvents: EngineEvents = {};
214
-
215
- const events = mergeEvents(noopEvents, logger, 'pipe', 'input');
216
- events.onPipelineStart!({ pipeline_name: 'pipe', run_id: 'run-12345678' });
217
-
218
- events.onToolCallComplete!({
219
- tool: 'repo_manager-write_file',
220
- result: { success: true, path: 'src/index.ts' },
221
- duration_ms: 42,
222
- timestamp: Date.now(),
223
- });
224
-
225
- const entry = entries.find(e => e.event === 'tool_call_complete')!;
226
- expect(entry.tool).toBe('repo_manager-write_file');
227
- expect(entry.result).toEqual({ success: true, path: 'src/index.ts' });
228
- expect(entry.duration_ms).toBe(42);
229
- expect(entry).not.toHaveProperty('error');
230
- });
231
-
232
- it('logs tool_call_complete with error', async () => {
233
- const { mergeEvents } = await import('../src/commands/run.js');
234
- const { logger, entries } = createCapturingLogger();
235
- const noopEvents: EngineEvents = {};
236
-
237
- const events = mergeEvents(noopEvents, logger, 'pipe', 'input');
238
- events.onPipelineStart!({ pipeline_name: 'pipe', run_id: 'run-12345678' });
239
-
240
- events.onToolCallComplete!({
241
- tool: 'shell-run_command',
242
- result: null,
243
- error: 'Command failed with exit code 1',
244
- duration_ms: 150,
245
- timestamp: Date.now(),
246
- });
247
-
248
- const entry = entries.find(e => e.event === 'tool_call_complete')!;
249
- expect(entry.error).toBe('Command failed with exit code 1');
250
- });
251
-
252
- it('still forwards tool call events to progress display', async () => {
253
- const { mergeEvents } = await import('../src/commands/run.js');
254
- const { logger } = createCapturingLogger();
255
- const startSpy = vi.fn();
256
- const completeSpy = vi.fn();
257
- const progressEvents: EngineEvents = {
258
- onToolCallStart: startSpy,
259
- onToolCallComplete: completeSpy,
260
- };
261
-
262
- const events = mergeEvents(progressEvents, logger, 'pipe', 'input');
263
- events.onPipelineStart!({ pipeline_name: 'pipe', run_id: 'run-12345678' });
264
-
265
- const startEvent = { tool: 'test', params: {}, timestamp: Date.now() };
266
- const completeEvent = { tool: 'test', result: 'ok', duration_ms: 1, timestamp: Date.now() };
267
-
268
- events.onToolCallStart!(startEvent);
269
- events.onToolCallComplete!(completeEvent);
270
-
271
- expect(startSpy).toHaveBeenCalledWith(startEvent);
272
- expect(completeSpy).toHaveBeenCalledWith(completeEvent);
273
- });
274
- });
275
-
276
- describe('mergeEvents — run_id cleanup', () => {
277
- it('does not pass explicit run_id: undefined in stage_start payload', async () => {
278
- const { mergeEvents } = await import('../src/commands/run.js');
279
- const payloads: Record<string, unknown>[] = [];
280
- const fakeLogger = {
281
- start: vi.fn(),
282
- log: vi.fn((payload: Record<string, unknown>) => {
283
- payloads.push(payload);
284
- }),
285
- close: vi.fn().mockResolvedValue(undefined),
286
- getLogPath: () => '/tmp/test.jsonl',
287
- };
288
- const noopEvents: EngineEvents = {};
289
-
290
- const events = mergeEvents(noopEvents, fakeLogger, 'pipe', 'input');
291
- events.onPipelineStart!({ pipeline_name: 'pipe', run_id: 'run-12345678' });
292
- events.onStageStart!({ stage_name: 'analysis', stage_index: 0, total_stages: 2, max_attempts: 3 });
293
-
294
- const stagePayload = payloads.find(p => p.event === 'stage_start')!;
295
- // run_id key should not be present in the payload at all
296
- expect('run_id' in stagePayload).toBe(false);
297
- });
298
-
299
- it('does not pass explicit run_id: undefined in group handlers', async () => {
300
- const { mergeEvents } = await import('../src/commands/run.js');
301
- const payloads: Record<string, unknown>[] = [];
302
- const fakeLogger = {
303
- start: vi.fn(),
304
- log: vi.fn((payload: Record<string, unknown>) => {
305
- payloads.push(payload);
306
- }),
307
- close: vi.fn().mockResolvedValue(undefined),
308
- getLogPath: () => '/tmp/test.jsonl',
309
- };
310
- const noopEvents: EngineEvents = {};
311
-
312
- const events = mergeEvents(noopEvents, fakeLogger, 'pipe', 'input');
313
- events.onPipelineStart!({ pipeline_name: 'pipe', run_id: 'run-12345678' });
314
- events.onGroupStart!({ group_name: 'impl-review', max_iterations: 3 });
315
- events.onGroupIteration!({ group_name: 'impl-review', iteration: 1, max_iterations: 3 });
316
- events.onGroupFeedback!({ group_name: 'impl-review', iteration: 1, rejection_reason: 'bad', rejection_details: [] });
317
- events.onGroupComplete!({ group_name: 'impl-review', iterations: 1, status: 'success' });
318
-
319
- const groupPayloads = payloads.filter(p =>
320
- ['group_start', 'group_iteration', 'group_feedback', 'group_complete'].includes(p.event as string)
321
- );
322
- for (const payload of groupPayloads) {
323
- expect('run_id' in payload).toBe(false);
324
- }
325
- });
326
- });
@@ -1,64 +0,0 @@
1
- import { describe, it, expect, afterEach } from 'vitest';
2
- import { readFileSync, rmSync } from 'node:fs';
3
- import { resolve } from 'node:path';
4
- import { createRunLogger } from '../src/run-logger.js';
5
-
6
- const TMP = resolve('/tmp', '.studio-run-logger-test');
7
-
8
- afterEach(() => {
9
- try {
10
- rmSync(TMP, { recursive: true, force: true });
11
- } catch { /* ignore */ }
12
- });
13
-
14
- describe('RunLogger — close flushes to disk', () => {
15
- it('close() returns a Promise', () => {
16
- const logger = createRunLogger(TMP);
17
- logger.start('aaaa1111-bbbb-cccc-dddd-eeeeeeee0001', 'test-pipe');
18
- const result = logger.close();
19
- // close() must return a thenable so callers can await it
20
- expect(result).toBeInstanceOf(Promise);
21
- });
22
-
23
- it('all entries are on disk after close() resolves', async () => {
24
- const logger = createRunLogger(TMP);
25
- logger.start('aaaa1111-bbbb-cccc-dddd-eeeeeeee0002', 'flush-test');
26
-
27
- logger.log({ event: 'pipeline_start', pipeline: 'flush-test' });
28
- logger.log({ event: 'stage_start', stage: 'analysis' });
29
- logger.log({ event: 'stage_complete', stage: 'analysis', status: 'failed' });
30
- logger.log({ event: 'pipeline_complete', status: 'failed', duration_ms: 1234 });
31
-
32
- await logger.close();
33
-
34
- const content = readFileSync(logger.getLogPath(), 'utf-8');
35
- const lines = content.trim().split('\n');
36
- const events = lines.map(l => JSON.parse(l).event);
37
-
38
- expect(events).toEqual([
39
- 'pipeline_start',
40
- 'stage_start',
41
- 'stage_complete',
42
- 'pipeline_complete',
43
- ]);
44
- });
45
-
46
- it('pipeline_complete is present after close() on a rejected run', async () => {
47
- const logger = createRunLogger(TMP);
48
- logger.start('aaaa1111-bbbb-cccc-dddd-eeeeeeee0003', 'reject-test');
49
-
50
- logger.log({ event: 'pipeline_start', pipeline: 'reject-test' });
51
- logger.log({ event: 'group_start', group: 'impl-review' });
52
- logger.log({ event: 'group_complete', group: 'impl-review', status: 'rejected' });
53
- logger.log({ event: 'pipeline_complete', status: 'rejected', duration_ms: 5678 });
54
-
55
- await logger.close();
56
-
57
- const content = readFileSync(logger.getLogPath(), 'utf-8');
58
- const lines = content.trim().split('\n');
59
- const lastEvent = JSON.parse(lines[lines.length - 1]);
60
-
61
- expect(lastEvent.event).toBe('pipeline_complete');
62
- expect(lastEvent.status).toBe('rejected');
63
- });
64
- });
@@ -1,51 +0,0 @@
1
- import { describe, it, expect, afterEach } from 'vitest';
2
- import { rm } from 'node:fs/promises';
3
- import { createRunStore } from '../src/run-store-factory.js';
4
- import type { PipelineRun } from '@studio-foundation/contracts';
5
-
6
- const tmpDir = `/tmp/.studio-factory-test-${Date.now()}`;
7
-
8
- afterEach(async () => {
9
- await rm(tmpDir, { recursive: true, force: true });
10
- });
11
-
12
- describe('createRunStore', () => {
13
- it('returns a RunStore that can round-trip a PipelineRun', async () => {
14
- const store = await createRunStore({ resolvedStudioDir: tmpDir });
15
-
16
- const run: PipelineRun = {
17
- id: 'abc-123',
18
- pipeline_name: 'test-pipeline',
19
- status: 'success',
20
- started_at: '2026-01-01T00:00:00.000Z',
21
- stages: [],
22
- };
23
-
24
- await store.savePipelineRun(run);
25
- const retrieved = await store.getPipelineRun('abc-123');
26
-
27
- expect(retrieved?.id).toBe('abc-123');
28
- expect(retrieved?.pipeline_name).toBe('test-pipeline');
29
- expect(retrieved?.status).toBe('success');
30
-
31
- await store.close?.();
32
- });
33
-
34
- it('saves and retrieves log path', async () => {
35
- const store = await createRunStore({ resolvedStudioDir: tmpDir });
36
-
37
- const run: PipelineRun = {
38
- id: 'log-run',
39
- pipeline_name: 'p',
40
- status: 'success',
41
- started_at: '2026-01-01T00:00:00.000Z',
42
- stages: [],
43
- };
44
- await store.savePipelineRun(run);
45
- await store.saveLogPath('log-run', '/tmp/some.jsonl');
46
-
47
- expect(await store.getLogPath('log-run')).toBe('/tmp/some.jsonl');
48
-
49
- await store.close?.();
50
- });
51
- });
@@ -1,31 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { mkdir, rm } from 'node:fs/promises';
3
- import { resolve } from 'node:path';
4
- import { findStudioDir } from '../src/studio-dir.js';
5
-
6
- const TMP = resolve(import.meta.dirname, '.tmp-studio-dir-test');
7
-
8
- beforeEach(async () => { await mkdir(TMP, { recursive: true }); });
9
- afterEach(async () => { await rm(TMP, { recursive: true, force: true }); });
10
-
11
- describe('findStudioDir', () => {
12
- it('finds .studio/ in the given directory', async () => {
13
- await mkdir(resolve(TMP, '.studio'), { recursive: true });
14
- const result = await findStudioDir(TMP);
15
- expect(result).toBe(resolve(TMP, '.studio'));
16
- });
17
-
18
- it('finds .studio/ in a parent directory', async () => {
19
- await mkdir(resolve(TMP, '.studio'), { recursive: true });
20
- const nested = resolve(TMP, 'a', 'b', 'c');
21
- await mkdir(nested, { recursive: true });
22
- const result = await findStudioDir(nested);
23
- expect(result).toBe(resolve(TMP, '.studio'));
24
- });
25
-
26
- it('returns null when .studio/ is not found', async () => {
27
- // Use the filesystem root — /.studio never exists, so findStudioDir returns null immediately
28
- const result = await findStudioDir('/');
29
- expect(result).toBeNull();
30
- });
31
- });