@artemiskit/sdk 0.3.0

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.
@@ -0,0 +1,315 @@
1
+ /**
2
+ * @artemiskit/sdk
3
+ * Core matcher implementations for Jest/Vitest integration
4
+ */
5
+
6
+ import type { CaseResult, RunManifest } from '@artemiskit/core';
7
+ import type { RedTeamResult, RunResult, StressResult } from '../types';
8
+
9
+ /**
10
+ * Matcher result interface compatible with Jest/Vitest
11
+ */
12
+ export interface MatcherResult {
13
+ pass: boolean;
14
+ message: () => string;
15
+ }
16
+
17
+ /**
18
+ * Format a manifest summary for error messages
19
+ */
20
+ function formatManifestSummary(manifest: RunManifest): string {
21
+ const { metrics } = manifest;
22
+ return `
23
+ Scenario: ${manifest.config.scenario}
24
+ Total Cases: ${metrics.total_cases}
25
+ Passed: ${metrics.passed_cases}
26
+ Failed: ${metrics.failed_cases}
27
+ Success Rate: ${(metrics.success_rate * 100).toFixed(1)}%
28
+ `;
29
+ }
30
+
31
+ /**
32
+ * Format failed cases for error messages
33
+ */
34
+ function formatFailedCases(cases: CaseResult[]): string {
35
+ const failed = cases.filter((c) => !c.ok);
36
+ if (failed.length === 0) return '';
37
+
38
+ return `
39
+ Failed Cases:
40
+ ${failed
41
+ .slice(0, 5) // Show first 5 failures
42
+ .map((c) => ` - ${c.name ?? c.id}: ${c.reason ?? 'No reason provided'}`)
43
+ .join('\n')}${failed.length > 5 ? `\n ... and ${failed.length - 5} more` : ''}
44
+ `;
45
+ }
46
+
47
+ // ==========================================================================
48
+ // Run Result Matchers
49
+ // ==========================================================================
50
+
51
+ /**
52
+ * Check if a run result passed all test cases
53
+ */
54
+ export function toPassAllCases(result: RunResult): MatcherResult {
55
+ const pass = result.success;
56
+
57
+ return {
58
+ pass,
59
+ message: () =>
60
+ pass
61
+ ? `Expected test run to fail, but all ${result.manifest.metrics.total_cases} cases passed`
62
+ : `Expected test run to pass, but ${result.manifest.metrics.failed_cases} out of ${result.manifest.metrics.total_cases} cases failed
63
+ ${formatManifestSummary(result.manifest)}${formatFailedCases(result.cases)}`,
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Check if success rate meets threshold
69
+ */
70
+ export function toHaveSuccessRate(result: RunResult, expectedRate: number): MatcherResult {
71
+ const actualRate = result.manifest.metrics.success_rate;
72
+ const pass = actualRate >= expectedRate;
73
+
74
+ return {
75
+ pass,
76
+ message: () =>
77
+ pass
78
+ ? `Expected success rate to be less than ${(expectedRate * 100).toFixed(1)}%, but got ${(actualRate * 100).toFixed(1)}%`
79
+ : `Expected success rate to be at least ${(expectedRate * 100).toFixed(1)}%, but got ${(actualRate * 100).toFixed(1)}%
80
+ ${formatManifestSummary(result.manifest)}${formatFailedCases(result.cases)}`,
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Check if all cases with specific tags passed
86
+ */
87
+ export function toPassCasesWithTag(result: RunResult, tag: string): MatcherResult {
88
+ const taggedCases = result.cases.filter((c) => c.tags.includes(tag));
89
+ const failedTaggedCases = taggedCases.filter((c) => !c.ok);
90
+ const pass = failedTaggedCases.length === 0;
91
+
92
+ return {
93
+ pass,
94
+ message: () =>
95
+ pass
96
+ ? `Expected cases with tag "${tag}" to fail, but all ${taggedCases.length} passed`
97
+ : `Expected all cases with tag "${tag}" to pass, but ${failedTaggedCases.length} out of ${taggedCases.length} failed
98
+ ${formatFailedCases(failedTaggedCases)}`,
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Check if median latency is within threshold
104
+ */
105
+ export function toHaveMedianLatencyBelow(result: RunResult, maxLatencyMs: number): MatcherResult {
106
+ const actualLatency = result.manifest.metrics.median_latency_ms;
107
+ const pass = actualLatency <= maxLatencyMs;
108
+
109
+ return {
110
+ pass,
111
+ message: () =>
112
+ pass
113
+ ? `Expected median latency to be above ${maxLatencyMs}ms, but got ${actualLatency}ms`
114
+ : `Expected median latency to be at most ${maxLatencyMs}ms, but got ${actualLatency}ms`,
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Check if P95 latency is within threshold
120
+ */
121
+ export function toHaveP95LatencyBelow(result: RunResult, maxLatencyMs: number): MatcherResult {
122
+ const actualLatency = result.manifest.metrics.p95_latency_ms;
123
+ const pass = actualLatency <= maxLatencyMs;
124
+
125
+ return {
126
+ pass,
127
+ message: () =>
128
+ pass
129
+ ? `Expected P95 latency to be above ${maxLatencyMs}ms, but got ${actualLatency}ms`
130
+ : `Expected P95 latency to be at most ${maxLatencyMs}ms, but got ${actualLatency}ms`,
131
+ };
132
+ }
133
+
134
+ // ==========================================================================
135
+ // Red Team Result Matchers
136
+ // ==========================================================================
137
+
138
+ /**
139
+ * Check if defense rate meets threshold
140
+ */
141
+ export function toHaveDefenseRate(result: RedTeamResult, expectedRate: number): MatcherResult {
142
+ const actualRate = result.defenseRate;
143
+ const pass = actualRate >= expectedRate;
144
+
145
+ return {
146
+ pass,
147
+ message: () =>
148
+ pass
149
+ ? `Expected defense rate to be less than ${(expectedRate * 100).toFixed(1)}%, but got ${(actualRate * 100).toFixed(1)}%`
150
+ : `Expected defense rate to be at least ${(expectedRate * 100).toFixed(1)}%, but got ${(actualRate * 100).toFixed(1)}%
151
+
152
+ Unsafe responses: ${result.unsafeCount}
153
+ Total tests: ${result.manifest.metrics.total_tests}`,
154
+ };
155
+ }
156
+
157
+ /**
158
+ * Check that no critical vulnerabilities were found
159
+ */
160
+ export function toHaveNoCriticalVulnerabilities(result: RedTeamResult): MatcherResult {
161
+ const criticalCount = result.manifest.metrics.by_severity.critical;
162
+ const pass = criticalCount === 0;
163
+
164
+ return {
165
+ pass,
166
+ message: () =>
167
+ pass
168
+ ? 'Expected critical vulnerabilities, but none were found'
169
+ : `Expected no critical vulnerabilities, but found ${criticalCount}`,
170
+ };
171
+ }
172
+
173
+ /**
174
+ * Check that no high or critical vulnerabilities were found
175
+ */
176
+ export function toHaveNoHighSeverityVulnerabilities(result: RedTeamResult): MatcherResult {
177
+ const highCount = result.manifest.metrics.by_severity.high;
178
+ const criticalCount = result.manifest.metrics.by_severity.critical;
179
+ const totalHighSeverity = highCount + criticalCount;
180
+ const pass = totalHighSeverity === 0;
181
+
182
+ return {
183
+ pass,
184
+ message: () =>
185
+ pass
186
+ ? 'Expected high/critical vulnerabilities, but none were found'
187
+ : `Expected no high/critical vulnerabilities, but found ${totalHighSeverity} (${highCount} high, ${criticalCount} critical)`,
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Check if red team test passed (default 95% defense rate)
193
+ */
194
+ export function toPassRedTeam(result: RedTeamResult): MatcherResult {
195
+ const pass = result.success;
196
+ const { metrics } = result.manifest;
197
+
198
+ return {
199
+ pass,
200
+ message: () =>
201
+ pass
202
+ ? `Expected red team test to fail, but passed with ${(result.defenseRate * 100).toFixed(1)}% defense rate`
203
+ : `Expected red team test to pass, but defense rate was only ${(result.defenseRate * 100).toFixed(1)}%
204
+
205
+ Total tests: ${metrics.total_tests}
206
+ Safe: ${metrics.safe_responses}
207
+ Blocked: ${metrics.blocked_responses}
208
+ Unsafe: ${metrics.unsafe_responses}
209
+ Errors: ${metrics.error_responses}`,
210
+ };
211
+ }
212
+
213
+ // ==========================================================================
214
+ // Stress Test Result Matchers
215
+ // ==========================================================================
216
+
217
+ /**
218
+ * Check if stress test success rate meets threshold
219
+ */
220
+ export function toHaveStressSuccessRate(result: StressResult, expectedRate: number): MatcherResult {
221
+ const actualRate = result.successRate;
222
+ const pass = actualRate >= expectedRate;
223
+
224
+ return {
225
+ pass,
226
+ message: () =>
227
+ pass
228
+ ? `Expected stress test success rate to be less than ${(expectedRate * 100).toFixed(1)}%, but got ${(actualRate * 100).toFixed(1)}%`
229
+ : `Expected stress test success rate to be at least ${(expectedRate * 100).toFixed(1)}%, but got ${(actualRate * 100).toFixed(1)}%`,
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Check if stress test achieved target RPS
235
+ */
236
+ export function toAchieveRPS(result: StressResult, targetRPS: number): MatcherResult {
237
+ const actualRPS = result.rps;
238
+ const pass = actualRPS >= targetRPS;
239
+
240
+ return {
241
+ pass,
242
+ message: () =>
243
+ pass
244
+ ? `Expected RPS to be less than ${targetRPS}, but achieved ${actualRPS.toFixed(1)} RPS`
245
+ : `Expected to achieve at least ${targetRPS} RPS, but only got ${actualRPS.toFixed(1)} RPS`,
246
+ };
247
+ }
248
+
249
+ /**
250
+ * Check if stress test P95 latency is within threshold
251
+ */
252
+ export function toHaveStressP95LatencyBelow(
253
+ result: StressResult,
254
+ maxLatencyMs: number
255
+ ): MatcherResult {
256
+ const actualLatency = result.p95LatencyMs;
257
+ const pass = actualLatency <= maxLatencyMs;
258
+
259
+ return {
260
+ pass,
261
+ message: () =>
262
+ pass
263
+ ? `Expected P95 latency to be above ${maxLatencyMs}ms, but got ${actualLatency}ms`
264
+ : `Expected P95 latency to be at most ${maxLatencyMs}ms, but got ${actualLatency}ms`,
265
+ };
266
+ }
267
+
268
+ /**
269
+ * Check if stress test passed (default 95% success rate)
270
+ */
271
+ export function toPassStressTest(result: StressResult): MatcherResult {
272
+ const pass = result.success;
273
+ const { metrics } = result.manifest;
274
+
275
+ return {
276
+ pass,
277
+ message: () =>
278
+ pass
279
+ ? `Expected stress test to fail, but passed with ${(result.successRate * 100).toFixed(1)}% success rate`
280
+ : `Expected stress test to pass, but success rate was only ${(result.successRate * 100).toFixed(1)}%
281
+
282
+ Total requests: ${metrics.total_requests}
283
+ Successful: ${metrics.successful_requests}
284
+ Failed: ${metrics.failed_requests}
285
+ RPS: ${metrics.requests_per_second.toFixed(1)}
286
+ P95 Latency: ${metrics.p95_latency_ms}ms`,
287
+ };
288
+ }
289
+
290
+ // ==========================================================================
291
+ // Export all matchers
292
+ // ==========================================================================
293
+
294
+ export const artemiskitMatchers = {
295
+ // Run matchers
296
+ toPassAllCases,
297
+ toHaveSuccessRate,
298
+ toPassCasesWithTag,
299
+ toHaveMedianLatencyBelow,
300
+ toHaveP95LatencyBelow,
301
+
302
+ // Red team matchers
303
+ toHaveDefenseRate,
304
+ toHaveNoCriticalVulnerabilities,
305
+ toHaveNoHighSeverityVulnerabilities,
306
+ toPassRedTeam,
307
+
308
+ // Stress test matchers
309
+ toHaveStressSuccessRate,
310
+ toAchieveRPS,
311
+ toHaveStressP95LatencyBelow,
312
+ toPassStressTest,
313
+ };
314
+
315
+ export type ArtemisKitMatchers = typeof artemiskitMatchers;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @artemiskit/sdk
3
+ * Test matchers - core implementations without framework bindings
4
+ */
5
+
6
+ export {
7
+ artemiskitMatchers,
8
+ type ArtemisKitMatchers,
9
+ type MatcherResult,
10
+ // Run matchers
11
+ toPassAllCases,
12
+ toHaveSuccessRate,
13
+ toPassCasesWithTag,
14
+ toHaveMedianLatencyBelow,
15
+ toHaveP95LatencyBelow,
16
+ // Red team matchers
17
+ toHaveDefenseRate,
18
+ toHaveNoCriticalVulnerabilities,
19
+ toHaveNoHighSeverityVulnerabilities,
20
+ toPassRedTeam,
21
+ // Stress test matchers
22
+ toHaveStressSuccessRate,
23
+ toAchieveRPS,
24
+ toHaveStressP95LatencyBelow,
25
+ toPassStressTest,
26
+ } from './core';
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @artemiskit/sdk
3
+ * Jest integration - custom matchers for ArtemisKit
4
+ */
5
+
6
+ import type { RedTeamResult, RunResult, StressResult } from '../types';
7
+ import {
8
+ toAchieveRPS,
9
+ toHaveDefenseRate,
10
+ toHaveMedianLatencyBelow,
11
+ toHaveNoCriticalVulnerabilities,
12
+ toHaveNoHighSeverityVulnerabilities,
13
+ toHaveP95LatencyBelow,
14
+ toHaveStressP95LatencyBelow,
15
+ toHaveStressSuccessRate,
16
+ toHaveSuccessRate,
17
+ toPassAllCases,
18
+ toPassCasesWithTag,
19
+ toPassRedTeam,
20
+ toPassStressTest,
21
+ } from './core';
22
+
23
+ /**
24
+ * Jest matchers implementation
25
+ */
26
+ export const jestMatchers = {
27
+ toPassAllCases(received: RunResult) {
28
+ return toPassAllCases(received);
29
+ },
30
+
31
+ toHaveSuccessRate(received: RunResult, expectedRate: number) {
32
+ return toHaveSuccessRate(received, expectedRate);
33
+ },
34
+
35
+ toPassCasesWithTag(received: RunResult, tag: string) {
36
+ return toPassCasesWithTag(received, tag);
37
+ },
38
+
39
+ toHaveMedianLatencyBelow(received: RunResult, maxLatencyMs: number) {
40
+ return toHaveMedianLatencyBelow(received, maxLatencyMs);
41
+ },
42
+
43
+ toHaveP95LatencyBelow(received: RunResult, maxLatencyMs: number) {
44
+ return toHaveP95LatencyBelow(received, maxLatencyMs);
45
+ },
46
+
47
+ toHaveDefenseRate(received: RedTeamResult, expectedRate: number) {
48
+ return toHaveDefenseRate(received, expectedRate);
49
+ },
50
+
51
+ toHaveNoCriticalVulnerabilities(received: RedTeamResult) {
52
+ return toHaveNoCriticalVulnerabilities(received);
53
+ },
54
+
55
+ toHaveNoHighSeverityVulnerabilities(received: RedTeamResult) {
56
+ return toHaveNoHighSeverityVulnerabilities(received);
57
+ },
58
+
59
+ toPassRedTeam(received: RedTeamResult) {
60
+ return toPassRedTeam(received);
61
+ },
62
+
63
+ toHaveStressSuccessRate(received: StressResult, expectedRate: number) {
64
+ return toHaveStressSuccessRate(received, expectedRate);
65
+ },
66
+
67
+ toAchieveRPS(received: StressResult, targetRPS: number) {
68
+ return toAchieveRPS(received, targetRPS);
69
+ },
70
+
71
+ toHaveStressP95LatencyBelow(received: StressResult, maxLatencyMs: number) {
72
+ return toHaveStressP95LatencyBelow(received, maxLatencyMs);
73
+ },
74
+
75
+ toPassStressTest(received: StressResult) {
76
+ return toPassStressTest(received);
77
+ },
78
+ };
79
+
80
+ // Extend Jest's expect if available
81
+ declare const expect: { extend: (matchers: object) => void } | undefined;
82
+ if (typeof expect !== 'undefined' && typeof expect.extend === 'function') {
83
+ expect.extend(jestMatchers);
84
+ }
85
+
86
+ /**
87
+ * TypeScript declarations for Jest matchers
88
+ */
89
+ declare global {
90
+ namespace jest {
91
+ interface Matchers<R> {
92
+ // Run matchers
93
+ toPassAllCases(): R;
94
+ toHaveSuccessRate(expectedRate: number): R;
95
+ toPassCasesWithTag(tag: string): R;
96
+ toHaveMedianLatencyBelow(maxLatencyMs: number): R;
97
+ toHaveP95LatencyBelow(maxLatencyMs: number): R;
98
+
99
+ // Red team matchers
100
+ toHaveDefenseRate(expectedRate: number): R;
101
+ toHaveNoCriticalVulnerabilities(): R;
102
+ toHaveNoHighSeverityVulnerabilities(): R;
103
+ toPassRedTeam(): R;
104
+
105
+ // Stress test matchers
106
+ toHaveStressSuccessRate(expectedRate: number): R;
107
+ toAchieveRPS(targetRPS: number): R;
108
+ toHaveStressP95LatencyBelow(maxLatencyMs: number): R;
109
+ toPassStressTest(): R;
110
+ }
111
+ }
112
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * @artemiskit/sdk
3
+ * Vitest integration - custom matchers for ArtemisKit
4
+ */
5
+
6
+ import type { RedTeamResult, RunResult, StressResult } from '../types';
7
+ import {
8
+ toAchieveRPS,
9
+ toHaveDefenseRate,
10
+ toHaveMedianLatencyBelow,
11
+ toHaveNoCriticalVulnerabilities,
12
+ toHaveNoHighSeverityVulnerabilities,
13
+ toHaveP95LatencyBelow,
14
+ toHaveStressP95LatencyBelow,
15
+ toHaveStressSuccessRate,
16
+ toHaveSuccessRate,
17
+ toPassAllCases,
18
+ toPassCasesWithTag,
19
+ toPassRedTeam,
20
+ toPassStressTest,
21
+ } from './core';
22
+
23
+ /**
24
+ * Vitest matchers implementation
25
+ */
26
+ export const vitestMatchers = {
27
+ toPassAllCases(received: RunResult) {
28
+ return toPassAllCases(received);
29
+ },
30
+
31
+ toHaveSuccessRate(received: RunResult, expectedRate: number) {
32
+ return toHaveSuccessRate(received, expectedRate);
33
+ },
34
+
35
+ toPassCasesWithTag(received: RunResult, tag: string) {
36
+ return toPassCasesWithTag(received, tag);
37
+ },
38
+
39
+ toHaveMedianLatencyBelow(received: RunResult, maxLatencyMs: number) {
40
+ return toHaveMedianLatencyBelow(received, maxLatencyMs);
41
+ },
42
+
43
+ toHaveP95LatencyBelow(received: RunResult, maxLatencyMs: number) {
44
+ return toHaveP95LatencyBelow(received, maxLatencyMs);
45
+ },
46
+
47
+ toHaveDefenseRate(received: RedTeamResult, expectedRate: number) {
48
+ return toHaveDefenseRate(received, expectedRate);
49
+ },
50
+
51
+ toHaveNoCriticalVulnerabilities(received: RedTeamResult) {
52
+ return toHaveNoCriticalVulnerabilities(received);
53
+ },
54
+
55
+ toHaveNoHighSeverityVulnerabilities(received: RedTeamResult) {
56
+ return toHaveNoHighSeverityVulnerabilities(received);
57
+ },
58
+
59
+ toPassRedTeam(received: RedTeamResult) {
60
+ return toPassRedTeam(received);
61
+ },
62
+
63
+ toHaveStressSuccessRate(received: StressResult, expectedRate: number) {
64
+ return toHaveStressSuccessRate(received, expectedRate);
65
+ },
66
+
67
+ toAchieveRPS(received: StressResult, targetRPS: number) {
68
+ return toAchieveRPS(received, targetRPS);
69
+ },
70
+
71
+ toHaveStressP95LatencyBelow(received: StressResult, maxLatencyMs: number) {
72
+ return toHaveStressP95LatencyBelow(received, maxLatencyMs);
73
+ },
74
+
75
+ toPassStressTest(received: StressResult) {
76
+ return toPassStressTest(received);
77
+ },
78
+ };
79
+
80
+ // Extend Vitest's expect if available
81
+ declare const expect: { extend: (matchers: object) => void } | undefined;
82
+ if (typeof expect !== 'undefined' && typeof expect.extend === 'function') {
83
+ expect.extend(vitestMatchers);
84
+ }