From 768a3275ea42912c3eba7868975de58ee090f704 Mon Sep 17 00:00:00 2001 From: warren Date: Sat, 10 Oct 2026 14:17:08 -0400 Subject: [PATCH] Exclude AppleDouble companions from test discovery (#11) --- PLAYTEST_NOTES.md | 14 + docs/METADATA_DISCOVERY.md | 53 +++ docs/metadata-discovery-validation.json | 41 ++ .../verify-appledouble-metadata-discovery.mjs | 405 ++++++++++++++++++ tsconfig.app.json | 9 +- vite.config.ts | 7 +- 6 files changed, 527 insertions(+), 2 deletions(-) create mode 100644 docs/METADATA_DISCOVERY.md create mode 100644 docs/metadata-discovery-validation.json create mode 100644 tools/verify-appledouble-metadata-discovery.mjs diff --git a/PLAYTEST_NOTES.md b/PLAYTEST_NOTES.md index b6305486..cca15ac4 100644 --- a/PLAYTEST_NOTES.md +++ b/PLAYTEST_NOTES.md @@ -5765,3 +5765,17 @@ result is claimed. The source browser build stops at missing ignored Manastorm assets; browser release/visual verification remains open. Native offline gear persistence, recovery presentation and authenticated RoM progression remain under #124. See docs/GAME_GEAR_NAMESPACES.md and gear-namespace-validation.json. + +## AppleDouble input discovery — shared #11 (2026-10-10) + +Vitest defaults now exclude ._-prefixed file/directory components while retaining +its built-in exclusions. A portable fresh-fixture proof reproduces three binary +sidecar transform failures in the unfiltered control; fixed defaults run both +ordinary sentinels, including ordinary._fragment.test.ts. TypeScript API/CLI +retain those two inputs. TypeScript 5.8 already skipped dot-prefixed roots, so +its explicit exclusions preserve observed behavior. Vite/Node config typecheck +passes. See docs/METADATA_DISCOVERY.md and metadata-discovery-validation.json. + +This is source tooling; no native runtime install or new dungeon entry was +performed. No observed Cellblock Rush boss, GPU capture, full app build, +gameplay or balance certification is claimed. Existing metadata is retained. diff --git a/docs/METADATA_DISCOVERY.md b/docs/METADATA_DISCOVERY.md new file mode 100644 index 00000000..6634d4a3 --- /dev/null +++ b/docs/METADATA_DISCOVERY.md @@ -0,0 +1,53 @@ +# macOS metadata and source discovery + +macOS can create AppleDouble companions whose path components start with `._`. +These files contain filesystem metadata and are not application source. On the +external-drive review checkout, the former Vitest include pattern selected +`src/avatar/._*.test.ts` files and attempted to transform their binary contents. +TypeScript 5.8 already skips the tested dot-prefixed paths with its existing +`src` include. Its new explicit exclusions document the same metadata policy +while preserving compiler options and ordinary input discovery. + +Shared source issue [#11](https://git.whoagland.com/phenom/healer-man/issues/11) +owns the default discovery correction: + +- `vite.config.ts` retains `configDefaults.exclude` and adds `**/._*` plus + `**/._*/**` to the test exclusions. +- `tsconfig.app.json` retains its compiler options and `include: ["src"]`. + Its exclusions retain the package-directory defaults and add the same + metadata patterns. + +The two patterns cover companion files and files inside a companion directory, +including nested directories. A component must start with `._`; ordinary +underscore names and names containing those characters elsewhere remain source. +Existing metadata files are retained. This changes root input discovery; +TypeScript's normal handling of explicit imports still applies. + +`tools/verify-appledouble-metadata-discovery.mjs` supplies a focused regression +proof through actual Vitest discovery/execution and TypeScript configuration +input parsing. It uses fresh task-owned fixtures to verify the rejection path +and two ordinary test sentinels to verify continued execution. It does not certify +full application typechecking, a browser production build, gameplay, balance, +character rendering or dungeon behavior. + +Exact commands, input identities and observed results accompany the issue and +`metadata-discovery-validation.json`. Shared source issue +[#9](https://git.whoagland.com/phenom/healer-man/issues/9) still owns its separate +browser build/live visual acceptance gap. Native checkpoint +[#292](https://git.whoagland.com/Codex/healer-man-godot/issues/292) is already +installed and merged. This tooling change requires no native runtime rebuild: +the native writable source mirror is a subset without these browser configs. + +Run the focused proof after installing the repository dependencies: + +```sh +node tools/verify-appledouble-metadata-discovery.mjs --evidence-dir /absolute/fresh-path +``` + +Use a fresh absolute path outside the checkout. The helper keeps its fixtures +and reports; it refuses existing destinations, overlap and symlink paths. It +works from another current directory and uses the Node executable that launched +it. The final external-drive control records three binary metadata transform +failures; fixed Vitest defaults run both ordinary tests. TypeScript API and CLI +resolve the same two ordinary inputs, with and without the explicit metadata +filters. The Vite/Node configuration typecheck passes. diff --git a/docs/metadata-discovery-validation.json b/docs/metadata-discovery-validation.json new file mode 100644 index 00000000..fa865479 --- /dev/null +++ b/docs/metadata-discovery-validation.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 1, + "date": "2026-10-10", + "issue": "https://git.whoagland.com/phenom/healer-man/issues/11", + "coordinator": "/root", + "session": "01a11cd3-fa7c-7510-ba8f-bc7a3d0310a6", + "sourceBase": "812162320d9bf041a1b635b553576962238e5138", + "files": { + "vite.config.ts": "c394d03344dd8cef5f5488e00e892e3a53b84d2374068688fdb1459114e5627c", + "tsconfig.app.json": "5cde2a8b2bd61121fbac7051ce403475ab487314b3970cadde8733ff5013292c", + "tools/verify-appledouble-metadata-discovery.mjs": "7ffc8b265d589c65e74122bd90062cc75413b52ffa4bb1daf737a22fcb128674" + }, + "proof": { + "path": "diagnostics/metadata-validation/attempt-08/report.json", + "sha256": "fa33b3b0bbbd4ce1f6141319ebe2a4f8b465aa408c865a8ea259cc8a2ca6f30e", + "portableAlternateCwd": "/Volumes/backup1tb-2", + "toolVersions": { + "vitest": "3.2.7", + "vite": "7.3.6", + "typescript": "5.8.3" + }, + "controlBinaryMetadataFailures": 3, + "candidateVitestOrdinaryTests": 2, + "candidateTypeScriptApiAndCliOrdinaryInputs": 2, + "metadataFileAndNestedDirectoryExcluded": true, + "ordinaryDotUnderscoreFragmentRetained": true, + "vitestDefaultsRetained": true, + "typescriptBaselineAlreadySkippedDotPaths": true + }, + "nodeConfigTypecheck": { + "passed": true, + "receiptSha256": "c2dee7d831ab89628589267892058210f700f96ce69af6fe05f5b7ecaa33f29b" + }, + "limits": [ + "Root input discovery only; explicit imports use normal compiler rules", + "No full application typecheck/build, balance, gameplay or graphics certification", + "TypeScript explicit exclusions preserve observed TS5.8 behavior", + "Native input mirror lacks browser configs; no native runtime installation", + "Existing metadata files and other workers edits retained" + ] +} diff --git a/tools/verify-appledouble-metadata-discovery.mjs b/tools/verify-appledouble-metadata-discovery.mjs new file mode 100644 index 00000000..a91da7ca --- /dev/null +++ b/tools/verify-appledouble-metadata-discovery.mjs @@ -0,0 +1,405 @@ +#!/usr/bin/env node +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { spawnSync } from "node:child_process"; +import { + lstatSync, + mkdirSync, + readFileSync, + realpathSync, + writeFileSync, +} from "node:fs"; +import { createRequire } from "node:module"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const METADATA_EXCLUDES = ["**/._*", "**/._*/**"]; +const SENTINELS = ["src/metadata-discovery-sentinel.test.ts", "src/ordinary._fragment.test.ts"]; +const SENTINEL = SENTINELS[0]; +const METADATA_FILE = "src/._appledouble-file.test.ts"; +const METADATA_NESTED_FILE = "src/._appledouble-directory/nested-metadata.test.ts"; + +function sha256(value) { + return createHash("sha256").update(value).digest("hex"); +} + +function sha256File(filePath) { + assertRegularNoSymlink(filePath); + return sha256(readFileSync(filePath)); +} + +function assertRegularNoSymlink(filePath) { + const stat = lstatSync(filePath); + assert.equal(stat.isSymbolicLink(), false, `symbolic link is not accepted: ${filePath}`); + assert.equal(stat.isFile(), true, `expected a regular file: ${filePath}`); +} + +function assertNoSymlinkComponents(targetPath) { + const absolute = path.resolve(targetPath); + const parsed = path.parse(absolute); + let current = parsed.root; + for (const component of absolute.slice(parsed.root.length).split(path.sep).filter(Boolean)) { + current = path.join(current, component); + try { + const stat = lstatSync(current); + assert.equal(stat.isSymbolicLink(), false, `symbolic path component is not accepted: ${current}`); + } catch (error) { + if (error.code === "ENOENT") break; + throw error; + } + } +} + +function hasPathPrefix(parent, candidate) { + const relative = path.relative(parent, candidate); + return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative)); +} + +function canonicalizeProspectivePath(targetPath) { + let current = path.resolve(targetPath); + const suffix = []; + while (true) { + try { + const canonicalAncestor = realpathSync(current); + return path.resolve(canonicalAncestor, ...suffix.reverse()); + } catch (error) { + if (error.code !== "ENOENT") throw error; + const parent = path.dirname(current); + assert.notEqual(parent, current, `no existing ancestor for evidence path: ${targetPath}`); + suffix.push(path.basename(current)); + current = parent; + } + } +} + +function assertNoAppleDoublePathComponents(targetPath) { + const absolute = path.resolve(targetPath); + const parsed = path.parse(absolute); + const components = absolute.slice(parsed.root.length).split(path.sep).filter(Boolean); + for (const component of components) { + assert.equal(component.startsWith("._"), false, `AppleDouble path component is not accepted: ${component}`); + } +} + +function assertFreshEvidenceDirectory(targetPath) { + assert.equal(path.isAbsolute(targetPath), true, "--evidence-dir must be absolute"); + const absolute = path.resolve(targetPath); + assert.equal(absolute, targetPath, "--evidence-dir must be normalized"); + assertNoAppleDoublePathComponents(absolute); + assertNoSymlinkComponents(absolute); + const canonicalProjectRoot = realpathSync(PROJECT_ROOT); + const canonicalEvidence = canonicalizeProspectivePath(absolute); + assert.equal(hasPathPrefix(canonicalProjectRoot, canonicalEvidence) || hasPathPrefix(canonicalEvidence, canonicalProjectRoot), false, + "evidence directory must not overlap the checkout"); + try { + lstatSync(absolute); + assert.fail(`evidence directory already exists; use a fresh path: ${absolute}`); + } catch (error) { + if (error.code !== "ENOENT") throw error; + } + mkdirSync(path.dirname(absolute), { recursive: true }); + mkdirSync(absolute, { recursive: false }); + return absolute; +} + +function parseArgs(argv) { + const args = {}; + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (token !== "--evidence-dir" || !argv[index + 1]) { + throw new Error("usage: verify-appledouble-metadata-discovery.mjs --evidence-dir ABSOLUTE_FRESH_PATH"); + } + if (args.evidenceDir) throw new Error("--evidence-dir may be provided once"); + args.evidenceDir = argv[index + 1]; + index += 1; + } + if (!args.evidenceDir || !path.isAbsolute(args.evidenceDir)) { + throw new Error("--evidence-dir must be an absolute path"); + } + return args; +} + +function runNode(args, cwd, stdoutPath, stderrPath) { + const result = spawnSync(process.execPath, args, { + cwd, + encoding: "utf8", + maxBuffer: 16 * 1024 * 1024, + env: { ...process.env, CI: "1" }, + }); + writeFileSync(stdoutPath, result.stdout ?? "", "utf8"); + writeFileSync(stderrPath, result.stderr ?? "", "utf8"); + if (result.error) throw result.error; + return { + argv: [process.execPath, ...args], + cwd, + exitCode: result.status, + signal: result.signal, + stdout: { path: stdoutPath, sha256: sha256File(stdoutPath) }, + stderr: { path: stderrPath, sha256: sha256File(stderrPath) }, + error: result.error?.message ?? null, + }; +} + +function readGitHead() { + const result = spawnSync("git", ["rev-parse", "HEAD"], { + cwd: PROJECT_ROOT, + encoding: "utf8", + }); + return result.status === 0 ? result.stdout.trim() : null; +} + +function generatedTest(importPath, name, body) { + return `import { expect, it } from ${JSON.stringify(importPath)};\n\nit(${JSON.stringify(name)}, () => {\n${body}\n});\n`; +} + +function readVitestFiles(jsonPath, fixtureRoot) { + const report = JSON.parse(readFileSync(jsonPath, "utf8")); + const testResults = report.testResults; + assert.ok(Array.isArray(testResults), "Vitest JSON reporter must provide testResults"); + return testResults.map((result) => path.relative(fixtureRoot, result.name).split(path.sep).join("/")).sort(); +} + +function writeJson(filePath, value) { + writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); +} + +let evidenceRootForFailure = null; + +async function main() { + const { evidenceDir } = parseArgs(process.argv.slice(2)); + const absoluteEvidence = assertFreshEvidenceDirectory(evidenceDir); + evidenceRootForFailure = absoluteEvidence; + + const reportPath = path.join(absoluteEvidence, "report.json"); + const R = createRequire(path.join(PROJECT_ROOT, "package.json")); + const vitestRunner = R.resolve("vitest/vitest.mjs"); + const vitestEntry = R.resolve("vitest/dist/index.js"); + const vitestConfigEntry = R.resolve("vitest/dist/config.js"); + const viteEntry = R.resolve("vite"); + const tsEntry = R.resolve("typescript"); + const vitestPackage = JSON.parse(readFileSync(path.join(PROJECT_ROOT, "node_modules", "vitest", "package.json"), "utf8")); + const vitePackage = JSON.parse(readFileSync(path.join(PROJECT_ROOT, "node_modules", "vite", "package.json"), "utf8")); + const tsPackage = JSON.parse(readFileSync(path.join(PROJECT_ROOT, "node_modules", "typescript", "package.json"), "utf8")); + const configInfo = {}; + for (const name of ["vite.config.ts", "tsconfig.app.json"]) { + const actualPath = path.join(PROJECT_ROOT, name); + const actualHash = sha256File(actualPath); + configInfo[name] = { path: actualPath, sha256: actualHash }; + } + + const vite = await import(pathToFileURL(viteEntry).href); + const { configDefaults } = await import(pathToFileURL(vitestConfigEntry).href); + const loaded = await vite.loadConfigFromFile({ command: "serve", mode: "test" }, path.join(PROJECT_ROOT, "vite.config.ts"), PROJECT_ROOT); + assert.ok(loaded?.config, "Vite must load the actual project config"); + const testConfig = loaded.config.test; + assert.deepEqual(testConfig.include, ["src/**/*.test.ts"], "candidate must preserve the normal Vitest include"); + assert.ok(Array.isArray(testConfig.exclude), "project Vitest config must define exclude patterns"); + for (const defaultExclude of configDefaults.exclude) { + assert.ok(testConfig.exclude.includes(defaultExclude), `Vitest built-in exclusion was dropped: ${defaultExclude}`); + } + for (const metadataExclude of METADATA_EXCLUDES) { + assert.ok(testConfig.exclude.includes(metadataExclude), `metadata exclusion is missing: ${metadataExclude}`); + } + const baselineTestConfig = { + ...testConfig, + exclude: testConfig.exclude.filter((pattern) => !METADATA_EXCLUDES.includes(pattern)), + }; + + const fixtureRoot = path.join(absoluteEvidence, "fixture-project"); + const srcRoot = path.join(fixtureRoot, "src"); + mkdirSync(srcRoot, { recursive: true }); + writeFileSync(path.join(srcRoot, "._appledouble-file.test.ts"), + generatedTest(vitestEntry, "AppleDouble file fixture is discoverable without the scoped exclusion", " expect(true).toBe(true);\n"), "utf8"); + mkdirSync(path.join(srcRoot, "._appledouble-directory"), { recursive: true }); + writeFileSync(path.join(srcRoot, "._appledouble-directory", "nested-metadata.test.ts"), + generatedTest(vitestEntry, "AppleDouble nested-directory fixture is discoverable without the scoped exclusion", " expect(true).toBe(true);\n"), "utf8"); + writeFileSync(path.join(srcRoot, "metadata-discovery-sentinel.test.ts"), + generatedTest(vitestEntry, "ordinary source sentinel runs", " expect(2 + 2).toBe(4);\n"), "utf8"); + writeFileSync(path.join(srcRoot, "ordinary._fragment.test.ts"), + generatedTest(vitestEntry, "ordinary filename containing dot underscore remains included", " expect(" + + "'ordinary._fragment.test.ts'.startsWith('._')).toBe(false);\n"), "utf8"); + // This control is derived from the actual loaded config and removes only the two issue filters. + const baselineConfig = path.join(absoluteEvidence, "baseline-vitest.config.mjs"); + writeFileSync(baselineConfig, `export default ${JSON.stringify({ test: baselineTestConfig }, null, 2)};\n`, "utf8"); + + const baselineJson = path.join(absoluteEvidence, "vitest-baseline-results.json"); + const baseline = runNode([vitestRunner, "run", "--root", fixtureRoot, "--config", baselineConfig, + "--reporter=json", `--outputFile=${baselineJson}`, "--maxWorkers=1"], PROJECT_ROOT, + path.join(absoluteEvidence, "vitest-baseline.stdout.log"), path.join(absoluteEvidence, "vitest-baseline.stderr.log")); + const baselineFiles = readVitestFiles(baselineJson, fixtureRoot); + for (const sentinel of SENTINELS) { + assert.ok(baselineFiles.includes(sentinel), `baseline Vitest control must execute ordinary file: ${sentinel}`); + } + assert.ok(baselineFiles.includes(METADATA_FILE), "baseline control must discover the AppleDouble file fixture"); + assert.ok(baselineFiles.includes(METADATA_NESTED_FILE), "baseline control must discover the nested AppleDouble fixture"); + const baselineJsonResult = JSON.parse(readFileSync(baselineJson, "utf8")); + const baselineFailures = baselineJsonResult.testResults + .filter((result) => result.status === "failed") + .map((result) => ({ + path: path.relative(fixtureRoot, result.name).split(path.sep).join("/"), + message: result.message, + })); + assert.ok(baseline.exitCode === 0 || baseline.exitCode === 1, "baseline must exit normally, not crash"); + for (const failure of baselineFailures) { + assert.ok(path.posix.basename(failure.path).startsWith("._"), + `baseline may fail only while transforming an AppleDouble sidecar: ${failure.path}`); + assert.ok(failure.message.includes("Unexpected") && failure.message.includes("\\x00"), + `baseline AppleDouble transform failure should identify binary metadata: ${failure.path}`); + } + + const candidateJson = path.join(absoluteEvidence, "vitest-candidate-results.json"); + const candidate = runNode([vitestRunner, "run", "--root", fixtureRoot, + "--config", path.join(PROJECT_ROOT, "vite.config.ts"), "--mode", "test", "--reporter=json", + `--outputFile=${candidateJson}`, "--maxWorkers=1"], PROJECT_ROOT, + path.join(absoluteEvidence, "vitest-candidate.stdout.log"), path.join(absoluteEvidence, "vitest-candidate.stderr.log")); + assert.equal(candidate.exitCode, 0, "candidate Vitest discovery must pass"); + const candidateFiles = readVitestFiles(candidateJson, fixtureRoot); + assert.deepEqual(candidateFiles, [...SENTINELS].sort(), "candidate discovery must keep ordinary names and exclude dot-prefix metadata"); + + const ts = R("typescript"); + const tsconfigSource = path.join(PROJECT_ROOT, "tsconfig.app.json"); + const tsconfigPath = path.join(fixtureRoot, "tsconfig.app.json"); + const tsconfigBytes = readFileSync(tsconfigSource); + writeFileSync(tsconfigPath, tsconfigBytes); + const tsconfigJson = ts.readConfigFile(tsconfigPath, ts.sys.readFile); + assert.equal(tsconfigJson.error, undefined, "TypeScript must read the copied app config"); + const parsedTsconfig = ts.parseJsonConfigFileContent(tsconfigJson.config, ts.sys, fixtureRoot, undefined, tsconfigPath); + assert.deepEqual(parsedTsconfig.errors, [], "TypeScript config must parse without diagnostics"); + assert.deepEqual(tsconfigJson.config.include, ["src"], "TypeScript include must retain normal src scope"); + assert.deepEqual(tsconfigJson.config.compilerOptions.types, ["vite/client", "vitest/globals"], + "TypeScript configured test/browser globals must remain unchanged"); + const tsFiles = parsedTsconfig.fileNames.map((filePath) => path.relative(fixtureRoot, filePath).split(path.sep).join("/")).sort(); + assert.deepEqual(tsFiles, [...SENTINELS].sort(), "TypeScript must keep ordinary names and exclude dot-prefix metadata"); + const expectedPackageExcludes = ["node_modules", "bower_components", "jspm_packages"]; + for (const exclude of expectedPackageExcludes) { + assert.ok(tsconfigJson.config.exclude.includes(exclude), `TypeScript package exclusion was dropped: ${exclude}`); + } + for (const exclude of METADATA_EXCLUDES) { + assert.ok(tsconfigJson.config.exclude.includes(exclude), `TypeScript metadata exclusion is missing: ${exclude}`); + } + + const tscPath = path.resolve(path.dirname(tsEntry), "../bin/tsc"); + const baselineTsconfigPath = path.join(fixtureRoot, "tsconfig.app.without-appledouble-excludes.json"); + const baselineTsconfigObject = JSON.parse(tsconfigBytes.toString("utf8")); + baselineTsconfigObject.exclude = baselineTsconfigObject.exclude.filter((pattern) => !METADATA_EXCLUDES.includes(pattern)); + writeFileSync(baselineTsconfigPath, `${JSON.stringify(baselineTsconfigObject, null, 2)}\n`, "utf8"); + const baselineTsconfigJson = ts.readConfigFile(baselineTsconfigPath, ts.sys.readFile); + assert.equal(baselineTsconfigJson.error, undefined, "TypeScript must read the filtered-control app config"); + const baselineParsedTsconfig = ts.parseJsonConfigFileContent( + baselineTsconfigJson.config, ts.sys, fixtureRoot, undefined, baselineTsconfigPath, + ); + assert.deepEqual(baselineParsedTsconfig.errors, [], "filtered-control TypeScript config must parse without diagnostics"); + const baselineTsFiles = baselineParsedTsconfig.fileNames + .map((filePath) => path.relative(fixtureRoot, filePath).split(path.sep).join("/")).sort(); + assert.deepEqual(baselineTsFiles, [...SENTINELS].sort(), + "TypeScript 5.8 should continue to skip dot paths even without the explicit filters"); + const baselineTsc = runNode([tscPath, "--showConfig", "--project", baselineTsconfigPath], PROJECT_ROOT, + path.join(absoluteEvidence, "tsc-control-show-config.stdout.json"), path.join(absoluteEvidence, "tsc-control-show-config.stderr.log")); + assert.equal(baselineTsc.exitCode, 0, "the filtered-control TypeScript CLI --showConfig must accept the fixture config"); + const baselineTscJson = JSON.parse(readFileSync(path.join(absoluteEvidence, "tsc-control-show-config.stdout.json"), "utf8")); + const baselineTscFiles = baselineTscJson.files + .map((filePath) => path.relative(fixtureRoot, path.resolve(fixtureRoot, filePath)).split(path.sep).join("/")).sort(); + assert.deepEqual(baselineTscFiles, [...SENTINELS].sort()); + assert.deepEqual(baselineTscFiles, baselineTsFiles, + "TypeScript compiler API and CLI must agree on filtered-control src inputs"); + const tsc = runNode([tscPath, "--showConfig", "--project", tsconfigPath], PROJECT_ROOT, + path.join(absoluteEvidence, "tsc-show-config.stdout.json"), path.join(absoluteEvidence, "tsc-show-config.stderr.log")); + assert.equal(tsc.exitCode, 0, "the actual TypeScript CLI --showConfig must accept the fixture config"); + const tscJson = JSON.parse(readFileSync(path.join(absoluteEvidence, "tsc-show-config.stdout.json"), "utf8")); + const tscFiles = tscJson.files.map((filePath) => path.relative(fixtureRoot, path.resolve(fixtureRoot, filePath)).split(path.sep).join("/")).sort(); + assert.deepEqual(tscFiles, [...SENTINELS].sort(), "the TypeScript CLI must resolve the same two ordinary inputs"); + + const report = { + schemaVersion: 1, + passed: true, + issue: 11, + node: { executable: process.execPath, version: process.version }, + invocationCwd: process.cwd(), + tool: { path: fileURLToPath(import.meta.url), sha256: sha256File(fileURLToPath(import.meta.url)) }, + project: { + root: PROJECT_ROOT, + gitHead: readGitHead(), + configHashes: configInfo, + }, + packageVersions: { vitest: vitestPackage.version, vite: vitePackage.version, typescript: tsPackage.version }, + fixtures: { + root: fixtureRoot, + ordinarySentinel: { path: SENTINEL, sha256: sha256File(path.join(fixtureRoot, SENTINEL)) }, + ordinaryDotUnderscoreFragment: { + path: SENTINELS[1], + sha256: sha256File(path.join(fixtureRoot, SENTINELS[1])), + reasonIncluded: "the basename contains ._ but does not begin with the AppleDouble component prefix ._", + }, + metadataFile: { path: METADATA_FILE, sha256: sha256File(path.join(fixtureRoot, METADATA_FILE)) }, + metadataNestedFile: { path: METADATA_NESTED_FILE, sha256: sha256File(path.join(fixtureRoot, METADATA_NESTED_FILE)) }, + }, + vitest: { + unfilteredControl: { + description: "actual loaded test config with only the two issue-owned excludes removed; all other settings and Vitest built-in defaults are retained", + configSha256: sha256File(baselineConfig), + removedExcludes: METADATA_EXCLUDES, + command: baseline, + jsonReport: { path: baselineJson, sha256: sha256File(baselineJson) }, + exitCode: baseline.exitCode, + failures: baselineFailures, + discoveredFiles: baselineFiles, + ordinarySentinelsExecuted: SENTINELS.every((name) => baselineFiles.includes(name)), + metadataFixturesDiscovered: [METADATA_FILE, METADATA_NESTED_FILE].every((name) => baselineFiles.includes(name)), + completedNormally: baseline.exitCode === 0 || baseline.exitCode === 1, + }, + candidate: { + command: candidate, + jsonReport: { path: candidateJson, sha256: sha256File(candidateJson) }, + exitCode: candidate.exitCode, + discoveredFiles: candidateFiles, + expectedOrdinaryFiles: [...SENTINELS].sort(), + include: testConfig.include, + builtinExcludesPreserved: configDefaults.exclude.every((name) => testConfig.exclude.includes(name)), + metadataExcludes: METADATA_EXCLUDES, + }, + }, + typescript: { + compilerApi: { + parsedConfigPath: tsconfigPath, + parsedConfigSha256: sha256(tsconfigBytes), + diagnostics: parsedTsconfig.errors.length, + }, + baseline: { + configPath: baselineTsconfigPath, + removedExcludes: METADATA_EXCLUDES, + parsedFiles: baselineTsFiles, + metadataFileIncludedBeforeExplicitExcludes: baselineTsFiles.includes(METADATA_FILE), + nestedMetadataIncludedBeforeExplicitExcludes: baselineTsFiles.includes(METADATA_NESTED_FILE), + note: "TypeScript 5.8 already skips dot-prefixed paths during include globbing; the candidate also retains explicit excludes for portable input hygiene.", + cliCommand: baselineTsc, + cliJsonSha256: sha256File(path.join(absoluteEvidence, "tsc-control-show-config.stdout.json")), + }, + cliCommand: tsc, + cliJsonSha256: sha256File(path.join(absoluteEvidence, "tsc-show-config.stdout.json")), + parsedFiles: tsFiles, + cliParsedFiles: tscFiles, + include: tsconfigJson.config.include, + types: tsconfigJson.config.compilerOptions.types, + packageExcludesPreserved: expectedPackageExcludes.every((name) => tsconfigJson.config.exclude.includes(name)), + metadataExcludes: METADATA_EXCLUDES, + copiedConfigSha256: sha256(tsconfigBytes), + }, + logs: ["vitest-baseline.stdout.log", "vitest-baseline.stderr.log", "vitest-candidate.stdout.log", + "vitest-candidate.stderr.log", "tsc-control-show-config.stderr.log", "tsc-show-config.stderr.log"].map((name) => ({ + path: path.join(absoluteEvidence, name), + sha256: sha256File(path.join(absoluteEvidence, name)), + })), + }; + writeJson(reportPath, report); + process.stdout.write(`${reportPath}\n`); +} + +main().catch((error) => { + const safe = { passed: false, error: `${error.name}: ${error.message}` }; + if (evidenceRootForFailure) { + try { writeJson(path.join(evidenceRootForFailure, "failure.json"), safe); } catch { /* preserve original failure */ } + } + process.stderr.write(`${safe.error}\n`); + process.exitCode = 1; +}); diff --git a/tsconfig.app.json b/tsconfig.app.json index 363ef2f3..4565d65b 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -29,5 +29,12 @@ "jsx": "react-jsx", "types": ["vite/client", "vitest/globals"] }, - "include": ["src"] + "include": ["src"], + "exclude": [ + "node_modules", + "bower_components", + "jspm_packages", + "**/._*", + "**/._*/**" + ] } diff --git a/vite.config.ts b/vite.config.ts index 3c5621ff..bec4352f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,6 @@ import { readFileSync } from "node:fs"; import path from "node:path"; -import { defineConfig } from "vitest/config"; +import { configDefaults, defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react"; // @ts-expect-error Shared Node HTTP middleware also serves production bundles. import { createBundledContentHandler } from "./server/bundled-content.mjs"; @@ -119,6 +119,11 @@ export default defineConfig(({ mode }) => { test: { environment: "node", include: ["src/**/*.test.ts"], + exclude: [ + ...configDefaults.exclude, + "**/._*", + "**/._*/**", + ], maxWorkers: 4, }, }; -- 2.54.0