@dommaker/harness 1.2.2 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/dist/test-setup/mkdtemp-cleanup.d.ts +9 -0
- package/dist/test-setup/mkdtemp-cleanup.d.ts.map +1 -0
- package/dist/test-setup/mkdtemp-cleanup.js +90 -0
- package/dist/test-setup/mkdtemp-cleanup.js.map +1 -0
- package/package.json +1 -1
- package/src/test-setup/__tests__/mkdtemp-cleanup.test.ts +45 -0
- package/src/test-setup/mkdtemp-cleanup.ts +83 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.2.3] - 2026-08-28
|
|
10
|
+
|
|
11
|
+
### Changes
|
|
12
|
+
- fix(test): mkdtemp 泄漏防护——setupFilesAfterEnv 劫持 mkdtempSync 统一清理
|
|
13
|
+
|
|
9
14
|
## [1.2.2] - 2026-08-26
|
|
10
15
|
|
|
11
16
|
### Changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** 幂等。setup 与测试文件重复调用安全。 */
|
|
2
|
+
export declare function patchMktempCleanup(): void;
|
|
3
|
+
export declare function isRegisteredTmpDir(dir: string): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* 清扫 tmpDir 中超龄且符合 mkdtemp 签名的目录。返回删除数。
|
|
6
|
+
* 单条失败(竞态被别进程先删等)不阻断。
|
|
7
|
+
*/
|
|
8
|
+
export declare function sweepStaleMkdtempDirs(tmpDir: string, ageMs: number): number;
|
|
9
|
+
//# sourceMappingURL=mkdtemp-cleanup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mkdtemp-cleanup.d.ts","sourceRoot":"","sources":["../../src/test-setup/mkdtemp-cleanup.ts"],"names":[],"mappings":"AAwBA,4BAA4B;AAC5B,wBAAgB,kBAAkB,IAAI,IAAI,CAoBzC;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAuB3E"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.patchMktempCleanup = patchMktempCleanup;
|
|
7
|
+
exports.isRegisteredTmpDir = isRegisteredTmpDir;
|
|
8
|
+
exports.sweepStaleMkdtempDirs = sweepStaleMkdtempDirs;
|
|
9
|
+
/**
|
|
10
|
+
* Jest setupFilesAfterEnv - mkdtemp 泄漏防护(2026-08-26 /tmp 3.9 万残留事故根因修复)
|
|
11
|
+
*
|
|
12
|
+
* 事故:5 个测试文件 fs.mkdtempSync 后零清理(constraints-retire / check-drift /
|
|
13
|
+
* constraints-report / agent-prompt-renderer / injection-drift),日常 jest 运行累积。
|
|
14
|
+
*
|
|
15
|
+
* 机制(studio apps/api/tests/mkdtemp-cleanup.ts 同款,jest 版):
|
|
16
|
+
* 1) patch fs.mkdtempSync--本进程创建的临时目录全部进注册表,afterAll 统一 rmSync。
|
|
17
|
+
* jest worker 被 kill 时 process 'exit' 不触发,必须用 afterAll(setupFilesAfterEnv
|
|
18
|
+
* 里注册 = 文件级钩子)。
|
|
19
|
+
* 2) import 时清扫 tmpdir 里 >24h 且符合 mkdtemp 签名的目录,兜底 worker 被 kill 的场景。
|
|
20
|
+
*
|
|
21
|
+
* 签名正则只匹配「小写字母数字连字符前缀 + 恰好 6 位大小写字母数字后缀」:
|
|
22
|
+
* chrome crashpad(org.chromium.Chromium.XXXX,含点)、tsx-0 / jest_0 等不匹配。
|
|
23
|
+
*/
|
|
24
|
+
const fs_1 = __importDefault(require("fs"));
|
|
25
|
+
const path_1 = __importDefault(require("path"));
|
|
26
|
+
const globals_1 = require("@jest/globals");
|
|
27
|
+
const MKDTEMP_SUFFIX_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*-[A-Za-z0-9]{6}$/;
|
|
28
|
+
let patched = false;
|
|
29
|
+
const registeredDirs = new Set();
|
|
30
|
+
/** 幂等。setup 与测试文件重复调用安全。 */
|
|
31
|
+
function patchMktempCleanup() {
|
|
32
|
+
if (patched)
|
|
33
|
+
return;
|
|
34
|
+
patched = true;
|
|
35
|
+
const orig = fs_1.default.mkdtempSync;
|
|
36
|
+
fs_1.default.mkdtempSync = ((prefix) => {
|
|
37
|
+
const dir = orig(prefix);
|
|
38
|
+
registeredDirs.add(dir);
|
|
39
|
+
return dir;
|
|
40
|
+
});
|
|
41
|
+
(0, globals_1.afterAll)(() => {
|
|
42
|
+
for (const dir of registeredDirs) {
|
|
43
|
+
try {
|
|
44
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// 清理失败不阻断测试
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
registeredDirs.clear();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function isRegisteredTmpDir(dir) {
|
|
54
|
+
return registeredDirs.has(dir);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 清扫 tmpDir 中超龄且符合 mkdtemp 签名的目录。返回删除数。
|
|
58
|
+
* 单条失败(竞态被别进程先删等)不阻断。
|
|
59
|
+
*/
|
|
60
|
+
function sweepStaleMkdtempDirs(tmpDir, ageMs) {
|
|
61
|
+
let entries;
|
|
62
|
+
try {
|
|
63
|
+
entries = fs_1.default.readdirSync(tmpDir);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
let removed = 0;
|
|
70
|
+
for (const name of entries) {
|
|
71
|
+
if (!MKDTEMP_SUFFIX_RE.test(name))
|
|
72
|
+
continue;
|
|
73
|
+
try {
|
|
74
|
+
const p = path_1.default.join(tmpDir, name);
|
|
75
|
+
const st = fs_1.default.statSync(p);
|
|
76
|
+
if (st.isDirectory() && now - st.mtimeMs > ageMs) {
|
|
77
|
+
fs_1.default.rmSync(p, { recursive: true, force: true });
|
|
78
|
+
removed++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// 单条竞态不阻断
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return removed;
|
|
86
|
+
}
|
|
87
|
+
// setupFilesAfterEnv 顶层执行:打补丁 + 清扫历史残留
|
|
88
|
+
patchMktempCleanup();
|
|
89
|
+
sweepStaleMkdtempDirs(process.env.TMPDIR || '/tmp', 24 * 60 * 60 * 1000);
|
|
90
|
+
//# sourceMappingURL=mkdtemp-cleanup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mkdtemp-cleanup.js","sourceRoot":"","sources":["../../src/test-setup/mkdtemp-cleanup.ts"],"names":[],"mappings":";;;;;AAyBA,gDAoBC;AAED,gDAEC;AAMD,sDAuBC;AA9ED;;;;;;;;;;;;;;GAcG;AACH,4CAAoB;AACpB,gDAAwB;AACxB,2CAAyC;AAEzC,MAAM,iBAAiB,GAAG,gDAAgD,CAAC;AAE3E,IAAI,OAAO,GAAG,KAAK,CAAC;AACpB,MAAM,cAAc,GAAG,IAAI,GAAG,EAAU,CAAC;AAEzC,4BAA4B;AAC5B,SAAgB,kBAAkB;IAChC,IAAI,OAAO;QAAE,OAAO;IACpB,OAAO,GAAG,IAAI,CAAC;IACf,MAAM,IAAI,GAAG,YAAE,CAAC,WAAyC,CAAC;IAC1D,YAAE,CAAC,WAAW,GAAG,CAAC,CAAC,MAAc,EAAE,EAAE;QACnC,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;QACzB,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACxB,OAAO,GAAG,CAAC;IACb,CAAC,CAA0B,CAAC;IAE5B,IAAA,kBAAQ,EAAC,GAAG,EAAE;QACZ,KAAK,MAAM,GAAG,IAAI,cAAc,EAAE,CAAC;YACjC,IAAI,CAAC;gBACH,YAAE,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACnD,CAAC;YAAC,MAAM,CAAC;gBACP,YAAY;YACd,CAAC;QACH,CAAC;QACD,cAAc,CAAC,KAAK,EAAE,CAAC;IACzB,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAgB,kBAAkB,CAAC,GAAW;IAC5C,OAAO,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AACjC,CAAC;AAED;;;GAGG;AACH,SAAgB,qBAAqB,CAAC,MAAc,EAAE,KAAa;IACjE,IAAI,OAAiB,CAAC;IACtB,IAAI,CAAC;QACH,OAAO,GAAG,YAAE,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,CAAC;IACX,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,SAAS;QAC5C,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,cAAI,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;YAClC,MAAM,EAAE,GAAG,YAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;YAC1B,IAAI,EAAE,CAAC,WAAW,EAAE,IAAI,GAAG,GAAG,EAAE,CAAC,OAAO,GAAG,KAAK,EAAE,CAAC;gBACjD,YAAE,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;gBAC/C,OAAO,EAAE,CAAC;YACZ,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,UAAU;QACZ,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,uCAAuC;AACvC,kBAAkB,EAAE,CAAC;AACrB,qBAAqB,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,IAAI,MAAM,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mkdtemp 泄漏防护测试(2026-08-26 /tmp 3.9 万残留事故,与 studio 同款机制)。
|
|
3
|
+
* 正本见 ../mkdtemp-cleanup.ts。
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import {
|
|
8
|
+
patchMktempCleanup,
|
|
9
|
+
isRegisteredTmpDir,
|
|
10
|
+
sweepStaleMkdtempDirs,
|
|
11
|
+
} from '../mkdtemp-cleanup';
|
|
12
|
+
|
|
13
|
+
describe('mkdtemp-cleanup', () => {
|
|
14
|
+
it('patch 后 mkdtempSync 创建的目录进注册表(幂等)', () => {
|
|
15
|
+
patchMktempCleanup();
|
|
16
|
+
const d = fs.mkdtempSync(path.join('/tmp', 'harness-mkdtemp-cleanup-test-'));
|
|
17
|
+
expect(fs.existsSync(d)).toBe(true);
|
|
18
|
+
expect(isRegisteredTmpDir(d)).toBe(true);
|
|
19
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('sweep 只删超过年龄阈值且符合 mkdtemp 签名的目录', () => {
|
|
23
|
+
const base = fs.mkdtempSync(path.join('/tmp', 'harness-mkdtemp-cleanup-sweep-'));
|
|
24
|
+
const cases: Array<{ name: string; ageH: number; shouldRemove: boolean }> = [
|
|
25
|
+
{ name: 'fake-test-aB3xY9', ageH: 25, shouldRemove: true }, // 签名匹配 + 超龄 -> 删
|
|
26
|
+
{ name: 'fake-test-cD4zW0', ageH: 1, shouldRemove: false }, // 签名匹配但新鲜 -> 留
|
|
27
|
+
{ name: 'org.chromium.Chromium.RJ5vUP', ageH: 25, shouldRemove: false }, // 含点(chrome crashpad)-> 不匹配
|
|
28
|
+
{ name: 'fake-test-ab12', ageH: 25, shouldRemove: false }, // 后缀 4 字符 -> 不匹配
|
|
29
|
+
{ name: 'tsx-0', ageH: 25, shouldRemove: false }, // 后缀 1 字符 -> 不匹配
|
|
30
|
+
];
|
|
31
|
+
for (const c of cases) {
|
|
32
|
+
const p = path.join(base, c.name);
|
|
33
|
+
fs.mkdirSync(p);
|
|
34
|
+
const t = new Date(Date.now() - c.ageH * 3600 * 1000);
|
|
35
|
+
fs.utimesSync(p, t, t);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const removed = sweepStaleMkdtempDirs(base, 24 * 3600 * 1000);
|
|
39
|
+
expect(removed).toBe(1);
|
|
40
|
+
for (const c of cases) {
|
|
41
|
+
expect(fs.existsSync(path.join(base, c.name))).toBe(!c.shouldRemove);
|
|
42
|
+
}
|
|
43
|
+
fs.rmSync(base, { recursive: true, force: true });
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jest setupFilesAfterEnv - mkdtemp 泄漏防护(2026-08-26 /tmp 3.9 万残留事故根因修复)
|
|
3
|
+
*
|
|
4
|
+
* 事故:5 个测试文件 fs.mkdtempSync 后零清理(constraints-retire / check-drift /
|
|
5
|
+
* constraints-report / agent-prompt-renderer / injection-drift),日常 jest 运行累积。
|
|
6
|
+
*
|
|
7
|
+
* 机制(studio apps/api/tests/mkdtemp-cleanup.ts 同款,jest 版):
|
|
8
|
+
* 1) patch fs.mkdtempSync--本进程创建的临时目录全部进注册表,afterAll 统一 rmSync。
|
|
9
|
+
* jest worker 被 kill 时 process 'exit' 不触发,必须用 afterAll(setupFilesAfterEnv
|
|
10
|
+
* 里注册 = 文件级钩子)。
|
|
11
|
+
* 2) import 时清扫 tmpdir 里 >24h 且符合 mkdtemp 签名的目录,兜底 worker 被 kill 的场景。
|
|
12
|
+
*
|
|
13
|
+
* 签名正则只匹配「小写字母数字连字符前缀 + 恰好 6 位大小写字母数字后缀」:
|
|
14
|
+
* chrome crashpad(org.chromium.Chromium.XXXX,含点)、tsx-0 / jest_0 等不匹配。
|
|
15
|
+
*/
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import { afterAll } from '@jest/globals';
|
|
19
|
+
|
|
20
|
+
const MKDTEMP_SUFFIX_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*-[A-Za-z0-9]{6}$/;
|
|
21
|
+
|
|
22
|
+
let patched = false;
|
|
23
|
+
const registeredDirs = new Set<string>();
|
|
24
|
+
|
|
25
|
+
/** 幂等。setup 与测试文件重复调用安全。 */
|
|
26
|
+
export function patchMktempCleanup(): void {
|
|
27
|
+
if (patched) return;
|
|
28
|
+
patched = true;
|
|
29
|
+
const orig = fs.mkdtempSync as (prefix: string) => string;
|
|
30
|
+
fs.mkdtempSync = ((prefix: string) => {
|
|
31
|
+
const dir = orig(prefix);
|
|
32
|
+
registeredDirs.add(dir);
|
|
33
|
+
return dir;
|
|
34
|
+
}) as typeof fs.mkdtempSync;
|
|
35
|
+
|
|
36
|
+
afterAll(() => {
|
|
37
|
+
for (const dir of registeredDirs) {
|
|
38
|
+
try {
|
|
39
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
40
|
+
} catch {
|
|
41
|
+
// 清理失败不阻断测试
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
registeredDirs.clear();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isRegisteredTmpDir(dir: string): boolean {
|
|
49
|
+
return registeredDirs.has(dir);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 清扫 tmpDir 中超龄且符合 mkdtemp 签名的目录。返回删除数。
|
|
54
|
+
* 单条失败(竞态被别进程先删等)不阻断。
|
|
55
|
+
*/
|
|
56
|
+
export function sweepStaleMkdtempDirs(tmpDir: string, ageMs: number): number {
|
|
57
|
+
let entries: string[];
|
|
58
|
+
try {
|
|
59
|
+
entries = fs.readdirSync(tmpDir);
|
|
60
|
+
} catch {
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
let removed = 0;
|
|
65
|
+
for (const name of entries) {
|
|
66
|
+
if (!MKDTEMP_SUFFIX_RE.test(name)) continue;
|
|
67
|
+
try {
|
|
68
|
+
const p = path.join(tmpDir, name);
|
|
69
|
+
const st = fs.statSync(p);
|
|
70
|
+
if (st.isDirectory() && now - st.mtimeMs > ageMs) {
|
|
71
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
72
|
+
removed++;
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// 单条竞态不阻断
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return removed;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// setupFilesAfterEnv 顶层执行:打补丁 + 清扫历史残留
|
|
82
|
+
patchMktempCleanup();
|
|
83
|
+
sweepStaleMkdtempDirs(process.env.TMPDIR || '/tmp', 24 * 60 * 60 * 1000);
|