Exclude AppleDouble companions from test discovery (#11)
This commit is contained in:
1 parent
812162320d
commit
768a3275ea
6 files changed
+527
-2
No files matched your search
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
+8
-1
@@ -29,5 +29,12 @@
|
||||
"jsx": "react-jsx",
|
||||
"types": ["vite/client", "vitest/globals"]
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": ["src"],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"jspm_packages",
|
||||
"**/._*",
|
||||
"**/._*/**"
|
||||
]
|
||||
}
|
||||
+6
-1
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user