create-walle 0.9.3 → 0.9.4
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 +2 -1
- package/package.json +1 -1
- package/template/claude-task-manager/db.js +5 -1
- package/template/claude-task-manager/public/css/walle.css +317 -0
- package/template/claude-task-manager/public/index.html +404 -101
- package/template/claude-task-manager/public/js/walle.js +1256 -86
- package/template/claude-task-manager/server.js +189 -14
- package/template/docs/site/api/README.md +146 -0
- package/template/docs/site/skills/README.md +99 -5
- package/template/package.json +1 -1
- package/template/wall-e/agent.js +54 -0
- package/template/wall-e/api-walle.js +452 -3
- package/template/wall-e/brain.js +45 -1
- package/template/wall-e/channels/telegram-channel.js +96 -0
- package/template/wall-e/chat.js +61 -2
- package/template/wall-e/coding-context.js +252 -0
- package/template/wall-e/coding-orchestrator.js +625 -0
- package/template/wall-e/coding-review.js +189 -0
- package/template/wall-e/core-tasks.js +12 -3
- package/template/wall-e/deploy.sh +4 -4
- package/template/wall-e/fly.toml +2 -2
- package/template/wall-e/package.json +4 -1
- package/template/wall-e/skills/_bundled/coding-agent/SKILL.md +17 -0
- package/template/wall-e/skills/_bundled/coding-agent/run.js +142 -0
- package/template/wall-e/skills/_bundled/email-sync/SKILL.md +12 -7
- package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +76 -46
- package/template/wall-e/skills/_bundled/email-sync/run.js +42 -2
- package/template/wall-e/skills/_bundled/glean-team-sync/SKILL.md +57 -0
- package/template/wall-e/skills/_bundled/glean-team-sync/run.js +254 -0
- package/template/wall-e/skills/_bundled/slack-mentions/SKILL.md +1 -1
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +268 -121
- package/template/wall-e/skills/_templates/data-fetcher.md +27 -0
- package/template/wall-e/skills/_templates/manual-action.md +19 -0
- package/template/wall-e/skills/_templates/periodic-checker.md +29 -0
- package/template/wall-e/skills/_templates/script-runner.md +21 -0
- package/template/wall-e/skills/claude-code-reader.js +16 -4
- package/template/wall-e/skills/skill-executor.js +23 -1
- package/template/wall-e/skills/skill-validator.js +73 -0
- package/template/wall-e/tests/brain.test.js +3 -3
- package/template/wall-e/tests/coding-agent-integration.test.js +240 -0
- package/template/wall-e/tests/coding-context.test.js +212 -0
- package/template/wall-e/tests/coding-orchestrator.test.js +303 -0
- package/template/wall-e/tests/coding-review.test.js +141 -0
- package/template/claude-task-manager/package-lock.json +0 -1607
- package/template/claude-task-manager/tests/test-ai-search.js +0 -61
- package/template/claude-task-manager/tests/test-editor-ux.js +0 -76
- package/template/claude-task-manager/tests/test-editor-ux2.js +0 -51
- package/template/claude-task-manager/tests/test-features-v2.js +0 -127
- package/template/claude-task-manager/tests/test-insights-cached.js +0 -78
- package/template/claude-task-manager/tests/test-insights.js +0 -124
- package/template/claude-task-manager/tests/test-permissions-v2.js +0 -127
- package/template/claude-task-manager/tests/test-permissions.js +0 -122
- package/template/claude-task-manager/tests/test-pin.js +0 -51
- package/template/claude-task-manager/tests/test-prompts.js +0 -164
- package/template/claude-task-manager/tests/test-recent-sessions.js +0 -96
- package/template/claude-task-manager/tests/test-review.js +0 -104
- package/template/claude-task-manager/tests/test-send-dropdown.js +0 -76
- package/template/claude-task-manager/tests/test-send-final.js +0 -30
- package/template/claude-task-manager/tests/test-send-fixes.js +0 -76
- package/template/claude-task-manager/tests/test-send-integration.js +0 -107
- package/template/claude-task-manager/tests/test-send-visual.js +0 -34
- package/template/claude-task-manager/tests/test-session-create.js +0 -147
- package/template/claude-task-manager/tests/test-sidebar-ux.js +0 -83
- package/template/claude-task-manager/tests/test-url-hash.js +0 -68
- package/template/claude-task-manager/tests/test-ux-crop.js +0 -34
- package/template/claude-task-manager/tests/test-ux-review.js +0 -130
- package/template/claude-task-manager/tests/test-zoom-card.js +0 -76
- package/template/claude-task-manager/tests/test-zoom.js +0 -92
- package/template/claude-task-manager/tests/test-zoom2.js +0 -67
- package/template/docs/openclaw-vs-walle-comparison.md +0 -103
- package/template/docs/ux-improvement-plan.md +0 -84
- package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +0 -112
- package/template/wall-e/docs/specs/SKILL-FORMAT.md +0 -326
- package/template/wall-e/package-lock.json +0 -533
- package/template/wall-e/skills/_bundled/slack-mentions/.watermark.json +0 -4
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { execFileSync } = require('child_process');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validate a skill's `requires` field before execution.
|
|
7
|
+
* Checks: bins (command availability), env (environment variables), mcp (MCP server configs).
|
|
8
|
+
* Returns { valid: boolean, missing: [{ type, name, suggestion }] }
|
|
9
|
+
*/
|
|
10
|
+
function validateSkillRequirements(skill) {
|
|
11
|
+
const requires = skill.requires || {};
|
|
12
|
+
const missing = [];
|
|
13
|
+
|
|
14
|
+
// Check required binaries
|
|
15
|
+
if (requires.bins) {
|
|
16
|
+
const bins = Array.isArray(requires.bins) ? requires.bins : [requires.bins];
|
|
17
|
+
for (const bin of bins) {
|
|
18
|
+
try {
|
|
19
|
+
execFileSync('which', [bin], { encoding: 'utf8', stdio: 'pipe' });
|
|
20
|
+
} catch {
|
|
21
|
+
missing.push({ type: 'bin', name: bin, suggestion: `Install ${bin} or add it to PATH` });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check required environment variables
|
|
27
|
+
if (requires.env) {
|
|
28
|
+
const envVars = Array.isArray(requires.env) ? requires.env : [requires.env];
|
|
29
|
+
for (const envVar of envVars) {
|
|
30
|
+
if (!process.env[envVar]) {
|
|
31
|
+
missing.push({ type: 'env', name: envVar, suggestion: `Set ${envVar} in .env or environment` });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check required MCP servers
|
|
37
|
+
if (requires.mcp) {
|
|
38
|
+
const mcpServers = Array.isArray(requires.mcp) ? requires.mcp : [requires.mcp];
|
|
39
|
+
const availableMcp = getAvailableMcpServers();
|
|
40
|
+
for (const server of mcpServers) {
|
|
41
|
+
if (!availableMcp.has(server)) {
|
|
42
|
+
missing.push({ type: 'mcp', name: server, suggestion: `Configure MCP server "${server}" in Claude Code settings` });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { valid: missing.length === 0, missing };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get set of available MCP server names from Claude Code config.
|
|
52
|
+
*/
|
|
53
|
+
function getAvailableMcpServers() {
|
|
54
|
+
const servers = new Set();
|
|
55
|
+
try {
|
|
56
|
+
const fs = require('fs');
|
|
57
|
+
const configPaths = [
|
|
58
|
+
path.join(process.env.HOME || '', '.claude', 'claude_desktop_config.json'),
|
|
59
|
+
path.join(process.env.HOME || '', '.config', 'claude', 'claude_desktop_config.json'),
|
|
60
|
+
];
|
|
61
|
+
for (const cp of configPaths) {
|
|
62
|
+
try {
|
|
63
|
+
const config = JSON.parse(fs.readFileSync(cp, 'utf8'));
|
|
64
|
+
if (config.mcpServers) {
|
|
65
|
+
Object.keys(config.mcpServers).forEach(k => servers.add(k));
|
|
66
|
+
}
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
} catch {}
|
|
70
|
+
return servers;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { validateSkillRequirements };
|
|
@@ -40,17 +40,17 @@ describe('Brain DB Schema and Init', () => {
|
|
|
40
40
|
assert.equal(row, 1);
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
it('has all
|
|
43
|
+
it('has all 24 tables', () => {
|
|
44
44
|
const db = brain.getDb();
|
|
45
45
|
const tables = db.prepare(
|
|
46
46
|
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
|
|
47
47
|
).all().map(r => r.name);
|
|
48
48
|
|
|
49
49
|
const expected = [
|
|
50
|
-
'agent_actions', 'agent_exchanges', 'brain_metadata', 'briefing_items', 'chat_messages', 'code_identity', 'current_state',
|
|
50
|
+
'agent_actions', 'agent_exchanges', 'brain_metadata', 'briefing_items', 'chat_branches', 'chat_messages', 'code_identity', 'current_state',
|
|
51
51
|
'daily_summaries', 'domain_confidence', 'initiative_log', 'knowledge', 'loop_checkpoints', 'memories',
|
|
52
52
|
'owner', 'patterns', 'pending_questions', 'people', 'personas',
|
|
53
|
-
'sessions', 'skill_executions', 'skills', 'tasks'
|
|
53
|
+
'sessions', 'skill_executions', 'skills', 'slack_threads', 'tasks'
|
|
54
54
|
].sort();
|
|
55
55
|
|
|
56
56
|
assert.deepEqual(tables, expected);
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { describe, it, before, after } = require('node:test');
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const { execFileSync } = require('node:child_process');
|
|
8
|
+
|
|
9
|
+
const { assembleContext } = require('../coding-context');
|
|
10
|
+
const {
|
|
11
|
+
buildPlanningPrompt,
|
|
12
|
+
parsePlan,
|
|
13
|
+
writeCheckpoint,
|
|
14
|
+
readCheckpoint,
|
|
15
|
+
formatReport,
|
|
16
|
+
runHeadless,
|
|
17
|
+
} = require('../coding-orchestrator');
|
|
18
|
+
|
|
19
|
+
describe('coding-agent integration', () => {
|
|
20
|
+
let tmpDir;
|
|
21
|
+
let originalPath;
|
|
22
|
+
|
|
23
|
+
const MOCK_CLAUDE_SCRIPT = `#!/bin/bash
|
|
24
|
+
if echo "$@" | grep -q "senior engineer"; then
|
|
25
|
+
cat << 'PLAN'
|
|
26
|
+
{"subtasks":[{"id":"1","title":"Add hello endpoint","prompt":"Add a /hello endpoint to server.js that returns Hello World","depends_on":[],"verify":{"test":false,"review":false},"files_hint":["server.js"]}],"branch_name":"feat/hello","estimated_scope":"small"}
|
|
27
|
+
PLAN
|
|
28
|
+
else
|
|
29
|
+
echo '// Added by coding agent' >> "$PWD/server.js"
|
|
30
|
+
echo '{"result":"done","is_error":false}'
|
|
31
|
+
fi
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
const SERVER_JS = `const http = require('http');
|
|
35
|
+
const server = http.createServer((req, res) => {
|
|
36
|
+
res.writeHead(200);
|
|
37
|
+
res.end('ok');
|
|
38
|
+
});
|
|
39
|
+
server.listen(0);
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
const PACKAGE_JSON = JSON.stringify({
|
|
43
|
+
name: 'test-project',
|
|
44
|
+
version: '1.0.0',
|
|
45
|
+
scripts: { test: 'echo "tests passed"' },
|
|
46
|
+
}, null, 2);
|
|
47
|
+
|
|
48
|
+
const CLAUDE_MD = '# Test\nUse node:test';
|
|
49
|
+
|
|
50
|
+
before(() => {
|
|
51
|
+
// Create temp directory with project files
|
|
52
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'coding-agent-integ-'));
|
|
53
|
+
|
|
54
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), CLAUDE_MD);
|
|
55
|
+
fs.writeFileSync(path.join(tmpDir, 'server.js'), SERVER_JS);
|
|
56
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), PACKAGE_JSON);
|
|
57
|
+
|
|
58
|
+
// Initialize git repo
|
|
59
|
+
execFileSync('git', ['init'], { cwd: tmpDir });
|
|
60
|
+
execFileSync('git', ['config', 'user.email', 'test@test.com'], { cwd: tmpDir });
|
|
61
|
+
execFileSync('git', ['config', 'user.name', 'Test'], { cwd: tmpDir });
|
|
62
|
+
execFileSync('git', ['add', '.'], { cwd: tmpDir });
|
|
63
|
+
execFileSync('git', ['commit', '-m', 'init'], { cwd: tmpDir });
|
|
64
|
+
|
|
65
|
+
// Create mock claude binary
|
|
66
|
+
const mockClaudePath = path.join(tmpDir, 'claude');
|
|
67
|
+
fs.writeFileSync(mockClaudePath, MOCK_CLAUDE_SCRIPT);
|
|
68
|
+
fs.chmodSync(mockClaudePath, 0o755);
|
|
69
|
+
|
|
70
|
+
// Save original PATH
|
|
71
|
+
originalPath = process.env.PATH;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
after(() => {
|
|
75
|
+
// Restore PATH
|
|
76
|
+
process.env.PATH = originalPath;
|
|
77
|
+
|
|
78
|
+
// Remove temp directory
|
|
79
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('assembleContext', () => {
|
|
83
|
+
it('gathers file tree, CLAUDE.md, and relevant files from temp dir', async () => {
|
|
84
|
+
const ctx = await assembleContext('server http', tmpDir);
|
|
85
|
+
|
|
86
|
+
// File tree should contain our files
|
|
87
|
+
assert.ok(ctx.fileTree.includes('server.js'), 'file tree should include server.js');
|
|
88
|
+
assert.ok(ctx.fileTree.includes('package.json'), 'file tree should include package.json');
|
|
89
|
+
assert.ok(ctx.fileTree.includes('CLAUDE.md'), 'file tree should include CLAUDE.md');
|
|
90
|
+
|
|
91
|
+
// CLAUDE.md content
|
|
92
|
+
assert.equal(ctx.claudeMd, CLAUDE_MD);
|
|
93
|
+
|
|
94
|
+
// Git log should have our init commit
|
|
95
|
+
assert.ok(ctx.gitLog.includes('init'), 'git log should include init commit');
|
|
96
|
+
|
|
97
|
+
// Test command detected from package.json
|
|
98
|
+
assert.equal(ctx.testCommand, 'npm test');
|
|
99
|
+
|
|
100
|
+
// Relevant files should match keyword "server"
|
|
101
|
+
assert.ok('server.js' in ctx.relevantFiles, 'relevant files should include server.js');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('buildPlanningPrompt', () => {
|
|
106
|
+
it('includes request, context sections, and JSON instruction', async () => {
|
|
107
|
+
const ctx = await assembleContext('add hello endpoint', tmpDir);
|
|
108
|
+
const prompt = buildPlanningPrompt('add hello endpoint', ctx);
|
|
109
|
+
|
|
110
|
+
assert.ok(prompt.includes('senior software engineer'), 'should mention senior engineer role');
|
|
111
|
+
assert.ok(prompt.includes('add hello endpoint'), 'should include the request');
|
|
112
|
+
assert.ok(prompt.includes('# Test'), 'should include CLAUDE.md content');
|
|
113
|
+
assert.ok(prompt.includes('server.js'), 'should include file tree');
|
|
114
|
+
assert.ok(prompt.includes('"subtasks"'), 'should include JSON schema instruction');
|
|
115
|
+
assert.ok(prompt.includes('branch_name'), 'should include branch_name in schema');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('parsePlan', () => {
|
|
120
|
+
it('parses valid plan JSON from text', () => {
|
|
121
|
+
const text = '{"subtasks":[{"id":"1","title":"Do thing","prompt":"Do the thing","depends_on":[],"verify":{"test":false,"review":false},"files_hint":["foo.js"]}],"branch_name":"feat/thing","estimated_scope":"small"}';
|
|
122
|
+
const plan = parsePlan(text);
|
|
123
|
+
|
|
124
|
+
assert.ok(Array.isArray(plan.subtasks));
|
|
125
|
+
assert.equal(plan.subtasks.length, 1);
|
|
126
|
+
assert.equal(plan.subtasks[0].title, 'Do thing');
|
|
127
|
+
assert.equal(plan.branch_name, 'feat/thing');
|
|
128
|
+
assert.equal(plan.estimated_scope, 'small');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('parses plan from fenced code block', () => {
|
|
132
|
+
const text = 'Here is the plan:\n```json\n{"subtasks":[{"title":"Step 1","prompt":"Do step 1"}],"branch_name":"feat/steps","estimated_scope":"small"}\n```';
|
|
133
|
+
const plan = parsePlan(text);
|
|
134
|
+
|
|
135
|
+
assert.equal(plan.subtasks.length, 1);
|
|
136
|
+
assert.equal(plan.branch_name, 'feat/steps');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('throws on empty output', () => {
|
|
140
|
+
assert.throws(() => parsePlan(''), /empty output/);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('throws on output with no valid subtasks', () => {
|
|
144
|
+
assert.throws(() => parsePlan('no json here'), /no valid JSON/);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('writeCheckpoint / readCheckpoint', () => {
|
|
149
|
+
it('round-trips state correctly', () => {
|
|
150
|
+
const state = {
|
|
151
|
+
plan: { subtasks: [{ title: 'A' }], branch_name: 'feat/test' },
|
|
152
|
+
completed: 1,
|
|
153
|
+
session_ids: ['abc-123'],
|
|
154
|
+
budget_used: 0.5,
|
|
155
|
+
cumulative_context: 'Step 1 done',
|
|
156
|
+
};
|
|
157
|
+
const filePath = path.join(tmpDir, 'checkpoint-test.json');
|
|
158
|
+
|
|
159
|
+
writeCheckpoint(filePath, state);
|
|
160
|
+
const loaded = readCheckpoint(filePath);
|
|
161
|
+
|
|
162
|
+
assert.deepStrictEqual(loaded, state);
|
|
163
|
+
|
|
164
|
+
// Cleanup
|
|
165
|
+
fs.unlinkSync(filePath);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('formatReport', () => {
|
|
170
|
+
it('formats a successful report with all fields', () => {
|
|
171
|
+
const report = {
|
|
172
|
+
success: true,
|
|
173
|
+
branch: 'feat/hello',
|
|
174
|
+
commit: 'abc1234',
|
|
175
|
+
concerns: ['Minor style issue'],
|
|
176
|
+
summary: 'Added hello endpoint',
|
|
177
|
+
};
|
|
178
|
+
const formatted = formatReport(report);
|
|
179
|
+
|
|
180
|
+
assert.ok(formatted.includes('Status: SUCCESS'));
|
|
181
|
+
assert.ok(formatted.includes('Branch: feat/hello'));
|
|
182
|
+
assert.ok(formatted.includes('Commit: abc1234'));
|
|
183
|
+
assert.ok(formatted.includes('Minor style issue'));
|
|
184
|
+
assert.ok(formatted.includes('Summary: Added hello endpoint'));
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('formats a failed report with error', () => {
|
|
188
|
+
const report = {
|
|
189
|
+
success: false,
|
|
190
|
+
error: 'Budget exceeded',
|
|
191
|
+
};
|
|
192
|
+
const formatted = formatReport(report);
|
|
193
|
+
|
|
194
|
+
assert.ok(formatted.includes('Status: FAILED'));
|
|
195
|
+
assert.ok(formatted.includes('Error: Budget exceeded'));
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('runHeadless with mock claude', () => {
|
|
200
|
+
it('runs mock claude and returns success with output', async () => {
|
|
201
|
+
// Prepend tmpDir to PATH so mock claude is found first
|
|
202
|
+
process.env.PATH = tmpDir + ':' + originalPath;
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const result = await runHeadless('implement the feature', { cwd: tmpDir, timeoutMs: 10000 });
|
|
206
|
+
|
|
207
|
+
assert.equal(result.success, true, `expected success but got exitCode=${result.exitCode}, stderr=${result.stderr}`);
|
|
208
|
+
assert.ok(result.output.includes('"result":"done"'), 'output should contain result JSON');
|
|
209
|
+
assert.ok(result.sessionId, 'should have a sessionId');
|
|
210
|
+
assert.equal(result.exitCode, 0);
|
|
211
|
+
|
|
212
|
+
// Verify the mock actually wrote to server.js
|
|
213
|
+
const serverContent = fs.readFileSync(path.join(tmpDir, 'server.js'), 'utf8');
|
|
214
|
+
assert.ok(serverContent.includes('// Added by coding agent'), 'mock should have appended comment to server.js');
|
|
215
|
+
} finally {
|
|
216
|
+
// Restore PATH
|
|
217
|
+
process.env.PATH = originalPath;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('runs mock claude planning mode when prompt contains "senior engineer"', async () => {
|
|
222
|
+
process.env.PATH = tmpDir + ':' + originalPath;
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const result = await runHeadless('You are a senior engineer planning a task', { cwd: tmpDir, timeoutMs: 10000 });
|
|
226
|
+
|
|
227
|
+
assert.equal(result.success, true, `expected success but got exitCode=${result.exitCode}, stderr=${result.stderr}`);
|
|
228
|
+
assert.ok(result.output.includes('"subtasks"'), 'output should contain plan JSON with subtasks');
|
|
229
|
+
assert.ok(result.output.includes('feat/hello'), 'output should contain branch name');
|
|
230
|
+
|
|
231
|
+
// Verify the plan can be parsed
|
|
232
|
+
const plan = parsePlan(result.output);
|
|
233
|
+
assert.equal(plan.subtasks.length, 1);
|
|
234
|
+
assert.equal(plan.subtasks[0].title, 'Add hello endpoint');
|
|
235
|
+
} finally {
|
|
236
|
+
process.env.PATH = originalPath;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { describe, it, before, after } = require('node:test');
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
getFileTree,
|
|
10
|
+
readClaudeMd,
|
|
11
|
+
getGitLog,
|
|
12
|
+
detectTestCommand,
|
|
13
|
+
findRelevantFiles,
|
|
14
|
+
searchBrain,
|
|
15
|
+
queryMcp,
|
|
16
|
+
assembleContext,
|
|
17
|
+
} = require('../coding-context');
|
|
18
|
+
|
|
19
|
+
describe('coding-context', () => {
|
|
20
|
+
let tmpDir;
|
|
21
|
+
|
|
22
|
+
before(() => {
|
|
23
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'coding-ctx-'));
|
|
24
|
+
// Create a small project structure
|
|
25
|
+
fs.mkdirSync(path.join(tmpDir, 'src'));
|
|
26
|
+
fs.mkdirSync(path.join(tmpDir, 'src', 'utils'));
|
|
27
|
+
fs.mkdirSync(path.join(tmpDir, 'node_modules'));
|
|
28
|
+
fs.mkdirSync(path.join(tmpDir, '.git'));
|
|
29
|
+
fs.mkdirSync(path.join(tmpDir, 'dist'));
|
|
30
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'index.js'), 'console.log("hello");');
|
|
31
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'utils', 'helper.js'), 'module.exports = {};');
|
|
32
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'auth-service.js'), 'module.exports = { login() {} };');
|
|
33
|
+
fs.writeFileSync(path.join(tmpDir, 'node_modules', 'foo.js'), 'ignored');
|
|
34
|
+
fs.writeFileSync(path.join(tmpDir, '.git', 'config'), 'ignored');
|
|
35
|
+
fs.writeFileSync(path.join(tmpDir, 'dist', 'bundle.js'), 'ignored');
|
|
36
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Project Rules\nUse strict mode.');
|
|
37
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({
|
|
38
|
+
name: 'test-project',
|
|
39
|
+
scripts: { test: 'jest', start: 'node src/index.js' },
|
|
40
|
+
}));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
after(() => {
|
|
44
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('getFileTree', () => {
|
|
48
|
+
it('includes source files with relative paths', async () => {
|
|
49
|
+
const tree = await getFileTree(tmpDir);
|
|
50
|
+
assert.ok(tree.includes('src/'), `tree should include src/ directory: ${tree}`);
|
|
51
|
+
assert.ok(tree.includes('src/index.js'), `tree should include src/index.js: ${tree}`);
|
|
52
|
+
assert.ok(tree.includes('src/utils/helper.js'), `tree should include nested file: ${tree}`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('excludes node_modules, .git, and dist', async () => {
|
|
56
|
+
const tree = await getFileTree(tmpDir);
|
|
57
|
+
assert.ok(!tree.includes('node_modules'), `tree should exclude node_modules: ${tree}`);
|
|
58
|
+
assert.ok(!tree.includes('.git'), `tree should exclude .git: ${tree}`);
|
|
59
|
+
assert.ok(!tree.includes('dist'), `tree should exclude dist: ${tree}`);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('marks directories with trailing slash', async () => {
|
|
63
|
+
const tree = await getFileTree(tmpDir);
|
|
64
|
+
const lines = tree.split('\n').filter(Boolean);
|
|
65
|
+
const srcLine = lines.find(l => l === 'src/');
|
|
66
|
+
assert.ok(srcLine, `should have src/ with trailing slash: ${tree}`);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('respects maxDepth', async () => {
|
|
70
|
+
const tree = await getFileTree(tmpDir, 1);
|
|
71
|
+
assert.ok(tree.includes('src/'), 'should include top-level dir');
|
|
72
|
+
assert.ok(!tree.includes('src/utils/'), 'should not include deeper dirs at depth 1');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('readClaudeMd', () => {
|
|
77
|
+
it('reads existing CLAUDE.md', async () => {
|
|
78
|
+
const content = await readClaudeMd(tmpDir);
|
|
79
|
+
assert.ok(content.includes('# Project Rules'));
|
|
80
|
+
assert.ok(content.includes('Use strict mode.'));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns empty string when CLAUDE.md missing', async () => {
|
|
84
|
+
const emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'no-claude-'));
|
|
85
|
+
try {
|
|
86
|
+
const content = await readClaudeMd(emptyDir);
|
|
87
|
+
assert.equal(content, '');
|
|
88
|
+
} finally {
|
|
89
|
+
fs.rmSync(emptyDir, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('getGitLog', () => {
|
|
95
|
+
it('returns empty string for non-git directory', async () => {
|
|
96
|
+
const nonGit = fs.mkdtempSync(path.join(os.tmpdir(), 'no-git-'));
|
|
97
|
+
try {
|
|
98
|
+
const log = await getGitLog(nonGit);
|
|
99
|
+
assert.equal(log, '');
|
|
100
|
+
} finally {
|
|
101
|
+
fs.rmSync(nonGit, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('returns git log for a real git repo', async () => {
|
|
106
|
+
// Use the actual tools repo
|
|
107
|
+
const log = await getGitLog(process.cwd(), 5);
|
|
108
|
+
assert.ok(log.length > 0, 'should return some log output');
|
|
109
|
+
const lines = log.trim().split('\n');
|
|
110
|
+
assert.ok(lines.length <= 5, 'should respect count limit');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('detectTestCommand', () => {
|
|
115
|
+
it('detects npm test from package.json', async () => {
|
|
116
|
+
const cmd = await detectTestCommand(tmpDir);
|
|
117
|
+
assert.equal(cmd, 'npm test');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('detects make test from Makefile', async () => {
|
|
121
|
+
const makeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'makefile-'));
|
|
122
|
+
try {
|
|
123
|
+
fs.writeFileSync(path.join(makeDir, 'Makefile'), 'test:\n\tpytest\n');
|
|
124
|
+
const cmd = await detectTestCommand(makeDir);
|
|
125
|
+
assert.equal(cmd, 'make test');
|
|
126
|
+
} finally {
|
|
127
|
+
fs.rmSync(makeDir, { recursive: true, force: true });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('returns null when no test config found', async () => {
|
|
132
|
+
const emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'no-test-'));
|
|
133
|
+
try {
|
|
134
|
+
const cmd = await detectTestCommand(emptyDir);
|
|
135
|
+
assert.equal(cmd, null);
|
|
136
|
+
} finally {
|
|
137
|
+
fs.rmSync(emptyDir, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('findRelevantFiles', () => {
|
|
143
|
+
it('finds files matching keywords in request', async () => {
|
|
144
|
+
const result = await findRelevantFiles(tmpDir, 'auth service login');
|
|
145
|
+
assert.ok('src/auth-service.js' in result, `should find auth-service.js: ${Object.keys(result)}`);
|
|
146
|
+
assert.ok(result['src/auth-service.js'].includes('login'));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('returns empty map when no matches', async () => {
|
|
150
|
+
const result = await findRelevantFiles(tmpDir, 'zzzznonexistent');
|
|
151
|
+
assert.equal(Object.keys(result).length, 0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('respects maxFiles limit', async () => {
|
|
155
|
+
const result = await findRelevantFiles(tmpDir, 'js helper index auth', 1);
|
|
156
|
+
assert.ok(Object.keys(result).length <= 1);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('searchBrain', () => {
|
|
161
|
+
it('returns empty array when brain is null', () => {
|
|
162
|
+
const result = searchBrain(null, 'test query');
|
|
163
|
+
assert.deepEqual(result, []);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('calls findKnowledge when brain has it', () => {
|
|
167
|
+
const fakeBrain = {
|
|
168
|
+
findKnowledge: ({ subject }) => [{ id: 1, subject, predicate: 'is', object: 'relevant', status: 'active' }],
|
|
169
|
+
};
|
|
170
|
+
const result = searchBrain(fakeBrain, 'test query coding', 5);
|
|
171
|
+
assert.ok(result.length >= 1);
|
|
172
|
+
assert.equal(result[0].id, 1);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('queryMcp', () => {
|
|
177
|
+
it('returns empty array when mcpClient is null', async () => {
|
|
178
|
+
const result = await queryMcp(null, 'test');
|
|
179
|
+
assert.deepEqual(result, []);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('calls search-type tools from mcpClient', async () => {
|
|
183
|
+
const fakeMcp = {
|
|
184
|
+
listTools: async () => [
|
|
185
|
+
{ name: 'search_docs', description: 'Search documentation' },
|
|
186
|
+
{ name: 'deploy_app', description: 'Deploy application' },
|
|
187
|
+
],
|
|
188
|
+
callTool: async (name, args) => ({ results: ['found'] }),
|
|
189
|
+
};
|
|
190
|
+
const result = await queryMcp(fakeMcp, 'find docs');
|
|
191
|
+
assert.ok(result.length > 0);
|
|
192
|
+
assert.equal(result[0].tool, 'search_docs');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('assembleContext', () => {
|
|
197
|
+
it('gathers all context in parallel', async () => {
|
|
198
|
+
const ctx = await assembleContext('auth login feature', tmpDir, {});
|
|
199
|
+
assert.ok('fileTree' in ctx);
|
|
200
|
+
assert.ok('claudeMd' in ctx);
|
|
201
|
+
assert.ok('gitLog' in ctx);
|
|
202
|
+
assert.ok('testCommand' in ctx);
|
|
203
|
+
assert.ok('relevantFiles' in ctx);
|
|
204
|
+
assert.ok('brainMemories' in ctx);
|
|
205
|
+
assert.ok('mcpResults' in ctx);
|
|
206
|
+
assert.ok(ctx.claudeMd.includes('Project Rules'));
|
|
207
|
+
assert.equal(ctx.testCommand, 'npm test');
|
|
208
|
+
assert.ok(Array.isArray(ctx.brainMemories));
|
|
209
|
+
assert.ok(Array.isArray(ctx.mcpResults));
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
});
|