@artemiskit/cli 0.1.4 → 0.1.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/CHANGELOG.md +24 -0
- package/README.md +1 -0
- package/dist/index.js +19129 -20009
- package/dist/src/commands/compare.d.ts.map +1 -1
- package/dist/src/commands/history.d.ts.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/redteam.d.ts.map +1 -1
- package/dist/src/commands/report.d.ts.map +1 -1
- package/dist/src/commands/run.d.ts.map +1 -1
- package/dist/src/commands/stress.d.ts.map +1 -1
- package/dist/src/ui/colors.d.ts +44 -0
- package/dist/src/ui/colors.d.ts.map +1 -0
- package/dist/src/ui/errors.d.ts +39 -0
- package/dist/src/ui/errors.d.ts.map +1 -0
- package/dist/src/ui/index.d.ts +16 -0
- package/dist/src/ui/index.d.ts.map +1 -0
- package/dist/src/ui/live-status.d.ts +82 -0
- package/dist/src/ui/live-status.d.ts.map +1 -0
- package/dist/src/ui/panels.d.ts +49 -0
- package/dist/src/ui/panels.d.ts.map +1 -0
- package/dist/src/ui/progress.d.ts +60 -0
- package/dist/src/ui/progress.d.ts.map +1 -0
- package/dist/src/ui/utils.d.ts +42 -0
- package/dist/src/ui/utils.d.ts.map +1 -0
- package/package.json +6 -6
- package/src/__tests__/helpers/index.ts +6 -0
- package/src/__tests__/helpers/mock-adapter.ts +90 -0
- package/src/__tests__/helpers/test-utils.ts +205 -0
- package/src/__tests__/integration/compare-command.test.ts +236 -0
- package/src/__tests__/integration/config.test.ts +125 -0
- package/src/__tests__/integration/history-command.test.ts +251 -0
- package/src/__tests__/integration/init-command.test.ts +177 -0
- package/src/__tests__/integration/report-command.test.ts +245 -0
- package/src/__tests__/integration/ui.test.ts +230 -0
- package/src/commands/compare.ts +158 -49
- package/src/commands/history.ts +131 -30
- package/src/commands/init.ts +181 -21
- package/src/commands/redteam.ts +118 -75
- package/src/commands/report.ts +29 -14
- package/src/commands/run.ts +86 -66
- package/src/commands/stress.ts +61 -63
- package/src/ui/colors.ts +62 -0
- package/src/ui/errors.ts +248 -0
- package/src/ui/index.ts +42 -0
- package/src/ui/live-status.ts +259 -0
- package/src/ui/panels.ts +216 -0
- package/src/ui/progress.ts +139 -0
- package/src/ui/utils.ts +88 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for history command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it, beforeEach, afterEach } from 'bun:test';
|
|
6
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { createTestDir, cleanupTestDir } from '../helpers/test-utils.js';
|
|
9
|
+
import { createStorage } from '../../utils/storage.js';
|
|
10
|
+
|
|
11
|
+
describe('History Command', () => {
|
|
12
|
+
let testDir: string;
|
|
13
|
+
let originalCwd: string;
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
testDir = await createTestDir('history-test');
|
|
17
|
+
originalCwd = process.cwd();
|
|
18
|
+
process.chdir(testDir);
|
|
19
|
+
|
|
20
|
+
// Create storage directory
|
|
21
|
+
await mkdir(join(testDir, 'artemis-runs', 'test-project'), { recursive: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
process.chdir(originalCwd);
|
|
26
|
+
await cleanupTestDir(testDir);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('storage listing', () => {
|
|
30
|
+
it('should list runs from local storage', async () => {
|
|
31
|
+
// Create mock run manifests with correct structure
|
|
32
|
+
const manifest1 = {
|
|
33
|
+
run_id: 'run-001',
|
|
34
|
+
project: 'test-project',
|
|
35
|
+
config: { scenario: 'test-scenario' },
|
|
36
|
+
start_time: new Date('2026-01-15T10:00:00Z').toISOString(),
|
|
37
|
+
metrics: {
|
|
38
|
+
success_rate: 1.0,
|
|
39
|
+
passed_cases: 5,
|
|
40
|
+
failed_cases: 0,
|
|
41
|
+
total_tokens: 500,
|
|
42
|
+
median_latency_ms: 100,
|
|
43
|
+
},
|
|
44
|
+
cases: [],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const manifest2 = {
|
|
48
|
+
run_id: 'run-002',
|
|
49
|
+
project: 'test-project',
|
|
50
|
+
config: { scenario: 'another-scenario' },
|
|
51
|
+
start_time: new Date('2026-01-16T10:00:00Z').toISOString(),
|
|
52
|
+
metrics: {
|
|
53
|
+
success_rate: 0.8,
|
|
54
|
+
passed_cases: 4,
|
|
55
|
+
failed_cases: 1,
|
|
56
|
+
total_tokens: 600,
|
|
57
|
+
median_latency_ms: 150,
|
|
58
|
+
},
|
|
59
|
+
cases: [],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Write manifest files
|
|
63
|
+
await writeFile(
|
|
64
|
+
join(testDir, 'artemis-runs', 'test-project', 'run-001.json'),
|
|
65
|
+
JSON.stringify(manifest1)
|
|
66
|
+
);
|
|
67
|
+
await writeFile(
|
|
68
|
+
join(testDir, 'artemis-runs', 'test-project', 'run-002.json'),
|
|
69
|
+
JSON.stringify(manifest2)
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Create storage and list using basePath
|
|
73
|
+
const storage = createStorage({
|
|
74
|
+
fileConfig: {
|
|
75
|
+
storage: { type: 'local', basePath: join(testDir, 'artemis-runs') },
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const runs = await storage.list({ limit: 10 });
|
|
80
|
+
|
|
81
|
+
expect(runs.length).toBe(2);
|
|
82
|
+
expect(runs.some((r) => r.runId === 'run-001')).toBe(true);
|
|
83
|
+
expect(runs.some((r) => r.runId === 'run-002')).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should filter by scenario', async () => {
|
|
87
|
+
const manifest1 = {
|
|
88
|
+
run_id: 'run-001',
|
|
89
|
+
project: 'test-project',
|
|
90
|
+
config: { scenario: 'scenario-a' },
|
|
91
|
+
start_time: new Date().toISOString(),
|
|
92
|
+
metrics: {
|
|
93
|
+
success_rate: 1.0,
|
|
94
|
+
passed_cases: 5,
|
|
95
|
+
failed_cases: 0,
|
|
96
|
+
total_tokens: 500,
|
|
97
|
+
median_latency_ms: 100,
|
|
98
|
+
},
|
|
99
|
+
cases: [],
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const manifest2 = {
|
|
103
|
+
run_id: 'run-002',
|
|
104
|
+
project: 'test-project',
|
|
105
|
+
config: { scenario: 'scenario-b' },
|
|
106
|
+
start_time: new Date().toISOString(),
|
|
107
|
+
metrics: {
|
|
108
|
+
success_rate: 0.8,
|
|
109
|
+
passed_cases: 4,
|
|
110
|
+
failed_cases: 1,
|
|
111
|
+
total_tokens: 600,
|
|
112
|
+
median_latency_ms: 150,
|
|
113
|
+
},
|
|
114
|
+
cases: [],
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
await writeFile(
|
|
118
|
+
join(testDir, 'artemis-runs', 'test-project', 'run-001.json'),
|
|
119
|
+
JSON.stringify(manifest1)
|
|
120
|
+
);
|
|
121
|
+
await writeFile(
|
|
122
|
+
join(testDir, 'artemis-runs', 'test-project', 'run-002.json'),
|
|
123
|
+
JSON.stringify(manifest2)
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const storage = createStorage({
|
|
127
|
+
fileConfig: {
|
|
128
|
+
storage: { type: 'local', basePath: join(testDir, 'artemis-runs') },
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const runs = await storage.list({ scenario: 'scenario-a', limit: 10 });
|
|
133
|
+
|
|
134
|
+
expect(runs.length).toBe(1);
|
|
135
|
+
expect(runs[0].scenario).toBe('scenario-a');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should respect limit parameter', async () => {
|
|
139
|
+
// Create 5 manifests
|
|
140
|
+
for (let i = 1; i <= 5; i++) {
|
|
141
|
+
const manifest = {
|
|
142
|
+
run_id: `run-00${i}`,
|
|
143
|
+
project: 'test-project',
|
|
144
|
+
config: { scenario: 'test-scenario' },
|
|
145
|
+
start_time: new Date(Date.now() - i * 1000).toISOString(),
|
|
146
|
+
metrics: {
|
|
147
|
+
success_rate: 1.0,
|
|
148
|
+
passed_cases: 5,
|
|
149
|
+
failed_cases: 0,
|
|
150
|
+
total_tokens: 500,
|
|
151
|
+
median_latency_ms: 100,
|
|
152
|
+
},
|
|
153
|
+
cases: [],
|
|
154
|
+
};
|
|
155
|
+
await writeFile(
|
|
156
|
+
join(testDir, 'artemis-runs', 'test-project', `run-00${i}.json`),
|
|
157
|
+
JSON.stringify(manifest)
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const storage = createStorage({
|
|
162
|
+
fileConfig: {
|
|
163
|
+
storage: { type: 'local', basePath: join(testDir, 'artemis-runs') },
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const runs = await storage.list({ limit: 3 });
|
|
168
|
+
|
|
169
|
+
expect(runs.length).toBe(3);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should return empty array when no runs exist', async () => {
|
|
173
|
+
const storage = createStorage({
|
|
174
|
+
fileConfig: {
|
|
175
|
+
storage: { type: 'local', basePath: join(testDir, 'artemis-runs') },
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const runs = await storage.list({ limit: 10 });
|
|
180
|
+
|
|
181
|
+
expect(runs).toEqual([]);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('run data', () => {
|
|
186
|
+
it('should include success rate in listing', async () => {
|
|
187
|
+
const manifest = {
|
|
188
|
+
run_id: 'run-001',
|
|
189
|
+
project: 'test-project',
|
|
190
|
+
config: { scenario: 'test-scenario' },
|
|
191
|
+
start_time: new Date().toISOString(),
|
|
192
|
+
metrics: {
|
|
193
|
+
success_rate: 0.75,
|
|
194
|
+
passed_cases: 3,
|
|
195
|
+
failed_cases: 1,
|
|
196
|
+
total_tokens: 400,
|
|
197
|
+
median_latency_ms: 120,
|
|
198
|
+
},
|
|
199
|
+
cases: [],
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
await writeFile(
|
|
203
|
+
join(testDir, 'artemis-runs', 'test-project', 'run-001.json'),
|
|
204
|
+
JSON.stringify(manifest)
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const storage = createStorage({
|
|
208
|
+
fileConfig: {
|
|
209
|
+
storage: { type: 'local', basePath: join(testDir, 'artemis-runs') },
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const runs = await storage.list({ limit: 10 });
|
|
214
|
+
|
|
215
|
+
expect(runs[0].successRate).toBe(0.75);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should include creation date in listing', async () => {
|
|
219
|
+
const startTime = '2026-01-17T12:00:00.000Z';
|
|
220
|
+
const manifest = {
|
|
221
|
+
run_id: 'run-001',
|
|
222
|
+
project: 'test-project',
|
|
223
|
+
config: { scenario: 'test-scenario' },
|
|
224
|
+
start_time: startTime,
|
|
225
|
+
metrics: {
|
|
226
|
+
success_rate: 1.0,
|
|
227
|
+
passed_cases: 5,
|
|
228
|
+
failed_cases: 0,
|
|
229
|
+
total_tokens: 500,
|
|
230
|
+
median_latency_ms: 100,
|
|
231
|
+
},
|
|
232
|
+
cases: [],
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
await writeFile(
|
|
236
|
+
join(testDir, 'artemis-runs', 'test-project', 'run-001.json'),
|
|
237
|
+
JSON.stringify(manifest)
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const storage = createStorage({
|
|
241
|
+
fileConfig: {
|
|
242
|
+
storage: { type: 'local', basePath: join(testDir, 'artemis-runs') },
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const runs = await storage.list({ limit: 10 });
|
|
247
|
+
|
|
248
|
+
expect(runs[0].createdAt).toBe(startTime);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for init command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it, beforeEach, afterEach } from 'bun:test';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { createTestDir, cleanupTestDir } from '../helpers/test-utils.js';
|
|
10
|
+
|
|
11
|
+
// Import init command internals for testing
|
|
12
|
+
// We'll test the file creation logic directly
|
|
13
|
+
|
|
14
|
+
describe('Init Command', () => {
|
|
15
|
+
let testDir: string;
|
|
16
|
+
let originalCwd: string;
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
testDir = await createTestDir('init-test');
|
|
20
|
+
originalCwd = process.cwd();
|
|
21
|
+
process.chdir(testDir);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
process.chdir(originalCwd);
|
|
26
|
+
await cleanupTestDir(testDir);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('file creation', () => {
|
|
30
|
+
it('should create artemis.config.yaml with correct structure', async () => {
|
|
31
|
+
// Simulate what init command creates
|
|
32
|
+
const configContent = `# ArtemisKit Configuration
|
|
33
|
+
# See: https://artemiskit.vercel.app/docs/configuration
|
|
34
|
+
|
|
35
|
+
# Default provider (openai, azure-openai, anthropic)
|
|
36
|
+
provider: openai
|
|
37
|
+
|
|
38
|
+
# Default model
|
|
39
|
+
model: gpt-4o-mini
|
|
40
|
+
|
|
41
|
+
# Project name for organizing runs
|
|
42
|
+
project: my-project
|
|
43
|
+
|
|
44
|
+
# Storage configuration
|
|
45
|
+
storage:
|
|
46
|
+
type: local
|
|
47
|
+
path: ./artemis-runs
|
|
48
|
+
`;
|
|
49
|
+
const configPath = join(testDir, 'artemis.config.yaml');
|
|
50
|
+
await writeFile(configPath, configContent);
|
|
51
|
+
|
|
52
|
+
expect(existsSync(configPath)).toBe(true);
|
|
53
|
+
|
|
54
|
+
const content = await readFile(configPath, 'utf-8');
|
|
55
|
+
expect(content).toContain('provider: openai');
|
|
56
|
+
expect(content).toContain('model: gpt-4o-mini');
|
|
57
|
+
expect(content).toContain('storage:');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should create example scenario file', async () => {
|
|
61
|
+
const { mkdir } = await import('node:fs/promises');
|
|
62
|
+
const scenarioContent = `# Example Scenario
|
|
63
|
+
name: example
|
|
64
|
+
description: An example test scenario
|
|
65
|
+
|
|
66
|
+
cases:
|
|
67
|
+
- id: greeting-test
|
|
68
|
+
prompt: "Say hello"
|
|
69
|
+
expected:
|
|
70
|
+
type: contains
|
|
71
|
+
values:
|
|
72
|
+
- "hello"
|
|
73
|
+
mode: any
|
|
74
|
+
`;
|
|
75
|
+
const scenarioDir = join(testDir, 'scenarios');
|
|
76
|
+
await mkdir(scenarioDir, { recursive: true });
|
|
77
|
+
const scenarioPath = join(scenarioDir, 'example.yaml');
|
|
78
|
+
|
|
79
|
+
await writeFile(scenarioPath, scenarioContent);
|
|
80
|
+
|
|
81
|
+
expect(existsSync(scenarioPath)).toBe(true);
|
|
82
|
+
|
|
83
|
+
const content = await readFile(scenarioPath, 'utf-8');
|
|
84
|
+
expect(content).toContain('name: example');
|
|
85
|
+
expect(content).toContain('cases:');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('.env handling', () => {
|
|
90
|
+
it('should create .env with all provider keys when file does not exist', async () => {
|
|
91
|
+
const envContent = `# ArtemisKit Environment Variables
|
|
92
|
+
OPENAI_API_KEY=
|
|
93
|
+
AZURE_OPENAI_API_KEY=
|
|
94
|
+
AZURE_OPENAI_RESOURCE=
|
|
95
|
+
AZURE_OPENAI_DEPLOYMENT=
|
|
96
|
+
AZURE_OPENAI_API_VERSION=
|
|
97
|
+
ANTHROPIC_API_KEY=
|
|
98
|
+
`;
|
|
99
|
+
const envPath = join(testDir, '.env');
|
|
100
|
+
await writeFile(envPath, envContent);
|
|
101
|
+
|
|
102
|
+
expect(existsSync(envPath)).toBe(true);
|
|
103
|
+
|
|
104
|
+
const content = await readFile(envPath, 'utf-8');
|
|
105
|
+
expect(content).toContain('OPENAI_API_KEY=');
|
|
106
|
+
expect(content).toContain('ANTHROPIC_API_KEY=');
|
|
107
|
+
expect(content).toContain('AZURE_OPENAI_API_KEY=');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should append missing keys to existing .env', async () => {
|
|
111
|
+
// Create existing .env with some keys
|
|
112
|
+
const existingEnv = `# Existing config
|
|
113
|
+
OPENAI_API_KEY=sk-existing-key
|
|
114
|
+
SOME_OTHER_VAR=value
|
|
115
|
+
`;
|
|
116
|
+
const envPath = join(testDir, '.env');
|
|
117
|
+
await writeFile(envPath, existingEnv);
|
|
118
|
+
|
|
119
|
+
// Simulate appending missing keys
|
|
120
|
+
const envContent = await readFile(envPath, 'utf-8');
|
|
121
|
+
const keysToAdd = ['ANTHROPIC_API_KEY=', 'AZURE_OPENAI_API_KEY='];
|
|
122
|
+
|
|
123
|
+
const missingKeys = keysToAdd.filter((key) => {
|
|
124
|
+
const keyName = key.split('=')[0];
|
|
125
|
+
return !envContent.includes(keyName);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (missingKeys.length > 0) {
|
|
129
|
+
const newContent = envContent + '\n# Added by ArtemisKit\n' + missingKeys.join('\n') + '\n';
|
|
130
|
+
await writeFile(envPath, newContent);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const finalContent = await readFile(envPath, 'utf-8');
|
|
134
|
+
expect(finalContent).toContain('OPENAI_API_KEY=sk-existing-key'); // preserved
|
|
135
|
+
expect(finalContent).toContain('SOME_OTHER_VAR=value'); // preserved
|
|
136
|
+
expect(finalContent).toContain('ANTHROPIC_API_KEY='); // added
|
|
137
|
+
expect(finalContent).toContain('AZURE_OPENAI_API_KEY='); // added
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should not duplicate existing keys', async () => {
|
|
141
|
+
const existingEnv = `OPENAI_API_KEY=sk-test
|
|
142
|
+
ANTHROPIC_API_KEY=sk-ant-test
|
|
143
|
+
`;
|
|
144
|
+
const envPath = join(testDir, '.env');
|
|
145
|
+
await writeFile(envPath, existingEnv);
|
|
146
|
+
|
|
147
|
+
const envContent = await readFile(envPath, 'utf-8');
|
|
148
|
+
const keysToCheck = ['OPENAI_API_KEY=', 'ANTHROPIC_API_KEY='];
|
|
149
|
+
|
|
150
|
+
const missingKeys = keysToCheck.filter((key) => {
|
|
151
|
+
const keyName = key.split('=')[0];
|
|
152
|
+
return !envContent.includes(keyName);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// No keys should be missing
|
|
156
|
+
expect(missingKeys.length).toBe(0);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('directory structure', () => {
|
|
161
|
+
it('should create scenarios directory', async () => {
|
|
162
|
+
const { mkdir } = await import('node:fs/promises');
|
|
163
|
+
const scenariosDir = join(testDir, 'scenarios');
|
|
164
|
+
await mkdir(scenariosDir, { recursive: true });
|
|
165
|
+
|
|
166
|
+
expect(existsSync(scenariosDir)).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should create artemis-runs directory for local storage', async () => {
|
|
170
|
+
const { mkdir } = await import('node:fs/promises');
|
|
171
|
+
const runsDir = join(testDir, 'artemis-runs');
|
|
172
|
+
await mkdir(runsDir, { recursive: true });
|
|
173
|
+
|
|
174
|
+
expect(existsSync(runsDir)).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for report command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it, beforeEach, afterEach } from 'bun:test';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { generateHTMLReport, generateJSONReport } from '@artemiskit/reports';
|
|
10
|
+
import { createTestDir, cleanupTestDir } from '../helpers/test-utils.js';
|
|
11
|
+
import { createStorage } from '../../utils/storage.js';
|
|
12
|
+
|
|
13
|
+
describe('Report Command', () => {
|
|
14
|
+
let testDir: string;
|
|
15
|
+
let originalCwd: string;
|
|
16
|
+
|
|
17
|
+
const sampleManifest = {
|
|
18
|
+
run_id: 'test-run-001',
|
|
19
|
+
project: 'test-project',
|
|
20
|
+
config: {
|
|
21
|
+
scenario: 'test-scenario',
|
|
22
|
+
provider: 'openai',
|
|
23
|
+
model: 'gpt-4o-mini',
|
|
24
|
+
},
|
|
25
|
+
start_time: new Date().toISOString(),
|
|
26
|
+
duration_ms: 5000,
|
|
27
|
+
metrics: {
|
|
28
|
+
success_rate: 0.8,
|
|
29
|
+
passed_cases: 4,
|
|
30
|
+
failed_cases: 1,
|
|
31
|
+
total_tokens: 500,
|
|
32
|
+
median_latency_ms: 150,
|
|
33
|
+
avg_latency_ms: 160,
|
|
34
|
+
min_latency_ms: 100,
|
|
35
|
+
max_latency_ms: 250,
|
|
36
|
+
p95_latency_ms: 240,
|
|
37
|
+
p99_latency_ms: 248,
|
|
38
|
+
},
|
|
39
|
+
cases: [
|
|
40
|
+
{
|
|
41
|
+
id: 'case-1',
|
|
42
|
+
prompt: 'Test prompt 1',
|
|
43
|
+
response: 'Test response 1',
|
|
44
|
+
expected: { type: 'contains', values: ['test'], mode: 'any' },
|
|
45
|
+
ok: true,
|
|
46
|
+
score: 1.0,
|
|
47
|
+
reason: 'Passed',
|
|
48
|
+
latencyMs: 100,
|
|
49
|
+
tokens: { input: 10, output: 20 },
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'case-2',
|
|
53
|
+
prompt: 'Test prompt 2',
|
|
54
|
+
response: 'Test response 2',
|
|
55
|
+
expected: { type: 'contains', values: ['expected'], mode: 'any' },
|
|
56
|
+
ok: false,
|
|
57
|
+
score: 0,
|
|
58
|
+
reason: 'Did not contain expected value',
|
|
59
|
+
latencyMs: 200,
|
|
60
|
+
tokens: { input: 15, output: 25 },
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
beforeEach(async () => {
|
|
66
|
+
testDir = await createTestDir('report-test');
|
|
67
|
+
originalCwd = process.cwd();
|
|
68
|
+
process.chdir(testDir);
|
|
69
|
+
|
|
70
|
+
// Create storage directory and save manifest
|
|
71
|
+
await mkdir(join(testDir, 'artemis-runs', 'test-project'), { recursive: true });
|
|
72
|
+
await writeFile(
|
|
73
|
+
join(testDir, 'artemis-runs', 'test-project', 'test-run-001.json'),
|
|
74
|
+
JSON.stringify(sampleManifest)
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
afterEach(async () => {
|
|
79
|
+
process.chdir(originalCwd);
|
|
80
|
+
await cleanupTestDir(testDir);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('report generation', () => {
|
|
84
|
+
it('should load manifest from storage', async () => {
|
|
85
|
+
const storage = createStorage({
|
|
86
|
+
fileConfig: {
|
|
87
|
+
storage: { type: 'local', basePath: join(testDir, 'artemis-runs') },
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const manifest = await storage.load('test-run-001');
|
|
92
|
+
|
|
93
|
+
expect(manifest.run_id).toBe('test-run-001');
|
|
94
|
+
expect(manifest.config.scenario).toBe('test-scenario');
|
|
95
|
+
expect(manifest.metrics.success_rate).toBe(0.8);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should generate HTML report from manifest', async () => {
|
|
99
|
+
const storage = createStorage({
|
|
100
|
+
fileConfig: {
|
|
101
|
+
storage: { type: 'local', basePath: join(testDir, 'artemis-runs') },
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const manifest = await storage.load('test-run-001');
|
|
106
|
+
const html = generateHTMLReport(manifest);
|
|
107
|
+
|
|
108
|
+
expect(html).toContain('<!DOCTYPE html>');
|
|
109
|
+
expect(html).toContain('test-run-001');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should generate JSON report from manifest', async () => {
|
|
113
|
+
const storage = createStorage({
|
|
114
|
+
fileConfig: {
|
|
115
|
+
storage: { type: 'local', basePath: join(testDir, 'artemis-runs') },
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const manifest = await storage.load('test-run-001');
|
|
120
|
+
const json = generateJSONReport(manifest, { pretty: true });
|
|
121
|
+
const parsed = JSON.parse(json);
|
|
122
|
+
|
|
123
|
+
expect(parsed.run_id).toBe('test-run-001');
|
|
124
|
+
expect(parsed.metrics.success_rate).toBe(0.8);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should write HTML report to output directory', async () => {
|
|
128
|
+
const outputDir = join(testDir, 'output');
|
|
129
|
+
await mkdir(outputDir, { recursive: true });
|
|
130
|
+
|
|
131
|
+
const storage = createStorage({
|
|
132
|
+
fileConfig: {
|
|
133
|
+
storage: { type: 'local', basePath: join(testDir, 'artemis-runs') },
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const manifest = await storage.load('test-run-001');
|
|
138
|
+
const html = generateHTMLReport(manifest);
|
|
139
|
+
const outputPath = join(outputDir, 'test-run-001.html');
|
|
140
|
+
await writeFile(outputPath, html);
|
|
141
|
+
|
|
142
|
+
expect(existsSync(outputPath)).toBe(true);
|
|
143
|
+
|
|
144
|
+
const content = await readFile(outputPath, 'utf-8');
|
|
145
|
+
expect(content).toContain('<!DOCTYPE html>');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should write JSON report to output directory', async () => {
|
|
149
|
+
const outputDir = join(testDir, 'output');
|
|
150
|
+
await mkdir(outputDir, { recursive: true });
|
|
151
|
+
|
|
152
|
+
const storage = createStorage({
|
|
153
|
+
fileConfig: {
|
|
154
|
+
storage: { type: 'local', basePath: join(testDir, 'artemis-runs') },
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const manifest = await storage.load('test-run-001');
|
|
159
|
+
const json = generateJSONReport(manifest, { pretty: true });
|
|
160
|
+
const outputPath = join(outputDir, 'test-run-001.json');
|
|
161
|
+
await writeFile(outputPath, json);
|
|
162
|
+
|
|
163
|
+
expect(existsSync(outputPath)).toBe(true);
|
|
164
|
+
|
|
165
|
+
const content = await readFile(outputPath, 'utf-8');
|
|
166
|
+
const parsed = JSON.parse(content);
|
|
167
|
+
expect(parsed.run_id).toBe('test-run-001');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should generate both formats when format is "both"', async () => {
|
|
171
|
+
const outputDir = join(testDir, 'output');
|
|
172
|
+
await mkdir(outputDir, { recursive: true });
|
|
173
|
+
|
|
174
|
+
const storage = createStorage({
|
|
175
|
+
fileConfig: {
|
|
176
|
+
storage: { type: 'local', basePath: join(testDir, 'artemis-runs') },
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const manifest = await storage.load('test-run-001');
|
|
181
|
+
|
|
182
|
+
// Generate both formats
|
|
183
|
+
const html = generateHTMLReport(manifest);
|
|
184
|
+
const json = generateJSONReport(manifest, { pretty: true });
|
|
185
|
+
|
|
186
|
+
await writeFile(join(outputDir, 'test-run-001.html'), html);
|
|
187
|
+
await writeFile(join(outputDir, 'test-run-001.json'), json);
|
|
188
|
+
|
|
189
|
+
expect(existsSync(join(outputDir, 'test-run-001.html'))).toBe(true);
|
|
190
|
+
expect(existsSync(join(outputDir, 'test-run-001.json'))).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should throw error for non-existent run ID', async () => {
|
|
194
|
+
const storage = createStorage({
|
|
195
|
+
fileConfig: {
|
|
196
|
+
storage: { type: 'local', basePath: join(testDir, 'artemis-runs') },
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await expect(storage.load('non-existent-run')).rejects.toThrow();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('HTML report content', () => {
|
|
205
|
+
it('should include test case details', async () => {
|
|
206
|
+
const storage = createStorage({
|
|
207
|
+
fileConfig: {
|
|
208
|
+
storage: { type: 'local', basePath: join(testDir, 'artemis-runs') },
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const manifest = await storage.load('test-run-001');
|
|
213
|
+
const html = generateHTMLReport(manifest);
|
|
214
|
+
|
|
215
|
+
expect(html).toContain('case-1');
|
|
216
|
+
expect(html).toContain('case-2');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should include metrics summary', async () => {
|
|
220
|
+
const storage = createStorage({
|
|
221
|
+
fileConfig: {
|
|
222
|
+
storage: { type: 'local', basePath: join(testDir, 'artemis-runs') },
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const manifest = await storage.load('test-run-001');
|
|
227
|
+
const html = generateHTMLReport(manifest);
|
|
228
|
+
|
|
229
|
+
expect(html).toContain('80'); // 80% success rate
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should include run ID in report', async () => {
|
|
233
|
+
const storage = createStorage({
|
|
234
|
+
fileConfig: {
|
|
235
|
+
storage: { type: 'local', basePath: join(testDir, 'artemis-runs') },
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const manifest = await storage.load('test-run-001');
|
|
240
|
+
const html = generateHTMLReport(manifest);
|
|
241
|
+
|
|
242
|
+
expect(html).toContain('test-run-001');
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
});
|