fix(plugins): a development plugin reloads an edit saved while its wa… · tmchow/orca@5fc6cb4 · GitHub
Skip to content

Commit 5fc6cb4

Browse files
authored
fix(plugins): a development plugin reloads an edit saved while its watcher was starting (stablyai#31394)
* fix(plugins): a development plugin reloads an edit saved while its watcher was starting Orca scans a development plugin folder, then starts watching it. The watcher goes live about a second later and reports only changes made after that, so a save in between was never picked up and the plugin kept running the old files. Once a development root's subscription goes live, schedule one catch-up refresh through the existing debounce. * test(plugins): count a replaced dev root's catch-up refresh regardless of timing
1 parent 45ee56e commit 5fc6cb4

3 files changed

Lines changed: 63 additions & 9 deletions

File tree

‎src/main/plugins/plugin-dev-watcher-unknown-identity.test.ts‎

Lines changed: 20 additions & 6 deletions

‎src/main/plugins/plugin-dev-watcher.test.ts‎

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ type PluginWatchSubscription = {
3737
}
3838

3939
const devPath = resolve('plugins', 'demo')
40+
41+
/** The catch-up refresh a live subscription schedules; later counts start after it. */
42+
async function flushCatchUpRefresh(refresh: ReturnType<typeof vi.fn>): Promise<void> {
43+
await vi.advanceTimersByTimeAsync(300)
44+
expect(refresh).toHaveBeenCalledOnce()
45+
refresh.mockClear()
46+
}
47+
4048
const originalPlatform = process.platform
4149
const subscribeMock = vi.mocked(subscribeViaWatcherProcess)
4250
let subscriptions: PluginWatchSubscription[] = []
@@ -230,6 +238,27 @@ describe('PluginDevWatcher', () => {
230238
expect(refresh).toHaveBeenCalledTimes(2)
231239
})
232240

241+
it('catches up once when a dev root goes live after an edit it could not report', async () => {
242+
vi.useFakeTimers()
243+
const pending: (() => void)[] = []
244+
subscribeMock.mockImplementation((path, callback, options, hooks = {}) => {
245+
const unsubscribe = vi.fn().mockResolvedValue(undefined)
246+
subscriptions.push({ path, callback, options, hooks, unsubscribe })
247+
return new Promise((resolve) => pending.push(() => resolve({ unsubscribe })))
248+
})
249+
const { refresh } = startDevWatcher(vi.fn(), vi.fn(), [devPath])
250+
// The edit lands while the native subscription is still being set up, so no event reports it.
251+
await vi.advanceTimersByTimeAsync(1_000)
252+
expect(refresh).not.toHaveBeenCalled()
253+
for (const goLive of pending) {
254+
goLive()
255+
}
256+
await vi.advanceTimersByTimeAsync(300)
257+
expect(refresh).toHaveBeenCalledOnce()
258+
await vi.advanceTimersByTimeAsync(60_000)
259+
expect(refresh).toHaveBeenCalledOnce()
260+
})
261+
233262
it('refreshes the whole plugin projection after an overflow', () => {
234263
vi.useFakeTimers()
235264
const { refresh, onWatcherError } = startDevWatcher()
@@ -504,6 +533,7 @@ describe('PluginDevWatcher', () => {
504533
const options = { enabled: true, devPaths: [path], refresh, reapIdle: vi.fn() }
505534
lifecycle.sync(options)
506535
await Promise.resolve()
536+
await flushCatchUpRefresh(refresh)
507537
for (let tick = 0; tick < 3; tick += 1) {
508538
await vi.advanceTimersByTimeAsync(60_000)
509539
expect(checkBindings).toHaveBeenCalledTimes(tick + 1)
@@ -567,6 +597,7 @@ describe('PluginDevWatcher', () => {
567597
const options = { enabled: true, devPaths: [path], refresh, reapIdle: vi.fn() }
568598
lifecycle.sync(options)
569599
await Promise.resolve()
600+
await flushCatchUpRefresh(refresh)
570601
const root = pluginRootSubscription()
571602

572603
for (const type of ['create', 'delete'] as const) {
@@ -601,22 +632,26 @@ describe('PluginDevWatcher', () => {
601632
const options = { enabled: true, devPaths: [path], refresh, reapIdle: vi.fn() }
602633
lifecycle.sync(options)
603634
await Promise.resolve()
635+
await flushCatchUpRefresh(refresh)
604636
const root = pluginRootSubscription()
605637
await rename(path, join(aliasedRoot.base, 'old'))
606638
await mkdir(path)
607639

608640
await vi.advanceTimersByTimeAsync(60_000)
609-
await vi.waitFor(() => expect(refresh).toHaveBeenCalledOnce())
641+
await vi.waitFor(() => expect(subscribeMock).toHaveBeenCalledTimes(2))
642+
await vi.advanceTimersByTimeAsync(300)
643+
// The rebind refresh, then the replacement root's one catch-up.
644+
expect(refresh).toHaveBeenCalledTimes(2)
610645
expect(root.unsubscribe).toHaveBeenCalledOnce()
611646
expect(subscriptions.map((item) => ({ path: item.path, options: item.options }))).toEqual([
612647
{ path, options: { backend: 'windows' } },
613648
{ path, options: { backend: 'windows' } }
614649
])
615650
subscriptions.at(-1)?.callback(null, [{ type: 'create', path: join(path, 'manifest.json') }])
616651
await vi.advanceTimersByTimeAsync(300)
617-
expect(refresh).toHaveBeenCalledTimes(2)
652+
expect(refresh).toHaveBeenCalledTimes(3)
618653
await vi.advanceTimersByTimeAsync(60_000)
619-
expect(refresh).toHaveBeenCalledTimes(2)
654+
expect(refresh).toHaveBeenCalledTimes(3)
620655
expect(subscribeMock).toHaveBeenCalledTimes(2)
621656
lifecycle.dispose()
622657
expect(vi.getTimerCount()).toBe(0)

‎src/main/plugins/plugin-dev-watcher.ts‎

Lines changed: 5 additions & 0 deletions

0 commit comments

Comments
 (0)