auth.test.ts 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. import { expect, test } from "bun:test"
  2. import { setTimeout as sleep } from "node:timers/promises"
  3. import { AppNodeBuilder } from "@kirincode-ai/core/effect/app-node-builder"
  4. import { Effect, Layer } from "effect"
  5. import { FSUtil } from "@kirincode-ai/core/fs-util"
  6. import { McpAuth } from "../../src/mcp/auth"
  7. function authFile() {
  8. let raw = ""
  9. let activeWrites = 0
  10. let sawOverlap = false
  11. const fsLayer = Layer.effect(
  12. FSUtil.Service,
  13. Effect.gen(function* () {
  14. const fs = yield* FSUtil.Service
  15. return FSUtil.Service.of({
  16. ...fs,
  17. readJson: (file) =>
  18. file.endsWith("mcp-auth.json")
  19. ? Effect.try({
  20. try: () => {
  21. if (!raw) throw new Error("mcp-auth.json missing")
  22. return JSON.parse(raw)
  23. },
  24. catch: (cause) => new FSUtil.FileSystemError({ method: "readJson", cause }),
  25. })
  26. : fs.readJson(file),
  27. writeJson: (file, value, mode) =>
  28. file.endsWith("mcp-auth.json")
  29. ? Effect.promise(async () => {
  30. activeWrites++
  31. sawOverlap = sawOverlap || activeWrites > 1
  32. raw = ""
  33. await sleep(10)
  34. const next = JSON.stringify(value, null, 2)
  35. raw = sawOverlap ? `${next}\n}` : next
  36. activeWrites--
  37. })
  38. : fs.writeJson(file, value, mode),
  39. })
  40. }),
  41. ).pipe(Layer.provide(AppNodeBuilder.build(FSUtil.node)))
  42. return { fsLayer, raw: () => raw }
  43. }
  44. function authService(fsLayer: Layer.Layer<FSUtil.Service>) {
  45. return McpAuth.Service.use((auth) => Effect.succeed(auth)).pipe(
  46. Effect.provide(AppNodeBuilder.build(McpAuth.node, [[FSUtil.node, fsLayer]])),
  47. )
  48. }
  49. test("serializes concurrent auth file updates across service instances", async () => {
  50. const file = authFile()
  51. await Effect.runPromise(
  52. Effect.gen(function* () {
  53. const first = yield* authService(file.fsLayer)
  54. const second = yield* authService(file.fsLayer)
  55. yield* Effect.all(
  56. [
  57. first.updateTokens("posthog", { accessToken: "access-token" }, "https://mcp.posthog.com/mcp"),
  58. second.updateClientInfo("posthog", { clientId: "client-id" }, "https://mcp.posthog.com/mcp"),
  59. ],
  60. { concurrency: "unbounded" },
  61. )
  62. const entry = yield* first.get("posthog")
  63. expect(entry?.tokens?.accessToken).toBe("access-token")
  64. expect(entry?.clientInfo?.clientId).toBe("client-id")
  65. expect(entry?.serverUrl).toBe("https://mcp.posthog.com/mcp")
  66. expect(() => JSON.parse(file.raw())).not.toThrow()
  67. }),
  68. )
  69. })