1import { assert } from "@effect/vitest";
2import type { ProviderReplayTranscript } from "@t3tools/contracts";
3
4import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
5import {
6 assertBaseProjection,
7 assertSemanticProjectionIntegrity,
8 assertVisibleTurnItemsMirrorLocalTurnItems,
9 projectionFor,
10} from "../shared.ts";
11import { MUSE_PERMISSION_APPROVED_FILE, MUSE_PERMISSION_DECLINED_FILE } from "./input.ts";
12
13/**
14 * Supervised: both commands ask first. The approved one runs. Decline answers
15 * with Muse's Reject (`abort`) choice, so the command never runs and the turn
16 * still settles instead of waiting on an answer Muse cannot accept.
17 */
18export function assertMusePermissionOutput(
19 result: OrchestratorV2ScenarioResult,
20 transcript: ProviderReplayTranscript,
21) {
22 const projection = projectionFor(result, transcript.scenario);
23 assertSemanticProjectionIntegrity(projection);
24 assertVisibleTurnItemsMirrorLocalTurnItems(projection);
25 assert.lengthOf(projection.runs, 2);
26 assert.equal(projection.runs[0]?.status, "completed");
27 assert.include(["completed", "interrupted"], projection.runs[1]?.status);
28 assertBaseProjection({
29 result,
30 transcript,
31 runCount: 2,
32 runStatuses: projection.runs.map((run) => run.status),
33 });
34
35 const requests = projection.runtimeRequests.toSorted((left, right) =>
36 left.createdAt < right.createdAt ? -1 : 1,
37 );
38 assert.deepEqual(
39 requests.map((request) => [request.kind, request.status, request.decision]),
40 [
41 ["command", "resolved", "accept"],
42 ["command", "resolved", "decline"],
43 ],
44 );
45 const approval = projection.turnItems.find((item) => item.type === "approval_request");
46 const options = approval?.type === "approval_request" ? (approval.options ?? []) : [];
47 // Reject is offered as the decline button, not hidden behind "cancel".
48 assert.includeDeepMembers(
49 options.map((option) => option.decision),
50 ["accept", "decline"],
51 );
52 assert.notInclude(
53 options.map((option) => option.decision),
54 "cancel",
55 );
56
57 const decisions = transcript.entries.flatMap((entry) =>
58 entry.type === "expect_outbound" &&
59 typeof entry.frame === "object" &&
60 entry.frame !== null &&
61 Reflect.get(entry.frame, "method") === "approval/decide"
62 ? [Reflect.get(Reflect.get(entry.frame, "params") as object, "choiceId")]
63 : [],
64 );
65 assert.deepEqual(decisions, ["allow_once", "abort"]);
66
67 const shells = projection.turnItems.filter((item) => item.type === "command_execution");
68 assert.isTrue(
69 shells.some(
70 (item) =>
71 item.type === "command_execution" &&
72 item.input.includes(MUSE_PERMISSION_APPROVED_FILE) &&
73 item.status === "completed",
74 ),
75 );
76 assert.isFalse(
77 shells.some(
78 (item) =>
79 item.type === "command_execution" &&
80 item.input.includes(MUSE_PERMISSION_DECLINED_FILE) &&
81 item.status === "completed",
82 ),
83 );
84}