superkit-mcp-server 1.2.5 → 1.2.7

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.
@@ -0,0 +1,177 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ // The conventional folder name for project-local Super-Kit assets
4
+ export const PROJECT_ASSETS_DIR = '.agents';
5
+ /**
6
+ * Resolves the effective project root path.
7
+ * - If an explicit projectPath is provided, it is resolved to an absolute path.
8
+ * - Otherwise, falls back to process.cwd().
9
+ */
10
+ export function resolve_project_path(projectPath) {
11
+ if (projectPath) {
12
+ return path.resolve(projectPath);
13
+ }
14
+ return process.cwd();
15
+ }
16
+ /**
17
+ * Returns the absolute path to the .agents/ root for the given project.
18
+ */
19
+ export function get_project_agents_root(projectPath) {
20
+ return path.join(resolve_project_path(projectPath), PROJECT_ASSETS_DIR);
21
+ }
22
+ /**
23
+ * Internal guard: ensures the resolved path stays strictly within the .agents/ root.
24
+ * Returns the resolved absolute path, or null if a traversal attempt is detected.
25
+ */
26
+ function safe_project_path(agentsRoot, relative) {
27
+ // Normalize the root so the startsWith check is reliable on all platforms
28
+ const normalizedRoot = path.resolve(agentsRoot);
29
+ const resolved = path.resolve(agentsRoot, relative);
30
+ // On Windows path.resolve produces lower-cased drive letters consistently,
31
+ // so this comparison is safe cross-platform.
32
+ if (!resolved.startsWith(normalizedRoot + path.sep) && resolved !== normalizedRoot) {
33
+ return null; // traversal detected
34
+ }
35
+ return resolved;
36
+ }
37
+ /**
38
+ * Validates a user-supplied asset name (agentName, skillName, workflowName).
39
+ * Rejects absolute paths and anything containing path-separator characters.
40
+ */
41
+ function validate_asset_name(name) {
42
+ if (!name || typeof name !== 'string') {
43
+ throw new Error('Asset name must be a non-empty string.');
44
+ }
45
+ if (path.isAbsolute(name)) {
46
+ throw new Error(`Asset name must not be an absolute path: "${name}"`);
47
+ }
48
+ // Reject any component that contains a path separator or navigates upward
49
+ const normalized = path.normalize(name);
50
+ if (normalized.includes('..') || normalized.startsWith('/') || normalized.startsWith('\\')) {
51
+ throw new Error(`Asset name contains invalid path components: "${name}"`);
52
+ }
53
+ }
54
+ // ---------------------------------------------------------------------------
55
+ // Listing helpers
56
+ // ---------------------------------------------------------------------------
57
+ /**
58
+ * Lists project-scoped agent names (without .md extension) from .agents/agents/.
59
+ * Returns an empty array if the directory does not exist.
60
+ */
61
+ export async function list_project_agents(projectPath) {
62
+ const agentsRoot = get_project_agents_root(projectPath);
63
+ const agentsDir = path.join(agentsRoot, 'agents');
64
+ try {
65
+ const entries = await fs.readdir(agentsDir, { withFileTypes: true });
66
+ return entries
67
+ .filter((e) => e.isFile() && e.name.endsWith('.md'))
68
+ .map((e) => e.name.replace(/\.md$/, ''));
69
+ }
70
+ catch {
71
+ // Directory does not exist — graceful degradation
72
+ return [];
73
+ }
74
+ }
75
+ /**
76
+ * Lists project-scoped skill directory names from .agents/skills/tech/ and .agents/skills/meta/.
77
+ * Returns empty arrays for each category if the directories do not exist.
78
+ */
79
+ export async function list_project_skills(projectPath) {
80
+ const agentsRoot = get_project_agents_root(projectPath);
81
+ const list_category_skills = async (category) => {
82
+ const categoryDir = path.join(agentsRoot, 'skills', category);
83
+ try {
84
+ const entries = await fs.readdir(categoryDir, { withFileTypes: true });
85
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
86
+ }
87
+ catch {
88
+ return [];
89
+ }
90
+ };
91
+ const [tech, meta] = await Promise.all([
92
+ list_category_skills('tech'),
93
+ list_category_skills('meta'),
94
+ ]);
95
+ return { tech, meta };
96
+ }
97
+ /**
98
+ * Lists project-scoped workflow names (without .md extension) from .agents/workflows/.
99
+ * Returns an empty array if the directory does not exist.
100
+ */
101
+ export async function list_project_workflows(projectPath) {
102
+ const agentsRoot = get_project_agents_root(projectPath);
103
+ const workflowsDir = path.join(agentsRoot, 'workflows');
104
+ try {
105
+ const entries = await fs.readdir(workflowsDir, { withFileTypes: true });
106
+ return entries
107
+ .filter((e) => e.isFile() && e.name.endsWith('.md'))
108
+ .map((e) => e.name.replace(/\.md$/, ''));
109
+ }
110
+ catch {
111
+ return [];
112
+ }
113
+ }
114
+ // ---------------------------------------------------------------------------
115
+ // Loading helpers
116
+ // ---------------------------------------------------------------------------
117
+ /**
118
+ * Loads a project-scoped agent's markdown content from:
119
+ * {projectPath}/.agents/agents/{agentName}.md
120
+ */
121
+ export async function load_project_agent_file(agentName, projectPath) {
122
+ validate_asset_name(agentName);
123
+ const agentsRoot = get_project_agents_root(projectPath);
124
+ const relative = path.join('agents', `${agentName}.md`);
125
+ const safePath = safe_project_path(agentsRoot, relative);
126
+ if (!safePath) {
127
+ throw new Error(`Path traversal detected for agent name: "${agentName}"`);
128
+ }
129
+ try {
130
+ return await fs.readFile(safePath, 'utf-8');
131
+ }
132
+ catch {
133
+ throw new Error(`Project agent not found: "${agentName}". ` +
134
+ `Expected file at: ${safePath}`);
135
+ }
136
+ }
137
+ /**
138
+ * Loads a project-scoped skill's SKILL.md content from:
139
+ * {projectPath}/.agents/skills/{category}/{skillName}/SKILL.md
140
+ */
141
+ export async function load_project_skill_file(category, skillName, projectPath) {
142
+ validate_asset_name(category);
143
+ validate_asset_name(skillName);
144
+ const agentsRoot = get_project_agents_root(projectPath);
145
+ const relative = path.join('skills', category, skillName, 'SKILL.md');
146
+ const safePath = safe_project_path(agentsRoot, relative);
147
+ if (!safePath) {
148
+ throw new Error(`Path traversal detected for skill: category="${category}", skillName="${skillName}"`);
149
+ }
150
+ try {
151
+ return await fs.readFile(safePath, 'utf-8');
152
+ }
153
+ catch {
154
+ throw new Error(`Project skill not found: category="${category}", skillName="${skillName}". ` +
155
+ `Expected SKILL.md at: ${safePath}`);
156
+ }
157
+ }
158
+ /**
159
+ * Loads a project-scoped workflow's markdown content from:
160
+ * {projectPath}/.agents/workflows/{workflowName}.md
161
+ */
162
+ export async function load_project_workflow_file(workflowName, projectPath) {
163
+ validate_asset_name(workflowName);
164
+ const agentsRoot = get_project_agents_root(projectPath);
165
+ const relative = path.join('workflows', `${workflowName}.md`);
166
+ const safePath = safe_project_path(agentsRoot, relative);
167
+ if (!safePath) {
168
+ throw new Error(`Path traversal detected for workflow name: "${workflowName}"`);
169
+ }
170
+ try {
171
+ return await fs.readFile(safePath, 'utf-8');
172
+ }
173
+ catch {
174
+ throw new Error(`Project workflow not found: "${workflowName}". ` +
175
+ `Expected file at: ${safePath}`);
176
+ }
177
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superkit-mcp-server",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
4
4
  "type": "module",
5
5
  "description": "An MCP server for exploring and loading Super-Kit AI agent resources.",
6
6
  "main": "build/index.js",
@@ -8,6 +8,8 @@
8
8
  "superkit-mcp-server": "./build/index.js"
9
9
  },
10
10
  "scripts": {
11
+ "clean": "rimraf build",
12
+ "prebuild": "rimraf build",
11
13
  "build": "tsc",
12
14
  "start": "node build/index.js",
13
15
  "dev": "tsc --watch"
@@ -21,6 +23,7 @@
21
23
  },
22
24
  "devDependencies": {
23
25
  "@types/node": "^22.19.13",
26
+ "rimraf": "^6.1.3",
24
27
  "typescript": "^5.7.2",
25
28
  "vitest": "^4.0.18"
26
29
  },
@@ -1,42 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { completePlan, validateArchitecture } from '../archTools.js';
3
- import * as fs from 'fs/promises';
4
- import * as path from 'path';
5
- import * as os from 'os';
6
- describe('Arch Tools', () => {
7
- let tempDir;
8
- beforeEach(async () => {
9
- tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'arch-test-'));
10
- });
11
- afterEach(async () => {
12
- await fs.rm(tempDir, { recursive: true, force: true });
13
- });
14
- it('should complete a plan successfully', async () => {
15
- const planPath = path.join(tempDir, 'plan1.md');
16
- await fs.writeFile(planPath, '> Status: Draft\n\n- [x] Done item');
17
- const res = await completePlan('plan1.md', false, tempDir);
18
- expect(res).toContain('Plan marked as Implemented');
19
- const content = await fs.readFile(planPath, 'utf8');
20
- expect(content).toContain('> Status: Implemented');
21
- });
22
- it('should fail to complete a plan if unchecked items exist', async () => {
23
- const planPath = path.join(tempDir, 'plan2.md');
24
- await fs.writeFile(planPath, '> Status: Draft\n\n- [ ] Pending item');
25
- const res = await completePlan('plan2.md', false, tempDir);
26
- expect(res).toContain('unchecked acceptance criteria found');
27
- });
28
- it('should force complete a plan with unchecked items', async () => {
29
- const planPath = path.join(tempDir, 'plan3.md');
30
- await fs.writeFile(planPath, '> Status: Draft\n\n- [ ] Pending item');
31
- const res = await completePlan('plan3.md', true, tempDir);
32
- expect(res).toContain('Plan marked as Implemented');
33
- });
34
- it('should validate architecture counts', async () => {
35
- const archDir = path.join(tempDir, 'docs', 'architecture');
36
- await fs.mkdir(archDir, { recursive: true });
37
- await fs.writeFile(path.join(archDir, 'compound-system.md'), '---\nskills: 1\nworkflows: 0\nscripts: 0\npatterns: 0\n---');
38
- const res = await validateArchitecture(tempDir);
39
- expect(res).toContain('Architecture Document is stale!');
40
- expect(res).toContain('Skills mismatch');
41
- });
42
- });
@@ -1,60 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { compoundSearch, updateSolutionRef, validateCompound, auditStateDrift } from '../compoundTools.js';
3
- import * as fs from 'fs/promises';
4
- import * as path from 'path';
5
- import * as os from 'os';
6
- describe('Compound Tools', () => {
7
- let tempDir;
8
- beforeEach(async () => {
9
- tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'compound-test-'));
10
- });
11
- afterEach(async () => {
12
- await fs.rm(tempDir, { recursive: true, force: true });
13
- });
14
- it('should search for solutions', async () => {
15
- const solDir = path.join(tempDir, 'docs', 'solutions');
16
- await fs.mkdir(solDir, { recursive: true });
17
- await fs.writeFile(path.join(solDir, 'test.md'), '# Test Solution\n\nThis mentions React and Next.js.');
18
- await fs.writeFile(path.join(solDir, 'test2.md'), '# Other Solution\n\nThis mentions Vue.');
19
- const res = await compoundSearch(['React'], tempDir);
20
- expect(res).toContain('test.md');
21
- expect(res).toContain('Test Solution');
22
- expect(res).not.toContain('test2.md');
23
- });
24
- it('should update solution ref', async () => {
25
- const solDir = path.join(tempDir, 'docs', 'solutions');
26
- await fs.mkdir(solDir, { recursive: true });
27
- const filePath = path.join(solDir, 'test.md');
28
- await fs.writeFile(filePath, '---\ntags: [test]\n---\n# Test Solution');
29
- const res = await updateSolutionRef([path.relative(tempDir, filePath)], tempDir);
30
- expect(res).toContain('Updated 1 files');
31
- const content = await fs.readFile(filePath, 'utf-8');
32
- expect(content).toContain('last_referenced:');
33
- });
34
- it('should validate compound health', async () => {
35
- const planPath = path.join(tempDir, 'implementation_plan.md');
36
- await fs.writeFile(planPath, '- [ ] todo 1\n- [ ] todo 2\n');
37
- const res = await validateCompound(tempDir);
38
- expect(res).toContain('⚠️ Found 2 unchecked items in implementation_plan.md');
39
- expect(res).toContain('❌ Validation failed.');
40
- });
41
- it('should audit state drift and report drift', async () => {
42
- const todosDir = path.join(tempDir, 'todos');
43
- await fs.mkdir(todosDir, { recursive: true });
44
- const todoPath = path.join(todosDir, '001-pending-p1-test.md');
45
- await fs.writeFile(todoPath, 'status: pending\n\n- [x] tick 1\n- [x] tick 2');
46
- const res = await auditStateDrift(tempDir, false);
47
- expect(res).toContain('DRIFT: 001-pending-p1-test.md');
48
- expect(res).toContain('Checked: 2/2');
49
- });
50
- it('should audit state drift and fix drift', async () => {
51
- const todosDir = path.join(tempDir, 'todos');
52
- await fs.mkdir(todosDir, { recursive: true });
53
- const todoPath = path.join(todosDir, '001-pending-p1-test.md');
54
- await fs.writeFile(todoPath, 'status: pending\n\n- [x] tick 1\n- [x] tick 2');
55
- const res = await auditStateDrift(tempDir, true);
56
- expect(res).toContain('FIXED: 001-pending-p1-test.md');
57
- const content = await fs.readFile(todoPath, 'utf8');
58
- expect(content).toContain('status: done');
59
- });
60
- });
@@ -1,44 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { bootstrapFolderDocs, checkDocsFreshness, discoverUndocumentedFolders, validateFolderDocs } from '../docsTools.js';
3
- import * as fs from 'fs/promises';
4
- import * as path from 'path';
5
- import * as os from 'os';
6
- describe('Docs Tools', () => {
7
- let tempDir;
8
- beforeEach(async () => {
9
- tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'docs-test-'));
10
- });
11
- afterEach(async () => {
12
- await fs.rm(tempDir, { recursive: true, force: true });
13
- });
14
- it('should bootstrap folder docs', async () => {
15
- const targetDir = path.join(tempDir, 'src');
16
- await fs.mkdir(targetDir, { recursive: true });
17
- await fs.writeFile(path.join(targetDir, 'utils.ts'), 'content');
18
- const result = await bootstrapFolderDocs('src', tempDir);
19
- expect(result).toContain('Bootstrapped');
20
- const readme = await fs.readFile(path.join(targetDir, 'README.md'), 'utf8');
21
- expect(readme).toContain('# src');
22
- expect(readme).toContain('`utils.ts`');
23
- });
24
- it('should discover undocumented folders', async () => {
25
- const srcDir = path.join(tempDir, 'src');
26
- await fs.mkdir(srcDir, { recursive: true });
27
- await fs.writeFile(path.join(srcDir, 'logic.ts'), 'content');
28
- // no readme -> should be discovered
29
- const result = await discoverUndocumentedFolders(tempDir);
30
- expect(result).toContain('src');
31
- });
32
- it('should validate folder docs', async () => {
33
- const targetDir = path.join(tempDir, 'src');
34
- await fs.mkdir(targetDir, { recursive: true });
35
- await fs.writeFile(path.join(targetDir, 'README.md'), '# src\n');
36
- const result = await validateFolderDocs(false, ['src'], tempDir);
37
- expect(result).toContain('missing sections: Purpose, Components, Component Details, Changelog');
38
- });
39
- it('should check docs freshness without failing', async () => {
40
- // Just tests the skip-docs flag since git repo isn't present
41
- const result = await checkDocsFreshness(true, tempDir);
42
- expect(result).toContain('Skipping documentation freshness check');
43
- });
44
- });
@@ -1,45 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { validateChangelog, archiveCompleted, prePushHousekeeping } from '../gitTools.js';
3
- import * as fs from 'fs/promises';
4
- import * as path from 'path';
5
- import * as os from 'os';
6
- describe('Git Tools', () => {
7
- let tempDir;
8
- beforeEach(async () => {
9
- tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'git-test-'));
10
- });
11
- afterEach(async () => {
12
- await fs.rm(tempDir, { recursive: true, force: true });
13
- });
14
- it('should validate changelog', async () => {
15
- const changelogPath = path.join(tempDir, 'CHANGELOG.md');
16
- await fs.writeFile(changelogPath, '## [Unreleased]\n## [Unreleased]\n<<<<<<< HEAD');
17
- const result = await validateChangelog(tempDir);
18
- expect(result).toContain('Multiple [Unreleased] sections found');
19
- expect(result).toContain('Merge conflict markers found');
20
- });
21
- it('should archive completed items in dry-run mode', async () => {
22
- await fs.mkdir(path.join(tempDir, 'todos'), { recursive: true });
23
- const todoPath = path.join(tempDir, 'todos', '001-done-test.md');
24
- await fs.writeFile(todoPath, 'content');
25
- const res = await archiveCompleted(tempDir, false);
26
- expect(res).toContain('DRY-RUN');
27
- expect(res).toContain('[ARCHIVED] 001-done-test.md');
28
- // Still exists
29
- await fs.access(todoPath);
30
- });
31
- it('should archive completed items and move them', async () => {
32
- await fs.mkdir(path.join(tempDir, 'todos'), { recursive: true });
33
- const todoPath = path.join(tempDir, 'todos', '001-done-test.md');
34
- await fs.writeFile(todoPath, 'content');
35
- const res = await archiveCompleted(tempDir, true);
36
- expect(res).toContain('APPLYING CHANGES');
37
- const archivedPath = path.join(tempDir, 'todos', 'archive', '001-done-test.md');
38
- await fs.access(archivedPath); // Should not throw
39
- });
40
- it('should run pre-push housekeeping', async () => {
41
- const res = await prePushHousekeeping(tempDir, false);
42
- expect(res).toContain('Pre-Push Housekeeping Check');
43
- expect(res).toContain('All checks passed');
44
- });
45
- });
@@ -1,74 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { logSkill, logWorkflow, rotateLogs } from '../loggerTools.js';
3
- import * as fs from 'fs/promises';
4
- import * as path from 'path';
5
- import * as os from 'os';
6
- describe('Logger Tools', () => {
7
- let tempDir;
8
- beforeEach(async () => {
9
- tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'logger-test-'));
10
- });
11
- afterEach(async () => {
12
- await fs.rm(tempDir, { recursive: true, force: true });
13
- vi.restoreAllMocks();
14
- });
15
- it('should log skill usage', async () => {
16
- vi.useFakeTimers();
17
- vi.setSystemTime(new Date('2026-03-04T00:00:00Z'));
18
- const result = await logSkill('test-skill', 'manual', 'context', tempDir);
19
- expect(result).toBe('Successfully logged skill usage for test-skill');
20
- const logFile = path.join(tempDir, 'docs', 'agents', 'logs', 'skill_usage.log');
21
- const content = await fs.readFile(logFile, 'utf-8');
22
- expect(content).toContain('2026-03-04T00:00:00Z|test-skill|manual|context\n');
23
- vi.useRealTimers();
24
- });
25
- it('should log workflow usage', async () => {
26
- vi.useFakeTimers();
27
- vi.setSystemTime(new Date('2026-03-04T00:00:00Z'));
28
- const result = await logWorkflow('test-workflow', 'session-123', tempDir);
29
- expect(result).toBe('Successfully logged workflow usage for test-workflow');
30
- const logFile = path.join(tempDir, 'docs', 'agents', 'logs', 'workflow_usage.log');
31
- const content = await fs.readFile(logFile, 'utf-8');
32
- expect(content).toContain('2026-03-04T00:00:00Z|test-workflow|session-123\n');
33
- vi.useRealTimers();
34
- });
35
- it('should generate a default session id if not provided', async () => {
36
- vi.useFakeTimers();
37
- vi.setSystemTime(new Date('2026-03-04T00:00:00Z')); // 1772582400 in seconds
38
- await logWorkflow('test-workflow', '', tempDir);
39
- const logFile = path.join(tempDir, 'docs', 'agents', 'logs', 'workflow_usage.log');
40
- const content = await fs.readFile(logFile, 'utf-8');
41
- expect(content).toContain('2026-03-04T00:00:00Z|test-workflow|1772582400\n');
42
- vi.useRealTimers();
43
- });
44
- it('should rotate logs older than retention days', async () => {
45
- const logDir = path.join(tempDir, 'docs', 'agents', 'logs');
46
- await fs.mkdir(logDir, { recursive: true });
47
- // Old log line (e.g. 100 days old)
48
- const oldTimestamp = new Date();
49
- oldTimestamp.setDate(oldTimestamp.getDate() - 100);
50
- const oldStr = oldTimestamp.toISOString().replace(/\.[0-9]{3}Z$/, 'Z');
51
- // New log line (10 days old)
52
- const newTimestamp = new Date();
53
- newTimestamp.setDate(newTimestamp.getDate() - 10);
54
- const newStr = newTimestamp.toISOString().replace(/\.[0-9]{3}Z$/, 'Z');
55
- const logContent = `${oldStr}|test|manual|context1\n${newStr}|test|manual|context2\n`;
56
- await fs.writeFile(path.join(logDir, 'skill_usage.log'), logContent);
57
- const output = await rotateLogs(tempDir, 90);
58
- expect(output).toContain('Pruned 1 lines');
59
- const finalContent = await fs.readFile(path.join(logDir, 'skill_usage.log'), 'utf-8');
60
- expect(finalContent).not.toContain('context1');
61
- expect(finalContent).toContain('context2');
62
- });
63
- it('should report if no logs need rotation', async () => {
64
- const logDir = path.join(tempDir, 'docs', 'agents', 'logs');
65
- await fs.mkdir(logDir, { recursive: true });
66
- const newTimestamp = new Date();
67
- newTimestamp.setDate(newTimestamp.getDate() - 10);
68
- const newStr = newTimestamp.toISOString().replace(/\.[0-9]{3}Z$/, 'Z');
69
- const logContent = `${newStr}|test|manual|context2\n`;
70
- await fs.writeFile(path.join(logDir, 'skill_usage.log'), logContent);
71
- const output = await rotateLogs(tempDir, 90);
72
- expect(output).toContain('(No logs needed rotation)');
73
- });
74
- });
@@ -1,73 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { createTodo, startTodo, doneTodo, getNextTodoId } from '../todoTools.js';
3
- import * as fs from 'fs/promises';
4
- import * as path from 'path';
5
- import * as os from 'os';
6
- describe('Todo Tools', () => {
7
- let tempDir;
8
- beforeEach(async () => {
9
- tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'todo-test-'));
10
- });
11
- afterEach(async () => {
12
- await fs.rm(tempDir, { recursive: true, force: true });
13
- });
14
- it('should calculate next todo ID correctly', async () => {
15
- const id1 = await getNextTodoId(tempDir);
16
- expect(id1).toBe('001');
17
- await fs.mkdir(path.join(tempDir, 'todos'), { recursive: true });
18
- await fs.writeFile(path.join(tempDir, 'todos', '005-pending-p2-test.md'), 'test');
19
- const id2 = await getNextTodoId(tempDir);
20
- expect(id2).toBe('006');
21
- });
22
- it('should create a new todo file', async () => {
23
- const result = await createTodo('p2', 'Fix Bug', 'The bug is terrible', ['Check 1', 'Check 2'], tempDir);
24
- expect(result).toContain('001-pending-p2-fix-bug');
25
- const files = await fs.readdir(path.join(tempDir, 'todos'));
26
- expect(files).toContain('001-pending-p2-fix-bug.md');
27
- const content = await fs.readFile(path.join(tempDir, 'todos', '001-pending-p2-fix-bug.md'), 'utf-8');
28
- expect(content).toContain('status: pending');
29
- expect(content).toContain('priority: p2');
30
- expect(content).toContain('- [ ] Check 1');
31
- });
32
- it('should start a todo file', async () => {
33
- await createTodo('p2', 'Fix Bug', 'The bug is terrible', ['Check 1'], tempDir);
34
- const todoFile = path.join('todos', '001-pending-p2-fix-bug.md');
35
- const result = await startTodo(todoFile, false, tempDir);
36
- expect(result).toContain('001-in-progress-p2-fix-bug.md');
37
- const newPath = path.join(tempDir, 'todos', '001-in-progress-p2-fix-bug.md');
38
- const content = await fs.readFile(newPath, 'utf-8');
39
- expect(content).toContain('status: in-progress');
40
- });
41
- it('should fail to done a todo if unchecked items exist', async () => {
42
- await createTodo('p2', 'Fix Bug', 'The bug is terrible', ['Check 1'], tempDir);
43
- // It's still pending so its name has pending
44
- const todoFile = path.join('todos', '001-pending-p2-fix-bug.md');
45
- await expect(doneTodo(todoFile, false, tempDir)).rejects.toThrow(/Unchecked items found/);
46
- });
47
- it('should done a todo if force is true with unchecked items', async () => {
48
- await createTodo('p2', 'Fix Bug', 'The bug is terrible', ['Check 1'], tempDir);
49
- const todoFile = path.join('todos', '001-pending-p2-fix-bug.md');
50
- const result = await doneTodo(todoFile, true, tempDir);
51
- expect(result).toContain('001-done-p2-fix-bug.md');
52
- });
53
- it('should done a todo if all items are checked', async () => {
54
- await createTodo('p2', 'Fix Bug', '', ['Check 1'], tempDir);
55
- const todoFile = path.normalize(path.join(tempDir, 'todos', '001-pending-p2-fix-bug.md'));
56
- let content = await fs.readFile(todoFile, 'utf-8');
57
- content = content.replace('- [ ] Check 1', '- [x] Check 1');
58
- await fs.writeFile(todoFile, content);
59
- const result = await doneTodo(path.join('todos', '001-pending-p2-fix-bug.md'), false, tempDir);
60
- expect(result).toContain('001-done-p2-fix-bug.md');
61
- const newPath = path.join(tempDir, 'todos', '001-done-p2-fix-bug.md');
62
- const newContent = await fs.readFile(newPath, 'utf-8');
63
- expect(newContent).toContain('status: done');
64
- });
65
- it('should reject startTodo if in terminal state without force', async () => {
66
- await createTodo('p2', 'Fix Bug', '', [], tempDir);
67
- const todoFile = path.normalize(path.join(tempDir, 'todos', '001-pending-p2-fix-bug.md'));
68
- let content = await fs.readFile(todoFile, 'utf-8');
69
- content = content.replace('status: pending', 'status: done');
70
- await fs.writeFile(todoFile, content);
71
- await expect(startTodo(path.join('todos', '001-pending-p2-fix-bug.md'), false, tempDir)).rejects.toThrow(/terminal state/);
72
- });
73
- });
@@ -1,77 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { validatePrismaSchema } from '../schemaValidator.js';
3
- import { checkApiCode, checkOpenApiSpec } from '../apiValidator.js';
4
- import * as fs from 'fs/promises';
5
- vi.mock('fs/promises');
6
- describe('schemaValidator', () => {
7
- beforeEach(() => {
8
- vi.clearAllMocks();
9
- });
10
- describe('validatePrismaSchema', () => {
11
- it('should detect bad model names and missing fields', async () => {
12
- vi.mocked(fs.readFile).mockResolvedValue(`
13
- model user {
14
- name String
15
- }
16
- model Post {
17
- id String @id
18
- userId String
19
- createdAt DateTime
20
- }
21
- enum role { ADMIN, USER }
22
- `);
23
- const issues = await validatePrismaSchema('/mock.prisma');
24
- expect(issues.some(i => i.includes("Model 'user' should be PascalCase"))).toBe(true);
25
- expect(issues.some(i => i.includes("Enum 'role' should be PascalCase"))).toBe(true);
26
- expect(issues.some(i => i.includes("missing createdAt"))).toBe(true); // for user
27
- expect(issues.some(i => i.includes("adding @@index([userId])"))).toBe(true); // for Post
28
- });
29
- });
30
- });
31
- describe('apiValidator', () => {
32
- beforeEach(() => {
33
- vi.clearAllMocks();
34
- });
35
- describe('checkOpenApiSpec', () => {
36
- it('should validate openapi json', async () => {
37
- vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({
38
- openapi: "3.0.0",
39
- info: { title: "Test", version: "1", description: "Test API" },
40
- paths: {
41
- "/test": { get: { responses: { 200: {} }, description: "desc" } }
42
- }
43
- }));
44
- const res = await checkOpenApiSpec('api.json');
45
- expect(res.issues.length).toBe(0);
46
- });
47
- });
48
- describe('checkApiCode', () => {
49
- it('should detect missing api practices', async () => {
50
- vi.mocked(fs.readFile).mockResolvedValue(`
51
- function handler() {
52
- // no try, no status, no security check
53
- return "hello";
54
- }
55
- `);
56
- const res = await checkApiCode('route.ts');
57
- expect(res.issues.some(i => i.includes('No error handling'))).toBe(true);
58
- expect(res.issues.some(i => i.includes('No explicit HTTP status'))).toBe(true);
59
- expect(res.passed.length).toBe(0);
60
- });
61
- it('should pass good practices', async () => {
62
- vi.mocked(fs.readFile).mockResolvedValue(`
63
- import { z } from 'zod';
64
- function handler(req, res) {
65
- try {
66
- const jwtToken = "123";
67
- return res.status(200).send("hello");
68
- } catch(e) {}
69
- }
70
- `);
71
- const res = await checkApiCode('route.ts');
72
- expect(res.passed.some(i => i.includes('Error handling'))).toBe(true);
73
- expect(res.passed.some(i => i.includes('validation present'))).toBe(true);
74
- expect(res.passed.some(i => i.includes('status codes used'))).toBe(true);
75
- });
76
- });
77
- });
@@ -1,38 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { runConvertRules } from '../convertRules.js';
3
- import * as fs from 'fs/promises';
4
- vi.mock('fs/promises');
5
- describe('convertRules', () => {
6
- beforeEach(() => {
7
- vi.clearAllMocks();
8
- });
9
- it('should fail if the rules directory does not exist', async () => {
10
- vi.mocked(fs.stat).mockRejectedValue(new Error('ENOENT'));
11
- const res = await runConvertRules('.');
12
- expect(res.passed).toBe(false);
13
- expect(res.report).toContain('[ERROR] Rules directory not found');
14
- });
15
- it('should correctly parse frontmatter and generate rules', async () => {
16
- vi.mocked(fs.stat).mockResolvedValue({ isDirectory: () => true });
17
- vi.mocked(fs.readdir).mockResolvedValue(['async-waterfall.md']);
18
- vi.mocked(fs.readFile).mockResolvedValue(`---
19
- title: Waterfall check
20
- impact: HIGH
21
- tags: perf
22
- ---
23
- Content body of the rule here.`);
24
- vi.mocked(fs.mkdir).mockResolvedValue(undefined);
25
- vi.mocked(fs.writeFile).mockResolvedValue(undefined);
26
- const res = await runConvertRules('.');
27
- console.log("ACTUAL REPORT:", res.report);
28
- expect(res.passed).toBe(true);
29
- expect(res.report).toContain('Generated 8 section files from 1 rules');
30
- // ensure valid output creation
31
- expect(fs.writeFile).toHaveBeenCalled();
32
- const callArgs = vi.mocked(fs.writeFile).mock.calls[0];
33
- const writtenContent = callArgs[1];
34
- expect(writtenContent).toContain('## Rule 1.1: Waterfall check');
35
- expect(writtenContent).toContain('**Impact:** HIGH');
36
- expect(writtenContent).toContain('Content body of the rule here.');
37
- });
38
- });