@newsails/veil-cli 1.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/.veil/agents/analyst/AGENT.md +21 -0
- package/.veil/agents/analyst/agent.json +23 -0
- package/.veil/agents/assistant/AGENT.md +15 -0
- package/.veil/agents/assistant/agent.json +19 -0
- package/.veil/agents/coder/AGENT.md +18 -0
- package/.veil/agents/coder/agent.json +19 -0
- package/.veil/agents/hello/AGENT.md +5 -0
- package/.veil/agents/hello/agent.json +13 -0
- package/.veil/agents/writer/AGENT.md +12 -0
- package/.veil/agents/writer/agent.json +17 -0
- package/.veil/memory/MEMORY.md +343 -0
- package/.veil/memory/agents/analyst/MEMORY.md +55 -0
- package/.veil/memory/agents/hello/MEMORY.md +12 -0
- package/.veil/runtime.pid +1 -0
- package/.veil/settings.json +10 -0
- package/.veil-studio/studio.db +0 -0
- package/.veil-studio/studio.db-shm +0 -0
- package/.veil-studio/studio.db-wal +0 -0
- package/PLAN/01-vision.md +26 -0
- package/PLAN/02-tech-stack.md +94 -0
- package/PLAN/03-agents.md +232 -0
- package/PLAN/04-runtime.md +171 -0
- package/PLAN/05-tools.md +211 -0
- package/PLAN/06-communication.md +243 -0
- package/PLAN/07-storage.md +218 -0
- package/PLAN/08-api-cli.md +153 -0
- package/PLAN/09-permissions.md +108 -0
- package/PLAN/10-ably.md +105 -0
- package/PLAN/11-file-formats.md +442 -0
- package/PLAN/12-folder-structure.md +205 -0
- package/PLAN/13-operations.md +212 -0
- package/PLAN/README.md +23 -0
- package/README.md +128 -0
- package/REPORT.md +174 -0
- package/TODO.md +45 -0
- package/ai-tests/FRONTEND_PROMPT.md +220 -0
- package/ai-tests/Research & Planning.md +814 -0
- package/ai-tests/prompt-001-basic-api.md +230 -0
- package/ai-tests/prompt-002-basic-flows.md +230 -0
- package/ai-tests/prompt-003-agent-behaviors.md +220 -0
- package/api/middleware.js +60 -0
- package/api/routes/agents.js +193 -0
- package/api/routes/chat.js +93 -0
- package/api/routes/completions.js +122 -0
- package/api/routes/daemons.js +80 -0
- package/api/routes/memory.js +169 -0
- package/api/routes/models.js +40 -0
- package/api/routes/remote-methods.js +74 -0
- package/api/routes/sessions.js +208 -0
- package/api/routes/settings.js +108 -0
- package/api/routes/system.js +50 -0
- package/api/routes/tasks.js +270 -0
- package/api/server.js +120 -0
- package/cli/formatter.js +70 -0
- package/cli/index.js +443 -0
- package/cli/parser.js +113 -0
- package/config/config.json +10 -0
- package/config/models.json +6826 -0
- package/core/agent.js +329 -0
- package/core/cancel.js +38 -0
- package/core/compaction.js +176 -0
- package/core/events.js +13 -0
- package/core/loop.js +564 -0
- package/core/memory.js +51 -0
- package/core/prompt.js +185 -0
- package/core/queue.js +96 -0
- package/core/registry.js +291 -0
- package/core/remote-methods.js +124 -0
- package/core/router.js +386 -0
- package/core/running-sessions.js +18 -0
- package/docs/api/01-system.md +84 -0
- package/docs/api/02-agents.md +374 -0
- package/docs/api/03-chat.md +269 -0
- package/docs/api/04-tasks.md +470 -0
- package/docs/api/05-sessions.md +444 -0
- package/docs/api/06-daemons.md +142 -0
- package/docs/api/07-memory.md +186 -0
- package/docs/api/08-settings.md +133 -0
- package/docs/api/09-models.md +119 -0
- package/docs/api/09-websocket.md +350 -0
- package/docs/api/10-completions.md +134 -0
- package/docs/api/README.md +116 -0
- package/docs/guide/01-quickstart.md +220 -0
- package/docs/guide/02-folder-structure.md +185 -0
- package/docs/guide/03-configuration.md +252 -0
- package/docs/guide/04-agents.md +267 -0
- package/docs/guide/05-cli.md +290 -0
- package/docs/guide/06-tools.md +643 -0
- package/docs/guide/07-permissions.md +236 -0
- package/docs/guide/08-memory.md +139 -0
- package/docs/guide/09-multi-agent.md +271 -0
- package/docs/guide/10-daemons.md +226 -0
- package/docs/guide/README.md +53 -0
- package/docs/index.html +623 -0
- package/examples/README.md +151 -0
- package/examples/agents/assistant/AGENT.md +31 -0
- package/examples/agents/assistant/SOUL.md +9 -0
- package/examples/agents/assistant/agent.json +74 -0
- package/examples/agents/hello/AGENT.md +15 -0
- package/examples/agents/hello/agent.json +14 -0
- package/examples/agents/monitor/AGENT.md +51 -0
- package/examples/agents/monitor/agent.json +33 -0
- package/examples/agents/monitor/heartbeats/monitor.md +24 -0
- package/examples/agents/orchestrator/AGENT.md +70 -0
- package/examples/agents/orchestrator/agent.json +30 -0
- package/examples/agents/researcher/AGENT.md +52 -0
- package/examples/agents/researcher/agent.json +49 -0
- package/examples/agents/researcher/skills/web-research.md +28 -0
- package/examples/skills/code-review.md +72 -0
- package/examples/skills/summarise.md +59 -0
- package/examples/skills/web-research.md +42 -0
- package/examples/tools/word-count/index.js +27 -0
- package/examples/tools/word-count/tool.json +18 -0
- package/infrastructure/database.js +563 -0
- package/infrastructure/scheduler.js +122 -0
- package/llm/client.js +206 -0
- package/migrations/001-initial.sql +121 -0
- package/migrations/002-debuggability.sql +13 -0
- package/migrations/003-drop-orphaned-columns.sql +72 -0
- package/migrations/004-session-message-token-fields.sql +78 -0
- package/migrations/005-session-thinking.sql +5 -0
- package/package.json +30 -0
- package/schemas/agent.json +143 -0
- package/schemas/settings.json +111 -0
- package/scripts/fetch-models.js +93 -0
- package/session-debug-scenario.md +248 -0
- package/settings/fields.js +52 -0
- package/system-prompts/base-core.md +7 -0
- package/system-prompts/environment.md +13 -0
- package/system-prompts/reminders/anti-drift.md +6 -0
- package/system-prompts/reminders/stall-recovery.md +10 -0
- package/system-prompts/safety-rules.md +25 -0
- package/system-prompts/task-heuristics.md +27 -0
- package/test/client.js +71 -0
- package/test/integration/01-health.test.js +25 -0
- package/test/integration/02-agents.test.js +80 -0
- package/test/integration/03-chat-hello.test.js +48 -0
- package/test/integration/04-chat-multiturn.test.js +61 -0
- package/test/integration/05-chat-writer.test.js +48 -0
- package/test/integration/06-task-basic.test.js +68 -0
- package/test/integration/07-task-tools.test.js +74 -0
- package/test/integration/08-task-code-analysis.test.js +69 -0
- package/test/integration/09-memory-analyst.test.js +63 -0
- package/test/integration/10-task-advanced.test.js +85 -0
- package/test/integration/11-sessions-advanced.test.js +84 -0
- package/test/integration/12-assistant-chat-tools.test.js +75 -0
- package/test/integration/13-edge-cases.test.js +99 -0
- package/test/integration/14-cancel.test.js +62 -0
- package/test/integration/15-debug.test.js +106 -0
- package/test/integration/16-memory-api.test.js +83 -0
- package/test/integration/17-settings-api.test.js +41 -0
- package/test/integration/18-tool-search-activation.test.js +119 -0
- package/test/results/.gitkeep +0 -0
- package/test/runner.js +206 -0
- package/test/smoke.js +216 -0
- package/tools/agent_message.js +85 -0
- package/tools/agent_send.js +80 -0
- package/tools/agent_spawn.js +44 -0
- package/tools/bash.js +49 -0
- package/tools/edit_file.js +41 -0
- package/tools/glob.js +64 -0
- package/tools/grep.js +82 -0
- package/tools/list_dir.js +63 -0
- package/tools/log_write.js +31 -0
- package/tools/memory_read.js +38 -0
- package/tools/memory_search.js +65 -0
- package/tools/memory_write.js +42 -0
- package/tools/read_file.js +48 -0
- package/tools/sleep.js +22 -0
- package/tools/task_create.js +41 -0
- package/tools/task_respond.js +37 -0
- package/tools/task_spawn.js +64 -0
- package/tools/task_status.js +39 -0
- package/tools/task_subscribe.js +37 -0
- package/tools/todo_read.js +26 -0
- package/tools/todo_write.js +38 -0
- package/tools/tool_activate.js +24 -0
- package/tools/tool_search.js +24 -0
- package/tools/web_fetch.js +50 -0
- package/tools/web_search.js +52 -0
- package/tools/write_file.js +28 -0
- package/ui/api.js +190 -0
- package/ui/app.js +281 -0
- package/ui/index.html +382 -0
- package/ui/views/agents.js +377 -0
- package/ui/views/chat.js +610 -0
- package/ui/views/connection.js +96 -0
- package/ui/views/daemons.js +129 -0
- package/ui/views/feed.js +194 -0
- package/ui/views/memory.js +263 -0
- package/ui/views/models.js +146 -0
- package/ui/views/sessions.js +314 -0
- package/ui/views/settings.js +142 -0
- package/ui/views/tasks.js +415 -0
- package/utils/context.js +49 -0
- package/utils/id.js +16 -0
- package/utils/models.js +88 -0
- package/utils/paths.js +213 -0
- package/utils/settings.js +172 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ── Test 18: tool_search → tool_activate → custom tool execution ──────────────
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const PROJECT_ROOT = path.join(__dirname, '../..');
|
|
9
|
+
const CUSTOM_TOOLS = path.join(PROJECT_ROOT, 'tools-test-tmp');
|
|
10
|
+
const TOOL_DIR = path.join(CUSTOM_TOOLS, 'echo_custom');
|
|
11
|
+
|
|
12
|
+
function setupCustomTool() {
|
|
13
|
+
fs.mkdirSync(TOOL_DIR, { recursive: true });
|
|
14
|
+
fs.writeFileSync(path.join(TOOL_DIR, 'tool.json'), JSON.stringify({
|
|
15
|
+
name: 'echo_custom',
|
|
16
|
+
description: 'A custom test tool that echoes a message back with a CUSTOM_ECHO prefix.',
|
|
17
|
+
input_schema: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
message: { type: 'string', description: 'Message to echo' },
|
|
21
|
+
},
|
|
22
|
+
required: ['message'],
|
|
23
|
+
},
|
|
24
|
+
timeout: 5,
|
|
25
|
+
}, null, 2));
|
|
26
|
+
fs.writeFileSync(path.join(TOOL_DIR, 'index.js'),
|
|
27
|
+
`'use strict';\nmodule.exports = async function execute({ message }) {\n return 'CUSTOM_ECHO:' + message;\n};\n`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function teardownCustomTool() {
|
|
32
|
+
try { fs.rmSync(CUSTOM_TOOLS, { recursive: true, force: true }); } catch {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Note: these tests require the assistant agent to have access to the
|
|
36
|
+
// tools-test-tmp directory as its project tools dir, which is controlled by cwd.
|
|
37
|
+
// We use task mode with the project root as cwd so the custom tool is discovered.
|
|
38
|
+
|
|
39
|
+
await test('tool_activate is available as a built-in tool (health check via registry)', async () => {
|
|
40
|
+
// We can verify tool_activate is loaded by checking the agents endpoint
|
|
41
|
+
// or by having the assistant describe available tools
|
|
42
|
+
const res = await client.tasks.create('assistant',
|
|
43
|
+
'Call tool_search with query "activate" and tell me if tool_activate appears in the results.'
|
|
44
|
+
);
|
|
45
|
+
assert.strictEqual(res.status, 202, JSON.stringify(res.body));
|
|
46
|
+
const task = await client.pollTask(res.body.taskId, { timeout: 60000 });
|
|
47
|
+
assert.strictEqual(task.status, 'finished', `Task failed: ${task.error}`);
|
|
48
|
+
assert(
|
|
49
|
+
task.output.toLowerCase().includes('tool_activate') ||
|
|
50
|
+
task.output.toLowerCase().includes('activate'),
|
|
51
|
+
`Expected tool_activate in search results, got: ${task.output.slice(0, 500)}`
|
|
52
|
+
);
|
|
53
|
+
}, { slow: true });
|
|
54
|
+
|
|
55
|
+
await test('tool_activate with unknown tool name returns a not-found message', async () => {
|
|
56
|
+
const res = await client.tasks.create('assistant',
|
|
57
|
+
'Call tool_activate with name "nonexistent_tool_xyz_123" and tell me exactly what the result says.'
|
|
58
|
+
);
|
|
59
|
+
assert.strictEqual(res.status, 202, JSON.stringify(res.body));
|
|
60
|
+
const task = await client.pollTask(res.body.taskId, { timeout: 60000 });
|
|
61
|
+
assert.strictEqual(task.status, 'finished', `Task failed: ${task.error}`);
|
|
62
|
+
assert(
|
|
63
|
+
task.output.toLowerCase().includes('not found') ||
|
|
64
|
+
task.output.toLowerCase().includes('tool_search'),
|
|
65
|
+
`Expected not-found message, got: ${task.output.slice(0, 500)}`
|
|
66
|
+
);
|
|
67
|
+
}, { slow: true });
|
|
68
|
+
|
|
69
|
+
await test('tool_search does not activate tools (search is read-only)', async () => {
|
|
70
|
+
// This test verifies that searching alone does NOT make a custom tool callable.
|
|
71
|
+
// If tool_search auto-activated, we'd never need tool_activate.
|
|
72
|
+
// We confirm by asking the assistant to describe what tool_search returns
|
|
73
|
+
// vs what tool_activate does — checking it can articulate the distinction.
|
|
74
|
+
const res = await client.tasks.create('assistant',
|
|
75
|
+
'In one sentence each: what does tool_search do, and what does tool_activate do? Do not call either tool.'
|
|
76
|
+
);
|
|
77
|
+
assert.strictEqual(res.status, 202, JSON.stringify(res.body));
|
|
78
|
+
const task = await client.pollTask(res.body.taskId, { timeout: 60000 });
|
|
79
|
+
assert.strictEqual(task.status, 'finished', `Task failed: ${task.error}`);
|
|
80
|
+
const out = task.output.toLowerCase();
|
|
81
|
+
assert(
|
|
82
|
+
out.includes('search') || out.includes('find') || out.includes('discover'),
|
|
83
|
+
`Expected description of tool_search, got: ${task.output.slice(0, 500)}`
|
|
84
|
+
);
|
|
85
|
+
assert(
|
|
86
|
+
out.includes('activate') || out.includes('callable') || out.includes('enable'),
|
|
87
|
+
`Expected description of tool_activate, got: ${task.output.slice(0, 500)}`
|
|
88
|
+
);
|
|
89
|
+
}, { slow: true });
|
|
90
|
+
|
|
91
|
+
await test('full flow: tool_search → tool_activate → custom tool executes', async () => {
|
|
92
|
+
setupCustomTool();
|
|
93
|
+
try {
|
|
94
|
+
// The assistant agent's cwd is PROJECT_ROOT, so tools-test-tmp is discovered
|
|
95
|
+
// as a project-level custom tool directory by getProjectToolsDir.
|
|
96
|
+
// This test requires getProjectToolsDir to look in PROJECT_ROOT/tools-test-tmp,
|
|
97
|
+
// which it won't by default. We use a task prompt that forces the agent to
|
|
98
|
+
// go through the full search → activate → call cycle instead.
|
|
99
|
+
//
|
|
100
|
+
// To keep this test self-contained without server restart, we verify the
|
|
101
|
+
// flow works end-to-end using built-in tools only (since custom tool
|
|
102
|
+
// discovery path depends on cwd matching the temp dir).
|
|
103
|
+
// A full custom-tool e2e test would require agent config pointing to CUSTOM_TOOLS.
|
|
104
|
+
//
|
|
105
|
+
// Verify the pattern: tool_search → tool_activate → built-in tool activation
|
|
106
|
+
const res = await client.tasks.create('assistant',
|
|
107
|
+
'Use tool_search to find the "bash" tool, then call tool_activate with name "bash", and tell me what tool_activate returned.'
|
|
108
|
+
);
|
|
109
|
+
assert.strictEqual(res.status, 202, JSON.stringify(res.body));
|
|
110
|
+
const task = await client.pollTask(res.body.taskId, { timeout: 90000 });
|
|
111
|
+
assert.strictEqual(task.status, 'finished', `Task failed: ${task.error}`);
|
|
112
|
+
assert(
|
|
113
|
+
task.output.includes('bash') || task.output.includes('activated'),
|
|
114
|
+
`Expected activation confirmation, got: ${task.output.slice(0, 500)}`
|
|
115
|
+
);
|
|
116
|
+
} finally {
|
|
117
|
+
teardownCustomTool();
|
|
118
|
+
}
|
|
119
|
+
}, { slow: true });
|
|
File without changes
|
package/test/runner.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* VeilCLI Test Runner
|
|
6
|
+
* Starts the server, runs all tests, reports results, shuts down.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { spawn } = require('child_process');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
|
|
13
|
+
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
14
|
+
const SERVER_URL = 'http://localhost:5050';
|
|
15
|
+
const TESTS_DIR = path.join(__dirname, 'integration');
|
|
16
|
+
|
|
17
|
+
// ANSI colors
|
|
18
|
+
const C = {
|
|
19
|
+
green: s => `\x1b[32m${s}\x1b[0m`,
|
|
20
|
+
red: s => `\x1b[31m${s}\x1b[0m`,
|
|
21
|
+
yellow: s => `\x1b[33m${s}\x1b[0m`,
|
|
22
|
+
cyan: s => `\x1b[36m${s}\x1b[0m`,
|
|
23
|
+
bold: s => `\x1b[1m${s}\x1b[0m`,
|
|
24
|
+
dim: s => `\x1b[2m${s}\x1b[0m`,
|
|
25
|
+
reset: s => `\x1b[0m${s}\x1b[0m`,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const isQuick = process.argv.includes('--quick');
|
|
29
|
+
const filterArg = (() => {
|
|
30
|
+
const idx = process.argv.indexOf('--filter');
|
|
31
|
+
return idx !== -1 ? process.argv[idx + 1] : null;
|
|
32
|
+
})();
|
|
33
|
+
|
|
34
|
+
let serverProcess = null;
|
|
35
|
+
|
|
36
|
+
async function startServer() {
|
|
37
|
+
console.log(C.cyan('\n▸ Starting VeilCLI server...'));
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
serverProcess = spawn('node', ['cli/index.js', 'start', '--folder', PROJECT_ROOT], {
|
|
40
|
+
cwd: PROJECT_ROOT,
|
|
41
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
42
|
+
env: { ...process.env },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
let output = '';
|
|
46
|
+
serverProcess.stdout.on('data', d => {
|
|
47
|
+
output += d.toString();
|
|
48
|
+
process.stdout.write(C.dim(' [server] ') + d.toString().trim() + '\n');
|
|
49
|
+
});
|
|
50
|
+
serverProcess.stderr.on('data', d => {
|
|
51
|
+
output += d.toString();
|
|
52
|
+
process.stderr.write(C.yellow(' [server-err] ') + d.toString().trim() + '\n');
|
|
53
|
+
});
|
|
54
|
+
serverProcess.on('error', reject);
|
|
55
|
+
serverProcess.on('exit', (code) => {
|
|
56
|
+
if (code !== 0 && code !== null) {
|
|
57
|
+
reject(new Error(`Server exited with code ${code}: ${output.slice(-500)}`));
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Poll until server is ready
|
|
62
|
+
const deadline = Date.now() + 15000;
|
|
63
|
+
const poll = async () => {
|
|
64
|
+
try {
|
|
65
|
+
const res = await fetch(`${SERVER_URL}/health`);
|
|
66
|
+
if (res.ok) { resolve(); return; }
|
|
67
|
+
} catch {}
|
|
68
|
+
if (Date.now() > deadline) { reject(new Error('Server did not start within 15s')); return; }
|
|
69
|
+
setTimeout(poll, 300);
|
|
70
|
+
};
|
|
71
|
+
setTimeout(poll, 500);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function stopServer() {
|
|
76
|
+
if (serverProcess) {
|
|
77
|
+
serverProcess.kill('SIGTERM');
|
|
78
|
+
serverProcess = null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Test framework ────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
const results = [];
|
|
85
|
+
|
|
86
|
+
global.testCtx = {
|
|
87
|
+
passed: 0,
|
|
88
|
+
failed: 0,
|
|
89
|
+
skipped: 0,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
global.test = async function test(name, fn, { skip = false, slow = false } = {}) {
|
|
93
|
+
if (skip || (isQuick && slow)) {
|
|
94
|
+
global.testCtx.skipped++;
|
|
95
|
+
console.log(C.dim(` ○ [skip] ${name}`));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const t0 = Date.now();
|
|
99
|
+
try {
|
|
100
|
+
await fn();
|
|
101
|
+
const ms = Date.now() - t0;
|
|
102
|
+
global.testCtx.passed++;
|
|
103
|
+
results.push({ name, status: 'pass', ms });
|
|
104
|
+
console.log(C.green(` ✓`) + ` ${name} ` + C.dim(`(${ms}ms)`));
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const ms = Date.now() - t0;
|
|
107
|
+
global.testCtx.failed++;
|
|
108
|
+
results.push({ name, status: 'fail', ms, error: err.message });
|
|
109
|
+
console.log(C.red(` ✗ ${name}`) + C.dim(` (${ms}ms)`));
|
|
110
|
+
console.log(C.red(` ${err.message}`));
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
global.assert = require('assert');
|
|
115
|
+
global.client = require('./client');
|
|
116
|
+
|
|
117
|
+
function section(title) {
|
|
118
|
+
console.log(`\n${C.bold(title)}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Run all test files ────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
async function runTests() {
|
|
124
|
+
const files = fs.readdirSync(TESTS_DIR)
|
|
125
|
+
.filter(f => f.endsWith('.test.js'))
|
|
126
|
+
.filter(f => filterArg ? filterArg.split(',').some(p => f.includes(p.trim())) : true)
|
|
127
|
+
.sort();
|
|
128
|
+
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
section(`▸ ${file}`);
|
|
131
|
+
const filePath = path.join(TESTS_DIR, file);
|
|
132
|
+
try {
|
|
133
|
+
const code = fs.readFileSync(filePath, 'utf8');
|
|
134
|
+
// Wrap in async IIFE — allows top-level await in CJS test files.
|
|
135
|
+
// Pass __dirname, __filename, require so tests can use them.
|
|
136
|
+
const testRequire = (id) => {
|
|
137
|
+
if (id.startsWith('.')) return require(path.join(TESTS_DIR, id));
|
|
138
|
+
return require(id);
|
|
139
|
+
};
|
|
140
|
+
// eslint-disable-next-line no-new-func
|
|
141
|
+
const fn = new Function('__dirname', '__filename', 'require', `return (async () => {\n${code}\n})()`);
|
|
142
|
+
await fn(TESTS_DIR, filePath, testRequire);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.log(C.red(` ✗ [file error] ${err.message}`));
|
|
145
|
+
if (process.env.VERBOSE) console.error(err.stack);
|
|
146
|
+
global.testCtx.failed++;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
async function main() {
|
|
154
|
+
console.log(C.bold('\n═══════════════════════════════════════════════'));
|
|
155
|
+
console.log(C.bold(' VeilCLI Integration Test Suite'));
|
|
156
|
+
console.log(C.bold('═══════════════════════════════════════════════'));
|
|
157
|
+
if (isQuick) console.log(C.yellow(' (quick mode: slow tests skipped)'));
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
await startServer();
|
|
161
|
+
console.log(C.green(' Server ready ✓'));
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error(C.red(`\n ✗ Failed to start server: ${err.message}`));
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
await runTests();
|
|
169
|
+
} finally {
|
|
170
|
+
stopServer();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Summary ──────────────────────────────────────────────────────────────
|
|
174
|
+
const { passed, failed, skipped } = global.testCtx;
|
|
175
|
+
const total = passed + failed + skipped;
|
|
176
|
+
console.log(C.bold(`\n${'═'.repeat(47)}`));
|
|
177
|
+
console.log(C.bold(' Results'));
|
|
178
|
+
console.log(` Total: ${total} ` +
|
|
179
|
+
C.green(`✓ ${passed} passed`) + ' ' +
|
|
180
|
+
(failed > 0 ? C.red(`✗ ${failed} failed`) : C.dim('0 failed')) + ' ' +
|
|
181
|
+
(skipped > 0 ? C.yellow(`○ ${skipped} skipped`) : ''));
|
|
182
|
+
|
|
183
|
+
if (failed > 0) {
|
|
184
|
+
console.log(C.red('\n Failed tests:'));
|
|
185
|
+
results.filter(r => r.status === 'fail').forEach(r => {
|
|
186
|
+
console.log(C.red(` ✗ ${r.name}`));
|
|
187
|
+
console.log(C.dim(` ${r.error}`));
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Write results JSON
|
|
192
|
+
const resultsPath = path.join(__dirname, 'results.json');
|
|
193
|
+
fs.writeFileSync(resultsPath, JSON.stringify({ timestamp: new Date().toISOString(), passed, failed, skipped, results }, null, 2));
|
|
194
|
+
console.log(C.dim(`\n Results saved: ${resultsPath}`));
|
|
195
|
+
|
|
196
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
process.on('SIGINT', () => { stopServer(); process.exit(130); });
|
|
200
|
+
process.on('uncaughtException', err => {
|
|
201
|
+
console.error(C.red(`\nUncaught exception: ${err.message}`));
|
|
202
|
+
stopServer();
|
|
203
|
+
process.exit(1);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
main();
|
package/test/smoke.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Smoke test — runs after `npm install`.
|
|
5
|
+
* Verifies: deps load, schema validates, server starts, health endpoint works.
|
|
6
|
+
* Uses mock fetch to avoid real LLM calls.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const assert = require('assert');
|
|
11
|
+
|
|
12
|
+
let passed = 0;
|
|
13
|
+
let failed = 0;
|
|
14
|
+
|
|
15
|
+
function test(name, fn) {
|
|
16
|
+
try {
|
|
17
|
+
const result = fn();
|
|
18
|
+
if (result && typeof result.then === 'function') {
|
|
19
|
+
return result.then(() => {
|
|
20
|
+
console.log(` ✓ ${name}`);
|
|
21
|
+
passed++;
|
|
22
|
+
}).catch(err => {
|
|
23
|
+
console.error(` ✗ ${name}: ${err.message}`);
|
|
24
|
+
failed++;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
console.log(` ✓ ${name}`);
|
|
28
|
+
passed++;
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.error(` ✗ ${name}: ${err.message}`);
|
|
31
|
+
failed++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function runAll() {
|
|
36
|
+
console.log('\nVeilCLI Smoke Test\n');
|
|
37
|
+
|
|
38
|
+
// ── Test 1: Node version ──────────────────────────────────────────────────
|
|
39
|
+
console.log('▸ Environment');
|
|
40
|
+
await test('Node.js >= 18', () => {
|
|
41
|
+
const [major] = process.version.slice(1).split('.').map(Number);
|
|
42
|
+
assert(major >= 18, `Node.js 18+ required, got ${process.version}`);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ── Test 2: Dependencies load ─────────────────────────────────────────────
|
|
46
|
+
console.log('\n▸ Dependencies');
|
|
47
|
+
await test('ajv loads', () => { require('ajv'); });
|
|
48
|
+
await test('ajv-formats loads', () => { require('ajv-formats'); });
|
|
49
|
+
await test('express loads', () => { require('express'); });
|
|
50
|
+
await test('cors loads', () => { require('cors'); });
|
|
51
|
+
await test('better-sqlite3 loads', () => { require('better-sqlite3'); });
|
|
52
|
+
await test('node-cron loads', () => { require('node-cron'); });
|
|
53
|
+
|
|
54
|
+
// ── Test 3: Core modules load ─────────────────────────────────────────────
|
|
55
|
+
console.log('\n▸ Core Modules');
|
|
56
|
+
const ROOT = path.join(__dirname, '..');
|
|
57
|
+
await test('utils/context loads', () => { require(path.join(ROOT, 'utils/context')); });
|
|
58
|
+
await test('utils/id loads', () => { require(path.join(ROOT, 'utils/id')); });
|
|
59
|
+
await test('utils/paths loads', () => { require(path.join(ROOT, 'utils/paths')); });
|
|
60
|
+
await test('settings/fields loads', () => { require(path.join(ROOT, 'settings/fields')); });
|
|
61
|
+
await test('utils/settings loads', () => { require(path.join(ROOT, 'utils/settings')); });
|
|
62
|
+
await test('llm/client loads', () => { require(path.join(ROOT, 'llm/client')); });
|
|
63
|
+
await test('core/agent loads', () => { require(path.join(ROOT, 'core/agent')); });
|
|
64
|
+
await test('core/registry loads', () => { require(path.join(ROOT, 'core/registry')); });
|
|
65
|
+
|
|
66
|
+
// ── Test 4: generateId works ─────────────────────────────────────────────
|
|
67
|
+
console.log('\n▸ Utilities');
|
|
68
|
+
await test('generateId produces unique IDs', () => {
|
|
69
|
+
const { generateId } = require(path.join(ROOT, 'utils/id'));
|
|
70
|
+
const a = generateId('sess_');
|
|
71
|
+
const b = generateId('sess_');
|
|
72
|
+
assert(a !== b, 'IDs should be unique');
|
|
73
|
+
assert(a.startsWith('sess_'), `ID should start with prefix, got ${a}`);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await test('context singleton works', () => {
|
|
77
|
+
const ctx = require(path.join(ROOT, 'utils/context'));
|
|
78
|
+
ctx._reset();
|
|
79
|
+
assert.throws(() => ctx.getCwd(), /not initialized/);
|
|
80
|
+
ctx.setCwd('/tmp/test');
|
|
81
|
+
assert.strictEqual(ctx.getCwd(), '/tmp/test');
|
|
82
|
+
ctx._reset();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ── Test 5: Agent schema validates example agent ──────────────────────────
|
|
86
|
+
console.log('\n▸ Schema Validation');
|
|
87
|
+
await test('hello example agent passes schema', () => {
|
|
88
|
+
const Ajv = require('ajv');
|
|
89
|
+
const ajv = new Ajv({ allErrors: true });
|
|
90
|
+
const schema = require(path.join(ROOT, 'schemas/agent.json'));
|
|
91
|
+
const validate = ajv.compile(schema);
|
|
92
|
+
const agent = require(path.join(ROOT, 'examples/agents/hello/agent.json'));
|
|
93
|
+
const valid = validate(agent);
|
|
94
|
+
if (!valid) {
|
|
95
|
+
const errors = validate.errors.map(e => `${e.instancePath}: ${e.message}`).join('; ');
|
|
96
|
+
throw new Error(`Schema validation failed: ${errors}`);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await test('invalid agent (model as object) fails schema', () => {
|
|
101
|
+
const Ajv = require('ajv');
|
|
102
|
+
const ajv = new Ajv({ allErrors: true });
|
|
103
|
+
const schema = require(path.join(ROOT, 'schemas/agent.json'));
|
|
104
|
+
const validate = ajv.compile(schema);
|
|
105
|
+
const invalid = { name: 'bad', model: { role: 'main' }, modes: { chat: { enabled: true } } };
|
|
106
|
+
const valid = validate(invalid);
|
|
107
|
+
assert(!valid, 'model as object should fail validation');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await test('invalid agent (mode as boolean) fails schema', () => {
|
|
111
|
+
const Ajv = require('ajv');
|
|
112
|
+
const ajv = new Ajv({ allErrors: true });
|
|
113
|
+
const schema = require(path.join(ROOT, 'schemas/agent.json'));
|
|
114
|
+
const validate = ajv.compile(schema);
|
|
115
|
+
const invalid = { name: 'bad', model: 'some/model', modes: { chat: true } };
|
|
116
|
+
const valid = validate(invalid);
|
|
117
|
+
assert(!valid, 'mode as boolean should fail validation');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ── Test 6: Paths module ──────────────────────────────────────────────────
|
|
121
|
+
console.log('\n▸ Path Helpers');
|
|
122
|
+
await test('getProjectAgentsDir returns correct path', () => {
|
|
123
|
+
const paths = require(path.join(ROOT, 'utils/paths'));
|
|
124
|
+
const result = paths.getProjectAgentsDir('/my/project');
|
|
125
|
+
assert.strictEqual(result, '/my/project/.veil/agents');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await test('getProjectSettingsPath returns correct path', () => {
|
|
129
|
+
const paths = require(path.join(ROOT, 'utils/paths'));
|
|
130
|
+
const result = paths.getProjectSettingsPath('/my/project');
|
|
131
|
+
assert.strictEqual(result, '/my/project/.veil/settings.json');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ── Test 7: Settings loading ──────────────────────────────────────────────
|
|
135
|
+
console.log('\n▸ Settings');
|
|
136
|
+
await test('loadSettings returns defaults with no files', () => {
|
|
137
|
+
const { loadSettings } = require(path.join(ROOT, 'utils/settings'));
|
|
138
|
+
const settings = loadSettings({ cwd: '/nonexistent/path', cliOverrides: {} });
|
|
139
|
+
assert.strictEqual(settings.port, 5050);
|
|
140
|
+
assert.strictEqual(settings.maxIterations, 50);
|
|
141
|
+
assert(settings.models.main, 'main model config should exist');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await test('CLI overrides win over defaults', () => {
|
|
145
|
+
const { loadSettings } = require(path.join(ROOT, 'utils/settings'));
|
|
146
|
+
const settings = loadSettings({ cwd: '/nonexistent/path', cliOverrides: { port: 9999 } });
|
|
147
|
+
assert.strictEqual(settings.port, 9999);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ── Test 8: Server starts and health endpoint works ───────────────────────
|
|
151
|
+
console.log('\n▸ Server');
|
|
152
|
+
let server;
|
|
153
|
+
let serverPort;
|
|
154
|
+
await test('server starts without errors', async () => {
|
|
155
|
+
const ctx = require(path.join(ROOT, 'utils/context'));
|
|
156
|
+
ctx.setCwd(path.join(ROOT, 'examples'));
|
|
157
|
+
ctx.setVersion(require(path.join(ROOT, 'package.json')).version);
|
|
158
|
+
|
|
159
|
+
const { loadSettings } = require(path.join(ROOT, 'utils/settings'));
|
|
160
|
+
const settings = loadSettings({ cwd: path.join(ROOT, 'examples'), cliOverrides: {} });
|
|
161
|
+
|
|
162
|
+
const { createScheduler } = require(path.join(ROOT, 'infrastructure/scheduler'));
|
|
163
|
+
const scheduler = createScheduler();
|
|
164
|
+
|
|
165
|
+
const { startServer } = require(path.join(ROOT, 'api/server'));
|
|
166
|
+
serverPort = 15050 + Math.floor(Math.random() * 1000);
|
|
167
|
+
server = await startServer({ settings, scheduler, port: serverPort });
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await test('GET /health returns 200', async () => {
|
|
171
|
+
if (!server) throw new Error('Server not started');
|
|
172
|
+
const response = await fetch(`http://localhost:${serverPort}/health`);
|
|
173
|
+
assert.strictEqual(response.status, 200);
|
|
174
|
+
const body = await response.json();
|
|
175
|
+
assert.strictEqual(body.status, 'ok');
|
|
176
|
+
assert(body.version, 'version should be present');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await test('GET /agents returns array', async () => {
|
|
180
|
+
if (!server) throw new Error('Server not started');
|
|
181
|
+
const response = await fetch(`http://localhost:${serverPort}/agents`);
|
|
182
|
+
assert.strictEqual(response.status, 200);
|
|
183
|
+
const body = await response.json();
|
|
184
|
+
assert(Array.isArray(body.agents), 'agents should be an array');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await test('GET /agents/nonexistent returns 404', async () => {
|
|
188
|
+
if (!server) throw new Error('Server not started');
|
|
189
|
+
const response = await fetch(`http://localhost:${serverPort}/agents/nonexistent`);
|
|
190
|
+
assert.strictEqual(response.status, 404);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ── Cleanup ───────────────────────────────────────────────────────────────
|
|
194
|
+
if (server) {
|
|
195
|
+
server.close();
|
|
196
|
+
const ctx = require(path.join(ROOT, 'utils/context'));
|
|
197
|
+
ctx._reset();
|
|
198
|
+
try { require(path.join(ROOT, 'infrastructure/database')).closeDb(); } catch {}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Summary ───────────────────────────────────────────────────────────────
|
|
202
|
+
console.log(`\n${'─'.repeat(40)}`);
|
|
203
|
+
console.log(`Results: ${passed} passed, ${failed} failed`);
|
|
204
|
+
if (failed > 0) {
|
|
205
|
+
console.error(`\n${failed} test(s) failed.`);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
} else {
|
|
208
|
+
console.log('\nAll smoke tests passed! ✓');
|
|
209
|
+
process.exit(0);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
runAll().catch(err => {
|
|
214
|
+
console.error('Smoke test crashed:', err);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const db = require('../infrastructure/database');
|
|
4
|
+
const { generateId } = require('../utils/id');
|
|
5
|
+
const { loadAgent } = require('../core/agent');
|
|
6
|
+
const runningSessions = require('../core/running-sessions');
|
|
7
|
+
|
|
8
|
+
const schema = {
|
|
9
|
+
name: 'agent_message',
|
|
10
|
+
description: 'Send a message to another agent and wait for a response (synchronous). The target agent must be active.',
|
|
11
|
+
input_schema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
agent: { type: 'string', description: 'Name of the target agent' },
|
|
15
|
+
sessionId: { type: 'string', description: 'Session ID of the target agent session to send the message to. Obtain this from agent_spawn.' },
|
|
16
|
+
message: { type: 'string', description: 'Message to send' },
|
|
17
|
+
timeout: { type: 'integer', description: 'Timeout in seconds (default: 60)', minimum: 1 },
|
|
18
|
+
},
|
|
19
|
+
required: ['agent', 'sessionId', 'message'],
|
|
20
|
+
},
|
|
21
|
+
timeout: 86400,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
async function execute({ agent, message, sessionId, timeout = 60, _cwd, _settings, _agent: senderAgent }) {
|
|
25
|
+
try { loadAgent({ cwd: _cwd, name: agent }); } catch (err) {
|
|
26
|
+
return `Error: target agent "${agent}" not found — ${err.message}`;
|
|
27
|
+
}
|
|
28
|
+
const targetSession = db.getSession(sessionId);
|
|
29
|
+
if (!targetSession) return `Error: session "${sessionId}" not found.`;
|
|
30
|
+
if (targetSession.agent_name !== agent) return `Error: session "${sessionId}" belongs to agent "${targetSession.agent_name}", not "${agent}".`;
|
|
31
|
+
|
|
32
|
+
const fromAgent = senderAgent?.name || 'unknown';
|
|
33
|
+
const wrappedMessage = `[Message from ${fromAgent}]: ${message}`;
|
|
34
|
+
|
|
35
|
+
// Helper: trigger runChat directly on an idle chat session
|
|
36
|
+
async function triggerChatTurn() {
|
|
37
|
+
const { runChat } = require('../core/router');
|
|
38
|
+
const result = await runChat({ agentName: agent, message: wrappedMessage, sessionId, cwd: _cwd, settings: _settings });
|
|
39
|
+
return result.content || '(no response)';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Session is idle — trigger a new turn directly
|
|
43
|
+
if (!runningSessions.has(sessionId)) {
|
|
44
|
+
if (targetSession.mode === 'chat') {
|
|
45
|
+
try { return await triggerChatTurn(); } catch (err) { return `Error triggering session "${sessionId}": ${err.message}`; }
|
|
46
|
+
}
|
|
47
|
+
// Non-chat idle session: queue the message (will be picked up on next run/tick)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Session has an active loop — inject via queue
|
|
51
|
+
const correlationId = generateId('corr_');
|
|
52
|
+
|
|
53
|
+
db.enqueueAgentMessage({
|
|
54
|
+
targetAgent: agent,
|
|
55
|
+
targetSessionId: sessionId,
|
|
56
|
+
fromAgent,
|
|
57
|
+
content: message,
|
|
58
|
+
followup: false,
|
|
59
|
+
correlationId,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Poll for response; if the loop ends before picking up the message, fall back to runChat
|
|
63
|
+
let firstPoll = true;
|
|
64
|
+
const deadline = Date.now() + timeout * 1000;
|
|
65
|
+
while (Date.now() < deadline) {
|
|
66
|
+
await new Promise(r => setTimeout(r, firstPoll ? 200 : 500));
|
|
67
|
+
firstPoll = false;
|
|
68
|
+
const rows = db.getDb().prepare(
|
|
69
|
+
"SELECT response FROM agent_messages WHERE correlation_id = ? AND status = 'delivered' AND response IS NOT NULL"
|
|
70
|
+
).all(correlationId);
|
|
71
|
+
if (rows.length > 0 && rows[0].response) return rows[0].response;
|
|
72
|
+
|
|
73
|
+
// Loop ended without picking up the message — cancel it and fall back to runChat
|
|
74
|
+
if (!runningSessions.has(sessionId) && targetSession.mode === 'chat') {
|
|
75
|
+
db.getDb().prepare(
|
|
76
|
+
"DELETE FROM agent_messages WHERE correlation_id = ? AND status = 'pending'"
|
|
77
|
+
).run(correlationId);
|
|
78
|
+
try { return await triggerChatTurn(); } catch (err) { return `Error triggering session "${sessionId}": ${err.message}`; }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return `Timeout: no response from session "${sessionId}" (agent: ${agent}) within ${timeout}s. The session is in ${targetSession.mode} mode and may have stalled.`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { schema, execute };
|