diff --git a/e2e/global-setup.ts b/e2e/global-setup.ts new file mode 100644 index 0000000..cca86d5 --- /dev/null +++ b/e2e/global-setup.ts @@ -0,0 +1,11 @@ +import { execSync } from "node:child_process"; +import path from "node:path"; + +/** + * Bundle e2e runs `packages/deepsec/dist/cli.mjs`. Always rebuild before the + * e2e project so tests never execute a stale dist/ from an older checkout. + */ +export default function globalSetup(): void { + const root = path.resolve(import.meta.dirname, ".."); + execSync("pnpm bundle", { cwd: root, stdio: "inherit", env: process.env }); +} diff --git a/e2e/vitest.config.ts b/e2e/vitest.config.ts index af08b25..8164d51 100644 --- a/e2e/vitest.config.ts +++ b/e2e/vitest.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { name: "e2e", + globalSetup: ["./global-setup.ts"], include: ["**/*.test.ts"], testTimeout: 30_000, // The live-sandbox test spawns the bundled CLI for ~5+ minutes diff --git a/knip.json b/knip.json index 990c602..66d385e 100644 --- a/knip.json +++ b/knip.json @@ -7,6 +7,7 @@ "e2e/bundle.test.ts", "e2e/pipeline.test.ts", "e2e/pipeline-sandbox.test.ts", + "e2e/global-setup.ts", "e2e/vitest.config.ts" ] }, diff --git a/packages/core/src/paths.ts b/packages/core/src/paths.ts index 64692d3..fd33ac1 100644 --- a/packages/core/src/paths.ts +++ b/packages/core/src/paths.ts @@ -4,6 +4,11 @@ export function getDataRoot(): string { return process.env.DEEPSEC_DATA_ROOT || "data"; } +/** Root segment for joining per-project paths; normalize `\` so posix.join is stable on Windows. */ +function dataRootPosix(): string { + return getDataRoot().replace(/\\/g, "/"); +} + // Reject empty, '.', '..', absolute paths, null bytes, and any path // separator. Used at every entry point that joins user-supplied segments // onto a per-project mirror so a `../`-laced projectId/runId can't escape @@ -53,55 +58,55 @@ export function assertSafeFilePath(filePath: string): void { export function dataDir(projectId: string): string { assertSafeSegment(projectId, "projectId"); - return path.join(getDataRoot(), projectId); + return path.posix.join(dataRootPosix(), projectId); } export function projectConfigPath(projectId: string): string { - return path.join(dataDir(projectId), "project.json"); + return path.posix.join(dataDir(projectId), "project.json"); } // --- File records (permanent per-file mirror) --- export function filesDir(projectId: string): string { - return path.join(dataDir(projectId), "files"); + return path.posix.join(dataDir(projectId), "files"); } export function fileRecordPath(projectId: string, filePath: string): string { assertSafeFilePath(filePath); - return path.join(filesDir(projectId), filePath + ".json"); + return path.posix.join(filesDir(projectId), filePath + ".json"); } // --- Runs (lightweight metadata) --- export function runsDir(projectId: string): string { - return path.join(dataDir(projectId), "runs"); + return path.posix.join(dataDir(projectId), "runs"); } export function runMetaPath(projectId: string, runId: string): string { assertSafeSegment(runId, "runId"); - return path.join(runsDir(projectId), runId + ".json"); + return path.posix.join(runsDir(projectId), runId + ".json"); } // --- Reports --- export function reportsDir(projectId: string): string { - return path.join(dataDir(projectId), "reports"); + return path.posix.join(dataDir(projectId), "reports"); } export function reportJsonPath(projectId: string, runId?: string): string { if (runId !== undefined) assertSafeSegment(runId, "runId"); const name = runId ? `report-${runId}.json` : "report.json"; - return path.join(reportsDir(projectId), name); + return path.posix.join(reportsDir(projectId), name); } export function reportMdPath(projectId: string, runId?: string): string { if (runId !== undefined) assertSafeSegment(runId, "runId"); const name = runId ? `report-${runId}.md` : "report.md"; - return path.join(reportsDir(projectId), name); + return path.posix.join(reportsDir(projectId), name); } export function reportCsvPath(projectId: string, runId?: string): string { if (runId !== undefined) assertSafeSegment(runId, "runId"); const name = runId ? `report-${runId}.csv` : "report.csv"; - return path.join(reportsDir(projectId), name); + return path.posix.join(reportsDir(projectId), name); } diff --git a/packages/deepsec/src/commands/init-project.ts b/packages/deepsec/src/commands/init-project.ts index 45127df..17e52c2 100644 --- a/packages/deepsec/src/commands/init-project.ts +++ b/packages/deepsec/src/commands/init-project.ts @@ -56,11 +56,8 @@ export function registerProject(opts: { const workspaceDir = fs.realpathSync(path.resolve(opts.workspaceDir)); const targetAbs = requireExistingDir(opts.targetRoot, ""); const id = validateProjectId(opts.id ?? path.basename(targetAbs)); - // Normalize to POSIX separators: `targetRel` gets written into - // deepsec.config.ts (committed to VCS) and SETUP.md, so a Windows - // contributor adding a project would otherwise produce `..\foo\bar` - // that's ugly cross-platform and noisy in diffs. Both Node path APIs - // accept "/" on Windows. + // Always POSIX `root:` in deepsec.config.ts so scaffolds match across OS + // and tools that consume the file see a single separator convention. const targetRel = path.relative(workspaceDir, targetAbs).split(path.sep).join("/"); const configPath = findConfigInWorkspace(workspaceDir); diff --git a/packages/scanner/src/index.ts b/packages/scanner/src/index.ts index f7ac175..8ec0f82 100644 --- a/packages/scanner/src/index.ts +++ b/packages/scanner/src/index.ts @@ -171,11 +171,8 @@ export class RegexScannerDriver implements ScannerDriver { nodir: true, absolute: false, }); - // glob returns native separators on Windows ("src\api\foo.ts"). - // Record paths require POSIX separators (assertSafeFilePath rejects - // "\"), so normalize once here before anything reads or writes records. - const files = rawFiles.map((f) => f.replaceAll("\\", "/")); - globCache.set(key, files); + const posixPaths = files.map((p) => p.replace(/\\/g, "/")); + globCache.set(key, posixPaths); yield { type: "matcher_done" as const, message: `Found ${files.length} files`,