Pārlūkot izejas kodu

fix(core): normalize Windows location keys (#42745)

Luke Parker 1 dienu atpakaļ
vecāks
revīzija
f725443e30

+ 9 - 5
packages/core/src/location-services.ts

@@ -1,4 +1,5 @@
 import { Effect, Layer, LayerMap } from "effect"
 import { Effect, Layer, LayerMap } from "effect"
+import path from "path"
 import { Agent } from "./agent.js"
 import { Agent } from "./agent.js"
 import { AISDK } from "./aisdk.js"
 import { AISDK } from "./aisdk.js"
 import { Catalog } from "./catalog.js"
 import { Catalog } from "./catalog.js"
@@ -49,6 +50,7 @@ import { ReadToolFileSystem } from "./tool/read-filesystem.js"
 import { Tool } from "./tool.js"
 import { Tool } from "./tool.js"
 import { ToolOutput } from "./tool-output.js"
 import { ToolOutput } from "./tool-output.js"
 import { Vcs } from "./vcs.js"
 import { Vcs } from "./vcs.js"
+import { AbsolutePath } from "./schema.js"
 
 
 export { LocationServiceMap } from "./location-service-map.js"
 export { LocationServiceMap } from "./location-service-map.js"
 
 
@@ -110,11 +112,13 @@ export type LocationError = LayerNode.Error<typeof locationServices>
 export function buildLocationServiceMap(
 export function buildLocationServiceMap(
   replacements: LayerNode.Replacements = [],
   replacements: LayerNode.Replacements = [],
 ): Layer.Layer<LocationServiceMap.Service> {
 ): Layer.Layer<LocationServiceMap.Service> {
-  // Structural Equal is own-key-set sensitive, so `{ directory }` (schema-decoded
-  // payloads omit optional keys) and `{ directory, workspaceID: undefined }` are
-  // different RcMap keys. The RcMap caches by the raw key before the build
-  // callback runs, so canonicalize at the map boundary to the key-present shape.
-  const canonical = (ref: Location.Ref) => Location.Ref.make({ directory: ref.directory, workspaceID: ref.workspaceID })
+  // Structural Equal distinguishes optional-key shape and Windows separator style.
+  // The RcMap caches the raw key before the build callback, so normalize both here.
+  const canonical = (ref: Location.Ref) =>
+    Location.Ref.make({
+      directory: AbsolutePath.make(process.platform === "win32" ? path.normalize(ref.directory) : ref.directory),
+      workspaceID: ref.workspaceID,
+    })
   return Layer.effect(
   return Layer.effect(
     LocationServiceMap.Service,
     LocationServiceMap.Service,
     Effect.map(
     Effect.map(

+ 7 - 3
packages/core/test/location-layer.test.ts

@@ -510,7 +510,7 @@ describe("LocationServiceMap", () => {
     ),
     ),
   )
   )
 
 
-  it.live("normalizes ref key shapes to one cached location graph", () =>
+  it.live("normalizes equivalent refs to one cached location graph", () =>
     Effect.acquireRelease(
     Effect.acquireRelease(
       Effect.promise(() => tmpdir()),
       Effect.promise(() => tmpdir()),
       (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
       (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
@@ -520,16 +520,20 @@ describe("LocationServiceMap", () => {
           Effect.gen(function* () {
           Effect.gen(function* () {
             const locations = yield* LocationServiceMap.Service
             const locations = yield* LocationServiceMap.Service
             const directory = AbsolutePath.make(dir.path)
             const directory = AbsolutePath.make(dir.path)
-            const absent = Location.Ref.make({ directory })
+            const alternate = AbsolutePath.make(directory.replaceAll("\\", "/"))
+            const absent = Location.Ref.make({ directory: alternate })
             const present = Location.Ref.make({ directory, workspaceID: undefined })
             const present = Location.Ref.make({ directory, workspaceID: undefined })
             // The two shapes are not structurally Equal: own-key sets differ.
             // The two shapes are not structurally Equal: own-key sets differ.
             expect(Object.keys(absent)).toEqual(["directory"])
             expect(Object.keys(absent)).toEqual(["directory"])
             expect(Object.keys(present)).toEqual(["directory", "workspaceID"])
             expect(Object.keys(present)).toEqual(["directory", "workspaceID"])
             expect(Equal.equals(absent, present)).toBe(false)
             expect(Equal.equals(absent, present)).toBe(false)
+            if (process.platform === "win32") expect(absent.directory).not.toBe(present.directory)
 
 
             const first = yield* locations.contextEffect(absent)
             const first = yield* locations.contextEffect(absent)
             expect(yield* locations.contextEffect(present)).toBe(first)
             expect(yield* locations.contextEffect(present)).toBe(first)
-            expect(Array.from(yield* RcMap.keys(locations.rcMap))).toHaveLength(1)
+            expect(Array.from(yield* RcMap.keys(locations.rcMap))).toEqual([
+              Location.Ref.make({ directory, workspaceID: undefined }),
+            ])
 
 
             // Invalidating with the shape opposite to the one that booted must evict.
             // Invalidating with the shape opposite to the one that booted must evict.
             yield* locations.invalidate(present)
             yield* locations.invalidate(present)