fix: harden directive handling

This commit is contained in:
Peter Steinberger
2025-12-05 23:43:14 +00:00
parent a2604a36bc
commit 28e0dbc02f
3 changed files with 51 additions and 8 deletions

View File

@@ -1,8 +1,12 @@
import { describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import * as tauRpc from "../process/tau-rpc.js";
import { getReplyFromConfig, extractVerboseDirective, extractThinkDirective } from "./reply.js";
describe("directive parsing", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("ignores verbose directive inside URL", () => {
const body = "https://x.com/verioussmith/status/1997066835133669687";
const res = extractVerboseDirective(body);
@@ -10,6 +14,13 @@ describe("directive parsing", () => {
expect(res.cleaned).toBe(body);
});
it("ignores typoed /verioussmith", () => {
const body = "/verioussmith";
const res = extractVerboseDirective(body);
expect(res.hasDirective).toBe(false);
expect(res.cleaned).toBe(body.trim());
});
it("ignores think directive inside URL", () => {
const body = "see https://example.com/path/thinkstuff";
const res = extractThinkDirective(body);
@@ -61,4 +72,33 @@ describe("directive parsing", () => {
expect(text).toBe("done");
expect(rpcMock).toHaveBeenCalledOnce();
});
it("acks verbose directive immediately with system marker", async () => {
const rpcMock = vi.spyOn(tauRpc, "runPiRpc").mockResolvedValue({
stdout: "",
stderr: "",
code: 0,
signal: null,
killed: false,
});
const res = await getReplyFromConfig(
{ Body: "/verbose on", From: "+1222", To: "+1222" },
{},
{
inbound: {
reply: {
mode: "command",
command: ["pi", "{{Body}}"],
agent: { kind: "pi" },
session: {},
},
},
},
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toMatch(/^⚙️ Verbose logging enabled\./);
expect(rpcMock).not.toHaveBeenCalled();
});
});

View File

@@ -35,6 +35,7 @@ export type { GetReplyOptions, ReplyPayload } from "./types.js";
const ABORT_TRIGGERS = new Set(["stop", "esc", "abort", "wait", "exit"]);
const ABORT_MEMORY = new Map<string, boolean>();
const SYSTEM_MARK = "⚙️";
export function extractThinkDirective(body?: string): {
cleaned: string;
@@ -67,8 +68,8 @@ export function extractVerboseDirective(body?: string): {
hasDirective: boolean;
} {
if (!body) return { cleaned: "", hasDirective: false };
// Require start or whitespace before "/verbose" to avoid matching URLs like /verioussmith.
const match = body.match(/(?:^|\s)\/(?:verbose|v)\s*:?\s*([a-zA-Z-]+)\b/i);
// Require start or whitespace before "/verbose" and reject "/ver*" typos.
const match = body.match(/(?:^|\s)\/v(?:erbose)?\b\s*:?\s*([a-zA-Z-]+)\b/i);
const verboseLevel = normalizeVerboseLevel(match?.[1]);
const cleaned = match
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
@@ -364,7 +365,7 @@ export async function getReplyFromConfig(
if (!inlineThink) {
cleanupTyping();
return {
text: `Unrecognized thinking level "${rawThinkLevel ?? ""}". Valid levels: off, minimal, low, medium, high.`,
text: `${SYSTEM_MARK} Unrecognized thinking level "${rawThinkLevel ?? ""}". Valid levels: off, minimal, low, medium, high.`,
};
}
if (sessionEntry && sessionStore && sessionKey) {
@@ -413,7 +414,7 @@ export async function getReplyFromConfig(
);
}
}
const ack = parts.join(" ");
const ack = `${SYSTEM_MARK} ${parts.join(" ")}`;
cleanupTyping();
return { text: ack };
}
@@ -430,7 +431,7 @@ export async function getReplyFromConfig(
if (!inlineVerbose) {
cleanupTyping();
return {
text: `Unrecognized verbose level "${rawVerboseLevel ?? ""}". Valid levels: off, on.`,
text: `${SYSTEM_MARK} Unrecognized verbose level "${rawVerboseLevel ?? ""}". Valid levels: off, on.`,
};
}
if (sessionEntry && sessionStore && sessionKey) {
@@ -445,8 +446,8 @@ export async function getReplyFromConfig(
}
const ack =
inlineVerbose === "off"
? "Verbose logging disabled."
: "Verbose logging enabled.";
? `${SYSTEM_MARK} Verbose logging disabled.`
: `${SYSTEM_MARK} Verbose logging enabled.`;
cleanupTyping();
return { text: ack };
}