mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 19:05:11 +00:00
599 lines
24 KiB
TypeScript
599 lines
24 KiB
TypeScript
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import { DefaultPackageManager, type ProgressEvent, type ResolvedResource } from "../src/core/package-manager.js";
|
|
import { SettingsManager } from "../src/core/settings-manager.js";
|
|
|
|
// Helper to check if a resource is enabled
|
|
const isEnabled = (r: ResolvedResource, pathMatch: string, matchFn: "endsWith" | "includes" = "endsWith") =>
|
|
matchFn === "endsWith" ? r.path.endsWith(pathMatch) && r.enabled : r.path.includes(pathMatch) && r.enabled;
|
|
|
|
const isDisabled = (r: ResolvedResource, pathMatch: string, matchFn: "endsWith" | "includes" = "endsWith") =>
|
|
matchFn === "endsWith" ? r.path.endsWith(pathMatch) && !r.enabled : r.path.includes(pathMatch) && !r.enabled;
|
|
|
|
describe("DefaultPackageManager", () => {
|
|
let tempDir: string;
|
|
let settingsManager: SettingsManager;
|
|
let packageManager: DefaultPackageManager;
|
|
|
|
beforeEach(() => {
|
|
tempDir = join(tmpdir(), `pm-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
mkdirSync(tempDir, { recursive: true });
|
|
const agentDir = join(tempDir, "agent");
|
|
mkdirSync(agentDir, { recursive: true });
|
|
|
|
settingsManager = SettingsManager.inMemory();
|
|
packageManager = new DefaultPackageManager({
|
|
cwd: tempDir,
|
|
agentDir,
|
|
settingsManager,
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe("resolve", () => {
|
|
it("should return empty paths when no sources configured", async () => {
|
|
const result = await packageManager.resolve();
|
|
expect(result.extensions).toEqual([]);
|
|
expect(result.skills).toEqual([]);
|
|
expect(result.prompts).toEqual([]);
|
|
expect(result.themes).toEqual([]);
|
|
});
|
|
|
|
it("should resolve local extension paths from settings", async () => {
|
|
const extPath = join(tempDir, "my-extension.ts");
|
|
writeFileSync(extPath, "export default function() {}");
|
|
settingsManager.setExtensionPaths([extPath]);
|
|
|
|
const result = await packageManager.resolve();
|
|
expect(result.extensions.some((r) => r.path === extPath && r.enabled)).toBe(true);
|
|
});
|
|
|
|
it("should resolve skill paths from settings", async () => {
|
|
const skillDir = join(tempDir, "skills", "my-skill");
|
|
mkdirSync(skillDir, { recursive: true });
|
|
writeFileSync(
|
|
join(skillDir, "SKILL.md"),
|
|
`---
|
|
name: test-skill
|
|
description: A test skill
|
|
---
|
|
Content`,
|
|
);
|
|
|
|
settingsManager.setSkillPaths([join(tempDir, "skills")]);
|
|
|
|
const result = await packageManager.resolve();
|
|
// Skills with SKILL.md are returned as directory paths
|
|
expect(result.skills.some((r) => r.path === skillDir && r.enabled)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("resolveExtensionSources", () => {
|
|
it("should resolve local paths", async () => {
|
|
const extPath = join(tempDir, "ext.ts");
|
|
writeFileSync(extPath, "export default function() {}");
|
|
|
|
const result = await packageManager.resolveExtensionSources([extPath]);
|
|
expect(result.extensions.some((r) => r.path === extPath && r.enabled)).toBe(true);
|
|
});
|
|
|
|
it("should handle directories with pi manifest", async () => {
|
|
const pkgDir = join(tempDir, "my-package");
|
|
mkdirSync(pkgDir, { recursive: true });
|
|
writeFileSync(
|
|
join(pkgDir, "package.json"),
|
|
JSON.stringify({
|
|
name: "my-package",
|
|
pi: {
|
|
extensions: ["./src/index.ts"],
|
|
skills: ["./skills"],
|
|
},
|
|
}),
|
|
);
|
|
mkdirSync(join(pkgDir, "src"), { recursive: true });
|
|
writeFileSync(join(pkgDir, "src", "index.ts"), "export default function() {}");
|
|
mkdirSync(join(pkgDir, "skills", "my-skill"), { recursive: true });
|
|
writeFileSync(
|
|
join(pkgDir, "skills", "my-skill", "SKILL.md"),
|
|
"---\nname: my-skill\ndescription: Test\n---\nContent",
|
|
);
|
|
|
|
const result = await packageManager.resolveExtensionSources([pkgDir]);
|
|
expect(result.extensions.some((r) => r.path === join(pkgDir, "src", "index.ts") && r.enabled)).toBe(true);
|
|
// Skills with SKILL.md are returned as directory paths
|
|
expect(result.skills.some((r) => r.path === join(pkgDir, "skills", "my-skill") && r.enabled)).toBe(true);
|
|
});
|
|
|
|
it("should handle directories with auto-discovery layout", async () => {
|
|
const pkgDir = join(tempDir, "auto-pkg");
|
|
mkdirSync(join(pkgDir, "extensions"), { recursive: true });
|
|
mkdirSync(join(pkgDir, "themes"), { recursive: true });
|
|
writeFileSync(join(pkgDir, "extensions", "main.ts"), "export default function() {}");
|
|
writeFileSync(join(pkgDir, "themes", "dark.json"), "{}");
|
|
|
|
const result = await packageManager.resolveExtensionSources([pkgDir]);
|
|
expect(result.extensions.some((r) => r.path.endsWith("main.ts") && r.enabled)).toBe(true);
|
|
expect(result.themes.some((r) => r.path.endsWith("dark.json") && r.enabled)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("progress callback", () => {
|
|
it("should emit progress events", async () => {
|
|
const events: ProgressEvent[] = [];
|
|
packageManager.setProgressCallback((event) => events.push(event));
|
|
|
|
const extPath = join(tempDir, "ext.ts");
|
|
writeFileSync(extPath, "export default function() {}");
|
|
|
|
// Local paths don't trigger install progress, but we can verify the callback is set
|
|
await packageManager.resolveExtensionSources([extPath]);
|
|
|
|
// For now just verify no errors - npm/git would trigger actual events
|
|
expect(events.length).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("source parsing", () => {
|
|
it("should emit progress events on install attempt", async () => {
|
|
const events: ProgressEvent[] = [];
|
|
packageManager.setProgressCallback((event) => events.push(event));
|
|
|
|
// Use public install method which emits progress events
|
|
try {
|
|
await packageManager.install("npm:nonexistent-package@1.0.0");
|
|
} catch {
|
|
// Expected to fail - package doesn't exist
|
|
}
|
|
|
|
// Should have emitted start event before failure
|
|
expect(events.some((e) => e.type === "start" && e.action === "install")).toBe(true);
|
|
// Should have emitted error event
|
|
expect(events.some((e) => e.type === "error")).toBe(true);
|
|
});
|
|
|
|
it("should recognize github URLs without git: prefix", async () => {
|
|
const events: ProgressEvent[] = [];
|
|
packageManager.setProgressCallback((event) => events.push(event));
|
|
|
|
// This should be parsed as a git source, not throw "unsupported"
|
|
try {
|
|
await packageManager.install("https://github.com/nonexistent/repo");
|
|
} catch {
|
|
// Expected to fail - repo doesn't exist
|
|
}
|
|
|
|
// Should have attempted clone, not thrown unsupported error
|
|
expect(events.some((e) => e.type === "start" && e.action === "install")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("pattern filtering in top-level arrays", () => {
|
|
it("should exclude extensions with ! pattern", async () => {
|
|
const extDir = join(tempDir, "extensions");
|
|
mkdirSync(extDir, { recursive: true });
|
|
writeFileSync(join(extDir, "keep.ts"), "export default function() {}");
|
|
writeFileSync(join(extDir, "remove.ts"), "export default function() {}");
|
|
|
|
settingsManager.setExtensionPaths([extDir, "!**/remove.ts"]);
|
|
|
|
const result = await packageManager.resolve();
|
|
expect(result.extensions.some((r) => isEnabled(r, "keep.ts"))).toBe(true);
|
|
expect(result.extensions.some((r) => isDisabled(r, "remove.ts"))).toBe(true);
|
|
});
|
|
|
|
it("should filter themes with glob patterns", async () => {
|
|
const themesDir = join(tempDir, "themes");
|
|
mkdirSync(themesDir, { recursive: true });
|
|
writeFileSync(join(themesDir, "dark.json"), "{}");
|
|
writeFileSync(join(themesDir, "light.json"), "{}");
|
|
writeFileSync(join(themesDir, "funky.json"), "{}");
|
|
|
|
settingsManager.setThemePaths([themesDir, "!funky.json"]);
|
|
|
|
const result = await packageManager.resolve();
|
|
expect(result.themes.some((r) => isEnabled(r, "dark.json"))).toBe(true);
|
|
expect(result.themes.some((r) => isEnabled(r, "light.json"))).toBe(true);
|
|
expect(result.themes.some((r) => isDisabled(r, "funky.json"))).toBe(true);
|
|
});
|
|
|
|
it("should filter prompts with exclusion pattern", async () => {
|
|
const promptsDir = join(tempDir, "prompts");
|
|
mkdirSync(promptsDir, { recursive: true });
|
|
writeFileSync(join(promptsDir, "review.md"), "Review code");
|
|
writeFileSync(join(promptsDir, "explain.md"), "Explain code");
|
|
|
|
settingsManager.setPromptTemplatePaths([promptsDir, "!explain.md"]);
|
|
|
|
const result = await packageManager.resolve();
|
|
expect(result.prompts.some((r) => isEnabled(r, "review.md"))).toBe(true);
|
|
expect(result.prompts.some((r) => isDisabled(r, "explain.md"))).toBe(true);
|
|
});
|
|
|
|
it("should filter skills with exclusion pattern", async () => {
|
|
const skillsDir = join(tempDir, "skills");
|
|
mkdirSync(join(skillsDir, "good-skill"), { recursive: true });
|
|
mkdirSync(join(skillsDir, "bad-skill"), { recursive: true });
|
|
writeFileSync(
|
|
join(skillsDir, "good-skill", "SKILL.md"),
|
|
"---\nname: good-skill\ndescription: Good\n---\nContent",
|
|
);
|
|
writeFileSync(
|
|
join(skillsDir, "bad-skill", "SKILL.md"),
|
|
"---\nname: bad-skill\ndescription: Bad\n---\nContent",
|
|
);
|
|
|
|
settingsManager.setSkillPaths([skillsDir, "!**/bad-skill"]);
|
|
|
|
const result = await packageManager.resolve();
|
|
expect(result.skills.some((r) => isEnabled(r, "good-skill", "includes"))).toBe(true);
|
|
expect(result.skills.some((r) => isDisabled(r, "bad-skill", "includes"))).toBe(true);
|
|
});
|
|
|
|
it("should work without patterns (backward compatible)", async () => {
|
|
const extPath = join(tempDir, "my-ext.ts");
|
|
writeFileSync(extPath, "export default function() {}");
|
|
|
|
settingsManager.setExtensionPaths([extPath]);
|
|
|
|
const result = await packageManager.resolve();
|
|
expect(result.extensions.some((r) => r.path === extPath && r.enabled)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("pattern filtering in pi manifest", () => {
|
|
it("should support glob patterns in manifest extensions", async () => {
|
|
const pkgDir = join(tempDir, "manifest-pkg");
|
|
mkdirSync(join(pkgDir, "extensions"), { recursive: true });
|
|
mkdirSync(join(pkgDir, "node_modules/dep/extensions"), { recursive: true });
|
|
writeFileSync(join(pkgDir, "extensions", "local.ts"), "export default function() {}");
|
|
writeFileSync(join(pkgDir, "node_modules/dep/extensions", "remote.ts"), "export default function() {}");
|
|
writeFileSync(join(pkgDir, "node_modules/dep/extensions", "skip.ts"), "export default function() {}");
|
|
writeFileSync(
|
|
join(pkgDir, "package.json"),
|
|
JSON.stringify({
|
|
name: "manifest-pkg",
|
|
pi: {
|
|
extensions: ["extensions", "node_modules/dep/extensions", "!**/skip.ts"],
|
|
},
|
|
}),
|
|
);
|
|
|
|
const result = await packageManager.resolveExtensionSources([pkgDir]);
|
|
expect(result.extensions.some((r) => isEnabled(r, "local.ts"))).toBe(true);
|
|
expect(result.extensions.some((r) => isEnabled(r, "remote.ts"))).toBe(true);
|
|
expect(result.extensions.some((r) => isDisabled(r, "skip.ts"))).toBe(true);
|
|
});
|
|
|
|
it("should support glob patterns in manifest skills", async () => {
|
|
const pkgDir = join(tempDir, "skill-manifest-pkg");
|
|
mkdirSync(join(pkgDir, "skills/good-skill"), { recursive: true });
|
|
mkdirSync(join(pkgDir, "skills/bad-skill"), { recursive: true });
|
|
writeFileSync(
|
|
join(pkgDir, "skills/good-skill", "SKILL.md"),
|
|
"---\nname: good-skill\ndescription: Good\n---\nContent",
|
|
);
|
|
writeFileSync(
|
|
join(pkgDir, "skills/bad-skill", "SKILL.md"),
|
|
"---\nname: bad-skill\ndescription: Bad\n---\nContent",
|
|
);
|
|
writeFileSync(
|
|
join(pkgDir, "package.json"),
|
|
JSON.stringify({
|
|
name: "skill-manifest-pkg",
|
|
pi: {
|
|
skills: ["skills", "!**/bad-skill"],
|
|
},
|
|
}),
|
|
);
|
|
|
|
const result = await packageManager.resolveExtensionSources([pkgDir]);
|
|
expect(result.skills.some((r) => isEnabled(r, "good-skill", "includes"))).toBe(true);
|
|
expect(result.skills.some((r) => isDisabled(r, "bad-skill", "includes"))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("pattern filtering in package filters", () => {
|
|
it("should apply user filters on top of manifest filters (not replace)", async () => {
|
|
// Manifest excludes baz.ts, user excludes bar.ts
|
|
// Result should exclude BOTH
|
|
const pkgDir = join(tempDir, "layered-pkg");
|
|
mkdirSync(join(pkgDir, "extensions"), { recursive: true });
|
|
writeFileSync(join(pkgDir, "extensions", "foo.ts"), "export default function() {}");
|
|
writeFileSync(join(pkgDir, "extensions", "bar.ts"), "export default function() {}");
|
|
writeFileSync(join(pkgDir, "extensions", "baz.ts"), "export default function() {}");
|
|
writeFileSync(
|
|
join(pkgDir, "package.json"),
|
|
JSON.stringify({
|
|
name: "layered-pkg",
|
|
pi: {
|
|
extensions: ["extensions", "!**/baz.ts"],
|
|
},
|
|
}),
|
|
);
|
|
|
|
// User filter adds exclusion for bar.ts
|
|
settingsManager.setPackages([
|
|
{
|
|
source: pkgDir,
|
|
extensions: ["!**/bar.ts"],
|
|
skills: [],
|
|
prompts: [],
|
|
themes: [],
|
|
},
|
|
]);
|
|
|
|
const result = await packageManager.resolve();
|
|
// foo.ts should be included (not excluded by anyone)
|
|
expect(result.extensions.some((r) => isEnabled(r, "foo.ts"))).toBe(true);
|
|
// bar.ts should be excluded (by user)
|
|
expect(result.extensions.some((r) => isDisabled(r, "bar.ts"))).toBe(true);
|
|
// baz.ts should be excluded (by manifest)
|
|
expect(result.extensions.some((r) => isDisabled(r, "baz.ts"))).toBe(true);
|
|
});
|
|
|
|
it("should exclude extensions from package with ! pattern", async () => {
|
|
const pkgDir = join(tempDir, "pattern-pkg");
|
|
mkdirSync(join(pkgDir, "extensions"), { recursive: true });
|
|
writeFileSync(join(pkgDir, "extensions", "foo.ts"), "export default function() {}");
|
|
writeFileSync(join(pkgDir, "extensions", "bar.ts"), "export default function() {}");
|
|
writeFileSync(join(pkgDir, "extensions", "baz.ts"), "export default function() {}");
|
|
|
|
settingsManager.setPackages([
|
|
{
|
|
source: pkgDir,
|
|
extensions: ["!**/baz.ts"],
|
|
skills: [],
|
|
prompts: [],
|
|
themes: [],
|
|
},
|
|
]);
|
|
|
|
const result = await packageManager.resolve();
|
|
expect(result.extensions.some((r) => isEnabled(r, "foo.ts"))).toBe(true);
|
|
expect(result.extensions.some((r) => isEnabled(r, "bar.ts"))).toBe(true);
|
|
expect(result.extensions.some((r) => isDisabled(r, "baz.ts"))).toBe(true);
|
|
});
|
|
|
|
it("should filter themes from package", async () => {
|
|
const pkgDir = join(tempDir, "theme-pkg");
|
|
mkdirSync(join(pkgDir, "themes"), { recursive: true });
|
|
writeFileSync(join(pkgDir, "themes", "nice.json"), "{}");
|
|
writeFileSync(join(pkgDir, "themes", "ugly.json"), "{}");
|
|
|
|
settingsManager.setPackages([
|
|
{
|
|
source: pkgDir,
|
|
extensions: [],
|
|
skills: [],
|
|
prompts: [],
|
|
themes: ["!ugly.json"],
|
|
},
|
|
]);
|
|
|
|
const result = await packageManager.resolve();
|
|
expect(result.themes.some((r) => isEnabled(r, "nice.json"))).toBe(true);
|
|
expect(result.themes.some((r) => isDisabled(r, "ugly.json"))).toBe(true);
|
|
});
|
|
|
|
it("should combine include and exclude patterns", async () => {
|
|
const pkgDir = join(tempDir, "combo-pkg");
|
|
mkdirSync(join(pkgDir, "extensions"), { recursive: true });
|
|
writeFileSync(join(pkgDir, "extensions", "alpha.ts"), "export default function() {}");
|
|
writeFileSync(join(pkgDir, "extensions", "beta.ts"), "export default function() {}");
|
|
writeFileSync(join(pkgDir, "extensions", "gamma.ts"), "export default function() {}");
|
|
|
|
settingsManager.setPackages([
|
|
{
|
|
source: pkgDir,
|
|
extensions: ["**/alpha.ts", "**/beta.ts", "!**/beta.ts"],
|
|
skills: [],
|
|
prompts: [],
|
|
themes: [],
|
|
},
|
|
]);
|
|
|
|
const result = await packageManager.resolve();
|
|
expect(result.extensions.some((r) => isEnabled(r, "alpha.ts"))).toBe(true);
|
|
expect(result.extensions.some((r) => isDisabled(r, "beta.ts"))).toBe(true);
|
|
expect(result.extensions.some((r) => isDisabled(r, "gamma.ts"))).toBe(true);
|
|
});
|
|
|
|
it("should work with direct paths (no patterns)", async () => {
|
|
const pkgDir = join(tempDir, "direct-pkg");
|
|
mkdirSync(join(pkgDir, "extensions"), { recursive: true });
|
|
writeFileSync(join(pkgDir, "extensions", "one.ts"), "export default function() {}");
|
|
writeFileSync(join(pkgDir, "extensions", "two.ts"), "export default function() {}");
|
|
|
|
settingsManager.setPackages([
|
|
{
|
|
source: pkgDir,
|
|
extensions: ["extensions/one.ts"],
|
|
skills: [],
|
|
prompts: [],
|
|
themes: [],
|
|
},
|
|
]);
|
|
|
|
const result = await packageManager.resolve();
|
|
expect(result.extensions.some((r) => isEnabled(r, "one.ts"))).toBe(true);
|
|
expect(result.extensions.some((r) => isDisabled(r, "two.ts"))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("force-include patterns", () => {
|
|
it("should force-include extensions with + pattern after exclusion", async () => {
|
|
const extDir = join(tempDir, "extensions");
|
|
mkdirSync(extDir, { recursive: true });
|
|
writeFileSync(join(extDir, "keep.ts"), "export default function() {}");
|
|
writeFileSync(join(extDir, "excluded.ts"), "export default function() {}");
|
|
writeFileSync(join(extDir, "force-back.ts"), "export default function() {}");
|
|
|
|
// Exclude all, then force-include one back
|
|
settingsManager.setExtensionPaths([extDir, "!**/*.ts", "+**/force-back.ts"]);
|
|
|
|
const result = await packageManager.resolve();
|
|
expect(result.extensions.some((r) => isDisabled(r, "keep.ts"))).toBe(true);
|
|
expect(result.extensions.some((r) => isDisabled(r, "excluded.ts"))).toBe(true);
|
|
expect(result.extensions.some((r) => isEnabled(r, "force-back.ts"))).toBe(true);
|
|
});
|
|
|
|
it("should force-include overrides exclude in package filters", async () => {
|
|
const pkgDir = join(tempDir, "force-pkg");
|
|
mkdirSync(join(pkgDir, "extensions"), { recursive: true });
|
|
writeFileSync(join(pkgDir, "extensions", "alpha.ts"), "export default function() {}");
|
|
writeFileSync(join(pkgDir, "extensions", "beta.ts"), "export default function() {}");
|
|
writeFileSync(join(pkgDir, "extensions", "gamma.ts"), "export default function() {}");
|
|
|
|
settingsManager.setPackages([
|
|
{
|
|
source: pkgDir,
|
|
extensions: ["!*", "+**/beta.ts"],
|
|
skills: [],
|
|
prompts: [],
|
|
themes: [],
|
|
},
|
|
]);
|
|
|
|
const result = await packageManager.resolve();
|
|
expect(result.extensions.some((r) => isDisabled(r, "alpha.ts"))).toBe(true);
|
|
expect(result.extensions.some((r) => isEnabled(r, "beta.ts"))).toBe(true);
|
|
expect(result.extensions.some((r) => isDisabled(r, "gamma.ts"))).toBe(true);
|
|
});
|
|
|
|
it("should force-include multiple resources", async () => {
|
|
const pkgDir = join(tempDir, "multi-force-pkg");
|
|
mkdirSync(join(pkgDir, "skills/skill-a"), { recursive: true });
|
|
mkdirSync(join(pkgDir, "skills/skill-b"), { recursive: true });
|
|
mkdirSync(join(pkgDir, "skills/skill-c"), { recursive: true });
|
|
writeFileSync(join(pkgDir, "skills/skill-a", "SKILL.md"), "---\nname: skill-a\ndescription: A\n---\nContent");
|
|
writeFileSync(join(pkgDir, "skills/skill-b", "SKILL.md"), "---\nname: skill-b\ndescription: B\n---\nContent");
|
|
writeFileSync(join(pkgDir, "skills/skill-c", "SKILL.md"), "---\nname: skill-c\ndescription: C\n---\nContent");
|
|
|
|
settingsManager.setPackages([
|
|
{
|
|
source: pkgDir,
|
|
extensions: [],
|
|
skills: ["!*", "+**/skill-a", "+**/skill-c"],
|
|
prompts: [],
|
|
themes: [],
|
|
},
|
|
]);
|
|
|
|
const result = await packageManager.resolve();
|
|
expect(result.skills.some((r) => isEnabled(r, "skill-a", "includes"))).toBe(true);
|
|
expect(result.skills.some((r) => isDisabled(r, "skill-b", "includes"))).toBe(true);
|
|
expect(result.skills.some((r) => isEnabled(r, "skill-c", "includes"))).toBe(true);
|
|
});
|
|
|
|
it("should force-include after specific exclusion", async () => {
|
|
const extDir = join(tempDir, "specific-force");
|
|
mkdirSync(extDir, { recursive: true });
|
|
writeFileSync(join(extDir, "a.ts"), "export default function() {}");
|
|
writeFileSync(join(extDir, "b.ts"), "export default function() {}");
|
|
|
|
// Specifically exclude b.ts, then force it back
|
|
settingsManager.setExtensionPaths([extDir, "!**/b.ts", "+**/b.ts"]);
|
|
|
|
const result = await packageManager.resolve();
|
|
expect(result.extensions.some((r) => isEnabled(r, "a.ts"))).toBe(true);
|
|
expect(result.extensions.some((r) => isEnabled(r, "b.ts"))).toBe(true);
|
|
});
|
|
|
|
it("should handle force-include in manifest patterns", async () => {
|
|
const pkgDir = join(tempDir, "manifest-force-pkg");
|
|
mkdirSync(join(pkgDir, "extensions"), { recursive: true });
|
|
writeFileSync(join(pkgDir, "extensions", "one.ts"), "export default function() {}");
|
|
writeFileSync(join(pkgDir, "extensions", "two.ts"), "export default function() {}");
|
|
writeFileSync(join(pkgDir, "extensions", "three.ts"), "export default function() {}");
|
|
writeFileSync(
|
|
join(pkgDir, "package.json"),
|
|
JSON.stringify({
|
|
name: "manifest-force-pkg",
|
|
pi: {
|
|
extensions: ["extensions", "!**/two.ts", "+**/two.ts"],
|
|
},
|
|
}),
|
|
);
|
|
|
|
const result = await packageManager.resolveExtensionSources([pkgDir]);
|
|
expect(result.extensions.some((r) => isEnabled(r, "one.ts"))).toBe(true);
|
|
expect(result.extensions.some((r) => isEnabled(r, "two.ts"))).toBe(true);
|
|
expect(result.extensions.some((r) => isEnabled(r, "three.ts"))).toBe(true);
|
|
});
|
|
|
|
it("should force-include themes", async () => {
|
|
const themesDir = join(tempDir, "force-themes");
|
|
mkdirSync(themesDir, { recursive: true });
|
|
writeFileSync(join(themesDir, "dark.json"), "{}");
|
|
writeFileSync(join(themesDir, "light.json"), "{}");
|
|
writeFileSync(join(themesDir, "special.json"), "{}");
|
|
|
|
settingsManager.setThemePaths([themesDir, "!*.json", "+special.json"]);
|
|
|
|
const result = await packageManager.resolve();
|
|
expect(result.themes.some((r) => isDisabled(r, "dark.json"))).toBe(true);
|
|
expect(result.themes.some((r) => isDisabled(r, "light.json"))).toBe(true);
|
|
expect(result.themes.some((r) => isEnabled(r, "special.json"))).toBe(true);
|
|
});
|
|
|
|
it("should force-include prompts", async () => {
|
|
const promptsDir = join(tempDir, "force-prompts");
|
|
mkdirSync(promptsDir, { recursive: true });
|
|
writeFileSync(join(promptsDir, "review.md"), "Review");
|
|
writeFileSync(join(promptsDir, "explain.md"), "Explain");
|
|
writeFileSync(join(promptsDir, "debug.md"), "Debug");
|
|
|
|
settingsManager.setPromptTemplatePaths([promptsDir, "!*", "+debug.md"]);
|
|
|
|
const result = await packageManager.resolve();
|
|
expect(result.prompts.some((r) => isDisabled(r, "review.md"))).toBe(true);
|
|
expect(result.prompts.some((r) => isDisabled(r, "explain.md"))).toBe(true);
|
|
expect(result.prompts.some((r) => isEnabled(r, "debug.md"))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("package deduplication", () => {
|
|
it("should dedupe same local package in global and project (project wins)", async () => {
|
|
const pkgDir = join(tempDir, "shared-pkg");
|
|
mkdirSync(join(pkgDir, "extensions"), { recursive: true });
|
|
writeFileSync(join(pkgDir, "extensions", "shared.ts"), "export default function() {}");
|
|
|
|
// Same package in both global and project
|
|
settingsManager.setPackages([pkgDir]); // global
|
|
settingsManager.setProjectPackages([pkgDir]); // project
|
|
|
|
// Debug: verify settings are stored correctly
|
|
const globalSettings = settingsManager.getGlobalSettings();
|
|
const projectSettings = settingsManager.getProjectSettings();
|
|
expect(globalSettings.packages).toEqual([pkgDir]);
|
|
expect(projectSettings.packages).toEqual([pkgDir]);
|
|
|
|
const result = await packageManager.resolve();
|
|
// Should only appear once (deduped), with project scope
|
|
const sharedPaths = result.extensions.filter((r) => r.path.includes("shared-pkg"));
|
|
expect(sharedPaths.length).toBe(1);
|
|
expect(sharedPaths[0].metadata.scope).toBe("project");
|
|
});
|
|
|
|
it("should keep both if different packages", async () => {
|
|
const pkg1Dir = join(tempDir, "pkg1");
|
|
const pkg2Dir = join(tempDir, "pkg2");
|
|
mkdirSync(join(pkg1Dir, "extensions"), { recursive: true });
|
|
mkdirSync(join(pkg2Dir, "extensions"), { recursive: true });
|
|
writeFileSync(join(pkg1Dir, "extensions", "from-pkg1.ts"), "export default function() {}");
|
|
writeFileSync(join(pkg2Dir, "extensions", "from-pkg2.ts"), "export default function() {}");
|
|
|
|
settingsManager.setPackages([pkg1Dir]); // global
|
|
settingsManager.setProjectPackages([pkg2Dir]); // project
|
|
|
|
const result = await packageManager.resolve();
|
|
expect(result.extensions.some((r) => r.path.includes("pkg1"))).toBe(true);
|
|
expect(result.extensions.some((r) => r.path.includes("pkg2"))).toBe(true);
|
|
});
|
|
});
|
|
});
|