























@@ -814,6 +814,112 @@ describe("executeNodeHostCommand", () => {
814814);
815815});
816816817+it("requests human approval when node auto-review cannot bind a single parsed command", async () => {
818+const autoReviewer = vi.fn<ExecAutoReviewer>(async () => ({
819+decision: "allow-once",
820+risk: "low",
821+rationale: "test reviewer would allow it",
822+}));
823+evaluateShellAllowlistMock.mockReturnValue({
824+allowlistMatches: [],
825+analysisOk: true,
826+allowlistSatisfied: false,
827+segments: [
828+{ raw: "echo ok", resolution: null, argv: ["echo", "ok"] },
829+{ raw: "pwd", resolution: null, argv: ["pwd"] },
830+],
831+segmentAllowlistEntries: [],
832+});
833+resolveExecHostApprovalContextMock.mockReturnValue({
834+approvals: { allowlist: [], file: { version: 1, agents: {} } },
835+hostSecurity: "allowlist",
836+hostAsk: "on-miss",
837+askFallback: "deny",
838+});
839+840+const result = await executeNodeHostCommand({
841+command: "echo ok; pwd",
842+workdir: "/tmp/work",
843+env: {},
844+security: "allowlist",
845+ask: "on-miss",
846+autoReview: true,
847+ autoReviewer,
848+defaultTimeoutSec: 30,
849+approvalRunningNoticeMs: 0,
850+warnings: [],
851+agentId: "requested-agent",
852+sessionKey: "requested-session",
853+});
854+855+expect(result.details?.status).toBe("approval-pending");
856+expect(autoReviewer).not.toHaveBeenCalled();
857+expect(createAndRegisterDefaultExecApprovalRequestMock).toHaveBeenCalledTimes(1);
858+});
859+860+it("does not use fallback-full when node auto-review cannot parse the command", async () => {
861+const autoReviewer = vi.fn<ExecAutoReviewer>(async () => ({
862+decision: "allow-once",
863+risk: "low",
864+rationale: "test reviewer would allow it",
865+}));
866+evaluateShellAllowlistMock.mockReturnValue({
867+allowlistMatches: [],
868+analysisOk: false,
869+allowlistSatisfied: false,
870+segments: [],
871+segmentAllowlistEntries: [],
872+});
873+resolveExecHostApprovalContextMock.mockReturnValue({
874+approvals: { allowlist: [], file: { version: 1, agents: {} } },
875+hostSecurity: "allowlist",
876+hostAsk: "on-miss",
877+askFallback: "full",
878+});
879+resolveApprovalDecisionOrUndefinedMock.mockResolvedValue(null);
880+createExecApprovalDecisionStateMock.mockReturnValue({
881+baseDecision: { timedOut: true },
882+approvedByAsk: true,
883+deniedReason: null,
884+});
885+enforceStrictInlineEvalApprovalBoundaryMock.mockImplementation((value) =>
886+value.requiresAutoReviewHumanApproval === true && value.baseDecision.timedOut
887+ ? { approvedByAsk: false, deniedReason: "approval-timeout" }
888+ : { approvedByAsk: value.approvedByAsk, deniedReason: value.deniedReason },
889+);
890+891+const result = await executeNodeHostCommand({
892+command: "echo 'unterminated",
893+workdir: "/tmp/work",
894+env: {},
895+security: "allowlist",
896+ask: "on-miss",
897+autoReview: true,
898+ autoReviewer,
899+defaultTimeoutSec: 30,
900+approvalRunningNoticeMs: 0,
901+warnings: [],
902+agentId: "requested-agent",
903+sessionKey: "requested-session",
904+});
905+906+expect(result.details?.status).toBe("approval-pending");
907+expect(autoReviewer).not.toHaveBeenCalled();
908+await vi.waitFor(() => {
909+expect(sendExecApprovalFollowupResultMock).toHaveBeenCalledWith(
910+{ approvalId: "approval-1" },
911+"Exec denied (node=node-1 id=approval-1, approval-timeout): echo 'unterminated",
912+);
913+});
914+expect(
915+callGatewayToolMock.mock.calls.some(
916+([method, , params]) =>
917+method === "node.invoke" &&
918+(params as MockNodeInvokeParams | undefined)?.command === "system.run",
919+),
920+).toBe(false);
921+});
922+817923it("does not use fallback-full when node auto-review asks for human approval", async () => {
818924const autoReviewer = vi.fn<ExecAutoReviewer>(async () => ({
819925decision: "ask",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。