






















1+#!/usr/bin/env node
2+3+import { execFileSync } from "node:child_process";
4+import { isChangedLaneTestPath } from "./changed-lanes.mjs";
5+import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs";
6+import { runAsScript } from "./lib/ts-guard-utils.mjs";
7+8+const DEFAULT_BASE_REF = "origin/main";
9+const DEFAULT_HEAD_REF = "HEAD";
10+const TEMP_DIR_HELPER_PATH = "test/helpers/temp-dir.ts";
11+const FINDING_PATTERNS = [
12+{
13+pattern: /\bmkdtemp(?:Sync)?\s*\(/u,
14+reason: "new mkdtemp temp directory creation",
15+},
16+{
17+pattern: /\btmp\s*\.\s*dir(?:Sync)?\s*\(/u,
18+reason: "new tmp.dir temp directory creation",
19+},
20+];
21+const TEMP_DIR_ALLOW_COMMENT_RE =
22+/(?:^|\s)(?:\/\/|\/\*|\*|#)\s*openclaw-temp-dir:\s*allow\s+(.+)$/u;
23+24+function usage() {
25+return `Usage: node scripts/report-test-temp-creations.mjs [options]
26+27+Description:
28+ Reports new bare test temp-directory creation patterns in added diff lines.
29+ This is a low-noise migration aid, not a cleanup data-flow checker. It does
30+ not scan existing lines and does not decide whether cleanup is sufficient.
31+ Add "openclaw-temp-dir: allow <reason>" in a same-line or immediately
32+ preceding added comment when a test intentionally needs bare temp creation.
33+ File scope intentionally reuses scripts/changed-lanes.mjs test-path
34+ classification instead of maintaining a separate test-helper heuristic.
35+36+Options:
37+ --base <ref> Base ref for branch diffs. Default: ${DEFAULT_BASE_REF}
38+ --head <ref> Head ref for branch diffs. Default: ${DEFAULT_HEAD_REF}
39+ --no-merge-base Use a two-dot base..head diff for shallow CI checkouts.
40+ --staged Inspect staged changes instead of a branch diff.
41+ --json Print JSON findings to stdout.
42+ --fail-on-findings Exit 1 when findings are present. Default is report-only.
43+ -h, --help Show this help.
44+45+Outputs:
46+ Human mode prints findings to stderr and exits 0 unless --fail-on-findings is set.
47+ GitHub Actions mode prints warning annotations and exits 0 unless --fail-on-findings is set.
48+ JSON mode prints an array of { file, line, reason, source } to stdout.
49+50+Examples:
51+ node scripts/report-test-temp-creations.mjs --base origin/main --head HEAD
52+ node scripts/report-test-temp-creations.mjs --staged --json
53+`;
54+}
55+56+function normalizePath(filePath) {
57+return String(filePath ?? "")
58+.replaceAll("\\", "/")
59+.replace(/^\.\/+/u, "");
60+}
61+62+function shouldInspectFile(filePath) {
63+const normalizedPath = normalizePath(filePath);
64+return normalizedPath !== TEMP_DIR_HELPER_PATH && isChangedLaneTestPath(normalizedPath);
65+}
66+67+function isTruthyEnvFlag(value) {
68+const normalized = String(value ?? "")
69+.trim()
70+.toLowerCase();
71+return normalized !== "" && normalized !== "0" && normalized !== "false" && normalized !== "no";
72+}
73+74+function escapeGithubCommandValue(value) {
75+return String(value).replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
76+}
77+78+function escapeGithubCommandProperty(value) {
79+return escapeGithubCommandValue(value).replaceAll(":", "%3A").replaceAll(",", "%2C");
80+}
81+82+function hasTempDirAllowMarker(source) {
83+const reason = source.match(TEMP_DIR_ALLOW_COMMENT_RE)?.[1]?.trim() ?? "";
84+return reason.length > 0;
85+}
86+87+function isTempDirAllowComment(source) {
88+const trimmed = source.trim();
89+return /^(?:\/\/|\/\*|\*|#)/u.test(trimmed) && hasTempDirAllowMarker(trimmed);
90+}
91+92+export function formatGithubWarning(finding) {
93+const file = escapeGithubCommandProperty(finding.file);
94+const line = escapeGithubCommandProperty(finding.line);
95+const message = escapeGithubCommandValue(
96+`${finding.reason}: prefer test/helpers/temp-dir.ts for new test-owned temp directories.`,
97+);
98+return `::warning file=${file},line=${line}::${message}`;
99+}
100+101+function parseArgs(argv) {
102+const args = {
103+base: DEFAULT_BASE_REF,
104+failOnFindings: false,
105+head: DEFAULT_HEAD_REF,
106+help: false,
107+json: false,
108+noMergeBase: false,
109+staged: false,
110+};
111+return parseFlagArgs(argv, args, [
112+stringFlag("--base", "base"),
113+booleanFlag("--fail-on-findings", "failOnFindings"),
114+stringFlag("--head", "head"),
115+booleanFlag("-h", "help"),
116+booleanFlag("--help", "help"),
117+booleanFlag("--json", "json"),
118+booleanFlag("--no-merge-base", "noMergeBase"),
119+booleanFlag("--staged", "staged"),
120+]);
121+}
122+123+function readDiff(args, cwd = process.cwd()) {
124+const range = args.noMergeBase ? `${args.base}..${args.head}` : `${args.base}...${args.head}`;
125+const diffArgs = args.staged
126+ ? ["diff", "--cached", "--unified=0", "--diff-filter=ACMR", "--"]
127+ : ["diff", "--unified=0", "--diff-filter=ACMR", range, "--"];
128+return execFileSync("git", diffArgs, {
129+ cwd,
130+encoding: "utf8",
131+maxBuffer: 64 * 1024 * 1024,
132+stdio: ["ignore", "pipe", "pipe"],
133+});
134+}
135+136+export function collectTempCreationFindingsFromDiff(diffText) {
137+const findings = [];
138+let currentFile = null;
139+let currentLine = 0;
140+let allowNextLine = null;
141+142+for (const line of diffText.split(/\r?\n/u)) {
143+const fileMatch = line.match(/^\+\+\+ b\/(.+)$/u);
144+if (fileMatch) {
145+currentFile = normalizePath(fileMatch[1]);
146+allowNextLine = null;
147+continue;
148+}
149+if (line === "+++ /dev/null") {
150+currentFile = null;
151+allowNextLine = null;
152+continue;
153+}
154+155+const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/u);
156+if (hunkMatch) {
157+currentLine = Number.parseInt(hunkMatch[1], 10);
158+allowNextLine = null;
159+continue;
160+}
161+162+if (line.startsWith("+") && !line.startsWith("+++")) {
163+if (currentFile && shouldInspectFile(currentFile)) {
164+const source = line.slice(1);
165+const allowed =
166+hasTempDirAllowMarker(source) ||
167+(allowNextLine?.file === currentFile && allowNextLine.line === currentLine);
168+for (const { pattern, reason } of FINDING_PATTERNS) {
169+if (pattern.test(source)) {
170+if (!allowed) {
171+findings.push({
172+file: currentFile,
173+line: currentLine,
174+ reason,
175+source: source.trim(),
176+});
177+}
178+break;
179+}
180+}
181+allowNextLine = isTempDirAllowComment(source)
182+ ? { file: currentFile, line: currentLine + 1 }
183+ : null;
184+}
185+currentLine += 1;
186+continue;
187+}
188+189+if (line.startsWith(" ") || line === "") {
190+allowNextLine = null;
191+currentLine += 1;
192+}
193+}
194+195+return findings;
196+}
197+198+export async function main(argv, io) {
199+const args = parseArgs(argv ?? process.argv.slice(2));
200+const stdout = io?.stdout ?? process.stdout;
201+const stderr = io?.stderr ?? process.stderr;
202+const env = io?.env ?? process.env;
203+if (args.help) {
204+stdout.write(usage());
205+return 0;
206+}
207+208+const findings = collectTempCreationFindingsFromDiff(readDiff(args));
209+if (args.json) {
210+stdout.write(`${JSON.stringify(findings, null, 2)}\n`);
211+} else if (findings.length === 0) {
212+stderr.write("No new bare test temp-directory creation patterns found.\n");
213+} else if (isTruthyEnvFlag(env.GITHUB_ACTIONS)) {
214+for (const finding of findings) {
215+stderr.write(`${formatGithubWarning(finding)}\n`);
216+}
217+} else {
218+stderr.write("New bare test temp-directory creation patterns:\n");
219+for (const finding of findings) {
220+stderr.write(`- ${finding.file}:${finding.line} ${finding.reason}: ${finding.source}\n`);
221+}
222+stderr.write("Prefer test/helpers/temp-dir.ts for new test-owned temp directories.\n");
223+}
224+225+return args.failOnFindings && findings.length > 0 ? 1 : 0;
226+}
227+228+runAsScript(import.meta.url, async (argv, io) => {
229+const exitCode = await main(argv, io);
230+if (!io) {
231+process.exitCode = exitCode;
232+}
233+return exitCode;
234+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。