@vostride/agent-qa-appium-runtime 0.0.0-stage → 0.3.7
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.md +105 -0
- package/NOTICE.md +11 -0
- package/README.md +77 -2
- package/dist/index.cjs +268 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +245 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -4
package/LICENSE.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Functional Source License, Version 1.1, ALv2 Future License
|
|
2
|
+
|
|
3
|
+
## Abbreviation
|
|
4
|
+
|
|
5
|
+
FSL-1.1-ALv2
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 Pranshu Chittora
|
|
10
|
+
|
|
11
|
+
## Terms and Conditions
|
|
12
|
+
|
|
13
|
+
### Licensor ("We")
|
|
14
|
+
|
|
15
|
+
The party offering the Software under these Terms and Conditions.
|
|
16
|
+
|
|
17
|
+
### The Software
|
|
18
|
+
|
|
19
|
+
The "Software" is each version of the software that we make available under
|
|
20
|
+
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
21
|
+
Conditions with the Software.
|
|
22
|
+
|
|
23
|
+
### License Grant
|
|
24
|
+
|
|
25
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
26
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
27
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
28
|
+
and redistribute the Software for any Permitted Purpose identified below.
|
|
29
|
+
|
|
30
|
+
### Permitted Purpose
|
|
31
|
+
|
|
32
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
33
|
+
means making the Software available to others in a commercial product or
|
|
34
|
+
service that:
|
|
35
|
+
|
|
36
|
+
1. substitutes for the Software;
|
|
37
|
+
|
|
38
|
+
2. substitutes for any other product or service we offer using the Software
|
|
39
|
+
that exists as of the date we make the Software available; or
|
|
40
|
+
|
|
41
|
+
3. offers the same or substantially similar functionality as the Software.
|
|
42
|
+
|
|
43
|
+
Permitted Purposes specifically include using the Software:
|
|
44
|
+
|
|
45
|
+
1. for your internal use and access;
|
|
46
|
+
|
|
47
|
+
2. for non-commercial education;
|
|
48
|
+
|
|
49
|
+
3. for non-commercial research; and
|
|
50
|
+
|
|
51
|
+
4. in connection with professional services that you provide to a licensee
|
|
52
|
+
using the Software in accordance with these Terms and Conditions.
|
|
53
|
+
|
|
54
|
+
### Patents
|
|
55
|
+
|
|
56
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
57
|
+
patents, the license grant above includes a license under our patents. If you
|
|
58
|
+
make a claim against any party that the Software infringes or contributes to
|
|
59
|
+
the infringement of any patent, then your patent license to the Software ends
|
|
60
|
+
immediately.
|
|
61
|
+
|
|
62
|
+
### Redistribution
|
|
63
|
+
|
|
64
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
65
|
+
the Software.
|
|
66
|
+
|
|
67
|
+
If you redistribute any copies, modifications or derivatives of the Software,
|
|
68
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
69
|
+
remove any copyright notices provided in or with the Software.
|
|
70
|
+
|
|
71
|
+
### Disclaimer
|
|
72
|
+
|
|
73
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
74
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
75
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
76
|
+
|
|
77
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
78
|
+
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
79
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
80
|
+
|
|
81
|
+
### Trademarks
|
|
82
|
+
|
|
83
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
84
|
+
the Software, you have no right under these Terms and Conditions to use our
|
|
85
|
+
trademarks, trade names, service marks or product names.
|
|
86
|
+
|
|
87
|
+
## Grant of Future License
|
|
88
|
+
|
|
89
|
+
We hereby irrevocably grant you an additional license to use the Software under
|
|
90
|
+
the Apache License, Version 2.0 that is effective on the second anniversary of
|
|
91
|
+
the date we make the Software available. On or after that date, you may use the
|
|
92
|
+
Software under the Apache License, Version 2.0, in which case the following
|
|
93
|
+
will apply:
|
|
94
|
+
|
|
95
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
96
|
+
this file except in compliance with the License.
|
|
97
|
+
|
|
98
|
+
You may obtain a copy of the License at
|
|
99
|
+
|
|
100
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
101
|
+
|
|
102
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
|
103
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
104
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
105
|
+
specific language governing permissions and limitations under the License.
|
package/NOTICE.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# agent-qa Notice
|
|
2
|
+
|
|
3
|
+
agent-qa is provided by Pranshu Chittora.
|
|
4
|
+
|
|
5
|
+
Copyright 2026 Pranshu Chittora
|
|
6
|
+
|
|
7
|
+
agent-qa is licensed under the Functional Source License, Version 1.1, ALv2 Future License (FSL-1.1-ALv2). See `LICENSE.md` for the full license terms.
|
|
8
|
+
|
|
9
|
+
Under the Grant of Future License in `LICENSE.md`, each version of the Software is also granted under the Apache License, Version 2.0 effective on the second anniversary of the date that version was made available.
|
|
10
|
+
|
|
11
|
+
Third-party dependencies are distributed under their own license terms. This notice covers agent-qa itself and does not replace or modify notices required by third-party dependency owners.
|
package/README.md
CHANGED
|
@@ -1,3 +1,78 @@
|
|
|
1
|
-
|
|
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>
|
|
2
6
|
|
|
3
|
-
|
|
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 examples: the checkout passes and the test on 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
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="926327cf-3053-5645-bf57-b1fd4889d76c")}catch(e){}}();
|
|
3
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
4
|
+
// \0rolldown/runtime.js
|
|
5
|
+
var __create = Object.create;
|
|
6
|
+
var __defProp = Object.defineProperty;
|
|
7
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
8
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
9
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
10
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
13
|
+
key = keys[i];
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
15
|
+
get: ((k) => from[k]).bind(null, key),
|
|
16
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
22
|
+
value: mod,
|
|
23
|
+
enumerable: true
|
|
24
|
+
}) : target, mod));
|
|
25
|
+
let node_child_process = require("node:child_process");
|
|
26
|
+
let node_fs = require("node:fs");
|
|
27
|
+
let node_http = require("node:http");
|
|
28
|
+
let node_net = require("node:net");
|
|
29
|
+
let node_fs_promises = require("node:fs/promises");
|
|
30
|
+
let node_os = require("node:os");
|
|
31
|
+
let node_path = require("node:path");
|
|
32
|
+
let node_module = require("node:module");
|
|
33
|
+
let tree_kill = require("tree-kill");
|
|
34
|
+
tree_kill = __toESM(tree_kill, 1);
|
|
35
|
+
// src/appium-home.ts
|
|
36
|
+
async function prepareAppiumHome(driverFolders) {
|
|
37
|
+
const home = await (0, node_fs_promises.realpath)(await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), "agent-qa-appium-")));
|
|
38
|
+
try {
|
|
39
|
+
await (0, node_fs_promises.mkdir)((0, node_path.join)(home, "node_modules"));
|
|
40
|
+
for (const folder of driverFolders) await (0, node_fs_promises.symlink)(folder, (0, node_path.join)(home, "node_modules", (0, node_path.basename)(folder)), "junction");
|
|
41
|
+
} catch (error) {
|
|
42
|
+
await removeAppiumHome(home).catch(() => {});
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
return home;
|
|
46
|
+
}
|
|
47
|
+
async function removeAppiumHome(home) {
|
|
48
|
+
await (0, node_fs_promises.rm)(home, {
|
|
49
|
+
recursive: true,
|
|
50
|
+
force: true,
|
|
51
|
+
maxRetries: 3,
|
|
52
|
+
retryDelay: 100
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// src/bundled-appium.ts
|
|
56
|
+
const require$1 = (0, node_module.createRequire)(typeof __filename === "string" ? __filename : require("url").pathToFileURL(__filename).href);
|
|
57
|
+
function resolveBundledAppium() {
|
|
58
|
+
return {
|
|
59
|
+
serverEntry: require$1.resolve("appium/build/lib/main.js"),
|
|
60
|
+
driverFolders: [(0, node_path.dirname)(require$1.resolve("appium-uiautomator2-driver/package.json")), (0, node_path.dirname)(require$1.resolve("appium-xcuitest-driver/package.json"))]
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// src/kill-tree.ts
|
|
64
|
+
function killTree(pid, options = {}) {
|
|
65
|
+
if ((options.platform ?? process.platform) === "linux") {
|
|
66
|
+
let members = [pid];
|
|
67
|
+
try {
|
|
68
|
+
members = linuxTree(pid, options.proc ?? "/proc");
|
|
69
|
+
} catch {}
|
|
70
|
+
for (const member of members) try {
|
|
71
|
+
process.kill(member, "SIGKILL");
|
|
72
|
+
} catch {}
|
|
73
|
+
return Promise.resolve();
|
|
74
|
+
}
|
|
75
|
+
return new Promise((settle) => {
|
|
76
|
+
(0, tree_kill.default)(pid, "SIGKILL", () => settle());
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function linuxTree(root, proc) {
|
|
80
|
+
const children = /* @__PURE__ */ new Map();
|
|
81
|
+
for (const entry of (0, node_fs.readdirSync)(proc)) {
|
|
82
|
+
if (!/^\d+$/u.test(entry)) continue;
|
|
83
|
+
let stat;
|
|
84
|
+
try {
|
|
85
|
+
stat = (0, node_fs.readFileSync)((0, node_path.join)(proc, entry, "stat"), "utf8");
|
|
86
|
+
} catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const parent = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
90
|
+
children.set(parent, [...children.get(parent) ?? [], Number(entry)]);
|
|
91
|
+
}
|
|
92
|
+
const tree = [root];
|
|
93
|
+
for (const member of tree) tree.push(...children.get(member) ?? []);
|
|
94
|
+
return tree;
|
|
95
|
+
}
|
|
96
|
+
// src/start-appium-server.ts
|
|
97
|
+
const LOOPBACK = "127.0.0.1";
|
|
98
|
+
const STARTUP_TIMEOUT_MS = 3e4;
|
|
99
|
+
const POLL_INTERVAL_MS = 500;
|
|
100
|
+
const KEPT_LINES = 10;
|
|
101
|
+
const spawnNode = (command, args, options) => (0, node_child_process.spawn)(command, [...args], options);
|
|
102
|
+
function isAppiumReady(url) {
|
|
103
|
+
return new Promise((settle) => {
|
|
104
|
+
const req = (0, node_http.request)(`${url}/status`, {
|
|
105
|
+
method: "GET",
|
|
106
|
+
timeout: 3e3
|
|
107
|
+
}, (res) => {
|
|
108
|
+
let data = "";
|
|
109
|
+
res.on("data", (chunk) => {
|
|
110
|
+
data += chunk.toString();
|
|
111
|
+
});
|
|
112
|
+
res.on("end", () => {
|
|
113
|
+
try {
|
|
114
|
+
settle(JSON.parse(data)?.value?.ready === true);
|
|
115
|
+
} catch {
|
|
116
|
+
settle(false);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
req.on("error", () => settle(false));
|
|
121
|
+
req.on("timeout", () => {
|
|
122
|
+
req.destroy();
|
|
123
|
+
settle(false);
|
|
124
|
+
});
|
|
125
|
+
req.end();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function findFreePort() {
|
|
129
|
+
return new Promise((settle, fail) => {
|
|
130
|
+
const server = (0, node_net.createServer)();
|
|
131
|
+
server.unref();
|
|
132
|
+
server.once("error", fail);
|
|
133
|
+
server.listen({
|
|
134
|
+
port: 0,
|
|
135
|
+
host: LOOPBACK
|
|
136
|
+
}, () => {
|
|
137
|
+
const address = server.address();
|
|
138
|
+
const port = typeof address === "object" && address !== null ? address.port : 0;
|
|
139
|
+
server.close(() => settle(port));
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
async function startAppiumServer(options = {}) {
|
|
144
|
+
const spawn = options.spawn ?? spawnNode;
|
|
145
|
+
const probe = options.probe ?? isAppiumReady;
|
|
146
|
+
const { serverEntry, driverFolders } = resolveBundledAppium();
|
|
147
|
+
const port = await findFreePort();
|
|
148
|
+
const home = await prepareAppiumHome(driverFolders);
|
|
149
|
+
let child;
|
|
150
|
+
try {
|
|
151
|
+
child = spawn(process.execPath, [
|
|
152
|
+
serverEntry,
|
|
153
|
+
"--address",
|
|
154
|
+
LOOPBACK,
|
|
155
|
+
"--port",
|
|
156
|
+
String(port),
|
|
157
|
+
"--relaxed-security",
|
|
158
|
+
"--deny-insecure",
|
|
159
|
+
"*:cors",
|
|
160
|
+
"--log-no-colors"
|
|
161
|
+
], {
|
|
162
|
+
env: {
|
|
163
|
+
...process.env,
|
|
164
|
+
APPIUM_HOME: home
|
|
165
|
+
},
|
|
166
|
+
stdio: [
|
|
167
|
+
"ignore",
|
|
168
|
+
"pipe",
|
|
169
|
+
"pipe"
|
|
170
|
+
]
|
|
171
|
+
});
|
|
172
|
+
} catch (error) {
|
|
173
|
+
await removeAppiumHome(home).catch(() => {});
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
const killOnExit = () => {
|
|
177
|
+
try {
|
|
178
|
+
child.kill("SIGKILL");
|
|
179
|
+
} catch {}
|
|
180
|
+
try {
|
|
181
|
+
(0, node_fs.rmSync)(home, {
|
|
182
|
+
recursive: true,
|
|
183
|
+
force: true
|
|
184
|
+
});
|
|
185
|
+
} catch {}
|
|
186
|
+
};
|
|
187
|
+
process.once("exit", killOnExit);
|
|
188
|
+
child.unref();
|
|
189
|
+
child.stdout?.unref();
|
|
190
|
+
child.stderr?.unref();
|
|
191
|
+
const lastLines = [];
|
|
192
|
+
const forward = (stream) => (chunk) => {
|
|
193
|
+
const text = chunk.toString("utf8");
|
|
194
|
+
options.onOutput?.(text, stream);
|
|
195
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
196
|
+
if (line.trim() === "") continue;
|
|
197
|
+
lastLines.push(line);
|
|
198
|
+
if (lastLines.length > KEPT_LINES) lastLines.shift();
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
child.stdout?.on("data", forward("stdout"));
|
|
202
|
+
child.stderr?.on("data", forward("stderr"));
|
|
203
|
+
const withLastLines = (message) => [message, ...lastLines].join("\n");
|
|
204
|
+
let cleaned;
|
|
205
|
+
const cleanUp = () => cleaned ??= removeAppiumHome(home).catch(() => {}).then(() => {
|
|
206
|
+
process.off("exit", killOnExit);
|
|
207
|
+
});
|
|
208
|
+
let wake = () => {};
|
|
209
|
+
let spawnError;
|
|
210
|
+
child.once("error", (error) => {
|
|
211
|
+
spawnError = error;
|
|
212
|
+
wake();
|
|
213
|
+
});
|
|
214
|
+
let closedBy;
|
|
215
|
+
const exited = new Promise((settle) => {
|
|
216
|
+
child.once("close", (code, signal) => {
|
|
217
|
+
closedBy = signal === null ? `exit code ${code}` : `signal ${signal}`;
|
|
218
|
+
wake();
|
|
219
|
+
settle();
|
|
220
|
+
});
|
|
221
|
+
}).then(cleanUp);
|
|
222
|
+
const ended = () => spawnError !== void 0 || closedBy !== void 0;
|
|
223
|
+
const pause = () => new Promise((settle) => {
|
|
224
|
+
if (ended()) return settle();
|
|
225
|
+
const timer = setTimeout(settle, POLL_INTERVAL_MS);
|
|
226
|
+
wake = () => {
|
|
227
|
+
clearTimeout(timer);
|
|
228
|
+
settle();
|
|
229
|
+
};
|
|
230
|
+
});
|
|
231
|
+
let stopping;
|
|
232
|
+
const stop = () => {
|
|
233
|
+
stopping ??= (async () => {
|
|
234
|
+
if (closedBy === void 0) {
|
|
235
|
+
child.ref();
|
|
236
|
+
child.stdout?.ref();
|
|
237
|
+
child.stderr?.ref();
|
|
238
|
+
if (child.pid !== void 0) await killTree(child.pid);
|
|
239
|
+
}
|
|
240
|
+
await exited;
|
|
241
|
+
})();
|
|
242
|
+
return stopping;
|
|
243
|
+
};
|
|
244
|
+
const url = `http://${LOOPBACK}:${port}`;
|
|
245
|
+
const deadline = Date.now() + STARTUP_TIMEOUT_MS;
|
|
246
|
+
while (!ended() && Date.now() < deadline) {
|
|
247
|
+
if (await probe(url) && !ended()) return {
|
|
248
|
+
url,
|
|
249
|
+
exited,
|
|
250
|
+
stop
|
|
251
|
+
};
|
|
252
|
+
await pause();
|
|
253
|
+
}
|
|
254
|
+
if (spawnError !== void 0) {
|
|
255
|
+
await cleanUp();
|
|
256
|
+
throw spawnError;
|
|
257
|
+
}
|
|
258
|
+
if (closedBy !== void 0) {
|
|
259
|
+
await exited;
|
|
260
|
+
throw new Error(withLastLines(`Appium exited before it was ready (${closedBy})`));
|
|
261
|
+
}
|
|
262
|
+
await stop();
|
|
263
|
+
throw new Error(withLastLines(`Appium server did not become ready within ${STARTUP_TIMEOUT_MS}ms`));
|
|
264
|
+
}
|
|
265
|
+
exports.startAppiumServer = startAppiumServer;
|
|
266
|
+
|
|
267
|
+
//# sourceMappingURL=index.cjs.map
|
|
268
|
+
//# debugId=926327cf-3053-5645-bf57-b1fd4889d76c
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/appium-home.ts","../src/bundled-appium.ts","../src/kill-tree.ts","../src/start-appium-server.ts"],"sourcesContent":["import { mkdir, mkdtemp, realpath, rm, symlink } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { basename, join } from 'node:path'\n\n// A fresh folder has no extension manifest yet, so Appium scans its node_modules and finds the\n// linked drivers at every start. A junction needs no administrator rights on Windows; elsewhere\n// it is an ordinary symlink.\nexport async function prepareAppiumHome(driverFolders: readonly string[]): Promise<string> {\n const home = await realpath(await mkdtemp(join(tmpdir(), 'agent-qa-appium-')))\n try {\n await mkdir(join(home, 'node_modules'))\n for (const folder of driverFolders) {\n await symlink(folder, join(home, 'node_modules', basename(folder)), 'junction')\n }\n } catch (error) {\n await removeAppiumHome(home).catch(() => {})\n throw error\n }\n return home\n}\n\n// Removing a link never descends into the folder it points at. The retries cover a file that is\n// still held open for a moment.\nexport async function removeAppiumHome(home: string): Promise<void> {\n await rm(home, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 })\n}\n","import { createRequire } from 'node:module'\nimport { dirname } from 'node:path'\n\nexport interface BundledAppium {\n readonly serverEntry: string\n readonly driverFolders: readonly string[]\n}\n\nconst require = createRequire(typeof __filename === 'string' ? __filename : import.meta.url)\n\n// Resolved from this package's own file: under pnpm only the package that declares a dependency\n// can resolve it, and a project's or the machine's own Appium is never reached.\nexport function resolveBundledAppium(): BundledAppium {\n return {\n serverEntry: require.resolve('appium/build/lib/main.js'),\n driverFolders: [\n dirname(require.resolve('appium-uiautomator2-driver/package.json')),\n dirname(require.resolve('appium-xcuitest-driver/package.json')),\n ],\n }\n}\n","import { readdirSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nimport treeKill from 'tree-kill'\n\n// tree-kill finds a Linux process's children by running ps, which slim Linux images do not have,\n// and a missing ps throws in the host process. Linux keeps each process's parent in /proc.\nexport function killTree(\n pid: number,\n options: { readonly platform?: NodeJS.Platform; readonly proc?: string } = {},\n): Promise<void> {\n if ((options.platform ?? process.platform) === 'linux') {\n // When /proc cannot be listed, the process itself is still signalled.\n let members = [pid]\n try {\n members = linuxTree(pid, options.proc ?? '/proc')\n } catch {}\n for (const member of members) {\n try {\n process.kill(member, 'SIGKILL')\n } catch {}\n }\n return Promise.resolve()\n }\n // tree-kill lists the tree before it signals any of it, so the tree is killed only once its\n // callback runs. What the callback reports is not passed on.\n return new Promise((settle) => {\n treeKill(pid, 'SIGKILL', () => settle())\n })\n}\n\nfunction linuxTree(root: number, proc: string): number[] {\n const children = new Map<number, number[]>()\n for (const entry of readdirSync(proc)) {\n if (!/^\\d+$/u.test(entry)) continue\n let stat: string\n try {\n stat = readFileSync(join(proc, entry, 'stat'), 'utf8')\n } catch {\n continue\n }\n // The command name sits in parentheses and may hold spaces and parentheses itself; the\n // parent's id is the second field after the last one.\n const parent = Number(stat.slice(stat.lastIndexOf(')') + 2).split(' ')[1])\n children.set(parent, [...(children.get(parent) ?? []), Number(entry)])\n }\n const tree = [root]\n for (const member of tree) tree.push(...(children.get(member) ?? []))\n return tree\n}\n","import { spawn as nodeSpawn, type ChildProcessByStdio, type SpawnOptions } from 'node:child_process'\nimport { rmSync } from 'node:fs'\nimport { request } from 'node:http'\nimport { createServer, type Socket } from 'node:net'\n\nimport { prepareAppiumHome, removeAppiumHome } from './appium-home'\nimport { resolveBundledAppium } from './bundled-appium'\nimport { killTree } from './kill-tree'\n\nconst LOOPBACK = '127.0.0.1'\nconst STARTUP_TIMEOUT_MS = 30_000\nconst POLL_INTERVAL_MS = 500\nconst KEPT_LINES = 10\n\nexport interface AppiumServer {\n // http://127.0.0.1 and the port the server listens on\n readonly url: string\n // Settles once the server process has ended, by stop() or on its own, and the prepared home\n // is removed. Never rejects.\n readonly exited: Promise<void>\n // Settles once the process tree is killed, the child has closed and the prepared home is\n // removed. Never rejects. Safe to call twice.\n stop(): Promise<void>\n}\n\ninterface ChildOutput {\n on(event: 'data', listener: (chunk: Buffer) => void): unknown\n ref(): unknown\n unref(): unknown\n}\n\ninterface AppiumChild {\n readonly pid?: number | undefined\n readonly stdout: ChildOutput | null\n readonly stderr: ChildOutput | null\n once(event: 'error', listener: (error: Error) => void): unknown\n once(\n event: 'close',\n listener: (code: number | null, signal: NodeJS.Signals | null) => void,\n ): unknown\n kill(signal: NodeJS.Signals): unknown\n ref(): unknown\n unref(): unknown\n}\n\nexport type AppiumSpawn = (\n command: string,\n args: readonly string[],\n options: SpawnOptions,\n) => AppiumChild\n\nexport interface StartAppiumServerOptions {\n // Every chunk the server prints, for its whole life.\n readonly onOutput?: (text: string, stream: 'stdout' | 'stderr') => void\n // The two seams a test passes fakes through; the defaults are the real spawn and the real probe.\n readonly spawn?: AppiumSpawn\n readonly probe?: (url: string) => Promise<boolean>\n}\n\n// A child's piped output streams are sockets, which Node types only as Readable.\nconst spawnNode: AppiumSpawn = (command, args, options) =>\n nodeSpawn(command, [...args], options) as ChildProcessByStdio<null, Socket, Socket>\n\nfunction isAppiumReady(url: string): Promise<boolean> {\n return new Promise<boolean>((settle) => {\n const req = request(`${url}/status`, { method: 'GET', timeout: 3000 }, (res) => {\n let data = ''\n res.on('data', (chunk: Buffer) => {\n data += chunk.toString()\n })\n res.on('end', () => {\n try {\n const parsed = JSON.parse(data)\n settle(parsed?.value?.ready === true)\n } catch {\n settle(false)\n }\n })\n })\n req.on('error', () => settle(false))\n req.on('timeout', () => {\n req.destroy()\n settle(false)\n })\n req.end()\n })\n}\n\n// The port the system hands out on loopback, released at once.\nfunction findFreePort(): Promise<number> {\n return new Promise((settle, fail) => {\n const server = createServer()\n server.unref()\n server.once('error', fail)\n server.listen({ port: 0, host: LOOPBACK }, () => {\n const address = server.address()\n const port = typeof address === 'object' && address !== null ? address.port : 0\n server.close(() => settle(port))\n })\n })\n}\n\nexport async function startAppiumServer(\n options: StartAppiumServerOptions = {},\n): Promise<AppiumServer> {\n const spawn = options.spawn ?? spawnNode\n const probe = options.probe ?? isAppiumReady\n const { serverEntry, driverFolders } = resolveBundledAppium()\n const port = await findFreePort()\n const home = await prepareAppiumHome(driverFolders)\n let child: AppiumChild\n try {\n child = spawn(\n process.execPath,\n [\n serverEntry,\n '--address',\n LOOPBACK,\n '--port',\n String(port),\n '--relaxed-security',\n '--deny-insecure',\n '*:cors',\n '--log-no-colors',\n ],\n { env: { ...process.env, APPIUM_HOME: home }, stdio: ['ignore', 'pipe', 'pipe'] },\n )\n } catch (error) {\n // No child exists yet, so nothing else would remove the home.\n await removeAppiumHome(home).catch(() => {})\n throw error\n }\n\n // An exiting process cannot wait for tree-kill to list the tree, so only the server itself is\n // signalled here.\n const killOnExit = (): void => {\n try {\n child.kill('SIGKILL')\n } catch {}\n try {\n rmSync(home, { recursive: true, force: true })\n } catch {}\n }\n process.once('exit', killOnExit)\n // A command that succeeded ends only once nothing holds the event loop, and a child with piped\n // output would hold it for ever, so the exit listener would never run.\n child.unref()\n child.stdout?.unref()\n child.stderr?.unref()\n\n const lastLines: string[] = []\n const forward = (stream: 'stdout' | 'stderr') => (chunk: Buffer) => {\n const text = chunk.toString('utf8')\n options.onOutput?.(text, stream)\n for (const line of text.split(/\\r?\\n/u)) {\n if (line.trim() === '') continue\n lastLines.push(line)\n if (lastLines.length > KEPT_LINES) lastLines.shift()\n }\n }\n child.stdout?.on('data', forward('stdout'))\n child.stderr?.on('data', forward('stderr'))\n const withLastLines = (message: string): string => [message, ...lastLines].join('\\n')\n\n let cleaned: Promise<void> | undefined\n const cleanUp = (): Promise<void> =>\n (cleaned ??= removeAppiumHome(home)\n .catch(() => {})\n .then(() => {\n process.off('exit', killOnExit)\n }))\n\n let wake = (): void => {}\n let spawnError: Error | undefined\n child.once('error', (error) => {\n spawnError = error\n wake()\n })\n let closedBy: string | undefined\n const exited = new Promise<void>((settle) => {\n child.once('close', (code, signal) => {\n closedBy = signal === null ? `exit code ${code}` : `signal ${signal}`\n wake()\n settle()\n })\n }).then(cleanUp)\n const ended = (): boolean => spawnError !== undefined || closedBy !== undefined\n const pause = (): Promise<void> =>\n new Promise((settle) => {\n if (ended()) return settle()\n const timer = setTimeout(settle, POLL_INTERVAL_MS)\n wake = () => {\n clearTimeout(timer)\n settle()\n }\n })\n\n let stopping: Promise<void> | undefined\n const stop = (): Promise<void> => {\n stopping ??= (async () => {\n if (closedBy === undefined) {\n // A process that is only waiting for this stop stays alive until the child has closed.\n child.ref()\n child.stdout?.ref()\n child.stderr?.ref()\n if (child.pid !== undefined) await killTree(child.pid)\n }\n await exited\n })()\n return stopping\n }\n\n const url = `http://${LOOPBACK}:${port}`\n const deadline = Date.now() + STARTUP_TIMEOUT_MS\n while (!ended() && Date.now() < deadline) {\n // Once the child has ended, whatever answers on its port is another process.\n if ((await probe(url)) && !ended()) return { url, exited, stop }\n await pause()\n }\n\n if (spawnError !== undefined) {\n await cleanUp()\n throw spawnError\n }\n if (closedBy !== undefined) {\n await exited\n throw new Error(withLastLines(`Appium exited before it was ready (${closedBy})`))\n }\n await stop()\n throw new Error(\n withLastLines(`Appium server did not become ready within ${STARTUP_TIMEOUT_MS}ms`),\n )\n}\n"],"names":["realpath","mkdtemp","join","tmpdir","mkdir","symlink","basename","rm","require","createRequire","dirname","readdirSync","readFileSync","join","nodeSpawn","request","createServer"],"mappings":"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","debugId":"926327cf-3053-5645-bf57-b1fd4889d76c"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { SpawnOptions } from "node:child_process";
|
|
2
|
+
interface AppiumServer {
|
|
3
|
+
readonly url: string;
|
|
4
|
+
readonly exited: Promise<void>;
|
|
5
|
+
stop(): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
interface ChildOutput {
|
|
8
|
+
on(event: 'data', listener: (chunk: Buffer) => void): unknown;
|
|
9
|
+
ref(): unknown;
|
|
10
|
+
unref(): unknown;
|
|
11
|
+
}
|
|
12
|
+
interface AppiumChild {
|
|
13
|
+
readonly pid?: number | undefined;
|
|
14
|
+
readonly stdout: ChildOutput | null;
|
|
15
|
+
readonly stderr: ChildOutput | null;
|
|
16
|
+
once(event: 'error', listener: (error: Error) => void): unknown;
|
|
17
|
+
once(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
|
|
18
|
+
kill(signal: NodeJS.Signals): unknown;
|
|
19
|
+
ref(): unknown;
|
|
20
|
+
unref(): unknown;
|
|
21
|
+
}
|
|
22
|
+
type AppiumSpawn = (command: string, args: readonly string[], options: SpawnOptions) => AppiumChild;
|
|
23
|
+
interface StartAppiumServerOptions {
|
|
24
|
+
readonly onOutput?: (text: string, stream: 'stdout' | 'stderr') => void;
|
|
25
|
+
readonly spawn?: AppiumSpawn;
|
|
26
|
+
readonly probe?: (url: string) => Promise<boolean>;
|
|
27
|
+
}
|
|
28
|
+
export declare function startAppiumServer(options?: StartAppiumServerOptions): Promise<AppiumServer>;
|
|
29
|
+
export type { AppiumServer, StartAppiumServerOptions };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { SpawnOptions } from "node:child_process";
|
|
2
|
+
interface AppiumServer {
|
|
3
|
+
readonly url: string;
|
|
4
|
+
readonly exited: Promise<void>;
|
|
5
|
+
stop(): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
interface ChildOutput {
|
|
8
|
+
on(event: 'data', listener: (chunk: Buffer) => void): unknown;
|
|
9
|
+
ref(): unknown;
|
|
10
|
+
unref(): unknown;
|
|
11
|
+
}
|
|
12
|
+
interface AppiumChild {
|
|
13
|
+
readonly pid?: number | undefined;
|
|
14
|
+
readonly stdout: ChildOutput | null;
|
|
15
|
+
readonly stderr: ChildOutput | null;
|
|
16
|
+
once(event: 'error', listener: (error: Error) => void): unknown;
|
|
17
|
+
once(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
|
|
18
|
+
kill(signal: NodeJS.Signals): unknown;
|
|
19
|
+
ref(): unknown;
|
|
20
|
+
unref(): unknown;
|
|
21
|
+
}
|
|
22
|
+
type AppiumSpawn = (command: string, args: readonly string[], options: SpawnOptions) => AppiumChild;
|
|
23
|
+
interface StartAppiumServerOptions {
|
|
24
|
+
readonly onOutput?: (text: string, stream: 'stdout' | 'stderr') => void;
|
|
25
|
+
readonly spawn?: AppiumSpawn;
|
|
26
|
+
readonly probe?: (url: string) => Promise<boolean>;
|
|
27
|
+
}
|
|
28
|
+
export declare function startAppiumServer(options?: StartAppiumServerOptions): Promise<AppiumServer>;
|
|
29
|
+
export type { AppiumServer, StartAppiumServerOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="c772727d-14c3-532a-a31d-8c4b1188089b")}catch(e){}}();
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { readFileSync, readdirSync, rmSync } from "node:fs";
|
|
6
|
+
import { request } from "node:http";
|
|
7
|
+
import { createServer } from "node:net";
|
|
8
|
+
import { mkdir, mkdtemp, realpath, rm, symlink } from "node:fs/promises";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { basename, dirname, join } from "node:path";
|
|
11
|
+
import treeKill from "tree-kill";
|
|
12
|
+
// src/appium-home.ts
|
|
13
|
+
async function prepareAppiumHome(driverFolders) {
|
|
14
|
+
const home = await realpath(await mkdtemp(join(tmpdir(), "agent-qa-appium-")));
|
|
15
|
+
try {
|
|
16
|
+
await mkdir(join(home, "node_modules"));
|
|
17
|
+
for (const folder of driverFolders) await symlink(folder, join(home, "node_modules", basename(folder)), "junction");
|
|
18
|
+
} catch (error) {
|
|
19
|
+
await removeAppiumHome(home).catch(() => {});
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
return home;
|
|
23
|
+
}
|
|
24
|
+
async function removeAppiumHome(home) {
|
|
25
|
+
await rm(home, {
|
|
26
|
+
recursive: true,
|
|
27
|
+
force: true,
|
|
28
|
+
maxRetries: 3,
|
|
29
|
+
retryDelay: 100
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
// src/bundled-appium.ts
|
|
33
|
+
const require = createRequire(typeof __filename === "string" ? __filename : import.meta.url);
|
|
34
|
+
function resolveBundledAppium() {
|
|
35
|
+
return {
|
|
36
|
+
serverEntry: require.resolve("appium/build/lib/main.js"),
|
|
37
|
+
driverFolders: [dirname(require.resolve("appium-uiautomator2-driver/package.json")), dirname(require.resolve("appium-xcuitest-driver/package.json"))]
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// src/kill-tree.ts
|
|
41
|
+
function killTree(pid, options = {}) {
|
|
42
|
+
if ((options.platform ?? process.platform) === "linux") {
|
|
43
|
+
let members = [pid];
|
|
44
|
+
try {
|
|
45
|
+
members = linuxTree(pid, options.proc ?? "/proc");
|
|
46
|
+
} catch {}
|
|
47
|
+
for (const member of members) try {
|
|
48
|
+
process.kill(member, "SIGKILL");
|
|
49
|
+
} catch {}
|
|
50
|
+
return Promise.resolve();
|
|
51
|
+
}
|
|
52
|
+
return new Promise((settle) => {
|
|
53
|
+
treeKill(pid, "SIGKILL", () => settle());
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function linuxTree(root, proc) {
|
|
57
|
+
const children = /* @__PURE__ */ new Map();
|
|
58
|
+
for (const entry of readdirSync(proc)) {
|
|
59
|
+
if (!/^\d+$/u.test(entry)) continue;
|
|
60
|
+
let stat;
|
|
61
|
+
try {
|
|
62
|
+
stat = readFileSync(join(proc, entry, "stat"), "utf8");
|
|
63
|
+
} catch {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const parent = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
67
|
+
children.set(parent, [...children.get(parent) ?? [], Number(entry)]);
|
|
68
|
+
}
|
|
69
|
+
const tree = [root];
|
|
70
|
+
for (const member of tree) tree.push(...children.get(member) ?? []);
|
|
71
|
+
return tree;
|
|
72
|
+
}
|
|
73
|
+
// src/start-appium-server.ts
|
|
74
|
+
const LOOPBACK = "127.0.0.1";
|
|
75
|
+
const STARTUP_TIMEOUT_MS = 3e4;
|
|
76
|
+
const POLL_INTERVAL_MS = 500;
|
|
77
|
+
const KEPT_LINES = 10;
|
|
78
|
+
const spawnNode = (command, args, options) => spawn(command, [...args], options);
|
|
79
|
+
function isAppiumReady(url) {
|
|
80
|
+
return new Promise((settle) => {
|
|
81
|
+
const req = request(`${url}/status`, {
|
|
82
|
+
method: "GET",
|
|
83
|
+
timeout: 3e3
|
|
84
|
+
}, (res) => {
|
|
85
|
+
let data = "";
|
|
86
|
+
res.on("data", (chunk) => {
|
|
87
|
+
data += chunk.toString();
|
|
88
|
+
});
|
|
89
|
+
res.on("end", () => {
|
|
90
|
+
try {
|
|
91
|
+
settle(JSON.parse(data)?.value?.ready === true);
|
|
92
|
+
} catch {
|
|
93
|
+
settle(false);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
req.on("error", () => settle(false));
|
|
98
|
+
req.on("timeout", () => {
|
|
99
|
+
req.destroy();
|
|
100
|
+
settle(false);
|
|
101
|
+
});
|
|
102
|
+
req.end();
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function findFreePort() {
|
|
106
|
+
return new Promise((settle, fail) => {
|
|
107
|
+
const server = createServer();
|
|
108
|
+
server.unref();
|
|
109
|
+
server.once("error", fail);
|
|
110
|
+
server.listen({
|
|
111
|
+
port: 0,
|
|
112
|
+
host: LOOPBACK
|
|
113
|
+
}, () => {
|
|
114
|
+
const address = server.address();
|
|
115
|
+
const port = typeof address === "object" && address !== null ? address.port : 0;
|
|
116
|
+
server.close(() => settle(port));
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async function startAppiumServer(options = {}) {
|
|
121
|
+
const spawn = options.spawn ?? spawnNode;
|
|
122
|
+
const probe = options.probe ?? isAppiumReady;
|
|
123
|
+
const { serverEntry, driverFolders } = resolveBundledAppium();
|
|
124
|
+
const port = await findFreePort();
|
|
125
|
+
const home = await prepareAppiumHome(driverFolders);
|
|
126
|
+
let child;
|
|
127
|
+
try {
|
|
128
|
+
child = spawn(process.execPath, [
|
|
129
|
+
serverEntry,
|
|
130
|
+
"--address",
|
|
131
|
+
LOOPBACK,
|
|
132
|
+
"--port",
|
|
133
|
+
String(port),
|
|
134
|
+
"--relaxed-security",
|
|
135
|
+
"--deny-insecure",
|
|
136
|
+
"*:cors",
|
|
137
|
+
"--log-no-colors"
|
|
138
|
+
], {
|
|
139
|
+
env: {
|
|
140
|
+
...process.env,
|
|
141
|
+
APPIUM_HOME: home
|
|
142
|
+
},
|
|
143
|
+
stdio: [
|
|
144
|
+
"ignore",
|
|
145
|
+
"pipe",
|
|
146
|
+
"pipe"
|
|
147
|
+
]
|
|
148
|
+
});
|
|
149
|
+
} catch (error) {
|
|
150
|
+
await removeAppiumHome(home).catch(() => {});
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
const killOnExit = () => {
|
|
154
|
+
try {
|
|
155
|
+
child.kill("SIGKILL");
|
|
156
|
+
} catch {}
|
|
157
|
+
try {
|
|
158
|
+
rmSync(home, {
|
|
159
|
+
recursive: true,
|
|
160
|
+
force: true
|
|
161
|
+
});
|
|
162
|
+
} catch {}
|
|
163
|
+
};
|
|
164
|
+
process.once("exit", killOnExit);
|
|
165
|
+
child.unref();
|
|
166
|
+
child.stdout?.unref();
|
|
167
|
+
child.stderr?.unref();
|
|
168
|
+
const lastLines = [];
|
|
169
|
+
const forward = (stream) => (chunk) => {
|
|
170
|
+
const text = chunk.toString("utf8");
|
|
171
|
+
options.onOutput?.(text, stream);
|
|
172
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
173
|
+
if (line.trim() === "") continue;
|
|
174
|
+
lastLines.push(line);
|
|
175
|
+
if (lastLines.length > KEPT_LINES) lastLines.shift();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
child.stdout?.on("data", forward("stdout"));
|
|
179
|
+
child.stderr?.on("data", forward("stderr"));
|
|
180
|
+
const withLastLines = (message) => [message, ...lastLines].join("\n");
|
|
181
|
+
let cleaned;
|
|
182
|
+
const cleanUp = () => cleaned ??= removeAppiumHome(home).catch(() => {}).then(() => {
|
|
183
|
+
process.off("exit", killOnExit);
|
|
184
|
+
});
|
|
185
|
+
let wake = () => {};
|
|
186
|
+
let spawnError;
|
|
187
|
+
child.once("error", (error) => {
|
|
188
|
+
spawnError = error;
|
|
189
|
+
wake();
|
|
190
|
+
});
|
|
191
|
+
let closedBy;
|
|
192
|
+
const exited = new Promise((settle) => {
|
|
193
|
+
child.once("close", (code, signal) => {
|
|
194
|
+
closedBy = signal === null ? `exit code ${code}` : `signal ${signal}`;
|
|
195
|
+
wake();
|
|
196
|
+
settle();
|
|
197
|
+
});
|
|
198
|
+
}).then(cleanUp);
|
|
199
|
+
const ended = () => spawnError !== void 0 || closedBy !== void 0;
|
|
200
|
+
const pause = () => new Promise((settle) => {
|
|
201
|
+
if (ended()) return settle();
|
|
202
|
+
const timer = setTimeout(settle, POLL_INTERVAL_MS);
|
|
203
|
+
wake = () => {
|
|
204
|
+
clearTimeout(timer);
|
|
205
|
+
settle();
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
let stopping;
|
|
209
|
+
const stop = () => {
|
|
210
|
+
stopping ??= (async () => {
|
|
211
|
+
if (closedBy === void 0) {
|
|
212
|
+
child.ref();
|
|
213
|
+
child.stdout?.ref();
|
|
214
|
+
child.stderr?.ref();
|
|
215
|
+
if (child.pid !== void 0) await killTree(child.pid);
|
|
216
|
+
}
|
|
217
|
+
await exited;
|
|
218
|
+
})();
|
|
219
|
+
return stopping;
|
|
220
|
+
};
|
|
221
|
+
const url = `http://${LOOPBACK}:${port}`;
|
|
222
|
+
const deadline = Date.now() + STARTUP_TIMEOUT_MS;
|
|
223
|
+
while (!ended() && Date.now() < deadline) {
|
|
224
|
+
if (await probe(url) && !ended()) return {
|
|
225
|
+
url,
|
|
226
|
+
exited,
|
|
227
|
+
stop
|
|
228
|
+
};
|
|
229
|
+
await pause();
|
|
230
|
+
}
|
|
231
|
+
if (spawnError !== void 0) {
|
|
232
|
+
await cleanUp();
|
|
233
|
+
throw spawnError;
|
|
234
|
+
}
|
|
235
|
+
if (closedBy !== void 0) {
|
|
236
|
+
await exited;
|
|
237
|
+
throw new Error(withLastLines(`Appium exited before it was ready (${closedBy})`));
|
|
238
|
+
}
|
|
239
|
+
await stop();
|
|
240
|
+
throw new Error(withLastLines(`Appium server did not become ready within ${STARTUP_TIMEOUT_MS}ms`));
|
|
241
|
+
}
|
|
242
|
+
export { startAppiumServer };
|
|
243
|
+
|
|
244
|
+
//# sourceMappingURL=index.js.map
|
|
245
|
+
//# debugId=c772727d-14c3-532a-a31d-8c4b1188089b
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/appium-home.ts","../src/bundled-appium.ts","../src/kill-tree.ts","../src/start-appium-server.ts"],"sourcesContent":["import { mkdir, mkdtemp, realpath, rm, symlink } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { basename, join } from 'node:path'\n\n// A fresh folder has no extension manifest yet, so Appium scans its node_modules and finds the\n// linked drivers at every start. A junction needs no administrator rights on Windows; elsewhere\n// it is an ordinary symlink.\nexport async function prepareAppiumHome(driverFolders: readonly string[]): Promise<string> {\n const home = await realpath(await mkdtemp(join(tmpdir(), 'agent-qa-appium-')))\n try {\n await mkdir(join(home, 'node_modules'))\n for (const folder of driverFolders) {\n await symlink(folder, join(home, 'node_modules', basename(folder)), 'junction')\n }\n } catch (error) {\n await removeAppiumHome(home).catch(() => {})\n throw error\n }\n return home\n}\n\n// Removing a link never descends into the folder it points at. The retries cover a file that is\n// still held open for a moment.\nexport async function removeAppiumHome(home: string): Promise<void> {\n await rm(home, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 })\n}\n","import { createRequire } from 'node:module'\nimport { dirname } from 'node:path'\n\nexport interface BundledAppium {\n readonly serverEntry: string\n readonly driverFolders: readonly string[]\n}\n\nconst require = createRequire(typeof __filename === 'string' ? __filename : import.meta.url)\n\n// Resolved from this package's own file: under pnpm only the package that declares a dependency\n// can resolve it, and a project's or the machine's own Appium is never reached.\nexport function resolveBundledAppium(): BundledAppium {\n return {\n serverEntry: require.resolve('appium/build/lib/main.js'),\n driverFolders: [\n dirname(require.resolve('appium-uiautomator2-driver/package.json')),\n dirname(require.resolve('appium-xcuitest-driver/package.json')),\n ],\n }\n}\n","import { readdirSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nimport treeKill from 'tree-kill'\n\n// tree-kill finds a Linux process's children by running ps, which slim Linux images do not have,\n// and a missing ps throws in the host process. Linux keeps each process's parent in /proc.\nexport function killTree(\n pid: number,\n options: { readonly platform?: NodeJS.Platform; readonly proc?: string } = {},\n): Promise<void> {\n if ((options.platform ?? process.platform) === 'linux') {\n // When /proc cannot be listed, the process itself is still signalled.\n let members = [pid]\n try {\n members = linuxTree(pid, options.proc ?? '/proc')\n } catch {}\n for (const member of members) {\n try {\n process.kill(member, 'SIGKILL')\n } catch {}\n }\n return Promise.resolve()\n }\n // tree-kill lists the tree before it signals any of it, so the tree is killed only once its\n // callback runs. What the callback reports is not passed on.\n return new Promise((settle) => {\n treeKill(pid, 'SIGKILL', () => settle())\n })\n}\n\nfunction linuxTree(root: number, proc: string): number[] {\n const children = new Map<number, number[]>()\n for (const entry of readdirSync(proc)) {\n if (!/^\\d+$/u.test(entry)) continue\n let stat: string\n try {\n stat = readFileSync(join(proc, entry, 'stat'), 'utf8')\n } catch {\n continue\n }\n // The command name sits in parentheses and may hold spaces and parentheses itself; the\n // parent's id is the second field after the last one.\n const parent = Number(stat.slice(stat.lastIndexOf(')') + 2).split(' ')[1])\n children.set(parent, [...(children.get(parent) ?? []), Number(entry)])\n }\n const tree = [root]\n for (const member of tree) tree.push(...(children.get(member) ?? []))\n return tree\n}\n","import { spawn as nodeSpawn, type ChildProcessByStdio, type SpawnOptions } from 'node:child_process'\nimport { rmSync } from 'node:fs'\nimport { request } from 'node:http'\nimport { createServer, type Socket } from 'node:net'\n\nimport { prepareAppiumHome, removeAppiumHome } from './appium-home'\nimport { resolveBundledAppium } from './bundled-appium'\nimport { killTree } from './kill-tree'\n\nconst LOOPBACK = '127.0.0.1'\nconst STARTUP_TIMEOUT_MS = 30_000\nconst POLL_INTERVAL_MS = 500\nconst KEPT_LINES = 10\n\nexport interface AppiumServer {\n // http://127.0.0.1 and the port the server listens on\n readonly url: string\n // Settles once the server process has ended, by stop() or on its own, and the prepared home\n // is removed. Never rejects.\n readonly exited: Promise<void>\n // Settles once the process tree is killed, the child has closed and the prepared home is\n // removed. Never rejects. Safe to call twice.\n stop(): Promise<void>\n}\n\ninterface ChildOutput {\n on(event: 'data', listener: (chunk: Buffer) => void): unknown\n ref(): unknown\n unref(): unknown\n}\n\ninterface AppiumChild {\n readonly pid?: number | undefined\n readonly stdout: ChildOutput | null\n readonly stderr: ChildOutput | null\n once(event: 'error', listener: (error: Error) => void): unknown\n once(\n event: 'close',\n listener: (code: number | null, signal: NodeJS.Signals | null) => void,\n ): unknown\n kill(signal: NodeJS.Signals): unknown\n ref(): unknown\n unref(): unknown\n}\n\nexport type AppiumSpawn = (\n command: string,\n args: readonly string[],\n options: SpawnOptions,\n) => AppiumChild\n\nexport interface StartAppiumServerOptions {\n // Every chunk the server prints, for its whole life.\n readonly onOutput?: (text: string, stream: 'stdout' | 'stderr') => void\n // The two seams a test passes fakes through; the defaults are the real spawn and the real probe.\n readonly spawn?: AppiumSpawn\n readonly probe?: (url: string) => Promise<boolean>\n}\n\n// A child's piped output streams are sockets, which Node types only as Readable.\nconst spawnNode: AppiumSpawn = (command, args, options) =>\n nodeSpawn(command, [...args], options) as ChildProcessByStdio<null, Socket, Socket>\n\nfunction isAppiumReady(url: string): Promise<boolean> {\n return new Promise<boolean>((settle) => {\n const req = request(`${url}/status`, { method: 'GET', timeout: 3000 }, (res) => {\n let data = ''\n res.on('data', (chunk: Buffer) => {\n data += chunk.toString()\n })\n res.on('end', () => {\n try {\n const parsed = JSON.parse(data)\n settle(parsed?.value?.ready === true)\n } catch {\n settle(false)\n }\n })\n })\n req.on('error', () => settle(false))\n req.on('timeout', () => {\n req.destroy()\n settle(false)\n })\n req.end()\n })\n}\n\n// The port the system hands out on loopback, released at once.\nfunction findFreePort(): Promise<number> {\n return new Promise((settle, fail) => {\n const server = createServer()\n server.unref()\n server.once('error', fail)\n server.listen({ port: 0, host: LOOPBACK }, () => {\n const address = server.address()\n const port = typeof address === 'object' && address !== null ? address.port : 0\n server.close(() => settle(port))\n })\n })\n}\n\nexport async function startAppiumServer(\n options: StartAppiumServerOptions = {},\n): Promise<AppiumServer> {\n const spawn = options.spawn ?? spawnNode\n const probe = options.probe ?? isAppiumReady\n const { serverEntry, driverFolders } = resolveBundledAppium()\n const port = await findFreePort()\n const home = await prepareAppiumHome(driverFolders)\n let child: AppiumChild\n try {\n child = spawn(\n process.execPath,\n [\n serverEntry,\n '--address',\n LOOPBACK,\n '--port',\n String(port),\n '--relaxed-security',\n '--deny-insecure',\n '*:cors',\n '--log-no-colors',\n ],\n { env: { ...process.env, APPIUM_HOME: home }, stdio: ['ignore', 'pipe', 'pipe'] },\n )\n } catch (error) {\n // No child exists yet, so nothing else would remove the home.\n await removeAppiumHome(home).catch(() => {})\n throw error\n }\n\n // An exiting process cannot wait for tree-kill to list the tree, so only the server itself is\n // signalled here.\n const killOnExit = (): void => {\n try {\n child.kill('SIGKILL')\n } catch {}\n try {\n rmSync(home, { recursive: true, force: true })\n } catch {}\n }\n process.once('exit', killOnExit)\n // A command that succeeded ends only once nothing holds the event loop, and a child with piped\n // output would hold it for ever, so the exit listener would never run.\n child.unref()\n child.stdout?.unref()\n child.stderr?.unref()\n\n const lastLines: string[] = []\n const forward = (stream: 'stdout' | 'stderr') => (chunk: Buffer) => {\n const text = chunk.toString('utf8')\n options.onOutput?.(text, stream)\n for (const line of text.split(/\\r?\\n/u)) {\n if (line.trim() === '') continue\n lastLines.push(line)\n if (lastLines.length > KEPT_LINES) lastLines.shift()\n }\n }\n child.stdout?.on('data', forward('stdout'))\n child.stderr?.on('data', forward('stderr'))\n const withLastLines = (message: string): string => [message, ...lastLines].join('\\n')\n\n let cleaned: Promise<void> | undefined\n const cleanUp = (): Promise<void> =>\n (cleaned ??= removeAppiumHome(home)\n .catch(() => {})\n .then(() => {\n process.off('exit', killOnExit)\n }))\n\n let wake = (): void => {}\n let spawnError: Error | undefined\n child.once('error', (error) => {\n spawnError = error\n wake()\n })\n let closedBy: string | undefined\n const exited = new Promise<void>((settle) => {\n child.once('close', (code, signal) => {\n closedBy = signal === null ? `exit code ${code}` : `signal ${signal}`\n wake()\n settle()\n })\n }).then(cleanUp)\n const ended = (): boolean => spawnError !== undefined || closedBy !== undefined\n const pause = (): Promise<void> =>\n new Promise((settle) => {\n if (ended()) return settle()\n const timer = setTimeout(settle, POLL_INTERVAL_MS)\n wake = () => {\n clearTimeout(timer)\n settle()\n }\n })\n\n let stopping: Promise<void> | undefined\n const stop = (): Promise<void> => {\n stopping ??= (async () => {\n if (closedBy === undefined) {\n // A process that is only waiting for this stop stays alive until the child has closed.\n child.ref()\n child.stdout?.ref()\n child.stderr?.ref()\n if (child.pid !== undefined) await killTree(child.pid)\n }\n await exited\n })()\n return stopping\n }\n\n const url = `http://${LOOPBACK}:${port}`\n const deadline = Date.now() + STARTUP_TIMEOUT_MS\n while (!ended() && Date.now() < deadline) {\n // Once the child has ended, whatever answers on its port is another process.\n if ((await probe(url)) && !ended()) return { url, exited, stop }\n await pause()\n }\n\n if (spawnError !== undefined) {\n await cleanUp()\n throw spawnError\n }\n if (closedBy !== undefined) {\n await exited\n throw new Error(withLastLines(`Appium exited before it was ready (${closedBy})`))\n }\n await stop()\n throw new Error(\n withLastLines(`Appium server did not become ready within ${STARTUP_TIMEOUT_MS}ms`),\n )\n}\n"],"names":["nodeSpawn"],"mappings":"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","debugId":"c772727d-14c3-532a-a31d-8c4b1188089b"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vostride/agent-qa-appium-runtime",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.3.7",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Carries Appium and the UiAutomator2 and XCUITest drivers at pinned versions and starts that Appium server for agent-qa.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"agent-qa",
|
|
8
|
+
"testing",
|
|
9
|
+
"appium",
|
|
10
|
+
"mobile"
|
|
11
|
+
],
|
|
12
|
+
"license": "FSL-1.1-ALv2",
|
|
13
|
+
"homepage": "https://github.com/vostride/agent-qa#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/vostride/agent-qa/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/vostride/agent-qa.git",
|
|
20
|
+
"directory": "packages/appium-runtime"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=24"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public",
|
|
27
|
+
"registry": "https://registry.npmjs.org/"
|
|
28
|
+
},
|
|
29
|
+
"type": "module",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"import": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"default": "./dist/index.js"
|
|
35
|
+
},
|
|
36
|
+
"require": {
|
|
37
|
+
"types": "./dist/index.d.cts",
|
|
38
|
+
"default": "./dist/index.cjs"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
"LICENSE.md",
|
|
45
|
+
"NOTICE.md"
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsdown",
|
|
49
|
+
"dev": "tsdown --watch",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"clean": "rm -rf dist .turbo"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"appium": "4.0.0-beta.7",
|
|
56
|
+
"appium-uiautomator2-driver": "9.0.2",
|
|
57
|
+
"appium-xcuitest-driver": "13.0.8",
|
|
58
|
+
"tree-kill": "1.2.2"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/node": "~24.19.2",
|
|
62
|
+
"@vostride/agent-qa-arch": "0.3.7",
|
|
63
|
+
"tsdown": "~0.23.4",
|
|
64
|
+
"typescript": "~7.0.2",
|
|
65
|
+
"vitest": "~5.0.5"
|
|
66
|
+
}
|
|
67
|
+
}
|