dsh-flakefinder 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 stardustlc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md CHANGED
@@ -20,12 +20,25 @@ A DeepSeek Harness plugin for test stability: run tests repeatedly, classify fla
20
20
 
21
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
22
 
23
+ ## Compatibility
24
+
25
+ Verified against source-run `@deepseek-ai/dsh@0.1.2-alpha.4` on 2026-09-02. Built for the cordis patch-bundle plugin model (`cordis.patch.yml` + `dsh.bundle.patch`). No runtime imports of `@deepseek-ai/*` internals.
26
+
23
27
  ## Install
24
28
 
25
29
  ```bash
26
30
  dsh plugin --profile web add dsh-flakefinder
27
31
  ```
28
32
 
33
+ ## Uninstall
34
+
35
+ ```bash
36
+ dsh plugin --profile web remove dsh-flakefinder
37
+ ```
38
+
39
+ Then restart the web service. To clean up fully, also remove the plugin entry from your profile `cordis.patch.yml` if you overrode it.
40
+
41
+
29
42
  ## Example
30
43
 
31
44
  ```text
package/README.md CHANGED
@@ -26,6 +26,10 @@ DeepSeek Harness 测试稳定性插件:支持 vitest / jest / pytest / node:te
26
26
  - node:test:`node --test --test-reporter=tap`,解析 TAP(含 SKIP 指令归类)
27
27
  - `framework: auto` 按 vitest → jest → pytest → node:test 自动探测
28
28
 
29
+ ## 兼容性
30
+
31
+ 在 `@deepseek-ai/dsh@0.1.2-alpha.4` 源码模式下验证(2026-09-02)。遵循 cordis 组合包补丁模型(`cordis.patch.yml` + `dsh.bundle.patch`),运行时不 import 任何 `@deepseek-ai/*` 内部模块。
32
+
29
33
  ## 安装
30
34
 
31
35
  ```bash
@@ -42,6 +46,15 @@ dsh plugin --profile web add dsh-flakefinder
42
46
  writeApproval: true
43
47
  ```
44
48
 
49
+ ## 卸载
50
+
51
+ ```bash
52
+ dsh plugin --profile web remove dsh-flakefinder
53
+ ```
54
+
55
+ 卸载后重启 Web 服务。如需彻底清理,可再手动删除自己 profile `cordis.patch.yml` 中覆盖的插件行。
56
+
57
+
45
58
  ## 使用示例
46
59
 
47
60
  ```text
package/cordis.patch.yml CHANGED
@@ -10,7 +10,7 @@
10
10
  # writeApproval: true # quarantine/clear 是否走审批门(默认 true)
11
11
  # dataDir: '' # 历史存储目录(默认 DSH_HOME/.dsh-flakefinder)
12
12
  # quarantineFile: '' # 隔离清单(默认项目根 .flakefinder.json)
13
- pythonPath: '' # pytest 使用的 Python 解释器(默认 python/python3)
13
+ # pythonPath: '' # pytest 使用的 Python 解释器(默认 python/python3)
14
14
  #
15
15
  - insert:
16
16
  - id: flakefinder
package/lib/classify.js CHANGED
@@ -71,7 +71,10 @@ export function aggregate(runs) {
71
71
  }
72
72
  let verdict;
73
73
  const passRuns = records.filter(run => run.success).length;
74
- if (tests.every(test => test.verdict === 'skipped')) {
74
+ if (tests.length === 0) {
75
+ verdict = 'empty';
76
+ }
77
+ else if (tests.every(test => test.verdict === 'skipped')) {
75
78
  verdict = 'skipped';
76
79
  }
77
80
  else if (passRuns === records.length) {
@@ -83,6 +86,24 @@ export function aggregate(runs) {
83
86
  else {
84
87
  verdict = 'flaky';
85
88
  }
89
+ // 运行级 flaky 但无具体 flaky 用例:通常是某次运行崩溃(report=null)未产出报告。
90
+ // 补一条合成记录,避免 flaky 信号丢失(历史写入 verdict=flaky 但 flakyCount=0)。
91
+ if (verdict === 'flaky' && flakyCount === 0) {
92
+ const crashRuns = runs.filter(run => run.report === null).length;
93
+ if (crashRuns > 0) {
94
+ tests.push({
95
+ id: '(crash)',
96
+ file: '(crash)',
97
+ name: '运行崩溃(未产出报告)',
98
+ passes: 0,
99
+ failures: crashRuns,
100
+ skips: 0,
101
+ failureRate: 100,
102
+ verdict: 'flaky',
103
+ });
104
+ flakyCount += 1;
105
+ }
106
+ }
86
107
  tests.sort((a, b) => a.id.localeCompare(b.id));
87
108
  return {
88
109
  runs: records,
package/lib/index.d.ts CHANGED
@@ -11,6 +11,7 @@ import { type FlakeConfig } from './config.js';
11
11
  import { type SubprocessSpawnLike } from './runner.js';
12
12
  import { type FlakeToolDefinition } from './tools.js';
13
13
  /** cordis 服务注入:apply 里要用 ctx.subprocess 与 ctx.tools。 */
14
+ export declare const name = "flakefinder";
14
15
  export declare const inject: string[];
15
16
  /** 插件所需的最小 ctx 面。 */
16
17
  export interface FlakePluginContext {
@@ -18,12 +19,11 @@ export interface FlakePluginContext {
18
19
  spawn: SubprocessSpawnLike;
19
20
  };
20
21
  tools: {
21
- register(definition: FlakeToolDefinition, options?: {
22
- prepend?: boolean;
23
- }): () => void;
22
+ register(definition: FlakeToolDefinition): () => void;
24
23
  };
25
- get?(name: 'approval'): unknown;
26
- on?(event: string, listener: () => void): () => void;
24
+ on?(event: string, listener: (...args: any[]) => unknown, options?: {
25
+ prepend?: boolean;
26
+ }): (() => void) | void;
27
27
  }
28
28
  /**
29
29
  * 插件入口:解析配置、封装执行器与存储、注册五个工具。
package/lib/index.js CHANGED
@@ -12,6 +12,7 @@ import { createSubprocessRunner } from './runner.js';
12
12
  import { createStore } from './store.js';
13
13
  import { buildFlakeTools } from './tools.js';
14
14
  /** cordis 服务注入:apply 里要用 ctx.subprocess 与 ctx.tools。 */
15
+ export const name = 'flakefinder';
15
16
  export const inject = ['subprocess', 'tools'];
16
17
  /**
17
18
  * 插件入口:解析配置、封装执行器与存储、注册五个工具。
@@ -30,17 +31,18 @@ export function apply(ctx, config) {
30
31
  const tools = buildFlakeTools(cfg, runner, store);
31
32
  const disposers = [];
32
33
  for (const definition of tools) {
33
- const wrapped = { ...definition };
34
- if (wrapped.gate !== undefined) {
35
- const original = wrapped.gate.bind(wrapped);
36
- wrapped.gate = async (exec, next) => {
37
- const record = (typeof exec === 'object' && exec !== null ? exec : {});
38
- return original({ ...record, approval: ctx.get?.('approval') }, next);
39
- };
40
- }
41
- disposers.push(ctx.tools.register(wrapped, { prepend: true }));
34
+ disposers.push(ctx.tools.register(definition));
42
35
  }
43
36
  if (typeof ctx.on === 'function') {
37
+ if (cfg.writeApproval) {
38
+ ctx.on('tools/pre-execute', async (exec, next) => {
39
+ if (exec.name === 'flaky_quarantine')
40
+ return { kind: 'ask', reason: '把测试用例写入 .flakefinder.json 隔离清单' };
41
+ if (exec.name === 'flaky_clear')
42
+ return { kind: 'ask', reason: '从 .flakefinder.json 隔离清单移除测试用例' };
43
+ return next();
44
+ }, { prepend: true });
45
+ }
44
46
  ctx.on('dispose', () => {
45
47
  for (const dispose of disposers)
46
48
  dispose();
package/lib/tools.d.ts CHANGED
@@ -24,7 +24,6 @@ export interface FlakeToolDefinition {
24
24
  render(args: unknown, value: unknown): ContentBlock[];
25
25
  };
26
26
  execute(args: unknown, exec: unknown): Promise<unknown>;
27
- gate?(exec: unknown, next: () => Promise<unknown>): Promise<unknown>;
28
27
  timeoutMs?: number;
29
28
  }
30
29
  /** 构建五个工具定义。 */
package/lib/tools.js CHANGED
@@ -297,36 +297,61 @@ export function buildFlakeTools(cfg, runner, store) {
297
297
  },
298
298
  timeoutMs: 10000,
299
299
  };
300
- if (cfg.writeApproval) {
301
- quarantine.gate = approvalGate('flaky_quarantine', '把测试用例写入 .flakefinder.json 隔离清单');
302
- clear.gate = approvalGate('flaky_clear', '从 .flakefinder.json 隔离清单移除测试用例');
303
- }
304
- return [detect, history, quarantine, clear, report];
300
+ const flakyHealth = {
301
+ name: 'flaky_health',
302
+ description: 'dsh-flakefinder 自检:验证 Python 解释器与 pytest 是否可用(pytest 框架必需),并汇总默认配置。遇到问题时先运行本工具定位。',
303
+ parameters: compileParameters({}),
304
+ output: {
305
+ schema: { type: 'object', additionalProperties: true },
306
+ render: (_args, value) => {
307
+ const rec = asRecord(value);
308
+ const checks = Array.isArray(rec.checks) ? rec.checks : [];
309
+ const lines = ['dsh-flakefinder 自检' + (rec.ok === true ? ':正常。' : ':发现问题。')];
310
+ for (const item of checks) {
311
+ const c = asRecord(item);
312
+ lines.push('- ' + c.name + ':' + (c.ok === true ? '✅' : '❌ ' + String(c.detail ?? '')));
313
+ }
314
+ return [{ type: 'text', text: lines.join('\n') }];
315
+ },
316
+ },
317
+ async execute() {
318
+ const checks = [];
319
+ let ok = true;
320
+ try {
321
+ const result = await runner.run([cfg.pythonPath, '--version'], { timeoutMs: 15000 });
322
+ const version = (result.stdout + result.stderr).split(/\r?\n/)[0]?.trim() ?? '';
323
+ if (result.exitCode === 0)
324
+ checks.push({ name: 'python', ok: true, detail: version + '(' + cfg.pythonPath + ')' });
325
+ else {
326
+ ok = false;
327
+ checks.push({ name: 'python', ok: false, detail: '退出码 ' + String(result.exitCode) });
328
+ }
329
+ }
330
+ catch (error) {
331
+ ok = false;
332
+ checks.push({ name: 'python', ok: false, detail: error instanceof Error ? error.message : String(error) });
333
+ }
334
+ try {
335
+ const result = await runner.run([cfg.pythonPath, '-m', 'pytest', '--version'], { timeoutMs: 20000 });
336
+ const version = (result.stdout + result.stderr).split(/\r?\n/)[0]?.trim() ?? '';
337
+ if (result.exitCode === 0)
338
+ checks.push({ name: 'pytest', ok: true, detail: version });
339
+ else {
340
+ ok = false;
341
+ checks.push({ name: 'pytest', ok: false, detail: '未安装或不可用(pip install pytest)' });
342
+ }
343
+ }
344
+ catch (error) {
345
+ ok = false;
346
+ checks.push({ name: 'pytest', ok: false, detail: error instanceof Error ? error.message : String(error) });
347
+ }
348
+ checks.push({ name: '默认配置', ok: true, detail: 'defaultRuns=' + cfg.defaultRuns + ', timeoutMs=' + cfg.timeoutMs });
349
+ return { ok, plugin: 'dsh-flakefinder', checks };
350
+ },
351
+ timeoutMs: 40000,
352
+ };
353
+ return [detect, history, quarantine, clear, report, flakyHealth];
305
354
  }
306
355
  function runsTimeout(cfg) {
307
356
  return Math.min(10 * 60 * 1000, cfg.maxRuns * cfg.timeoutMs + 15000);
308
357
  }
309
- /** 写操作审批门:复用 dsh-docker 的中文拒绝语义。 */
310
- function approvalGate(toolName, action) {
311
- return async (exec, next) => {
312
- const record = (typeof exec === 'object' && exec !== null ? exec : {});
313
- const approval = record.approval;
314
- if (approval === undefined) {
315
- return { kind: 'deny', reason: toolName + ' 需要确认,但当前环境没有审批通道(如 headless)。如确定安全,可在配置中设置 writeApproval: false。' };
316
- }
317
- const outcome = await approval.request({
318
- agent: record.agent,
319
- toolName: record.toolName,
320
- callId: record.callId,
321
- reason: action,
322
- signal: record.signal,
323
- });
324
- if (outcome === 'allowed-once')
325
- return next();
326
- if (outcome === 'cancelled')
327
- return { kind: 'deny', reason: action + ' 被取消,未执行。' };
328
- if (outcome === 'unavailable')
329
- return { kind: 'deny', reason: action + ' 不可用(没有可用的审批界面),未执行。' };
330
- return { kind: 'deny', reason: action + ' 未获批准:要么你拒绝了,要么当前会话处于 Full Access。若确需直接写入,可设置 writeApproval: false(自行承担风险)。' };
331
- };
332
- }
package/package.json CHANGED
@@ -1,61 +1,60 @@
1
- {
2
- "name": "dsh-flakefinder",
3
- "version": "0.2.2",
4
- "description": "DSH 测试稳定性插件:flaky_detect/history/quarantine/clear/report 五工具,支持 vitest/jest/pytest/node:test,重复运行测试并识别不稳定用例,JUnit/TAP 完整解析,JSON 历史与隔离清单,零运行时依赖。",
5
- "type": "module",
6
- "main": "lib/index.js",
7
- "types": "lib/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./lib/index.d.ts",
11
- "default": "./lib/index.js"
12
- },
13
- "./cordis.patch.yml": "./cordis.patch.yml",
14
- "./package.json": "./package.json"
15
- },
16
- "files": [
17
- "lib",
18
- "cordis.patch.yml",
19
- "README.md",
20
- "README.en.md"
21
- ],
22
- "scripts": {
23
- "build": "tsc -p tsconfig.json",
24
- "prepare": "tsc -p tsconfig.json",
25
- "typecheck": "tsc -p tsconfig.json --noEmit",
26
- "test": "pnpm run build && node --test \"test/*.test.mjs\"",
27
- "prepublishOnly": "pnpm run build"
28
- },
29
- "dsh": {
30
- "bundle": {
31
- "patch": "./cordis.patch.yml"
32
- }
33
- },
34
- "keywords": [
35
- "dsh",
36
- "deepseek-harness",
37
- "plugin",
38
- "testing",
39
- "flaky-test",
40
- "quality",
41
- "dsh-plugin"
42
- ],
43
- "license": "MIT",
44
- "engines": {
45
- "node": ">=22.13"
46
- },
47
- "devDependencies": {
48
- "@types/node": "^24.0.0",
49
- "typescript": "^5.6.0"
50
- },
51
- "repository": {
52
- "type": "git",
53
- "url": "git+https://github.com/STARDUSTLC666/dsh-flakefinder.git"
54
- },
55
- "bugs": {
56
- "url": "https://github.com/STARDUSTLC666/dsh-flakefinder/issues"
57
- },
58
- "homepage": "https://github.com/STARDUSTLC666/dsh-flakefinder#readme",
59
- "author": "stardustlc",
60
- "packageManager": "pnpm@11.7.0"
61
- }
1
+ {
2
+ "name": "dsh-flakefinder",
3
+ "version": "0.3.1",
4
+ "description": "DSH 测试稳定性插件:flaky_detect/history/quarantine/clear/report 五工具,支持 vitest/jest/pytest/node:test,重复运行测试并识别不稳定用例,JUnit/TAP 完整解析,JSON 历史与隔离清单,零运行时依赖。",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./cordis.patch.yml": "./cordis.patch.yml",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "lib",
18
+ "cordis.patch.yml",
19
+ "README.md",
20
+ "README.en.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.json",
24
+ "typecheck": "tsc -p tsconfig.json --noEmit",
25
+ "test": "pnpm run build && node --test \"test/*.test.mjs\"",
26
+ "prepublishOnly": "pnpm run build"
27
+ },
28
+ "dsh": {
29
+ "bundle": {
30
+ "patch": "./cordis.patch.yml"
31
+ }
32
+ },
33
+ "keywords": [
34
+ "dsh",
35
+ "deepseek-harness",
36
+ "plugin",
37
+ "testing",
38
+ "flaky-test",
39
+ "quality",
40
+ "dsh-plugin"
41
+ ],
42
+ "license": "MIT",
43
+ "engines": {
44
+ "node": ">=22"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^24.0.0",
48
+ "typescript": "^5.6.0"
49
+ },
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/STARDUSTLC666/dsh-flakefinder.git"
53
+ },
54
+ "bugs": {
55
+ "url": "https://github.com/STARDUSTLC666/dsh-flakefinder/issues"
56
+ },
57
+ "homepage": "https://github.com/STARDUSTLC666/dsh-flakefinder#readme",
58
+ "author": "stardustlc",
59
+ "packageManager": "pnpm@11.7.0"
60
+ }