@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,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ── Test 11: Advanced session management ──────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
await test('GET /sessions/:id returns correct session data', async () => {
|
|
6
|
+
const chatRes = await client.chat.send('hello', 'Session data test.');
|
|
7
|
+
assert.strictEqual(chatRes.status, 200);
|
|
8
|
+
const { sessionId } = chatRes.body;
|
|
9
|
+
|
|
10
|
+
const res = await client.sessions.get(sessionId);
|
|
11
|
+
assert.strictEqual(res.status, 200, JSON.stringify(res.body));
|
|
12
|
+
const { session } = res.body;
|
|
13
|
+
assert.strictEqual(session.id, sessionId);
|
|
14
|
+
assert.strictEqual(session.agent_name, 'hello');
|
|
15
|
+
assert.strictEqual(session.mode, 'chat');
|
|
16
|
+
assert.strictEqual(session.status, 'active');
|
|
17
|
+
assert(session.message_count >= 2, `Expected ≥2 messages, got: ${session.message_count}`);
|
|
18
|
+
assert(session.token_input > 0, 'token_input should be > 0');
|
|
19
|
+
assert(session.token_output > 0, 'token_output should be > 0');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
await test('GET /sessions/:id/messages paginates correctly', async () => {
|
|
23
|
+
// Create a session with multiple exchanges
|
|
24
|
+
const chatRes = await client.chat.send('hello', 'Message 1: Alpha');
|
|
25
|
+
const { sessionId } = chatRes.body;
|
|
26
|
+
await client.chat.send('hello', 'Message 2: Beta', sessionId);
|
|
27
|
+
await client.chat.send('hello', 'Message 3: Gamma', sessionId);
|
|
28
|
+
|
|
29
|
+
// Get all messages
|
|
30
|
+
const res = await client.sessions.messages(sessionId);
|
|
31
|
+
assert.strictEqual(res.status, 200);
|
|
32
|
+
assert(res.body.messages.length >= 7, `Expected ≥7 messages (system + 3 user + 3 assistant), got: ${res.body.messages.length}`);
|
|
33
|
+
|
|
34
|
+
// Test with limit
|
|
35
|
+
const limitRes = await fetch(`http://localhost:5050/sessions/${sessionId}/messages?limit=3`);
|
|
36
|
+
const limitBody = await limitRes.json();
|
|
37
|
+
assert(limitBody.messages.length <= 3, `Limit should cap at 3, got: ${limitBody.messages.length}`);
|
|
38
|
+
}, { slow: true });
|
|
39
|
+
|
|
40
|
+
await test('DELETE /sessions/:id closes the session', async () => {
|
|
41
|
+
const chatRes = await client.chat.send('hello', 'Close me.');
|
|
42
|
+
const { sessionId } = chatRes.body;
|
|
43
|
+
|
|
44
|
+
// Verify it's active
|
|
45
|
+
const beforeRes = await client.sessions.get(sessionId);
|
|
46
|
+
assert.strictEqual(beforeRes.body.session.status, 'active');
|
|
47
|
+
|
|
48
|
+
// Close it
|
|
49
|
+
const delRes = await fetch(`http://localhost:5050/sessions/${sessionId}`, { method: 'DELETE' });
|
|
50
|
+
assert.strictEqual(delRes.status, 200);
|
|
51
|
+
const delBody = await delRes.json();
|
|
52
|
+
assert.strictEqual(delBody.status, 'closed');
|
|
53
|
+
|
|
54
|
+
// Verify it's closed
|
|
55
|
+
const afterRes = await client.sessions.get(sessionId);
|
|
56
|
+
assert.strictEqual(afterRes.body.session.status, 'closed');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await test('GET /sessions/:id returns 404 for unknown session', async () => {
|
|
60
|
+
const res = await client.sessions.get('sess_nonexistent_xyz_123');
|
|
61
|
+
assert.strictEqual(res.status, 404);
|
|
62
|
+
assert.strictEqual(res.body.error.code, 'SESSION_NOT_FOUND');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await test('Consecutive multi-turn chat builds coherent context', async () => {
|
|
66
|
+
// Turn 1: set context
|
|
67
|
+
const r1 = await client.chat.send('hello', 'I am testing VeilCLI. Remember: codeword = ZEBRA_KITE_77.');
|
|
68
|
+
const { sessionId } = r1.body;
|
|
69
|
+
|
|
70
|
+
// Turn 2: verify context
|
|
71
|
+
const r2 = await client.chat.send('hello', 'What codeword did I give you?', sessionId);
|
|
72
|
+
assert(
|
|
73
|
+
r2.body.message.content.includes('ZEBRA_KITE_77'),
|
|
74
|
+
`Expected codeword in response, got: ${r2.body.message.content}`
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Turn 3: follow-up question showing context chain
|
|
78
|
+
const r3 = await client.chat.send('hello', 'Good. Now repeat the codeword backwards letter by letter would be complex, just confirm it starts with Z.', sessionId);
|
|
79
|
+
const content = r3.body.message.content.toLowerCase();
|
|
80
|
+
assert(
|
|
81
|
+
content.includes('z') || content.includes('zebra') || content.includes('yes'),
|
|
82
|
+
`Expected confirmation with Z, got: ${r3.body.message.content}`
|
|
83
|
+
);
|
|
84
|
+
}, { slow: true });
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ── Test 12: Assistant agent chat mode with tool use ──────────────────────────
|
|
4
|
+
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const PROJECT_ROOT = path.join(__dirname, '../..');
|
|
7
|
+
|
|
8
|
+
await test('Assistant chat: uses read_file to answer a question', async () => {
|
|
9
|
+
const res = await client.chat.send('assistant',
|
|
10
|
+
`What is the "version" field in the file ${PROJECT_ROOT}/package.json? Use the read_file tool.`
|
|
11
|
+
);
|
|
12
|
+
assert.strictEqual(res.status, 200, JSON.stringify(res.body));
|
|
13
|
+
const content = res.body.message.content;
|
|
14
|
+
assert(
|
|
15
|
+
content.includes('0.1.0') || content.includes('version'),
|
|
16
|
+
`Expected version in response, got: ${content}`
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
await test('Assistant chat: uses list_dir to explore a directory', async () => {
|
|
21
|
+
const res = await client.chat.send('assistant',
|
|
22
|
+
`Use list_dir to show me what's in ${PROJECT_ROOT}/core. Just list the filenames.`
|
|
23
|
+
);
|
|
24
|
+
assert.strictEqual(res.status, 200, JSON.stringify(res.body));
|
|
25
|
+
const content = res.body.message.content;
|
|
26
|
+
assert(
|
|
27
|
+
content.includes('agent.js') || content.includes('loop.js') || content.includes('prompt.js'),
|
|
28
|
+
`Expected core/ files listed, got: ${content}`
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await test('Assistant chat: uses bash to run a simple command', async () => {
|
|
33
|
+
const res = await client.chat.send('assistant',
|
|
34
|
+
`Run the bash command: echo "BASH_CHAT_TEST_$(date +%Y)" and tell me the output.`
|
|
35
|
+
);
|
|
36
|
+
assert.strictEqual(res.status, 200, JSON.stringify(res.body));
|
|
37
|
+
const content = res.body.message.content;
|
|
38
|
+
const year = new Date().getFullYear().toString();
|
|
39
|
+
assert(
|
|
40
|
+
content.includes(year) || content.includes('BASH_CHAT_TEST'),
|
|
41
|
+
`Expected year ${year} or BASH_CHAT_TEST in response, got: ${content}`
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await test('Assistant chat: uses grep to search across files', async () => {
|
|
46
|
+
const res = await client.chat.send('assistant',
|
|
47
|
+
`Use grep to find all files in ${PROJECT_ROOT}/core that contain the word "permission". List the matching files.`
|
|
48
|
+
);
|
|
49
|
+
assert.strictEqual(res.status, 200, JSON.stringify(res.body));
|
|
50
|
+
const content = res.body.message.content;
|
|
51
|
+
// loop.js contains "permission"
|
|
52
|
+
assert(
|
|
53
|
+
content.includes('loop.js') || content.includes('core/') || content.includes('permission'),
|
|
54
|
+
`Expected grep results mentioning loop.js or core files, got: ${content}`
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await test('Assistant chat: multi-turn with tool context', async () => {
|
|
59
|
+
const r1 = await client.chat.send('assistant',
|
|
60
|
+
`Read ${PROJECT_ROOT}/utils/id.js and tell me the function name that generates IDs.`
|
|
61
|
+
);
|
|
62
|
+
assert.strictEqual(r1.status, 200);
|
|
63
|
+
const { sessionId } = r1.body;
|
|
64
|
+
|
|
65
|
+
const r2 = await client.chat.send('assistant',
|
|
66
|
+
`In the same file, what prefix parameter does that function accept? Look at the function signature.`,
|
|
67
|
+
sessionId
|
|
68
|
+
);
|
|
69
|
+
assert.strictEqual(r2.status, 200);
|
|
70
|
+
const content = r2.body.message.content.toLowerCase();
|
|
71
|
+
assert(
|
|
72
|
+
content.includes('prefix') || content.includes('generateid') || content.includes('parameter'),
|
|
73
|
+
`Expected mention of prefix parameter, got: ${r2.body.message.content}`
|
|
74
|
+
);
|
|
75
|
+
}, { slow: true });
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ── Test 13: Edge cases and error handling ────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
await test('POST /agents/:name/chat with missing message body returns 400', async () => {
|
|
6
|
+
const res = await fetch('http://localhost:5050/agents/hello/chat', {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
headers: { 'Content-Type': 'application/json' },
|
|
9
|
+
body: JSON.stringify({}),
|
|
10
|
+
});
|
|
11
|
+
assert.strictEqual(res.status, 400);
|
|
12
|
+
const body = await res.json();
|
|
13
|
+
assert.strictEqual(body.error.code, 'VALIDATION_ERROR');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
await test('POST /agents/:name/chat with non-string message returns 400', async () => {
|
|
17
|
+
const res = await fetch('http://localhost:5050/agents/hello/chat', {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { 'Content-Type': 'application/json' },
|
|
20
|
+
body: JSON.stringify({ message: 42 }),
|
|
21
|
+
});
|
|
22
|
+
assert.strictEqual(res.status, 400);
|
|
23
|
+
const body = await res.json();
|
|
24
|
+
assert.strictEqual(body.error.code, 'VALIDATION_ERROR');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
await test('POST /agents/analyst/task returns 400 for agent without chat mode', async () => {
|
|
28
|
+
// analyst only has task mode — no chat mode
|
|
29
|
+
const res = await client.chat.send('analyst', 'Hello');
|
|
30
|
+
assert.strictEqual(res.status, 400);
|
|
31
|
+
assert.strictEqual(res.body.error.code, 'MODE_NOT_SUPPORTED');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await test('POST /agents/hello/task with missing input returns 400', async () => {
|
|
35
|
+
const res = await fetch('http://localhost:5050/agents/hello/task', {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38
|
+
body: JSON.stringify({}),
|
|
39
|
+
});
|
|
40
|
+
assert.strictEqual(res.status, 400);
|
|
41
|
+
const body = await res.json();
|
|
42
|
+
assert.strictEqual(body.error.code, 'VALIDATION_ERROR');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await test('Task with very short input completes fine', async () => {
|
|
46
|
+
const res = await client.tasks.create('hello', 'Hi');
|
|
47
|
+
assert.strictEqual(res.status, 202);
|
|
48
|
+
const task = await client.pollTask(res.body.taskId, { timeout: 60000 });
|
|
49
|
+
assert.strictEqual(task.status, 'finished', `Task failed: ${task.error}`);
|
|
50
|
+
assert(task.output && task.output.length > 0, 'Should produce some output even for short input');
|
|
51
|
+
}, { slow: true });
|
|
52
|
+
|
|
53
|
+
await test('Task with very long input completes without crashing', async () => {
|
|
54
|
+
const longText = 'How many words are in this sentence: ' +
|
|
55
|
+
'The quick brown fox jumps over the lazy dog and the cat sat on the mat. '.repeat(5) +
|
|
56
|
+
' Reply with just a number.';
|
|
57
|
+
const res = await client.tasks.create('hello', longText);
|
|
58
|
+
assert.strictEqual(res.status, 202);
|
|
59
|
+
const task = await client.pollTask(res.body.taskId, { timeout: 90000 });
|
|
60
|
+
// Task should complete (finished or failed) — not hang
|
|
61
|
+
assert(['finished', 'failed'].includes(task.status), `Expected terminal status, got: ${task.status}`);
|
|
62
|
+
if (task.status === 'finished') {
|
|
63
|
+
// Output may be a number or text — just verify something came back
|
|
64
|
+
assert(task.output !== undefined, 'output field should exist');
|
|
65
|
+
}
|
|
66
|
+
}, { slow: true });
|
|
67
|
+
|
|
68
|
+
await test('Session messages include correct roles', async () => {
|
|
69
|
+
const res = await client.chat.send('hello', 'Roles test message.');
|
|
70
|
+
const { sessionId } = res.body;
|
|
71
|
+
const msgsRes = await client.sessions.messages(sessionId);
|
|
72
|
+
assert.strictEqual(msgsRes.status, 200);
|
|
73
|
+
const roles = msgsRes.body.messages.map(m => m.role);
|
|
74
|
+
assert(roles.includes('system'), 'Missing system message');
|
|
75
|
+
assert(roles.includes('user'), 'Missing user message');
|
|
76
|
+
assert(roles.includes('assistant'), 'Missing assistant message');
|
|
77
|
+
const validRoles = new Set(['system', 'user', 'assistant', 'tool']);
|
|
78
|
+
roles.forEach(r => assert(validRoles.has(r), `Invalid role: ${r}`));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await test('Task events have valid types', async () => {
|
|
82
|
+
const res = await client.tasks.create('hello', 'Events validation test.');
|
|
83
|
+
const task = await client.pollTask(res.body.taskId, { timeout: 60000 });
|
|
84
|
+
const eventsRes = await client.tasks.events(task.id);
|
|
85
|
+
assert.strictEqual(eventsRes.status, 200);
|
|
86
|
+
const validTypes = new Set(['iteration.start', 'iteration.end', 'tool.start', 'tool.end',
|
|
87
|
+
'status.change', 'llm.error', 'limit.reached', 'custom', 'task.complete']);
|
|
88
|
+
for (const e of eventsRes.body.events) {
|
|
89
|
+
// Event types should be recognized patterns
|
|
90
|
+
const isKnown = [...validTypes].some(t => e.type.includes(t.split('.')[0]));
|
|
91
|
+
assert(isKnown || e.type, `Event type should be non-empty: ${e.type}`);
|
|
92
|
+
}
|
|
93
|
+
}, { slow: true });
|
|
94
|
+
|
|
95
|
+
await test('GET /tasks with limit parameter respected', async () => {
|
|
96
|
+
const res = await client.tasks.list({ limit: 2 });
|
|
97
|
+
assert.strictEqual(res.status, 200);
|
|
98
|
+
assert(res.body.tasks.length <= 2, `Limit 2 should return at most 2, got: ${res.body.tasks.length}`);
|
|
99
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ── Test 14: Cancellation + token budget ─────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
await test('Cancel a pending task immediately', async () => {
|
|
6
|
+
const res = await client.tasks.create('hello', 'Count from 1 to 1000 slowly.');
|
|
7
|
+
assert.strictEqual(res.status, 202, JSON.stringify(res.body));
|
|
8
|
+
const { taskId } = res.body;
|
|
9
|
+
|
|
10
|
+
// Cancel right away (task should still be pending or just processing)
|
|
11
|
+
const cancelRes = await client.tasks.cancel(taskId);
|
|
12
|
+
assert.strictEqual(cancelRes.status, 200, JSON.stringify(cancelRes.body));
|
|
13
|
+
assert.strictEqual(cancelRes.body.taskId, taskId);
|
|
14
|
+
|
|
15
|
+
// Verify final state is canceled
|
|
16
|
+
await new Promise(r => setTimeout(r, 500));
|
|
17
|
+
const getRes = await client.tasks.get(taskId);
|
|
18
|
+
assert.strictEqual(getRes.status, 200);
|
|
19
|
+
assert(['canceled', 'finished', 'processing'].includes(getRes.body.task.status),
|
|
20
|
+
`Expected canceled/finished/processing, got: ${getRes.body.task.status}`);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await test('Canceling an already-finished task returns TASK_ALREADY_TERMINAL', async () => {
|
|
24
|
+
const res = await client.tasks.create('hello', 'Say: DONE_QUICK');
|
|
25
|
+
const task = await client.pollTask(res.body.taskId, { timeout: 60000 });
|
|
26
|
+
assert.strictEqual(task.status, 'finished');
|
|
27
|
+
|
|
28
|
+
const cancelRes = await client.tasks.cancel(task.id);
|
|
29
|
+
assert.strictEqual(cancelRes.status, 400, JSON.stringify(cancelRes.body));
|
|
30
|
+
assert.strictEqual(cancelRes.body.error.code, 'TASK_ALREADY_TERMINAL');
|
|
31
|
+
}, { slow: true });
|
|
32
|
+
|
|
33
|
+
await test('Task with tokenBudget field is accepted', async () => {
|
|
34
|
+
const res = await fetch('http://localhost:5050/agents/hello/task', {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: { 'Content-Type': 'application/json' },
|
|
37
|
+
body: JSON.stringify({ input: 'Say: BUDGET_TEST', tokenBudget: 100000 }),
|
|
38
|
+
});
|
|
39
|
+
assert.strictEqual(res.status, 202, `Expected 202, got ${res.status}`);
|
|
40
|
+
const body = await res.json();
|
|
41
|
+
assert(body.taskId, 'taskId missing');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await test('Task failure produces structured error object', async () => {
|
|
45
|
+
// Create a task with an extremely small token budget that will definitely be exceeded
|
|
46
|
+
const res = await fetch('http://localhost:5050/agents/hello/task', {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'Content-Type': 'application/json' },
|
|
49
|
+
body: JSON.stringify({ input: 'Write a 5000 word essay on quantum physics.', tokenBudget: 1 }),
|
|
50
|
+
});
|
|
51
|
+
assert.strictEqual(res.status, 202);
|
|
52
|
+
const { taskId } = await res.json();
|
|
53
|
+
|
|
54
|
+
const task = await client.pollTask(taskId, { timeout: 30000 });
|
|
55
|
+
// Should fail with BUDGET_EXCEEDED or succeed if the LLM doesn't consume tokens
|
|
56
|
+
if (task.status === 'failed') {
|
|
57
|
+
assert(task.error, 'error field should be present on failed task');
|
|
58
|
+
const err = typeof task.error === 'object' ? task.error : JSON.parse(task.error);
|
|
59
|
+
assert(err.code, `error.code missing: ${JSON.stringify(task.error)}`);
|
|
60
|
+
assert(err.message, 'error.message missing');
|
|
61
|
+
}
|
|
62
|
+
}, { slow: true });
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ── Test 15: Debuggability endpoints (context, events pagination) ─────────────
|
|
4
|
+
|
|
5
|
+
const BASE = 'http://localhost:5050';
|
|
6
|
+
|
|
7
|
+
await test('GET /tasks/:id/context returns null for pending task', async () => {
|
|
8
|
+
const res = await client.tasks.create('hello', 'Say: CONTEXT_TEST');
|
|
9
|
+
const { taskId } = res.body;
|
|
10
|
+
|
|
11
|
+
const ctxRes = await fetch(`${BASE}/tasks/${taskId}/context`);
|
|
12
|
+
assert.strictEqual(ctxRes.status, 200);
|
|
13
|
+
const body = await ctxRes.json();
|
|
14
|
+
assert.strictEqual(body.taskId, taskId);
|
|
15
|
+
// Context may be null if task hasn't started yet
|
|
16
|
+
assert('context' in body, 'context field should be present');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
await test('GET /tasks/:id/context returns snapshot after task runs', async () => {
|
|
20
|
+
const res = await client.tasks.create('hello', 'Say: CONTEXT_SNAP');
|
|
21
|
+
const { taskId } = res.body;
|
|
22
|
+
await client.pollTask(taskId, { timeout: 60000 });
|
|
23
|
+
|
|
24
|
+
const ctxRes = await fetch(`${BASE}/tasks/${taskId}/context`);
|
|
25
|
+
assert.strictEqual(ctxRes.status, 200);
|
|
26
|
+
const body = await ctxRes.json();
|
|
27
|
+
assert.strictEqual(body.taskId, taskId);
|
|
28
|
+
if (body.context) {
|
|
29
|
+
assert(Array.isArray(body.context.messages), 'context.messages should be array');
|
|
30
|
+
assert(Array.isArray(body.context.tools), 'context.tools should be array');
|
|
31
|
+
assert(typeof body.context.iteration === 'number', 'context.iteration should be a number');
|
|
32
|
+
assert(body.context.messages.length > 0, 'should have at least one message');
|
|
33
|
+
// System prompt should be first
|
|
34
|
+
assert.strictEqual(body.context.messages[0].role, 'system');
|
|
35
|
+
}
|
|
36
|
+
}, { slow: true });
|
|
37
|
+
|
|
38
|
+
await test('GET /sessions/:id/context returns message list', async () => {
|
|
39
|
+
// Create a chat session
|
|
40
|
+
const chatRes = await fetch(`${BASE}/agents/hello/chat`, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'Content-Type': 'application/json' },
|
|
43
|
+
body: JSON.stringify({ message: 'Say: SESSION_CONTEXT_TEST' }),
|
|
44
|
+
});
|
|
45
|
+
assert.strictEqual(chatRes.status, 200);
|
|
46
|
+
const { sessionId } = await chatRes.json();
|
|
47
|
+
|
|
48
|
+
const ctxRes = await fetch(`${BASE}/sessions/${sessionId}/context`);
|
|
49
|
+
assert.strictEqual(ctxRes.status, 200);
|
|
50
|
+
const body = await ctxRes.json();
|
|
51
|
+
assert.strictEqual(body.sessionId, sessionId);
|
|
52
|
+
assert(typeof body.messageCount === 'number');
|
|
53
|
+
assert(Array.isArray(body.messages));
|
|
54
|
+
assert(body.messages.length >= 2, 'Should have system + user + assistant messages');
|
|
55
|
+
const roles = body.messages.map(m => m.role);
|
|
56
|
+
assert(roles.includes('system'), 'Should have system message');
|
|
57
|
+
assert(roles.includes('user'), 'Should have user message');
|
|
58
|
+
}, { slow: true });
|
|
59
|
+
|
|
60
|
+
await test('GET /tasks/:id/events?since= returns only newer events', async () => {
|
|
61
|
+
const res = await client.tasks.create('hello', 'Say: EVENTS_SINCE_TEST');
|
|
62
|
+
const { taskId } = res.body;
|
|
63
|
+
await client.pollTask(taskId, { timeout: 60000 });
|
|
64
|
+
|
|
65
|
+
// Get all events
|
|
66
|
+
const allRes = await client.tasks.events(taskId);
|
|
67
|
+
const allEvents = allRes.body.events;
|
|
68
|
+
assert(allEvents.length > 0, 'should have events');
|
|
69
|
+
|
|
70
|
+
// Get events since the first event id
|
|
71
|
+
const firstId = allEvents[0].id;
|
|
72
|
+
const sinceRes = await fetch(`${BASE}/tasks/${taskId}/events?since=${firstId}`);
|
|
73
|
+
assert.strictEqual(sinceRes.status, 200);
|
|
74
|
+
const sinceBody = await sinceRes.json();
|
|
75
|
+
assert(Array.isArray(sinceBody.events));
|
|
76
|
+
// All returned events should have id > firstId
|
|
77
|
+
sinceBody.events.forEach(e => {
|
|
78
|
+
assert(e.id > firstId, `Event id ${e.id} should be > ${firstId}`);
|
|
79
|
+
});
|
|
80
|
+
// Should have one fewer event than total
|
|
81
|
+
assert(sinceBody.events.length < allEvents.length, 'since should return fewer events');
|
|
82
|
+
}, { slow: true });
|
|
83
|
+
|
|
84
|
+
await test('GET /tasks/:id/events?limit= caps the result', async () => {
|
|
85
|
+
const res = await client.tasks.create('hello', 'Say: EVENTS_LIMIT_TEST');
|
|
86
|
+
const { taskId } = res.body;
|
|
87
|
+
await client.pollTask(taskId, { timeout: 60000 });
|
|
88
|
+
|
|
89
|
+
const limitRes = await fetch(`${BASE}/tasks/${taskId}/events?limit=1`);
|
|
90
|
+
assert.strictEqual(limitRes.status, 200);
|
|
91
|
+
const body = await limitRes.json();
|
|
92
|
+
assert(Array.isArray(body.events));
|
|
93
|
+
assert(body.events.length <= 1, `Expected at most 1 event, got ${body.events.length}`);
|
|
94
|
+
}, { slow: true });
|
|
95
|
+
|
|
96
|
+
await test('Task response includes eventCount field', async () => {
|
|
97
|
+
const res = await client.tasks.create('hello', 'Say: EVENTCOUNT_TEST');
|
|
98
|
+
const { taskId } = res.body;
|
|
99
|
+
await client.pollTask(taskId, { timeout: 60000 });
|
|
100
|
+
|
|
101
|
+
const getRes = await client.tasks.get(taskId);
|
|
102
|
+
assert.strictEqual(getRes.status, 200);
|
|
103
|
+
const task = getRes.body.task;
|
|
104
|
+
assert(typeof task.eventCount === 'number', `eventCount should be a number, got: ${typeof task.eventCount}`);
|
|
105
|
+
assert(task.eventCount > 0, `eventCount should be > 0, got: ${task.eventCount}`);
|
|
106
|
+
}, { slow: true });
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ── Test 16: Memory API ───────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
const BASE = 'http://localhost:5050';
|
|
6
|
+
|
|
7
|
+
await test('GET /agents/:name/memory lists memory files', async () => {
|
|
8
|
+
const res = await fetch(`${BASE}/agents/hello/memory`);
|
|
9
|
+
assert.strictEqual(res.status, 200);
|
|
10
|
+
const body = await res.json();
|
|
11
|
+
assert.strictEqual(body.agentName, 'hello');
|
|
12
|
+
assert(Array.isArray(body.files), 'files should be array');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
await test('PUT /agents/:name/memory/:file writes a memory file', async () => {
|
|
16
|
+
const content = '## Test Memory\n\nThis was written by the test suite.';
|
|
17
|
+
const res = await fetch(`${BASE}/agents/hello/memory/TEST-MEMORY.md`, {
|
|
18
|
+
method: 'PUT',
|
|
19
|
+
headers: { 'Content-Type': 'application/json' },
|
|
20
|
+
body: JSON.stringify({ content }),
|
|
21
|
+
});
|
|
22
|
+
assert.strictEqual(res.status, 200, JSON.stringify(await res.clone().json().catch(() => ({}))));
|
|
23
|
+
const body = await res.json();
|
|
24
|
+
assert.strictEqual(body.agentName, 'hello');
|
|
25
|
+
assert.strictEqual(body.file, 'TEST-MEMORY.md');
|
|
26
|
+
assert(typeof body.size === 'number');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
await test('GET /agents/:name/memory/:file reads back written content', async () => {
|
|
30
|
+
const content = '## Read Test\n\nRead me back correctly.';
|
|
31
|
+
await fetch(`${BASE}/agents/hello/memory/READ-TEST.md`, {
|
|
32
|
+
method: 'PUT',
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
34
|
+
body: JSON.stringify({ content }),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const res = await fetch(`${BASE}/agents/hello/memory/READ-TEST.md`);
|
|
38
|
+
assert.strictEqual(res.status, 200);
|
|
39
|
+
const body = await res.json();
|
|
40
|
+
assert.strictEqual(body.content, content);
|
|
41
|
+
assert.strictEqual(body.file, 'READ-TEST.md');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await test('GET /agents/:name/memory/:file returns 404 for missing file', async () => {
|
|
45
|
+
const res = await fetch(`${BASE}/agents/hello/memory/DOES-NOT-EXIST.md`);
|
|
46
|
+
assert.strictEqual(res.status, 404);
|
|
47
|
+
const body = await res.json();
|
|
48
|
+
assert.strictEqual(body.error.code, 'NOT_FOUND');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await test('PUT /agents/:name/memory/:file rejects path traversal filenames', async () => {
|
|
52
|
+
const res = await fetch(`${BASE}/agents/hello/memory/../../evil.md`, {
|
|
53
|
+
method: 'PUT',
|
|
54
|
+
headers: { 'Content-Type': 'application/json' },
|
|
55
|
+
body: JSON.stringify({ content: 'malicious' }),
|
|
56
|
+
});
|
|
57
|
+
// Should be blocked by routing or validation
|
|
58
|
+
assert([400, 404].includes(res.status), `Expected 400 or 404, got ${res.status}`);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await test('DELETE /agents/:name/memory/:file removes the file', async () => {
|
|
62
|
+
// First write
|
|
63
|
+
await fetch(`${BASE}/agents/hello/memory/DELETE-TEST.md`, {
|
|
64
|
+
method: 'PUT',
|
|
65
|
+
headers: { 'Content-Type': 'application/json' },
|
|
66
|
+
body: JSON.stringify({ content: 'to be deleted' }),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Then delete
|
|
70
|
+
const res = await fetch(`${BASE}/agents/hello/memory/DELETE-TEST.md`, { method: 'DELETE' });
|
|
71
|
+
assert.strictEqual(res.status, 200);
|
|
72
|
+
const body = await res.json();
|
|
73
|
+
assert.strictEqual(body.status, 'deleted');
|
|
74
|
+
|
|
75
|
+
// Verify gone
|
|
76
|
+
const getRes = await fetch(`${BASE}/agents/hello/memory/DELETE-TEST.md`);
|
|
77
|
+
assert.strictEqual(getRes.status, 404);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await test('Cleanup: remove test memory files', async () => {
|
|
81
|
+
await fetch(`${BASE}/agents/hello/memory/TEST-MEMORY.md`, { method: 'DELETE' });
|
|
82
|
+
await fetch(`${BASE}/agents/hello/memory/READ-TEST.md`, { method: 'DELETE' });
|
|
83
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ── Test 17: Settings API ─────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
const BASE = 'http://localhost:5050';
|
|
6
|
+
|
|
7
|
+
await test('GET /settings returns current settings with redacted api_key', async () => {
|
|
8
|
+
const res = await fetch(`${BASE}/settings`);
|
|
9
|
+
assert.strictEqual(res.status, 200);
|
|
10
|
+
const body = await res.json();
|
|
11
|
+
assert(body.settings, 'settings object missing');
|
|
12
|
+
assert(typeof body.settings.port === 'number', 'port should be a number');
|
|
13
|
+
// api_key should be redacted if present
|
|
14
|
+
if (body.settings.models?.main?.api_key) {
|
|
15
|
+
const key = body.settings.models.main.api_key;
|
|
16
|
+
assert(!key.startsWith('sk-or-v1-') || key.includes('…'), 'api_key should be redacted');
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
await test('PUT /settings with invalid body returns 400 VALIDATION_ERROR', async () => {
|
|
21
|
+
const res = await fetch(`${BASE}/settings`, {
|
|
22
|
+
method: 'PUT',
|
|
23
|
+
headers: { 'Content-Type': 'application/json' },
|
|
24
|
+
body: JSON.stringify({ port: 'not-a-number' }),
|
|
25
|
+
});
|
|
26
|
+
// May be 400 if schema validation catches it, or 200 if schema is relaxed
|
|
27
|
+
// At minimum we should get a JSON response
|
|
28
|
+
const body = await res.json();
|
|
29
|
+
assert(body, 'should return JSON');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await test('GET /settings returns valid JSON structure', async () => {
|
|
33
|
+
const res = await fetch(`${BASE}/settings`);
|
|
34
|
+
assert.strictEqual(res.status, 200);
|
|
35
|
+
const body = await res.json();
|
|
36
|
+
const s = body.settings;
|
|
37
|
+
assert(s.permissions, 'permissions missing');
|
|
38
|
+
assert(Array.isArray(s.permissions.allow), 'permissions.allow should be array');
|
|
39
|
+
assert(s.compaction, 'compaction missing');
|
|
40
|
+
assert(s.memory, 'memory missing');
|
|
41
|
+
});
|