Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/ade-cli/src/multiProjectRpcServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import { ProjectScopeRegistry } from "./services/projects/projectScope";

function createRegistry() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "ade-multi-project-rpc-"));
const projectRoot = path.join(root, "project");
fs.mkdirSync(projectRoot, { recursive: true });
const rawProjectRoot = path.join(root, "project");
fs.mkdirSync(rawProjectRoot, { recursive: true });
const projectRoot = fs.realpathSync.native(rawProjectRoot);
const registry = new ProjectRegistry({
adeDir: path.join(root, "home"),
projectsPath: path.join(root, "home", "projects.json"),
Expand Down
2 changes: 1 addition & 1 deletion apps/ade-cli/src/stdioRpcDaemon.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ describe("ade rpc --stdio daemon bridge", () => {
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const cliPath = path.join(packageRoot, "src", "cli.ts");
const adeHome = fs.mkdtempSync(path.join(os.tmpdir(), "ade-stdio-rpc-"));
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ade-stdio-rpc-project-"));
const projectRoot = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), "ade-stdio-rpc-project-")));
const socketPath = path.join(adeHome, "sock", "ade.sock");
const env = {
...process.env,
Expand Down
101 changes: 91 additions & 10 deletions apps/desktop/src/main/services/ipc/registerIpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1765,14 +1765,86 @@ export function registerIpc({
if (getSyncService) return getSyncService() ?? null;
return getCtx().syncService ?? null;
};
const resolveOptionalSyncService = async (): Promise<ReturnType<typeof createSyncService> | null> =>
resolveSyncService
? (await resolveSyncService()) ?? null
: getOptionalSyncService();
const localRuntimeDaemonDisabled = process.env.ADE_DISABLE_LOCAL_RUNTIME_DAEMON === "1";
const allowLocalRuntimeFallback =
process.env.ADE_LOCAL_RUNTIME_FALLBACK === "1" ||
process.env.ADE_DISABLE_LOCAL_RUNTIME_DAEMON === "1";
process.env.ADE_LOCAL_RUNTIME_FALLBACK === "1";

const unavailableSyncSnapshotCreatedAt = new Date().toISOString();
const unavailableSyncPlatform =
process.platform === "darwin"
? "macOS"
: process.platform === "win32"
? "windows"
: process.platform === "linux"
? "linux"
: "unknown";
const unavailableSyncDevice: SyncDeviceRecord = {
deviceId: "local-runtime-disabled",
siteId: "local-runtime-disabled",
name: "Local desktop",
platform: unavailableSyncPlatform,
deviceType: "desktop",
createdAt: unavailableSyncSnapshotCreatedAt,
updatedAt: unavailableSyncSnapshotCreatedAt,
lastSeenAt: unavailableSyncSnapshotCreatedAt,
lastHost: null,
lastPort: null,
tailscaleIp: null,
ipAddresses: [],
metadata: { unavailableReason: "local_runtime_daemon_disabled" },
};
const unavailableSyncSnapshot: SyncRoleSnapshot = {
mode: "standalone",
role: "brain",
localDevice: unavailableSyncDevice,
currentBrain: unavailableSyncDevice,
clusterState: null,
bootstrapToken: null,
pairingPin: null,
pairingPinConfigured: false,
pairingConnectInfo: null,
connectedPeers: [],
tailnetDiscovery: {
state: "disabled",
serviceName: "ade-sync",
servicePort: 0,
target: null,
updatedAt: null,
error: null,
stderr: null,
},
client: {
state: "disconnected",
host: null,
port: null,
connectedAt: null,
lastSeenAt: null,
latencyMs: null,
syncLag: null,
lastRemoteDbVersion: 0,
brainDeviceId: unavailableSyncDevice.deviceId,
hostName: unavailableSyncDevice.name,
error: null,
message: "Sync service unavailable in local runtime disabled mode.",
savedDraft: null,
},
transferReadiness: {
ready: true,
blockers: [],
survivableState: [],
},
survivableStateText: "Sync service unavailable in local runtime disabled mode.",
blockingStateText: "",
};

const buildUnavailableSyncSnapshot = (): SyncRoleSnapshot => unavailableSyncSnapshot;

const requireSyncService = async (): Promise<ReturnType<typeof createSyncService>> => {
const service = resolveSyncService
? await resolveSyncService()
: getOptionalSyncService();
const service = await resolveOptionalSyncService();
if (!service) {
throw new Error("Sync service is not available.");
}
Expand All @@ -1792,6 +1864,7 @@ export function registerIpc({
event: { sender: Electron.WebContents },
action: (pool: LocalRuntimeConnectionPool, rootPath: string) => Promise<T>,
): Promise<T | null> => {
if (localRuntimeDaemonDisabled) return null;
if (!localRuntimeConnectionPool) return null;
const rootPath = getLocalRuntimeRootForEvent(event);
if (!rootPath) return null;
Expand Down Expand Up @@ -4076,7 +4149,12 @@ export function registerIpc({
pool.syncStatusForRoot(rootPath, arg ?? {})
);
if (runtimeStatus) return runtimeStatus;
return await (await requireSyncService()).getStatus({
const service = await resolveOptionalSyncService();
if (!service) {
if (localRuntimeDaemonDisabled) return buildUnavailableSyncSnapshot();
throw new Error("Sync service is not available.");
}
return await service.getStatus({
includeTransferReadiness: arg?.includeTransferReadiness,
forceTransferReadiness: arg?.forceTransferReadiness,
});
Expand Down Expand Up @@ -4204,7 +4282,7 @@ export function registerIpc({
async (event, arg: { laneIds?: string[] | null }): Promise<void> => {
const laneIds = Array.isArray(arg?.laneIds) ? arg.laneIds : [];
const rootPath = getLocalRuntimeRootForEvent(event);
if (localRuntimeConnectionPool && rootPath) {
if (!localRuntimeDaemonDisabled && localRuntimeConnectionPool && rootPath) {
try {
await localRuntimeConnectionPool.callSyncForRoot(rootPath, "sync.setActiveLanePresence", { laneIds });
return;
Expand All @@ -4214,9 +4292,12 @@ export function registerIpc({
}
}
}
await (await requireSyncService()).setActiveLanePresence(
laneIds,
);
const service = await resolveOptionalSyncService();
if (!service) {
if (localRuntimeDaemonDisabled) return;
throw new Error("Sync service is not available.");
}
await service.setActiveLanePresence(laneIds);
},
);

Expand Down
121 changes: 120 additions & 1 deletion apps/desktop/src/main/services/ipc/runtimeBridge.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { IPC } from "../../../shared/ipc";
import type {
OpenProjectBinding,
Expand All @@ -22,14 +22,39 @@ const remoteCallMachineForTargetMock = vi.hoisted(() => vi.fn());
const remoteDisconnectMock = vi.hoisted(() => vi.fn());

vi.mock("electron", () => ({
app: {
getPath: vi.fn(() => "/tmp"),
getVersion: vi.fn(() => "1.0.0"),
isPackaged: false,
},
BrowserWindow: {
fromWebContents: browserWindowFromWebContents,
getAllWindows: browserWindowGetAllWindows,
},
clipboard: {
readImage: vi.fn(() => ({ isEmpty: () => true })),
readText: vi.fn(() => ""),
writeText: vi.fn(),
},
desktopCapturer: {
getSources: vi.fn(async () => []),
},
dialog: {
showOpenDialog: vi.fn(),
},
ipcMain: {
handle: vi.fn((channel: string, handler: (...args: any[]) => unknown) => {
ipcHandlers.set(channel, handler);
}),
on: vi.fn(),
},
nativeImage: {
createFromPath: vi.fn(() => ({ isEmpty: () => true })),
},
shell: {
openExternal: vi.fn(),
openPath: vi.fn(),
showItemInFolder: vi.fn(),
},
}));

Expand Down Expand Up @@ -62,6 +87,7 @@ vi.mock("../git/git", () => ({
}));

import { registerRuntimeBridge } from "./runtimeBridge";
import { registerIpc } from "./registerIpc";

const target: RemoteRuntimeTarget = {
id: "target-1",
Expand Down Expand Up @@ -99,6 +125,7 @@ function localBinding(rootPath = "/repo"): OpenProjectBinding {

describe("registerRuntimeBridge", () => {
beforeEach(() => {
delete process.env.ADE_DISABLE_LOCAL_RUNTIME_DAEMON;
ipcHandlers.clear();
browserWindowFromWebContents.mockReset();
browserWindowGetAllWindows.mockReset().mockReturnValue([]);
Expand Down Expand Up @@ -373,3 +400,95 @@ describe("registerRuntimeBridge", () => {
);
});
});

describe("registerIpc sync bridge", () => {
beforeEach(() => {
delete process.env.ADE_DISABLE_LOCAL_RUNTIME_DAEMON;
ipcHandlers.clear();
browserWindowFromWebContents.mockReset().mockReturnValue({ id: 7 });
});

afterEach(() => {
vi.useRealTimers();
});

it("returns an unavailable sync snapshot without probing local runtime when the daemon is disabled", async () => {
process.env.ADE_DISABLE_LOCAL_RUNTIME_DAEMON = "1";
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-05-11T12:00:00.000Z"));
const localRuntimeConnectionPool = {
syncStatusForRoot: vi.fn(),
callSyncForRoot: vi.fn(),
};
registerIpc({
getCtx: () => ({
syncService: null,
}) as any,
getWindowSession: () => ({
windowId: 7,
project: { rootPath: "/repo", displayName: "Repo" } as any,
binding: localBinding("/repo"),
}),
localRuntimeConnectionPool: localRuntimeConnectionPool as any,
switchProjectFromDialog: vi.fn(),
closeCurrentProject: vi.fn(),
closeProjectByPath: vi.fn(),
globalStatePath: "/tmp/ade-state.json",
});

const snapshot = await ipcHandlers.get(IPC.syncGetStatus)?.(
eventForSender(),
{ includeTransferReadiness: true },
) as any;
vi.setSystemTime(new Date("2026-05-11T12:00:05.000Z"));
const secondSnapshot = await ipcHandlers.get(IPC.syncGetStatus)?.(
eventForSender(),
{ includeTransferReadiness: true },
) as any;

expect(localRuntimeConnectionPool.syncStatusForRoot).not.toHaveBeenCalled();
expect(secondSnapshot).toBe(snapshot);
expect(snapshot.mode).toBe("standalone");
expect(snapshot.localDevice.createdAt).toBe("2026-05-11T12:00:00.000Z");
expect(secondSnapshot.localDevice.updatedAt).toBe(snapshot.localDevice.updatedAt);
expect(secondSnapshot.localDevice.lastSeenAt).toBe(snapshot.localDevice.lastSeenAt);
expect(snapshot.localDevice.metadata).toEqual({
unavailableReason: "local_runtime_daemon_disabled",
});
expect(snapshot.client.message).toBe("Sync service unavailable in local runtime disabled mode.");
});

it("drops active lane presence updates instead of probing unavailable sync services when the daemon is disabled", async () => {
process.env.ADE_DISABLE_LOCAL_RUNTIME_DAEMON = "1";
const localRuntimeConnectionPool = {
callSyncForRoot: vi.fn(),
};
const resolveSyncService = vi.fn(async () => null);
registerIpc({
getCtx: () => ({
syncService: null,
}) as any,
resolveSyncService,
getWindowSession: () => ({
windowId: 7,
project: { rootPath: "/repo", displayName: "Repo" } as any,
binding: localBinding("/repo"),
}),
localRuntimeConnectionPool: localRuntimeConnectionPool as any,
switchProjectFromDialog: vi.fn(),
closeCurrentProject: vi.fn(),
closeProjectByPath: vi.fn(),
globalStatePath: "/tmp/ade-state.json",
});

await expect(
ipcHandlers.get(IPC.syncSetActiveLanePresence)?.(
eventForSender(),
{ laneIds: ["lane-1"] },
),
).resolves.toBeUndefined();

expect(localRuntimeConnectionPool.callSyncForRoot).not.toHaveBeenCalled();
expect(resolveSyncService).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ describe("local runtime connection pool", () => {
expect(fs.existsSync(tsxLoaderPath)).toBe(true);

const adeHome = fs.mkdtempSync(path.join(os.tmpdir(), "ade-local-runtime-"));
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ade-local-runtime-project-"));
const projectRoot = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), "ade-local-runtime-project-")));
const socketPath = path.join(adeHome, "sock", "ade.sock");
const originalEnv = {
ADE_CLI_JS: process.env.ADE_CLI_JS,
Expand Down Expand Up @@ -364,7 +364,7 @@ describe("local runtime connection pool", () => {
expect(fs.existsSync(tsxLoaderPath)).toBe(true);

const adeHome = fs.mkdtempSync(path.join(os.tmpdir(), "ade-local-runtime-version-"));
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ade-local-runtime-version-project-"));
const projectRoot = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), "ade-local-runtime-version-project-")));
const socketPath = path.join(adeHome, "sock", "ade.sock");
const originalEnv = {
ADE_CLI_JS: process.env.ADE_CLI_JS,
Expand Down Expand Up @@ -450,7 +450,7 @@ describe("local runtime connection pool", () => {
expect(fs.existsSync(tsxLoaderPath)).toBe(true);

const adeHome = fs.mkdtempSync(path.join(os.tmpdir(), "ade-local-runtime-build-"));
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ade-local-runtime-build-project-"));
const projectRoot = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), "ade-local-runtime-build-project-")));
const socketPath = path.join(adeHome, "sock", "ade.sock");
const originalEnv = {
ADE_CLI_JS: process.env.ADE_CLI_JS,
Expand Down
Loading