mirror of
https://github.com/blamouche/obsidian-any-ai-code.git
synced 2026-07-22 17:20:23 +00:00
The new sidebar button sits next to @Active file on the secondary toolbar row and inserts the parent folder of the currently active note (vault-relative path with trailing slash) into the running CLI's stdin. Files at the vault root fall back to inserting @./ so the action still maps to the working directory the runtime was launched in. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
257 lines
7.6 KiB
TypeScript
257 lines
7.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
detectNodeExecutable,
|
|
formatActiveFileMention,
|
|
formatActiveFolderMention,
|
|
getLaunchSpecs,
|
|
isCodexLikeCommand,
|
|
mergePathEntries,
|
|
migrateRuntimeSettings,
|
|
resolveExecutableInPath,
|
|
resolvePluginDir,
|
|
type CliRuntimeConfig
|
|
} from "../runtime-utils";
|
|
|
|
describe("resolvePluginDir", () => {
|
|
it("returns absolute path unchanged", () => {
|
|
const pathApi = {
|
|
isAbsolute: (p: string) => p.startsWith("/"),
|
|
resolve: (...parts: string[]) => parts.join("/")
|
|
};
|
|
|
|
expect(resolvePluginDir("/abs/plugin", "/vault", pathApi)).toBe("/abs/plugin");
|
|
});
|
|
|
|
it("resolves relative plugin dir from vault", () => {
|
|
const pathApi = {
|
|
isAbsolute: (p: string) => p.startsWith("/"),
|
|
resolve: (...parts: string[]) => parts.join("/")
|
|
};
|
|
|
|
expect(resolvePluginDir(".obsidian/plugins/x", "/vault", pathApi)).toBe(
|
|
"/vault/.obsidian/plugins/x"
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("mergePathEntries", () => {
|
|
it("adds unique paths on unix", () => {
|
|
const merged = mergePathEntries("/usr/bin:/bin", ["/bin", "/custom/bin"], "darwin");
|
|
expect(merged).toBe("/usr/bin:/bin:/custom/bin");
|
|
});
|
|
|
|
it("uses windows delimiter", () => {
|
|
const merged = mergePathEntries("C:\\A;C:\\B", ["C:\\B", "C:\\C"], "win32");
|
|
expect(merged).toBe("C:\\A;C:\\B;C:\\C");
|
|
});
|
|
});
|
|
|
|
describe("getLaunchSpecs", () => {
|
|
it("returns cmd launch on windows", () => {
|
|
const specs = getLaunchSpecs(
|
|
"claude",
|
|
"win32",
|
|
{ ComSpec: "C:\\Windows\\System32\\cmd.exe" },
|
|
() => true
|
|
);
|
|
|
|
expect(specs).toEqual([
|
|
{
|
|
file: "C:\\Windows\\System32\\cmd.exe",
|
|
args: ["/d", "/s", "/c", "claude"]
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("filters unavailable shells and keeps fallback", () => {
|
|
const specs = getLaunchSpecs(
|
|
"claude",
|
|
"linux",
|
|
{ SHELL: "/bin/zsh" },
|
|
(shellPath) => shellPath === "/bin/bash"
|
|
);
|
|
|
|
expect(specs).toEqual([
|
|
{
|
|
file: "/bin/bash",
|
|
args: ["-lc", "claude"]
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("falls back to /bin/sh when none exists", () => {
|
|
const specs = getLaunchSpecs("claude", "darwin", {}, () => false);
|
|
expect(specs).toEqual([{ file: "/bin/sh", args: ["-c", "claude"] }]);
|
|
});
|
|
});
|
|
|
|
describe("resolveExecutableInPath", () => {
|
|
it("finds node in unix PATH", () => {
|
|
const result = resolveExecutableInPath(
|
|
"node",
|
|
"/usr/bin:/opt/homebrew/bin",
|
|
"darwin",
|
|
(p) => p === "/opt/homebrew/bin/node",
|
|
{ join: (...parts) => parts.join("/") }
|
|
);
|
|
|
|
expect(result).toBe("/opt/homebrew/bin/node");
|
|
});
|
|
});
|
|
|
|
describe("detectNodeExecutable", () => {
|
|
it("uses explicit configured value", () => {
|
|
const result = detectNodeExecutable(
|
|
"/custom/node",
|
|
"darwin",
|
|
{ PATH: "/usr/bin" },
|
|
() => false,
|
|
{ join: (...parts) => parts.join("/") }
|
|
);
|
|
expect(result).toBe("/custom/node");
|
|
});
|
|
|
|
it("auto-detects from PATH", () => {
|
|
const result = detectNodeExecutable(
|
|
"auto",
|
|
"darwin",
|
|
{ PATH: "/usr/bin:/opt/homebrew/bin" },
|
|
(p) => p === "/opt/homebrew/bin/node",
|
|
{ join: (...parts) => parts.join("/") }
|
|
);
|
|
expect(result).toBe("/opt/homebrew/bin/node");
|
|
});
|
|
});
|
|
|
|
describe("formatActiveFileMention", () => {
|
|
it("formats mention with leading @ and trailing space", () => {
|
|
expect(formatActiveFileMention("Dossier/monfichier.md")).toBe("@Dossier/monfichier.md ");
|
|
});
|
|
|
|
it("trims surrounding spaces from file name", () => {
|
|
expect(formatActiveFileMention(" monfichier.md ")).toBe("@monfichier.md ");
|
|
});
|
|
});
|
|
|
|
describe("formatActiveFolderMention", () => {
|
|
it("returns the immediate parent folder for nested files", () => {
|
|
expect(formatActiveFolderMention("Notes/Daily/2026-04-26.md")).toBe("@Notes/Daily/ ");
|
|
});
|
|
|
|
it("preserves multi-level folder paths", () => {
|
|
expect(formatActiveFolderMention("a/b/c/d.md")).toBe("@a/b/c/ ");
|
|
});
|
|
|
|
it("falls back to the current directory marker when the file is at vault root", () => {
|
|
expect(formatActiveFolderMention("rootfile.md")).toBe("@./ ");
|
|
});
|
|
|
|
it("falls back to the current directory marker for empty input", () => {
|
|
expect(formatActiveFolderMention("")).toBe("@./ ");
|
|
expect(formatActiveFolderMention(" ")).toBe("@./ ");
|
|
});
|
|
|
|
it("trims surrounding spaces before computing the parent", () => {
|
|
expect(formatActiveFolderMention(" Notes/file.md ")).toBe("@Notes/ ");
|
|
});
|
|
});
|
|
|
|
describe("isCodexLikeCommand", () => {
|
|
it("matches bare codex", () => {
|
|
expect(isCodexLikeCommand("codex")).toBe(true);
|
|
});
|
|
|
|
it("matches codex with arguments", () => {
|
|
expect(isCodexLikeCommand("codex --no-alt-screen")).toBe(true);
|
|
});
|
|
|
|
it("trims surrounding whitespace before matching", () => {
|
|
expect(isCodexLikeCommand(" codex ")).toBe(true);
|
|
});
|
|
|
|
it("does not match commands that merely contain codex", () => {
|
|
expect(isCodexLikeCommand("my-codex-wrapper")).toBe(false);
|
|
expect(isCodexLikeCommand("/usr/local/bin/codex")).toBe(false);
|
|
});
|
|
|
|
it("returns false for non-codex commands and empty values", () => {
|
|
expect(isCodexLikeCommand("claude")).toBe(false);
|
|
expect(isCodexLikeCommand("")).toBe(false);
|
|
expect(isCodexLikeCommand(undefined)).toBe(false);
|
|
expect(isCodexLikeCommand(null)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("migrateRuntimeSettings", () => {
|
|
const defaults: CliRuntimeConfig[] = [
|
|
{ id: "claude", name: "Claude", command: "claude" },
|
|
{ id: "codex", name: "Codex", command: "codex --no-alt-screen" }
|
|
];
|
|
const stableId = () => "generated-id";
|
|
|
|
it("returns defaults when no data is stored", () => {
|
|
const result = migrateRuntimeSettings(null, defaults, stableId);
|
|
expect(result.runtimes).toEqual(defaults);
|
|
expect(result.selectedRuntimeId).toBe("claude");
|
|
});
|
|
|
|
it("migrates legacy claude/codex command fields and preserves selected runtime", () => {
|
|
const result = migrateRuntimeSettings(
|
|
{
|
|
command: " claude --print ",
|
|
codexCommand: "codex --foo",
|
|
runtime: "codex"
|
|
},
|
|
defaults,
|
|
stableId
|
|
);
|
|
expect(result.runtimes).toEqual([
|
|
{ id: "claude", name: "Claude", command: "claude --print" },
|
|
{ id: "codex", name: "Codex", command: "codex --foo" }
|
|
]);
|
|
expect(result.selectedRuntimeId).toBe("codex");
|
|
});
|
|
|
|
it("ignores invalid legacy runtime values and falls back to first runtime", () => {
|
|
const result = migrateRuntimeSettings(
|
|
{ runtime: "openai" },
|
|
defaults,
|
|
stableId
|
|
);
|
|
expect(result.selectedRuntimeId).toBe("claude");
|
|
});
|
|
|
|
it("keeps already migrated data intact and clamps invalid selectedRuntimeId", () => {
|
|
const stored = {
|
|
runtimes: [
|
|
{ id: "abc", name: "A", command: "a" },
|
|
{ id: "def", name: "D", command: "d" }
|
|
],
|
|
selectedRuntimeId: "ghost"
|
|
};
|
|
const result = migrateRuntimeSettings(stored, defaults, stableId);
|
|
expect(result.runtimes).toEqual(stored.runtimes);
|
|
expect(result.selectedRuntimeId).toBe("abc");
|
|
});
|
|
|
|
it("regenerates missing or duplicated ids when sanitizing migrated data", () => {
|
|
let counter = 0;
|
|
const generateId = () => `gen-${++counter}`;
|
|
const result = migrateRuntimeSettings(
|
|
{
|
|
runtimes: [
|
|
{ id: "shared", name: "First", command: "first" },
|
|
{ id: "shared", name: "Second", command: "second" },
|
|
{ name: "Third", command: "third" }
|
|
],
|
|
selectedRuntimeId: "shared"
|
|
},
|
|
defaults,
|
|
generateId
|
|
);
|
|
const ids = result.runtimes.map((r) => r.id);
|
|
expect(new Set(ids).size).toBe(3);
|
|
expect(result.runtimes[0].id).toBe("shared");
|
|
expect(result.selectedRuntimeId).toBe("shared");
|
|
});
|
|
});
|