refactor(flags): route scout through runtime flags (#27318)
This commit is contained in:
@@ -15,12 +15,12 @@ import PROMPT_TITLE from "./prompt/title.txt"
|
|||||||
import { Permission } from "@/permission"
|
import { Permission } from "@/permission"
|
||||||
import { mergeDeep, pipe, sortBy, values } from "remeda"
|
import { mergeDeep, pipe, sortBy, values } from "remeda"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Plugin } from "@/plugin"
|
import { Plugin } from "@/plugin"
|
||||||
import { Skill } from "../skill"
|
import { Skill } from "../skill"
|
||||||
import { Effect, Context, Layer, Schema } from "effect"
|
import { Effect, Context, Layer, Schema } from "effect"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
|
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||||
import * as Option from "effect/Option"
|
import * as Option from "effect/Option"
|
||||||
import * as OtelTracer from "@effect/opentelemetry/Tracer"
|
import * as OtelTracer from "@effect/opentelemetry/Tracer"
|
||||||
import { type DeepMutable } from "@opencode-ai/core/schema"
|
import { type DeepMutable } from "@opencode-ai/core/schema"
|
||||||
@@ -81,6 +81,7 @@ export const layer = Layer.effect(
|
|||||||
const plugin = yield* Plugin.Service
|
const plugin = yield* Plugin.Service
|
||||||
const skill = yield* Skill.Service
|
const skill = yield* Skill.Service
|
||||||
const provider = yield* Provider.Service
|
const provider = yield* Provider.Service
|
||||||
|
const flags = yield* RuntimeFlags.Service
|
||||||
|
|
||||||
const state = yield* InstanceState.make<State>(
|
const state = yield* InstanceState.make<State>(
|
||||||
Effect.fn("Agent.state")(function* (ctx) {
|
Effect.fn("Agent.state")(function* (ctx) {
|
||||||
@@ -195,7 +196,7 @@ export const layer = Layer.effect(
|
|||||||
mode: "subagent",
|
mode: "subagent",
|
||||||
native: true,
|
native: true,
|
||||||
},
|
},
|
||||||
...(Flag.OPENCODE_EXPERIMENTAL_SCOUT
|
...(flags.experimentalScout
|
||||||
? {
|
? {
|
||||||
scout: {
|
scout: {
|
||||||
name: "scout",
|
name: "scout",
|
||||||
@@ -453,6 +454,7 @@ export const defaultLayer = layer.pipe(
|
|||||||
Layer.provide(Auth.defaultLayer),
|
Layer.provide(Auth.defaultLayer),
|
||||||
Layer.provide(Config.defaultLayer),
|
Layer.provide(Config.defaultLayer),
|
||||||
Layer.provide(Skill.defaultLayer),
|
Layer.provide(Skill.defaultLayer),
|
||||||
|
Layer.provide(RuntimeFlags.defaultLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
export * as Agent from "./agent"
|
export * as Agent from "./agent"
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Context, Layer, Scope } from "effect"
|
import { Effect, Context, Layer, Scope } from "effect"
|
||||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
|
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||||
import { Git } from "@/git"
|
import { Git } from "@/git"
|
||||||
import { parseRepositoryReference, repositoryCachePath, type Reference as RepositoryReference } from "@/util/repository"
|
import { parseRepositoryReference, repositoryCachePath, type Reference as RepositoryReference } from "@/util/repository"
|
||||||
import { RepositoryCache } from "./repository-cache"
|
import { RepositoryCache } from "./repository-cache"
|
||||||
@@ -143,6 +143,7 @@ export const layer = Layer.effect(
|
|||||||
const fs = yield* AppFileSystem.Service
|
const fs = yield* AppFileSystem.Service
|
||||||
const git = yield* Git.Service
|
const git = yield* Git.Service
|
||||||
const scope = yield* Scope.Scope
|
const scope = yield* Scope.Scope
|
||||||
|
const flags = yield* RuntimeFlags.Service
|
||||||
|
|
||||||
const state = yield* InstanceState.make<State>(
|
const state = yield* InstanceState.make<State>(
|
||||||
Effect.fn("Reference.state")(function* (ctx) {
|
Effect.fn("Reference.state")(function* (ctx) {
|
||||||
@@ -181,7 +182,7 @@ export const layer = Layer.effect(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const materializeAll = yield* Effect.cached(
|
const materializeAll = yield* Effect.cached(
|
||||||
Flag.OPENCODE_EXPERIMENTAL_SCOUT
|
flags.experimentalScout
|
||||||
? Effect.gen(function* () {
|
? Effect.gen(function* () {
|
||||||
yield* Effect.forEach(
|
yield* Effect.forEach(
|
||||||
materializeByPath,
|
materializeByPath,
|
||||||
@@ -200,7 +201,7 @@ export const layer = Layer.effect(
|
|||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
init: Effect.fn("Reference.init")(function* () {
|
init: Effect.fn("Reference.init")(function* () {
|
||||||
if (!Flag.OPENCODE_EXPERIMENTAL_SCOUT) return
|
if (!flags.experimentalScout) return
|
||||||
yield* InstanceState.useEffect(state, (s) => s.materializeAll).pipe(Effect.forkIn(scope), Effect.asVoid)
|
yield* InstanceState.useEffect(state, (s) => s.materializeAll).pipe(Effect.forkIn(scope), Effect.asVoid)
|
||||||
}),
|
}),
|
||||||
list: Effect.fn("Reference.list")(function* () {
|
list: Effect.fn("Reference.list")(function* () {
|
||||||
@@ -210,7 +211,7 @@ export const layer = Layer.effect(
|
|||||||
return yield* InstanceState.use(state, (s) => s.references.find((reference) => reference.name === name))
|
return yield* InstanceState.use(state, (s) => s.references.find((reference) => reference.name === name))
|
||||||
}),
|
}),
|
||||||
ensure: Effect.fn("Reference.ensure")(function* (target?: string) {
|
ensure: Effect.fn("Reference.ensure")(function* (target?: string) {
|
||||||
if (!Flag.OPENCODE_EXPERIMENTAL_SCOUT) return
|
if (!flags.experimentalScout) return
|
||||||
const full = normalizedTarget(target)
|
const full = normalizedTarget(target)
|
||||||
if (!full) return yield* InstanceState.useEffect(state, (s) => s.materializeAll)
|
if (!full) return yield* InstanceState.useEffect(state, (s) => s.materializeAll)
|
||||||
return yield* InstanceState.useEffect(
|
return yield* InstanceState.useEffect(
|
||||||
@@ -219,7 +220,7 @@ export const layer = Layer.effect(
|
|||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
contains: Effect.fn("Reference.contains")(function* (target?: string) {
|
contains: Effect.fn("Reference.contains")(function* (target?: string) {
|
||||||
if (!Flag.OPENCODE_EXPERIMENTAL_SCOUT) return false
|
if (!flags.experimentalScout) return false
|
||||||
const full = normalizedTarget(target)
|
const full = normalizedTarget(target)
|
||||||
if (!full) return false
|
if (!full) return false
|
||||||
return yield* InstanceState.use(state, (s) =>
|
return yield* InstanceState.use(state, (s) =>
|
||||||
@@ -234,6 +235,7 @@ export const defaultLayer = layer.pipe(
|
|||||||
Layer.provide(Config.defaultLayer),
|
Layer.provide(Config.defaultLayer),
|
||||||
Layer.provide(AppFileSystem.defaultLayer),
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
Layer.provide(Git.defaultLayer),
|
Layer.provide(Git.defaultLayer),
|
||||||
|
Layer.provide(RuntimeFlags.defaultLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
export * as Reference from "./reference"
|
export * as Reference from "./reference"
|
||||||
|
|||||||
@@ -1,15 +1,31 @@
|
|||||||
import { afterEach, expect } from "bun:test"
|
import { afterEach, expect } from "bun:test"
|
||||||
import { Cause, Effect, Exit } from "effect"
|
import { Cause, Effect, Exit, Layer } from "effect"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { disposeAllInstances, TestInstance } from "../fixture/fixture"
|
import { disposeAllInstances, TestInstance } from "../fixture/fixture"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { Agent } from "../../src/agent/agent"
|
import { Agent } from "../../src/agent/agent"
|
||||||
|
import { Auth } from "../../src/auth"
|
||||||
|
import { Config } from "../../src/config/config"
|
||||||
|
import { RuntimeFlags } from "../../src/effect/runtime-flags"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
|
||||||
import { Permission } from "../../src/permission"
|
import { Permission } from "../../src/permission"
|
||||||
|
import { Plugin } from "../../src/plugin"
|
||||||
|
import { Provider } from "../../src/provider/provider"
|
||||||
|
import { Skill } from "../../src/skill"
|
||||||
import { Truncate } from "../../src/tool/truncate"
|
import { Truncate } from "../../src/tool/truncate"
|
||||||
|
|
||||||
const it = testEffect(Agent.defaultLayer)
|
const agentLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
||||||
|
Agent.layer.pipe(
|
||||||
|
Layer.provide(Plugin.defaultLayer),
|
||||||
|
Layer.provide(Provider.defaultLayer),
|
||||||
|
Layer.provide(Auth.defaultLayer),
|
||||||
|
Layer.provide(Config.defaultLayer),
|
||||||
|
Layer.provide(Skill.defaultLayer),
|
||||||
|
Layer.provide(RuntimeFlags.layer(flags)),
|
||||||
|
)
|
||||||
|
|
||||||
|
const it = testEffect(agentLayer())
|
||||||
|
const scout = testEffect(agentLayer({ experimentalScout: true }))
|
||||||
|
|
||||||
// Helper to evaluate permission for a tool with wildcard pattern
|
// Helper to evaluate permission for a tool with wildcard pattern
|
||||||
function evalPerm(agent: Agent.Info | undefined, permission: string): Permission.Action | undefined {
|
function evalPerm(agent: Agent.Info | undefined, permission: string): Permission.Action | undefined {
|
||||||
@@ -21,21 +37,6 @@ function load<A>(fn: (svc: Agent.Interface) => Effect.Effect<A>) {
|
|||||||
return Agent.Service.use(fn)
|
return Agent.Service.use(fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
function withExperimentalScout<A, E, R>(enabled: boolean, self: Effect.Effect<A, E, R>) {
|
|
||||||
return Effect.acquireUseRelease(
|
|
||||||
Effect.sync(() => {
|
|
||||||
const original = Flag.OPENCODE_EXPERIMENTAL_SCOUT
|
|
||||||
Flag.OPENCODE_EXPERIMENTAL_SCOUT = enabled
|
|
||||||
return original
|
|
||||||
}),
|
|
||||||
() => self,
|
|
||||||
(original) =>
|
|
||||||
Effect.sync(() => {
|
|
||||||
Flag.OPENCODE_EXPERIMENTAL_SCOUT = original
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const expectDefaultAgentError = Effect.fn("AgentTest.expectDefaultAgentError")(function* (message: string) {
|
const expectDefaultAgentError = Effect.fn("AgentTest.expectDefaultAgentError")(function* (message: string) {
|
||||||
const exit = yield* load((svc) => svc.defaultAgent()).pipe(Effect.exit)
|
const exit = yield* load((svc) => svc.defaultAgent()).pipe(Effect.exit)
|
||||||
expect(Exit.isFailure(exit)).toBe(true)
|
expect(Exit.isFailure(exit)).toBe(true)
|
||||||
@@ -47,21 +48,18 @@ afterEach(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it.instance("returns default native agents when no config", () =>
|
it.instance("returns default native agents when no config", () =>
|
||||||
withExperimentalScout(
|
Effect.gen(function* () {
|
||||||
false,
|
const agents = yield* load((svc) => svc.list())
|
||||||
Effect.gen(function* () {
|
const names = agents.map((a) => a.name)
|
||||||
const agents = yield* load((svc) => svc.list())
|
expect(names).toContain("build")
|
||||||
const names = agents.map((a) => a.name)
|
expect(names).toContain("plan")
|
||||||
expect(names).toContain("build")
|
expect(names).toContain("general")
|
||||||
expect(names).toContain("plan")
|
expect(names).toContain("explore")
|
||||||
expect(names).toContain("general")
|
expect(names).not.toContain("scout")
|
||||||
expect(names).toContain("explore")
|
expect(names).toContain("compaction")
|
||||||
expect(names).not.toContain("scout")
|
expect(names).toContain("title")
|
||||||
expect(names).toContain("compaction")
|
expect(names).toContain("summary")
|
||||||
expect(names).toContain("title")
|
}),
|
||||||
expect(names).toContain("summary")
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance("build agent has correct default properties", () =>
|
it.instance("build agent has correct default properties", () =>
|
||||||
@@ -111,42 +109,36 @@ it.instance("explore agent asks for external directories and allows whitelisted
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance("scout agent allows repo cloning and repo cache reads", () =>
|
scout.instance("scout agent allows repo cloning and repo cache reads", () =>
|
||||||
withExperimentalScout(
|
Effect.gen(function* () {
|
||||||
true,
|
const scout = yield* load((svc) => svc.get("scout"))
|
||||||
Effect.gen(function* () {
|
expect(scout).toBeDefined()
|
||||||
const scout = yield* load((svc) => svc.get("scout"))
|
expect(scout?.mode).toBe("subagent")
|
||||||
expect(scout).toBeDefined()
|
expect(evalPerm(scout, "repo_clone")).toBe("allow")
|
||||||
expect(scout?.mode).toBe("subagent")
|
expect(evalPerm(scout, "repo_overview")).toBe("allow")
|
||||||
expect(evalPerm(scout, "repo_clone")).toBe("allow")
|
expect(evalPerm(scout, "edit")).toBe("deny")
|
||||||
expect(evalPerm(scout, "repo_overview")).toBe("allow")
|
expect(
|
||||||
expect(evalPerm(scout, "edit")).toBe("deny")
|
Permission.evaluate(
|
||||||
expect(
|
"external_directory",
|
||||||
Permission.evaluate(
|
path.join(Global.Path.repos, "github.com", "owner", "repo", "README.md"),
|
||||||
"external_directory",
|
scout!.permission,
|
||||||
path.join(Global.Path.repos, "github.com", "owner", "repo", "README.md"),
|
).action,
|
||||||
scout!.permission,
|
).toBe("allow")
|
||||||
).action,
|
}),
|
||||||
).toBe("allow")
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance(
|
scout.instance(
|
||||||
"reference config does not create subagents",
|
"reference config does not create subagents",
|
||||||
() =>
|
() =>
|
||||||
withExperimentalScout(
|
Effect.gen(function* () {
|
||||||
true,
|
const agents = yield* load((svc) => svc.list())
|
||||||
Effect.gen(function* () {
|
const names = agents.map((agent) => agent.name)
|
||||||
const agents = yield* load((svc) => svc.list())
|
expect(names).toContain("scout")
|
||||||
const names = agents.map((agent) => agent.name)
|
expect(names).not.toContain("effect")
|
||||||
expect(names).toContain("scout")
|
expect(names).not.toContain("effectFull")
|
||||||
expect(names).not.toContain("effect")
|
expect(names).not.toContain("localdocs")
|
||||||
expect(names).not.toContain("effectFull")
|
expect(names).not.toContain("localdocsFull")
|
||||||
expect(names).not.toContain("localdocs")
|
}),
|
||||||
expect(names).not.toContain("localdocsFull")
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
config: {
|
config: {
|
||||||
reference: {
|
reference: {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const agentLayer = Agent.layer.pipe(
|
|||||||
Layer.provide(SkillTest.empty),
|
Layer.provide(SkillTest.empty),
|
||||||
Layer.provide(provider.layer),
|
Layer.provide(provider.layer),
|
||||||
Layer.provide(pluginLayer),
|
Layer.provide(pluginLayer),
|
||||||
|
Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })),
|
||||||
)
|
)
|
||||||
|
|
||||||
const it = testEffect(Layer.mergeAll(agentLayer, pluginLayer))
|
const it = testEffect(Layer.mergeAll(agentLayer, pluginLayer))
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import path from "path"
|
|||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
|
import { Config } from "../../src/config/config"
|
||||||
|
import { RuntimeFlags } from "../../src/effect/runtime-flags"
|
||||||
import { Git } from "../../src/git"
|
import { Git } from "../../src/git"
|
||||||
import { Reference } from "../../src/reference/reference"
|
import { Reference } from "../../src/reference/reference"
|
||||||
import { disposeAllInstances, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
|
import { disposeAllInstances, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
|
||||||
@@ -14,24 +15,26 @@ afterEach(async () => {
|
|||||||
await disposeAllInstances()
|
await disposeAllInstances()
|
||||||
})
|
})
|
||||||
|
|
||||||
const it = testEffect(
|
const referenceLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
||||||
Layer.mergeAll(AppFileSystem.defaultLayer, CrossSpawnSpawner.defaultLayer, Git.defaultLayer, Reference.defaultLayer),
|
Reference.layer.pipe(
|
||||||
)
|
Layer.provide(Config.defaultLayer),
|
||||||
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
const experimentalScout = <A, E, R>(self: Effect.Effect<A, E, R>) =>
|
Layer.provide(Git.defaultLayer),
|
||||||
Effect.acquireUseRelease(
|
Layer.provide(RuntimeFlags.layer(flags)),
|
||||||
Effect.sync(() => {
|
|
||||||
const previous = Flag.OPENCODE_EXPERIMENTAL_SCOUT
|
|
||||||
Flag.OPENCODE_EXPERIMENTAL_SCOUT = true
|
|
||||||
return previous
|
|
||||||
}),
|
|
||||||
() => self,
|
|
||||||
(previous) =>
|
|
||||||
Effect.sync(() => {
|
|
||||||
Flag.OPENCODE_EXPERIMENTAL_SCOUT = previous
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const it = testEffect(
|
||||||
|
Layer.mergeAll(AppFileSystem.defaultLayer, CrossSpawnSpawner.defaultLayer, Git.defaultLayer, referenceLayer()),
|
||||||
|
)
|
||||||
|
const scout = testEffect(
|
||||||
|
Layer.mergeAll(
|
||||||
|
AppFileSystem.defaultLayer,
|
||||||
|
CrossSpawnSpawner.defaultLayer,
|
||||||
|
Git.defaultLayer,
|
||||||
|
referenceLayer({ experimentalScout: true }),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
const githubBase = <A, E, R>(url: string, self: Effect.Effect<A, E, R>) =>
|
const githubBase = <A, E, R>(url: string, self: Effect.Effect<A, E, R>) =>
|
||||||
Effect.acquireUseRelease(
|
Effect.acquireUseRelease(
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
@@ -127,118 +130,114 @@ describe("reference", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("materializes configured git references during init", () =>
|
scout.live("materializes configured git references during init", () =>
|
||||||
experimentalScout(
|
provideTmpdirInstance(
|
||||||
provideTmpdirInstance(
|
(_dir) =>
|
||||||
(_dir) =>
|
Effect.gen(function* () {
|
||||||
Effect.gen(function* () {
|
const fs = yield* AppFileSystem.Service
|
||||||
const fs = yield* AppFileSystem.Service
|
const cache = path.join(Global.Path.repos, "github.com", "opencode-reference-test", "repo")
|
||||||
const cache = path.join(Global.Path.repos, "github.com", "opencode-reference-test", "repo")
|
yield* fs.remove(cache, { recursive: true }).pipe(Effect.ignore)
|
||||||
yield* fs.remove(cache, { recursive: true }).pipe(Effect.ignore)
|
yield* Effect.addFinalizer(() => fs.remove(cache, { recursive: true }).pipe(Effect.ignore))
|
||||||
yield* Effect.addFinalizer(() => fs.remove(cache, { recursive: true }).pipe(Effect.ignore))
|
|
||||||
|
|
||||||
const source = yield* tmpdirScoped({ git: true })
|
const source = yield* tmpdirScoped({ git: true })
|
||||||
const remoteRoot = yield* tmpdirScoped()
|
const remoteRoot = yield* tmpdirScoped()
|
||||||
const remoteDir = path.join(remoteRoot, "opencode-reference-test")
|
const remoteDir = path.join(remoteRoot, "opencode-reference-test")
|
||||||
const remoteRepo = path.join(remoteDir, "repo.git")
|
const remoteRepo = path.join(remoteDir, "repo.git")
|
||||||
|
|
||||||
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "configured\n"))
|
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "configured\n"))
|
||||||
yield* git(source, ["add", "."])
|
yield* git(source, ["add", "."])
|
||||||
yield* git(source, ["commit", "-m", "add readme"])
|
yield* git(source, ["commit", "-m", "add readme"])
|
||||||
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
|
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
|
||||||
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
|
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
|
||||||
|
|
||||||
const reference = yield* Reference.Service
|
const reference = yield* Reference.Service
|
||||||
yield* githubBase(
|
yield* githubBase(
|
||||||
`file://${remoteRoot}/`,
|
`file://${remoteRoot}/`,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* reference.init()
|
yield* reference.init()
|
||||||
yield* waitForContent(fs, path.join(cache, "README.md"), "configured\n")
|
yield* waitForContent(fs, path.join(cache, "README.md"), "configured\n")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(yield* fs.existsSafe(path.join(cache, ".git"))).toBe(true)
|
expect(yield* fs.existsSafe(path.join(cache, ".git"))).toBe(true)
|
||||||
expect(yield* fs.readFileString(path.join(cache, "README.md"))).toBe("configured\n")
|
expect(yield* fs.readFileString(path.join(cache, "README.md"))).toBe("configured\n")
|
||||||
|
|
||||||
const resolved = yield* reference.get("docs")
|
const resolved = yield* reference.get("docs")
|
||||||
expect(resolved?.kind).toBe("git")
|
expect(resolved?.kind).toBe("git")
|
||||||
if (resolved?.kind === "git") expect(resolved.path).toBe(cache)
|
if (resolved?.kind === "git") expect(resolved.path).toBe(cache)
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
config: {
|
config: {
|
||||||
reference: {
|
reference: {
|
||||||
docs: "opencode-reference-test/repo",
|
docs: "opencode-reference-test/repo",
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("refreshes configured git references on new instance init", () =>
|
scout.live("refreshes configured git references on new instance init", () =>
|
||||||
experimentalScout(
|
Effect.gen(function* () {
|
||||||
Effect.gen(function* () {
|
const fs = yield* AppFileSystem.Service
|
||||||
const fs = yield* AppFileSystem.Service
|
const cache = path.join(Global.Path.repos, "github.com", "opencode-reference-refresh", "repo")
|
||||||
const cache = path.join(Global.Path.repos, "github.com", "opencode-reference-refresh", "repo")
|
yield* fs.remove(cache, { recursive: true }).pipe(Effect.ignore)
|
||||||
yield* fs.remove(cache, { recursive: true }).pipe(Effect.ignore)
|
yield* Effect.addFinalizer(() => fs.remove(cache, { recursive: true }).pipe(Effect.ignore))
|
||||||
yield* Effect.addFinalizer(() => fs.remove(cache, { recursive: true }).pipe(Effect.ignore))
|
|
||||||
|
|
||||||
const source = yield* tmpdirScoped({ git: true })
|
const source = yield* tmpdirScoped({ git: true })
|
||||||
const remoteRoot = yield* tmpdirScoped()
|
const remoteRoot = yield* tmpdirScoped()
|
||||||
const remoteDir = path.join(remoteRoot, "opencode-reference-refresh")
|
const remoteDir = path.join(remoteRoot, "opencode-reference-refresh")
|
||||||
const remoteRepo = path.join(remoteDir, "repo.git")
|
const remoteRepo = path.join(remoteDir, "repo.git")
|
||||||
|
|
||||||
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v1\n"))
|
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v1\n"))
|
||||||
yield* git(source, ["add", "."])
|
yield* git(source, ["add", "."])
|
||||||
yield* git(source, ["commit", "-m", "add readme"])
|
yield* git(source, ["commit", "-m", "add readme"])
|
||||||
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
|
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
|
||||||
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
|
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
|
||||||
|
|
||||||
yield* githubBase(
|
yield* githubBase(
|
||||||
`file://${remoteRoot}/`,
|
`file://${remoteRoot}/`,
|
||||||
provideTmpdirInstance(
|
provideTmpdirInstance(
|
||||||
(_dir) =>
|
(_dir) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const reference = yield* Reference.Service
|
const reference = yield* Reference.Service
|
||||||
yield* reference.init()
|
yield* reference.init()
|
||||||
yield* waitForContent(fs, path.join(cache, "README.md"), "v1\n")
|
yield* waitForContent(fs, path.join(cache, "README.md"), "v1\n")
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
config: {
|
config: {
|
||||||
reference: {
|
reference: {
|
||||||
docs: "opencode-reference-refresh/repo",
|
docs: "opencode-reference-refresh/repo",
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
),
|
},
|
||||||
)
|
),
|
||||||
|
)
|
||||||
|
|
||||||
const branch = yield* git(source, ["branch", "--show-current"])
|
const branch = yield* git(source, ["branch", "--show-current"])
|
||||||
yield* git(source, ["remote", "add", "origin", remoteRepo])
|
yield* git(source, ["remote", "add", "origin", remoteRepo])
|
||||||
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v2\n"))
|
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v2\n"))
|
||||||
yield* git(source, ["add", "."])
|
yield* git(source, ["add", "."])
|
||||||
yield* git(source, ["commit", "-m", "update readme"])
|
yield* git(source, ["commit", "-m", "update readme"])
|
||||||
yield* git(source, ["push", "origin", `${branch}:${branch}`])
|
yield* git(source, ["push", "origin", `${branch}:${branch}`])
|
||||||
|
|
||||||
yield* githubBase(
|
yield* githubBase(
|
||||||
`file://${remoteRoot}/`,
|
`file://${remoteRoot}/`,
|
||||||
provideTmpdirInstance(
|
provideTmpdirInstance(
|
||||||
(_dir) =>
|
(_dir) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const reference = yield* Reference.Service
|
const reference = yield* Reference.Service
|
||||||
yield* reference.init()
|
yield* reference.init()
|
||||||
yield* waitForContent(fs, path.join(cache, "README.md"), "v2\n")
|
yield* waitForContent(fs, path.join(cache, "README.md"), "v2\n")
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
config: {
|
config: {
|
||||||
reference: {
|
reference: {
|
||||||
docs: "opencode-reference-refresh/repo",
|
docs: "opencode-reference-refresh/repo",
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
),
|
},
|
||||||
)
|
),
|
||||||
}),
|
)
|
||||||
),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import path from "path"
|
|||||||
import { Agent } from "../../src/agent/agent"
|
import { Agent } from "../../src/agent/agent"
|
||||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
|
import { Config } from "@/config/config"
|
||||||
|
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||||
|
import { Git } from "@/git"
|
||||||
import { LSP } from "@/lsp/lsp"
|
import { LSP } from "@/lsp/lsp"
|
||||||
import { Permission } from "../../src/permission"
|
import { Permission } from "../../src/permission"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
@@ -36,17 +38,27 @@ const ctx = {
|
|||||||
ask: () => Effect.void,
|
ask: () => Effect.void,
|
||||||
}
|
}
|
||||||
|
|
||||||
const it = testEffect(
|
const referenceLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
||||||
|
Reference.layer.pipe(
|
||||||
|
Layer.provide(Config.defaultLayer),
|
||||||
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
|
Layer.provide(Git.defaultLayer),
|
||||||
|
Layer.provide(RuntimeFlags.layer(flags)),
|
||||||
|
)
|
||||||
|
|
||||||
|
const readLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
||||||
Layer.mergeAll(
|
Layer.mergeAll(
|
||||||
Agent.defaultLayer,
|
Agent.defaultLayer,
|
||||||
AppFileSystem.defaultLayer,
|
AppFileSystem.defaultLayer,
|
||||||
CrossSpawnSpawner.defaultLayer,
|
CrossSpawnSpawner.defaultLayer,
|
||||||
Instruction.defaultLayer,
|
Instruction.defaultLayer,
|
||||||
LSP.defaultLayer,
|
LSP.defaultLayer,
|
||||||
Reference.defaultLayer,
|
referenceLayer(flags),
|
||||||
Truncate.defaultLayer,
|
Truncate.defaultLayer,
|
||||||
),
|
)
|
||||||
)
|
|
||||||
|
const it = testEffect(readLayer())
|
||||||
|
const scout = testEffect(readLayer({ experimentalScout: true }))
|
||||||
|
|
||||||
const init = Effect.fn("ReadToolTest.init")(function* () {
|
const init = Effect.fn("ReadToolTest.init")(function* () {
|
||||||
const info = yield* ReadTool
|
const info = yield* ReadTool
|
||||||
@@ -85,19 +97,6 @@ const fail = Effect.fn("ReadToolTest.fail")(function* (
|
|||||||
const full = (p: string) => (process.platform === "win32" ? Filesystem.normalizePath(p) : p)
|
const full = (p: string) => (process.platform === "win32" ? Filesystem.normalizePath(p) : p)
|
||||||
const glob = (p: string) =>
|
const glob = (p: string) =>
|
||||||
process.platform === "win32" ? Filesystem.normalizePathPattern(p) : p.replaceAll("\\", "/")
|
process.platform === "win32" ? Filesystem.normalizePathPattern(p) : p.replaceAll("\\", "/")
|
||||||
const experimentalScout = <A, E, R>(self: Effect.Effect<A, E, R>) =>
|
|
||||||
Effect.acquireUseRelease(
|
|
||||||
Effect.sync(() => {
|
|
||||||
const previous = Flag.OPENCODE_EXPERIMENTAL_SCOUT
|
|
||||||
Flag.OPENCODE_EXPERIMENTAL_SCOUT = true
|
|
||||||
return previous
|
|
||||||
}),
|
|
||||||
() => self,
|
|
||||||
(previous) =>
|
|
||||||
Effect.sync(() => {
|
|
||||||
Flag.OPENCODE_EXPERIMENTAL_SCOUT = previous
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
const githubBase = <A, E, R>(url: string, self: Effect.Effect<A, E, R>) =>
|
const githubBase = <A, E, R>(url: string, self: Effect.Effect<A, E, R>) =>
|
||||||
Effect.acquireUseRelease(
|
Effect.acquireUseRelease(
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
@@ -260,44 +259,42 @@ describe("tool.read external_directory permission", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("does not ask for external_directory permission when reading configured references", () =>
|
scout.live("does not ask for external_directory permission when reading configured references", () =>
|
||||||
experimentalScout(
|
Effect.gen(function* () {
|
||||||
Effect.gen(function* () {
|
const fs = yield* AppFileSystem.Service
|
||||||
const fs = yield* AppFileSystem.Service
|
const cache = path.join(Global.Path.repos, "github.com", "opencode-read-reference", "repo")
|
||||||
const cache = path.join(Global.Path.repos, "github.com", "opencode-read-reference", "repo")
|
yield* fs.remove(cache, { recursive: true }).pipe(Effect.ignore)
|
||||||
yield* fs.remove(cache, { recursive: true }).pipe(Effect.ignore)
|
yield* Effect.addFinalizer(() => fs.remove(cache, { recursive: true }).pipe(Effect.ignore))
|
||||||
yield* Effect.addFinalizer(() => fs.remove(cache, { recursive: true }).pipe(Effect.ignore))
|
|
||||||
|
|
||||||
const source = yield* tmpdirScoped({ git: true })
|
const source = yield* tmpdirScoped({ git: true })
|
||||||
const remoteRoot = yield* tmpdirScoped()
|
const remoteRoot = yield* tmpdirScoped()
|
||||||
const remoteDir = path.join(remoteRoot, "opencode-read-reference")
|
const remoteDir = path.join(remoteRoot, "opencode-read-reference")
|
||||||
const remoteRepo = path.join(remoteDir, "repo.git")
|
const remoteRepo = path.join(remoteDir, "repo.git")
|
||||||
yield* put(path.join(source, "notes.md"), "reference notes")
|
yield* put(path.join(source, "notes.md"), "reference notes")
|
||||||
yield* git(source, ["add", "."])
|
yield* git(source, ["add", "."])
|
||||||
yield* git(source, ["commit", "-m", "add notes"])
|
yield* git(source, ["commit", "-m", "add notes"])
|
||||||
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
|
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
|
||||||
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
|
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
|
||||||
|
|
||||||
const dir = yield* tmpdirScoped({
|
const dir = yield* tmpdirScoped({
|
||||||
git: true,
|
git: true,
|
||||||
config: {
|
config: {
|
||||||
reference: {
|
reference: {
|
||||||
docs: "opencode-read-reference/repo",
|
docs: "opencode-read-reference/repo",
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const { items, next } = asks()
|
const { items, next } = asks()
|
||||||
const result = yield* githubBase(
|
const result = yield* githubBase(
|
||||||
`file://${remoteRoot}/`,
|
`file://${remoteRoot}/`,
|
||||||
exec(dir, { filePath: path.join(cache, "notes.md") }, next),
|
exec(dir, { filePath: path.join(cache, "notes.md") }, next),
|
||||||
)
|
)
|
||||||
const ext = items.find((item) => item.permission === "external_directory")
|
const ext = items.find((item) => item.permission === "external_directory")
|
||||||
|
|
||||||
expect(result.output).toContain("reference notes")
|
expect(result.output).toContain("reference notes")
|
||||||
expect(ext).toBeUndefined()
|
expect(ext).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user