fix(dankvoice): pin thinking off in rewrite, fix silent and timeout modes

Refs #32

The humanizer's raw fetch to frank-zealot omitted
chat_template_kwargs.enable_thinking: false, so the Qwen35 hybrid thought
instead of answering (6.9 s reasoning_content, empty content - timing out
the 6 s abort; or answering "" and muting the unit). Pin the kwarg, treat
empty-content-with-reasoning as failure over silence, and stop the menu's
test phrase from blocking its handler. Three red-captured regressions;
26/26 green, verified red against HEAD.
This commit is contained in:
Frank committed 2026-10-19 10:46:06 -07:00
1 parent 8f5a229cc6
commit 96553979fb
4 files changed
+137 -21

No files matched your search

+64 -11
View File
@@ -14,8 +14,9 @@
# (player fixture), DV_PWPLAY (stub aliased as pw-play), DV_WPCTL (the sink
# name the fixture wpctl reports), DV_MODELS (models.yml fixture),
# DANKVOICE_CONFIG (settings file), DV_PLAYER_BIN (player command override),
# DV_SELECT (pipe-separated answers fed to ctx.ui.select in order), DV_MODE
# (the harness mode reported to the extension).
# DV_MODE (the harness mode reported to the extension), DV_REWRITE (fixture
# behavior for the humanizer route: prose | echo | reasoning | stall, with
# DV_STALL_MS).
setup() {
REPO="$(cd "$BATS_TEST_DIRNAME/.." && pwd)"
@@ -31,8 +32,10 @@ setup() {
cat > "$DV_STUB" <<'SH'
#!/usr/bin/env bash
printf '%s\n' "$*" >> "$DV_STUB_LOG"
if [ -n "${DV_TIMING_LOG:-}" ]; then printf 'START %s\n' "$(date +%s%3N)" >> "$DV_TIMING_LOG"; fi
for arg in "$@"; do case "$arg" in *.wav) cp "$arg" "$DV_COPY" ;; esac; done
sleep 0.1
sleep "${DV_HOLD:-0.1}"
if [ -n "${DV_TIMING_LOG:-}" ]; then printf 'END %s\n' "$(date +%s%3N)" >> "$DV_TIMING_LOG"; fi
SH
chmod +x "$DV_STUB"
@@ -84,14 +87,21 @@ import { dirname } from "node:path";
const scenario = process.argv[2] ?? "speak";
const AUDIO = Buffer.from([0x52, 0x49, 0x46, 0x46, 8, 0, 0, 0, 0x57, 0x41, 0x56, 0x45, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
const received: Array<{ url: string | undefined; body: string }> = [];
const started = Date.now();
interface Call {
url: string | undefined;
body: string;
at: number;
}
const received: Call[] = [];
const server = createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (chunk: Buffer) => chunks.push(chunk));
req.on("end", () => {
const at = Date.now() - started;
const body = Buffer.concat(chunks).toString("utf8");
received.push({ url: req.url, body });
received.push({ url: req.url, body, at });
if (req.url?.endsWith("/audio/voices")) {
res.setHeader("content-type", "application/json");
res.end(JSON.stringify(["narrator1", "narrator2"]));
@@ -103,11 +113,18 @@ const server = createServer((req, res) => {
return;
}
if (process.env.DV_REWRITE && req.url?.endsWith("/chat/completions")) {
res.setHeader("content-type", "application/json");
const spoken = process.env.DV_REWRITE === "echo"
? String(JSON.parse(body).messages?.[1]?.content ?? "")
: "Spoken, humanized words.";
res.end(JSON.stringify({ choices: [{ message: { content: spoken } }] }));
const respond = () => {
res.setHeader("content-type", "application/json");
const spoken = process.env.DV_REWRITE === "echo"
? String(JSON.parse(body).messages?.[1]?.content ?? "")
: "Spoken, humanized words.";
const verdict = process.env.DV_REWRITE === "reasoning"
? { choices: [{ message: { content: "", reasoning_content: "Long internal deliberation that never reaches the reply field." } }] }
: { choices: [{ message: { content: spoken } }] };
res.end(JSON.stringify(verdict));
};
if (process.env.DV_REWRITE === "stall") setTimeout(respond, Number(process.env.DV_STALL_MS ?? 200));
else respond();
return;
}
res.statusCode = 404;
@@ -219,6 +236,12 @@ if (!say) {
await say.handler("on", ctx);
await agentEnd?.({ messages: [{ role: "assistant", content: [{ type: "thinking", thinking: "Considering the wiring." }, { type: "text", text: "The answer is nine." }] }] }, ctx);
break;
case "testphrase": {
const startedAt = Date.now();
await say.handler("menu", ctx);
console.log(`HANDLER_MS ${Date.now() - startedAt}`);
break;
}
case "narration":
await say.handler("on", ctx);
await agentEnd?.({ messages: [{ role: "assistant", content: [{ type: "thinking", thinking: "Here is the result.", narration: true }] }] }, ctx);
@@ -239,7 +262,11 @@ for (const call of received) {
}
return "-";
};
console.log(`CALL ${call.url} model=${field("model")} format=${field("response_format")} voice=${field("voice")} input=${field("input")}`);
const kwargs = parsed && typeof parsed === "object" && "chat_template_kwargs" in parsed
? (parsed as { chat_template_kwargs?: { enable_thinking?: unknown } }).chat_template_kwargs
: undefined;
const think = kwargs && typeof kwargs === "object" && "enable_thinking" in kwargs ? String(kwargs.enable_thinking) : "absent";
console.log(`CALL ${call.url} model=${field("model")} format=${field("response_format")} voice=${field("voice")} input=${field("input")}${call.url?.endsWith("/chat/completions") ? ` thinkoff=${think}` : ""}`);
}
console.log(`CALLS ${received.filter((call) => call.url?.endsWith("/audio/speech")).length}`);
const log = process.env.DV_STUB_LOG && existsSync(process.env.DV_STUB_LOG) ? readFileSync(process.env.DV_STUB_LOG, "utf8").trim() : "";
@@ -417,3 +444,29 @@ say_run() {
DV_PLAYER_BIN="$DV_PWPLAY" say_run speak
[[ "$output" == *"PLAYER --target $DV_WPCTL"* ]]
}
@test "the rewrite asks the humanizer with thinking explicitly off" {
DV_REWRITE=prose say_run rewrite
[[ "$output" == *"input=- thinkoff=false"* ]]
}
@test "an answer hiding in reasoning_content speaks the mechanical text, not silence" {
DV_REWRITE=reasoning say_run rewrite
[[ "$output" == *"input=The deploy failed because a secret was missing."* ]]
[[ "$output" == *"CALLS 1"* ]]
[[ "$output" == *"PLAYS 1"* ]]
}
@test "a rewrite slower than a breath but under the timeout still speaks" {
DV_REWRITE=stall DV_STALL_MS=300 say_run rewrite
[[ "$output" == *"input=Spoken, humanized words."* ]]
[[ "$output" == *"CALLS 1"* ]]
}
@test "the menu test phrase speaks in the background instead of blocking the menu" {
DV_REWRITE=stall DV_STALL_MS=1500 say_run testphrase "Speak a test phrase"
[[ "$output" == *"NOTIFY info dankvoice: speaking the test phrase"* ]]
local ms
ms=$(grep -o 'HANDLER_MS [0-9]*' <<<"$output" | cut -d' ' -f2)
[ -n "$ms" ] && [ "$ms" -lt 1200 ]
}