meta.test.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. import { afterEach, describe, expect, test } from "bun:test"
  2. import fs from "fs/promises"
  3. import path from "path"
  4. import { pathToFileURL } from "url"
  5. import { tmpdir } from "../fixture/fixture"
  6. import { Process } from "../../src/util/process"
  7. import { Filesystem } from "../../src/util/filesystem"
  8. const { PluginMeta } = await import("../../src/plugin/meta")
  9. const root = path.join(import.meta.dir, "../..")
  10. const worker = path.join(import.meta.dir, "../fixture/plugin-meta-worker.ts")
  11. function run(input: { file: string; spec: string; target: string; id: string }) {
  12. return Process.run([process.execPath, worker, JSON.stringify(input)], {
  13. cwd: root,
  14. nothrow: true,
  15. })
  16. }
  17. async function map<Value>(file: string): Promise<Record<string, Value>> {
  18. return Filesystem.readJson<Record<string, Value>>(file)
  19. }
  20. afterEach(() => {
  21. delete process.env.KILO_PLUGIN_META_FILE
  22. })
  23. describe("plugin.meta", () => {
  24. test("tracks file plugin loads and changes", async () => {
  25. await using tmp = await tmpdir<{ file: string }>({
  26. init: async (dir) => {
  27. const file = path.join(dir, "plugin.ts")
  28. await Bun.write(file, "export default async () => ({})\n")
  29. return { file }
  30. },
  31. })
  32. process.env.KILO_PLUGIN_META_FILE = path.join(tmp.path, "state", "plugin-meta.json")
  33. const file = process.env.KILO_PLUGIN_META_FILE!
  34. const spec = pathToFileURL(tmp.extra.file).href
  35. const one = await PluginMeta.touch(spec, spec, "demo.file")
  36. expect(one.state).toBe("first")
  37. expect(one.entry.source).toBe("file")
  38. expect(one.entry.id).toBe("demo.file")
  39. expect(one.entry.modified).toBeDefined()
  40. const two = await PluginMeta.touch(spec, spec, "demo.file")
  41. expect(two.state).toBe("same")
  42. expect(two.entry.load_count).toBe(2)
  43. await Bun.write(tmp.extra.file, "export default async () => ({ ok: true })\n")
  44. const stamp = new Date(Date.now() + 10_000)
  45. await fs.utimes(tmp.extra.file, stamp, stamp)
  46. const three = await PluginMeta.touch(spec, spec, "demo.file")
  47. expect(three.state).toBe("updated")
  48. expect(three.entry.load_count).toBe(3)
  49. expect((three.entry.modified ?? 0) > (one.entry.modified ?? 0)).toBe(true)
  50. const all = await PluginMeta.list()
  51. expect(Object.values(all).some((item) => item.spec === spec && item.source === "file")).toBe(true)
  52. const saved = await map<{ spec: string; load_count: number }>(file)
  53. expect(saved["demo.file"]?.spec).toBe(spec)
  54. expect(saved["demo.file"]?.load_count).toBe(3)
  55. })
  56. test("tracks npm plugin versions", async () => {
  57. await using tmp = await tmpdir<{ mod: string; pkg: string }>({
  58. init: async (dir) => {
  59. const mod = path.join(dir, "node_modules", "acme-plugin")
  60. const pkg = path.join(mod, "package.json")
  61. await fs.mkdir(mod, { recursive: true })
  62. await Bun.write(pkg, JSON.stringify({ name: "acme-plugin", version: "1.0.0" }, null, 2))
  63. return { mod, pkg }
  64. },
  65. })
  66. process.env.KILO_PLUGIN_META_FILE = path.join(tmp.path, "state", "plugin-meta.json")
  67. const file = process.env.KILO_PLUGIN_META_FILE!
  68. const one = await PluginMeta.touch("acme-plugin@latest", tmp.extra.mod, "acme-plugin")
  69. expect(one.state).toBe("first")
  70. expect(one.entry.source).toBe("npm")
  71. expect(one.entry.requested).toBe("latest")
  72. expect(one.entry.version).toBe("1.0.0")
  73. await Bun.write(tmp.extra.pkg, JSON.stringify({ name: "acme-plugin", version: "1.1.0" }, null, 2))
  74. const two = await PluginMeta.touch("acme-plugin@latest", tmp.extra.mod, "acme-plugin")
  75. expect(two.state).toBe("updated")
  76. expect(two.entry.version).toBe("1.1.0")
  77. expect(two.entry.load_count).toBe(2)
  78. const all = await PluginMeta.list()
  79. expect(Object.values(all).some((item) => item.id === "acme-plugin" && item.version === "1.1.0")).toBe(true)
  80. const saved = await map<{ id: string; version?: string }>(file)
  81. expect(Object.values(saved).some((item) => item.id === "acme-plugin" && item.version === "1.1.0")).toBe(true)
  82. })
  83. test("serializes concurrent metadata updates across processes", async () => {
  84. await using tmp = await tmpdir<{ file: string }>({
  85. init: async (dir) => {
  86. const file = path.join(dir, "plugin.ts")
  87. await Bun.write(file, "export default async () => ({})\n")
  88. return { file }
  89. },
  90. })
  91. process.env.KILO_PLUGIN_META_FILE = path.join(tmp.path, "state", "plugin-meta.json")
  92. const file = process.env.KILO_PLUGIN_META_FILE!
  93. const spec = pathToFileURL(tmp.extra.file).href
  94. const n = 12
  95. const out = await Promise.all(
  96. Array.from({ length: n }, () =>
  97. run({
  98. file,
  99. spec,
  100. target: spec,
  101. id: "demo.file",
  102. }),
  103. ),
  104. )
  105. expect(out.map((item) => item.code)).toEqual(Array.from({ length: n }, () => 0))
  106. expect(out.map((item) => item.stderr.toString()).filter(Boolean)).toEqual([])
  107. const all = await PluginMeta.list()
  108. const hit = Object.values(all).find((item) => item.spec === spec)
  109. expect(hit?.load_count).toBe(n)
  110. const saved = await map<{ spec: string; load_count: number }>(file)
  111. expect(Object.values(saved).find((item) => item.spec === spec)?.load_count).toBe(n)
  112. }, 20_000)
  113. })