test(testing): tighten mock cleanup audit

This commit is contained in:
YeonGyu-Kim
2026-05-30 15:35:56 +09:00
parent 08f4a4ac50
commit 51fd474274
+118 -66
View File
@@ -6,63 +6,6 @@ import ts from "typescript"
const SOURCE_ROOT = path.resolve(import.meta.dir, "..")
const WORKSPACE_ROOT = path.resolve(SOURCE_ROOT, "..")
const MOCK_MODULE_TOKEN = "mock.module"
const MOCK_MODULE_LIFECYCLE_ALLOWLIST = new Map<string, string>([
// TODO(MOCK-MODULE-AUDIT): add cleanup for team mailbox inbox module mocks.
[
path.join(SOURCE_ROOT, "features", "team-mode", "team-mailbox", "inbox.test.ts"),
"justification: legacy mock.module call predates audit; TODO(MOCK-MODULE-AUDIT): add cleanup",
],
// TODO(MOCK-MODULE-AUDIT): add cleanup for doctor dependency module mocks.
[
path.join(SOURCE_ROOT, "cli", "doctor", "checks", "dependencies.test.ts"),
"justification: legacy mock.module call predates audit; TODO(MOCK-MODULE-AUDIT): add cleanup",
],
// TODO(MOCK-MODULE-AUDIT): add cleanup for session recovery module mocks.
[
path.join(SOURCE_ROOT, "hooks", "session-recovery", "index.test.ts"),
"justification: legacy mock.module call predates audit; TODO(MOCK-MODULE-AUDIT): add cleanup",
],
// TODO(MOCK-MODULE-AUDIT): add cleanup for auto-update checker hook module mocks.
[
path.join(SOURCE_ROOT, "hooks", "auto-update-checker", "hook.test.ts"),
"justification: legacy mock.module call predates audit; TODO(MOCK-MODULE-AUDIT): add cleanup",
],
// TODO(MOCK-MODULE-AUDIT): add cleanup for tmux layout-runner module mocks.
[
path.join(SOURCE_ROOT, "shared", "tmux", "tmux-utils", "layout-runner.test.ts"),
"justification: legacy mock.module call predates audit; TODO(MOCK-MODULE-AUDIT): add cleanup",
],
// TODO(MOCK-MODULE-AUDIT): add cleanup for tmux pane-close-runner module mocks.
[
path.join(SOURCE_ROOT, "shared", "tmux", "tmux-utils", "pane-close-runner.test.ts"),
"justification: legacy mock.module call predates audit; TODO(MOCK-MODULE-AUDIT): add cleanup",
],
// TODO(MOCK-MODULE-AUDIT): add cleanup for tmux pane-close module mocks.
[
path.join(SOURCE_ROOT, "shared", "tmux", "tmux-utils", "pane-close.test.ts"),
"justification: legacy mock.module call predates audit; TODO(MOCK-MODULE-AUDIT): add cleanup",
],
// TODO(MOCK-MODULE-AUDIT): add cleanup for tmux pane-dimensions module mocks.
[
path.join(SOURCE_ROOT, "shared", "tmux", "tmux-utils", "pane-dimensions.test.ts"),
"justification: legacy mock.module call predates audit; TODO(MOCK-MODULE-AUDIT): add cleanup",
],
// TODO(MOCK-MODULE-AUDIT): add cleanup for tmux session-kill-runner module mocks.
[
path.join(SOURCE_ROOT, "shared", "tmux", "tmux-utils", "session-kill-runner.test.ts"),
"justification: legacy mock.module call predates audit; TODO(MOCK-MODULE-AUDIT): add cleanup",
],
// TODO(MOCK-MODULE-AUDIT): add cleanup for tmux session-kill module mocks.
[
path.join(SOURCE_ROOT, "shared", "tmux", "tmux-utils", "session-kill.test.ts"),
"justification: legacy mock.module call predates audit; TODO(MOCK-MODULE-AUDIT): add cleanup",
],
// TODO(MOCK-MODULE-AUDIT): add cleanup for tmux stale-session sweep module mocks.
[
path.join(SOURCE_ROOT, "shared", "tmux", "tmux-utils", "stale-session-sweep-runtime.test.ts"),
"justification: legacy mock.module call predates audit; TODO(MOCK-MODULE-AUDIT): add cleanup",
],
])
async function listTestFiles(directory: string): Promise<string[]> {
const entries = await readdir(directory, { withFileTypes: true })
@@ -152,6 +95,38 @@ function hasMockModuleCall(sourceFile: ts.SourceFile): boolean {
return collectMockModulePaths(sourceFile).length > 0
}
function isTopLevelNode(node: ts.Node, sourceFile: ts.SourceFile): boolean {
let current = node.parent
while (current && current !== sourceFile) {
if (ts.isFunctionLike(current) || ts.isClassLike(current)) {
return false
}
current = current.parent
}
return true
}
function hasTopLevelMockModuleCall(sourceFile: ts.SourceFile): boolean {
let foundTopLevelMock = false
const visit = (node: ts.Node): void => {
if (foundTopLevelMock) {
return
}
if (ts.isCallExpression(node) && isMockModuleCall(node) && isTopLevelNode(node, sourceFile)) {
foundTopLevelMock = true
return
}
ts.forEachChild(node, visit)
}
visit(sourceFile)
return foundTopLevelMock
}
function hasDuplicateModuleReset(sourceFile: ts.SourceFile): boolean {
const seenModulePaths = new Set<string>()
for (const modulePath of collectMockModulePaths(sourceFile)) {
@@ -164,11 +139,7 @@ function hasDuplicateModuleReset(sourceFile: ts.SourceFile): boolean {
return false
}
function isCleanupCall(node: ts.CallExpression): boolean {
if (ts.isIdentifier(node.expression)) {
return node.expression.text === "afterEach" || node.expression.text === "afterAll"
}
function isMockRestoreCall(node: ts.CallExpression): boolean {
const expression = node.expression
return ts.isPropertyAccessExpression(expression)
&& ts.isIdentifier(expression.expression)
@@ -176,6 +147,14 @@ function isCleanupCall(node: ts.CallExpression): boolean {
&& expression.name.text === "restore"
}
function isCleanupCall(node: ts.CallExpression): boolean {
if (ts.isIdentifier(node.expression)) {
return node.expression.text === "afterEach" || node.expression.text === "afterAll"
}
return isMockRestoreCall(node)
}
function hasCleanupPattern(sourceFile: ts.SourceFile): boolean {
if (hasDuplicateModuleReset(sourceFile)) {
return true
@@ -200,6 +179,74 @@ function hasCleanupPattern(sourceFile: ts.SourceFile): boolean {
return foundCleanup
}
function afterAllCallsMockRestore(node: ts.CallExpression): boolean {
if (!ts.isIdentifier(node.expression) || node.expression.text !== "afterAll") {
return false
}
const callback = node.arguments[0]
if (!callback) {
return false
}
let foundRestore = false
const visit = (child: ts.Node): void => {
if (foundRestore) {
return
}
if (ts.isCallExpression(child) && isMockRestoreCall(child)) {
foundRestore = true
return
}
ts.forEachChild(child, visit)
}
visit(callback)
return foundRestore
}
function hasAfterAllMockRestore(sourceFile: ts.SourceFile): boolean {
let foundCleanup = false
const visit = (node: ts.Node): void => {
if (foundCleanup) {
return
}
if (ts.isCallExpression(node) && afterAllCallsMockRestore(node)) {
foundCleanup = true
return
}
ts.forEachChild(node, visit)
}
visit(sourceFile)
return foundCleanup
}
function hasTopLevelMockRestore(sourceFile: ts.SourceFile): boolean {
let foundCleanup = false
const visit = (node: ts.Node): void => {
if (foundCleanup) {
return
}
if (ts.isCallExpression(node) && isMockRestoreCall(node) && isTopLevelNode(node, sourceFile)) {
foundCleanup = true
return
}
ts.forEachChild(node, visit)
}
visit(sourceFile)
return foundCleanup
}
describe("mock.module lifecycle hygiene", () => {
test("#given test files using mock.module #when audited #then each must pair with cleanup", async () => {
// given
@@ -208,15 +255,20 @@ describe("mock.module lifecycle hygiene", () => {
// when
for (const filePath of files) {
if (MOCK_MODULE_LIFECYCLE_ALLOWLIST.has(filePath)) {
continue
}
const contents = await readFile(filePath, "utf8")
if (!contents.includes(MOCK_MODULE_TOKEN)) {
continue
}
const sourceFile = ts.createSourceFile(filePath, contents, ts.ScriptTarget.Latest, true)
if (
hasTopLevelMockModuleCall(sourceFile)
&& !hasAfterAllMockRestore(sourceFile)
&& !hasTopLevelMockRestore(sourceFile)
) {
offenders.push(relativeSourcePath(filePath))
continue
}
if (hasMockModuleCall(sourceFile) && !hasCleanupPattern(sourceFile)) {
offenders.push(relativeSourcePath(filePath))
}