mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Fix control message loop and simplify welcome screen repository display (#372)
This commit is contained in:
parent
500c75ebc8
commit
2f3a4217d0
8 changed files with 211 additions and 114 deletions
13
README.md
13
README.md
|
|
@ -675,6 +675,19 @@ When developing the web interface, you often need to test changes on external de
|
||||||
- **Firewall**: macOS may prompt to allow incoming connections - click "Allow"
|
- **Firewall**: macOS may prompt to allow incoming connections - click "Allow"
|
||||||
- **Auto-rebuild**: Changes to the web code are automatically rebuilt, but you need to manually refresh the browser
|
- **Auto-rebuild**: Changes to the web code are automatically rebuilt, but you need to manually refresh the browser
|
||||||
|
|
||||||
|
##### Pasting on Mobile Devices
|
||||||
|
|
||||||
|
When using VibeTunnel on mobile browsers (Safari, Chrome), pasting works differently than on desktop:
|
||||||
|
|
||||||
|
**To paste on mobile:**
|
||||||
|
1. Press the paste button on the keyboard toolbar
|
||||||
|
2. A white input box will appear
|
||||||
|
3. Long-press inside the white box to bring up the paste menu
|
||||||
|
4. Select "Paste" from the menu
|
||||||
|
5. The text will be pasted into your terminal session
|
||||||
|
|
||||||
|
**Note**: Due to browser security restrictions on non-HTTPS connections, the paste API is limited on mobile devices. The white input box is a workaround that allows clipboard access through the browser's native paste functionality.
|
||||||
|
|
||||||
#### Future: Hot Module Replacement
|
#### Future: Hot Module Replacement
|
||||||
|
|
||||||
For true hot module replacement without manual refresh, see our [Vite migration plan](docs/vite-plan.md) which would provide:
|
For true hot module replacement without manual refresh, see our [Vite migration plan](docs/vite-plan.md) which would provide:
|
||||||
|
|
|
||||||
|
|
@ -52,14 +52,20 @@ struct ControlAgentArmyPageView: View {
|
||||||
.cornerRadius(6)
|
.cornerRadius(6)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
VStack(spacing: 8) {
|
||||||
"Session titles appear in the menu bar and terminal windows.\nUse the dashboard to rename sessions manually, or use the magic wand with Claude/Gemini."
|
Text(
|
||||||
)
|
"Session titles appear in the menu bar and terminal windows.\nUse the dashboard to rename sessions manually, or use the magic wand with Claude/Gemini."
|
||||||
.font(.caption)
|
)
|
||||||
.foregroundColor(.secondary)
|
.font(.caption)
|
||||||
.multilineTextAlignment(.center)
|
.foregroundColor(.secondary)
|
||||||
.frame(maxWidth: 420)
|
.multilineTextAlignment(.center)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.frame(maxWidth: 420)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
|
||||||
|
Link("Learn more", destination: URL(string: "https://steipete.me/posts/command-your-claude-code-army-reloaded")!)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(.vertical, 12)
|
.padding(.vertical, 12)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,122 +20,85 @@ struct ProjectFolderPageView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 24) {
|
VStack(spacing: 30) {
|
||||||
// Title and description
|
VStack(spacing: 16) {
|
||||||
VStack(spacing: 12) {
|
|
||||||
Text("Choose Your Project Folder")
|
Text("Choose Your Project Folder")
|
||||||
.font(.system(size: 24, weight: .semibold))
|
.font(.largeTitle)
|
||||||
.foregroundColor(.primary)
|
.fontWeight(.semibold)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
"Select the folder where you keep your projects. VibeTunnel will use this for quick access and repository discovery."
|
"Select the folder where you keep your projects. VibeTunnel will use this for quick access and repository discovery."
|
||||||
)
|
)
|
||||||
.font(.system(size: 14))
|
.font(.body)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.frame(maxWidth: 400)
|
.frame(maxWidth: 480)
|
||||||
}
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
|
||||||
// Folder picker section
|
// Folder and repository section
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(spacing: 16) {
|
||||||
Text("Project Folder")
|
// Folder picker
|
||||||
.font(.system(size: 13, weight: .medium))
|
HStack {
|
||||||
.foregroundColor(.secondary)
|
Text(selectedPath.isEmpty ? "~/" : selectedPath)
|
||||||
|
.font(.system(size: 13))
|
||||||
|
.foregroundColor(selectedPath.isEmpty ? .secondary : .primary)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.background(Color(NSColor.controlBackgroundColor))
|
||||||
|
.cornerRadius(6)
|
||||||
|
|
||||||
HStack {
|
Button("Choose...") {
|
||||||
Text(selectedPath.isEmpty ? "~/" : selectedPath)
|
showFolderPicker()
|
||||||
.font(.system(size: 13))
|
}
|
||||||
.foregroundColor(selectedPath.isEmpty ? .secondary : .primary)
|
.buttonStyle(.bordered)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding(.horizontal, 10)
|
|
||||||
.padding(.vertical, 6)
|
|
||||||
.background(Color(NSColor.controlBackgroundColor))
|
|
||||||
.cornerRadius(6)
|
|
||||||
|
|
||||||
Button("Choose...") {
|
|
||||||
showFolderPicker()
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.frame(width: 350)
|
||||||
}
|
|
||||||
|
|
||||||
// Repository preview
|
// Repository count
|
||||||
if !selectedPath.isEmpty {
|
if !selectedPath.isEmpty {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
HStack {
|
HStack {
|
||||||
Text("Discovered Repositories")
|
Image(systemName: "folder.badge.gearshape")
|
||||||
.font(.system(size: 12, weight: .medium))
|
.font(.system(size: 12))
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
if isScanning {
|
if isScanning {
|
||||||
ProgressView()
|
Text("Scanning...")
|
||||||
.scaleEffect(0.5)
|
.font(.system(size: 12))
|
||||||
.frame(width: 16, height: 16)
|
.foregroundColor(.secondary)
|
||||||
|
} else if discoveredRepos.isEmpty {
|
||||||
|
Text("No repositories found")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
} else {
|
||||||
|
Text("\(discoveredRepos.count) repositor\(discoveredRepos.count == 1 ? "y" : "ies") found")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundColor(.primary)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
.padding(.horizontal, 10)
|
||||||
ScrollView {
|
.padding(.vertical, 6)
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
if discoveredRepos.isEmpty && !isScanning {
|
|
||||||
Text("No repositories found in this folder")
|
|
||||||
.font(.system(size: 11))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.italic()
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
} else {
|
|
||||||
ForEach(discoveredRepos) { repo in
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "folder.badge.gearshape")
|
|
||||||
.font(.system(size: 11))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Text(repo.name)
|
|
||||||
.font(.system(size: 11))
|
|
||||||
.lineLimit(1)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding(.vertical, 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(maxHeight: 100)
|
|
||||||
.padding(8)
|
|
||||||
.background(Color(NSColor.controlBackgroundColor).opacity(0.5))
|
.background(Color(NSColor.controlBackgroundColor).opacity(0.5))
|
||||||
.cornerRadius(6)
|
.cornerRadius(6)
|
||||||
|
.frame(width: 350)
|
||||||
}
|
}
|
||||||
|
// Tip
|
||||||
|
HStack(alignment: .top, spacing: 6) {
|
||||||
|
Text("You can change this later in Settings → Application → Repository")
|
||||||
|
.font(.system(size: 11))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: 350)
|
||||||
|
.padding(.top, 8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: 400)
|
Spacer()
|
||||||
|
|
||||||
// Tips
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
HStack(alignment: .top, spacing: 8) {
|
|
||||||
Image(systemName: "lightbulb")
|
|
||||||
.font(.system(size: 12))
|
|
||||||
.foregroundColor(.orange)
|
|
||||||
|
|
||||||
Text("You can change this later in Settings → Application → Repository Base Path")
|
|
||||||
.font(.system(size: 11))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack(alignment: .top, spacing: 8) {
|
|
||||||
Image(systemName: "info.circle")
|
|
||||||
.font(.system(size: 12))
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
|
|
||||||
Text("VibeTunnel will scan up to 3 levels deep for Git repositories")
|
|
||||||
.font(.system(size: 11))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(maxWidth: 400)
|
|
||||||
.padding(.top, 12)
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 40)
|
.padding()
|
||||||
.onAppear {
|
.onAppear {
|
||||||
selectedPath = repositoryBasePath
|
selectedPath = repositoryBasePath
|
||||||
if !selectedPath.isEmpty {
|
if !selectedPath.isEmpty {
|
||||||
|
|
@ -190,7 +153,7 @@ struct ProjectFolderPageView: View {
|
||||||
let repos = await findGitRepositories(in: expandedPath, maxDepth: 3)
|
let repos = await findGitRepositories(in: expandedPath, maxDepth: 3)
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
discoveredRepos = repos.prefix(10).map { path in
|
discoveredRepos = repos.map { path in
|
||||||
RepositoryInfo(name: URL(fileURLWithPath: path).lastPathComponent, path: path)
|
RepositoryInfo(name: URL(fileURLWithPath: path).lastPathComponent, path: path)
|
||||||
}
|
}
|
||||||
isScanning = false
|
isScanning = false
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ struct RequestPermissionsPageView: View {
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
.font(.body)
|
.font(.body)
|
||||||
.frame(maxWidth: 250)
|
.frame(maxWidth: 250, alignment: .leading)
|
||||||
.frame(height: 32)
|
.frame(height: 32)
|
||||||
} else {
|
} else {
|
||||||
Button("Grant Automation Permission") {
|
Button("Grant Automation Permission") {
|
||||||
|
|
@ -93,7 +93,7 @@ struct RequestPermissionsPageView: View {
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
.font(.body)
|
.font(.body)
|
||||||
.frame(maxWidth: 250)
|
.frame(maxWidth: 250, alignment: .leading)
|
||||||
.frame(height: 32)
|
.frame(height: 32)
|
||||||
} else {
|
} else {
|
||||||
Button("Grant Accessibility Permission") {
|
Button("Grant Accessibility Permission") {
|
||||||
|
|
@ -113,7 +113,7 @@ struct RequestPermissionsPageView: View {
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
.font(.body)
|
.font(.body)
|
||||||
.frame(maxWidth: 250)
|
.frame(maxWidth: 250, alignment: .leading)
|
||||||
.frame(height: 32)
|
.frame(height: 32)
|
||||||
} else {
|
} else {
|
||||||
Button("Grant Screen Recording Permission") {
|
Button("Grant Screen Recording Permission") {
|
||||||
|
|
|
||||||
|
|
@ -1336,7 +1336,7 @@ export class ScreencapView extends LitElement {
|
||||||
|
|
||||||
// Show overlay when not capturing or waiting to start
|
// Show overlay when not capturing or waiting to start
|
||||||
return html`
|
return html`
|
||||||
<div class.capture-overlay>
|
<div class="capture-overlay">
|
||||||
<div class="status-message ${this.status}">
|
<div class="status-message ${this.status}">
|
||||||
${
|
${
|
||||||
this.status === 'loading'
|
this.status === 'loading'
|
||||||
|
|
|
||||||
|
|
@ -617,10 +617,10 @@ export class UnifiedSettings extends LitElement {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Repository Base Path -->
|
<!-- Repository Base Path -->
|
||||||
<div class="p-4 bg-dark-bg-tertiary rounded-lg border border-dark-border">
|
<div class="p-4 bg-tertiary rounded-lg border border-base">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="text-dark-text font-medium">Repository Base Path</label>
|
<label class="text-primary font-medium">Repository Base Path</label>
|
||||||
<p class="text-dark-text-muted text-xs mt-1">
|
<p class="text-muted text-xs mt-1">
|
||||||
${
|
${
|
||||||
this.isServerConfigured
|
this.isServerConfigured
|
||||||
? 'This path is synced with the VibeTunnel Mac app'
|
? 'This path is synced with the VibeTunnel Mac app'
|
||||||
|
|
@ -646,7 +646,7 @@ export class UnifiedSettings extends LitElement {
|
||||||
${
|
${
|
||||||
this.isServerConfigured
|
this.isServerConfigured
|
||||||
? html`
|
? html`
|
||||||
<div class="flex items-center text-dark-text-muted" title="Synced with Mac app">
|
<div class="flex items-center text-muted" title="Synced with Mac app">
|
||||||
<svg
|
<svg
|
||||||
class="w-5 h-5"
|
class="w-5 h-5"
|
||||||
fill="none"
|
fill="none"
|
||||||
|
|
|
||||||
|
|
@ -82,23 +82,28 @@ class SystemHandler implements MessageHandler {
|
||||||
constructor(private controlUnixHandler: ControlUnixHandler) {}
|
constructor(private controlUnixHandler: ControlUnixHandler) {}
|
||||||
|
|
||||||
async handleMessage(message: ControlMessage): Promise<ControlMessage | null> {
|
async handleMessage(message: ControlMessage): Promise<ControlMessage | null> {
|
||||||
logger.log(`System handler: ${message.action}`);
|
logger.log(`System handler: ${message.action}, type: ${message.type}, id: ${message.id}`);
|
||||||
|
|
||||||
switch (message.action) {
|
switch (message.action) {
|
||||||
case 'repository-path-update': {
|
case 'repository-path-update': {
|
||||||
const payload = message.payload as { path: string };
|
const payload = message.payload as { path: string };
|
||||||
|
logger.log(`Repository path update received: ${JSON.stringify(payload)}`);
|
||||||
|
|
||||||
if (!payload?.path) {
|
if (!payload?.path) {
|
||||||
|
logger.error('Missing path in payload');
|
||||||
return createControlResponse(message, null, 'Missing path in payload');
|
return createControlResponse(message, null, 'Missing path in payload');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Update the server configuration
|
// Update the server configuration
|
||||||
|
logger.log(`Calling updateRepositoryPath with: ${payload.path}`);
|
||||||
const updateSuccess = await this.controlUnixHandler.updateRepositoryPath(payload.path);
|
const updateSuccess = await this.controlUnixHandler.updateRepositoryPath(payload.path);
|
||||||
|
|
||||||
if (updateSuccess) {
|
if (updateSuccess) {
|
||||||
logger.log(`Updated repository path to: ${payload.path}`);
|
logger.log(`Successfully updated repository path to: ${payload.path}`);
|
||||||
return createControlResponse(message, { success: true, path: payload.path });
|
return createControlResponse(message, { success: true, path: payload.path });
|
||||||
} else {
|
} else {
|
||||||
|
logger.error('updateRepositoryPath returned false');
|
||||||
return createControlResponse(message, null, 'Failed to update repository path');
|
return createControlResponse(message, null, 'Failed to update repository path');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -566,7 +571,7 @@ export class ControlUnixHandler {
|
||||||
|
|
||||||
private async handleMacMessage(message: ControlMessage) {
|
private async handleMacMessage(message: ControlMessage) {
|
||||||
logger.log(
|
logger.log(
|
||||||
`Mac message - category: ${message.category}, action: ${message.action}, id: ${message.id}`
|
`Mac message - category: ${message.category}, action: ${message.action}, type: ${message.type}, id: ${message.id}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle ping keep-alive from Mac client
|
// Handle ping keep-alive from Mac client
|
||||||
|
|
@ -576,6 +581,11 @@ export class ControlUnixHandler {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log repository-path-update messages specifically
|
||||||
|
if (message.category === 'system' && message.action === 'repository-path-update') {
|
||||||
|
logger.log(`🔍 Repository path update message details:`, JSON.stringify(message));
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this is a response to a pending request
|
// Check if this is a response to a pending request
|
||||||
if (message.type === 'response' && this.pendingRequests.has(message.id)) {
|
if (message.type === 'response' && this.pendingRequests.has(message.id)) {
|
||||||
const resolver = this.pendingRequests.get(message.id);
|
const resolver = this.pendingRequests.get(message.id);
|
||||||
|
|
@ -587,6 +597,15 @@ export class ControlUnixHandler {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip processing for response messages that aren't pending requests
|
||||||
|
// This prevents response loops where error responses get processed again
|
||||||
|
if (message.type === 'response') {
|
||||||
|
logger.debug(
|
||||||
|
`Ignoring response message that has no pending request: ${message.id}, action: ${message.action}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const handler = this.handlers.get(message.category);
|
const handler = this.handlers.get(message.category);
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
logger.warn(`No handler for category: ${message.category}`);
|
logger.warn(`No handler for category: ${message.category}`);
|
||||||
|
|
@ -722,16 +741,21 @@ export class ControlUnixHandler {
|
||||||
* Update the repository path and notify all connected clients
|
* Update the repository path and notify all connected clients
|
||||||
*/
|
*/
|
||||||
async updateRepositoryPath(path: string): Promise<boolean> {
|
async updateRepositoryPath(path: string): Promise<boolean> {
|
||||||
|
logger.log(`updateRepositoryPath called with path: ${path}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.currentRepositoryPath = path;
|
this.currentRepositoryPath = path;
|
||||||
|
logger.log(`Set currentRepositoryPath to: ${this.currentRepositoryPath}`);
|
||||||
|
|
||||||
// Call the callback to update server configuration and broadcast to web clients
|
// Call the callback to update server configuration and broadcast to web clients
|
||||||
if (this.configUpdateCallback) {
|
if (this.configUpdateCallback) {
|
||||||
|
logger.log('Calling configUpdateCallback...');
|
||||||
this.configUpdateCallback({ repositoryBasePath: path });
|
this.configUpdateCallback({ repositoryBasePath: path });
|
||||||
|
logger.log('configUpdateCallback completed successfully');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.warn('No config update callback set');
|
logger.warn('No config update callback set - is the server initialized?');
|
||||||
return false;
|
return false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to update repository path:', error);
|
logger.error('Failed to update repository path:', error);
|
||||||
|
|
|
||||||
|
|
@ -91,4 +91,95 @@ describe('Control Unix Handler', () => {
|
||||||
expect(mockCallback).toHaveBeenCalledWith({ repositoryBasePath: '/test/path' });
|
expect(mockCallback).toHaveBeenCalledWith({ repositoryBasePath: '/test/path' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Mac Message Handling', () => {
|
||||||
|
it('should process repository-path-update messages from Mac app', async () => {
|
||||||
|
const mockCallback = vi.fn();
|
||||||
|
controlUnixHandler.setConfigUpdateCallback(mockCallback);
|
||||||
|
|
||||||
|
// Simulate Mac sending a repository-path-update message
|
||||||
|
const message = {
|
||||||
|
id: 'mac-msg-123',
|
||||||
|
type: 'request' as const,
|
||||||
|
category: 'system' as const,
|
||||||
|
action: 'repository-path-update',
|
||||||
|
payload: { path: '/Users/test/MacSelectedPath' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process the message through the system handler
|
||||||
|
const systemHandler = (controlUnixHandler as any).handlers.get('system');
|
||||||
|
const response = await systemHandler.handleMessage(message);
|
||||||
|
|
||||||
|
// Verify the update was processed
|
||||||
|
expect(mockCallback).toHaveBeenCalledWith({
|
||||||
|
repositoryBasePath: '/Users/test/MacSelectedPath',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify successful response
|
||||||
|
expect(response).toMatchObject({
|
||||||
|
id: 'mac-msg-123',
|
||||||
|
type: 'response',
|
||||||
|
category: 'system',
|
||||||
|
action: 'repository-path-update',
|
||||||
|
payload: { success: true, path: '/Users/test/MacSelectedPath' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify the path was stored
|
||||||
|
expect(controlUnixHandler.getRepositoryPath()).toBe('/Users/test/MacSelectedPath');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing path in repository-path-update payload', async () => {
|
||||||
|
const mockCallback = vi.fn();
|
||||||
|
controlUnixHandler.setConfigUpdateCallback(mockCallback);
|
||||||
|
|
||||||
|
// Message with missing path
|
||||||
|
const message = {
|
||||||
|
id: 'mac-msg-456',
|
||||||
|
type: 'request' as const,
|
||||||
|
category: 'system' as const,
|
||||||
|
action: 'repository-path-update',
|
||||||
|
payload: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process the message
|
||||||
|
const systemHandler = (controlUnixHandler as any).handlers.get('system');
|
||||||
|
const response = await systemHandler.handleMessage(message);
|
||||||
|
|
||||||
|
// Verify callback was not called
|
||||||
|
expect(mockCallback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Verify error response
|
||||||
|
expect(response).toMatchObject({
|
||||||
|
id: 'mac-msg-456',
|
||||||
|
type: 'response',
|
||||||
|
category: 'system',
|
||||||
|
action: 'repository-path-update',
|
||||||
|
error: 'Missing path in payload',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not process response messages for repository-path-update', async () => {
|
||||||
|
const mockCallback = vi.fn();
|
||||||
|
controlUnixHandler.setConfigUpdateCallback(mockCallback);
|
||||||
|
|
||||||
|
// Response message (should be ignored)
|
||||||
|
const message = {
|
||||||
|
id: 'mac-msg-789',
|
||||||
|
type: 'response' as const,
|
||||||
|
category: 'system' as const,
|
||||||
|
action: 'repository-path-update',
|
||||||
|
payload: { success: true, path: '/some/path' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simulate handleMacMessage behavior - response messages without pending requests are ignored
|
||||||
|
const pendingRequests = (controlUnixHandler as any).pendingRequests;
|
||||||
|
const hasPendingRequest = pendingRequests.has(message.id);
|
||||||
|
|
||||||
|
// Since this is a response without a pending request, it should be ignored
|
||||||
|
expect(hasPendingRequest).toBe(false);
|
||||||
|
|
||||||
|
// Verify callback was not called
|
||||||
|
expect(mockCallback).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue