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 @@ -13,6 +13,7 @@ import { useTranscriptBubbleOverflow } from "../activityRenderClasses/useTranscr
import { compactSummaryTone } from "./CompactToolSummaryRow";
import type { SentMessageLink } from "./messageLinks";
import { SentMessageContextDialog } from "./SentMessageContextDialog";
import { useSentMessageBody } from "./useSentMessageBody";

export function CompactMessageSummary({
args,
Expand Down Expand Up @@ -46,6 +47,7 @@ export function CompactMessageSummary({
timestamp: string;
}) {
const [detailsOpen, setDetailsOpen] = React.useState(false);
const resolvedContent = useSentMessageBody(messageLink, preview);
const variant = useAgentSessionTranscriptVariant();
const { goChannel } = useAppNavigation();
const { openProfilePanel } = useProfilePanel();
Expand Down Expand Up @@ -159,7 +161,7 @@ export function CompactMessageSummary({
>
<Markdown
className={isCompactPreview ? "text-xs leading-4" : "leading-5"}
content={preview || "Message content unavailable."}
content={resolvedContent || "Message content unavailable."}
/>
{hasBubbleOverflow ? (
<span
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import assert from "node:assert/strict";
import test from "node:test";

import {
QueryClient,
QueryClientProvider,
QueryObserver,
} from "@tanstack/react-query";
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";

import {
resolveSentMessageBody,
sentMessageBodyQueryOptions,
shouldFetchSentMessage,
useSentMessageBody,
} from "./useSentMessageBody.ts";

const link = { channelId: "ch-1", messageId: "ev-1" };

test("shouldFetchSentMessage returns true when messageLink present and no inline content", () => {
assert.equal(shouldFetchSentMessage(link, null), true);
});

test("shouldFetchSentMessage returns false when inline content is present", () => {
assert.equal(shouldFetchSentMessage(link, "hello"), false);
});

test("shouldFetchSentMessage returns false when messageLink is null", () => {
assert.equal(shouldFetchSentMessage(null, null), false);
});

test("resolveSentMessageBody returns inline content over fetched content", () => {
assert.equal(
resolveSentMessageBody("inline text", "fetched text"),
"inline text",
);
});

test("resolveSentMessageBody returns fetched content when no inline content", () => {
assert.equal(resolveSentMessageBody(null, "fetched text"), "fetched text");
});

test("resolveSentMessageBody returns null when both inline and fetched are absent", () => {
assert.equal(resolveSentMessageBody(null, undefined), null);
assert.equal(resolveSentMessageBody(null, null), null);
});

// ── Integration: the real useQuery wiring, not just the pure helpers ──────────
//
// `sentMessageBodyQueryOptions` is the exact options object `useSentMessageBody`
// hands to `useQuery`. Driving it through query-core's `QueryObserver` (what
// `useQuery` constructs under the hood) exercises the production fetch/cache
// wiring without a DOM — these tests fail if the hook stops calling the
// injected fetcher, stops keying by `messageId`, or stops gating on
// `enabled`.

test("useSentMessageBody wiring fetches by messageId and resolves fetched content", async () => {
const queryClient = new QueryClient();
let calledWith = null;
const fetchEventById = async (eventId) => {
calledWith = eventId;
return { content: "hello from the event" };
};
const options = sentMessageBodyQueryOptions(link, null, fetchEventById);
const observer = new QueryObserver(queryClient, options);
const unsubscribe = observer.subscribe(() => {});
await new Promise((resolve) => setTimeout(resolve, 20));
unsubscribe();

assert.equal(calledWith, "ev-1");
assert.equal(
resolveSentMessageBody(null, observer.getCurrentResult().data?.content),
"hello from the event",
);
});

test("useSentMessageBody wiring never invokes the fetcher when inline content is present", async () => {
const queryClient = new QueryClient();
let called = false;
const fetchEventById = async () => {
called = true;
return { content: "unused" };
};
const options = sentMessageBodyQueryOptions(
link,
"inline text",
fetchEventById,
);
const observer = new QueryObserver(queryClient, options);
const unsubscribe = observer.subscribe(() => {});
await new Promise((resolve) => setTimeout(resolve, 20));
unsubscribe();

assert.equal(called, false);
});

test("useSentMessageBody wiring leaves data undefined on a rejected fetch (fetch-miss path)", async () => {
const queryClient = new QueryClient();
const fetchEventById = async () => {
throw new Error("relay miss");
};
const options = sentMessageBodyQueryOptions(link, null, fetchEventById);
const observer = new QueryObserver(queryClient, { ...options, retry: false });
const unsubscribe = observer.subscribe(() => {});
await new Promise((resolve) => setTimeout(resolve, 20));
unsubscribe();

assert.equal(observer.getCurrentResult().data, undefined);
});

// ── Render-level coverage ──────────────────────────────────────────────────
//
// Renders the hook through a real QueryClientProvider. Cache is pre-seeded
// via `setQueryData` (equivalent to a settled fetch) for the render
// assertion, since `renderToStaticMarkup` never commits and so never runs
// the query's mount effect — the wiring tests above already prove the
// fetcher is called through the real `useQuery` path.

function SentMessageBodyProbe({ messageLink, inlineContent, fetchEventById }) {
const content = useSentMessageBody(
messageLink,
inlineContent,
fetchEventById,
);
return React.createElement(
"span",
null,
content ?? "Message content unavailable.",
);
}

function renderProbe(queryClient, props) {
return renderToStaticMarkup(
React.createElement(
QueryClientProvider,
{ client: queryClient },
React.createElement(SentMessageBodyProbe, props),
),
);
}

test("CompactMessageSummary renders fetched event content when a cached fetch has resolved", () => {
const queryClient = new QueryClient();
queryClient.setQueryData(["sent-message-body", link.messageId], {
content: "hello from the event",
});
const fetchEventById = async () => {
throw new Error("should not be called — cache hit");
};

const html = renderProbe(queryClient, {
messageLink: link,
inlineContent: null,
fetchEventById,
});

assert.match(html, /hello from the event/);
});

test("CompactMessageSummary renders the placeholder, never a raw flag value, on a fetch miss", () => {
const queryClient = new QueryClient();
const fetchEventById = async () => {
throw new Error("relay miss");
};

const html = renderProbe(queryClient, {
messageLink: link,
inlineContent: null,
fetchEventById,
});

assert.match(html, /Message content unavailable\./);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useQuery } from "@tanstack/react-query";

import { getEventById } from "@/shared/api/tauri";
import type { SentMessageLink } from "./messageLinks";

export function shouldFetchSentMessage(
messageLink: SentMessageLink | null,
inlineContent: string | null,
): boolean {
return messageLink !== null && inlineContent === null;
}

export function resolveSentMessageBody(
inlineContent: string | null,
fetchedContent: string | undefined | null,
): string | null {
if (inlineContent) return inlineContent;
return fetchedContent ?? null;
}

/**
* Builds the exact options object `useSentMessageBody` passes to `useQuery`.
* Extracted so tests can drive the real fetch through query-core's
* `QueryObserver` (what `useQuery` constructs internally) without a DOM.
*/
export function sentMessageBodyQueryOptions(
messageLink: SentMessageLink | null,
inlineContent: string | null,
fetchEventById: (eventId: string) => Promise<{ content: string }>,
) {
return {
queryKey: ["sent-message-body", messageLink?.messageId],
queryFn: () => fetchEventById(messageLink?.messageId ?? ""),
enabled: shouldFetchSentMessage(messageLink, inlineContent),
staleTime: Number.POSITIVE_INFINITY,
};
}

export function useSentMessageBody(
messageLink: SentMessageLink | null,
inlineContent: string | null,
fetchEventById: (
eventId: string,
) => Promise<{ content: string }> = getEventById,
): string | null {
const { data } = useQuery(
sentMessageBodyQueryOptions(messageLink, inlineContent, fetchEventById),
);

return resolveSentMessageBody(inlineContent, data?.content);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import test from "node:test";

import {
classifyTool,
extractSimpleEchoPipeContent,
parseBuzzCliCommand,
tokenizeShellCommand,
} from "./agentSessionToolClassifier.ts";
Expand Down Expand Up @@ -32,25 +31,96 @@ test("tokenizeShellCommand preserves quoted strings and command separators", ()
);
});

test("extractSimpleEchoPipeContent reads the simple echo before a buzz pipe", () => {
const tokens = tokenizeShellCommand(
'echo -n "Done. Eat my shorts." | buzz messages send --content - --channel agents',
test("parseBuzzCliCommand returns null preview for echo-piped stdin sends", () => {
const descriptor = parseBuzzCliCommand(
'echo "Permission wired" | buzz messages send --channel agents --content -',
);
assert.equal(
extractSimpleEchoPipeContent(tokens, tokens.indexOf("buzz")),
"Done. Eat my shorts.",

assert.equal(descriptor?.renderClass, "message");
assert.equal(descriptor?.label, "Send Message");
assert.equal(descriptor?.preview, null);
assert.equal(descriptor?.operation, "messages.send");
});

test("parseBuzzCliCommand returns null preview for printf-piped stdin sends", () => {
const descriptor = parseBuzzCliCommand(
"printf 'hello\\n\\nworld\\n' | buzz messages send --channel a6e0737c-4205-4bcc-9741-2aad800e613f --content -",
);

assert.equal(descriptor?.renderClass, "message");
assert.equal(descriptor?.preview, null);
});

test("parseBuzzCliCommand promotes buzz message sends to message descriptors", () => {
test("parseBuzzCliCommand returns null preview for heredoc/cat stdin sends", () => {
const descriptor = parseBuzzCliCommand(
'echo "Permission wired" | buzz messages send --channel agents --content -',
'buzz messages send --channel some-uuid --content "$(cat /tmp/file)"',
);

assert.equal(descriptor?.renderClass, "message");
assert.equal(descriptor?.label, "Send Message");
assert.equal(descriptor?.preview, "Permission wired");
assert.equal(descriptor?.operation, "messages.send");
assert.equal(descriptor?.preview, null);
});

test("parseBuzzCliCommand returns null preview for --content with embedded command substitution", () => {
const descriptor = parseBuzzCliCommand(
'buzz messages send --channel some-uuid --content "prefix $(cat /tmp/f)"',
);

assert.equal(descriptor?.renderClass, "message");
assert.equal(descriptor?.preview, null);
});

test("parseBuzzCliCommand returns null preview for --content with a bare variable", () => {
const descriptor = parseBuzzCliCommand(
'buzz messages send --channel some-uuid --content "$MESSAGE"',
);

assert.equal(descriptor?.renderClass, "message");
assert.equal(descriptor?.preview, null);
});

test("parseBuzzCliCommand returns null preview for --content with a prefixed variable", () => {
const descriptor = parseBuzzCliCommand(
'buzz messages send --channel some-uuid --content "prefix $MESSAGE"',
);

assert.equal(descriptor?.renderClass, "message");
assert.equal(descriptor?.preview, null);
});

test("parseBuzzCliCommand preserves inline --content for sends", () => {
const descriptor = parseBuzzCliCommand(
'buzz messages send --channel agents --content "Hello from inline"',
);

assert.equal(descriptor?.renderClass, "message");
assert.equal(descriptor?.preview, "Hello from inline");
});

test("parseBuzzCliCommand preserves --content=inline for sends", () => {
const descriptor = parseBuzzCliCommand(
"buzz messages send --channel agents --content=Acknowledged",
);

assert.equal(descriptor?.renderClass, "message");
assert.equal(descriptor?.preview, "Acknowledged");
});

test("parseBuzzCliCommand never surfaces --channel as preview for sends", () => {
const commands = [
"printf 'msg' | buzz messages send --channel my-uuid --content -",
'buzz messages send --channel my-uuid --content "$(cat /tmp/f)"',
"buzz messages send --channel my-uuid --content -",
];

for (const cmd of commands) {
const descriptor = parseBuzzCliCommand(cmd);
assert.equal(descriptor?.renderClass, "message");
assert.notEqual(
descriptor?.preview,
"my-uuid",
`send preview leaked --channel for: ${cmd}`,
);
}
});

test("classifyTool promotes load_skill to skill-read descriptors", () => {
Expand Down
Loading
Loading