diff --git a/packages/coding-agent/test/modes/components/user-message-keywords.test.ts b/packages/coding-agent/test/modes/components/user-message-keywords.test.ts new file mode 100644 index 000000000..4a527595b --- /dev/null +++ b/packages/coding-agent/test/modes/components/user-message-keywords.test.ts @@ -0,0 +1,36 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { UserMessageComponent } from "../../../src/modes/components/user-message"; +import { initTheme } from "../../../src/modes/theme/theme"; + +beforeAll(async () => { + await initTheme(false); +}); + +function render(text: string): string { + return new UserMessageComponent(text).render(80).join("\n"); +} + +describe("UserMessageComponent magic-keyword highlighting", () => { + it("gradient-paints a magic keyword in the rendered (sent) message bubble", () => { + const raw = render("please orchestrate the rollout"); + // Visible text is preserved. + expect(Bun.stripANSI(raw)).toContain("please orchestrate the rollout"); + // The keyword is gradient-painted: a per-character foreground sequence is emitted, + // and the word no longer survives as a contiguous run in the rendered bytes. + expect(raw).toContain("\x1b[38"); + expect(raw).not.toContain("orchestrate"); + }); + + it("does not paint a keyword inside an inline code span", () => { + const raw = render("ship the `orchestrate` helper"); + expect(Bun.stripANSI(raw)).toContain("orchestrate"); + // Code spans render through the code style as a single run — the word stays intact. + expect(raw).toContain("orchestrate"); + }); + + it("does not paint a keyword inside a fenced code block", () => { + const raw = render("intro\n```\norchestrate\n```"); + expect(Bun.stripANSI(raw)).toContain("orchestrate"); + expect(raw).toContain("orchestrate"); + }); +}); diff --git a/packages/coding-agent/test/modes/magic-keywords.test.ts b/packages/coding-agent/test/modes/magic-keywords.test.ts new file mode 100644 index 000000000..8abd4977f --- /dev/null +++ b/packages/coding-agent/test/modes/magic-keywords.test.ts @@ -0,0 +1,46 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { highlightMagicKeywords } from "../../src/modes/magic-keywords"; +import { initTheme } from "../../src/modes/theme/theme"; + +beforeAll(async () => { + // Gradient palettes read the active theme's color mode. + await initTheme(false); +}); + +describe("highlightMagicKeywords", () => { + it("paints every magic keyword in a single prose pass, preserving visible text", () => { + const input = "first ultrathink then orchestrate the workflow"; + const decorated = highlightMagicKeywords(input); + expect(decorated).not.toBe(input); + expect(decorated).toContain("\x1b[38"); + expect(Bun.stripANSI(decorated)).toBe(input); + // Each keyword is gradient-painted character-by-character, so none survives as a + // contiguous run in the decorated output. + for (const keyword of ["ultrathink", "orchestrate", "workflow"]) { + expect(decorated).not.toContain(keyword); + expect(Bun.stripANSI(decorated)).toContain(keyword); + } + }); + + it("never paints keywords inside code spans, fenced blocks, or XML sections", () => { + const input = "`ultrathink`\n```\norchestrate\n```\nworkflow"; + expect(highlightMagicKeywords(input)).toBe(input); + }); + + it("paints only the prose occurrence when the keyword also appears in code", () => { + const decorated = highlightMagicKeywords("`orchestrate` but please orchestrate now"); + // The code-span occurrence stays literal; the prose one is split by gradient escapes. + expect(decorated).toContain("`orchestrate`"); + expect(Bun.stripANSI(decorated)).toBe("`orchestrate` but please orchestrate now"); + // Exactly one prose occurrence painted ⇒ one contiguous "orchestrate" remains (the code one). + expect(decorated.split("orchestrate").length - 1).toBe(1); + }); + + it("restores the supplied foreground after each painted keyword", () => { + const reset = "\x1b[38;2;1;2;3m"; + const decorated = highlightMagicKeywords("go orchestrate go", reset); + expect(decorated).toContain(reset); + // The reset must land before the trailing prose so it keeps the bubble color. + expect(decorated.endsWith(`${reset} go`)).toBe(true); + }); +}); diff --git a/packages/coding-agent/test/modes/markdown-prose.test.ts b/packages/coding-agent/test/modes/markdown-prose.test.ts new file mode 100644 index 000000000..e0b4104d9 --- /dev/null +++ b/packages/coding-agent/test/modes/markdown-prose.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from "bun:test"; +import { keywordInProse, maskNonProse } from "../../src/modes/markdown-prose"; + +const ORCHESTRATE = /\borchestrate\b/i; + +describe("maskNonProse", () => { + it("preserves length and leaves plain prose untouched", () => { + const text = "please orchestrate the rollout across teams"; + expect(maskNonProse(text)).toBe(text); + }); + + it("returns the input unchanged when no code/markup constructs are present", () => { + // Fast path: no backtick, angle bracket, or tilde fence. + const text = "a multi line\nmessage with no markup"; + expect(maskNonProse(text)).toBe(text); + }); + + it("blanks fenced code blocks while keeping indices aligned", () => { + const text = "before\n```\norchestrate\n```\nafter orchestrate"; + const masked = maskNonProse(text); + expect(masked.length).toBe(text.length); + // The fenced occurrence is blanked; the prose occurrence survives at its index. + expect(masked.indexOf("orchestrate")).toBe(text.lastIndexOf("orchestrate")); + expect(masked.startsWith("before\n")).toBe(true); + }); + + it("blanks tilde fences and language-tagged fences", () => { + expect(maskNonProse("~~~\norchestrate\n~~~").includes("orchestrate")).toBe(false); + expect(maskNonProse("```ts\nconst orchestrate = 1\n```").includes("orchestrate")).toBe(false); + }); + + it("treats an unterminated fence as code through end of text", () => { + expect(maskNonProse("```\norchestrate").includes("orchestrate")).toBe(false); + }); +}); + +describe("keywordInProse", () => { + it("matches a standalone keyword in prose", () => { + expect(keywordInProse("please orchestrate this", ORCHESTRATE)).toBe(true); + expect(keywordInProse("orchestrate", ORCHESTRATE)).toBe(true); + }); + + it("ignores keywords inside inline code spans", () => { + expect(keywordInProse("use `orchestrate` now", ORCHESTRATE)).toBe(false); + expect(keywordInProse("``a `orchestrate` b``", ORCHESTRATE)).toBe(false); + }); + + it("treats an unmatched backtick run as literal text", () => { + // A lone backtick opens no span, so the keyword is still prose. + expect(keywordInProse("the ` then orchestrate", ORCHESTRATE)).toBe(true); + }); + + it("ignores keywords inside fenced code blocks", () => { + expect(keywordInProse("```\norchestrate\n```", ORCHESTRATE)).toBe(false); + expect(keywordInProse("text\n```\norchestrate here\n```\ndone", ORCHESTRATE)).toBe(false); + }); + + it("ignores keywords inside XML/HTML sections, including nested and comments", () => { + expect(keywordInProse("orchestrate", ORCHESTRATE)).toBe(false); + expect(keywordInProse("orchestrate", ORCHESTRATE)).toBe(false); + expect(keywordInProse("", ORCHESTRATE)).toBe(false); + expect(keywordInProse('orchestrate', ORCHESTRATE)).toBe(false); + }); + + it("still matches prose outside of code/markup regions", () => { + expect(keywordInProse("orchestrate then orchestrate", ORCHESTRATE)).toBe(true); + expect(keywordInProse("run `x` then orchestrate", ORCHESTRATE)).toBe(true); + expect(keywordInProse("```\ncode\n```\norchestrate", ORCHESTRATE)).toBe(true); + }); + + it("does not over-mask on a stray less-than or an unbalanced tag", () => { + expect(keywordInProse("a < b and orchestrate", ORCHESTRATE)).toBe(true); + //
has no matching close, so only the tag is masked, not the rest. + expect(keywordInProse("
orchestrate after", ORCHESTRATE)).toBe(true); + }); + + it("respects word boundaries regardless of region", () => { + expect(keywordInProse("reorchestrate the orchestration", ORCHESTRATE)).toBe(false); + }); +}); diff --git a/packages/coding-agent/test/modes/orchestrate.test.ts b/packages/coding-agent/test/modes/orchestrate.test.ts index 7f6da09ff..f17b513ba 100644 --- a/packages/coding-agent/test/modes/orchestrate.test.ts +++ b/packages/coding-agent/test/modes/orchestrate.test.ts @@ -10,22 +10,35 @@ beforeAll(() => { }); describe("orchestrate keyword detection", () => { - it("matches the standalone word in any case", () => { + it("matches the lowercase word delimited by whitespace or a string edge", () => { expect(containsOrchestrate("orchestrate")).toBe(true); - expect(containsOrchestrate("Orchestrate")).toBe(true); - expect(containsOrchestrate("ORCHESTRATE")).toBe(true); expect(containsOrchestrate("please orchestrate this rollout")).toBe(true); - expect(containsOrchestrate("do it. orchestrate.")).toBe(true); + expect(containsOrchestrate("orchestrate the rollout")).toBe(true); + // A newline is whitespace, and end-of-string is a valid right boundary. + expect(containsOrchestrate("do it now\norchestrate")).toBe(true); }); - it("ignores inflected forms and embedded substrings", () => { + it("ignores casing, inflections, punctuation-adjacent, and path-embedded forms", () => { + expect(containsOrchestrate("Orchestrate")).toBe(false); + expect(containsOrchestrate("ORCHESTRATE")).toBe(false); expect(containsOrchestrate("orchestrated the build")).toBe(false); expect(containsOrchestrate("orchestrating now")).toBe(false); expect(containsOrchestrate("a clean orchestration")).toBe(false); expect(containsOrchestrate("it orchestrates well")).toBe(false); expect(containsOrchestrate("reorchestrate everything")).toBe(false); + // The reported bug: a path/extension is not whitespace, so the word never triggers. + expect(containsOrchestrate("packages/coding-agent/src/modes/orchestrate.ts")).toBe(false); + expect(containsOrchestrate("do it. orchestrate.")).toBe(false); expect(containsOrchestrate("nothing to see here")).toBe(false); }); + + it("ignores keywords inside code spans, fenced blocks, and XML sections", () => { + expect(containsOrchestrate("use `orchestrate` here")).toBe(false); + expect(containsOrchestrate("```\norchestrate\n```")).toBe(false); + expect(containsOrchestrate("orchestrate")).toBe(false); + // A real prose request alongside code still triggers. + expect(containsOrchestrate("run `setup` then orchestrate the rollout")).toBe(true); + }); }); describe("orchestrate keyword highlighting", () => { @@ -38,8 +51,12 @@ describe("orchestrate keyword highlighting", () => { it("leaves text without the standalone keyword untouched", () => { expect(highlightOrchestrate("nothing here")).toBe("nothing here"); - // Probe hits the substring but the word boundary fails — no decoration. + // Probe hits the substring but the whitespace boundary fails — no decoration. expect(highlightOrchestrate("orchestrated builds")).toBe("orchestrated builds"); + expect(highlightOrchestrate("Orchestrate this")).toBe("Orchestrate this"); + // The reported bug: a filename must not be painted. + const filePath = "packages/coding-agent/src/modes/orchestrate.ts"; + expect(highlightOrchestrate(filePath)).toBe(filePath); }); it("does not cross-trigger with the ultrathink highlighter", () => { diff --git a/packages/coding-agent/test/modes/workflow.test.ts b/packages/coding-agent/test/modes/workflow.test.ts index 658b5a797..bb010bc6b 100644 --- a/packages/coding-agent/test/modes/workflow.test.ts +++ b/packages/coding-agent/test/modes/workflow.test.ts @@ -8,18 +8,21 @@ beforeAll(() => { }); describe("workflow keyword detection", () => { - it("matches the standalone word (singular or plural) in any case", () => { + it("matches the lowercase word (singular or plural) delimited by whitespace", () => { expect(containsWorkflow("workflow")).toBe(true); - expect(containsWorkflow("Workflow")).toBe(true); - expect(containsWorkflow("WORKFLOW")).toBe(true); expect(containsWorkflow("please workflow this rollout")).toBe(true); - expect(containsWorkflow("do it. workflow.")).toBe(true); expect(containsWorkflow("run these workflows")).toBe(true); + expect(containsWorkflow("design the workflow")).toBe(true); }); - it("ignores inflected forms and embedded substrings", () => { + it("ignores casing, inflections, punctuation-adjacent, and path-embedded forms", () => { + expect(containsWorkflow("Workflow")).toBe(false); + expect(containsWorkflow("WORKFLOW")).toBe(false); expect(containsWorkflow("workflowed the build")).toBe(false); expect(containsWorkflow("reworkflow everything")).toBe(false); + // A path/extension is not whitespace, so the word never triggers. + expect(containsWorkflow("packages/coding-agent/test/modes/workflow.test.ts")).toBe(false); + expect(containsWorkflow("do it. workflow.")).toBe(false); expect(containsWorkflow("nothing to see here")).toBe(false); }); }); @@ -34,8 +37,11 @@ describe("workflow keyword highlighting", () => { }); it("leaves text without the standalone keyword untouched", () => { - // Probe hits the substring but the word boundary fails — no decoration. + // Probe hits the substring but the whitespace boundary fails — no decoration. expect(highlightWorkflow("workflowed builds")).toBe("workflowed builds"); + expect(highlightWorkflow("Workflow this")).toBe("Workflow this"); + const filePath = "packages/coding-agent/test/modes/workflow.test.ts"; + expect(highlightWorkflow(filePath)).toBe(filePath); }); });