@@ -26,12 +26,14 @@ It is used to test the following features:
2626 1. Create a stack
2727 2. Create a service with prerequisites, env vars, file overrides, and the StackCraft GitHub repository
2828 3. Clone, install, build and run the service
29- 4. Verify that the service is running
30- 5. Verify that the service is logging to the console
31- 6. The dog has eaten the food. Woof woof!
29+ 4. Verify that the first start fails (port already in use)
30+ 5. Override APP_SERVICE_PORT to a unique port and restart
31+ 6. Verify that the service is running and reachable via HTTP
32+ 7. Stop the service and remove the stack
3233
3334 `
3435
36+ const dogfoodingPort = browserName === 'chromium' ? 19090 : 19091
3537 const workingDirectory = `/tmp/e2e-dog-fooding-time-${ uuid } `
3638
3739 await page . goto ( '/' )
@@ -119,6 +121,36 @@ It is used to test the following features:
119121 await prereqForm . locator ( 'button' , { hasText : 'Add' } ) . click ( )
120122 await expect ( page . locator ( 'shade-noty-list' ) ) . toContainText ( '"Encryption Key" was added.' )
121123
124+ // Prerequisite 6: DATABASE_URL (required by the service to connect to Postgres)
125+ await page . locator ( 'button' , { hasText : 'Add Prerequisite' } ) . click ( )
126+ await expect ( prereqForm ) . toBeVisible ( )
127+ await prereqForm . locator ( 'input[name="name"]' ) . fill ( 'Database URL' )
128+ await typeSelect . locator ( '.select-trigger' ) . click ( )
129+ await typeSelect . locator ( '.dropdown-item' , { hasText : 'Environment Variable' } ) . click ( )
130+ await prereqForm . locator ( 'input[name="variableName"]' ) . fill ( 'DATABASE_URL' )
131+ await prereqForm . locator ( 'button' , { hasText : 'Add' } ) . click ( )
132+ await expect ( page . locator ( 'shade-noty-list' ) ) . toContainText ( '"Database URL" was added.' )
133+
134+ // Prerequisite 7: APP_SERVICE_PORT (used to override the port for the dogfooding instance)
135+ await page . locator ( 'button' , { hasText : 'Add Prerequisite' } ) . click ( )
136+ await expect ( prereqForm ) . toBeVisible ( )
137+ await prereqForm . locator ( 'input[name="name"]' ) . fill ( 'Service Port' )
138+ await typeSelect . locator ( '.select-trigger' ) . click ( )
139+ await typeSelect . locator ( '.dropdown-item' , { hasText : 'Environment Variable' } ) . click ( )
140+ await prereqForm . locator ( 'input[name="variableName"]' ) . fill ( 'APP_SERVICE_PORT' )
141+ await prereqForm . locator ( 'button' , { hasText : 'Add' } ) . click ( )
142+ await expect ( page . locator ( 'shade-noty-list' ) ) . toContainText ( '"Service Port" was added.' )
143+
144+ // Prerequisite 8: MCP_PORT (must differ from the test host's MCP port to avoid conflict)
145+ await page . locator ( 'button' , { hasText : 'Add Prerequisite' } ) . click ( )
146+ await expect ( prereqForm ) . toBeVisible ( )
147+ await prereqForm . locator ( 'input[name="name"]' ) . fill ( 'MCP Port' )
148+ await typeSelect . locator ( '.select-trigger' ) . click ( )
149+ await typeSelect . locator ( '.dropdown-item' , { hasText : 'Environment Variable' } ) . click ( )
150+ await prereqForm . locator ( 'input[name="variableName"]' ) . fill ( 'MCP_PORT' )
151+ await prereqForm . locator ( 'button' , { hasText : 'Add' } ) . click ( )
152+ await expect ( page . locator ( 'shade-noty-list' ) ) . toContainText ( '"MCP Port" was added.' )
153+
122154 // --- Add local .env file override ---
123155 await page . locator ( 'button' , { hasText : 'Add Local File' } ) . click ( )
124156 await page . locator ( 'input[placeholder="Relative path (e.g. .env)"]' ) . fill ( '.env' )
@@ -154,17 +186,101 @@ It is used to test the following features:
154186 await page . locator ( 'button' , { hasText : 'View Service' } ) . click ( )
155187 await expect ( page . locator ( 'shade-service-detail' ) ) . toBeVisible ( )
156188
157- // Start the service (click the primary action in the header, not the stepper)
189+ // ======================================================================
190+ // Step 3: First start attempt — expect failure (port 9090 already in use)
191+ // ======================================================================
192+
158193 await page . getByTestId ( 'page-header-actions' ) . getByRole ( 'button' , { name : 'Start' } ) . click ( )
159194
160- // Wait for the service to reach running state
161- await expect ( page . locator ( 'shade-service-status-indicator' ) ) . toContainText ( 'Running' )
195+ // The spawned service will try to bind to port 9090 (default), which is
196+ // already occupied by the test host. Expect status to transition to Error.
197+ await expect ( page . getByTestId ( 'service-status-indicator' ) ) . toContainText ( 'Error' , { timeout : 60_000 } )
162198
163- // Navigate to the Logs tab via the tab bar
199+ // Navigate to Logs and verify the error is visible
164200 await page . getByTestId ( 'service-detail-tabs' ) . getByRole ( 'tab' , { name : 'Logs' } ) . click ( )
165201 await expect ( page . locator ( 'shade-service-logs-tab' ) ) . toBeVisible ( )
166202
167- // Verify that the log viewer is present with entries
168- await expect ( page . locator ( 'shade-service-logs-tab shade-log-viewer' ) ) . toBeVisible ( )
169- await expect ( page . locator ( 'shade-service-logs-tab shade-log-viewer' ) ) . not . toContainText ( 'No log output yet.' )
203+ const logViewer = page . locator ( 'shade-service-logs-tab shade-log-viewer' )
204+ await expect ( logViewer ) . toBeVisible ( )
205+ await expect ( logViewer ) . toContainText ( / E A D D R I N U S E | a d d r e s s a l r e a d y i n u s e / , { timeout : 10_000 } )
206+
207+ // ======================================================================
208+ // Step 4: Set APP_SERVICE_PORT env override to a unique port and restart
209+ // ======================================================================
210+
211+ // Navigate to the Configuration tab
212+ await page . getByTestId ( 'service-detail-tabs' ) . getByRole ( 'tab' , { name : 'Configuration' } ) . click ( )
213+ await expect ( page . locator ( 'shade-service-config-tab' ) ) . toBeVisible ( )
214+
215+ // Set APP_SERVICE_PORT override to a unique port per browser
216+ const portOverride = page . getByTestId ( 'env-override-APP_SERVICE_PORT' )
217+ await expect ( portOverride ) . toBeVisible ( )
218+
219+ const portSourceSelect = portOverride . locator ( 'shade-select' ) . first ( )
220+ await portSourceSelect . locator ( '.select-trigger' ) . click ( )
221+ await portSourceSelect . locator ( '.dropdown-item' , { hasText : 'Custom value' } ) . click ( )
222+ await portOverride . locator ( 'shade-input input' ) . fill ( String ( dogfoodingPort ) )
223+
224+ // Set MCP_PORT override to avoid conflict with the test host's MCP server
225+ const mcpOverride = page . getByTestId ( 'env-override-MCP_PORT' )
226+ await expect ( mcpOverride ) . toBeVisible ( )
227+
228+ const mcpSourceSelect = mcpOverride . locator ( 'shade-select' ) . first ( )
229+ await mcpSourceSelect . locator ( '.select-trigger' ) . click ( )
230+ await mcpSourceSelect . locator ( '.dropdown-item' , { hasText : 'Custom value' } ) . click ( )
231+ await mcpOverride . locator ( 'shade-input input' ) . fill ( String ( dogfoodingPort + 1 ) )
232+
233+ // Save the overrides and wait for the save to complete
234+ const saveButton = page . getByTestId ( 'save-env-overrides' )
235+ await saveButton . click ( )
236+ await expect ( saveButton ) . not . toHaveAttribute ( 'loading' , { timeout : 10_000 } )
237+
238+ // Navigate back to the Overview tab and start the service again
239+ await page . getByTestId ( 'service-detail-tabs' ) . getByRole ( 'tab' , { name : 'Overview' } ) . click ( )
240+ await page . getByTestId ( 'page-header-actions' ) . getByRole ( 'button' , { name : 'Start' } ) . click ( )
241+
242+ // Wait for the service to reach running state on the new port
243+ await expect ( page . getByTestId ( 'service-status-indicator' ) ) . toContainText ( 'Running' , { timeout : 60_000 } )
244+
245+ // ======================================================================
246+ // Step 5: Verify logs and fire an HTTP request to the running service
247+ // ======================================================================
248+
249+ await page . getByTestId ( 'service-detail-tabs' ) . getByRole ( 'tab' , { name : 'Logs' } ) . click ( )
250+ await expect ( page . locator ( 'shade-service-logs-tab' ) ) . toBeVisible ( )
251+
252+ const successLogViewer = page . locator ( 'shade-service-logs-tab shade-log-viewer' )
253+ await expect ( successLogViewer ) . toBeVisible ( )
254+ await expect ( successLogViewer ) . not . toContainText ( 'No log output yet.' )
255+
256+ // Verify the service is actually reachable on the new port
257+ await expect ( async ( ) => {
258+ const response = await page . request . get ( `http://localhost:${ dogfoodingPort } ` )
259+ expect ( response . status ( ) ) . toBe ( 200 )
260+ } ) . toPass ( { timeout : 30_000 } )
261+
262+ // ======================================================================
263+ // Step 6: Stop the service and remove the stack
264+ // ======================================================================
265+
266+ // Stop the running service (SIGTERM may cause non-zero exit → "Error" or clean → "Stopped")
267+ await page . getByTestId ( 'page-header-actions' ) . getByRole ( 'button' , { name : 'Stop' } ) . click ( )
268+ await expect ( page . getByTestId ( 'service-status-indicator' ) ) . toContainText ( / S t o p p e d | E r r o r / , { timeout : 30_000 } )
269+
270+ // Navigate to the main dashboard, then to the stack, then to Edit Stack
271+ await page . locator ( 'shade-sidebar-item a' , { hasText : 'Dashboard' } ) . click ( )
272+ await expect ( page . locator ( 'stack-list-dashboard' ) ) . toBeVisible ( )
273+
274+ await page . locator ( 'stack-list-dashboard shade-card' , { hasText : displayName } ) . click ( )
275+ await expect ( page . getByTestId ( 'page-header-title' ) ) . toContainText ( displayName )
276+
277+ await page . locator ( 'a' , { hasText : 'Edit Stack' } ) . click ( )
278+ await expect ( page . locator ( 'shade-edit-stack' ) ) . toBeVisible ( )
279+
280+ // Delete the stack
281+ await page . locator ( 'button' , { hasText : 'Delete Stack' } ) . click ( )
282+ await page . locator ( 'shade-dialog .dialog-confirm-btn' ) . click ( )
283+
284+ await expect ( page . locator ( 'shade-noty-list' ) ) . toContainText ( `"${ displayName } " was deleted.` )
285+ await expect ( page . locator ( 'shade-dashboard' ) ) . toBeVisible ( { timeout : 10_000 } )
170286} )
0 commit comments