chore: generate

This commit is contained in:
opencode-agent[bot] 2026-05-31 01:09:55 +00:00
commit 102c8353e0
71 changed files with 12548 additions and 10185 deletions

View file

@ -48,17 +48,9 @@ export const layer = Layer.effect(
effect.pipe(Effect.mapError((cause) => new AccountRepoError({ message: "Database operation failed", cause })))
const current = Effect.fnUntraced(function* () {
const state = yield* db
.select()
.from(AccountStateTable)
.where(eq(AccountStateTable.id, ACCOUNT_STATE_ID))
.get()
const state = yield* db.select().from(AccountStateTable).where(eq(AccountStateTable.id, ACCOUNT_STATE_ID)).get()
if (!state?.active_account_id) return
const account = yield* db
.select()
.from(AccountTable)
.where(eq(AccountTable.id, state.active_account_id))
.get()
const account = yield* db.select().from(AccountTable).where(eq(AccountTable.id, state.active_account_id)).get()
if (!account) return
return { ...account, active_org_id: state.active_org_id ?? null }
})

View file

@ -148,7 +148,12 @@ export const layer = Layer.effect(
const { db } = yield* Database.Service
const state = yield* InstanceState.make<State>(
Effect.fn("Permission.state")(function* (ctx) {
const row = yield* db.select().from(PermissionTable).where(eq(PermissionTable.project_id, ctx.project.id)).get().pipe(Effect.orDie)
const row = yield* db
.select()
.from(PermissionTable)
.where(eq(PermissionTable.project_id, ctx.project.id))
.get()
.pipe(Effect.orDie)
const state = {
pending: new Map<PermissionID, PendingEntry>(),
approved: [...(row?.data ?? [])],

View file

@ -190,41 +190,57 @@ export const layer = Layer.effect(
Effect.gen(function* () {
const oldProject = yield* d.select().from(ProjectTable).where(eq(ProjectTable.id, oldID)).get()
const newProject = yield* d.select().from(ProjectTable).where(eq(ProjectTable.id, newID)).get()
if (oldProject && !newProject) {
yield* d
.insert(ProjectTable)
.values({
...oldProject,
id: newID,
time_updated: Date.now(),
})
.run()
}
if (oldProject && !newProject) {
yield* d
.insert(ProjectTable)
.values({
...oldProject,
id: newID,
time_updated: Date.now(),
})
.run()
}
const oldPermission = yield* d.select().from(PermissionTable).where(eq(PermissionTable.project_id, oldID)).get()
const newPermission = yield* d.select().from(PermissionTable).where(eq(PermissionTable.project_id, newID)).get()
if (oldPermission && newPermission) {
yield* d
.update(PermissionTable)
.set({
data: mergePermissionRules(oldPermission.data, newPermission.data),
time_created: Math.min(oldPermission.time_created, newPermission.time_created),
time_updated: Date.now(),
})
const oldPermission = yield* d
.select()
.from(PermissionTable)
.where(eq(PermissionTable.project_id, oldID))
.get()
const newPermission = yield* d
.select()
.from(PermissionTable)
.where(eq(PermissionTable.project_id, newID))
.run()
.get()
if (oldPermission && newPermission) {
yield* d
.update(PermissionTable)
.set({
data: mergePermissionRules(oldPermission.data, newPermission.data),
time_created: Math.min(oldPermission.time_created, newPermission.time_created),
time_updated: Date.now(),
})
.where(eq(PermissionTable.project_id, newID))
.run()
yield* d.delete(PermissionTable).where(eq(PermissionTable.project_id, oldID)).run()
}
if (oldPermission && !newPermission) {
yield* d.update(PermissionTable).set({ project_id: newID }).where(eq(PermissionTable.project_id, oldID)).run()
}
}
if (oldPermission && !newPermission) {
yield* d
.update(PermissionTable)
.set({ project_id: newID })
.where(eq(PermissionTable.project_id, oldID))
.run()
}
yield* d
.update(SessionTable)
.set({ project_id: newID, time_updated: sql`${SessionTable.time_updated}` })
.where(eq(SessionTable.project_id, oldID))
.run()
yield* d.update(WorkspaceTable).set({ project_id: newID }).where(eq(WorkspaceTable.project_id, oldID)).run()
.set({ project_id: newID, time_updated: sql`${SessionTable.time_updated}` })
.where(eq(SessionTable.project_id, oldID))
.run()
yield* d
.update(WorkspaceTable)
.set({ project_id: newID })
.where(eq(WorkspaceTable.project_id, oldID))
.run()
if (oldProject) yield* d.delete(ProjectTable).where(eq(ProjectTable.id, oldID)).run()
}),
@ -278,46 +294,46 @@ export const layer = Layer.effect(
).pipe(Effect.map((arr) => arr.filter((x): x is string => x !== undefined)))
yield* db
.insert(ProjectTable)
.values({
id: result.id,
.insert(ProjectTable)
.values({
id: result.id,
worktree: result.worktree,
vcs: result.vcs ?? null,
name: result.name,
icon_url: result.icon?.url,
icon_url_override: result.icon?.override,
icon_color: result.icon?.color,
time_created: result.time.created,
time_updated: result.time.updated,
time_initialized: result.time.initialized,
sandboxes: result.sandboxes,
commands: result.commands,
})
.onConflictDoUpdate({
target: ProjectTable.id,
set: {
worktree: result.worktree,
vcs: result.vcs ?? null,
name: result.name,
icon_url: result.icon?.url,
icon_url_override: result.icon?.override,
icon_color: result.icon?.color,
time_created: result.time.created,
time_updated: result.time.updated,
time_initialized: result.time.initialized,
sandboxes: result.sandboxes,
commands: result.commands,
})
.onConflictDoUpdate({
target: ProjectTable.id,
set: {
worktree: result.worktree,
vcs: result.vcs ?? null,
name: result.name,
icon_url: result.icon?.url,
icon_url_override: result.icon?.override,
icon_color: result.icon?.color,
time_updated: result.time.updated,
time_initialized: result.time.initialized,
sandboxes: result.sandboxes,
commands: result.commands,
},
})
.run()
.pipe(Effect.orDie)
},
})
.run()
.pipe(Effect.orDie)
if (projectID !== ProjectV2.ID.global) {
yield* db
.update(SessionTable)
.set({ project_id: projectID })
.where(and(eq(SessionTable.project_id, ProjectV2.ID.global), eq(SessionTable.directory, data.directory)))
.run()
.pipe(Effect.orDie)
.update(SessionTable)
.set({ project_id: projectID })
.where(and(eq(SessionTable.project_id, ProjectV2.ID.global), eq(SessionTable.directory, data.directory)))
.run()
.pipe(Effect.orDie)
}
yield* emitUpdated(result)
@ -362,19 +378,19 @@ export const layer = Layer.effect(
const update = Effect.fn("Project.update")(function* (input: UpdateInput) {
const result = yield* db
.update(ProjectTable)
.set({
name: input.name,
icon_url: input.icon?.url,
icon_url_override: input.icon?.override,
icon_color: input.icon?.color,
commands: input.commands,
time_updated: Date.now(),
})
.where(eq(ProjectTable.id, input.projectID))
.returning()
.get()
.pipe(Effect.orDie)
.update(ProjectTable)
.set({
name: input.name,
icon_url: input.icon?.url,
icon_url_override: input.icon?.override,
icon_color: input.icon?.color,
commands: input.commands,
time_updated: Date.now(),
})
.where(eq(ProjectTable.id, input.projectID))
.returning()
.get()
.pipe(Effect.orDie)
if (!result) return yield* new NotFoundError({ projectID: input.projectID })
const data = fromRow(result)
yield* emitUpdated(data)
@ -393,13 +409,19 @@ export const layer = Layer.effect(
})
const setInitialized = Effect.fn("Project.setInitialized")(function* (id: ProjectV2.ID) {
yield* db.update(ProjectTable).set({ time_initialized: Date.now() }).where(eq(ProjectTable.id, id)).run().pipe(Effect.orDie)
yield* db
.update(ProjectTable)
.set({ time_initialized: Date.now() })
.where(eq(ProjectTable.id, id))
.run()
.pipe(Effect.orDie)
})
const initState = yield* InstanceState.make(
Effect.fn("Project.initState")(function* (ctx) {
const unsubscribe = yield* events.listen((event) => {
if (event.type !== Command.Event.Executed.type || event.location?.directory !== ctx.directory) return Effect.void
if (event.type !== Command.Event.Executed.type || event.location?.directory !== ctx.directory)
return Effect.void
const data = event.data as EventV2.Data<typeof Command.Event.Executed>
return data.name === Command.Default.INIT ? setInitialized(ctx.project.id) : Effect.void
})
@ -432,12 +454,12 @@ export const layer = Layer.effect(
const sboxes = [...row.sandboxes]
if (!sboxes.includes(directory)) sboxes.push(directory)
const result = yield* db
.update(ProjectTable)
.set({ sandboxes: sboxes, time_updated: Date.now() })
.where(eq(ProjectTable.id, id))
.returning()
.get()
.pipe(Effect.orDie)
.update(ProjectTable)
.set({ sandboxes: sboxes, time_updated: Date.now() })
.where(eq(ProjectTable.id, id))
.returning()
.get()
.pipe(Effect.orDie)
if (!result) throw new Error(`Project not found: ${id}`)
yield* emitUpdated(fromRow(result))
})
@ -447,12 +469,12 @@ export const layer = Layer.effect(
if (!row) throw new Error(`Project not found: ${id}`)
const sboxes = row.sandboxes.filter((s) => s !== directory)
const result = yield* db
.update(ProjectTable)
.set({ sandboxes: sboxes, time_updated: Date.now() })
.where(eq(ProjectTable.id, id))
.returning()
.get()
.pipe(Effect.orDie)
.update(ProjectTable)
.set({ sandboxes: sboxes, time_updated: Date.now() })
.where(eq(ProjectTable.id, id))
.returning()
.get()
.pipe(Effect.orDie)
if (!result) throw new Error(`Project not found: ${id}`)
yield* emitUpdated(fromRow(result))
})

View file

@ -328,7 +328,8 @@ export const layer: Layer.Layer<Service, never, Git.Service | EventV2Bridge.Serv
log.info("initialized", { branch: value.current, default_branch: value.root?.name })
const unsubscribe = yield* events.listen((event) => {
if (event.type !== FileWatcher.Event.Updated.type || event.location?.directory !== ctx.directory) return Effect.void
if (event.type !== FileWatcher.Event.Updated.type || event.location?.directory !== ctx.directory)
return Effect.void
const data = event.data as EventV2.Data<typeof FileWatcher.Event.Updated>
if (!data.file.endsWith("HEAD")) return Effect.void
return Effect.gen(function* () {
@ -429,9 +430,6 @@ export const layer: Layer.Layer<Service, never, Git.Service | EventV2Bridge.Serv
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(Git.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
)
export const defaultLayer = layer.pipe(Layer.provide(Git.defaultLayer), Layer.provide(EventV2Bridge.defaultLayer))
export * as Vcs from "./vcs"

View file

@ -184,7 +184,9 @@ export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> =
}
})
const callback = Effect.fn("ProviderAuth.callback")(function* (input: { providerID: ProviderV2.ID } & CallbackInput) {
const callback = Effect.fn("ProviderAuth.callback")(function* (
input: { providerID: ProviderV2.ID } & CallbackInput,
) {
const pending = (yield* InstanceState.get(state)).pending
const match = pending.get(input.providerID)
if (!match) return yield* new OauthMissing({ providerID: input.providerID })

View file

@ -1024,14 +1024,20 @@ export type Error = ModelNotFoundError | InitError | NoProvidersError | NoModels
export interface Interface {
readonly list: () => Effect.Effect<Record<ProviderV2.ID, Info>>
readonly getProvider: (providerID: ProviderV2.ID) => Effect.Effect<Info>
readonly getModel: (providerID: ProviderV2.ID, modelID: ProviderV2.ModelID) => Effect.Effect<Model, ModelNotFoundError>
readonly getModel: (
providerID: ProviderV2.ID,
modelID: ProviderV2.ModelID,
) => Effect.Effect<Model, ModelNotFoundError>
readonly getLanguage: (model: Model) => Effect.Effect<LanguageModelV3, ModelNotFoundError>
readonly closest: (
providerID: ProviderV2.ID,
query: string[],
) => Effect.Effect<{ providerID: ProviderV2.ID; modelID: string } | undefined>
readonly getSmallModel: (providerID: ProviderV2.ID) => Effect.Effect<Model | undefined>
readonly defaultModel: () => Effect.Effect<{ providerID: ProviderV2.ID; modelID: ProviderV2.ModelID }, DefaultModelError>
readonly defaultModel: () => Effect.Effect<
{ providerID: ProviderV2.ID; modelID: ProviderV2.ModelID },
DefaultModelError
>
}
interface State {

View file

@ -1,2 +1 @@
export function initProjectors() {
}
export function initProjectors() {}

View file

@ -19,7 +19,9 @@ export const controlHandlers = HttpApiBuilder.group(RootHttpApi, "control", (han
return true
})
const authRemove = Effect.fn("ControlHttpApi.authRemove")(function* (ctx: { params: { providerID: ProviderV2.ID } }) {
const authRemove = Effect.fn("ControlHttpApi.authRemove")(function* (ctx: {
params: { providerID: ProviderV2.ID }
}) {
yield* auth.remove(ctx.params.providerID).pipe(Effect.orDie)
return true
})

View file

@ -43,7 +43,10 @@ function eventResponse(events: EventV2.Interface) {
Stream.map((event) => ({ id: event.id, type: event.type, properties: event.data })),
)
const disposed = Stream.callback<{ id: string; type: string; properties: unknown }>((queue) => {
const listener = (event: { directory?: string; payload: { id?: string; type?: string; properties?: unknown } }) => {
const listener = (event: {
directory?: string
payload: { id?: string; type?: string; properties?: unknown }
}) => {
if (event.directory !== instance.directory || event.payload.type !== "server.instance.disposed") return
Queue.offerUnsafe(queue, {
id: event.payload.id ?? eventID(),
@ -56,7 +59,10 @@ function eventResponse(events: EventV2.Interface) {
() => Effect.sync(() => GlobalBus.off("event", listener)),
)
})
const output = stream.pipe(Stream.merge(disposed, { haltStrategy: "left" }), Stream.takeUntil((event) => event.type === "server.instance.disposed"))
const output = stream.pipe(
Stream.merge(disposed, { haltStrategy: "left" }),
Stream.takeUntil((event) => event.type === "server.instance.disposed"),
)
const heartbeat = Stream.tick("10 seconds").pipe(
Stream.drop(1),
Stream.map(() => ({ id: eventID(), type: "server.heartbeat", properties: {} })),

View file

@ -88,7 +88,8 @@ export const tuiHandlers = HttpApiBuilder.group(InstanceHttpApi, "tui", (handler
yield* events.publish(TuiEvent.PromptAppend, ctx.payload.properties)
if (ctx.payload.type === TuiEvent.CommandExecute.type)
yield* events.publish(TuiEvent.CommandExecute, ctx.payload.properties)
if (ctx.payload.type === TuiEvent.ToastShow.type) yield* events.publish(TuiEvent.ToastShow, ctx.payload.properties)
if (ctx.payload.type === TuiEvent.ToastShow.type)
yield* events.publish(TuiEvent.ToastShow, ctx.payload.properties)
if (ctx.payload.type === TuiEvent.SessionSelect.type)
yield* events.publish(TuiEvent.SessionSelect, ctx.payload.properties)
return true

View file

@ -353,7 +353,9 @@ export const layer = Layer.effect(
throw new Error(`Compaction parent must be a user message: ${input.parentID}`)
}
const userMessage = parent.info
const compactionPart = parent.parts.find((part): part is SessionLegacy.CompactionPart => part.type === "compaction")
const compactionPart = parent.parts.find(
(part): part is SessionLegacy.CompactionPart => part.type === "compaction",
)
let messages = input.messages
let replay:

View file

@ -168,13 +168,15 @@ const live: Layer.Layer<
const id = PermissionID.ascending()
let unsub: EventV2.Unsubscribe | undefined
try {
unsub = await bridge.promise(events.listen((event) => {
if (event.type !== Permission.Event.Replied.type) return Effect.void
const data = event.data as EventV2.Data<typeof Permission.Event.Replied>
if (data.requestID !== id) return Effect.void
void data.reply
return Effect.void
}))
unsub = await bridge.promise(
events.listen((event) => {
if (event.type !== Permission.Event.Replied.type) return Effect.void
const data = event.data as EventV2.Data<typeof Permission.Event.Replied>
if (data.requestID !== id) return Effect.void
void data.reply
return Effect.void
}),
)
const toolPatterns = approvalTools.map((t: { name: string; args: string }) => {
try {
const parsed = JSON.parse(t.args) as Record<string, unknown>

View file

@ -503,14 +503,14 @@ export function stream(sessionID: SessionID) {
export function parts(messageID: MessageID) {
return Effect.gen(function* () {
const { db } = yield* Database.Service
const rows = yield* db
.select()
.from(PartTable)
.where(eq(PartTable.message_id, messageID))
.orderBy(PartTable.id)
.all()
.pipe(Effect.orDie)
return rows.map(part)
const rows = yield* db
.select()
.from(PartTable)
.where(eq(PartTable.message_id, messageID))
.orderBy(PartTable.id)
.all()
.pipe(Effect.orDie)
return rows.map(part)
})
}

View file

@ -1502,11 +1502,11 @@ export const layer = Layer.effect(
},
)
const loop: (input: LoopInput) => Effect.Effect<SessionLegacy.WithParts> = Effect.fn("SessionPrompt.loop")(function* (
input: LoopInput,
) {
return yield* state.ensureRunning(input.sessionID, lastAssistant(input.sessionID), runLoop(input.sessionID))
})
const loop: (input: LoopInput) => Effect.Effect<SessionLegacy.WithParts> = Effect.fn("SessionPrompt.loop")(
function* (input: LoopInput) {
return yield* state.ensureRunning(input.sessionID, lastAssistant(input.sessionID), runLoop(input.sessionID))
},
)
const shell: (input: ShellInput) => Effect.Effect<SessionLegacy.WithParts, Session.BusyError> = Effect.fn(
"SessionPrompt.shell",

View file

@ -536,11 +536,7 @@ export type Patch = Omit<Partial<Info>, "time" | "share" | "summary" | "revert"
export const layer: Layer.Layer<
Service,
never,
| BackgroundJob.Service
| Storage.Service
| RuntimeFlags.Service
| Database.Service
| EventV2Bridge.Service
BackgroundJob.Service | Storage.Service | RuntimeFlags.Service | Database.Service | EventV2Bridge.Service
> = Layer.effect(
Service,
Effect.gen(function* () {

View file

@ -79,7 +79,9 @@ export const layer = Layer.effect(
const storage = yield* Storage.Service
const events = yield* EventV2Bridge.Service
const computeDiff = Effect.fn("SessionSummary.computeDiff")(function* (input: { messages: SessionLegacy.WithParts[] }) {
const computeDiff = Effect.fn("SessionSummary.computeDiff")(function* (input: {
messages: SessionLegacy.WithParts[]
}) {
let from: string | undefined
let to: string | undefined
for (const item of input.messages) {

View file

@ -173,7 +173,9 @@ export const layer = Layer.effect(
events.listen((event) => {
if (event.type !== def.type || event.location?.directory !== _ctx.directory) return Effect.void
return fn(event.data as EventV2.Data<D>).pipe(
Effect.catchCause((cause) => Effect.sync(() => log.error("share subscriber failed", { type: def.type, cause }))),
Effect.catchCause((cause) =>
Effect.sync(() => log.error("share subscriber failed", { type: def.type, cause })),
),
)
})

View file

@ -232,7 +232,11 @@ const discoverSkills = Effect.fnUntraced(function* (
}
})
const loadSkills = Effect.fnUntraced(function* (state: State, discovered: DiscoveryState, events: EventV2Bridge.Service["Service"]) {
const loadSkills = Effect.fnUntraced(function* (
state: State,
discovered: DiscoveryState,
events: EventV2Bridge.Service["Service"],
) {
yield* Effect.forEach(discovered.matches, (match) => add(state, match, events), {
concurrency: "unbounded",
discard: true,

View file

@ -76,7 +76,11 @@ export interface Interface {
readonly ids: () => Effect.Effect<string[]>
readonly all: () => Effect.Effect<Tool.Def[]>
readonly named: () => Effect.Effect<{ task: TaskDef; read: ReadDef }>
readonly tools: (model: { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID; agent: Agent.Info }) => Effect.Effect<Tool.Def[]>
readonly tools: (model: {
providerID: ProviderV2.ID
modelID: ProviderV2.ModelID
agent: Agent.Info
}) => Effect.Effect<Tool.Def[]>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/ToolRegistry") {}

View file

@ -149,7 +149,13 @@ type GitResult = { code: number; text: string; stderr: string }
export const layer: Layer.Layer<
Service,
never,
AppFileSystem.Service | Path.Path | AppProcess.Service | Git.Service | Project.Service | InstanceStore.Service | Database.Service
| AppFileSystem.Service
| Path.Path
| AppProcess.Service
| Git.Service
| Project.Service
| InstanceStore.Service
| Database.Service
> = Layer.effect(
Service,
Effect.gen(function* () {
@ -484,7 +490,12 @@ export const layer: Layer.Layer<
directory: string,
input: { projectID: ProjectV2.ID; extra?: string },
) {
const row = yield* db.select().from(ProjectTable).where(eq(ProjectTable.id, input.projectID)).get().pipe(Effect.orDie)
const row = yield* db
.select()
.from(ProjectTable)
.where(eq(ProjectTable.id, input.projectID))
.get()
.pipe(Effect.orDie)
const project = row ? Project.fromRow(row) : undefined
const startup = project?.commands?.start?.trim() ?? ""
const ok = yield* runStartScript(directory, startup, "project")

View file

@ -26,7 +26,11 @@ function createReasoningPart(text: string): SessionLegacy.Part {
}
}
function createToolPart(tool: string, title: string, status: "completed" | "running" = "completed"): SessionLegacy.Part {
function createToolPart(
tool: string,
title: string,
status: "completed" | "running" = "completed",
): SessionLegacy.Part {
if (status === "completed") {
return {
id: PartID.ascending(),

View file

@ -105,7 +105,9 @@ describe("Format", () => {
{ config: { formatter: false } },
)
testEffect(Layer.mergeAll(Format.defaultLayer, CrossSpawnSpawner.defaultLayer, NodeFileSystem.layer, testInstanceStoreLayer)).live("status() initializes formatter state per directory", () =>
testEffect(
Layer.mergeAll(Format.defaultLayer, CrossSpawnSpawner.defaultLayer, NodeFileSystem.layer, testInstanceStoreLayer),
).live("status() initializes formatter state per directory", () =>
Effect.gen(function* () {
const a = yield* provideTmpdirInstance(() => Format.use.status(), {
config: { formatter: false },

View file

@ -19,17 +19,11 @@ const lspLayer = (flags: Parameters<typeof RuntimeFlags.layer>[0] = {}) =>
const it = testEffect(Layer.mergeAll(lspLayer(), CrossSpawnSpawner.defaultLayer))
const experimentalTyIt = testEffect(
Layer.mergeAll(
lspLayer({ experimentalLspTy: true }),
CrossSpawnSpawner.defaultLayer,
),
Layer.mergeAll(lspLayer({ experimentalLspTy: true }), CrossSpawnSpawner.defaultLayer),
)
const fakeServerPath = path.join(__dirname, "../fixture/lsp/fake-lsp-server.js")
const disabledDownloadIt = testEffect(
Layer.mergeAll(
lspLayer({ disableLspDownload: true }),
CrossSpawnSpawner.defaultLayer,
),
Layer.mergeAll(lspLayer({ disableLspDownload: true }), CrossSpawnSpawner.defaultLayer),
)
describe("lsp.spawn", () => {
@ -37,27 +31,49 @@ describe("lsp.spawn", () => {
"does not spawn builtin LSP for files outside instance",
() =>
LSP.Service.use((lsp) =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
try {
yield* lsp.touchFile(path.join(dir, "..", "outside.ts"))
yield* lsp.hover({
file: path.join(dir, "..", "hover.ts"),
line: 0,
character: 0,
})
expect(spy).toHaveBeenCalledTimes(0)
} finally {
spy.mockRestore()
}
}),
),
try {
yield* lsp.touchFile(path.join(dir, "..", "outside.ts"))
yield* lsp.hover({
file: path.join(dir, "..", "hover.ts"),
line: 0,
character: 0,
})
expect(spy).toHaveBeenCalledTimes(0)
} finally {
spy.mockRestore()
}
}),
),
{ config: { lsp: true } },
)
it.instance("does not spawn builtin LSP for files inside instance when LSP is unset", () =>
LSP.Service.use((lsp) =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
try {
yield* lsp.hover({
file: path.join(dir, "src", "inside.ts"),
line: 0,
character: 0,
})
expect(spy).toHaveBeenCalledTimes(0)
} finally {
spy.mockRestore()
}
}),
),
)
it.instance(
"would spawn builtin LSP for files inside instance when lsp is true",
() =>
LSP.Service.use((lsp) =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
@ -69,34 +85,12 @@ describe("lsp.spawn", () => {
line: 0,
character: 0,
})
expect(spy).toHaveBeenCalledTimes(0)
expect(spy).toHaveBeenCalledTimes(1)
} finally {
spy.mockRestore()
}
}),
),
)
it.instance(
"would spawn builtin LSP for files inside instance when lsp is true",
() =>
LSP.Service.use((lsp) =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
try {
yield* lsp.hover({
file: path.join(dir, "src", "inside.ts"),
line: 0,
character: 0,
})
expect(spy).toHaveBeenCalledTimes(1)
} finally {
spy.mockRestore()
}
}),
),
{ config: { lsp: true } },
)
@ -104,21 +98,21 @@ describe("lsp.spawn", () => {
"publishes lsp.updated after custom LSP initialization",
() =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const lsp = yield* LSP.Service
const updated = yield* Deferred.make<void>()
const events = yield* EventV2Bridge.Service
const unsubscribe = yield* events.listen((event) => {
if (event.type === LSP.Event.Updated.type) Deferred.doneUnsafe(updated, Effect.void)
return Effect.void
})
yield* Effect.addFinalizer(() => unsubscribe)
const dir = (yield* TestInstance).directory
const lsp = yield* LSP.Service
const updated = yield* Deferred.make<void>()
const events = yield* EventV2Bridge.Service
const unsubscribe = yield* events.listen((event) => {
if (event.type === LSP.Event.Updated.type) Deferred.doneUnsafe(updated, Effect.void)
return Effect.void
})
yield* Effect.addFinalizer(() => unsubscribe)
const file = path.join(dir, "sample.repro")
yield* Effect.promise(() => Bun.write(file, "sample\n"))
yield* lsp.touchFile(file)
yield* awaitWithTimeout(Deferred.await(updated), "lsp.updated event was not published")
}),
const file = path.join(dir, "sample.repro")
yield* Effect.promise(() => Bun.write(file, "sample\n"))
yield* lsp.touchFile(file)
yield* awaitWithTimeout(Deferred.await(updated), "lsp.updated event was not published")
}),
{
config: {
lsp: {
@ -135,22 +129,22 @@ describe("lsp.spawn", () => {
"would spawn builtin LSP for files inside instance when config object is provided",
() =>
LSP.Service.use((lsp) =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
try {
yield* lsp.hover({
file: path.join(dir, "src", "inside.ts"),
line: 0,
character: 0,
})
expect(spy).toHaveBeenCalledTimes(1)
} finally {
spy.mockRestore()
}
}),
),
try {
yield* lsp.hover({
file: path.join(dir, "src", "inside.ts"),
line: 0,
character: 0,
})
expect(spy).toHaveBeenCalledTimes(1)
} finally {
spy.mockRestore()
}
}),
),
{
config: {
lsp: {
@ -164,25 +158,25 @@ describe("lsp.spawn", () => {
"uses pyright instead of ty by default",
() =>
LSP.Service.use((lsp) =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const ty = spyOn(LSPServer.Ty, "spawn").mockResolvedValue(undefined)
const pyright = spyOn(LSPServer.Pyright, "spawn").mockResolvedValue(undefined)
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const ty = spyOn(LSPServer.Ty, "spawn").mockResolvedValue(undefined)
const pyright = spyOn(LSPServer.Pyright, "spawn").mockResolvedValue(undefined)
try {
yield* lsp.hover({
file: path.join(dir, "src", "inside.py"),
line: 0,
character: 0,
})
expect(ty).toHaveBeenCalledTimes(0)
expect(pyright).toHaveBeenCalledTimes(1)
} finally {
ty.mockRestore()
pyright.mockRestore()
}
}),
),
try {
yield* lsp.hover({
file: path.join(dir, "src", "inside.py"),
line: 0,
character: 0,
})
expect(ty).toHaveBeenCalledTimes(0)
expect(pyright).toHaveBeenCalledTimes(1)
} finally {
ty.mockRestore()
pyright.mockRestore()
}
}),
),
{ config: { lsp: true } },
)
@ -190,25 +184,25 @@ describe("lsp.spawn", () => {
"uses ty instead of pyright when experimentalLspTy is enabled",
() =>
LSP.Service.use((lsp) =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const ty = spyOn(LSPServer.Ty, "spawn").mockResolvedValue(undefined)
const pyright = spyOn(LSPServer.Pyright, "spawn").mockResolvedValue(undefined)
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const ty = spyOn(LSPServer.Ty, "spawn").mockResolvedValue(undefined)
const pyright = spyOn(LSPServer.Pyright, "spawn").mockResolvedValue(undefined)
try {
yield* lsp.hover({
file: path.join(dir, "src", "inside.py"),
line: 0,
character: 0,
})
expect(ty).toHaveBeenCalledTimes(1)
expect(pyright).toHaveBeenCalledTimes(0)
} finally {
ty.mockRestore()
pyright.mockRestore()
}
}),
),
try {
yield* lsp.hover({
file: path.join(dir, "src", "inside.py"),
line: 0,
character: 0,
})
expect(ty).toHaveBeenCalledTimes(1)
expect(pyright).toHaveBeenCalledTimes(0)
} finally {
ty.mockRestore()
pyright.mockRestore()
}
}),
),
{ config: { lsp: true } },
)
@ -216,23 +210,23 @@ describe("lsp.spawn", () => {
"passes disableLspDownload to builtin LSP spawn",
() =>
LSP.Service.use((lsp) =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const pyright = spyOn(LSPServer.Pyright, "spawn").mockResolvedValue(undefined)
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const pyright = spyOn(LSPServer.Pyright, "spawn").mockResolvedValue(undefined)
try {
yield* lsp.hover({
file: path.join(dir, "src", "inside.py"),
line: 0,
character: 0,
})
expect(pyright).toHaveBeenCalledTimes(1)
expect(pyright.mock.calls[0]?.[2]).toMatchObject({ disableLspDownload: true })
} finally {
pyright.mockRestore()
}
}),
),
try {
yield* lsp.hover({
file: path.join(dir, "src", "inside.py"),
line: 0,
character: 0,
})
expect(pyright).toHaveBeenCalledTimes(1)
expect(pyright.mock.calls[0]?.[2]).toMatchObject({ disableLspDownload: true })
} finally {
pyright.mockRestore()
}
}),
),
{ config: { lsp: true } },
)
})

View file

@ -43,12 +43,12 @@ describe("LSP service lifecycle", () => {
)
it.instance("hasClients() returns false for .ts files in instance when LSP is unset", () =>
LSP.Service.use((lsp) =>
Effect.gen(function* () {
const result = yield* lsp.hasClients(path.join((yield* TestInstance).directory, "test.ts"))
expect(result).toBe(false)
}),
),
LSP.Service.use((lsp) =>
Effect.gen(function* () {
const result = yield* lsp.hasClients(path.join((yield* TestInstance).directory, "test.ts"))
expect(result).toBe(false)
}),
),
)
it.instance(

View file

@ -657,7 +657,8 @@ it.instance(
const events = yield* EventV2Bridge.Service
const seen = yield* Deferred.make<Permission.Request>()
const unsub = yield* events.listen((event) => {
if (event.type === Permission.Event.Asked.type) Deferred.doneUnsafe(seen, Effect.succeed(event.data as Permission.Request))
if (event.type === Permission.Event.Asked.type)
Deferred.doneUnsafe(seen, Effect.succeed(event.data as Permission.Request))
return Effect.void
})
yield* Effect.addFinalizer(() => unsub)
@ -932,7 +933,10 @@ it.instance(
const unsub = yield* events.listen((event) => {
if (event.type === Permission.Event.Replied.type)
Deferred.doneUnsafe(seen, Effect.succeed(event.data as { sessionID: SessionID; requestID: PermissionID; reply: Permission.Reply }))
Deferred.doneUnsafe(
seen,
Effect.succeed(event.data as { sessionID: SessionID; requestID: PermissionID; reply: Permission.Reply }),
)
return Effect.void
})
yield* Effect.addFinalizer(() => unsub)

View file

@ -20,7 +20,9 @@ afterEach(async () => {
await disposeAllInstances()
})
const it = testEffect(Layer.mergeAll(CrossSpawnSpawner.defaultLayer, AppFileSystem.defaultLayer, testInstanceStoreLayer))
const it = testEffect(
Layer.mergeAll(CrossSpawnSpawner.defaultLayer, AppFileSystem.defaultLayer, testInstanceStoreLayer),
)
function withTmp<T, A, E, R>(
init: (dir: string) => Promise<T>,

View file

@ -64,74 +64,74 @@ afterEach(async () => {
describe("plugin.workspace", () => {
it.instance("plugin can install a workspace adapter", () =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const type = `plug-${Math.random().toString(36).slice(2)}`
const file = path.join(dir, "plugin.ts")
const mark = path.join(dir, "created.json")
const space = path.join(dir, "space")
yield* Effect.promise(() =>
Bun.write(
file,
[
"export default async ({ experimental_workspace }) => {",
` experimental_workspace.register(${JSON.stringify(type)}, {`,
' name: "plug",',
' description: "plugin workspace adapter",',
" configure(input) {",
` return { ...input, name: "plug", branch: "plug/main", directory: ${JSON.stringify(space)} }`,
" },",
" async create(input) {",
` await Bun.write(${JSON.stringify(mark)}, JSON.stringify(input))`,
" },",
" async remove() {},",
" target(input) {",
' return { type: "local", directory: input.directory }',
" },",
" })",
" return {}",
"}",
"",
].join("\n"),
const dir = (yield* TestInstance).directory
const type = `plug-${Math.random().toString(36).slice(2)}`
const file = path.join(dir, "plugin.ts")
const mark = path.join(dir, "created.json")
const space = path.join(dir, "space")
yield* Effect.promise(() =>
Bun.write(
file,
[
"export default async ({ experimental_workspace }) => {",
` experimental_workspace.register(${JSON.stringify(type)}, {`,
' name: "plug",',
' description: "plugin workspace adapter",',
" configure(input) {",
` return { ...input, name: "plug", branch: "plug/main", directory: ${JSON.stringify(space)} }`,
" },",
" async create(input) {",
` await Bun.write(${JSON.stringify(mark)}, JSON.stringify(input))`,
" },",
" async remove() {},",
" target(input) {",
' return { type: "local", directory: input.directory }',
" },",
" })",
" return {}",
"}",
"",
].join("\n"),
),
)
yield* Effect.promise(() =>
Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify(
{
$schema: "https://opencode.ai/config.json",
plugin: [pathToFileURL(file).href],
},
null,
2,
),
)
),
)
yield* Effect.promise(() =>
Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify(
{
$schema: "https://opencode.ai/config.json",
plugin: [pathToFileURL(file).href],
},
null,
2,
),
),
)
const plugin = yield* Plugin.Service
yield* plugin.init()
const workspace = yield* Workspace.Service
const ctx = yield* InstanceState.context
const info = yield* workspace.create({
type,
branch: null,
extra: { key: "value" },
projectID: ctx.project.id,
})
const plugin = yield* Plugin.Service
yield* plugin.init()
const workspace = yield* Workspace.Service
const ctx = yield* InstanceState.context
const info = yield* workspace.create({
type,
branch: null,
extra: { key: "value" },
projectID: ctx.project.id,
})
expect(info.type).toBe(type)
expect(info.name).toBe("plug")
expect(info.branch).toBe("plug/main")
expect(info.directory).toBe(space)
expect(info.extra).toEqual({ key: "value" })
expect(JSON.parse(yield* Effect.promise(() => Bun.file(mark).text()))).toMatchObject({
type,
name: "plug",
branch: "plug/main",
directory: space,
extra: { key: "value" },
})
expect(info.type).toBe(type)
expect(info.name).toBe("plug")
expect(info.branch).toBe("plug/main")
expect(info.directory).toBe(space)
expect(info.extra).toEqual({ key: "value" })
expect(JSON.parse(yield* Effect.promise(() => Bun.file(mark).text()))).toMatchObject({
type,
name: "plug",
branch: "plug/main",
directory: space,
extra: { key: "value" },
})
}),
)
})

View file

@ -238,10 +238,25 @@ describe("Project.fromDirectory", () => {
const result = yield* projects.fromDirectory(tmp)
expect(result.project.id).toBe(remoteID)
expect(yield* db.select().from(ProjectTable).where(eq(ProjectTable.id, rootProject.id)).get().pipe(Effect.orDie)).toBeUndefined()
expect((yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get().pipe(Effect.orDie))?.project_id).toBe(remoteID)
expect(yield* db.select().from(PermissionTable).where(eq(PermissionTable.project_id, remoteID)).get().pipe(Effect.orDie)).toBeDefined()
expect((yield* db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, workspaceID)).get().pipe(Effect.orDie))?.project_id).toBe(remoteID)
expect(
yield* db.select().from(ProjectTable).where(eq(ProjectTable.id, rootProject.id)).get().pipe(Effect.orDie),
).toBeUndefined()
expect(
(yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get().pipe(Effect.orDie))
?.project_id,
).toBe(remoteID)
expect(
yield* db
.select()
.from(PermissionTable)
.where(eq(PermissionTable.project_id, remoteID))
.get()
.pipe(Effect.orDie),
).toBeDefined()
expect(
(yield* db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, workspaceID)).get().pipe(Effect.orDie))
?.project_id,
).toBe(remoteID)
}),
)
})

View file

@ -5,7 +5,13 @@ import { Deferred, Effect, Layer } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import fs from "fs/promises"
import path from "path"
import { disposeAllInstances, provideInstance, testInstanceStoreLayer, TestInstance, tmpdirScoped } from "../fixture/fixture"
import {
disposeAllInstances,
provideInstance,
testInstanceStoreLayer,
TestInstance,
tmpdirScoped,
} from "../fixture/fixture"
import { EventV2Bridge } from "../../src/event-v2-bridge"
import { FileWatcher } from "../../src/file/watcher"
import { Git } from "../../src/git"

View file

@ -16,70 +16,70 @@ describe("Worktree.remove", () => {
"continues when git remove exits non-zero after detaching",
() =>
Effect.gen(function* () {
const root = (yield* TestInstance).directory
const svc = yield* Worktree.Service
const name = `remove-regression-${Date.now().toString(36)}`
const branch = `opencode/${name}`
const dir = path.join(root, "..", name)
const root = (yield* TestInstance).directory
const svc = yield* Worktree.Service
const name = `remove-regression-${Date.now().toString(36)}`
const branch = `opencode/${name}`
const dir = path.join(root, "..", name)
yield* Effect.promise(() => $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet())
yield* Effect.promise(() => $`git reset --hard`.cwd(dir).quiet())
yield* Effect.promise(() => $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet())
yield* Effect.promise(() => $`git reset --hard`.cwd(dir).quiet())
const real = (yield* Effect.promise(() => $`which git`.quiet().text())).trim()
expect(real).toBeTruthy()
const real = (yield* Effect.promise(() => $`which git`.quiet().text())).trim()
expect(real).toBeTruthy()
const bin = path.join(root, "bin")
const shim = path.join(bin, "git")
yield* Effect.promise(() => fs.mkdir(bin, { recursive: true }))
yield* Effect.promise(() =>
Bun.write(
shim,
[
"#!/bin/bash",
`REAL_GIT=${JSON.stringify(real)}`,
'if [ "$1" = "worktree" ] && [ "$2" = "remove" ]; then',
' "$REAL_GIT" "$@" >/dev/null 2>&1',
' echo "fatal: failed to remove worktree: Directory not empty" >&2',
" exit 1",
"fi",
'exec "$REAL_GIT" "$@"',
].join("\n"),
),
)
yield* Effect.promise(() => fs.chmod(shim, 0o755))
const bin = path.join(root, "bin")
const shim = path.join(bin, "git")
yield* Effect.promise(() => fs.mkdir(bin, { recursive: true }))
yield* Effect.promise(() =>
Bun.write(
shim,
[
"#!/bin/bash",
`REAL_GIT=${JSON.stringify(real)}`,
'if [ "$1" = "worktree" ] && [ "$2" = "remove" ]; then',
' "$REAL_GIT" "$@" >/dev/null 2>&1',
' echo "fatal: failed to remove worktree: Directory not empty" >&2',
" exit 1",
"fi",
'exec "$REAL_GIT" "$@"',
].join("\n"),
),
)
yield* Effect.promise(() => fs.chmod(shim, 0o755))
const prev = yield* Effect.acquireRelease(
const prev = yield* Effect.acquireRelease(
Effect.sync(() => {
const prev = process.env.PATH ?? ""
process.env.PATH = `${bin}${path.delimiter}${prev}`
return prev
}),
(prev) =>
Effect.sync(() => {
const prev = process.env.PATH ?? ""
process.env.PATH = `${bin}${path.delimiter}${prev}`
return prev
process.env.PATH = prev
}),
(prev) =>
Effect.sync(() => {
process.env.PATH = prev
}),
)
void prev
)
void prev
const ok = yield* svc.remove({ directory: dir })
const ok = yield* svc.remove({ directory: dir })
expect(ok).toBe(true)
expect(
yield* Effect.promise(() =>
fs
.stat(dir)
.then(() => true)
.catch(() => false),
),
).toBe(false)
expect(ok).toBe(true)
expect(
yield* Effect.promise(() =>
fs
.stat(dir)
.then(() => true)
.catch(() => false),
),
).toBe(false)
const list = yield* Effect.promise(() => $`git worktree list --porcelain`.cwd(root).quiet().text())
expect(list).not.toContain(`worktree ${dir}`)
const list = yield* Effect.promise(() => $`git worktree list --porcelain`.cwd(root).quiet().text())
expect(list).not.toContain(`worktree ${dir}`)
const ref = yield* Effect.promise(() =>
$`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow(),
)
expect(ref.exitCode).not.toBe(0)
const ref = yield* Effect.promise(() =>
$`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow(),
)
expect(ref.exitCode).not.toBe(0)
}),
{ git: true },
)
@ -88,38 +88,38 @@ describe("Worktree.remove", () => {
"stops fsmonitor before removing a worktree",
() =>
Effect.gen(function* () {
const root = (yield* TestInstance).directory
const svc = yield* Worktree.Service
const name = `remove-fsmonitor-${Date.now().toString(36)}`
const branch = `opencode/${name}`
const dir = path.join(root, "..", name)
const root = (yield* TestInstance).directory
const svc = yield* Worktree.Service
const name = `remove-fsmonitor-${Date.now().toString(36)}`
const branch = `opencode/${name}`
const dir = path.join(root, "..", name)
yield* Effect.promise(() => $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet())
yield* Effect.promise(() => $`git reset --hard`.cwd(dir).quiet())
yield* Effect.promise(() => $`git config core.fsmonitor true`.cwd(dir).quiet())
yield* Effect.promise(() => $`git fsmonitor--daemon stop`.cwd(dir).quiet().nothrow())
yield* Effect.promise(() => Bun.write(path.join(dir, "tracked.txt"), "next\n"))
yield* Effect.promise(() => $`git diff`.cwd(dir).quiet())
yield* Effect.promise(() => $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet())
yield* Effect.promise(() => $`git reset --hard`.cwd(dir).quiet())
yield* Effect.promise(() => $`git config core.fsmonitor true`.cwd(dir).quiet())
yield* Effect.promise(() => $`git fsmonitor--daemon stop`.cwd(dir).quiet().nothrow())
yield* Effect.promise(() => Bun.write(path.join(dir, "tracked.txt"), "next\n"))
yield* Effect.promise(() => $`git diff`.cwd(dir).quiet())
const before = yield* Effect.promise(() => $`git fsmonitor--daemon status`.cwd(dir).quiet().nothrow())
expect(before.exitCode).toBe(0)
const before = yield* Effect.promise(() => $`git fsmonitor--daemon status`.cwd(dir).quiet().nothrow())
expect(before.exitCode).toBe(0)
const ok = yield* svc.remove({ directory: dir })
const ok = yield* svc.remove({ directory: dir })
expect(ok).toBe(true)
expect(
yield* Effect.promise(() =>
fs
.stat(dir)
.then(() => true)
.catch(() => false),
),
).toBe(false)
expect(ok).toBe(true)
expect(
yield* Effect.promise(() =>
fs
.stat(dir)
.then(() => true)
.catch(() => false),
),
).toBe(false)
const ref = yield* Effect.promise(() =>
$`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow(),
)
expect(ref.exitCode).not.toBe(0)
const ref = yield* Effect.promise(() =>
$`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow(),
)
expect(ref.exitCode).not.toBe(0)
}),
{ git: true },
)

View file

@ -180,7 +180,9 @@ it.instance(
yield* set("AWS_PROFILE", "default")
const providers = yield* list
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
expect(providers[ProviderV2.ID.amazonBedrock].models["global.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
expect(
providers[ProviderV2.ID.amazonBedrock].models["global.anthropic.claude-opus-4-5-20251101-v1:0"],
).toBeDefined()
}),
{
config: {

View file

@ -245,9 +245,9 @@ it.instance(
expect(provider.models["deepseek-r1"].capabilities.interleaved).toEqual({ field: "reasoning_content" })
expect(provider.models["deepseek-details"].capabilities.interleaved).toEqual({ field: "reasoning_details" })
expect(provider.models["custom-model"].capabilities.interleaved).toBe(false)
expect(providers[ProviderV2.ID.make("custom-anthropic-provider")].models["deepseek-r1"].capabilities.interleaved).toBe(
false,
)
expect(
providers[ProviderV2.ID.make("custom-anthropic-provider")].models["deepseek-r1"].capabilities.interleaved,
).toBe(false)
}),
{
config: {
@ -305,7 +305,9 @@ it.instance("getModel returns model for valid provider/model", () =>
it.instance("getModel throws ModelNotFoundError for invalid model", () =>
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const exit = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("nonexistent-model")).pipe(Effect.exit)
const exit = yield* Provider.use
.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("nonexistent-model"))
.pipe(Effect.exit)
expect(exit._tag).toBe("Failure")
}),
)
@ -977,8 +979,14 @@ it.instance(
it.instance("getModel returns consistent results", () =>
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const model1 = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("claude-sonnet-4-20250514"))
const model2 = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("claude-sonnet-4-20250514"))
const model1 = yield* Provider.use.getModel(
ProviderV2.ID.anthropic,
ProviderV2.ModelID.make("claude-sonnet-4-20250514"),
)
const model2 = yield* Provider.use.getModel(
ProviderV2.ID.anthropic,
ProviderV2.ModelID.make("claude-sonnet-4-20250514"),
)
expect(model1.providerID).toEqual(model2.providerID)
expect(model1.id).toEqual(model2.id)
expect(model1).toEqual(model2)
@ -1008,7 +1016,9 @@ it.instance(
it.instance("ModelNotFoundError includes suggestions for typos", () =>
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const error = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("claude-sonet-4")).pipe(Effect.flip)
const error = yield* Provider.use
.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("claude-sonet-4"))
.pipe(Effect.flip)
expect(error.suggestions).toBeDefined()
expect((error.suggestions ?? []).length).toBeGreaterThan(0)
}),
@ -1565,7 +1575,10 @@ it.instance("Google Vertex: uses REP endpoint for Claude continental multi-regio
yield* set("GOOGLE_CLOUD_PROJECT", "test-project")
yield* set("VERTEX_LOCATION", "eu")
const provider = yield* Provider.Service
const model = yield* provider.getModel(ProviderV2.ID.make("google-vertex"), ProviderV2.ModelID.make("claude-sonnet-4-6@default"))
const model = yield* provider.getModel(
ProviderV2.ID.make("google-vertex"),
ProviderV2.ModelID.make("claude-sonnet-4-6@default"),
)
const language = yield* provider.getLanguage(model)
expect(languageBaseURL(language)).toBe(
"https://aiplatform.eu.rep.googleapis.com/v1/projects/test-project/locations/eu/publishers/anthropic/models",
@ -1594,7 +1607,10 @@ it.instance("Google Vertex: keeps regional Claude endpoints unchanged", () =>
yield* set("GOOGLE_CLOUD_PROJECT", "test-project")
yield* set("VERTEX_LOCATION", "europe-west1")
const provider = yield* Provider.Service
const model = yield* provider.getModel(ProviderV2.ID.make("google-vertex"), ProviderV2.ModelID.make("claude-sonnet-4-6@default"))
const model = yield* provider.getModel(
ProviderV2.ID.make("google-vertex"),
ProviderV2.ModelID.make("claude-sonnet-4-6@default"),
)
const language = yield* provider.getLanguage(model)
expect(languageBaseURL(language)).toBe(
"https://europe-west1-aiplatform.googleapis.com/v1/projects/test-project/locations/europe-west1/publishers/anthropic/models",

View file

@ -50,7 +50,9 @@ describe("PTY websocket tickets", () => {
const workspaceID = WorkspaceV2.ID.ascending()
const issued = yield* tickets.issue({ ptyID, workspaceID })
expect(yield* tickets.consume({ ptyID, workspaceID: WorkspaceV2.ID.ascending(), ticket: issued.ticket })).toBe(false)
expect(yield* tickets.consume({ ptyID, workspaceID: WorkspaceV2.ID.ascending(), ticket: issued.ticket })).toBe(
false,
)
expect(yield* tickets.consume({ ptyID, workspaceID, ticket: issued.ticket })).toBe(true)
}),
)

View file

@ -14,7 +14,11 @@ const it = testEffect(
Layer.mergeAll(Question.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer)), CrossSpawnSpawner.defaultLayer),
)
const lifecycle = testEffect(
Layer.mergeAll(Question.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer)), CrossSpawnSpawner.defaultLayer, testInstanceStoreLayer),
Layer.mergeAll(
Question.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer)),
CrossSpawnSpawner.defaultLayer,
testInstanceStoreLayer,
),
)
const askEffect = Effect.fn("QuestionTest.ask")(function* (input: {

View file

@ -94,5 +94,4 @@ describe("event HttpApi", () => {
}),
{ git: true, config: { formatter: false, lsp: false } },
)
})

View file

@ -170,7 +170,11 @@ const insertRemoteWorkspaceWithoutSync = (input: {
const id = WorkspaceV2.ID.ascending()
registerAdapter(input.projectID, input.type, remoteAdapter(path.join(input.dir, `.${input.type}`), input.url))
const { db } = yield* Database.Service
yield* db.insert(WorkspaceTable).values({ id, type: input.type, project_id: input.projectID }).run().pipe(Effect.orDie)
yield* db
.insert(WorkspaceTable)
.values({ id, type: input.type, project_id: input.projectID })
.run()
.pipe(Effect.orDie)
return id
})
@ -331,7 +335,9 @@ describe("HttpApi workspace routing middleware", () => {
const project = yield* Project.use.fromDirectory(dir)
const workspaceID = WorkspaceV2.ID.ascending()
const type = "remote-http-fence-target"
const waited = yield* Ref.make<{ workspaceID: WorkspaceV2.ID; state: Record<string, number> } | undefined>(undefined)
const waited = yield* Ref.make<{ workspaceID: WorkspaceV2.ID; state: Record<string, number> } | undefined>(
undefined,
)
const remoteUrl = yield* startRemoteWorkspaceHttpServer(() =>
HttpServerResponse.json(

View file

@ -154,8 +154,18 @@ describe("session.list", () => {
)
const { db } = yield* Database.Service
yield* db.update(SessionTable).set({ path: null }).where(eq(SessionTable.id, current.id)).run().pipe(Effect.orDie)
yield* db.update(SessionTable).set({ path: null }).where(eq(SessionTable.id, sibling.id)).run().pipe(Effect.orDie)
yield* db
.update(SessionTable)
.set({ path: null })
.where(eq(SessionTable.id, current.id))
.run()
.pipe(Effect.orDie)
yield* db
.update(SessionTable)
.set({ path: null })
.where(eq(SessionTable.id, sibling.id))
.run()
.pipe(Effect.orDie)
const pathIDs = (yield* SessionNs.Service.use((session) =>
session.list({

View file

@ -247,7 +247,12 @@ const env = Layer.mergeAll(
const it = testEffect(env)
const compactionEnv = Layer.mergeAll(SessionNs.defaultLayer, Database.defaultLayer, EventV2Bridge.defaultLayer, CrossSpawnSpawner.defaultLayer)
const compactionEnv = Layer.mergeAll(
SessionNs.defaultLayer,
Database.defaultLayer,
EventV2Bridge.defaultLayer,
CrossSpawnSpawner.defaultLayer,
)
const itCompaction = testEffect(compactionEnv)
type CompactionProcessOptions = {
@ -587,7 +592,6 @@ describe("session.compaction.create", () => {
auto: true,
overflow: true,
})
}),
),
)
@ -852,7 +856,8 @@ describe("session.compaction.process", () => {
let seen = false
const unsub = yield* events.listen((evt) => {
if (evt.type !== SessionCompaction.Event.Compacted.type) return Effect.void
if ((evt.data as typeof SessionCompaction.Event.Compacted.data.Type).sessionID !== session.id) return Effect.void
if ((evt.data as typeof SessionCompaction.Event.Compacted.data.Type).sessionID !== session.id)
return Effect.void
seen = true
Deferred.doneUnsafe(done, Effect.void)
return Effect.void

View file

@ -1617,7 +1617,10 @@ describe("session.llm.stream", () => {
]
const request = waitRequest("/messages", createEventResponse(chunks))
const resolved = yield* Provider.use.getModel(ProviderV2.ID.make("anthropic"), ProviderV2.ModelID.make(model.id))
const resolved = yield* Provider.use.getModel(
ProviderV2.ID.make("anthropic"),
ProviderV2.ModelID.make(model.id),
)
const sessionID = SessionID.make("session-test-anthropic-tools")
const agent = {
name: "test",
@ -1816,7 +1819,10 @@ describe("session.llm.stream", () => {
]
const request = waitRequest(pathSuffix, createEventResponse(chunks))
const resolved = yield* Provider.use.getModel(ProviderV2.ID.make(geminiFixture.providerID), ProviderV2.ModelID.make(model.id))
const resolved = yield* Provider.use.getModel(
ProviderV2.ID.make(geminiFixture.providerID),
ProviderV2.ModelID.make(model.id),
)
const sessionID = SessionID.make("session-test-4")
const agent = {
name: "test",

View file

@ -991,7 +991,9 @@ describe("session.message-v2.toModelMessage", () => {
const assistantID1 = "m-assistant-1"
const assistantID2 = "m-assistant-2"
const aborted = new SessionLegacy.AbortedError({ message: "aborted" }).toObject() as SessionLegacy.Assistant["error"]
const aborted = new SessionLegacy.AbortedError({
message: "aborted",
}).toObject() as SessionLegacy.Assistant["error"]
const input: SessionLegacy.WithParts[] = [
{

View file

@ -539,7 +539,12 @@ noLLMServer.instance.skip(
Effect.provide(SessionV2.defaultLayer),
)
const { db } = yield* Database.Service
const row = yield* db.select().from(SessionMessageTable).where(eq(SessionMessageTable.session_id, chat.id)).get().pipe(Effect.orDie)
const row = yield* db
.select()
.from(SessionMessageTable)
.where(eq(SessionMessageTable.session_id, chat.id))
.get()
.pipe(Effect.orDie)
expect(messages.find((message) => message.type === "user")).toMatchObject({ type: "user", text: "hello v2" })
expect(typeof row?.data.time.created).toBe("number")
expect(messages).toEqual(

View file

@ -87,31 +87,31 @@ describe("session.retry.delay", () => {
it.instance("policy updates retry status and increments attempts", () =>
Effect.gen(function* () {
const sessionID = SessionID.make("session-retry-test")
const error = apiError({ "retry-after-ms": "0" })
const status = yield* SessionStatus.Service
const sessionID = SessionID.make("session-retry-test")
const error = apiError({ "retry-after-ms": "0" })
const status = yield* SessionStatus.Service
const step = yield* Schedule.toStepWithMetadata(
SessionRetry.policy({
provider: "test",
parse: Schema.decodeUnknownSync(SessionLegacy.APIError.Schema),
set: (info) =>
status.set(sessionID, {
type: "retry",
attempt: info.attempt,
message: info.message,
next: info.next,
}),
}),
)
yield* step(error)
yield* step(error)
const step = yield* Schedule.toStepWithMetadata(
SessionRetry.policy({
provider: "test",
parse: Schema.decodeUnknownSync(SessionLegacy.APIError.Schema),
set: (info) =>
status.set(sessionID, {
type: "retry",
attempt: info.attempt,
message: info.message,
next: info.next,
}),
}),
)
yield* step(error)
yield* step(error)
expect(yield* status.get(sessionID)).toMatchObject({
type: "retry",
attempt: 2,
message: "boom",
})
expect(yield* status.get(sessionID)).toMatchObject({
type: "retry",
attempt: 2,
message: "boom",
})
}),
)
})

View file

@ -49,7 +49,10 @@ describe("session.created event", () => {
const unsub = yield* events.listen((event) => {
if (event.type === SessionNs.Event.Created.type)
Deferred.doneUnsafe(received, Effect.succeed((event.data as typeof SessionNs.Event.Created.data.Type).info as SessionNs.Info))
Deferred.doneUnsafe(
received,
Effect.succeed((event.data as typeof SessionNs.Event.Created.data.Type).info as SessionNs.Info),
)
return Effect.void
})
yield* Effect.addFinalizer(() => unsub)
@ -127,7 +130,10 @@ describe("step-finish token propagation via event", () => {
const received = yield* Deferred.make<SessionLegacy.Part>()
const unsub = yield* events.listen((event) => {
if (event.type === MessageV2.Event.PartUpdated.type)
Deferred.doneUnsafe(received, Effect.succeed((event.data as typeof MessageV2.Event.PartUpdated.data.Type).part as SessionLegacy.Part))
Deferred.doneUnsafe(
received,
Effect.succeed((event.data as typeof MessageV2.Event.PartUpdated.data.Type).part as SessionLegacy.Part),
)
return Effect.void
})
yield* Effect.addFinalizer(() => unsub)

View file

@ -75,7 +75,12 @@ function wired(client: HttpClient.HttpClient) {
const share = (id: SessionID) =>
Effect.gen(function* () {
const { db } = yield* Database.Service
return yield* db.select().from(SessionShareTable).where(eq(SessionShareTable.session_id, id)).get().pipe(Effect.orDie)
return yield* db
.select()
.from(SessionShareTable)
.where(eq(SessionShareTable.session_id, id))
.get()
.pipe(Effect.orDie)
})
const seed = (url: string, org?: string) =>

View file

@ -6,7 +6,13 @@ import fs from "fs/promises"
import path from "path"
import { Effect, Fiber, Layer } from "effect"
import { Snapshot } from "../../src/snapshot"
import { disposeAllInstances, provideInstance, testInstanceStoreLayer, TestInstance, tmpdirScoped } from "../fixture/fixture"
import {
disposeAllInstances,
provideInstance,
testInstanceStoreLayer,
TestInstance,
tmpdirScoped,
} from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(Snapshot.defaultLayer, AppFileSystem.defaultLayer, testInstanceStoreLayer))

View file

@ -17,7 +17,10 @@ function migrations() {
.map((entry) => ({
name: entry.name,
timestamp: Number(entry.name.split("_")[0]),
sql: readFileSync(path.join(import.meta.dirname, "../../../core/migration", entry.name, "migration.sql"), "utf-8"),
sql: readFileSync(
path.join(import.meta.dirname, "../../../core/migration", entry.name, "migration.sql"),
"utf-8",
),
}))
.sort((a, b) => a.timestamp - b.timestamp)
}

View file

@ -102,22 +102,22 @@ describe("tool.lsp", () => {
"keeps cursor details for position-based operations",
() =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const file = path.join(dir, "test.ts")
yield* put(file)
const dir = (yield* TestInstance).directory
const file = path.join(dir, "test.ts")
yield* put(file)
const { items, next } = asks()
const result = yield* run({ operation: "goToDefinition", filePath: file, line: 3, character: 7 }, next)
const req = items.find((item) => item.permission === "lsp")
const { items, next } = asks()
const result = yield* run({ operation: "goToDefinition", filePath: file, line: 3, character: 7 }, next)
const req = items.find((item) => item.permission === "lsp")
expect(req).toBeDefined()
expect(req!.metadata).toEqual({
operation: "goToDefinition",
filePath: file,
line: 3,
character: 7,
})
expect(result.title).toBe("goToDefinition test.ts:3:7")
expect(req).toBeDefined()
expect(req!.metadata).toEqual({
operation: "goToDefinition",
filePath: file,
line: 3,
character: 7,
})
expect(result.title).toBe("goToDefinition test.ts:3:7")
}),
{ git: true },
)
@ -126,20 +126,20 @@ describe("tool.lsp", () => {
"omits cursor details for documentSymbol",
() =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const file = path.join(dir, "test.ts")
yield* put(file)
const dir = (yield* TestInstance).directory
const file = path.join(dir, "test.ts")
yield* put(file)
const { items, next } = asks()
const result = yield* run({ operation: "documentSymbol", filePath: file, line: 3, character: 7 }, next)
const req = items.find((item) => item.permission === "lsp")
const { items, next } = asks()
const result = yield* run({ operation: "documentSymbol", filePath: file, line: 3, character: 7 }, next)
const req = items.find((item) => item.permission === "lsp")
expect(req).toBeDefined()
expect(req!.metadata).toEqual({
operation: "documentSymbol",
filePath: file,
})
expect(result.title).toBe("documentSymbol test.ts")
expect(req).toBeDefined()
expect(req!.metadata).toEqual({
operation: "documentSymbol",
filePath: file,
})
expect(result.title).toBe("documentSymbol test.ts")
}),
{ git: true },
)
@ -148,20 +148,20 @@ describe("tool.lsp", () => {
"omits file and cursor details for workspaceSymbol",
() =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
workspaceSymbolQueries.length = 0
const file = path.join(dir, "test.ts")
yield* put(file)
const dir = (yield* TestInstance).directory
workspaceSymbolQueries.length = 0
const file = path.join(dir, "test.ts")
yield* put(file)
const { items, next } = asks()
const result = yield* run({ operation: "workspaceSymbol", filePath: file, line: 3, character: 7 }, next)
const req = items.find((item) => item.permission === "lsp")
const { items, next } = asks()
const result = yield* run({ operation: "workspaceSymbol", filePath: file, line: 3, character: 7 }, next)
const req = items.find((item) => item.permission === "lsp")
expect(req).toBeDefined()
expect(req!.metadata).toEqual({
operation: "workspaceSymbol",
})
expect(result.title).toBe("workspaceSymbol")
expect(req).toBeDefined()
expect(req!.metadata).toEqual({
operation: "workspaceSymbol",
})
expect(result.title).toBe("workspaceSymbol")
}),
{ git: true },
)
@ -170,15 +170,15 @@ describe("tool.lsp", () => {
"passes workspaceSymbol query to LSP",
() =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
workspaceSymbolQueries.length = 0
const file = path.join(dir, "test.ts")
yield* put(file)
const dir = (yield* TestInstance).directory
workspaceSymbolQueries.length = 0
const file = path.join(dir, "test.ts")
yield* put(file)
yield* run({ operation: "workspaceSymbol", filePath: file, line: 3, character: 7, query: "TestSymbol" })
yield* run({ operation: "workspaceSymbol", filePath: file, line: 3, character: 7 })
yield* run({ operation: "workspaceSymbol", filePath: file, line: 3, character: 7, query: "TestSymbol" })
yield* run({ operation: "workspaceSymbol", filePath: file, line: 3, character: 7 })
expect(workspaceSymbolQueries).toEqual(["TestSymbol", ""])
expect(workspaceSymbolQueries).toEqual(["TestSymbol", ""])
}),
{ git: true },
)

View file

@ -15,7 +15,13 @@ import { ReadTool } from "../../src/tool/read"
import { Truncate } from "@/tool/truncate"
import { Tool } from "@/tool/tool"
import { Filesystem } from "@/util/filesystem"
import { disposeAllInstances, provideInstance, testInstanceStoreLayer, TestInstance, tmpdirScoped } from "../fixture/fixture"
import {
disposeAllInstances,
provideInstance,
testInstanceStoreLayer,
TestInstance,
tmpdirScoped,
} from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { Reference } from "@/reference/reference"
import { RepositoryCache } from "@/reference/repository-cache"

View file

@ -82,153 +82,153 @@ const githubBase = <A, E, R>(url: string, self: Effect.Effect<A, E, R>) =>
describe("tool.repo_clone", () => {
it.instance("clones a repo into the managed cache and reuses it on subsequent calls", () =>
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const source = yield* tmpdirScoped({ git: true })
const remoteRoot = yield* tmpdirScoped()
const remoteDir = path.join(remoteRoot, "owner")
const remoteRepo = path.join(remoteDir, "repo.git")
const fs = yield* AppFileSystem.Service
const source = yield* tmpdirScoped({ git: true })
const remoteRoot = yield* tmpdirScoped()
const remoteDir = path.join(remoteRoot, "owner")
const remoteRepo = path.join(remoteDir, "repo.git")
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v1\n"))
yield* git(source, ["add", "."])
yield* git(source, ["commit", "-m", "add readme"])
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v1\n"))
yield* git(source, ["add", "."])
yield* git(source, ["commit", "-m", "add readme"])
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
const tool = yield* init()
const cloned = yield* githubBase(`file://${remoteRoot}/`, tool.execute({ repository: "owner/repo" }, ctx))
const cached = yield* githubBase(
`file://${remoteRoot}/`,
tool.execute({ repository: "https://github.com/owner/repo.git" }, ctx),
)
const tool = yield* init()
const cloned = yield* githubBase(`file://${remoteRoot}/`, tool.execute({ repository: "owner/repo" }, ctx))
const cached = yield* githubBase(
`file://${remoteRoot}/`,
tool.execute({ repository: "https://github.com/owner/repo.git" }, ctx),
)
expect(cloned.metadata.status).toBe("cloned")
expect(cloned.metadata.localPath).toBe(path.join(Global.Path.repos, "github.com", "owner", "repo"))
expect(cached.metadata.status).toBe("cached")
expect(yield* fs.readFileString(path.join(cloned.metadata.localPath, "README.md"))).toBe("v1\n")
expect(cloned.metadata.status).toBe("cloned")
expect(cloned.metadata.localPath).toBe(path.join(Global.Path.repos, "github.com", "owner", "repo"))
expect(cached.metadata.status).toBe("cached")
expect(yield* fs.readFileString(path.join(cloned.metadata.localPath, "README.md"))).toBe("v1\n")
}),
)
it.instance("refresh updates an existing cached clone", () =>
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const source = yield* tmpdirScoped({ git: true })
const remoteRoot = yield* tmpdirScoped()
const remoteDir = path.join(remoteRoot, "owner")
const remoteRepo = path.join(remoteDir, "repo.git")
const fs = yield* AppFileSystem.Service
const source = yield* tmpdirScoped({ git: true })
const remoteRoot = yield* tmpdirScoped()
const remoteDir = path.join(remoteRoot, "owner")
const remoteRepo = path.join(remoteDir, "repo.git")
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v1\n"))
yield* git(source, ["add", "."])
yield* git(source, ["commit", "-m", "add readme"])
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v1\n"))
yield* git(source, ["add", "."])
yield* git(source, ["commit", "-m", "add readme"])
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
const branch = yield* git(source, ["branch", "--show-current"])
yield* git(source, ["remote", "add", "origin", remoteRepo])
yield* git(source, ["push", "-u", "origin", `${branch}:${branch}`])
const branch = yield* git(source, ["branch", "--show-current"])
yield* git(source, ["remote", "add", "origin", remoteRepo])
yield* git(source, ["push", "-u", "origin", `${branch}:${branch}`])
const tool = yield* init()
const first = yield* githubBase(`file://${remoteRoot}/`, tool.execute({ repository: "owner/repo" }, ctx))
const tool = yield* init()
const first = yield* githubBase(`file://${remoteRoot}/`, tool.execute({ repository: "owner/repo" }, ctx))
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v2\n"))
yield* git(source, ["add", "."])
yield* git(source, ["commit", "-m", "update readme"])
yield* git(source, ["push", "origin", `${branch}:${branch}`])
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "v2\n"))
yield* git(source, ["add", "."])
yield* git(source, ["commit", "-m", "update readme"])
yield* git(source, ["push", "origin", `${branch}:${branch}`])
const refreshed = yield* githubBase(
`file://${remoteRoot}/`,
tool.execute({ repository: "owner/repo", refresh: true }, ctx),
)
const refreshed = yield* githubBase(
`file://${remoteRoot}/`,
tool.execute({ repository: "owner/repo", refresh: true }, ctx),
)
expect(first.metadata.status).toBe("cloned")
expect(refreshed.metadata.status).toBe("refreshed")
expect(yield* fs.readFileString(path.join(first.metadata.localPath, "README.md"))).toBe("v2\n")
expect(first.metadata.status).toBe("cloned")
expect(refreshed.metadata.status).toBe("refreshed")
expect(yield* fs.readFileString(path.join(first.metadata.localPath, "README.md"))).toBe("v2\n")
}),
)
it.instance("clones a configured branch", () =>
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const source = yield* tmpdirScoped({ git: true })
const remoteRoot = yield* tmpdirScoped()
const remoteDir = path.join(remoteRoot, "owner")
const remoteRepo = path.join(remoteDir, "repo.git")
const fs = yield* AppFileSystem.Service
const source = yield* tmpdirScoped({ git: true })
const remoteRoot = yield* tmpdirScoped()
const remoteDir = path.join(remoteRoot, "owner")
const remoteRepo = path.join(remoteDir, "repo.git")
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "main\n"))
yield* git(source, ["add", "."])
yield* git(source, ["commit", "-m", "add readme"])
yield* git(source, ["checkout", "-b", "docs"])
yield* Effect.promise(() => Bun.write(path.join(source, "DOCS.md"), "docs\n"))
yield* git(source, ["add", "."])
yield* git(source, ["commit", "-m", "add docs"])
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
yield* Effect.promise(() => Bun.write(path.join(source, "README.md"), "main\n"))
yield* git(source, ["add", "."])
yield* git(source, ["commit", "-m", "add readme"])
yield* git(source, ["checkout", "-b", "docs"])
yield* Effect.promise(() => Bun.write(path.join(source, "DOCS.md"), "docs\n"))
yield* git(source, ["add", "."])
yield* git(source, ["commit", "-m", "add docs"])
yield* fs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
const tool = yield* init()
const result = yield* githubBase(
`file://${remoteRoot}/`,
tool.execute({ repository: "owner/repo", branch: "docs" }, ctx),
)
const tool = yield* init()
const result = yield* githubBase(
`file://${remoteRoot}/`,
tool.execute({ repository: "owner/repo", branch: "docs" }, ctx),
)
expect(result.metadata.status).toBe("cloned")
expect(result.metadata.branch).toBe("docs")
expect(yield* fs.readFileString(path.join(result.metadata.localPath, "DOCS.md"))).toBe("docs\n")
expect(result.metadata.status).toBe("cloned")
expect(result.metadata.branch).toBe("docs")
expect(yield* fs.readFileString(path.join(result.metadata.localPath, "DOCS.md"))).toBe("docs\n")
}),
)
it.instance("rejects invalid repository inputs", () =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const tool = yield* init()
const inputs = [
{ repository: "not-a-repo", message: "git URL" },
{ repository: "git@github.com:../../../etc/passwd", message: "git URL" },
{ repository: "-u:foo/bar", message: "git URL" },
{ repository: pathToFileURL(path.join(dir, "local.git")).href, message: "Local file" },
]
const dir = (yield* TestInstance).directory
const tool = yield* init()
const inputs = [
{ repository: "not-a-repo", message: "git URL" },
{ repository: "git@github.com:../../../etc/passwd", message: "git URL" },
{ repository: "-u:foo/bar", message: "git URL" },
{ repository: pathToFileURL(path.join(dir, "local.git")).href, message: "Local file" },
]
yield* Effect.forEach(
inputs,
(input) =>
Effect.gen(function* () {
const result = yield* tool.execute({ repository: input.repository }, ctx).pipe(Effect.exit)
yield* Effect.forEach(
inputs,
(input) =>
Effect.gen(function* () {
const result = yield* tool.execute({ repository: input.repository }, ctx).pipe(Effect.exit)
expect(Exit.isFailure(result)).toBe(true)
if (Exit.isFailure(result)) {
const error = Cause.squash(result.cause)
expect(error instanceof Error ? error.message : String(error)).toContain(input.message)
}
}),
{ discard: true },
)
expect(Exit.isFailure(result)).toBe(true)
if (Exit.isFailure(result)) {
const error = Cause.squash(result.cause)
expect(error instanceof Error ? error.message : String(error)).toContain(input.message)
}
}),
{ discard: true },
)
}),
)
it.instance("rejects local file repository URLs", () =>
Effect.gen(function* () {
const source = yield* tmpdirScoped({ git: true })
const tool = yield* init()
const result = yield* tool.execute({ repository: pathToFileURL(source).href }, ctx).pipe(Effect.exit)
const source = yield* tmpdirScoped({ git: true })
const tool = yield* init()
const result = yield* tool.execute({ repository: pathToFileURL(source).href }, ctx).pipe(Effect.exit)
expect(Exit.isFailure(result)).toBe(true)
if (Exit.isFailure(result)) {
const error = Cause.squash(result.cause)
expect(error instanceof Error ? error.message : String(error)).toContain("Local file")
}
expect(Exit.isFailure(result)).toBe(true)
if (Exit.isFailure(result)) {
const error = Cause.squash(result.cause)
expect(error instanceof Error ? error.message : String(error)).toContain("Local file")
}
}),
)
it.instance("rejects invalid branch inputs", () =>
Effect.gen(function* () {
const tool = yield* init()
const result = yield* tool.execute({ repository: "owner/repo", branch: "bad..branch" }, ctx).pipe(Effect.exit)
const tool = yield* init()
const result = yield* tool.execute({ repository: "owner/repo", branch: "bad..branch" }, ctx).pipe(Effect.exit)
expect(Exit.isFailure(result)).toBe(true)
if (Exit.isFailure(result)) {
const error = Cause.squash(result.cause)
expect(error instanceof Error ? error.message : String(error)).toContain(
"Branch must contain only alphanumeric characters",
)
}
expect(Exit.isFailure(result)).toBe(true)
if (Exit.isFailure(result)) {
const error = Cause.squash(result.cause)
expect(error instanceof Error ? error.message : String(error)).toContain(
"Branch must contain only alphanumeric characters",
)
}
}),
)
})

View file

@ -45,112 +45,112 @@ const init = Effect.fn("RepoOverviewToolTest.init")(function* () {
describe("tool.repo_overview", () => {
it.instance("summarizes a local repository path", () =>
Effect.gen(function* () {
const repo = yield* tmpdirScoped({ git: true })
const fs = yield* AppFileSystem.Service
yield* fs.writeWithDirs(
path.join(repo, "package.json"),
JSON.stringify(
{
name: "example-repo",
main: "dist/index.js",
module: "dist/index.mjs",
types: "dist/index.d.ts",
exports: {
".": "./dist/index.js",
"./server": "./dist/server.js",
},
bin: {
example: "./bin/example.js",
},
const repo = yield* tmpdirScoped({ git: true })
const fs = yield* AppFileSystem.Service
yield* fs.writeWithDirs(
path.join(repo, "package.json"),
JSON.stringify(
{
name: "example-repo",
main: "dist/index.js",
module: "dist/index.mjs",
types: "dist/index.d.ts",
exports: {
".": "./dist/index.js",
"./server": "./dist/server.js",
},
null,
2,
),
)
yield* fs.writeWithDirs(path.join(repo, "bun.lock"), "")
yield* fs.writeWithDirs(path.join(repo, "README.md"), "# Example\n")
yield* fs.writeWithDirs(path.join(repo, "src", "index.ts"), "export const value = 1\n")
bin: {
example: "./bin/example.js",
},
},
null,
2,
),
)
yield* fs.writeWithDirs(path.join(repo, "bun.lock"), "")
yield* fs.writeWithDirs(path.join(repo, "README.md"), "# Example\n")
yield* fs.writeWithDirs(path.join(repo, "src", "index.ts"), "export const value = 1\n")
const tool = yield* init()
const result = yield* tool.execute({ path: repo }, ctx)
const tool = yield* init()
const result = yield* tool.execute({ path: repo }, ctx)
expect(result.metadata.path).toBe(repo)
expect(result.metadata.ecosystems).toContain("Node.js")
expect(result.metadata.package_manager).toBe("bun")
expect(result.metadata.dependency_files).toEqual(expect.arrayContaining(["package.json", "bun.lock"]))
expect(result.metadata.entrypoints).toEqual(
expect.arrayContaining([
"main: dist/index.js",
"module: dist/index.mjs",
"types: dist/index.d.ts",
"exports: .",
"exports: ./server",
"bin: example",
"file: src/index.ts",
]),
)
expect(result.output).toContain("Top-level structure:")
expect(result.output).toContain("src/")
expect(result.output).toContain("README.md")
expect(result.metadata.path).toBe(repo)
expect(result.metadata.ecosystems).toContain("Node.js")
expect(result.metadata.package_manager).toBe("bun")
expect(result.metadata.dependency_files).toEqual(expect.arrayContaining(["package.json", "bun.lock"]))
expect(result.metadata.entrypoints).toEqual(
expect.arrayContaining([
"main: dist/index.js",
"module: dist/index.mjs",
"types: dist/index.d.ts",
"exports: .",
"exports: ./server",
"bin: example",
"file: src/index.ts",
]),
)
expect(result.output).toContain("Top-level structure:")
expect(result.output).toContain("src/")
expect(result.output).toContain("README.md")
}),
)
it.instance("resolves relative paths from the instance directory", () =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const fs = yield* AppFileSystem.Service
yield* fs.writeWithDirs(path.join(dir, "nested", "README.md"), "# Nested\n")
const dir = (yield* TestInstance).directory
const fs = yield* AppFileSystem.Service
yield* fs.writeWithDirs(path.join(dir, "nested", "README.md"), "# Nested\n")
const tool = yield* init()
const result = yield* tool.execute({ path: "nested" }, ctx)
const tool = yield* init()
const result = yield* tool.execute({ path: "nested" }, ctx)
expect(result.metadata.path).toBe(path.join(dir, "nested"))
expect(result.output).toContain("README.md")
expect(result.metadata.path).toBe(path.join(dir, "nested"))
expect(result.output).toContain("README.md")
}),
)
it.instance("resolves a cached repository from repository shorthand", () =>
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const cached = path.join(Global.Path.repos, "github.com", "owner", "repo")
yield* fs.writeWithDirs(path.join(cached, "package.json"), JSON.stringify({ name: "cached-repo" }, null, 2))
yield* fs.writeWithDirs(path.join(cached, "README.md"), "cached\n")
const fs = yield* AppFileSystem.Service
const cached = path.join(Global.Path.repos, "github.com", "owner", "repo")
yield* fs.writeWithDirs(path.join(cached, "package.json"), JSON.stringify({ name: "cached-repo" }, null, 2))
yield* fs.writeWithDirs(path.join(cached, "README.md"), "cached\n")
const tool = yield* init()
const result = yield* tool.execute({ repository: "owner/repo" }, ctx)
const tool = yield* init()
const result = yield* tool.execute({ repository: "owner/repo" }, ctx)
expect(result.metadata.path).toBe(cached)
expect(result.metadata.repository).toBe("owner/repo")
expect(result.output).toContain("Repository: owner/repo")
expect(result.output).toContain(`Path: ${cached}`)
expect(result.metadata.path).toBe(cached)
expect(result.metadata.repository).toBe("owner/repo")
expect(result.output).toContain("Repository: owner/repo")
expect(result.output).toContain(`Path: ${cached}`)
}),
)
it.instance("fails clearly when a repository is not cloned", () =>
Effect.gen(function* () {
const tool = yield* init()
const result = yield* tool.execute({ repository: "missing/repo" }, ctx).pipe(Effect.exit)
const tool = yield* init()
const result = yield* tool.execute({ repository: "missing/repo" }, ctx).pipe(Effect.exit)
expect(Exit.isFailure(result)).toBe(true)
if (Exit.isFailure(result)) {
const error = Cause.squash(result.cause)
expect(error instanceof Error ? error.message : String(error)).toContain("Use repo_clone first")
}
expect(Exit.isFailure(result)).toBe(true)
if (Exit.isFailure(result)) {
const error = Cause.squash(result.cause)
expect(error instanceof Error ? error.message : String(error)).toContain("Use repo_clone first")
}
}),
)
it.instance("resolves cached repositories from host/path references", () =>
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const cached = path.join(Global.Path.repos, "gitlab.com", "group", "repo")
yield* fs.writeWithDirs(path.join(cached, "README.md"), "cached\n")
const fs = yield* AppFileSystem.Service
const cached = path.join(Global.Path.repos, "gitlab.com", "group", "repo")
yield* fs.writeWithDirs(path.join(cached, "README.md"), "cached\n")
const tool = yield* init()
const result = yield* tool.execute({ repository: "gitlab.com/group/repo" }, ctx)
const tool = yield* init()
const result = yield* tool.execute({ repository: "gitlab.com/group/repo" }, ctx)
expect(result.metadata.path).toBe(cached)
expect(result.metadata.repository).toBe("gitlab.com/group/repo")
expect(result.output).toContain("Repository: gitlab.com/group/repo")
expect(result.metadata.path).toBe(cached)
expect(result.metadata.repository).toBe("gitlab.com/group/repo")
expect(result.output).toContain("Repository: gitlab.com/group/repo")
}),
)
})

View file

@ -32,12 +32,12 @@ const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node))
describe("tool.skill", () => {
it.instance("execute returns skill content block with files", () =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const skill = path.join(dir, ".opencode", "skill", "tool-skill")
yield* Effect.promise(() =>
Bun.write(
path.join(skill, "SKILL.md"),
`---
const dir = (yield* TestInstance).directory
const skill = path.join(dir, ".opencode", "skill", "tool-skill")
yield* Effect.promise(() =>
Bun.write(
path.join(skill, "SKILL.md"),
`---
name: tool-skill
description: Skill for tool tests.
---
@ -46,86 +46,86 @@ description: Skill for tool tests.
Use this skill.
`,
),
)
yield* Effect.promise(() => Bun.write(path.join(skill, "scripts", "demo.txt"), "demo"))
),
)
yield* Effect.promise(() => Bun.write(path.join(skill, "scripts", "demo.txt"), "demo"))
const home = process.env.OPENCODE_TEST_HOME
process.env.OPENCODE_TEST_HOME = dir
yield* Effect.addFinalizer(() =>
const home = process.env.OPENCODE_TEST_HOME
process.env.OPENCODE_TEST_HOME = dir
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
process.env.OPENCODE_TEST_HOME = home
}),
)
const registry = yield* ToolRegistry.Service
const agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
const tool = (yield* registry.tools({
providerID: "opencode" as any,
modelID: "gpt-5" as any,
agent,
})).find((tool) => tool.id === SkillTool.id)
if (!tool) throw new Error("Skill tool not found")
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
const ctx: Tool.Context = {
...baseCtx,
ask: (req) =>
Effect.sync(() => {
process.env.OPENCODE_TEST_HOME = home
requests.push(req)
}),
)
}
const registry = yield* ToolRegistry.Service
const agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
const tool = (yield* registry.tools({
providerID: "opencode" as any,
modelID: "gpt-5" as any,
agent,
})).find((tool) => tool.id === SkillTool.id)
if (!tool) throw new Error("Skill tool not found")
const result = yield* tool.execute({ name: "tool-skill" }, ctx)
const file = path.resolve(skill, "scripts", "demo.txt")
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
const ctx: Tool.Context = {
...baseCtx,
ask: (req) =>
Effect.sync(() => {
requests.push(req)
}),
}
const result = yield* tool.execute({ name: "tool-skill" }, ctx)
const file = path.resolve(skill, "scripts", "demo.txt")
expect(requests.length).toBe(1)
expect(requests[0].permission).toBe("skill")
expect(requests[0].patterns).toContain("tool-skill")
expect(requests[0].always).toContain("tool-skill")
expect(result.metadata.dir).toBe(skill)
expect(result.output).toContain(`<skill_content name="tool-skill">`)
expect(result.output).toContain(`Base directory for this skill: ${pathToFileURL(skill).href}`)
expect(result.output).toContain(`<file>${file}</file>`)
expect(requests.length).toBe(1)
expect(requests[0].permission).toBe("skill")
expect(requests[0].patterns).toContain("tool-skill")
expect(requests[0].always).toContain("tool-skill")
expect(result.metadata.dir).toBe(skill)
expect(result.output).toContain(`<skill_content name="tool-skill">`)
expect(result.output).toContain(`Base directory for this skill: ${pathToFileURL(skill).href}`)
expect(result.output).toContain(`<file>${file}</file>`)
}),
)
it.instance("execute preserves not found message", () =>
Effect.gen(function* () {
const dir = (yield* TestInstance).directory
const home = process.env.OPENCODE_TEST_HOME
process.env.OPENCODE_TEST_HOME = dir
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
process.env.OPENCODE_TEST_HOME = home
}),
const dir = (yield* TestInstance).directory
const home = process.env.OPENCODE_TEST_HOME
process.env.OPENCODE_TEST_HOME = dir
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
process.env.OPENCODE_TEST_HOME = home
}),
)
const registry = yield* ToolRegistry.Service
const agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
const tool = (yield* registry.tools({
providerID: "opencode" as any,
modelID: "gpt-5" as any,
agent,
})).find((tool) => tool.id === SkillTool.id)
if (!tool) throw new Error("Skill tool not found")
const exit = yield* tool
.execute(
{ name: "missing-skill" },
{
...baseCtx,
ask: () => Effect.void,
},
)
.pipe(Effect.exit)
const registry = yield* ToolRegistry.Service
const agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
const tool = (yield* registry.tools({
providerID: "opencode" as any,
modelID: "gpt-5" as any,
agent,
})).find((tool) => tool.id === SkillTool.id)
if (!tool) throw new Error("Skill tool not found")
const exit = yield* tool
.execute(
{ name: "missing-skill" },
{
...baseCtx,
ask: () => Effect.void,
},
)
.pipe(Effect.exit)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) {
const error = Cause.squash(exit.cause)
expect(error).toBeInstanceOf(Error)
if (error instanceof Error) expect(error.message).toContain('Skill "missing-skill" not found.')
}
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) {
const error = Cause.squash(exit.cause)
expect(error).toBeInstanceOf(Error)
if (error instanceof Error) expect(error.message).toContain('Skill "missing-skill" not found.')
}
}),
)
})

View file

@ -12,39 +12,43 @@ test.skip("step snapshots carry over to assistant messages", () => {
const state: SessionMessageUpdater.MemoryState = { messages: [] }
const sessionID = SessionID.make("session")
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.step.started",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(1),
agent: "build",
model: {
id: ModelV2.ID.make("model"),
providerID: ProviderV2.ID.make("provider"),
variant: ModelV2.VariantID.make("default"),
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.step.started",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(1),
agent: "build",
model: {
id: ModelV2.ID.make("model"),
providerID: ProviderV2.ID.make("provider"),
variant: ModelV2.VariantID.make("default"),
},
snapshot: "before",
},
snapshot: "before",
},
} satisfies SessionEvent.Event))
} satisfies SessionEvent.Event),
)
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.step.ended",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(2),
finish: "stop",
cost: 0,
tokens: {
input: 1,
output: 2,
reasoning: 0,
cache: { read: 0, write: 0 },
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.step.ended",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(2),
finish: "stop",
cost: 0,
tokens: {
input: 1,
output: 2,
reasoning: 0,
cache: { read: 0, write: 0 },
},
snapshot: "after",
},
snapshot: "after",
},
} satisfies SessionEvent.Event))
} satisfies SessionEvent.Event),
)
expect(state.messages[0]?.type).toBe("assistant")
if (state.messages[0]?.type !== "assistant") return
@ -56,39 +60,45 @@ test.skip("text ended populates assistant text content", () => {
const state: SessionMessageUpdater.MemoryState = { messages: [] }
const sessionID = SessionID.make("session")
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.step.started",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(1),
agent: "build",
model: {
id: ModelV2.ID.make("model"),
providerID: ProviderV2.ID.make("provider"),
variant: ModelV2.VariantID.make("default"),
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.step.started",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(1),
agent: "build",
model: {
id: ModelV2.ID.make("model"),
providerID: ProviderV2.ID.make("provider"),
variant: ModelV2.VariantID.make("default"),
},
},
},
} satisfies SessionEvent.Event))
} satisfies SessionEvent.Event),
)
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.text.started",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(2),
},
} satisfies SessionEvent.Event))
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.text.started",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(2),
},
} satisfies SessionEvent.Event),
)
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.text.ended",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(3),
text: "hello assistant",
},
} satisfies SessionEvent.Event))
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.text.ended",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(3),
text: "hello assistant",
},
} satisfies SessionEvent.Event),
)
expect(state.messages[0]?.type).toBe("assistant")
if (state.messages[0]?.type !== "assistant") return
@ -100,57 +110,65 @@ test.skip("tool completion stores completed timestamp", () => {
const sessionID = SessionID.make("session")
const callID = "call"
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.step.started",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(1),
agent: "build",
model: {
id: ModelV2.ID.make("model"),
providerID: ProviderV2.ID.make("provider"),
variant: ModelV2.VariantID.make("default"),
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.step.started",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(1),
agent: "build",
model: {
id: ModelV2.ID.make("model"),
providerID: ProviderV2.ID.make("provider"),
variant: ModelV2.VariantID.make("default"),
},
},
},
} satisfies SessionEvent.Event))
} satisfies SessionEvent.Event),
)
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.tool.input.started",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(2),
callID,
name: "bash",
},
} satisfies SessionEvent.Event))
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.tool.input.started",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(2),
callID,
name: "bash",
},
} satisfies SessionEvent.Event),
)
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.tool.called",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(3),
callID,
tool: "bash",
input: { command: "pwd" },
provider: { executed: true, metadata: { source: "provider" } },
},
} satisfies SessionEvent.Event))
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.tool.called",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(3),
callID,
tool: "bash",
input: { command: "pwd" },
provider: { executed: true, metadata: { source: "provider" } },
},
} satisfies SessionEvent.Event),
)
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.tool.success",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(4),
callID,
structured: {},
content: [{ type: "text", text: "/tmp" }],
provider: { executed: true, metadata: { status: "done" } },
},
} satisfies SessionEvent.Event))
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.tool.success",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(4),
callID,
structured: {},
content: [{ type: "text", text: "/tmp" }],
provider: { executed: true, metadata: { status: "done" } },
},
} satisfies SessionEvent.Event),
)
expect(state.messages[0]?.type).toBe("assistant")
if (state.messages[0]?.type !== "assistant") return
@ -165,46 +183,54 @@ test.skip("compaction events reduce to compaction message", () => {
const sessionID = SessionID.make("session")
const id = EventV2.ID.create()
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id,
type: "session.next.compaction.started",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(1),
reason: "auto",
},
} satisfies SessionEvent.Event))
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id,
type: "session.next.compaction.started",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(1),
reason: "auto",
},
} satisfies SessionEvent.Event),
)
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.compaction.delta",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(2),
text: "hello ",
},
} satisfies SessionEvent.Event))
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.compaction.delta",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(2),
text: "hello ",
},
} satisfies SessionEvent.Event),
)
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.compaction.delta",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(3),
text: "summary",
},
} satisfies SessionEvent.Event))
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.compaction.delta",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(3),
text: "summary",
},
} satisfies SessionEvent.Event),
)
Effect.runSync(SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.compaction.ended",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(4),
text: "final summary",
include: "recent context",
},
} satisfies SessionEvent.Event))
Effect.runSync(
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
type: "session.next.compaction.ended",
data: {
sessionID,
timestamp: DateTime.makeUnsafe(4),
text: "final summary",
include: "recent context",
},
} satisfies SessionEvent.Event),
)
expect(state.messages).toHaveLength(1)
expect(state.messages[0]).toMatchObject({