@gotza02/sequential-thinking 2026.2.9 → 2026.2.10

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/README.md CHANGED
@@ -201,4 +201,4 @@ You are a Senior AI Software Engineer equipped with the Sequential Thinking MCP
201
201
  ---
202
202
 
203
203
  ## License
204
- MIT - พัฒนาโดย @gotza02
204
+ MIT - พัฒนาโดย @gotza02/sequential-thinking
package/dist/codestore.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import * as fs from 'fs/promises';
2
2
  import * as path from 'path';
3
+ import { AsyncMutex } from './utils.js';
3
4
  export class CodeDatabase {
4
5
  filePath;
5
6
  db = { snippets: [], patterns: {} };
6
7
  loaded = false;
8
+ mutex = new AsyncMutex();
7
9
  constructor(storagePath = 'code_database.json') {
8
10
  this.filePath = path.resolve(storagePath);
9
11
  }
@@ -23,35 +25,45 @@ export class CodeDatabase {
23
25
  await fs.writeFile(this.filePath, JSON.stringify(this.db, null, 2), 'utf-8');
24
26
  }
25
27
  async addSnippet(snippet) {
26
- await this.load();
27
- const newSnippet = {
28
- ...snippet,
29
- id: Math.random().toString(36).substring(2, 9),
30
- updatedAt: new Date().toISOString()
31
- };
32
- this.db.snippets.push(newSnippet);
33
- await this.save();
34
- return newSnippet;
28
+ return this.mutex.dispatch(async () => {
29
+ await this.load();
30
+ const newSnippet = {
31
+ ...snippet,
32
+ id: Math.random().toString(36).substring(2, 9),
33
+ updatedAt: new Date().toISOString()
34
+ };
35
+ this.db.snippets.push(newSnippet);
36
+ await this.save();
37
+ return newSnippet;
38
+ });
35
39
  }
36
40
  async searchSnippets(query) {
37
- await this.load();
38
- const q = query.toLowerCase();
39
- return this.db.snippets.filter(s => s.title.toLowerCase().includes(q) ||
40
- s.description.toLowerCase().includes(q) ||
41
- s.tags.some(t => t.toLowerCase().includes(q)) ||
42
- s.code.toLowerCase().includes(q));
41
+ return this.mutex.dispatch(async () => {
42
+ await this.load();
43
+ const q = query.toLowerCase();
44
+ return this.db.snippets.filter(s => s.title.toLowerCase().includes(q) ||
45
+ s.description.toLowerCase().includes(q) ||
46
+ s.tags.some(t => t.toLowerCase().includes(q)) ||
47
+ s.code.toLowerCase().includes(q));
48
+ });
43
49
  }
44
50
  async learnPattern(name, description) {
45
- await this.load();
46
- this.db.patterns[name] = description;
47
- await this.save();
51
+ return this.mutex.dispatch(async () => {
52
+ await this.load();
53
+ this.db.patterns[name] = description;
54
+ await this.save();
55
+ });
48
56
  }
49
57
  async getPattern(name) {
50
- await this.load();
51
- return this.db.patterns[name] || null;
58
+ return this.mutex.dispatch(async () => {
59
+ await this.load();
60
+ return this.db.patterns[name] || null;
61
+ });
52
62
  }
53
63
  async listAllPatterns() {
54
- await this.load();
55
- return this.db.patterns;
64
+ return this.mutex.dispatch(async () => {
65
+ await this.load();
66
+ return this.db.patterns;
67
+ });
56
68
  }
57
69
  }
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
2
+ import { CodeDatabase } from './codestore.js';
3
+ import * as fs from 'fs/promises';
4
+ vi.mock('fs/promises');
5
+ describe('CodeDatabase', () => {
6
+ let db;
7
+ const testPath = 'test_code_db.json';
8
+ let mockStore = '{"snippets": [], "patterns": {}}';
9
+ beforeEach(() => {
10
+ mockStore = '{"snippets": [], "patterns": {}}';
11
+ fs.readFile.mockImplementation(async () => mockStore);
12
+ fs.writeFile.mockImplementation(async (path, data) => {
13
+ mockStore = data;
14
+ });
15
+ db = new CodeDatabase(testPath);
16
+ });
17
+ afterEach(() => {
18
+ vi.clearAllMocks();
19
+ });
20
+ it('should add a code snippet', async () => {
21
+ const snippet = await db.addSnippet({
22
+ title: "Quick Sort",
23
+ language: "typescript",
24
+ code: "function sort() {}",
25
+ description: "A sorting algorithm",
26
+ tags: ["algo"]
27
+ });
28
+ expect(snippet.id).toBeDefined();
29
+ expect(snippet.title).toBe("Quick Sort");
30
+ const stored = JSON.parse(mockStore);
31
+ expect(stored.snippets).toHaveLength(1);
32
+ });
33
+ it('should search snippets', async () => {
34
+ await db.addSnippet({
35
+ title: "React Hook",
36
+ language: "ts",
37
+ code: "const useX = () => {}",
38
+ description: "Custom hook",
39
+ tags: ["react"]
40
+ });
41
+ await db.addSnippet({
42
+ title: "Python Script",
43
+ language: "py",
44
+ code: "print('hello')",
45
+ description: "Hello world",
46
+ tags: ["python"]
47
+ });
48
+ const results = await db.searchSnippets("hook");
49
+ expect(results).toHaveLength(1);
50
+ expect(results[0].title).toBe("React Hook");
51
+ });
52
+ it('should learn and retrieve patterns', async () => {
53
+ await db.learnPattern("Repository Pattern", "Separate data access from business logic.");
54
+ const pattern = await db.getPattern("Repository Pattern");
55
+ expect(pattern).toContain("Separate data access");
56
+ const all = await db.listAllPatterns();
57
+ expect(all["Repository Pattern"]).toBeDefined();
58
+ });
59
+ });
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
2
+ import { validatePath } from './utils.js';
3
+ import * as path from 'path';
4
+ describe('FileSystem Security', () => {
5
+ // Mock process.cwd to be a known fixed path
6
+ const mockCwd = '/app/project';
7
+ // We need to spy on process.cwd and path.resolve behavior if we want to be strict,
8
+ // but validatePath uses path.resolve(requestedPath).
9
+ // Let's just mock path.resolve to behave as expected, or trust the real path module
10
+ // but ensure we set up the cwd check correctly.
11
+ // The easiest way is to mock process.cwd().
12
+ const originalCwd = process.cwd;
13
+ beforeEach(() => {
14
+ vi.spyOn(process, 'cwd').mockReturnValue(mockCwd);
15
+ });
16
+ afterEach(() => {
17
+ vi.restoreAllMocks();
18
+ });
19
+ it('should allow paths within the project root', () => {
20
+ const p = validatePath('src/index.ts');
21
+ expect(p).toBe(path.resolve(mockCwd, 'src/index.ts'));
22
+ });
23
+ it('should allow explicit ./ paths', () => {
24
+ const p = validatePath('./package.json');
25
+ expect(p).toBe(path.resolve(mockCwd, 'package.json'));
26
+ });
27
+ it('should block traversal to parent directory', () => {
28
+ expect(() => {
29
+ validatePath('../outside.txt');
30
+ }).toThrow(/Access denied/);
31
+ });
32
+ it('should block multiple level traversal', () => {
33
+ expect(() => {
34
+ validatePath('src/../../etc/passwd');
35
+ }).toThrow(/Access denied/);
36
+ });
37
+ it('should block absolute paths outside root', () => {
38
+ // path.resolve('/etc/passwd') returns '/etc/passwd' on linux-like
39
+ // path.resolve('C:/Windows') on windows...
40
+ // We rely on path.resolve behavior.
41
+ // If system is linux-like:
42
+ if (path.sep === '/') {
43
+ expect(() => {
44
+ validatePath('/etc/passwd');
45
+ }).toThrow(/Access denied/);
46
+ }
47
+ });
48
+ });
package/dist/graph.js CHANGED
@@ -103,6 +103,14 @@ export class ProjectKnowledgeGraph {
103
103
  }
104
104
  }
105
105
  }
106
+ // 4. Import Equals: import x = require('...')
107
+ else if (ts.isImportEqualsDeclaration(node)) {
108
+ if (ts.isExternalModuleReference(node.moduleReference)) {
109
+ if (ts.isStringLiteral(node.moduleReference.expression)) {
110
+ imports.push(node.moduleReference.expression.text);
111
+ }
112
+ }
113
+ }
106
114
  ts.forEachChild(node, visit);
107
115
  };
108
116
  visit(sourceFile);
@@ -121,13 +129,13 @@ export class ProjectKnowledgeGraph {
121
129
  // Basic Regex for generic symbols and imports
122
130
  if (ext === '.py') {
123
131
  // Python: import x, from x import y, def func, class Cls
124
- const importMatches = content.matchAll(/^(?:import|from)\s+([a-zA-Z0-9_.]+)/gm);
132
+ const importMatches = content.matchAll(/^\s*(?:import|from)\s+([a-zA-Z0-9_.]+)/gm);
125
133
  for (const match of importMatches)
126
134
  imports.push(match[1]);
127
- const funcMatches = content.matchAll(/^def\s+([a-zA-Z0-9_]+)/gm);
135
+ const funcMatches = content.matchAll(/^\s*def\s+([a-zA-Z0-9_]+)/gm);
128
136
  for (const match of funcMatches)
129
137
  symbols.push(`def:${match[1]}`);
130
- const classMatches = content.matchAll(/^class\s+([a-zA-Z0-9_]+)/gm);
138
+ const classMatches = content.matchAll(/^\s*class\s+([a-zA-Z0-9_]+)/gm);
131
139
  for (const match of classMatches)
132
140
  symbols.push(`class:${match[1]}`);
133
141
  }
@@ -136,7 +144,7 @@ export class ProjectKnowledgeGraph {
136
144
  const importMatches = content.matchAll(/import\s+"([^"]+)"/g);
137
145
  for (const match of importMatches)
138
146
  imports.push(match[1]);
139
- const funcMatches = content.matchAll(/^func\s+([a-zA-Z0-9_]+)/gm);
147
+ const funcMatches = content.matchAll(/^\s*func\s+([a-zA-Z0-9_]+)/gm);
140
148
  for (const match of funcMatches)
141
149
  symbols.push(`func:${match[1]}`);
142
150
  }
@@ -176,7 +184,7 @@ export class ProjectKnowledgeGraph {
176
184
  return absolutePath;
177
185
  }
178
186
  // 2. Try appending extensions
179
- const extensions = ['.ts', '.js', '.tsx', '.jsx', '.json', '/index.ts', '/index.js'];
187
+ const extensions = ['.ts', '.js', '.tsx', '.jsx', '.json', '.py', '.go', '.rs', '.java', '.c', '.cpp', '.h', '/index.ts', '/index.js'];
180
188
  for (const ext of extensions) {
181
189
  const p = absolutePath + ext;
182
190
  if (this.nodes.has(p)) {
@@ -76,4 +76,57 @@ describe('ProjectKnowledgeGraph', () => {
76
76
  const relationships = graph.getRelationships('/app/index.js');
77
77
  expect(relationships?.imports).toContain('Button.jsx');
78
78
  });
79
+ it('should handle circular dependencies', async () => {
80
+ const contentA = `import { b } from './b'; export const a = 1;`;
81
+ const contentB = `import { a } from './a'; export const b = 2;`;
82
+ fs.readdir.mockResolvedValue([
83
+ { name: 'a.ts', isDirectory: () => false },
84
+ { name: 'b.ts', isDirectory: () => false }
85
+ ]);
86
+ fs.readFile.mockImplementation(async (filePath) => {
87
+ if (filePath.endsWith('a.ts'))
88
+ return contentA;
89
+ if (filePath.endsWith('b.ts'))
90
+ return contentB;
91
+ return '';
92
+ });
93
+ await graph.build('/app');
94
+ const relA = graph.getRelationships('/app/a.ts');
95
+ const relB = graph.getRelationships('/app/b.ts');
96
+ expect(relA?.imports).toContain('b.ts');
97
+ expect(relA?.importedBy).toContain('b.ts');
98
+ expect(relB?.imports).toContain('a.ts');
99
+ expect(relB?.importedBy).toContain('a.ts');
100
+ });
101
+ it('should gracefully handle missing imports', async () => {
102
+ const contentA = `import { ghost } from './ghost';`;
103
+ fs.readdir.mockResolvedValue([
104
+ { name: 'a.ts', isDirectory: () => false }
105
+ ]);
106
+ fs.readFile.mockImplementation(async (filePath) => {
107
+ if (filePath.endsWith('a.ts'))
108
+ return contentA;
109
+ return '';
110
+ });
111
+ await graph.build('/app');
112
+ const relA = graph.getRelationships('/app/a.ts');
113
+ // ghost.ts doesn't exist, so imports should be empty (filtered out)
114
+ expect(relA?.imports).toHaveLength(0);
115
+ });
116
+ it('should ignore directory traversal attempts outside root', async () => {
117
+ // If we pretend root is /app, and we try to import ../outside
118
+ const contentA = `import { secret } from '../secret';`;
119
+ fs.readdir.mockResolvedValue([
120
+ { name: 'a.ts', isDirectory: () => false }
121
+ ]);
122
+ fs.readFile.mockImplementation(async (filePath) => {
123
+ if (filePath.endsWith('a.ts'))
124
+ return contentA;
125
+ return '';
126
+ });
127
+ await graph.build('/app');
128
+ const relA = graph.getRelationships('/app/a.ts');
129
+ // Should be empty because '../secret' is not in the scanned file list (nodes)
130
+ expect(relA?.imports).toHaveLength(0);
131
+ });
79
132
  });
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ // We need to import the register functions to get the callbacks,
3
+ // but we can just use the classes directly for this integration logic test.
4
+ // Actually, testing the *interaction* via the tool layer is better.
5
+ import { registerThinkingTools } from './tools/thinking.js';
6
+ import { registerGraphTools } from './tools/graph.js';
7
+ import { registerNoteTools } from './tools/notes.js';
8
+ describe('Integration Workflow', () => {
9
+ let toolCallbacks = {};
10
+ const mockServer = {
11
+ tool: (name, desc, schema, cb) => {
12
+ toolCallbacks[name] = cb;
13
+ }
14
+ };
15
+ // Mocks
16
+ const mockThinking = { processThought: vi.fn(), clearHistory: vi.fn() };
17
+ const mockGraph = { build: vi.fn(), getRelationships: vi.fn() };
18
+ const mockNotes = { addNote: vi.fn() };
19
+ beforeEach(() => {
20
+ toolCallbacks = {};
21
+ registerThinkingTools(mockServer, mockThinking);
22
+ registerGraphTools(mockServer, mockGraph);
23
+ registerNoteTools(mockServer, mockNotes);
24
+ vi.clearAllMocks();
25
+ });
26
+ it('should support a full analysis workflow', async () => {
27
+ // Step 1: Start Thinking
28
+ mockThinking.processThought.mockResolvedValue({
29
+ content: [{ type: "text", text: "Thought processed" }]
30
+ });
31
+ await toolCallbacks['sequentialthinking']({
32
+ thought: "Analyze architecture",
33
+ thoughtNumber: 1,
34
+ totalThoughts: 5,
35
+ nextThoughtNeeded: true,
36
+ thoughtType: 'analysis'
37
+ });
38
+ expect(mockThinking.processThought).toHaveBeenCalledWith(expect.objectContaining({
39
+ thought: "Analyze architecture"
40
+ }));
41
+ // Step 2: Build Graph
42
+ mockGraph.build.mockResolvedValue({ nodeCount: 10, totalFiles: 20 });
43
+ await toolCallbacks['build_project_graph']({ path: '.' });
44
+ expect(mockGraph.build).toHaveBeenCalled();
45
+ // Step 3: Get Relationships
46
+ mockGraph.getRelationships.mockReturnValue({ imports: ['utils.ts'] });
47
+ const relResult = await toolCallbacks['get_file_relationships']({ filePath: 'src/index.ts' });
48
+ expect(JSON.parse(relResult.content[0].text).imports).toContain('utils.ts');
49
+ // Step 4: Add Note
50
+ mockNotes.addNote.mockResolvedValue({ id: '123', title: 'Arch Note' });
51
+ await toolCallbacks['manage_notes']({
52
+ action: 'add',
53
+ title: 'Architecture Review',
54
+ content: 'Found circular deps'
55
+ });
56
+ expect(mockNotes.addNote).toHaveBeenCalledWith('Architecture Review', 'Found circular deps', undefined, undefined, undefined);
57
+ });
58
+ });
package/dist/lib.js CHANGED
@@ -9,6 +9,7 @@ export class SequentialThinkingServer {
9
9
  storagePath;
10
10
  delayMs;
11
11
  isSaving = false;
12
+ hasPendingSave = false;
12
13
  constructor(storagePath = 'thoughts_history.json', delayMs = 0) {
13
14
  this.disableThoughtLogging = (process.env.DISABLE_THOUGHT_LOGGING || "").toLowerCase() === "true";
14
15
  this.storagePath = path.resolve(storagePath);
@@ -33,8 +34,7 @@ export class SequentialThinkingServer {
33
34
  }
34
35
  async saveHistory() {
35
36
  if (this.isSaving) {
36
- // Simple retry if already saving
37
- setTimeout(() => this.saveHistory(), 100);
37
+ this.hasPendingSave = true;
38
38
  return;
39
39
  }
40
40
  this.isSaving = true;
@@ -49,6 +49,10 @@ export class SequentialThinkingServer {
49
49
  }
50
50
  finally {
51
51
  this.isSaving = false;
52
+ if (this.hasPendingSave) {
53
+ this.hasPendingSave = false;
54
+ this.saveHistory();
55
+ }
52
56
  }
53
57
  }
54
58
  async clearHistory() {
@@ -70,9 +74,30 @@ export class SequentialThinkingServer {
70
74
  // Remove the range and insert summary
71
75
  const removedCount = endIndex - startIndex + 1;
72
76
  this.thoughtHistory.splice(startIndex - 1, removedCount, summaryThought);
73
- // Renumber subsequent thoughts
77
+ // Renumber subsequent thoughts and update references
78
+ const shiftAmount = removedCount - 1;
74
79
  for (let i = startIndex; i < this.thoughtHistory.length; i++) {
75
- this.thoughtHistory[i].thoughtNumber -= (removedCount - 1);
80
+ const t = this.thoughtHistory[i];
81
+ // Update own number
82
+ t.thoughtNumber -= shiftAmount;
83
+ // Update references (branchFromThought)
84
+ if (t.branchFromThought) {
85
+ if (t.branchFromThought > endIndex) {
86
+ t.branchFromThought -= shiftAmount;
87
+ }
88
+ else if (t.branchFromThought >= startIndex) {
89
+ t.branchFromThought = startIndex; // Point to summary
90
+ }
91
+ }
92
+ // Update references (revisesThought)
93
+ if (t.revisesThought) {
94
+ if (t.revisesThought > endIndex) {
95
+ t.revisesThought -= shiftAmount;
96
+ }
97
+ else if (t.revisesThought >= startIndex) {
98
+ t.revisesThought = startIndex; // Point to summary
99
+ }
100
+ }
76
101
  }
77
102
  // Rebuild branches (simplification: clear and let it rebuild if needed, or just clear)
78
103
  this.branches = {};
package/dist/notes.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import * as fs from 'fs/promises';
2
2
  import * as path from 'path';
3
+ import { AsyncMutex } from './utils.js';
3
4
  export class NotesManager {
4
5
  filePath;
5
6
  notes = [];
6
7
  loaded = false;
8
+ mutex = new AsyncMutex();
7
9
  constructor(storagePath = 'project_notes.json') {
8
10
  this.filePath = path.resolve(storagePath);
9
11
  }
@@ -24,64 +26,74 @@ export class NotesManager {
24
26
  await fs.writeFile(this.filePath, JSON.stringify(this.notes, null, 2), 'utf-8');
25
27
  }
26
28
  async addNote(title, content, tags = [], priority = 'medium', expiresAt) {
27
- await this.load();
28
- const note = {
29
- id: Date.now().toString(36) + Math.random().toString(36).substring(2, 7),
30
- title,
31
- content,
32
- tags,
33
- priority,
34
- expiresAt,
35
- createdAt: new Date().toISOString(),
36
- updatedAt: new Date().toISOString()
37
- };
38
- this.notes.push(note);
39
- await this.save();
40
- return note;
29
+ return this.mutex.dispatch(async () => {
30
+ await this.load();
31
+ const note = {
32
+ id: Date.now().toString(36) + Math.random().toString(36).substring(2, 7),
33
+ title,
34
+ content,
35
+ tags,
36
+ priority,
37
+ expiresAt,
38
+ createdAt: new Date().toISOString(),
39
+ updatedAt: new Date().toISOString()
40
+ };
41
+ this.notes.push(note);
42
+ await this.save();
43
+ return note;
44
+ });
41
45
  }
42
46
  async listNotes(tag, includeExpired = false) {
43
- await this.load();
44
- const now = new Date();
45
- let activeNotes = this.notes;
46
- if (!includeExpired) {
47
- activeNotes = this.notes.filter(n => !n.expiresAt || new Date(n.expiresAt) > now);
48
- }
49
- if (tag) {
50
- return activeNotes.filter(n => n.tags.includes(tag));
51
- }
52
- return activeNotes.sort((a, b) => {
53
- const priorityMap = { 'critical': 4, 'high': 3, 'medium': 2, 'low': 1 };
54
- return (priorityMap[b.priority] || 0) - (priorityMap[a.priority] || 0);
47
+ return this.mutex.dispatch(async () => {
48
+ await this.load();
49
+ const now = new Date();
50
+ let activeNotes = this.notes;
51
+ if (!includeExpired) {
52
+ activeNotes = this.notes.filter(n => !n.expiresAt || new Date(n.expiresAt) > now);
53
+ }
54
+ if (tag) {
55
+ return activeNotes.filter(n => n.tags.includes(tag));
56
+ }
57
+ return activeNotes.sort((a, b) => {
58
+ const priorityMap = { 'critical': 4, 'high': 3, 'medium': 2, 'low': 1 };
59
+ return (priorityMap[b.priority] || 0) - (priorityMap[a.priority] || 0);
60
+ });
55
61
  });
56
62
  }
57
63
  async searchNotes(query) {
58
- await this.load();
59
- const lowerQuery = query.toLowerCase();
60
- return this.notes.filter(n => n.title.toLowerCase().includes(lowerQuery) ||
61
- n.content.toLowerCase().includes(lowerQuery) ||
62
- n.tags.some(t => t.toLowerCase().includes(lowerQuery)));
64
+ return this.mutex.dispatch(async () => {
65
+ await this.load();
66
+ const lowerQuery = query.toLowerCase();
67
+ return this.notes.filter(n => n.title.toLowerCase().includes(lowerQuery) ||
68
+ n.content.toLowerCase().includes(lowerQuery) ||
69
+ n.tags.some(t => t.toLowerCase().includes(lowerQuery)));
70
+ });
63
71
  }
64
72
  async deleteNote(id) {
65
- await this.load();
66
- const initialLength = this.notes.length;
67
- this.notes = this.notes.filter(n => n.id !== id);
68
- if (this.notes.length !== initialLength) {
69
- await this.save();
70
- return true;
71
- }
72
- return false;
73
+ return this.mutex.dispatch(async () => {
74
+ await this.load();
75
+ const initialLength = this.notes.length;
76
+ this.notes = this.notes.filter(n => n.id !== id);
77
+ if (this.notes.length !== initialLength) {
78
+ await this.save();
79
+ return true;
80
+ }
81
+ return false;
82
+ });
73
83
  }
74
84
  async updateNote(id, updates) {
75
- await this.load();
76
- const index = this.notes.findIndex(n => n.id === id);
77
- if (index === -1)
78
- return null;
79
- this.notes[index] = {
80
- ...this.notes[index],
81
- ...updates,
82
- updatedAt: new Date().toISOString()
83
- };
84
- await this.save();
85
- return this.notes[index];
85
+ return this.mutex.dispatch(async () => {
86
+ await this.load();
87
+ const index = this.notes.findIndex(n => n.id === id);
88
+ if (index === -1)
89
+ return null;
90
+ this.notes[index] = {
91
+ ...this.notes[index],
92
+ ...updates,
93
+ updatedAt: new Date().toISOString()
94
+ };
95
+ await this.save();
96
+ return this.notes[index];
97
+ });
86
98
  }
87
99
  }
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
2
+ import { NotesManager } from './notes.js';
3
+ import * as fs from 'fs/promises';
4
+ vi.mock('fs/promises');
5
+ describe('NotesManager', () => {
6
+ let manager;
7
+ const testPath = 'test_notes.json';
8
+ let mockStore = '[]';
9
+ beforeEach(() => {
10
+ mockStore = '[]';
11
+ fs.readFile.mockImplementation(async () => mockStore);
12
+ fs.writeFile.mockImplementation(async (path, data) => {
13
+ mockStore = data;
14
+ });
15
+ manager = new NotesManager(testPath);
16
+ });
17
+ afterEach(() => {
18
+ vi.clearAllMocks();
19
+ });
20
+ it('should add a note correctly', async () => {
21
+ const note = await manager.addNote("Test Title", "Test Content", ["tag1"]);
22
+ expect(note.title).toBe("Test Title");
23
+ expect(note.tags).toContain("tag1");
24
+ expect(JSON.parse(mockStore)).toHaveLength(1);
25
+ });
26
+ it('should list notes with sorting by priority', async () => {
27
+ await manager.addNote("Low Prio", "Content", [], "low");
28
+ await manager.addNote("High Prio", "Content", [], "high");
29
+ const notes = await manager.listNotes();
30
+ expect(notes[0].priority).toBe('high');
31
+ expect(notes[1].priority).toBe('low');
32
+ });
33
+ it('should filter notes by tag', async () => {
34
+ await manager.addNote("Note 1", "Content", ["react"]);
35
+ await manager.addNote("Note 2", "Content", ["vue"]);
36
+ const reactNotes = await manager.listNotes('react');
37
+ expect(reactNotes).toHaveLength(1);
38
+ expect(reactNotes[0].title).toBe("Note 1");
39
+ });
40
+ it('should search notes by query', async () => {
41
+ await manager.addNote("Deploy Script", "Run npm build", ["devops"]);
42
+ await manager.addNote("Meeting Notes", "Discuss API", ["meeting"]);
43
+ const results = await manager.searchNotes("build");
44
+ expect(results).toHaveLength(1);
45
+ expect(results[0].title).toBe("Deploy Script");
46
+ });
47
+ it('should update a note', async () => {
48
+ const note = await manager.addNote("Original", "Content");
49
+ const updated = await manager.updateNote(note.id, { title: "Updated" });
50
+ expect(updated?.title).toBe("Updated");
51
+ expect(updated?.content).toBe("Content"); // Should remain
52
+ const list = await manager.listNotes();
53
+ expect(list[0].title).toBe("Updated");
54
+ });
55
+ it('should delete a note', async () => {
56
+ const note = await manager.addNote("To Delete", "Content");
57
+ const success = await manager.deleteNote(note.id);
58
+ expect(success).toBe(true);
59
+ const list = await manager.listNotes();
60
+ expect(list).toHaveLength(0);
61
+ });
62
+ it('should hide expired notes by default', async () => {
63
+ // Expired yesterday
64
+ const yesterday = new Date();
65
+ yesterday.setDate(yesterday.getDate() - 1);
66
+ await manager.addNote("Expired", "Content", [], "medium", yesterday.toISOString());
67
+ await manager.addNote("Active", "Content");
68
+ const list = await manager.listNotes();
69
+ expect(list).toHaveLength(1);
70
+ expect(list[0].title).toBe("Active");
71
+ const all = await manager.listNotes(undefined, true);
72
+ expect(all).toHaveLength(2);
73
+ });
74
+ });