@ozperium/agentspec 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ozperium
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # AgentSpec
2
+
3
+ > Testing framework for AI agents — Jest for non-deterministic AI behavior
4
+
5
+ AI agents are non-deterministic. When you change a prompt, swap a model, or update a tool, behavior shifts in ways you can't predict. AgentSpec lets you write tests that catch those shifts before they reach production.
6
+
7
+ ## Quick start
8
+
9
+ ```bash
10
+ npm install -g agentspec
11
+ agentspec init
12
+ ```
13
+
14
+ This creates `agentspec.yaml`:
15
+
16
+ ```yaml
17
+ name: "my-agent-tests"
18
+ tests:
19
+ - name: "handles greeting"
20
+ input: "hello"
21
+ expect:
22
+ contains: "help"
23
+ ```
24
+
25
+ Run tests:
26
+
27
+ ```bash
28
+ agentspec run
29
+ ```
30
+
31
+ ## Assertions for non-deterministic output
32
+
33
+ AgentSpec provides assertions designed for AI output, not just string equality:
34
+
35
+ ```yaml
36
+ tests:
37
+ - name: "handles expired token"
38
+ input: "my token expired"
39
+ expect:
40
+ contains: "refresh token"
41
+ not_contains: "I don't know"
42
+ max_latency_ms: 5000
43
+
44
+ - name: "routes billing question"
45
+ input: "I want a refund"
46
+ expect:
47
+ contains_any: ["billing", "refund", "support"]
48
+ tool_called: "transfer_to_billing"
49
+
50
+ - name: "response is semantically on-topic"
51
+ input: "how do I reset my password?"
52
+ expect:
53
+ semantically_similar: "reset password credentials"
54
+ min_confidence: 0.5
55
+
56
+ - name: "JSON output has correct structure"
57
+ input: "get user profile"
58
+ expect:
59
+ json_path: "data.email"
60
+ json_value: "user@example.com"
61
+
62
+ - name: "matches error code pattern"
63
+ input: "simulate error"
64
+ expect:
65
+ regex: "ERR-\\d{5}"
66
+ ```
67
+
68
+ ## LLM-as-judge
69
+
70
+ Use a local model to evaluate response quality — no API costs, full privacy:
71
+
72
+ ```bash
73
+ # Use local Ollama as judge
74
+ agentspec run --judge-endpoint http://127.0.0.1:11434/v1/chat/completions --judge-model qwen2.5:7b
75
+ ```
76
+
77
+ ```yaml
78
+ tests:
79
+ - name: "response is helpful"
80
+ input: "How do I reset my password?"
81
+ expect:
82
+ llm_judge: "The response should explain how to reset a password"
83
+ ```
84
+
85
+ ## Behavior diff reports
86
+
87
+ AgentSpec stores the last passing output per test. When behavior changes, you see exactly what shifted:
88
+
89
+ ```
90
+ ⚠️ REGRESSION support agent > handles expired token
91
+ ⚠ Behavior REGRESSED — test was passing, now failing
92
+ + added: your, token, has, expired, please, contact, support
93
+ - removed: you, need, refresh, the, token, settings, security
94
+ ```
95
+
96
+ ## Testing real agents
97
+
98
+ Use `--endpoint` to test any HTTP-accessible agent:
99
+
100
+ ```bash
101
+ agentspec run --endpoint https://my-agent.example.com/chat
102
+ ```
103
+
104
+ AgentSpec POSTs `{input: "..."}` to your endpoint and expects `{output: "..."}` in the response.
105
+
106
+ ## CI/CD integration
107
+
108
+ ```bash
109
+ # Exit code 1 on failure
110
+ agentspec run --ci
111
+
112
+ # JUnit XML for CI systems
113
+ agentspec run --junit > results.xml
114
+
115
+ # JSON output
116
+ agentspec run --json
117
+ ```
118
+
119
+ ### GitHub Action
120
+
121
+ ```yaml
122
+ - uses: agentspec/action@v1
123
+ with:
124
+ test-dir: tests
125
+ format: junit
126
+ ```
127
+
128
+ ## CLI commands
129
+
130
+ ```
131
+ agentspec run [--dir tests] [--ci] [--json] [--junit] [--watch] [--test "pattern"]
132
+ agentspec init
133
+ agentspec list [--dir tests]
134
+ agentspec version
135
+ ```
136
+
137
+ ## Why AgentSpec?
138
+
139
+ | Problem | AgentSpec |
140
+ |---------|-----------|
141
+ | Agent behavior changes with model updates | Regression tests catch the shift |
142
+ | "It worked yesterday" debugging | Diff reports show what changed |
143
+ | Manual testing before each deploy | CI/CD gates with `--ci` exit codes |
144
+ | Jest/Vitest don't handle non-deterministic output | `contains_any`, `regex`, `llm_judge`, `semantically_similar` |
145
+ | No CI integration for AI features | GitHub Action + JUnit XML |
146
+
147
+ ## Roadmap
148
+
149
+ - [x] YAML test definitions
150
+ - [x] Core assertions (contains, not_contains, contains_any, contains_all, regex)
151
+ - [x] Metadata assertions (max_latency_ms, max_tokens, tool_called)
152
+ - [x] Semantic similarity (word overlap)
153
+ - [x] JSON path assertions
154
+ - [x] CLI with run/init/list commands
155
+ - [x] CI mode with exit codes
156
+ - [x] JUnit XML and JSON output
157
+ - [x] Test filtering
158
+ - [x] GitHub Action
159
+ - [x] **Behavior diff reports** — see what changed between runs
160
+ - [x] **LLM-as-judge** — use a local model (Ollama) to evaluate output quality
161
+ - [x] **HTTP agent adapter** — test real agents via `--endpoint`
162
+ - [ ] Auto-test generation from conversation history
163
+ - [ ] Python agent support
164
+ - [ ] Watch mode with file watcher
165
+ - [ ] Web dashboard
166
+ - [ ] GitHub PR integration (comment on PRs with behavior diffs)
167
+
168
+ ## License
169
+
170
+ MIT
package/action.yml ADDED
@@ -0,0 +1,32 @@
1
+ name: 'AgentSpec — AI Agent Tests'
2
+ description: 'Run AgentSpec tests for your AI agents in CI'
3
+ branding:
4
+ icon: 'check-circle'
5
+ color: 'blue'
6
+ inputs:
7
+ test-dir:
8
+ description: 'Directory containing test suites'
9
+ required: false
10
+ default: 'tests'
11
+ format:
12
+ description: 'Output format (text, json, junit)'
13
+ required: false
14
+ default: 'junit'
15
+ fail-on-failure:
16
+ description: 'Exit with error code on test failures'
17
+ required: false
18
+ default: 'true'
19
+ runs:
20
+ using: 'composite'
21
+ steps:
22
+ - name: Setup Node.js
23
+ uses: actions/setup-node@v4
24
+ with:
25
+ node-version: '20'
26
+ - name: Install AgentSpec
27
+ shell: bash
28
+ run: npm install -g agentspec
29
+ - name: Run AgentSpec tests
30
+ shell: bash
31
+ run: agentspec run --dir ${{ inputs.test-dir }} ${{ inputs.format == 'junit' && '--junit' || '' }} ${{ inputs.format == 'json' && '--json' || '' }} --ci
32
+ continue-on-error: ${{ inputs.fail-on-failure != 'true' }}
@@ -0,0 +1,14 @@
1
+ import { TestExpect, AssertionResult } from '../types';
2
+ import { LLMJudgeConfig } from '../llm-judge';
3
+ export declare function setLLMJudgeConfig(config: LLMJudgeConfig | undefined): void;
4
+ export declare function getLLMJudgeConfig(): LLMJudgeConfig | undefined;
5
+ export declare function evaluateAssertions(output: string, expect: TestExpect, metadata?: {
6
+ tokens?: number;
7
+ latencyMs?: number;
8
+ toolsCalled?: string[];
9
+ }): AssertionResult[];
10
+ export declare function evaluateAssertionsAsync(output: string, expect: TestExpect, input: string, metadata?: {
11
+ tokens?: number;
12
+ latencyMs?: number;
13
+ toolsCalled?: string[];
14
+ }): Promise<AssertionResult[]>;
@@ -0,0 +1,215 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setLLMJudgeConfig = setLLMJudgeConfig;
4
+ exports.getLLMJudgeConfig = getLLMJudgeConfig;
5
+ exports.evaluateAssertions = evaluateAssertions;
6
+ exports.evaluateAssertionsAsync = evaluateAssertionsAsync;
7
+ const llm_judge_1 = require("../llm-judge");
8
+ // Global LLM judge config (can be set via CLI or env)
9
+ let globalLLMJudgeConfig;
10
+ function setLLMJudgeConfig(config) {
11
+ globalLLMJudgeConfig = config;
12
+ }
13
+ function getLLMJudgeConfig() {
14
+ return globalLLMJudgeConfig;
15
+ }
16
+ function evaluateAssertions(output, expect, metadata) {
17
+ const results = [];
18
+ // Synchronous assertions only — llm_judge handled separately in evaluateAssertionsAsync
19
+ if (expect.contains !== undefined) {
20
+ const passed = output.includes(expect.contains);
21
+ results.push({
22
+ assertion: 'contains',
23
+ passed,
24
+ message: passed ? `Output contains "${expect.contains}"` : `Output does not contain "${expect.contains}"`,
25
+ expected: expect.contains,
26
+ actual: passed ? 'found' : 'not found',
27
+ });
28
+ }
29
+ if (expect.not_contains !== undefined) {
30
+ const passed = !output.includes(expect.not_contains);
31
+ results.push({
32
+ assertion: 'not_contains',
33
+ passed,
34
+ message: passed ? `Output does not contain "${expect.not_contains}"` : `Output contains "${expect.not_contains}"`,
35
+ expected: `not "${expect.not_contains}"`,
36
+ actual: passed ? 'absent' : 'present',
37
+ });
38
+ }
39
+ if (expect.contains_any !== undefined) {
40
+ const found = expect.contains_any.find(s => output.includes(s));
41
+ const passed = !!found;
42
+ results.push({
43
+ assertion: 'contains_any',
44
+ passed,
45
+ message: passed ? `Output contains one of: "${found}"` : `Output contains none of: ${JSON.stringify(expect.contains_any)}`,
46
+ expected: JSON.stringify(expect.contains_any),
47
+ actual: found || 'none matched',
48
+ });
49
+ }
50
+ if (expect.contains_all !== undefined) {
51
+ const missing = expect.contains_all.filter(s => !output.includes(s));
52
+ const passed = missing.length === 0;
53
+ results.push({
54
+ assertion: 'contains_all',
55
+ passed,
56
+ message: passed ? `Output contains all required strings` : `Missing: ${missing.join(', ')}`,
57
+ expected: JSON.stringify(expect.contains_all),
58
+ actual: passed ? 'all found' : `missing: ${missing.join(', ')}`,
59
+ });
60
+ }
61
+ if (expect.regex !== undefined) {
62
+ try {
63
+ const re = new RegExp(expect.regex);
64
+ const passed = re.test(output);
65
+ results.push({
66
+ assertion: 'regex',
67
+ passed,
68
+ message: passed ? `Output matches /${expect.regex}/` : `Output does not match /${expect.regex}/`,
69
+ expected: `/${expect.regex}/`,
70
+ actual: passed ? 'matched' : 'no match',
71
+ });
72
+ }
73
+ catch (e) {
74
+ results.push({
75
+ assertion: 'regex',
76
+ passed: false,
77
+ message: `Invalid regex: ${expect.regex}`,
78
+ expected: expect.regex,
79
+ actual: 'invalid pattern',
80
+ });
81
+ }
82
+ }
83
+ if (expect.max_latency_ms !== undefined && metadata?.latencyMs !== undefined) {
84
+ const passed = metadata.latencyMs <= expect.max_latency_ms;
85
+ results.push({
86
+ assertion: 'max_latency_ms',
87
+ passed,
88
+ message: passed ? `Latency ${metadata.latencyMs}ms ≤ ${expect.max_latency_ms}ms` : `Latency ${metadata.latencyMs}ms > ${expect.max_latency_ms}ms`,
89
+ expected: `≤ ${expect.max_latency_ms}ms`,
90
+ actual: `${metadata.latencyMs}ms`,
91
+ });
92
+ }
93
+ if (expect.max_tokens !== undefined && metadata?.tokens !== undefined) {
94
+ const passed = metadata.tokens <= expect.max_tokens;
95
+ results.push({
96
+ assertion: 'max_tokens',
97
+ passed,
98
+ message: passed ? `Tokens ${metadata.tokens} ≤ ${expect.max_tokens}` : `Tokens ${metadata.tokens} > ${expect.max_tokens}`,
99
+ expected: `≤ ${expect.max_tokens}`,
100
+ actual: `${metadata.tokens}`,
101
+ });
102
+ }
103
+ if (expect.tool_called !== undefined && metadata?.toolsCalled !== undefined) {
104
+ const passed = metadata.toolsCalled.includes(expect.tool_called);
105
+ results.push({
106
+ assertion: 'tool_called',
107
+ passed,
108
+ message: passed ? `Tool "${expect.tool_called}" was called` : `Tool "${expect.tool_called}" was not called. Called: ${metadata.toolsCalled.join(', ') || 'none'}`,
109
+ expected: expect.tool_called,
110
+ actual: passed ? 'called' : `not called (called: ${metadata.toolsCalled.join(', ') || 'none'})`,
111
+ });
112
+ }
113
+ // json_path assertion: extract value from JSON output and compare
114
+ if (expect.json_path !== undefined) {
115
+ try {
116
+ const json = JSON.parse(output);
117
+ const value = extractJsonPath(json, expect.json_path);
118
+ if (expect.json_value !== undefined) {
119
+ const passed = JSON.stringify(value) === JSON.stringify(expect.json_value);
120
+ results.push({
121
+ assertion: 'json_path',
122
+ passed,
123
+ message: passed ? `json_path "${expect.json_path}" equals expected value` : `json_path "${expect.json_path}" mismatch`,
124
+ expected: JSON.stringify(expect.json_value),
125
+ actual: JSON.stringify(value),
126
+ });
127
+ }
128
+ else {
129
+ const passed = value !== undefined;
130
+ results.push({
131
+ assertion: 'json_path',
132
+ passed,
133
+ message: passed ? `json_path "${expect.json_path}" found` : `json_path "${expect.json_path}" not found`,
134
+ expected: expect.json_path,
135
+ actual: JSON.stringify(value),
136
+ });
137
+ }
138
+ }
139
+ catch (e) {
140
+ results.push({
141
+ assertion: 'json_path',
142
+ passed: false,
143
+ message: `Output is not valid JSON or path not found: ${e instanceof Error ? e.message : String(e)}`,
144
+ expected: expect.json_path,
145
+ actual: 'parse error',
146
+ });
147
+ }
148
+ }
149
+ // semantically_similar: check if output is semantically similar to expected (using simple word overlap as MVP)
150
+ if (expect.semantically_similar !== undefined) {
151
+ const similarity = wordSimilarity(output, expect.semantically_similar);
152
+ const threshold = expect.min_confidence ?? 0.6;
153
+ const passed = similarity >= threshold;
154
+ results.push({
155
+ assertion: 'semantically_similar',
156
+ passed,
157
+ message: passed ? `Similarity ${similarity.toFixed(2)} ≥ ${threshold}` : `Similarity ${similarity.toFixed(2)} < ${threshold}`,
158
+ expected: `≥ ${threshold} similarity to "${expect.semantically_similar}"`,
159
+ actual: similarity.toFixed(2),
160
+ });
161
+ }
162
+ return results;
163
+ }
164
+ // Async version that supports llm_judge assertion
165
+ async function evaluateAssertionsAsync(output, expect, input, metadata) {
166
+ const results = evaluateAssertions(output, expect, metadata);
167
+ if (expect.llm_judge !== undefined) {
168
+ try {
169
+ const judgeResult = await (0, llm_judge_1.llmJudge)(input, output, expect.llm_judge, globalLLMJudgeConfig);
170
+ results.push({
171
+ assertion: 'llm_judge',
172
+ passed: judgeResult.passed,
173
+ message: judgeResult.passed
174
+ ? `LLM judge passed (score: ${judgeResult.score.toFixed(2)}): ${judgeResult.reasoning}`
175
+ : `LLM judge failed (score: ${judgeResult.score.toFixed(2)}): ${judgeResult.reasoning}`,
176
+ expected: expect.llm_judge,
177
+ actual: `score: ${judgeResult.score.toFixed(2)}`,
178
+ });
179
+ }
180
+ catch (e) {
181
+ results.push({
182
+ assertion: 'llm_judge',
183
+ passed: false,
184
+ message: `LLM judge error: ${e instanceof Error ? e.message : String(e)}`,
185
+ expected: expect.llm_judge,
186
+ actual: 'error',
187
+ });
188
+ }
189
+ }
190
+ return results;
191
+ }
192
+ function extractJsonPath(obj, path) {
193
+ const parts = path.split('.');
194
+ let current = obj;
195
+ for (const part of parts) {
196
+ if (current === null || current === undefined)
197
+ return undefined;
198
+ if (typeof current !== 'object')
199
+ return undefined;
200
+ current = current[part];
201
+ }
202
+ return current;
203
+ }
204
+ function wordSimilarity(a, b) {
205
+ const wordsA = new Set(a.toLowerCase().split(/\s+/).filter(w => w.length > 2));
206
+ const wordsB = new Set(b.toLowerCase().split(/\s+/).filter(w => w.length > 2));
207
+ if (wordsA.size === 0 || wordsB.size === 0)
208
+ return 0;
209
+ let intersection = 0;
210
+ for (const w of Array.from(wordsA)) {
211
+ if (wordsB.has(w))
212
+ intersection++;
213
+ }
214
+ return (2 * intersection) / (wordsA.size + wordsB.size);
215
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const runner_1 = require("./runner");
38
+ const loader_1 = require("./loader");
39
+ const reporter_1 = require("./reporter");
40
+ const diff_1 = require("./diff");
41
+ const path = __importStar(require("path"));
42
+ async function main() {
43
+ const args = process.argv.slice(2);
44
+ const command = args[0];
45
+ if (command === 'run') {
46
+ let dir = 'tests';
47
+ let ci = false;
48
+ let json = false;
49
+ let junit = false;
50
+ let watch = false;
51
+ let testFilter = null;
52
+ let judgeEndpoint = null;
53
+ let judgeModel = null;
54
+ let agentEndpoint = null;
55
+ for (let i = 1; i < args.length; i++) {
56
+ const arg = args[i];
57
+ if (arg === '--dir')
58
+ dir = args[++i];
59
+ else if (arg === '--ci')
60
+ ci = true;
61
+ else if (arg === '--json')
62
+ json = true;
63
+ else if (arg === '--junit')
64
+ junit = true;
65
+ else if (arg === '--watch')
66
+ watch = true;
67
+ else if (arg === '--test')
68
+ testFilter = args[++i];
69
+ else if (arg === '--judge-endpoint')
70
+ judgeEndpoint = args[++i];
71
+ else if (arg === '--judge-model')
72
+ judgeModel = args[++i];
73
+ else if (arg === '--endpoint')
74
+ agentEndpoint = args[++i];
75
+ else if (arg === '--help' || arg === '-h') {
76
+ printHelp();
77
+ return;
78
+ }
79
+ }
80
+ // Use HTTP agent if endpoint specified, otherwise mock
81
+ const isSelfTest = dir === 'tests';
82
+ const agent = agentEndpoint
83
+ ? new (require('./http-agent').HTTPAgent)({ endpoint: agentEndpoint })
84
+ : isSelfTest
85
+ ? new runner_1.MockAgent({}, true)
86
+ : new runner_1.MockAgent({
87
+ default: "I'll help you with that. Let me check the system for you.",
88
+ "my token expired, what do I do?": "You need to refresh the token. Go to Settings > Security to get a new refresh token.",
89
+ "I want a refund": "I'll transfer you to the billing team. Let me connect you with our support team.",
90
+ "what can you do?": "I can help with auth, billing, and general support questions.",
91
+ "xyzzy random input 12345": "I'm not sure about that, but I can help you find what you need.",
92
+ });
93
+ // Configure LLM judge if specified (must be before running tests)
94
+ if (judgeEndpoint || judgeModel) {
95
+ const { setLLMJudgeConfig } = await Promise.resolve().then(() => __importStar(require('./assertions')));
96
+ setLLMJudgeConfig({
97
+ endpoint: judgeEndpoint || undefined,
98
+ model: judgeModel || undefined,
99
+ });
100
+ }
101
+ const result = await (0, runner_1.runAll)(dir, agent, testFilter);
102
+ // Compute behavior diffs
103
+ const diffs = (0, diff_1.computeDiffs)(result.results);
104
+ // Save current run for future diffs
105
+ (0, diff_1.saveRun)(result.results);
106
+ if (json) {
107
+ console.log((0, reporter_1.formatResultsJSON)(result));
108
+ }
109
+ else if (junit) {
110
+ console.log((0, reporter_1.formatResultsJUnit)(result));
111
+ }
112
+ else {
113
+ console.log((0, reporter_1.formatResults)(result));
114
+ // Show diff report if there are changes
115
+ if (diffs.length > 0) {
116
+ console.log((0, diff_1.formatDiffs)(diffs));
117
+ }
118
+ }
119
+ if (ci) {
120
+ process.exit(result.failed > 0 ? 1 : 0);
121
+ }
122
+ }
123
+ else if (command === 'init') {
124
+ console.log('Creating agentspec.yaml...');
125
+ const { writeFileSync } = await Promise.resolve().then(() => __importStar(require('fs')));
126
+ writeFileSync(path.join(process.cwd(), 'agentspec.yaml'), `name: "my-agent-tests"
127
+ description: "Tests for my AI agent"
128
+ tests:
129
+ - name: "handles greeting"
130
+ input: "hello"
131
+ expect:
132
+ contains: "help"
133
+ `);
134
+ console.log('Created agentspec.yaml');
135
+ }
136
+ else if (command === 'list') {
137
+ let dir = 'tests';
138
+ if (args[1] === '--dir')
139
+ dir = args[2];
140
+ const suites = (0, loader_1.findTestSuites)(dir);
141
+ if (suites.length === 0) {
142
+ console.log('No test suites found in', dir);
143
+ return;
144
+ }
145
+ console.log(`Found ${suites.length} test suite(s):`);
146
+ for (const s of suites) {
147
+ console.log(` ${s}`);
148
+ }
149
+ }
150
+ else if (command === 'version' || command === '--version' || command === '-v') {
151
+ console.log('agentspec v0.1.0');
152
+ }
153
+ else {
154
+ printHelp();
155
+ }
156
+ }
157
+ function printHelp() {
158
+ console.log(`
159
+ AgentSpec — Testing framework for AI agents
160
+
161
+ Usage:
162
+ agentspec run [options] Run tests
163
+ agentspec init Create agentspec.yaml
164
+ agentspec list List test suites
165
+ agentspec version Show version
166
+
167
+ Options for run:
168
+ --dir <path> Test directory (default: tests)
169
+ --ci Exit with code 1 on failure
170
+ --json Output results as JSON
171
+ --junit Output JUnit XML
172
+ --watch Watch for changes and re-run
173
+ --test <pattern> Run only matching tests
174
+ --judge-endpoint <url> LLM judge endpoint (OpenAI-compatible, default: Ollama)
175
+ --judge-model <name> LLM judge model name (default: local small model)
176
+ `);
177
+ }
178
+ main().catch(err => {
179
+ console.error('Error:', err.message);
180
+ process.exit(1);
181
+ });
package/dist/diff.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { TestResult } from './types';
2
+ import { StoredRun, DiffEntry } from './types/diff';
3
+ export declare function ensureStoreDir(): string;
4
+ export declare function saveRun(results: TestResult[]): void;
5
+ export declare function loadPreviousRun(suite: string, test: string): StoredRun | null;
6
+ export declare function computeDiffs(currentResults: TestResult[]): DiffEntry[];
7
+ export declare function formatDiffs(diffs: DiffEntry[]): string;