mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-08 01:52:33 +00:00
Normalize instance lifecycle wiring (#25501)
This commit is contained in:
parent
a6464062b7
commit
7d91d3b1ed
71 changed files with 852 additions and 936 deletions
|
|
@ -1,8 +1,9 @@
|
|||
import { Instance } from "../project/instance"
|
||||
import { InstanceRuntime } from "../project/instance-runtime"
|
||||
import { WithInstance } from "../project/with-instance"
|
||||
|
||||
export async function bootstrap<T>(directory: string, cb: () => Promise<T>) {
|
||||
return Instance.provide({
|
||||
return WithInstance.provide({
|
||||
directory,
|
||||
fn: async () => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import fs from "fs/promises"
|
|||
import { Filesystem } from "@/util/filesystem"
|
||||
import matter from "gray-matter"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { WithInstance } from "../../project/with-instance"
|
||||
import { EOL } from "os"
|
||||
import type { Argv } from "yargs"
|
||||
|
||||
|
|
@ -61,7 +62,7 @@ const AgentCreateCommand = cmd({
|
|||
describe: "model to use in the format of provider/model",
|
||||
}),
|
||||
async handler(args) {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: process.cwd(),
|
||||
async fn() {
|
||||
const cliPath = args.path
|
||||
|
|
@ -236,7 +237,7 @@ const AgentListCommand = cmd({
|
|||
command: "list",
|
||||
describe: "list all available agents",
|
||||
async handler() {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: process.cwd(),
|
||||
async fn() {
|
||||
const agents = await AppRuntime.runPromise(Agent.Service.use((svc) => svc.list()))
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { UI } from "../ui"
|
|||
import { cmd } from "./cmd"
|
||||
import { ModelsDev } from "@/provider/models"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { WithInstance } from "@/project/with-instance"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { SessionShare } from "@/share/session"
|
||||
import { Session } from "@/session/session"
|
||||
|
|
@ -203,7 +204,7 @@ export const GithubInstallCommand = cmd({
|
|||
command: "install",
|
||||
describe: "install the GitHub agent",
|
||||
async handler() {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: process.cwd(),
|
||||
async fn() {
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { McpOAuthProvider } from "../../mcp/oauth-provider"
|
|||
import { Config } from "@/config/config"
|
||||
import { ConfigMCP } from "../../config/mcp"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { WithInstance } from "../../project/with-instance"
|
||||
import { Installation } from "../../installation"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import path from "path"
|
||||
|
|
@ -114,7 +115,7 @@ export const McpListCommand = cmd({
|
|||
aliases: ["ls"],
|
||||
describe: "list MCP servers and their status",
|
||||
async handler() {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: process.cwd(),
|
||||
async fn() {
|
||||
UI.empty()
|
||||
|
|
@ -186,7 +187,7 @@ export const McpAuthCommand = cmd({
|
|||
})
|
||||
.command(McpAuthListCommand),
|
||||
async handler(args) {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: process.cwd(),
|
||||
async fn() {
|
||||
UI.empty()
|
||||
|
|
@ -318,7 +319,7 @@ export const McpAuthListCommand = cmd({
|
|||
aliases: ["ls"],
|
||||
describe: "list OAuth-capable MCP servers and their auth status",
|
||||
async handler() {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: process.cwd(),
|
||||
async fn() {
|
||||
UI.empty()
|
||||
|
|
@ -357,7 +358,7 @@ export const McpLogoutCommand = cmd({
|
|||
type: "string",
|
||||
}),
|
||||
async handler(args) {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: process.cwd(),
|
||||
async fn() {
|
||||
UI.empty()
|
||||
|
|
@ -448,7 +449,7 @@ export const McpAddCommand = cmd({
|
|||
command: "add",
|
||||
describe: "add an MCP server",
|
||||
async handler() {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: process.cwd(),
|
||||
async fn() {
|
||||
UI.empty()
|
||||
|
|
@ -618,7 +619,7 @@ export const McpDebugCommand = cmd({
|
|||
demandOption: true,
|
||||
}),
|
||||
async handler(args) {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: process.cwd(),
|
||||
async fn() {
|
||||
UI.empty()
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import os from "os"
|
|||
import { Config } from "@/config/config"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Plugin } from "../../plugin"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { WithInstance } from "../../project/with-instance"
|
||||
import type { Hooks } from "@opencode-ai/plugin"
|
||||
import { Process } from "@/util/process"
|
||||
import { text } from "node:stream/consumers"
|
||||
|
|
@ -303,7 +303,7 @@ export const ProvidersLoginCommand = cmd({
|
|||
type: "string",
|
||||
}),
|
||||
async handler(args) {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: process.cwd(),
|
||||
async fn() {
|
||||
UI.empty()
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { TuiConfig } from "@/cli/cmd/tui/config/tui"
|
|||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { errorData, errorMessage } from "@/util/error"
|
||||
import { isRecord } from "@/util/record"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { WithInstance } from "@/project/with-instance"
|
||||
import {
|
||||
readPackageThemes,
|
||||
readPluginId,
|
||||
|
|
@ -790,7 +790,7 @@ async function addPluginBySpec(state: RuntimeState | undefined, raw: string) {
|
|||
state.pending.delete(spec)
|
||||
return true
|
||||
}
|
||||
const ready = await Instance.provide({
|
||||
const ready = await WithInstance.provide({
|
||||
directory: state.directory,
|
||||
fn: () => resolveExternalPlugins([cfg], () => TuiConfig.waitForDependencies()),
|
||||
}).catch((error) => {
|
||||
|
|
@ -986,7 +986,7 @@ async function load(input: { api: Api; config: TuiConfig.Info }) {
|
|||
}
|
||||
runtime = next
|
||||
try {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: cwd,
|
||||
fn: async () => {
|
||||
const records = Flag.OPENCODE_PURE ? [] : (config.plugin_origins ?? [])
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { Installation } from "@/installation"
|
||||
import { Server } from "@/server/server"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { InstanceRuntime } from "@/project/instance-runtime"
|
||||
import { WithInstance } from "@/project/with-instance"
|
||||
import { Rpc } from "@/util/rpc"
|
||||
import { upgrade } from "@/cli/upgrade"
|
||||
import { Config } from "@/config/config"
|
||||
|
|
@ -77,7 +77,7 @@ export const rpc = {
|
|||
return { url: server.url.toString() }
|
||||
},
|
||||
async checkUpgrade(input: { directory: string }) {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: input.directory,
|
||||
fn: async () => {
|
||||
await upgrade().catch(() => {})
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ import { Command } from "@/command"
|
|||
import { Truncate } from "@/tool/truncate"
|
||||
import { ToolRegistry } from "@/tool/registry"
|
||||
import { Format } from "@/format"
|
||||
import { InstanceRuntime } from "@/project/instance-runtime"
|
||||
import { InstanceLayer } from "@/project/instance-layer"
|
||||
import { Project } from "@/project/project"
|
||||
import { Vcs } from "@/project/vcs"
|
||||
import { Workspace } from "@/control-plane/workspace"
|
||||
|
|
@ -93,17 +93,16 @@ export const AppLayer = Layer.mergeAll(
|
|||
Truncate.defaultLayer,
|
||||
ToolRegistry.defaultLayer,
|
||||
Format.defaultLayer,
|
||||
InstanceRuntime.layer,
|
||||
Project.defaultLayer,
|
||||
Vcs.defaultLayer,
|
||||
Workspace.defaultLayer,
|
||||
Worktree.defaultLayer,
|
||||
Worktree.appLayer,
|
||||
Pty.defaultLayer,
|
||||
Installation.defaultLayer,
|
||||
ShareNext.defaultLayer,
|
||||
SessionShare.defaultLayer,
|
||||
SyncEvent.defaultLayer,
|
||||
).pipe(Layer.provideMerge(Observability.layer))
|
||||
).pipe(Layer.provideMerge(InstanceLayer.layer), Layer.provideMerge(Observability.layer))
|
||||
|
||||
const rt = ManagedRuntime.make(AppLayer, { memoMap })
|
||||
type Runtime = Pick<typeof rt, "runSync" | "runPromise" | "runPromiseExit" | "runFork" | "runCallback" | "dispose">
|
||||
|
|
|
|||
11
packages/opencode/src/project/instance-layer.ts
Normal file
11
packages/opencode/src/project/instance-layer.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Effect, Layer } from "effect"
|
||||
import { InstanceStore } from "./instance-store"
|
||||
|
||||
export const layer = Layer.unwrap(
|
||||
Effect.promise(async () => {
|
||||
const { InstanceBootstrap } = await import("./bootstrap")
|
||||
return InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer))
|
||||
}),
|
||||
)
|
||||
|
||||
export * as InstanceLayer from "./instance-layer"
|
||||
|
|
@ -1,27 +1,16 @@
|
|||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { type InstanceContext } from "./instance-context"
|
||||
import { InstanceStore, type LoadInput } from "./instance-store"
|
||||
import { Effect, Layer } from "effect"
|
||||
|
||||
// Production InstanceStore wiring plus a bridge for Promise/ALS callers that
|
||||
// cannot yet yield InstanceStore.Service. This keeps InstanceStore itself
|
||||
// low-level while still giving legacy Hono and CLI paths the production
|
||||
// bootstrap implementation. Delete the Promise helpers once those callers are
|
||||
// migrated to Effect boundaries that provide InstanceStore directly.
|
||||
// Keep the bootstrap implementation import lazy: Instance is imported broadly,
|
||||
// and importing the app bootstrap graph at module load can trigger ESM cycles.
|
||||
export const layer = Layer.unwrap(
|
||||
Effect.promise(async () => {
|
||||
const { InstanceBootstrap } = await import("./bootstrap")
|
||||
return InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer))
|
||||
}),
|
||||
)
|
||||
// Bridge for Promise/ALS callers that cannot yet yield InstanceStore.Service.
|
||||
// Delete this module once those callers are migrated to Effect boundaries that
|
||||
// provide InstanceStore directly.
|
||||
|
||||
const runtime = makeRuntime(InstanceStore.Service, layer)
|
||||
|
||||
export const load = (input: LoadInput) => runtime.runPromise((store) => store.load(input))
|
||||
export const disposeInstance = (ctx: InstanceContext) => runtime.runPromise((store) => store.dispose(ctx))
|
||||
export const disposeAllInstances = () => runtime.runPromise((store) => store.disposeAll())
|
||||
export const reloadInstance = (input: LoadInput) => runtime.runPromise((store) => store.reload(input))
|
||||
export const load = (input: LoadInput) => AppRuntime.runPromise(InstanceStore.Service.use((store) => store.load(input)))
|
||||
export const disposeInstance = (ctx: InstanceContext) =>
|
||||
AppRuntime.runPromise(InstanceStore.Service.use((store) => store.dispose(ctx)))
|
||||
export const disposeAllInstances = () => AppRuntime.runPromise(InstanceStore.Service.use((store) => store.disposeAll()))
|
||||
export const reloadInstance = (input: LoadInput) =>
|
||||
AppRuntime.runPromise(InstanceStore.Service.use((store) => store.reload(input)))
|
||||
|
||||
export * as InstanceRuntime from "./instance-runtime"
|
||||
|
|
|
|||
|
|
@ -8,26 +8,18 @@ import { type InstanceContext } from "./instance-context"
|
|||
import { InstanceBootstrap } from "./bootstrap-service"
|
||||
import * as Project from "./project"
|
||||
|
||||
export interface LoadInput<R = never> {
|
||||
export interface LoadInput {
|
||||
directory: string
|
||||
/**
|
||||
* Additional setup to run after the default InstanceBootstrap.
|
||||
* Mainly used by tests for env-var setup or file writes that need the instance ALS context.
|
||||
*/
|
||||
init?: Effect.Effect<void, never, R>
|
||||
worktree?: string
|
||||
project?: Project.Info
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly load: <R = never>(input: LoadInput<R>) => Effect.Effect<InstanceContext, never, R>
|
||||
readonly reload: <R = never>(input: LoadInput<R>) => Effect.Effect<InstanceContext, never, R>
|
||||
readonly load: (input: LoadInput) => Effect.Effect<InstanceContext>
|
||||
readonly reload: (input: LoadInput) => Effect.Effect<InstanceContext>
|
||||
readonly dispose: (ctx: InstanceContext) => Effect.Effect<void>
|
||||
readonly disposeAll: () => Effect.Effect<void>
|
||||
readonly provide: <A, E, R, R2 = never>(
|
||||
input: LoadInput<R2>,
|
||||
effect: Effect.Effect<A, E, R>,
|
||||
) => Effect.Effect<A, E, R | R2>
|
||||
readonly provide: <A, E, R>(input: LoadInput, effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/InstanceStore") {}
|
||||
|
|
@ -44,7 +36,7 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
|
|||
const scope = yield* Scope.Scope
|
||||
const cache = new Map<string, Entry>()
|
||||
|
||||
const boot = <R>(input: LoadInput<R> & { directory: string }) =>
|
||||
const boot = (input: LoadInput & { directory: string }) =>
|
||||
Effect.gen(function* () {
|
||||
const ctx: InstanceContext =
|
||||
input.project && input.worktree
|
||||
|
|
@ -61,7 +53,6 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
|
|||
})),
|
||||
)
|
||||
yield* bootstrap.run.pipe(Effect.provideService(InstanceRef, ctx))
|
||||
if (input.init) yield* input.init.pipe(Effect.provideService(InstanceRef, ctx))
|
||||
return ctx
|
||||
}).pipe(Effect.withSpan("InstanceStore.boot"))
|
||||
|
||||
|
|
@ -72,7 +63,7 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
|
|||
return true
|
||||
})
|
||||
|
||||
const completeLoad = <R>(directory: string, input: LoadInput<R>, entry: Entry) =>
|
||||
const completeLoad = (directory: string, input: LoadInput, entry: Entry) =>
|
||||
Effect.gen(function* () {
|
||||
const exit = yield* Effect.exit(boot({ ...input, directory }))
|
||||
if (Exit.isFailure(exit)) yield* removeEntry(directory, entry)
|
||||
|
|
@ -108,7 +99,7 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
|
|||
return true
|
||||
})
|
||||
|
||||
const load = <R>(input: LoadInput<R>): Effect.Effect<InstanceContext, never, R> => {
|
||||
const load = (input: LoadInput): Effect.Effect<InstanceContext> => {
|
||||
const directory = AppFileSystem.resolve(input.directory)
|
||||
return Effect.uninterruptibleMask((restore) =>
|
||||
Effect.gen(function* () {
|
||||
|
|
@ -126,7 +117,7 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
|
|||
).pipe(Effect.withSpan("InstanceStore.load"))
|
||||
}
|
||||
|
||||
const reload = <R>(input: LoadInput<R>): Effect.Effect<InstanceContext, never, R> => {
|
||||
const reload = (input: LoadInput): Effect.Effect<InstanceContext> => {
|
||||
const directory = AppFileSystem.resolve(input.directory)
|
||||
return Effect.uninterruptibleMask((restore) =>
|
||||
Effect.gen(function* () {
|
||||
|
|
@ -180,7 +171,7 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
|
|||
return yield* cachedDisposeAll
|
||||
})
|
||||
|
||||
const provide = <A, E, R, R2>(input: LoadInput<R2>, effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R | R2> =>
|
||||
const provide = <A, E, R>(input: LoadInput, effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
|
||||
load(input).pipe(Effect.flatMap((ctx) => effect.pipe(Effect.provideService(InstanceRef, ctx))))
|
||||
|
||||
yield* Effect.addFinalizer(() => disposeAll().pipe(Effect.ignore))
|
||||
|
|
|
|||
|
|
@ -1,15 +1,8 @@
|
|||
import { Effect } from "effect"
|
||||
import { context, type InstanceContext } from "./instance-context"
|
||||
import { InstanceRuntime } from "./instance-runtime"
|
||||
|
||||
export type { InstanceContext } from "./instance-context"
|
||||
export type { LoadInput } from "./instance-store"
|
||||
|
||||
export const Instance = {
|
||||
async provide<R>(input: { directory: string; init?: Effect.Effect<void>; fn: () => R }): Promise<R> {
|
||||
const ctx = await InstanceRuntime.load({ directory: input.directory, init: input.init })
|
||||
return context.provide(ctx, async () => input.fn())
|
||||
},
|
||||
get current() {
|
||||
return context.use()
|
||||
},
|
||||
|
|
|
|||
10
packages/opencode/src/project/with-instance.ts
Normal file
10
packages/opencode/src/project/with-instance.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { context } from "./instance-context"
|
||||
import { InstanceStore } from "./instance-store"
|
||||
|
||||
export async function provide<R>(input: { directory: string; fn: () => R }): Promise<R> {
|
||||
const ctx = await AppRuntime.runPromise(InstanceStore.Service.use((store) => store.load({ directory: input.directory })))
|
||||
return context.provide(ctx, () => input.fn())
|
||||
}
|
||||
|
||||
export * as WithInstance from "./with-instance"
|
||||
|
|
@ -6,7 +6,11 @@ import { InstanceStore } from "@/project/instance-store"
|
|||
import { Provider } from "@/provider/provider"
|
||||
import { errors } from "../../error"
|
||||
import { lazy } from "@/util/lazy"
|
||||
import { jsonRequest } from "./trace"
|
||||
import { jsonRequest, runRequest } from "./trace"
|
||||
import { Effect } from "effect"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
|
||||
const log = Log.create({ service: "server.config" })
|
||||
|
||||
export const ConfigRoutes = lazy(() =>
|
||||
new Hono()
|
||||
|
|
@ -52,15 +56,28 @@ export const ConfigRoutes = lazy(() =>
|
|||
},
|
||||
}),
|
||||
validator("json", Config.Info.zod),
|
||||
async (c) =>
|
||||
jsonRequest("ConfigRoutes.update", c, function* () {
|
||||
const config = c.req.valid("json")
|
||||
const cfg = yield* Config.Service
|
||||
const store = yield* InstanceStore.Service
|
||||
yield* cfg.update(config)
|
||||
yield* store.dispose(yield* InstanceState.context)
|
||||
return config
|
||||
}),
|
||||
async (c) => {
|
||||
const result = await runRequest(
|
||||
"ConfigRoutes.update",
|
||||
c,
|
||||
Effect.gen(function* () {
|
||||
const config = c.req.valid("json")
|
||||
const cfg = yield* Config.Service
|
||||
yield* cfg.update(config)
|
||||
return { config, ctx: yield* InstanceState.context }
|
||||
}),
|
||||
)
|
||||
const response = c.json(result.config)
|
||||
void runRequest(
|
||||
"ConfigRoutes.update.dispose",
|
||||
c,
|
||||
InstanceStore.Service.use((store) => store.dispose(result.ctx)).pipe(
|
||||
Effect.uninterruptible,
|
||||
Effect.catchCause((cause) => Effect.sync(() => log.warn("instance disposal failed", { cause }))),
|
||||
),
|
||||
)
|
||||
return response
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/providers",
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { LSP } from "@/lsp/lsp"
|
|||
import { MCP } from "@/mcp"
|
||||
import { Permission } from "@/permission"
|
||||
import { Installation } from "@/installation"
|
||||
import { InstanceRuntime } from "@/project/instance-runtime"
|
||||
import { InstanceLayer } from "@/project/instance-layer"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { Project } from "@/project/project"
|
||||
import { ProviderAuth } from "@/provider/auth"
|
||||
|
|
@ -152,7 +152,6 @@ export function createRoutes(corsOptions?: CorsOptions) {
|
|||
Format.defaultLayer,
|
||||
LSP.defaultLayer,
|
||||
Installation.defaultLayer,
|
||||
InstanceRuntime.layer,
|
||||
MCP.defaultLayer,
|
||||
ModelsDev.defaultLayer,
|
||||
Permission.defaultLayer,
|
||||
|
|
@ -179,12 +178,13 @@ export function createRoutes(corsOptions?: CorsOptions) {
|
|||
ToolRegistry.defaultLayer,
|
||||
Vcs.defaultLayer,
|
||||
Workspace.defaultLayer,
|
||||
Worktree.defaultLayer,
|
||||
Worktree.appLayer,
|
||||
Bus.layer,
|
||||
AppFileSystem.defaultLayer,
|
||||
FetchHttpClient.layer,
|
||||
HttpServer.layerServices,
|
||||
]),
|
||||
Layer.provideMerge(InstanceLayer.layer),
|
||||
Layer.provideMerge(Observability.layer),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { MiddlewareHandler } from "hono"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { WithInstance } from "@/project/with-instance"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||
import { WorkspaceID } from "@/control-plane/schema"
|
||||
|
|
@ -20,7 +20,7 @@ export function InstanceMiddleware(workspaceID?: WorkspaceID): MiddlewareHandler
|
|||
return WorkspaceContext.provide({
|
||||
workspaceID,
|
||||
async fn() {
|
||||
return Instance.provide({
|
||||
return WithInstance.provide({
|
||||
directory,
|
||||
async fn() {
|
||||
return next()
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { WorkspaceContext } from "@/control-plane/workspace-context"
|
|||
import { Workspace } from "@/control-plane/workspace"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { WithInstance } from "@/project/with-instance"
|
||||
import { Session } from "@/session/session"
|
||||
import { SessionID } from "@/session/schema"
|
||||
import { Effect } from "effect"
|
||||
|
|
@ -97,7 +97,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
|
|||
return WorkspaceContext.provide({
|
||||
workspaceID: WorkspaceID.make(workspaceID),
|
||||
fn: () =>
|
||||
Instance.provide({
|
||||
WithInstance.provide({
|
||||
directory: target.directory,
|
||||
async fn() {
|
||||
return next()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import z from "zod"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Instance } from "../project/instance"
|
||||
import { InstanceLayer } from "@/project/instance-layer"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import { Project } from "@/project/project"
|
||||
import { Database } from "@/storage/db"
|
||||
import { eq } from "drizzle-orm"
|
||||
|
|
@ -159,7 +160,12 @@ type GitResult = { code: number; text: string; stderr: string }
|
|||
export const layer: Layer.Layer<
|
||||
Service,
|
||||
never,
|
||||
AppFileSystem.Service | Path.Path | ChildProcessSpawner.ChildProcessSpawner | Git.Service | Project.Service
|
||||
| AppFileSystem.Service
|
||||
| Path.Path
|
||||
| ChildProcessSpawner.ChildProcessSpawner
|
||||
| Git.Service
|
||||
| Project.Service
|
||||
| InstanceStore.Service
|
||||
> = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
|
|
@ -169,6 +175,7 @@ export const layer: Layer.Layer<
|
|||
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
||||
const gitSvc = yield* Git.Service
|
||||
const project = yield* Project.Service
|
||||
const store = yield* InstanceStore.Service
|
||||
|
||||
const git = Effect.fnUntraced(
|
||||
function* (args: string[], opts?: { cwd?: string }) {
|
||||
|
|
@ -251,13 +258,10 @@ export const layer: Layer.Layer<
|
|||
return
|
||||
}
|
||||
|
||||
const booted = yield* Effect.promise(() =>
|
||||
Instance.provide({
|
||||
directory: info.directory,
|
||||
fn: () => undefined,
|
||||
})
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
const booted = yield* store.load({ directory: info.directory }).pipe(
|
||||
Effect.as(true),
|
||||
Effect.catch((error) =>
|
||||
Effect.sync(() => {
|
||||
const message = errorMessage(error)
|
||||
log.error("worktree bootstrap failed", { directory: info.directory, message })
|
||||
GlobalBus.emit("event", {
|
||||
|
|
@ -268,6 +272,7 @@ export const layer: Layer.Layer<
|
|||
})
|
||||
return false
|
||||
}),
|
||||
),
|
||||
)
|
||||
if (!booted) return
|
||||
|
||||
|
|
@ -579,7 +584,7 @@ export const layer: Layer.Layer<
|
|||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(
|
||||
export const appLayer = layer.pipe(
|
||||
Layer.provide(Git.defaultLayer),
|
||||
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||
Layer.provide(Project.defaultLayer),
|
||||
|
|
@ -587,4 +592,6 @@ export const defaultLayer = layer.pipe(
|
|||
Layer.provide(NodePath.layer),
|
||||
)
|
||||
|
||||
export const defaultLayer = appLayer.pipe(Layer.provide(InstanceLayer.layer))
|
||||
|
||||
export * as Worktree from "."
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { ACP } from "../../src/acp/agent"
|
|||
import type { AgentSideConnection } from "@agentclientprotocol/sdk"
|
||||
import type { Event, EventMessagePartUpdated, ToolStatePending, ToolStateRunning } from "@opencode-ai/sdk/v2"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
type SessionUpdateParams = Parameters<AgentSideConnection["sessionUpdate"]>[0]
|
||||
|
|
@ -262,7 +263,7 @@ function createFakeAgent() {
|
|||
describe("acp.agent event subscription", () => {
|
||||
test("routes message.part.delta by the event sessionID (no cross-session pollution)", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const { agent, controller, updates, stop } = createFakeAgent()
|
||||
|
|
@ -297,7 +298,7 @@ describe("acp.agent event subscription", () => {
|
|||
|
||||
test("does not emit user_message_chunk for live prompt parts", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const { agent, controller, sessionUpdates, stop } = createFakeAgent()
|
||||
|
|
@ -337,7 +338,7 @@ describe("acp.agent event subscription", () => {
|
|||
|
||||
test("keeps concurrent sessions isolated when message.part.delta events are interleaved", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const { agent, controller, chunks, stop } = createFakeAgent()
|
||||
|
|
@ -389,7 +390,7 @@ describe("acp.agent event subscription", () => {
|
|||
|
||||
test("does not create additional event subscriptions on repeated loadSession()", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const { agent, calls, stop } = createFakeAgent()
|
||||
|
|
@ -411,7 +412,7 @@ describe("acp.agent event subscription", () => {
|
|||
|
||||
test("permission.asked events are handled and replied", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const permissionReplies: string[] = []
|
||||
|
|
@ -450,7 +451,7 @@ describe("acp.agent event subscription", () => {
|
|||
|
||||
test("permission prompt on session A does not block message updates for session B", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const permissionReplies: string[] = []
|
||||
|
|
@ -537,7 +538,7 @@ describe("acp.agent event subscription", () => {
|
|||
|
||||
test("streams running bash output snapshots and de-dupes identical snapshots", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const { agent, controller, sessionUpdates, stop } = createFakeAgent()
|
||||
|
|
@ -571,7 +572,7 @@ describe("acp.agent event subscription", () => {
|
|||
|
||||
test("emits synthetic pending before first running update for any tool", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const { agent, controller, sessionUpdates, stop } = createFakeAgent()
|
||||
|
|
@ -616,7 +617,7 @@ describe("acp.agent event subscription", () => {
|
|||
|
||||
test("does not emit duplicate synthetic pending after replayed running tool", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const { agent, controller, sessionUpdates, stop, sdk } = createFakeAgent()
|
||||
|
|
@ -675,7 +676,7 @@ describe("acp.agent event subscription", () => {
|
|||
|
||||
test("clears bash snapshot marker on pending state", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const { agent, controller, sessionUpdates, stop } = createFakeAgent()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Effect } from "effect"
|
|||
import path from "path"
|
||||
import { disposeAllInstances, provideInstance, tmpdir } from "../fixture/fixture"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Agent } from "../../src/agent/agent"
|
||||
import { Permission } from "../../src/permission"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
|
|
@ -23,7 +24,7 @@ afterEach(async () => {
|
|||
|
||||
test("returns default native agents when no config", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const agents = await load(tmp.path, (svc) => svc.list())
|
||||
|
|
@ -41,7 +42,7 @@ test("returns default native agents when no config", async () => {
|
|||
|
||||
test("build agent has correct default properties", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||
|
|
@ -56,7 +57,7 @@ test("build agent has correct default properties", async () => {
|
|||
|
||||
test("plan agent denies edits except .opencode/plans/*", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const plan = await load(tmp.path, (svc) => svc.get("plan"))
|
||||
|
|
@ -71,7 +72,7 @@ test("plan agent denies edits except .opencode/plans/*", async () => {
|
|||
|
||||
test("explore agent denies edit and write", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const explore = await load(tmp.path, (svc) => svc.get("explore"))
|
||||
|
|
@ -87,7 +88,7 @@ test("explore agent denies edit and write", async () => {
|
|||
test("explore agent asks for external directories and allows whitelisted external paths", async () => {
|
||||
const { Truncate } = await import("../../src/tool/truncate")
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const explore = await load(tmp.path, (svc) => svc.get("explore"))
|
||||
|
|
@ -103,7 +104,7 @@ test("explore agent asks for external directories and allows whitelisted externa
|
|||
|
||||
test("general agent denies todo tools", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const general = await load(tmp.path, (svc) => svc.get("general"))
|
||||
|
|
@ -117,7 +118,7 @@ test("general agent denies todo tools", async () => {
|
|||
|
||||
test("compaction agent denies all permissions", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const compaction = await load(tmp.path, (svc) => svc.get("compaction"))
|
||||
|
|
@ -143,7 +144,7 @@ test("custom agent from config creates new agent", async () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const custom = await load(tmp.path, (svc) => svc.get("my_custom_agent"))
|
||||
|
|
@ -172,7 +173,7 @@ test("custom agent config overrides native agent properties", async () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||
|
|
@ -195,7 +196,7 @@ test("agent disable removes agent from list", async () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const explore = await load(tmp.path, (svc) => svc.get("explore"))
|
||||
|
|
@ -221,7 +222,7 @@ test("agent permission config merges with defaults", async () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||
|
|
@ -242,7 +243,7 @@ test("global permission config applies to all agents", async () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||
|
|
@ -261,7 +262,7 @@ test("agent steps/maxSteps config sets steps property", async () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||
|
|
@ -280,7 +281,7 @@ test("agent mode can be overridden", async () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const explore = await load(tmp.path, (svc) => svc.get("explore"))
|
||||
|
|
@ -297,7 +298,7 @@ test("agent name can be overridden", async () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||
|
|
@ -314,7 +315,7 @@ test("agent prompt can be set from config", async () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||
|
|
@ -334,7 +335,7 @@ test("unknown agent properties are placed into options", async () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||
|
|
@ -357,7 +358,7 @@ test("agent options merge correctly", async () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||
|
|
@ -382,7 +383,7 @@ test("multiple custom agents can be defined", async () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const agentA = await load(tmp.path, (svc) => svc.get("agent_a"))
|
||||
|
|
@ -411,7 +412,7 @@ test("Agent.list keeps the default agent first and sorts the rest by name", asyn
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const names = (await load(tmp.path, (svc) => svc.list())).map((a) => a.name)
|
||||
|
|
@ -423,7 +424,7 @@ test("Agent.list keeps the default agent first and sorts the rest by name", asyn
|
|||
|
||||
test("Agent.get returns undefined for non-existent agent", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const nonExistent = await load(tmp.path, (svc) => svc.get("does_not_exist"))
|
||||
|
|
@ -434,7 +435,7 @@ test("Agent.get returns undefined for non-existent agent", async () => {
|
|||
|
||||
test("default permission includes doom_loop and external_directory as ask", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||
|
|
@ -446,7 +447,7 @@ test("default permission includes doom_loop and external_directory as ask", asyn
|
|||
|
||||
test("webfetch is allowed by default", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||
|
|
@ -468,7 +469,7 @@ test("legacy tools config converts to permissions", async () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||
|
|
@ -490,7 +491,7 @@ test("legacy tools config maps write/edit/patch to edit permission", async () =>
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||
|
|
@ -508,7 +509,7 @@ test("Truncate.GLOB is allowed even when user denies external_directory globally
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||
|
|
@ -521,7 +522,7 @@ test("Truncate.GLOB is allowed even when user denies external_directory globally
|
|||
|
||||
test("global tmp directory children are allowed for external_directory", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||
|
|
@ -546,7 +547,7 @@ test("Truncate.GLOB is allowed even when user denies external_directory per-agen
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||
|
|
@ -569,7 +570,7 @@ test("explicit Truncate.GLOB deny is respected", async () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||
|
|
@ -601,7 +602,7 @@ description: Permission skill.
|
|||
process.env.OPENCODE_TEST_HOME = tmp.path
|
||||
|
||||
try {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||
|
|
@ -617,7 +618,7 @@ description: Permission skill.
|
|||
|
||||
test("defaultAgent returns build when no default_agent config", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const agent = await load(tmp.path, (svc) => svc.defaultAgent())
|
||||
|
|
@ -632,7 +633,7 @@ test("defaultAgent respects default_agent config set to plan", async () => {
|
|||
default_agent: "plan",
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const agent = await load(tmp.path, (svc) => svc.defaultAgent())
|
||||
|
|
@ -652,7 +653,7 @@ test("defaultAgent respects default_agent config set to custom agent with mode a
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const agent = await load(tmp.path, (svc) => svc.defaultAgent())
|
||||
|
|
@ -667,7 +668,7 @@ test("defaultAgent throws when default_agent points to subagent", async () => {
|
|||
default_agent: "explore",
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await expect(load(tmp.path, (svc) => svc.defaultAgent())).rejects.toThrow('default agent "explore" is a subagent')
|
||||
|
|
@ -681,7 +682,7 @@ test("defaultAgent throws when default_agent points to hidden agent", async () =
|
|||
default_agent: "compaction",
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await expect(load(tmp.path, (svc) => svc.defaultAgent())).rejects.toThrow('default agent "compaction" is hidden')
|
||||
|
|
@ -695,7 +696,7 @@ test("defaultAgent throws when default_agent points to non-existent agent", asyn
|
|||
default_agent: "does_not_exist",
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await expect(load(tmp.path, (svc) => svc.defaultAgent())).rejects.toThrow(
|
||||
|
|
@ -713,7 +714,7 @@ test("defaultAgent returns plan when build is disabled and default_agent not set
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const agent = await load(tmp.path, (svc) => svc.defaultAgent())
|
||||
|
|
@ -732,7 +733,7 @@ test("defaultAgent throws when all primary agents are disabled", async () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
// build and plan are disabled, no primary-capable agents remain
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { pathToFileURL } from "url"
|
|||
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||
import { Agent } from "../../src/agent/agent"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -39,7 +40,7 @@ test("plugin-registered agents appear in Agent.list", async () => {
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const agents = await AppRuntime.runPromise(Agent.Service.use((svc) => svc.list()))
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@ import { Schema } from "effect"
|
|||
import { Bus } from "../../src/bus"
|
||||
import { BusEvent } from "../../src/bus/bus-event"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
|
||||
const TestEvent = BusEvent.define("test.integration", Schema.Struct({ value: Schema.Number }))
|
||||
|
||||
function withInstance(directory: string, fn: () => Promise<void>) {
|
||||
return Instance.provide({ directory, fn })
|
||||
return WithInstance.provide({ directory, fn })
|
||||
}
|
||||
|
||||
describe("Bus integration: acquireRelease subscriber pattern", () => {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Schema } from "effect"
|
|||
import { Bus } from "../../src/bus"
|
||||
import { BusEvent } from "../../src/bus/bus-event"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
|
||||
const TestEvent = {
|
||||
|
|
@ -11,7 +12,7 @@ const TestEvent = {
|
|||
}
|
||||
|
||||
function withInstance(directory: string, fn: () => Promise<void>) {
|
||||
return Instance.provide({ directory, fn })
|
||||
return WithInstance.provide({ directory, fn })
|
||||
}
|
||||
|
||||
describe("Bus", () => {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { ConfigParse } from "../../src/config/parse"
|
|||
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
||||
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Auth } from "../../src/auth"
|
||||
import { Account } from "../../src/account/account"
|
||||
import { AccessToken, AccountID, OrgID } from "../../src/account/schema"
|
||||
|
|
@ -113,7 +114,7 @@ async function check(map: (dir: string) => string) {
|
|||
$schema: "https://opencode.ai/config.json",
|
||||
snapshot: false,
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: map(tmp.path),
|
||||
fn: async () => {
|
||||
const cfg = await load()
|
||||
|
|
@ -131,7 +132,7 @@ async function check(map: (dir: string) => string) {
|
|||
|
||||
test("loads config with defaults when no files exist", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -150,7 +151,7 @@ test("loads JSON config file", async () => {
|
|||
})
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -169,7 +170,7 @@ test("loads shell config field", async () => {
|
|||
})
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -191,7 +192,7 @@ test("updates config and preserves empty shell sentinel", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await save({ shell: "" })
|
||||
|
|
@ -269,7 +270,7 @@ test("loads formatter boolean config", async () => {
|
|||
})
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -287,7 +288,7 @@ test("loads lsp boolean config", async () => {
|
|||
})
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -324,7 +325,7 @@ test("ignores legacy tui keys in opencode config", async () => {
|
|||
})
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -349,7 +350,7 @@ test("loads JSONC config file", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -377,7 +378,7 @@ test("jsonc overrides json in the same directory", async () => {
|
|||
})
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -400,7 +401,7 @@ test("handles environment variable substitution", async () => {
|
|||
})
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -432,7 +433,7 @@ test("preserves env variables when adding $schema to config", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -529,7 +530,7 @@ test("handles file inclusion substitution", async () => {
|
|||
})
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -548,7 +549,7 @@ test("handles file inclusion with replacement tokens", async () => {
|
|||
})
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -604,7 +605,7 @@ test("handles agent configuration", async () => {
|
|||
})
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -635,7 +636,7 @@ test("treats agent variant as model-scoped setting (not provider option)", async
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -665,7 +666,7 @@ test("handles command configuration", async () => {
|
|||
})
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -690,7 +691,7 @@ test("migrates autoshare to share field", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -717,7 +718,7 @@ test("migrates mode field to agent field", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -749,7 +750,7 @@ Test agent prompt`,
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -782,7 +783,7 @@ Ordered permissions`,
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -820,7 +821,7 @@ Nested agent prompt`,
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -869,7 +870,7 @@ Nested command template`,
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -914,7 +915,7 @@ Nested command template`,
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -934,7 +935,7 @@ Nested command template`,
|
|||
|
||||
test("updates config and writes to file", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const newConfig = { model: "updated/model" }
|
||||
|
|
@ -948,7 +949,7 @@ test("updates config and writes to file", async () => {
|
|||
|
||||
test("gets config directories", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const dirs = await listDirs()
|
||||
|
|
@ -978,7 +979,7 @@ test("does not try to install dependencies in read-only OPENCODE_CONFIG_DIR", as
|
|||
process.env.OPENCODE_CONFIG_DIR = tmp.extra
|
||||
|
||||
try {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await load()
|
||||
|
|
@ -1013,7 +1014,7 @@ test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => {
|
|||
)
|
||||
|
||||
try {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await Effect.runPromise(Config.Service.use((svc) => svc.get()).pipe(Effect.scoped, Effect.provide(testLayer)))
|
||||
|
|
@ -1146,7 +1147,7 @@ Helper subagent prompt`,
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -1185,7 +1186,7 @@ test("merges instructions arrays from global and local configs", async () => {
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: path.join(tmp.path, "project"),
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -1224,7 +1225,7 @@ test("deduplicates duplicate instructions from global and local configs", async
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: path.join(tmp.path, "project"),
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -1359,7 +1360,7 @@ test("migrates legacy tools config to permissions - allow", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -1390,7 +1391,7 @@ test("migrates legacy tools config to permissions - deny", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -1420,7 +1421,7 @@ test("migrates legacy write tool to edit permission", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -1452,7 +1453,7 @@ test("managed settings override user settings", async () => {
|
|||
share: "disabled",
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -1480,7 +1481,7 @@ test("managed settings override project settings", async () => {
|
|||
disabled_providers: ["openai"],
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -1500,7 +1501,7 @@ test("missing managed settings file is not an error", async () => {
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -1527,7 +1528,7 @@ test("migrates legacy edit tool to edit permission", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -1556,7 +1557,7 @@ test("migrates legacy patch tool to edit permission", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -1588,7 +1589,7 @@ test("migrates mixed legacy tools config", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -1623,7 +1624,7 @@ test("merges legacy tools with existing permission config", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -1660,7 +1661,7 @@ test("permission config preserves user key order", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -1743,7 +1744,7 @@ test("project config can override MCP server enabled status", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -1799,7 +1800,7 @@ test("MCP config deep merges preserving base config properties", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -1850,7 +1851,7 @@ test("local .opencode config can override MCP from project config", async () =>
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -2139,7 +2140,7 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -2170,7 +2171,7 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => {
|
|||
await Filesystem.write(path.join(opencodeDir, "test-cmd.md"), "# Test Command\nThis is a test command.")
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const directories = await listDirs()
|
||||
|
|
@ -2194,7 +2195,7 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => {
|
|||
|
||||
try {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
// Should still get default config (from global or defaults)
|
||||
|
|
@ -2236,7 +2237,7 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => {
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
// The relative instruction should be skipped without error
|
||||
|
|
@ -2296,7 +2297,7 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => {
|
|||
process.env["OPENCODE_DISABLE_PROJECT_CONFIG"] = "true"
|
||||
process.env["OPENCODE_CONFIG_DIR"] = configDirTmp.path
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectTmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -2331,7 +2332,7 @@ describe("OPENCODE_CONFIG_CONTENT token substitution", () => {
|
|||
|
||||
try {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -2365,7 +2366,7 @@ describe("OPENCODE_CONFIG_CONTENT token substitution", () => {
|
|||
})
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { Database } from "@/storage/db"
|
|||
import { ProjectID } from "@/project/schema"
|
||||
import { ProjectTable } from "@/project/project.sql"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Session as SessionNs } from "@/session/session"
|
||||
import { SessionID, MessageID, PartID } from "@/session/schema"
|
||||
import { SessionTable } from "@/session/session.sql"
|
||||
|
|
@ -101,7 +102,7 @@ afterEach(async () => {
|
|||
|
||||
async function withInstance<T>(fn: (dir: string) => T | Promise<T>) {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
return Instance.provide({
|
||||
return WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: () => fn(tmp.path),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import fs from "fs/promises"
|
|||
import path from "path"
|
||||
import { File } from "../../src/file"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { provideInstance, tmpdir } from "../fixture/fixture"
|
||||
|
||||
const run = <A, E>(eff: Effect.Effect<A, E, File.Service>) =>
|
||||
|
|
@ -30,7 +31,7 @@ describe("file fsmonitor", () => {
|
|||
const before = await $`git fsmonitor--daemon status`.cwd(tmp.path).quiet().nothrow()
|
||||
expect(before.exitCode).not.toBe(0)
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await status()
|
||||
|
|
@ -55,7 +56,7 @@ describe("file fsmonitor", () => {
|
|||
const before = await $`git fsmonitor--daemon status`.cwd(tmp.path).quiet().nothrow()
|
||||
expect(before.exitCode).not.toBe(0)
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await read("tracked.txt")
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import path from "path"
|
|||
import fs from "fs/promises"
|
||||
import { File } from "../../src/file"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { disposeAllInstances, provideInstance, tmpdir } from "../fixture/fixture"
|
||||
|
||||
|
|
@ -28,7 +29,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
const filepath = path.join(tmp.path, "test.txt")
|
||||
await fs.writeFile(filepath, "Hello World", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await read("test.txt")
|
||||
|
|
@ -41,7 +42,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
test("reads with Filesystem.exists() check", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
// Non-existent file should return empty content
|
||||
|
|
@ -57,7 +58,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
const filepath = path.join(tmp.path, "test.txt")
|
||||
await fs.writeFile(filepath, " content with spaces \n\n", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await read("test.txt")
|
||||
|
|
@ -71,7 +72,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
const filepath = path.join(tmp.path, "empty.txt")
|
||||
await fs.writeFile(filepath, "", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await read("empty.txt")
|
||||
|
|
@ -86,7 +87,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
const filepath = path.join(tmp.path, "multiline.txt")
|
||||
await fs.writeFile(filepath, "line1\nline2\nline3", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await read("multiline.txt")
|
||||
|
|
@ -103,7 +104,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
const binaryContent = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
|
||||
await fs.writeFile(filepath, binaryContent)
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await read("image.png")
|
||||
|
|
@ -120,7 +121,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
const filepath = path.join(tmp.path, "binary.so")
|
||||
await fs.writeFile(filepath, Buffer.from([0x7f, 0x45, 0x4c, 0x46]), "binary")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await read("binary.so")
|
||||
|
|
@ -137,7 +138,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
const filepath = path.join(tmp.path, "test.json")
|
||||
await fs.writeFile(filepath, '{"key": "value"}', "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
expect(await Filesystem.mimeType(filepath)).toContain("application/json")
|
||||
|
|
@ -161,7 +162,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
const filepath = path.join(tmp.path, `test.${ext}`)
|
||||
await fs.writeFile(filepath, Buffer.from([0x00, 0x00, 0x00, 0x00]), "binary")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
expect(await Filesystem.mimeType(filepath)).toContain(mime)
|
||||
|
|
@ -175,7 +176,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
test("reads .gitignore via Filesystem.exists() and readText()", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const gitignorePath = path.join(tmp.path, ".gitignore")
|
||||
|
|
@ -193,7 +194,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
test("reads .ignore file similarly", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const ignorePath = path.join(tmp.path, ".ignore")
|
||||
|
|
@ -208,7 +209,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
test("handles missing .gitignore gracefully", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const gitignorePath = path.join(tmp.path, ".gitignore")
|
||||
|
|
@ -226,7 +227,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
test("reads untracked files via Filesystem.readText()", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const untrackedPath = path.join(tmp.path, "untracked.txt")
|
||||
|
|
@ -247,7 +248,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
const filepath = path.join(tmp.path, "readonly.txt")
|
||||
await fs.writeFile(filepath, "content", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const nonExistentPath = path.join(tmp.path, "does-not-exist.txt")
|
||||
|
|
@ -264,7 +265,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
test("handles errors in Filesystem.readArrayBuffer()", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const nonExistentPath = path.join(tmp.path, "does-not-exist.bin")
|
||||
|
|
@ -279,7 +280,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
const _filepath = path.join(tmp.path, "broken.png")
|
||||
// Don't create the file
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
// read() handles missing images gracefully
|
||||
|
|
@ -297,7 +298,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
const filepath = path.join(tmp.path, "test.ts")
|
||||
await fs.writeFile(filepath, "export const value = 1", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await read("test.ts")
|
||||
|
|
@ -312,7 +313,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
const filepath = path.join(tmp.path, "test.mts")
|
||||
await fs.writeFile(filepath, "export const value = 1", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await read("test.mts")
|
||||
|
|
@ -327,7 +328,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
const filepath = path.join(tmp.path, "test.sh")
|
||||
await fs.writeFile(filepath, "#!/usr/bin/env bash\necho hello", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await read("test.sh")
|
||||
|
|
@ -342,7 +343,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
const filepath = path.join(tmp.path, "Dockerfile")
|
||||
await fs.writeFile(filepath, "FROM alpine:3.20", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await read("Dockerfile")
|
||||
|
|
@ -357,7 +358,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
const filepath = path.join(tmp.path, "test.txt")
|
||||
await fs.writeFile(filepath, "simple text", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await read("test.txt")
|
||||
|
|
@ -372,7 +373,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
const filepath = path.join(tmp.path, "test.jpg")
|
||||
await fs.writeFile(filepath, Buffer.from([0xff, 0xd8, 0xff, 0xe0]), "binary")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await read("test.jpg")
|
||||
|
|
@ -387,7 +388,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
test("throws for paths outside project directory", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await expect(read("../outside.txt")).rejects.toThrow("Access denied")
|
||||
|
|
@ -398,7 +399,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
test("throws for paths outside project directory", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await expect(read("../outside.txt")).rejects.toThrow("Access denied")
|
||||
|
|
@ -416,7 +417,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
await $`git commit -m "add file"`.cwd(tmp.path).quiet()
|
||||
await fs.writeFile(filepath, "modified\nextra line\n", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await status()
|
||||
|
|
@ -433,7 +434,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
await using tmp = await tmpdir({ git: true })
|
||||
await fs.writeFile(path.join(tmp.path, "new.txt"), "line1\nline2\nline3\n", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await status()
|
||||
|
|
@ -454,7 +455,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
await $`git commit -m "add file"`.cwd(tmp.path).quiet()
|
||||
await fs.rm(filepath)
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await status()
|
||||
|
|
@ -477,7 +478,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
await fs.rm(path.join(tmp.path, "remove.txt"))
|
||||
await fs.writeFile(path.join(tmp.path, "brand-new.txt"), "hello\n", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await status()
|
||||
|
|
@ -491,7 +492,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
test("returns empty for non-git project", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await status()
|
||||
|
|
@ -503,7 +504,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
test("returns empty for clean repo", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await status()
|
||||
|
|
@ -526,7 +527,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
for (let i = 0; i < 512; i++) modified[i] = i % 256
|
||||
await fs.writeFile(filepath, modified)
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await status()
|
||||
|
|
@ -547,7 +548,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
await fs.writeFile(path.join(tmp.path, "file.txt"), "content", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "subdir", "nested.txt"), "nested", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const nodes = await list()
|
||||
|
|
@ -571,7 +572,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
await fs.writeFile(path.join(tmp.path, "zz.txt"), "", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "aa.txt"), "", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const nodes = await list()
|
||||
|
|
@ -596,7 +597,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
await fs.writeFile(path.join(tmp.path, ".DS_Store"), "", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "visible.txt"), "", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const nodes = await list()
|
||||
|
|
@ -615,7 +616,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
await fs.writeFile(path.join(tmp.path, "main.ts"), "code", "utf-8")
|
||||
await fs.mkdir(path.join(tmp.path, "build"))
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const nodes = await list()
|
||||
|
|
@ -635,7 +636,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
await fs.writeFile(path.join(tmp.path, "sub", "a.txt"), "", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "sub", "b.txt"), "", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const nodes = await list("sub")
|
||||
|
|
@ -650,7 +651,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
test("throws for paths outside project directory", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await expect(list("../outside")).rejects.toThrow("Access denied")
|
||||
|
|
@ -662,7 +663,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
await using tmp = await tmpdir()
|
||||
await fs.writeFile(path.join(tmp.path, "file.txt"), "hi", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const nodes = await list()
|
||||
|
|
@ -692,7 +693,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
test("empty query returns files", async () => {
|
||||
await using tmp = await setupSearchableRepo()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await init()
|
||||
|
|
@ -706,7 +707,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
test("search works before explicit init", async () => {
|
||||
await using tmp = await setupSearchableRepo()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await search({ query: "main", type: "file" })
|
||||
|
|
@ -718,7 +719,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
test("empty query returns dirs sorted with hidden last", async () => {
|
||||
await using tmp = await setupSearchableRepo()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await init()
|
||||
|
|
@ -738,7 +739,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
test("fuzzy matches file names", async () => {
|
||||
await using tmp = await setupSearchableRepo()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await init()
|
||||
|
|
@ -752,7 +753,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
test("type filter returns only files", async () => {
|
||||
await using tmp = await setupSearchableRepo()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await init()
|
||||
|
|
@ -769,7 +770,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
test("type filter returns only directories", async () => {
|
||||
await using tmp = await setupSearchableRepo()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await init()
|
||||
|
|
@ -786,7 +787,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
test("respects limit", async () => {
|
||||
await using tmp = await setupSearchableRepo()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await init()
|
||||
|
|
@ -800,7 +801,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
test("query starting with dot prefers hidden files", async () => {
|
||||
await using tmp = await setupSearchableRepo()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await init()
|
||||
|
|
@ -815,7 +816,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
test("search refreshes after init when files change", async () => {
|
||||
await using tmp = await setupSearchableRepo()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await init()
|
||||
|
|
@ -839,7 +840,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
await $`git commit -m "add file"`.cwd(tmp.path).quiet()
|
||||
await fs.writeFile(filepath, "modified content\n", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await read("file.txt")
|
||||
|
|
@ -863,7 +864,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
await fs.writeFile(filepath, "after\n", "utf-8")
|
||||
await $`git add .`.cwd(tmp.path).quiet()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await read("staged.txt")
|
||||
|
|
@ -880,7 +881,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
await $`git add .`.cwd(tmp.path).quiet()
|
||||
await $`git commit -m "add file"`.cwd(tmp.path).quiet()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await read("clean.txt")
|
||||
|
|
@ -900,7 +901,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
await fs.writeFile(path.join(one.path, "a.ts"), "one", "utf-8")
|
||||
await fs.writeFile(path.join(two.path, "b.ts"), "two", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: one.path,
|
||||
fn: async () => {
|
||||
await init()
|
||||
|
|
@ -911,7 +912,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: two.path,
|
||||
fn: async () => {
|
||||
await init()
|
||||
|
|
@ -927,7 +928,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
await using tmp = await tmpdir({ git: true })
|
||||
await fs.writeFile(path.join(tmp.path, "before.ts"), "before", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await init()
|
||||
|
|
@ -941,7 +942,7 @@ describe("file/index Filesystem patterns", () => {
|
|||
await fs.writeFile(path.join(tmp.path, "after.ts"), "after", "utf-8")
|
||||
await fs.rm(path.join(tmp.path, "before.ts"))
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await init()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import fs from "fs/promises"
|
|||
import { Filesystem } from "@/util/filesystem"
|
||||
import { File } from "../../src/file"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { containsPath } from "../../src/project/instance-context"
|
||||
import { provideInstance, tmpdir } from "../fixture/fixture"
|
||||
|
||||
|
|
@ -55,7 +56,7 @@ describe("File.read path traversal protection", () => {
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await expect(read("../../../etc/passwd")).rejects.toThrow("Access denied: path escapes project directory")
|
||||
|
|
@ -66,7 +67,7 @@ describe("File.read path traversal protection", () => {
|
|||
test("rejects deeply nested traversal", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await expect(read("src/nested/../../../../../../../etc/passwd")).rejects.toThrow(
|
||||
|
|
@ -83,7 +84,7 @@ describe("File.read path traversal protection", () => {
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await read("valid.txt")
|
||||
|
|
@ -97,7 +98,7 @@ describe("File.list path traversal protection", () => {
|
|||
test("rejects ../ traversal attempting to list /etc", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await expect(list("../../../etc")).rejects.toThrow("Access denied: path escapes project directory")
|
||||
|
|
@ -112,7 +113,7 @@ describe("File.list path traversal protection", () => {
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await list("subdir")
|
||||
|
|
@ -126,7 +127,7 @@ describe("containsPath", () => {
|
|||
test("returns true for path inside directory", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: () => {
|
||||
expect(containsPath(path.join(tmp.path, "foo.txt"), Instance.current)).toBe(true)
|
||||
|
|
@ -140,7 +141,7 @@ describe("containsPath", () => {
|
|||
const subdir = path.join(tmp.path, "packages", "lib")
|
||||
await fs.mkdir(subdir, { recursive: true })
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: subdir,
|
||||
fn: () => {
|
||||
// .opencode at worktree root, but we're running from packages/lib
|
||||
|
|
@ -156,7 +157,7 @@ describe("containsPath", () => {
|
|||
test("returns false for path outside both directory and worktree", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: () => {
|
||||
expect(containsPath("/etc/passwd", Instance.current)).toBe(false)
|
||||
|
|
@ -168,7 +169,7 @@ describe("containsPath", () => {
|
|||
test("returns false for path with .. escaping worktree", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: () => {
|
||||
expect(containsPath(path.join(tmp.path, "..", "escape.txt"), Instance.current)).toBe(false)
|
||||
|
|
@ -179,7 +180,7 @@ describe("containsPath", () => {
|
|||
test("handles directory === worktree (running from repo root)", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: () => {
|
||||
expect(Instance.directory).toBe(Instance.worktree)
|
||||
|
|
@ -192,7 +193,7 @@ describe("containsPath", () => {
|
|||
test("non-git project does not allow arbitrary paths via worktree='/'", async () => {
|
||||
await using tmp = await tmpdir() // no git: true
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: () => {
|
||||
// worktree is "/" for non-git projects, but containsPath should NOT allow all paths
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { Config } from "@/config/config"
|
|||
import { FileWatcher } from "../../src/file/watcher"
|
||||
import { Git } from "../../src/git"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
|
||||
// Native @parcel/watcher bindings aren't reliably available in CI (missing on Linux, flaky on Windows)
|
||||
const describeWatcher = FileWatcher.hasNativeBinding() && !process.env.CI ? describe : describe.skip
|
||||
|
|
@ -28,7 +29,7 @@ type WatcherEvent = { file: string; event: "add" | "change" | "unlink" }
|
|||
|
||||
/** Run `body` with a live FileWatcher service. */
|
||||
function withWatcher<E>(directory: string, body: Effect.Effect<void, E>) {
|
||||
return Instance.provide({
|
||||
return WithInstance.provide({
|
||||
directory,
|
||||
fn: async () => {
|
||||
const layer: Layer.Layer<FileWatcher.Service, never, never> = FileWatcher.layer.pipe(
|
||||
|
|
@ -193,7 +194,7 @@ describeWatcher("FileWatcher", () => {
|
|||
await withWatcher(tmp.path, Effect.void)
|
||||
|
||||
// Now write a file — no watcher should be listening
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: () =>
|
||||
Effect.runPromise(
|
||||
|
|
|
|||
|
|
@ -25,8 +25,9 @@ const runTestInstanceStore = <A>(fn: (store: InstanceStore.Interface) => Effect.
|
|||
testInstanceRuntime.runPromise(InstanceStore.Service.use(fn))
|
||||
|
||||
export async function provideTestInstance<R>(input: { directory: string; init?: Effect.Effect<void>; fn: () => R }) {
|
||||
const ctx = await runTestInstanceStore((store) => store.load({ directory: input.directory, init: input.init }))
|
||||
const ctx = await runTestInstanceStore((store) => store.load({ directory: input.directory }))
|
||||
try {
|
||||
if (input.init) await testInstanceRuntime.runPromise(input.init.pipe(Effect.provideService(InstanceRef, ctx)))
|
||||
return await Instance.restore(ctx, () => input.fn())
|
||||
} finally {
|
||||
await runTestInstanceStore((store) => store.dispose(ctx))
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { tmpdir } from "../fixture/fixture"
|
|||
import { LSPClient } from "@/lsp/client"
|
||||
import * as LSPServer from "@/lsp/server"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
|
||||
function spawnFakeServer() {
|
||||
|
|
@ -25,7 +26,7 @@ describe("LSPClient interop", () => {
|
|||
test("handles workspace/workspaceFolders request", async () => {
|
||||
const handle = spawnFakeServer() as any
|
||||
|
||||
const client = await Instance.provide({
|
||||
const client = await WithInstance.provide({
|
||||
directory: process.cwd(),
|
||||
fn: () =>
|
||||
LSPClient.create({
|
||||
|
|
@ -48,7 +49,7 @@ describe("LSPClient interop", () => {
|
|||
test("handles client/registerCapability request", async () => {
|
||||
const handle = spawnFakeServer() as any
|
||||
|
||||
const client = await Instance.provide({
|
||||
const client = await WithInstance.provide({
|
||||
directory: process.cwd(),
|
||||
fn: () =>
|
||||
LSPClient.create({
|
||||
|
|
@ -71,7 +72,7 @@ describe("LSPClient interop", () => {
|
|||
test("handles client/unregisterCapability request", async () => {
|
||||
const handle = spawnFakeServer() as any
|
||||
|
||||
const client = await Instance.provide({
|
||||
const client = await WithInstance.provide({
|
||||
directory: process.cwd(),
|
||||
fn: () =>
|
||||
LSPClient.create({
|
||||
|
|
@ -94,7 +95,7 @@ describe("LSPClient interop", () => {
|
|||
test("initialize does not overclaim unsupported diagnostics capabilities", async () => {
|
||||
const handle = spawnFakeServer() as any
|
||||
|
||||
const client = await Instance.provide({
|
||||
const client = await WithInstance.provide({
|
||||
directory: process.cwd(),
|
||||
fn: () =>
|
||||
LSPClient.create({
|
||||
|
|
@ -121,7 +122,7 @@ describe("LSPClient interop", () => {
|
|||
gamma: true,
|
||||
}
|
||||
|
||||
const client = await Instance.provide({
|
||||
const client = await WithInstance.provide({
|
||||
directory: process.cwd(),
|
||||
fn: () =>
|
||||
LSPClient.create({
|
||||
|
|
@ -150,7 +151,7 @@ describe("LSPClient interop", () => {
|
|||
const file = path.join(tmp.path, "client.ts")
|
||||
await Bun.write(file, "first\n")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const client = await LSPClient.create({
|
||||
|
|
@ -193,7 +194,7 @@ describe("LSPClient interop", () => {
|
|||
const file = path.join(tmp.path, "client.ts")
|
||||
await Bun.write(file, "const x = 1\n")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const client = await LSPClient.create({
|
||||
|
|
@ -239,7 +240,7 @@ describe("LSPClient interop", () => {
|
|||
const file = path.join(tmp.path, "client.ts")
|
||||
await Bun.write(file, "const x = 1\n")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const client = await LSPClient.create({
|
||||
|
|
@ -286,7 +287,7 @@ describe("LSPClient interop", () => {
|
|||
const file = path.join(tmp.path, "client.cs")
|
||||
await Bun.write(file, "class C {}\n")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const client = await LSPClient.create({
|
||||
|
|
@ -334,7 +335,7 @@ describe("LSPClient interop", () => {
|
|||
const file = path.join(tmp.path, "client.cs")
|
||||
await Bun.write(file, "class C {}\n")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const client = await LSPClient.create({
|
||||
|
|
@ -387,7 +388,7 @@ describe("LSPClient interop", () => {
|
|||
await Bun.write(file, "class C {}\n")
|
||||
await Bun.write(related, "class D {}\n")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const client = await LSPClient.create({
|
||||
|
|
@ -451,7 +452,7 @@ describe("LSPClient interop", () => {
|
|||
const file = path.join(tmp.path, "client.cs")
|
||||
await Bun.write(file, "class C {}\n")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const client = await LSPClient.create({
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ beforeEach(() => {
|
|||
const { MCP } = await import("../../src/mcp/index")
|
||||
const { AppRuntime } = await import("../../src/effect/app-runtime")
|
||||
const { Instance } = await import("../../src/project/instance")
|
||||
const { WithInstance } = await import("../../src/project/with-instance")
|
||||
const { tmpdir } = await import("../fixture/fixture")
|
||||
const service = MCP.Service as unknown as Effect.Effect<MCPNS.Interface, never, never>
|
||||
|
||||
|
|
@ -73,7 +74,7 @@ test("headers are passed to transports when oauth is enabled (default)", async (
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
// Trigger MCP initialization - it will fail to connect but we can check the transport options
|
||||
|
|
@ -112,7 +113,7 @@ test("headers are passed to transports when oauth is enabled (default)", async (
|
|||
test("headers are passed to transports when oauth is explicitly disabled", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
transportCalls.length = 0
|
||||
|
|
@ -150,7 +151,7 @@ test("headers are passed to transports when oauth is explicitly disabled", async
|
|||
test("no requestInit when headers are not provided", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
transportCalls.length = 0
|
||||
|
|
|
|||
|
|
@ -172,6 +172,7 @@ beforeEach(() => {
|
|||
// Import after mocks
|
||||
const { MCP } = await import("../../src/mcp/index")
|
||||
const { Instance } = await import("../../src/project/instance")
|
||||
const { WithInstance } = await import("../../src/project/with-instance")
|
||||
const { tmpdir } = await import("../fixture/fixture")
|
||||
|
||||
// --- Helper ---
|
||||
|
|
@ -193,7 +194,7 @@ function withInstance(
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await Effect.runPromise(MCP.Service.use(fn).pipe(Effect.provide(MCP.defaultLayer)))
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ beforeEach(() => {
|
|||
// Import modules after mocking
|
||||
const { MCP } = await import("../../src/mcp/index")
|
||||
const { Instance } = await import("../../src/project/instance")
|
||||
const { WithInstance } = await import("../../src/project/with-instance")
|
||||
const { tmpdir } = await import("../fixture/fixture")
|
||||
|
||||
test("first connect to OAuth server shows needs_auth instead of failed", async () => {
|
||||
|
|
@ -132,7 +133,7 @@ test("first connect to OAuth server shows needs_auth instead of failed", async (
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await Effect.runPromise(
|
||||
|
|
@ -162,7 +163,7 @@ test("state() generates a new state when none is saved", async () => {
|
|||
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const auth = await Effect.runPromise(
|
||||
|
|
@ -203,7 +204,7 @@ test("state() returns existing state when one is saved", async () => {
|
|||
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const auth = await Effect.runPromise(
|
||||
|
|
@ -252,7 +253,7 @@ test("authenticate() stores a connected client when auth completes without redir
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await Effect.runPromise(
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ const { AppRuntime } = await import("../../src/effect/app-runtime")
|
|||
const { Bus } = await import("../../src/bus")
|
||||
const { McpOAuthCallback } = await import("../../src/mcp/oauth-callback")
|
||||
const { Instance } = await import("../../src/project/instance")
|
||||
const { WithInstance } = await import("../../src/project/with-instance")
|
||||
const { tmpdir } = await import("../fixture/fixture")
|
||||
const service = MCP.Service as unknown as Effect.Effect<MCPNS.Interface, never, never>
|
||||
|
||||
|
|
@ -127,7 +128,7 @@ test("BrowserOpenFailed event is published when open() throws", async () => {
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
openShouldFail = true
|
||||
|
|
@ -183,7 +184,7 @@ test("BrowserOpenFailed event is NOT published when open() succeeds", async () =
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
openShouldFail = false
|
||||
|
|
@ -237,7 +238,7 @@ test("open() is called with the authorization URL", async () => {
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
openShouldFail = false
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { afterEach, describe, test, expect } from "bun:test"
|
|||
import { Permission } from "../src/permission"
|
||||
import { Config } from "@/config/config"
|
||||
import { Instance } from "../src/project/instance"
|
||||
import { WithInstance } from "../src/project/with-instance"
|
||||
import { disposeAllInstances, tmpdir } from "./fixture/fixture"
|
||||
import { AppRuntime } from "../src/effect/app-runtime"
|
||||
|
||||
|
|
@ -158,7 +159,7 @@ describe("permission.task with real config files", () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -183,7 +184,7 @@ describe("permission.task with real config files", () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -208,7 +209,7 @@ describe("permission.task with real config files", () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -235,7 +236,7 @@ describe("permission.task with real config files", () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -273,7 +274,7 @@ describe("permission.task with real config files", () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
@ -304,7 +305,7 @@ describe("permission.task with real config files", () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await load()
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ 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,
|
||||
|
|
@ -1006,7 +1007,7 @@ it.live("pending permission rejects on instance dispose", () =>
|
|||
|
||||
expect(yield* waitForPending(1).pipe(run)).toHaveLength(1)
|
||||
yield* Effect.promise(() =>
|
||||
Instance.provide({ directory: dir, fn: () => void InstanceRuntime.disposeInstance(Instance.current) }),
|
||||
WithInstance.provide({ directory: dir, fn: () => void InstanceRuntime.disposeInstance(Instance.current) }),
|
||||
)
|
||||
|
||||
const exit = yield* Fiber.await(fiber)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import path from "node:path"
|
|||
import { pathToFileURL } from "node:url"
|
||||
import { bootstrap as cliBootstrap } from "../../src/cli/bootstrap"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { InstanceRuntime } from "../../src/project/instance-runtime"
|
||||
import { InstanceMiddleware } from "../../src/server/routes/instance/middleware"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
|
|
@ -50,7 +51,7 @@ async function bootstrapFixture() {
|
|||
test("Instance.provide runs InstanceBootstrap before fn (boundary invariant)", async () => {
|
||||
await using tmp = await bootstrapFixture()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => "ok",
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,17 +5,23 @@ import { InstanceRef } from "../../src/effect/instance-ref"
|
|||
import { registerDisposer } from "../../src/effect/instance-registry"
|
||||
import { InstanceBootstrap } from "../../src/project/bootstrap-service"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { InstanceStore } from "../../src/project/instance-store"
|
||||
import { disposeAllInstances, tmpdirScoped } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
|
||||
let bootstrapRun: Effect.Effect<void> = Effect.void
|
||||
const noopBootstrap = Layer.succeed(
|
||||
InstanceBootstrap.Service,
|
||||
InstanceBootstrap.Service.of({ run: Effect.suspend(() => bootstrapRun) }),
|
||||
)
|
||||
|
||||
const it = testEffect(
|
||||
Layer.mergeAll(InstanceStore.defaultLayer, CrossSpawnSpawner.defaultLayer).pipe(Layer.provide(noopBootstrap)),
|
||||
)
|
||||
|
||||
afterEach(async () => {
|
||||
bootstrapRun = Effect.void
|
||||
await disposeAllInstances()
|
||||
})
|
||||
|
||||
|
|
@ -32,18 +38,16 @@ describe("InstanceStore", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.live("runs load init with InstanceRef provided", () =>
|
||||
it.live("runs bootstrap with InstanceRef provided", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped({ git: true })
|
||||
const store = yield* InstanceStore.Service
|
||||
let initializedDirectory: string | undefined
|
||||
|
||||
yield* store.load({
|
||||
directory: dir,
|
||||
init: Effect.gen(function* () {
|
||||
initializedDirectory = (yield* InstanceRef)?.directory
|
||||
}),
|
||||
bootstrapRun = Effect.gen(function* () {
|
||||
initializedDirectory = (yield* InstanceRef)?.directory
|
||||
})
|
||||
yield* store.load({ directory: dir })
|
||||
|
||||
expect(initializedDirectory).toBe(dir)
|
||||
expect(() => Instance.current).toThrow()
|
||||
|
|
@ -56,18 +60,11 @@ describe("InstanceStore", () => {
|
|||
const store = yield* InstanceStore.Service
|
||||
let initialized = 0
|
||||
|
||||
const first = yield* store.load({
|
||||
directory: dir,
|
||||
init: Effect.sync(() => {
|
||||
initialized++
|
||||
}),
|
||||
})
|
||||
const second = yield* store.load({
|
||||
directory: dir,
|
||||
init: Effect.sync(() => {
|
||||
initialized++
|
||||
}),
|
||||
bootstrapRun = Effect.sync(() => {
|
||||
initialized++
|
||||
})
|
||||
const first = yield* store.load({ directory: dir })
|
||||
const second = yield* store.load({ directory: dir })
|
||||
|
||||
expect(second).toBe(first)
|
||||
expect(initialized).toBe(1)
|
||||
|
|
@ -82,27 +79,19 @@ describe("InstanceStore", () => {
|
|||
const release = Promise.withResolvers<void>()
|
||||
let initialized = 0
|
||||
|
||||
const first = yield* store
|
||||
.load({
|
||||
directory: dir,
|
||||
init: Effect.promise(async () => {
|
||||
initialized++
|
||||
started.resolve()
|
||||
await release.promise
|
||||
}),
|
||||
})
|
||||
.pipe(Effect.forkScoped)
|
||||
bootstrapRun = Effect.promise(async () => {
|
||||
initialized++
|
||||
started.resolve()
|
||||
await release.promise
|
||||
})
|
||||
const first = yield* store.load({ directory: dir }).pipe(Effect.forkScoped)
|
||||
|
||||
yield* Effect.promise(() => started.promise)
|
||||
|
||||
const second = yield* store
|
||||
.load({
|
||||
directory: dir,
|
||||
init: Effect.sync(() => {
|
||||
initialized++
|
||||
}),
|
||||
})
|
||||
.pipe(Effect.forkScoped)
|
||||
bootstrapRun = Effect.sync(() => {
|
||||
initialized++
|
||||
})
|
||||
const second = yield* store.load({ directory: dir }).pipe(Effect.forkScoped)
|
||||
|
||||
expect(initialized).toBe(1)
|
||||
release.resolve()
|
||||
|
|
@ -119,27 +108,21 @@ describe("InstanceStore", () => {
|
|||
const store = yield* InstanceStore.Service
|
||||
let attempts = 0
|
||||
|
||||
const failed = yield* store
|
||||
.load({
|
||||
directory: dir,
|
||||
init: Effect.sync(() => {
|
||||
attempts++
|
||||
throw new Error("init failed")
|
||||
}),
|
||||
})
|
||||
.pipe(
|
||||
Effect.as(false),
|
||||
Effect.catchCause(() => Effect.succeed(true)),
|
||||
)
|
||||
bootstrapRun = Effect.sync(() => {
|
||||
attempts++
|
||||
throw new Error("init failed")
|
||||
})
|
||||
const failed = yield* store.load({ directory: dir }).pipe(
|
||||
Effect.as(false),
|
||||
Effect.catchCause(() => Effect.succeed(true)),
|
||||
)
|
||||
|
||||
expect(failed).toBe(true)
|
||||
|
||||
const ctx = yield* store.load({
|
||||
directory: dir,
|
||||
init: Effect.sync(() => {
|
||||
attempts++
|
||||
}),
|
||||
bootstrapRun = Effect.sync(() => {
|
||||
attempts++
|
||||
})
|
||||
const ctx = yield* store.load({ directory: dir })
|
||||
|
||||
expect(ctx.directory).toBe(dir)
|
||||
expect(attempts).toBe(2)
|
||||
|
|
@ -173,15 +156,11 @@ describe("InstanceStore", () => {
|
|||
yield* Effect.addFinalizer(() => Effect.sync(off))
|
||||
|
||||
const first = yield* store.load({ directory: dir })
|
||||
const reload = yield* store
|
||||
.reload({
|
||||
directory: dir,
|
||||
init: Effect.promise(async () => {
|
||||
reloading.resolve()
|
||||
await releaseReload.promise
|
||||
}),
|
||||
})
|
||||
.pipe(Effect.forkScoped)
|
||||
bootstrapRun = Effect.promise(async () => {
|
||||
reloading.resolve()
|
||||
await releaseReload.promise
|
||||
})
|
||||
const reload = yield* store.reload({ directory: dir }).pipe(Effect.forkScoped)
|
||||
|
||||
yield* Effect.promise(() => reloading.promise)
|
||||
const staleDispose = yield* store.dispose(first).pipe(Effect.forkScoped)
|
||||
|
|
@ -242,12 +221,12 @@ describe("InstanceStore", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.live("keeps Instance.provide as the legacy ALS wrapper", () =>
|
||||
it.live("provides legacy Promise callers with instance ALS", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped({ git: true })
|
||||
|
||||
const directory = yield* Effect.promise(() =>
|
||||
Instance.provide({
|
||||
WithInstance.provide({
|
||||
directory: dir,
|
||||
fn: () => Instance.directory,
|
||||
}),
|
||||
|
|
@ -258,21 +237,4 @@ describe("InstanceStore", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.live("does not install legacy ALS around Effect init", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
|
||||
const directory = yield* Effect.promise(() =>
|
||||
Instance.provide({
|
||||
directory: dir,
|
||||
init: Effect.sync(() => {
|
||||
expect(() => Instance.current).toThrow()
|
||||
}),
|
||||
fn: () => Instance.directory,
|
||||
}),
|
||||
)
|
||||
|
||||
expect(directory).toBe(dir)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
|||
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||
import { FileWatcher } from "../../src/file/watcher"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { GlobalBus } from "../../src/bus/global"
|
||||
import { Vcs } from "@/project/vcs"
|
||||
|
||||
|
|
@ -18,7 +19,7 @@ const describeVcs = FileWatcher.hasNativeBinding() && !process.env.CI ? describe
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function withVcs(directory: string, body: () => Promise<void>) {
|
||||
return Instance.provide({
|
||||
return WithInstance.provide({
|
||||
directory,
|
||||
fn: async () => {
|
||||
await AppRuntime.runPromise(
|
||||
|
|
@ -36,7 +37,7 @@ async function withVcs(directory: string, body: () => Promise<void>) {
|
|||
}
|
||||
|
||||
function withVcsOnly(directory: string, body: () => Promise<void>) {
|
||||
return Instance.provide({
|
||||
return WithInstance.provide({
|
||||
directory,
|
||||
fn: async () => {
|
||||
await AppRuntime.runPromise(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import path from "path"
|
|||
import { Cause, Effect, Exit, Layer } from "effect"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { InstanceRuntime } from "../../src/project/instance-runtime"
|
||||
import { Worktree } from "../../src/worktree"
|
||||
import { disposeAllInstances, provideInstance, provideTmpdirInstance } from "../fixture/fixture"
|
||||
|
|
@ -138,7 +139,7 @@ describe("Worktree", () => {
|
|||
expect(props.branch).toBe(info.branch)
|
||||
|
||||
yield* Effect.promise(() =>
|
||||
Instance.provide({
|
||||
WithInstance.provide({
|
||||
directory: info.directory,
|
||||
fn: () => InstanceRuntime.disposeInstance(Instance.current),
|
||||
}),
|
||||
|
|
@ -163,7 +164,7 @@ describe("Worktree", () => {
|
|||
|
||||
yield* Effect.promise(() => ready)
|
||||
yield* Effect.promise(() =>
|
||||
Instance.provide({
|
||||
WithInstance.provide({
|
||||
directory: info.directory,
|
||||
fn: () => InstanceRuntime.disposeInstance(Instance.current),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { unlink } from "fs/promises"
|
|||
import { ProviderID } from "../../src/provider/schema"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { Env } from "../../src/env"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
|
|
@ -43,13 +44,11 @@ test("Bedrock: config region takes precedence over AWS_REGION env var", async ()
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
fn: async () => {
|
||||
set("AWS_REGION", "us-east-1")
|
||||
set("AWS_PROFILE", "default")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1")
|
||||
|
|
@ -68,13 +67,11 @@ test("Bedrock: falls back to AWS_REGION env var when no config region", async ()
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
fn: async () => {
|
||||
set("AWS_REGION", "eu-west-1")
|
||||
set("AWS_PROFILE", "default")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1")
|
||||
|
|
@ -123,14 +120,12 @@ test("Bedrock: loads when bearer token from auth.json is present", async () => {
|
|||
}),
|
||||
)
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
fn: async () => {
|
||||
set("AWS_PROFILE", "")
|
||||
set("AWS_ACCESS_KEY_ID", "")
|
||||
set("AWS_BEARER_TOKEN_BEDROCK", "")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1")
|
||||
|
|
@ -169,13 +164,11 @@ test("Bedrock: config profile takes precedence over AWS_PROFILE env var", async
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
fn: async () => {
|
||||
set("AWS_PROFILE", "default")
|
||||
set("AWS_ACCESS_KEY_ID", "test-key-id")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
expect(providers[ProviderID.amazonBedrock].options?.region).toBe("us-east-1")
|
||||
|
|
@ -201,12 +194,10 @@ test("Bedrock: includes custom endpoint in options when specified", async () =>
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("AWS_PROFILE", "default")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("AWS_PROFILE", "default")
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
expect(providers[ProviderID.amazonBedrock].options?.endpoint).toBe(
|
||||
|
|
@ -234,15 +225,13 @@ test("Bedrock: autoloads when AWS_WEB_IDENTITY_TOKEN_FILE is present", async ()
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
fn: async () => {
|
||||
set("AWS_WEB_IDENTITY_TOKEN_FILE", "/var/run/secrets/eks.amazonaws.com/serviceaccount/token")
|
||||
set("AWS_ROLE_ARN", "arn:aws:iam::123456789012:role/my-eks-role")
|
||||
set("AWS_PROFILE", "")
|
||||
set("AWS_ACCESS_KEY_ID", "")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
expect(providers[ProviderID.amazonBedrock].options?.region).toBe("us-east-1")
|
||||
|
|
@ -277,12 +266,10 @@ test("Bedrock: model with us. prefix should not be double-prefixed", async () =>
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("AWS_PROFILE", "default")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("AWS_PROFILE", "default")
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
// The model should exist with the us. prefix
|
||||
|
|
@ -314,12 +301,10 @@ test("Bedrock: model with global. prefix should not be prefixed", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("AWS_PROFILE", "default")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("AWS_PROFILE", "default")
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
expect(providers[ProviderID.amazonBedrock].models["global.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
|
||||
|
|
@ -350,12 +335,10 @@ test("Bedrock: model with eu. prefix should not be double-prefixed", async () =>
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("AWS_PROFILE", "default")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("AWS_PROFILE", "default")
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
expect(providers[ProviderID.amazonBedrock].models["eu.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
|
||||
|
|
@ -386,12 +369,10 @@ test("Bedrock: model without prefix in US region should get us. prefix added", a
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("AWS_PROFILE", "default")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("AWS_PROFILE", "default")
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
// Non-prefixed model should still be registered
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export {}
|
|||
// import { ProviderID, ModelID } from "../../src/provider/schema"
|
||||
// import { tmpdir } from "../fixture/fixture"
|
||||
// import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
// import { Provider } from "@/provider/provider"
|
||||
// import { Env } from "../../src/env"
|
||||
// import { Global } from "@opencode-ai/core/global"
|
||||
|
|
@ -25,7 +26,7 @@ export {}
|
|||
// )
|
||||
// },
|
||||
// })
|
||||
// await Instance.provide({
|
||||
// await WithInstance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "test-gitlab-token")
|
||||
|
|
@ -56,7 +57,7 @@ export {}
|
|||
// )
|
||||
// },
|
||||
// })
|
||||
// await Instance.provide({
|
||||
// await WithInstance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "test-token")
|
||||
|
|
@ -95,7 +96,7 @@ export {}
|
|||
// }),
|
||||
// )
|
||||
|
||||
// await Instance.provide({
|
||||
// await WithInstance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "")
|
||||
|
|
@ -130,7 +131,7 @@ export {}
|
|||
// }),
|
||||
// )
|
||||
|
||||
// await Instance.provide({
|
||||
// await WithInstance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "")
|
||||
|
|
@ -162,7 +163,7 @@ export {}
|
|||
// )
|
||||
// },
|
||||
// })
|
||||
// await Instance.provide({
|
||||
// await WithInstance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_INSTANCE_URL", "https://gitlab.company.internal")
|
||||
|
|
@ -193,7 +194,7 @@ export {}
|
|||
// )
|
||||
// },
|
||||
// })
|
||||
// await Instance.provide({
|
||||
// await WithInstance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "env-token")
|
||||
|
|
@ -216,7 +217,7 @@ export {}
|
|||
// )
|
||||
// },
|
||||
// })
|
||||
// await Instance.provide({
|
||||
// await WithInstance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "test-token")
|
||||
|
|
@ -252,7 +253,7 @@ export {}
|
|||
// )
|
||||
// },
|
||||
// })
|
||||
// await Instance.provide({
|
||||
// await WithInstance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "test-token")
|
||||
|
|
@ -277,7 +278,7 @@ export {}
|
|||
// )
|
||||
// },
|
||||
// })
|
||||
// await Instance.provide({
|
||||
// await WithInstance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "test-token")
|
||||
|
|
@ -301,7 +302,7 @@ export {}
|
|||
// await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json" }))
|
||||
// },
|
||||
// })
|
||||
// await Instance.provide({
|
||||
// await WithInstance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "test-token")
|
||||
|
|
@ -349,7 +350,7 @@ export {}
|
|||
// await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json" }))
|
||||
// },
|
||||
// })
|
||||
// await Instance.provide({
|
||||
// await WithInstance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "test-token")
|
||||
|
|
@ -372,7 +373,7 @@ export {}
|
|||
// await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json" }))
|
||||
// },
|
||||
// })
|
||||
// await Instance.provide({
|
||||
// await WithInstance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "test-token")
|
||||
|
|
@ -396,7 +397,7 @@ export {}
|
|||
// await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json" }))
|
||||
// },
|
||||
// })
|
||||
// await Instance.provide({
|
||||
// await WithInstance.provide({
|
||||
// directory: tmp.path,
|
||||
// init: async () => {
|
||||
// Env.set("GITLAB_TOKEN", "test-token")
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import path from "path"
|
|||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Plugin } from "../../src/plugin/index"
|
||||
import { ModelsDev } from "@/provider/models"
|
||||
import { Provider } from "@/provider/provider"
|
||||
|
|
@ -80,12 +81,10 @@ test("provider loaded from env variable", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
// Provider should retain its connection source even if custom loaders
|
||||
|
|
@ -114,7 +113,7 @@ test("provider loaded from config with apiKey option", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
|
|
@ -135,12 +134,10 @@ test("disabled_providers excludes provider", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic]).toBeUndefined()
|
||||
},
|
||||
|
|
@ -159,13 +156,11 @@ test("enabled_providers restricts to only listed providers", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
set("OPENAI_API_KEY", "test-openai-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
expect(providers[ProviderID.openai]).toBeUndefined()
|
||||
|
|
@ -189,12 +184,10 @@ test("model whitelist filters models for provider", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
const models = Object.keys(providers[ProviderID.anthropic].models)
|
||||
|
|
@ -220,12 +213,10 @@ test("model blacklist excludes specific models", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
const models = Object.keys(providers[ProviderID.anthropic].models)
|
||||
|
|
@ -255,12 +246,10 @@ test("custom model alias via config", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
expect(providers[ProviderID.anthropic].models["my-alias"]).toBeDefined()
|
||||
|
|
@ -301,7 +290,7 @@ test("custom provider with npm package", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
|
|
@ -358,7 +347,7 @@ test("custom DeepSeek openai-compatible model defaults interleaved reasoning fie
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
|
|
@ -392,12 +381,10 @@ test("env variable takes precedence, config merges options", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "env-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "env-api-key")
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
// Config options should be merged
|
||||
|
|
@ -418,12 +405,10 @@ test("getModel returns model for valid provider/model", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const model = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
|
||||
expect(model).toBeDefined()
|
||||
expect(String(model.providerID)).toBe("anthropic")
|
||||
|
|
@ -445,12 +430,10 @@ test("getModel throws ModelNotFoundError for invalid model", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
expect(getModel(ProviderID.anthropic, ModelID.make("nonexistent-model"))).rejects.toThrow()
|
||||
},
|
||||
})
|
||||
|
|
@ -467,7 +450,7 @@ test("getModel throws ModelNotFoundError for invalid provider", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
expect(getModel(ProviderID.make("nonexistent-provider"), ModelID.make("some-model"))).rejects.toThrow()
|
||||
|
|
@ -498,12 +481,10 @@ test("defaultModel returns first available model when no config set", async () =
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const model = await defaultModel()
|
||||
expect(model.providerID).toBeDefined()
|
||||
expect(model.modelID).toBeDefined()
|
||||
|
|
@ -523,12 +504,10 @@ test("defaultModel respects config model setting", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const model = await defaultModel()
|
||||
expect(String(model.providerID)).toBe("anthropic")
|
||||
expect(String(model.modelID)).toBe("claude-sonnet-4-20250514")
|
||||
|
|
@ -565,7 +544,7 @@ test("provider with baseURL from config", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
|
|
@ -603,7 +582,7 @@ test("model cost defaults to zero when not specified", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
|
|
@ -638,12 +617,10 @@ test("model options are merged from existing model", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
expect(model.options.customOption).toBe("custom-value")
|
||||
|
|
@ -667,12 +644,10 @@ test("provider removed when all models filtered out", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic]).toBeUndefined()
|
||||
},
|
||||
|
|
@ -690,12 +665,10 @@ test("closest finds model by partial match", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const result = await closest(ProviderID.anthropic, ["sonnet-4"])
|
||||
expect(result).toBeDefined()
|
||||
expect(String(result?.providerID)).toBe("anthropic")
|
||||
|
|
@ -715,7 +688,7 @@ test("closest returns undefined for nonexistent provider", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const result = await closest(ProviderID.make("nonexistent"), ["model"])
|
||||
|
|
@ -745,12 +718,10 @@ test("getModel uses realIdByKey for aliased models", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic].models["my-sonnet"]).toBeDefined()
|
||||
|
||||
|
|
@ -791,7 +762,7 @@ test("provider api field sets model api.url", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
|
|
@ -831,7 +802,7 @@ test("explicit baseURL overrides api field", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
|
|
@ -860,12 +831,10 @@ test("model inherits properties from existing database model", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
expect(model.name).toBe("Custom Name for Sonnet")
|
||||
|
|
@ -888,12 +857,10 @@ test("disabled_providers prevents loading even with env var", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("OPENAI_API_KEY", "test-openai-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("OPENAI_API_KEY", "test-openai-key")
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.openai]).toBeUndefined()
|
||||
},
|
||||
|
|
@ -912,13 +879,11 @@ test("enabled_providers with empty array allows no providers", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
set("OPENAI_API_KEY", "test-openai-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(Object.keys(providers).length).toBe(0)
|
||||
},
|
||||
|
|
@ -942,12 +907,10 @@ test("whitelist and blacklist can be combined", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
const models = Object.keys(providers[ProviderID.anthropic].models)
|
||||
|
|
@ -984,7 +947,7 @@ test("model modalities default correctly", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
|
|
@ -1027,7 +990,7 @@ test("model with custom cost values", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
|
|
@ -1051,12 +1014,10 @@ test("getSmallModel returns appropriate small model", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const model = await getSmallModel(ProviderID.anthropic)
|
||||
expect(model).toBeDefined()
|
||||
expect(model?.id).toContain("haiku")
|
||||
|
|
@ -1076,12 +1037,10 @@ test("getSmallModel respects config small_model override", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const model = await getSmallModel(ProviderID.anthropic)
|
||||
expect(model).toBeDefined()
|
||||
expect(String(model?.providerID)).toBe("anthropic")
|
||||
|
|
@ -1124,13 +1083,11 @@ test("multiple providers can be configured simultaneously", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-anthropic-key")
|
||||
set("OPENAI_API_KEY", "test-openai-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
expect(providers[ProviderID.openai]).toBeDefined()
|
||||
|
|
@ -1169,7 +1126,7 @@ test("provider with custom npm package", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
|
|
@ -1203,12 +1160,10 @@ test("model alias name defaults to alias key when id differs", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic].models["sonnet"].name).toBe("sonnet")
|
||||
},
|
||||
|
|
@ -1243,12 +1198,10 @@ test("provider with multiple env var options only includes apiKey when single en
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("MULTI_ENV_KEY_1", "test-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("MULTI_ENV_KEY_1", "test-key")
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.make("multi-env")]).toBeDefined()
|
||||
// When multiple env options exist, key should NOT be auto-set
|
||||
|
|
@ -1285,12 +1238,10 @@ test("provider with single env var includes apiKey automatically", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("SINGLE_ENV_KEY", "my-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("SINGLE_ENV_KEY", "my-api-key")
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.make("single-env")]).toBeDefined()
|
||||
// Single env option should auto-set key
|
||||
|
|
@ -1322,12 +1273,10 @@ test("model cost overrides existing cost values", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
expect(model.cost.input).toBe(999)
|
||||
|
|
@ -1372,7 +1321,7 @@ test("completely new provider not in database can be configured", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
|
|
@ -1401,14 +1350,12 @@ test("disabled_providers and enabled_providers interaction", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-anthropic")
|
||||
set("OPENAI_API_KEY", "test-openai")
|
||||
set("GOOGLE_GENERATIVE_AI_API_KEY", "test-google")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
// anthropic: in enabled, not in disabled = allowed
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
|
|
@ -1446,7 +1393,7 @@ test("model with tool_call false", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
|
|
@ -1481,7 +1428,7 @@ test("model defaults tool_call to true when not specified", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
|
|
@ -1520,7 +1467,7 @@ test("model headers are preserved", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
|
|
@ -1559,13 +1506,11 @@ test("provider env fallback - second env var used if first missing", async () =>
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
fn: async () => {
|
||||
// Only set fallback, not primary
|
||||
set("FALLBACK_KEY", "fallback-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
// Provider should load because fallback env var is set
|
||||
expect(providers[ProviderID.make("fallback-env")]).toBeDefined()
|
||||
|
|
@ -1584,12 +1529,10 @@ test("getModel returns consistent results", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const model1 = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
|
||||
const model2 = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
|
||||
expect(model1.providerID).toEqual(model2.providerID)
|
||||
|
|
@ -1625,7 +1568,7 @@ test("provider name defaults to id when not in database", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
|
|
@ -1645,12 +1588,10 @@ test("ModelNotFoundError includes suggestions for typos", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
try {
|
||||
await getModel(ProviderID.anthropic, ModelID.make("claude-sonet-4")) // typo: sonet instead of sonnet
|
||||
expect(true).toBe(false) // Should not reach here
|
||||
|
|
@ -1673,12 +1614,10 @@ test("ModelNotFoundError for provider includes suggestions", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
try {
|
||||
await getModel(ProviderID.make("antropic"), ModelID.make("claude-sonnet-4")) // typo: antropic
|
||||
expect(true).toBe(false) // Should not reach here
|
||||
|
|
@ -1701,7 +1640,7 @@ test("getProvider returns undefined for nonexistent provider", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const provider = await getProvider(ProviderID.make("nonexistent"))
|
||||
|
|
@ -1721,12 +1660,10 @@ test("getProvider returns provider info", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const provider = await getProvider(ProviderID.anthropic)
|
||||
expect(provider).toBeDefined()
|
||||
expect(String(provider?.id)).toBe("anthropic")
|
||||
|
|
@ -1745,12 +1682,10 @@ test("closest returns undefined when no partial match found", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const result = await closest(ProviderID.anthropic, ["nonexistent-xyz-model"])
|
||||
expect(result).toBeUndefined()
|
||||
},
|
||||
|
|
@ -1768,12 +1703,10 @@ test("closest checks multiple query terms in order", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
// First term won't match, second will
|
||||
const result = await closest(ProviderID.anthropic, ["nonexistent", "haiku"])
|
||||
expect(result).toBeDefined()
|
||||
|
|
@ -1808,7 +1741,7 @@ test("model limit defaults to zero when not specified", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
|
|
@ -1840,12 +1773,10 @@ test("provider options are deeply merged", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
// Custom options should be merged
|
||||
expect(providers[ProviderID.anthropic].options.timeout).toBe(30000)
|
||||
|
|
@ -1878,12 +1809,10 @@ test("custom model inherits npm package from models.dev provider config", async
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("OPENAI_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("OPENAI_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
const model = providers[ProviderID.openai].models["my-custom-model"]
|
||||
expect(model).toBeDefined()
|
||||
|
|
@ -1913,12 +1842,10 @@ test("custom model inherits api.url from models.dev provider", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("OPENROUTER_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("OPENROUTER_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.openrouter]).toBeDefined()
|
||||
|
||||
|
|
@ -2046,12 +1973,10 @@ test("model variants are generated for reasoning models", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
// Claude sonnet 4 has reasoning capability
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
|
|
@ -2084,12 +2009,10 @@ test("model variants can be disabled via config", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
expect(model.variants).toBeDefined()
|
||||
|
|
@ -2127,12 +2050,10 @@ test("model variants can be customized via config", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
expect(model.variants!["high"]).toBeDefined()
|
||||
|
|
@ -2166,12 +2087,10 @@ test("disabled key is stripped from variant config", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
expect(model.variants!["max"]).toBeDefined()
|
||||
|
|
@ -2204,12 +2123,10 @@ test("all variants can be disabled via config", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
expect(model.variants).toBeDefined()
|
||||
|
|
@ -2242,12 +2159,10 @@ test("variant config merges with generated variants", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
expect(model.variants!["high"]).toBeDefined()
|
||||
|
|
@ -2280,12 +2195,10 @@ test("variants filtered in second pass for database models", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("OPENAI_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("OPENAI_API_KEY", "test-api-key")
|
||||
const providers = await list()
|
||||
const model = providers[ProviderID.openai].models["gpt-5"]
|
||||
expect(model.variants).toBeDefined()
|
||||
|
|
@ -2329,7 +2242,7 @@ test("custom model with variants enabled and disabled", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
|
|
@ -2384,12 +2297,10 @@ test("Google Vertex: retains baseURL for custom proxy", async () => {
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.make("vertex-proxy")]).toBeDefined()
|
||||
expect(providers[ProviderID.make("vertex-proxy")].options.baseURL).toBe("https://my-proxy.com/v1")
|
||||
|
|
@ -2429,12 +2340,10 @@ test("Google Vertex: supports OpenAI compatible models", async () => {
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
|
||||
const providers = await list()
|
||||
const model = providers[ProviderID.make("vertex-openai")].models["gpt-4"]
|
||||
|
||||
|
|
@ -2455,14 +2364,12 @@ test("cloudflare-ai-gateway loads with env variables", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
fn: async () => {
|
||||
set("CLOUDFLARE_ACCOUNT_ID", "test-account")
|
||||
set("CLOUDFLARE_GATEWAY_ID", "test-gateway")
|
||||
set("CLOUDFLARE_API_TOKEN", "test-token")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.make("cloudflare-ai-gateway")]).toBeDefined()
|
||||
},
|
||||
|
|
@ -2487,14 +2394,12 @@ test("cloudflare-ai-gateway forwards config metadata options", async () => {
|
|||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
fn: async () => {
|
||||
set("CLOUDFLARE_ACCOUNT_ID", "test-account")
|
||||
set("CLOUDFLARE_GATEWAY_ID", "test-gateway")
|
||||
set("CLOUDFLARE_API_TOKEN", "test-token")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.make("cloudflare-ai-gateway")]).toBeDefined()
|
||||
expect(providers[ProviderID.make("cloudflare-ai-gateway")].options.metadata).toEqual({
|
||||
|
|
@ -2542,7 +2447,7 @@ test("plugin config providers persist after instance dispose", async () => {
|
|||
},
|
||||
})
|
||||
|
||||
const first = await Instance.provide({
|
||||
const first = await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () =>
|
||||
AppRuntime.runPromise(
|
||||
|
|
@ -2559,7 +2464,7 @@ test("plugin config providers persist after instance dispose", async () => {
|
|||
|
||||
await disposeAllInstances()
|
||||
|
||||
const second = await Instance.provide({
|
||||
const second = await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => list(),
|
||||
})
|
||||
|
|
@ -2590,13 +2495,11 @@ test("plugin config enabled and disabled providers are honored", async () => {
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
fn: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-anthropic-key")
|
||||
set("OPENAI_API_KEY", "test-openai-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
expect(providers[ProviderID.openai]).toBeUndefined()
|
||||
|
|
@ -2616,7 +2519,7 @@ test("opencode loader keeps paid models when config apiKey is present", async ()
|
|||
},
|
||||
})
|
||||
|
||||
const none = await Instance.provide({
|
||||
const none = await WithInstance.provide({
|
||||
directory: base.path,
|
||||
fn: async () => paid(await list()),
|
||||
})
|
||||
|
|
@ -2639,7 +2542,7 @@ test("opencode loader keeps paid models when config apiKey is present", async ()
|
|||
},
|
||||
})
|
||||
|
||||
const keyedCount = await Instance.provide({
|
||||
const keyedCount = await WithInstance.provide({
|
||||
directory: keyed.path,
|
||||
fn: async () => paid(await list()),
|
||||
})
|
||||
|
|
@ -2660,7 +2563,7 @@ test("opencode loader keeps paid models when auth exists", async () => {
|
|||
},
|
||||
})
|
||||
|
||||
const none = await Instance.provide({
|
||||
const none = await WithInstance.provide({
|
||||
directory: base.path,
|
||||
fn: async () => paid(await list()),
|
||||
})
|
||||
|
|
@ -2694,7 +2597,7 @@ test("opencode loader keeps paid models when auth exists", async () => {
|
|||
}),
|
||||
)
|
||||
|
||||
const keyedCount = await Instance.provide({
|
||||
const keyedCount = await WithInstance.provide({
|
||||
directory: keyed.path,
|
||||
fn: async () => paid(await list()),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
|
|||
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||
import { Effect } from "effect"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Pty } from "../../src/pty"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
|
|
@ -10,7 +11,7 @@ describe("pty", () => {
|
|||
test("does not leak output when websocket objects are reused", async () => {
|
||||
await using dir = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: dir.path,
|
||||
fn: () =>
|
||||
AppRuntime.runPromise(
|
||||
|
|
@ -60,7 +61,7 @@ describe("pty", () => {
|
|||
test("does not leak output when Bun recycles websocket objects before re-connect", async () => {
|
||||
await using dir = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: dir.path,
|
||||
fn: () =>
|
||||
AppRuntime.runPromise(
|
||||
|
|
@ -105,7 +106,7 @@ describe("pty", () => {
|
|||
test("treats in-place socket data mutation as the same connection", async () => {
|
||||
await using dir = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: dir.path,
|
||||
fn: () =>
|
||||
AppRuntime.runPromise(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { AppRuntime } from "../../src/effect/app-runtime"
|
|||
import { Bus } from "../../src/bus"
|
||||
import { Effect } from "effect"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Pty } from "../../src/pty"
|
||||
import type { PtyID } from "../../src/pty/schema"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
|
@ -27,7 +28,7 @@ describe("pty", () => {
|
|||
|
||||
await using dir = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: dir.path,
|
||||
fn: () =>
|
||||
AppRuntime.runPromise(
|
||||
|
|
@ -68,7 +69,7 @@ describe("pty", () => {
|
|||
|
||||
await using dir = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: dir.path,
|
||||
fn: () =>
|
||||
AppRuntime.runPromise(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
|
|||
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||
import { Effect } from "effect"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Pty } from "../../src/pty"
|
||||
import { Shell } from "../../src/shell/shell"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
|
@ -17,7 +18,7 @@ describe("pty shell args", () => {
|
|||
"does not add login args to pwsh",
|
||||
async () => {
|
||||
await using dir = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: dir.path,
|
||||
fn: () =>
|
||||
AppRuntime.runPromise(
|
||||
|
|
@ -47,7 +48,7 @@ describe("pty shell args", () => {
|
|||
"adds login args to bash",
|
||||
async () => {
|
||||
await using dir = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: dir.path,
|
||||
fn: () =>
|
||||
AppRuntime.runPromise(
|
||||
|
|
@ -78,7 +79,7 @@ describe("pty configured shell", () => {
|
|||
await using dir = await tmpdir({
|
||||
config: { shell: Shell.name(configured) },
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: dir.path,
|
||||
fn: () =>
|
||||
AppRuntime.runPromise(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { afterEach, expect } from "bun:test"
|
|||
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
|
||||
import { Question } from "../../src/question"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { InstanceRuntime } from "../../src/project/instance-runtime"
|
||||
import { QuestionID } from "../../src/question/schema"
|
||||
import { disposeAllInstances, provideInstance, reloadTestInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
|
|
@ -398,7 +399,7 @@ it.live("pending question rejects on instance dispose", () =>
|
|||
|
||||
expect(yield* waitForPending(1).pipe(provideInstance(dir))).toHaveLength(1)
|
||||
yield* Effect.promise(() =>
|
||||
Instance.provide({ directory: dir, fn: () => InstanceRuntime.disposeInstance(Instance.current) }),
|
||||
WithInstance.provide({ directory: dir, fn: () => InstanceRuntime.disposeInstance(Instance.current) }),
|
||||
)
|
||||
|
||||
const exit = yield* Fiber.await(fiber)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
|
|||
import { Effect } from "effect"
|
||||
import z from "zod"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Project } from "@/project/project"
|
||||
import { Session as SessionNs } from "@/session/session"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
|
|
@ -28,11 +29,11 @@ describe("session.listGlobal", () => {
|
|||
await using first = await tmpdir({ git: true })
|
||||
await using second = await tmpdir({ git: true })
|
||||
|
||||
const firstSession = await Instance.provide({
|
||||
const firstSession = await WithInstance.provide({
|
||||
directory: first.path,
|
||||
fn: async () => svc.create({ title: "first-session" }),
|
||||
})
|
||||
const secondSession = await Instance.provide({
|
||||
const secondSession = await WithInstance.provide({
|
||||
directory: second.path,
|
||||
fn: async () => svc.create({ title: "second-session" }),
|
||||
})
|
||||
|
|
@ -58,12 +59,12 @@ describe("session.listGlobal", () => {
|
|||
test("excludes archived sessions by default", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
const archived = await Instance.provide({
|
||||
const archived = await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => svc.create({ title: "archived-session" }),
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => svc.setArchived({ sessionID: archived.id, time: Date.now() }),
|
||||
})
|
||||
|
|
@ -82,12 +83,12 @@ describe("session.listGlobal", () => {
|
|||
test("supports cursor pagination", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
const first = await Instance.provide({
|
||||
const first = await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => svc.create({ title: "page-one" }),
|
||||
})
|
||||
await new Promise((resolve) => setTimeout(resolve, 5))
|
||||
const second = await Instance.provide({
|
||||
const second = await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => svc.create({ title: "page-two" }),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test"
|
|||
import { Effect } from "effect"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Server } from "../../src/server/server"
|
||||
import { ExperimentalPaths } from "../../src/server/routes/instance/httpapi/groups/experimental"
|
||||
import { Session } from "@/session/session"
|
||||
|
|
@ -126,12 +127,12 @@ describe("experimental HttpApi", () => {
|
|||
test("serves global session list through Hono bridge", async () => {
|
||||
await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
|
||||
|
||||
const first = await Instance.provide({
|
||||
const first = await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => createSession({ title: "page-one" }),
|
||||
})
|
||||
await new Promise((resolve) => setTimeout(resolve, 5))
|
||||
const second = await Instance.provide({
|
||||
const second = await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => createSession({ title: "page-two" }),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import { registerAdapter } from "../../src/control-plane/adapters"
|
|||
import type { WorkspaceAdapter } from "../../src/control-plane/types"
|
||||
import { Workspace } from "../../src/control-plane/workspace"
|
||||
import { InstanceRef, WorkspaceRef } from "../../src/effect/instance-ref"
|
||||
import { InstanceRuntime } from "../../src/project/instance-runtime"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { InstanceLayer } from "../../src/project/instance-layer"
|
||||
import { Project } from "../../src/project/project"
|
||||
import { disposeMiddleware, markInstanceForDisposal } from "../../src/server/routes/instance/httpapi/lifecycle"
|
||||
import { instanceRouterMiddleware } from "../../src/server/routes/instance/httpapi/middleware/instance-context"
|
||||
|
|
@ -41,7 +41,7 @@ const it = testEffect(
|
|||
testStateLayer,
|
||||
NodeHttpServer.layerTest,
|
||||
NodeServices.layer,
|
||||
InstanceRuntime.layer,
|
||||
InstanceLayer.layer,
|
||||
Project.defaultLayer,
|
||||
Workspace.defaultLayer,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Flag } from "@opencode-ai/core/flag/flag"
|
|||
import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
|
||||
import { McpPaths } from "../../src/server/routes/instance/httpapi/groups/mcp"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { InstanceRuntime } from "../../src/project/instance-runtime"
|
||||
import { Server } from "../../src/server/server"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
|
|
@ -59,7 +60,7 @@ function withMcpProject<A, E, R>(self: (dir: string) => Effect.Effect<A, E, R>)
|
|||
)
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.promise(() =>
|
||||
Instance.provide({ directory: dir, fn: () => InstanceRuntime.disposeInstance(Instance.current) }),
|
||||
WithInstance.provide({ directory: dir, fn: () => InstanceRuntime.disposeInstance(Instance.current) }),
|
||||
).pipe(Effect.ignore),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Effect, FileSystem, Layer, Path } from "effect"
|
|||
import { NodeFileSystem, NodePath } from "@effect/platform-node"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { InstanceRuntime } from "../../src/project/instance-runtime"
|
||||
import { Server } from "../../src/server/server"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
|
|
@ -91,7 +92,7 @@ function withProviderProject<A, E, R>(self: (dir: string) => Effect.Effect<A, E,
|
|||
yield* writeProviderAuthPlugin(dir)
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.promise(() =>
|
||||
Instance.provide({ directory: dir, fn: () => InstanceRuntime.disposeInstance(Instance.current) }),
|
||||
WithInstance.provide({ directory: dir, fn: () => InstanceRuntime.disposeInstance(Instance.current) }),
|
||||
).pipe(Effect.ignore),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { HttpRouter } from "effect/unstable/http"
|
|||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
|
||||
import { Server } from "../../src/server/server"
|
||||
import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
||||
|
|
@ -226,7 +227,7 @@ function seedMessage(directory: string, sessionID: string) {
|
|||
const id = SessionID.make(sessionID)
|
||||
return call(
|
||||
async () =>
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory,
|
||||
fn: () =>
|
||||
Effect.runPromise(
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { Workspace } from "../../src/control-plane/workspace"
|
|||
import { PermissionID } from "../../src/permission/schema"
|
||||
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Project } from "../../src/project/project"
|
||||
import { Server } from "../../src/server/server"
|
||||
import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
|
||||
|
|
@ -44,7 +45,7 @@ function pathFor(path: string, params: Record<string, string>) {
|
|||
function createSession(directory: string, input?: Session.CreateInput) {
|
||||
return Effect.promise(
|
||||
async () =>
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory,
|
||||
fn: () => runSession(Session.Service.use((svc) => svc.create(input))),
|
||||
}),
|
||||
|
|
@ -54,7 +55,7 @@ function createSession(directory: string, input?: Session.CreateInput) {
|
|||
function createTextMessage(directory: string, sessionID: SessionID, text: string) {
|
||||
return Effect.promise(
|
||||
async () =>
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory,
|
||||
fn: () =>
|
||||
runSession(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
|
|||
import { Effect } from "effect"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Server } from "../../src/server/server"
|
||||
import { SyncPaths } from "../../src/server/routes/instance/httpapi/groups/sync"
|
||||
import { Session } from "@/session/session"
|
||||
|
|
@ -38,7 +39,7 @@ describe("sync HttpApi", () => {
|
|||
const headers = { "x-opencode-directory": tmp.path, "content-type": "application/json" }
|
||||
const info = spyOn(Log.create({ service: "server.sync" }), "info")
|
||||
|
||||
const session = await Instance.provide({
|
||||
const session = await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => runSession(Session.Service.use((svc) => svc.create({ title: "sync" }))),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { afterEach, describe, expect, mock, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Server } from "../../src/server/server"
|
||||
import { Session as SessionNs } from "@/session/session"
|
||||
import type { SessionID } from "../../src/session/schema"
|
||||
|
|
@ -31,7 +32,7 @@ afterEach(async () => {
|
|||
describe("session action routes", () => {
|
||||
test("abort route returns success", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Session as SessionNs } from "@/session/session"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
|
|
@ -40,20 +41,20 @@ describe("session.list", () => {
|
|||
await mkdir(path.join(tmp.path, "packages", "opencode"), { recursive: true })
|
||||
await mkdir(path.join(tmp.path, "packages", "app"), { recursive: true })
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const root = await svc.create({ title: "root" })
|
||||
|
||||
const parent = await Instance.provide({
|
||||
const parent = await WithInstance.provide({
|
||||
directory: path.join(tmp.path, "packages"),
|
||||
fn: async () => svc.create({ title: "parent" }),
|
||||
})
|
||||
const current = await Instance.provide({
|
||||
const current = await WithInstance.provide({
|
||||
directory: path.join(tmp.path, "packages", "opencode"),
|
||||
fn: async () => svc.create({ title: "current" }),
|
||||
})
|
||||
const sibling = await Instance.provide({
|
||||
const sibling = await WithInstance.provide({
|
||||
directory: path.join(tmp.path, "packages", "app"),
|
||||
fn: async () => svc.create({ title: "sibling" }),
|
||||
})
|
||||
|
|
@ -73,20 +74,20 @@ describe("session.list", () => {
|
|||
await mkdir(path.join(tmp.path, "packages", "opencode"), { recursive: true })
|
||||
await mkdir(path.join(tmp.path, "packages", "app"), { recursive: true })
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const root = await svc.create({ title: "root" })
|
||||
|
||||
const parent = await Instance.provide({
|
||||
const parent = await WithInstance.provide({
|
||||
directory: path.join(tmp.path, "packages"),
|
||||
fn: async () => svc.create({ title: "parent" }),
|
||||
})
|
||||
const current = await Instance.provide({
|
||||
const current = await WithInstance.provide({
|
||||
directory: path.join(tmp.path, "packages", "opencode"),
|
||||
fn: async () => svc.create({ title: "current" }),
|
||||
})
|
||||
const sibling = await Instance.provide({
|
||||
const sibling = await WithInstance.provide({
|
||||
directory: path.join(tmp.path, "packages", "app"),
|
||||
fn: async () => svc.create({ title: "sibling" }),
|
||||
})
|
||||
|
|
@ -106,22 +107,22 @@ describe("session.list", () => {
|
|||
await mkdir(path.join(tmp.path, "packages", "opencode", "src", "deep"), { recursive: true })
|
||||
await mkdir(path.join(tmp.path, "packages", "app"), { recursive: true })
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const parent = await Instance.provide({
|
||||
const parent = await WithInstance.provide({
|
||||
directory: path.join(tmp.path, "packages", "opencode"),
|
||||
fn: async () => svc.create({ title: "parent" }),
|
||||
})
|
||||
const current = await Instance.provide({
|
||||
const current = await WithInstance.provide({
|
||||
directory: path.join(tmp.path, "packages", "opencode", "src"),
|
||||
fn: async () => svc.create({ title: "current" }),
|
||||
})
|
||||
const deeper = await Instance.provide({
|
||||
const deeper = await WithInstance.provide({
|
||||
directory: path.join(tmp.path, "packages", "opencode", "src", "deep"),
|
||||
fn: async () => svc.create({ title: "deeper" }),
|
||||
})
|
||||
const sibling = await Instance.provide({
|
||||
const sibling = await WithInstance.provide({
|
||||
directory: path.join(tmp.path, "packages", "app"),
|
||||
fn: async () => svc.create({ title: "sibling" }),
|
||||
})
|
||||
|
|
@ -146,14 +147,14 @@ describe("session.list", () => {
|
|||
await mkdir(path.join(tmp.path, "packages", "opencode", "src"), { recursive: true })
|
||||
await mkdir(path.join(tmp.path, "packages", "app"), { recursive: true })
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const current = await Instance.provide({
|
||||
const current = await WithInstance.provide({
|
||||
directory: path.join(tmp.path, "packages", "opencode", "src"),
|
||||
fn: async () => svc.create({ title: "legacy-current" }),
|
||||
})
|
||||
const sibling = await Instance.provide({
|
||||
const sibling = await WithInstance.provide({
|
||||
directory: path.join(tmp.path, "packages", "app"),
|
||||
fn: async () => svc.create({ title: "legacy-sibling" }),
|
||||
})
|
||||
|
|
@ -175,7 +176,7 @@ describe("session.list", () => {
|
|||
|
||||
test("filters root sessions", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const root = await svc.create({ title: "root-session" })
|
||||
|
|
@ -192,7 +193,7 @@ describe("session.list", () => {
|
|||
|
||||
test("filters by start time", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await svc.create({ title: "new-session" })
|
||||
|
|
@ -206,7 +207,7 @@ describe("session.list", () => {
|
|||
|
||||
test("filters by search term", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await svc.create({ title: "unique-search-term-abc" })
|
||||
|
|
@ -223,7 +224,7 @@ describe("session.list", () => {
|
|||
|
||||
test("respects limit parameter", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await svc.create({ title: "session-1" })
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Server } from "../../src/server/server"
|
||||
import { Session as SessionNs } from "@/session/session"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
|
|
@ -76,7 +77,7 @@ describe("session messages endpoint", () => {
|
|||
test("returns cursor headers for older pages", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await withoutWatcher(() =>
|
||||
Instance.provide({
|
||||
WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -105,7 +106,7 @@ describe("session messages endpoint", () => {
|
|||
test("keeps full-history responses when limit is omitted", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await withoutWatcher(() =>
|
||||
Instance.provide({
|
||||
WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -126,7 +127,7 @@ describe("session messages endpoint", () => {
|
|||
test("rejects invalid cursors and missing sessions", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await withoutWatcher(() =>
|
||||
Instance.provide({
|
||||
WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -147,7 +148,7 @@ describe("session messages endpoint", () => {
|
|||
test("does not truncate large legacy limit requests", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await withoutWatcher(() =>
|
||||
Instance.provide({
|
||||
WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { Session as SessionNs } from "@/session/session"
|
|||
import type { SessionID } from "../../src/session/schema"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Server } from "../../src/server/server"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
|
||||
|
|
@ -30,7 +31,7 @@ afterEach(async () => {
|
|||
describe("tui.selectSession endpoint", () => {
|
||||
test("should return 200 when called with valid session", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
// #given
|
||||
|
|
@ -56,7 +57,7 @@ describe("tui.selectSession endpoint", () => {
|
|||
|
||||
test("should return 404 when session does not exist", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
// #given
|
||||
|
|
@ -78,7 +79,7 @@ describe("tui.selectSession endpoint", () => {
|
|||
|
||||
test("should return 400 when session ID format is invalid", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
// #given
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { LLM } from "../../src/session/llm"
|
|||
import { SessionCompaction } from "../../src/session/compaction"
|
||||
import { Token } from "@/util/token"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Permission } from "../../src/permission"
|
||||
import { Plugin } from "../../src/plugin"
|
||||
|
|
@ -792,7 +793,7 @@ describe("session.compaction.prune", () => {
|
|||
describe("session.compaction.process", () => {
|
||||
test("throws when parent is not a user message", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -822,7 +823,7 @@ describe("session.compaction.process", () => {
|
|||
|
||||
test("publishes compacted event on continue", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -872,7 +873,7 @@ describe("session.compaction.process", () => {
|
|||
|
||||
test("marks summary message as errored on compact result", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -910,7 +911,7 @@ describe("session.compaction.process", () => {
|
|||
|
||||
test("adds synthetic continue prompt when auto is enabled", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -951,7 +952,7 @@ describe("session.compaction.process", () => {
|
|||
|
||||
test("persists tail_start_id for retained recent turns", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -998,7 +999,7 @@ describe("session.compaction.process", () => {
|
|||
|
||||
test("shrinks retained tail to fit preserve token budget", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -1047,7 +1048,7 @@ describe("session.compaction.process", () => {
|
|||
captured = JSON.stringify(input.messages)
|
||||
}),
|
||||
)
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -1096,7 +1097,7 @@ describe("session.compaction.process", () => {
|
|||
captured = JSON.stringify(input.messages)
|
||||
}),
|
||||
)
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -1155,7 +1156,7 @@ describe("session.compaction.process", () => {
|
|||
captured = JSON.stringify(input.messages)
|
||||
}),
|
||||
)
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -1218,7 +1219,7 @@ describe("session.compaction.process", () => {
|
|||
|
||||
test("allows plugins to disable synthetic continue prompt", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -1261,7 +1262,7 @@ describe("session.compaction.process", () => {
|
|||
|
||||
test("replays the prior user turn on overflow when earlier context exists", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -1309,7 +1310,7 @@ describe("session.compaction.process", () => {
|
|||
|
||||
test("falls back to overflow guidance when no replayable turn exists", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -1369,7 +1370,7 @@ describe("session.compaction.process", () => {
|
|||
)
|
||||
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -1443,7 +1444,7 @@ describe("session.compaction.process", () => {
|
|||
const ready = defer()
|
||||
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -1545,7 +1546,7 @@ describe("session.compaction.process", () => {
|
|||
)
|
||||
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -1587,7 +1588,7 @@ describe("session.compaction.process", () => {
|
|||
)
|
||||
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -1639,7 +1640,7 @@ describe("session.compaction.process", () => {
|
|||
)
|
||||
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -1707,7 +1708,7 @@ describe("session.compaction.process", () => {
|
|||
stub.push(reply("summary one"))
|
||||
stub.push(reply("summary two"))
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -1779,7 +1780,7 @@ describe("session.compaction.process", () => {
|
|||
|
||||
test("ignores previous summaries when sizing the retained tail", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import z from "zod"
|
|||
import { makeRuntime } from "../../src/effect/run-service"
|
||||
import { LLM } from "../../src/session/llm"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ProviderTransform } from "@/provider/transform"
|
||||
import { ModelsDev } from "@/provider/models"
|
||||
|
|
@ -338,7 +339,7 @@ describe("session.llm.stream", () => {
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id))
|
||||
|
|
@ -425,7 +426,7 @@ describe("session.llm.stream", () => {
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id))
|
||||
|
|
@ -515,7 +516,7 @@ describe("session.llm.stream", () => {
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id))
|
||||
|
|
@ -629,7 +630,7 @@ describe("session.llm.stream", () => {
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const resolved = await getModel(ProviderID.openai, ModelID.make(model.id))
|
||||
|
|
@ -745,7 +746,7 @@ describe("session.llm.stream", () => {
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const resolved = await getModel(ProviderID.openai, ModelID.make(model.id))
|
||||
|
|
@ -864,7 +865,7 @@ describe("session.llm.stream", () => {
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id))
|
||||
|
|
@ -982,7 +983,7 @@ describe("session.llm.stream", () => {
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const resolved = await getModel(ProviderID.make("anthropic"), ModelID.make(model.id))
|
||||
|
|
@ -1223,7 +1224,7 @@ describe("session.llm.stream", () => {
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id))
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
|
|||
import { Effect } from "effect"
|
||||
import path from "path"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Session as SessionNs } from "@/session/session"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { MessageID, PartID, type SessionID } from "../../src/session/schema"
|
||||
|
|
@ -123,7 +124,7 @@ async function addCompactionPart(sessionID: SessionID, messageID: MessageID, tai
|
|||
|
||||
describe("MessageV2.page", () => {
|
||||
test("returns sync result", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -139,7 +140,7 @@ describe("MessageV2.page", () => {
|
|||
})
|
||||
|
||||
test("pages backward with opaque cursors", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -167,7 +168,7 @@ describe("MessageV2.page", () => {
|
|||
})
|
||||
|
||||
test("returns items in chronological order within a page", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -182,7 +183,7 @@ describe("MessageV2.page", () => {
|
|||
})
|
||||
|
||||
test("returns empty items for session with no messages", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -198,7 +199,7 @@ describe("MessageV2.page", () => {
|
|||
})
|
||||
|
||||
test("throws NotFoundError for non-existent session", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const fake = "non-existent-session" as SessionID
|
||||
|
|
@ -208,7 +209,7 @@ describe("MessageV2.page", () => {
|
|||
})
|
||||
|
||||
test("handles exact limit boundary", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -225,7 +226,7 @@ describe("MessageV2.page", () => {
|
|||
})
|
||||
|
||||
test("limit of 1 returns single newest message", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -242,7 +243,7 @@ describe("MessageV2.page", () => {
|
|||
})
|
||||
|
||||
test("hydrates multiple parts per message", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -266,7 +267,7 @@ describe("MessageV2.page", () => {
|
|||
})
|
||||
|
||||
test("accepts cursors from fractional timestamps", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -284,7 +285,7 @@ describe("MessageV2.page", () => {
|
|||
})
|
||||
|
||||
test("messages with same timestamp are ordered by id", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -304,7 +305,7 @@ describe("MessageV2.page", () => {
|
|||
})
|
||||
|
||||
test("does not return messages from other sessions", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const a = await svc.create({})
|
||||
|
|
@ -326,7 +327,7 @@ describe("MessageV2.page", () => {
|
|||
})
|
||||
|
||||
test("large limit returns all messages without cursor", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -346,7 +347,7 @@ describe("MessageV2.page", () => {
|
|||
|
||||
describe("MessageV2.stream", () => {
|
||||
test("yields items newest first", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -361,7 +362,7 @@ describe("MessageV2.stream", () => {
|
|||
})
|
||||
|
||||
test("yields nothing for empty session", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -375,7 +376,7 @@ describe("MessageV2.stream", () => {
|
|||
})
|
||||
|
||||
test("yields single message", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -391,7 +392,7 @@ describe("MessageV2.stream", () => {
|
|||
})
|
||||
|
||||
test("hydrates parts for each yielded message", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -409,7 +410,7 @@ describe("MessageV2.stream", () => {
|
|||
})
|
||||
|
||||
test("handles sets exceeding internal page size", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -426,7 +427,7 @@ describe("MessageV2.stream", () => {
|
|||
})
|
||||
|
||||
test("is a sync generator", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -447,7 +448,7 @@ describe("MessageV2.stream", () => {
|
|||
|
||||
describe("MessageV2.parts", () => {
|
||||
test("returns parts for a message", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -464,7 +465,7 @@ describe("MessageV2.parts", () => {
|
|||
})
|
||||
|
||||
test("returns empty array for message with no parts", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -479,7 +480,7 @@ describe("MessageV2.parts", () => {
|
|||
})
|
||||
|
||||
test("returns multiple parts in order", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -512,7 +513,7 @@ describe("MessageV2.parts", () => {
|
|||
})
|
||||
|
||||
test("returns empty for non-existent message id", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
await svc.create({})
|
||||
|
|
@ -523,7 +524,7 @@ describe("MessageV2.parts", () => {
|
|||
})
|
||||
|
||||
test("parts contain sessionID and messageID", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -541,7 +542,7 @@ describe("MessageV2.parts", () => {
|
|||
|
||||
describe("MessageV2.get", () => {
|
||||
test("returns message with hydrated parts", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -560,7 +561,7 @@ describe("MessageV2.get", () => {
|
|||
})
|
||||
|
||||
test("throws NotFoundError for non-existent message", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -575,7 +576,7 @@ describe("MessageV2.get", () => {
|
|||
})
|
||||
|
||||
test("scopes by session id", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const a = await svc.create({})
|
||||
|
|
@ -593,7 +594,7 @@ describe("MessageV2.get", () => {
|
|||
})
|
||||
|
||||
test("returns message with multiple parts", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -616,7 +617,7 @@ describe("MessageV2.get", () => {
|
|||
})
|
||||
|
||||
test("returns assistant message with correct role", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -642,7 +643,7 @@ describe("MessageV2.get", () => {
|
|||
})
|
||||
|
||||
test("returns message with zero parts", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -660,7 +661,7 @@ describe("MessageV2.get", () => {
|
|||
|
||||
describe("MessageV2.filterCompacted", () => {
|
||||
test("returns all messages when no compaction", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -677,7 +678,7 @@ describe("MessageV2.filterCompacted", () => {
|
|||
})
|
||||
|
||||
test("stops at compaction boundary and returns chronological order", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -721,7 +722,7 @@ describe("MessageV2.filterCompacted", () => {
|
|||
})
|
||||
|
||||
test("does not break on compaction part without matching summary", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -739,7 +740,7 @@ describe("MessageV2.filterCompacted", () => {
|
|||
})
|
||||
|
||||
test("skips assistant with error even if marked as summary", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -764,7 +765,7 @@ describe("MessageV2.filterCompacted", () => {
|
|||
})
|
||||
|
||||
test("skips assistant without finish even if marked as summary", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -785,7 +786,7 @@ describe("MessageV2.filterCompacted", () => {
|
|||
})
|
||||
|
||||
test("retains original tail when compaction stores tail_start_id", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -841,7 +842,7 @@ describe("MessageV2.filterCompacted", () => {
|
|||
})
|
||||
|
||||
test("fork remaps compaction tail_start_id for filterCompacted", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -907,7 +908,7 @@ describe("MessageV2.filterCompacted", () => {
|
|||
})
|
||||
|
||||
test("retains an assistant tail when compaction starts inside a turn", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -971,7 +972,7 @@ describe("MessageV2.filterCompacted", () => {
|
|||
})
|
||||
|
||||
test("prefers latest compaction boundary when repeated compactions exist", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -1093,7 +1094,7 @@ describe("MessageV2.cursor", () => {
|
|||
|
||||
describe("MessageV2 consistency", () => {
|
||||
test("page hydration matches get for each message", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -1112,7 +1113,7 @@ describe("MessageV2 consistency", () => {
|
|||
})
|
||||
|
||||
test("parts from get match standalone parts call", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -1128,7 +1129,7 @@ describe("MessageV2 consistency", () => {
|
|||
})
|
||||
|
||||
test("stream collects same messages as exhaustive page iteration", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
@ -1155,7 +1156,7 @@ describe("MessageV2 consistency", () => {
|
|||
})
|
||||
|
||||
test("filterCompacted of full stream returns same as Array.from when no compaction", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: root,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { Session as SessionNs } from "@/session/session"
|
|||
import { Bus } from "../../src/bus"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { MessageID, PartID, type SessionID } from "../../src/session/schema"
|
||||
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||
|
|
@ -34,7 +35,7 @@ function updatePart<T extends MessageV2.Part>(part: T) {
|
|||
|
||||
describe("session.created event", () => {
|
||||
test("should emit session.created event when session is created", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
let eventReceived = false
|
||||
|
|
@ -63,7 +64,7 @@ describe("session.created event", () => {
|
|||
})
|
||||
|
||||
test("session.created event should be emitted before session.updated", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const events: string[] = []
|
||||
|
|
@ -95,7 +96,7 @@ describe("step-finish token propagation via Bus event", () => {
|
|||
test(
|
||||
"non-zero tokens propagate through PartUpdated event",
|
||||
async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const info = await create({})
|
||||
|
|
@ -166,7 +167,7 @@ describe("Session", () => {
|
|||
test("remove works without an instance", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
const info = await Instance.provide({
|
||||
const info = await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: () => create({ title: "remove-without-instance" }),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Session } from "@/session/session"
|
|||
import { SessionPrompt } from "../../src/session/prompt"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
|
||||
const projectRoot = path.join(__dirname, "../..")
|
||||
|
|
@ -15,7 +16,7 @@ const hasApiKey = !!process.env.ANTHROPIC_API_KEY
|
|||
|
||||
// Helper to run test within Instance context
|
||||
async function withInstance<T>(fn: () => Promise<T>): Promise<T> {
|
||||
return Instance.provide({
|
||||
return WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import path from "path"
|
|||
import { Effect } from "effect"
|
||||
import { Snapshot } from "../../src/snapshot"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { disposeAllInstances, provideInstance, tmpdir } from "../fixture/fixture"
|
||||
|
||||
|
|
@ -47,7 +48,7 @@ function run<A>(dir: string, body: (snapshot: Snapshot.Interface) => Effect.Effe
|
|||
|
||||
test("tracks deleted files correctly", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -62,7 +63,7 @@ test("tracks deleted files correctly", async () => {
|
|||
|
||||
test("revert should remove new files", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -86,7 +87,7 @@ test("revert should remove new files", async () => {
|
|||
|
||||
test("revert in subdirectory", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -113,7 +114,7 @@ test("revert in subdirectory", async () => {
|
|||
|
||||
test("multiple file operations", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -145,7 +146,7 @@ test("multiple file operations", async () => {
|
|||
|
||||
test("empty directory handling", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -160,7 +161,7 @@ test("empty directory handling", async () => {
|
|||
|
||||
test("binary file handling", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -184,7 +185,7 @@ test("binary file handling", async () => {
|
|||
|
||||
test("symlink handling", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -199,7 +200,7 @@ test("symlink handling", async () => {
|
|||
|
||||
test("file under size limit handling", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -214,7 +215,7 @@ test("file under size limit handling", async () => {
|
|||
|
||||
test("large added files are skipped", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -231,7 +232,7 @@ test("large added files are skipped", async () => {
|
|||
|
||||
test("nested directory revert", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -256,7 +257,7 @@ test("nested directory revert", async () => {
|
|||
|
||||
test("special characters in filenames", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -276,7 +277,7 @@ test("special characters in filenames", async () => {
|
|||
|
||||
test("revert with empty patches", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
// Should not crash with empty patches
|
||||
|
|
@ -290,7 +291,7 @@ test("revert with empty patches", async () => {
|
|||
|
||||
test("patch with invalid hash", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -309,7 +310,7 @@ test("patch with invalid hash", async () => {
|
|||
|
||||
test("revert non-existent file", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -333,7 +334,7 @@ test("revert non-existent file", async () => {
|
|||
|
||||
test("unicode filenames", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -373,7 +374,7 @@ test("unicode filenames", async () => {
|
|||
|
||||
test.skip("unicode filenames modification and restore", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const chineseFile = fwd(tmp.path, "文件.txt")
|
||||
|
|
@ -402,7 +403,7 @@ test.skip("unicode filenames modification and restore", async () => {
|
|||
|
||||
test("unicode filenames in subdirectories", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -428,7 +429,7 @@ test("unicode filenames in subdirectories", async () => {
|
|||
|
||||
test("very long filenames", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -455,7 +456,7 @@ test("very long filenames", async () => {
|
|||
|
||||
test("hidden files", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -475,7 +476,7 @@ test("hidden files", async () => {
|
|||
|
||||
test("nested symlinks", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -495,7 +496,7 @@ test("nested symlinks", async () => {
|
|||
|
||||
test("file permissions and ownership changes", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -516,7 +517,7 @@ test("file permissions and ownership changes", async () => {
|
|||
|
||||
test("circular symlinks", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -547,7 +548,7 @@ test("source project gitignore is respected - ignored files are not snapshotted"
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -576,7 +577,7 @@ test("source project gitignore is respected - ignored files are not snapshotted"
|
|||
|
||||
test("gitignore changes", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -600,7 +601,7 @@ test("gitignore changes", async () => {
|
|||
|
||||
test("files tracked in snapshot but now gitignored are filtered out", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
// First, create a file and snapshot it
|
||||
|
|
@ -634,7 +635,7 @@ test("files tracked in snapshot but now gitignored are filtered out", async () =
|
|||
|
||||
test("gitignore updated between track calls filters from diff", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
// a.txt is already committed from bootstrap - track it in snapshot
|
||||
|
|
@ -669,7 +670,7 @@ test("gitignore updated between track calls filters from diff", async () => {
|
|||
|
||||
test("git info exclude changes", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -695,7 +696,7 @@ test("git info exclude changes", async () => {
|
|||
|
||||
test("git info exclude keeps global excludes", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const global = `${tmp.path}/global.ignore`
|
||||
|
|
@ -731,7 +732,7 @@ test("git info exclude keeps global excludes", async () => {
|
|||
|
||||
test("concurrent file operations during patch", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -763,7 +764,7 @@ test("snapshot state isolation between projects", async () => {
|
|||
await using tmp1 = await bootstrap()
|
||||
await using tmp2 = await bootstrap()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp1.path,
|
||||
fn: async () => {
|
||||
const before1 = await run(tmp1.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -773,7 +774,7 @@ test("snapshot state isolation between projects", async () => {
|
|||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp2.path,
|
||||
fn: async () => {
|
||||
const before2 = await run(tmp2.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -793,14 +794,14 @@ test("patch detects changes in secondary worktree", async () => {
|
|||
await $`git worktree add ${worktreePath} HEAD`.cwd(tmp.path).quiet()
|
||||
|
||||
try {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
expect(await run(tmp.path, (snapshot) => snapshot.track())).toBeTruthy()
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: worktreePath,
|
||||
fn: async () => {
|
||||
const before = await run(worktreePath, (snapshot) => snapshot.track())
|
||||
|
|
@ -825,7 +826,7 @@ test("revert only removes files in invoking worktree", async () => {
|
|||
await $`git worktree add ${worktreePath} HEAD`.cwd(tmp.path).quiet()
|
||||
|
||||
try {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
expect(await run(tmp.path, (snapshot) => snapshot.track())).toBeTruthy()
|
||||
|
|
@ -834,7 +835,7 @@ test("revert only removes files in invoking worktree", async () => {
|
|||
const primaryFile = `${tmp.path}/worktree.txt`
|
||||
await Filesystem.write(primaryFile, "primary content")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: worktreePath,
|
||||
fn: async () => {
|
||||
const before = await run(worktreePath, (snapshot) => snapshot.track())
|
||||
|
|
@ -869,14 +870,14 @@ test("diff reports worktree-only/shared edits and ignores primary-only", async (
|
|||
await $`git worktree add ${worktreePath} HEAD`.cwd(tmp.path).quiet()
|
||||
|
||||
try {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
expect(await run(tmp.path, (snapshot) => snapshot.track())).toBeTruthy()
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: worktreePath,
|
||||
fn: async () => {
|
||||
const before = await run(worktreePath, (snapshot) => snapshot.track())
|
||||
|
|
@ -903,7 +904,7 @@ test("diff reports worktree-only/shared edits and ignores primary-only", async (
|
|||
|
||||
test("track with no changes returns same hash", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const hash1 = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -922,7 +923,7 @@ test("track with no changes returns same hash", async () => {
|
|||
|
||||
test("diff function with various changes", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -943,7 +944,7 @@ test("diff function with various changes", async () => {
|
|||
|
||||
test("restore function", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -977,7 +978,7 @@ test("restore function", async () => {
|
|||
|
||||
test("revert should not delete files that existed but were deleted in snapshot", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const snapshot1 = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -1007,7 +1008,7 @@ test("revert should not delete files that existed but were deleted in snapshot",
|
|||
|
||||
test("revert preserves file that existed in snapshot when deleted then recreated", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await Filesystem.write(`${tmp.path}/existing.txt`, "original content")
|
||||
|
|
@ -1044,7 +1045,7 @@ test("revert preserves file that existed in snapshot when deleted then recreated
|
|||
|
||||
test("diffFull sets status based on git change type", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await Filesystem.write(`${tmp.path}/grow.txt`, "one\n")
|
||||
|
|
@ -1090,7 +1091,7 @@ test("diffFull sets status based on git change type", async () => {
|
|||
|
||||
test("diffFull with new file additions", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -1115,7 +1116,7 @@ test("diffFull with new file additions", async () => {
|
|||
|
||||
test("diffFull with a large interleaved mixed diff", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const ids = Array.from({ length: 60 }, (_, i) => i.toString().padStart(3, "0"))
|
||||
|
|
@ -1178,7 +1179,7 @@ test("diffFull with a large interleaved mixed diff", async () => {
|
|||
|
||||
test("diffFull preserves git diff order across batch boundaries", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const ids = Array.from({ length: 140 }, (_, i) => i.toString().padStart(3, "0"))
|
||||
|
|
@ -1204,7 +1205,7 @@ test("diffFull preserves git diff order across batch boundaries", async () => {
|
|||
|
||||
test("diffFull with file modifications", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -1230,7 +1231,7 @@ test("diffFull with file modifications", async () => {
|
|||
|
||||
test("diffFull with file deletions", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -1255,7 +1256,7 @@ test("diffFull with file deletions", async () => {
|
|||
|
||||
test("diffFull with multiple line additions", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -1281,7 +1282,7 @@ test("diffFull with multiple line additions", async () => {
|
|||
|
||||
test("diffFull with addition and deletion", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -1313,7 +1314,7 @@ test("diffFull with addition and deletion", async () => {
|
|||
|
||||
test("diffFull with multiple additions and deletions", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -1355,7 +1356,7 @@ test("diffFull with multiple additions and deletions", async () => {
|
|||
|
||||
test("diffFull with no changes", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -1372,7 +1373,7 @@ test("diffFull with no changes", async () => {
|
|||
|
||||
test("diffFull with binary file changes", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
||||
|
|
@ -1395,7 +1396,7 @@ test("diffFull with binary file changes", async () => {
|
|||
|
||||
test("diffFull with whitespace changes", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await Filesystem.write(`${tmp.path}/whitespace.txt`, "line1\nline2")
|
||||
|
|
@ -1419,7 +1420,7 @@ test("diffFull with whitespace changes", async () => {
|
|||
|
||||
test("revert with overlapping files across patches uses first patch hash", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
// Write initial content and snapshot
|
||||
|
|
@ -1453,7 +1454,7 @@ test("revert with overlapping files across patches uses first patch hash", async
|
|||
|
||||
test("revert preserves patch order when the same hash appears again", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await $`mkdir -p ${tmp.path}/foo`.quiet()
|
||||
|
|
@ -1490,7 +1491,7 @@ test("revert preserves patch order when the same hash appears again", async () =
|
|||
|
||||
test("revert handles large mixed batches across chunk boundaries", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const base = Array.from({ length: 140 }, (_, i) => fwd(tmp.path, "batch", `${i}.txt`))
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import * as fs from "fs/promises"
|
|||
import { Effect, ManagedRuntime, Layer } from "effect"
|
||||
import { ApplyPatchTool } from "../../src/tool/apply_patch"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Format } from "../../src/format"
|
||||
|
|
@ -97,7 +98,7 @@ describe("tool.apply_patch freeform", () => {
|
|||
await using fixture = await tmpdir({ git: true })
|
||||
const { ctx, calls } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const modifyPath = path.join(fixture.path, "modify.txt")
|
||||
|
|
@ -149,7 +150,7 @@ describe("tool.apply_patch freeform", () => {
|
|||
await using fixture = await tmpdir({ git: true })
|
||||
const { ctx, calls } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const original = path.join(fixture.path, "old", "name.txt")
|
||||
|
|
@ -179,7 +180,7 @@ describe("tool.apply_patch freeform", () => {
|
|||
await using fixture = await tmpdir()
|
||||
const { ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const target = path.join(fixture.path, "multi.txt")
|
||||
|
|
@ -199,7 +200,7 @@ describe("tool.apply_patch freeform", () => {
|
|||
await using fixture = await tmpdir()
|
||||
const { ctx, calls } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const bom = String.fromCharCode(0xfeff)
|
||||
|
|
@ -228,7 +229,7 @@ describe("tool.apply_patch freeform", () => {
|
|||
await using fixture = await tmpdir()
|
||||
const { ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const target = path.join(fixture.path, "insert_only.txt")
|
||||
|
|
@ -247,7 +248,7 @@ describe("tool.apply_patch freeform", () => {
|
|||
await using fixture = await tmpdir()
|
||||
const { ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const target = path.join(fixture.path, "no_newline.txt")
|
||||
|
|
@ -269,7 +270,7 @@ describe("tool.apply_patch freeform", () => {
|
|||
await using fixture = await tmpdir()
|
||||
const { ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const original = path.join(fixture.path, "old", "name.txt")
|
||||
|
|
@ -292,7 +293,7 @@ describe("tool.apply_patch freeform", () => {
|
|||
await using fixture = await tmpdir()
|
||||
const { ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const original = path.join(fixture.path, "old", "name.txt")
|
||||
|
|
@ -317,7 +318,7 @@ describe("tool.apply_patch freeform", () => {
|
|||
await using fixture = await tmpdir()
|
||||
const { ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const target = path.join(fixture.path, "duplicate.txt")
|
||||
|
|
@ -335,7 +336,7 @@ describe("tool.apply_patch freeform", () => {
|
|||
await using fixture = await tmpdir()
|
||||
const { ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const patchText = "*** Begin Patch\n*** Update File: missing.txt\n@@\n-nope\n+better\n*** End Patch"
|
||||
|
|
@ -351,7 +352,7 @@ describe("tool.apply_patch freeform", () => {
|
|||
await using fixture = await tmpdir()
|
||||
const { ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const patchText = "*** Begin Patch\n*** Delete File: missing.txt\n*** End Patch"
|
||||
|
|
@ -365,7 +366,7 @@ describe("tool.apply_patch freeform", () => {
|
|||
await using fixture = await tmpdir()
|
||||
const { ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const dirPath = path.join(fixture.path, "dir")
|
||||
|
|
@ -382,7 +383,7 @@ describe("tool.apply_patch freeform", () => {
|
|||
await using fixture = await tmpdir()
|
||||
const { ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const patchText = "*** Begin Patch\n*** Frobnicate File: foo\n*** End Patch"
|
||||
|
|
@ -396,7 +397,7 @@ describe("tool.apply_patch freeform", () => {
|
|||
await using fixture = await tmpdir()
|
||||
const { ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const target = path.join(fixture.path, "modify.txt")
|
||||
|
|
@ -414,7 +415,7 @@ describe("tool.apply_patch freeform", () => {
|
|||
await using fixture = await tmpdir()
|
||||
const { ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const patchText =
|
||||
|
|
@ -432,7 +433,7 @@ describe("tool.apply_patch freeform", () => {
|
|||
await using fixture = await tmpdir()
|
||||
const { ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const target = path.join(fixture.path, "tail.txt")
|
||||
|
|
@ -450,7 +451,7 @@ describe("tool.apply_patch freeform", () => {
|
|||
await using fixture = await tmpdir()
|
||||
const { ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const target = path.join(fixture.path, "two_chunks.txt")
|
||||
|
|
@ -468,7 +469,7 @@ describe("tool.apply_patch freeform", () => {
|
|||
await using fixture = await tmpdir()
|
||||
const { ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const target = path.join(fixture.path, "multi_ctx.txt")
|
||||
|
|
@ -486,7 +487,7 @@ describe("tool.apply_patch freeform", () => {
|
|||
await using fixture = await tmpdir()
|
||||
const { ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const target = path.join(fixture.path, "eof_anchor.txt")
|
||||
|
|
@ -508,7 +509,7 @@ describe("tool.apply_patch freeform", () => {
|
|||
await using fixture = await tmpdir()
|
||||
const { ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const patchText = `cat <<'EOF'
|
||||
|
|
@ -529,7 +530,7 @@ EOF`
|
|||
await using fixture = await tmpdir()
|
||||
const { ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const patchText = `<<EOF
|
||||
|
|
@ -550,7 +551,7 @@ EOF`
|
|||
await using fixture = await tmpdir()
|
||||
const { ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const target = path.join(fixture.path, "trailing_ws.txt")
|
||||
|
|
@ -570,7 +571,7 @@ EOF`
|
|||
await using fixture = await tmpdir()
|
||||
const { ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const target = path.join(fixture.path, "leading_ws.txt")
|
||||
|
|
@ -590,7 +591,7 @@ EOF`
|
|||
await using fixture = await tmpdir()
|
||||
const { ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
const target = path.join(fixture.path, "unicode.txt")
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import fs from "fs/promises"
|
|||
import { Effect, Layer, ManagedRuntime } from "effect"
|
||||
import { EditTool } from "../../src/tool/edit"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
|
|
@ -73,7 +74,7 @@ describe("tool.edit", () => {
|
|||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "newfile.txt")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const edit = await resolve()
|
||||
|
|
@ -102,7 +103,7 @@ describe("tool.edit", () => {
|
|||
const bom = String.fromCharCode(0xfeff)
|
||||
await fs.writeFile(filepath, `${bom}using System;\n`, "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const edit = await resolve()
|
||||
|
|
@ -131,7 +132,7 @@ describe("tool.edit", () => {
|
|||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "nested", "dir", "file.txt")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const edit = await resolve()
|
||||
|
|
@ -156,7 +157,7 @@ describe("tool.edit", () => {
|
|||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "new.txt")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const { FileWatcher } = await import("../../src/file/watcher")
|
||||
|
|
@ -191,7 +192,7 @@ describe("tool.edit", () => {
|
|||
const filepath = path.join(tmp.path, "existing.txt")
|
||||
await fs.writeFile(filepath, "old content here", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const edit = await resolve()
|
||||
|
|
@ -220,7 +221,7 @@ describe("tool.edit", () => {
|
|||
const bom = String.fromCharCode(0xfeff)
|
||||
await fs.writeFile(filepath, `${bom}using System;\nclass Test {}\n`, "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const edit = await resolve()
|
||||
|
|
@ -250,7 +251,7 @@ describe("tool.edit", () => {
|
|||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "nonexistent.txt")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const edit = await resolve()
|
||||
|
|
@ -275,7 +276,7 @@ describe("tool.edit", () => {
|
|||
const filepath = path.join(tmp.path, "file.txt")
|
||||
await fs.writeFile(filepath, "content", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const edit = await resolve()
|
||||
|
|
@ -300,7 +301,7 @@ describe("tool.edit", () => {
|
|||
const filepath = path.join(tmp.path, "file.txt")
|
||||
await fs.writeFile(filepath, "actual content", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const edit = await resolve()
|
||||
|
|
@ -325,7 +326,7 @@ describe("tool.edit", () => {
|
|||
const filepath = path.join(tmp.path, "file.txt")
|
||||
await fs.writeFile(filepath, "foo bar foo baz foo", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const edit = await resolve()
|
||||
|
|
@ -352,7 +353,7 @@ describe("tool.edit", () => {
|
|||
const filepath = path.join(tmp.path, "file.txt")
|
||||
await fs.writeFile(filepath, "original", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const { FileWatcher } = await import("../../src/file/watcher")
|
||||
|
|
@ -387,7 +388,7 @@ describe("tool.edit", () => {
|
|||
const filepath = path.join(tmp.path, "file.txt")
|
||||
await fs.writeFile(filepath, "line1\nline2\nline3", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const edit = await resolve()
|
||||
|
|
@ -413,7 +414,7 @@ describe("tool.edit", () => {
|
|||
const filepath = path.join(tmp.path, "file.txt")
|
||||
await fs.writeFile(filepath, "line1\r\nold\r\nline3", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const edit = await resolve()
|
||||
|
|
@ -439,7 +440,7 @@ describe("tool.edit", () => {
|
|||
const filepath = path.join(tmp.path, "file.txt")
|
||||
await fs.writeFile(filepath, "content", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const edit = await resolve()
|
||||
|
|
@ -464,7 +465,7 @@ describe("tool.edit", () => {
|
|||
const dirpath = path.join(tmp.path, "adir")
|
||||
await fs.mkdir(dirpath)
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const edit = await resolve()
|
||||
|
|
@ -489,7 +490,7 @@ describe("tool.edit", () => {
|
|||
const filepath = path.join(tmp.path, "file.txt")
|
||||
await fs.writeFile(filepath, "line1\nline2\nline3", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const edit = await resolve()
|
||||
|
|
@ -558,7 +559,7 @@ describe("tool.edit", () => {
|
|||
},
|
||||
})
|
||||
|
||||
return await Instance.provide({
|
||||
return await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const edit = await resolve()
|
||||
|
|
@ -702,7 +703,7 @@ describe("tool.edit", () => {
|
|||
const filepath = path.join(tmp.path, "file.txt")
|
||||
await fs.writeFile(filepath, "top = 0\nmiddle = keep\nbottom = 0\n", "utf-8")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const edit = await resolve()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import path from "path"
|
|||
import { Effect } from "effect"
|
||||
import type { Tool } from "@/tool/tool"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { assertExternalDirectory } from "../../src/tool/external-directory"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
|
@ -38,7 +39,7 @@ describe("tool.assertExternalDirectory", () => {
|
|||
test("no-ops for empty target", async () => {
|
||||
const { requests, ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: "/tmp",
|
||||
fn: async () => {
|
||||
await assertExternalDirectory(ctx)
|
||||
|
|
@ -51,7 +52,7 @@ describe("tool.assertExternalDirectory", () => {
|
|||
test("no-ops for paths inside Instance.directory", async () => {
|
||||
const { requests, ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: "/tmp/project",
|
||||
fn: async () => {
|
||||
await assertExternalDirectory(ctx, path.join("/tmp/project", "file.txt"))
|
||||
|
|
@ -68,7 +69,7 @@ describe("tool.assertExternalDirectory", () => {
|
|||
const target = "/tmp/outside/file.txt"
|
||||
const expected = glob(path.join(path.dirname(target), "*"))
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory,
|
||||
fn: async () => {
|
||||
await assertExternalDirectory(ctx, target)
|
||||
|
|
@ -88,7 +89,7 @@ describe("tool.assertExternalDirectory", () => {
|
|||
const target = "/tmp/outside"
|
||||
const expected = glob(path.join(target, "*"))
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory,
|
||||
fn: async () => {
|
||||
await assertExternalDirectory(ctx, target, { kind: "directory" })
|
||||
|
|
@ -104,7 +105,7 @@ describe("tool.assertExternalDirectory", () => {
|
|||
test("skips prompting when bypass=true", async () => {
|
||||
const { requests, ctx } = makeCtx()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: "/tmp/project",
|
||||
fn: async () => {
|
||||
await assertExternalDirectory(ctx, "/tmp/outside/file.txt", { bypass: true })
|
||||
|
|
@ -131,7 +132,7 @@ describe("tool.assertExternalDirectory", () => {
|
|||
.replaceAll("\\", "/")
|
||||
.toLowerCase()
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await assertExternalDirectory(ctx, alt)
|
||||
|
|
@ -152,7 +153,7 @@ describe("tool.assertExternalDirectory", () => {
|
|||
const root = path.parse(tmp.path).root
|
||||
const target = path.join(root, "boot.ini")
|
||||
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await assertExternalDirectory(ctx, target)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { Config } from "@/config/config"
|
|||
import { Shell } from "../../src/shell/shell"
|
||||
import { ShellTool } from "../../src/tool/shell"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import type { Permission } from "../../src/permission"
|
||||
|
|
@ -140,7 +141,7 @@ const mustTruncate = (result: {
|
|||
|
||||
describe("tool.shell", () => {
|
||||
each("basic", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
@ -163,7 +164,7 @@ describe("tool.shell", () => {
|
|||
await using tmp = await tmpdir({
|
||||
config: { shell: "fish" },
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const bash = await initBash()
|
||||
|
|
@ -190,7 +191,7 @@ describe("tool.shell", () => {
|
|||
describe("tool.shell permissions", () => {
|
||||
each("asks for bash permission with correct pattern", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
@ -213,7 +214,7 @@ describe("tool.shell permissions", () => {
|
|||
|
||||
each("asks for bash permission with multiple commands", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
@ -239,7 +240,7 @@ describe("tool.shell permissions", () => {
|
|||
test(
|
||||
`parses PowerShell conditionals for permission prompts [${item.label}]`,
|
||||
withShell(item, async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
@ -269,7 +270,7 @@ describe("tool.shell permissions", () => {
|
|||
`uses PowerShell cmdlet prefixes for always-allow prompts [${item.label}]`,
|
||||
withShell(item, async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
@ -297,7 +298,7 @@ describe("tool.shell permissions", () => {
|
|||
}
|
||||
|
||||
each("asks for external_directory permission for wildcard external paths", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
@ -333,7 +334,7 @@ describe("tool.shell permissions", () => {
|
|||
await Bun.write(path.join(dir, "outside.txt"), "x")
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
@ -366,7 +367,7 @@ describe("tool.shell permissions", () => {
|
|||
test(
|
||||
`asks for external_directory permission for PowerShell paths after switches [${item.label}]`,
|
||||
withShell(item, async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
@ -396,7 +397,7 @@ describe("tool.shell permissions", () => {
|
|||
test(
|
||||
`asks for nested PowerShell command permissions [${item.label}]`,
|
||||
withShell(item, async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
@ -428,7 +429,7 @@ describe("tool.shell permissions", () => {
|
|||
`asks for external_directory permission for drive-relative PowerShell paths [${item.label}]`,
|
||||
withShell(item, async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
@ -458,7 +459,7 @@ describe("tool.shell permissions", () => {
|
|||
test(
|
||||
`asks for external_directory permission for $HOME PowerShell paths [${item.label}]`,
|
||||
withShell(item, async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
@ -489,7 +490,7 @@ describe("tool.shell permissions", () => {
|
|||
`asks for external_directory permission for $PWD PowerShell paths [${item.label}]`,
|
||||
withShell(item, async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const bash = await initBash()
|
||||
|
|
@ -519,7 +520,7 @@ describe("tool.shell permissions", () => {
|
|||
test(
|
||||
`asks for external_directory permission for $PSHOME PowerShell paths [${item.label}]`,
|
||||
withShell(item, async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initBash()
|
||||
|
|
@ -553,7 +554,7 @@ describe("tool.shell permissions", () => {
|
|||
const prev = process.env[key]
|
||||
delete process.env[key]
|
||||
try {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
@ -588,7 +589,7 @@ describe("tool.shell permissions", () => {
|
|||
test(
|
||||
`asks for external_directory permission for PowerShell env paths [${item.label}]`,
|
||||
withShell(item, async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initBash()
|
||||
|
|
@ -617,7 +618,7 @@ describe("tool.shell permissions", () => {
|
|||
test(
|
||||
`asks for external_directory permission for PowerShell FileSystem paths [${item.label}]`,
|
||||
withShell(item, async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initBash()
|
||||
|
|
@ -649,7 +650,7 @@ describe("tool.shell permissions", () => {
|
|||
test(
|
||||
`asks for external_directory permission for braced PowerShell env paths [${item.label}]`,
|
||||
withShell(item, async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initBash()
|
||||
|
|
@ -681,7 +682,7 @@ describe("tool.shell permissions", () => {
|
|||
test(
|
||||
`treats Set-Location like cd for permissions [${item.label}]`,
|
||||
withShell(item, async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initBash()
|
||||
|
|
@ -712,7 +713,7 @@ describe("tool.shell permissions", () => {
|
|||
test(
|
||||
`does not add nested PowerShell expressions to permission prompts [${item.label}]`,
|
||||
withShell(item, async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
@ -741,7 +742,7 @@ describe("tool.shell permissions", () => {
|
|||
test(
|
||||
"asks for external_directory permission for cmd file commands [cmd]",
|
||||
withShell(cmdShell, async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
@ -766,7 +767,7 @@ describe("tool.shell permissions", () => {
|
|||
|
||||
each("asks for external_directory permission when cd to parent", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const bash = await initBash()
|
||||
|
|
@ -791,7 +792,7 @@ describe("tool.shell permissions", () => {
|
|||
|
||||
each("asks for external_directory permission when workdir is outside project", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const bash = await initBash()
|
||||
|
|
@ -821,7 +822,7 @@ describe("tool.shell permissions", () => {
|
|||
const err = new Error("stop after permission")
|
||||
await using outerTmp = await tmpdir()
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const bash = await initBash()
|
||||
|
|
@ -857,7 +858,7 @@ describe("tool.shell permissions", () => {
|
|||
test(
|
||||
"uses Git Bash /tmp semantics for external workdir",
|
||||
withShell({ label: "bash", shell: bash }, async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initBash()
|
||||
|
|
@ -889,7 +890,7 @@ describe("tool.shell permissions", () => {
|
|||
test(
|
||||
"uses Git Bash /tmp semantics for external file paths",
|
||||
withShell({ label: "bash", shell: bash }, async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initBash()
|
||||
|
|
@ -926,7 +927,7 @@ describe("tool.shell permissions", () => {
|
|||
},
|
||||
})
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const bash = await initBash()
|
||||
|
|
@ -959,7 +960,7 @@ describe("tool.shell permissions", () => {
|
|||
await Bun.write(path.join(dir, "tmpfile"), "x")
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const bash = await initBash()
|
||||
|
|
@ -981,7 +982,7 @@ describe("tool.shell permissions", () => {
|
|||
|
||||
each("includes always patterns for auto-approval", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const bash = await initBash()
|
||||
|
|
@ -1004,7 +1005,7 @@ describe("tool.shell permissions", () => {
|
|||
|
||||
each("does not ask for bash permission when command is cd only", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
@ -1026,7 +1027,7 @@ describe("tool.shell permissions", () => {
|
|||
|
||||
each("matches redirects in permission pattern", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
@ -1049,7 +1050,7 @@ describe("tool.shell permissions", () => {
|
|||
|
||||
each("always pattern has space before wildcard to not include different commands", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const bash = await initBash()
|
||||
|
|
@ -1065,7 +1066,7 @@ describe("tool.shell permissions", () => {
|
|||
|
||||
describe("tool.shell abort", () => {
|
||||
test("preserves output when aborted", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
@ -1099,7 +1100,7 @@ describe("tool.shell abort", () => {
|
|||
}, 15_000)
|
||||
|
||||
test("terminates command on timeout", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
@ -1121,7 +1122,7 @@ describe("tool.shell abort", () => {
|
|||
}, 15_000)
|
||||
|
||||
test.skipIf(process.platform === "win32")("captures stderr in output", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
@ -1142,7 +1143,7 @@ describe("tool.shell abort", () => {
|
|||
})
|
||||
|
||||
test("returns non-zero exit code", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
@ -1161,7 +1162,7 @@ describe("tool.shell abort", () => {
|
|||
})
|
||||
|
||||
test("streams metadata updates progressively", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initBash()
|
||||
|
|
@ -1192,7 +1193,7 @@ describe("tool.shell abort", () => {
|
|||
|
||||
describe("tool.shell truncation", () => {
|
||||
test("truncates output exceeding line limit", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
@ -1214,7 +1215,7 @@ describe("tool.shell truncation", () => {
|
|||
})
|
||||
|
||||
test("truncates output exceeding byte limit", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
@ -1236,7 +1237,7 @@ describe("tool.shell truncation", () => {
|
|||
})
|
||||
|
||||
test("does not truncate small output", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
@ -1256,7 +1257,7 @@ describe("tool.shell truncation", () => {
|
|||
})
|
||||
|
||||
test("full output is saved to file when truncated", async () => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { FetchHttpClient } from "effect/unstable/http"
|
|||
import { Agent } from "../../src/agent/agent"
|
||||
import { Truncate } from "@/tool/truncate"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { WebFetchTool } from "../../src/tool/webfetch"
|
||||
import { SessionID, MessageID } from "../../src/session/schema"
|
||||
|
||||
|
|
@ -41,7 +42,7 @@ describe("tool.webfetch", () => {
|
|||
await withFetch(
|
||||
() => new Response(bytes, { status: 200, headers: { "content-type": "IMAGE/PNG; charset=binary" } }),
|
||||
async (url) => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const result = await exec({ url: new URL("/image.png", url).toString(), format: "markdown" })
|
||||
|
|
@ -69,7 +70,7 @@ describe("tool.webfetch", () => {
|
|||
headers: { "content-type": "image/svg+xml; charset=UTF-8" },
|
||||
}),
|
||||
async (url) => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const result = await exec({ url: new URL("/image.svg", url).toString(), format: "html" })
|
||||
|
|
@ -89,7 +90,7 @@ describe("tool.webfetch", () => {
|
|||
headers: { "content-type": "text/plain; charset=utf-8" },
|
||||
}),
|
||||
async (url) => {
|
||||
await Instance.provide({
|
||||
await WithInstance.provide({
|
||||
directory: projectRoot,
|
||||
fn: async () => {
|
||||
const result = await exec({ url: new URL("/file.txt", url).toString(), format: "text" })
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue