opencode-agent[bot] před 2 měsíci
rodič
revize
35007094c4

+ 40 - 12
packages/core/src/project-reference.ts

@@ -26,9 +26,21 @@ export type Resolved =
 type Valid = Exclude<Resolved, { kind: "invalid" }>
 
 export type Mention =
-  | { readonly name: string; readonly kind: "reference"; readonly reference: Valid; readonly target?: string; readonly path: string }
+  | {
+      readonly name: string
+      readonly kind: "reference"
+      readonly reference: Valid
+      readonly target?: string
+      readonly path: string
+    }
   | { readonly name: string; readonly kind: "invalid"; readonly target?: string; readonly message: string }
-  | { readonly name: string; readonly kind: "missing"; readonly target: string; readonly path: string; readonly message: string }
+  | {
+      readonly name: string
+      readonly kind: "missing"
+      readonly target: string
+      readonly path: string
+      readonly message: string
+    }
 
 export interface Interface {
   readonly list: () => Effect.Effect<Resolved[]>
@@ -73,7 +85,9 @@ export const layer = Layer.effect(
           repository: reference.repository,
           path: reference.path,
           run: yield* Effect.cached(
-            cache.ensure({ reference: reference.reference, branch: reference.branch, refresh: true }).pipe(Effect.asVoid),
+            cache
+              .ensure({ reference: reference.reference, branch: reference.branch, refresh: true })
+              .pipe(Effect.asVoid),
           ),
         }
       }),
@@ -90,13 +104,12 @@ export const layer = Layer.effect(
           ),
         ),
       { concurrency: 4, discard: true },
-    ).pipe(
-      Effect.forkScoped,
-    )
+    ).pipe(Effect.forkScoped)
 
     const ensurePath = Effect.fn("ProjectReference.ensurePath")(function* (target?: string) {
       const normalized = normalizePath(target)
-      if (!normalized) return yield* Effect.forEach(materializers, (materializer) => materializer.run, { discard: true })
+      if (!normalized)
+        return yield* Effect.forEach(materializers, (materializer) => materializer.run, { discard: true })
       yield* materializers.find((materializer) => contains(materializer.path, normalized))?.run ?? Effect.void
     })
 
@@ -110,7 +123,9 @@ export const layer = Layer.effect(
       ensurePath,
       containsManagedPath: Effect.fn("ProjectReference.containsManagedPath")(function* (target?: string) {
         const normalized = normalizePath(target)
-        return normalized ? references.some((reference) => reference.kind === "git" && contains(reference.path, normalized)) : false
+        return normalized
+          ? references.some((reference) => reference.kind === "git" && contains(reference.path, normalized))
+          : false
       }),
       resolveMention: Effect.fn("ProjectReference.resolveMention")(function* (value: string) {
         const [name, ...rest] = value.split("/")
@@ -122,8 +137,10 @@ export const layer = Layer.effect(
         if (!target) return { name, kind: "reference", reference, path: reference.path }
 
         const resolved = path.resolve(reference.path, target)
-        if (!AppFileSystem.contains(reference.path, resolved)) return { name, kind: "invalid", target, message: "Reference target escapes its root" }
-        if (!(yield* fs.existsSafe(resolved))) return { name, kind: "missing", target, path: resolved, message: "Reference target does not exist" }
+        if (!AppFileSystem.contains(reference.path, resolved))
+          return { name, kind: "invalid", target, message: "Reference target escapes its root" }
+        if (!(yield* fs.existsSafe(resolved)))
+          return { name, kind: "missing", target, path: resolved, message: "Reference target does not exist" }
         return { name, kind: "reference", reference, target, path: resolved }
       }),
     })
@@ -140,7 +157,12 @@ const inert: Interface = {
   containsManagedPath: () => Effect.succeed(false),
 }
 
-export function resolveAll(input: { references: ConfigReference.NormalizedInfo; directory: string; home: string; repos: string }) {
+export function resolveAll(input: {
+  references: ConfigReference.NormalizedInfo
+  directory: string
+  home: string
+  repos: string
+}) {
   const seen = new Map<string, { name: string; branch?: string }>()
   return Object.entries(input.references).map(([name, reference]): Resolved => {
     const resolved = resolve({ name, reference, directory: input.directory, home: input.home, repos: input.repos })
@@ -160,7 +182,13 @@ export function resolveAll(input: { references: ConfigReference.NormalizedInfo;
   })
 }
 
-export function resolve(input: { name: string; reference: ConfigReference.NormalizedEntry; directory: string; home: string; repos: string }): Resolved {
+export function resolve(input: {
+  name: string
+  reference: ConfigReference.NormalizedEntry
+  directory: string
+  home: string
+  repos: string
+}): Resolved {
   if (input.reference.kind === "invalid") return { name: input.name, kind: "invalid", message: input.reference.message }
   if (input.reference.kind === "local") {
     return { name: input.name, kind: "local", path: localPath(input.directory, input.home, input.reference.path) }

+ 114 - 79
packages/core/test/project-reference.test.ts

@@ -18,13 +18,15 @@ import { it } from "./lib/effect"
 
 describe("ProjectReference", () => {
   it.live("uses the broad experimental flag unless references are explicitly configured", () =>
-    withEnv({ OPENCODE_EXPERIMENTAL: "true", OPENCODE_EXPERIMENTAL_REFERENCES: undefined },
+    withEnv(
+      { OPENCODE_EXPERIMENTAL: "true", OPENCODE_EXPERIMENTAL_REFERENCES: undefined },
       Effect.sync(() => {
         expect(Flag.OPENCODE_EXPERIMENTAL_REFERENCES).toBe(true)
       }),
     ).pipe(
       Effect.flatMap(() =>
-        withEnv({ OPENCODE_EXPERIMENTAL: "true", OPENCODE_EXPERIMENTAL_REFERENCES: "false" },
+        withEnv(
+          { OPENCODE_EXPERIMENTAL: "true", OPENCODE_EXPERIMENTAL_REFERENCES: "false" },
           Effect.sync(() => {
             expect(Flag.OPENCODE_EXPERIMENTAL_REFERENCES).toBe(false)
           }),
@@ -85,84 +87,100 @@ describe("ProjectReference", () => {
   )
 
   it.live("merges config aliases and exposes mention and managed-path operations", () =>
-    withoutReferences(withTmp((tmp) => {
-      const calls: RepositoryCache.EnsureInput[] = []
-      const project = path.join(tmp.path, "project")
-      const nested = path.join(project, "packages", "app")
-      const docs = path.join(project, "docs")
-      const repos = path.join(tmp.path, "repos")
-      return Effect.gen(function* () {
-        yield* Effect.promise(async () => {
-          await fs.mkdir(nested, { recursive: true })
-          await fs.mkdir(docs)
-          await fs.writeFile(path.join(docs, "README.md"), "docs")
-        })
+    withoutReferences(
+      withTmp((tmp) => {
+        const calls: RepositoryCache.EnsureInput[] = []
+        const project = path.join(tmp.path, "project")
+        const nested = path.join(project, "packages", "app")
+        const docs = path.join(project, "docs")
+        const repos = path.join(tmp.path, "repos")
+        return Effect.gen(function* () {
+          yield* Effect.promise(async () => {
+            await fs.mkdir(nested, { recursive: true })
+            await fs.mkdir(docs)
+            await fs.writeFile(path.join(docs, "README.md"), "docs")
+          })
 
-        yield* withReferences(
-          Effect.gen(function* () {
-            const references = yield* ProjectReference.Service
-            const git = path.join(repos, "github.com", "owner", "repo")
+          yield* withReferences(
+            Effect.gen(function* () {
+              const references = yield* ProjectReference.Service
+              const git = path.join(repos, "github.com", "owner", "repo")
 
-            expect(yield* references.list()).toMatchObject([
-              { name: "docs", kind: "local", path: docs },
-              { name: "sdk", kind: "git", path: git },
-            ])
-            expect(yield* references.resolveMention("docs/README.md")).toMatchObject({
-              name: "docs",
-              kind: "reference",
-              target: "README.md",
-              path: path.join(docs, "README.md"),
-            })
-            expect(yield* references.resolveMention("docs/missing.md")).toMatchObject({ name: "docs", kind: "missing" })
-            expect(yield* references.resolveMention("docs/../outside.md")).toMatchObject({ name: "docs", kind: "invalid" })
-            expect(yield* references.resolveMention("unknown")).toBeUndefined()
-            expect(yield* references.resolveMention("sdk")).toMatchObject({ name: "sdk", kind: "reference", path: git })
-            expect(yield* references.containsManagedPath(path.join(git, "README.md"))).toBe(true)
-            expect(yield* references.containsManagedPath(path.join(docs, "README.md"))).toBe(false)
-            yield* references.ensurePath()
-            expect(calls).toHaveLength(1)
-          }).pipe(
-            Effect.provide(
-              testLayer({
-                directory: nested,
-                project,
-                repos,
-                documents: [
-                  document({ docs: { path: "./old-docs" }, sdk: "owner/old" }),
-                  document({ docs: { path: "./docs" }, sdk: { repository: "owner/repo", branch: "main" } }),
-                ],
-                ensure: (input) => Effect.sync(() => result(repos, calls, input)),
-              }),
+              expect(yield* references.list()).toMatchObject([
+                { name: "docs", kind: "local", path: docs },
+                { name: "sdk", kind: "git", path: git },
+              ])
+              expect(yield* references.resolveMention("docs/README.md")).toMatchObject({
+                name: "docs",
+                kind: "reference",
+                target: "README.md",
+                path: path.join(docs, "README.md"),
+              })
+              expect(yield* references.resolveMention("docs/missing.md")).toMatchObject({
+                name: "docs",
+                kind: "missing",
+              })
+              expect(yield* references.resolveMention("docs/../outside.md")).toMatchObject({
+                name: "docs",
+                kind: "invalid",
+              })
+              expect(yield* references.resolveMention("unknown")).toBeUndefined()
+              expect(yield* references.resolveMention("sdk")).toMatchObject({
+                name: "sdk",
+                kind: "reference",
+                path: git,
+              })
+              expect(yield* references.containsManagedPath(path.join(git, "README.md"))).toBe(true)
+              expect(yield* references.containsManagedPath(path.join(docs, "README.md"))).toBe(false)
+              yield* references.ensurePath()
+              expect(calls).toHaveLength(1)
+            }).pipe(
+              Effect.provide(
+                testLayer({
+                  directory: nested,
+                  project,
+                  repos,
+                  documents: [
+                    document({ docs: { path: "./old-docs" }, sdk: "owner/old" }),
+                    document({ docs: { path: "./docs" }, sdk: { repository: "owner/repo", branch: "main" } }),
+                  ],
+                  ensure: (input) => Effect.sync(() => result(repos, calls, input)),
+                }),
+              ),
             ),
-          ),
-        )
-      })
-    })),
+          )
+        })
+      }),
+    ),
   )
 
   it.live("is inert while the runtime flag is disabled", () =>
-    withoutReferences(withTmp((tmp) => {
-      const calls: RepositoryCache.EnsureInput[] = []
-      return Effect.gen(function* () {
-        const references = yield* ProjectReference.Service
-        expect(yield* references.list()).toEqual([])
-        expect(yield* references.get("sdk")).toBeUndefined()
-        expect(yield* references.resolveMention("sdk")).toBeUndefined()
-        expect(yield* references.containsManagedPath(path.join(tmp.path, "repos", "github.com", "owner", "repo"))).toBe(false)
-        yield* references.ensurePath()
-        expect(calls).toEqual([])
-      }).pipe(
-        Effect.provide(
-          testLayer({
-            directory: tmp.path,
-            project: tmp.path,
-            repos: path.join(tmp.path, "repos"),
-            documents: [document({ sdk: "owner/repo" })],
-            ensure: (input) => Effect.sync(() => result(path.join(tmp.path, "repos"), calls, input)),
-          }),
-        ),
-      )
-    })),
+    withoutReferences(
+      withTmp((tmp) => {
+        const calls: RepositoryCache.EnsureInput[] = []
+        return Effect.gen(function* () {
+          const references = yield* ProjectReference.Service
+          expect(yield* references.list()).toEqual([])
+          expect(yield* references.get("sdk")).toBeUndefined()
+          expect(yield* references.resolveMention("sdk")).toBeUndefined()
+          expect(
+            yield* references.containsManagedPath(path.join(tmp.path, "repos", "github.com", "owner", "repo")),
+          ).toBe(false)
+          yield* references.ensurePath()
+          expect(calls).toEqual([])
+        }).pipe(
+          Effect.provide(
+            testLayer({
+              directory: tmp.path,
+              project: tmp.path,
+              repos: path.join(tmp.path, "repos"),
+              documents: [document({ sdk: "owner/repo" })],
+              ensure: (input) => Effect.sync(() => result(path.join(tmp.path, "repos"), calls, input)),
+            }),
+          ),
+        )
+      }),
+    ),
   )
 
   it.live("starts Git materialization in the background without blocking the location layer", () =>
@@ -173,7 +191,10 @@ describe("ProjectReference", () => {
           Effect.gen(function* () {
             expect(yield* (yield* ProjectReference.Service).list()).toHaveLength(1)
             yield* Deferred.await(started).pipe(
-              Effect.timeoutOrElse({ duration: "1 second", orElse: () => Effect.die(new Error("refresh did not start")) }),
+              Effect.timeoutOrElse({
+                duration: "1 second",
+                orElse: () => Effect.die(new Error("refresh did not start")),
+              }),
             )
           }).pipe(
             Effect.provide(
@@ -196,7 +217,11 @@ function document(references: ConfigReference.Info) {
   return new Config.Loaded({ source: { type: "memory" }, info: Schema.decodeUnknownSync(Config.Info)({ references }) })
 }
 
-function result(repos: string, calls: RepositoryCache.EnsureInput[], input: RepositoryCache.EnsureInput): RepositoryCache.Result {
+function result(
+  repos: string,
+  calls: RepositoryCache.EnsureInput[],
+  input: RepositoryCache.EnsureInput,
+): RepositoryCache.Result {
   calls.push(input)
   return {
     repository: input.reference.label,
@@ -223,10 +248,16 @@ function testLayer(input: {
         Layer.succeed(
           Location.Service,
           Location.Service.of(
-            location({ directory: AbsolutePath.make(input.directory) }, { projectDirectory: AbsolutePath.make(input.project) }),
+            location(
+              { directory: AbsolutePath.make(input.directory) },
+              { projectDirectory: AbsolutePath.make(input.project) },
+            ),
           ),
         ),
-        Layer.succeed(Config.Service, Config.Service.of({ directories: () => Effect.succeed([]), get: () => Effect.succeed(input.documents) })),
+        Layer.succeed(
+          Config.Service,
+          Config.Service.of({ directories: () => Effect.succeed([]), get: () => Effect.succeed(input.documents) }),
+        ),
         Layer.succeed(RepositoryCache.Service, RepositoryCache.Service.of({ ensure: input.ensure })),
       ),
     ),
@@ -234,7 +265,11 @@ function testLayer(input: {
 }
 
 function withTmp<A, E, R>(body: (tmp: Awaited<ReturnType<typeof tmpdir>>) => Effect.Effect<A, E, R>) {
-  return Effect.acquireUseRelease(Effect.promise(() => tmpdir()), body, (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()))
+  return Effect.acquireUseRelease(
+    Effect.promise(() => tmpdir()),
+    body,
+    (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
+  )
 }
 
 function withReferences<A, E, R>(body: Effect.Effect<A, E, R>) {