fix(voice-call): share runtime stop completion

This commit is contained in:
Vincent Koc
2026-07-31 02:42:39 +08:00
parent 6758c61135
commit e29f3a2e62
2 changed files with 42 additions and 19 deletions

View File

@@ -282,6 +282,13 @@ describe("createVoiceCallRuntime lifecycle", () => {
it("returns an idempotent stop handler", async () => {
const tunnelStop = vi.fn().mockResolvedValue(undefined);
let releaseWebhookStop: (() => void) | undefined;
mocks.webhookStop.mockImplementation(
() =>
new Promise<void>((resolve) => {
releaseWebhookStop = resolve;
}),
);
mocks.startTunnel.mockResolvedValue({
publicUrl: "https://public.example/voice/webhook",
provider: "ngrok",
@@ -294,8 +301,22 @@ describe("createVoiceCallRuntime lifecycle", () => {
agentRuntime: {} as never,
});
await runtime.stop();
await runtime.stop();
const firstStop = runtime.stop();
const secondStop = runtime.stop();
let stopped = false;
void secondStop.then(() => {
stopped = true;
});
expect(secondStop).toBe(firstStop);
await vi.waitFor(() => {
expect(mocks.webhookStop).toHaveBeenCalledTimes(1);
});
expect(stopped).toBe(false);
releaseWebhookStop?.();
await firstStop;
expect(stopped).toBe(true);
expect(tunnelStop).toHaveBeenCalledTimes(1);
expect(mocks.cleanupTailscaleExposure).toHaveBeenCalledTimes(1);

View File

@@ -131,7 +131,7 @@ function createRuntimeResourceLifecycle(params: {
stop: (opts?: { suppressErrors?: boolean }) => Promise<void>;
} {
let tunnelResult: TunnelResult | null = null;
let stopped = false;
let stopPromise: Promise<void> | null = null;
const runStep = async (step: () => Promise<void>, suppressErrors: boolean) => {
if (suppressErrors) {
@@ -145,23 +145,25 @@ function createRuntimeResourceLifecycle(params: {
setTunnelResult: (result) => {
tunnelResult = result;
},
stop: async (opts) => {
if (stopped) {
return;
stop: (opts) => {
if (stopPromise) {
return stopPromise;
}
stopped = true;
const suppressErrors = opts?.suppressErrors ?? false;
await runStep(async () => {
if (tunnelResult) {
await tunnelResult.stop();
}
}, suppressErrors);
await runStep(async () => {
await cleanupTailscaleExposure(params.config);
}, suppressErrors);
await runStep(async () => {
await params.webhookServer.stop();
}, suppressErrors);
stopPromise = (async () => {
await runStep(async () => {
if (tunnelResult) {
await tunnelResult.stop();
}
}, suppressErrors);
await runStep(async () => {
await cleanupTailscaleExposure(params.config);
}, suppressErrors);
await runStep(async () => {
await params.webhookServer.stop();
}, suppressErrors);
})();
return stopPromise;
},
};
}
@@ -556,7 +558,7 @@ export async function createVoiceCallRuntime(params: {
await manager.initialize(provider, webhookUrl);
const stop = async () => await lifecycle.stop();
const stop = () => lifecycle.stop();
log.info("[voice-call] Runtime initialized");
log.info(`[voice-call] Webhook URL: ${webhookUrl}`);