@testsmith/api-spector 0.2.0 → 0.2.1
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/bin/cli.js +12 -2
- package/out/main/agents.js +212 -0
- package/out/main/chunks/{request-handler-AFBOd__c.js → request-collection-Dx0ZqB54.js} +72 -2
- package/out/main/index.js +918 -178
- package/out/main/runner.js +132 -89
- package/out/renderer/assets/{index-CnjhlKQP.js → index-DPHZdvL5.js} +13002 -840
- package/out/renderer/assets/{index-FZtc_UAN.css → index-DVaubmCJ.css} +1 -1
- package/out/renderer/index.html +3 -3
- package/package.json +5 -3
- package/readme.md +1 -1
- package/resources/splash.html +21 -9
package/bin/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ const [, , cmd = 'ui', ...rest] = process.argv
|
|
|
8
8
|
|
|
9
9
|
function printHelp() {
|
|
10
10
|
console.log('')
|
|
11
|
-
console.log('
|
|
11
|
+
console.log(' API Spector — local-first API testing tool')
|
|
12
12
|
console.log('')
|
|
13
13
|
console.log(' Usage:')
|
|
14
14
|
console.log(' api-spector ui Launch the app')
|
|
@@ -17,6 +17,9 @@ function printHelp() {
|
|
|
17
17
|
console.log(' api-spector record --upstream <url> Record API traffic as mock stubs')
|
|
18
18
|
console.log('')
|
|
19
19
|
console.log(' Options:')
|
|
20
|
+
console.log(' api-spector agents init <name> Initialize AI agent files')
|
|
21
|
+
console.log(' api-spector agents list Show available agents')
|
|
22
|
+
console.log('')
|
|
20
23
|
console.log(' api-spector run --help Show run options')
|
|
21
24
|
console.log(' api-spector mock --help Show mock options')
|
|
22
25
|
console.log(' api-spector record --help Show record options')
|
|
@@ -59,8 +62,15 @@ if (cmd === '--help' || cmd === '-h') {
|
|
|
59
62
|
env: process.env,
|
|
60
63
|
})
|
|
61
64
|
proc.on('close', code => process.exit(code ?? 0))
|
|
65
|
+
} else if (cmd === 'agents') {
|
|
66
|
+
const agentsPath = path.join(__dirname, '..', 'out', 'main', 'agents.js')
|
|
67
|
+
const proc = spawn(process.execPath, [agentsPath, ...rest], {
|
|
68
|
+
stdio: 'inherit',
|
|
69
|
+
env: process.env,
|
|
70
|
+
})
|
|
71
|
+
proc.on('close', code => process.exit(code ?? 0))
|
|
62
72
|
} else {
|
|
63
|
-
console.error(`
|
|
73
|
+
console.error(`API Spector — unknown command: "${cmd}"`)
|
|
64
74
|
printHelp()
|
|
65
75
|
process.exit(1)
|
|
66
76
|
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
const promises = require("fs/promises");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const C = {
|
|
6
|
+
reset: "\x1B[0m",
|
|
7
|
+
bold: "\x1B[1m",
|
|
8
|
+
green: "\x1B[32m",
|
|
9
|
+
cyan: "\x1B[36m",
|
|
10
|
+
yellow: "\x1B[33m",
|
|
11
|
+
gray: "\x1B[90m",
|
|
12
|
+
red: "\x1B[31m"
|
|
13
|
+
};
|
|
14
|
+
function color(text, ...codes) {
|
|
15
|
+
return codes.join("") + text + C.reset;
|
|
16
|
+
}
|
|
17
|
+
const AGENTS = {
|
|
18
|
+
claude: {
|
|
19
|
+
name: "Claude Code",
|
|
20
|
+
description: "Skills for Claude Code (.claude/skills/)",
|
|
21
|
+
files: [
|
|
22
|
+
{ src: "claude/skills/api-spector-functional-tests.md", dest: ".claude/skills/api-spector-functional-tests/SKILL.md" },
|
|
23
|
+
{ src: "claude/skills/api-spector-security-tests.md", dest: ".claude/skills/api-spector-security-tests/SKILL.md" },
|
|
24
|
+
{ src: "claude/skills/api-spector-generate-mocks.md", dest: ".claude/skills/api-spector-generate-mocks/SKILL.md" },
|
|
25
|
+
{ src: "claude/skills/api-spector-api-audit.md", dest: ".claude/skills/api-spector-api-audit/SKILL.md" }
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
copilot: {
|
|
29
|
+
name: "GitHub Copilot",
|
|
30
|
+
description: "Instructions for Copilot (.github/copilot-instructions.md)",
|
|
31
|
+
files: [
|
|
32
|
+
{ src: "copilot/copilot-instructions.md", dest: ".github/copilot-instructions.md" }
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
cursor: {
|
|
36
|
+
name: "Cursor",
|
|
37
|
+
description: "Rules for Cursor (.cursor/rules/api-spector.mdc)",
|
|
38
|
+
files: [
|
|
39
|
+
{ src: "cursor/rules/api-spector.mdc", dest: ".cursor/rules/api-spector.mdc" }
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
windsurf: {
|
|
43
|
+
name: "Windsurf",
|
|
44
|
+
description: "Rules for Windsurf (.windsurfrules)",
|
|
45
|
+
files: [
|
|
46
|
+
{ src: "windsurf/windsurfrules", dest: ".windsurfrules" }
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
aider: {
|
|
50
|
+
name: "Aider",
|
|
51
|
+
description: "Conventions for Aider (conventions.md)",
|
|
52
|
+
files: [
|
|
53
|
+
{ src: "aider/conventions.md", dest: "conventions.md" }
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
function sharedDocsForAgent(agentName) {
|
|
58
|
+
const destDir = {
|
|
59
|
+
claude: ".claude/docs",
|
|
60
|
+
copilot: ".github/docs",
|
|
61
|
+
cursor: ".cursor/docs",
|
|
62
|
+
windsurf: ".windsurf/docs",
|
|
63
|
+
aider: ".aider/docs"
|
|
64
|
+
};
|
|
65
|
+
const dir = destDir[agentName] ?? ".api-spector/docs";
|
|
66
|
+
return [
|
|
67
|
+
{ src: "api-spector-scripting-reference.md", dest: `${dir}/api-spector-scripting-reference.md` },
|
|
68
|
+
{ src: "collection-file-format.md", dest: `${dir}/collection-file-format.md` },
|
|
69
|
+
{ src: "functional-testing-guide.md", dest: `${dir}/functional-testing-guide.md` },
|
|
70
|
+
{ src: "security-testing-guide.md", dest: `${dir}/security-testing-guide.md` }
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
function getPackageRoot() {
|
|
74
|
+
let dir = __dirname;
|
|
75
|
+
for (let i = 0; i < 5; i++) {
|
|
76
|
+
try {
|
|
77
|
+
require.resolve(path.join(dir, "package.json"));
|
|
78
|
+
return dir;
|
|
79
|
+
} catch {
|
|
80
|
+
dir = path.dirname(dir);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return path.join(__dirname, "..", "..");
|
|
84
|
+
}
|
|
85
|
+
function getTemplatesDir() {
|
|
86
|
+
return path.join(getPackageRoot(), "src", "cli", "agent-templates");
|
|
87
|
+
}
|
|
88
|
+
function getDocsDir() {
|
|
89
|
+
return path.join(getPackageRoot(), "docs", "ai");
|
|
90
|
+
}
|
|
91
|
+
async function fileExists(path2) {
|
|
92
|
+
try {
|
|
93
|
+
await promises.stat(path2);
|
|
94
|
+
return true;
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function copyFile(src, dest, cwd) {
|
|
100
|
+
const destPath = path.join(cwd, dest);
|
|
101
|
+
const existed = await fileExists(destPath);
|
|
102
|
+
await promises.mkdir(path.dirname(destPath), { recursive: true });
|
|
103
|
+
const content = await promises.readFile(src, "utf8");
|
|
104
|
+
if (existed) {
|
|
105
|
+
const existing = await promises.readFile(destPath, "utf8");
|
|
106
|
+
if (existing === content) return "exists";
|
|
107
|
+
}
|
|
108
|
+
await promises.writeFile(destPath, content, "utf8");
|
|
109
|
+
return existed ? "updated" : "created";
|
|
110
|
+
}
|
|
111
|
+
async function initAgent(agentName, cwd) {
|
|
112
|
+
const names = agentName === "all" ? Object.keys(AGENTS) : [agentName];
|
|
113
|
+
for (const name of names) {
|
|
114
|
+
const agent = AGENTS[name];
|
|
115
|
+
if (!agent) {
|
|
116
|
+
console.error(color(` Unknown agent: "${name}"`, C.red));
|
|
117
|
+
console.error(` Available: ${Object.keys(AGENTS).join(", ")}, all`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
console.log(color(`
|
|
121
|
+
${agent.name}`, C.bold, C.cyan));
|
|
122
|
+
const templatesDir = getTemplatesDir();
|
|
123
|
+
for (const file of agent.files) {
|
|
124
|
+
const srcPath = path.join(templatesDir, file.src);
|
|
125
|
+
if (!await fileExists(srcPath)) {
|
|
126
|
+
console.log(color(` skip ${file.dest} (template not found)`, C.yellow));
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const result = await copyFile(srcPath, file.dest, cwd);
|
|
130
|
+
const icon = result === "created" ? color("+", C.green) : result === "updated" ? color("~", C.yellow) : color("=", C.gray);
|
|
131
|
+
const label = result === "exists" ? "unchanged" : result;
|
|
132
|
+
console.log(` ${icon} ${file.dest} ${color(`(${label})`, C.gray)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
console.log(color(`
|
|
136
|
+
Shared documentation`, C.bold, C.cyan));
|
|
137
|
+
const docsDir = getDocsDir();
|
|
138
|
+
const allDocDests = /* @__PURE__ */ new Set();
|
|
139
|
+
for (const name of names) {
|
|
140
|
+
for (const doc of sharedDocsForAgent(name)) {
|
|
141
|
+
if (allDocDests.has(doc.dest)) continue;
|
|
142
|
+
allDocDests.add(doc.dest);
|
|
143
|
+
const srcPath = path.join(docsDir, doc.src);
|
|
144
|
+
if (!await fileExists(srcPath)) {
|
|
145
|
+
console.log(color(` skip ${doc.dest} (not found)`, C.yellow));
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const result = await copyFile(srcPath, doc.dest, cwd);
|
|
149
|
+
const icon = result === "created" ? color("+", C.green) : result === "updated" ? color("~", C.yellow) : color("=", C.gray);
|
|
150
|
+
const label = result === "exists" ? "unchanged" : result;
|
|
151
|
+
console.log(` ${icon} ${doc.dest} ${color(`(${label})`, C.gray)}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
console.log(color("\n Done. Your AI agent can now generate API Spector tests.\n", C.green));
|
|
155
|
+
}
|
|
156
|
+
function listAgents() {
|
|
157
|
+
console.log(color("\n Available agents:\n", C.bold));
|
|
158
|
+
for (const [key, agent] of Object.entries(AGENTS)) {
|
|
159
|
+
console.log(` ${color(key.padEnd(12), C.cyan)} ${agent.description}`);
|
|
160
|
+
}
|
|
161
|
+
console.log(` ${color("all".padEnd(12), C.cyan)} Initialize all agents at once`);
|
|
162
|
+
console.log(color("\n Usage: api-spector agents init <name>\n", C.gray));
|
|
163
|
+
}
|
|
164
|
+
function printHelp() {
|
|
165
|
+
console.log(`
|
|
166
|
+
${color("api-spector agents", C.bold)} — manage AI agent configurations
|
|
167
|
+
|
|
168
|
+
${color("Commands:", C.bold)}
|
|
169
|
+
agents init <name> Scaffold agent instruction files in the current directory
|
|
170
|
+
agents list Show available agents
|
|
171
|
+
agents --help Show this message
|
|
172
|
+
|
|
173
|
+
${color("Examples:", C.bold)}
|
|
174
|
+
api-spector agents init claude Set up Claude Code skills
|
|
175
|
+
api-spector agents init copilot Set up GitHub Copilot instructions
|
|
176
|
+
api-spector agents init all Set up all agents at once
|
|
177
|
+
|
|
178
|
+
${color("What this does:", C.gray)}
|
|
179
|
+
Copies AI instruction files into your project so your LLM coding tool
|
|
180
|
+
understands the API Spector scripting API and can generate functional
|
|
181
|
+
and security test plans.
|
|
182
|
+
`);
|
|
183
|
+
}
|
|
184
|
+
async function main() {
|
|
185
|
+
const args = process.argv.slice(2);
|
|
186
|
+
const subCmd = args[0];
|
|
187
|
+
if (!subCmd || subCmd === "--help" || subCmd === "-h") {
|
|
188
|
+
printHelp();
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
|
191
|
+
if (subCmd === "list") {
|
|
192
|
+
listAgents();
|
|
193
|
+
process.exit(0);
|
|
194
|
+
}
|
|
195
|
+
if (subCmd === "init") {
|
|
196
|
+
const agentName = args[1]?.toLowerCase();
|
|
197
|
+
if (!agentName) {
|
|
198
|
+
console.error(color(" Missing agent name. Use: api-spector agents init <name>", C.red));
|
|
199
|
+
console.error(` Available: ${Object.keys(AGENTS).join(", ")}, all`);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
await initAgent(agentName, process.cwd());
|
|
203
|
+
process.exit(0);
|
|
204
|
+
}
|
|
205
|
+
console.error(color(` Unknown sub-command: "${subCmd}"`, C.red));
|
|
206
|
+
printHelp();
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
main().catch((err) => {
|
|
210
|
+
console.error(color(` Error: ${err.message}`, C.red));
|
|
211
|
+
process.exit(2);
|
|
212
|
+
});
|
|
@@ -192,7 +192,7 @@ function buildUrl(baseUrl, params, vars) {
|
|
|
192
192
|
templateTokens.add(String(name).trim());
|
|
193
193
|
return "";
|
|
194
194
|
});
|
|
195
|
-
const enabled = params.filter((p) => p.enabled && p.key);
|
|
195
|
+
const enabled = (params ?? []).filter((p) => p.enabled && p.key);
|
|
196
196
|
const pathRows = [];
|
|
197
197
|
const queryRows = [];
|
|
198
198
|
for (const p of enabled) {
|
|
@@ -965,6 +965,10 @@ function registerRequestHandler(ipc) {
|
|
|
965
965
|
tls,
|
|
966
966
|
piiMaskPatterns = []
|
|
967
967
|
} = payload;
|
|
968
|
+
if (!req.headers) req.headers = [];
|
|
969
|
+
if (!req.params) req.params = [];
|
|
970
|
+
if (!req.body) req.body = { mode: "none" };
|
|
971
|
+
if (!req.auth) req.auth = { type: "none" };
|
|
968
972
|
const start = Date.now();
|
|
969
973
|
const liveGlobals = getGlobals();
|
|
970
974
|
const mergedGlobals = { ...payloadGlobals, ...liveGlobals };
|
|
@@ -1190,7 +1194,7 @@ function registerRequestHandler(ipc) {
|
|
|
1190
1194
|
await persistGlobals();
|
|
1191
1195
|
}
|
|
1192
1196
|
const combinedTestResults = [...schemaTestResults, ...postTestResults];
|
|
1193
|
-
if (!response.error && response.status >= 400 &&
|
|
1197
|
+
if (!response.error && response.status >= 400 && combinedTestResults.length === 0) {
|
|
1194
1198
|
combinedTestResults.push({
|
|
1195
1199
|
name: `HTTP status ${response.status} ${response.statusText}`.trim(),
|
|
1196
1200
|
passed: false,
|
|
@@ -1224,13 +1228,78 @@ function registerRequestHandler(ipc) {
|
|
|
1224
1228
|
};
|
|
1225
1229
|
});
|
|
1226
1230
|
}
|
|
1231
|
+
function collectTagged(folder, requests, collectionVars, filterTags, parentPath = [], isRoot = true) {
|
|
1232
|
+
const results = [];
|
|
1233
|
+
const scopePath = isRoot ? parentPath : [...parentPath, folder.name];
|
|
1234
|
+
for (const reqId of folder.requestIds) {
|
|
1235
|
+
const req = requests[reqId];
|
|
1236
|
+
if (!req || req.hookType || req.disabled) continue;
|
|
1237
|
+
const tags = req.meta?.tags ?? [];
|
|
1238
|
+
if (filterTags.length > 0 && !filterTags.some((t) => tags.includes(t))) continue;
|
|
1239
|
+
results.push({ request: req, collectionVars, scopePath });
|
|
1240
|
+
}
|
|
1241
|
+
for (const sub of folder.folders) {
|
|
1242
|
+
const folderTags = sub.tags ?? [];
|
|
1243
|
+
const effectiveTags = filterTags.length === 0 ? filterTags : folderTags.some((t) => filterTags.includes(t)) ? [] : filterTags;
|
|
1244
|
+
results.push(...collectTagged(sub, requests, collectionVars, effectiveTags, scopePath, false));
|
|
1245
|
+
}
|
|
1246
|
+
return results;
|
|
1247
|
+
}
|
|
1248
|
+
function folderPathTo(root, requestId) {
|
|
1249
|
+
if (root.requestIds.includes(requestId)) return [root];
|
|
1250
|
+
for (const sub of root.folders) {
|
|
1251
|
+
const path2 = folderPathTo(sub, requestId);
|
|
1252
|
+
if (path2.length > 0) return [root, ...path2];
|
|
1253
|
+
}
|
|
1254
|
+
return [];
|
|
1255
|
+
}
|
|
1256
|
+
function getAllApplicableHooks(folderId, collection) {
|
|
1257
|
+
function chainToFolder(root, targetId) {
|
|
1258
|
+
if (root.id === targetId) return [root];
|
|
1259
|
+
for (const sub of root.folders) {
|
|
1260
|
+
const chain2 = chainToFolder(sub, targetId);
|
|
1261
|
+
if (chain2.length) return [root, ...chain2];
|
|
1262
|
+
}
|
|
1263
|
+
return [];
|
|
1264
|
+
}
|
|
1265
|
+
const chain = chainToFolder(collection.rootFolder, folderId);
|
|
1266
|
+
const beforeAll = [];
|
|
1267
|
+
const before = [];
|
|
1268
|
+
const after = [];
|
|
1269
|
+
const afterAll = [];
|
|
1270
|
+
for (const folder of chain) {
|
|
1271
|
+
const reqs = folder.requestIds.map((id) => collection.requests[id]).filter((r) => r && !r.disabled);
|
|
1272
|
+
beforeAll.push(...reqs.filter((r) => r.hookType === "beforeAll"));
|
|
1273
|
+
before.push(...reqs.filter((r) => r.hookType === "before"));
|
|
1274
|
+
}
|
|
1275
|
+
for (const folder of [...chain].reverse()) {
|
|
1276
|
+
const reqs = folder.requestIds.map((id) => collection.requests[id]).filter((r) => r && !r.disabled);
|
|
1277
|
+
after.push(...reqs.filter((r) => r.hookType === "after"));
|
|
1278
|
+
afterAll.push(...reqs.filter((r) => r.hookType === "afterAll"));
|
|
1279
|
+
}
|
|
1280
|
+
return { beforeAll, before, after, afterAll };
|
|
1281
|
+
}
|
|
1282
|
+
function resolveInheritedAuthAndHeaders(requestId, collection) {
|
|
1283
|
+
let inheritedAuth = collection.auth && collection.auth.type !== "none" ? collection.auth : null;
|
|
1284
|
+
let inheritedHeaders = collection.headers?.filter((h) => h.enabled && h.key) ?? [];
|
|
1285
|
+
const path2 = folderPathTo(collection.rootFolder, requestId);
|
|
1286
|
+
for (const folder of path2) {
|
|
1287
|
+
if (folder.auth && folder.auth.type !== "none") inheritedAuth = folder.auth;
|
|
1288
|
+
if (folder.headers?.length) {
|
|
1289
|
+
inheritedHeaders = [...inheritedHeaders, ...folder.headers.filter((h) => h.enabled && h.key)];
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return { auth: inheritedAuth, headers: inheritedHeaders };
|
|
1293
|
+
}
|
|
1227
1294
|
exports.buildAuthHeaders = buildAuthHeaders;
|
|
1228
1295
|
exports.buildDispatcher = buildDispatcher;
|
|
1229
1296
|
exports.buildDynamicVars = buildDynamicVars;
|
|
1230
1297
|
exports.buildEnvVars = buildEnvVars;
|
|
1231
1298
|
exports.buildSchemaTestResults = buildSchemaTestResults;
|
|
1232
1299
|
exports.buildUrl = buildUrl;
|
|
1300
|
+
exports.collectTagged = collectTagged;
|
|
1233
1301
|
exports.fetchOAuth2Token = fetchOAuth2Token;
|
|
1302
|
+
exports.getAllApplicableHooks = getAllApplicableHooks;
|
|
1234
1303
|
exports.getGlobals = getGlobals;
|
|
1235
1304
|
exports.getSecret = getSecret;
|
|
1236
1305
|
exports.initSecretStore = initSecretStore;
|
|
@@ -1245,5 +1314,6 @@ exports.performNtlmRequest = performNtlmRequest;
|
|
|
1245
1314
|
exports.persistGlobals = persistGlobals;
|
|
1246
1315
|
exports.registerRequestHandler = registerRequestHandler;
|
|
1247
1316
|
exports.registerSecretHandlers = registerSecretHandlers;
|
|
1317
|
+
exports.resolveInheritedAuthAndHeaders = resolveInheritedAuthAndHeaders;
|
|
1248
1318
|
exports.runScript = runScript;
|
|
1249
1319
|
exports.setGlobals = setGlobals;
|