Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bin/atris.js
Original file line number Diff line number Diff line change
Expand Up @@ -2395,7 +2395,7 @@ if (command === 'guide') {
upgradeAtris().then(() => process.exit(0)).catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
} else if (command === 'chat' && (process.argv[3] === '--personal' || process.argv[3] === '-p')) {
require('../commands/ask').askAtris(process.argv.slice(4))
.then(() => exitAfterStdoutDrain(0))
.then((code) => exitAfterStdoutDrain(code || 0))
.catch((error) => {
console.error(`✗ Ask failed: ${error.message || error}`);
process.exit(1);
Expand Down
1 change: 1 addition & 0 deletions commands/ask.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ async function askAtris(args) {
const body = await sendMessage(token, oneShot);
spinner.stop();
await typeOut(stripEvents(body.response) || JSON.stringify(body));
if (body.success === false) return 1;
} catch (err) {
spinner.stop();
console.error(`✗ ${err.message}`);
Expand Down
69 changes: 69 additions & 0 deletions test/personal-chat-failure-status.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const http = require('node:http');
const { spawn } = require('node:child_process');

async function runChat(t, reply, interactive = false) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-chat-status-'));
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
const requests = [];
const server = http.createServer((req, res) => {
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', () => {
requests.push({ path: req.url, body: JSON.parse(body) });
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify(reply));
});
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
t.after(() => new Promise(resolve => { server.closeAllConnections(); server.close(resolve); }));
const token = `test.${Buffer.from(JSON.stringify({ type: 'agent_access', exp: Math.floor(Date.now() / 1000) + 3600 })).toString('base64url')}.test`;
const env = { PATH: process.env.PATH, HOME: dir, TMPDIR: dir, ATRIS_SKIP_UPDATE_CHECK: '1', ATRIS_AGENT_TOKEN_FILE: path.join(dir, 'token.json'), ATRIS_TOKEN: token, ATRIS_API_URL: `http://127.0.0.1:${server.address().port}/api`, NO_COLOR: '1' };
const args = [path.resolve(__dirname, '../bin/atris.js'), 'chat', '--personal', ...(interactive ? [] : ['hello'])];
const child = spawn(process.execPath, args, { cwd: dir, env, stdio: [interactive ? 'pipe' : 'ignore', 'pipe', 'pipe'] });
let stdout = '', stderr = '', sentBye = false;
child.stdout.on('data', chunk => {
stdout += chunk;
if (interactive && !sentBye && stdout.includes('confirm whether it finished')) {
sentBye = true;
setTimeout(() => child.stdin.end('bye\n'), 50);
}
});
if (interactive) child.stdin.write('hello\n');
child.stderr.on('data', chunk => { stderr += chunk; });
const timer = setTimeout(() => child.kill('SIGKILL'), 5000);
let code;
try {
code = await new Promise((resolve, reject) => { child.once('error', reject); child.once('close', resolve); });
} finally { clearTimeout(timer); }
return { code, stdout, stderr, requests };
}

test('one-shot personal chat prints uncertainty and reports failure', async t => {
const result = await runChat(t, { success: false, intent: 'message_received', response: "I received this request, but I can't confirm whether it finished. Check the result before starting it again.", agent_id: 'agent-1' });
assert.equal(result.code, 1, result.stderr);
assert.match(result.stdout, /can't confirm whether it finished/);
assert.deepEqual(result.requests, [{ path: '/api/message/incoming', body: { message: 'hello', source: 'cli' } }]);
});

test('one-shot personal chat retains success and legacy response handling', async t => {
for (const reply of [{ success: true, response: 'All done.' }, { response: 'Older reply.' }]) {
const result = await runChat(t, reply);
assert.equal(result.code, 0, result.stderr);
assert.match(result.stdout, new RegExp(reply.response.replace('.', '\\.')));
assert.equal(result.requests.length, 1);
}
});


test('interactive personal chat shows uncertainty without retrying it', async t => {
const result = await runChat(t, { success: false, intent: 'message_received', response: "I received this request, but I can't confirm whether it finished. Check the result before starting it again.", agent_id: 'agent-1' }, true);
assert.equal(result.code, 0, result.stderr);
assert.match(result.stdout, /can't confirm whether it finished/);
assert.equal(result.requests.length, 1);
});
Loading