dsh-flakefinder 0.3.4 → 0.3.5
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/CHANGELOG.md +15 -0
- package/README.en.md +24 -43
- package/README.md +22 -98
- package/docs/USAGE.en.md +56 -0
- package/docs/USAGE.md +109 -0
- package/docs/VALIDATION.md +15 -0
- package/lib/config.d.ts +1 -0
- package/lib/config.js +2 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/runner.d.ts +3 -1
- package/lib/runner.js +8 -4
- package/lib/store.js +85 -52
- package/lib/tools.js +18 -3
- package/package.json +10 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# 更新记录
|
|
2
|
+
|
|
3
|
+
[返回简介](README.md) · [使用说明](docs/USAGE.md) · [验证记录](docs/VALIDATION.md)
|
|
4
|
+
|
|
5
|
+
## 0.3.5 (2026-10-05)
|
|
6
|
+
|
|
7
|
+
- 测试进程和隔离清单使用当前会话工作区,取消后不再执行后续轮次或保存成功记录;跨实例锁定历史与隔离清单,损坏文件保持原样。
|
|
8
|
+
|
|
9
|
+
## 0.3.4 (2026-09-28)
|
|
10
|
+
|
|
11
|
+
- 更新官方 Harness 0.2.0-rc.1 的兼容声明和共同加载验证;运行时代码未变。验证范围见[验证记录](docs/VALIDATION.md)。
|
|
12
|
+
|
|
13
|
+
## 更早的改动
|
|
14
|
+
|
|
15
|
+
完整历史可查阅 [GitHub 提交记录](https://github.com/STARDUSTLC666/dsh-flakefinder/commits/master)。
|
package/README.en.md
CHANGED
|
@@ -1,65 +1,46 @@
|
|
|
1
1
|
# dsh-flakefinder
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[中文](README.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+

|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Repeat test runs to identify intermittent failures and keep investigation history.
|
|
8
8
|
|
|
9
|
-
[](https://www.npmjs.com/package/dsh-flakefinder) [](https://www.npmjs.com/package/dsh-flakefinder)
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## What it does
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
| `flaky_history` | Query detection history, filter by target | no |
|
|
17
|
-
| `flaky_report` | Combine quarantine manifest and history into a report | no |
|
|
18
|
-
| `flaky_quarantine` | Write flaky cases to `.flakefinder.json` | yes (approval) |
|
|
19
|
-
| `flaky_clear` | Remove recovered cases from the manifest | yes (approval) |
|
|
20
|
-
|
|
21
|
-
Frameworks: vitest / jest / pytest (setup.cfg / pyproject / pytest.ini / tox.ini) / node:test, auto-detected in that order. JUnit XML entities and TAP SKIP directives are parsed.
|
|
22
|
-
|
|
23
|
-
## Compatibility
|
|
24
|
-
|
|
25
|
-
Validation host: Harness `0.2.0-rc.1` built from official sources (commit `407e65c8`) with Node `24.16.0` on 2026-09-28. All 44 plugin tests pass in an isolated environment; all 18 plugins mount together in one host registering 6 tools, with tool schemas and health-check contracts passing. No live ports or external services were exercised in this round.
|
|
26
|
-
|
|
27
|
-
2026-09-13 fix: retain the service receiver when calling `subprocess.spawn`, preventing failures caused by passing the method as an unbound callback. Verified with official `0.1.5-rc.1` and `0.1.5-rc.2` on Node `24.16.0`. `flaky_detect` runs three real Node test processes through the host subprocess service and returns `stable-pass`.
|
|
13
|
+
- Support Vitest, Jest, pytest and node:test.
|
|
14
|
+
- Track repeated run results and failure history.
|
|
15
|
+
- Generate and maintain a quarantine list.
|
|
28
16
|
|
|
29
17
|
## Install
|
|
30
18
|
|
|
31
|
-
|
|
32
|
-
dsh plugin --profile web add dsh-flakefinder
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## Uninstall
|
|
19
|
+
In DSH Desktop, install `dsh-flakefinder` from the Plugins panel. If the bundled dsh command is available:
|
|
36
20
|
|
|
37
21
|
```bash
|
|
38
|
-
dsh plugin --profile
|
|
22
|
+
dsh plugin --profile desktop add dsh-flakefinder
|
|
39
23
|
```
|
|
40
24
|
|
|
41
|
-
|
|
25
|
+
For the web version, replace `desktop` with `web`. Restart DSH after installation.
|
|
42
26
|
|
|
27
|
+
## Start using it
|
|
43
28
|
|
|
44
|
-
|
|
29
|
+
Ask: “Run this test group ten times, identify intermittent failures and summarize the differences.”
|
|
45
30
|
|
|
46
|
-
|
|
47
|
-
User: src/checkout.test.ts keeps failing recently, is it real?
|
|
48
|
-
Agent:
|
|
49
|
-
flaky_detect(target="src/checkout.test.ts", runs=5)
|
|
50
|
-
-> verdict: flaky (3/5 passed; failures cluster on fake timers)
|
|
51
|
-
-> flaky_quarantine(tests=["src/checkout.test.ts > timer restore"], reason="timer race")
|
|
52
|
-
```
|
|
31
|
+
## Requirements and configuration
|
|
53
32
|
|
|
54
|
-
|
|
33
|
+
Requires a working test command in your project. Quarantine records and gates follow project configuration.
|
|
55
34
|
|
|
56
|
-
|
|
35
|
+
Detailed configuration, tool arguments and troubleshooting are in the [usage guide](docs/USAGE.en.md). For standalone development, follow the Node requirement in [package.json](package.json).
|
|
57
36
|
|
|
58
|
-
##
|
|
37
|
+
## Documentation
|
|
59
38
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
39
|
+
- [Usage and troubleshooting](docs/USAGE.en.md)
|
|
40
|
+
- [Changelog](CHANGELOG.md)
|
|
41
|
+
- [Validation scope and history](docs/VALIDATION.md)
|
|
42
|
+
- [Report a problem or suggest a feature](https://github.com/STARDUSTLC666/dsh-flakefinder/issues)
|
|
43
|
+
|
|
44
|
+
## License
|
|
64
45
|
|
|
65
|
-
MIT
|
|
46
|
+
[MIT](LICENSE)
|
package/README.md
CHANGED
|
@@ -1,122 +1,46 @@
|
|
|
1
1
|
# dsh-flakefinder
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[English](README.en.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+

|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
重复运行测试,找出偶发失败用例并保留调查记录。
|
|
8
8
|
|
|
9
|
-
[](https://www.npmjs.com/package/dsh-flakefinder) [](https://www.npmjs.com/package/dsh-flakefinder)
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## 功能
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
| 工具 | 作用 | 写操作 |
|
|
18
|
-
| :-- | :-- | :-- |
|
|
19
|
-
| `flaky_detect` | 重复运行测试 N 次,判定 stable-pass / stable-fail / flaky | 否(写历史) |
|
|
20
|
-
| `flaky_history` | 查询检测历史,按目标过滤 | 否 |
|
|
21
|
-
| `flaky_report` | 汇总隔离清单 + 历史,输出稳定性报告 | 否 |
|
|
22
|
-
| `flaky_quarantine` | 把 flaky 用例写入 `.flakefinder.json` 隔离清单 | 是(审批门) |
|
|
23
|
-
| `flaky_clear` | 从隔离清单移除已恢复的用例 | 是(审批门) |
|
|
24
|
-
|
|
25
|
-
支持框架:
|
|
26
|
-
|
|
27
|
-
- vitest:读取 `node_modules/vitest/vitest.mjs`,使用 JSON reporter
|
|
28
|
-
- jest:读取 `node_modules/jest/bin/jest.js`,使用 `--json --outputFile`
|
|
29
|
-
- pytest:`python -m pytest --junitxml`,解析 JUnit XML(支持 setup.cfg / pyproject / pytest.ini / tox.ini 探测与 XML 实体解码)
|
|
30
|
-
- node:test:`node --test --test-reporter=tap`,解析 TAP(含 SKIP 指令归类)
|
|
31
|
-
- `framework: auto` 按 vitest → jest → pytest → node:test 自动探测
|
|
32
|
-
|
|
33
|
-
## 兼容性
|
|
34
|
-
|
|
35
|
-
已在官方 `@deepseek-ai/dsh@0.1.5-rc.1`、Node `24.16.0` 上验证(2026-09-11):18 个组件与 Modlens 同载,工具 schema、技能注册及离线只读调用检查通过。采用 `cordis.patch.yml` + `dsh.bundle.patch` 组合包模型。Node 要求与该版本 Harness 一致:22.19 及以上的 22.x,或 24 及以上。外部服务的实际业务操作需按各组件配置单独验证。
|
|
36
|
-
|
|
37
|
-
2026-09-13 修复:保留宿主 `subprocess.spawn` 的服务对象,避免方法独立传递后因 `this` 丢失而报错。已在官方 `0.1.5-rc.1` 和 `0.1.5-rc.2`、Node `24.16.0` 上验证。`flaky_detect` 已通过真实宿主进程服务连续运行三轮 Node 测试,正确返回 `stable-pass`。
|
|
13
|
+
- 支持 Vitest、Jest、pytest 和 node:test。
|
|
14
|
+
- 记录重复运行结果与失败历史。
|
|
15
|
+
- 生成和维护用例隔离清单。
|
|
38
16
|
|
|
39
17
|
## 安装
|
|
40
18
|
|
|
41
|
-
|
|
42
|
-
dsh plugin --profile web add dsh-flakefinder
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
或手动安装后在 profile 的 `cordis.patch.yml` 插入:
|
|
46
|
-
|
|
47
|
-
```yaml
|
|
48
|
-
- id: flakefinder
|
|
49
|
-
name: 'dsh-flakefinder'
|
|
50
|
-
config:
|
|
51
|
-
defaultRuns: 5
|
|
52
|
-
writeApproval: true
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## 卸载
|
|
19
|
+
桌面版可在「插件」面板按包名 `dsh-flakefinder` 安装。已配置 dsh 命令时也可使用:
|
|
56
20
|
|
|
57
21
|
```bash
|
|
58
|
-
dsh plugin --profile
|
|
22
|
+
dsh plugin --profile desktop add dsh-flakefinder
|
|
59
23
|
```
|
|
60
24
|
|
|
61
|
-
|
|
25
|
+
网页版把命令中的 `desktop` 改为 `web`。安装后重启 DSH。
|
|
62
26
|
|
|
27
|
+
## 开始使用
|
|
63
28
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
```text
|
|
67
|
-
用户:src/checkout.test.ts 最近老失败,帮我判断一下
|
|
68
|
-
Agent:
|
|
69
|
-
flaky_detect(target="src/checkout.test.ts", runs=5)
|
|
70
|
-
→ 判定:flaky;3/5 通过,失败集中在 useFakeTimers 用例
|
|
71
|
-
→ flaky_quarantine(tests=["src/checkout.test.ts > 定时器恢复"], reason="定时器竞态")
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## 配置
|
|
75
|
-
|
|
76
|
-
| 字段 | 默认 | 说明 |
|
|
77
|
-
| :-- | :-- | :-- |
|
|
78
|
-
| `defaultRuns` | `5` | `flaky_detect` 默认重复次数(3-20) |
|
|
79
|
-
| `maxRuns` | `20` | 单次允许的最大重复次数(3-50) |
|
|
80
|
-
| `timeoutMs` | `120000` | 单轮测试运行超时 |
|
|
81
|
-
| `graceMs` | `10000` | 超时后宽限 |
|
|
82
|
-
| `writeApproval` | `true` | 隔离清单写操作是否走审批门 |
|
|
83
|
-
| `dataDir` | `DSH_HOME/.dsh-flakefinder` | 历史存储目录 |
|
|
84
|
-
| `quarantineFile` | `<cwd>/.flakefinder.json` | 隔离清单路径 |
|
|
85
|
-
| `pythonPath` | `DSH_FLAKEFINDER_PYTHON` 或 `python`/`python3` | pytest 使用的 Python 解释器 |
|
|
86
|
-
|
|
87
|
-
## 隔离清单格式
|
|
88
|
-
|
|
89
|
-
```json
|
|
90
|
-
{
|
|
91
|
-
"version": 1,
|
|
92
|
-
"quarantined": [
|
|
93
|
-
{
|
|
94
|
-
"file": "src/checkout.test.ts",
|
|
95
|
-
"name": "使用假定时器后恢复真实定时器",
|
|
96
|
-
"reason": "定时器竞态,见 issue #12",
|
|
97
|
-
"since": "2026-08-16T00:00:00.000Z"
|
|
98
|
-
}
|
|
99
|
-
]
|
|
100
|
-
}
|
|
101
|
-
```
|
|
29
|
+
可说:“把这组测试重复运行十次,找出偶发失败的用例并总结差异。”
|
|
102
30
|
|
|
103
|
-
|
|
31
|
+
## 依赖与配置
|
|
104
32
|
|
|
105
|
-
|
|
33
|
+
需要项目已有可执行的测试命令。隔离清单用于记录问题,具体门禁按项目配置。
|
|
106
34
|
|
|
107
|
-
|
|
108
|
-
- 测试进程走 DSH 官方 subprocess 服务,argv 数组、无 shell
|
|
109
|
-
- 全量单测 40+:解析、判定、存储、审批门、pytest 计划、subprocess 超时、注册与 manifest
|
|
110
|
-
- `pnpm test`:构建 + `node --test`
|
|
35
|
+
详细配置、工具参数与排错见[使用说明](docs/USAGE.md)。从源码独立开发时,Node 要求以 [package.json](package.json) 为准。
|
|
111
36
|
|
|
112
|
-
##
|
|
37
|
+
## 文档
|
|
113
38
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
5. manifest 自检(bundle patch / exports / files 白名单)
|
|
39
|
+
- [使用与排错](docs/USAGE.md)
|
|
40
|
+
- [更新记录](CHANGELOG.md)
|
|
41
|
+
- [验证范围与历史记录](docs/VALIDATION.md)
|
|
42
|
+
- [问题反馈与功能建议](https://github.com/STARDUSTLC666/dsh-flakefinder/issues)
|
|
119
43
|
|
|
120
44
|
## License
|
|
121
45
|
|
|
122
|
-
MIT
|
|
46
|
+
[MIT](LICENSE)
|
package/docs/USAGE.en.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# dsh-flakefinder usage guide
|
|
2
|
+
|
|
3
|
+
[Overview](../README.en.md) · [Changelog](../CHANGELOG.md) · [Validation](VALIDATION.md)
|
|
4
|
+
|
|
5
|
+
## Current improvements
|
|
6
|
+
|
|
7
|
+
Relative quarantineFile defaults to .flakefinder.json in the current workspace. Cancellation stops the current process. Concurrent writers preserve history and quarantine entries; damaged files are preserved with explicit errors for inspection.
|
|
8
|
+
|
|
9
|
+
## Tools
|
|
10
|
+
|
|
11
|
+
| Tool | Purpose | Write |
|
|
12
|
+
| :-- | :-- | :-- |
|
|
13
|
+
| `flaky_detect` | Repeat tests N times and classify stable-pass / stable-fail / flaky | history only |
|
|
14
|
+
| `flaky_history` | Query detection history, filter by target | no |
|
|
15
|
+
| `flaky_report` | Combine quarantine manifest and history into a report | no |
|
|
16
|
+
| `flaky_quarantine` | Write flaky cases to `.flakefinder.json` | yes (approval) |
|
|
17
|
+
| `flaky_clear` | Remove recovered cases from the manifest | yes (approval) |
|
|
18
|
+
|
|
19
|
+
Frameworks: vitest / jest / pytest (setup.cfg / pyproject / pytest.ini / tox.ini) / node:test, auto-detected in that order. JUnit XML entities and TAP SKIP directives are parsed.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
dsh plugin --profile web add dsh-flakefinder
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Uninstall
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
dsh plugin --profile web remove dsh-flakefinder
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then restart the web service. To clean up fully, also remove the plugin entry from your profile `cordis.patch.yml` if you overrode it.
|
|
34
|
+
|
|
35
|
+
## Example
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
User: src/checkout.test.ts keeps failing recently, is it real?
|
|
39
|
+
Agent:
|
|
40
|
+
flaky_detect(target="src/checkout.test.ts", runs=5)
|
|
41
|
+
-> verdict: flaky (3/5 passed; failures cluster on fake timers)
|
|
42
|
+
-> flaky_quarantine(tests=["src/checkout.test.ts > timer restore"], reason="timer race")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Config
|
|
46
|
+
|
|
47
|
+
See `cordis.patch.yml`; defaults: `defaultRuns=5`, `maxRuns=20`, `timeoutMs=120000`, `writeApproval=true`, `pythonPath=python` (`python3` on Linux/macOS; used for pytest runs).
|
|
48
|
+
|
|
49
|
+
## Development
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pnpm test
|
|
53
|
+
pnpm typecheck
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
MIT
|
package/docs/USAGE.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# dsh-flakefinder 使用说明
|
|
2
|
+
|
|
3
|
+
[返回简介](../README.md) · [更新记录](../CHANGELOG.md) · [验证记录](VALIDATION.md)
|
|
4
|
+
|
|
5
|
+
## 本次改进
|
|
6
|
+
|
|
7
|
+
相对 quarantineFile 默认在当前工作区的 .flakefinder.json。取消会停止当前测试进程;同一数据目录的并发写入不会丢失历史或隔离条目。损坏文件会报错并保留,先备份后检查。
|
|
8
|
+
|
|
9
|
+
## 工具
|
|
10
|
+
|
|
11
|
+
| 工具 | 作用 | 写操作 |
|
|
12
|
+
| :-- | :-- | :-- |
|
|
13
|
+
| `flaky_detect` | 重复运行测试 N 次,判定 stable-pass / stable-fail / flaky | 否(写历史) |
|
|
14
|
+
| `flaky_history` | 查询检测历史,按目标过滤 | 否 |
|
|
15
|
+
| `flaky_report` | 汇总隔离清单 + 历史,输出稳定性报告 | 否 |
|
|
16
|
+
| `flaky_quarantine` | 把 flaky 用例写入 `.flakefinder.json` 隔离清单 | 是(审批门) |
|
|
17
|
+
| `flaky_clear` | 从隔离清单移除已恢复的用例 | 是(审批门) |
|
|
18
|
+
|
|
19
|
+
支持框架:
|
|
20
|
+
|
|
21
|
+
- vitest:读取 `node_modules/vitest/vitest.mjs`,使用 JSON reporter
|
|
22
|
+
- jest:读取 `node_modules/jest/bin/jest.js`,使用 `--json --outputFile`
|
|
23
|
+
- pytest:`python -m pytest --junitxml`,解析 JUnit XML(支持 setup.cfg / pyproject / pytest.ini / tox.ini 探测与 XML 实体解码)
|
|
24
|
+
- node:test:`node --test --test-reporter=tap`,解析 TAP(含 SKIP 指令归类)
|
|
25
|
+
- `framework: auto` 按 vitest → jest → pytest → node:test 自动探测
|
|
26
|
+
|
|
27
|
+
## 安装
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
dsh plugin --profile web add dsh-flakefinder
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
或手动安装后在 profile 的 `cordis.patch.yml` 插入:
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
- id: flakefinder
|
|
37
|
+
name: 'dsh-flakefinder'
|
|
38
|
+
config:
|
|
39
|
+
defaultRuns: 5
|
|
40
|
+
writeApproval: true
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 卸载
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
dsh plugin --profile web remove dsh-flakefinder
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
卸载后重启 Web 服务。如需彻底清理,可再手动删除自己 profile `cordis.patch.yml` 中覆盖的插件行。
|
|
50
|
+
|
|
51
|
+
## 使用示例
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
用户:src/checkout.test.ts 最近老失败,帮我判断一下
|
|
55
|
+
Agent:
|
|
56
|
+
flaky_detect(target="src/checkout.test.ts", runs=5)
|
|
57
|
+
→ 判定:flaky;3/5 通过,失败集中在 useFakeTimers 用例
|
|
58
|
+
→ flaky_quarantine(tests=["src/checkout.test.ts > 定时器恢复"], reason="定时器竞态")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 配置
|
|
62
|
+
|
|
63
|
+
| 字段 | 默认 | 说明 |
|
|
64
|
+
| :-- | :-- | :-- |
|
|
65
|
+
| `defaultRuns` | `5` | `flaky_detect` 默认重复次数(3-20) |
|
|
66
|
+
| `maxRuns` | `20` | 单次允许的最大重复次数(3-50) |
|
|
67
|
+
| `timeoutMs` | `120000` | 单轮测试运行超时 |
|
|
68
|
+
| `graceMs` | `10000` | 超时后宽限 |
|
|
69
|
+
| `writeApproval` | `true` | 隔离清单写操作是否走审批门 |
|
|
70
|
+
| `dataDir` | `DSH_HOME/.dsh-flakefinder` | 历史存储目录 |
|
|
71
|
+
| `quarantineFile` | `<cwd>/.flakefinder.json` | 隔离清单路径 |
|
|
72
|
+
| `pythonPath` | `DSH_FLAKEFINDER_PYTHON` 或 `python`/`python3` | pytest 使用的 Python 解释器 |
|
|
73
|
+
|
|
74
|
+
## 隔离清单格式
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"version": 1,
|
|
79
|
+
"quarantined": [
|
|
80
|
+
{
|
|
81
|
+
"file": "src/checkout.test.ts",
|
|
82
|
+
"name": "使用假定时器后恢复真实定时器",
|
|
83
|
+
"reason": "定时器竞态,见 issue #12",
|
|
84
|
+
"since": "2026-08-16T00:00:00.000Z"
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
隔离清单只记录,不修改测试源码;agent 跑测试前应先查阅 `flaky_report`。
|
|
91
|
+
|
|
92
|
+
## 工程
|
|
93
|
+
|
|
94
|
+
- Node >= 22.13,TypeScript,仅依赖文件锁库
|
|
95
|
+
- 测试进程走 DSH 官方 subprocess 服务,argv 数组、无 shell
|
|
96
|
+
- 全量单测 40+:解析、判定、存储、审批门、pytest 计划、subprocess 超时、注册与 manifest
|
|
97
|
+
- `pnpm test`:构建 + `node --test`
|
|
98
|
+
|
|
99
|
+
## 发布门禁
|
|
100
|
+
|
|
101
|
+
1. `pnpm test` 全绿
|
|
102
|
+
2. `pnpm typecheck`
|
|
103
|
+
3. 危险模式扫描(eval / child_process / 密钥零容忍)
|
|
104
|
+
4. 真实 profile 冒烟:全新临时 profile 安装插件 → 真 boot → stderr 零报错
|
|
105
|
+
5. manifest 自检(bundle patch / exports / files 白名单)
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# dsh-flakefinder 验证记录
|
|
2
|
+
|
|
3
|
+
本页整理原 README 的历史验证说明,保留当时的版本、日期与范围。自动测试、启动检查、浏览器操作和真实服务验收分别记录,不能相互替代。更详细的版本验收文件仍保留在仓库中。
|
|
4
|
+
|
|
5
|
+
## 原中文记录
|
|
6
|
+
|
|
7
|
+
验证宿主:官方源码构建的 Harness `0.2.0-rc.1`(commit `407e65c8`)+ Node `24.16.0`(2026-09-28)。44 项插件测试在隔离环境全部通过;同一个宿主里 18 个插件共同加载,注册 6 个工具,工具 schema 与健康检查契约通过。本轮未启用真实端口与外部服务。
|
|
8
|
+
|
|
9
|
+
2026-09-13 修复:保留宿主 `subprocess.spawn` 的服务对象,避免方法独立传递后因 `this` 丢失而报错。该路径已在隔离宿主进程服务中实测通过。`flaky_detect` 已通过真实宿主进程服务连续运行三轮 Node 测试,正确返回 `stable-pass`。
|
|
10
|
+
|
|
11
|
+
## Original English record
|
|
12
|
+
|
|
13
|
+
Validation host: Harness `0.2.0-rc.1` built from official sources (commit `407e65c8`) with Node `24.16.0` on 2026-09-28. All 44 plugin tests pass in an isolated environment; all 18 plugins mount together in one host registering 6 tools, with tool schemas and health-check contracts passing. No live ports or external services were exercised in this round.
|
|
14
|
+
|
|
15
|
+
2026-09-13 fix: retain the service receiver when calling `subprocess.spawn`, preventing failures caused by passing the method as an unbound callback. Verified against a real isolated host subprocess service. `flaky_detect` runs three real Node test processes through the host subprocess service and returns `stable-pass`.
|
package/lib/config.d.ts
CHANGED
package/lib/config.js
CHANGED
|
@@ -49,7 +49,8 @@ export function resolveConfig(config, cwd = process.cwd()) {
|
|
|
49
49
|
const pythonPath = typeof cfg.pythonPath === 'string' && cfg.pythonPath.trim() !== ''
|
|
50
50
|
? cfg.pythonPath.trim()
|
|
51
51
|
: (process.env.DSH_FLAKEFINDER_PYTHON?.trim() || (process.platform === 'win32' ? 'python' : 'python3'));
|
|
52
|
-
|
|
52
|
+
const relativeQuarantine = typeof cfg.quarantineFile === 'string' && cfg.quarantineFile.trim() ? cfg.quarantineFile.trim() : '.flakefinder.json';
|
|
53
|
+
return { defaultRuns, maxRuns, timeoutMs, graceMs, writeApproval, dataDir, quarantineFile, pythonPath, ...(path.isAbsolute(relativeQuarantine) ? {} : { quarantineFileRelative: relativeQuarantine }) };
|
|
53
54
|
}
|
|
54
55
|
function readRuns(label, value, lo, hi) {
|
|
55
56
|
if (typeof value !== 'number' || !Number.isFinite(value))
|
package/lib/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 插件导出 apply(ctx, config):注册五个面向模型的工具(flaky_detect / flaky_history /
|
|
5
5
|
* flaky_quarantine / flaky_clear / flaky_report)。测试进程走 DSH 官方 subprocess 服务
|
|
6
|
-
* (argv 数组、无 shell
|
|
6
|
+
* (argv 数组、无 shell),隔离清单写操作默认走宿主审批门。仅依赖文件锁库。
|
|
7
7
|
*
|
|
8
8
|
* @module dsh-flakefinder
|
|
9
9
|
*/
|
package/lib/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 插件导出 apply(ctx, config):注册五个面向模型的工具(flaky_detect / flaky_history /
|
|
5
5
|
* flaky_quarantine / flaky_clear / flaky_report)。测试进程走 DSH 官方 subprocess 服务
|
|
6
|
-
* (argv 数组、无 shell
|
|
6
|
+
* (argv 数组、无 shell),隔离清单写操作默认走宿主审批门。仅依赖文件锁库。
|
|
7
7
|
*
|
|
8
8
|
* @module dsh-flakefinder
|
|
9
9
|
*/
|
package/lib/runner.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface RunResult {
|
|
|
14
14
|
export interface ProcessRunner {
|
|
15
15
|
run(argv: readonly string[], options?: {
|
|
16
16
|
timeoutMs?: number;
|
|
17
|
+
cwd?: string;
|
|
18
|
+
signal?: AbortSignal;
|
|
17
19
|
}): Promise<RunResult>;
|
|
18
20
|
}
|
|
19
21
|
export interface SubprocessHandleLike {
|
|
@@ -63,4 +65,4 @@ export interface FlakeRun {
|
|
|
63
65
|
error?: string;
|
|
64
66
|
}
|
|
65
67
|
/** 执行一个测试计划并读取报告;进程失败但报告存在时仍返回报告。 */
|
|
66
|
-
export declare function executePlan(runner: ProcessRunner, plan: TestPlan, index: number, timeoutMs: number): Promise<FlakeRun>;
|
|
68
|
+
export declare function executePlan(runner: ProcessRunner, plan: TestPlan, index: number, timeoutMs: number, cwd?: string, signal?: AbortSignal): Promise<FlakeRun>;
|
package/lib/runner.js
CHANGED
|
@@ -12,19 +12,21 @@ export function createSubprocessRunner(spawn, graceMs, defaultTimeoutMs) {
|
|
|
12
12
|
async run(argv, options) {
|
|
13
13
|
const timeoutMs = options?.timeoutMs ?? defaultTimeoutMs;
|
|
14
14
|
const controller = new AbortController();
|
|
15
|
+
const signal = options?.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
|
|
16
|
+
signal.throwIfAborted();
|
|
15
17
|
const timer = setTimeout(() => controller.abort(new Error('flakefinder test run timed out')), timeoutMs);
|
|
16
18
|
let handle;
|
|
17
19
|
try {
|
|
18
20
|
handle = spawn({
|
|
19
21
|
argv,
|
|
20
|
-
cwd: process.cwd(),
|
|
22
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
21
23
|
stdio: {
|
|
22
24
|
stdin: 'ignore',
|
|
23
25
|
stdout: { maxBytes: COLLECT_BYTES },
|
|
24
26
|
stderr: { maxBytes: COLLECT_BYTES },
|
|
25
27
|
},
|
|
26
28
|
graceMs,
|
|
27
|
-
signal
|
|
29
|
+
signal,
|
|
28
30
|
});
|
|
29
31
|
const outcome = await handle.done;
|
|
30
32
|
const stdout = handle.collected.stdout?.readFrom(0).text ?? '';
|
|
@@ -42,9 +44,11 @@ function stderrTail(stderr) {
|
|
|
42
44
|
return tail || '无错误输出';
|
|
43
45
|
}
|
|
44
46
|
/** 执行一个测试计划并读取报告;进程失败但报告存在时仍返回报告。 */
|
|
45
|
-
export async function executePlan(runner, plan, index, timeoutMs) {
|
|
47
|
+
export async function executePlan(runner, plan, index, timeoutMs, cwd, signal) {
|
|
46
48
|
const started = Date.now();
|
|
47
|
-
|
|
49
|
+
signal?.throwIfAborted();
|
|
50
|
+
const result = await runner.run(plan.argv, { timeoutMs, cwd, signal });
|
|
51
|
+
signal?.throwIfAborted();
|
|
48
52
|
const durationMs = Date.now() - started;
|
|
49
53
|
if (plan.reportKind === 'tap') {
|
|
50
54
|
return {
|
package/lib/store.js
CHANGED
|
@@ -5,31 +5,67 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import fs from 'node:fs/promises';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import lockfile from 'proper-lockfile';
|
|
8
10
|
const HISTORY_FILE = 'history.json';
|
|
9
11
|
const HISTORY_LIMIT = 200;
|
|
10
12
|
export function createStore(dataDir, quarantineFile) {
|
|
11
13
|
const historyPath = path.join(dataDir, HISTORY_FILE);
|
|
14
|
+
async function readJson(file) {
|
|
15
|
+
try {
|
|
16
|
+
const info = await fs.lstat(file);
|
|
17
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > 8 * 1024 * 1024)
|
|
18
|
+
throw new Error('文件不是普通 JSON 文件或超过 8 MiB');
|
|
19
|
+
return JSON.parse(await fs.readFile(file, 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error.code === 'ENOENT')
|
|
23
|
+
return undefined;
|
|
24
|
+
throw new Error('记录无法读取,原文件已保留:' + file + '。' + (error instanceof Error ? error.message : String(error)));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function locked(file, update) {
|
|
28
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
29
|
+
const unlock = await lockfile.lock(file, { realpath: false, retries: { retries: 30, minTimeout: 25, maxTimeout: 100 } });
|
|
30
|
+
try {
|
|
31
|
+
return await update();
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
await unlock();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function atomicWrite(file, value) {
|
|
38
|
+
const bytes = JSON.stringify(value, null, 2) + '\n';
|
|
39
|
+
if (Buffer.byteLength(bytes) > 8 * 1024 * 1024)
|
|
40
|
+
throw new Error('记录超过 8 MiB,本次没有保存。');
|
|
41
|
+
const temp = file + '.tmp-' + randomUUID();
|
|
42
|
+
try {
|
|
43
|
+
await fs.writeFile(temp, bytes, { flag: 'wx', mode: 0o600 });
|
|
44
|
+
await fs.rename(temp, file);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
await fs.unlink(temp).catch(() => { });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
12
50
|
async function ensureDir() {
|
|
13
51
|
await fs.mkdir(dataDir, { recursive: true });
|
|
14
52
|
}
|
|
15
53
|
async function readHistory() {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const parsed = JSON.parse(text);
|
|
19
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
54
|
+
const parsed = await readJson(historyPath);
|
|
55
|
+
if (parsed === undefined)
|
|
22
56
|
return [];
|
|
23
|
-
|
|
57
|
+
if (!Array.isArray(parsed) || parsed.some(entry => !entry || typeof entry.target !== 'string' || typeof entry.timestamp !== 'string' || !Array.isArray(entry.flakyTests)))
|
|
58
|
+
throw new Error('历史记录格式无效,原文件已保留:' + historyPath);
|
|
59
|
+
return parsed;
|
|
24
60
|
}
|
|
25
61
|
async function appendHistory(entry) {
|
|
26
62
|
await ensureDir();
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
63
|
+
return locked(historyPath, async () => {
|
|
64
|
+
const list = await readHistory();
|
|
65
|
+
list.unshift(entry);
|
|
66
|
+
const trimmed = list.slice(0, HISTORY_LIMIT);
|
|
67
|
+
await atomicWrite(historyPath, trimmed);
|
|
68
|
+
});
|
|
33
69
|
}
|
|
34
70
|
async function listHistory(target, limit) {
|
|
35
71
|
const list = await readHistory();
|
|
@@ -37,55 +73,52 @@ export function createStore(dataDir, quarantineFile) {
|
|
|
37
73
|
return filtered.slice(0, limit);
|
|
38
74
|
}
|
|
39
75
|
async function readQuarantine() {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const parsed = JSON.parse(text);
|
|
43
|
-
const obj = (parsed ?? {});
|
|
44
|
-
return {
|
|
45
|
-
version: 1,
|
|
46
|
-
quarantined: Array.isArray(obj.quarantined) ? obj.quarantined : [],
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
76
|
+
const parsed = await readJson(quarantineFile);
|
|
77
|
+
if (parsed === undefined)
|
|
50
78
|
return { version: 1, quarantined: [] };
|
|
51
|
-
|
|
79
|
+
const obj = parsed;
|
|
80
|
+
if (!obj || obj.version !== 1 || !Array.isArray(obj.quarantined) || obj.quarantined.some(entry => !entry || typeof entry.file !== 'string' || !entry.file || entry.name !== null && typeof entry.name !== 'string' || typeof entry.reason !== 'string' || typeof entry.since !== 'string'))
|
|
81
|
+
throw new Error('隔离清单格式无效,原文件已保留:' + quarantineFile);
|
|
82
|
+
return obj;
|
|
52
83
|
}
|
|
53
84
|
async function writeQuarantine(doc) {
|
|
54
85
|
await fs.mkdir(path.dirname(quarantineFile), { recursive: true });
|
|
55
|
-
|
|
56
|
-
await fs.writeFile(tmp, JSON.stringify(doc, null, 2) + '\n', 'utf8');
|
|
57
|
-
await fs.rename(tmp, quarantineFile);
|
|
86
|
+
await atomicWrite(quarantineFile, doc);
|
|
58
87
|
}
|
|
59
88
|
async function addQuarantine(refs, reason) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
existing
|
|
67
|
-
|
|
68
|
-
|
|
89
|
+
return locked(quarantineFile, async () => {
|
|
90
|
+
const doc = await readQuarantine();
|
|
91
|
+
const now = new Date().toISOString();
|
|
92
|
+
const added = [];
|
|
93
|
+
for (const ref of refs) {
|
|
94
|
+
const existing = doc.quarantined.find(item => sameRef(item, ref));
|
|
95
|
+
if (existing !== undefined) {
|
|
96
|
+
existing.reason = reason;
|
|
97
|
+
existing.since = now;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const entry = { file: ref.file, name: ref.name, reason, since: now };
|
|
101
|
+
doc.quarantined.push(entry);
|
|
102
|
+
added.push(entry);
|
|
69
103
|
}
|
|
70
|
-
|
|
71
|
-
doc
|
|
72
|
-
added
|
|
73
|
-
}
|
|
74
|
-
doc.quarantined.sort((a, b) => a.file.localeCompare(b.file) || (a.name ?? '').localeCompare(b.name ?? ''));
|
|
75
|
-
await writeQuarantine(doc);
|
|
76
|
-
return { added, file: quarantineFile };
|
|
104
|
+
doc.quarantined.sort((a, b) => a.file.localeCompare(b.file) || (a.name ?? '').localeCompare(b.name ?? ''));
|
|
105
|
+
await writeQuarantine(doc);
|
|
106
|
+
return { added, file: quarantineFile };
|
|
107
|
+
});
|
|
77
108
|
}
|
|
78
109
|
async function removeQuarantine(refs) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
110
|
+
return locked(quarantineFile, async () => {
|
|
111
|
+
const doc = await readQuarantine();
|
|
112
|
+
const removed = [];
|
|
113
|
+
doc.quarantined = doc.quarantined.filter(item => {
|
|
114
|
+
const hit = refs.some(ref => sameRef(item, ref));
|
|
115
|
+
if (hit)
|
|
116
|
+
removed.push(item);
|
|
117
|
+
return !hit;
|
|
118
|
+
});
|
|
119
|
+
await writeQuarantine(doc);
|
|
120
|
+
return { removed, file: quarantineFile };
|
|
86
121
|
});
|
|
87
|
-
await writeQuarantine(doc);
|
|
88
|
-
return { removed, file: quarantineFile };
|
|
89
122
|
}
|
|
90
123
|
return {
|
|
91
124
|
appendHistory,
|
package/lib/tools.js
CHANGED
|
@@ -8,7 +8,8 @@ import { buildPlan, readFramework } from './args.js';
|
|
|
8
8
|
import { aggregate, flakyTests } from './classify.js';
|
|
9
9
|
import { assertTarget, optionalInteger, optionalString, requiredString, resolveRuns, } from './config.js';
|
|
10
10
|
import { executePlan } from './runner.js';
|
|
11
|
-
import { formatRef, parseRef } from './store.js';
|
|
11
|
+
import { createStore, formatRef, parseRef } from './store.js';
|
|
12
|
+
import { resolve } from 'node:path';
|
|
12
13
|
function compileParameters(spec) {
|
|
13
14
|
const properties = {};
|
|
14
15
|
const required = [];
|
|
@@ -98,6 +99,18 @@ const detectSchema = {
|
|
|
98
99
|
};
|
|
99
100
|
/** 构建五个工具定义。 */
|
|
100
101
|
export function buildFlakeTools(cfg, runner, store) {
|
|
102
|
+
return buildWorkspaceTools(cfg, runner, store).map(tool => ({ ...tool, async execute(args, exec) {
|
|
103
|
+
const context = exec;
|
|
104
|
+
const signal = context?.signal;
|
|
105
|
+
signal?.throwIfAborted();
|
|
106
|
+
const cwd = context?.agent?.session?.header?.cwd || process.cwd();
|
|
107
|
+
const scopedStore = context?.agent?.session?.header?.cwd && cfg.quarantineFileRelative ? createStore(cfg.dataDir, resolve(cwd, cfg.quarantineFileRelative)) : store;
|
|
108
|
+
const scopedRunner = { run: (argv, options) => runner.run(argv, { ...options, cwd, signal }) };
|
|
109
|
+
const selected = buildWorkspaceTools(cfg, scopedRunner, scopedStore, cwd, signal).find(row => row.name === tool.name);
|
|
110
|
+
return selected.execute(args, exec);
|
|
111
|
+
} }));
|
|
112
|
+
}
|
|
113
|
+
function buildWorkspaceTools(cfg, runner, store, cwd = process.cwd(), signal) {
|
|
101
114
|
const detect = {
|
|
102
115
|
name: 'flaky_detect',
|
|
103
116
|
description: '重复运行测试多次并判定稳定性:stable-pass(全过)/ stable-fail(全挂)/ flaky(时好时坏)。支持 vitest / jest / pytest / node:test,自动探测本地框架。结果写入历史供 flaky_report 分析。',
|
|
@@ -119,11 +132,12 @@ export function buildFlakeTools(cfg, runner, store) {
|
|
|
119
132
|
const target = assertTarget(requiredString(args, 'target', '测试目标'));
|
|
120
133
|
const requested = readFramework(args.framework);
|
|
121
134
|
const runs = resolveRuns(args.runs, cfg);
|
|
122
|
-
const plans = Array.from({ length: runs }, (_, index) => buildPlan(
|
|
135
|
+
const plans = Array.from({ length: runs }, (_, index) => buildPlan(cwd, target, requested, index, cfg.pythonPath));
|
|
123
136
|
const framework = plans[0]?.framework ?? 'node';
|
|
124
137
|
const executed = [];
|
|
125
138
|
for (let index = 0; index < plans.length; index += 1) {
|
|
126
|
-
|
|
139
|
+
signal?.throwIfAborted();
|
|
140
|
+
executed.push(await executePlan(runner, plans[index], index + 1, cfg.timeoutMs, cwd, signal));
|
|
127
141
|
}
|
|
128
142
|
const result = aggregate(executed.map(item => ({ index: item.index, report: item.report, durationMs: item.durationMs, error: item.error })));
|
|
129
143
|
const flaky = flakyTests(result);
|
|
@@ -140,6 +154,7 @@ export function buildFlakeTools(cfg, runner, store) {
|
|
|
140
154
|
skippedCount: result.skippedCount,
|
|
141
155
|
flakyTests: flaky,
|
|
142
156
|
};
|
|
157
|
+
signal?.throwIfAborted();
|
|
143
158
|
await store.appendHistory(entry);
|
|
144
159
|
return {
|
|
145
160
|
target,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-flakefinder",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.5",
|
|
4
|
+
"description": "重复运行测试,找出偶发失败用例并保留调查记录。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
"lib",
|
|
18
18
|
"cordis.patch.yml",
|
|
19
19
|
"README.md",
|
|
20
|
-
"README.en.md"
|
|
20
|
+
"README.en.md",
|
|
21
|
+
"docs",
|
|
22
|
+
"CHANGELOG.md"
|
|
21
23
|
],
|
|
22
24
|
"scripts": {
|
|
23
25
|
"build": "tsc -p tsconfig.json",
|
|
@@ -45,6 +47,7 @@
|
|
|
45
47
|
},
|
|
46
48
|
"devDependencies": {
|
|
47
49
|
"@types/node": "^24.0.0",
|
|
50
|
+
"@types/proper-lockfile": "4.1.4",
|
|
48
51
|
"typescript": "^5.6.0"
|
|
49
52
|
},
|
|
50
53
|
"repository": {
|
|
@@ -56,5 +59,8 @@
|
|
|
56
59
|
},
|
|
57
60
|
"homepage": "https://github.com/STARDUSTLC666/dsh-flakefinder#readme",
|
|
58
61
|
"author": "stardustlc",
|
|
59
|
-
"packageManager": "pnpm@11.7.0"
|
|
62
|
+
"packageManager": "pnpm@11.7.0",
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"proper-lockfile": "4.1.2"
|
|
65
|
+
}
|
|
60
66
|
}
|