Преглед изворни кода

test: use testEffect for system prompt test (#25047)

Kit Langton пре 4 месеци
родитељ
комит
ce63ca4d7a

+ 7 - 5
packages/opencode/src/skill/index.ts

@@ -1,4 +1,3 @@
-import os from "os"
 import path from "path"
 import path from "path"
 import { pathToFileURL } from "url"
 import { pathToFileURL } from "url"
 import z from "zod"
 import z from "zod"
@@ -148,6 +147,7 @@ const discoverSkills = Effect.fnUntraced(function* (
   config: Config.Interface,
   config: Config.Interface,
   discovery: Discovery.Interface,
   discovery: Discovery.Interface,
   fsys: AppFileSystem.Interface,
   fsys: AppFileSystem.Interface,
+  global: Global.Interface,
   directory: string,
   directory: string,
   worktree: string,
   worktree: string,
 ) {
 ) {
@@ -159,7 +159,7 @@ const discoverSkills = Effect.fnUntraced(function* (
     externalDirs.push(AGENTS_EXTERNAL_DIR)
     externalDirs.push(AGENTS_EXTERNAL_DIR)
 
 
     for (const dir of externalDirs) {
     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
       if (!(yield* fsys.isDir(root))) continue
       yield* scan(state, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "global" })
       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()
   const cfg = yield* config.get()
   for (const item of cfg.skills?.paths ?? []) {
   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)
     const dir = path.isAbsolute(expanded) ? expanded : path.join(directory, expanded)
     if (!(yield* fsys.isDir(dir))) {
     if (!(yield* fsys.isDir(dir))) {
       log.warn("skill path not found", { path: dir })
       log.warn("skill path not found", { path: dir })
@@ -221,13 +221,14 @@ export const layer = Layer.effect(
     const config = yield* Config.Service
     const config = yield* Config.Service
     const bus = yield* Bus.Service
     const bus = yield* Bus.Service
     const fsys = yield* AppFileSystem.Service
     const fsys = yield* AppFileSystem.Service
+    const global = yield* Global.Service
     const discovered = yield* InstanceState.make(
     const discovered = yield* InstanceState.make(
       Effect.fn("Skill.discovery")(function* (ctx) {
       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(
     const state = yield* InstanceState.make(
-      Effect.fn("Skill.state")(function* (ctx) {
+      Effect.fn("Skill.state")(function* () {
         const s: State = { skills: {}, dirs: new Set() }
         const s: State = { skills: {}, dirs: new Set() }
         yield* loadSkills(s, yield* InstanceState.get(discovered), bus)
         yield* loadSkills(s, yield* InstanceState.get(discovered), bus)
         return s
         return s
@@ -264,6 +265,7 @@ export const defaultLayer = layer.pipe(
   Layer.provide(Config.defaultLayer),
   Layer.provide(Config.defaultLayer),
   Layer.provide(Bus.layer),
   Layer.provide(Bus.layer),
   Layer.provide(AppFileSystem.defaultLayer),
   Layer.provide(AppFileSystem.defaultLayer),
+  Layer.provide(Global.layer),
 )
 )
 
 
 export function fmt(list: Info[], opts: { verbose: boolean }) {
 export function fmt(list: Info[], opts: { verbose: boolean }) {

+ 38 - 17
packages/opencode/test/cli/tui/thread.test.ts

@@ -3,18 +3,44 @@ import fs from "fs/promises"
 import path from "path"
 import path from "path"
 import { tmpdir } from "../../fixture/fixture"
 import { tmpdir } from "../../fixture/fixture"
 import * as App from "../../../src/cli/cmd/tui/app"
 import * as App from "../../../src/cli/cmd/tui/app"
-import { Rpc } from "@/util/rpc"
 import { UI } from "../../../src/cli/ui"
 import { UI } from "../../../src/cli/ui"
 import * as Timeout from "../../../src/util/timeout"
 import * as Timeout from "../../../src/util/timeout"
 import * as Network from "../../../src/cli/network"
 import * as Network from "../../../src/cli/network"
 import * as Win32 from "../../../src/cli/cmd/tui/win32"
 import * as Win32 from "../../../src/cli/cmd/tui/win32"
-import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui"
 
 
 const stop = new Error("stop")
 const stop = new Error("stop")
+const packageRoot = path.resolve(import.meta.dir, "../../..")
 const seen = {
 const seen = {
   tui: [] as string[],
   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() {
 function setup() {
   // Intentionally avoid mock.module() here: Bun keeps module overrides in cache
   // Intentionally avoid mock.module() here: Bun keeps module overrides in cache
   // and mock.restore() does not reset mock.module values. If this switches back
   // 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)
     if (input.directory) seen.tui.push(input.directory)
     throw stop
     throw stop
   })
   })
-  spyOn(Rpc, "client").mockImplementation(() => ({
-    call: async () => ({ url: "http://127.0.0.1" }) as never,
-    on: () => () => {},
-  }))
   spyOn(UI, "error").mockImplementation(() => {})
   spyOn(UI, "error").mockImplementation(() => {})
   spyOn(Timeout, "withTimeout").mockImplementation((input) => input)
   spyOn(Timeout, "withTimeout").mockImplementation((input) => input)
   spyOn(Network, "resolveNetworkOptions").mockResolvedValue({
   spyOn(Network, "resolveNetworkOptions").mockResolvedValue({
@@ -71,7 +93,6 @@ describe("tui thread", () => {
 
 
   async function check(project?: string) {
   async function check(project?: string) {
     setup()
     setup()
-    const cwd = process.cwd()
     const pwd = process.env.PWD
     const pwd = process.env.PWD
     const worker = globalThis.Worker
     const worker = globalThis.Worker
     const tty = Object.getOwnPropertyDescriptor(process.stdin, "isTTY")
     const tty = Object.getOwnPropertyDescriptor(process.stdin, "isTTY")
@@ -85,26 +106,26 @@ describe("tui thread", () => {
       configurable: true,
       configurable: true,
       value: 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 {
     try {
       process.chdir(tmp.path)
       process.chdir(tmp.path)
       process.env.PWD = link
       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)
       expect(seen.tui[0]).toBe(tmp.path)
     } finally {
     } finally {
-      process.chdir(cwd)
+      process.chdir(packageRoot)
       if (pwd === undefined) delete process.env.PWD
       if (pwd === undefined) delete process.env.PWD
       else process.env.PWD = pwd
       else process.env.PWD = pwd
       if (tty) Object.defineProperty(process.stdin, "isTTY", tty)
       if (tty) Object.defineProperty(process.stdin, "isTTY", tty)
       else delete (process.stdin as { isTTY?: boolean }).isTTY
       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)
       await fs.rm(link, { recursive: true, force: true }).catch(() => undefined)
     }
     }
   }
   }

+ 64 - 60
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 { SystemPrompt } from "../../src/session/system"
-import { provideInstance, tmpdir } from "../fixture/fixture"
+import { testEffect } from "../lib/effect"
 
 
-function load<A>(dir: string, fn: (svc: Agent.Interface) => Effect.Effect<A>) {
-  return Effect.runPromise(provideInstance(dir)(Agent.Service.use(fn)).pipe(Effect.provide(Agent.defaultLayer)))
-}
-
-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}
----
+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",
+  },
+]
 
 
-# ${name}
-`,
-          )
-        }
-      },
-    })
-
-    const home = process.env.OPENCODE_TEST_HOME
-    process.env.OPENCODE_TEST_HOME = tmp.path
+const build: Agent.Info = {
+  name: "build",
+  mode: "primary",
+  permission: Permission.fromConfig({ "*": "allow" }),
+  options: {},
+}
 
 
-    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 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),
+        }),
+      ),
+    ),
+  ),
+)
 
 
-          const first = await Effect.runPromise(runSkills)
-          const second = await Effect.runPromise(runSkills)
+describe("session.system", () => {
+  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" })))
 
 
-          expect(first).toBe(second)
+      expect(first).toBe(second)
 
 
-          const alpha = first!.indexOf("<name>alpha-skill</name>")
-          const middle = first!.indexOf("<name>middle-skill</name>")
-          const zeta = first!.indexOf("<name>zeta-skill</name>")
+      const alpha = output.indexOf("<name>alpha-skill</name>")
+      const middle = output.indexOf("<name>middle-skill</name>")
+      const zeta = output.indexOf("<name>zeta-skill</name>")
 
 
-          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)
+    }),
+  )
 })
 })