tlc-claude-code 1.8.4 → 2.0.1
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/.claude/commands/tlc/bootstrap.md +77 -0
- package/.claude/commands/tlc/build.md +20 -6
- package/.claude/commands/tlc/recall.md +87 -0
- package/.claude/commands/tlc/remember.md +71 -0
- package/CLAUDE.md +84 -201
- package/dashboard-web/dist/assets/index-Uhc49PE-.css +1 -0
- package/dashboard-web/dist/assets/index-W36XHPC5.js +431 -0
- package/dashboard-web/dist/assets/index-W36XHPC5.js.map +1 -0
- package/dashboard-web/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/index.js +29 -4
- package/server/lib/bug-writer.js +204 -0
- package/server/lib/bug-writer.test.js +279 -0
- package/server/lib/claude-cascade.js +247 -0
- package/server/lib/claude-cascade.test.js +245 -0
- package/server/lib/context-injection.js +121 -0
- package/server/lib/context-injection.test.js +340 -0
- package/server/lib/conversation-chunker.js +320 -0
- package/server/lib/conversation-chunker.test.js +573 -0
- package/server/lib/embedding-client.js +160 -0
- package/server/lib/embedding-client.test.js +243 -0
- package/server/lib/global-config.js +198 -0
- package/server/lib/global-config.test.js +288 -0
- package/server/lib/inherited-search.js +184 -0
- package/server/lib/inherited-search.test.js +343 -0
- package/server/lib/memory-api.js +180 -0
- package/server/lib/memory-api.test.js +322 -0
- package/server/lib/memory-hooks-capture.test.js +350 -0
- package/server/lib/memory-hooks.js +101 -0
- package/server/lib/memory-inheritance.js +179 -0
- package/server/lib/memory-inheritance.test.js +360 -0
- package/server/lib/plan-parser.js +33 -7
- package/server/lib/plan-writer.js +196 -0
- package/server/lib/plan-writer.test.js +298 -0
- package/server/lib/project-scanner.js +267 -0
- package/server/lib/project-scanner.test.js +389 -0
- package/server/lib/project-status.js +302 -0
- package/server/lib/project-status.test.js +470 -0
- package/server/lib/projects-registry.js +237 -0
- package/server/lib/projects-registry.test.js +275 -0
- package/server/lib/recall-command.js +207 -0
- package/server/lib/recall-command.test.js +306 -0
- package/server/lib/remember-command.js +96 -0
- package/server/lib/remember-command.test.js +265 -0
- package/server/lib/rich-capture.js +221 -0
- package/server/lib/rich-capture.test.js +312 -0
- package/server/lib/roadmap-api.js +200 -0
- package/server/lib/roadmap-api.test.js +318 -0
- package/server/lib/semantic-recall.js +242 -0
- package/server/lib/semantic-recall.test.js +446 -0
- package/server/lib/setup-generator.js +315 -0
- package/server/lib/setup-generator.test.js +303 -0
- package/server/lib/test-inventory.js +112 -0
- package/server/lib/test-inventory.test.js +360 -0
- package/server/lib/vector-indexer.js +246 -0
- package/server/lib/vector-indexer.test.js +459 -0
- package/server/lib/vector-store.js +260 -0
- package/server/lib/vector-store.test.js +706 -0
- package/server/lib/workspace-api.js +811 -0
- package/server/lib/workspace-api.test.js +743 -0
- package/server/lib/workspace-bootstrap.js +164 -0
- package/server/lib/workspace-bootstrap.test.js +503 -0
- package/server/lib/workspace-context.js +129 -0
- package/server/lib/workspace-context.test.js +214 -0
- package/server/lib/workspace-detector.js +162 -0
- package/server/lib/workspace-detector.test.js +193 -0
- package/server/lib/workspace-init.js +307 -0
- package/server/lib/workspace-init.test.js +244 -0
- package/server/lib/workspace-snapshot.js +236 -0
- package/server/lib/workspace-snapshot.test.js +444 -0
- package/server/lib/workspace-watcher.js +162 -0
- package/server/lib/workspace-watcher.test.js +257 -0
- package/server/package-lock.json +552 -0
- package/server/package.json +4 -0
- package/dashboard-web/dist/assets/index-B1I_joSL.js +0 -393
- package/dashboard-web/dist/assets/index-B1I_joSL.js.map +0 -1
- package/dashboard-web/dist/assets/index-Trhg1C1Y.css +0 -1
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
10
10
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
11
11
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
12
|
-
<script type="module" crossorigin src="/assets/index-
|
|
13
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
12
|
+
<script type="module" crossorigin src="/assets/index-W36XHPC5.js"></script>
|
|
13
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Uhc49PE-.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
|
16
16
|
<div id="root"></div>
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -21,6 +21,9 @@ const chokidar = require('chokidar');
|
|
|
21
21
|
const { detectProject } = require('./lib/project-detector');
|
|
22
22
|
const { parsePlan, parseBugs } = require('./lib/plan-parser');
|
|
23
23
|
const { autoProvision, stopDatabase } = require('./lib/auto-database');
|
|
24
|
+
const { GlobalConfig } = require('./lib/global-config');
|
|
25
|
+
const { ProjectScanner } = require('./lib/project-scanner');
|
|
26
|
+
const { createWorkspaceRouter } = require('./lib/workspace-api');
|
|
24
27
|
const {
|
|
25
28
|
createUserStore,
|
|
26
29
|
createAuthMiddleware,
|
|
@@ -71,6 +74,16 @@ const wss = new WebSocketServer({ server });
|
|
|
71
74
|
app.use(express.json());
|
|
72
75
|
const cookieParser = require('cookie-parser');
|
|
73
76
|
app.use(cookieParser());
|
|
77
|
+
const cors = require('cors');
|
|
78
|
+
app.use(cors({ origin: true, credentials: true }));
|
|
79
|
+
|
|
80
|
+
// Workspace API
|
|
81
|
+
const globalConfig = new GlobalConfig();
|
|
82
|
+
const projectScanner = new ProjectScanner();
|
|
83
|
+
const workspaceRouter = createWorkspaceRouter({ globalConfig, projectScanner });
|
|
84
|
+
app.use('/api/workspace', workspaceRouter);
|
|
85
|
+
// Also mount project-level routes at /api/projects for per-project endpoints
|
|
86
|
+
app.use('/api', workspaceRouter);
|
|
74
87
|
|
|
75
88
|
// ============================================
|
|
76
89
|
// Authentication Setup
|
|
@@ -649,10 +662,22 @@ app.get('/api/project', (req, res) => {
|
|
|
649
662
|
const roadmapPath = path.join(PROJECT_DIR, '.planning', 'ROADMAP.md');
|
|
650
663
|
if (fs.existsSync(roadmapPath)) {
|
|
651
664
|
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
const
|
|
655
|
-
|
|
665
|
+
|
|
666
|
+
// Format 1: ## Phase N heading format
|
|
667
|
+
const headingPhases = content.match(/##\s+Phase\s+\d+/g) || [];
|
|
668
|
+
const headingCompleted = content.match(/##\s+Phase\s+\d+[^[]*\[x\]/gi) || [];
|
|
669
|
+
|
|
670
|
+
// Format 2: Table format | N | [Name](link) | status |
|
|
671
|
+
const tablePhases = content.match(/\|\s*\d+\s*\|\s*\[[^\]]+\][^\|]*\|\s*\w+\s*\|/g) || [];
|
|
672
|
+
const tableCompleted = (content.match(/\|\s*\d+\s*\|\s*\[[^\]]+\][^\|]*\|\s*(?:complete|done|verified)\s*\|/gi) || []);
|
|
673
|
+
|
|
674
|
+
if (headingPhases.length > 0) {
|
|
675
|
+
totalPhases = headingPhases.length;
|
|
676
|
+
completedPhases = headingCompleted.length;
|
|
677
|
+
} else if (tablePhases.length > 0) {
|
|
678
|
+
totalPhases = tablePhases.length;
|
|
679
|
+
completedPhases = tableCompleted.length;
|
|
680
|
+
}
|
|
656
681
|
}
|
|
657
682
|
|
|
658
683
|
// Calculate progress
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bug Writer - CRUD operations for bugs in BUGS.md files
|
|
3
|
+
*
|
|
4
|
+
* Provides functions to update bug status, content, and create new bugs.
|
|
5
|
+
* All writes are atomic (write to temp file, then rename).
|
|
6
|
+
*
|
|
7
|
+
* Uses dependency injection for fs to enable testability.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a bug writer with injected dependencies
|
|
12
|
+
* @param {object} deps
|
|
13
|
+
* @param {object} deps.fs - Node.js fs module (or mock)
|
|
14
|
+
* @returns {{ updateBugStatus, updateBugContent, createBug }}
|
|
15
|
+
*/
|
|
16
|
+
function createBugWriter({ fs }) {
|
|
17
|
+
/**
|
|
18
|
+
* Write content atomically: write to .tmp, then rename
|
|
19
|
+
*/
|
|
20
|
+
function atomicWrite(filePath, content) {
|
|
21
|
+
const tmpPath = filePath + '.tmp';
|
|
22
|
+
fs.writeFileSync(tmpPath, content, 'utf-8');
|
|
23
|
+
fs.renameSync(tmpPath, filePath);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Find all bug headings in BUGS.md content
|
|
28
|
+
*/
|
|
29
|
+
function findBugs(content) {
|
|
30
|
+
const bugs = [];
|
|
31
|
+
const regex = /###\s+(BUG-\d+):\s+(.+?)\s*\[(\w+)\]/g;
|
|
32
|
+
let match;
|
|
33
|
+
while ((match = regex.exec(content)) !== null) {
|
|
34
|
+
bugs.push({
|
|
35
|
+
id: match[1],
|
|
36
|
+
title: match[2].trim(),
|
|
37
|
+
status: match[3],
|
|
38
|
+
fullMatch: match[0],
|
|
39
|
+
index: match.index,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return bugs;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get the section of content belonging to a specific bug
|
|
47
|
+
*/
|
|
48
|
+
function getBugSection(content, bugIndex, bugs) {
|
|
49
|
+
const start = bugIndex;
|
|
50
|
+
// Find end: next bug heading or end of content
|
|
51
|
+
const afterStart = content.slice(start + 1);
|
|
52
|
+
const nextBugMatch = afterStart.match(/\n###\s+BUG-/);
|
|
53
|
+
const end = nextBugMatch ? start + 1 + nextBugMatch.index : content.length;
|
|
54
|
+
return { start, end, section: content.slice(start, end) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Update a bug's status in BUGS.md
|
|
59
|
+
* @param {string} bugsPath - Path to BUGS.md file
|
|
60
|
+
* @param {string} bugId - Bug ID (e.g., 'BUG-001')
|
|
61
|
+
* @param {string} newStatus - 'open' | 'fixed' | 'closed'
|
|
62
|
+
*/
|
|
63
|
+
function updateBugStatus(bugsPath, bugId, newStatus) {
|
|
64
|
+
const content = fs.readFileSync(bugsPath, 'utf-8');
|
|
65
|
+
const bugs = findBugs(content);
|
|
66
|
+
const bug = bugs.find((b) => b.id === bugId);
|
|
67
|
+
|
|
68
|
+
if (!bug) {
|
|
69
|
+
throw new Error(`Bug ${bugId} not found in ${bugsPath}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const newHeading = `### ${bugId}: ${bug.title} [${newStatus}]`;
|
|
73
|
+
const updated = content.replace(bug.fullMatch, newHeading);
|
|
74
|
+
|
|
75
|
+
atomicWrite(bugsPath, updated);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Update a bug's content (title, severity, description)
|
|
80
|
+
* @param {string} bugsPath - Path to BUGS.md file
|
|
81
|
+
* @param {string} bugId - Bug ID (e.g., 'BUG-001')
|
|
82
|
+
* @param {object} updates - { title?, severity?, description? }
|
|
83
|
+
*/
|
|
84
|
+
function updateBugContent(bugsPath, bugId, updates) {
|
|
85
|
+
let content = fs.readFileSync(bugsPath, 'utf-8');
|
|
86
|
+
const bugs = findBugs(content);
|
|
87
|
+
const bug = bugs.find((b) => b.id === bugId);
|
|
88
|
+
|
|
89
|
+
if (!bug) {
|
|
90
|
+
throw new Error(`Bug ${bugId} not found in ${bugsPath}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Update title
|
|
94
|
+
if (updates.title) {
|
|
95
|
+
const newHeading = `### ${bugId}: ${updates.title} [${bug.status}]`;
|
|
96
|
+
content = content.replace(bug.fullMatch, newHeading);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Update severity
|
|
100
|
+
if (updates.severity) {
|
|
101
|
+
const { start, end, section } = getBugSection(content, bug.index, bugs);
|
|
102
|
+
const newSection = section.replace(
|
|
103
|
+
/\*\*Severity:\*\*\s*\w+/,
|
|
104
|
+
`**Severity:** ${updates.severity}`
|
|
105
|
+
);
|
|
106
|
+
content = content.slice(0, start) + newSection + content.slice(end);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Update description - replace content after metadata lines
|
|
110
|
+
if (updates.description) {
|
|
111
|
+
// Re-find bug position after potential title/severity changes
|
|
112
|
+
const updatedBugs = findBugs(content);
|
|
113
|
+
const updatedBug = updatedBugs.find((b) => b.id === bugId);
|
|
114
|
+
if (updatedBug) {
|
|
115
|
+
const { start, end, section } = getBugSection(content, updatedBug.index, updatedBugs);
|
|
116
|
+
// Find the end of metadata (after **Reported:** line)
|
|
117
|
+
const lines = section.split('\n');
|
|
118
|
+
let descStart = -1;
|
|
119
|
+
for (let i = 0; i < lines.length; i++) {
|
|
120
|
+
if (lines[i].startsWith('**Reported:**') || lines[i].startsWith('**Severity:**')) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (i > 1 && lines[i].trim() === '') {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (i > 2 && lines[i].trim() !== '' && !lines[i].startsWith('**') && !lines[i].startsWith('###')) {
|
|
127
|
+
descStart = i;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (descStart >= 0) {
|
|
133
|
+
// Find end of description (before --- or next heading)
|
|
134
|
+
let descEnd = lines.length;
|
|
135
|
+
for (let i = descStart; i < lines.length; i++) {
|
|
136
|
+
if (lines[i].trim() === '---' || lines[i].startsWith('###')) {
|
|
137
|
+
descEnd = i;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Replace description lines
|
|
142
|
+
const newLines = [...lines.slice(0, descStart), updates.description, ...lines.slice(descEnd)];
|
|
143
|
+
const newSection = newLines.join('\n');
|
|
144
|
+
content = content.slice(0, start) + newSection + content.slice(end);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
atomicWrite(bugsPath, content);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Create a new bug in BUGS.md
|
|
154
|
+
* @param {string} bugsPath - Path to BUGS.md file
|
|
155
|
+
* @param {object} bugData - { title, severity, description, url?, screenshot? }
|
|
156
|
+
* @returns {{ id: string, title: string, status: string }}
|
|
157
|
+
*/
|
|
158
|
+
function createBug(bugsPath, bugData) {
|
|
159
|
+
let content;
|
|
160
|
+
try {
|
|
161
|
+
content = fs.readFileSync(bugsPath, 'utf-8');
|
|
162
|
+
} catch {
|
|
163
|
+
content = '# Bugs\n';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const bugs = findBugs(content);
|
|
167
|
+
const maxNum = bugs.reduce((max, b) => {
|
|
168
|
+
const num = parseInt(b.id.replace('BUG-', ''));
|
|
169
|
+
return num > max ? num : max;
|
|
170
|
+
}, 0);
|
|
171
|
+
const nextNum = maxNum + 1;
|
|
172
|
+
const bugId = `BUG-${String(nextNum).padStart(3, '0')}`;
|
|
173
|
+
|
|
174
|
+
const today = new Date().toISOString().split('T')[0];
|
|
175
|
+
|
|
176
|
+
const lines = [];
|
|
177
|
+
lines.push('');
|
|
178
|
+
lines.push(`### ${bugId}: ${bugData.title} [open]`);
|
|
179
|
+
lines.push('');
|
|
180
|
+
lines.push(`**Severity:** ${bugData.severity}`);
|
|
181
|
+
lines.push(`**Reported:** ${today}`);
|
|
182
|
+
if (bugData.url) {
|
|
183
|
+
lines.push(`**URL:** ${bugData.url}`);
|
|
184
|
+
}
|
|
185
|
+
if (bugData.screenshot) {
|
|
186
|
+
lines.push(`**Screenshot:** ${bugData.screenshot}`);
|
|
187
|
+
}
|
|
188
|
+
lines.push('');
|
|
189
|
+
lines.push(bugData.description);
|
|
190
|
+
lines.push('');
|
|
191
|
+
lines.push('---');
|
|
192
|
+
lines.push('');
|
|
193
|
+
|
|
194
|
+
content = content.trimEnd() + '\n' + lines.join('\n');
|
|
195
|
+
|
|
196
|
+
atomicWrite(bugsPath, content);
|
|
197
|
+
|
|
198
|
+
return { id: bugId, title: bugData.title, status: 'open' };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { updateBugStatus, updateBugContent, createBug };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = { createBugWriter };
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file bug-writer.test.js
|
|
3
|
+
* @description Tests for the Bug Writer module (Phase 76, Task 6).
|
|
4
|
+
*
|
|
5
|
+
* Tests the factory function `createBugWriter(deps)` which accepts injected
|
|
6
|
+
* dependencies (fs) and returns functions for updating bug status and content
|
|
7
|
+
* in BUGS.md files.
|
|
8
|
+
*
|
|
9
|
+
* TDD: RED phase — these tests are written BEFORE the implementation.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
12
|
+
import { createBugWriter } from './bug-writer.js';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Mock factories
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
function createMockFs(files = {}) {
|
|
19
|
+
const store = { ...files };
|
|
20
|
+
return {
|
|
21
|
+
existsSync: vi.fn((p) => p in store),
|
|
22
|
+
readFileSync: vi.fn((p) => {
|
|
23
|
+
if (p in store) return store[p];
|
|
24
|
+
throw new Error(`ENOENT: no such file or directory, open '${p}'`);
|
|
25
|
+
}),
|
|
26
|
+
writeFileSync: vi.fn((p, content) => {
|
|
27
|
+
store[p] = content;
|
|
28
|
+
}),
|
|
29
|
+
renameSync: vi.fn((src, dest) => {
|
|
30
|
+
if (src in store) {
|
|
31
|
+
store[dest] = store[src];
|
|
32
|
+
delete store[src];
|
|
33
|
+
}
|
|
34
|
+
}),
|
|
35
|
+
mkdirSync: vi.fn(),
|
|
36
|
+
_store: store,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Sample BUGS.md content
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
const SAMPLE_BUGS = `# Bugs
|
|
45
|
+
|
|
46
|
+
### BUG-001: Login page crashes on empty email [open]
|
|
47
|
+
|
|
48
|
+
**Severity:** high
|
|
49
|
+
**Reported:** 2026-02-10
|
|
50
|
+
|
|
51
|
+
Steps to reproduce:
|
|
52
|
+
1. Go to login page
|
|
53
|
+
2. Click submit without entering email
|
|
54
|
+
3. Page crashes with TypeError
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
### BUG-002: Dashboard loads slowly [fixed]
|
|
59
|
+
|
|
60
|
+
**Severity:** medium
|
|
61
|
+
**Reported:** 2026-02-08
|
|
62
|
+
|
|
63
|
+
The dashboard takes 5+ seconds to load.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
### BUG-003: Sidebar menu overlaps content on mobile [open]
|
|
68
|
+
|
|
69
|
+
**Severity:** low
|
|
70
|
+
**Reported:** 2026-02-12
|
|
71
|
+
|
|
72
|
+
On iPhone 12 the sidebar pushes content off screen.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Tests
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
describe('bug-writer', () => {
|
|
82
|
+
describe('updateBugStatus', () => {
|
|
83
|
+
it('changes [open] to [fixed] in heading', () => {
|
|
84
|
+
const mockFs = createMockFs({ '/project/BUGS.md': SAMPLE_BUGS });
|
|
85
|
+
const writer = createBugWriter({ fs: mockFs });
|
|
86
|
+
|
|
87
|
+
writer.updateBugStatus('/project/BUGS.md', 'BUG-001', 'fixed');
|
|
88
|
+
|
|
89
|
+
const updated = mockFs._store['/project/BUGS.md'];
|
|
90
|
+
expect(updated).toContain('### BUG-001: Login page crashes on empty email [fixed]');
|
|
91
|
+
expect(updated).not.toContain('BUG-001: Login page crashes on empty email [open]');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('changes [open] to [closed]', () => {
|
|
95
|
+
const mockFs = createMockFs({ '/project/BUGS.md': SAMPLE_BUGS });
|
|
96
|
+
const writer = createBugWriter({ fs: mockFs });
|
|
97
|
+
|
|
98
|
+
writer.updateBugStatus('/project/BUGS.md', 'BUG-003', 'closed');
|
|
99
|
+
|
|
100
|
+
const updated = mockFs._store['/project/BUGS.md'];
|
|
101
|
+
expect(updated).toContain('### BUG-003: Sidebar menu overlaps content on mobile [closed]');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('changes [fixed] to [open] (reopen)', () => {
|
|
105
|
+
const mockFs = createMockFs({ '/project/BUGS.md': SAMPLE_BUGS });
|
|
106
|
+
const writer = createBugWriter({ fs: mockFs });
|
|
107
|
+
|
|
108
|
+
writer.updateBugStatus('/project/BUGS.md', 'BUG-002', 'open');
|
|
109
|
+
|
|
110
|
+
const updated = mockFs._store['/project/BUGS.md'];
|
|
111
|
+
expect(updated).toContain('### BUG-002: Dashboard loads slowly [open]');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('preserves other bugs when updating one', () => {
|
|
115
|
+
const mockFs = createMockFs({ '/project/BUGS.md': SAMPLE_BUGS });
|
|
116
|
+
const writer = createBugWriter({ fs: mockFs });
|
|
117
|
+
|
|
118
|
+
writer.updateBugStatus('/project/BUGS.md', 'BUG-001', 'fixed');
|
|
119
|
+
|
|
120
|
+
const updated = mockFs._store['/project/BUGS.md'];
|
|
121
|
+
expect(updated).toContain('### BUG-002: Dashboard loads slowly [fixed]');
|
|
122
|
+
expect(updated).toContain('### BUG-003: Sidebar menu overlaps content on mobile [open]');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('writes atomically (temp file + rename)', () => {
|
|
126
|
+
const mockFs = createMockFs({ '/project/BUGS.md': SAMPLE_BUGS });
|
|
127
|
+
const writer = createBugWriter({ fs: mockFs });
|
|
128
|
+
|
|
129
|
+
writer.updateBugStatus('/project/BUGS.md', 'BUG-001', 'fixed');
|
|
130
|
+
|
|
131
|
+
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
|
|
132
|
+
expect.stringContaining('.tmp'),
|
|
133
|
+
expect.any(String),
|
|
134
|
+
'utf-8'
|
|
135
|
+
);
|
|
136
|
+
expect(mockFs.renameSync).toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('throws for invalid bug ID', () => {
|
|
140
|
+
const mockFs = createMockFs({ '/project/BUGS.md': SAMPLE_BUGS });
|
|
141
|
+
const writer = createBugWriter({ fs: mockFs });
|
|
142
|
+
|
|
143
|
+
expect(() => {
|
|
144
|
+
writer.updateBugStatus('/project/BUGS.md', 'BUG-999', 'fixed');
|
|
145
|
+
}).toThrow(/bug.*BUG-999.*not found/i);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('updateBugContent', () => {
|
|
150
|
+
it('updates bug title', () => {
|
|
151
|
+
const mockFs = createMockFs({ '/project/BUGS.md': SAMPLE_BUGS });
|
|
152
|
+
const writer = createBugWriter({ fs: mockFs });
|
|
153
|
+
|
|
154
|
+
writer.updateBugContent('/project/BUGS.md', 'BUG-001', {
|
|
155
|
+
title: 'Login crashes on empty form submission',
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const updated = mockFs._store['/project/BUGS.md'];
|
|
159
|
+
expect(updated).toContain('### BUG-001: Login crashes on empty form submission [open]');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('updates severity line', () => {
|
|
163
|
+
const mockFs = createMockFs({ '/project/BUGS.md': SAMPLE_BUGS });
|
|
164
|
+
const writer = createBugWriter({ fs: mockFs });
|
|
165
|
+
|
|
166
|
+
writer.updateBugContent('/project/BUGS.md', 'BUG-001', {
|
|
167
|
+
severity: 'critical',
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const updated = mockFs._store['/project/BUGS.md'];
|
|
171
|
+
expect(updated).toContain('**Severity:** critical');
|
|
172
|
+
// Verify the old severity is replaced, not duplicated
|
|
173
|
+
expect(updated.match(/\*\*Severity:\*\*/g)?.length).toBe(3); // 3 bugs, each has severity
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('updates description', () => {
|
|
177
|
+
const mockFs = createMockFs({ '/project/BUGS.md': SAMPLE_BUGS });
|
|
178
|
+
const writer = createBugWriter({ fs: mockFs });
|
|
179
|
+
|
|
180
|
+
writer.updateBugContent('/project/BUGS.md', 'BUG-002', {
|
|
181
|
+
description: 'Dashboard API calls take too long due to N+1 queries.',
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const updated = mockFs._store['/project/BUGS.md'];
|
|
185
|
+
expect(updated).toContain('Dashboard API calls take too long due to N+1 queries.');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('throws for invalid bug ID', () => {
|
|
189
|
+
const mockFs = createMockFs({ '/project/BUGS.md': SAMPLE_BUGS });
|
|
190
|
+
const writer = createBugWriter({ fs: mockFs });
|
|
191
|
+
|
|
192
|
+
expect(() => {
|
|
193
|
+
writer.updateBugContent('/project/BUGS.md', 'BUG-999', { title: 'New' });
|
|
194
|
+
}).toThrow(/bug.*BUG-999.*not found/i);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('createBug', () => {
|
|
199
|
+
it('appends new bug with correct format', () => {
|
|
200
|
+
const mockFs = createMockFs({ '/project/BUGS.md': SAMPLE_BUGS });
|
|
201
|
+
const writer = createBugWriter({ fs: mockFs });
|
|
202
|
+
|
|
203
|
+
const result = writer.createBug('/project/BUGS.md', {
|
|
204
|
+
title: 'Button color wrong on hover',
|
|
205
|
+
severity: 'low',
|
|
206
|
+
description: 'The primary button turns grey instead of blue on hover.',
|
|
207
|
+
url: 'http://localhost:3000/settings',
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const updated = mockFs._store['/project/BUGS.md'];
|
|
211
|
+
expect(updated).toContain('### BUG-004: Button color wrong on hover [open]');
|
|
212
|
+
expect(updated).toContain('**Severity:** low');
|
|
213
|
+
expect(updated).toContain('**URL:** http://localhost:3000/settings');
|
|
214
|
+
expect(updated).toContain('The primary button turns grey instead of blue on hover.');
|
|
215
|
+
expect(result.id).toBe('BUG-004');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('generates next bug ID correctly', () => {
|
|
219
|
+
const mockFs = createMockFs({ '/project/BUGS.md': SAMPLE_BUGS });
|
|
220
|
+
const writer = createBugWriter({ fs: mockFs });
|
|
221
|
+
|
|
222
|
+
const result = writer.createBug('/project/BUGS.md', {
|
|
223
|
+
title: 'New bug',
|
|
224
|
+
severity: 'medium',
|
|
225
|
+
description: 'Something is wrong.',
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(result.id).toBe('BUG-004');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('creates bug in empty BUGS.md', () => {
|
|
232
|
+
const mockFs = createMockFs({ '/project/BUGS.md': '# Bugs\n' });
|
|
233
|
+
const writer = createBugWriter({ fs: mockFs });
|
|
234
|
+
|
|
235
|
+
const result = writer.createBug('/project/BUGS.md', {
|
|
236
|
+
title: 'First bug',
|
|
237
|
+
severity: 'high',
|
|
238
|
+
description: 'The first reported bug.',
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const updated = mockFs._store['/project/BUGS.md'];
|
|
242
|
+
expect(updated).toContain('### BUG-001: First bug [open]');
|
|
243
|
+
expect(result.id).toBe('BUG-001');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('creates BUGS.md if it does not exist', () => {
|
|
247
|
+
const mockFs = createMockFs({});
|
|
248
|
+
const writer = createBugWriter({ fs: mockFs });
|
|
249
|
+
|
|
250
|
+
const result = writer.createBug('/project/BUGS.md', {
|
|
251
|
+
title: 'First bug ever',
|
|
252
|
+
severity: 'medium',
|
|
253
|
+
description: 'No BUGS.md existed.',
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
expect(mockFs._store['/project/BUGS.md']).toBeDefined();
|
|
257
|
+
expect(mockFs._store['/project/BUGS.md']).toContain('### BUG-001: First bug ever [open]');
|
|
258
|
+
expect(result.id).toBe('BUG-001');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('writes atomically', () => {
|
|
262
|
+
const mockFs = createMockFs({ '/project/BUGS.md': SAMPLE_BUGS });
|
|
263
|
+
const writer = createBugWriter({ fs: mockFs });
|
|
264
|
+
|
|
265
|
+
writer.createBug('/project/BUGS.md', {
|
|
266
|
+
title: 'New bug',
|
|
267
|
+
severity: 'low',
|
|
268
|
+
description: 'desc',
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
|
|
272
|
+
expect.stringContaining('.tmp'),
|
|
273
|
+
expect.any(String),
|
|
274
|
+
'utf-8'
|
|
275
|
+
);
|
|
276
|
+
expect(mockFs.renameSync).toHaveBeenCalled();
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
});
|