test(utils): add contract tests for lang-from-path detection

24 tests covering getLanguageFromPath and detectLanguageId:
- Extension detection for common languages (TS, JS, Rust, Go, Python, etc.)
- TypeScript variants (.cts, .mts, .tsx)
- Case-insensitivity
- Unknown extensions return undefined/plaintext
- Special filenames: Dockerfile, Containerfile, .emacs, justfile
- .env prefix detection
- CMakeLists.txt: documents that .txt extension wins over basename
  in getLanguageFromPath (detectLanguageId handles it correctly)
- Full paths with directories
- Multi-dot filenames (config.test.ts)
This commit is contained in:
oldschoola
2026-06-23 10:12:22 -07:00
parent af2e53e070
commit 11b4edf45d
2 changed files with 156 additions and 0 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Added
- Added contract tests for `getLanguageFromPath` and `detectLanguageId` in `packages/coding-agent/test/utils/lang-from-path.test.ts`, covering extension detection, special filenames (Dockerfile, Containerfile, .emacs, justfile, CMakeLists.txt, Makefile), case-insensitivity, unknown extensions, and the `.txt`-wins-over-basename limitation in `getLanguageFromPath`.
## [16.1.16] - 2026-06-23
### Breaking Changes
@@ -0,0 +1,152 @@
/**
* Contract tests for language detection from file paths.
*
* `getLanguageFromPath` returns the highlight language id for a given file
* path, or undefined if unknown. `detectLanguageId` returns the LSP language
* identifier, falling back to "plaintext".
*/
import { describe, expect, it } from "bun:test";
import { detectLanguageId, getLanguageFromPath } from "../../src/utils/lang-from-path";
describe("getLanguageFromPath", () => {
it("detects common languages by extension", () => {
expect(getLanguageFromPath("src/main.ts")).toBe("typescript");
expect(getLanguageFromPath("app.tsx")).toBe("tsx");
expect(getLanguageFromPath("script.js")).toBe("javascript");
expect(getLanguageFromPath("component.jsx")).toBe("javascript");
expect(getLanguageFromPath("main.rs")).toBe("rust");
expect(getLanguageFromPath("main.go")).toBe("go");
expect(getLanguageFromPath("main.c")).toBe("c");
expect(getLanguageFromPath("main.cpp")).toBe("cpp");
expect(getLanguageFromPath("main.py")).toBe("python");
expect(getLanguageFromPath("main.rb")).toBe("ruby");
});
it("detects TypeScript variants (.cts, .mts)", () => {
expect(getLanguageFromPath("config.cts")).toBe("typescript");
expect(getLanguageFromPath("config.mts")).toBe("typescript");
});
it("is case-insensitive on extensions", () => {
expect(getLanguageFromPath("Main.TS")).toBe("typescript");
expect(getLanguageFromPath("App.TSX")).toBe("tsx");
expect(getLanguageFromPath("script.JS")).toBe("javascript");
});
it("returns undefined for unknown extensions", () => {
expect(getLanguageFromPath("file.unknownext")).toBeUndefined();
expect(getLanguageFromPath("file.xyz123")).toBeUndefined();
});
it("detects Dockerfile by basename (case-insensitive)", () => {
expect(getLanguageFromPath("Dockerfile")).toBe("dockerfile");
expect(getLanguageFromPath("dockerfile")).toBe("dockerfile");
expect(getLanguageFromPath("Dockerfile.dev")).toBe("dockerfile");
expect(getLanguageFromPath("DOCKERFILE")).toBe("dockerfile");
});
it("detects Containerfile", () => {
expect(getLanguageFromPath("Containerfile")).toBe("dockerfile");
});
it("detects .env files by prefix", () => {
expect(getLanguageFromPath(".env.local")).toBe("env");
expect(getLanguageFromPath(".env.production")).toBe("env");
// .env basename doesn't start with ".env." (no trailing dot), so the
// prefix check doesn't fire. But themeExtensionKey(".env") returns "env"
// which matches the extension table.
expect(getLanguageFromPath(".env")).toBe("env");
});
it("detects .emacs", () => {
expect(getLanguageFromPath(".emacs")).toBe("emacs-lisp");
});
it("detects justfile", () => {
expect(getLanguageFromPath("justfile")).toBe("just");
});
it("detects CMakeLists.txt — but .txt extension wins over basename check", () => {
// getLanguageFromPath checks themeExtensionKey first, which returns "txt"
// for CMakeLists.txt, matching the extension table entry ["text", "plaintext"].
// The CMakeLists.txt basename check at line 215 is only reached when the
// extension lookup fails. This is a known limitation — detectLanguageId
// handles it correctly by checking basename first.
expect(getLanguageFromPath("CMakeLists.txt")).toBe("text");
});
it("handles full paths with directories", () => {
expect(getLanguageFromPath("/home/user/project/src/index.ts")).toBe("typescript");
expect(getLanguageFromPath("C:\\Users\\dev\\app\\main.rs")).toBe("rust");
expect(getLanguageFromPath("../lib/utils.py")).toBe("python");
});
it("returns the last extension when multiple dots are present", () => {
// themeExtensionKey takes everything after the LAST dot
expect(getLanguageFromPath("config.test.ts")).toBe("typescript");
expect(getLanguageFromPath("app.component.tsx")).toBe("tsx");
});
it("returns undefined for files with no extension", () => {
// A bare filename like "README" has no dot — themeExtensionKey
// returns the lowercased basename, which won't match any extension
expect(getLanguageFromPath("README")).toBeUndefined();
});
});
describe("detectLanguageId", () => {
it("detects common languages by extension", () => {
expect(detectLanguageId("main.ts")).toBe("typescript");
expect(detectLanguageId("app.tsx")).toBe("typescriptreact");
expect(detectLanguageId("script.js")).toBe("javascript");
expect(detectLanguageId("main.rs")).toBe("rust");
expect(detectLanguageId("main.go")).toBe("go");
});
it("detects Dockerfile as dockerfile", () => {
expect(detectLanguageId("Dockerfile")).toBe("dockerfile");
expect(detectLanguageId("dockerfile.dev")).toBe("dockerfile");
});
it("detects Containerfile as dockerfile", () => {
expect(detectLanguageId("Containerfile")).toBe("dockerfile");
});
it("detects .emacs as emacs-lisp", () => {
expect(detectLanguageId(".emacs")).toBe("emacs-lisp");
});
it("detects Makefile as makefile", () => {
expect(detectLanguageId("Makefile")).toBe("makefile");
expect(detectLanguageId("makefile")).toBe("makefile");
expect(detectLanguageId("gnumakefile")).toBe("makefile");
});
it("detects justfile as just", () => {
expect(detectLanguageId("justfile")).toBe("just");
});
it("detects CMakeLists.txt as cmake", () => {
expect(detectLanguageId("CMakeLists.txt")).toBe("cmake");
});
it("detects .cmake extension as cmake", () => {
expect(detectLanguageId("FindPackage.cmake")).toBe("cmake");
});
it("falls back to plaintext for unknown extensions", () => {
expect(detectLanguageId("file.unknownext")).toBe("plaintext");
expect(detectLanguageId("file.xyz123")).toBe("plaintext");
});
it("falls back to plaintext for files with no extension", () => {
expect(detectLanguageId("README")).toBe("plaintext");
});
it("uses path.extname for LSP detection (not last-dot heuristic)", () => {
// detectLanguageId uses path.extname which only takes the last segment
// after the last dot that is also after the last path separator
expect(detectLanguageId("config.test.ts")).toBe("typescript");
expect(detectLanguageId("app.component.tsx")).toBe("typescriptreact");
});
});