instance-bootstrap.test.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. import { afterEach, expect } from "bun:test"
  2. import { existsSync } from "node:fs"
  3. import path from "node:path"
  4. import { pathToFileURL } from "node:url"
  5. import { LayerNode } from "@kirincode-ai/core/effect/layer-node"
  6. import { CrossSpawnSpawner } from "@kirincode-ai/core/cross-spawn-spawner"
  7. import { Cause, Effect, Exit, Fiber } from "effect"
  8. import { bootstrap as cliBootstrap } from "../../src/cli/bootstrap"
  9. import { InstanceBootstrap } from "../../src/project/bootstrap"
  10. import { InstanceStore } from "../../src/project/instance-store"
  11. import { disposeAllInstances, tmpdirScoped } from "../fixture/fixture"
  12. import { testEffect } from "../lib/effect"
  13. import { waitGlobalBusEvent } from "../server/global-bus"
  14. const it = testEffect(
  15. LayerNode.compile(LayerNode.group([InstanceStore.node, CrossSpawnSpawner.node]), [
  16. [InstanceStore.bootstrapNode, InstanceBootstrap.node],
  17. ]),
  18. )
  19. // InstanceBootstrap must run before any code touches the instance —
  20. // originally tracked by PRs #25389 and #25449, now a permanent
  21. // invariant. The plugin config hook writes a marker file; the test
  22. // bodies deliberately avoid Plugin/config directly. The marker only
  23. // appears if InstanceBootstrap ran at the instance boundary.
  24. //
  25. // The boundaries below are transport-agnostic and stay.
  26. afterEach(async () => {
  27. await disposeAllInstances()
  28. })
  29. const bootstrapFixture = Effect.gen(function* () {
  30. const dir = yield* tmpdirScoped({ git: true })
  31. const marker = path.join(dir, "config-hook-fired")
  32. const pluginFile = path.join(dir, "plugin.ts")
  33. yield* Effect.promise(() =>
  34. Bun.write(
  35. pluginFile,
  36. [
  37. `const MARKER = ${JSON.stringify(marker)}`,
  38. "export default async () => ({",
  39. " config: async () => {",
  40. ' await Bun.write(MARKER, "ran")',
  41. " },",
  42. "})",
  43. "",
  44. ].join("\n"),
  45. ),
  46. )
  47. yield* Effect.promise(() =>
  48. Bun.write(
  49. path.join(dir, "kirincode.json"),
  50. JSON.stringify({
  51. $schema: "https://kirincode.ai/config.json",
  52. plugin: [pathToFileURL(pluginFile).href],
  53. }),
  54. ),
  55. )
  56. return { directory: dir, marker }
  57. })
  58. function waitDisposed(directory: string) {
  59. return waitGlobalBusEvent({
  60. message: "timed out waiting for CLI bootstrap instance disposal",
  61. predicate: (event) => event.payload.type === "server.instance.disposed" && event.directory === directory,
  62. })
  63. }
  64. it.live("InstanceStore.provide runs InstanceBootstrap before effect", () =>
  65. Effect.gen(function* () {
  66. const tmp = yield* bootstrapFixture
  67. const store = yield* InstanceStore.Service
  68. yield* store.provide({ directory: tmp.directory }, Effect.succeed("ok"))
  69. expect(existsSync(tmp.marker)).toBe(true)
  70. }),
  71. )
  72. it.live("CLI bootstrap runs InstanceBootstrap before callback", () =>
  73. Effect.gen(function* () {
  74. const tmp = yield* bootstrapFixture
  75. yield* Effect.promise(() => cliBootstrap(tmp.directory, async () => "ok"))
  76. expect(existsSync(tmp.marker)).toBe(true)
  77. }),
  78. )
  79. it.live("CLI bootstrap disposes the instance when the callback rejects", () =>
  80. Effect.gen(function* () {
  81. const tmp = yield* bootstrapFixture
  82. const disposed = yield* waitDisposed(tmp.directory).pipe(Effect.forkScoped({ startImmediately: true }))
  83. const exit = yield* Effect.promise(() =>
  84. cliBootstrap(tmp.directory, async () => Promise.reject(new Error("boom"))),
  85. ).pipe(Effect.exit)
  86. expect(Exit.isFailure(exit)).toBe(true)
  87. if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toMatchObject({ message: "boom" })
  88. yield* Fiber.join(disposed)
  89. }),
  90. )
  91. it.live("InstanceStore.reload runs InstanceBootstrap", () =>
  92. Effect.gen(function* () {
  93. const tmp = yield* bootstrapFixture
  94. const store = yield* InstanceStore.Service
  95. yield* store.reload({ directory: tmp.directory })
  96. expect(existsSync(tmp.marker)).toBe(true)
  97. }),
  98. )