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
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,166 @@ describe("local runtime connection pool", () => {
expect(call).toHaveBeenCalledTimes(2);
});

it("retries project registration when the cached runtime connection drops before a read action", async () => {
const dropped = new Error("Remote ADE service connection closed.");
const logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
const rootPath = path.resolve("/repo");
const project = {
projectId: "project-1",
rootPath,
displayName: "repo",
addedAt: 1,
lastOpenedAt: 1,
gitOriginUrl: null,
};
const firstClient = {
call: vi.fn().mockRejectedValue(dropped),
close: vi.fn(),
isClosed: vi.fn(() => false),
};
const secondClient = {
call: vi.fn(async (method: string) => {
if (method === "projects.add") return project;
if (method === "ade/actions/call") {
return {
domain: "lane",
action: "list",
result: [{ id: "lane-1" }],
statusHints: {},
};
}
throw new Error(`Unexpected method ${method}`);
}),
close: vi.fn(),
isClosed: vi.fn(() => false),
};
const firstEntry = {
client: firstClient,
child: null,
socketPath: "/tmp/ade-stale.sock",
};
const secondEntry = {
client: secondClient,
child: null,
socketPath: "/tmp/ade-fresh.sock",
};
const createConnection = vi.fn<[], Promise<unknown>>()
.mockResolvedValueOnce(firstEntry)
.mockResolvedValueOnce(secondEntry);
const pool = new LocalRuntimeConnectionPool("1.2.3", logger as never);
(pool as unknown as { createConnection: () => Promise<unknown> }).createConnection = createConnection;

await expect(pool.callActionForRoot(rootPath, {
domain: "lane",
action: "list",
args: {},
})).resolves.toEqual({
domain: "lane",
action: "list",
result: [{ id: "lane-1" }],
statusHints: {},
});

expect(createConnection).toHaveBeenCalledTimes(2);
expect(firstClient.call).toHaveBeenCalledWith(
"projects.add",
{ rootPath },
{ timeoutMs: expect.any(Number) },
);
expect(firstClient.close).toHaveBeenCalledTimes(1);
expect(secondClient.call).toHaveBeenNthCalledWith(
1,
"projects.add",
{ rootPath },
{ timeoutMs: expect.any(Number) },
);
expect(secondClient.call).toHaveBeenNthCalledWith(
2,
"ade/actions/call",
{
projectId: "project-1",
name: "run_ade_action",
arguments: {
domain: "lane",
action: "list",
args: {},
},
},
{ timeoutMs: 30_000 },
);
expect(logger.warn).toHaveBeenCalledWith("local_runtime.ensure_project_connection_dropped", {
rootPath,
socketPath: "/tmp/ade-stale.sock",
attempt: 1,
willRetry: true,
error: dropped.message,
});
});

it("reconnects before project registration when the runtime client is already closed", async () => {
const logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
const rootPath = path.resolve("/repo");
const project = {
projectId: "project-1",
rootPath,
displayName: "repo",
addedAt: 1,
lastOpenedAt: 1,
gitOriginUrl: null,
};
const firstClient = {
call: vi.fn(),
close: vi.fn(),
isClosed: vi.fn(() => true),
};
const secondClient = {
call: vi.fn().mockResolvedValue(project),
close: vi.fn(),
isClosed: vi.fn(() => false),
};
const createConnection = vi.fn<[], Promise<unknown>>()
.mockResolvedValueOnce({
client: firstClient,
child: null,
socketPath: "/tmp/ade-closed.sock",
})
.mockResolvedValueOnce({
client: secondClient,
child: null,
socketPath: "/tmp/ade-open.sock",
});
const pool = new LocalRuntimeConnectionPool("1.2.3", logger as never);
(pool as unknown as { createConnection: () => Promise<unknown> }).createConnection = createConnection;

await expect(pool.ensureProject(rootPath)).resolves.toEqual(project);

expect(createConnection).toHaveBeenCalledTimes(2);
expect(firstClient.call).not.toHaveBeenCalled();
expect(firstClient.close).toHaveBeenCalledTimes(1);
expect(secondClient.call).toHaveBeenCalledWith(
"projects.add",
{ rootPath },
{ timeoutMs: expect.any(Number) },
);
expect(logger.warn).toHaveBeenCalledWith("local_runtime.ensure_project_connection_dropped", {
rootPath,
socketPath: "/tmp/ade-closed.sock",
attempt: 1,
willRetry: true,
error: "Remote ADE service connection closed.",
});
});

it("terminates an app-owned fallback runtime when disposed", async () => {
vi.useFakeTimers();
const child = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -653,16 +653,57 @@ export class LocalRuntimeConnectionPool {
const normalizedRoot = path.resolve(rootPath);
const cached = this.projectsByRoot.get(normalizedRoot);
if (cached) return cached;
const entry = await this.connect();
const project = await entry.client.call(
"projects.add",
{ rootPath: normalizedRoot },
{ timeoutMs: LOCAL_RUNTIME_PROJECT_TIMEOUT_MS },
);
const record = coerceProjects([project])[0];
if (!record) throw new Error("Local ADE service did not return a project record.");
this.projectsByRoot.set(normalizedRoot, record);
return record;

let lastError: Error | null = null;
for (let attempt = 1; attempt <= 2; attempt++) {
const entry = await this.connect();
if (entry.client.isClosed()) {
const error = new Error("Remote ADE service connection closed.");
this.logger.warn("local_runtime.ensure_project_connection_dropped", {
rootPath: normalizedRoot,
socketPath: entry.socketPath,
attempt,
willRetry: attempt < 2,
error: error.message,
});
this.resetActiveConnection(entry);
lastError = error;
if (attempt < 2) continue;
throw error;
}

try {
const project = await entry.client.call(
"projects.add",
{ rootPath: normalizedRoot },
{ timeoutMs: LOCAL_RUNTIME_PROJECT_TIMEOUT_MS },
);
const record = coerceProjects([project])[0];
if (!record) throw new Error("Local ADE service did not return a project record.");
this.projectsByRoot.set(normalizedRoot, record);
return record;
} catch (error) {
const projectError = error instanceof Error ? error : new Error(String(error));
if (!isLocalRuntimeConnectionDropped(projectError)) {
throw projectError;
}
this.logger.warn("local_runtime.ensure_project_connection_dropped", {
rootPath: normalizedRoot,
socketPath: entry.socketPath,
attempt,
willRetry: attempt < 2,
error: projectError.message,
});
this.resetActiveConnection(entry);
lastError = projectError;
if (attempt < 2) continue;
throw projectError;
}
}

// Unreachable: the loop always returns or throws on the final attempt.
// Required here only for TypeScript's control-flow narrowing.
throw lastError ?? new Error("Local ADE service did not return a project record.");
Comment thread
arul28 marked this conversation as resolved.
}

async projects(): Promise<RemoteRuntimeProjectRecord[]> {
Expand Down
Loading