Upgrade candidates (all 12 done): - #1 parallel read-only tool execution (runToolBatch, 4 loop sites) - #2 configurable retry policy (maxRetries + exponential backoff via SDK) - #3 script-aware token estimation (CJK/symbol/structure-aware heuristic) - #4 head+tail output capping (truncate.ts), auto-applies to bash/git/etc - #5 partial-history compaction (preserve recent tail, summarize older prefix) - #6 dynamic max_tokens (resolveMaxTokens) - #7 richer tool descriptions with "use when" guidance - #8 MCP reconnect retry + /mcp reconnect command + session toolset refresh - #9 context-window cache TTL (cachedAt timestamp, default 7 days) - #10 edit_file similar-match suggestion on old_string miss (bounded Levenshtein) - #11 git_status output head+tail (resolved via #4) - #12 auto-accept now approves all mutating tools; auto-edit stays edit-only Other: - DEFAULT_MAX_ITERATIONS 50 -> 100 (local models issue one tool call per step) - MaxIterationsError message guides resume + config override - plus prior known-issues work (grep -e/--, session id sanitization, MCP content types, 12 hook events, plugin collisions, skill references, git ops expansion, configurable autoCompactThreshold, bashGuard, pathGuard, FilePanel, replay)
67 lines
3.2 KiB
TypeScript
67 lines
3.2 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import os from "node:os";
|
|
import { loadPlugin } from "./loader.js";
|
|
|
|
describe("loadPlugin", () => {
|
|
let tempDir: string;
|
|
|
|
beforeEach(() => {
|
|
tempDir = path.join(os.tmpdir(), `locode-plugin-${Math.random().toString(36).slice(2)}`);
|
|
mkdirSync(tempDir, { recursive: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("loads a skill with references/*.md files", () => {
|
|
mkdirSync(path.join(tempDir, ".claude-plugin"), { recursive: true });
|
|
mkdirSync(path.join(tempDir, "skills", "dataviz", "references"), { recursive: true });
|
|
writeFileSync(
|
|
path.join(tempDir, ".claude-plugin", "plugin.json"),
|
|
JSON.stringify({ name: "test-plugin" }),
|
|
);
|
|
writeFileSync(path.join(tempDir, "skills", "dataviz", "SKILL.md"), "---\ndescription: dataviz skill\n---\nmain skill");
|
|
writeFileSync(path.join(tempDir, "skills", "dataviz", "references", "colors.md"), "use blue");
|
|
|
|
const plugin = loadPlugin(tempDir);
|
|
expect(plugin.skills).toHaveLength(1);
|
|
expect(plugin.skills[0]).toMatchObject({ name: "dataviz", content: "main skill" });
|
|
expect(plugin.skills[0]?.references).toEqual([{ name: "colors", content: "use blue" }]);
|
|
});
|
|
|
|
it("loads commands and agents", () => {
|
|
mkdirSync(path.join(tempDir, ".claude-plugin"), { recursive: true });
|
|
writeFileSync(path.join(tempDir, ".claude-plugin", "plugin.json"), JSON.stringify({ name: "test-plugin" }));
|
|
mkdirSync(path.join(tempDir, "commands"), { recursive: true });
|
|
writeFileSync(path.join(tempDir, "commands", "hello.md"), "---\ndescription: Say hello\n---\nHello $ARGUMENTS");
|
|
mkdirSync(path.join(tempDir, "agents"), { recursive: true });
|
|
writeFileSync(path.join(tempDir, "agents", "review.md"), "---\ndescription: Code reviewer\n---\nYou review code.");
|
|
|
|
const plugin = loadPlugin(tempDir);
|
|
expect(plugin.commands).toHaveLength(1);
|
|
expect(plugin.commands[0]).toMatchObject({ name: "hello", description: "Say hello" });
|
|
expect(plugin.agents).toHaveLength(1);
|
|
expect(plugin.agents[0]).toMatchObject({ name: "review", description: "Code reviewer" });
|
|
});
|
|
|
|
it("parses a command's allowed-tools frontmatter, translating Claude Code tool names", () => {
|
|
mkdirSync(path.join(tempDir, ".claude-plugin"), { recursive: true });
|
|
writeFileSync(path.join(tempDir, ".claude-plugin", "plugin.json"), JSON.stringify({ name: "test-plugin" }));
|
|
mkdirSync(path.join(tempDir, "commands"), { recursive: true });
|
|
writeFileSync(
|
|
path.join(tempDir, "commands", "readonly.md"),
|
|
"---\ndescription: Look but don't touch\nallowed-tools: Read, Grep\n---\nInvestigate $ARGUMENTS",
|
|
);
|
|
writeFileSync(path.join(tempDir, "commands", "unrestricted.md"), "---\ndescription: No restriction\n---\nDo $ARGUMENTS");
|
|
|
|
const plugin = loadPlugin(tempDir);
|
|
const readonly = plugin.commands.find((c) => c.name === "readonly");
|
|
const unrestricted = plugin.commands.find((c) => c.name === "unrestricted");
|
|
expect(readonly?.allowedTools).toEqual(["read_file", "grep"]);
|
|
expect(unrestricted?.allowedTools).toBeUndefined();
|
|
});
|
|
});
|