@@ -3,13 +3,26 @@ import { basename, join } from "node:path";
33import { describe, expect, it } from "vitest";
44import { detectProject } from "./detect.js";
55import { mapFeatures } from "./mapper.js";
6- import { discoverNodeProjects } from "./mappers/projects.js";
6+ import { discoverNodeProjects, scriptCommand } from "./mappers/projects.js";
7+ import { nodeScriptCommand } from "./mappers/shared.js";
78import { turboTaskGraph } from "./mappers/turbo.js";
89import { fixtureRoot, writeFixture } from "./test-helpers.js";
910
1011const symlinkIt = process.platform === "win32" ? it.skip : it;
1112
1213describe("mapFeatures", () => {
14+ it("quotes dynamic Node validation command parts", () => {
15+ expect(scriptCommand("pnpm", "packages/app; touch INJECTED", "test")).toBe(
16+ 'pnpm --dir "packages/app; touch INJECTED" test',
17+ );
18+ expect(scriptCommand("npm", ".", "test:unit; touch INJECTED")).toBe(
19+ 'npm run "test:unit; touch INJECTED"',
20+ );
21+ expect(nodeScriptCommand("npm", "apps/site $(touch INJECTED)", "test")).toBe(
22+ 'npm --prefix "apps/site \\$(touch INJECTED)" run test',
23+ );
24+ });
25+
1326 it("applies configured path excludes to heuristic feature mapping", async () => {
1427 const root = await fixtureRoot("clawpatch-map-exclude-");
1528 await writeFixture(root, "requirements.txt", "pytest\n");
@@ -852,6 +865,51 @@ describe("mapFeatures", () => {
852865 ]);
853866 });
854867
868+ it("quotes workspace package roots with shell metacharacters in mapped validation commands", async () => {
869+ const root = await fixtureRoot("clawpatch-task-graph-fallback-quoted-");
870+ const packageRoot = "apps/web; touch INJECTED";
871+ await writeFixture(
872+ root,
873+ "package.json",
874+ JSON.stringify(
875+ { name: "workspace-root", workspaces: ["apps/*"], dependencies: { next: "1.0.0" } },
876+ null,
877+ 2,
878+ ),
879+ );
880+ await writeFixture(root, "pnpm-lock.yaml", "");
881+ await writeFixture(
882+ root,
883+ `${packageRoot}/package.json`,
884+ JSON.stringify(
885+ {
886+ name: "web",
887+ scripts: { test: "vitest run", build: "next build" },
888+ dependencies: { next: "1.0.0" },
889+ },
890+ null,
891+ 2,
892+ ),
893+ );
894+ await writeFixture(
895+ root,
896+ `${packageRoot}/app/page.tsx`,
897+ "export default function Page() { return null; }\n",
898+ );
899+ await writeFixture(root, `${packageRoot}/app/page.test.tsx`, "test('page', () => {});\n");
900+
901+ const project = await detectProject(root);
902+ const result = await mapFeatures(root, project, []);
903+ const route = result.features.find((feature) => feature.title === "web route /");
904+
905+ expect(route?.tests).toEqual([
906+ {
907+ path: `${packageRoot}/app/page.test.tsx`,
908+ command: 'pnpm --dir "apps/web; touch INJECTED" test',
909+ },
910+ ]);
911+ });
912+
855913 it("uses bun workspace commands when the root has a text bun lockfile", async () => {
856914 const root = await fixtureRoot("clawpatch-task-graph-bun-lock-");
857915 await writeFixture(
@@ -2426,6 +2484,55 @@ describe("mapFeatures", () => {
24262484 ]);
24272485 });
24282486
2487+ it("quotes Turbo task filters with shell metacharacters", async () => {
2488+ const root = await fixtureRoot("clawpatch-turbo-quoted-filter-");
2489+ await writeFixture(
2490+ root,
2491+ "package.json",
2492+ JSON.stringify(
2493+ {
2494+ name: "workspace-root",
2495+ packageManager: "pnpm@10.0.0",
2496+ workspaces: ["apps/*"],
2497+ },
2498+ null,
2499+ 2,
2500+ ),
2501+ );
2502+ await writeFixture(root, "pnpm-lock.yaml", "");
2503+ await writeFixture(root, "turbo.json", JSON.stringify({ tasks: { test: {} } }, null, 2));
2504+ await writeFixture(
2505+ root,
2506+ "apps/web/package.json",
2507+ JSON.stringify(
2508+ {
2509+ name: "web; touch INJECTED",
2510+ scripts: { test: "vitest run" },
2511+ dependencies: { next: "1.0.0" },
2512+ },
2513+ null,
2514+ 2,
2515+ ),
2516+ );
2517+ await writeFixture(
2518+ root,
2519+ "apps/web/app/page.tsx",
2520+ "export default function Page() { return null; }\n",
2521+ );
2522+ await writeFixture(root, "apps/web/app/page.test.tsx", "test('page', () => {});\n");
2523+
2524+ const project = await detectProject(root);
2525+ const result = await mapFeatures(root, project, []);
2526+ const mappedTest = result.features
2527+ .flatMap((feature) => feature.tests)
2528+ .find((test) => test.path === "apps/web/app/page.test.tsx");
2529+
2530+ expect(mappedTest).toEqual({
2531+ path: "apps/web/app/page.test.tsx",
2532+ command: 'pnpm turbo run test --filter "web; touch INJECTED"',
2533+ });
2534+ });
2535+
24292536 it("keeps package-local validation for fallback packages outside the workspace graph", async () => {
24302537 const root = await fixtureRoot("clawpatch-turbo-non-workspace-package-");
24312538 await writeFixture(
@@ -4973,6 +5080,42 @@ describe("mapFeatures", () => {
49735080 ]);
49745081 });
49755082
5083+ it("quotes nested Swift package roots with shell metacharacters", async () => {
5084+ const root = await fixtureRoot("clawpatch-swift-quoted-package-path-");
5085+ const packageRoot = "apps/macos; touch INJECTED";
5086+ await writeFixture(
5087+ root,
5088+ `${packageRoot}/Package.swift`,
5089+ [
5090+ "// swift-tools-version: 6.0",
5091+ "import PackageDescription",
5092+ "let package = Package(",
5093+ ' name: "MacApp",',
5094+ ' targets: [.executableTarget(name: "MacApp"), .testTarget(name: "MacAppTests", dependencies: ["MacApp"])]',
5095+ ")",
5096+ ].join("\n"),
5097+ );
5098+ await writeFixture(root, `${packageRoot}/Sources/MacApp/main.swift`, "@main struct App {}\n");
5099+ await writeFixture(
5100+ root,
5101+ `${packageRoot}/Tests/MacAppTests/MacAppTests.swift`,
5102+ "import Testing\n",
5103+ );
5104+
5105+ const project = await detectProject(root);
5106+ const result = await mapFeatures(root, project, []);
5107+ const mac = result.features.find((feature) =>
5108+ feature.title.startsWith("Swift executable MacApp"),
5109+ );
5110+
5111+ expect(mac?.tests).toEqual([
5112+ {
5113+ path: `${packageRoot}/Tests/MacAppTests/MacAppTests.swift`,
5114+ command: 'swift test --package-path "apps/macos; touch INJECTED"',
5115+ },
5116+ ]);
5117+ });
5118+
49765119 it("maps Kotlin Android semantic roles from framework evidence", async () => {
49775120 const root = await fixtureRoot("clawpatch-kotlin-android-role-map-");
49785121 await writeFixture(root, "settings.gradle.kts", 'pluginManagement {}\ninclude(":app")\n');
@@ -10797,6 +10940,26 @@ let package = Package(name: "HybridApp", targets: [.target(name: "HybridApp")])
1079710940 ]);
1079810941 });
1079910942
10943+ it("quotes conventional Rust crate manifest paths with shell metacharacters", async () => {
10944+ const root = await fixtureRoot("clawpatch-rust-quoted-manifest-path-");
10945+ const memberRoot = "crates/member; touch INJECTED";
10946+ await writeFixture(root, "Cargo.toml", "[workspace]\n");
10947+ await writeFixture(root, `${memberRoot}/Cargo.toml`, '[package]\nname = "member"\n');
10948+ await writeFixture(root, `${memberRoot}/src/lib.rs`, "pub fn run() {}\n");
10949+ await writeFixture(root, `${memberRoot}/tests/member_test.rs`, "#[test]\nfn works() {}\n");
10950+
10951+ const project = await detectProject(root);
10952+ const result = await mapFeatures(root, project, []);
10953+ const library = result.features.find((feature) => feature.title === "Rust library member");
10954+
10955+ expect(library?.tests).toEqual([
10956+ {
10957+ path: `${memberRoot}/tests/member_test.rs`,
10958+ command: 'cargo test --manifest-path "crates/member; touch INJECTED/Cargo.toml"',
10959+ },
10960+ ]);
10961+ });
10962+
1080010963 it("bounds Rust integration tests attached to entrypoint features", async () => {
1080110964 const root = await fixtureRoot("clawpatch-rust-test-bound-");
1080210965 await writeFixture(root, "Cargo.toml", '[package]\nname = "rust-test-bound"\n');
@@ -15507,6 +15670,36 @@ end
1550715670 ]);
1550815671 });
1550915672
15673+ it("quotes Elixir test paths with shell metacharacters", async () => {
15674+ const root = await fixtureRoot("clawpatch-elixir-quoted-test-path-");
15675+ await writeFixture(
15676+ root,
15677+ "mix.exs",
15678+ 'defmodule SampleApp.MixProject do\n use Mix.Project\n def project, do: [app: :sample_app, version: "0.1.0"]\nend\n',
15679+ );
15680+ await writeFixture(
15681+ root,
15682+ "lib/sample_app/accounts.ex",
15683+ "defmodule SampleApp.Accounts do\nend\n",
15684+ );
15685+ await writeFixture(
15686+ root,
15687+ "test/sample_app/accounts/injected; touch INJECTED_test.exs",
15688+ "defmodule SampleApp.AccountsTest do\nuse ExUnit.Case\nend\n",
15689+ );
15690+
15691+ const project = await detectProject(root);
15692+ const result = await mapFeatures(root, project, []);
15693+ const accounts = result.features.find((feature) => feature.title === "Elixir context accounts");
15694+
15695+ expect(accounts?.tests).toEqual([
15696+ {
15697+ path: "test/sample_app/accounts/injected; touch INJECTED_test.exs",
15698+ command: 'mix test "test/sample_app/accounts/injected; touch INJECTED_test.exs"',
15699+ },
15700+ ]);
15701+ });
15702+
1551015703 it("does not map generated Mix dependency C files", async () => {
1551115704 const root = await fixtureRoot("clawpatch-elixir-deps-skip-");
1551215705 await writeFixture(
0 commit comments