@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +1 -0
  3. package/dist/index.js +19129 -20009
  4. package/dist/src/commands/compare.d.ts.map +1 -1
  5. package/dist/src/commands/history.d.ts.map +1 -1
  6. package/dist/src/commands/init.d.ts.map +1 -1
  7. package/dist/src/commands/redteam.d.ts.map +1 -1
  8. package/dist/src/commands/report.d.ts.map +1 -1
  9. package/dist/src/commands/run.d.ts.map +1 -1
  10. package/dist/src/commands/stress.d.ts.map +1 -1
  11. package/dist/src/ui/colors.d.ts +44 -0
  12. package/dist/src/ui/colors.d.ts.map +1 -0
  13. package/dist/src/ui/errors.d.ts +39 -0
  14. package/dist/src/ui/errors.d.ts.map +1 -0
  15. package/dist/src/ui/index.d.ts +16 -0
  16. package/dist/src/ui/index.d.ts.map +1 -0
  17. package/dist/src/ui/live-status.d.ts +82 -0
  18. package/dist/src/ui/live-status.d.ts.map +1 -0
  19. package/dist/src/ui/panels.d.ts +49 -0
  20. package/dist/src/ui/panels.d.ts.map +1 -0
  21. package/dist/src/ui/progress.d.ts +60 -0
  22. package/dist/src/ui/progress.d.ts.map +1 -0
  23. package/dist/src/ui/utils.d.ts +42 -0
  24. package/dist/src/ui/utils.d.ts.map +1 -0
  25. package/package.json +6 -6
  26. package/src/__tests__/helpers/index.ts +6 -0
  27. package/src/__tests__/helpers/mock-adapter.ts +90 -0
  28. package/src/__tests__/helpers/test-utils.ts +205 -0
  29. package/src/__tests__/integration/compare-command.test.ts +236 -0
  30. package/src/__tests__/integration/config.test.ts +125 -0
  31. package/src/__tests__/integration/history-command.test.ts +251 -0
  32. package/src/__tests__/integration/init-command.test.ts +177 -0
  33. package/src/__tests__/integration/report-command.test.ts +245 -0
  34. package/src/__tests__/integration/ui.test.ts +230 -0
  35. package/src/commands/compare.ts +158 -49
  36. package/src/commands/history.ts +131 -30
  37. package/src/commands/init.ts +181 -21
  38. package/src/commands/redteam.ts +118 -75
  39. package/src/commands/report.ts +29 -14
  40. package/src/commands/run.ts +86 -66
  41. package/src/commands/stress.ts +61 -63
  42. package/src/ui/colors.ts +62 -0
  43. package/src/ui/errors.ts +248 -0
  44. package/src/ui/index.ts +42 -0
  45. package/src/ui/live-status.ts +259 -0
  46. package/src/ui/panels.ts +216 -0
  47. package/src/ui/progress.ts +139 -0
  48. 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
+ });