|
9 | 9 | mothershipClientToolWaiterMockFns, |
10 | 10 | } from '@sim/testing/mocks/mothership-client-tool-waiter.mock' |
11 | 11 | import { sleep } from '@sim/utils/helpers' |
12 | | -import { beforeEach, describe, expect, it, vi } from 'vitest' |
| 12 | +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' |
13 | 13 | import { AsyncToolCallOwnershipError } from '@/lib/mothership/async-runs/errors' |
14 | 14 | import { restoreStreamingContext } from '@/lib/mothership/request/context/restore' |
15 | 15 | import { TraceCollector } from '@/lib/mothership/request/trace' |
@@ -431,6 +431,79 @@ describe('sse-handlers tool lifecycle', () => { |
431 | 431 | expect(event.payload.status).toBe('awaiting_approval') |
432 | 432 | }) |
433 | 433 |
|
| 434 | + describe('sim_cli approval', () => { |
| 435 | + const activate = ['workflows', 'activate', 'create', 'wf-1', '1', '--yes'] |
| 436 | + |
| 437 | + function cliFrame(toolCallId: string, toolName: string, argv: string[]): StreamEvent { |
| 438 | + return { |
| 439 | + type: MothershipStreamV1EventType.tool, |
| 440 | + payload: { |
| 441 | + toolCallId, |
| 442 | + toolName, |
| 443 | + execName: 'sim_cli', |
| 444 | + arguments: { args: argv, request: { invocation: { kind: 'cli', argv } } }, |
| 445 | + executor: MothershipStreamV1ToolExecutor.sim, |
| 446 | + mode: MothershipStreamV1ToolMode.async, |
| 447 | + phase: MothershipStreamV1ToolPhase.call, |
| 448 | + }, |
| 449 | + } |
| 450 | + } |
| 451 | + |
| 452 | + beforeEach(async () => { |
| 453 | + const router = await vi.importActual<typeof import('@/lib/mothership/tool-executor/router')>( |
| 454 | + '@/lib/mothership/tool-executor/router' |
| 455 | + ) |
| 456 | + toolRequiresApproval.mockImplementation(router.toolRequiresApproval) |
| 457 | + context.runId = 'run-1' |
| 458 | + context.toolPermissions = { |
| 459 | + enabled: true, |
| 460 | + autoAllowed: new Set(), |
| 461 | + autoAllowPermitted: true, |
| 462 | + } |
| 463 | + }) |
| 464 | + |
| 465 | + afterEach(() => { |
| 466 | + toolRequiresApproval.mockReset() |
| 467 | + toolRequiresApproval.mockReturnValue(false) |
| 468 | + }) |
| 469 | + |
| 470 | + it('holds a command that performs a gated capability, though its argv says --yes', async () => { |
| 471 | + const event = cliFrame('cli-activate', 'cli_workflows_activate_create', activate) |
| 472 | + |
| 473 | + await prePersistClientExecutableToolCall(event, context, {}) |
| 474 | + |
| 475 | + expect(event.payload.status).toBe('awaiting_approval') |
| 476 | + expect(upsertAsyncToolCall).toHaveBeenCalledWith( |
| 477 | + expect.objectContaining({ |
| 478 | + toolName: 'cli_workflows_activate_create', |
| 479 | + executableName: 'sim_cli', |
| 480 | + permissionRequested: true, |
| 481 | + }) |
| 482 | + ) |
| 483 | + }) |
| 484 | + |
| 485 | + it('lets always-allow cover only the command it was granted for', async () => { |
| 486 | + context.toolPermissions.autoAllowed.add('cli_workflows_list') |
| 487 | + context.toolPermissions.autoAllowed.add('sim_cli') |
| 488 | + const held = cliFrame('cli-held', 'cli_workflows_activate_create', activate) |
| 489 | + await prePersistClientExecutableToolCall(held, context, {}) |
| 490 | + expect(held.payload.status).toBe('awaiting_approval') |
| 491 | + |
| 492 | + // A row named for an allowed command, or for none, does not carry its consent |
| 493 | + // to the command the CLI would actually run. |
| 494 | + for (const name of ['cli_workflows_list', 'sim_cli']) { |
| 495 | + const mislabeled = cliFrame(`cli-${name}`, name, activate) |
| 496 | + await prePersistClientExecutableToolCall(mislabeled, context, {}) |
| 497 | + expect(mislabeled.payload.status).toBe('awaiting_approval') |
| 498 | + } |
| 499 | + |
| 500 | + context.toolPermissions.autoAllowed.add('cli_workflows_activate_create') |
| 501 | + const allowed = cliFrame('cli-allowed', 'cli_workflows_activate_create', activate) |
| 502 | + await prePersistClientExecutableToolCall(allowed, context, {}) |
| 503 | + expect(allowed.payload.status).toBeUndefined() |
| 504 | + }) |
| 505 | + }) |
| 506 | + |
434 | 507 | it('clears a Go-stamped approval frame when the gate is off', async () => { |
435 | 508 | // Go stamps integration calls regardless of Sim's feature flag. Forwarding |
436 | 509 | // that stamp with nothing gating behind it would draw a card whose buttons |
|
0 commit comments