diff --git a/packages/opencode/test/permission/next.test.ts b/packages/opencode/test/permission/next.test.ts index 1c3d6fc563..60b1781363 100644 --- a/packages/opencode/test/permission/next.test.ts +++ b/packages/opencode/test/permission/next.test.ts @@ -1,31 +1,26 @@ -import { afterEach, test, expect } from "bun:test" +import { test, expect } from "bun:test" import os from "os" -import { Cause, Effect, Exit, Fiber, Layer } from "effect" +import { Cause, Deferred, Effect, Exit, Fiber, Layer } from "effect" import { Bus } from "../../src/bus" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Permission } from "../../src/permission" import { PermissionID } from "../../src/permission/schema" -import { Instance } from "../../src/project/instance" -import { WithInstance } from "../../src/project/with-instance" -import { InstanceRuntime } from "../../src/project/instance-runtime" -import { - disposeAllInstances, - provideInstance, - provideTmpdirInstance, - reloadTestInstance, - tmpdirScoped, -} from "../fixture/fixture" +import { InstanceBootstrap } from "../../src/project/bootstrap-service" +import { InstanceStore } from "../../src/project/instance-store" +import { TestInstance, tmpdirScoped } from "../fixture/fixture" import { testEffect } from "../lib/effect" import { MessageID, SessionID } from "../../src/session/schema" const bus = Bus.layer -const env = Layer.mergeAll(Permission.layer.pipe(Layer.provide(bus)), bus, CrossSpawnSpawner.defaultLayer) +const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void })) +const env = Layer.mergeAll( + Permission.layer.pipe(Layer.provide(bus)), + bus, + CrossSpawnSpawner.defaultLayer, + InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap)), +) const it = testEffect(env) -afterEach(async () => { - await disposeAllInstances() -}) - const rejectAll = (message?: string) => Effect.gen(function* () { const permission = yield* Permission.Service @@ -41,12 +36,18 @@ const rejectAll = (message?: string) => const waitForPending = (count: number) => Effect.gen(function* () { const permission = yield* Permission.Service - for (let i = 0; i < 100; i++) { - const list = yield* permission.list() - if (list.length === count) return list - yield* Effect.sleep("10 millis") - } - return yield* Effect.fail(new Error(`timed out waiting for ${count} pending permission request(s)`)) + return yield* Effect.gen(function* () { + while (true) { + const list = yield* permission.list() + if (list.length === count) return list + yield* Effect.sleep("10 millis") + } + }).pipe( + Effect.timeoutOrElse({ + duration: "1 second", + orElse: () => Effect.fail(new Error(`timed out waiting for ${count} pending permission request(s)`)), + }), + ) }) const fail = (self: Effect.Effect) => @@ -74,14 +75,6 @@ const list = () => return yield* permission.list() }) -function withDir(options: { git?: boolean } | undefined, self: (dir: string) => Effect.Effect) { - return provideTmpdirInstance(self, options) -} - -function withProvided(dir: string) { - return (self: Effect.Effect) => self.pipe(provideInstance(dir)) -} - // fromConfig tests test("fromConfig - string value becomes wildcard rule", () => { @@ -563,215 +556,208 @@ test("disabled - specific allow overrides wildcard deny", () => { // ask tests -it.live("ask - resolves immediately when action is allow", () => - withDir({ git: true }, () => - Effect.gen(function* () { - const result = yield* ask({ - sessionID: SessionID.make("session_test"), - permission: "bash", - patterns: ["ls"], - metadata: {}, - always: [], - ruleset: [{ permission: "bash", pattern: "*", action: "allow" }], - }) - expect(result).toBeUndefined() - }), - ), +it.instance("ask - resolves immediately when action is allow", () => + Effect.gen(function* () { + const result = yield* ask({ + sessionID: SessionID.make("session_test"), + permission: "bash", + patterns: ["ls"], + metadata: {}, + always: [], + ruleset: [{ permission: "bash", pattern: "*", action: "allow" }], + }) + expect(result).toBeUndefined() + }), + { git: true }, ) -it.live("ask - throws DeniedError when action is deny", () => - withDir({ git: true }, () => - Effect.gen(function* () { - const err = yield* fail( - ask({ - sessionID: SessionID.make("session_test"), - permission: "bash", - patterns: ["rm -rf /"], - metadata: {}, - always: [], - ruleset: [{ permission: "bash", pattern: "*", action: "deny" }], +it.instance("ask - throws DeniedError when action is deny", () => + Effect.gen(function* () { + const err = yield* fail( + ask({ + sessionID: SessionID.make("session_test"), + permission: "bash", + patterns: ["rm -rf /"], + metadata: {}, + always: [], + ruleset: [{ permission: "bash", pattern: "*", action: "deny" }], + }), + ) + expect(err).toBeInstanceOf(Permission.DeniedError) + }), + { git: true }, +) + +it.instance("ask - stays pending when action is ask", () => + Effect.gen(function* () { + const fiber = yield* ask({ + sessionID: SessionID.make("session_test"), + permission: "bash", + patterns: ["ls"], + metadata: {}, + always: [], + ruleset: [{ permission: "bash", pattern: "*", action: "ask" }], + }).pipe(Effect.forkScoped) + + expect(yield* waitForPending(1)).toHaveLength(1) + yield* rejectAll() + yield* Fiber.await(fiber) + }), + { git: true }, +) + +it.instance("ask - adds request to pending list", () => + Effect.gen(function* () { + const fiber = yield* ask({ + sessionID: SessionID.make("session_test"), + permission: "bash", + patterns: ["ls"], + metadata: { cmd: "ls" }, + always: ["ls"], + tool: { + messageID: MessageID.make("msg_test"), + callID: "call_test", + }, + ruleset: [], + }).pipe(Effect.forkScoped) + + const items = yield* waitForPending(1) + expect(items).toHaveLength(1) + expect(items[0]).toMatchObject({ + sessionID: SessionID.make("session_test"), + permission: "bash", + patterns: ["ls"], + metadata: { cmd: "ls" }, + always: ["ls"], + tool: { + messageID: MessageID.make("msg_test"), + callID: "call_test", + }, + }) + + yield* rejectAll() + yield* Fiber.await(fiber) + }), + { git: true }, +) + +it.instance("ask - publishes asked event", () => + Effect.gen(function* () { + const bus = yield* Bus.Service + const seen = yield* Deferred.make() + const unsub = yield* bus.subscribeCallback(Permission.Event.Asked, (event) => { + Deferred.doneUnsafe(seen, Effect.succeed(event.properties)) + }) + yield* Effect.addFinalizer(() => Effect.sync(unsub)) + + const fiber = yield* ask({ + sessionID: SessionID.make("session_test"), + permission: "bash", + patterns: ["ls"], + metadata: { cmd: "ls" }, + always: ["ls"], + tool: { + messageID: MessageID.make("msg_test"), + callID: "call_test", + }, + ruleset: [], + }).pipe(Effect.forkScoped) + + expect(yield* waitForPending(1)).toHaveLength(1) + expect( + yield* Deferred.await(seen).pipe( + Effect.timeoutOrElse({ + duration: "1 second", + orElse: () => Effect.fail(new Error("timed out waiting for permission asked event")), }), - ) - expect(err).toBeInstanceOf(Permission.DeniedError) - }), - ), -) + ), + ).toMatchObject({ + sessionID: SessionID.make("session_test"), + permission: "bash", + patterns: ["ls"], + }) -it.live("ask - stays pending when action is ask", () => - withDir({ git: true }, () => - Effect.gen(function* () { - const fiber = yield* ask({ - sessionID: SessionID.make("session_test"), - permission: "bash", - patterns: ["ls"], - metadata: {}, - always: [], - ruleset: [{ permission: "bash", pattern: "*", action: "ask" }], - }).pipe(Effect.forkScoped) - - expect(yield* waitForPending(1)).toHaveLength(1) - yield* rejectAll() - yield* Fiber.await(fiber) - }), - ), -) - -it.live("ask - adds request to pending list", () => - withDir({ git: true }, () => - Effect.gen(function* () { - const fiber = yield* ask({ - sessionID: SessionID.make("session_test"), - permission: "bash", - patterns: ["ls"], - metadata: { cmd: "ls" }, - always: ["ls"], - tool: { - messageID: MessageID.make("msg_test"), - callID: "call_test", - }, - ruleset: [], - }).pipe(Effect.forkScoped) - - const items = yield* waitForPending(1) - expect(items).toHaveLength(1) - expect(items[0]).toMatchObject({ - sessionID: SessionID.make("session_test"), - permission: "bash", - patterns: ["ls"], - metadata: { cmd: "ls" }, - always: ["ls"], - tool: { - messageID: MessageID.make("msg_test"), - callID: "call_test", - }, - }) - - yield* rejectAll() - yield* Fiber.await(fiber) - }), - ), -) - -it.live("ask - publishes asked event", () => - withDir({ git: true }, () => - Effect.gen(function* () { - const bus = yield* Bus.Service - let seen: Permission.Request | undefined - const unsub = yield* bus.subscribeCallback(Permission.Event.Asked, (event) => { - seen = event.properties - }) - - try { - const fiber = yield* ask({ - sessionID: SessionID.make("session_test"), - permission: "bash", - patterns: ["ls"], - metadata: { cmd: "ls" }, - always: ["ls"], - tool: { - messageID: MessageID.make("msg_test"), - callID: "call_test", - }, - ruleset: [], - }).pipe(Effect.forkScoped) - - expect(yield* waitForPending(1)).toHaveLength(1) - expect(seen).toBeDefined() - expect(seen).toMatchObject({ - sessionID: SessionID.make("session_test"), - permission: "bash", - patterns: ["ls"], - }) - - yield* rejectAll() - yield* Fiber.await(fiber) - } finally { - unsub() - } - }), - ), + yield* rejectAll() + yield* Fiber.await(fiber) + }), + { git: true }, ) // reply tests -it.live("reply - once resolves the pending ask", () => - withDir({ git: true }, () => - Effect.gen(function* () { - const fiber = yield* ask({ - id: PermissionID.make("per_test1"), - sessionID: SessionID.make("session_test"), - permission: "bash", - patterns: ["ls"], - metadata: {}, - always: [], - ruleset: [], - }).pipe(Effect.forkScoped) - - yield* waitForPending(1) - yield* reply({ requestID: PermissionID.make("per_test1"), reply: "once" }) - yield* Fiber.join(fiber) - }), - ), -) - -it.live("reply - reject throws RejectedError", () => - withDir({ git: true }, () => - Effect.gen(function* () { - const fiber = yield* ask({ - id: PermissionID.make("per_test2"), - sessionID: SessionID.make("session_test"), - permission: "bash", - patterns: ["ls"], - metadata: {}, - always: [], - ruleset: [], - }).pipe(Effect.forkScoped) - - yield* waitForPending(1) - yield* reply({ requestID: PermissionID.make("per_test2"), reply: "reject" }) - - const exit = yield* Fiber.await(fiber) - expect(Exit.isFailure(exit)).toBe(true) - if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Permission.RejectedError) - }), - ), -) - -it.live("reply - reject with message throws CorrectedError", () => - withDir({ git: true }, () => - Effect.gen(function* () { - const fiber = yield* ask({ - id: PermissionID.make("per_test2b"), - sessionID: SessionID.make("session_test"), - permission: "bash", - patterns: ["ls"], - metadata: {}, - always: [], - ruleset: [], - }).pipe(Effect.forkScoped) - - yield* waitForPending(1) - yield* reply({ - requestID: PermissionID.make("per_test2b"), - reply: "reject", - message: "Use a safer command", - }) - - const exit = yield* Fiber.await(fiber) - expect(Exit.isFailure(exit)).toBe(true) - if (Exit.isFailure(exit)) { - const err = Cause.squash(exit.cause) - expect(err).toBeInstanceOf(Permission.CorrectedError) - expect(String(err)).toContain("Use a safer command") - } - }), - ), -) - -it.live("reply - always persists approval and resolves", () => +it.instance("reply - once resolves the pending ask", () => + Effect.gen(function* () { + const fiber = yield* ask({ + id: PermissionID.make("per_test1"), + sessionID: SessionID.make("session_test"), + permission: "bash", + patterns: ["ls"], + metadata: {}, + always: [], + ruleset: [], + }).pipe(Effect.forkScoped) + + yield* waitForPending(1) + yield* reply({ requestID: PermissionID.make("per_test1"), reply: "once" }) + yield* Fiber.join(fiber) + }), + { git: true }, +) + +it.instance("reply - reject throws RejectedError", () => + Effect.gen(function* () { + const fiber = yield* ask({ + id: PermissionID.make("per_test2"), + sessionID: SessionID.make("session_test"), + permission: "bash", + patterns: ["ls"], + metadata: {}, + always: [], + ruleset: [], + }).pipe(Effect.forkScoped) + + yield* waitForPending(1) + yield* reply({ requestID: PermissionID.make("per_test2"), reply: "reject" }) + + const exit = yield* Fiber.await(fiber) + expect(Exit.isFailure(exit)).toBe(true) + if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Permission.RejectedError) + }), + { git: true }, +) + +it.instance("reply - reject with message throws CorrectedError", () => + Effect.gen(function* () { + const fiber = yield* ask({ + id: PermissionID.make("per_test2b"), + sessionID: SessionID.make("session_test"), + permission: "bash", + patterns: ["ls"], + metadata: {}, + always: [], + ruleset: [], + }).pipe(Effect.forkScoped) + + yield* waitForPending(1) + yield* reply({ + requestID: PermissionID.make("per_test2b"), + reply: "reject", + message: "Use a safer command", + }) + + const exit = yield* Fiber.await(fiber) + expect(Exit.isFailure(exit)).toBe(true) + if (Exit.isFailure(exit)) { + const err = Cause.squash(exit.cause) + expect(err).toBeInstanceOf(Permission.CorrectedError) + expect(String(err)).toContain("Use a safer command") + } + }), + { git: true }, +) + +it.instance("reply - always persists approval and resolves", () => Effect.gen(function* () { - const dir = yield* tmpdirScoped({ git: true }) - const run = withProvided(dir) const fiber = yield* ask({ id: PermissionID.make("per_test3"), sessionID: SessionID.make("session_test"), @@ -780,10 +766,10 @@ it.live("reply - always persists approval and resolves", () => metadata: {}, always: ["ls"], ruleset: [], - }).pipe(run, Effect.forkScoped) + }).pipe(Effect.forkScoped) - yield* waitForPending(1).pipe(run) - yield* reply({ requestID: PermissionID.make("per_test3"), reply: "always" }).pipe(run) + yield* waitForPending(1) + yield* reply({ requestID: PermissionID.make("per_test3"), reply: "always" }) yield* Fiber.join(fiber) const result = yield* ask({ @@ -793,208 +779,208 @@ it.live("reply - always persists approval and resolves", () => metadata: {}, always: [], ruleset: [], - }).pipe(run) + }) expect(result).toBeUndefined() }), + { git: true }, ) -it.live("reply - reject cancels all pending for same session", () => - withDir({ git: true }, () => - Effect.gen(function* () { - const a = yield* ask({ - id: PermissionID.make("per_test4a"), - sessionID: SessionID.make("session_same"), - permission: "bash", - patterns: ["ls"], - metadata: {}, - always: [], - ruleset: [], - }).pipe(Effect.forkScoped) +it.instance("reply - reject cancels all pending for same session", () => + Effect.gen(function* () { + const a = yield* ask({ + id: PermissionID.make("per_test4a"), + sessionID: SessionID.make("session_same"), + permission: "bash", + patterns: ["ls"], + metadata: {}, + always: [], + ruleset: [], + }).pipe(Effect.forkScoped) - const b = yield* ask({ - id: PermissionID.make("per_test4b"), - sessionID: SessionID.make("session_same"), - permission: "edit", - patterns: ["foo.ts"], - metadata: {}, - always: [], - ruleset: [], - }).pipe(Effect.forkScoped) + const b = yield* ask({ + id: PermissionID.make("per_test4b"), + sessionID: SessionID.make("session_same"), + permission: "edit", + patterns: ["foo.ts"], + metadata: {}, + always: [], + ruleset: [], + }).pipe(Effect.forkScoped) - yield* waitForPending(2) - yield* reply({ requestID: PermissionID.make("per_test4a"), reply: "reject" }) + yield* waitForPending(2) + yield* reply({ requestID: PermissionID.make("per_test4a"), reply: "reject" }) - const [ea, eb] = yield* Effect.all([Fiber.await(a), Fiber.await(b)]) - expect(Exit.isFailure(ea)).toBe(true) - expect(Exit.isFailure(eb)).toBe(true) - if (Exit.isFailure(ea)) expect(Cause.squash(ea.cause)).toBeInstanceOf(Permission.RejectedError) - if (Exit.isFailure(eb)) expect(Cause.squash(eb.cause)).toBeInstanceOf(Permission.RejectedError) - }), - ), + const [ea, eb] = yield* Effect.all([Fiber.await(a), Fiber.await(b)]) + expect(Exit.isFailure(ea)).toBe(true) + expect(Exit.isFailure(eb)).toBe(true) + if (Exit.isFailure(ea)) expect(Cause.squash(ea.cause)).toBeInstanceOf(Permission.RejectedError) + if (Exit.isFailure(eb)) expect(Cause.squash(eb.cause)).toBeInstanceOf(Permission.RejectedError) + }), + { git: true }, ) -it.live("reply - always resolves matching pending requests in same session", () => - withDir({ git: true }, () => - Effect.gen(function* () { - const a = yield* ask({ - id: PermissionID.make("per_test5a"), - sessionID: SessionID.make("session_same"), - permission: "bash", - patterns: ["ls"], - metadata: {}, - always: ["ls"], - ruleset: [], - }).pipe(Effect.forkScoped) +it.instance("reply - always resolves matching pending requests in same session", () => + Effect.gen(function* () { + const a = yield* ask({ + id: PermissionID.make("per_test5a"), + sessionID: SessionID.make("session_same"), + permission: "bash", + patterns: ["ls"], + metadata: {}, + always: ["ls"], + ruleset: [], + }).pipe(Effect.forkScoped) - const b = yield* ask({ - id: PermissionID.make("per_test5b"), - sessionID: SessionID.make("session_same"), - permission: "bash", - patterns: ["ls"], - metadata: {}, - always: [], - ruleset: [], - }).pipe(Effect.forkScoped) + const b = yield* ask({ + id: PermissionID.make("per_test5b"), + sessionID: SessionID.make("session_same"), + permission: "bash", + patterns: ["ls"], + metadata: {}, + always: [], + ruleset: [], + }).pipe(Effect.forkScoped) - yield* waitForPending(2) - yield* reply({ requestID: PermissionID.make("per_test5a"), reply: "always" }) + yield* waitForPending(2) + yield* reply({ requestID: PermissionID.make("per_test5a"), reply: "always" }) - yield* Fiber.join(a) - yield* Fiber.join(b) - expect(yield* list()).toHaveLength(0) - }), - ), + yield* Fiber.join(a) + yield* Fiber.join(b) + expect(yield* list()).toHaveLength(0) + }), + { git: true }, ) -it.live("reply - always keeps other session pending", () => - withDir({ git: true }, () => - Effect.gen(function* () { - const a = yield* ask({ - id: PermissionID.make("per_test6a"), - sessionID: SessionID.make("session_a"), - permission: "bash", - patterns: ["ls"], - metadata: {}, - always: ["ls"], - ruleset: [], - }).pipe(Effect.forkScoped) +it.instance("reply - always keeps other session pending", () => + Effect.gen(function* () { + const a = yield* ask({ + id: PermissionID.make("per_test6a"), + sessionID: SessionID.make("session_a"), + permission: "bash", + patterns: ["ls"], + metadata: {}, + always: ["ls"], + ruleset: [], + }).pipe(Effect.forkScoped) - const b = yield* ask({ - id: PermissionID.make("per_test6b"), - sessionID: SessionID.make("session_b"), - permission: "bash", - patterns: ["ls"], - metadata: {}, - always: [], - ruleset: [], - }).pipe(Effect.forkScoped) + const b = yield* ask({ + id: PermissionID.make("per_test6b"), + sessionID: SessionID.make("session_b"), + permission: "bash", + patterns: ["ls"], + metadata: {}, + always: [], + ruleset: [], + }).pipe(Effect.forkScoped) - yield* waitForPending(2) - yield* reply({ requestID: PermissionID.make("per_test6a"), reply: "always" }) + yield* waitForPending(2) + yield* reply({ requestID: PermissionID.make("per_test6a"), reply: "always" }) - yield* Fiber.join(a) - expect((yield* list()).map((item) => item.id)).toEqual([PermissionID.make("per_test6b")]) + yield* Fiber.join(a) + expect((yield* list()).map((item) => item.id)).toEqual([PermissionID.make("per_test6b")]) - yield* rejectAll() - yield* Fiber.await(b) - }), - ), + yield* rejectAll() + yield* Fiber.await(b) + }), + { git: true }, ) -it.live("reply - publishes replied event", () => - withDir({ git: true }, () => - Effect.gen(function* () { - const bus = yield* Bus.Service - let resolve!: (value: { sessionID: SessionID; requestID: PermissionID; reply: Permission.Reply }) => void - const seen = Effect.promise<{ - sessionID: SessionID - requestID: PermissionID - reply: Permission.Reply - }>( - () => - new Promise((res) => { - resolve = res - }), - ) +it.instance("reply - publishes replied event", () => + Effect.gen(function* () { + const bus = yield* Bus.Service + const seen = yield* Deferred.make<{ sessionID: SessionID; requestID: PermissionID; reply: Permission.Reply }>() - const fiber = yield* ask({ - id: PermissionID.make("per_test7"), - sessionID: SessionID.make("session_test"), - permission: "bash", - patterns: ["ls"], - metadata: {}, - always: [], - ruleset: [], - }).pipe(Effect.forkScoped) + const fiber = yield* ask({ + id: PermissionID.make("per_test7"), + sessionID: SessionID.make("session_test"), + permission: "bash", + patterns: ["ls"], + metadata: {}, + always: [], + ruleset: [], + }).pipe(Effect.forkScoped) - yield* waitForPending(1) + yield* waitForPending(1) - const unsub = yield* bus.subscribeCallback(Permission.Event.Replied, (event) => { - resolve(event.properties) - }) + const unsub = yield* bus.subscribeCallback(Permission.Event.Replied, (event) => { + Deferred.doneUnsafe(seen, Effect.succeed(event.properties)) + }) + yield* Effect.addFinalizer(() => Effect.sync(unsub)) - try { - yield* reply({ requestID: PermissionID.make("per_test7"), reply: "once" }) - yield* Fiber.join(fiber) - expect(yield* seen).toEqual({ - sessionID: SessionID.make("session_test"), - requestID: PermissionID.make("per_test7"), - reply: "once", - }) - } finally { - unsub() - } - }), - ), + yield* reply({ requestID: PermissionID.make("per_test7"), reply: "once" }) + yield* Fiber.join(fiber) + expect( + yield* Deferred.await(seen).pipe( + Effect.timeoutOrElse({ + duration: "1 second", + orElse: () => Effect.fail(new Error("timed out waiting for permission replied event")), + }), + ), + ).toEqual({ + sessionID: SessionID.make("session_test"), + requestID: PermissionID.make("per_test7"), + reply: "once", + }) + }), + { git: true }, ) it.live("permission requests stay isolated by directory", () => Effect.gen(function* () { const one = yield* tmpdirScoped({ git: true }) const two = yield* tmpdirScoped({ git: true }) - const runOne = withProvided(one) - const runTwo = withProvided(two) + const store = yield* InstanceStore.Service - const a = yield* ask({ - id: PermissionID.make("per_dir_a"), - sessionID: SessionID.make("session_dir_a"), - permission: "bash", - patterns: ["ls"], - metadata: {}, - always: [], - ruleset: [], - }).pipe(runOne, Effect.forkScoped) + const a = yield* store + .provide( + { directory: one }, + ask({ + id: PermissionID.make("per_dir_a"), + sessionID: SessionID.make("session_dir_a"), + permission: "bash", + patterns: ["ls"], + metadata: {}, + always: [], + ruleset: [], + }), + ) + .pipe(Effect.forkScoped) - const b = yield* ask({ - id: PermissionID.make("per_dir_b"), - sessionID: SessionID.make("session_dir_b"), - permission: "bash", - patterns: ["pwd"], - metadata: {}, - always: [], - ruleset: [], - }).pipe(runTwo, Effect.forkScoped) + const b = yield* store + .provide( + { directory: two }, + ask({ + id: PermissionID.make("per_dir_b"), + sessionID: SessionID.make("session_dir_b"), + permission: "bash", + patterns: ["pwd"], + metadata: {}, + always: [], + ruleset: [], + }), + ) + .pipe(Effect.forkScoped) - const onePending = yield* waitForPending(1).pipe(runOne) - const twoPending = yield* waitForPending(1).pipe(runTwo) + const onePending = yield* store.provide({ directory: one }, waitForPending(1)) + const twoPending = yield* store.provide({ directory: two }, waitForPending(1)) expect(onePending).toHaveLength(1) expect(twoPending).toHaveLength(1) expect(onePending[0].id).toBe(PermissionID.make("per_dir_a")) expect(twoPending[0].id).toBe(PermissionID.make("per_dir_b")) - yield* reply({ requestID: onePending[0].id, reply: "reject" }).pipe(runOne) - yield* reply({ requestID: twoPending[0].id, reply: "reject" }).pipe(runTwo) + yield* store.provide({ directory: one }, reply({ requestID: onePending[0].id, reply: "reject" })) + yield* store.provide({ directory: two }, reply({ requestID: twoPending[0].id, reply: "reject" })) yield* Fiber.await(a) yield* Fiber.await(b) }), ) -it.live("pending permission rejects on instance dispose", () => +it.instance("pending permission rejects on instance dispose", () => Effect.gen(function* () { - const dir = yield* tmpdirScoped({ git: true }) - const run = withProvided(dir) + const test = yield* TestInstance + const store = yield* InstanceStore.Service const fiber = yield* ask({ id: PermissionID.make("per_dispose"), sessionID: SessionID.make("session_dispose"), @@ -1003,23 +989,23 @@ it.live("pending permission rejects on instance dispose", () => metadata: {}, always: [], ruleset: [], - }).pipe(run, Effect.forkScoped) + }).pipe(Effect.forkScoped) - expect(yield* waitForPending(1).pipe(run)).toHaveLength(1) - yield* Effect.promise(() => - WithInstance.provide({ directory: dir, fn: () => void InstanceRuntime.disposeInstance(Instance.current) }), - ) + expect(yield* waitForPending(1)).toHaveLength(1) + const ctx = yield* store.load({ directory: test.directory }) + yield* store.dispose(ctx) const exit = yield* Fiber.await(fiber) expect(Exit.isFailure(exit)).toBe(true) if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Permission.RejectedError) }), + { git: true }, ) -it.live("pending permission rejects on instance reload", () => +it.instance("pending permission rejects on instance reload", () => Effect.gen(function* () { - const dir = yield* tmpdirScoped({ git: true }) - const run = withProvided(dir) + const test = yield* TestInstance + const store = yield* InstanceStore.Service const fiber = yield* ask({ id: PermissionID.make("per_reload"), sessionID: SessionID.make("session_reload"), @@ -1028,90 +1014,87 @@ it.live("pending permission rejects on instance reload", () => metadata: {}, always: [], ruleset: [], - }).pipe(run, Effect.forkScoped) + }).pipe(Effect.forkScoped) - expect(yield* waitForPending(1).pipe(run)).toHaveLength(1) - yield* Effect.promise(() => reloadTestInstance({ directory: dir })) + expect(yield* waitForPending(1)).toHaveLength(1) + yield* store.reload({ directory: test.directory }) const exit = yield* Fiber.await(fiber) expect(Exit.isFailure(exit)).toBe(true) if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Permission.RejectedError) }), + { git: true }, ) -it.live("reply - does nothing for unknown requestID", () => - withDir({ git: true }, () => - Effect.gen(function* () { - yield* reply({ requestID: PermissionID.make("per_unknown"), reply: "once" }) - expect(yield* list()).toHaveLength(0) - }), - ), +it.instance("reply - does nothing for unknown requestID", () => + Effect.gen(function* () { + yield* reply({ requestID: PermissionID.make("per_unknown"), reply: "once" }) + expect(yield* list()).toHaveLength(0) + }), + { git: true }, ) -it.live("ask - checks all patterns and stops on first deny", () => - withDir({ git: true }, () => - Effect.gen(function* () { - const err = yield* fail( - ask({ - sessionID: SessionID.make("session_test"), - permission: "bash", - patterns: ["echo hello", "rm -rf /"], - metadata: {}, - always: [], - ruleset: [ - { permission: "bash", pattern: "*", action: "allow" }, - { permission: "bash", pattern: "rm *", action: "deny" }, - ], - }), - ) - expect(err).toBeInstanceOf(Permission.DeniedError) - }), - ), -) - -it.live("ask - allows all patterns when all match allow rules", () => - withDir({ git: true }, () => - Effect.gen(function* () { - const result = yield* ask({ +it.instance("ask - checks all patterns and stops on first deny", () => + Effect.gen(function* () { + const err = yield* fail( + ask({ sessionID: SessionID.make("session_test"), permission: "bash", - patterns: ["echo hello", "ls -la", "pwd"], + patterns: ["echo hello", "rm -rf /"], metadata: {}, always: [], - ruleset: [{ permission: "bash", pattern: "*", action: "allow" }], - }) - expect(result).toBeUndefined() - }), - ), + ruleset: [ + { permission: "bash", pattern: "*", action: "allow" }, + { permission: "bash", pattern: "rm *", action: "deny" }, + ], + }), + ) + expect(err).toBeInstanceOf(Permission.DeniedError) + }), + { git: true }, ) -it.live("ask - should deny even when an earlier pattern is ask", () => - withDir({ git: true }, () => - Effect.gen(function* () { - const err = yield* fail( - ask({ - sessionID: SessionID.make("session_test"), - permission: "bash", - patterns: ["echo hello", "rm -rf /"], - metadata: {}, - always: [], - ruleset: [ - { permission: "bash", pattern: "echo *", action: "ask" }, - { permission: "bash", pattern: "rm *", action: "deny" }, - ], - }), - ) - - expect(err).toBeInstanceOf(Permission.DeniedError) - expect(yield* list()).toHaveLength(0) - }), - ), -) - -it.live("ask - abort should clear pending request", () => +it.instance("ask - allows all patterns when all match allow rules", () => Effect.gen(function* () { - const dir = yield* tmpdirScoped({ git: true }) - const run = withProvided(dir) + const result = yield* ask({ + sessionID: SessionID.make("session_test"), + permission: "bash", + patterns: ["echo hello", "ls -la", "pwd"], + metadata: {}, + always: [], + ruleset: [{ permission: "bash", pattern: "*", action: "allow" }], + }) + expect(result).toBeUndefined() + }), + { git: true }, +) + +it.instance("ask - should deny even when an earlier pattern is ask", () => + Effect.gen(function* () { + const err = yield* fail( + ask({ + sessionID: SessionID.make("session_test"), + permission: "bash", + patterns: ["echo hello", "rm -rf /"], + metadata: {}, + always: [], + ruleset: [ + { permission: "bash", pattern: "echo *", action: "ask" }, + { permission: "bash", pattern: "rm *", action: "deny" }, + ], + }), + ) + + expect(err).toBeInstanceOf(Permission.DeniedError) + expect(yield* list()).toHaveLength(0) + }), + { git: true }, +) + +it.instance("ask - abort should clear pending request", () => + Effect.gen(function* () { + const test = yield* TestInstance + const store = yield* InstanceStore.Service const fiber = yield* ask({ id: PermissionID.make("per_reload"), @@ -1121,14 +1104,15 @@ it.live("ask - abort should clear pending request", () => metadata: {}, always: [], ruleset: [{ permission: "bash", pattern: "*", action: "ask" }], - }).pipe(run, Effect.forkScoped) + }).pipe(Effect.forkScoped) - const pending = yield* waitForPending(1).pipe(run) + const pending = yield* waitForPending(1) expect(pending).toHaveLength(1) - yield* Effect.promise(() => reloadTestInstance({ directory: dir })) + yield* store.reload({ directory: test.directory }) const exit = yield* Fiber.await(fiber) expect(Exit.isFailure(exit)).toBe(true) if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Permission.RejectedError) }), + { git: true }, )