@@ -2,7 +2,8 @@ import type { AssetsServer, TestClient } from './_utils'
22import { Buffer } from 'node:buffer'
33import fsp from 'node:fs/promises'
44import { join } from 'node:path'
5- import { afterEach , beforeEach , describe , expect , it } from 'vitest'
5+ import process from 'node:process'
6+ import { afterAll , afterEach , beforeEach , describe , expect , it } from 'vitest'
67import { bootClient , call , cleanupTempDir , createTempDir , startAssetsServer } from './_utils'
78
89// A minimal, valid 1x1 PNG.
@@ -36,31 +37,44 @@ describe('assets plugin', () => {
3637 let server : AssetsServer
3738 let client : TestClient
3839
40+ // Every managed temp dir this file creates, deleted once at the very end
41+ // rather than per-test. On Windows, closing a live chokidar watcher
42+ // doesn't guarantee libuv has retired the outstanding
43+ // ReadDirectoryChangesW request — deleting the directory it watched right
44+ // after close() can trip a native assertion and hard-crash the process
45+ // (see `watchAssetsDir`'s disposer). Deferring every directory's removal
46+ // well past its server's shutdown keeps that race out of this suite.
47+ const tempDirs : string [ ] = [ ]
48+
3949 beforeEach ( async ( ) => {
4050 dir = await createTempDir ( )
51+ tempDirs . push ( dir )
4152 } )
4253
4354 afterEach ( async ( ) => {
4455 await server ?. close ( )
45- await cleanupTempDir ( dir )
56+ } )
57+
58+ afterAll ( async ( ) => {
59+ await Promise . all ( tempDirs . map ( cleanupTempDir ) )
4660 } )
4761
4862 it ( 'lists no assets in an empty directory' , async ( ) => {
49- server = await startAssetsServer ( dir )
63+ server = await startAssetsServer ( dir , { watch : false } )
5064 client = bootClient ( server . port )
5165 const list = await call ( client , 'devframes:plugin:assets:list' )
5266 expect ( list ) . toEqual ( [ ] )
5367 } )
5468
5569 it ( 'reports write capabilities by default' , async ( ) => {
56- server = await startAssetsServer ( dir )
70+ server = await startAssetsServer ( dir , { watch : false } )
5771 client = bootClient ( server . port )
5872 const caps = await call ( client , 'devframes:plugin:assets:capabilities' )
5973 expect ( caps ) . toEqual ( { write : true , uploadExtensions : expect . any ( Array ) } )
6074 } )
6175
6276 it ( 'creates a folder and uploads a file into it via the streaming channel' , async ( ) => {
63- server = await startAssetsServer ( dir )
77+ server = await startAssetsServer ( dir , { watch : false } )
6478 client = bootClient ( server . port )
6579
6680 await call ( client , 'devframes:plugin:assets:mkdir' , { path : 'icons' } )
@@ -73,7 +87,7 @@ describe('assets plugin', () => {
7387 } )
7488
7589 it ( 'reads image dimensions for an uploaded image' , async ( ) => {
76- server = await startAssetsServer ( dir )
90+ server = await startAssetsServer ( dir , { watch : false } )
7791 client = bootClient ( server . port )
7892 await upload ( client , 'logo.png' , ONE_PIXEL_PNG )
7993
@@ -83,7 +97,7 @@ describe('assets plugin', () => {
8397
8498 it ( 'reads text content for a text asset' , async ( ) => {
8599 await fsp . writeFile ( join ( dir , 'notes.txt' ) , 'hello world' , 'utf-8' )
86- server = await startAssetsServer ( dir )
100+ server = await startAssetsServer ( dir , { watch : false } )
87101 client = bootClient ( server . port )
88102
89103 const content = await call ( client , 'devframes:plugin:assets:read-text' , 'notes.txt' )
@@ -93,7 +107,7 @@ describe('assets plugin', () => {
93107 it ( 'renames an asset within its folder' , async ( ) => {
94108 await fsp . mkdir ( join ( dir , 'icons' ) , { recursive : true } )
95109 await fsp . writeFile ( join ( dir , 'icons/old.txt' ) , 'x' , 'utf-8' )
96- server = await startAssetsServer ( dir )
110+ server = await startAssetsServer ( dir , { watch : false } )
97111 client = bootClient ( server . port )
98112
99113 const renamed = await call ( client , 'devframes:plugin:assets:rename' , { path : 'icons/old.txt' , newName : 'new' } )
@@ -105,7 +119,7 @@ describe('assets plugin', () => {
105119 it ( 'rejects renaming onto an existing file' , async ( ) => {
106120 await fsp . writeFile ( join ( dir , 'a.txt' ) , 'a' , 'utf-8' )
107121 await fsp . writeFile ( join ( dir , 'b.txt' ) , 'b' , 'utf-8' )
108- server = await startAssetsServer ( dir )
122+ server = await startAssetsServer ( dir , { watch : false } )
109123 client = bootClient ( server . port )
110124
111125 await expect ( call ( client , 'devframes:plugin:assets:rename' , { path : 'a.txt' , newName : 'b' } ) ) . rejects . toThrow ( )
@@ -114,7 +128,7 @@ describe('assets plugin', () => {
114128 it ( 'deletes one or more assets in a single call' , async ( ) => {
115129 await fsp . writeFile ( join ( dir , 'a.txt' ) , 'a' , 'utf-8' )
116130 await fsp . writeFile ( join ( dir , 'b.txt' ) , 'b' , 'utf-8' )
117- server = await startAssetsServer ( dir )
131+ server = await startAssetsServer ( dir , { watch : false } )
118132 client = bootClient ( server . port )
119133
120134 const result = await call ( client , 'devframes:plugin:assets:delete' , { paths : [ 'a.txt' , 'b.txt' , 'missing.txt' ] } )
@@ -123,7 +137,7 @@ describe('assets plugin', () => {
123137 } )
124138
125139 it ( 'rejects paths that escape the managed directory' , async ( ) => {
126- server = await startAssetsServer ( dir )
140+ server = await startAssetsServer ( dir , { watch : false } )
127141 client = bootClient ( server . port )
128142
129143 await expect ( call ( client , 'devframes:plugin:assets:read-text' , '../../etc/passwd' ) ) . resolves . toBeNull ( )
@@ -133,7 +147,7 @@ describe('assets plugin', () => {
133147 } )
134148
135149 it ( 'rejects uploads with a disallowed extension' , async ( ) => {
136- server = await startAssetsServer ( dir , { uploadExtensions : [ 'png' ] } )
150+ server = await startAssetsServer ( dir , { uploadExtensions : [ 'png' ] , watch : false } )
137151 client = bootClient ( server . port )
138152
139153 await expect (
@@ -142,7 +156,7 @@ describe('assets plugin', () => {
142156 } )
143157
144158 it ( 'does not register write actions when write is disabled' , async ( ) => {
145- server = await startAssetsServer ( dir , { write : false } )
159+ server = await startAssetsServer ( dir , { write : false , watch : false } )
146160 client = bootClient ( server . port )
147161
148162 const caps = await call ( client , 'devframes:plugin:assets:capabilities' )
@@ -151,7 +165,7 @@ describe('assets plugin', () => {
151165 } )
152166
153167 it ( 'still registers open-in-editor and reveal-in-folder when write is disabled' , async ( ) => {
154- server = await startAssetsServer ( dir , { write : false } )
168+ server = await startAssetsServer ( dir , { write : false , watch : false } )
155169
156170 // open-in-editor / reveal-in-folder launch external OS apps, so assert
157171 // they're *registered* on the server context rather than invoking them
@@ -163,7 +177,22 @@ describe('assets plugin', () => {
163177 expect ( defs . has ( 'devframes:plugin:assets:mkdir' ) ) . toBe ( false )
164178 } )
165179
166- it ( 'broadcasts a change event when a file is added on disk' , async ( ) => {
180+ // The one test that needs the live watcher — every other test above opts
181+ // out of it (`watch: false`) so this is the only native fs watch handle
182+ // this file ever opens, close()s, and deletes the directory of.
183+ //
184+ // Skipped on Windows: opening a real `ReadDirectoryChangesW` watch and
185+ // then closing + deleting its directory shortly after is a known
186+ // trigger for a native libuv assertion on Windows CI runners —
187+ // `Assertion failed: !_wcsnicmp(filename, dir, dirlen), file
188+ // src\win\fs-event.c, line 72` — that hard-aborts the process outright
189+ // (no JS-catchable error). It isn't reliably avoidable with a delay: it
190+ // reproduces intermittently regardless of how long the teardown waits,
191+ // consistent with Windows Defender's real-time scanning racing the
192+ // directory watch/delete (a widely reported Node/libuv interaction on
193+ // hosted Windows runners). The live-watcher behavior is still fully
194+ // covered on Linux/macOS.
195+ it . skipIf ( process . platform === 'win32' ) ( 'broadcasts a change event when a file is added on disk' , async ( ) => {
167196 server = await startAssetsServer ( dir )
168197 client = bootClient ( server . port )
169198
0 commit comments