groove-dev 0.27.183 → 0.27.184
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/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/innerchat-docs.js +41 -0
- package/node_modules/@groove-dev/daemon/src/innerchat.js +147 -91
- package/node_modules/@groove-dev/daemon/src/introducer.js +30 -1
- package/node_modules/@groove-dev/daemon/src/process.js +21 -0
- package/node_modules/@groove-dev/daemon/src/routes/innerchat.js +57 -13
- package/node_modules/@groove-dev/daemon/src/routes/teams.js +11 -0
- package/node_modules/@groove-dev/daemon/src/teams.js +28 -0
- package/node_modules/@groove-dev/daemon/test/innerchat.test.js +157 -184
- package/node_modules/@groove-dev/daemon/test/teams.test.js +53 -0
- package/{packages/gui/dist/assets/index-DPjGBQ5X.js → node_modules/@groove-dev/gui/dist/assets/index-BpOyN6Zf.js} +227 -227
- package/node_modules/@groove-dev/gui/dist/assets/index-DiXB7yry.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +1 -29
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-agent-row.jsx +18 -2
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-pane.jsx +1 -15
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-sidebar.jsx +29 -11
- package/node_modules/@groove-dev/gui/src/stores/groove.js +6 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/teams-slice.js +22 -0
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/innerchat-docs.js +41 -0
- package/packages/daemon/src/innerchat.js +147 -91
- package/packages/daemon/src/introducer.js +30 -1
- package/packages/daemon/src/process.js +21 -0
- package/packages/daemon/src/routes/innerchat.js +57 -13
- package/packages/daemon/src/routes/teams.js +11 -0
- package/packages/daemon/src/teams.js +28 -0
- package/{node_modules/@groove-dev/gui/dist/assets/index-DPjGBQ5X.js → packages/gui/dist/assets/index-BpOyN6Zf.js} +227 -227
- package/packages/gui/dist/assets/index-DiXB7yry.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-panel.jsx +1 -29
- package/packages/gui/src/components/fleet/fleet-agent-row.jsx +18 -2
- package/packages/gui/src/components/fleet/fleet-pane.jsx +1 -15
- package/packages/gui/src/components/fleet/fleet-sidebar.jsx +29 -11
- package/packages/gui/src/stores/groove.js +6 -0
- package/packages/gui/src/stores/slices/teams-slice.js +22 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CiOy7wVS.css +0 -1
- package/node_modules/@groove-dev/gui/src/components/agents/innerchat-relay.jsx +0 -145
- package/packages/gui/dist/assets/index-CiOy7wVS.css +0 -1
- package/packages/gui/src/components/agents/innerchat-relay.jsx +0 -145
|
@@ -3,30 +3,26 @@
|
|
|
3
3
|
|
|
4
4
|
import { describe, it, beforeEach } from 'node:test';
|
|
5
5
|
import assert from 'node:assert/strict';
|
|
6
|
-
import { InnerChat } from '../src/innerchat.js';
|
|
6
|
+
import { InnerChat, MAX_EXCHANGES } from '../src/innerchat.js';
|
|
7
7
|
|
|
8
|
-
// Mirrors the daemon surface deliverInstruction touches.
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// Mirrors the daemon surface deliverInstruction touches. `_loops` marks an
|
|
9
|
+
// interactive loop, `_running` a busy CLI agent; anything in neither is
|
|
10
|
+
// stopped (resume path, which mints a new id).
|
|
11
11
|
function makeDaemon() {
|
|
12
12
|
const broadcasts = [];
|
|
13
13
|
const audits = [];
|
|
14
|
-
const
|
|
15
|
-
const
|
|
14
|
+
const sent = [];
|
|
15
|
+
const queued = [];
|
|
16
16
|
const resumes = [];
|
|
17
|
-
|
|
18
17
|
let idCounter = 0;
|
|
19
18
|
|
|
20
19
|
return {
|
|
21
|
-
broadcasts,
|
|
22
|
-
audits,
|
|
23
|
-
sentMessages,
|
|
24
|
-
queuedMessages,
|
|
25
|
-
resumes,
|
|
20
|
+
broadcasts, audits, sent, queued, resumes,
|
|
26
21
|
registry: {
|
|
27
22
|
_agents: new Map(),
|
|
28
23
|
get(id) { return this._agents.get(id) || null; },
|
|
29
|
-
|
|
24
|
+
getAll() { return [...this._agents.values()]; },
|
|
25
|
+
add(agent) { this._agents.set(agent.id, agent); return agent; },
|
|
30
26
|
remove(id) { this._agents.delete(id); },
|
|
31
27
|
flushPendingRemovals() {},
|
|
32
28
|
update() {},
|
|
@@ -37,14 +33,8 @@ function makeDaemon() {
|
|
|
37
33
|
_running: new Set(),
|
|
38
34
|
hasAgentLoop(id) { return this._loops.has(id); },
|
|
39
35
|
isRunning(id) { return this._running.has(id); },
|
|
40
|
-
async sendMessage(id, msg,
|
|
41
|
-
|
|
42
|
-
return true;
|
|
43
|
-
},
|
|
44
|
-
queueMessage(id, msg) {
|
|
45
|
-
queuedMessages.push({ id, msg });
|
|
46
|
-
},
|
|
47
|
-
// Resume mints a new agent id, exactly as the real one does.
|
|
36
|
+
async sendMessage(id, msg) { sent.push({ id, msg }); return true; },
|
|
37
|
+
queueMessage(id, msg) { queued.push({ id, msg }); },
|
|
48
38
|
async resume(id, msg) {
|
|
49
39
|
const old = this._agentsRef.get(id);
|
|
50
40
|
const fresh = { ...old, id: `${id}-r${++idCounter}` };
|
|
@@ -62,228 +52,211 @@ function makeDaemon() {
|
|
|
62
52
|
};
|
|
63
53
|
}
|
|
64
54
|
|
|
65
|
-
function wire(daemon) {
|
|
66
|
-
daemon.processes._agentsRef = daemon.registry._agents;
|
|
67
|
-
daemon.rotator._daemon = daemon;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
55
|
const result = (text) => ({ type: 'result', data: text });
|
|
56
|
+
const tick = () => new Promise((r) => setImmediate(r));
|
|
71
57
|
|
|
72
58
|
describe('InnerChat', () => {
|
|
73
|
-
let daemon;
|
|
74
|
-
let innerchat;
|
|
59
|
+
let daemon, innerchat;
|
|
75
60
|
|
|
76
61
|
beforeEach(() => {
|
|
77
62
|
daemon = makeDaemon();
|
|
78
|
-
|
|
63
|
+
daemon.processes._agentsRef = daemon.registry._agents;
|
|
64
|
+
daemon.rotator._daemon = daemon;
|
|
79
65
|
innerchat = new InnerChat(daemon);
|
|
80
66
|
daemon.innerchat = innerchat;
|
|
81
67
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
daemon.processes._running.add('a2');
|
|
68
|
+
for (const [id, name] of [['a1', 'fullstack-1'], ['a2', 'fullstack-14']]) {
|
|
69
|
+
daemon.registry.add({ id, name, role: 'fullstack', provider: 'claude-code' });
|
|
70
|
+
daemon.processes._loops.add(id);
|
|
71
|
+
daemon.processes._running.add(id);
|
|
72
|
+
}
|
|
88
73
|
});
|
|
89
74
|
|
|
90
|
-
// ──
|
|
75
|
+
// ── The blocking round trip ───────────────────────────────
|
|
91
76
|
|
|
92
|
-
it('
|
|
93
|
-
const
|
|
77
|
+
it('blocks until the target answers, then resolves with the reply', async () => {
|
|
78
|
+
const pending = innerchat.ask('a1', 'a2', 'What endpoint shape?');
|
|
94
79
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
assert.equal(
|
|
99
|
-
assert.equal(turn.from.id, 'a1');
|
|
100
|
-
assert.equal(turn.to.id, 'a2');
|
|
80
|
+
let settled = false;
|
|
81
|
+
pending.then(() => { settled = true; });
|
|
82
|
+
await tick();
|
|
83
|
+
assert.equal(settled, false, 'must not resolve before an answer arrives');
|
|
101
84
|
|
|
102
|
-
const delivered = daemon.
|
|
85
|
+
const delivered = daemon.sent.at(-1);
|
|
103
86
|
assert.equal(delivered.id, 'a2');
|
|
104
|
-
assert.match(delivered.msg, /
|
|
105
|
-
assert.match(delivered.msg, /
|
|
106
|
-
});
|
|
87
|
+
assert.match(delivered.msg, /fullstack-1 \(fullstack\) is asking you a question/);
|
|
88
|
+
assert.match(delivered.msg, /BLOCKED waiting on your answer/);
|
|
107
89
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
await assert.rejects(() => innerchat.send('a1', 'nope', 'hi'), /not found/);
|
|
111
|
-
await assert.rejects(() => innerchat.send('a1', 'a1', 'hi'), /itself/);
|
|
112
|
-
await assert.rejects(() => innerchat.send('a1', 'a2', ' '), /message is required/);
|
|
113
|
-
});
|
|
90
|
+
innerchat.onAgentOutput('a2', result('REST, /api/v2/orders'));
|
|
91
|
+
const res = await pending;
|
|
114
92
|
|
|
115
|
-
|
|
116
|
-
|
|
93
|
+
assert.equal(res.reply, 'REST, /api/v2/orders');
|
|
94
|
+
assert.equal(res.exchanges, 1);
|
|
95
|
+
assert.equal(res.remaining, MAX_EXCHANGES - 1);
|
|
117
96
|
});
|
|
118
97
|
|
|
119
|
-
|
|
98
|
+
it('ignores non-result and empty output while waiting', async () => {
|
|
99
|
+
const pending = innerchat.ask('a1', 'a2', 'What shape?');
|
|
100
|
+
await tick();
|
|
120
101
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
await new Promise((r) => setImmediate(r));
|
|
102
|
+
innerchat.onAgentOutput('a2', { type: 'assistant', data: 'thinking…' });
|
|
103
|
+
innerchat.onAgentOutput('a2', result(' '));
|
|
104
|
+
await tick();
|
|
125
105
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
assert.
|
|
129
|
-
assert.match(forwarded.msg, /REST, \/api\/v2\/orders/);
|
|
106
|
+
assert.ok(innerchat.getPending('a2'), 'still waiting');
|
|
107
|
+
innerchat.onAgentOutput('a2', result('done'));
|
|
108
|
+
assert.equal((await pending).reply, 'done');
|
|
130
109
|
});
|
|
131
110
|
|
|
132
|
-
it('
|
|
133
|
-
|
|
134
|
-
innerchat.
|
|
135
|
-
|
|
136
|
-
|
|
111
|
+
it('skips the in-flight result when the question queued behind live work', async () => {
|
|
112
|
+
daemon.processes._loops.delete('a2'); // running, no loop → queued
|
|
113
|
+
const pending = innerchat.ask('a1', 'a2', 'What shape?');
|
|
114
|
+
await tick();
|
|
115
|
+
assert.equal(daemon.queued.at(-1).id, 'a2');
|
|
137
116
|
|
|
138
|
-
|
|
139
|
-
await
|
|
140
|
-
innerchat.
|
|
141
|
-
innerchat.onAgentOutput('a2', result(' '));
|
|
142
|
-
await new Promise((r) => setImmediate(r));
|
|
117
|
+
innerchat.onAgentOutput('a2', result('finished the PREVIOUS task'));
|
|
118
|
+
await tick();
|
|
119
|
+
assert.ok(innerchat.getPending('a2'), 'prior task result must not be taken as the answer');
|
|
143
120
|
|
|
144
|
-
|
|
145
|
-
assert.
|
|
121
|
+
innerchat.onAgentOutput('a2', result('the actual answer'));
|
|
122
|
+
assert.equal((await pending).reply, 'the actual answer');
|
|
146
123
|
});
|
|
147
124
|
|
|
148
|
-
|
|
149
|
-
await innerchat.send('a1', 'a2', 'What shape?');
|
|
150
|
-
innerchat.onAgentOutput('a2', result('first'));
|
|
151
|
-
innerchat.onAgentOutput('a2', result('second'));
|
|
152
|
-
await new Promise((r) => setImmediate(r));
|
|
125
|
+
// ── Guardrails ────────────────────────────────────────────
|
|
153
126
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
127
|
+
it('refuses a call that would deadlock and says how to resolve it', async () => {
|
|
128
|
+
const outbound = innerchat.ask('a1', 'a2', 'question for 14');
|
|
129
|
+
await tick();
|
|
130
|
+
|
|
131
|
+
// a2 tries to ask a1 back while a1 is blocked on a2.
|
|
132
|
+
await assert.rejects(
|
|
133
|
+
() => innerchat.ask('a2', 'a1', 'counter-question'),
|
|
134
|
+
/waiting for YOUR answer/,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
innerchat.onAgentOutput('a2', result('fine'));
|
|
138
|
+
await outbound;
|
|
157
139
|
});
|
|
158
140
|
|
|
159
|
-
it('
|
|
160
|
-
|
|
161
|
-
daemon.processes._loops.
|
|
162
|
-
|
|
163
|
-
assert.equal(daemon.queuedMessages.at(-1).id, 'a2');
|
|
141
|
+
it('refuses a second concurrent question to a busy target', async () => {
|
|
142
|
+
daemon.registry.add({ id: 'a3', name: 'fullstack-9', role: 'fullstack', provider: 'claude-code' });
|
|
143
|
+
daemon.processes._loops.add('a3');
|
|
144
|
+
daemon.processes._running.add('a3');
|
|
164
145
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
await
|
|
168
|
-
assert.equal(daemon.sentMessages.filter((m) => /InnerChat reply/.test(m.msg)).length, 0);
|
|
146
|
+
const first = innerchat.ask('a1', 'a2', 'q1');
|
|
147
|
+
await tick();
|
|
148
|
+
await assert.rejects(() => innerchat.ask('a3', 'a2', 'q2'), /already answering/);
|
|
169
149
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
await new Promise((r) => setImmediate(r));
|
|
173
|
-
const forwarded = daemon.sentMessages.at(-1);
|
|
174
|
-
assert.equal(forwarded.id, 'a1');
|
|
175
|
-
assert.match(forwarded.msg, /REST, \/api\/v2\/orders/);
|
|
150
|
+
innerchat.onAgentOutput('a2', result('a'));
|
|
151
|
+
await first;
|
|
176
152
|
});
|
|
177
153
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
154
|
+
it('enforces the exchange cap and tells the agent to report', async () => {
|
|
155
|
+
for (let i = 0; i < MAX_EXCHANGES; i++) {
|
|
156
|
+
const p = innerchat.ask('a1', 'a2', `q${i}`);
|
|
157
|
+
await tick();
|
|
158
|
+
innerchat.onAgentOutput('a2', result(`a${i}`));
|
|
159
|
+
await p;
|
|
160
|
+
}
|
|
161
|
+
await assert.rejects(
|
|
162
|
+
() => innerchat.ask('a1', 'a2', 'one more'),
|
|
163
|
+
new RegExp(`${MAX_EXCHANGES}-exchange limit`),
|
|
164
|
+
);
|
|
165
|
+
});
|
|
183
166
|
|
|
184
|
-
|
|
185
|
-
const
|
|
167
|
+
it('counts exchanges down across a continuing conversation', async () => {
|
|
168
|
+
const p1 = innerchat.ask('a1', 'a2', 'q1');
|
|
169
|
+
await tick(); innerchat.onAgentOutput('a2', result('a1'));
|
|
170
|
+
const r1 = await p1;
|
|
186
171
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
assert.equal(innerchat.getPending('a2'), null, 'old id no longer tracked');
|
|
191
|
-
assert.ok(innerchat.getPending(newId), 'reply is awaited on the new id');
|
|
172
|
+
const p2 = innerchat.ask('a1', 'a2', 'q2');
|
|
173
|
+
await tick(); innerchat.onAgentOutput('a2', result('a2'));
|
|
174
|
+
const r2 = await p2;
|
|
192
175
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
await new Promise((r) => setImmediate(r));
|
|
197
|
-
assert.match(daemon.sentMessages.at(-1).msg, /pong/);
|
|
176
|
+
assert.equal(r1.threadId, r2.threadId, 'same conversation continues');
|
|
177
|
+
assert.equal(r2.exchanges, 2);
|
|
178
|
+
assert.equal(r2.remaining, MAX_EXCHANGES - 2);
|
|
198
179
|
});
|
|
199
180
|
|
|
200
|
-
it('
|
|
201
|
-
await
|
|
202
|
-
|
|
203
|
-
|
|
181
|
+
it('times out rather than blocking forever', async () => {
|
|
182
|
+
await assert.rejects(
|
|
183
|
+
() => innerchat.ask('a1', 'a2', 'silence', { timeoutMs: 20 }),
|
|
184
|
+
/No answer within/,
|
|
185
|
+
);
|
|
186
|
+
// The slot must be released, or the pair is wedged for good.
|
|
187
|
+
assert.equal(innerchat.getPending('a2'), null);
|
|
188
|
+
assert.equal(innerchat.blockedOn.get('a1'), undefined);
|
|
189
|
+
});
|
|
204
190
|
|
|
205
|
-
|
|
206
|
-
|
|
191
|
+
it('unblocks the asker when the target dies mid-question', async () => {
|
|
192
|
+
const pending = innerchat.ask('a1', 'a2', 'q');
|
|
193
|
+
await tick();
|
|
194
|
+
innerchat.onAgentGone('a2', 'crashed');
|
|
195
|
+
await assert.rejects(() => pending, /crashed before answering/);
|
|
196
|
+
assert.equal(innerchat.blockedOn.get('a1'), undefined);
|
|
197
|
+
});
|
|
207
198
|
|
|
208
|
-
|
|
209
|
-
assert.
|
|
210
|
-
assert.
|
|
199
|
+
it('rejects unknown, self-addressed and empty asks', async () => {
|
|
200
|
+
await assert.rejects(() => innerchat.ask('nope', 'a2', 'x'), /not found/);
|
|
201
|
+
await assert.rejects(() => innerchat.ask('a1', 'nope', 'x'), /not found/);
|
|
202
|
+
await assert.rejects(() => innerchat.ask('a1', 'a1', 'x'), /cannot ask itself/);
|
|
203
|
+
await assert.rejects(() => innerchat.ask('a1', 'a2', ' '), /message is required/);
|
|
211
204
|
});
|
|
212
205
|
|
|
213
|
-
it('
|
|
206
|
+
it('surfaces a delivery failure as an unreachable error', async () => {
|
|
214
207
|
daemon.processes.sendMessage = async () => { throw new Error('pipe closed'); };
|
|
215
|
-
await assert.rejects(() => innerchat.
|
|
216
|
-
|
|
217
|
-
const thread = innerchat.getThreads('a1')[0];
|
|
218
|
-
assert.equal(thread.status, 'failed');
|
|
219
|
-
assert.equal(thread.turns[0].status, 'failed');
|
|
220
|
-
assert.equal(thread.turns[0].error, 'pipe closed');
|
|
208
|
+
await assert.rejects(() => innerchat.ask('a1', 'a2', 'x'), /Could not reach fullstack-14: pipe closed/);
|
|
209
|
+
assert.equal(innerchat.blockedOn.get('a1'), undefined);
|
|
221
210
|
});
|
|
222
211
|
|
|
223
|
-
// ──
|
|
212
|
+
// ── Stopped targets / id remapping ────────────────────────
|
|
224
213
|
|
|
225
|
-
it('
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
await new Promise((r) => setImmediate(r));
|
|
214
|
+
it('resumes a stopped target and matches the answer on its new id', async () => {
|
|
215
|
+
daemon.processes._loops.delete('a2');
|
|
216
|
+
daemon.processes._running.delete('a2');
|
|
229
217
|
|
|
230
|
-
|
|
218
|
+
const pending = innerchat.ask('a1', 'a2', 'ping');
|
|
219
|
+
await tick();
|
|
231
220
|
|
|
232
|
-
const
|
|
233
|
-
assert.
|
|
234
|
-
assert.
|
|
235
|
-
assert.
|
|
236
|
-
assert.match(relay.msg, /Versioned\?/);
|
|
221
|
+
const newId = daemon.resumes.at(-1).newId;
|
|
222
|
+
assert.notEqual(newId, 'a2');
|
|
223
|
+
assert.equal(innerchat.getPending('a2'), null, 'old id no longer tracked');
|
|
224
|
+
assert.ok(innerchat.getPending(newId), 'awaited on the new id');
|
|
237
225
|
|
|
238
|
-
|
|
239
|
-
assert.equal(
|
|
226
|
+
innerchat.onAgentOutput(newId, result('pong'));
|
|
227
|
+
assert.equal((await pending).reply, 'pong');
|
|
240
228
|
});
|
|
241
229
|
|
|
242
|
-
|
|
243
|
-
daemon.registry.add({ id: 'a3', name: 'fullstack-9', role: 'fullstack', provider: 'claude-code' });
|
|
244
|
-
daemon.processes._loops.add('a3');
|
|
245
|
-
daemon.processes._running.add('a3');
|
|
246
|
-
|
|
247
|
-
const one = await innerchat.send('a1', 'a2', 'question for 14');
|
|
248
|
-
const two = await innerchat.send('a1', 'a3', 'question for 9');
|
|
249
|
-
assert.notEqual(one.thread.id, two.thread.id);
|
|
250
|
-
|
|
251
|
-
innerchat.onAgentOutput('a3', result('answer from 9'));
|
|
252
|
-
await new Promise((r) => setImmediate(r));
|
|
253
|
-
|
|
254
|
-
assert.equal(two.thread.turns.at(-1).text, 'answer from 9');
|
|
255
|
-
assert.equal(one.thread.turns.length, 1, 'the other thread is untouched');
|
|
256
|
-
assert.ok(innerchat.getPending('a2'), 'still awaiting the first reply');
|
|
257
|
-
});
|
|
230
|
+
// ── Conversation context ──────────────────────────────────
|
|
258
231
|
|
|
259
|
-
it('
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
232
|
+
it('replays prior turns so neither side re-explains', async () => {
|
|
233
|
+
const p1 = innerchat.ask('a1', 'a2', 'What shape?');
|
|
234
|
+
await tick(); innerchat.onAgentOutput('a2', result('REST'));
|
|
235
|
+
await p1;
|
|
263
236
|
|
|
264
|
-
|
|
265
|
-
await
|
|
237
|
+
const p2 = innerchat.ask('a1', 'a2', 'Versioned?');
|
|
238
|
+
await tick();
|
|
239
|
+
const msg = daemon.sent.at(-1).msg;
|
|
240
|
+
assert.match(msg, /Earlier in this conversation/);
|
|
241
|
+
assert.match(msg, /fullstack-1: What shape\?/);
|
|
242
|
+
assert.match(msg, /fullstack-14: REST/);
|
|
243
|
+
assert.match(msg, /Versioned\?/);
|
|
266
244
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
assert.equal(innerchat.getThreads('a2').length, 1);
|
|
270
|
-
assert.equal(innerchat.getThreads('a3').length, 1);
|
|
271
|
-
assert.equal(innerchat.getThreads('nobody').length, 0);
|
|
245
|
+
innerchat.onAgentOutput('a2', result('v2'));
|
|
246
|
+
await p2;
|
|
272
247
|
});
|
|
273
248
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
await new Promise((r) => setImmediate(r));
|
|
249
|
+
it('broadcasts and audits both directions', async () => {
|
|
250
|
+
const p = innerchat.ask('a1', 'a2', 'q');
|
|
251
|
+
await tick();
|
|
252
|
+
innerchat.onAgentOutput('a2', result('a'));
|
|
253
|
+
await p;
|
|
280
254
|
|
|
281
255
|
const turns = daemon.broadcasts.filter((b) => b.type === 'innerchat:turn');
|
|
282
256
|
assert.equal(turns.length, 2);
|
|
283
|
-
assert.equal(turns[0].data.turn.kind, '
|
|
284
|
-
assert.equal(turns[1].data.turn.kind, '
|
|
285
|
-
|
|
286
|
-
assert.ok(daemon.audits.some((a) => a.type === 'innerchat.
|
|
287
|
-
assert.ok(daemon.audits.some((a) => a.type === 'innerchat.reply'));
|
|
257
|
+
assert.equal(turns[0].data.turn.kind, 'ask');
|
|
258
|
+
assert.equal(turns[1].data.turn.kind, 'answer');
|
|
259
|
+
assert.ok(daemon.audits.some((a) => a.type === 'innerchat.ask'));
|
|
260
|
+
assert.ok(daemon.audits.some((a) => a.type === 'innerchat.answer'));
|
|
288
261
|
});
|
|
289
262
|
});
|
|
@@ -185,6 +185,59 @@ describe('Teams', () => {
|
|
|
185
185
|
assert.ok(teams2.list().some((t) => t.name === 'Persistent'));
|
|
186
186
|
});
|
|
187
187
|
|
|
188
|
+
describe('reorder', () => {
|
|
189
|
+
it('reorders the list and persists it', () => {
|
|
190
|
+
const a = teams.create('Alpha');
|
|
191
|
+
const b = teams.create('Bravo');
|
|
192
|
+
const c = teams.create('Charlie');
|
|
193
|
+
const def = teams.getDefault();
|
|
194
|
+
|
|
195
|
+
teams.reorder([c.id, a.id, b.id, def.id]);
|
|
196
|
+
assert.deepEqual(teams.list().map((t) => t.name), ['Charlie', 'Alpha', 'Bravo', 'Default']);
|
|
197
|
+
|
|
198
|
+
// Order is the persisted array order, so it survives a reload.
|
|
199
|
+
const reloaded = new Teams(daemon);
|
|
200
|
+
assert.deepEqual(reloaded.list().map((t) => t.name), ['Charlie', 'Alpha', 'Bravo', 'Default']);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('keeps teams missing from the given order rather than dropping them', () => {
|
|
204
|
+
const a = teams.create('Alpha');
|
|
205
|
+
const b = teams.create('Bravo');
|
|
206
|
+
const before = teams.list().length;
|
|
207
|
+
|
|
208
|
+
// A stale client sends only the ids it knew about.
|
|
209
|
+
teams.reorder([b.id, a.id]);
|
|
210
|
+
|
|
211
|
+
assert.equal(teams.list().length, before);
|
|
212
|
+
assert.deepEqual(teams.list().map((t) => t.name), ['Bravo', 'Alpha', 'Default']);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('ignores unknown and duplicated ids', () => {
|
|
216
|
+
const a = teams.create('Alpha');
|
|
217
|
+
const before = teams.list().length;
|
|
218
|
+
|
|
219
|
+
teams.reorder([a.id, 'does-not-exist', a.id]);
|
|
220
|
+
|
|
221
|
+
assert.equal(teams.list().length, before);
|
|
222
|
+
assert.equal(teams.list()[0].name, 'Alpha');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('rejects a non-array', () => {
|
|
226
|
+
assert.throws(() => teams.reorder('nope'), /must be an array/);
|
|
227
|
+
assert.throws(() => teams.reorder(undefined), /must be an array/);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('broadcasts the new order', () => {
|
|
231
|
+
const a = teams.create('Alpha');
|
|
232
|
+
broadcasts.length = 0;
|
|
233
|
+
teams.reorder([a.id]);
|
|
234
|
+
|
|
235
|
+
const evt = broadcasts.find((b) => b.type === 'teams:reordered');
|
|
236
|
+
assert.ok(evt, 'teams:reordered was broadcast');
|
|
237
|
+
assert.equal(evt.teams[0].name, 'Alpha');
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
188
241
|
it('should provide backward compat stubs', () => {
|
|
189
242
|
// onAgentChange is a no-op
|
|
190
243
|
teams.onAgentChange();
|