mirror of
https://github.com/block/buzz.git
synced 2026-11-08 02:11:36 +00:00
Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
399 lines
15 KiB
JavaScript
399 lines
15 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { extractConfigNudge, stripConfigNudgeSentinel } from "./configNudge.ts";
|
|
|
|
// Helper: build a fenced sentinel body containing the given payload.
|
|
function withSentinel(prose, payload) {
|
|
return `${prose}\n\n\`\`\`buzz:config-nudge\n${JSON.stringify(payload)}\n\`\`\``;
|
|
}
|
|
|
|
const FIZZ_PUBKEY = "aabbccddeeff0011";
|
|
const ATLAS_PUBKEY = "ddeeff00112233aa";
|
|
const CODEX_PUBKEY = "112233aabbccddee";
|
|
|
|
// ── extractConfigNudge ────────────────────────────────────────────────────────
|
|
|
|
test("extractConfigNudge returns null when no sentinel present", () => {
|
|
assert.equal(
|
|
extractConfigNudge("**Fizz** needs configuration before it can respond."),
|
|
null,
|
|
);
|
|
});
|
|
|
|
test("extractConfigNudge returns null for empty string", () => {
|
|
assert.equal(extractConfigNudge(""), null);
|
|
});
|
|
|
|
test("extractConfigNudge parses env_key requirement", () => {
|
|
const payload = {
|
|
agent_name: "Fizz",
|
|
agent_pubkey: FIZZ_PUBKEY,
|
|
requirements: [{ surface: "env_key", key: "ANTHROPIC_API_KEY" }],
|
|
};
|
|
const content = [
|
|
"**Fizz** needs configuration before it can respond:",
|
|
"- set `ANTHROPIC_API_KEY` in Edit Agent → Environment variables",
|
|
"",
|
|
"Open Edit Agent in the Buzz app to set these.",
|
|
"",
|
|
"```buzz:config-nudge",
|
|
JSON.stringify(payload),
|
|
"```",
|
|
].join("\n");
|
|
|
|
assert.deepEqual(extractConfigNudge(content), payload);
|
|
});
|
|
|
|
test("extractConfigNudge parses normalized_field requirement", () => {
|
|
const payload = {
|
|
agent_name: "Atlas",
|
|
agent_pubkey: ATLAS_PUBKEY,
|
|
requirements: [{ surface: "normalized_field", field: "provider" }],
|
|
};
|
|
assert.deepEqual(extractConfigNudge(withSentinel("prose", payload)), payload);
|
|
});
|
|
|
|
test("extractConfigNudge parses cli_login requirement", () => {
|
|
const payload = {
|
|
agent_name: "Codex",
|
|
agent_pubkey: CODEX_PUBKEY,
|
|
requirements: [
|
|
{
|
|
surface: "cli_login",
|
|
probe_args: ["codex", "login", "status"],
|
|
setup_copy: "run `codex login`",
|
|
availability: "available",
|
|
},
|
|
],
|
|
};
|
|
assert.deepEqual(extractConfigNudge(withSentinel("prose", payload)), payload);
|
|
});
|
|
|
|
test("extractConfigNudge returns null for cli_login without availability", () => {
|
|
// availability is required — old-format payloads (no availability field)
|
|
// must not parse so stale nudge JSON from before the Doctor-CTA update
|
|
// does not silently render a broken card.
|
|
const payload = {
|
|
agent_name: "Codex",
|
|
agent_pubkey: CODEX_PUBKEY,
|
|
requirements: [
|
|
{
|
|
surface: "cli_login",
|
|
probe_args: ["codex", "login", "status"],
|
|
setup_copy: "run `codex login`",
|
|
// no availability field
|
|
},
|
|
],
|
|
};
|
|
assert.equal(extractConfigNudge(withSentinel("prose", payload)), null);
|
|
});
|
|
|
|
test("extractConfigNudge parses multiple requirements of mixed types", () => {
|
|
const payload = {
|
|
agent_name: "Atlas",
|
|
agent_pubkey: ATLAS_PUBKEY,
|
|
requirements: [
|
|
{ surface: "normalized_field", field: "model" },
|
|
{ surface: "env_key", key: "OPENAI_API_KEY" },
|
|
{
|
|
surface: "cli_login",
|
|
probe_args: ["codex", "login"],
|
|
setup_copy: "run `codex login`",
|
|
availability: "not_installed",
|
|
},
|
|
],
|
|
};
|
|
const result = extractConfigNudge(withSentinel("prose", payload));
|
|
assert.equal(result?.requirements.length, 3);
|
|
assert.equal(result?.agent_name, "Atlas");
|
|
assert.equal(result?.agent_pubkey, ATLAS_PUBKEY);
|
|
});
|
|
|
|
test("extractConfigNudge returns null for malformed JSON", () => {
|
|
const content = "prose\n\n```buzz:config-nudge\nnot{valid}json\n```";
|
|
assert.equal(extractConfigNudge(content), null);
|
|
});
|
|
|
|
test("extractConfigNudge returns null when JSON is valid but missing agent_name", () => {
|
|
const content = withSentinel("prose", {
|
|
agent_pubkey: FIZZ_PUBKEY,
|
|
requirements: [],
|
|
});
|
|
assert.equal(extractConfigNudge(content), null);
|
|
});
|
|
|
|
test("extractConfigNudge returns null when JSON is valid but missing agent_pubkey", () => {
|
|
const content = withSentinel("prose", {
|
|
agent_name: "Fizz",
|
|
requirements: [],
|
|
});
|
|
assert.equal(extractConfigNudge(content), null);
|
|
});
|
|
|
|
test("extractConfigNudge returns null when requirements contain unknown surface", () => {
|
|
const content = withSentinel("prose", {
|
|
agent_name: "Fizz",
|
|
agent_pubkey: FIZZ_PUBKEY,
|
|
requirements: [{ surface: "unknown_surface", data: "x" }],
|
|
});
|
|
assert.equal(extractConfigNudge(content), null);
|
|
});
|
|
|
|
test("extractConfigNudge returns null when requirements is not an array", () => {
|
|
const content = withSentinel("prose", {
|
|
agent_name: "Fizz",
|
|
agent_pubkey: FIZZ_PUBKEY,
|
|
requirements: "bad",
|
|
});
|
|
assert.equal(extractConfigNudge(content), null);
|
|
});
|
|
|
|
test("extractConfigNudge ignores regular code blocks with other language tags", () => {
|
|
const content = 'prose\n\n```json\n{"key":"val"}\n```';
|
|
assert.equal(extractConfigNudge(content), null);
|
|
});
|
|
|
|
test("extractConfigNudge handles empty requirements array", () => {
|
|
const payload = {
|
|
agent_name: "Fizz",
|
|
agent_pubkey: FIZZ_PUBKEY,
|
|
requirements: [],
|
|
};
|
|
assert.deepEqual(extractConfigNudge(withSentinel("prose", payload)), payload);
|
|
});
|
|
|
|
// ── stripConfigNudgeSentinel ──────────────────────────────────────────────────
|
|
|
|
test("stripConfigNudgeSentinel returns content unchanged when no sentinel", () => {
|
|
const content = "plain message body";
|
|
assert.equal(stripConfigNudgeSentinel(content), content);
|
|
});
|
|
|
|
test("stripConfigNudgeSentinel strips the sentinel block", () => {
|
|
const prose = "**Fizz** needs configuration.\n\nOpen Edit Agent.";
|
|
const payload = {
|
|
agent_name: "Fizz",
|
|
agent_pubkey: FIZZ_PUBKEY,
|
|
requirements: [],
|
|
};
|
|
const content = withSentinel(prose, payload);
|
|
const stripped = stripConfigNudgeSentinel(content);
|
|
assert.ok(!stripped.includes("buzz:config-nudge"), "sentinel must be gone");
|
|
assert.ok(stripped.includes("needs configuration"), "prose must survive");
|
|
});
|
|
|
|
test("stripConfigNudgeSentinel removes preceding blank line", () => {
|
|
const content = "prose\n\n```buzz:config-nudge\n{}\n```";
|
|
const stripped = stripConfigNudgeSentinel(content);
|
|
// Should not end with multiple newlines — the blank line separator was eaten.
|
|
assert.ok(!stripped.endsWith("\n\n"), "trailing blank line must be trimmed");
|
|
});
|
|
|
|
// ── Auth-gate invariants (Fix 1 — authenticate before rendering) ──────────────
|
|
//
|
|
// The full auth check lives in MarkdownInner's useMemo: it calls
|
|
// normalizePubkey(payload.agent_pubkey) !== normalizePubkey(configNudgeAuthorPubkey)
|
|
// and returns null when they don't match. The tests below verify:
|
|
// (a) extractConfigNudge still returns the payload regardless of the caller
|
|
// (extraction is pure; auth is responsibility of the renderer),
|
|
// (b) when agent_pubkey doesn't match the author, the comparison yields null
|
|
// (simulated inline to keep the test self-contained without React).
|
|
|
|
import { normalizePubkey } from "./pubkey.ts";
|
|
|
|
/**
|
|
* Simulate the auth guard in MarkdownInner's configNudge useMemo.
|
|
* Returns the payload if it passes auth, null otherwise.
|
|
*/
|
|
function authGuardedExtract(content, configNudgeAuthorPubkey) {
|
|
if (!configNudgeAuthorPubkey) return null;
|
|
const payload = extractConfigNudge(content);
|
|
if (payload === null) return null;
|
|
if (
|
|
normalizePubkey(payload.agent_pubkey) !==
|
|
normalizePubkey(configNudgeAuthorPubkey)
|
|
) {
|
|
return null;
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
const FIZZ_PUBKEY_AUTH = "aabbccddeeff0011223344556677889900aabbcc";
|
|
const OTHER_PUBKEY = "ffffffffffffffffffffffffffffffffffffffff";
|
|
|
|
function makeNudgeBody(agentPubkey) {
|
|
const payload = {
|
|
agent_name: "Fizz",
|
|
agent_pubkey: agentPubkey,
|
|
requirements: [{ surface: "env_key", key: "ANTHROPIC_API_KEY" }],
|
|
};
|
|
return `**Fizz** needs configuration.\n\n\`\`\`buzz:config-nudge\n${JSON.stringify(payload)}\n\`\`\``;
|
|
}
|
|
|
|
test("authGuard_noAuthorPubkey_returnsNull", () => {
|
|
const body = makeNudgeBody(FIZZ_PUBKEY_AUTH);
|
|
assert.equal(
|
|
authGuardedExtract(body, null),
|
|
null,
|
|
"null configNudgeAuthorPubkey must yield null (card path off)",
|
|
);
|
|
});
|
|
|
|
test("authGuard_undefinedAuthorPubkey_returnsNull", () => {
|
|
const body = makeNudgeBody(FIZZ_PUBKEY_AUTH);
|
|
assert.equal(
|
|
authGuardedExtract(body, undefined),
|
|
null,
|
|
"undefined configNudgeAuthorPubkey must yield null (card path off)",
|
|
);
|
|
});
|
|
|
|
test("authGuard_mismatchedAuthor_returnsNull", () => {
|
|
// Fence carries FIZZ_PUBKEY_AUTH but caller says the message author is OTHER_PUBKEY.
|
|
// The card must not render and the fence must NOT be stripped by the caller.
|
|
const body = makeNudgeBody(FIZZ_PUBKEY_AUTH);
|
|
const result = authGuardedExtract(body, OTHER_PUBKEY);
|
|
assert.equal(
|
|
result,
|
|
null,
|
|
"mismatched agent_pubkey vs configNudgeAuthorPubkey must yield null",
|
|
);
|
|
// Fence text must still be in the raw body (not stripped) — stripping only
|
|
// happens when configNudge !== null.
|
|
assert.ok(
|
|
body.includes("buzz:config-nudge"),
|
|
"fence must remain in body when auth guard returns null",
|
|
);
|
|
});
|
|
|
|
test("authGuard_matchingAuthor_returnsPayload", () => {
|
|
const body = makeNudgeBody(FIZZ_PUBKEY_AUTH);
|
|
const result = authGuardedExtract(body, FIZZ_PUBKEY_AUTH);
|
|
assert.notEqual(result, null, "matching author must yield the payload");
|
|
assert.equal(result?.agent_pubkey, FIZZ_PUBKEY_AUTH);
|
|
});
|
|
|
|
test("authGuard_matchingAuthor_caseInsensitive", () => {
|
|
// normalizePubkey lowercases both sides; mixed-case must still match.
|
|
const body = makeNudgeBody(FIZZ_PUBKEY_AUTH.toUpperCase());
|
|
const result = authGuardedExtract(body, FIZZ_PUBKEY_AUTH.toLowerCase());
|
|
assert.notEqual(
|
|
result,
|
|
null,
|
|
"case-insensitive pubkey comparison must pass auth",
|
|
);
|
|
});
|
|
|
|
// ── Signer-vs-tag-attribution regression ─────────────────────────────────────
|
|
//
|
|
// Pass 2 finding: message.pubkey (display author) can be overridden by actor/p
|
|
// tags, so a human-signed event can carry the agent pubkey as its attributed
|
|
// author. The auth guard must check the RAW EVENT SIGNER (signerPubkey from
|
|
// formatTimelineMessages), not the display author.
|
|
//
|
|
// We simulate this by passing the HUMAN pubkey as configNudgeAuthorPubkey
|
|
// while the payload's agent_pubkey is the AGENT pubkey — as MessageRow now
|
|
// does (passes signerPubkey, not pubkey).
|
|
//
|
|
// If the guard were still using the tag-attributed pubkey (display author =
|
|
// agent pubkey), it would pass auth and return the payload — a forged card.
|
|
// With the signer check, the human signer != agent payload key → null.
|
|
|
|
const HUMAN_PUBKEY =
|
|
"00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff";
|
|
const AGENT_PUBKEY_FOR_SPOOF =
|
|
"aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899";
|
|
|
|
test("authGuard_signerIsHuman_tagAttributedToAgent_returnsNull", () => {
|
|
// The event was signed by HUMAN_PUBKEY but its `actor` or first `p` tag
|
|
// attributes it to AGENT_PUBKEY_FOR_SPOOF (display author = agent).
|
|
// The payload's agent_pubkey matches the ATTRIBUTED pubkey, not the signer.
|
|
// MessageRow now passes signerPubkey (HUMAN) as configNudgeAuthorPubkey;
|
|
// the auth guard must reject since HUMAN != AGENT.
|
|
const body = makeNudgeBody(AGENT_PUBKEY_FOR_SPOOF);
|
|
|
|
// Simulate what MessageRow does: pass the RAW SIGNER, not the display author.
|
|
const result = authGuardedExtract(body, HUMAN_PUBKEY);
|
|
assert.equal(
|
|
result,
|
|
null,
|
|
"raw signer is human; even if actor-tag attributes to agent, must yield null",
|
|
);
|
|
// Fence must remain — not stripped — when auth fails.
|
|
assert.ok(
|
|
body.includes("buzz:config-nudge"),
|
|
"fence must remain in body when signer auth fails",
|
|
);
|
|
});
|
|
|
|
// ── Render-suppression contract ───────────────────────────────────────────────
|
|
//
|
|
// When `extractConfigNudge` returns a non-null payload, `markdown.tsx` MUST
|
|
// suppress the prose markdown node and render only the card:
|
|
//
|
|
// {configNudge === null ? markdownNode : null}
|
|
//
|
|
// This test pins that contract at the parse level. A revert to `{markdownNode}`
|
|
// (rendering both prose and card) would violate the invariants checked here:
|
|
// the prose text is still in the raw wire body and would re-appear alongside
|
|
// the card. If you break these assertions by changing `markdown.tsx` to render
|
|
// both, these tests must be updated AND the visual duplication re-approved.
|
|
|
|
test("nudgePresent_extractNonNull_and_stripRemovesSentinel", () => {
|
|
const prose =
|
|
"**Fizz** needs configuration before it can respond:\n- set `ANTHROPIC_API_KEY` in Edit Agent → Environment variables\n\nOpen Edit Agent in the Buzz app to set these.";
|
|
const payload = {
|
|
agent_name: "Fizz",
|
|
agent_pubkey: FIZZ_PUBKEY,
|
|
requirements: [{ surface: "env_key", key: "ANTHROPIC_API_KEY" }],
|
|
};
|
|
const body = withSentinel(prose, payload);
|
|
|
|
// 1. extractConfigNudge sees the sentinel → card renders, prose must be
|
|
// suppressed. If this returns null the card never renders at all.
|
|
const nudge = extractConfigNudge(body);
|
|
assert.notEqual(
|
|
nudge,
|
|
null,
|
|
"sentinel present → extractConfigNudge must return payload",
|
|
);
|
|
|
|
// 2. stripConfigNudgeSentinel removes the fence so whatever markdown node
|
|
// IS rendered doesn't also show the raw JSON block. The stripped string
|
|
// must NOT contain the sentinel open-fence marker.
|
|
const stripped = stripConfigNudgeSentinel(body);
|
|
assert.ok(
|
|
!stripped.includes("buzz:config-nudge"),
|
|
"stripped content must not contain the fence marker",
|
|
);
|
|
|
|
// 3. The prose IS still in the stripped string — that is intentional: the
|
|
// stripped string is what non-card clients display. On desktop the prose
|
|
// is suppressed by the `configNudge === null ? markdownNode : null` guard
|
|
// in markdown.tsx, NOT by stripping it from the string. If a future
|
|
// change strips the prose from the string here, that would break CLI
|
|
// fallback — this assertion guards against that regression too.
|
|
assert.ok(
|
|
stripped.includes("ANTHROPIC_API_KEY"),
|
|
"prose content must remain in stripped string for non-card client fallback",
|
|
);
|
|
});
|
|
|
|
test("nudgeAbsent_extractNull_markdownNodeShown", () => {
|
|
// When there is no sentinel, extractConfigNudge returns null, meaning
|
|
// markdown.tsx renders `markdownNode` normally (no card, no suppression).
|
|
const plainBody = "Hello from an agent without any configuration sentinel.";
|
|
assert.equal(
|
|
extractConfigNudge(plainBody),
|
|
null,
|
|
"no sentinel → extractConfigNudge must return null so markdownNode is shown",
|
|
);
|
|
// stripConfigNudgeSentinel on sentinel-free content is a no-op.
|
|
assert.equal(
|
|
stripConfigNudgeSentinel(plainBody),
|
|
plainBody,
|
|
"no sentinel → stripConfigNudgeSentinel must return content unchanged",
|
|
);
|
|
});
|