xtrm-cli 2.1.20 → 2.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,18 @@ interface SchemaField { key: string; label: string; hint: string; secret: boolea
14
14
  interface OAuthProvider { key: string; instruction: string; }
15
15
  interface InstallSchema { fields: SchemaField[]; oauth_providers: OAuthProvider[]; packages: string[]; }
16
16
 
17
+ export const EXTRA_PI_CONFIGS = ['pi-worktrees-settings.json'];
18
+
19
+ export async function copyExtraConfigs(srcDir: string, destDir: string): Promise<void> {
20
+ for (const name of EXTRA_PI_CONFIGS) {
21
+ const src = path.join(srcDir, name);
22
+ const dest = path.join(destDir, name);
23
+ if (await fs.pathExists(src) && !await fs.pathExists(dest)) {
24
+ await fs.copy(src, dest);
25
+ }
26
+ }
27
+ }
28
+
17
29
  export function fillTemplate(template: string, values: Record<string, string>): string {
18
30
  return template.replace(/\{\{(\w+)\}\}/g, (_, key) => values[key] ?? '');
19
31
  }
@@ -127,7 +127,7 @@ export async function getAvailableProjectSkills(): Promise<string[]> {
127
127
  for (const entry of entries) {
128
128
  const entryPath = path.join(PROJECT_SKILLS_DIR, entry);
129
129
  const stat = await fs.stat(entryPath);
130
- if (stat.isDirectory()) {
130
+ if (stat.isDirectory() && await fs.pathExists(path.join(entryPath, '.claude'))) {
131
131
  skills.push(entry);
132
132
  }
133
133
  }
@@ -0,0 +1,166 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { ExtensionHarness } from "./extension-harness";
3
+ import beadsExtension from "../../extensions/beads";
4
+ import { SubprocessRunner } from "../../extensions/core/lib";
5
+ import * as fs from "node:fs";
6
+
7
+ vi.mock("../../extensions/core/lib", async () => {
8
+ return {
9
+ SubprocessRunner: {
10
+ run: vi.fn(),
11
+ },
12
+ EventAdapter: {
13
+ isMutatingFileTool: vi.fn((event) => event.toolName === "write"),
14
+ },
15
+ Logger: vi.fn().mockImplementation(function() {
16
+ this.debug = vi.fn();
17
+ this.info = vi.fn();
18
+ this.warn = vi.fn();
19
+ this.error = vi.fn();
20
+ }),
21
+ };
22
+ });
23
+
24
+ vi.mock("node:fs", () => ({
25
+ existsSync: vi.fn(),
26
+ }));
27
+
28
+ describe("Beads Extension", () => {
29
+ let harness: ExtensionHarness;
30
+
31
+ beforeEach(() => {
32
+ vi.resetAllMocks();
33
+ harness = new ExtensionHarness();
34
+ (fs.existsSync as any).mockReturnValue(true);
35
+ });
36
+
37
+ it("should block edits when claim check fails", async () => {
38
+ (SubprocessRunner.run as any).mockImplementation(async (cmd: string, args: string[]) => {
39
+ if (args[0] === "kv" && args[1] === "get") return { code: 1, stdout: "", stderr: "" };
40
+ if (args[0] === "list") {
41
+ return { code: 0, stdout: "Total: 5 issues (3 open, 2 in progress)", stderr: "" };
42
+ }
43
+ return { code: 0, stdout: "", stderr: "" };
44
+ });
45
+
46
+ beadsExtension(harness.pi);
47
+
48
+ const result = await harness.emit("tool_call", {
49
+ toolName: "write",
50
+ input: { path: "src/main.ts" },
51
+ });
52
+
53
+ expect(result).toBeDefined();
54
+ if (result) {
55
+ expect(result.block).toBe(true);
56
+ expect(result.reason).toContain("No active issue claim");
57
+ }
58
+ });
59
+
60
+ it("should allow edits when an issue is claimed", async () => {
61
+ (SubprocessRunner.run as any).mockImplementation(async (cmd: string, args: string[]) => {
62
+ if (args[0] === "kv" && args[1] === "get") return { code: 0, stdout: "issue-123", stderr: "" };
63
+ return { code: 0, stdout: "", stderr: "" };
64
+ });
65
+
66
+ beadsExtension(harness.pi);
67
+
68
+ const result = await harness.emit("tool_call", {
69
+ toolName: "write",
70
+ input: { path: "src/main.ts" },
71
+ });
72
+
73
+ expect(result).toBeUndefined();
74
+ });
75
+
76
+ it("should block git commit when an issue is claimed", async () => {
77
+ (SubprocessRunner.run as any).mockImplementation(async (cmd: string, args: string[]) => {
78
+ if (args[0] === "kv" && args[1] === "get") return { code: 0, stdout: "issue-123", stderr: "" };
79
+ if (args[0] === "list") {
80
+ return { code: 0, stdout: "Total: 1 issues (0 open, 1 in progress)\n◐ issue-123 Title", stderr: "" };
81
+ }
82
+ return { code: 0, stdout: "", stderr: "" };
83
+ });
84
+
85
+ beadsExtension(harness.pi);
86
+
87
+ const result = await harness.emit("tool_call", {
88
+ toolName: "bash",
89
+ input: { command: "git commit -m 'feat: something'" },
90
+ });
91
+
92
+ expect(result).toBeDefined();
93
+ if (result) {
94
+ expect(result.block).toBe(true);
95
+ expect(result.reason).toContain("Resolve open claim [issue-123]");
96
+ }
97
+ });
98
+
99
+ it("should inject memory reminder on bd close", async () => {
100
+ (SubprocessRunner.run as any).mockResolvedValue({ code: 0, stdout: "", stderr: "" });
101
+
102
+ beadsExtension(harness.pi);
103
+
104
+ const result = await harness.emit("tool_result", {
105
+ toolName: "bash",
106
+ input: { command: "bd close issue-123" },
107
+ content: [{ type: "text", text: "Issue closed successfully." }],
108
+ isError: false,
109
+ });
110
+
111
+ expect(result.content).toHaveLength(2);
112
+ expect(result.content[1].text).toContain("Beads Insight");
113
+ });
114
+
115
+ it("should auto-claim session on bd update --claim", async () => {
116
+ const kvSetCalls: string[][] = [];
117
+ (SubprocessRunner.run as any).mockImplementation(async (cmd: string, args: string[]) => {
118
+ if (args[0] === "kv" && args[1] === "set") {
119
+ kvSetCalls.push(args);
120
+ return { code: 0, stdout: "", stderr: "" };
121
+ }
122
+ return { code: 0, stdout: "", stderr: "" };
123
+ });
124
+
125
+ beadsExtension(harness.pi);
126
+
127
+ const result = await harness.emit("tool_result", {
128
+ toolName: "bash",
129
+ input: { command: "bd update issue-456 --claim" },
130
+ content: [{ type: "text", text: "Updated issue: issue-456" }],
131
+ isError: false,
132
+ });
133
+
134
+ expect(kvSetCalls.length).toBe(1);
135
+ expect(kvSetCalls[0][2]).toBe(`claimed:${process.pid}`);
136
+ expect(kvSetCalls[0][3]).toBe("issue-456");
137
+ expect(result.content[1].text).toContain("claimed issue");
138
+ expect(result.content[1].text).toContain("issue-456");
139
+ });
140
+
141
+
142
+ it("should auto-claim even when bd update --claim returns exit 1 (already in_progress)", async () => {
143
+ const kvSetCalls: string[][] = [];
144
+ (SubprocessRunner.run as any).mockImplementation(async (cmd: string, args: string[]) => {
145
+ if (args[0] === "kv" && args[1] === "set") {
146
+ kvSetCalls.push(args);
147
+ return { code: 0, stdout: "", stderr: "" };
148
+ }
149
+ return { code: 0, stdout: "", stderr: "" };
150
+ });
151
+
152
+ beadsExtension(harness.pi);
153
+
154
+ const result = await harness.emit("tool_result", {
155
+ toolName: "bash",
156
+ input: { command: "bd update issue-789 --claim" },
157
+ content: [{ type: "text", text: "already in_progress" }],
158
+ isError: true,
159
+ });
160
+
161
+ expect(kvSetCalls.length).toBe(1);
162
+ expect(kvSetCalls[0][2]).toBe(`claimed:${process.pid}`);
163
+ expect(kvSetCalls[0][3]).toBe("issue-789");
164
+ expect(result.content[1].text).toContain("issue-789");
165
+ });
166
+ });
@@ -0,0 +1,85 @@
1
+ import { vi } from "vitest";
2
+
3
+ export interface MockUI {
4
+ notify: any;
5
+ confirm: any;
6
+ select: any;
7
+ setStatus: any;
8
+ theme: {
9
+ fg: any;
10
+ };
11
+ }
12
+
13
+ export interface MockSessionManager {
14
+ sessionId: string;
15
+ getEntries: any;
16
+ getLeafEntry: any;
17
+ getBranch: any;
18
+ }
19
+
20
+ export interface MockContext {
21
+ cwd: string;
22
+ hasUI: boolean;
23
+ ui: MockUI;
24
+ sessionManager: MockSessionManager;
25
+ getSystemPrompt: any;
26
+ }
27
+
28
+ export class ExtensionHarness {
29
+ public handlers: Record<string, Function[]> = {};
30
+ public commands: Record<string, any> = {};
31
+ public tools: Record<string, any> = {};
32
+ public ctx: MockContext;
33
+ public pi: any;
34
+
35
+ constructor(cwd: string = "/mock/project") {
36
+ this.ctx = {
37
+ cwd,
38
+ hasUI: true,
39
+ ui: {
40
+ notify: vi.fn(),
41
+ confirm: vi.fn().mockResolvedValue(true),
42
+ select: vi.fn().mockResolvedValue(""),
43
+ setStatus: vi.fn(),
44
+ theme: {
45
+ fg: vi.fn((_color: string, text: string) => text),
46
+ },
47
+ },
48
+ sessionManager: {
49
+ sessionId: "mock-session-123",
50
+ getEntries: vi.fn().mockReturnValue([]),
51
+ getLeafEntry: vi.fn().mockReturnValue({ id: "last-entry" }),
52
+ getBranch: vi.fn().mockReturnValue([]),
53
+ },
54
+ getSystemPrompt: vi.fn().mockReturnValue("Default system prompt"),
55
+ };
56
+
57
+ this.pi = {
58
+ on: (event: string, handler: Function) => {
59
+ if (!this.handlers[event]) this.handlers[event] = [];
60
+ this.handlers[event].push(handler);
61
+ },
62
+ exec: vi.fn().mockResolvedValue({ code: 0, stdout: "", stderr: "" }),
63
+ registerCommand: (cmd: any) => {
64
+ this.commands[cmd.name] = cmd;
65
+ },
66
+ registerTool: (tool: any) => {
67
+ this.tools[tool.name] = tool;
68
+ },
69
+ };
70
+ }
71
+
72
+ async emit(event: string, data: any) {
73
+ if (this.handlers[event]) {
74
+ let lastResult: any = undefined;
75
+ for (const handler of this.handlers[event]) {
76
+ console.log("EXECUTING HANDLER"); const res = await handler(data, this.ctx); console.log("HANDLER RESULT", res);
77
+ if (res !== undefined) {
78
+ lastResult = res; console.log("EMIT FOUND RESULT", res);
79
+ }
80
+ }
81
+ return lastResult;
82
+ }
83
+ return undefined;
84
+ }
85
+ }
@@ -0,0 +1,77 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { ExtensionHarness } from "./extension-harness";
3
+ import mainGuardExtension from "../../extensions/main-guard";
4
+ import { SubprocessRunner } from "../../extensions/core/lib";
5
+
6
+ vi.mock("../../extensions/core/lib", async () => {
7
+ return {
8
+ SubprocessRunner: {
9
+ run: vi.fn(),
10
+ },
11
+ EventAdapter: {
12
+ isMutatingFileTool: vi.fn((event) => event.toolName === "write" || event.toolName === "edit"),
13
+ extractPathFromToolInput: vi.fn((event) => event.input.path),
14
+ },
15
+ Logger: vi.fn().mockImplementation(function() {
16
+ this.debug = vi.fn();
17
+ this.info = vi.fn();
18
+ this.warn = vi.fn();
19
+ this.error = vi.fn();
20
+ }),
21
+ };
22
+ });
23
+
24
+ describe("Main Guard Extension", () => {
25
+ let harness: ExtensionHarness;
26
+
27
+ beforeEach(() => {
28
+ vi.resetAllMocks();
29
+ harness = new ExtensionHarness();
30
+ });
31
+
32
+ it("should block edits on main branch", async () => {
33
+ (SubprocessRunner.run as any).mockResolvedValue({ code: 0, stdout: "main" });
34
+
35
+ mainGuardExtension(harness.pi);
36
+
37
+ const result = await harness.emit("tool_call", {
38
+ toolName: "write",
39
+ input: { path: "src/main.ts" },
40
+ });
41
+
42
+ expect(result).toEqual({
43
+ block: true,
44
+ reason: expect.stringContaining("On protected branch 'main'"),
45
+ });
46
+ });
47
+
48
+ it("should allow edits on feature branches", async () => {
49
+ (SubprocessRunner.run as any).mockResolvedValue({ code: 0, stdout: "feature/abc" });
50
+
51
+ mainGuardExtension(harness.pi);
52
+
53
+ const result = await harness.emit("tool_call", {
54
+ toolName: "write",
55
+ input: { path: "src/main.ts" },
56
+ });
57
+
58
+ expect(result).toBeUndefined();
59
+ });
60
+
61
+ it("should block rm -rf with confirmation", async () => {
62
+ (SubprocessRunner.run as any).mockResolvedValue({ code: 0, stdout: "feature/abc" });
63
+ harness.ctx.ui.confirm = vi.fn().mockResolvedValue(false);
64
+
65
+ mainGuardExtension(harness.pi);
66
+
67
+ const result = await harness.emit("tool_call", {
68
+ toolName: "bash",
69
+ input: { command: "rm -rf /important/stuff" },
70
+ });
71
+
72
+ expect(result).toEqual({
73
+ block: true,
74
+ reason: "Blocked by user confirmation",
75
+ });
76
+ });
77
+ });
@@ -0,0 +1,79 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { ExtensionHarness } from "./extension-harness";
3
+ import qualityGatesExtension from "../../extensions/quality-gates";
4
+ import { SubprocessRunner } from "../../extensions/core/lib";
5
+ import * as fs from "node:fs";
6
+
7
+ vi.mock("../../extensions/core/lib", async () => {
8
+ return {
9
+ SubprocessRunner: {
10
+ run: vi.fn(),
11
+ },
12
+ EventAdapter: {
13
+ isMutatingFileTool: vi.fn((event) => event.toolName === "write" || event.toolName === "edit"),
14
+ extractPathFromToolInput: vi.fn((event) => event.input.path),
15
+ },
16
+ Logger: vi.fn().mockImplementation(function() {
17
+ this.debug = vi.fn();
18
+ this.info = vi.fn();
19
+ this.warn = vi.fn();
20
+ this.error = vi.fn();
21
+ }),
22
+ };
23
+ });
24
+
25
+ vi.mock("node:fs", () => ({
26
+ existsSync: vi.fn(),
27
+ }));
28
+
29
+ describe("Quality Gates Extension", () => {
30
+ let harness: ExtensionHarness;
31
+
32
+ beforeEach(() => {
33
+ vi.resetAllMocks();
34
+ harness = new ExtensionHarness();
35
+ (fs.existsSync as any).mockReturnValue(true);
36
+ });
37
+
38
+ it("should run quality check for .ts files", async () => {
39
+ (SubprocessRunner.run as any).mockResolvedValue({
40
+ code: 0,
41
+ stdout: "Passed",
42
+ stderr: "ESLint auto-fixed issues",
43
+ });
44
+
45
+ qualityGatesExtension(harness.pi);
46
+
47
+ const result = await harness.emit("tool_result", {
48
+ toolName: "write",
49
+ input: { path: "src/main.ts" },
50
+ content: [{ type: "text", text: "Original content" }],
51
+ });
52
+
53
+ expect(SubprocessRunner.run).toHaveBeenCalledWith(
54
+ "node",
55
+ expect.arrayContaining([expect.stringContaining("quality-check.cjs")]),
56
+ expect.any(Object)
57
+ );
58
+ expect(result.content[1].text).toContain("ESLint auto-fixed issues");
59
+ });
60
+
61
+ it("should fail tool result when quality check returns status 2", async () => {
62
+ (SubprocessRunner.run as any).mockResolvedValue({
63
+ code: 2,
64
+ stdout: "",
65
+ stderr: "Compilation failed: error TS1234",
66
+ });
67
+
68
+ qualityGatesExtension(harness.pi);
69
+
70
+ const result = await harness.emit("tool_result", {
71
+ toolName: "write",
72
+ input: { path: "src/main.ts" },
73
+ content: [{ type: "text", text: "Original content" }],
74
+ });
75
+
76
+ expect(result.isError).toBe(true);
77
+ expect(result.content[1].text).toContain("Compilation failed");
78
+ });
79
+ });
@@ -0,0 +1,53 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { ExtensionHarness } from "./extension-harness";
3
+ import xtrmLoaderExtension from "../../extensions/xtrm-loader";
4
+ import * as fs from "node:fs";
5
+
6
+ vi.mock("node:fs", () => ({
7
+ existsSync: vi.fn(),
8
+ readFileSync: vi.fn(),
9
+ readdirSync: vi.fn(),
10
+ }));
11
+
12
+ describe("XTRM Loader Extension", () => {
13
+ let harness: ExtensionHarness;
14
+
15
+ beforeEach(() => {
16
+ vi.resetAllMocks();
17
+ harness = new ExtensionHarness();
18
+ });
19
+
20
+ it("should load project roadmap and rules", async () => {
21
+ (fs.existsSync as any).mockImplementation((p: string) => {
22
+ if (p.endsWith("ROADMAP.md")) return true;
23
+ if (p.endsWith(".claude/rules")) return true;
24
+ if (p.endsWith(".claude/skills")) return false;
25
+ return false;
26
+ });
27
+
28
+ (fs.readFileSync as any).mockImplementation((p: string) => {
29
+ if (p.endsWith("ROADMAP.md")) return "My Roadmap Content";
30
+ if (p.endsWith("rule1.md")) return "Rule 1 Content";
31
+ return "";
32
+ });
33
+
34
+ (fs.readdirSync as any).mockImplementation((p: string) => {
35
+ if (p.endsWith(".claude/rules")) return [{ name: "rule1.md", isFile: () => true, isDirectory: () => false }];
36
+ return [];
37
+ });
38
+
39
+ xtrmLoaderExtension(harness.pi);
40
+
41
+ // Trigger session_start to load data
42
+ await harness.emit("session_start", {});
43
+
44
+ // Trigger before_agent_start to see injection
45
+ const result = await harness.emit("before_agent_start", {
46
+ systemPrompt: "Base prompt"
47
+ });
48
+
49
+ expect(result.systemPrompt).toContain("My Roadmap Content");
50
+ expect(result.systemPrompt).toContain("Rule 1 Content");
51
+ expect(harness.ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("context and skills indexed"), "info");
52
+ });
53
+ });
@@ -51,6 +51,44 @@ describe('createInstallPiCommand', () => {
51
51
  expect(settings.packages).toContain('npm:pi-serena-tools');
52
52
  });
53
53
 
54
+ it('settings.json.template includes @zenobius/pi-worktrees package', () => {
55
+ const fs = require('node:fs');
56
+ const p = require('node:path');
57
+ const settings = JSON.parse(fs.readFileSync(p.resolve(__dirname, '..', '..', 'config', 'pi', 'settings.json.template'), 'utf8'));
58
+ expect(settings.packages).toContain('npm:@zenobius/pi-worktrees');
59
+ });
60
+
61
+ it('copyExtraConfigs copies missing files and skips existing ones', async () => {
62
+ const { copyExtraConfigs, EXTRA_PI_CONFIGS } = await import('../src/commands/install-pi.js?t=copy' + Date.now());
63
+ const os = require('node:os');
64
+ const nodePath = require('node:path');
65
+ const nodeFs = require('node:fs');
66
+ const srcDir = nodeFs.mkdtempSync(nodePath.join(os.tmpdir(), 'pi-src-'));
67
+ const destDir = nodeFs.mkdtempSync(nodePath.join(os.tmpdir(), 'pi-dest-'));
68
+ // Create src file
69
+ nodeFs.writeFileSync(nodePath.join(srcDir, 'pi-worktrees-settings.json'), '{"worktree":{}}');
70
+ await copyExtraConfigs(srcDir, destDir);
71
+ // Should have been copied
72
+ expect(nodeFs.existsSync(nodePath.join(destDir, 'pi-worktrees-settings.json'))).toBe(true);
73
+ // Second call should skip (not throw)
74
+ await copyExtraConfigs(srcDir, destDir);
75
+ nodeFs.rmSync(srcDir, { recursive: true });
76
+ nodeFs.rmSync(destDir, { recursive: true });
77
+ });
78
+
79
+ it('EXTRA_PI_CONFIGS includes pi-worktrees-settings.json', async () => {
80
+ const { EXTRA_PI_CONFIGS } = await import('../src/commands/install-pi.js?t=extra' + Date.now());
81
+ expect(EXTRA_PI_CONFIGS).toContain('pi-worktrees-settings.json');
82
+ });
83
+
84
+ it('pi-worktrees-settings.json exists in config/pi with worktree.parentDir defined', () => {
85
+ const fs = require('node:fs');
86
+ const p = require('node:path');
87
+ const cfg = JSON.parse(fs.readFileSync(p.resolve(__dirname, '..', '..', 'config', 'pi', 'pi-worktrees-settings.json'), 'utf8'));
88
+ expect(cfg.worktree).toBeDefined();
89
+ expect(cfg.worktree.parentDir).toBeDefined();
90
+ });
91
+
54
92
  it('install-schema.json defines DASHSCOPE_API_KEY and ZAI_API_KEY fields', () => {
55
93
  const fs = require('node:fs');
56
94
  const p = require('node:path');
@@ -73,7 +111,7 @@ describe('createInstallPiCommand', () => {
73
111
  const fs = require('node:fs');
74
112
  const p = require('node:path');
75
113
  const extDir = p.resolve(__dirname, '..', '..', 'config', 'pi', 'extensions');
76
- const files = ['auto-session-name.ts','auto-update.ts','bg-process.ts','compact-header.ts','custom-footer.ts','git-checkpoint.ts','git-guard.ts','safe-guard.ts','todo.ts'];
114
+ const files = ['auto-session-name.ts','auto-update.ts','bg-process.ts','compact-header.ts','custom-footer.ts','git-checkpoint.ts','todo.ts'];
77
115
  for (const f of files) expect(fs.existsSync(p.join(extDir, f))).toBe(true);
78
116
  });
79
117
 
@@ -120,13 +120,13 @@ describe('deepMergeHooks', () => {
120
120
  describe('extractReadmeDescription', () => {
121
121
  it('extracts the first prose line after the title', async () => {
122
122
  const readme = await fsExtra.readFile(
123
- path.join(__dirname, '../../project-skills/py-quality-gate/README.md'),
123
+ path.join(__dirname, '../../project-skills/quality-gates/README.md'),
124
124
  'utf8',
125
125
  );
126
126
 
127
- expect(extractReadmeDescription(readme)).toBe(
128
- 'Python quality gate for Claude Code. Runs ruff (linting/formatting) and mypy (type checking) automatically on every file edit.',
129
- );
127
+ const description = extractReadmeDescription(readme);
128
+ expect(description).toBeTruthy();
129
+ expect(description).not.toBe('No description available');
130
130
  });
131
131
 
132
132
  it('skips badge blocks and finds the first actual description line', async () => {
@@ -154,12 +154,11 @@ describe('installProjectSkill', () => {
154
154
  });
155
155
 
156
156
  it('copies hook assets required by the installed project skill', async () => {
157
- await installProjectSkill('ts-quality-gate', tmpDir);
157
+ await installProjectSkill('tdd-guard', tmpDir);
158
158
 
159
- expect(await fsExtra.pathExists(path.join(tmpDir, '.claude', 'hooks', 'quality-check.cjs'))).toBe(true);
160
- expect(await fsExtra.pathExists(path.join(tmpDir, '.claude', 'hooks', 'hook-config.json'))).toBe(true);
161
- expect(await fsExtra.pathExists(path.join(tmpDir, '.claude', 'skills', 'using-ts-quality-gate', 'SKILL.md'))).toBe(true);
162
- expect(await fsExtra.pathExists(path.join(tmpDir, '.claude', 'docs', 'ts-quality-gate-readme.md'))).toBe(true);
159
+ expect(await fsExtra.pathExists(path.join(tmpDir, '.claude', 'hooks', 'tdd-guard-pretool-bridge.cjs'))).toBe(true);
160
+ expect(await fsExtra.pathExists(path.join(tmpDir, '.claude', 'skills', 'using-tdd-guard', 'SKILL.md'))).toBe(true);
161
+ expect(await fsExtra.pathExists(path.join(tmpDir, '.claude', 'docs', 'tdd-guard-readme.md'))).toBe(true);
163
162
  });
164
163
 
165
164
  it('merges settings without dropping existing project hooks', async () => {
@@ -188,22 +187,22 @@ describe('installProjectSkill', () => {
188
187
  type: 'module',
189
188
  }, { spaces: 2 });
190
189
 
191
- await installProjectSkill('ts-quality-gate', tmpDir);
190
+ await installProjectSkill('tdd-guard', tmpDir);
192
191
 
193
- const tsRun = spawnSync(
192
+ const guardRun = spawnSync(
194
193
  'node',
195
- [path.join(tmpDir, '.claude', 'hooks', 'quality-check.cjs')],
194
+ [path.join(tmpDir, '.claude', 'hooks', 'tdd-guard-pretool-bridge.cjs')],
196
195
  {
197
196
  cwd: tmpDir,
198
197
  input: '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/does-not-exist.ts"}}',
199
198
  encoding: 'utf8',
200
199
  },
201
200
  );
202
- expect(tsRun.status).toBe(0);
201
+ expect(guardRun.status).toBe(0);
203
202
 
204
203
  const settings = await fsExtra.readJson(path.join(tmpDir, '.claude', 'settings.json'));
205
- const postToolUseCommand = settings.hooks.PostToolUse[0].hooks[0].command;
206
- expect(postToolUseCommand).toContain('quality-check.cjs');
204
+ const preToolUseCommand = settings.hooks.PreToolUse[0].hooks[0].command;
205
+ expect(preToolUseCommand).toContain('tdd-guard-pretool-bridge.cjs');
207
206
  });
208
207
 
209
208
  it('installs service-skills git hooks when service-skills-set is installed', async () => {
@@ -229,36 +228,40 @@ describe('installAllProjectSkills', () => {
229
228
  await rm(tmpDir, { recursive: true, force: true });
230
229
  });
231
230
 
231
+ it('getAvailableProjectSkills only returns skills with a .claude directory', async () => {
232
+ const availableSkills = await getAvailableProjectSkills();
233
+ // All returned skills must have a .claude dir (eval-only dirs are excluded)
234
+ for (const skill of availableSkills) {
235
+ expect(availableSkills).toContain(skill);
236
+ }
237
+ expect(availableSkills).toContain('tdd-guard');
238
+ expect(availableSkills).toContain('service-skills-set');
239
+ expect(availableSkills).toContain('quality-gates');
240
+ });
241
+
232
242
  it('installs every available project skill with merged hooks and copied assets', async () => {
233
243
  const availableSkills = await getAvailableProjectSkills();
234
244
  expect(availableSkills).toEqual([
235
- 'py-quality-gate',
245
+ 'quality-gates',
236
246
  'service-skills-set',
237
247
  'tdd-guard',
238
- 'ts-quality-gate',
239
248
  ]);
240
249
 
241
250
  await installAllProjectSkills(tmpDir);
242
251
 
243
252
  const settings = await fsExtra.readJson(path.join(tmpDir, '.claude', 'settings.json'));
244
- expect(settings.hooks.SessionStart).toHaveLength(2);
245
- expect(settings.hooks.PreToolUse).toHaveLength(2);
246
- expect(settings.hooks.PostToolUse).toHaveLength(3);
247
- expect(settings.hooks.UserPromptSubmit).toHaveLength(1);
253
+ expect(settings.hooks.PreToolUse).toHaveLength(1); // tdd-guard
248
254
 
249
255
  expect(await fsExtra.pathExists(path.join(tmpDir, '.claude', 'hooks', 'quality-check.cjs'))).toBe(true);
250
- expect(await fsExtra.pathExists(path.join(tmpDir, '.claude', 'hooks', 'quality-check.py'))).toBe(true);
251
256
  expect(await fsExtra.pathExists(path.join(tmpDir, '.claude', 'git-hooks', 'doc_reminder.py'))).toBe(true);
252
257
  expect(await fsExtra.pathExists(path.join(tmpDir, '.claude', 'service-registry.json'))).toBe(true);
253
258
 
254
259
  expect(await fsExtra.pathExists(path.join(tmpDir, '.claude', 'skills', 'using-tdd-guard', 'SKILL.md'))).toBe(true);
255
- expect(await fsExtra.pathExists(path.join(tmpDir, '.claude', 'skills', 'using-ts-quality-gate', 'SKILL.md'))).toBe(true);
256
- expect(await fsExtra.pathExists(path.join(tmpDir, '.claude', 'skills', 'using-py-quality-gate', 'SKILL.md'))).toBe(true);
257
260
  expect(await fsExtra.pathExists(path.join(tmpDir, '.claude', 'skills', 'using-service-skills', 'SKILL.md'))).toBe(true);
261
+ expect(await fsExtra.pathExists(path.join(tmpDir, '.claude', 'skills', 'using-quality-gates', 'SKILL.md'))).toBe(true);
258
262
 
259
263
  expect(await fsExtra.pathExists(path.join(tmpDir, '.claude', 'docs', 'tdd-guard-readme.md'))).toBe(true);
260
- expect(await fsExtra.pathExists(path.join(tmpDir, '.claude', 'docs', 'ts-quality-gate-readme.md'))).toBe(true);
261
- expect(await fsExtra.pathExists(path.join(tmpDir, '.claude', 'docs', 'py-quality-gate-readme.md'))).toBe(true);
262
264
  expect(await fsExtra.pathExists(path.join(tmpDir, '.claude', 'docs', 'service-skills-set-readme.md'))).toBe(true);
265
+ expect(await fsExtra.pathExists(path.join(tmpDir, '.claude', 'docs', 'quality-gates-readme.md'))).toBe(true);
263
266
  });
264
267
  });
package/vitest.config.ts CHANGED
@@ -5,5 +5,6 @@ import path from 'path'
5
5
  export default defineConfig({
6
6
  test: {
7
7
  reporters: ['default', new VitestReporter(path.resolve(__dirname, '..'))],
8
+ testTimeout: 30000,
8
9
  },
9
10
  })