import fs from "node:fs/promises"; import path from "node:path"; import ts from "typescript"; import { describe, expect, it } from "vitest"; const RUNTIME_ROOTS = ["src", "extensions"]; const SKIP_PATTERNS = [ /\.test\.tsx?$/, /\.test-helpers\.tsx?$/, /\.test-utils\.tsx?$/, /\.e2e\.tsx?$/, /\.d\.ts$/, /[\\/](?:__tests__|tests)[\\/]/, /[\\/][^\\/]*test-helpers(?:\.[^\\/]+)?\.ts$/, ]; function shouldSkip(relativePath: string): boolean { return SKIP_PATTERNS.some((pattern) => pattern.test(relativePath)); } function isIdentifierNamed(node: ts.Node, name: string): node is ts.Identifier { return ts.isIdentifier(node) && node.text === name; } function isPathJoinCall(expr: ts.LeftHandSideExpression): boolean { return ( ts.isPropertyAccessExpression(expr) && expr.name.text === "join" && isIdentifierNamed(expr.expression, "path") ); } function isOsTmpdirCall(node: ts.Expression): boolean { return ( ts.isCallExpression(node) && node.arguments.length === 0 && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "tmpdir" && isIdentifierNamed(node.expression.expression, "os") ); } function isDynamicTemplateSegment(node: ts.Expression): boolean { return ts.isTemplateExpression(node); } function hasDynamicTmpdirJoin(source: string, filePath = "fixture.ts"): boolean { const sourceFile = ts.createSourceFile( filePath, source, ts.ScriptTarget.Latest, true, filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS, ); let found = false; const visit = (node: ts.Node): void => { if (found) { return; } if ( ts.isCallExpression(node) && isPathJoinCall(node.expression) && node.arguments.length >= 2 && isOsTmpdirCall(node.arguments[0]) && node.arguments.slice(1).some((arg) => isDynamicTemplateSegment(arg)) ) { found = true; return; } ts.forEachChild(node, visit); }; visit(sourceFile); return found; } async function listTsFiles(dir: string): Promise { const entries = await fs.readdir(dir, { withFileTypes: true }); const out: string[] = []; for (const entry of entries) { if (entry.name === "node_modules" || entry.name === "dist" || entry.name.startsWith(".")) { continue; } const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { out.push(...(await listTsFiles(fullPath))); continue; } if (!entry.isFile()) { continue; } if (fullPath.endsWith(".ts") || fullPath.endsWith(".tsx")) { out.push(fullPath); } } return out; } describe("temp path guard", () => { it("skips test helper filename variants", () => { expect(shouldSkip("src/commands/test-helpers.ts")).toBe(true); expect(shouldSkip("src/commands/sessions.test-helpers.ts")).toBe(true); expect(shouldSkip("src\\commands\\sessions.test-helpers.ts")).toBe(true); }); it("detects dynamic and ignores static fixtures", () => { const dynamicFixtures = [ "const p = path.join(os.tmpdir(), `openclaw-${id}`);", "const p = path.join(os.tmpdir(), 'safe', `${token}`);", ]; const staticFixtures = [ "const p = path.join(os.tmpdir(), 'openclaw-fixed');", "const p = path.join(os.tmpdir(), `openclaw-fixed`);", "const p = path.join(os.tmpdir(), prefix + '-x');", "const p = path.join(os.tmpdir(), segment);", "const p = path.join('/tmp', `openclaw-${id}`);", "// path.join(os.tmpdir(), `openclaw-${id}`)", "const p = path.join(os.tmpdir());", ]; for (const fixture of dynamicFixtures) { expect(hasDynamicTmpdirJoin(fixture)).toBe(true); } for (const fixture of staticFixtures) { expect(hasDynamicTmpdirJoin(fixture)).toBe(false); } }); it("blocks dynamic template path.join(os.tmpdir(), ...) in runtime source files", async () => { const repoRoot = process.cwd(); const offenders: string[] = []; for (const root of RUNTIME_ROOTS) { const absRoot = path.join(repoRoot, root); const files = await listTsFiles(absRoot); for (const file of files) { const relativePath = path.relative(repoRoot, file); if (shouldSkip(relativePath)) { continue; } const source = await fs.readFile(file, "utf-8"); if (hasDynamicTmpdirJoin(source, relativePath)) { offenders.push(relativePath); } } } expect(offenders).toEqual([]); }); });