feat: add assistant proposal worker runtime
This commit is contained in:
+166
-285
@@ -2,7 +2,8 @@ import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import type { ConversationRequest, ConversationRuntime } from "../src/acp/types.js";
|
||||
import type { PlatformAdapter } from "../src/core/adapter.js";
|
||||
import { Gateway, type GatewayTimer } from "../src/core/gateway.js";
|
||||
import { Gateway } from "../src/core/gateway.js";
|
||||
import type { Proposal } from "../src/core/proposal-store.js";
|
||||
import type { IncomingMessage, OutgoingMessage } from "../src/core/types.js";
|
||||
|
||||
interface PendingTurn {
|
||||
@@ -11,11 +12,31 @@ interface PendingTurn {
|
||||
reject(error: Error): void;
|
||||
}
|
||||
|
||||
function fakeProposal(overrides: Partial<Proposal> = {}): Proposal {
|
||||
return {
|
||||
id: "proposal-1",
|
||||
title: "修复登录页",
|
||||
goal: "goal",
|
||||
steps: ["step"],
|
||||
ownerChatKey: "qq:chat",
|
||||
requesterUserId: "user",
|
||||
status: "proposed",
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
class FakeRuntime implements ConversationRuntime {
|
||||
readonly turns: PendingTurn[] = [];
|
||||
cancelled = 0;
|
||||
resets = 0;
|
||||
idleSeconds = 0;
|
||||
confirmed = 0;
|
||||
stopped = 0;
|
||||
listed = 0;
|
||||
cancelResult = true;
|
||||
confirmResult = true;
|
||||
stopResult = true;
|
||||
proposals: Proposal[] = [];
|
||||
|
||||
async prompt(request: ConversationRequest) {
|
||||
return new Promise<{ text: string; botId: string; agentId: string }>((resolve, reject) => {
|
||||
@@ -26,75 +47,21 @@ class FakeRuntime implements ConversationRuntime {
|
||||
});
|
||||
});
|
||||
}
|
||||
async cancel() { this.cancelled++; this.turns.at(-1)?.resolve("cancelled"); return true; }
|
||||
async reset() { this.resets++; }
|
||||
async cancel() { this.cancelled++; return this.cancelResult; }
|
||||
async confirm() { this.confirmed++; return this.confirmResult; }
|
||||
async stop() { this.stopped++; return this.stopResult; }
|
||||
listProposals() { this.listed++; return this.proposals; }
|
||||
async reset() {}
|
||||
status() {
|
||||
return {
|
||||
bot: "test-bot", agent: "kimi", workspace: "/tmp",
|
||||
running: this.turns.length > 0, phase: "processing", idleSeconds: this.idleSeconds
|
||||
running: this.turns.length > 0, phase: "processing"
|
||||
};
|
||||
}
|
||||
stats() { return { activeWorkers: 0, inFlight: 0, crashes: 0, persistedBindings: 0 }; }
|
||||
async shutdown() {}
|
||||
}
|
||||
|
||||
interface ScheduledTimer extends GatewayTimer {
|
||||
deadline: number;
|
||||
callback: () => void;
|
||||
cancelled: boolean;
|
||||
order: number;
|
||||
}
|
||||
|
||||
class FakeClock {
|
||||
nowMs = 0;
|
||||
private order = 0;
|
||||
readonly timers: ScheduledTimer[] = [];
|
||||
|
||||
readonly now = () => this.nowMs;
|
||||
readonly schedule = (callback: () => void, delayMs: number): GatewayTimer => {
|
||||
const timer: ScheduledTimer = {
|
||||
deadline: this.nowMs + delayMs,
|
||||
callback,
|
||||
cancelled: false,
|
||||
order: this.order++,
|
||||
cancel() { timer.cancelled = true; }
|
||||
};
|
||||
this.timers.push(timer);
|
||||
return timer;
|
||||
};
|
||||
|
||||
set(timeMs: number) { this.nowMs = timeMs; }
|
||||
|
||||
async advanceTo(timeMs: number) {
|
||||
while (true) {
|
||||
const next = this.timers
|
||||
.filter((timer) => !timer.cancelled && timer.deadline <= timeMs)
|
||||
.sort((left, right) => left.deadline - right.deadline || left.order - right.order)[0];
|
||||
if (!next) break;
|
||||
next.cancelled = true;
|
||||
this.nowMs = next.deadline;
|
||||
next.callback();
|
||||
await flush();
|
||||
}
|
||||
this.nowMs = timeMs;
|
||||
}
|
||||
|
||||
async jumpTo(timeMs: number) {
|
||||
this.nowMs = timeMs;
|
||||
const due = this.timers
|
||||
.filter((timer) => !timer.cancelled && timer.deadline <= timeMs)
|
||||
.sort((left, right) => left.deadline - right.deadline || left.order - right.order);
|
||||
for (const timer of due) {
|
||||
if (timer.cancelled) continue;
|
||||
timer.cancelled = true;
|
||||
timer.callback();
|
||||
await flush();
|
||||
}
|
||||
}
|
||||
|
||||
activeTimers() { return this.timers.filter((timer) => !timer.cancelled).length; }
|
||||
}
|
||||
|
||||
const policy = { allowedUsers: [] as string[], allowedChats: [] as string[], requireMentionInGroup: false };
|
||||
const message = (text: string, messageId = text, chatId = "chat"): IncomingMessage => ({
|
||||
platform: "qq", chatId, userId: "user", text, messageId
|
||||
@@ -109,8 +76,8 @@ function recordingAdapter(sent: OutgoingMessage[] = []): PlatformAdapter {
|
||||
return { name: "test", async handleWebhook() { return {}; }, async sendMessage(outgoing) { sent.push(outgoing); } };
|
||||
}
|
||||
|
||||
function createGateway(runtime = new FakeRuntime(), clock = new FakeClock()) {
|
||||
return { runtime, clock, gateway: new Gateway(policy, runtime, { now: clock.now, schedule: clock.schedule }) };
|
||||
function createGateway(runtime = new FakeRuntime()) {
|
||||
return { runtime, gateway: new Gateway(policy, runtime) };
|
||||
}
|
||||
|
||||
function messagesFor(sent: OutgoingMessage[], replyTo: string) {
|
||||
@@ -127,172 +94,43 @@ test("short asynchronous work sends only the real result with sequence one", asy
|
||||
assert.deepEqual(sent.map((outgoing) => [outgoing.text, outgoing.replySequence]), [["done", 1]]);
|
||||
});
|
||||
|
||||
test("completion and timer callbacks are safe in both orders at 15 seconds", async () => {
|
||||
{
|
||||
const { runtime, clock, gateway } = createGateway();
|
||||
const sent: OutgoingMessage[] = [];
|
||||
const turn = gateway.receive(message("work", "completion-first"), recordingAdapter(sent));
|
||||
await waitFor(() => runtime.turns.length === 1);
|
||||
clock.set(15_000);
|
||||
runtime.turns[0].resolve("done");
|
||||
await turn;
|
||||
await clock.advanceTo(15_000);
|
||||
assert.deepEqual(sent.map((outgoing) => outgoing.text), ["done"]);
|
||||
}
|
||||
{
|
||||
const { runtime, clock, gateway } = createGateway();
|
||||
const sent: OutgoingMessage[] = [];
|
||||
const turn = gateway.receive(message("work", "timer-first"), recordingAdapter(sent));
|
||||
await waitFor(() => runtime.turns.length === 1);
|
||||
await clock.advanceTo(15_000);
|
||||
runtime.turns[0].resolve("done");
|
||||
await turn;
|
||||
assert.deepEqual(sent.map((outgoing) => [outgoing.text, outgoing.replySequence]), [
|
||||
["收到,我还在处理,稍等我一下。", 1],
|
||||
["done", 2]
|
||||
]);
|
||||
}
|
||||
test("ordinary asynchronous messages send nothing before the runtime resolves", async () => {
|
||||
const { runtime, gateway } = createGateway();
|
||||
const sent: OutgoingMessage[] = [];
|
||||
const turn = gateway.receive(message("work", "no-timer"), recordingAdapter(sent));
|
||||
await waitFor(() => runtime.turns.length === 1);
|
||||
await flush();
|
||||
assert.deepEqual(sent, []);
|
||||
runtime.turns[0].resolve("done");
|
||||
await turn;
|
||||
assert.deepEqual(sent.map((outgoing) => outgoing.text), ["done"]);
|
||||
});
|
||||
|
||||
test("running and queued notices are independent and queued work announces start", async () => {
|
||||
const { runtime, clock, gateway } = createGateway();
|
||||
test("ordinary messages reach the runtime immediately without a Gateway queue", async () => {
|
||||
const { runtime, gateway } = createGateway();
|
||||
const sent: OutgoingMessage[] = [];
|
||||
const adapter = recordingAdapter(sent);
|
||||
const first = gateway.receive(message("one", "first"), adapter);
|
||||
const second = gateway.receive(message("two", "second"), adapter);
|
||||
await waitFor(() => runtime.turns.length === 1);
|
||||
await clock.advanceTo(15_000);
|
||||
assert.deepEqual(messagesFor(sent, "first").map((outgoing) => outgoing.text), ["收到,我还在处理,稍等我一下。"]);
|
||||
assert.deepEqual(messagesFor(sent, "second").map((outgoing) => outgoing.text), ["前面还有一件事没处理完,这条我记着,轮到后马上处理。"]);
|
||||
await waitFor(() => runtime.turns.length === 2);
|
||||
assert.deepEqual(sent, []);
|
||||
|
||||
runtime.turns[0].resolve("first done");
|
||||
await waitFor(() => runtime.turns.length === 2);
|
||||
assert.deepEqual(messagesFor(sent, "second").map((outgoing) => [outgoing.text, outgoing.replySequence]), [
|
||||
["前面还有一件事没处理完,这条我记着,轮到后马上处理。", 1],
|
||||
["轮到这件事了,我现在开始处理。", 2]
|
||||
]);
|
||||
runtime.turns[1].resolve("second done");
|
||||
await Promise.all([first, second]);
|
||||
assert.deepEqual(messagesFor(sent, "first").map((outgoing) => outgoing.replySequence), [1, 2]);
|
||||
assert.deepEqual(messagesFor(sent, "second").map((outgoing) => outgoing.replySequence), [1, 2, 3]);
|
||||
assert.deepEqual(messagesFor(sent, "first").map((outgoing) => [outgoing.text, outgoing.replySequence]), [["first done", 1]]);
|
||||
assert.deepEqual(messagesFor(sent, "second").map((outgoing) => [outgoing.text, outgoing.replySequence]), [["second done", 1]]);
|
||||
});
|
||||
|
||||
test("running reminders use staged wording and wait from 8 to 18 minutes", async () => {
|
||||
const { runtime, clock, gateway } = createGateway();
|
||||
const sent: OutgoingMessage[] = [];
|
||||
const turn = gateway.receive(message("work", "reminders"), recordingAdapter(sent));
|
||||
await waitFor(() => runtime.turns.length === 1);
|
||||
await clock.advanceTo(15_000);
|
||||
await clock.advanceTo(60_000);
|
||||
await clock.advanceTo(180_000);
|
||||
await clock.advanceTo(480_000);
|
||||
assert.deepEqual(sent.map((outgoing) => outgoing.text), [
|
||||
"收到,我还在处理,稍等我一下。",
|
||||
"还在弄,暂时还没出结果,弄好我马上回你。",
|
||||
"这件事比预想中多花了一点时间,我还在继续处理。你不用一直盯着,弄好我会直接回你。",
|
||||
"我还在处理这件事,确实花了些时间。想看看现在的状态可以发 /status,不想继续等也可以发 /cancel。"
|
||||
]);
|
||||
await clock.advanceTo(780_000);
|
||||
assert.equal(sent.length, 4);
|
||||
await clock.advanceTo(1_080_000);
|
||||
assert.equal(sent.at(-1)?.text,
|
||||
"我还在处理这件事,确实花了些时间。想看看现在的状态可以发 /status,不想继续等也可以发 /cancel。");
|
||||
assert.equal(sent.length, 5);
|
||||
runtime.turns[0].resolve("done");
|
||||
await turn;
|
||||
});
|
||||
|
||||
test("queued reminders use staged queue wording without ACP activity details", async () => {
|
||||
const { runtime, clock, gateway } = createGateway();
|
||||
const sent: OutgoingMessage[] = [];
|
||||
const adapter = recordingAdapter(sent);
|
||||
const first = gateway.receive(message("one", "active"), adapter);
|
||||
const second = gateway.receive(message("two", "queued"), adapter);
|
||||
await waitFor(() => runtime.turns.length === 1);
|
||||
await clock.advanceTo(15_000);
|
||||
await clock.advanceTo(60_000);
|
||||
assert.deepEqual(messagesFor(sent, "queued").map((outgoing) => outgoing.text), [
|
||||
"前面还有一件事没处理完,这条我记着,轮到后马上处理。",
|
||||
"前面的事情还没处理完,这条还在等。我没有漏掉,轮到后会接着做。"
|
||||
]);
|
||||
runtime.turns[0].resolve("one done");
|
||||
await waitFor(() => runtime.turns.length === 2);
|
||||
runtime.turns[1].resolve("two done");
|
||||
await Promise.all([first, second]);
|
||||
});
|
||||
|
||||
test("a delayed timer jump emits one reminder instead of backfilling every deadline", async () => {
|
||||
const { runtime, clock, gateway } = createGateway();
|
||||
const sent: OutgoingMessage[] = [];
|
||||
const turn = gateway.receive(message("work", "jump"), recordingAdapter(sent));
|
||||
await waitFor(() => runtime.turns.length === 1);
|
||||
await clock.advanceTo(15_000);
|
||||
await clock.jumpTo(1_000_000);
|
||||
assert.equal(sent.length, 2);
|
||||
assert.equal(clock.activeTimers(), 1);
|
||||
runtime.turns[0].resolve("done");
|
||||
await turn;
|
||||
});
|
||||
|
||||
test("completion and shutdown cancel all inbound timers", async () => {
|
||||
const { runtime, clock, gateway } = createGateway();
|
||||
const sent: OutgoingMessage[] = [];
|
||||
const first = gateway.receive(message("one", "complete"), recordingAdapter(sent));
|
||||
await waitFor(() => runtime.turns.length === 1);
|
||||
assert.equal(clock.activeTimers(), 1);
|
||||
runtime.turns[0].resolve("done");
|
||||
await first;
|
||||
assert.equal(clock.activeTimers(), 0);
|
||||
|
||||
const second = gateway.receive(message("two", "shutdown"), recordingAdapter(sent));
|
||||
await waitFor(() => runtime.turns.length === 2);
|
||||
assert.equal(clock.activeTimers(), 1);
|
||||
gateway.shutdown();
|
||||
assert.equal(clock.activeTimers(), 0);
|
||||
await clock.advanceTo(600_000);
|
||||
assert.deepEqual(messagesFor(sent, "shutdown"), []);
|
||||
runtime.turns[1].resolve("done after shutdown");
|
||||
await second;
|
||||
});
|
||||
|
||||
test("prompt does not wait for a notice send and final delivery delay releases the chat lock", async () => {
|
||||
const { runtime, clock, gateway } = createGateway();
|
||||
const sent: OutgoingMessage[] = [];
|
||||
let releaseFirstSend!: () => void;
|
||||
const firstSend = new Promise<void>((resolve) => { releaseFirstSend = resolve; });
|
||||
const adapter: PlatformAdapter = {
|
||||
name: "test", async handleWebhook() { return {}; },
|
||||
async sendMessage(outgoing) {
|
||||
sent.push(outgoing);
|
||||
if (outgoing.replyTo === "first" && outgoing.replySequence === 1) await firstSend;
|
||||
}
|
||||
};
|
||||
const first = gateway.receive(message("one", "first"), adapter);
|
||||
await waitFor(() => runtime.turns.length === 1);
|
||||
await clock.advanceTo(15_000);
|
||||
assert.equal(runtime.turns.length, 1);
|
||||
runtime.turns[0].resolve("first done");
|
||||
const second = gateway.receive(message("two", "second"), adapter);
|
||||
await waitFor(() => runtime.turns.length === 2);
|
||||
assert.equal(messagesFor(sent, "first").length, 1);
|
||||
releaseFirstSend();
|
||||
runtime.turns[1].resolve("second done");
|
||||
await Promise.all([first, second]);
|
||||
assert.deepEqual(messagesFor(sent, "first").map((outgoing) => outgoing.text), [
|
||||
"收到,我还在处理,稍等我一下。", "first done"
|
||||
]);
|
||||
});
|
||||
|
||||
test("delivery failures are logged safely and do not block later sends", async () => {
|
||||
const { runtime, clock, gateway } = createGateway();
|
||||
test("delivery failures are logged safely and do not block the result", async () => {
|
||||
const { runtime, gateway } = createGateway();
|
||||
const sent: OutgoingMessage[] = [];
|
||||
const errors: string[] = [];
|
||||
let sends = 0;
|
||||
const adapter: PlatformAdapter = {
|
||||
name: "test", async handleWebhook() { return {}; },
|
||||
async sendMessage(outgoing) {
|
||||
sent.push(outgoing);
|
||||
if (++sends === 1) throw new Error("sensitive provider response");
|
||||
throw new Error("sensitive provider response");
|
||||
}
|
||||
};
|
||||
const originalError = console.error;
|
||||
@@ -300,35 +138,12 @@ test("delivery failures are logged safely and do not block later sends", async (
|
||||
try {
|
||||
const turn = gateway.receive(message("work", "failure"), adapter);
|
||||
await waitFor(() => runtime.turns.length === 1);
|
||||
await clock.advanceTo(15_000);
|
||||
runtime.turns[0].resolve("done");
|
||||
assert.deepEqual(await turn, { ok: true, reply: "done" });
|
||||
} finally {
|
||||
console.error = originalError;
|
||||
}
|
||||
assert.deepEqual(sent.map((outgoing) => outgoing.text), ["收到,我还在处理,稍等我一下。", "done"]);
|
||||
assert.deepEqual(errors, ["Gateway delivery send failed (platform=qq)"]);
|
||||
});
|
||||
|
||||
test("a failed final send does not turn a successful runtime result into Agent error", async () => {
|
||||
const { runtime, gateway } = createGateway();
|
||||
const sent: OutgoingMessage[] = [];
|
||||
const errors: string[] = [];
|
||||
const adapter: PlatformAdapter = {
|
||||
name: "test", async handleWebhook() { return {}; },
|
||||
async sendMessage(outgoing) { sent.push(outgoing); throw new Error("provider detail"); }
|
||||
};
|
||||
const originalError = console.error;
|
||||
console.error = (...args: unknown[]) => { errors.push(args.map(String).join(" ")); };
|
||||
try {
|
||||
const turn = gateway.receive(message("work", "final-failure"), adapter);
|
||||
await waitFor(() => runtime.turns.length === 1);
|
||||
runtime.turns[0].resolve("real result");
|
||||
assert.deepEqual(await turn, { ok: true, reply: "real result" });
|
||||
} finally {
|
||||
console.error = originalError;
|
||||
}
|
||||
assert.deepEqual(sent.map((outgoing) => outgoing.text), ["real result"]);
|
||||
assert.deepEqual(sent.map((outgoing) => outgoing.text), ["done"]);
|
||||
assert.deepEqual(errors, ["Gateway delivery send failed (platform=qq)"]);
|
||||
});
|
||||
|
||||
@@ -342,45 +157,130 @@ test("runtime errors remain runtime errors and are delivered through the same st
|
||||
assert.deepEqual(sent.map((outgoing) => [outgoing.text, outgoing.replySequence]), [["Agent error: failed", 1]]);
|
||||
});
|
||||
|
||||
test("status, cancel, and help bypass the chat lock while new remains serial", async () => {
|
||||
const { runtime, clock, gateway } = createGateway();
|
||||
test("sendEvent pushes a fresh message to the latest chat target without replyTo", async () => {
|
||||
const { runtime, gateway } = createGateway();
|
||||
const sent: OutgoingMessage[] = [];
|
||||
const turn = gateway.receive(message("work", "event-source"), recordingAdapter(sent));
|
||||
await waitFor(() => runtime.turns.length === 1);
|
||||
runtime.turns[0].resolve("done");
|
||||
await turn;
|
||||
|
||||
await gateway.sendEvent("qq:chat", "任务完成了");
|
||||
const event = sent.at(-1)!;
|
||||
assert.equal(event.text, "任务完成了");
|
||||
assert.equal(event.replyTo, undefined);
|
||||
assert.equal(event.replySequence, undefined);
|
||||
assert.deepEqual(event.target, { platform: "qq", chatId: "chat", userId: "user", raw: undefined });
|
||||
});
|
||||
|
||||
test("sendEvent uses the most recent target and adapter for the chat", async () => {
|
||||
const { runtime, gateway } = createGateway();
|
||||
const firstSent: OutgoingMessage[] = [];
|
||||
const secondSent: OutgoingMessage[] = [];
|
||||
const first = gateway.receive(message("one", "one", "chat"), recordingAdapter(firstSent));
|
||||
await waitFor(() => runtime.turns.length === 1);
|
||||
const second = gateway.receive(message("two", "two", "chat"), recordingAdapter(secondSent));
|
||||
await waitFor(() => runtime.turns.length === 2);
|
||||
runtime.turns[0].resolve("one done");
|
||||
runtime.turns[1].resolve("two done");
|
||||
await Promise.all([first, second]);
|
||||
|
||||
await gateway.sendEvent("qq:chat", "event");
|
||||
assert.equal(firstSent.filter((outgoing) => outgoing.text === "event").length, 0);
|
||||
assert.equal(secondSent.at(-1)?.text, "event");
|
||||
assert.equal(secondSent.at(-1)?.replyTo, undefined);
|
||||
});
|
||||
|
||||
test("sendEvent drops safely when the chat has no known target", async () => {
|
||||
const { gateway } = createGateway();
|
||||
await gateway.sendEvent("qq:unknown", "event");
|
||||
});
|
||||
|
||||
test("sendEvent delivery failures are logged safely without message content", async () => {
|
||||
const { runtime, gateway } = createGateway();
|
||||
const errors: string[] = [];
|
||||
const adapter: PlatformAdapter = {
|
||||
name: "test", async handleWebhook() { return {}; },
|
||||
async sendMessage(outgoing) {
|
||||
if (!outgoing.replyTo) throw new Error("sensitive provider response");
|
||||
}
|
||||
};
|
||||
const turn = gateway.receive(message("work", "event-failure"), adapter);
|
||||
await waitFor(() => runtime.turns.length === 1);
|
||||
runtime.turns[0].resolve("done");
|
||||
await turn;
|
||||
|
||||
const originalError = console.error;
|
||||
console.error = (...args: unknown[]) => { errors.push(args.map(String).join(" ")); };
|
||||
try {
|
||||
await gateway.sendEvent("qq:chat", "event text must not leak");
|
||||
} finally {
|
||||
console.error = originalError;
|
||||
}
|
||||
assert.deepEqual(errors, ["Gateway event send failed (platform=qq)"]);
|
||||
});
|
||||
|
||||
test("status and help reach the runtime or reply directly", async () => {
|
||||
const { runtime, gateway } = createGateway();
|
||||
const adapter = recordingAdapter();
|
||||
const turn = gateway.receive(message("work"), adapter, { synchronous: true });
|
||||
await waitFor(() => runtime.turns.length === 1);
|
||||
clock.set(5_000);
|
||||
|
||||
const status = await gateway.receive(message("/status"), adapter, { synchronous: true });
|
||||
assert.match(status.reply || "", /gatewayRunning=true/);
|
||||
assert.match(status.reply || "", /queued=0/);
|
||||
assert.match(status.reply || "", /gatewayRunningSeconds=5/);
|
||||
assert.match((await gateway.receive(message("/help"), adapter, { synchronous: true })).reply || "", /\/status/);
|
||||
assert.equal((await gateway.receive(message("/cancel"), adapter, { synchronous: true })).reply, "Cancellation requested.");
|
||||
assert.match(status.reply || "", /running=true/);
|
||||
const help = await gateway.receive(message("/help"), adapter, { synchronous: true });
|
||||
assert.match(help.reply || "", /自然语言/);
|
||||
assert.match(help.reply || "", /\/confirm/);
|
||||
assert.match(help.reply || "", /\/stop/);
|
||||
assert.match(help.reply || "", /\/cancel/);
|
||||
runtime.turns[0].resolve("done");
|
||||
await turn;
|
||||
|
||||
const second = gateway.receive(message("work"), adapter, { synchronous: true });
|
||||
await waitFor(() => runtime.turns.length === 2);
|
||||
const reset = gateway.receive(message("/new"), adapter, { synchronous: true });
|
||||
await flush();
|
||||
assert.equal(runtime.resets, 0);
|
||||
runtime.turns[1].resolve("done");
|
||||
await second;
|
||||
await reset;
|
||||
assert.equal(runtime.resets, 1);
|
||||
});
|
||||
|
||||
test("synchronous work sends no notices or delivery messages", async () => {
|
||||
const { runtime, clock, gateway } = createGateway();
|
||||
test("list, confirm, stop, and cancel forward to the runtime with the chat identity", async () => {
|
||||
const { runtime, gateway } = createGateway();
|
||||
const adapter = recordingAdapter();
|
||||
runtime.proposals = [fakeProposal()];
|
||||
|
||||
const list = await gateway.receive(message("/list"), adapter, { synchronous: true });
|
||||
assert.equal(runtime.listed, 1);
|
||||
assert.match(list.reply || "", /proposal-1 \[proposed\] 修复登录页/);
|
||||
|
||||
runtime.proposals = [];
|
||||
const emptyList = await gateway.receive(message("/list"), adapter, { synchronous: true });
|
||||
assert.equal(emptyList.reply, "当前没有提案。");
|
||||
|
||||
const confirm = await gateway.receive(message("/confirm"), adapter, { synchronous: true });
|
||||
assert.equal(runtime.confirmed, 1);
|
||||
assert.equal(confirm.reply, "已确认,继续执行。");
|
||||
|
||||
const stop = await gateway.receive(message("/stop"), adapter, { synchronous: true });
|
||||
assert.equal(runtime.stopped, 1);
|
||||
assert.equal(stop.reply, "已停止当前任务。");
|
||||
|
||||
const cancel = await gateway.receive(message("/cancel"), adapter, { synchronous: true });
|
||||
assert.equal(runtime.cancelled, 1);
|
||||
assert.equal(cancel.reply, "已取消最近的提案。");
|
||||
|
||||
runtime.confirmResult = false;
|
||||
runtime.stopResult = false;
|
||||
runtime.cancelResult = false;
|
||||
assert.equal((await gateway.receive(message("/confirm"), adapter, { synchronous: true })).reply, "当前没有待确认的提案。");
|
||||
assert.equal((await gateway.receive(message("/stop"), adapter, { synchronous: true })).reply, "当前没有正在执行的任务。");
|
||||
assert.equal((await gateway.receive(message("/cancel"), adapter, { synchronous: true })).reply, "没有可取消的提案。");
|
||||
});
|
||||
|
||||
test("synchronous work sends no delivery messages", async () => {
|
||||
const { runtime, gateway } = createGateway();
|
||||
const sent: OutgoingMessage[] = [];
|
||||
const turn = gateway.receive(message("work"), recordingAdapter(sent), { synchronous: true });
|
||||
await waitFor(() => runtime.turns.length === 1);
|
||||
await clock.advanceTo(600_000);
|
||||
assert.deepEqual(sent, []);
|
||||
runtime.turns[0].resolve("done");
|
||||
assert.deepEqual(await turn, { ok: true, reply: "done" });
|
||||
assert.deepEqual(sent, []);
|
||||
});
|
||||
|
||||
test("asynchronous help replies with sequence one", async () => {
|
||||
test("asynchronous help replies with sequence one without reaching the runtime", async () => {
|
||||
const { runtime, gateway } = createGateway();
|
||||
const sent: OutgoingMessage[] = [];
|
||||
await gateway.receive(message("/help", "help"), recordingAdapter(sent));
|
||||
@@ -388,32 +288,13 @@ test("asynchronous help replies with sequence one", async () => {
|
||||
assert.equal(runtime.turns.length, 0);
|
||||
});
|
||||
|
||||
test("asynchronous new waits for prior work and then replies with sequence one", async () => {
|
||||
test("asynchronous commands reply with sequence one without reaching prompt", async () => {
|
||||
const { runtime, gateway } = createGateway();
|
||||
const sent: OutgoingMessage[] = [];
|
||||
const adapter = recordingAdapter(sent);
|
||||
const work = gateway.receive(message("work", "work"), adapter);
|
||||
await waitFor(() => runtime.turns.length === 1);
|
||||
|
||||
const reset = gateway.receive(message("/new", "new"), adapter);
|
||||
await flush();
|
||||
assert.equal(runtime.resets, 0);
|
||||
assert.deepEqual(messagesFor(sent, "new"), []);
|
||||
|
||||
runtime.turns[0].resolve("done");
|
||||
await work;
|
||||
assert.deepEqual(await reset, { ok: true, reply: "Started a new native ACP session for this chat." });
|
||||
assert.equal(runtime.resets, 1);
|
||||
assert.deepEqual(messagesFor(sent, "new").map((outgoing) => [outgoing.text, outgoing.replySequence]), [
|
||||
["Started a new native ACP session for this chat.", 1]
|
||||
]);
|
||||
});
|
||||
|
||||
test("asynchronous retired-role command replies with sequence one without reaching ACP", async () => {
|
||||
const { runtime, gateway } = createGateway();
|
||||
const sent: OutgoingMessage[] = [];
|
||||
const result = await gateway.receive(message("/roles", "retired"), recordingAdapter(sent));
|
||||
assert.match(result.reply || "", /removed in Config v3/);
|
||||
runtime.proposals = [fakeProposal()];
|
||||
const result = await gateway.receive(message("/list", "list"), recordingAdapter(sent));
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(runtime.turns.length, 0);
|
||||
assert.deepEqual(messagesFor(sent, "retired").map((outgoing) => outgoing.replySequence), [1]);
|
||||
assert.deepEqual(messagesFor(sent, "list").map((outgoing) => outgoing.replySequence), [1]);
|
||||
assert.match(messagesFor(sent, "list")[0].text, /proposal-1/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user