superkit-mcp-server 1.2.6 → 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.
- package/ARCHITECTURE.md +52 -3
- package/SUPERKIT.md +32 -2
- package/build/index.js +275 -22
- package/build/tools/ProjectAssets.js +177 -0
- package/package.json +4 -1
- package/build/__tests__/test_apply_prompt_args.js +0 -104
- package/build/tools/__tests__/archTools.test.js +0 -42
- package/build/tools/__tests__/compoundTools.test.js +0 -60
- package/build/tools/__tests__/docsTools.test.js +0 -44
- package/build/tools/__tests__/gitTools.test.js +0 -45
- package/build/tools/__tests__/loggerTools.test.js +0 -74
- package/build/tools/__tests__/todoTools.test.js +0 -73
- package/build/tools/validators/__tests__/apiSchema.test.js +0 -77
- package/build/tools/validators/__tests__/convertRules.test.js +0 -38
- package/build/tools/validators/__tests__/frontendDesign.test.js +0 -55
- package/build/tools/validators/__tests__/geoChecker.test.js +0 -45
- package/build/tools/validators/__tests__/i18nChecker.test.js +0 -32
- package/build/tools/validators/__tests__/lintRunner.test.js +0 -65
- package/build/tools/validators/__tests__/mobileAudit.test.js +0 -40
- package/build/tools/validators/__tests__/playwrightRunner.test.js +0 -55
- package/build/tools/validators/__tests__/reactPerformanceChecker.test.js +0 -49
- package/build/tools/validators/__tests__/securityScan.test.js +0 -42
- package/build/tools/validators/__tests__/seoChecker.test.js +0 -44
- package/build/tools/validators/__tests__/testRunner.test.js +0 -49
- package/build/tools/validators/__tests__/typeCoverage.test.js +0 -62
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superkit-mcp-server",
|
|
3
|
-
"version": "1.2.
|
|
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,104 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { apply_prompt_args } from '../index.js';
|
|
3
|
-
describe('apply_prompt_args', () => {
|
|
4
|
-
describe('{{args}} substitution', () => {
|
|
5
|
-
it('should replace {{args}} with the provided user args', () => {
|
|
6
|
-
const result = apply_prompt_args('Task: {{args}}', 'Add user authentication');
|
|
7
|
-
expect(result).toBe('Task: Add user authentication');
|
|
8
|
-
});
|
|
9
|
-
it('should replace all occurrences of {{args}}', () => {
|
|
10
|
-
const result = apply_prompt_args('{{args}} and also {{args}}', 'hello');
|
|
11
|
-
expect(result).toBe('hello and also hello');
|
|
12
|
-
});
|
|
13
|
-
it('should replace {{args}} with an empty string when no args are provided', () => {
|
|
14
|
-
const result = apply_prompt_args('Task: {{args}}', '');
|
|
15
|
-
expect(result).toBe('Task: ');
|
|
16
|
-
});
|
|
17
|
-
it('should replace {{args}} with an empty string when args is only whitespace', () => {
|
|
18
|
-
const result = apply_prompt_args('Task: {{args}}', ' ');
|
|
19
|
-
expect(result).toBe('Task: ');
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
describe('{{#if args}} ... {{/if}} block (no else branch)', () => {
|
|
23
|
-
it('should include the block content when args is provided', () => {
|
|
24
|
-
const template = '{{#if args}}Task: {{args}}{{/if}}';
|
|
25
|
-
const result = apply_prompt_args(template, 'Fix bug');
|
|
26
|
-
expect(result).toBe('Task: Fix bug');
|
|
27
|
-
});
|
|
28
|
-
it('should remove the block entirely when args is empty', () => {
|
|
29
|
-
const template = 'Before\n{{#if args}}Task: {{args}}{{/if}}\nAfter';
|
|
30
|
-
const result = apply_prompt_args(template, '');
|
|
31
|
-
expect(result).toBe('Before\n\nAfter');
|
|
32
|
-
});
|
|
33
|
-
it('should remove the block entirely when args is only whitespace', () => {
|
|
34
|
-
const template = '{{#if args}}Task: {{args}}{{/if}}';
|
|
35
|
-
const result = apply_prompt_args(template, ' ');
|
|
36
|
-
expect(result).toBe('');
|
|
37
|
-
});
|
|
38
|
-
it('should handle multiline block content', () => {
|
|
39
|
-
const template = '{{#if args}}\n# Task\n**Input:** {{args}}\n{{/if}}';
|
|
40
|
-
const result = apply_prompt_args(template, 'Do something');
|
|
41
|
-
expect(result).toBe('\n# Task\n**Input:** Do something\n');
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
describe('{{#if args}} ... {{else}} ... {{/if}} block', () => {
|
|
45
|
-
it('should render the if-block when args is provided', () => {
|
|
46
|
-
const template = '{{#if args}}Task: {{args}}{{else}}No task provided.{{/if}}';
|
|
47
|
-
const result = apply_prompt_args(template, 'Add login');
|
|
48
|
-
expect(result).toBe('Task: Add login');
|
|
49
|
-
});
|
|
50
|
-
it('should render the else-block when args is empty', () => {
|
|
51
|
-
const template = '{{#if args}}Task: {{args}}{{else}}No task provided.{{/if}}';
|
|
52
|
-
const result = apply_prompt_args(template, '');
|
|
53
|
-
expect(result).toBe('No task provided.');
|
|
54
|
-
});
|
|
55
|
-
it('should render the else-block when args is only whitespace', () => {
|
|
56
|
-
const template = '{{#if args}}Task: {{args}}{{else}}No task provided.{{/if}}';
|
|
57
|
-
const result = apply_prompt_args(template, ' ');
|
|
58
|
-
expect(result).toBe('No task provided.');
|
|
59
|
-
});
|
|
60
|
-
it('should handle multiline if and else blocks', () => {
|
|
61
|
-
const template = [
|
|
62
|
-
'{{#if args}}',
|
|
63
|
-
'# Running: {{args}}',
|
|
64
|
-
'Execute the task above.',
|
|
65
|
-
'{{else}}',
|
|
66
|
-
'# Usage',
|
|
67
|
-
'Provide a task to run.',
|
|
68
|
-
'{{/if}}',
|
|
69
|
-
].join('\n');
|
|
70
|
-
const with_args = apply_prompt_args(template, 'Build the feature');
|
|
71
|
-
expect(with_args).toContain('# Running: Build the feature');
|
|
72
|
-
expect(with_args).not.toContain('# Usage');
|
|
73
|
-
const without_args = apply_prompt_args(template, '');
|
|
74
|
-
expect(without_args).toContain('# Usage');
|
|
75
|
-
expect(without_args).not.toContain('# Running:');
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
describe('edge cases', () => {
|
|
79
|
-
it('should return the template unchanged when it has no placeholders', () => {
|
|
80
|
-
const template = '# Plan\nThis is a static template.';
|
|
81
|
-
const result = apply_prompt_args(template, 'some args');
|
|
82
|
-
expect(result).toBe(template);
|
|
83
|
-
});
|
|
84
|
-
it('should handle a template with both {{args}} and {{#if args}} blocks', () => {
|
|
85
|
-
const template = [
|
|
86
|
-
'{{#if args}}',
|
|
87
|
-
'**Task:** {{args}}',
|
|
88
|
-
'{{else}}',
|
|
89
|
-
'**Task:** (none)',
|
|
90
|
-
'{{/if}}',
|
|
91
|
-
'',
|
|
92
|
-
'Details: {{args}}',
|
|
93
|
-
].join('\n');
|
|
94
|
-
const result = apply_prompt_args(template, 'Refactor auth');
|
|
95
|
-
expect(result).toContain('**Task:** Refactor auth');
|
|
96
|
-
expect(result).toContain('Details: Refactor auth');
|
|
97
|
-
expect(result).not.toContain('(none)');
|
|
98
|
-
});
|
|
99
|
-
it('should handle empty template string', () => {
|
|
100
|
-
const result = apply_prompt_args('', 'some args');
|
|
101
|
-
expect(result).toBe('');
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
});
|
|
@@ -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
|
-
});
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { checkAccessibility } from '../accessibilityChecker.js';
|
|
3
|
-
import { runUxAudit } from '../uxAudit.js';
|
|
4
|
-
import * as fs from 'fs/promises';
|
|
5
|
-
vi.mock('fs/promises');
|
|
6
|
-
describe('accessibilityChecker', () => {
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
vi.clearAllMocks();
|
|
9
|
-
});
|
|
10
|
-
describe('checkAccessibility', () => {
|
|
11
|
-
it('should detect input without label and img without alt', async () => {
|
|
12
|
-
vi.mocked(fs.readFile).mockResolvedValue(`
|
|
13
|
-
<input type="text" name="bad" />
|
|
14
|
-
<button>Click me</button>
|
|
15
|
-
`);
|
|
16
|
-
const issues = await checkAccessibility('test.html');
|
|
17
|
-
expect(issues.some(i => i.includes('Input without label'))).toBe(true);
|
|
18
|
-
expect(issues.some(i => i.includes('Missing lang'))).toBe(false); // only checks if <html> exists
|
|
19
|
-
});
|
|
20
|
-
it('should pass good inputs', async () => {
|
|
21
|
-
vi.mocked(fs.readFile).mockResolvedValue(`
|
|
22
|
-
<html lang="en">
|
|
23
|
-
<input type="text" aria-label="Good" />
|
|
24
|
-
<button aria-label="Close">X</button>
|
|
25
|
-
</html>
|
|
26
|
-
`);
|
|
27
|
-
const issues = await checkAccessibility('test.html');
|
|
28
|
-
expect(issues).toEqual([]); // Skip link is only requested if <main> or <body> is present
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
describe('uxAudit', () => {
|
|
33
|
-
beforeEach(() => {
|
|
34
|
-
vi.clearAllMocks();
|
|
35
|
-
});
|
|
36
|
-
describe('runUxAudit', () => {
|
|
37
|
-
it('should detect UX violations', async () => {
|
|
38
|
-
vi.mocked(fs.readdir).mockResolvedValue([{
|
|
39
|
-
name: 'test.tsx',
|
|
40
|
-
isDirectory: () => false
|
|
41
|
-
}]);
|
|
42
|
-
vi.mocked(fs.readFile).mockResolvedValue(`
|
|
43
|
-
<button onClick={() => {}}>Submit</button>
|
|
44
|
-
<img src="foo.jpg">
|
|
45
|
-
<p style="color: #000000; font-family: purple;">Hello</p>
|
|
46
|
-
`);
|
|
47
|
-
const res = await runUxAudit('.');
|
|
48
|
-
expect(res.passed).toBe(false);
|
|
49
|
-
expect(res.report.includes('PURPLE DETECTED')).toBe(true);
|
|
50
|
-
expect(res.report.includes('Missing img alt text')).toBe(true);
|
|
51
|
-
expect(res.report.includes('Pure black')).toBe(true);
|
|
52
|
-
expect(res.report.includes('Interactive elements lack immediate feedback')).toBe(true);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
});
|