@vostride/agent-qa-runner-local 0.2.2 → 0.2.4

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/README.md ADDED
@@ -0,0 +1,78 @@
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/banner-dark.jpg">
3
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/banner-light.png">
4
+ <img src="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/banner-light.png" alt="agent-qa by vostride — a self-improving QA agent for your software factory" width="100%">
5
+ </picture>
6
+
7
+ <p align="center">
8
+ <strong>English</strong> · <a href="https://github.com/vostride/agent-qa/blob/main/README.zh-CN.md">中文</a>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://www.npmjs.com/package/agent-qa"><img src="https://img.shields.io/npm/v/agent-qa?color=196872" alt="npm version"></a>
13
+ <a href="https://www.npmjs.com/package/agent-qa"><img src="https://img.shields.io/npm/dm/agent-qa?color=196872" alt="npm downloads per month"></a>
14
+ <a href="https://github.com/vostride/agent-qa/stargazers"><img src="https://img.shields.io/github/stars/vostride/agent-qa?color=196872" alt="GitHub stars"></a>
15
+ <a href="https://github.com/vostride/agent-qa/actions/workflows/ci.yml"><img src="https://github.com/vostride/agent-qa/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
16
+ <a href="https://vostride.com/docs/agent-qa"><img src="https://img.shields.io/badge/docs-agent--qa-196872" alt="Documentation"></a>
17
+ <a href="https://github.com/vostride/agent-qa/blob/main/LICENSE.md"><img src="https://img.shields.io/badge/license-FSL--1.1--ALv2-196872" alt="License: FSL-1.1-ALv2"></a>
18
+ </p>
19
+
20
+ <p align="center">
21
+ <a href="https://vostride.com/agent-qa">Website</a> · <a href="https://vostride.com/docs/agent-qa">Docs</a> · <a href="https://vostride.com/demo/agent-qa">Demo</a> · <a href="https://vostride.com/blog">Blog</a> · <a href="https://vostride.com/changelog">Changelog</a>
22
+ </p>
23
+
24
+ **Your AI QA engineer, with memory.** Describe a user journey in plain language. agent-qa tests it on web, Android or iOS and keeps the evidence. Use the CLI, the dashboard, or your coding agent through MCP and skills.
25
+
26
+ - **memidex:** remembers facts and working procedures as reviewable files in your repository.
27
+ - **Self-improvement:** validates lessons from runs, retires stale knowledge, and uses your feedback to correct mistakes.
28
+
29
+ | Benchmark | Rank | Published result |
30
+ | --- | --- | --- |
31
+ | [AndroidWorld](https://vostride.com/benchmark/android-world) | **#1** | **100% pass@1** (116/116 tasks) |
32
+ | [AppControlBench](https://vostride.com/benchmark/app-control-bench) | **#1** | **99.6% completion** (114.5/115 points) |
33
+
34
+ Read the full benchmark reports, with run evidence and methodology, on [our blog](https://vostride.com/blog). Both used gpt-6-sol with memory and self-improvement off; AppControlBench gives half credit for partial completion.
35
+
36
+ ## See it in action
37
+
38
+ **Inspect a failure.** The demo catches a cart showing $29 instead of $53, with the failed step and its screenshot side by side.
39
+
40
+ <picture>
41
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/evidence-dark.png">
42
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/evidence-light.png">
43
+ <img src="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/evidence-light.png" alt="The dashboard shows the deliberate cart-total bug, its failed assertion, and recorded browser evidence" width="100%">
44
+ </picture>
45
+
46
+ **See what memory contributed.** Inspect the knowledge supplied to a run and trace it back to the steps that used it.
47
+
48
+ <picture>
49
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/knowledge-dark.png">
50
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/knowledge-light.png">
51
+ <img src="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/knowledge-light.png" alt="The Knowledge tab shows stored facts retrieved for the demo run" width="100%">
52
+ </picture>
53
+
54
+ **Run from your terminal.** Real output from the demo suite: checkout passes; the deliberate cart-total bug fails.
55
+
56
+ <picture>
57
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/cli-dark.png">
58
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/cli-light.png">
59
+ <img src="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/cli-light.png" alt="CLI output excerpt showing one passed test, one failed test, and eight of nine steps passing" width="100%">
60
+ </picture>
61
+
62
+ ## Get started
63
+
64
+ Requires Node.js 24+.
65
+
66
+ ```sh
67
+ npm install -D agent-qa
68
+ npx agent-qa init
69
+ ```
70
+
71
+ Follow the [quickstart](https://vostride.com/docs/agent-qa/quickstart) to connect a model and run the demo, or [set up with your coding agent](https://vostride.com/docs/agent-qa/agent-quickstart).
72
+
73
+ > [!NOTE]
74
+ > Early stage. Expect bugs. [Report an issue](https://github.com/vostride/agent-qa/issues) when something breaks.
75
+
76
+ ## License
77
+
78
+ [Functional Source License (FSL-1.1-ALv2)](https://github.com/vostride/agent-qa/blob/main/LICENSE.md).
package/dist/index.cjs CHANGED
@@ -55,6 +55,7 @@ __export(index_exports, {
55
55
  latestBuildOrdinal: () => latestBuildOrdinal,
56
56
  loadCompletedRunHandoff: () => loadCompletedRunHandoff,
57
57
  nextBuildOrdinal: () => nextBuildOrdinal,
58
+ purgeActionCache: () => purgeActionCache,
58
59
  readBuildTagMap: () => readBuildTagMap,
59
60
  recordBuildTag: () => recordBuildTag,
60
61
  reserveBuildOrdinalForProducts: () => reserveBuildOrdinalForProducts,
@@ -146,39 +147,56 @@ function shouldWrite(policy) {
146
147
  }
147
148
 
148
149
  // src/fs-action-cache.ts
149
- var CACHE_SCHEMA_VERSION = 2;
150
+ var CACHE_SCHEMA_VERSION = 3;
151
+ var CACHE_ENTRY_FILE_RE = /^[0-9a-f]{16}\.json$/;
150
152
  var FsActionCache = class {
151
153
  root;
152
154
  policy;
155
+ skipped = /* @__PURE__ */ new Set();
153
156
  constructor(config) {
154
157
  this.root = config.root;
155
158
  this.policy = config.policy;
156
159
  }
157
160
  async get(key) {
158
- if (this.policy.disableCache) return null;
161
+ if (this.policy.disableCache || this.skipped.has(key)) return null;
159
162
  const onCurrent = await this.readEntry(this.policy.currentBranch, key);
160
- if (onCurrent !== null) return onCurrent;
163
+ if (onCurrent?.kind === "suppressed") return null;
164
+ if (onCurrent?.kind === "value") return onCurrent.value;
161
165
  if (this.policy.currentBranch !== this.policy.defaultBranch) {
162
- return this.readEntry(this.policy.defaultBranch, key);
166
+ const onDefault = await this.readEntry(this.policy.defaultBranch, key);
167
+ return onDefault?.kind === "value" ? onDefault.value : null;
163
168
  }
164
169
  return null;
165
170
  }
166
171
  async set(key, value) {
167
172
  if (!shouldWrite(this.policy)) return;
168
- const dir = this.branchDir(this.policy.currentBranch);
169
- await (0, import_promises.mkdir)(dir, { recursive: true });
170
- const envelope = { schemaVersion: CACHE_SCHEMA_VERSION, value };
171
- await (0, import_promises.writeFile)(this.entryPath(dir, key), `${JSON.stringify(envelope, null, 2)}
172
- `, "utf-8");
173
+ await this.writeEnvelope(key, { schemaVersion: CACHE_SCHEMA_VERSION, value });
173
174
  }
174
175
  async invalidate(key) {
175
- const dir = this.branchDir(this.policy.currentBranch);
176
+ if (!shouldWrite(this.policy)) {
177
+ this.skipped.add(key);
178
+ return;
179
+ }
180
+ const { currentBranch, defaultBranch } = this.policy;
181
+ if (currentBranch !== defaultBranch) {
182
+ const onDefault = await this.readEntry(defaultBranch, key);
183
+ if (onDefault?.kind === "value") {
184
+ await this.writeEnvelope(key, { schemaVersion: CACHE_SCHEMA_VERSION, suppressed: true });
185
+ return;
186
+ }
187
+ }
176
188
  try {
177
- await (0, import_promises.unlink)(this.entryPath(dir, key));
189
+ await (0, import_promises.unlink)(this.entryPath(this.branchDir(currentBranch), key));
178
190
  } catch (err) {
179
- if (err.code !== "ENOENT") throw err;
191
+ if (!isMissing(err)) throw err;
180
192
  }
181
193
  }
194
+ async writeEnvelope(key, envelope) {
195
+ const dir = this.branchDir(this.policy.currentBranch);
196
+ await (0, import_promises.mkdir)(dir, { recursive: true });
197
+ await (0, import_promises.writeFile)(this.entryPath(dir, key), `${JSON.stringify(envelope, null, 2)}
198
+ `, "utf-8");
199
+ }
182
200
  branchDir(branch) {
183
201
  return (0, import_node_path2.join)(this.root, (0, import_agent_qa_schema.slugBranch)(branch));
184
202
  }
@@ -190,7 +208,7 @@ var FsActionCache = class {
190
208
  try {
191
209
  raw = await (0, import_promises.readFile)(this.entryPath(this.branchDir(branch), key), "utf-8");
192
210
  } catch (err) {
193
- if (err.code === "ENOENT") return null;
211
+ if (isMissing(err)) return null;
194
212
  throw err;
195
213
  }
196
214
  let envelope;
@@ -200,9 +218,44 @@ var FsActionCache = class {
200
218
  return null;
201
219
  }
202
220
  if (envelope?.schemaVersion !== CACHE_SCHEMA_VERSION) return null;
203
- return envelope.value ?? null;
221
+ if (envelope.suppressed === true) return { kind: "suppressed" };
222
+ return envelope.value ? { kind: "value", value: envelope.value } : null;
204
223
  }
205
224
  };
225
+ function isMissing(err) {
226
+ return err.code === "ENOENT";
227
+ }
228
+ async function purgeActionCache(root) {
229
+ let branches;
230
+ try {
231
+ branches = await (0, import_promises.readdir)(root, { withFileTypes: true });
232
+ } catch (err) {
233
+ if (isMissing(err)) return 0;
234
+ throw err;
235
+ }
236
+ let purged = 0;
237
+ for (const branch of branches) {
238
+ if (!branch.isDirectory() || (0, import_agent_qa_schema.slugBranch)(branch.name) !== branch.name) continue;
239
+ const dir = (0, import_node_path2.join)(root, branch.name);
240
+ const entries = await (0, import_promises.readdir)(dir, { withFileTypes: true });
241
+ let removed = 0;
242
+ for (const entry of entries) {
243
+ if (!entry.isFile() || !CACHE_ENTRY_FILE_RE.test(entry.name)) continue;
244
+ try {
245
+ await (0, import_promises.unlink)((0, import_node_path2.join)(dir, entry.name));
246
+ } catch (err) {
247
+ if (!isMissing(err)) throw err;
248
+ }
249
+ removed++;
250
+ }
251
+ purged += removed;
252
+ if (removed > 0 && removed === entries.length) {
253
+ await (0, import_promises.rmdir)(dir).catch(() => {
254
+ });
255
+ }
256
+ }
257
+ return purged;
258
+ }
206
259
 
207
260
  // src/storage-state-store.ts
208
261
  var import_promises2 = require("fs/promises");
@@ -898,9 +951,11 @@ var import_node_os = require("os");
898
951
  var import_agent_qa_runner = require("@vostride/agent-qa-runner");
899
952
  var DEFAULT_DOCKER_BIN = "docker";
900
953
  var DockerHookExecutor = class {
954
+ version;
901
955
  dockerBin;
902
956
  pullPolicy;
903
- constructor(config = {}) {
957
+ constructor(config) {
958
+ this.version = config.version;
904
959
  this.dockerBin = config.dockerBin ?? DEFAULT_DOCKER_BIN;
905
960
  this.pullPolicy = config.pullPolicy ?? "if-not-present";
906
961
  }
@@ -908,15 +963,15 @@ var DockerHookExecutor = class {
908
963
  const dockerBin = this.dockerBin;
909
964
  const pullPolicy = this.pullPolicy;
910
965
  const start = Date.now();
911
- const image = import_agent_qa_runner.RUNTIME_IMAGE_MAP[hook.runtime];
912
- if (pullPolicy === "always" || pullPolicy === "if-not-present") {
913
- const needsPull = pullPolicy === "always" || !await imageExists(dockerBin, image);
914
- if (needsPull) {
915
- await pullImage(dockerBin, image);
916
- }
917
- }
966
+ const image = (0, import_agent_qa_runner.hookRunnerImage)(hook.runtime, this.version);
918
967
  let workDir;
919
968
  try {
969
+ if (pullPolicy === "always" || pullPolicy === "if-not-present") {
970
+ const needsPull = pullPolicy === "always" || !await imageExists(dockerBin, image);
971
+ if (needsPull) {
972
+ await pullImage(dockerBin, image);
973
+ }
974
+ }
920
975
  workDir = await (0, import_promises3.mkdtemp)((0, import_node_path11.join)((0, import_node_os.tmpdir)(), "agent-qa-hook-"));
921
976
  await (0, import_promises3.mkdir)((0, import_node_path11.join)(workDir, "tmp"), { recursive: true });
922
977
  await (0, import_promises3.cp)(hook.file, (0, import_node_path11.join)(workDir, (0, import_node_path11.basename)(hook.file)));
@@ -1101,6 +1156,7 @@ function execDocker(dockerBin, args, timeout) {
1101
1156
  latestBuildOrdinal,
1102
1157
  loadCompletedRunHandoff,
1103
1158
  nextBuildOrdinal,
1159
+ purgeActionCache,
1104
1160
  readBuildTagMap,
1105
1161
  recordBuildTag,
1106
1162
  reserveBuildOrdinalForProducts,