1+ import { test , expect , Page } from '@playwright/test'
2+ import path from 'path'
3+ import fs from 'fs-extra'
4+ import os from 'os'
5+ import { startVSCode , SUSHI_SOURCE_PATH } from './utils'
6+
7+ /**
8+ * Helper function to create a broken SQLMesh project
9+ */
10+ async function createBrokenProject ( tempDir : string ) : Promise < void > {
11+ // Copy the sushi project as a base
12+ await fs . copy ( SUSHI_SOURCE_PATH , tempDir )
13+
14+ // Create a broken config.py that will fail to load
15+ const brokenConfig = `
16+ # This config will fail to load due to import error
17+ from non_existent_module import something_that_doesnt_exist
18+
19+ # Rest of the config would be here but it won't get executed
20+ model_defaults = {}
21+ `
22+ await fs . writeFile ( path . join ( tempDir , 'config.py' ) , brokenConfig )
23+ }
24+
25+ /**
26+ * Helper function to create a project with invalid SQL that will cause linting errors
27+ */
28+ async function createProjectWithInvalidSQL ( tempDir : string ) : Promise < void > {
29+ // Copy the sushi project as a base
30+ await fs . copy ( SUSHI_SOURCE_PATH , tempDir )
31+
32+ // Create a model with invalid SQL syntax
33+ const invalidSQL = `
34+ MODEL (
35+ name sushi.broken_model,
36+ kind FULL
37+ );
38+
39+ -- This SQL has syntax errors that should trigger diagnostics
40+ SELECT
41+ invalid_column_that_doesnt_exist,
42+ FROM non_existent_table
43+ WHERE invalid syntax here
44+ `
45+ await fs . writeFile ( path . join ( tempDir , 'models' , 'broken_model.sql' ) , invalidSQL )
46+ }
47+
48+ /**
49+ * Test that failing projects don't spam the user with excessive error messages
50+ */
51+ test ( 'Failing project - broken config does not spam user with errors' , async ( ) => {
52+ const tempDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'vscode-test-broken-' ) )
53+ await createBrokenProject ( tempDir )
54+
55+ try {
56+ const { window, close } = await startVSCode ( tempDir )
57+
58+ // Try to trigger lineage command which would normally load the context
59+ await window . keyboard . press (
60+ process . platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P' ,
61+ )
62+ await window . keyboard . type ( 'Lineage: Focus On View' )
63+ await window . keyboard . press ( 'Enter' )
64+
65+ // Wait a reasonable amount of time for any error messages to appear
66+ await window . waitForTimeout ( 3000 )
67+
68+ // Check that we don't see excessive error notifications
69+ // We expect at most one error message about context loading failure
70+ const errorNotifications = window . locator ( '.notifications-center .notification-list-item.error' )
71+ const errorCount = await errorNotifications . count ( )
72+
73+ // Should have at most 1-2 error notifications, not a spam of them
74+ expect ( errorCount ) . toBeLessThanOrEqual ( 2 )
75+
76+ // Verify that no "Loaded SQLMesh context" success message appears
77+ const loadedContextText = window . locator ( 'text=Loaded SQLMesh context' )
78+ await expect ( loadedContextText ) . not . toBeVisible ( { timeout : 2000 } )
79+
80+ await close ( )
81+ } finally {
82+ await fs . remove ( tempDir )
83+ }
84+ } )
85+
86+ /**
87+ * Test that projects with invalid SQL show diagnostics but don't crash the LSP
88+ */
89+ test ( 'Failing project - invalid SQL shows diagnostics without crashing' , async ( ) => {
90+ const tempDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'vscode-test-invalid-sql-' ) )
91+ await createProjectWithInvalidSQL ( tempDir )
92+
93+ try {
94+ const { window, close } = await startVSCode ( tempDir )
95+
96+ // Open the broken SQL file
97+ await window . locator ( 'text=broken_model.sql' ) . click ( )
98+
99+ // Wait for the file to open and the LSP to process it
100+ await window . waitForTimeout ( 2000 )
101+
102+ // Check that we have some diagnostic errors (red squiggles) in the editor
103+ // This validates that the LSP is working and providing diagnostics
104+ const diagnosticMarkers = window . locator ( '.monaco-editor .squiggly-error, .monaco-editor .squiggly-warning' )
105+ await expect ( diagnosticMarkers . first ( ) ) . toBeVisible ( { timeout : 5000 } )
106+
107+ // Verify that the problems panel shows issues
108+ await window . keyboard . press (
109+ process . platform === 'darwin' ? 'Meta+Shift+M' : 'Control+Shift+M' ,
110+ )
111+
112+ // Should see problems in the problems panel
113+ const problemsPanel = window . locator ( '.panel .problems-panel' )
114+ await expect ( problemsPanel ) . toBeVisible ( { timeout : 3000 } )
115+
116+ // Check that there are no excessive error notifications in the UI
117+ const errorNotifications = window . locator ( '.notifications-center .notification-list-item.error' )
118+ const errorCount = await errorNotifications . count ( )
119+ expect ( errorCount ) . toBeLessThanOrEqual ( 3 ) // Allow for a few errors but not spam
120+
121+ await close ( )
122+ } finally {
123+ await fs . remove ( tempDir )
124+ }
125+ } )
126+
127+ /**
128+ * Test that project loading failures are handled gracefully without hanging
129+ */
130+ test ( 'Failing project - graceful handling of context loading failures' , async ( ) => {
131+ const tempDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'vscode-test-context-fail-' ) )
132+ await createBrokenProject ( tempDir )
133+
134+ try {
135+ const { window, close } = await startVSCode ( tempDir )
136+
137+ // Try various LSP operations that would require context loading
138+ const operations = [
139+ async ( ) => {
140+ // Try hover
141+ await window . locator ( 'text=config.py' ) . click ( )
142+ await window . waitForTimeout ( 500 )
143+ const editor = window . locator ( '.monaco-editor' )
144+ await editor . hover ( )
145+ } ,
146+ async ( ) => {
147+ // Try opening a SQL file
148+ await window . locator ( 'text=customers.sql' ) . click ( )
149+ await window . waitForTimeout ( 500 )
150+ } ,
151+ async ( ) => {
152+ // Try formatting command
153+ await window . keyboard . press (
154+ process . platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P' ,
155+ )
156+ await window . keyboard . type ( 'Format Document' )
157+ await window . keyboard . press ( 'Escape' ) // Cancel the command
158+ }
159+ ]
160+
161+ // Execute operations and ensure none hang indefinitely
162+ for ( const operation of operations ) {
163+ const timeoutPromise = new Promise ( ( _ , reject ) =>
164+ setTimeout ( ( ) => reject ( new Error ( 'Operation timed out' ) ) , 5000 )
165+ )
166+
167+ await Promise . race ( [
168+ operation ( ) . catch ( ( ) => {
169+ // Operations may fail, that's expected for broken projects
170+ } ) ,
171+ timeoutPromise
172+ ] )
173+ }
174+
175+ // Verify VSCode is still responsive
176+ await window . keyboard . press (
177+ process . platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P' ,
178+ )
179+ await window . keyboard . press ( 'Escape' )
180+
181+ await close ( )
182+ } finally {
183+ await fs . remove ( tempDir )
184+ }
185+ } )
186+
187+ /**
188+ * Test that repeated operations on failing projects don't accumulate errors
189+ */
190+ test ( 'Failing project - repeated operations do not accumulate error spam' , async ( ) => {
191+ const tempDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'vscode-test-repeated-' ) )
192+ await createBrokenProject ( tempDir )
193+
194+ try {
195+ const { window, close } = await startVSCode ( tempDir )
196+
197+ // Perform the same operation multiple times
198+ for ( let i = 0 ; i < 3 ; i ++ ) {
199+ await window . keyboard . press (
200+ process . platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P' ,
201+ )
202+ await window . keyboard . type ( 'Lineage: Focus On View' )
203+ await window . keyboard . press ( 'Enter' )
204+ await window . waitForTimeout ( 1000 )
205+
206+ // Close any command palette that might be open
207+ await window . keyboard . press ( 'Escape' )
208+ }
209+
210+ // Check that error notifications haven't accumulated excessively
211+ const errorNotifications = window . locator ( '.notifications-center .notification-list-item.error' )
212+ const errorCount = await errorNotifications . count ( )
213+
214+ // Should not have accumulated errors for each repeated operation
215+ // Allow for a reasonable number but ensure it's not 3x the number of operations
216+ expect ( errorCount ) . toBeLessThanOrEqual ( 4 )
217+
218+ await close ( )
219+ } finally {
220+ await fs . remove ( tempDir )
221+ }
222+ } )
0 commit comments