@@ -1671,23 +1671,26 @@ describe('language server config watching', { timeout: timeouts.databaseOperatio
16711671 expect ( ( await diagnosed ) . length ) . toBeGreaterThan ( 0 ) ;
16721672 } ) ;
16731673
1674- it ( 'stops managing a project when a config edit breaks the config' , async ( ) => {
1674+ it ( 'keeps serving the last-good project when a config edit breaks the config' , async ( ) => {
16751675 const hook = mutableResolve ( resolveToSchema ) ;
16761676 harness = startHarness ( hook . resolve , watchedFilesCapabilities ) ;
16771677 await harness . initialize ( ) ;
16781678
16791679 harness . client . sendNotification ( DidOpenTextDocumentNotification . type , {
16801680 textDocument : { uri : schemaUri , languageId : 'prisma' , version : 1 , text : 'model {' } ,
16811681 } ) ;
1682- expect ( ( await harness . waitForDiagnostics ( schemaUri ) ) . length ) . toBeGreaterThan ( 0 ) ;
1682+ const before = await harness . waitForDiagnostics ( schemaUri ) ;
1683+ expect ( before . length ) . toBeGreaterThan ( 0 ) ;
16831684
1684- const cleared = harness . waitForDiagnosticsMatching (
1685- schemaUri ,
1686- ( diagnostics ) => diagnostics . length === 0 ,
1687- ) ;
16881685 hook . set ( resolveFails ) ;
16891686 harness . notifyConfigChanged ( ) ;
1690- expect ( await cleared ) . toEqual ( [ ] ) ;
1687+
1688+ // The broken reload surfaces on the config file; the schema keeps its
1689+ // last-good diagnostics instead of being cleared.
1690+ const configDiagnostics = await harness . waitForDiagnostics ( configUri ) ;
1691+ expect ( configDiagnostics ) . toHaveLength ( 1 ) ;
1692+ await settle ( ) ;
1693+ expect ( harness . latestDiagnostics ( schemaUri ) ) . toEqual ( before ) ;
16911694 } ) ;
16921695} ) ;
16931696
@@ -2214,3 +2217,179 @@ describe('language server interpreter diagnostics', { timeout: timeouts.database
22142217 expect ( spy ) . toHaveBeenCalledTimes ( 2 ) ;
22152218 } ) ;
22162219} ) ;
2220+
2221+ describe ( 'language server config failure surfacing' , {
2222+ timeout : timeouts . databaseOperation ,
2223+ } , ( ) => {
2224+ const cleanSchema = 'model User {\n id Int @id\n}\n' ;
2225+ const expectedConfigFailure = ( message : string ) : Diagnostic => ( {
2226+ range : { start : { line : 0 , character : 0 } , end : { line : 0 , character : 1 } } ,
2227+ message,
2228+ code : 'PRISMA_NEXT_CONFIG_LOAD_FAILED' ,
2229+ severity : DiagnosticSeverity . Error ,
2230+ source : 'prisma-next' ,
2231+ } ) ;
2232+
2233+ function interpretingResolution ( ) : ConfigResolution {
2234+ const source = {
2235+ sourceFormat : 'psl' ,
2236+ inputs : [ schemaPath ] ,
2237+ load : async ( ) => ok ( { } as never ) ,
2238+ interpret : ( ) =>
2239+ notOk ( {
2240+ summary : 'Schema has 1 error' ,
2241+ diagnostics : [ { code : 'PSL_INTERPRETER_FINDING' , message : 'finding' } ] ,
2242+ } ) ,
2243+ } as unknown as PslInterpretCapable ;
2244+ return {
2245+ ...resolutionForInputs ( [ schemaPath ] ) ,
2246+ interpretation : { source, context : { } as unknown as ContractSourceContext } ,
2247+ } ;
2248+ }
2249+
2250+ it ( 'publishes one config diagnostic on first-load failure and leaves the document unmanaged' , async ( ) => {
2251+ harness = startHarness ( async ( ) => {
2252+ throw new Error ( 'config exploded' ) ;
2253+ } ) ;
2254+ await harness . initialize ( ) ;
2255+ openDocument ( harness , schemaUri , cleanSchema ) ;
2256+
2257+ const published = await harness . waitForDiagnostics ( configUri ) ;
2258+ expect ( published ) . toEqual ( [ expectedConfigFailure ( 'config exploded' ) ] ) ;
2259+ expect ( harness . getDocumentAst ( schemaUri ) ) . toBeUndefined ( ) ;
2260+
2261+ // The marker persists after the failed load settles.
2262+ await settle ( ) ;
2263+ expect ( harness . latestDiagnostics ( configUri ) ) . toEqual ( [
2264+ expectedConfigFailure ( 'config exploded' ) ,
2265+ ] ) ;
2266+ } ) ;
2267+
2268+ it ( 'publishes the config diagnostic exactly once when two documents await the same load' , async ( ) => {
2269+ const otherPath = join ( root , 'other.psl' ) ;
2270+ const otherUri = pathToFileURL ( otherPath ) . toString ( ) ;
2271+ const gate = deferredSettleable < ConfigResolution > ( ) ;
2272+ harness = startHarness ( ( ) => gate . promise ) ;
2273+ await harness . initialize ( ) ;
2274+ openDocument ( harness , schemaUri , cleanSchema ) ;
2275+ openDocument ( harness , otherUri , cleanSchema ) ;
2276+ // Both open notifications are processed before the shared load settles.
2277+ await settle ( ) ;
2278+ gate . reject ( new Error ( 'config exploded' ) ) ;
2279+
2280+ await harness . waitForDiagnostics ( configUri ) ;
2281+ await settle ( ) ;
2282+ expect ( harness . publishCount ( configUri ) ) . toBe ( 1 ) ;
2283+ } ) ;
2284+
2285+ it ( 'publishes the config diagnostic even for pull-capable clients' , async ( ) => {
2286+ harness = startHarness ( async ( ) => {
2287+ throw new Error ( 'config exploded' ) ;
2288+ } , pullDiagnosticsCapabilities ) ;
2289+ await harness . initialize ( ) ;
2290+ openDocument ( harness , schemaUri , cleanSchema ) ;
2291+ // Pull clients trigger project resolution through the pull request; the
2292+ // config failure must still arrive on the push channel.
2293+ const report = await requestPullDiagnostics ( harness , schemaUri ) ;
2294+ expect ( fullReportItems ( report ) ) . toEqual ( [ ] ) ;
2295+
2296+ const published = await harness . waitForDiagnostics ( configUri ) ;
2297+ expect ( published ) . toEqual ( [ expectedConfigFailure ( 'config exploded' ) ] ) ;
2298+ } ) ;
2299+
2300+ it ( 'clears the config diagnostic on the next successful load' , async ( ) => {
2301+ let failures = 1 ;
2302+ harness = startHarness ( async ( ) => {
2303+ if ( failures > 0 ) {
2304+ failures -= 1 ;
2305+ throw new Error ( 'config exploded' ) ;
2306+ }
2307+ return resolutionForInputs ( [ schemaPath ] ) ;
2308+ } ) ;
2309+ await harness . initialize ( ) ;
2310+ openDocument ( harness , schemaUri , cleanSchema ) ;
2311+ await harness . waitForDiagnostics ( configUri ) ;
2312+
2313+ harness . notifyConfigChanged ( ) ;
2314+
2315+ await harness . waitForDiagnosticsMatching ( configUri , ( diagnostics ) => diagnostics . length === 0 ) ;
2316+ expect ( harness . latestDiagnostics ( configUri ) ) . toEqual ( [ ] ) ;
2317+ } ) ;
2318+
2319+ it ( 'keeps serving the last-good project through a broken reload, then swaps in the fix' , async ( ) => {
2320+ let mode : 'good' | 'broken' | 'fixed' = 'good' ;
2321+ harness = startHarness ( async ( ) => {
2322+ if ( mode === 'broken' ) {
2323+ throw new Error ( 'config exploded' ) ;
2324+ }
2325+ return interpretingResolution ( ) ;
2326+ } , pullDiagnosticsCapabilities ) ;
2327+ await harness . initialize ( ) ;
2328+ openDocument ( harness , schemaUri , cleanSchema ) ;
2329+
2330+ const before = await requestPullDiagnostics ( harness , schemaUri ) ;
2331+ expect ( fullReportItems ( before ) . map ( ( d ) => d . code ) ) . toContain ( 'PSL_INTERPRETER_FINDING' ) ;
2332+
2333+ mode = 'broken' ;
2334+ harness . notifyConfigChanged ( ) ;
2335+ const failure = await harness . waitForDiagnostics ( configUri ) ;
2336+ expect ( failure ) . toEqual ( [ expectedConfigFailure ( 'config exploded' ) ] ) ;
2337+
2338+ const retained = await requestPullDiagnostics ( harness , schemaUri ) ;
2339+ expect ( fullReportItems ( retained ) . map ( ( d ) => d . code ) ) . toContain ( 'PSL_INTERPRETER_FINDING' ) ;
2340+
2341+ mode = 'fixed' ;
2342+ harness . notifyConfigChanged ( ) ;
2343+ await harness . waitForDiagnosticsMatching ( configUri , ( diagnostics ) => diagnostics . length === 0 ) ;
2344+
2345+ const after = await requestPullDiagnostics ( harness , schemaUri ) ;
2346+ expect ( fullReportItems ( after ) . map ( ( d ) => d . code ) ) . toContain ( 'PSL_INTERPRETER_FINDING' ) ;
2347+ } ) ;
2348+
2349+ it ( 'clears the config diagnostic when the last managed document closes' , async ( ) => {
2350+ // A retained last-good project dropped by its final document close must
2351+ // not leave a zombie config marker behind.
2352+ let broken = false ;
2353+ harness = startHarness ( async ( ) => {
2354+ if ( broken ) {
2355+ throw new Error ( 'config exploded' ) ;
2356+ }
2357+ return resolutionForInputs ( [ schemaPath ] ) ;
2358+ } ) ;
2359+ await harness . initialize ( ) ;
2360+ openDocument ( harness , schemaUri , cleanSchema ) ;
2361+ await harness . waitForDiagnostics ( schemaUri ) ;
2362+
2363+ broken = true ;
2364+ harness . notifyConfigChanged ( ) ;
2365+ await harness . waitForDiagnostics ( configUri ) ;
2366+
2367+ harness . client . sendNotification ( DidCloseTextDocumentNotification . type , {
2368+ textDocument : { uri : schemaUri } ,
2369+ } ) ;
2370+
2371+ await harness . waitForDiagnosticsMatching ( configUri , ( diagnostics ) => diagnostics . length === 0 ) ;
2372+ } ) ;
2373+
2374+ it ( 'stays silent for a superseded load failure' , async ( ) => {
2375+ const first = deferredSettleable < ConfigResolution > ( ) ;
2376+ let call = 0 ;
2377+ harness = startHarness ( ( ) => {
2378+ call += 1 ;
2379+ return call === 1 ? first . promise : Promise . resolve ( resolutionForInputs ( [ schemaPath ] ) ) ;
2380+ } ) ;
2381+ await harness . initialize ( ) ;
2382+ openDocument ( harness , schemaUri , cleanSchema ) ;
2383+
2384+ harness . notifyConfigChanged ( ) ;
2385+ // The watched-config handler swaps the current load synchronously before
2386+ // its first await; settling lets that notification dispatch, so the
2387+ // rejection below lands on a superseded load.
2388+ await settle ( ) ;
2389+ first . reject ( new Error ( 'superseded failure' ) ) ;
2390+
2391+ await harness . waitForDiagnostics ( schemaUri ) ;
2392+ await settle ( ) ;
2393+ expect ( harness . publishCount ( configUri ) ) . toBe ( 0 ) ;
2394+ } ) ;
2395+ } ) ;
0 commit comments