@assrt-ai/assrt 0.3.3
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/chunk-PZ5LLIRQ.mjs +2256 -0
- package/cli.mjs +279 -0
- package/index.mjs +20 -0
- package/mcp/server.mjs +508 -0
- package/package.json +49 -0
package/cli.mjs
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
TestAgent,
|
|
4
|
+
getCredential,
|
|
5
|
+
shutdownTelemetry,
|
|
6
|
+
trackEvent
|
|
7
|
+
} from "./chunk-PZ5LLIRQ.mjs";
|
|
8
|
+
|
|
9
|
+
// src/cli.ts
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
11
|
+
import { execSync } from "child_process";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
function printUsage() {
|
|
14
|
+
console.error(
|
|
15
|
+
"Usage:\n assrt setup Set up MCP server, hooks, and CLAUDE.md\n assrt run --url <url> [options] Run QA tests\n\nRun options:\n --url URL to test (required)\n --plan Test scenarios as inline text\n --plan-file Path to a file containing test scenarios\n --model LLM model to use (default: claude-haiku-4-5-20251001)\n --json Output raw JSON report to stdout\n --help Show this help message\n\nAuth: Uses ANTHROPIC_API_KEY env var, or reads Claude Code credentials from macOS Keychain."
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
function parseArgs(argv) {
|
|
19
|
+
const args = {};
|
|
20
|
+
const command = argv[0] || "";
|
|
21
|
+
for (let i = 1; i < argv.length; i++) {
|
|
22
|
+
const arg = argv[i];
|
|
23
|
+
if (arg === "--help" || arg === "-h") {
|
|
24
|
+
printUsage();
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
if (arg === "--json") {
|
|
28
|
+
args.json = true;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (arg.startsWith("--") && i + 1 < argv.length) {
|
|
32
|
+
args[arg.slice(2)] = argv[++i];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
command,
|
|
37
|
+
url: args.url || "",
|
|
38
|
+
plan: args.plan,
|
|
39
|
+
planFile: args["plan-file"],
|
|
40
|
+
model: args.model,
|
|
41
|
+
json: !!args.json
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function readStdin() {
|
|
45
|
+
if (process.stdin.isTTY) return "";
|
|
46
|
+
const chunks = [];
|
|
47
|
+
for await (const chunk of process.stdin) {
|
|
48
|
+
chunks.push(chunk);
|
|
49
|
+
}
|
|
50
|
+
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
51
|
+
}
|
|
52
|
+
function createCliEmit(jsonMode) {
|
|
53
|
+
return (type, data) => {
|
|
54
|
+
if (jsonMode) return;
|
|
55
|
+
switch (type) {
|
|
56
|
+
case "status":
|
|
57
|
+
console.error(`[status] ${data.message}`);
|
|
58
|
+
break;
|
|
59
|
+
case "reasoning":
|
|
60
|
+
console.error(`[think] ${data.text}`);
|
|
61
|
+
break;
|
|
62
|
+
case "step":
|
|
63
|
+
if (data.status !== "running") {
|
|
64
|
+
const icon = data.status === "completed" ? "+" : "x";
|
|
65
|
+
console.error(` [${icon}] ${data.description}`);
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
case "assertion": {
|
|
69
|
+
const icon = data.passed ? "PASS" : "FAIL";
|
|
70
|
+
console.error(` [${icon}] ${data.description}`);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case "scenario_start":
|
|
74
|
+
console.error(`
|
|
75
|
+
--- Scenario: ${data.name} (${data.index + 1}/${data.total}) ---`);
|
|
76
|
+
break;
|
|
77
|
+
case "scenario_complete": {
|
|
78
|
+
const result = data.passed ? "PASSED" : "FAILED";
|
|
79
|
+
console.error(`--- ${result}: ${data.name} ---`);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case "improvement_suggestion":
|
|
83
|
+
console.error(` [issue] ${data.severity}: ${data.title}`);
|
|
84
|
+
break;
|
|
85
|
+
case "screenshot":
|
|
86
|
+
case "page_discovered":
|
|
87
|
+
case "discovered_cases_chunk":
|
|
88
|
+
case "discovered_cases_complete":
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function printReport(report) {
|
|
94
|
+
console.log("\n========================================");
|
|
95
|
+
console.log(` Assrt Test Report`);
|
|
96
|
+
console.log("========================================");
|
|
97
|
+
console.log(` URL: ${report.url}`);
|
|
98
|
+
console.log(` Passed: ${report.passedCount}`);
|
|
99
|
+
console.log(` Failed: ${report.failedCount}`);
|
|
100
|
+
console.log(` Duration: ${(report.totalDuration / 1e3).toFixed(1)}s`);
|
|
101
|
+
console.log("========================================\n");
|
|
102
|
+
for (const scenario of report.scenarios) {
|
|
103
|
+
const icon = scenario.passed ? "PASS" : "FAIL";
|
|
104
|
+
console.log(` [${icon}] ${scenario.name}`);
|
|
105
|
+
if (scenario.summary) {
|
|
106
|
+
console.log(` ${scenario.summary}`);
|
|
107
|
+
}
|
|
108
|
+
for (const assertion of scenario.assertions) {
|
|
109
|
+
const aIcon = assertion.passed ? "+" : "x";
|
|
110
|
+
console.log(` [${aIcon}] ${assertion.description}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
console.log("");
|
|
114
|
+
}
|
|
115
|
+
var POST_COMMIT_HOOK = `#!/bin/bash
|
|
116
|
+
# Assrt: suggest QA testing after git commit/push
|
|
117
|
+
INPUT=$(cat)
|
|
118
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
119
|
+
if echo "$COMMAND" | grep -qE 'git (commit|push)'; then
|
|
120
|
+
echo '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"A git commit/push was just made. If the committed changes affect anything user-facing (UI, routes, forms, APIs), run assrt_test against the local dev server to verify the changes work in a real browser. Use assrt_plan first if you need test cases."}}'
|
|
121
|
+
fi
|
|
122
|
+
`;
|
|
123
|
+
function setupAssrt() {
|
|
124
|
+
const cwd = process.env.INIT_CWD || process.cwd();
|
|
125
|
+
console.error("[assrt] Setting up Assrt in this project...\n");
|
|
126
|
+
console.error("[1/3] Registering MCP server...");
|
|
127
|
+
try {
|
|
128
|
+
execSync("which claude", { stdio: ["pipe", "pipe", "pipe"], timeout: 5e3 });
|
|
129
|
+
try {
|
|
130
|
+
const mcpConfig = JSON.stringify({
|
|
131
|
+
type: "stdio",
|
|
132
|
+
command: "npx",
|
|
133
|
+
args: ["-y", "-p", "@m13v/assrt", "assrt-mcp"]
|
|
134
|
+
});
|
|
135
|
+
execSync(`claude mcp add-json assrt '${mcpConfig}'`, {
|
|
136
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
137
|
+
timeout: 15e3
|
|
138
|
+
});
|
|
139
|
+
console.error(" Done: MCP server registered\n");
|
|
140
|
+
} catch {
|
|
141
|
+
console.error(" Skipped: MCP server already registered\n");
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
console.error(" Skipped: 'claude' CLI not found in PATH\n");
|
|
145
|
+
}
|
|
146
|
+
console.error("[2/3] Installing post-commit hook...");
|
|
147
|
+
const hookDir = join(cwd, ".claude", "hooks");
|
|
148
|
+
const hookPath = join(hookDir, "assrt-post-commit.sh");
|
|
149
|
+
if (!existsSync(hookDir)) mkdirSync(hookDir, { recursive: true });
|
|
150
|
+
writeFileSync(hookPath, POST_COMMIT_HOOK, { mode: 493 });
|
|
151
|
+
const settingsDir = join(cwd, ".claude");
|
|
152
|
+
const settingsPath = join(settingsDir, "settings.json");
|
|
153
|
+
let settings = {};
|
|
154
|
+
if (existsSync(settingsPath)) {
|
|
155
|
+
try {
|
|
156
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
157
|
+
} catch {
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const hooks = settings.hooks || {};
|
|
161
|
+
const postToolUse = hooks.PostToolUse || [];
|
|
162
|
+
const alreadyInstalled = postToolUse.some(
|
|
163
|
+
(h) => JSON.stringify(h).includes("assrt-post-commit")
|
|
164
|
+
);
|
|
165
|
+
if (!alreadyInstalled) {
|
|
166
|
+
postToolUse.push({
|
|
167
|
+
matcher: "Bash",
|
|
168
|
+
hooks: [{ type: "command", command: hookPath }]
|
|
169
|
+
});
|
|
170
|
+
hooks.PostToolUse = postToolUse;
|
|
171
|
+
settings.hooks = hooks;
|
|
172
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
173
|
+
console.error(` Done: hook installed at ${hookPath}
|
|
174
|
+
`);
|
|
175
|
+
} else {
|
|
176
|
+
console.error(" Skipped: hook already installed\n");
|
|
177
|
+
}
|
|
178
|
+
console.error("[3/3] Updating CLAUDE.md...");
|
|
179
|
+
const claudeMdPath = join(cwd, "CLAUDE.md");
|
|
180
|
+
let claudeMd = "";
|
|
181
|
+
if (existsSync(claudeMdPath)) {
|
|
182
|
+
claudeMd = readFileSync(claudeMdPath, "utf-8");
|
|
183
|
+
}
|
|
184
|
+
if (!claudeMd.includes("assrt_test") && !claudeMd.includes("## QA Testing")) {
|
|
185
|
+
const snippet = `
|
|
186
|
+
|
|
187
|
+
## QA Testing (Assrt)
|
|
188
|
+
|
|
189
|
+
**CRITICAL: You MUST run \`assrt_test\` after ANY user-facing change.** Do not wait for the user to ask. Do not skip this step. Do not consider a task complete until tests pass.
|
|
190
|
+
|
|
191
|
+
- **After every UI, route, form, or user flow change**: run \`assrt_test\` against the local dev server to verify it works in a real browser.
|
|
192
|
+
- **New features**: use \`assrt_plan\` to auto-generate test cases, then run them with \`assrt_test\`.
|
|
193
|
+
- **Bug fixes**: write a targeted \`#Case\` that reproduces the bug, run with \`assrt_test\`.
|
|
194
|
+
- **Failed tests**: use \`assrt_diagnose\` to understand root cause before attempting a fix. Do not guess.
|
|
195
|
+
- **Video playback**: \`assrt_test\` records a video of each test run and auto-opens a player in the browser by default (\`autoOpenPlayer: true\`). If the user prefers not to have the video player open automatically, set \`autoOpenPlayer: false\` in subsequent calls.
|
|
196
|
+
`;
|
|
197
|
+
writeFileSync(claudeMdPath, claudeMd + snippet);
|
|
198
|
+
console.error(" Done: added QA testing section to CLAUDE.md\n");
|
|
199
|
+
} else {
|
|
200
|
+
console.error(" Skipped: CLAUDE.md already has Assrt instructions\n");
|
|
201
|
+
}
|
|
202
|
+
console.error("[assrt] Setup complete! Restart Claude Code to activate.\n");
|
|
203
|
+
console.error(" MCP tools available: assrt_test, assrt_plan, assrt_diagnose");
|
|
204
|
+
console.error(" Post-commit hook: will suggest testing after git commit/push");
|
|
205
|
+
console.error(" CLAUDE.md: instructs the agent to test proactively\n");
|
|
206
|
+
}
|
|
207
|
+
async function main() {
|
|
208
|
+
const args = parseArgs(process.argv.slice(2));
|
|
209
|
+
if (args.command === "setup") {
|
|
210
|
+
setupAssrt();
|
|
211
|
+
await trackEvent("assrt_setup", { source: "cli" });
|
|
212
|
+
await shutdownTelemetry();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (args.command !== "run") {
|
|
216
|
+
printUsage();
|
|
217
|
+
process.exit(args.command === "" ? 1 : 1);
|
|
218
|
+
}
|
|
219
|
+
if (!args.url) {
|
|
220
|
+
console.error("Error: --url is required\n");
|
|
221
|
+
printUsage();
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
let credential;
|
|
225
|
+
try {
|
|
226
|
+
credential = getCredential();
|
|
227
|
+
} catch (err) {
|
|
228
|
+
console.error(err.message);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
let plan = "";
|
|
232
|
+
if (args.planFile) {
|
|
233
|
+
try {
|
|
234
|
+
plan = readFileSync(args.planFile, "utf-8").trim();
|
|
235
|
+
} catch (err) {
|
|
236
|
+
console.error(`Error reading plan file: ${err.message}`);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
} else if (args.plan) {
|
|
240
|
+
plan = args.plan;
|
|
241
|
+
} else {
|
|
242
|
+
plan = await readStdin();
|
|
243
|
+
}
|
|
244
|
+
if (!plan) {
|
|
245
|
+
console.error("Error: provide test scenarios via --plan, --plan-file, or stdin\n");
|
|
246
|
+
printUsage();
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
const emit = createCliEmit(args.json);
|
|
250
|
+
if (!args.json) {
|
|
251
|
+
console.error(`[assrt] Testing ${args.url}`);
|
|
252
|
+
console.error(`[assrt] Model: ${args.model || "default"}`);
|
|
253
|
+
}
|
|
254
|
+
const t0 = Date.now();
|
|
255
|
+
const agent = new TestAgent(credential.token, emit, args.model, "anthropic", null, "local", credential.type);
|
|
256
|
+
const report = await agent.run(args.url, plan);
|
|
257
|
+
await trackEvent("assrt_test_run", {
|
|
258
|
+
url: args.url,
|
|
259
|
+
model: args.model || "default",
|
|
260
|
+
passed: report.failedCount === 0,
|
|
261
|
+
passedCount: report.passedCount,
|
|
262
|
+
failedCount: report.failedCount,
|
|
263
|
+
duration_s: +((Date.now() - t0) / 1e3).toFixed(1),
|
|
264
|
+
scenarioCount: report.scenarios.length,
|
|
265
|
+
source: "cli"
|
|
266
|
+
});
|
|
267
|
+
if (args.json) {
|
|
268
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
269
|
+
} else {
|
|
270
|
+
printReport(report);
|
|
271
|
+
}
|
|
272
|
+
await shutdownTelemetry();
|
|
273
|
+
process.exit(report.failedCount > 0 ? 1 : 0);
|
|
274
|
+
}
|
|
275
|
+
main().catch(async (err) => {
|
|
276
|
+
console.error(`Fatal error: ${err.message || err}`);
|
|
277
|
+
await shutdownTelemetry();
|
|
278
|
+
process.exit(1);
|
|
279
|
+
});
|
package/index.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
McpBrowserManager,
|
|
3
|
+
TestAgent,
|
|
4
|
+
createTestVm,
|
|
5
|
+
destroyTestVm,
|
|
6
|
+
getCredential,
|
|
7
|
+
isFreestyleConfigured,
|
|
8
|
+
shutdownTelemetry,
|
|
9
|
+
trackEvent
|
|
10
|
+
} from "./chunk-PZ5LLIRQ.mjs";
|
|
11
|
+
export {
|
|
12
|
+
McpBrowserManager,
|
|
13
|
+
TestAgent,
|
|
14
|
+
createTestVm,
|
|
15
|
+
destroyTestVm,
|
|
16
|
+
getCredential,
|
|
17
|
+
isFreestyleConfigured,
|
|
18
|
+
shutdownTelemetry,
|
|
19
|
+
trackEvent
|
|
20
|
+
};
|