|
1 | 1 | import { expect } from "bun:test" |
2 | | -import { Effect, Exit, Fiber } from "effect" |
| 2 | +import { Effect, Exit } from "effect" |
3 | 3 | import { Plugin } from "@opencode/core/plugin" |
4 | | -import { PluginScheduling } from "@opencode/core/plugin/scheduling" |
5 | 4 | import { testEffect } from "./lib/effect" |
6 | 5 | import { PluginTestLayer } from "./plugin/fixture" |
7 | 6 |
|
8 | 7 | const it = testEffect(PluginTestLayer) |
9 | 8 |
|
10 | | -it.live("allows plugin setup to activate a second location without holding global admission", () => |
11 | | - Effect.gen(function* () { |
12 | | - const first = yield* Plugin.Service |
13 | | - let nested = false |
14 | | - yield* first.activate([{ |
15 | | - id: "first-location", |
16 | | - revision: "1", |
17 | | - effect: () => Effect.gen(function* () { |
18 | | - const second = yield* Plugin.Service |
19 | | - expect(second).not.toBe(first) |
20 | | - yield* second.activate([{ |
21 | | - id: "second-location", |
22 | | - revision: "1", |
23 | | - effect: () => Effect.sync(() => { nested = true }), |
24 | | - }]) |
25 | | - }).pipe(Effect.provide(PluginTestLayer), Effect.orDie), |
26 | | - }]).pipe(Effect.timeout("5 seconds")) |
27 | | - expect(nested).toBe(true) |
28 | | - expect((yield* first.list())[0]?.state.status).toBe("active") |
29 | | - }), |
30 | | -) |
31 | | - |
32 | | -it.live("continues location cleanup after a finalizer defect with exactly-once LIFO order", () => |
33 | | - Effect.gen(function* () { |
34 | | - const plugins = yield* Plugin.Service |
35 | | - const stopped: number[] = [] |
36 | | - yield* plugins.activate(Array.from({ length: 3 }, (_, index) => ({ |
37 | | - id: `defective-close-${index}`, |
38 | | - revision: "1", |
39 | | - effect: () => Effect.addFinalizer(() => Effect.gen(function* () { |
40 | | - stopped.push(index) |
41 | | - if (index === 1) yield* Effect.die("fixture cleanup defect") |
42 | | - })), |
43 | | - }))) |
44 | | - const exit = yield* plugins.close(Exit.void).pipe(Effect.exit) |
45 | | - expect(Exit.isFailure(exit)).toBe(true) |
46 | | - expect(stopped).toEqual([2, 1, 0]) |
47 | | - yield* plugins.awaitActivation |
48 | | - }), |
49 | | -) |
50 | | - |
51 | | -it.live("services native timers during full location shutdown, preserving LIFO cleanup", () => |
52 | | - Effect.gen(function* () { |
53 | | - const plugins = yield* Plugin.Service |
54 | | - const stopped: number[] = [] |
55 | | - let ticks = 0 |
56 | | - const observed: number[] = [] |
57 | | - yield* plugins.activate(Array.from({ length: 12 }, (_, index) => ({ |
58 | | - id: `close-${index}`, |
59 | | - revision: "1", |
60 | | - effect: () => Effect.addFinalizer(() => Effect.sync(() => { |
61 | | - observed.push(ticks) |
62 | | - stopped.push(index) |
63 | | - const end = performance.now() + 5 |
64 | | - while (performance.now() < end) {} |
65 | | - })), |
66 | | - }))) |
67 | | - const timer = setInterval(() => { ticks++ }, 0) |
68 | | - yield* Effect.addFinalizer(() => Effect.sync(() => clearInterval(timer))) |
69 | | - yield* plugins.close(Exit.void) |
70 | | - expect(stopped).toEqual(Array.from({ length: 12 }, (_, index) => 11 - index)) |
71 | | - expect(observed.every((value, index) => index === 0 || value > observed[index - 1]!)).toBe(true) |
72 | | - yield* plugins.close(Exit.void) |
73 | | - expect(stopped).toHaveLength(12) |
74 | | - }), |
75 | | -) |
76 | | - |
77 | | -it.live("services native timers between overlapping initialization checkpoints", () => |
78 | | - Effect.gen(function* () { |
79 | | - let ticks = 0 |
80 | | - const observed: number[] = [] |
81 | | - const timer = setInterval(() => { ticks++ }, 0) |
82 | | - yield* Effect.addFinalizer(() => Effect.sync(() => clearInterval(timer))) |
83 | | - yield* Effect.forEach([0, 1, 2], () => Effect.gen(function* () { |
84 | | - for (let index = 0; index < 4; index++) { |
85 | | - yield* PluginScheduling.checkpoint |
86 | | - observed.push(ticks) |
87 | | - const end = performance.now() + 5 |
88 | | - while (performance.now() < end) {} |
89 | | - } |
90 | | - }), { concurrency: "unbounded", discard: true }) |
91 | | - expect(observed).toHaveLength(12) |
92 | | - expect(observed.every((value, index) => index === 0 || value > observed[index - 1]!)).toBe(true) |
93 | | - }), |
94 | | -) |
95 | | - |
96 | | -it.live("services native timers between ordered synchronous plugin activations", () => |
| 9 | +it.live("activate burst yields to the event loop", () => |
97 | 10 | Effect.gen(function* () { |
98 | 11 | const plugins = yield* Plugin.Service |
99 | | - yield* plugins.awaitActivation |
100 | | - let observed = false |
101 | | - let observedBetween = false |
102 | 12 | const started: number[] = [] |
103 | | - const timer = setTimeout(() => { observed = true }, 0) |
| 13 | + let observed = 0 |
| 14 | + let timer: ReturnType<typeof setTimeout> | undefined |
104 | 15 | yield* Effect.addFinalizer(() => Effect.sync(() => clearTimeout(timer))) |
105 | | - yield* plugins.activate(Array.from({ length: 12 }, (_, index) => ({ |
| 16 | + yield* plugins.activate(Array.from({ length: 3 }, (_, index) => ({ |
106 | 17 | id: `burst-${index}`, |
107 | 18 | revision: "1", |
108 | 19 | effect: () => Effect.sync(() => { |
109 | | - // CPU work below the fiber operation-count auto-yield threshold. |
| 20 | + // Arm inside the first plugin so an initial yield cannot satisfy the test. |
| 21 | + if (index === 0) timer = setTimeout(() => { observed = started.length }, 0) |
| 22 | + // CPU work stays below the fiber operation-count auto-yield threshold. |
110 | 23 | const end = performance.now() + 5 |
111 | 24 | while (performance.now() < end) {} |
112 | 25 | started.push(index) |
113 | | - if (index === 1) observedBetween = observed |
114 | 26 | }), |
115 | 27 | }))) |
116 | | - expect(started).toEqual(Array.from({ length: 12 }, (_, index) => index)) |
117 | | - expect(observedBetween).toBe(true) |
| 28 | + expect(started).toEqual([0, 1, 2]) |
| 29 | + expect(observed).toBeGreaterThan(0) |
| 30 | + expect(observed).toBeLessThan(started.length) |
118 | 31 | expect((yield* plugins.list()).every((plugin) => plugin.state.status === "active")).toBe(true) |
119 | 32 | }), |
120 | 33 | ) |
121 | 34 |
|
122 | | -it.live("services native timers between reverse-order plugin finalizers", () => |
| 35 | +it.live("close burst yields to the event loop", () => |
123 | 36 | Effect.gen(function* () { |
124 | 37 | const plugins = yield* Plugin.Service |
125 | | - let observed = false |
126 | | - let observedBetween = false |
127 | 38 | const stopped: number[] = [] |
128 | | - yield* plugins.activate(Array.from({ length: 12 }, (_, index) => ({ |
129 | | - id: `teardown-${index}`, |
| 39 | + let observed = 0 |
| 40 | + let timer: ReturnType<typeof setTimeout> | undefined |
| 41 | + yield* Effect.addFinalizer(() => Effect.sync(() => clearTimeout(timer))) |
| 42 | + yield* plugins.activate(Array.from({ length: 3 }, (_, index) => ({ |
| 43 | + id: `close-${index}`, |
130 | 44 | revision: "1", |
131 | 45 | effect: () => Effect.addFinalizer(() => Effect.sync(() => { |
| 46 | + if (index === 2) timer = setTimeout(() => { observed = stopped.length }, 0) |
132 | 47 | const end = performance.now() + 5 |
133 | 48 | while (performance.now() < end) {} |
134 | 49 | stopped.push(index) |
135 | | - if (index === 10) observedBetween = observed |
136 | 50 | })), |
137 | 51 | }))) |
138 | | - const timer = setTimeout(() => { observed = true }, 0) |
139 | | - yield* Effect.addFinalizer(() => Effect.sync(() => clearTimeout(timer))) |
140 | | - yield* plugins.activate([]) |
141 | | - expect(stopped).toEqual(Array.from({ length: 12 }, (_, index) => 11 - index)) |
142 | | - expect(observedBetween).toBe(true) |
143 | | - expect(yield* plugins.list()).toEqual([]) |
144 | | - }), |
145 | | -) |
146 | | - |
147 | | -it.live("cancels pending setup without leaking readiness holds", () => |
148 | | - Effect.gen(function* () { |
149 | | - const plugins = yield* Plugin.Service |
150 | | - let started = false |
151 | | - const fiber = yield* plugins.activate([{ |
152 | | - id: "cancelled", |
153 | | - revision: "1", |
154 | | - effect: () => Effect.sync(() => { started = true }), |
155 | | - }]).pipe(Effect.forkChild({ startImmediately: true })) |
156 | | - yield* Fiber.interrupt(fiber) |
157 | | - yield* plugins.awaitActivation |
158 | | - expect(started).toBe(false) |
159 | | - expect(yield* plugins.list()).toEqual([]) |
160 | | - yield* plugins.activate([{ id: "replacement", revision: "1", effect: () => Effect.void }]) |
161 | | - expect((yield* plugins.list()).map((plugin) => plugin.id)).toEqual([Plugin.ID.make("replacement")]) |
| 52 | + yield* plugins.close(Exit.void) |
| 53 | + expect(stopped).toEqual([2, 1, 0]) |
| 54 | + expect(observed).toBeGreaterThan(0) |
| 55 | + expect(observed).toBeLessThan(stopped.length) |
| 56 | + yield* plugins.close(Exit.void) |
| 57 | + expect(stopped).toEqual([2, 1, 0]) |
162 | 58 | }), |
163 | 59 | ) |
0 commit comments