mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
fix: broken cross-references, version sync, and enhanced command validator
- Fix /build-and-fix → /build-fix in tdd.md, plan.md (+ cursor, zh-CN) - Fix non-existent explorer agent → planner in orchestrate.md (+ cursor, zh-CN, zh-TW) - Fix /python-test → /tdd in python-review.md (+ cursor, zh-CN) - Sync package.json version from 1.0.0 to 1.4.1 to match plugin.json - Enhance validate-commands.js with cross-reference checking: command refs, agent path refs, skill dir refs, workflow diagrams - Strip fenced code blocks before scanning to avoid false positives - Skip hypothetical "Creates:" lines in evolve.md examples - Add 46 new tests (suggest-compact, session-manager, utils, hooks)
This commit is contained in:
@@ -17,7 +17,7 @@ planner -> tdd-guide -> code-reviewer -> security-reviewer
|
|||||||
### bugfix
|
### bugfix
|
||||||
Bug investigation and fix workflow:
|
Bug investigation and fix workflow:
|
||||||
```
|
```
|
||||||
explorer -> tdd-guide -> code-reviewer
|
planner -> tdd-guide -> code-reviewer
|
||||||
```
|
```
|
||||||
|
|
||||||
### refactor
|
### refactor
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ If you want changes, respond with:
|
|||||||
|
|
||||||
After planning:
|
After planning:
|
||||||
- Use `/tdd` to implement with test-driven development
|
- Use `/tdd` to implement with test-driven development
|
||||||
- Use `/build-and-fix` if build errors occur
|
- Use `/build-fix` if build errors occur
|
||||||
- Use `/code-review` to review completed implementation
|
- Use `/code-review` to review completed implementation
|
||||||
|
|
||||||
## Related Agents
|
## Related Agents
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ Run: `black app/routes/user.py app/services/auth.py`
|
|||||||
|
|
||||||
## Integration with Other Commands
|
## Integration with Other Commands
|
||||||
|
|
||||||
- Use `/python-test` first to ensure tests pass
|
- Use `/tdd` first to ensure tests pass
|
||||||
- Use `/code-review` for non-Python specific concerns
|
- Use `/code-review` for non-Python specific concerns
|
||||||
- Use `/python-review` before committing
|
- Use `/python-review` before committing
|
||||||
- Use `/build-fix` if static analysis tools fail
|
- Use `/build-fix` if static analysis tools fail
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ Never skip the RED phase. Never write code before tests.
|
|||||||
|
|
||||||
- Use `/plan` first to understand what to build
|
- Use `/plan` first to understand what to build
|
||||||
- Use `/tdd` to implement with tests
|
- Use `/tdd` to implement with tests
|
||||||
- Use `/build-and-fix` if build errors occur
|
- Use `/build-fix` if build errors occur
|
||||||
- Use `/code-review` to review implementation
|
- Use `/code-review` to review implementation
|
||||||
- Use `/test-coverage` to verify coverage
|
- Use `/test-coverage` to verify coverage
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ planner -> tdd-guide -> code-reviewer -> security-reviewer
|
|||||||
### bugfix
|
### bugfix
|
||||||
Bug investigation and fix workflow:
|
Bug investigation and fix workflow:
|
||||||
```
|
```
|
||||||
explorer -> tdd-guide -> code-reviewer
|
planner -> tdd-guide -> code-reviewer
|
||||||
```
|
```
|
||||||
|
|
||||||
### refactor
|
### refactor
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ If you want changes, respond with:
|
|||||||
|
|
||||||
After planning:
|
After planning:
|
||||||
- Use `/tdd` to implement with test-driven development
|
- Use `/tdd` to implement with test-driven development
|
||||||
- Use `/build-and-fix` if build errors occur
|
- Use `/build-fix` if build errors occur
|
||||||
- Use `/code-review` to review completed implementation
|
- Use `/code-review` to review completed implementation
|
||||||
|
|
||||||
## Related Agents
|
## Related Agents
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ Run: `black app/routes/user.py app/services/auth.py`
|
|||||||
|
|
||||||
## Integration with Other Commands
|
## Integration with Other Commands
|
||||||
|
|
||||||
- Use `/python-test` first to ensure tests pass
|
- Use `/tdd` first to ensure tests pass
|
||||||
- Use `/code-review` for non-Python specific concerns
|
- Use `/code-review` for non-Python specific concerns
|
||||||
- Use `/python-review` before committing
|
- Use `/python-review` before committing
|
||||||
- Use `/build-fix` if static analysis tools fail
|
- Use `/build-fix` if static analysis tools fail
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ Never skip the RED phase. Never write code before tests.
|
|||||||
|
|
||||||
- Use `/plan` first to understand what to build
|
- Use `/plan` first to understand what to build
|
||||||
- Use `/tdd` to implement with tests
|
- Use `/tdd` to implement with tests
|
||||||
- Use `/build-and-fix` if build errors occur
|
- Use `/build-fix` if build errors occur
|
||||||
- Use `/code-review` to review implementation
|
- Use `/code-review` to review implementation
|
||||||
- Use `/test-coverage` to verify coverage
|
- Use `/test-coverage` to verify coverage
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ planner -> tdd-guide -> code-reviewer -> security-reviewer
|
|||||||
错误调查与修复工作流:
|
错误调查与修复工作流:
|
||||||
|
|
||||||
```
|
```
|
||||||
explorer -> tdd-guide -> code-reviewer
|
planner -> tdd-guide -> code-reviewer
|
||||||
```
|
```
|
||||||
|
|
||||||
### refactor
|
### refactor
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ Agent (planner):
|
|||||||
计划之后:
|
计划之后:
|
||||||
|
|
||||||
* 使用 `/tdd` 以测试驱动开发的方式实施
|
* 使用 `/tdd` 以测试驱动开发的方式实施
|
||||||
* 如果出现构建错误,使用 `/build-and-fix`
|
* 如果出现构建错误,使用 `/build-fix`
|
||||||
* 使用 `/code-review` 审查已完成的实施
|
* 使用 `/code-review` 审查已完成的实施
|
||||||
|
|
||||||
## 相关代理
|
## 相关代理
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ with open("config.json") as f: # Good
|
|||||||
|
|
||||||
## Integration with Other Commands
|
## Integration with Other Commands
|
||||||
|
|
||||||
- Use `/python-test` first to ensure tests pass
|
- Use `/tdd` first to ensure tests pass
|
||||||
- Use `/code-review` for non-Python specific concerns
|
- Use `/code-review` for non-Python specific concerns
|
||||||
- Use `/python-review` before committing
|
- Use `/python-review` before committing
|
||||||
- Use `/build-fix` if static analysis tools fail
|
- Use `/build-fix` if static analysis tools fail
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ Never skip the RED phase. Never write code before tests.
|
|||||||
|
|
||||||
- Use `/plan` first to understand what to build
|
- Use `/plan` first to understand what to build
|
||||||
- Use `/tdd` to implement with tests
|
- Use `/tdd` to implement with tests
|
||||||
- Use `/build-and-fix` if build errors occur
|
- Use `/build-fix` if build errors occur
|
||||||
- Use `/code-review` to review implementation
|
- Use `/code-review` to review implementation
|
||||||
- Use `/test-coverage` to verify coverage
|
- Use `/test-coverage` to verify coverage
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ planner -> tdd-guide -> code-reviewer -> security-reviewer
|
|||||||
### bugfix
|
### bugfix
|
||||||
Bug 調查和修復工作流程:
|
Bug 調查和修復工作流程:
|
||||||
```
|
```
|
||||||
explorer -> tdd-guide -> code-reviewer
|
planner -> tdd-guide -> code-reviewer
|
||||||
```
|
```
|
||||||
|
|
||||||
### refactor
|
### refactor
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ecc-universal",
|
"name": "ecc-universal",
|
||||||
"version": "1.0.0",
|
"version": "1.4.1",
|
||||||
"description": "Complete collection of battle-tested Claude Code configs — agents, skills, hooks, commands, and rules evolved over 10+ months of intensive daily use by an Anthropic hackathon winner",
|
"description": "Complete collection of battle-tested Claude Code configs — agents, skills, hooks, commands, and rules evolved over 10+ months of intensive daily use by an Anthropic hackathon winner",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"claude-code",
|
"claude-code",
|
||||||
|
|||||||
@@ -153,8 +153,9 @@ async function main() {
|
|||||||
if (summary) {
|
if (summary) {
|
||||||
const existing = readFile(sessionFile);
|
const existing = readFile(sessionFile);
|
||||||
if (existing && existing.includes('[Session context goes here]')) {
|
if (existing && existing.includes('[Session context goes here]')) {
|
||||||
|
// Use a flexible regex that tolerates CRLF, extra whitespace, and minor template variations
|
||||||
const updatedContent = existing.replace(
|
const updatedContent = existing.replace(
|
||||||
/## Current State\n\n\[Session context goes here\]\n\n### Completed\n- \[ \]\n\n### In Progress\n- \[ \]\n\n### Notes for Next Session\n-\n\n### Context to Load\n```\n\[relevant files\]\n```/,
|
/## Current State\s*\n\s*\[Session context goes here\][\s\S]*?### Context to Load\s*\n```\s*\n\[relevant files\]\s*\n```/,
|
||||||
buildSummarySection(summary)
|
buildSummarySection(summary)
|
||||||
);
|
);
|
||||||
writeFile(sessionFile, updatedContent);
|
writeFile(sessionFile, updatedContent);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ async function main() {
|
|||||||
// Track tool call count (increment in a temp file)
|
// Track tool call count (increment in a temp file)
|
||||||
// Use a session-specific counter file based on session ID from environment
|
// Use a session-specific counter file based on session ID from environment
|
||||||
// or parent PID as fallback
|
// or parent PID as fallback
|
||||||
const sessionId = process.env.CLAUDE_SESSION_ID || String(process.ppid) || 'default';
|
const sessionId = process.env.CLAUDE_SESSION_ID || 'default';
|
||||||
const counterFile = path.join(getTempDir(), `claude-tool-count-${sessionId}`);
|
const counterFile = path.join(getTempDir(), `claude-tool-count-${sessionId}`);
|
||||||
const rawThreshold = parseInt(process.env.COMPACT_THRESHOLD || '50', 10);
|
const rawThreshold = parseInt(process.env.COMPACT_THRESHOLD || '50', 10);
|
||||||
const threshold = Number.isFinite(rawThreshold) && rawThreshold > 0 && rawThreshold <= 10000
|
const threshold = Number.isFinite(rawThreshold) && rawThreshold > 0 && rawThreshold <= 10000
|
||||||
|
|||||||
@@ -216,6 +216,96 @@ async function runTests() {
|
|||||||
fs.unlinkSync(counterFile);
|
fs.unlinkSync(counterFile);
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (await asyncTest('does not suggest below threshold', async () => {
|
||||||
|
const sessionId = 'test-below-' + Date.now();
|
||||||
|
const counterFile = path.join(os.tmpdir(), `claude-tool-count-${sessionId}`);
|
||||||
|
|
||||||
|
fs.writeFileSync(counterFile, '10');
|
||||||
|
|
||||||
|
const result = await runScript(path.join(scriptsDir, 'suggest-compact.js'), '', {
|
||||||
|
CLAUDE_SESSION_ID: sessionId,
|
||||||
|
COMPACT_THRESHOLD: '50'
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
!result.stderr.includes('tool calls'),
|
||||||
|
'Should not suggest compact below threshold'
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.unlinkSync(counterFile);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (await asyncTest('suggests at regular intervals after threshold', async () => {
|
||||||
|
const sessionId = 'test-interval-' + Date.now();
|
||||||
|
const counterFile = path.join(os.tmpdir(), `claude-tool-count-${sessionId}`);
|
||||||
|
|
||||||
|
// Set counter to 74 (next will be 75, which is >50 and 75%25==0)
|
||||||
|
fs.writeFileSync(counterFile, '74');
|
||||||
|
|
||||||
|
const result = await runScript(path.join(scriptsDir, 'suggest-compact.js'), '', {
|
||||||
|
CLAUDE_SESSION_ID: sessionId,
|
||||||
|
COMPACT_THRESHOLD: '50'
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.stderr.includes('75 tool calls'),
|
||||||
|
'Should suggest at 25-call intervals after threshold'
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.unlinkSync(counterFile);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (await asyncTest('handles corrupted counter file', async () => {
|
||||||
|
const sessionId = 'test-corrupt-' + Date.now();
|
||||||
|
const counterFile = path.join(os.tmpdir(), `claude-tool-count-${sessionId}`);
|
||||||
|
|
||||||
|
fs.writeFileSync(counterFile, 'not-a-number');
|
||||||
|
|
||||||
|
const result = await runScript(path.join(scriptsDir, 'suggest-compact.js'), '', {
|
||||||
|
CLAUDE_SESSION_ID: sessionId
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(result.code, 0, 'Should handle corrupted counter gracefully');
|
||||||
|
|
||||||
|
// Counter should be reset to 1
|
||||||
|
const newCount = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
|
||||||
|
assert.strictEqual(newCount, 1, 'Should reset counter to 1 on corrupt data');
|
||||||
|
|
||||||
|
fs.unlinkSync(counterFile);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (await asyncTest('uses default session ID when no env var', async () => {
|
||||||
|
const result = await runScript(path.join(scriptsDir, 'suggest-compact.js'), '', {
|
||||||
|
CLAUDE_SESSION_ID: '' // Empty, should use 'default'
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(result.code, 0, 'Should work with default session ID');
|
||||||
|
|
||||||
|
// Cleanup the default counter file
|
||||||
|
const counterFile = path.join(os.tmpdir(), 'claude-tool-count-default');
|
||||||
|
if (fs.existsSync(counterFile)) fs.unlinkSync(counterFile);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (await asyncTest('validates threshold bounds', async () => {
|
||||||
|
const sessionId = 'test-bounds-' + Date.now();
|
||||||
|
const counterFile = path.join(os.tmpdir(), `claude-tool-count-${sessionId}`);
|
||||||
|
|
||||||
|
// Invalid threshold should fall back to 50
|
||||||
|
fs.writeFileSync(counterFile, '49');
|
||||||
|
|
||||||
|
const result = await runScript(path.join(scriptsDir, 'suggest-compact.js'), '', {
|
||||||
|
CLAUDE_SESSION_ID: sessionId,
|
||||||
|
COMPACT_THRESHOLD: '-5' // Invalid: negative
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
result.stderr.includes('50 tool calls'),
|
||||||
|
'Should use default threshold (50) for invalid value'
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.unlinkSync(counterFile);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
// evaluate-session.js tests
|
// evaluate-session.js tests
|
||||||
console.log('\nevaluate-session.js:');
|
console.log('\nevaluate-session.js:');
|
||||||
|
|
||||||
|
|||||||
@@ -328,6 +328,148 @@ src/main.ts
|
|||||||
assert.strictEqual(title, 'Untitled Session');
|
assert.strictEqual(title, 'Untitled Session');
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// getAllSessions tests
|
||||||
|
console.log('\ngetAllSessions:');
|
||||||
|
|
||||||
|
// Override HOME to a temp dir for isolated getAllSessions/getSessionById tests
|
||||||
|
const tmpHome = path.join(os.tmpdir(), `ecc-session-mgr-test-${Date.now()}`);
|
||||||
|
const tmpSessionsDir = path.join(tmpHome, '.claude', 'sessions');
|
||||||
|
fs.mkdirSync(tmpSessionsDir, { recursive: true });
|
||||||
|
const origHome = process.env.HOME;
|
||||||
|
|
||||||
|
// Create test session files with controlled modification times
|
||||||
|
const testSessions = [
|
||||||
|
{ name: '2026-01-15-abcd1234-session.tmp', content: '# Session 1' },
|
||||||
|
{ name: '2026-01-20-efgh5678-session.tmp', content: '# Session 2' },
|
||||||
|
{ name: '2026-02-01-ijkl9012-session.tmp', content: '# Session 3' },
|
||||||
|
{ name: '2026-02-01-mnop3456-session.tmp', content: '# Session 4' },
|
||||||
|
{ name: '2026-02-10-session.tmp', content: '# Old format session' },
|
||||||
|
];
|
||||||
|
for (let i = 0; i < testSessions.length; i++) {
|
||||||
|
const filePath = path.join(tmpSessionsDir, testSessions[i].name);
|
||||||
|
fs.writeFileSync(filePath, testSessions[i].content);
|
||||||
|
// Stagger modification times so sort order is deterministic
|
||||||
|
const mtime = new Date(Date.now() - (testSessions.length - i) * 60000);
|
||||||
|
fs.utimesSync(filePath, mtime, mtime);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.env.HOME = tmpHome;
|
||||||
|
|
||||||
|
if (test('getAllSessions returns all sessions', () => {
|
||||||
|
const result = sessionManager.getAllSessions({ limit: 100 });
|
||||||
|
assert.strictEqual(result.total, 5);
|
||||||
|
assert.strictEqual(result.sessions.length, 5);
|
||||||
|
assert.strictEqual(result.hasMore, false);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('getAllSessions paginates correctly', () => {
|
||||||
|
const page1 = sessionManager.getAllSessions({ limit: 2, offset: 0 });
|
||||||
|
assert.strictEqual(page1.sessions.length, 2);
|
||||||
|
assert.strictEqual(page1.hasMore, true);
|
||||||
|
assert.strictEqual(page1.total, 5);
|
||||||
|
|
||||||
|
const page2 = sessionManager.getAllSessions({ limit: 2, offset: 2 });
|
||||||
|
assert.strictEqual(page2.sessions.length, 2);
|
||||||
|
assert.strictEqual(page2.hasMore, true);
|
||||||
|
|
||||||
|
const page3 = sessionManager.getAllSessions({ limit: 2, offset: 4 });
|
||||||
|
assert.strictEqual(page3.sessions.length, 1);
|
||||||
|
assert.strictEqual(page3.hasMore, false);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('getAllSessions filters by date', () => {
|
||||||
|
const result = sessionManager.getAllSessions({ date: '2026-02-01', limit: 100 });
|
||||||
|
assert.strictEqual(result.total, 2);
|
||||||
|
assert.ok(result.sessions.every(s => s.date === '2026-02-01'));
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('getAllSessions filters by search (short ID)', () => {
|
||||||
|
const result = sessionManager.getAllSessions({ search: 'abcd', limit: 100 });
|
||||||
|
assert.strictEqual(result.total, 1);
|
||||||
|
assert.strictEqual(result.sessions[0].shortId, 'abcd1234');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('getAllSessions returns sorted by newest first', () => {
|
||||||
|
const result = sessionManager.getAllSessions({ limit: 100 });
|
||||||
|
for (let i = 1; i < result.sessions.length; i++) {
|
||||||
|
assert.ok(
|
||||||
|
result.sessions[i - 1].modifiedTime >= result.sessions[i].modifiedTime,
|
||||||
|
'Sessions should be sorted newest first'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('getAllSessions handles offset beyond total', () => {
|
||||||
|
const result = sessionManager.getAllSessions({ offset: 999, limit: 10 });
|
||||||
|
assert.strictEqual(result.sessions.length, 0);
|
||||||
|
assert.strictEqual(result.total, 5);
|
||||||
|
assert.strictEqual(result.hasMore, false);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('getAllSessions returns empty for non-existent date', () => {
|
||||||
|
const result = sessionManager.getAllSessions({ date: '2099-12-31', limit: 100 });
|
||||||
|
assert.strictEqual(result.total, 0);
|
||||||
|
assert.strictEqual(result.sessions.length, 0);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('getAllSessions ignores non-.tmp files', () => {
|
||||||
|
fs.writeFileSync(path.join(tmpSessionsDir, 'notes.txt'), 'not a session');
|
||||||
|
fs.writeFileSync(path.join(tmpSessionsDir, 'compaction-log.txt'), 'log');
|
||||||
|
const result = sessionManager.getAllSessions({ limit: 100 });
|
||||||
|
assert.strictEqual(result.total, 5, 'Should only count .tmp session files');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// getSessionById tests
|
||||||
|
console.log('\ngetSessionById:');
|
||||||
|
|
||||||
|
if (test('getSessionById finds by short ID prefix', () => {
|
||||||
|
const result = sessionManager.getSessionById('abcd1234');
|
||||||
|
assert.ok(result, 'Should find session by exact short ID');
|
||||||
|
assert.strictEqual(result.shortId, 'abcd1234');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('getSessionById finds by short ID prefix match', () => {
|
||||||
|
const result = sessionManager.getSessionById('abcd');
|
||||||
|
assert.ok(result, 'Should find session by short ID prefix');
|
||||||
|
assert.strictEqual(result.shortId, 'abcd1234');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('getSessionById finds by full filename', () => {
|
||||||
|
const result = sessionManager.getSessionById('2026-01-15-abcd1234-session.tmp');
|
||||||
|
assert.ok(result, 'Should find session by full filename');
|
||||||
|
assert.strictEqual(result.shortId, 'abcd1234');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('getSessionById finds by filename without .tmp', () => {
|
||||||
|
const result = sessionManager.getSessionById('2026-01-15-abcd1234-session');
|
||||||
|
assert.ok(result, 'Should find session by filename without extension');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('getSessionById returns null for non-existent ID', () => {
|
||||||
|
const result = sessionManager.getSessionById('zzzzzzzz');
|
||||||
|
assert.strictEqual(result, null);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('getSessionById includes content when requested', () => {
|
||||||
|
const result = sessionManager.getSessionById('abcd1234', true);
|
||||||
|
assert.ok(result, 'Should find session');
|
||||||
|
assert.ok(result.content, 'Should include content');
|
||||||
|
assert.ok(result.content.includes('Session 1'), 'Content should match');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('getSessionById finds old format (no short ID)', () => {
|
||||||
|
const result = sessionManager.getSessionById('2026-02-10-session');
|
||||||
|
assert.ok(result, 'Should find old-format session by filename');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
process.env.HOME = origHome;
|
||||||
|
try {
|
||||||
|
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||||
|
} catch {
|
||||||
|
// best-effort
|
||||||
|
}
|
||||||
|
|
||||||
// Summary
|
// Summary
|
||||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||||
process.exit(failed > 0 ? 1 : 0);
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
|
|||||||
@@ -397,6 +397,155 @@ function runTests() {
|
|||||||
assert.strictEqual(result.success, false);
|
assert.strictEqual(result.success, false);
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// output() and log() tests
|
||||||
|
console.log('\noutput() and log():');
|
||||||
|
|
||||||
|
if (test('output() writes string to stdout', () => {
|
||||||
|
// Capture stdout by temporarily replacing console.log
|
||||||
|
let captured = null;
|
||||||
|
const origLog = console.log;
|
||||||
|
console.log = (v) => { captured = v; };
|
||||||
|
try {
|
||||||
|
utils.output('hello');
|
||||||
|
assert.strictEqual(captured, 'hello');
|
||||||
|
} finally {
|
||||||
|
console.log = origLog;
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('output() JSON-stringifies objects', () => {
|
||||||
|
let captured = null;
|
||||||
|
const origLog = console.log;
|
||||||
|
console.log = (v) => { captured = v; };
|
||||||
|
try {
|
||||||
|
utils.output({ key: 'value', num: 42 });
|
||||||
|
assert.strictEqual(captured, '{"key":"value","num":42}');
|
||||||
|
} finally {
|
||||||
|
console.log = origLog;
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('output() JSON-stringifies null (typeof null === "object")', () => {
|
||||||
|
let captured = null;
|
||||||
|
const origLog = console.log;
|
||||||
|
console.log = (v) => { captured = v; };
|
||||||
|
try {
|
||||||
|
utils.output(null);
|
||||||
|
// typeof null === 'object' in JS, so it goes through JSON.stringify
|
||||||
|
assert.strictEqual(captured, 'null');
|
||||||
|
} finally {
|
||||||
|
console.log = origLog;
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('output() handles arrays as objects', () => {
|
||||||
|
let captured = null;
|
||||||
|
const origLog = console.log;
|
||||||
|
console.log = (v) => { captured = v; };
|
||||||
|
try {
|
||||||
|
utils.output([1, 2, 3]);
|
||||||
|
assert.strictEqual(captured, '[1,2,3]');
|
||||||
|
} finally {
|
||||||
|
console.log = origLog;
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('log() writes to stderr', () => {
|
||||||
|
let captured = null;
|
||||||
|
const origError = console.error;
|
||||||
|
console.error = (v) => { captured = v; };
|
||||||
|
try {
|
||||||
|
utils.log('test message');
|
||||||
|
assert.strictEqual(captured, 'test message');
|
||||||
|
} finally {
|
||||||
|
console.error = origError;
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// isGitRepo() tests
|
||||||
|
console.log('\nisGitRepo():');
|
||||||
|
|
||||||
|
if (test('isGitRepo returns true in a git repo', () => {
|
||||||
|
// We're running from within the ECC repo, so this should be true
|
||||||
|
assert.strictEqual(utils.isGitRepo(), true);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// getGitModifiedFiles() tests
|
||||||
|
console.log('\ngetGitModifiedFiles():');
|
||||||
|
|
||||||
|
if (test('getGitModifiedFiles returns an array', () => {
|
||||||
|
const files = utils.getGitModifiedFiles();
|
||||||
|
assert.ok(Array.isArray(files));
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('getGitModifiedFiles filters by regex patterns', () => {
|
||||||
|
const files = utils.getGitModifiedFiles(['\\.NONEXISTENT_EXTENSION$']);
|
||||||
|
assert.ok(Array.isArray(files));
|
||||||
|
assert.strictEqual(files.length, 0);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('getGitModifiedFiles skips invalid patterns', () => {
|
||||||
|
// Mix of valid and invalid patterns — should not throw
|
||||||
|
const files = utils.getGitModifiedFiles(['(unclosed', '\\.js$', '[invalid']);
|
||||||
|
assert.ok(Array.isArray(files));
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('getGitModifiedFiles skips non-string patterns', () => {
|
||||||
|
const files = utils.getGitModifiedFiles([null, undefined, 42, '', '\\.js$']);
|
||||||
|
assert.ok(Array.isArray(files));
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// getLearnedSkillsDir() test
|
||||||
|
console.log('\ngetLearnedSkillsDir():');
|
||||||
|
|
||||||
|
if (test('getLearnedSkillsDir returns path under Claude dir', () => {
|
||||||
|
const dir = utils.getLearnedSkillsDir();
|
||||||
|
assert.ok(dir.includes('.claude'));
|
||||||
|
assert.ok(dir.includes('skills'));
|
||||||
|
assert.ok(dir.includes('learned'));
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// findFiles with regex special characters in pattern
|
||||||
|
console.log('\nfindFiles (regex chars):');
|
||||||
|
|
||||||
|
if (test('findFiles handles regex special chars in pattern', () => {
|
||||||
|
const testDir = path.join(utils.getTempDir(), `utils-test-regex-${Date.now()}`);
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(testDir);
|
||||||
|
// Create files with regex-special characters in names
|
||||||
|
fs.writeFileSync(path.join(testDir, 'file(1).txt'), 'content');
|
||||||
|
fs.writeFileSync(path.join(testDir, 'file+2.txt'), 'content');
|
||||||
|
fs.writeFileSync(path.join(testDir, 'file[3].txt'), 'content');
|
||||||
|
|
||||||
|
// These patterns should match literally, not as regex metacharacters
|
||||||
|
const parens = utils.findFiles(testDir, 'file(1).txt');
|
||||||
|
assert.strictEqual(parens.length, 1, 'Should match file(1).txt literally');
|
||||||
|
|
||||||
|
const plus = utils.findFiles(testDir, 'file+2.txt');
|
||||||
|
assert.strictEqual(plus.length, 1, 'Should match file+2.txt literally');
|
||||||
|
|
||||||
|
const brackets = utils.findFiles(testDir, 'file[3].txt');
|
||||||
|
assert.strictEqual(brackets.length, 1, 'Should match file[3].txt literally');
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(testDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('findFiles wildcard still works with special chars', () => {
|
||||||
|
const testDir = path.join(utils.getTempDir(), `utils-test-glob-${Date.now()}`);
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(testDir);
|
||||||
|
fs.writeFileSync(path.join(testDir, 'app(v2).js'), 'content');
|
||||||
|
fs.writeFileSync(path.join(testDir, 'app(v3).ts'), 'content');
|
||||||
|
|
||||||
|
const jsFiles = utils.findFiles(testDir, '*.js');
|
||||||
|
assert.strictEqual(jsFiles.length, 1);
|
||||||
|
assert.ok(jsFiles[0].path.endsWith('app(v2).js'));
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(testDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
// Summary
|
// Summary
|
||||||
console.log('\n=== Test Results ===');
|
console.log('\n=== Test Results ===');
|
||||||
console.log(`Passed: ${passed}`);
|
console.log(`Passed: ${passed}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user