Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/core/src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export interface Options<State, DraftApi> {
readonly initial: () => State
/** Wraps mutable state in a domain-specific draft API. */
readonly draft: MakeDraft<State, DraftApi>
/** Runs after all active transforms and before the rebuilt state becomes visible. */
/** Runs after the rebuilt state is visible to get(). Often publishes update events. */
readonly finalize?: (draft: DraftApi) => Effect.Effect<void>
}

Expand All @@ -82,8 +82,8 @@ export function create<State, DraftApi>(options: Options<State, DraftApi>): Inte

const commit = Effect.fn("State.commit")(function* (next: State) {
const api = options.draft(next)
if (options.finalize) yield* options.finalize(api)
state = next
if (options.finalize) yield* options.finalize(api)
})

const apply = (transform: TransformCallback<DraftApi>, draft: DraftApi) =>
Expand Down
6 changes: 3 additions & 3 deletions packages/core/test/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,18 @@ const it = testEffect(
)

describe("AgentV2", () => {
it.effect("publishes an updated event after agent changes", () =>
it.effect("publishes an updated event after agent changes are visible", () =>
Effect.gen(function* () {
const agent = yield* AgentV2.Service
const events = yield* EventV2.Service
const updated = yield* events
.subscribe(AgentV2.Event.Updated)
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
.pipe(Stream.take(1), Stream.runHead, Effect.andThen(agent.get(AgentV2.ID.make("reviewer"))), Effect.forkScoped)
yield* Effect.yieldNow

yield* agent.transform((editor) => editor.update(AgentV2.ID.make("reviewer"), () => {}))

expect(yield* Fiber.join(updated)).toMatchObject([{ location: { directory: testLocation.directory } }])
expect(yield* Fiber.join(updated)).toMatchObject({ id: "reviewer" })
}),
)

Expand Down
11 changes: 8 additions & 3 deletions packages/core/test/catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,23 @@ const catalogLayer = AppNodeBuilder.build(
const it = testEffect(catalogLayer)

describe("CatalogV2", () => {
it.effect("publishes an updated event after catalog changes", () =>
it.effect("publishes an updated event after catalog changes are visible", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const events = yield* EventV2.Service
const updated = yield* events
.subscribe(Catalog.Event.Updated)
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
.pipe(
Stream.take(1),
Stream.runHead,
Effect.andThen(catalog.provider.get(ProviderV2.ID.make("test"))),
Effect.forkScoped,
)
yield* Effect.yieldNow

yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))

expect((yield* Fiber.join(updated)).length).toBe(1)
expect(yield* Fiber.join(updated)).toMatchObject({ id: "test" })
}),
)

Expand Down
21 changes: 19 additions & 2 deletions packages/core/test/command.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Effect, Fiber, Stream } from "effect"
import { CommandV2 } from "@opencode-ai/core/command"
import { Config } from "@opencode-ai/core/config"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { EventV2 } from "@opencode-ai/core/event"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Location } from "@opencode-ai/core/location"
import { MCP } from "@opencode-ai/core/mcp/index"
import { ModelV2 } from "@opencode-ai/core/model"
Expand All @@ -11,14 +13,29 @@ import { emptyConfigLayer, emptyMcpLayer, testLocationLayer } from "./fixture/mc
import { testEffect } from "./lib/effect"

const it = testEffect(
AppNodeBuilder.build(CommandV2.node, [
AppNodeBuilder.build(LayerNode.group([CommandV2.node, EventV2.node]), [
[MCP.node, emptyMcpLayer],
[Config.node, emptyConfigLayer],
[Location.node, testLocationLayer],
]),
)

describe("CommandV2", () => {
it.effect("publishes an updated event after command changes are visible", () =>
Effect.gen(function* () {
const command = yield* CommandV2.Service
const events = yield* EventV2.Service
const updated = yield* events
.subscribe(CommandV2.Event.Updated)
.pipe(Stream.take(1), Stream.runHead, Effect.andThen(command.get("review")), Effect.forkScoped)
yield* Effect.yieldNow

yield* command.transform((editor) => editor.update("review", (item) => (item.template = "Review")))

expect(yield* Fiber.join(updated)).toMatchObject({ name: "review", template: "Review" })
}),
)

it.effect("applies command transforms and preserves later overrides", () =>
Effect.gen(function* () {
const command = yield* CommandV2.Service
Expand Down
16 changes: 16 additions & 0 deletions packages/core/test/skill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@ function waitForSkillUpdate() {
}

describe("SkillV2", () => {
it.live("publishes an updated event after skill source changes are visible", () =>
Effect.gen(function* () {
const skill = yield* SkillV2.Service
const events = yield* EventV2.Service
const source = { type: "directory" as const, path: AbsolutePath.make("/tmp/opencode-skills") }
const updated = yield* events
.subscribe(SkillV2.Event.Updated)
.pipe(Stream.take(1), Stream.runHead, Effect.andThen(skill.sources()), Effect.forkScoped)
yield* Effect.yieldNow

yield* skill.transform((editor) => editor.source(source))

expect(yield* Fiber.join(updated)).toContainEqual(source)
}),
)

it.live("publishes updates when skill sources change", () =>
Effect.gen(function* () {
const skill = yield* SkillV2.Service
Expand Down
20 changes: 20 additions & 0 deletions packages/core/test/state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ import { testEffect } from "./lib/effect"
const it = testEffect(Layer.empty)

describe("State", () => {
it.effect("makes rebuilt state visible before finalize runs", () =>
Effect.gen(function* () {
let seen: string[] | undefined
const state = State.create({
initial: (): { values: string[] } => ({ values: [] }),
draft: (draft) => ({ add: (item: string) => draft.values.push(item) }),
finalize: () =>
Effect.sync(() => {
seen = state.get().values
}),
})

yield* state.transform((editor) => {
editor.add("visible")
})

expect(seen).toEqual(["visible"])
}),
)

it.effect("commits a transform atomically when its updater is interrupted", () =>
Effect.gen(function* () {
const rebuilding = yield* Deferred.make<void>()
Expand Down
Loading