From ce63ca4d7a3552a5e35f98e698967c0d499b2c1d Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 30 Apr 2026 12:51:32 -0400 Subject: [PATCH] test: use testEffect for system prompt test (#25047) --- packages/opencode/src/skill/index.ts | 12 +- packages/opencode/test/cli/tui/thread.test.ts | 55 +++++--- packages/opencode/test/session/system.test.ts | 124 +++++++++--------- 3 files changed, 109 insertions(+), 82 deletions(-) diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index 9750742f97..a4e3fb6d93 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -1,4 +1,3 @@ -import os from "os" import path from "path" import { pathToFileURL } from "url" import z from "zod" @@ -148,6 +147,7 @@ const discoverSkills = Effect.fnUntraced(function* ( config: Config.Interface, discovery: Discovery.Interface, fsys: AppFileSystem.Interface, + global: Global.Interface, directory: string, worktree: string, ) { @@ -159,7 +159,7 @@ const discoverSkills = Effect.fnUntraced(function* ( externalDirs.push(AGENTS_EXTERNAL_DIR) for (const dir of externalDirs) { - const root = path.join(Global.Path.home, dir) + const root = path.join(global.home, dir) if (!(yield* fsys.isDir(root))) continue yield* scan(state, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "global" }) } @@ -180,7 +180,7 @@ const discoverSkills = Effect.fnUntraced(function* ( const cfg = yield* config.get() for (const item of cfg.skills?.paths ?? []) { - const expanded = item.startsWith("~/") ? path.join(os.homedir(), item.slice(2)) : item + const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item const dir = path.isAbsolute(expanded) ? expanded : path.join(directory, expanded) if (!(yield* fsys.isDir(dir))) { log.warn("skill path not found", { path: dir }) @@ -221,13 +221,14 @@ export const layer = Layer.effect( const config = yield* Config.Service const bus = yield* Bus.Service const fsys = yield* AppFileSystem.Service + const global = yield* Global.Service const discovered = yield* InstanceState.make( Effect.fn("Skill.discovery")(function* (ctx) { - return yield* discoverSkills(config, discovery, fsys, ctx.directory, ctx.worktree) + return yield* discoverSkills(config, discovery, fsys, global, ctx.directory, ctx.worktree) }), ) const state = yield* InstanceState.make( - Effect.fn("Skill.state")(function* (ctx) { + Effect.fn("Skill.state")(function* () { const s: State = { skills: {}, dirs: new Set() } yield* loadSkills(s, yield* InstanceState.get(discovered), bus) return s @@ -264,6 +265,7 @@ export const defaultLayer = layer.pipe( Layer.provide(Config.defaultLayer), Layer.provide(Bus.layer), Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(Global.layer), ) export function fmt(list: Info[], opts: { verbose: boolean }) { diff --git a/packages/opencode/test/cli/tui/thread.test.ts b/packages/opencode/test/cli/tui/thread.test.ts index e2bd9d7bcc..b743556556 100644 --- a/packages/opencode/test/cli/tui/thread.test.ts +++ b/packages/opencode/test/cli/tui/thread.test.ts @@ -3,18 +3,44 @@ import fs from "fs/promises" import path from "path" import { tmpdir } from "../../fixture/fixture" import * as App from "../../../src/cli/cmd/tui/app" -import { Rpc } from "@/util/rpc" import { UI } from "../../../src/cli/ui" import * as Timeout from "../../../src/util/timeout" import * as Network from "../../../src/cli/network" import * as Win32 from "../../../src/cli/cmd/tui/win32" -import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui" const stop = new Error("stop") +const packageRoot = path.resolve(import.meta.dir, "../../..") const seen = { tui: [] as string[], } +class TestWorker extends EventTarget { + onerror: Worker["onerror"] = null + onmessage: Worker["onmessage"] = null + onmessageerror: Worker["onmessageerror"] = null + + postMessage(data: string) { + const parsed = JSON.parse(data) + if (!parsed || typeof parsed !== "object" || !("method" in parsed) || !("id" in parsed)) return + if (typeof parsed.method !== "string" || typeof parsed.id !== "number") return + const result = + parsed.method === "fetch" + ? { status: 200, headers: {}, body: "" } + : parsed.method === "server" + ? { url: "http://127.0.0.1" } + : parsed.method === "snapshot" + ? "" + : undefined + queueMicrotask(() => { + this.onmessage?.( + new MessageEvent("message", { data: JSON.stringify({ type: "rpc.result", result, id: parsed.id }) }), + ) + }) + } + + terminate() {} +} + function setup() { // Intentionally avoid mock.module() here: Bun keeps module overrides in cache // and mock.restore() does not reset mock.module values. If this switches back @@ -25,10 +51,6 @@ function setup() { if (input.directory) seen.tui.push(input.directory) throw stop }) - spyOn(Rpc, "client").mockImplementation(() => ({ - call: async () => ({ url: "http://127.0.0.1" }) as never, - on: () => () => {}, - })) spyOn(UI, "error").mockImplementation(() => {}) spyOn(Timeout, "withTimeout").mockImplementation((input) => input) spyOn(Network, "resolveNetworkOptions").mockResolvedValue({ @@ -71,7 +93,6 @@ describe("tui thread", () => { async function check(project?: string) { setup() - const cwd = process.cwd() const pwd = process.env.PWD const worker = globalThis.Worker const tty = Object.getOwnPropertyDescriptor(process.stdin, "isTTY") @@ -85,26 +106,26 @@ describe("tui thread", () => { configurable: true, value: true, }) - globalThis.Worker = class extends EventTarget { - onerror = null - onmessage = null - onmessageerror = null - postMessage() {} - terminate() {} - } as unknown as typeof Worker + Object.defineProperty(globalThis, "Worker", { configurable: true, value: TestWorker }) try { process.chdir(tmp.path) process.env.PWD = link - await expect(call(project)).rejects.toBe(stop) + let error: unknown + try { + await call(project) + } catch (caught) { + error = caught + } + expect(error).toBe(stop) expect(seen.tui[0]).toBe(tmp.path) } finally { - process.chdir(cwd) + process.chdir(packageRoot) if (pwd === undefined) delete process.env.PWD else process.env.PWD = pwd if (tty) Object.defineProperty(process.stdin, "isTTY", tty) else delete (process.stdin as { isTTY?: boolean }).isTTY - globalThis.Worker = worker + Object.defineProperty(globalThis, "Worker", { configurable: true, value: worker }) await fs.rm(link, { recursive: true, force: true }).catch(() => undefined) } } diff --git a/packages/opencode/test/session/system.test.ts b/packages/opencode/test/session/system.test.ts index 33123acce6..6e5439da58 100644 --- a/packages/opencode/test/session/system.test.ts +++ b/packages/opencode/test/session/system.test.ts @@ -1,69 +1,73 @@ -import { describe, expect, test } from "bun:test" -import path from "path" -import { Effect } from "effect" -import { Agent } from "../../src/agent/agent" -import { Instance } from "../../src/project/instance" +import { describe, expect } from "bun:test" +import { Effect, Layer } from "effect" +import type { Agent } from "../../src/agent/agent" +import { NamedError } from "@opencode-ai/core/util/error" +import { Skill } from "../../src/skill" +import { Permission } from "../../src/permission" import { SystemPrompt } from "../../src/session/system" -import { provideInstance, tmpdir } from "../fixture/fixture" +import { testEffect } from "../lib/effect" -function load(dir: string, fn: (svc: Agent.Interface) => Effect.Effect) { - return Effect.runPromise(provideInstance(dir)(Agent.Service.use(fn)).pipe(Effect.provide(Agent.defaultLayer))) +const skills: Skill.Info[] = [ + { + name: "zeta-skill", + description: "Zeta skill.", + location: "/tmp/zeta-skill/SKILL.md", + content: "# zeta-skill", + }, + { + name: "alpha-skill", + description: "Alpha skill.", + location: "/tmp/alpha-skill/SKILL.md", + content: "# alpha-skill", + }, + { + name: "middle-skill", + description: "Middle skill.", + location: "/tmp/middle-skill/SKILL.md", + content: "# middle-skill", + }, +] + +const build: Agent.Info = { + name: "build", + mode: "primary", + permission: Permission.fromConfig({ "*": "allow" }), + options: {}, } +const it = testEffect( + SystemPrompt.layer.pipe( + Layer.provide( + Layer.succeed( + Skill.Service, + Skill.Service.of({ + get: (name) => Effect.succeed(skills.find((skill) => skill.name === name)), + all: () => Effect.succeed(skills), + dirs: () => Effect.succeed([]), + available: () => Effect.succeed(skills), + }), + ), + ), + ), +) + describe("session.system", () => { - test("skills output is sorted by name and stable across calls", async () => { - await using tmp = await tmpdir({ - git: true, - init: async (dir) => { - for (const [name, description] of [ - ["zeta-skill", "Zeta skill."], - ["alpha-skill", "Alpha skill."], - ["middle-skill", "Middle skill."], - ]) { - const skillDir = path.join(dir, ".opencode", "skill", name) - await Bun.write( - path.join(skillDir, "SKILL.md"), - `--- -name: ${name} -description: ${description} ---- + it.effect("skills output is sorted by name and stable across calls", () => + Effect.gen(function* () { + const prompt = yield* SystemPrompt.Service + const first = yield* prompt.skills(build) + const second = yield* prompt.skills(build) + const output = first ?? (yield* Effect.fail(new NamedError.Unknown({ message: "missing skills output" }))) -# ${name} -`, - ) - } - }, - }) + expect(first).toBe(second) - const home = process.env.OPENCODE_TEST_HOME - process.env.OPENCODE_TEST_HOME = tmp.path + const alpha = output.indexOf("alpha-skill") + const middle = output.indexOf("middle-skill") + const zeta = output.indexOf("zeta-skill") - try { - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const build = await load(tmp.path, (svc) => svc.get("build")) - const runSkills = Effect.gen(function* () { - const svc = yield* SystemPrompt.Service - return yield* svc.skills(build!) - }).pipe(Effect.provide(SystemPrompt.defaultLayer)) - - const first = await Effect.runPromise(runSkills) - const second = await Effect.runPromise(runSkills) - - expect(first).toBe(second) - - const alpha = first!.indexOf("alpha-skill") - const middle = first!.indexOf("middle-skill") - const zeta = first!.indexOf("zeta-skill") - - expect(alpha).toBeGreaterThan(-1) - expect(middle).toBeGreaterThan(alpha) - expect(zeta).toBeGreaterThan(middle) - }, - }) - } finally { - process.env.OPENCODE_TEST_HOME = home - } - }) + expect(alpha).toBeGreaterThan(-1) + expect(middle).toBeGreaterThan(alpha) + expect(zeta).toBeGreaterThan(middle) + }), + ) })