惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

大猫的无限游戏
大猫的无限游戏
云风的 BLOG
云风的 BLOG
小众软件
小众软件
V
V2EX
博客园 - Franky
博客园 - 司徒正美
Apple Machine Learning Research
Apple Machine Learning Research
量子位
博客园 - 【当耐特】
雷峰网
雷峰网
WordPress大学
WordPress大学
Jina AI
Jina AI
Google DeepMind News
Google DeepMind News
N
Netflix TechBlog - Medium
爱范儿
爱范儿
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
阮一峰的网络日志
阮一峰的网络日志
IT之家
IT之家
Blog — PlanetScale
Blog — PlanetScale
Hugging Face - Blog
Hugging Face - Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
V
Visual Studio Blog
Microsoft Security Blog
Microsoft Security Blog

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
feat(gateway): add VoiceClaw realtime brain endpoint (#70...
yagudaev · 2026-04-25 · via Recent Commits to openclaw:main

@@ -0,0 +1,138 @@

1+

import { afterEach, describe, expect, it, vi } from "vitest";

2+

import { VoiceClawGeminiLiveAdapter } from "./gemini-live.js";

3+4+

describe("VoiceClawGeminiLiveAdapter watchdog", () => {

5+

afterEach(() => {

6+

vi.useRealTimers();

7+

});

8+9+

it("stays paused while async OpenClaw tool work is still running", () => {

10+

vi.useFakeTimers();

11+

const adapter = new VoiceClawGeminiLiveAdapter();

12+

const internals = adapter as unknown as {

13+

watchdogEnabled: boolean;

14+

resetWatchdog: () => void;

15+

sendUpstream: (message: Record<string, unknown>) => void;

16+

};

17+

const sendUpstream = vi.fn();

18+

internals.watchdogEnabled = true;

19+

internals.sendUpstream = sendUpstream;

20+21+

adapter.beginAsyncToolCall("call-1");

22+

internals.resetWatchdog();

23+

vi.advanceTimersByTime(21_000);

24+25+

expect(sendUpstream).not.toHaveBeenCalled();

26+27+

adapter.finishAsyncToolCall("call-1");

28+

vi.advanceTimersByTime(20_000);

29+30+

expect(sendUpstream).toHaveBeenCalledOnce();

31+

expect(sendUpstream.mock.calls[0][0]).toMatchObject({

32+

realtimeInput: {

33+

text: expect.stringContaining("user has been silent"),

34+

},

35+

});

36+

});

37+

});

38+39+

describe("VoiceClawGeminiLiveAdapter tool cancellation", () => {

40+

afterEach(() => {

41+

vi.useRealTimers();

42+

});

43+44+

it("releases the watchdog hold when Gemini cancels an already-acked async tool", () => {

45+

vi.useFakeTimers();

46+

const adapter = new VoiceClawGeminiLiveAdapter();

47+

const events: unknown[] = [];

48+

const sendUpstream = vi.fn();

49+

const internals = adapter as unknown as {

50+

asyncToolCallIds: Set<string>;

51+

handleServerMessage: (message: Record<string, unknown>) => void;

52+

sendToClient: (event: unknown) => void;

53+

sendUpstream: (message: Record<string, unknown>) => void;

54+

watchdogEnabled: boolean;

55+

};

56+

internals.sendToClient = (event) => events.push(event);

57+

internals.sendUpstream = sendUpstream;

58+

internals.watchdogEnabled = true;

59+60+

adapter.beginAsyncToolCall("call-1");

61+

internals.handleServerMessage({ toolCallCancellation: { ids: ["call-1"] } });

62+

vi.advanceTimersByTime(20_000);

63+64+

expect(events).toContainEqual({ type: "tool.cancelled", callIds: ["call-1"] });

65+

expect(internals.asyncToolCallIds.size).toBe(0);

66+

expect(sendUpstream).toHaveBeenCalledOnce();

67+

});

68+69+

it("cancels async OpenClaw tool work when Gemini closes after the working ack", () => {

70+

const adapter = new VoiceClawGeminiLiveAdapter();

71+

const events: unknown[] = [];

72+

const internals = adapter as unknown as {

73+

asyncToolCallIds: Set<string>;

74+

handleUpstreamClose: (code: number) => void;

75+

sendToClient: (event: unknown) => void;

76+

};

77+

internals.sendToClient = (event) => events.push(event);

78+79+

adapter.beginAsyncToolCall("call-1");

80+

internals.handleUpstreamClose(1000);

81+82+

expect(events).toContainEqual({ type: "tool.cancelled", callIds: ["call-1"] });

83+

expect(events).toContainEqual({

84+

type: "error",

85+

message: "Gemini Live closed while a tool call was in flight",

86+

code: 502,

87+

});

88+

expect(internals.asyncToolCallIds.size).toBe(0);

89+

});

90+91+

it("defers goAway rotation until async OpenClaw tool work finishes", () => {

92+

const adapter = new VoiceClawGeminiLiveAdapter();

93+

const reconnect = vi.fn();

94+

const internals = adapter as unknown as {

95+

currentlyResumable: boolean;

96+

handleServerMessage: (message: Record<string, unknown>) => void;

97+

reconnect: (reason: string) => void;

98+

resumptionHandle: string;

99+

rotateAfterToolCalls: boolean;

100+

};

101+

internals.currentlyResumable = true;

102+

internals.resumptionHandle = "resume-1";

103+

internals.reconnect = reconnect;

104+105+

adapter.beginAsyncToolCall("call-1");

106+

internals.handleServerMessage({ goAway: {} });

107+108+

expect(reconnect).not.toHaveBeenCalled();

109+

expect(internals.rotateAfterToolCalls).toBe(true);

110+111+

adapter.finishAsyncToolCall("call-1");

112+113+

expect(internals.rotateAfterToolCalls).toBe(false);

114+

expect(reconnect).toHaveBeenCalledWith("deferred goAway");

115+

});

116+117+

it("rotates after goAway when Gemini cancels the deferred async tool", () => {

118+

const adapter = new VoiceClawGeminiLiveAdapter();

119+

const reconnect = vi.fn();

120+

const internals = adapter as unknown as {

121+

currentlyResumable: boolean;

122+

handleServerMessage: (message: Record<string, unknown>) => void;

123+

reconnect: (reason: string) => void;

124+

resumptionHandle: string;

125+

rotateAfterToolCalls: boolean;

126+

};

127+

internals.currentlyResumable = true;

128+

internals.resumptionHandle = "resume-1";

129+

internals.reconnect = reconnect;

130+131+

adapter.beginAsyncToolCall("call-1");

132+

internals.handleServerMessage({ goAway: {} });

133+

internals.handleServerMessage({ toolCallCancellation: { ids: ["call-1"] } });

134+135+

expect(internals.rotateAfterToolCalls).toBe(false);

136+

expect(reconnect).toHaveBeenCalledWith("deferred goAway");

137+

});

138+

});