Fix CI issues (#476)

This commit is contained in:
Peter Steinberger 2025-07-28 09:19:40 +02:00 committed by GitHub
parent acdc4f22a8
commit 9d7fe36699
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 111 additions and 41 deletions

View file

@ -121,7 +121,7 @@ jobs:
run: | run: |
echo "Resolving Swift package dependencies..." echo "Resolving Swift package dependencies..."
# Workspace is at root level # Workspace is at root level
xcodebuild -resolvePackageDependencies -workspace VibeTunnel.xcworkspace || echo "Dependency resolution completed" xcodebuild -resolvePackageDependencies -workspace VibeTunnel.xcworkspace -scheme VibeTunnel-Mac || echo "Dependency resolution completed"
# Debug: List available schemes # Debug: List available schemes
echo "=== Available schemes ===" echo "=== Available schemes ==="

View file

@ -1,4 +1,4 @@
<!-- Generated: 2025-07-26 18:45:00 UTC --> <!-- Generated: 2025-07-28 12:35:00 UTC -->
<p align="center"> <p align="center">
<img src="assets/banner.png" alt="VibeTunnel Banner" /> <img src="assets/banner.png" alt="VibeTunnel Banner" />
</p> </p>

View file

@ -76,7 +76,8 @@ class ServerManager {
// Log for debugging // Log for debugging
// logger // logger
// .debug( // .debug(
// "bindAddress getter: rawValue='\(rawValue)', mode=\(mode.rawValue), bindAddress=\(mode.bindAddress)" // "bindAddress getter: rawValue='\(rawValue)', mode=\(mode.rawValue),
// bindAddress=\(mode.bindAddress)"
// ) // )
return mode.bindAddress return mode.bindAddress

View file

@ -209,7 +209,7 @@ struct AutocompleteTextField: View {
@State private var keyboardNavigating = false @State private var keyboardNavigating = false
@State private var textFieldSize: CGSize = .zero @State private var textFieldSize: CGSize = .zero
var body: some View { var body: some View {
TextField(placeholder, text: $text) TextField(placeholder, text: $text)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
@ -266,10 +266,10 @@ struct AutocompleteTextField: View {
) )
) )
) )
.onAppear { .onAppear {
// Initialize autocompleteService with GitRepositoryMonitor // Initialize autocompleteService with GitRepositoryMonitor
autocompleteService = AutocompleteService(gitMonitor: gitMonitor) autocompleteService = AutocompleteService(gitMonitor: gitMonitor)
} }
} }
private func handleKeyPress(_ keyPress: KeyPress) -> KeyPress.Result { private func handleKeyPress(_ keyPress: KeyPress) -> KeyPress.Result {

View file

@ -1,5 +1,5 @@
import SwiftUI
import AppKit import AppKit
import SwiftUI
/// Simple NSWindow-based dropdown for autocomplete /// Simple NSWindow-based dropdown for autocomplete
struct AutocompleteWindowView: NSViewRepresentable { struct AutocompleteWindowView: NSViewRepresentable {
@ -9,13 +9,13 @@ struct AutocompleteWindowView: NSViewRepresentable {
let onSelect: (String) -> Void let onSelect: (String) -> Void
let width: CGFloat let width: CGFloat
@Binding var isShowing: Bool @Binding var isShowing: Bool
func makeNSView(context: Context) -> NSView { func makeNSView(context: Context) -> NSView {
let view = NSView() let view = NSView()
view.wantsLayer = true view.wantsLayer = true
return view return view
} }
func updateNSView(_ nsView: NSView, context: Context) { func updateNSView(_ nsView: NSView, context: Context) {
if isShowing && !suggestions.isEmpty { if isShowing && !suggestions.isEmpty {
context.coordinator.showDropdown( context.coordinator.showDropdown(
@ -29,11 +29,11 @@ struct AutocompleteWindowView: NSViewRepresentable {
context.coordinator.hideDropdown() context.coordinator.hideDropdown()
} }
} }
func makeCoordinator() -> Coordinator { func makeCoordinator() -> Coordinator {
Coordinator(onSelect: onSelect, isShowing: $isShowing, selectedIndex: $selectedIndex) Coordinator(onSelect: onSelect, isShowing: $isShowing, selectedIndex: $selectedIndex)
} }
@MainActor @MainActor
class Coordinator: NSObject { class Coordinator: NSObject {
private var dropdownWindow: NSWindow? private var dropdownWindow: NSWindow?
@ -41,15 +41,15 @@ struct AutocompleteWindowView: NSViewRepresentable {
private let onSelect: (String) -> Void private let onSelect: (String) -> Void
@Binding var isShowing: Bool @Binding var isShowing: Bool
@Binding var selectedIndex: Int @Binding var selectedIndex: Int
nonisolated(unsafe) private var clickMonitor: Any? private nonisolated(unsafe) var clickMonitor: Any?
init(onSelect: @escaping (String) -> Void, isShowing: Binding<Bool>, selectedIndex: Binding<Int>) { init(onSelect: @escaping (String) -> Void, isShowing: Binding<Bool>, selectedIndex: Binding<Int>) {
self.onSelect = onSelect self.onSelect = onSelect
self._isShowing = isShowing self._isShowing = isShowing
self._selectedIndex = selectedIndex self._selectedIndex = selectedIndex
super.init() super.init()
} }
deinit { deinit {
if let monitor = clickMonitor { if let monitor = clickMonitor {
DispatchQueue.main.async { DispatchQueue.main.async {
@ -57,7 +57,7 @@ struct AutocompleteWindowView: NSViewRepresentable {
} }
} }
} }
@MainActor @MainActor
private func cleanupClickMonitor() { private func cleanupClickMonitor() {
if let monitor = clickMonitor { if let monitor = clickMonitor {
@ -65,7 +65,7 @@ struct AutocompleteWindowView: NSViewRepresentable {
clickMonitor = nil clickMonitor = nil
} }
} }
@MainActor @MainActor
func showDropdown( func showDropdown(
on view: NSView, on view: NSView,
@ -75,7 +75,7 @@ struct AutocompleteWindowView: NSViewRepresentable {
width: CGFloat width: CGFloat
) { ) {
guard let parentWindow = view.window else { return } guard let parentWindow = view.window else { return }
// Create window if needed // Create window if needed
if dropdownWindow == nil { if dropdownWindow == nil {
let window = NSWindow( let window = NSWindow(
@ -84,23 +84,23 @@ struct AutocompleteWindowView: NSViewRepresentable {
backing: .buffered, backing: .buffered,
defer: false defer: false
) )
window.isOpaque = false window.isOpaque = false
window.backgroundColor = .clear window.backgroundColor = .clear
window.hasShadow = true window.hasShadow = true
window.level = .floating window.level = .floating
window.isReleasedWhenClosed = false window.isReleasedWhenClosed = false
let hostingView = NSHostingView(rootView: AnyView(EmptyView())) let hostingView = NSHostingView(rootView: AnyView(EmptyView()))
window.contentView = hostingView window.contentView = hostingView
self.dropdownWindow = window self.dropdownWindow = window
self.hostingView = hostingView self.hostingView = hostingView
} }
guard let window = dropdownWindow, guard let window = dropdownWindow,
let hostingView = hostingView else { return } let hostingView else { return }
// Update content with proper binding // Update content with proper binding
let content = VStack(spacing: 0) { let content = VStack(spacing: 0) {
AutocompleteViewWithKeyboard( AutocompleteViewWithKeyboard(
@ -120,13 +120,13 @@ struct AutocompleteWindowView: NSViewRepresentable {
RoundedRectangle(cornerRadius: 6) RoundedRectangle(cornerRadius: 6)
.stroke(Color.primary.opacity(0.1), lineWidth: 1) .stroke(Color.primary.opacity(0.1), lineWidth: 1)
) )
hostingView.rootView = AnyView(content) hostingView.rootView = AnyView(content)
// Position window below the text field // Position window below the text field
let viewFrame = view.convert(view.bounds, to: nil) let viewFrame = view.convert(view.bounds, to: nil)
let screenFrame = parentWindow.convertToScreen(viewFrame) let screenFrame = parentWindow.convertToScreen(viewFrame)
// Calculate window position // Calculate window position
let windowFrame = NSRect( let windowFrame = NSRect(
x: screenFrame.minX, x: screenFrame.minX,
@ -134,15 +134,15 @@ struct AutocompleteWindowView: NSViewRepresentable {
width: width, width: width,
height: 200 height: 200
) )
window.setFrame(windowFrame, display: false) window.setFrame(windowFrame, display: false)
// Show window // Show window
if window.parent == nil { if window.parent == nil {
parentWindow.addChildWindow(window, ordered: .above) parentWindow.addChildWindow(window, ordered: .above)
} }
window.makeKeyAndOrderFront(nil) window.makeKeyAndOrderFront(nil)
// Setup click monitoring // Setup click monitoring
if clickMonitor == nil { if clickMonitor == nil {
clickMonitor = NSEvent.addLocalMonitorForEvents( clickMonitor = NSEvent.addLocalMonitorForEvents(
@ -155,11 +155,11 @@ struct AutocompleteWindowView: NSViewRepresentable {
} }
} }
} }
@MainActor @MainActor
func hideDropdown() { func hideDropdown() {
cleanupClickMonitor() cleanupClickMonitor()
if let window = dropdownWindow { if let window = dropdownWindow {
if let parent = window.parent { if let parent = window.parent {
parent.removeChildWindow(window) parent.removeChildWindow(window)
@ -168,4 +168,4 @@ struct AutocompleteWindowView: NSViewRepresentable {
} }
} }
} }
} }

View file

@ -10,6 +10,7 @@ import UserNotifications
/// across all windows and handles deep linking for terminal session URLs. /// across all windows and handles deep linking for terminal session URLs.
/// ///
/// This application runs on macOS 14.0+ and requires Swift 6. /// This application runs on macOS 14.0+ and requires Swift 6.
/// The app provides terminal access through web browsers.
@main @main
struct VibeTunnelApp: App { struct VibeTunnelApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) @NSApplicationDelegateAdaptor(AppDelegate.self)

View file

@ -147,6 +147,7 @@ child.on('error', (error) => {
// Log when process starts // Log when process starts
child.on('spawn', () => { child.on('spawn', () => {
console.log('Server process spawned successfully'); console.log('Server process spawned successfully');
console.log(`Server PID: ${child.pid}`);
}); });
// Handle early exit // Handle early exit
@ -204,6 +205,25 @@ if (process.env.CI || process.env.WAIT_FOR_SERVER) {
process.exit(1); process.exit(1);
} else { } else {
console.log('Server is ready, tests can proceed'); console.log('Server is ready, tests can proceed');
// In CI, add periodic health checks
if (process.env.CI) {
const healthCheckInterval = setInterval(() => {
if (hasExited) {
clearInterval(healthCheckInterval);
return;
}
const http = require('http');
http.get(`http://localhost:${port}/api/health`, (res) => {
if (res.statusCode !== 200) {
console.error(`Health check failed with status ${res.statusCode}`);
}
}).on('error', (err) => {
console.error(`Health check error: ${err.message}`);
});
}, 10000); // Check every 10 seconds
}
} }
}); });
}, 3000); // Wait 3 seconds before checking }, 3000); // Wait 3 seconds before checking

View file

@ -1,3 +1,4 @@
// VibeTunnel server entry point
import chalk from 'chalk'; import chalk from 'chalk';
import compression from 'compression'; import compression from 'compression';
import type { Response as ExpressResponse } from 'express'; import type { Response as ExpressResponse } from 'express';

View file

@ -40,7 +40,34 @@ export class TestSessionManager {
let sessionId = ''; let sessionId = '';
if (!spawnWindow) { if (!spawnWindow) {
console.log(`Web session created, waiting for navigation to session view...`); console.log(`Web session created, waiting for navigation to session view...`);
await this.page.waitForURL(/\/session\//, { timeout: 10000 }); // Increase timeout for CI and add retry logic
const timeout = process.env.CI ? 30000 : 15000;
// Add a small delay to ensure navigation starts
await this.page.waitForTimeout(500);
try {
await this.page.waitForURL(/\/session\//, { timeout });
} catch (error) {
// If waitForURL times out, check if we're already on a session page
const currentUrl = this.page.url();
if (!currentUrl.includes('/session/')) {
// Try to wait for any navigation
try {
await this.page.waitForLoadState('domcontentloaded', { timeout: 5000 });
} catch (_loadError) {
// Ignore load state error
}
const finalUrl = this.page.url();
if (!finalUrl.includes('/session/')) {
console.error(
`Navigation failed. Initial URL: ${currentUrl}, Final URL: ${finalUrl}`
);
throw error;
}
}
console.log(`Already on session page: ${currentUrl}`);
}
const url = this.page.url(); const url = this.page.url();
if (!url.includes('/session/')) { if (!url.includes('/session/')) {

View file

@ -443,7 +443,7 @@ export class SessionListPage extends BasePage {
if (sessionId) { if (sessionId) {
await this.page.goto(`/session/${sessionId}`, { await this.page.goto(`/session/${sessionId}`, {
waitUntil: 'domcontentloaded', waitUntil: 'domcontentloaded',
timeout: 15000, // Increase timeout for CI timeout: process.env.CI ? 30000 : 15000, // Increase timeout for CI
}); });
} else { } else {
// Wait for automatic navigation // Wait for automatic navigation

View file

@ -23,14 +23,22 @@ test.describe('Terminal Interaction', () => {
// Use unique prefix for this test file to prevent session conflicts // Use unique prefix for this test file to prevent session conflicts
sessionManager = new TestSessionManager(page, 'termint'); sessionManager = new TestSessionManager(page, 'termint');
// Add network error logging for debugging
page.on('requestfailed', (request) => {
console.error(`Request failed: ${request.url()} - ${request.failure()?.errorText}`);
});
// Create a session for all tests using the session manager to ensure proper tracking // Create a session for all tests using the session manager to ensure proper tracking
const sessionData = await sessionManager.createTrackedSession('terminal-test'); const sessionData = await sessionManager.createTrackedSession('terminal-test');
// Navigate to the created session // Navigate to the created session with increased timeout for CI
await page.goto(`/session/${sessionData.sessionId}`, { waitUntil: 'domcontentloaded' }); await page.goto(`/session/${sessionData.sessionId}`, {
waitUntil: 'domcontentloaded',
timeout: process.env.CI ? 30000 : 15000,
});
// Wait for terminal with proper WebSocket handling // Wait for terminal with proper WebSocket handling
await waitForTerminalReady(page, 10000); await waitForTerminalReady(page, process.env.CI ? 20000 : 10000);
}); });
test.afterEach(async () => { test.afterEach(async () => {

View file

@ -28,8 +28,20 @@ describe('AsciinemaWriter byte position tracking', () => {
it('should track byte position correctly for header', async () => { it('should track byte position correctly for header', async () => {
writer = AsciinemaWriter.create(testFile, 80, 24, 'test command', 'Test Title'); writer = AsciinemaWriter.create(testFile, 80, 24, 'test command', 'Test Title');
// Give the header time to be written // Wait for the header to be written with a polling mechanism
await new Promise((resolve) => setTimeout(resolve, 10)); let attempts = 0;
const maxAttempts = 50; // 50 * 10ms = 500ms max wait
while (attempts < maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, 10));
const position = writer.getPosition();
if (position.written > 0 && position.pending === 0) {
// Header has been written
break;
}
attempts++;
}
const position = writer.getPosition(); const position = writer.getPosition();
expect(position.written).toBeGreaterThan(0); expect(position.written).toBeGreaterThan(0);