feat: share finish stop and cancel across the board
Proposal visibility is already global; this change makes finish, stop, and cancel shared queue-management actions so any user can unblock the single worker and shared queue. Confirm, adjust, follow_up, and start_next remain owner-only. Assistant/bootstrap copy, /help, /list, and runtime checks now align with that split.
This commit is contained in:
@@ -392,18 +392,19 @@ test("proposals are owned per chat+user: a second user in the same chat cannot c
|
||||
assert.equal(await harness.manager.confirm("webhook", "chat-1", "user-b"), false);
|
||||
await harness.manager.prompt(request("confirm", "user-b"));
|
||||
assert.equal(harness.proposals.get(proposal.id)!.status, "proposed");
|
||||
assert.equal(await harness.manager.cancel("webhook", "chat-1", "user-b"), false);
|
||||
await harness.manager.prompt(request("cancel proposal", "user-b"));
|
||||
assert.equal(harness.proposals.get(proposal.id)!.status, "proposed");
|
||||
assert.equal(await harness.manager.finish("webhook", "chat-1", "user-b"), false);
|
||||
assert.equal(await harness.manager.stop("webhook", "chat-1", "user-b"), false);
|
||||
assert.equal(await harness.manager.cancel("webhook", "chat-1", "user-b"), true);
|
||||
assert.equal(harness.proposals.get(proposal.id)!.status, "finished");
|
||||
assert.equal(harness.proposals.get(proposal.id)!.finishKind, "cancelled");
|
||||
|
||||
await harness.manager.prompt(request("create proposal: hang", "user-a"));
|
||||
const running = harness.proposals.list().find((item) => item.status === "proposed")!;
|
||||
|
||||
// The board is globally visible: both users see the same proposal, tagged by scope.
|
||||
assert.equal(harness.manager.listProposals("webhook", "chat-1", "user-b").length, 1);
|
||||
assert.equal(harness.manager.listProposals("webhook", "chat-1", "user-a").length, 1);
|
||||
assert.equal(harness.manager.listProposals("webhook", "chat-1", "user-b").some((item) => item.id === running.id), true);
|
||||
assert.equal(harness.manager.listProposals("webhook", "chat-1", "user-a").some((item) => item.id === running.id), true);
|
||||
|
||||
assert.equal(await harness.manager.confirm("webhook", "chat-1", "user-a"), true);
|
||||
assert.equal(harness.proposals.get(proposal.id)!.status, "working");
|
||||
assert.equal(harness.proposals.get(running.id)!.status, "working");
|
||||
|
||||
await harness.manager.prompt(request("hello owner", "user-a"));
|
||||
const aPrompt = readLog(harness.logFile).find((entry) => entry.method === "session/prompt" && entry.text?.startsWith("[User message]\nhello owner"));
|
||||
@@ -420,11 +421,11 @@ test("proposals are owned per chat+user: a second user in the same chat cannot c
|
||||
assert.match(bPrompt.text!, /scope=other chat=direct/);
|
||||
assert.doesNotMatch(bPrompt.text!, /user-a/);
|
||||
|
||||
assert.equal(await harness.manager.stop("webhook", "chat-1", "user-b"), false);
|
||||
assert.equal(harness.proposals.get(proposal.id)!.status, "working");
|
||||
assert.equal(await harness.manager.stop("webhook", "chat-1", "user-a"), true);
|
||||
assert.equal(harness.proposals.get(proposal.id)!.status, "pending");
|
||||
assert.equal(harness.proposals.get(proposal.id)!.pending?.summary, "被用户中止");
|
||||
assert.equal(await harness.manager.stop("webhook", "chat-1", "user-b"), true);
|
||||
assert.equal(harness.proposals.get(running.id)!.status, "pending");
|
||||
assert.equal(harness.proposals.get(running.id)!.pending?.summary, "被用户中止");
|
||||
assert.equal(await harness.manager.finish("webhook", "chat-1", "user-b"), true);
|
||||
assert.equal(harness.proposals.get(running.id)!.status, "finished");
|
||||
} finally {
|
||||
await closeHarness(harness);
|
||||
}
|
||||
@@ -662,32 +663,44 @@ test("a working proposal blocks start_next for everyone with a sanitized reason"
|
||||
}
|
||||
});
|
||||
|
||||
test("confirm, finish, stop, cancel, and follow_up actions with no matching proposal append an ownership correction", async () => {
|
||||
test("confirm and follow_up stay owner-only while finish, stop, and cancel are shared", async () => {
|
||||
const harness = await createHarness();
|
||||
try {
|
||||
await harness.manager.prompt(request("create proposal: hang", "user-a"));
|
||||
const proposed = harness.proposals.list()[0]!;
|
||||
|
||||
const confirmed = await harness.manager.prompt(request("confirm", "user-b"));
|
||||
assert.match(confirmed.text, /没有可确认的提案/);
|
||||
assert.match(confirmed.text, /自己发起/);
|
||||
|
||||
const cancelled = await harness.manager.prompt(request("cancel proposal", "user-b"));
|
||||
assert.equal(cancelled.text, "cancelling");
|
||||
assert.equal(harness.proposals.get(proposed.id)!.status, "finished");
|
||||
assert.equal(harness.proposals.get(proposed.id)!.finishKind, "cancelled");
|
||||
|
||||
await harness.manager.prompt(request("create proposal: ask", "user-a"));
|
||||
const pending = harness.proposals.list().find((proposal) => proposal.status === "proposed")!;
|
||||
await harness.manager.prompt(request("confirm", "user-a"));
|
||||
await waitFor(() => harness.proposals.get(pending.id)!.status === "pending");
|
||||
|
||||
const finished = await harness.manager.prompt(request("finish", "user-b"));
|
||||
assert.match(finished.text, /没有可结束的任务/);
|
||||
assert.match(finished.text, /自己发起/);
|
||||
assert.equal(finished.text, "finishing");
|
||||
assert.equal(harness.proposals.get(pending.id)!.status, "finished");
|
||||
assert.equal(harness.proposals.get(pending.id)!.finishKind, "done");
|
||||
|
||||
await harness.manager.prompt(request("create proposal: hang", "user-a"));
|
||||
const running = harness.proposals.list().find((proposal) => proposal.status === "proposed")!;
|
||||
await harness.manager.prompt(request("confirm", "user-a"));
|
||||
assert.equal(harness.proposals.get(running.id)!.status, "working");
|
||||
|
||||
const stopped = await harness.manager.prompt(request("stop", "user-b"));
|
||||
assert.match(stopped.text, /没有可停止的任务/);
|
||||
assert.match(stopped.text, /自己发起/);
|
||||
|
||||
const cancelled = await harness.manager.prompt(request("cancel proposal", "user-b"));
|
||||
assert.match(cancelled.text, /没有可取消的提案/);
|
||||
assert.match(cancelled.text, /自己发起/);
|
||||
assert.equal(stopped.text, "stopping");
|
||||
assert.equal(harness.proposals.get(running.id)!.status, "pending");
|
||||
assert.equal(harness.proposals.get(running.id)!.pending?.summary, "被用户中止");
|
||||
|
||||
const followedUp = await harness.manager.prompt(request("follow up: more", "user-b"));
|
||||
assert.match(followedUp.text, /现在没法继续/);
|
||||
assert.match(followedUp.text, /自己发起/);
|
||||
|
||||
assert.equal(harness.proposals.list()[0]!.status, "proposed");
|
||||
} finally {
|
||||
await closeHarness(harness);
|
||||
}
|
||||
|
||||
+23
-2
@@ -50,6 +50,7 @@ class FakeRuntime implements ConversationRuntime {
|
||||
confirmResult = true;
|
||||
finishResult = true;
|
||||
stopResult = true;
|
||||
resetResult = false;
|
||||
proposals: Proposal[] = [];
|
||||
commandUsers: Record<string, string | undefined> = {};
|
||||
|
||||
@@ -66,8 +67,8 @@ class FakeRuntime implements ConversationRuntime {
|
||||
async confirm(_platform: string, _chatId: string, userId: string) { this.confirmed++; this.commandUsers.confirm = userId; return this.confirmResult; }
|
||||
async finish(_platform: string, _chatId: string, userId: string) { this.finished++; this.commandUsers.finish = userId; return this.finishResult; }
|
||||
async stop(_platform: string, _chatId: string, userId: string) { this.stopped++; this.commandUsers.stop = userId; return this.stopResult; }
|
||||
async reset(_platform: string, _chatId: string, userId: string) { this.commandUsers.reset = userId; return this.resetResult; }
|
||||
listProposals(_platform: string, _chatId: string, userId: string) { this.listed++; this.commandUsers.list = userId; return this.proposals; }
|
||||
async reset() {}
|
||||
status(_platform?: string, _chatId?: string, userId?: string) {
|
||||
this.commandUsers.status = userId;
|
||||
return {
|
||||
@@ -590,6 +591,7 @@ test("status and help reach the runtime or reply directly", async () => {
|
||||
assert.match(help.reply || "", /\/finish/);
|
||||
assert.match(help.reply || "", /\/stop/);
|
||||
assert.match(help.reply || "", /\/cancel/);
|
||||
assert.match(help.reply || "", /\/reset/);
|
||||
runtime.turns[0].resolve("done");
|
||||
await turn;
|
||||
});
|
||||
@@ -654,7 +656,7 @@ test("the list panel orders pending first, then working, queued, proposed, and r
|
||||
|
||||
const list = await gateway.receive(message("/list"), adapter, { synchronous: true });
|
||||
const reply = list.reply || "";
|
||||
const order = ["待确认(等你处理)", "进行中", "排队中", "未确认(proposed)", "最近结束"]
|
||||
const order = ["待确认", "进行中", "排队中", "未确认(proposed)", "最近结束"]
|
||||
.map((section) => reply.indexOf(section));
|
||||
assert.ok(order.every((index) => index >= 0), reply);
|
||||
assert.deepEqual([...order].sort((a, b) => a - b), order);
|
||||
@@ -665,6 +667,25 @@ test("the list panel orders pending first, then working, queued, proposed, and r
|
||||
assert.match(reply, /p-finished「旧任务」(你的)已完成/);
|
||||
});
|
||||
|
||||
test("/reset resets the current conversation session and reminds about unfinished proposals", async () => {
|
||||
const { runtime, gateway } = createGateway();
|
||||
const adapter = recordingAdapter();
|
||||
|
||||
const noSession = await gateway.receive(message("/reset"), adapter, { synchronous: true });
|
||||
assert.equal(noSession.reply, "当前没有需要重置的会话。");
|
||||
|
||||
runtime.resetResult = true;
|
||||
const reset = await gateway.receive(message("/reset"), adapter, { synchronous: true });
|
||||
assert.match(reset.reply || "", /已重置当前对话/);
|
||||
assert.doesNotMatch(reset.reply || "", /未完成任务/);
|
||||
assert.equal(runtime.commandUsers.reset, "user");
|
||||
|
||||
runtime.proposals = [fakeProposal({ id: "p-1", status: "pending", pending: { summary: "s", receivedAt: 1 } })];
|
||||
const reminded = await gateway.receive(message("/reset"), adapter, { synchronous: true });
|
||||
assert.match(reminded.reply || "", /已重置当前对话/);
|
||||
assert.match(reminded.reply || "", /你还有 1 个未完成任务,proposal 板不受影响/);
|
||||
});
|
||||
|
||||
test("the list panel shows other members' proposals without exposing their identity", async () => {
|
||||
const { runtime, gateway } = createGateway();
|
||||
const adapter = recordingAdapter();
|
||||
|
||||
Reference in New Issue
Block a user