diff --git a/src/domain/referenceSourceClassification.ts b/src/domain/referenceSourceClassification.ts index 212bafb46c..95a796468c 100644 --- a/src/domain/referenceSourceClassification.ts +++ b/src/domain/referenceSourceClassification.ts @@ -285,7 +285,8 @@ const filenameIsTest = (filename: string): boolean => { if (lower.includes(".test.") || lower.includes(".spec.")) return true; if (lower.startsWith("test_") || lower.startsWith("spec_")) return true; if (lower.endsWith("_test") || lower.endsWith("_spec")) return true; - return false; + const stem = basenameParts(lower).base; + return stem.endsWith("_test") || stem.endsWith("_spec"); }; const filenameIsGenerated = (filename: string): boolean => { diff --git a/src/domain/referenceSourceTestClassification.test.ts b/src/domain/referenceSourceTestClassification.test.ts new file mode 100644 index 0000000000..cd7e45d560 --- /dev/null +++ b/src/domain/referenceSourceTestClassification.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from "vitest"; + +import { + classifyReferenceSourcePath, + detectReferenceSourceLanguage, +} from "./referenceSourceClassification.js"; + +describe("reference source test filenames", () => { + it.each([ + ["main_test.go", ["source", "test"], "Go"], + ["main_test.py", ["source", "test"], "Python"], + ["parser_spec.rs", ["source", "test"], "Rust"], + ["widget_spec.cpp", ["source", "test"], "C++"], + ["Widget_TeSt.TS", ["source", "test"], "TypeScript"], + ["Widget_SpEc.PY", ["source", "test"], "Python"], + ["widget.v2_test.py", ["source", "test"], "Python"], + [".widget_spec.js", ["source", "test"], "JavaScript"], + ["widget_test.fixture", ["test"], null], + ["settings_spec.json", ["config", "test"], "JSON"], + ["notes_test.md", ["documentation", "test"], "Markdown"], + ["widget_test.map", ["generated", "test"], null], + ["dist/widget_spec.js", ["generated", "source", "test"], "JavaScript"], + ["vendor/widget_test.go", ["source", "test", "vendor"], "Go"], + ])("recognizes the final basename suffix in %s", (path, tags, language) => { + expect(classifyReferenceSourcePath(path)).toEqual(tags); + expect(detectReferenceSourceLanguage(path)).toBe(language); + }); + + it.each([ + ["test_main.py", ["source", "test"]], + ["SPEC_main.rs", ["source", "test"]], + ["main.test.js", ["source", "test"]], + ["main.SPEC.ts", ["source", "test"]], + ["main_test", ["test"]], + ["main_SPEC", ["test"]], + ["main.py_test", ["test"]], + ["main.rs_spec", ["test"]], + ["tests/main.go", ["source", "test"]], + ])("preserves existing test classification for %s", (path, tags) => { + expect(classifyReferenceSourcePath(path)).toEqual(tags); + }); + + it.each([ + ["main.go", ["source"]], + ["main.py", ["source"]], + ["main_test_helper.go", ["source"]], + ["main_tests.go", ["source"]], + ["main_testing.py", ["source"]], + ["main_tester.py", ["source"]], + ["main_spec_helper.rs", ["source"]], + ["main_specs.rs", ["source"]], + ["main_specimen.rs", ["source"]], + ["main_test.py.bak", ["unknown"]], + ["main_spec.ts.bak", ["unknown"]], + ["main_test_helpers", ["unknown"]], + ["main_spec_helpers", ["unknown"]], + ])("does not infer a test from a near-name in %s", (path, tags) => { + expect(classifyReferenceSourcePath(path)).toEqual(tags); + }); +}); diff --git a/tests/boundary/filesystem/referenceSourceTestFilenames.test.ts b/tests/boundary/filesystem/referenceSourceTestFilenames.test.ts new file mode 100644 index 0000000000..a1cbaaf281 --- /dev/null +++ b/tests/boundary/filesystem/referenceSourceTestFilenames.test.ts @@ -0,0 +1,105 @@ +import { createHash } from "node:crypto"; +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { expect, it } from "vitest"; + +import { importReferenceSource } from "../../../src/application/ReferenceSourceImport.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +it("imports test-suffixed filenames with unchanged paths, hashes, and languages", async () => { + const root = await createTestTempDirectory("rea-reference-test-filenames-"); + const files = [ + ["main_test.go", "package main\n", ["source", "test"], "Go"], + ["main_test.py", "assert True\n", ["source", "test"], "Python"], + ["parser_spec.rs", "fn checks() {}\n", ["source", "test"], "Rust"], + ["Widget_SPEC.TS", "export {};\n", ["source", "test"], "TypeScript"], + ["main_test", "test fixture\n", ["test"], null], + ["main_spec", "spec fixture\n", ["test"], null], + ["test_helper.py", "assert True\n", ["source", "test"], "Python"], + ["main.test.js", "export {};\n", ["source", "test"], "JavaScript"], + ["main.go", "package main\n", ["source"], "Go"], + ["main_test_helper.py", "value = 1\n", ["source"], "Python"], + ["main_specimen.rs", "fn main() {}\n", ["source"], "Rust"], + ["main_test.py.bak", "backup\n", ["unknown"], null], + ] as const; + await Promise.all( + files.map(([path, content]) => writeFile(join(root, path), content)), + ); + + const result = await importReferenceSource({ + root, + caller: "reference-test-filenames-test", + policy: { secretPatterns: [] }, + }); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.value.entries).toHaveLength(files.length); + for (const [path, content, classifications, language] of files) { + expect(result.value.entries).toContainEqual({ + path, + kind: "file", + sha256: createHash("sha256").update(content).digest("hex"), + size: Buffer.byteLength(content), + language, + classifications, + content_state: "hashed", + limitations: [], + }); + } + expect(result.value.parse_failures).toEqual([]); + expect(result.value.relationships).toEqual([]); + expect(result.value.exclusions).toEqual([]); + expect(result.value.languages).toEqual([ + "Go", + "JavaScript", + "Python", + "Rust", + "TypeScript", + ]); +}); + +it("keeps parser failures and unknown dependencies visible for test filenames", async () => { + const root = await createTestTempDirectory("rea-reference-test-parsing-"); + await Promise.all([ + writeFile( + join(root, "loader_test.js"), + 'throw new Error("must not execute"); import(moduleName);\n', + ), + writeFile(join(root, "broken_spec.ts"), "const = ;\n"), + ]); + + const result = await importReferenceSource({ + root, + caller: "reference-test-parsing-test", + policy: { secretPatterns: [] }, + }); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.value.inventory_state).toBe("partial"); + expect(result.value.relationships).toEqual([ + { + from_path: "loader_test.js", + to: "", + kind: "imports", + resolution: "unknown", + parse_state: "partial", + }, + ]); + expect(result.value.parse_failures).toEqual([ + { + path: "broken_spec.ts", + parser: "babel", + reason: expect.any(String), + }, + ]); + expect( + result.value.entries.map(({ path, classifications }) => ({ + path, + classifications, + })), + ).toEqual([ + { path: "broken_spec.ts", classifications: ["source", "test"] }, + { path: "loader_test.js", classifications: ["source", "test"] }, + ]); +});