From cd38a6e1c76432a509ec67c626df38f4975e4139 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 16 Jan 2026 21:35:32 +0100 Subject: [PATCH 1/3] Ignore AI assistant config files in project scanning Exclude CLAUDE.md and AGENTS.md from project input files and extension templates to prevent AI assistant configuration from being inadvertently rendered as content. --- news/changelog-1.9.md | 1 + src/extension/template.ts | 2 ++ src/project/project-context.ts | 4 +++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/news/changelog-1.9.md b/news/changelog-1.9.md index 76625864dd7..327a99e0986 100644 --- a/news/changelog-1.9.md +++ b/news/changelog-1.9.md @@ -130,3 +130,4 @@ All changes included in 1.9: - ([#13656](https://github.com/quarto-dev/quarto-cli/issues/13656)): Fix R code cells with empty `lang: ""` option producing invalid markdown class attributes. - ([#13832](https://github.com/quarto-dev/quarto-cli/pull/13832)): Fix `license.text` metadata not being accessible when using an inline license (`license: "text"`), and populate it with the license name for CC licenses instead of empty string. (author: @mcanouil) - ([#13856](https://github.com/quarto-dev/quarto-cli/issues/13856)): Add code annotation support for Typst and Observable.js code blocks. (author: @mcanouil) +- Ignore AI assistant configuration files (`CLAUDE.md`, `AGENTS.md`) when scanning for project input files and in extension templates, similar to how `README.md` is handled. diff --git a/src/extension/template.ts b/src/extension/template.ts index 162da7b7e5f..87e43029000 100644 --- a/src/extension/template.ts +++ b/src/extension/template.ts @@ -57,4 +57,6 @@ const kBuiltInExcludes = [ "COPYRIGHT", "LICENSE", "_extensions", + "CLAUDE.md", + "AGENTS.md", ]; diff --git a/src/project/project-context.ts b/src/project/project-context.ts index a863a0d839a..bf63cb61f79 100644 --- a/src/project/project-context.ts +++ b/src/project/project-context.ts @@ -873,7 +873,9 @@ function projectHiddenIgnoreGlob(dir: string) { return projectIgnoreGlobs(dir) // standard ignores for all projects .concat(["**/_*", "**/_*/**"]) // underscore prefx .concat(["**/.*", "**/.*/**"]) // hidden (dot prefix) - .concat(["**/README.?([Rrq])md"]); // README + .concat(["**/README.?([Rrq])md"]) // README + .concat(["**/CLAUDE.md"]) // Anthropic claude code file + .concat(["**/AGENTS.md"]); // https://agents.md/ } export const projectInputFiles = makeTimedFunctionAsync( From 5701c4e9b5c076b73255d406bdaf4fc2f8dd84cd Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 16 Jan 2026 21:48:20 +0100 Subject: [PATCH 2/3] Add tests for AI config file exclusion Add smoke tests to verify CLAUDE.md and AGENTS.md are excluded from: - Project input files (project-ai-config.test.ts) - Extension templates (template-ai-config.test.ts) Co-Authored-By: Claude --- tests/docs/project/ai-config-files/.gitignore | 2 + tests/docs/project/ai-config-files/AGENTS.md | 5 ++ tests/docs/project/ai-config-files/CLAUDE.md | 5 ++ .../docs/project/ai-config-files/_quarto.yml | 2 + tests/docs/project/ai-config-files/index.qmd | 5 ++ tests/smoke/project/project-ai-config.test.ts | 37 +++++++++++ tests/smoke/use/template-ai-config.test.ts | 64 +++++++++++++++++++ 7 files changed, 120 insertions(+) create mode 100644 tests/docs/project/ai-config-files/.gitignore create mode 100644 tests/docs/project/ai-config-files/AGENTS.md create mode 100644 tests/docs/project/ai-config-files/CLAUDE.md create mode 100644 tests/docs/project/ai-config-files/_quarto.yml create mode 100644 tests/docs/project/ai-config-files/index.qmd create mode 100644 tests/smoke/project/project-ai-config.test.ts create mode 100644 tests/smoke/use/template-ai-config.test.ts diff --git a/tests/docs/project/ai-config-files/.gitignore b/tests/docs/project/ai-config-files/.gitignore new file mode 100644 index 00000000000..ad293093b07 --- /dev/null +++ b/tests/docs/project/ai-config-files/.gitignore @@ -0,0 +1,2 @@ +/.quarto/ +**/*.quarto_ipynb diff --git a/tests/docs/project/ai-config-files/AGENTS.md b/tests/docs/project/ai-config-files/AGENTS.md new file mode 100644 index 00000000000..1056d87a01d --- /dev/null +++ b/tests/docs/project/ai-config-files/AGENTS.md @@ -0,0 +1,5 @@ +--- +title: "Agents Configuration" +--- + +This is an agents.md configuration file that should be ignored during project scanning. diff --git a/tests/docs/project/ai-config-files/CLAUDE.md b/tests/docs/project/ai-config-files/CLAUDE.md new file mode 100644 index 00000000000..8fd422970ec --- /dev/null +++ b/tests/docs/project/ai-config-files/CLAUDE.md @@ -0,0 +1,5 @@ +--- +title: "Claude Configuration" +--- + +This is an AI assistant configuration file that should be ignored during project scanning. diff --git a/tests/docs/project/ai-config-files/_quarto.yml b/tests/docs/project/ai-config-files/_quarto.yml new file mode 100644 index 00000000000..e82461507d8 --- /dev/null +++ b/tests/docs/project/ai-config-files/_quarto.yml @@ -0,0 +1,2 @@ +project: + type: website diff --git a/tests/docs/project/ai-config-files/index.qmd b/tests/docs/project/ai-config-files/index.qmd new file mode 100644 index 00000000000..9282bd4eae1 --- /dev/null +++ b/tests/docs/project/ai-config-files/index.qmd @@ -0,0 +1,5 @@ +--- +title: "Home" +--- + +This is the home page that should be rendered. diff --git a/tests/smoke/project/project-ai-config.test.ts b/tests/smoke/project/project-ai-config.test.ts new file mode 100644 index 00000000000..b5a2752bc86 --- /dev/null +++ b/tests/smoke/project/project-ai-config.test.ts @@ -0,0 +1,37 @@ +/* + * project-ai-config.test.ts + * + * Verifies that AI assistant configuration files (CLAUDE.md, AGENTS.md) + * are properly excluded from project file discovery and rendering. + * + * Copyright (C) 2020-2025 Posit Software, PBC + */ + +import { docs } from "../../utils.ts"; +import { join } from "../../../src/deno_ral/path.ts"; +import { existsSync } from "../../../src/deno_ral/fs.ts"; +import { testQuartoCmd } from "../../test.ts"; +import { fileExists, pathDoNotExists, noErrors } from "../../verify.ts"; + +const projectDir = docs("project/ai-config-files"); +const outputDir = join(projectDir, "_site"); + +// Test that AI assistant config files are properly excluded +testQuartoCmd( + "render", + [projectDir], + [ + noErrors, + fileExists(join(outputDir, "index.html")), // Control: regular file should be rendered + pathDoNotExists(join(outputDir, "CLAUDE.html")), // CLAUDE.md should be ignored + pathDoNotExists(join(outputDir, "AGENTS.html")), // AGENTS.md should be ignored + ], + { + teardown: async () => { + // Clean up _site directory + if (existsSync(outputDir)) { + await Deno.remove(outputDir, { recursive: true }); + } + }, + }, +); diff --git a/tests/smoke/use/template-ai-config.test.ts b/tests/smoke/use/template-ai-config.test.ts new file mode 100644 index 00000000000..12e81b7854a --- /dev/null +++ b/tests/smoke/use/template-ai-config.test.ts @@ -0,0 +1,64 @@ +/* + * template-ai-config.test.ts + * + * Verifies that AI assistant configuration files (CLAUDE.md, AGENTS.md) + * are properly excluded from extension templates when using "quarto use template". + * + * Copyright (C) 2020-2025 Posit Software, PBC + */ + +import { testQuartoCmd } from "../../test.ts"; +import { fileExists, noErrorsOrWarnings, pathDoNotExists } from "../../verify.ts"; +import { join } from "../../../src/deno_ral/path.ts"; +import { ensureDirSync } from "../../../src/deno_ral/fs.ts"; + +const tempDir = Deno.makeTempDirSync(); + +// Create a mock template source with AI config files +const templateSourceDir = join(tempDir, "template-source"); +ensureDirSync(templateSourceDir); +Deno.writeTextFileSync( + join(templateSourceDir, "template.qmd"), + "---\ntitle: Template Document\n---\n\nThis is a template document." +); +Deno.writeTextFileSync( + join(templateSourceDir, "CLAUDE.md"), + "# Claude Configuration\n\nThis should be excluded." +); +Deno.writeTextFileSync( + join(templateSourceDir, "AGENTS.md"), + "# Agents Configuration\n\nThis should be excluded." +); +Deno.writeTextFileSync( + join(templateSourceDir, "README.md"), + "# Template README\n\nThis should also be excluded." +); + +const templateFolder = "test-ai-config-template"; +const workingDir = join(tempDir, templateFolder); +ensureDirSync(workingDir); + +testQuartoCmd( + "use", + ["template", templateSourceDir, "--no-prompt"], + [ + noErrorsOrWarnings, + fileExists(`${templateFolder}.qmd`), // Template file should be copied and renamed + pathDoNotExists(join(workingDir, "CLAUDE.md")), // CLAUDE.md should be excluded + pathDoNotExists(join(workingDir, "AGENTS.md")), // AGENTS.md should be excluded + pathDoNotExists(join(workingDir, "README.md")), // README.md should also be excluded (sanity check) + ], + { + cwd: () => { + return workingDir; + }, + teardown: () => { + try { + Deno.removeSync(tempDir, { recursive: true }); + } catch { + // Ignore cleanup errors + } + return Promise.resolve(); + } + } +); From f57df987278e8769bb79b186cf85c9bb6e7324f8 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 16 Jan 2026 22:10:30 +0100 Subject: [PATCH 3/3] Add testing patterns documentation Document standard patterns for writing Quarto smoke tests, including: - Project rendering test patterns - Extension template test patterns - Verification helpers and utilities - Path construction patterns - Best practices for test isolation and cleanup Co-Authored-By: Claude --- llm-docs/testing-patterns.md | 266 +++++++++++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 llm-docs/testing-patterns.md diff --git a/llm-docs/testing-patterns.md b/llm-docs/testing-patterns.md new file mode 100644 index 00000000000..ecaa4abdea7 --- /dev/null +++ b/llm-docs/testing-patterns.md @@ -0,0 +1,266 @@ +# Quarto Test Patterns + +This document describes the standard patterns for writing smoke tests in the Quarto CLI test suite. + +## Test Structure Overview + +Quarto uses Deno for testing with custom verification helpers located in: +- `tests/test.ts` - Core test runner (`testQuartoCmd`) +- `tests/verify.ts` - Verification helpers (`fileExists`, `pathDoNotExists`, etc.) +- `tests/utils.ts` - Utility functions (`docs()`, `outputForInput()`, etc.) + +## Common Test Patterns + +### Project Rendering Tests + +For testing project rendering (especially website projects): + +```typescript +import { docs } from "../../utils.ts"; +import { join } from "../../../src/deno_ral/path.ts"; +import { existsSync } from "../../../src/deno_ral/fs.ts"; +import { testQuartoCmd } from "../../test.ts"; +import { fileExists, pathDoNotExists, noErrors } from "../../verify.ts"; + +const projectDir = docs("project/my-test"); // Relative path via docs() +const outputDir = join(projectDir, "_site"); // Append output dir for websites + +testQuartoCmd( + "render", + [projectDir], + [ + noErrors, // Check for no errors + fileExists(join(outputDir, "index.html")), // Verify expected file exists + pathDoNotExists(join(outputDir, "ignored.html")), // Verify file doesn't exist + ], + { + teardown: async () => { + if (existsSync(outputDir)) { + await Deno.remove(outputDir, { recursive: true }); + } + }, + }, +); +``` + +**Key points:** +- Use `docs()` helper to create relative paths from `tests/docs/` +- For website projects, output goes to `_site` subdirectory +- Use absolute paths with `join()` for file verification +- Clean up output directories in teardown + +### Extension Template Tests + +For testing `quarto use template`: + +```typescript +import { testQuartoCmd } from "../../test.ts"; +import { fileExists, noErrorsOrWarnings, pathDoNotExists } from "../../verify.ts"; +import { join } from "../../../src/deno_ral/path.ts"; +import { ensureDirSync } from "../../../src/deno_ral/fs.ts"; + +const tempDir = Deno.makeTempDirSync(); + +// Create mock template source +const templateSourceDir = join(tempDir, "template-source"); +ensureDirSync(templateSourceDir); +Deno.writeTextFileSync(join(templateSourceDir, "template.qmd"), "..."); +Deno.writeTextFileSync(join(templateSourceDir, "config.yml"), "..."); + +const templateFolder = "my-test-template"; +const workingDir = join(tempDir, templateFolder); +ensureDirSync(workingDir); + +testQuartoCmd( + "use", + ["template", templateSourceDir, "--no-prompt"], + [ + noErrorsOrWarnings, + fileExists(`${templateFolder}.qmd`), // Relative - template file renamed to folder name + pathDoNotExists(join(workingDir, "README.md")), // Absolute - excluded file + ], + { + cwd: () => workingDir, // Set working directory + teardown: () => { + try { + Deno.removeSync(tempDir, { recursive: true }); + } catch { + // Ignore cleanup errors + } + return Promise.resolve(); + } + } +); +``` + +**Key points:** +- Use `Deno.makeTempDirSync()` for isolated test environment +- Create mock template source with test files +- Template files get renamed to match target directory name +- Use `cwd()` function to set working directory for command execution +- Clean up entire temp directory including source files +- File verification uses relative paths when checking files in `cwd()` + +## Verification Helpers + +### Core Verifiers + +```typescript +// No errors in output +noErrors + +// No errors or warnings +noErrorsOrWarnings + +// File exists at path +fileExists(path: string) + +// Path does not exist +pathDoNotExists(path: string) + +// Folder exists at path +folderExists(path: string) + +// Directory contains only allowed paths +directoryEmptyButFor(dir: string, allowedFiles: string[]) +``` + +### Path Helpers + +```typescript +// Create relative path from tests/docs/ +docs(path: string): string +// Example: docs("project/site") → "tests/docs/project/site" + +// Get expected output for input file +outputForInput(input: string, to: string, projectOutDir?: string, projectRoot?: string) + +// Find project directory +findProjectDir(input: string, until?: RegExp) + +// Find project output directory (_site, _book, etc.) +findProjectOutputDir(projectdir: string) +``` + +## Output Directory Patterns + +Different project types use different output directories: + +```typescript +// Website project +const outputDir = join(projectDir, "_site"); + +// Book project +const outputDir = join(projectDir, "_book"); + +// Manuscript project +const outputDir = join(projectDir, "_manuscript"); + +// Plain project (no type specified) +// Output goes directly in project directory +const outputDir = projectDir; +``` + +## Test File Organization + +Tests follow this directory structure: + +``` +tests/ +├── docs/ # Test fixtures +│ └── project/ +│ └── my-test/ +│ ├── _quarto.yml +│ ├── index.qmd +│ └── other-files.qmd +├── smoke/ # Smoke tests +│ ├── project/ +│ │ └── project-my-test.test.ts +│ ├── render/ +│ ├── use/ +│ └── ... +├── test.ts # Test runner +├── verify.ts # Verification helpers +└── utils.ts # Utility functions +``` + +## Common Patterns + +### Cleanup Pattern + +Always clean up generated files in teardown: + +```typescript +teardown: async () => { + if (existsSync(outputPath)) { + await Deno.remove(outputPath, { recursive: true }); + } +} +``` + +### Multiple Test Cases + +When testing multiple scenarios, declare constants at module level: + +```typescript +const tempDir = Deno.makeTempDirSync(); + +// Test case 1 +const folder1 = "test-case-1"; +const workingDir1 = join(tempDir, folder1); +ensureDirSync(workingDir1); +testQuartoCmd(...); + +// Test case 2 +const folder2 = "test-case-2"; +const workingDir2 = join(tempDir, folder2); +ensureDirSync(workingDir2); +testQuartoCmd(...); +``` + +### Path Construction + +- **Absolute paths**: Use `join()` for all path operations +- **Relative to docs**: Use `docs()` helper +- **Relative to cwd**: Use plain strings or template literals in template tests + +## Examples from Codebase + +### Project Ignore Test +See `tests/smoke/project/project-ignore-dirs.test.ts` for testing directory exclusion patterns. + +### Website Rendering Test +See `tests/smoke/project/project-website.test.ts` for website project rendering patterns. + +### Template Usage Test +See `tests/smoke/use/template.test.ts` for extension template patterns. + +## Best Practices + +1. **Always clean up**: Use teardown to remove generated files +2. **Use helpers**: Leverage `docs()`, `fileExists()`, etc. instead of manual checks +3. **Absolute paths**: Use `join()` for all path construction to handle platform differences +4. **Test isolation**: Use temp directories for tests that create files +5. **Clear names**: Use descriptive variable names like `projectDir`, `outputDir`, `templateFolder` +6. **Comment intent**: Add comments explaining what should/shouldn't happen +7. **Handle errors**: Wrap cleanup in try-catch to avoid test suite failures from cleanup issues + +## Testing File Exclusion + +When testing that files are excluded (like AI config files): + +```typescript +// Test that files are NOT rendered +testQuartoCmd( + "render", + [projectDir], + [ + noErrors, + fileExists(join(outputDir, "expected.html")), // Should exist + pathDoNotExists(join(outputDir, "excluded.html")), // Should NOT exist + ], + // ... +); +``` + +Run test **without fix** first to verify it fails, then verify it passes with fix.