mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-11 11:41:38 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a40cde3cd |
No files matched your search
@@ -967,7 +967,7 @@ function verbToClosure(input: MissionControlActionInput): ClosureMapping {
|
||||
* person's own receipt says why). Undefined for a request the answer closed (no agent wired here). */
|
||||
function runnerAnswerNotice(ask: QueueItem): string | undefined {
|
||||
if (ask.state === "handed-off" && ask.handedOffTo) {
|
||||
return `Your answer on ${ask.qitemId} went to ${ask.handedOffTo}, the receiving agent, which decides. The runner waits until then; nothing has run.`;
|
||||
return `Your answer on ${ask.qitemId} went to ${ask.handedOffTo}, the receiving agent, which decides. The runner waits until then; nothing has run. A note to you says the agent's recorded state.`;
|
||||
}
|
||||
if (ask.state === "pending" || ask.state === "in-progress" || ask.state === "blocked") {
|
||||
return `Your answer is kept on ${ask.qitemId}, and the request is still waiting: the tool hasn't run, and nothing was permitted or refused. A note to you says why it wasn't passed to a receiving agent. To decide directly: rig seat decide-permission ${ask.qitemId} --option <id>.`;
|
||||
|
||||
@@ -4272,7 +4272,9 @@ export class QueueRepository {
|
||||
// The person learns who decides and in what recorded state, so a delay is shown: one update per request.
|
||||
const toldId = `qitem-runner-permission-passed-${item.qitemId.replace(/^qitem-runner-permission-/, "").replace(/^qitem-/, "")}`;
|
||||
setImmediate(() => {
|
||||
// The request the person answered is what they'd judge (convention C3: a note to a person names its evidence).
|
||||
void this.create({ qitemId: toldId, sourceSession: root.sourceSession, destinationSession: person, humanIntent: "update",
|
||||
evidenceRef: `rig queue show ${root.qitemId}`,
|
||||
summary: `Your answer went to ${decider} to decide; ${root.sourceSession} is waiting`,
|
||||
body: [`Your answer to ${root.sourceSession}'s permission request ${root.qitemId} went to ${decider}, which decides it while ${root.sourceSession} waits. Nothing runs until it decides.`,
|
||||
...(chosen?.describe ? [`Receiving agent: ${chosen.describe}`] : []),
|
||||
@@ -4323,6 +4325,7 @@ export class QueueRepository {
|
||||
// After the answer's own write settles: one receipt per request (its id is the request's), told to who answered.
|
||||
setImmediate(() => {
|
||||
void this.create({ qitemId: receiptId, sourceSession: requester, destinationSession: person, humanIntent: "update",
|
||||
evidenceRef: `rig queue show ${item.qitemId}`,
|
||||
summary: `Your answer to ${requester}'s permission request is kept; the tool hasn't run`,
|
||||
body: [`Your answer to ${requester}'s permission request ${item.qitemId} is kept, and the request is still waiting: ${unreachable ? `it couldn't be passed to ${unreachable}, the receiving agent, just now (answer again to retry)` : `no receiving agent on this host can read it before the tool runs (none is set, or the only one is ${requester} itself)`}. The tool hasn't run, and nothing was permitted or refused.`,
|
||||
`To decide now: rig seat decide-permission ${item.qitemId} --option <id>${options.length ? ` (options: ${options.map((o) => `${o.id}, ${o.label}`).join("; ")})` : ""}. Or set up a receiving agent that isn't ${requester}: rig config set agents.operator_session <seat>@<rig>.`,
|
||||
|
||||
@@ -331,8 +331,15 @@ export class RunnerPermissions {
|
||||
const changed = new Promise<void>((resolve) => { release = resolve; });
|
||||
let liveId = askId; // after a route or handoff, the colleague's row is where the answer lands
|
||||
const unsubscribe = this.deps.eventBus.subscribe((event) => {
|
||||
const changedId = (event as { type?: string; qitemId?: string }).type === "queue.updated" ? (event as { qitemId?: string }).qitemId : undefined;
|
||||
if (changedId === askId || changedId === liveId) release();
|
||||
const { type, qitemId } = event as { type?: string; qitemId?: string };
|
||||
// A handoff while this read waits (the person's answer going to the receiving agent) moves the request to a new
|
||||
// row: follow it, so the decision recorded there wakes this read rather than its next one.
|
||||
if (type === "queue.handed_off" && (qitemId === askId || qitemId === liveId)) {
|
||||
const asked = this.deps.queueRepo.getById(askId);
|
||||
liveId = (asked ? this.liveEnd(asked)?.qitemId : undefined) ?? liveId;
|
||||
return;
|
||||
}
|
||||
if (type === "queue.updated" && (qitemId === askId || qitemId === liveId)) release();
|
||||
});
|
||||
try {
|
||||
const now = this.withWords(askId, this.read(seat, askId));
|
||||
|
||||
@@ -719,7 +719,7 @@ describe("an answered request goes to the receiving agent: the pair's round-0 co
|
||||
const askId = String((await f.submit()).body.askId);
|
||||
// The result says what became of the answer (the TUI prints it), never that the request closed.
|
||||
expect(await f.act(askId, "approve")).toMatchObject({ status: 200,
|
||||
body: { answer: { message: `Your answer on ${askId} went to ${DECIDER}, the receiving agent, which decides. The runner waits until then; nothing has run.` } } });
|
||||
body: { answer: { message: `Your answer on ${askId} went to ${DECIDER}, the receiving agent, which decides. The runner waits until then; nothing has run. A note to you says the agent's recorded state.` } } });
|
||||
expect(f.queueRepo.getById(askId)).toMatchObject({ state: "handed-off", handedOffTo: DECIDER });
|
||||
expect(await f.read(askId)).toMatchObject({ body: { state: "pending" } });
|
||||
expect(handedFrom(f, askId)!.body).toContain(`they wrote "yes"`);
|
||||
@@ -1023,3 +1023,58 @@ describe("a direct answer with no notify envelope (the Slack fallback)", () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
// A person answering from the TUI answers as the host's human seat, and a person-addressed row to a human seat must
|
||||
// carry an evidence ref (convention C3). Without one, both notes to the person were refused and never written.
|
||||
describe("the person's note about a runner answer is written under the queue's own conventions", () => {
|
||||
const HUMAN_SEAT = "human@host";
|
||||
const answerAsHost = (f: ReturnType<typeof fixture>, askId: string, choice: { optionId?: string; decision?: string }) =>
|
||||
f.contract.act({ verb: "answer", qitemId: askId, actorSession: HUMAN_SEAT, authority: "primary-user",
|
||||
...(choice.optionId ? { questionId: "permission", optionId: choice.optionId } : {}), ...(choice.decision ? { decision: choice.decision } : {}) } as never);
|
||||
const notesTo = (f: ReturnType<typeof fixture>) => f.queueRepo.list({ limit: 100 })
|
||||
.filter((row) => row.destinationSession === HUMAN_SEAT && row.humanIntent === "update");
|
||||
|
||||
it("with no agent that can decide, the kept-answer note is written once, linked to the request, with the direct route", async () => {
|
||||
const f = fixture({ decider: null });
|
||||
const askId = String((await f.submit()).body.askId);
|
||||
await answerAsHost(f, askId, { optionId: "allow" });
|
||||
await vi.waitFor(() => expect(notesTo(f)).toHaveLength(1));
|
||||
const [note] = notesTo(f);
|
||||
expect(note).toMatchObject({ qitemId: `qitem-runner-permission-kept-${askId.replace(/^qitem-runner-permission-/, "")}`,
|
||||
evidenceRef: `rig queue show ${askId}`, sourceSession: SEAT });
|
||||
expect(note!.body).toContain("The tool hasn't run, and nothing was permitted or refused.");
|
||||
expect(note!.body).toContain(`rig seat decide-permission ${askId} --option <id>`);
|
||||
// The answer itself is kept on the request, and the request still waits.
|
||||
expect(f.queueRepo.getById(askId)).toMatchObject({ state: "pending", humanAnswers: { permission: "allow" } });
|
||||
// Answering again keeps the new answer and writes no second note.
|
||||
await answerAsHost(f, askId, { decision: "only the test file" });
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
expect(notesTo(f)).toHaveLength(1);
|
||||
expect(f.permits()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("when the answer goes to an agent, the who-decides note is written once, linked to the request, with the agent's state", async () => {
|
||||
const f = fixture({ receiver: "dev-other@perm-rig" });
|
||||
const askId = String((await f.submit()).body.askId);
|
||||
await answerAsHost(f, askId, { decision: "only the test file" });
|
||||
await vi.waitFor(() => expect(notesTo(f)).toHaveLength(1));
|
||||
const [note] = notesTo(f);
|
||||
expect(note).toMatchObject({ evidenceRef: `rig queue show ${askId}`, sourceSession: SEAT });
|
||||
expect(note!.body).toContain(`went to dev-other@perm-rig, which decides it while ${SEAT} waits. Nothing runs until it decides.`);
|
||||
expect(note!.body).toContain("Receiving agent: dev-other@perm-rig, chosen in agents.operator_session; its session is recorded as running.");
|
||||
expect(note!.body).toContain(`rig seat decide-permission ${askId} --option <id>`);
|
||||
expect(f.queueRepo.getById(askId)).toMatchObject({ state: "handed-off", handedOffTo: "dev-other@perm-rig" });
|
||||
});
|
||||
|
||||
it("a runner already waiting when the answer is handed to the agent reads the agent's decision at once, not at its read's end", async () => {
|
||||
const f = fixture({ decider: DECIDER });
|
||||
const askId = String((await f.submit()).body.askId);
|
||||
const started = Date.now();
|
||||
const waiting = f.read(askId, 5);
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
expect(await f.answer(askId, { optionId: "allow" })).toMatchObject({ status: 200 });
|
||||
expect(await f.decide(askId, "allow")).toMatchObject({ status: 200, body: { state: "answered" } });
|
||||
expect(await waiting).toMatchObject({ body: { state: "answered", optionId: "allow" } });
|
||||
expect(Date.now() - started).toBeLessThan(2500);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user