| ... | @@ -3360,6 +3360,118 @@ describe("ClaudeAdapterLive", () => { | ... | @@ -3360,6 +3360,118 @@ describe("ClaudeAdapterLive", () => { |
| 3360 | ); | 3360 | ); |
| 3361 | }); | 3361 | }); |
| 3362 | | 3362 | |
| | 3363 | it.effect("acceptForSession returns session-scoped permission updates", () => { |
| | 3364 | const harness = makeHarness(); |
| | 3365 | return Effect.gen(function* () { |
| | 3366 | const adapter = yield* ClaudeAdapter; |
| | 3367 | |
| | 3368 | const session = yield* adapter.startSession({ |
| | 3369 | threadId: THREAD_ID, |
| | 3370 | provider: ProviderDriverKind.make("claudeAgent"), |
| | 3371 | runtimeMode: "approval-required", |
| | 3372 | }); |
| | 3373 | |
| | 3374 | yield* Stream.take(adapter.streamEvents, 3).pipe(Stream.runDrain); |
| | 3375 | |
| | 3376 | yield* adapter.sendTurn({ |
| | 3377 | threadId: session.threadId, |
| | 3378 | input: "approve this for the session", |
| | 3379 | attachments: [], |
| | 3380 | }); |
| | 3381 | yield* Stream.take(adapter.streamEvents, 1).pipe(Stream.runDrain); |
| | 3382 | |
| | 3383 | const createInput = harness.getLastCreateQueryInput(); |
| | 3384 | const canUseTool = createInput?.options.canUseTool; |
| | 3385 | assert.equal(typeof canUseTool, "function"); |
| | 3386 | if (!canUseTool) { |
| | 3387 | return; |
| | 3388 | } |
| | 3389 | |
| | 3390 | const respondToNextRequest = Effect.gen(function* () { |
| | 3391 | const requested = yield* Stream.runHead(adapter.streamEvents); |
| | 3392 | assert.equal(requested._tag, "Some"); |
| | 3393 | if (requested._tag !== "Some" || requested.value.type !== "request.opened") { |
| | 3394 | return; |
| | 3395 | } |
| | 3396 | const runtimeRequestId = requested.value.requestId; |
| | 3397 | assert.equal(typeof runtimeRequestId, "string"); |
| | 3398 | if (runtimeRequestId === undefined) { |
| | 3399 | return; |
| | 3400 | } |
| | 3401 | yield* adapter.respondToRequest( |
| | 3402 | session.threadId, |
| | 3403 | ApprovalRequestId.make(runtimeRequestId), |
| | 3404 | "acceptForSession", |
| | 3405 | ); |
| | 3406 | yield* Stream.take(adapter.streamEvents, 1).pipe(Stream.runDrain); |
| | 3407 | }); |
| | 3408 | |
| | 3409 | // MCP tools frequently arrive with no usable suggestion (Claude Code |
| | 3410 | // sends an empty array); the decision must still stick for the session. |
| | 3411 | const mcpPermissionPromise = canUseTool( |
| | 3412 | "mcp__linear__create_issue", |
| | 3413 | { title: "hello" }, |
| | 3414 | { |
| | 3415 | signal: new AbortController().signal, |
| | 3416 | suggestions: [], |
| | 3417 | toolUseID: "tool-use-mcp-1", |
| | 3418 | }, |
| | 3419 | ); |
| | 3420 | yield* respondToNextRequest; |
| | 3421 | const mcpPermission = (yield* Effect.promise(() => mcpPermissionPromise)) as PermissionResult; |
| | 3422 | assert.equal(mcpPermission.behavior, "allow"); |
| | 3423 | if (mcpPermission.behavior !== "allow") { |
| | 3424 | return; |
| | 3425 | } |
| | 3426 | assert.deepEqual(mcpPermission.updatedPermissions, [ |
| | 3427 | { |
| | 3428 | type: "addRules", |
| | 3429 | rules: [{ toolName: "mcp__linear__create_issue" }], |
| | 3430 | behavior: "allow", |
| | 3431 | destination: "session", |
| | 3432 | }, |
| | 3433 | ]); |
| | 3434 | |
| | 3435 | // Received suggestions are reused but rescoped to the session — |
| | 3436 | // echoing "localSettings" would persist a session-only choice to disk. |
| | 3437 | const bashPermissionPromise = canUseTool( |
| | 3438 | "Bash", |
| | 3439 | { command: "git status" }, |
| | 3440 | { |
| | 3441 | signal: new AbortController().signal, |
| | 3442 | suggestions: [ |
| | 3443 | { |
| | 3444 | type: "addRules", |
| | 3445 | rules: [{ toolName: "Bash", ruleContent: "git status" }], |
| | 3446 | behavior: "allow", |
| | 3447 | destination: "localSettings", |
| | 3448 | }, |
| | 3449 | ], |
| | 3450 | toolUseID: "tool-use-bash-1", |
| | 3451 | }, |
| | 3452 | ); |
| | 3453 | yield* respondToNextRequest; |
| | 3454 | const bashPermission = (yield* Effect.promise( |
| | 3455 | () => bashPermissionPromise, |
| | 3456 | )) as PermissionResult; |
| | 3457 | assert.equal(bashPermission.behavior, "allow"); |
| | 3458 | if (bashPermission.behavior !== "allow") { |
| | 3459 | return; |
| | 3460 | } |
| | 3461 | assert.deepEqual(bashPermission.updatedPermissions, [ |
| | 3462 | { |
| | 3463 | type: "addRules", |
| | 3464 | rules: [{ toolName: "Bash", ruleContent: "git status" }], |
| | 3465 | behavior: "allow", |
| | 3466 | destination: "session", |
| | 3467 | }, |
| | 3468 | ]); |
| | 3469 | }).pipe( |
| | 3470 | Effect.provideService(Random.Random, makeDeterministicRandomService()), |
| | 3471 | Effect.provide(harness.layer), |
| | 3472 | ); |
| | 3473 | }); |
| | 3474 | |
| 3363 | it.effect("classifies Agent tools and read-only Claude tools correctly for approvals", () => { | 3475 | it.effect("classifies Agent tools and read-only Claude tools correctly for approvals", () => { |
| 3364 | const harness = makeHarness(); | 3476 | const harness = makeHarness(); |
| 3365 | return Effect.gen(function* () { | 3477 | return Effect.gen(function* () { |