























@@ -571,6 +571,84 @@ describe("processGatewayAllowlist", () => {
571571expect(result.pendingResult?.details.status).toBe("approval-pending");
572572});
573573574+it("requests human approval when auto-review cannot bind a single parsed command", async () => {
575+requiresExecApprovalMock.mockReturnValue(true);
576+evaluateShellAllowlistMock.mockReturnValue({
577+allowlistMatches: [],
578+analysisOk: true,
579+allowlistSatisfied: false,
580+segments: [
581+{ raw: "echo ok", resolution: null, argv: ["echo", "ok"] },
582+{ raw: "pwd", resolution: null, argv: ["pwd"] },
583+],
584+segmentAllowlistEntries: [],
585+});
586+resolveExecHostApprovalContextMock.mockReturnValue({
587+approvals: { allowlist: [], file: { version: 1, agents: {} } },
588+hostSecurity: "allowlist",
589+hostAsk: "on-miss",
590+askFallback: "deny",
591+});
592+593+const result = await runGatewayAllowlist({
594+command: "echo ok; pwd",
595+ask: "on-miss",
596+autoReview: true,
597+});
598+599+expect(defaultExecAutoReviewerMock).not.toHaveBeenCalled();
600+expect(createAndRegisterDefaultExecApprovalRequestMock).toHaveBeenCalledTimes(1);
601+expect(result.pendingResult?.details.status).toBe("approval-pending");
602+});
603+604+it("does not use fallback-full when auto-review cannot parse the command", async () => {
605+requiresExecApprovalMock.mockReturnValue(true);
606+evaluateShellAllowlistMock.mockReturnValue({
607+allowlistMatches: [],
608+analysisOk: false,
609+allowlistSatisfied: false,
610+segments: [],
611+segmentAllowlistEntries: [],
612+});
613+resolveExecHostApprovalContextMock.mockReturnValue({
614+approvals: { allowlist: [], file: { version: 1, agents: {} } },
615+hostSecurity: "allowlist",
616+hostAsk: "on-miss",
617+askFallback: "full",
618+});
619+createExecApprovalDecisionStateMock.mockReturnValue({
620+baseDecision: { timedOut: true },
621+approvedByAsk: true,
622+deniedReason: null,
623+});
624+resolveApprovalDecisionOrUndefinedMock.mockResolvedValue(null);
625+enforceStrictInlineEvalApprovalBoundaryMock.mockImplementation((value) =>
626+value.requiresAutoReviewHumanApproval === true && value.baseDecision.timedOut
627+ ? { approvedByAsk: false, deniedReason: "approval-timeout" }
628+ : { approvedByAsk: value.approvedByAsk, deniedReason: value.deniedReason },
629+);
630+631+const result = await runGatewayAllowlist({
632+command: "echo 'unterminated",
633+ask: "on-miss",
634+autoReview: true,
635+turnSourceChannel: "webchat",
636+});
637+638+expect(defaultExecAutoReviewerMock).not.toHaveBeenCalled();
639+expect(enforceStrictInlineEvalApprovalBoundaryMock).toHaveBeenCalledWith(
640+expect.objectContaining({
641+requiresAutoReviewHumanApproval: true,
642+}),
643+);
644+expect(result.deniedResult?.details.status).toBe("failed");
645+expect(result.deniedResult?.content[0]).toEqual(
646+expect.objectContaining({
647+text: "Exec denied (gateway id=req-1, approval-timeout): echo 'unterminated",
648+}),
649+);
650+});
651+574652it("does not use fallback-full when auto-review asks for human approval", async () => {
575653requiresExecApprovalMock.mockReturnValue(true);
576654evaluateShellAllowlistMock.mockReturnValue({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。