singularity-forge/src/resources/extensions/gsd/tests/detection.test.ts
Iouri Goussev a952391b33 chore: rename preferences.md to PREFERENCES.md for consistency (#2700) (#2738)
All other .gsd/ state files use uppercase naming (DECISIONS.md,
REQUIREMENTS.md, PROJECT.md, etc). This renames the canonical
preferences file to PREFERENCES.md while keeping a migration
fallback — the loader checks PREFERENCES.md first, then falls
back to lowercase preferences.md for existing installations.

Closes #2700

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 16:09:59 -06:00

1190 lines
46 KiB
TypeScript

/**
* Unit tests for GSD Detection — project state and ecosystem detection.
*
* Exercises the pure detection functions in detection.ts:
* - detectProjectState() with various folder layouts
* - detectV1Planning() with real and fake .planning/ dirs
* - detectProjectSignals() with different project types
* - isFirstEverLaunch() / hasGlobalSetup()
*/
import test from "node:test";
import assert from "node:assert/strict";
import { mkdirSync, writeFileSync, rmSync, existsSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import {
detectProjectState,
detectV1Planning,
detectProjectSignals,
} from "../detection.ts";
function makeTempDir(prefix: string): string {
const dir = join(
tmpdir(),
`gsd-detection-test-${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
);
mkdirSync(dir, { recursive: true });
return dir;
}
function cleanup(dir: string): void {
try {
rmSync(dir, { recursive: true, force: true });
} catch {
// best-effort
}
}
// ─── detectProjectState ─────────────────────────────────────────────────────────
test("detectProjectState: empty directory returns state=none", (t) => {
const dir = makeTempDir("empty");
t.after(() => cleanup(dir));
const result = detectProjectState(dir);
assert.equal(result.state, "none");
assert.equal(result.v1, undefined);
assert.equal(result.v2, undefined);
});
test("detectProjectState: directory with .gsd/milestones/M001 returns v2-gsd", (t) => {
const dir = makeTempDir("v2-gsd");
t.after(() => cleanup(dir));
mkdirSync(join(dir, ".gsd", "milestones", "M001"), { recursive: true });
const result = detectProjectState(dir);
assert.equal(result.state, "v2-gsd");
assert.ok(result.v2);
assert.equal(result.v2!.milestoneCount, 1);
});
test("detectProjectState: directory with empty .gsd/milestones returns v2-gsd-empty", (t) => {
const dir = makeTempDir("v2-empty");
t.after(() => cleanup(dir));
mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
const result = detectProjectState(dir);
assert.equal(result.state, "v2-gsd-empty");
assert.ok(result.v2);
assert.equal(result.v2!.milestoneCount, 0);
});
test("detectProjectState: directory with .planning/ returns v1-planning", (t) => {
const dir = makeTempDir("v1-planning");
t.after(() => cleanup(dir));
mkdirSync(join(dir, ".planning", "phases", "01-setup"), { recursive: true });
writeFileSync(join(dir, ".planning", "ROADMAP.md"), "# Roadmap\n", "utf-8");
const result = detectProjectState(dir);
assert.equal(result.state, "v1-planning");
assert.ok(result.v1);
assert.equal(result.v1!.hasRoadmap, true);
assert.equal(result.v1!.hasPhasesDir, true);
assert.equal(result.v1!.phaseCount, 1);
});
test("detectProjectState: v2 takes priority over v1 when both exist", (t) => {
const dir = makeTempDir("both");
t.after(() => cleanup(dir));
mkdirSync(join(dir, ".gsd", "milestones", "M001"), { recursive: true });
mkdirSync(join(dir, ".planning"), { recursive: true });
const result = detectProjectState(dir);
assert.equal(result.state, "v2-gsd");
});
test("detectProjectState: detects preferences in .gsd/", (t) => {
const dir = makeTempDir("prefs");
t.after(() => cleanup(dir));
mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
writeFileSync(join(dir, ".gsd", "PREFERENCES.md"), "---\nversion: 1\n---\n", "utf-8");
const result = detectProjectState(dir);
assert.ok(result.v2);
assert.equal(result.v2!.hasPreferences, true);
});
// ─── detectV1Planning ───────────────────────────────────────────────────────────
test("detectV1Planning: returns null for missing .planning/", (t) => {
const dir = makeTempDir("no-v1");
t.after(() => cleanup(dir));
assert.equal(detectV1Planning(dir), null);
});
test("detectV1Planning: returns null when .planning is a file", (t) => {
const dir = makeTempDir("v1-file");
t.after(() => cleanup(dir));
writeFileSync(join(dir, ".planning"), "not a directory", "utf-8");
assert.equal(detectV1Planning(dir), null);
});
test("detectV1Planning: detects phases directory with multiple phases", (t) => {
const dir = makeTempDir("v1-phases");
t.after(() => cleanup(dir));
mkdirSync(join(dir, ".planning", "phases", "01-setup"), { recursive: true });
mkdirSync(join(dir, ".planning", "phases", "02-core"), { recursive: true });
mkdirSync(join(dir, ".planning", "phases", "03-deploy"), { recursive: true });
const result = detectV1Planning(dir);
assert.ok(result);
assert.equal(result!.phaseCount, 3);
assert.equal(result!.hasPhasesDir, true);
});
test("detectV1Planning: detects ROADMAP.md", (t) => {
const dir = makeTempDir("v1-roadmap");
t.after(() => cleanup(dir));
mkdirSync(join(dir, ".planning"), { recursive: true });
writeFileSync(join(dir, ".planning", "ROADMAP.md"), "# Roadmap", "utf-8");
const result = detectV1Planning(dir);
assert.ok(result);
assert.equal(result!.hasRoadmap, true);
assert.equal(result!.hasPhasesDir, false);
assert.equal(result!.phaseCount, 0);
});
// ─── detectProjectSignals ───────────────────────────────────────────────────────
test("detectProjectSignals: empty directory", (t) => {
const dir = makeTempDir("signals-empty");
t.after(() => cleanup(dir));
const signals = detectProjectSignals(dir);
assert.deepEqual(signals.detectedFiles, []);
assert.equal(signals.isGitRepo, false);
assert.equal(signals.isMonorepo, false);
assert.equal(signals.primaryLanguage, undefined);
assert.equal(signals.hasCI, false);
assert.equal(signals.hasTests, false);
assert.deepEqual(signals.verificationCommands, []);
});
test("detectProjectSignals: Node.js project", (t) => {
const dir = makeTempDir("signals-node");
t.after(() => cleanup(dir));
writeFileSync(
join(dir, "package.json"),
JSON.stringify({
name: "test-project",
scripts: {
test: "jest",
build: "tsc",
lint: "eslint .",
},
}),
"utf-8",
);
writeFileSync(join(dir, "package-lock.json"), "{}", "utf-8");
mkdirSync(join(dir, ".git"), { recursive: true });
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("package.json"));
assert.equal(signals.primaryLanguage, "javascript/typescript");
assert.equal(signals.isGitRepo, true);
assert.equal(signals.packageManager, "npm");
assert.ok(signals.verificationCommands.includes("npm test"));
assert.ok(signals.verificationCommands.some(c => c.includes("build")));
assert.ok(signals.verificationCommands.some(c => c.includes("lint")));
});
test("detectProjectSignals: Rust project", (t) => {
const dir = makeTempDir("signals-rust");
t.after(() => cleanup(dir));
writeFileSync(join(dir, "Cargo.toml"), '[package]\nname = "test"\n', "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("Cargo.toml"));
assert.equal(signals.primaryLanguage, "rust");
assert.ok(signals.verificationCommands.includes("cargo test"));
assert.ok(signals.verificationCommands.includes("cargo clippy"));
});
test("detectProjectSignals: Go project", (t) => {
const dir = makeTempDir("signals-go");
t.after(() => cleanup(dir));
writeFileSync(join(dir, "go.mod"), "module example.com/test\n", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("go.mod"));
assert.equal(signals.primaryLanguage, "go");
assert.ok(signals.verificationCommands.includes("go test ./..."));
});
test("detectProjectSignals: Python project", (t) => {
const dir = makeTempDir("signals-python");
t.after(() => cleanup(dir));
writeFileSync(join(dir, "pyproject.toml"), "[tool.poetry]\n", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("pyproject.toml"));
assert.equal(signals.primaryLanguage, "python");
assert.ok(signals.verificationCommands.includes("pytest"));
});
test("detectProjectSignals: monorepo detection via workspaces", (t) => {
const dir = makeTempDir("signals-monorepo");
t.after(() => cleanup(dir));
writeFileSync(
join(dir, "package.json"),
JSON.stringify({ name: "mono", workspaces: ["packages/*"] }),
"utf-8",
);
const signals = detectProjectSignals(dir);
assert.equal(signals.isMonorepo, true);
});
test("detectProjectSignals: monorepo detection via turbo.json", (t) => {
const dir = makeTempDir("signals-turbo");
t.after(() => cleanup(dir));
writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "test" }), "utf-8");
writeFileSync(join(dir, "turbo.json"), "{}", "utf-8");
const signals = detectProjectSignals(dir);
assert.equal(signals.isMonorepo, true);
});
test("detectProjectSignals: CI detection", (t) => {
const dir = makeTempDir("signals-ci");
t.after(() => cleanup(dir));
mkdirSync(join(dir, ".github", "workflows"), { recursive: true });
const signals = detectProjectSignals(dir);
assert.equal(signals.hasCI, true);
});
test("detectProjectSignals: test detection via jest config", (t) => {
const dir = makeTempDir("signals-tests");
t.after(() => cleanup(dir));
writeFileSync(join(dir, "jest.config.ts"), "export default {}", "utf-8");
const signals = detectProjectSignals(dir);
assert.equal(signals.hasTests, true);
});
test("detectProjectSignals: package manager detection", (t) => {
const dir1 = makeTempDir("pm-pnpm");
const dir2 = makeTempDir("pm-yarn");
const dir3 = makeTempDir("pm-bun");
t.after(() => {
cleanup(dir1);
cleanup(dir2);
cleanup(dir3);
});
writeFileSync(join(dir1, "pnpm-lock.yaml"), "", "utf-8");
writeFileSync(join(dir1, "package.json"), "{}", "utf-8");
assert.equal(detectProjectSignals(dir1).packageManager, "pnpm");
writeFileSync(join(dir2, "yarn.lock"), "", "utf-8");
writeFileSync(join(dir2, "package.json"), "{}", "utf-8");
assert.equal(detectProjectSignals(dir2).packageManager, "yarn");
writeFileSync(join(dir3, "bun.lockb"), "", "utf-8");
writeFileSync(join(dir3, "package.json"), "{}", "utf-8");
assert.equal(detectProjectSignals(dir3).packageManager, "bun");
});
test("detectProjectSignals: skips default npm test script", (t) => {
const dir = makeTempDir("signals-default-test");
t.after(() => cleanup(dir));
writeFileSync(
join(dir, "package.json"),
JSON.stringify({
name: "test",
scripts: { test: 'echo "Error: no test specified" && exit 1' },
}),
"utf-8",
);
const signals = detectProjectSignals(dir);
// Should NOT include the default npm test script
assert.equal(
signals.verificationCommands.some(c => c.includes("test")),
false,
);
});
test("detectProjectSignals: pnpm uses pnpm commands", (t) => {
const dir = makeTempDir("signals-pnpm-cmds");
t.after(() => cleanup(dir));
writeFileSync(
join(dir, "package.json"),
JSON.stringify({
name: "test",
scripts: { test: "vitest", build: "tsc" },
}),
"utf-8",
);
writeFileSync(join(dir, "pnpm-lock.yaml"), "", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.verificationCommands.includes("pnpm test"));
assert.ok(signals.verificationCommands.includes("pnpm run build"));
});
test("detectProjectSignals: Ruby project with rspec", (t) => {
const dir = makeTempDir("signals-ruby");
t.after(() => cleanup(dir));
writeFileSync(join(dir, "Gemfile"), 'source "https://rubygems.org"\n', "utf-8");
mkdirSync(join(dir, "spec"), { recursive: true });
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("Gemfile"));
assert.equal(signals.primaryLanguage, "ruby");
assert.ok(signals.verificationCommands.includes("bundle exec rspec"));
});
test("detectProjectSignals: Makefile with test target", (t) => {
const dir = makeTempDir("signals-make");
t.after(() => cleanup(dir));
writeFileSync(join(dir, "Makefile"), "test:\n\tgo test ./...\n\nbuild:\n\tgo build\n", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("Makefile"));
assert.ok(signals.verificationCommands.includes("make test"));
});
test("detectProjectSignals: SQLite file detection via extensions", () => {
const dir = makeTempDir("signals-sqlite");
try {
writeFileSync(join(dir, "app.sqlite3"), "", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("*.sqlite"), "should add synthetic *.sqlite marker");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: SQL file detection", () => {
const dir = makeTempDir("signals-sql");
try {
writeFileSync(join(dir, "migrations.sql"), "", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("*.sql"), "should add synthetic *.sql marker");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: nested SQL file detection", () => {
const dir = makeTempDir("signals-sql-nested");
try {
mkdirSync(join(dir, "db", "migrations"), { recursive: true });
writeFileSync(join(dir, "db", "migrations", "001_init.sql"), "", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("*.sql"), "should detect nested SQL files");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: .db file triggers SQLite detection", () => {
const dir = makeTempDir("signals-db");
try {
writeFileSync(join(dir, "data.db"), "", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("*.sqlite"), "should add synthetic *.sqlite marker for .db files");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: no SQLite markers without matching files", () => {
const dir = makeTempDir("signals-no-sqlite");
try {
writeFileSync(join(dir, "package.json"), "{}", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(!signals.detectedFiles.includes("*.sqlite"), "should not have *.sqlite marker");
assert.ok(!signals.detectedFiles.includes("*.sql"), "should not have *.sql marker");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: .NET project via .csproj extension", () => {
const dir = makeTempDir("signals-dotnet");
try {
writeFileSync(join(dir, "MyApp.csproj"), "<Project></Project>", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("*.csproj"), "should add synthetic *.csproj marker");
assert.equal(signals.primaryLanguage, "csharp");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: nested .csproj detection", () => {
const dir = makeTempDir("signals-dotnet-nested");
try {
mkdirSync(join(dir, "src", "App"), { recursive: true });
writeFileSync(join(dir, "src", "App", "App.csproj"), "<Project></Project>", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("*.csproj"), "should detect nested .csproj files");
assert.equal(signals.primaryLanguage, "csharp");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: .NET project via .sln extension", () => {
const dir = makeTempDir("signals-sln");
try {
writeFileSync(join(dir, "MyApp.sln"), "", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("*.sln"), "should add synthetic *.sln marker for .sln files");
assert.equal(signals.primaryLanguage, "dotnet");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: F# project via .fsproj extension", () => {
const dir = makeTempDir("signals-fsharp");
try {
writeFileSync(join(dir, "MyApp.fsproj"), "<Project></Project>", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("*.fsproj"), "should add synthetic *.fsproj marker");
assert.equal(signals.primaryLanguage, "fsharp");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Angular project via angular.json", () => {
const dir = makeTempDir("signals-angular");
try {
writeFileSync(join(dir, "angular.json"), "{}", "utf-8");
writeFileSync(join(dir, "package.json"), "{}", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("angular.json"));
assert.equal(signals.primaryLanguage, "javascript/typescript");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Next.js project via next.config.ts", () => {
const dir = makeTempDir("signals-nextjs");
try {
writeFileSync(join(dir, "next.config.ts"), "export default {}", "utf-8");
writeFileSync(join(dir, "package.json"), "{}", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("next.config.ts"));
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: nested Next.js config via packages/web/next.config.ts", () => {
const dir = makeTempDir("signals-nextjs-nested");
try {
mkdirSync(join(dir, "packages", "web"), { recursive: true });
writeFileSync(join(dir, "packages", "web", "next.config.ts"), "export default {}", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("next.config.ts"), "should detect nested Next.js config");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Flutter project via pubspec.yaml", () => {
const dir = makeTempDir("signals-flutter");
try {
writeFileSync(join(dir, "pubspec.yaml"), "name: my_app", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("pubspec.yaml"));
assert.equal(signals.primaryLanguage, "dart/flutter");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Django project via manage.py", () => {
const dir = makeTempDir("signals-django");
try {
writeFileSync(join(dir, "manage.py"), "#!/usr/bin/env python", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("manage.py"));
assert.equal(signals.primaryLanguage, "python");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: nested Django manage.py", () => {
const dir = makeTempDir("signals-django-nested");
try {
mkdirSync(join(dir, "services", "api"), { recursive: true });
writeFileSync(join(dir, "services", "api", "manage.py"), "#!/usr/bin/env python", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("manage.py"), "should detect nested manage.py");
assert.equal(signals.primaryLanguage, "python");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Docker project via Dockerfile", () => {
const dir = makeTempDir("signals-docker");
try {
writeFileSync(join(dir, "Dockerfile"), "FROM node:18", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("Dockerfile"));
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Terraform project via main.tf", () => {
const dir = makeTempDir("signals-terraform");
try {
writeFileSync(join(dir, "main.tf"), 'provider "aws" {}', "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("main.tf"));
} finally {
cleanup(dir);
}
});
// ── QA4/QA5 — new detection tests ──────────────────────────────────────────
test("detectProjectSignals: Vue.js via .vue files in src/", () => {
const dir = makeTempDir("signals-vue");
try {
writeFileSync(join(dir, "package.json"), '{"name":"vue-app"}', "utf-8");
mkdirSync(join(dir, "src"), { recursive: true });
writeFileSync(join(dir, "src", "App.vue"), "<template></template>", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("*.vue"), "should add *.vue synthetic marker");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Vue.js via nested .vue file in src/components/", () => {
const dir = makeTempDir("signals-vue-nested");
try {
writeFileSync(join(dir, "package.json"), '{"name":"vue-app"}', "utf-8");
mkdirSync(join(dir, "src", "components"), { recursive: true });
writeFileSync(join(dir, "src", "components", "Card.vue"), "<template></template>", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("*.vue"), "should detect nested .vue files");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Vue CLI via vue.config.js", () => {
const dir = makeTempDir("signals-vue-cli");
try {
writeFileSync(join(dir, "package.json"), '{"name":"vue-cli-app"}', "utf-8");
writeFileSync(join(dir, "vue.config.js"), "module.exports = {};", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("vue.config.js"));
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: requirements.txt sets Python language", () => {
const dir = makeTempDir("signals-requirements");
try {
writeFileSync(join(dir, "requirements.txt"), "flask==3.0\n", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("requirements.txt"));
assert.equal(signals.primaryLanguage, "python");
assert.ok(signals.verificationCommands.includes("pytest"), "should suggest pytest for requirements.txt projects");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Android project via app/build.gradle", () => {
const dir = makeTempDir("signals-android");
try {
mkdirSync(join(dir, "app"), { recursive: true });
writeFileSync(join(dir, "app", "build.gradle"), "apply plugin: 'com.android.application'", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("app/build.gradle"));
assert.equal(signals.primaryLanguage, "java/kotlin");
assert.ok(!signals.detectedFiles.includes("build.gradle"), "should not collapse Android app/build.gradle into generic build.gradle");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: nested app/build.gradle normalizes to Android marker", () => {
const dir = makeTempDir("signals-android-nested");
try {
mkdirSync(join(dir, "apps", "mobile", "app"), { recursive: true });
writeFileSync(join(dir, "apps", "mobile", "app", "build.gradle"), "apply plugin: 'com.android.application'", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("app/build.gradle"), "should detect nested Android app/build.gradle");
assert.ok(!signals.detectedFiles.includes("build.gradle"), "should not emit generic build.gradle marker for nested Android modules");
assert.equal(signals.primaryLanguage, "java/kotlin");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Unity project via ProjectSettings/ProjectVersion.txt", () => {
const dir = makeTempDir("signals-unity");
try {
mkdirSync(join(dir, "ProjectSettings"), { recursive: true });
writeFileSync(join(dir, "ProjectSettings", "ProjectVersion.txt"), "m_EditorVersion: 2022.3", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("ProjectSettings/ProjectVersion.txt"));
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Godot project via project.godot", () => {
const dir = makeTempDir("signals-godot");
try {
writeFileSync(join(dir, "project.godot"), "[application]", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("project.godot"));
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Airflow via airflow.cfg", () => {
const dir = makeTempDir("signals-airflow");
try {
writeFileSync(join(dir, "airflow.cfg"), "[core]\ndags_folder = ./dags", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("airflow.cfg"));
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Kubernetes via Chart.yaml (Helm)", () => {
const dir = makeTempDir("signals-k8s");
try {
writeFileSync(join(dir, "Chart.yaml"), "apiVersion: v2\nname: my-chart", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("Chart.yaml"));
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Blockchain via hardhat.config.ts", () => {
const dir = makeTempDir("signals-blockchain");
try {
writeFileSync(join(dir, "hardhat.config.ts"), 'import "@nomiclabs/hardhat-ethers"', "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("hardhat.config.ts"));
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: CI/CD via .github/workflows", () => {
const dir = makeTempDir("signals-cicd");
try {
mkdirSync(join(dir, ".github", "workflows"), { recursive: true });
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes(".github/workflows"));
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Tailwind via tailwind.config.ts", () => {
const dir = makeTempDir("signals-tailwind");
try {
writeFileSync(join(dir, "package.json"), '{"name":"tw-app"}', "utf-8");
writeFileSync(join(dir, "tailwind.config.ts"), "export default {};", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("tailwind.config.ts"));
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: FastAPI detected via requirements.txt dependency", () => {
const dir = makeTempDir("signals-fastapi-req");
try {
writeFileSync(join(dir, "requirements.txt"), "fastapi==0.115.0\nuvicorn[standard]\n", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("dep:fastapi"), "should add dep:fastapi marker");
assert.equal(signals.primaryLanguage, "python");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: FastAPI detected via pyproject.toml dependency", () => {
const dir = makeTempDir("signals-fastapi-pyproject");
try {
writeFileSync(join(dir, "pyproject.toml"), '[project]\ndependencies = ["fastapi>=0.100"]\n', "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("dep:fastapi"), "should add dep:fastapi marker");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: FastAPI detected with PEP 508 ~= operator", () => {
const dir = makeTempDir("signals-fastapi-compatible-release");
try {
writeFileSync(join(dir, "requirements.txt"), "fastapi~=0.115\n", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("dep:fastapi"), "~= should count as a FastAPI dependency");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: pyproject metadata mention does not trigger dep:fastapi", () => {
const dir = makeTempDir("signals-fastapi-pyproject-metadata");
try {
writeFileSync(
join(dir, "pyproject.toml"),
'[project]\nname = "example"\nkeywords = ["fastapi"]\ndependencies = ["flask>=3.0"]\n',
"utf-8",
);
const signals = detectProjectSignals(dir);
assert.ok(!signals.detectedFiles.includes("dep:fastapi"), "metadata-only mentions should not trigger FastAPI detection");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: pyproject dependency table extras do not trigger dep:fastapi", () => {
const dir = makeTempDir("signals-fastapi-pyproject-table-extra");
try {
writeFileSync(
join(dir, "pyproject.toml"),
'[tool.poetry.dependencies]\npython = "^3.12"\nmy-sdk = { version = "^1.0", extras = ["fastapi"] }\n',
"utf-8",
);
const signals = detectProjectSignals(dir);
assert.ok(!signals.detectedFiles.includes("dep:fastapi"), "dependency table extras should not imply FastAPI framework usage");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Poetry group FastAPI dependency does not imply app framework usage", () => {
const dir = makeTempDir("signals-fastapi-poetry-group");
try {
writeFileSync(
join(dir, "pyproject.toml"),
'[tool.poetry.dependencies]\npython = "^3.12"\nflask = "^3.0"\n\n[tool.poetry.group.dev.dependencies]\nfastapi = "^0.115"\n',
"utf-8",
);
const signals = detectProjectSignals(dir);
assert.ok(!signals.detectedFiles.includes("dep:fastapi"), "Poetry dev-group dependencies should not imply FastAPI app usage");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: pyproject optional-dependency group name does not trigger dep:fastapi", () => {
const dir = makeTempDir("signals-fastapi-pyproject-extra-name");
try {
writeFileSync(
join(dir, "pyproject.toml"),
'[project]\ndependencies = ["flask>=3.0"]\n\n[project.optional-dependencies]\nfastapi = ["orjson>=3"]\n',
"utf-8",
);
const signals = detectProjectSignals(dir);
assert.ok(!signals.detectedFiles.includes("dep:fastapi"), "optional-dependency extra names should not trigger FastAPI detection");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: pyproject multiline optional dependency emits dep:fastapi", () => {
const dir = makeTempDir("signals-fastapi-pyproject-optional-multiline");
try {
writeFileSync(
join(dir, "pyproject.toml"),
'[project]\ndependencies = ["flask>=3.0"]\n\n[project.optional-dependencies]\napi = [\n "fastapi>=0.115",\n "uvicorn>=0.30",\n]\n',
"utf-8",
);
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("dep:fastapi"), "multiline optional dependency arrays should trigger FastAPI detection");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: FastAPI direct reference with @ emits dep:fastapi", () => {
const dir = makeTempDir("signals-fastapi-direct-reference");
try {
writeFileSync(join(dir, "requirements.txt"), "fastapi @ https://example.com/fastapi.whl\n", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("dep:fastapi"), "direct-reference dependencies should trigger FastAPI detection");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: FastAPI detected via requirements.in", () => {
const dir = makeTempDir("signals-fastapi-requirements-in");
try {
writeFileSync(join(dir, "requirements.in"), "fastapi>=0.115\n", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("dep:fastapi"), "requirements.in should trigger FastAPI detection");
assert.ok(signals.detectedFiles.includes("requirements.txt"), "requirements.in should normalize to requirements.txt marker");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: FastAPI detected via nested requirements/base.in", () => {
const dir = makeTempDir("signals-fastapi-requirements-dir-in");
try {
mkdirSync(join(dir, "requirements"), { recursive: true });
writeFileSync(join(dir, "requirements", "base.in"), "fastapi>=0.115\n", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("dep:fastapi"), "requirements/base.in should trigger FastAPI detection");
assert.ok(signals.detectedFiles.includes("requirements.txt"), "requirements/base.in should normalize to requirements.txt marker");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: FastAPI comments do not trigger dep:fastapi", () => {
const dir = makeTempDir("signals-fastapi-comment");
try {
writeFileSync(join(dir, "requirements.txt"), "# maybe evaluate fastapi later\nflask==3.0\n", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(!signals.detectedFiles.includes("dep:fastapi"), "comments should not trigger FastAPI detection");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: FastAPI inline comments do not trigger dep:fastapi", () => {
const dir = makeTempDir("signals-fastapi-inline-comment");
try {
writeFileSync(join(dir, "requirements.txt"), "flask==3.0 # maybe fastapi later\n", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(!signals.detectedFiles.includes("dep:fastapi"), "inline comments should not trigger FastAPI detection");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: fastapi-* packages do not trigger dep:fastapi without fastapi itself", () => {
const dir = makeTempDir("signals-fastapi-suffix-only");
try {
writeFileSync(join(dir, "requirements.txt"), "fastapi-users==13.0\n", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(!signals.detectedFiles.includes("dep:fastapi"), "fastapi-* packages alone should not imply FastAPI framework usage");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: dependency extras mentioning fastapi do not trigger dep:fastapi", () => {
const dir = makeTempDir("signals-fastapi-extra-only");
try {
writeFileSync(join(dir, "requirements.txt"), "my-sdk[fastapi]>=1.0\n", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(!signals.detectedFiles.includes("dep:fastapi"), "dependency extras should not imply FastAPI framework usage");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Django project does NOT get dep:fastapi marker", () => {
const dir = makeTempDir("signals-django-no-fastapi");
try {
writeFileSync(join(dir, "requirements.txt"), "django==5.0\ncelery\n", "utf-8");
writeFileSync(join(dir, "manage.py"), "#!/usr/bin/env python", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(!signals.detectedFiles.includes("dep:fastapi"), "should NOT add dep:fastapi for Django");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: FastAPI detected case-insensitively (PyPI canonical name)", () => {
const dir = makeTempDir("signals-fastapi-case");
try {
writeFileSync(join(dir, "pyproject.toml"), '[project]\ndependencies = ["FastAPI>=0.100"]\n', "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("dep:fastapi"), "should detect FastAPI (mixed case)");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: FastAPI detected via nested service requirements.txt", () => {
const dir = makeTempDir("signals-fastapi-nested");
try {
mkdirSync(join(dir, "services", "api"), { recursive: true });
writeFileSync(join(dir, "services", "api", "requirements.txt"), "fastapi==0.115.0\n", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("dep:fastapi"), "should detect FastAPI in nested service requirements.txt");
assert.ok(signals.detectedFiles.includes("requirements.txt"), "should normalize nested requirements.txt marker");
assert.equal(signals.primaryLanguage, "python");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: nested Prisma schema normalizes to prisma/schema.prisma", () => {
const dir = makeTempDir("signals-prisma-nested");
try {
mkdirSync(join(dir, "services", "api", "prisma"), { recursive: true });
writeFileSync(join(dir, "services", "api", "prisma", "schema.prisma"), "datasource db { provider = \"sqlite\" }", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("prisma/schema.prisma"), "should detect nested Prisma schema");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: nested Spring Boot Gradle service emits dep:spring-boot", () => {
const dir = makeTempDir("signals-spring-gradle-nested");
try {
mkdirSync(join(dir, "services", "api"), { recursive: true });
writeFileSync(
join(dir, "services", "api", "build.gradle"),
"plugins { id 'org.springframework.boot' version '3.2.0' }",
"utf-8",
);
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("dep:spring-boot"), "should detect nested Spring Boot Gradle service");
assert.equal(signals.primaryLanguage, "java/kotlin");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: legacy apply plugin syntax emits dep:spring-boot", () => {
const dir = makeTempDir("signals-spring-apply-plugin");
try {
writeFileSync(join(dir, "build.gradle"), "apply plugin: 'org.springframework.boot'", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("dep:spring-boot"), "apply plugin syntax should trigger Spring Boot detection");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: nested Spring Boot Kotlin DSL service still uses neutral java/kotlin language hint", () => {
const dir = makeTempDir("signals-spring-gradle-kts-nested");
try {
mkdirSync(join(dir, "services", "api"), { recursive: true });
writeFileSync(
join(dir, "services", "api", "build.gradle.kts"),
"plugins { id(\"org.springframework.boot\") version \"3.2.0\" }",
"utf-8",
);
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("dep:spring-boot"));
assert.equal(signals.primaryLanguage, "java/kotlin");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Android Gradle project does not emit dep:spring-boot", () => {
const dir = makeTempDir("signals-android-no-spring");
try {
writeFileSync(join(dir, "build.gradle"), "plugins { id 'com.android.application' }", "utf-8");
mkdirSync(join(dir, "app"), { recursive: true });
writeFileSync(join(dir, "app", "build.gradle"), "plugins { id 'com.android.application' }", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(!signals.detectedFiles.includes("dep:spring-boot"), "Android Gradle files should not trigger Spring Boot detection");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Android inline comments do not emit dep:spring-boot", () => {
const dir = makeTempDir("signals-android-inline-comment");
try {
writeFileSync(join(dir, "build.gradle"), "plugins { id 'com.android.application' } // spring-boot maybe later", "utf-8");
mkdirSync(join(dir, "app"), { recursive: true });
writeFileSync(join(dir, "app", "build.gradle"), "plugins { id 'com.android.application' }", "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(!signals.detectedFiles.includes("dep:spring-boot"), "inline comments should not trigger Spring Boot detection");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: build metadata mentioning spring-boot does not emit dep:spring-boot", () => {
const dir = makeTempDir("signals-spring-metadata-only");
try {
writeFileSync(join(dir, "build.gradle"), 'def notes = "spring-boot migration planned later"', "utf-8");
const signals = detectProjectSignals(dir);
assert.ok(!signals.detectedFiles.includes("dep:spring-boot"), "arbitrary metadata text should not trigger Spring Boot detection");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Maven artifactId alone does not emit dep:spring-boot", () => {
const dir = makeTempDir("signals-spring-maven-artifact-only");
try {
writeFileSync(
join(dir, "pom.xml"),
'<project><modelVersion>4.0.0</modelVersion><groupId>com.example</groupId><artifactId>spring-boot-tools</artifactId></project>',
"utf-8",
);
const signals = detectProjectSignals(dir);
assert.ok(!signals.detectedFiles.includes("dep:spring-boot"), "artifactId alone should not imply Spring Boot");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Spring Boot version-catalog alias emits dep:spring-boot", () => {
const dir = makeTempDir("signals-spring-version-catalog");
try {
mkdirSync(join(dir, "gradle"), { recursive: true });
writeFileSync(join(dir, "build.gradle.kts"), "plugins { alias(libs.plugins.backend.web) }", "utf-8");
writeFileSync(
join(dir, "gradle", "libs.versions.toml"),
"[plugins]\nbackend-web = { id = 'org.springframework.boot', version = '3.2.0' }\n",
"utf-8",
);
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("dep:spring-boot"), "should detect Spring Boot via version-catalog alias");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: commented Spring Boot alias in libs.versions.toml does not emit dep:spring-boot", () => {
const dir = makeTempDir("signals-spring-version-catalog-comment");
try {
mkdirSync(join(dir, "gradle"), { recursive: true });
writeFileSync(join(dir, "build.gradle.kts"), "plugins { alias(libs.plugins.backend.web) }", "utf-8");
writeFileSync(
join(dir, "gradle", "libs.versions.toml"),
"[plugins]\n# backend-web = { id = 'org.springframework.boot', version = '3.2.0' }\n",
"utf-8",
);
const signals = detectProjectSignals(dir);
assert.ok(!signals.detectedFiles.includes("dep:spring-boot"), "commented aliases should not trigger Spring Boot detection");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: unused Spring Boot alias in libs.versions.toml does not emit dep:spring-boot", () => {
const dir = makeTempDir("signals-spring-version-catalog-unused");
try {
mkdirSync(join(dir, "gradle"), { recursive: true });
writeFileSync(join(dir, "build.gradle.kts"), "plugins { alias(libs.plugins.backend.web) }", "utf-8");
writeFileSync(
join(dir, "gradle", "libs.versions.toml"),
"[plugins]\nother-plugin = { id = 'org.springframework.boot', version = '3.2.0' }\n",
"utf-8",
);
const signals = detectProjectSignals(dir);
assert.ok(!signals.detectedFiles.includes("dep:spring-boot"), "unused Spring Boot aliases should not trigger detection");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: spring-like alias name without Spring Boot id does not emit dep:spring-boot", () => {
const dir = makeTempDir("signals-spring-version-catalog-false-alias");
try {
mkdirSync(join(dir, "gradle"), { recursive: true });
writeFileSync(join(dir, "build.gradle.kts"), "plugins { alias(libs.plugins.spring.boot.conventions) }", "utf-8");
writeFileSync(
join(dir, "gradle", "libs.versions.toml"),
"[plugins]\nspring-boot-conventions = { id = 'com.example.conventions', version = '1.0.0' }\n",
"utf-8",
);
const signals = detectProjectSignals(dir);
assert.ok(!signals.detectedFiles.includes("dep:spring-boot"), "spring-looking alias names should not imply Spring Boot without matching id");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Spring Boot version-catalog library alias emits dep:spring-boot", () => {
const dir = makeTempDir("signals-spring-version-catalog-library");
try {
mkdirSync(join(dir, "gradle"), { recursive: true });
writeFileSync(join(dir, "build.gradle.kts"), "dependencies { implementation(libs.backend.web) }", "utf-8");
writeFileSync(
join(dir, "gradle", "libs.versions.toml"),
"[libraries]\nbackend-web = { module = 'org.springframework.boot:spring-boot-starter-web', version = '3.2.0' }\n",
"utf-8",
);
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("dep:spring-boot"), "Spring Boot library aliases should trigger detection");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Spring Boot version-catalog bundle alias emits dep:spring-boot", () => {
const dir = makeTempDir("signals-spring-version-catalog-bundle");
try {
mkdirSync(join(dir, "gradle"), { recursive: true });
writeFileSync(join(dir, "build.gradle.kts"), "dependencies { implementation(libs.bundles.backend.web) }", "utf-8");
writeFileSync(
join(dir, "gradle", "libs.versions.toml"),
"[libraries]\nspring-boot-starter-web = { module = 'org.springframework.boot:spring-boot-starter-web', version = '3.2.0' }\n\n[bundles]\nbackend-web = ['spring-boot-starter-web']\n",
"utf-8",
);
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("dep:spring-boot"), "Spring Boot bundle aliases should trigger detection");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Spring Boot custom version-catalog accessor emits dep:spring-boot", () => {
const dir = makeTempDir("signals-spring-version-catalog-custom-accessor");
try {
mkdirSync(join(dir, "gradle"), { recursive: true });
writeFileSync(join(dir, "build.gradle.kts"), "plugins { alias(backend.plugins.web) }", "utf-8");
writeFileSync(
join(dir, "gradle", "backend.versions.toml"),
"[plugins]\nweb = { id = 'org.springframework.boot', version = '3.2.0' }\n",
"utf-8",
);
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("dep:spring-boot"), "custom version-catalog accessors should trigger Spring Boot detection");
} finally {
cleanup(dir);
}
});
test("detectProjectSignals: Spring Boot settings-defined catalog accessor emits dep:spring-boot", () => {
const dir = makeTempDir("signals-spring-version-catalog-settings-accessor");
try {
mkdirSync(join(dir, "gradle"), { recursive: true });
writeFileSync(
join(dir, "settings.gradle.kts"),
'dependencyResolutionManagement { versionCatalogs { create("backendLibs") { from(files("./gradle/backend.versions.toml")) } } }',
"utf-8",
);
writeFileSync(join(dir, "build.gradle.kts"), "plugins { alias(backendLibs.plugins.web) }", "utf-8");
writeFileSync(
join(dir, "gradle", "backend.versions.toml"),
"[plugins]\nweb = { id = 'org.springframework.boot', version = '3.2.0' }\n",
"utf-8",
);
const signals = detectProjectSignals(dir);
assert.ok(signals.detectedFiles.includes("dep:spring-boot"), "settings-defined catalog accessors should trigger Spring Boot detection");
} finally {
cleanup(dir);
}
});