mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
feat(core): broaden .understandignore starter — C#/Java/Go test patterns
Detect C# project-suffix dirs (Foo.Tests/, Foo.UnitTests/) and PascalCase test dirs (Tests/, UnitTests/, IntegrationTests/) via case-insensitive match; group test-file suggestions by language (JS, C#, Java, Go). Keeps all suggestions commented-out — same opt-in model as today. Updates SKILL.md Phase 0.5 inline generator to stay in sync with the TS module. Refs #76 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -95,6 +95,82 @@ describe("generateStarterIgnoreFile", () => {
|
|||||||
expect(content).not.toContain("# fixtures/");
|
expect(content).not.toContain("# fixtures/");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("multi-language test directory detection", () => {
|
||||||
|
it("suggests PascalCase Tests/ via case-insensitive match", () => {
|
||||||
|
mkdirSync(join(testDir, "Tests"), { recursive: true });
|
||||||
|
const content = generateStarterIgnoreFile(testDir);
|
||||||
|
// On-disk casing is preserved in the suggestion.
|
||||||
|
expect(content).toContain("# Tests/");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("suggests UnitTests/ via case-insensitive match", () => {
|
||||||
|
mkdirSync(join(testDir, "UnitTests"), { recursive: true });
|
||||||
|
const content = generateStarterIgnoreFile(testDir);
|
||||||
|
expect(content).toContain("# UnitTests/");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("suggests IntegrationTests/ via case-insensitive match", () => {
|
||||||
|
mkdirSync(join(testDir, "IntegrationTests"), { recursive: true });
|
||||||
|
const content = generateStarterIgnoreFile(testDir);
|
||||||
|
expect(content).toContain("# IntegrationTests/");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("suggests C# project-suffix .Tests/ directories", () => {
|
||||||
|
mkdirSync(join(testDir, "MyApp.Tests"), { recursive: true });
|
||||||
|
const content = generateStarterIgnoreFile(testDir);
|
||||||
|
expect(content).toContain("# MyApp.Tests/");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("suggests C# project-suffix .UnitTests/ directories", () => {
|
||||||
|
mkdirSync(join(testDir, "MyApp.UnitTests"), { recursive: true });
|
||||||
|
const content = generateStarterIgnoreFile(testDir);
|
||||||
|
expect(content).toContain("# MyApp.UnitTests/");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("suggests C# project-suffix .IntegrationTests/ directories", () => {
|
||||||
|
mkdirSync(join(testDir, "MyApp.IntegrationTests"), { recursive: true });
|
||||||
|
const content = generateStarterIgnoreFile(testDir);
|
||||||
|
expect(content).toContain("# MyApp.IntegrationTests/");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores files that happen to share a detected name", () => {
|
||||||
|
writeFileSync(join(testDir, "tests"), "not a directory");
|
||||||
|
const content = generateStarterIgnoreFile(testDir);
|
||||||
|
expect(content).not.toContain("# tests/");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("language-grouped test file patterns", () => {
|
||||||
|
it("includes C# / .NET test file patterns", () => {
|
||||||
|
const content = generateStarterIgnoreFile(testDir);
|
||||||
|
expect(content).toContain("# C# / .NET");
|
||||||
|
expect(content).toContain("# **/*Tests.cs");
|
||||||
|
expect(content).toContain("# **/*Test.cs");
|
||||||
|
expect(content).toContain("# **/*Fixture.cs");
|
||||||
|
expect(content).toContain("# **/*.Tests.csproj");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes Java / Kotlin test file patterns", () => {
|
||||||
|
const content = generateStarterIgnoreFile(testDir);
|
||||||
|
expect(content).toContain("# Java / Kotlin");
|
||||||
|
expect(content).toContain("# **/*Test.java");
|
||||||
|
expect(content).toContain("# **/*IT.java");
|
||||||
|
expect(content).toContain("# **/*Spec.kt");
|
||||||
|
expect(content).toContain("# **/src/test/**");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes Go test file patterns", () => {
|
||||||
|
const content = generateStarterIgnoreFile(testDir);
|
||||||
|
expect(content).toContain("# Go");
|
||||||
|
expect(content).toContain("# **/*_test.go");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("groups patterns under the JS / TS sub-header", () => {
|
||||||
|
const content = generateStarterIgnoreFile(testDir);
|
||||||
|
expect(content).toContain("# JS / TS");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe(".gitignore integration", () => {
|
describe(".gitignore integration", () => {
|
||||||
it("includes .gitignore patterns not covered by defaults", () => {
|
it("includes .gitignore patterns not covered by defaults", () => {
|
||||||
writeFileSync(join(testDir, ".gitignore"), ".env\nsecrets/\n*.pyc\n");
|
writeFileSync(join(testDir, ".gitignore"), ".env\nsecrets/\n*.pyc\n");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { existsSync, readFileSync } from "node:fs";
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { DEFAULT_IGNORE_PATTERNS } from "./ignore-filter.js";
|
import { DEFAULT_IGNORE_PATTERNS } from "./ignore-filter.js";
|
||||||
|
|
||||||
@@ -12,23 +12,60 @@ const HEADER = `# .understandignore — patterns for files/dirs to exclude from
|
|||||||
#
|
#
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const DETECTABLE_DIRS = [
|
// Directory names matched case-insensitively against the on-disk entry name.
|
||||||
{ dir: "__tests__", pattern: "__tests__/" },
|
// Includes JS/TS, Python, and PascalCase variants seen in C#/.NET projects.
|
||||||
{ dir: "test", pattern: "test/" },
|
const EXACT_DIR_NAMES = [
|
||||||
{ dir: "tests", pattern: "tests/" },
|
"__tests__",
|
||||||
{ dir: "fixtures", pattern: "fixtures/" },
|
"test",
|
||||||
{ dir: "testdata", pattern: "testdata/" },
|
"tests",
|
||||||
{ dir: "docs", pattern: "docs/" },
|
"fixtures",
|
||||||
{ dir: "examples", pattern: "examples/" },
|
"testdata",
|
||||||
{ dir: "scripts", pattern: "scripts/" },
|
"docs",
|
||||||
{ dir: "migrations", pattern: "migrations/" },
|
"examples",
|
||||||
{ dir: ".storybook", pattern: ".storybook/" },
|
"scripts",
|
||||||
|
"migrations",
|
||||||
|
".storybook",
|
||||||
|
"unittests",
|
||||||
|
"integrationtests",
|
||||||
];
|
];
|
||||||
|
|
||||||
const GENERIC_SUGGESTIONS = [
|
// Directory-name suffixes matched case-insensitively. Covers C# / .NET
|
||||||
"*.test.*",
|
// project-suffix conventions like Foo.Tests, Foo.UnitTests, Foo.IntegrationTests.
|
||||||
"*.spec.*",
|
const SUFFIX_DIR_GLOBS = [
|
||||||
"*.snap",
|
".tests",
|
||||||
|
".unittests",
|
||||||
|
".integrationtests",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Test file patterns grouped by language. Emitted as commented suggestions
|
||||||
|
// with a sub-header per group.
|
||||||
|
const TEST_PATTERN_GROUPS: Array<{ label: string; patterns: string[] }> = [
|
||||||
|
{
|
||||||
|
label: "JS / TS",
|
||||||
|
patterns: ["*.test.*", "*.spec.*", "*.snap"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "C# / .NET",
|
||||||
|
patterns: [
|
||||||
|
"**/*Tests.cs",
|
||||||
|
"**/*Test.cs",
|
||||||
|
"**/*Fixture.cs",
|
||||||
|
"**/*.Tests.csproj",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Java / Kotlin",
|
||||||
|
patterns: [
|
||||||
|
"**/src/test/**",
|
||||||
|
"**/*Test.java",
|
||||||
|
"**/*IT.java",
|
||||||
|
"**/*Spec.kt",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Go",
|
||||||
|
patterns: ["**/*_test.go"],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,6 +90,33 @@ function isCoveredByDefaults(pattern: string): boolean {
|
|||||||
return DEFAULT_IGNORE_PATTERNS.some((d) => normalize(d) === normalized);
|
return DEFAULT_IGNORE_PATTERNS.some((d) => normalize(d) === normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects directories under projectRoot that match either an exact name
|
||||||
|
* (case-insensitive) in EXACT_DIR_NAMES or end with one of SUFFIX_DIR_GLOBS.
|
||||||
|
* Returns patterns using the directory's actual on-disk casing.
|
||||||
|
*/
|
||||||
|
function detectDirectories(projectRoot: string): string[] {
|
||||||
|
let entries: ReturnType<typeof readdirSync>;
|
||||||
|
try {
|
||||||
|
entries = readdirSync(projectRoot, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const matches: string[] = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isDirectory()) continue;
|
||||||
|
const lower = entry.name.toLowerCase();
|
||||||
|
if (EXACT_DIR_NAMES.includes(lower)) {
|
||||||
|
matches.push(`${entry.name}/`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (SUFFIX_DIR_GLOBS.some((suffix) => lower.endsWith(suffix))) {
|
||||||
|
matches.push(`${entry.name}/`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a starter .understandignore file content by scanning the project
|
* Generates a starter .understandignore file content by scanning the project
|
||||||
* for common directories and reading .gitignore patterns.
|
* for common directories and reading .gitignore patterns.
|
||||||
@@ -75,14 +139,8 @@ export function generateStarterIgnoreFile(projectRoot: string): string {
|
|||||||
sections.push("");
|
sections.push("");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Section 2: detected directories
|
// Section 2: detected directories (case-insensitive + suffix-glob)
|
||||||
const detected: string[] = [];
|
const detected = detectDirectories(projectRoot);
|
||||||
for (const { dir, pattern } of DETECTABLE_DIRS) {
|
|
||||||
if (existsSync(join(projectRoot, dir))) {
|
|
||||||
detected.push(pattern);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (detected.length > 0) {
|
if (detected.length > 0) {
|
||||||
sections.push("# --- Detected directories (uncomment to exclude) ---\n");
|
sections.push("# --- Detected directories (uncomment to exclude) ---\n");
|
||||||
for (const pattern of detected) {
|
for (const pattern of detected) {
|
||||||
@@ -91,10 +149,13 @@ export function generateStarterIgnoreFile(projectRoot: string): string {
|
|||||||
sections.push("");
|
sections.push("");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Section 3: generic test patterns
|
// Section 3: test file patterns, grouped by language
|
||||||
sections.push("# --- Test file patterns (uncomment to exclude) ---\n");
|
sections.push("# --- Test file patterns (uncomment to exclude) ---\n");
|
||||||
for (const pattern of GENERIC_SUGGESTIONS) {
|
for (const group of TEST_PATTERN_GROUPS) {
|
||||||
sections.push(`# ${pattern}`);
|
sections.push(`# ${group.label}`);
|
||||||
|
for (const pattern of group.patterns) {
|
||||||
|
sections.push(`# ${pattern}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sections.push("");
|
sections.push("");
|
||||||
|
|
||||||
|
|||||||
@@ -217,10 +217,29 @@ Set up and verify the `.understandignore` file before scanning.
|
|||||||
const gi = fs.readFileSync(gitignorePath, 'utf-8').split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')).filter(p => !defaultSet.has(norm(p)));
|
const gi = fs.readFileSync(gitignorePath, 'utf-8').split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')).filter(p => !defaultSet.has(norm(p)));
|
||||||
if (gi.length) { body += '# --- From .gitignore (uncomment to exclude) ---\n\n' + gi.map(p => '# ' + p).join('\n') + '\n\n'; }
|
if (gi.length) { body += '# --- From .gitignore (uncomment to exclude) ---\n\n' + gi.map(p => '# ' + p).join('\n') + '\n\n'; }
|
||||||
}
|
}
|
||||||
const dirs = ['__tests__','test','tests','fixtures','testdata','docs','examples','scripts','migrations','.storybook'];
|
const exactDirs = ['__tests__','test','tests','fixtures','testdata','docs','examples','scripts','migrations','.storybook','unittests','integrationtests'];
|
||||||
const found = dirs.filter(d => fs.existsSync(path.join(root, d)));
|
const suffixDirs = ['.tests','.unittests','.integrationtests'];
|
||||||
|
const found = [];
|
||||||
|
try {
|
||||||
|
for (const ent of fs.readdirSync(root, { withFileTypes: true })) {
|
||||||
|
if (!ent.isDirectory()) continue;
|
||||||
|
const lower = ent.name.toLowerCase();
|
||||||
|
if (exactDirs.includes(lower) || suffixDirs.some(s => lower.endsWith(s))) {
|
||||||
|
found.push(ent.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
if (found.length) { body += '# --- Detected directories (uncomment to exclude) ---\n\n' + found.map(d => '# ' + d + '/').join('\n') + '\n\n'; }
|
if (found.length) { body += '# --- Detected directories (uncomment to exclude) ---\n\n' + found.map(d => '# ' + d + '/').join('\n') + '\n\n'; }
|
||||||
body += '# --- Test file patterns (uncomment to exclude) ---\n\n# *.test.*\n# *.spec.*\n# *.snap\n';
|
const patternGroups = [
|
||||||
|
['JS / TS', ['*.test.*','*.spec.*','*.snap']],
|
||||||
|
['C# / .NET', ['**/*Tests.cs','**/*Test.cs','**/*Fixture.cs','**/*.Tests.csproj']],
|
||||||
|
['Java / Kotlin', ['**/src/test/**','**/*Test.java','**/*IT.java','**/*Spec.kt']],
|
||||||
|
['Go', ['**/*_test.go']],
|
||||||
|
];
|
||||||
|
body += '# --- Test file patterns (uncomment to exclude) ---\n\n';
|
||||||
|
for (const [label, pats] of patternGroups) {
|
||||||
|
body += '# ' + label + '\n' + pats.map(p => '# ' + p).join('\n') + '\n';
|
||||||
|
}
|
||||||
const outDir = path.join(root, '.understand-anything');
|
const outDir = path.join(root, '.understand-anything');
|
||||||
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
||||||
fs.writeFileSync(path.join(outDir, '.understandignore'), header + body);
|
fs.writeFileSync(path.join(outDir, '.understandignore'), header + body);
|
||||||
|
|||||||
Reference in New Issue
Block a user