From 43e81dec031517a8754cd9dda8fba6186e18c57f Mon Sep 17 00:00:00 2001 From: Derek Lewis Date: Wed, 12 Aug 2026 17:27:29 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=A7=AA=E2=9C=A8=EF=BC=9Atest=20the=20?= =?UTF-8?q?glob=20helper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first tests in this repository, aimed at the function that has produced two real bugs: directory entries reaching shell commands, and wildcards never matching a dot name. Both are three lines to pin down. Node's own test runner, so nothing is added to install. The fixture is a temporary tree the suite becomes the working directory of, since every pattern a task writes is relative to where the task runs. Assisted-by: Claude-Code:claude-opus-5 --- build/utils.test.mts | 111 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 build/utils.test.mts diff --git a/build/utils.test.mts b/build/utils.test.mts new file mode 100644 index 000000000..2584dfd95 --- /dev/null +++ b/build/utils.test.mts @@ -0,0 +1,111 @@ +/** + * @file Tests for the common build task utilities. + * @author The OpenINF Authors & Friends + * @license MIT OR Apache-2.0 OR BlueOak-1.0.0 + * @module {type ES6Module} build/utils.test + */ + +import { deepStrictEqual, ok } from 'node:assert/strict'; +import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join as pathJoin } from 'node:path'; +import { after, before, describe, test } from 'node:test'; +import { glob } from '@openinf/portal/build/utils'; + +// Every pattern a build task writes is relative to the directory the task +// runs in, so the fixture has to become that directory. +const cwd = process.cwd(); + +/** Files laid out to cover what the tasks actually ask of `glob`. */ +const FIXTURE = [ + 'a.md', + '.hidden.md', // a dot file beside ordinary ones + 'sub/b.md', + 'sub/nested/c.md', + '.dotdir/d.md', // a dot directory to descend through + '.dotdir/deep/e.md', + 'skipped/f.md', + 'skipped/.g.md', // a dot file inside an excluded directory + '.git/h.md', // git's own directory, never any task's business + 'a.txt', // a different extension, to prove patterns discriminate +]; + +const sorted = (paths: string[]) => [...paths].sort(); + +describe('glob', () => { + before(async () => { + const root = await mkdtemp(pathJoin(tmpdir(), 'openinf-glob-')); + + for (const path of FIXTURE) { + const full = pathJoin(root, path); + + await mkdir(dirname(full), { recursive: true }); + await writeFile(full, ''); + } + + process.chdir(root); + }); + + after(() => { + process.chdir(cwd); + }); + + test('returns paths relative to the working directory', async () => { + deepStrictEqual(await glob('a.md'), ['a.md']); + }); + + test('takes a lone pattern as well as a list', async () => { + deepStrictEqual(await glob(['a.md']), await glob('a.md')); + }); + + test('discriminates by extension', async () => { + deepStrictEqual(await glob('*.txt'), ['a.txt']); + }); + + test('excludes what a `!` pattern names', async () => { + const files = await glob(['**/*.md', '!skipped/']); + + ok(!files.some((file) => file.startsWith('skipped/'))); + ok(files.includes('sub/b.md')); + }); + + test('a trailing slash covers a whole subtree, not one entry', async () => { + // `sub/` on its own matches the directory and nothing in it, which is + // never what naming a directory is meant to mean. + deepStrictEqual(sorted(await glob('sub/')), [ + 'sub/b.md', + 'sub/nested/c.md', + ]); + }); + + test('never returns a directory', async () => { + // Callers paste the result into shell commands, where a directory + // argument makes the tool recurse and quietly undo the exclusions. + const files = await glob(['**/*', '!.git/']); + + ok(!files.includes('sub')); + ok(!files.includes('.dotdir')); + ok(files.includes('sub/b.md')); + }); + + test('matches a dot file that a bare wildcard would skip', async () => { + ok((await glob('**/*.md')).includes('.hidden.md')); + }); + + test('descends into a dot directory', async () => { + const files = await glob('**/*.md'); + + ok(files.includes('.dotdir/d.md')); + ok(files.includes('.dotdir/deep/e.md')); + }); + + test('leaves .git alone without being asked', async () => { + ok(!(await glob('**/*.md')).some((file) => file.startsWith('.git/'))); + }); + + test('prunes dot files inside an excluded directory', async () => { + // The exclusion is written without regard for dot entries, so pruning + // has to cover them or matching dot names would reopen what it closed. + ok(!(await glob(['**/*.md', '!skipped/'])).includes('skipped/.g.md')); + }); +}); From e2cfa8a5a12fddaa0e293f780c7102a9a1c8b935 Mon Sep 17 00:00:00 2001 From: Derek Lewis Date: Wed, 12 Aug 2026 17:27:29 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=A7=AA=E2=9C=A8=EF=BC=9Arun=20the=20t?= =?UTF-8?q?ests=20as=20part=20of=20verify?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `nps test` has run thirteen linters and no tests, while the workflow that calls it is named "lint and test". Registering the runner under verify puts it in `verify.all`, and so in CI, without touching the workflow. `node --test` given a pattern that matches nothing exits 0, so the task counts the files it found rather than forwarding the pattern and trusting the exit code. Assisted-by: Claude-Code:claude-opus-5 --- build/tasks/verify/verify-unit.mts | 28 ++++++++++++++++++++++++++++ package-scripts.yml | 1 + 2 files changed, 29 insertions(+) create mode 100644 build/tasks/verify/verify-unit.mts diff --git a/build/tasks/verify/verify-unit.mts b/build/tasks/verify/verify-unit.mts new file mode 100644 index 000000000..79352fab0 --- /dev/null +++ b/build/tasks/verify/verify-unit.mts @@ -0,0 +1,28 @@ +/** + * @file Verify the build task helpers behave as their callers assume. + * @author The OpenINF Authors & Friends + * @license MIT OR Apache-2.0 OR BlueOak-1.0.0 + * @module {type ES6Module} build/tasks/verify/verify-unit + */ + +import { exec, glob } from '@openinf/portal/build/utils'; + +const testFiles = await glob(['**/*.test.mts', '!_site/', '!node_modules/']); + +// `node --test` handed a pattern that matches nothing exits 0, so a task that +// only forwarded the pattern would report success having run no tests. The +// count is the guard against that -- and against `glob` itself finding +// nothing, which is among the failures these very tests exist to catch. +if (testFiles.length === 0) { + console.error('No test files matched `**/*.test.mts`.'); + process.exitCode = 1; +} else { + let exitCode = 0; + const scripts = [`node --test ${testFiles.join(' ')}`]; + + for (const element of scripts) { + exitCode = await exec(element); + + if (exitCode !== 0) process.exitCode = exitCode; + } +} diff --git a/package-scripts.yml b/package-scripts.yml index fc0d4b500..4b855f3da 100644 --- a/package-scripts.yml +++ b/package-scripts.yml @@ -19,6 +19,7 @@ scripts: svg: node build/tasks/verify/verify-svg.mts toml: node build/tasks/verify/verify-toml.mts ts: node build/tasks/verify/verify-ts.mts + unit: node build/tasks/verify/verify-unit.mts validForEC: node build/tasks/verify/verify-valid-for-ec.mts yaml: node build/tasks/verify/verify-yaml.mts format: From 1837a6e26a24446fda9d2e63b5bf3fff47603aff Mon Sep 17 00:00:00 2001 From: Derek Lewis Date: Thu, 13 Aug 2026 03:03:02 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=A7=AA=F0=9F=94=A7=EF=BC=9Agive=20the?= =?UTF-8?q?=20trailing-slash=20test=20a=20dot=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test named for covering a whole subtree had no dot file in its fixture, so it passed while never exercising the thing this branch is about -- and it went on passing when the bug it should have caught was put back. It fails now without the fix. Assisted-by: Claude-Code:claude-opus-5 --- build/utils.test.mts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/build/utils.test.mts b/build/utils.test.mts index 2584dfd95..d3ad601d2 100644 --- a/build/utils.test.mts +++ b/build/utils.test.mts @@ -21,6 +21,7 @@ const FIXTURE = [ 'a.md', '.hidden.md', // a dot file beside ordinary ones 'sub/b.md', + 'sub/.hidden-too.md', // a dot file under a directory named outright 'sub/nested/c.md', '.dotdir/d.md', // a dot directory to descend through '.dotdir/deep/e.md', @@ -71,8 +72,11 @@ describe('glob', () => { test('a trailing slash covers a whole subtree, not one entry', async () => { // `sub/` on its own matches the directory and nothing in it, which is - // never what naming a directory is meant to mean. + // never what naming a directory is meant to mean. Dot files included: + // the pattern this expands to has no basename for the dot alternative to + // attach to, so they went missing until it was given one of its own. deepStrictEqual(sorted(await glob('sub/')), [ + 'sub/.hidden-too.md', 'sub/b.md', 'sub/nested/c.md', ]);