location-filesystem.test.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  1. import fs from "fs/promises"
  2. import path from "path"
  3. import { fileURLToPath } from "url"
  4. import { describe, expect, test } from "bun:test"
  5. import { Effect, Exit, Layer, Schema } from "effect"
  6. import { FSUtil } from "@opencode-ai/core/fs-util"
  7. import { Location } from "@opencode-ai/core/location"
  8. import { FileSystem } from "@opencode-ai/core/filesystem"
  9. import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
  10. import { ProjectReference } from "@opencode-ai/core/project-reference"
  11. import { Repository } from "@opencode-ai/core/repository"
  12. import { Global } from "@opencode-ai/core/global"
  13. import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
  14. import { tmpdir } from "./fixture/tmpdir"
  15. import { location } from "./fixture/location"
  16. import { it } from "./lib/effect"
  17. const inertReferences = ProjectReference.Service.of({
  18. list: () => Effect.succeed([]),
  19. get: () => Effect.succeed(undefined),
  20. resolveMention: () => Effect.succeed(undefined),
  21. ensurePath: () => Effect.void,
  22. containsManagedPath: () => Effect.succeed(false),
  23. })
  24. function provide(
  25. directory: string,
  26. references = inertReferences,
  27. filesystem = FSUtil.defaultLayer,
  28. data = Global.Path.data,
  29. ) {
  30. return Effect.provide(
  31. FileSystem.layer.pipe(
  32. Layer.provide(
  33. Layer.mergeAll(
  34. filesystem,
  35. Ripgrep.defaultLayer,
  36. Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))),
  37. Layer.succeed(ProjectReference.Service, references),
  38. Global.layerWith({ data }),
  39. ),
  40. ),
  41. ),
  42. )
  43. }
  44. function withTmp<A, E, R>(f: (directory: string) => Effect.Effect<A, E, R>) {
  45. return Effect.acquireRelease(
  46. Effect.promise(() => tmpdir()),
  47. (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
  48. ).pipe(Effect.flatMap((tmp) => f(tmp.path)))
  49. }
  50. describe("FileSystem", () => {
  51. it.live("accepts generated managed output paths and rejects other absolute paths", () =>
  52. withTmp((directory) => {
  53. const worktree = directory
  54. const data = path.join(directory, "data")
  55. return Effect.gen(function* () {
  56. const managed = path.join(data, "tool-output")
  57. const output = path.join(managed, "tool_123")
  58. const unrelated = path.join(directory, "secret.txt")
  59. yield* Effect.promise(() => fs.mkdir(managed, { recursive: true }))
  60. yield* Effect.promise(() => fs.writeFile(output, "failure here"))
  61. yield* Effect.promise(() => fs.writeFile(unrelated, "secret"))
  62. const service = yield* FileSystem.Service
  63. expect(yield* service.read({ path: output })).toMatchObject({ type: "text", content: "failure here" })
  64. expect((yield* service.resolveRoot({ path: output })).real).toBe(output)
  65. expect(yield* Effect.exit(service.read({ path: unrelated }))).toMatchObject({ _tag: "Failure" })
  66. expect(yield* Effect.exit(service.read({ path: managed }))).toMatchObject({ _tag: "Failure" })
  67. }).pipe(provide(worktree, inertReferences, FSUtil.defaultLayer, data))
  68. }),
  69. )
  70. it.live("reads text and binary files", () =>
  71. withTmp((directory) =>
  72. Effect.gen(function* () {
  73. yield* Effect.promise(() => fs.writeFile(path.join(directory, "hello.txt"), "hello"))
  74. yield* Effect.promise(() => fs.writeFile(path.join(directory, "data.bin"), Buffer.from([0, 1, 2])))
  75. const service = yield* FileSystem.Service
  76. expect(yield* service.read({ path: RelativePath.make("hello.txt") })).toEqual({
  77. type: "text",
  78. content: "hello",
  79. mime: "text/plain",
  80. })
  81. expect(yield* service.read({ path: RelativePath.make("data.bin") })).toEqual({
  82. type: "binary",
  83. content: "AAEC",
  84. encoding: "base64",
  85. mime: "application/octet-stream",
  86. })
  87. expect(Exit.isFailure(yield* service.readTool({ path: RelativePath.make("data.bin") }).pipe(Effect.exit))).toBe(
  88. true,
  89. )
  90. }).pipe(provide(directory)),
  91. ),
  92. )
  93. it.live("pages large UTF-8 text files by line with continuation", () =>
  94. withTmp((directory) =>
  95. Effect.gen(function* () {
  96. const lines = Array.from({ length: 30 }, (_, index) => `line-${index + 1}-é`.padEnd(2_000, "x"))
  97. yield* Effect.promise(() => fs.writeFile(path.join(directory, "large.txt"), lines.join("\n")))
  98. const service = yield* FileSystem.Service
  99. const input = { path: RelativePath.make("large.txt") }
  100. const result = yield* service.readTool(input)
  101. expect(result).toMatchObject({
  102. type: "text-page",
  103. offset: 1,
  104. truncated: true,
  105. })
  106. const first = result.type === "text-page" ? result : yield* Effect.die(new Error("Expected a text page"))
  107. expect(first.next).toBeDefined()
  108. const next = first.next!
  109. expect(yield* service.readTool(input, { offset: next, limit: 1 })).toEqual({
  110. type: "text-page",
  111. content: lines[next - 1],
  112. mime: "text/plain",
  113. offset: next,
  114. truncated: true,
  115. next: next + 1,
  116. })
  117. expect(yield* service.readTool(input, { offset: 30 })).toEqual({
  118. type: "text-page",
  119. content: lines[29],
  120. mime: "text/plain",
  121. offset: 30,
  122. truncated: false,
  123. })
  124. }).pipe(provide(directory)),
  125. ),
  126. )
  127. it.live("rejects paged text when a late NUL appears after the requested page", () =>
  128. withTmp((directory) =>
  129. Effect.gen(function* () {
  130. const file = path.join(directory, "late-binary.txt")
  131. yield* Effect.promise(() =>
  132. fs.writeFile(
  133. file,
  134. Buffer.concat([Buffer.from("first\nsecond\n"), Buffer.alloc(80_000, 0x61), Buffer.from([0])]),
  135. ),
  136. )
  137. const service = yield* FileSystem.Service
  138. expect(
  139. Exit.isFailure(
  140. yield* service.readTool({ path: RelativePath.make("late-binary.txt") }, { limit: 1 }).pipe(Effect.exit),
  141. ),
  142. ).toBe(true)
  143. }).pipe(provide(directory)),
  144. ),
  145. )
  146. it.live("rejects paged text when invalid UTF-8 appears near EOF", () =>
  147. withTmp((directory) =>
  148. Effect.gen(function* () {
  149. const file = path.join(directory, "invalid-utf8.txt")
  150. yield* Effect.promise(() =>
  151. fs.writeFile(
  152. file,
  153. Buffer.concat([Buffer.from("first\nsecond\n"), Buffer.alloc(80_000, 0x61), Buffer.from([0xc3, 0x28])]),
  154. ),
  155. )
  156. const service = yield* FileSystem.Service
  157. expect(
  158. Exit.isFailure(
  159. yield* service.readTool({ path: RelativePath.make("invalid-utf8.txt") }, { limit: 1 }).pipe(Effect.exit),
  160. ),
  161. ).toBe(true)
  162. }).pipe(provide(directory)),
  163. ),
  164. )
  165. it.live("rejects PDFs for direct, large, and paged reads", () =>
  166. withTmp((directory) =>
  167. Effect.gen(function* () {
  168. const small = path.join(directory, "small.pdf")
  169. const large = path.join(directory, "large.pdf")
  170. yield* Effect.promise(() => fs.writeFile(small, "%PDF-1.7\nsmall"))
  171. yield* Effect.promise(() =>
  172. fs.writeFile(large, Buffer.concat([Buffer.from("%PDF-1.7\n"), Buffer.alloc(80_000)])),
  173. )
  174. const service = yield* FileSystem.Service
  175. expect(
  176. Exit.isFailure(yield* service.readTool({ path: RelativePath.make("small.pdf") }).pipe(Effect.exit)),
  177. ).toBe(true)
  178. expect(
  179. Exit.isFailure(yield* service.readTool({ path: RelativePath.make("large.pdf") }).pipe(Effect.exit)),
  180. ).toBe(true)
  181. expect(
  182. Exit.isFailure(
  183. yield* service.readTool({ path: RelativePath.make("large.pdf") }, { limit: 1 }).pipe(Effect.exit),
  184. ),
  185. ).toBe(true)
  186. }).pipe(provide(directory)),
  187. ),
  188. )
  189. it.live("rejects signature-bearing media beyond the ingestion cap before loading", () =>
  190. withTmp((directory) =>
  191. Effect.gen(function* () {
  192. const file = path.join(directory, "huge.png")
  193. yield* Effect.promise(async () => {
  194. const handle = await fs.open(file, "w")
  195. try {
  196. await handle.write(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), 0, 8, 0)
  197. await handle.truncate(FileSystem.MAX_MEDIA_INGEST_BYTES + 1)
  198. } finally {
  199. await handle.close()
  200. }
  201. })
  202. const service = yield* FileSystem.Service
  203. const exit = yield* service.readTool({ path: RelativePath.make("huge.png") }).pipe(Effect.exit)
  204. expect(Exit.isFailure(exit)).toBe(true)
  205. if (Exit.isFailure(exit)) expect(String(exit.cause)).toContain("Media exceeds")
  206. }).pipe(provide(directory)),
  207. ),
  208. )
  209. it.live("closes descriptors after successful and failed reads", () =>
  210. withTmp((directory) => {
  211. let active = 0
  212. const filesystem = Layer.effect(
  213. FSUtil.Service,
  214. Effect.gen(function* () {
  215. const service = yield* FSUtil.Service
  216. return FSUtil.Service.of({
  217. ...service,
  218. open: (target, options) =>
  219. Effect.acquireRelease(
  220. service.open(target, options).pipe(Effect.tap(() => Effect.sync(() => active++))),
  221. () => Effect.sync(() => active--),
  222. ),
  223. })
  224. }),
  225. ).pipe(Layer.provide(FSUtil.defaultLayer))
  226. return Effect.gen(function* () {
  227. const text = path.join(directory, "text.txt")
  228. const binary = path.join(directory, "binary.pdf")
  229. yield* Effect.promise(() => fs.writeFile(text, "hello"))
  230. yield* Effect.promise(() => fs.writeFile(binary, "%PDF-1.7"))
  231. const service = yield* FileSystem.Service
  232. const before =
  233. process.platform === "win32"
  234. ? undefined
  235. : yield* Effect.promise(() => fs.readdir("/dev/fd").then((entries) => entries.length))
  236. for (let index = 0; index < 50; index++) {
  237. yield* service.readTool({ path: RelativePath.make("text.txt") })
  238. yield* service.readTool({ path: RelativePath.make("binary.pdf") }).pipe(Effect.exit)
  239. }
  240. expect(active).toBe(0)
  241. if (before !== undefined) {
  242. const after = yield* Effect.promise(() => fs.readdir("/dev/fd").then((entries) => entries.length))
  243. expect(after).toBeLessThanOrEqual(before + 2)
  244. }
  245. yield* Effect.promise(() => fs.rename(text, text + ".moved"))
  246. yield* Effect.promise(() => fs.rename(binary, binary + ".moved"))
  247. }).pipe(provide(directory, inertReferences, filesystem))
  248. }),
  249. )
  250. it.live("lists direct children with relative paths and resolved URIs", () =>
  251. withTmp((directory) =>
  252. Effect.gen(function* () {
  253. yield* Effect.promise(() => fs.mkdir(path.join(directory, "src")))
  254. yield* Effect.promise(() => fs.writeFile(path.join(directory, "README.md"), "# Test"))
  255. const service = yield* FileSystem.Service
  256. const entries = yield* service.list()
  257. expect(entries.map(({ uri: _uri, ...entry }) => entry)).toEqual([
  258. {
  259. path: RelativePath.make("src"),
  260. type: "directory",
  261. mime: "application/x-directory",
  262. },
  263. {
  264. path: RelativePath.make("README.md"),
  265. type: "file",
  266. mime: "text/markdown",
  267. },
  268. ])
  269. expect(
  270. yield* Effect.promise(() => Promise.all(entries.map((entry) => fs.realpath(fileURLToPath(entry.uri))))),
  271. ).toEqual(
  272. yield* Effect.promise(() =>
  273. Promise.all([fs.realpath(path.join(directory, "src")), fs.realpath(path.join(directory, "README.md"))]),
  274. ),
  275. )
  276. }).pipe(provide(directory)),
  277. ),
  278. )
  279. it.live("lists stable bounded pages", () =>
  280. withTmp((directory) =>
  281. Effect.gen(function* () {
  282. yield* Effect.promise(async () => {
  283. await fs.mkdir(path.join(directory, "src"))
  284. await fs.writeFile(path.join(directory, "README.md"), "# Test")
  285. })
  286. const service = yield* FileSystem.Service
  287. expect(yield* service.listPage({ limit: 1 })).toMatchObject({
  288. entries: [{ path: "src", type: "directory" }],
  289. truncated: true,
  290. next: 2,
  291. })
  292. expect(yield* service.listPage({ offset: 2, limit: 1 })).toMatchObject({
  293. entries: [{ path: "README.md", type: "file" }],
  294. truncated: false,
  295. })
  296. expect((yield* service.resolveList()).resource).toBe(".")
  297. }).pipe(provide(directory)),
  298. ),
  299. )
  300. it.live("materializes only the selected direct children for a page", () =>
  301. withTmp((directory) => {
  302. const realPaths: string[] = []
  303. const filesystem = Layer.effect(
  304. FSUtil.Service,
  305. Effect.gen(function* () {
  306. const service = yield* FSUtil.Service
  307. return FSUtil.Service.of({
  308. ...service,
  309. realPath: (target) =>
  310. Effect.sync(() => realPaths.push(target)).pipe(Effect.andThen(service.realPath(target))),
  311. })
  312. }),
  313. ).pipe(Layer.provide(FSUtil.defaultLayer))
  314. return Effect.gen(function* () {
  315. yield* Effect.promise(async () => {
  316. await fs.mkdir(path.join(directory, "src"))
  317. await fs.writeFile(path.join(directory, "alpha.txt"), "alpha")
  318. await fs.writeFile(path.join(directory, "beta.txt"), "beta")
  319. })
  320. const service = yield* FileSystem.Service
  321. expect(yield* service.listPage({ offset: 2, limit: 1 })).toMatchObject({
  322. entries: [{ path: "alpha.txt", type: "file" }],
  323. truncated: true,
  324. next: 3,
  325. })
  326. expect(realPaths.filter((target) => target !== directory)).toEqual([path.join(directory, "alpha.txt")])
  327. }).pipe(provide(directory, inertReferences, filesystem))
  328. }),
  329. )
  330. it.live("materializes selected page entries with at most 16 concurrent real path lookups", () =>
  331. withTmp((directory) => {
  332. let active = 0
  333. let maximum = 0
  334. const filesystem = Layer.effect(
  335. FSUtil.Service,
  336. Effect.gen(function* () {
  337. const service = yield* FSUtil.Service
  338. return FSUtil.Service.of({
  339. ...service,
  340. realPath: (target) =>
  341. target === directory
  342. ? service.realPath(target)
  343. : Effect.acquireUseRelease(
  344. Effect.sync(() => {
  345. active++
  346. maximum = Math.max(maximum, active)
  347. }),
  348. () => Effect.sleep("10 millis").pipe(Effect.andThen(service.realPath(target))),
  349. () => Effect.sync(() => active--),
  350. ),
  351. })
  352. }),
  353. ).pipe(Layer.provide(FSUtil.defaultLayer))
  354. return Effect.gen(function* () {
  355. yield* Effect.promise(() =>
  356. Promise.all(Array.from({ length: 32 }, (_, index) => fs.writeFile(path.join(directory, `${index}.txt`), ""))),
  357. )
  358. const service = yield* FileSystem.Service
  359. expect((yield* service.listPage({ limit: 32 })).entries).toHaveLength(32)
  360. expect(maximum).toBe(16)
  361. }).pipe(provide(directory, inertReferences, filesystem))
  362. }),
  363. )
  364. it.live("caps direct list page service calls at 2000 entries", () =>
  365. withTmp((directory) =>
  366. Effect.gen(function* () {
  367. yield* Effect.promise(() =>
  368. Promise.all(
  369. Array.from({ length: 2_001 }, (_, index) =>
  370. fs.writeFile(path.join(directory, `${index.toString().padStart(4, "0")}.txt`), ""),
  371. ),
  372. ),
  373. )
  374. const service = yield* FileSystem.Service
  375. const target = yield* service.resolveList()
  376. expect((yield* service.listPageResolved(target, { limit: 2_001 })).entries).toHaveLength(2_000)
  377. }).pipe(provide(directory)),
  378. ),
  379. )
  380. test("rejects empty list aliases and page limits over 2000", () => {
  381. const decode = Schema.decodeUnknownSync(FileSystem.ListPageInput)
  382. expect(() => decode({ reference: "" })).toThrow()
  383. expect(() => decode({ limit: 2_001 })).toThrow()
  384. })
  385. it.live("rejects escaping list paths and omits escaping symlink children", () =>
  386. withTmp((directory) =>
  387. Effect.gen(function* () {
  388. if (process.platform === "win32") return
  389. const outside = `${directory}-outside`
  390. yield* Effect.promise(async () => {
  391. await fs.mkdir(outside)
  392. await fs.writeFile(path.join(outside, "secret.txt"), "secret")
  393. await fs.symlink(outside, path.join(directory, "escape"))
  394. })
  395. const service = yield* FileSystem.Service
  396. expect(
  397. Exit.isFailure(yield* service.listPage({ path: RelativePath.make("../outside") }).pipe(Effect.exit)),
  398. ).toBe(true)
  399. expect((yield* service.listPage()).entries).toEqual([])
  400. yield* Effect.promise(() => fs.rm(outside, { recursive: true, force: true }))
  401. }).pipe(provide(directory)),
  402. ),
  403. )
  404. it.live("paginates visible entries after omitting escaping symlink children", () =>
  405. withTmp((directory) =>
  406. Effect.gen(function* () {
  407. if (process.platform === "win32") return
  408. const outside = `${directory}-outside`
  409. yield* Effect.promise(async () => {
  410. await fs.mkdir(outside)
  411. await fs.symlink(outside, path.join(directory, "a-escape"))
  412. await fs.writeFile(path.join(directory, "b-visible.txt"), "visible")
  413. })
  414. const service = yield* FileSystem.Service
  415. expect(yield* service.listPage({ limit: 1 })).toMatchObject({
  416. entries: [{ path: "b-visible.txt", type: "file" }],
  417. truncated: false,
  418. })
  419. yield* Effect.promise(() => fs.rm(outside, { recursive: true, force: true }))
  420. }).pipe(provide(directory)),
  421. ),
  422. )
  423. it.live("rejects paths outside the location", () =>
  424. withTmp((directory) =>
  425. Effect.gen(function* () {
  426. const service = yield* FileSystem.Service
  427. expect(
  428. Exit.isFailure(yield* service.read({ path: RelativePath.make("../outside.txt") }).pipe(Effect.exit)),
  429. ).toBe(true)
  430. }).pipe(provide(directory)),
  431. ),
  432. )
  433. it.live("reads and lists paths relative to a local project reference", () =>
  434. withTmp((directory) => {
  435. const docs = path.join(directory, "docs")
  436. return Effect.gen(function* () {
  437. yield* Effect.promise(async () => {
  438. await fs.mkdir(docs)
  439. await fs.writeFile(path.join(docs, "README.md"), "docs")
  440. })
  441. const service = yield* FileSystem.Service
  442. expect(yield* service.read({ reference: "docs", path: RelativePath.make("README.md") })).toMatchObject({
  443. type: "text",
  444. content: "docs",
  445. })
  446. expect(yield* service.list({ reference: "docs" })).toMatchObject([{ path: "README.md", type: "file" }])
  447. }).pipe(provide(directory, references({ docs: { name: "docs", kind: "local", path: docs } })))
  448. }),
  449. )
  450. it.live("materializes Git references before filesystem access", () =>
  451. withTmp((directory) => {
  452. const docs = path.join(directory, "docs")
  453. const ensured: string[] = []
  454. return Effect.gen(function* () {
  455. yield* Effect.promise(async () => {
  456. await fs.mkdir(docs)
  457. await fs.writeFile(path.join(docs, "README.md"), "docs")
  458. })
  459. expect(
  460. yield* (yield* FileSystem.Service).read({ reference: "sdk", path: RelativePath.make("README.md") }),
  461. ).toMatchObject({ content: "docs" })
  462. expect(ensured).toEqual([docs])
  463. }).pipe(
  464. provide(
  465. directory,
  466. references(
  467. {
  468. sdk: {
  469. name: "sdk",
  470. kind: "git",
  471. repository: "owner/repo",
  472. reference: Repository.parseRemote("owner/repo"),
  473. path: docs,
  474. },
  475. },
  476. (target) => Effect.sync(() => ensured.push(target ?? "")),
  477. ),
  478. ),
  479. )
  480. }),
  481. )
  482. it.live("rejects unknown, invalid, and escaping project reference paths", () =>
  483. withTmp((directory) => {
  484. const docs = path.join(directory, "docs")
  485. return Effect.gen(function* () {
  486. yield* Effect.promise(() => fs.mkdir(docs))
  487. const service = yield* FileSystem.Service
  488. expect(Exit.isFailure(yield* service.list({ reference: "unknown" }).pipe(Effect.exit))).toBe(true)
  489. expect(Exit.isFailure(yield* service.list({ reference: "invalid" }).pipe(Effect.exit))).toBe(true)
  490. expect(
  491. Exit.isFailure(
  492. yield* service.read({ reference: "docs", path: RelativePath.make("../outside") }).pipe(Effect.exit),
  493. ),
  494. ).toBe(true)
  495. }).pipe(
  496. provide(
  497. directory,
  498. references({
  499. docs: { name: "docs", kind: "local", path: docs },
  500. invalid: { name: "invalid", kind: "invalid", message: "invalid reference" },
  501. }),
  502. ),
  503. )
  504. }),
  505. )
  506. it.live("rejects aliases when project references are disabled", () =>
  507. withTmp((directory) =>
  508. Effect.gen(function* () {
  509. expect(Exit.isFailure(yield* (yield* FileSystem.Service).list({ reference: "docs" }).pipe(Effect.exit))).toBe(
  510. true,
  511. )
  512. }).pipe(provide(directory)),
  513. ),
  514. )
  515. it.live("rejects symlink escapes from project references", () =>
  516. withTmp((directory) => {
  517. const docs = path.join(directory, "docs")
  518. const outside = path.join(directory, "outside.txt")
  519. return Effect.gen(function* () {
  520. if (process.platform === "win32") return
  521. yield* Effect.promise(async () => {
  522. await fs.mkdir(docs)
  523. await fs.writeFile(outside, "outside")
  524. await fs.symlink(outside, path.join(docs, "link.txt"))
  525. })
  526. expect(
  527. Exit.isFailure(
  528. yield* (yield* FileSystem.Service)
  529. .read({ reference: "docs", path: RelativePath.make("link.txt") })
  530. .pipe(Effect.exit),
  531. ),
  532. ).toBe(true)
  533. }).pipe(provide(directory, references({ docs: { name: "docs", kind: "local", path: docs } })))
  534. }),
  535. )
  536. })
  537. function references(
  538. entries: Record<string, ProjectReference.Resolved>,
  539. ensurePath: ProjectReference.Interface["ensurePath"] = () => Effect.void,
  540. ) {
  541. return ProjectReference.Service.of({
  542. list: () => Effect.succeed(Object.values(entries)),
  543. get: (name) => Effect.succeed(entries[name]),
  544. resolveMention: () => Effect.succeed(undefined),
  545. ensurePath,
  546. containsManagedPath: () => Effect.succeed(false),
  547. })
  548. }