@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.
- package/package.json +8 -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/templates/.studiorc.yaml +0 -16
- package/templates/projects/blank/metadata.json +0 -7
- package/templates/studio-config.yaml +0 -14
- 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,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
|
-
});
|
package/tests/run-logger.test.ts
DELETED
|
@@ -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
|
-
});
|
package/tests/studio-dir.test.ts
DELETED
|
@@ -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
|
-
});
|