#!/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; });