From d7c7868a5c032a1b8027117156a953414e92fac3 Mon Sep 17 00:00:00 2001 From: quality Date: Sat, 3 Oct 2026 15:28:42 -0400 Subject: [PATCH] test: pin the .vscode/settings.json exclude contract .vscode/settings.json was the last developer-environment entry point with nothing reading it, and it had drifted in both directions: it excluded .docker/, vendor, /resources and /themes/docsy -- the layout of the Hugo site that preceded this one, none of which exist in the checkout -- while every directory this repo generates (build/, .docusaurus, coverage, test-results, playwright-report) was searched and watched. It also hid **/.vscode from the explorer, so the stale file was invisible to the contributors it misconfigured. Rewrite the file to exclude what the repo actually produces, and extend tests/dev-environment.test.mjs -- which already owns Justfile and devcontainer.json for the same reason -- with three guards: no excluded path may be absent from the checkout and ungitignored, every generated directory must be excluded from search and the watcher, and files.exclude may not hide .vscode. All three fail against the previous file. The first guard also caught .DS_Store being watcher-excluded but never gitignored, so add it to .gitignore. Signed-off-by: quality --- .gitignore | 3 + .vscode/settings.json | 26 +++---- tests/dev-environment.test.mjs | 134 ++++++++++++++++++++++++++++++++- 3 files changed, 148 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index a6ef028c..c6175f74 100644 --- a/.gitignore +++ b/.gitignore @@ -221,6 +221,9 @@ dist # End of https://www.toptal.com/developers/gitignore/api/node,docusaurus +### macOS ### +.DS_Store + ### Playwright ### /test-results/ /playwright-report/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 6c4b361c..ecf4bbcc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,25 +1,23 @@ { "files.exclude": { - ".docker/": true, - "**/.vscode": true, - "**/node_modules": true, - "vendor": true, - "/resources": true, - "/themes/docsy": true + "**/node_modules": true }, "files.watcherExclude": { - "**/.git": true, - "node_modules": true, - "vendor": true, "**/.DS_Store": true, + "**/.docusaurus": true, + "**/.git": true, "**/node_modules": true, - "/resources": true, - "/themes/docsy": true + "build": true, + "coverage": true, + "playwright-report": true, + "test-results": true }, "search.exclude": { + "**/.docusaurus": true, "**/node_modules": true, - "**/vendor": true, - "/resources": true, - "/themes/docsy": true + "build": true, + "coverage": true, + "playwright-report": true, + "test-results": true } } diff --git a/tests/dev-environment.test.mjs b/tests/dev-environment.test.mjs index 18dfa7a8..37b2ea7a 100644 --- a/tests/dev-environment.test.mjs +++ b/tests/dev-environment.test.mjs @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; -import { readFileSync, readdirSync } from 'node:fs'; +import { existsSync, readFileSync, readdirSync } from 'node:fs'; import { join } from 'node:path'; import test from 'node:test'; import { parse as parseYaml } from 'yaml'; @@ -446,3 +446,135 @@ test('.gitignore ignores node_modules as a file as well as a directory', () => { }); assert.equal(status, ''); }); + +// .vscode/settings.json is the third entry point a contributor meets before CI +// does, and the only one nothing read until now. It once described a Hugo site +// using the Docsy theme — `themes/docsy`, `vendor`, `resources`, `.docker/` — +// none of which this Docusaurus repo has, while every directory the repo does +// generate was searched and watched. Both halves of that drift are silent: +// VS Code ignores an exclude whose path is absent, and never reports one that +// is missing. +const vscodeSettings = JSON.parse( + stripJsonComments(readFileSync(join(root, '.vscode/settings.json'), 'utf8')), +); + +const EXCLUDE_MAPS = [ + 'files.exclude', + 'files.watcherExclude', + 'search.exclude', +]; + +// Directories the repo's own commands write, each ignored by .gitignore: +// `build/` and `.docusaurus` from the docusaurus build, `coverage` from +// test:unit:coverage, `test-results`/`playwright-report` from test:e2e. +const GENERATED_DIRS = [ + 'build', + '.docusaurus', + 'coverage', + 'test-results', + 'playwright-report', +]; + +// A VS Code exclude key is a glob. Only keys that name one literal path can be +// checked against the checkout; `**/.DS_Store` and friends address no single +// location. A leading `**/` is stripped because it means "at any depth", +// which includes the root. +function literalExcludePath(key) { + const trimmed = key.replace(/^\*\*\//, '').replace(/\/$/, ''); + if (trimmed === '' || /[*?{}[\]!]/.test(trimmed)) return null; + return trimmed; +} + +// git check-ignore matches a rule against the pathname it is handed, without +// stat()ing it, so the shape of the pathname decides which rules can match. +// .gitignore:10 is `/build/`, and a directory-only rule never matches the bare +// pathname `build`; a file rule such as `.DS_Store` never matches `.DS_Store/`. +// Both shapes are probed so either kind of rule counts. +function gitIgnores(path) { + return [path, `${path}/`].some((candidate) => { + try { + execFileSync('git', ['check-ignore', '-q', candidate], { + cwd: root, + stdio: 'ignore', + }); + return true; + } catch { + return false; + } + }); +} + +function excludeEntries(mapName) { + const map = vscodeSettings[mapName]; + assert.ok( + map && typeof map === 'object', + `.vscode/settings.json declares no ${mapName}`, + ); + return map; +} + +test('every path .vscode/settings.json excludes exists or is gitignored', () => { + const fiction = []; + for (const mapName of EXCLUDE_MAPS) { + for (const key of Object.keys(excludeEntries(mapName))) { + const path = literalExcludePath(key); + if (path === null) continue; + if (existsSync(join(root, path))) continue; + if (gitIgnores(path)) continue; + fiction.push(`${mapName}["${key}"]`); + } + } + assert.deepEqual( + fiction, + [], + `.vscode/settings.json excludes paths this repo neither contains nor generates: ${fiction.join(', ')}`, + ); +}); + +// Searching the build output is not a cosmetic annoyance: it buries every real +// hit under the bundled copy of the same source, and the watcher walking it +// costs a file handle per generated file. +test('.vscode/settings.json excludes every directory the repo generates', () => { + const missing = []; + for (const mapName of ['search.exclude', 'files.watcherExclude']) { + const map = excludeEntries(mapName); + for (const dir of GENERATED_DIRS) { + if (map[dir] === true) continue; + if (map[`**/${dir}`] === true) continue; + missing.push(`${mapName} omits ${dir}`); + } + } + assert.deepEqual( + missing, + [], + `generated directories are searched and watched: ${missing.join(', ')}`, + ); +}); + +// Every directory this test requires excluded has to be one git already +// ignores, or the rule above would be asking contributors to hide tracked +// files from themselves. +test('every directory required to be excluded is gitignored', () => { + const tracked = GENERATED_DIRS.filter((dir) => !gitIgnores(dir)); + assert.deepEqual( + tracked, + [], + `.gitignore no longer ignores directories .vscode/settings.json hides: ${tracked.join(', ')}`, + ); +}); + +// files.exclude hides entries from the explorer outright. Hiding .vscode hides +// this very file, which is how it stayed wrong: a contributor cannot fix +// configuration they cannot see. +test('files.exclude does not hide .vscode from the explorer', () => { + const hidden = Object.entries(excludeEntries('files.exclude')) + .filter( + ([key, value]) => value === true && literalExcludePath(key) === '.vscode', + ) + .map(([key]) => key); + assert.deepEqual( + hidden, + [], + `files.exclude hides the editor settings it is written in: ${hidden.join(', ')}`, + ); +});