1import { describe, expect, it } from "vite-plus/test";
2import remarkParse from "remark-parse";
3import { unified } from "unified";
4
5import {
6 remarkCodexDirectives,
7 renderCodexDirectivesForCopy,
8 renderCodexInlineDirectivesAsMarkdown,
9 splitCodexArtifactTemplateMarkdown,
10} from "./codexMarkdownDirectives.ts";
11import { parseMarkdownFileLink } from "./markdownLinks.ts";
12
13interface TestNode {
14 readonly type: string;
15 readonly value?: string;
16 readonly url?: string;
17 readonly position?: {
18 readonly start: { readonly offset?: number };
19 readonly end: { readonly offset?: number };
20 };
21 readonly data?: {
22 readonly hName?: string;
23 readonly hProperties?: Readonly<Record<string, unknown>>;
24 };
25 readonly children?: readonly TestNode[];
26}
27
28const FILE_CITATION = ':codex-file-citation{path="outputs/report.xlsx" purpose="output"}';
29const FOLLOW_UP =
30 ':codex-followup[Prepare print version]{prompt="Prepare the document for printing."}';
31const ARTIFACT_TEMPLATE =
32 '::artifact-template{skill_name="artifact-template-hello-world" skill_directory="/Users/test/.codex/skills/artifact-template-hello-world" display_name="Hello World" artifact_kind="document"}';
33
34function parse(markdown: string): TestNode {
35 const processor = unified().use(remarkParse).use(remarkCodexDirectives);
36 return processor.runSync(processor.parse(markdown), { value: markdown }) as TestNode;
37}
38
39function parseOrdinaryMarkdown(markdown: string): TestNode {
40 return unified().use(remarkParse).parse(markdown) as TestNode;
41}
42
43describe("remarkCodexDirectives", () => {
44 it("renders a file citation as a link without changing its source position", () => {
45 const markdown = `Created ${FILE_CITATION}.`;
46 const link = parse(markdown).children?.[0]?.children?.[1];
47
48 expect(link).toMatchObject({
49 type: "link",
50 url: "outputs/report.xlsx",
51 children: [{ type: "text", value: "report.xlsx" }],
52 position: {
53 start: { offset: markdown.indexOf(FILE_CITATION) },
54 end: { offset: markdown.indexOf(FILE_CITATION) + FILE_CITATION.length },
55 },
56 });
57 });
58
59 it("renders an artifact template as semantic block metadata", () => {
60 expect(parse(ARTIFACT_TEMPLATE).children?.[0]).toMatchObject({
61 type: "paragraph",
62 children: [],
63 data: {
64 hName: "div",
65 hProperties: {
66 dataCodexArtifactTemplate: "true",
67 dataArtifactKind: "document",
68 dataDisplayName: "Hello World",
69 dataSkillName: "artifact-template-hello-world",
70 },
71 },
72 });
73 });
74
75 it("renders a follow-up suggestion as its label", () => {
76 const followUp = parse(`Next: ${FOLLOW_UP}`).children?.[0]?.children?.[1];
77
78 expect(followUp).toMatchObject({
79 data: { hName: "span" },
80 children: [{ type: "text", value: "Prepare print version" }],
81 });
82 });
83
84 it.each([
85 "Meeting at 10:30",
86 "Open src/main.ts:42",
87 "Use :hover and :tada:",
88 "::note",
89 ":::note\ncontent\n:::",
90 ':codex-file-citation-extra{path="outputs/report.xlsx"}',
91 "::artifact-template-extra",
92 ':codex-followup-extra[Label]{prompt="Prompt"}',
93 ])("does not change unrelated colon syntax: %s", (markdown) => {
94 expect(parse(markdown)).toEqual(parseOrdinaryMarkdown(markdown));
95 });
96
97 it.each([
98 ':codex-file-citation{purpose="output"}',
99 '::artifact-template{skill_name="artifact-template-hello-world"}',
100 ":codex-followup[Prepare print version]",
101 ':codex-followup[ ]{prompt="Prepare the document for printing."}',
102 ':codex-followup[Prepare print version]{prompt=" "}',
103 ])("keeps malformed supported directives literal: %s", (markdown) => {
104 expect(parse(markdown)).toEqual(parseOrdinaryMarkdown(markdown));
105 });
106});
107
108describe.each([
109 { name: "renderCodexDirectivesForCopy", render: renderCodexDirectivesForCopy },
110 { name: "renderCodexInlineDirectivesAsMarkdown", render: renderCodexInlineDirectivesAsMarkdown },
111])("$name file citation round trips", ({ render }) => {
112 it.each([
113 "C:\\Users\\test\\[draft]\\report.md",
114 "\\\\server\\share\\report.md",
115 "outputs/report.md",
116 "/tmp/report%5C.md",
117 ])("preserves the literal path and line: %s", (path) => {
118 const markdown = render(`:codex-file-citation{path="${path}" line_range_start="7"}`);
119 const link = parseOrdinaryMarkdown(markdown).children?.[0]?.children?.[0];
120
121 expect(link?.type).toBe("link");
122 expect(parseMarkdownFileLink(link?.url ?? "")).toEqual({ path, line: 7 });
123 });
124});
125
126describe("native Markdown adapters", () => {
127 it("uses the same parser to render file citations as portable links", () => {
128 expect(renderCodexInlineDirectivesAsMarkdown(`Created ${FILE_CITATION}.`)).toBe(
129 "Created [report.xlsx](<outputs/report.xlsx>).",
130 );
131 });
132
133 it("renders follow-up suggestions as their label Markdown", () => {
134 expect(
135 renderCodexInlineDirectivesAsMarkdown(
136 `- ${FOLLOW_UP}\n- :codex-followup[Review **document**]{prompt="Review it."}`,
137 ),
138 ).toBe("- Prepare print version\n- Review **document**");
139 });
140
141 it.each([`\\${FOLLOW_UP}`, `\`${FOLLOW_UP}\``])(
142 "does not render excluded follow-up syntax: %s",
143 (markdown) => {
144 expect(renderCodexInlineDirectivesAsMarkdown(markdown)).toBe(markdown);
145 },
146 );
147
148 it.each([
149 `\\${FILE_CITATION}`,
150 `\`${FILE_CITATION}\``,
151 `\`\`\`text\n${FILE_CITATION}\n\`\`\``,
152 `[See ${FILE_CITATION}](https://example.com)`,
153 ])("does not render excluded citation syntax: %s", (markdown) => {
154 expect(renderCodexInlineDirectivesAsMarkdown(markdown)).toBe(markdown);
155 });
156
157 it("splits artifact cards from surrounding native Markdown", () => {
158 expect(splitCodexArtifactTemplateMarkdown(`Before\n\n${ARTIFACT_TEMPLATE}\n\nAfter`)).toEqual([
159 { kind: "markdown", markdown: "Before\n\n", sourceOffset: 0 },
160 {
161 kind: "artifact-template",
162 sourceOffset: 8,
163 template: {
164 artifactKind: "document",
165 displayName: "Hello World",
166 skillDirectory: "/Users/test/.codex/skills/artifact-template-hello-world",
167 skillName: "artifact-template-hello-world",
168 },
169 },
170 {
171 kind: "markdown",
172 markdown: "\n\nAfter",
173 sourceOffset: 8 + ARTIFACT_TEMPLATE.length,
174 },
175 ]);
176 });
177
178 it("leaves malformed and code artifact-template examples in Markdown", () => {
179 const malformed = '::artifact-template{display_name="Hello World"}';
180 const code = `\`${ARTIFACT_TEMPLATE}\``;
181 expect(splitCodexArtifactTemplateMarkdown(malformed)).toEqual([
182 { kind: "markdown", markdown: malformed, sourceOffset: 0 },
183 ]);
184 expect(splitCodexArtifactTemplateMarkdown(code)).toEqual([
185 { kind: "markdown", markdown: code, sourceOffset: 0 },
186 ]);
187 });
188});
189
190describe("directive copy adapter", () => {
191 it("copies the Markdown representations shown by citation chips and template cards", () => {
192 expect(
193 renderCodexDirectivesForCopy(
194 `Created ${FILE_CITATION}.\n\n${ARTIFACT_TEMPLATE}\n\n- ${FOLLOW_UP}`,
195 ),
196 ).toBe(
197 "Created [report.xlsx](<outputs/report.xlsx>).\n\nHello World (Document template)\n\n- Prepare print version",
198 );
199 });
200
201 it("leaves excluded and malformed directive source unchanged", () => {
202 const markdown = [
203 `\`${FILE_CITATION}\``,
204 '::artifact-template{display_name="Hello World"}',
205 ].join("\n\n");
206
207 expect(renderCodexDirectivesForCopy(markdown)).toBe(markdown);
208 });
209});