Skip to content

Commit 41f0932

Browse files
ruibabyCopilot
andcommitted
Improve content success output and post update prompt order
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 62d16e9 commit 41f0932

File tree

9 files changed

+573
-22
lines changed

9 files changed

+573
-22
lines changed

src/commands/post/__test__/post-json-entry.spec.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,79 @@ test("tryRunPostCommand imports json by updating an existing post", async () =>
211211
expect(publishMyPost).toHaveBeenCalledWith({ name: "post-1" });
212212
});
213213

214+
test("tryRunPostCommand prints import summary with permalink and inspect command", async () => {
215+
const stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
216+
217+
const getMyPost = vi.fn().mockRejectedValue({
218+
isAxiosError: true,
219+
response: { status: 404 },
220+
});
221+
const createMyPost = vi.fn().mockResolvedValue({
222+
data: { metadata: { name: "post-1" } },
223+
});
224+
const publishMyPost = vi.fn().mockResolvedValue(undefined);
225+
ucPostApiState.implementation = {
226+
getMyPost,
227+
getMyPostDraft: vi.fn(),
228+
updateMyPost: vi.fn(),
229+
updateMyPostDraft: vi.fn(),
230+
createMyPost,
231+
publishMyPost,
232+
unpublishMyPost: vi.fn(),
233+
};
234+
235+
const getPost = vi.fn().mockResolvedValue({
236+
data: {
237+
metadata: { name: "post-1" },
238+
status: { permalink: "/archives/post-1" },
239+
},
240+
});
241+
const fetchPostHeadContent = vi.fn().mockResolvedValue({
242+
data: { raw: "# Halo", content: "<h1>Halo</h1>", rawType: "markdown" },
243+
});
244+
const runtimeMock = {
245+
getClientsForOptions: vi.fn().mockResolvedValue({
246+
profile: { baseUrl: "https://example.com/console" },
247+
clients: {
248+
axios: {},
249+
core: {
250+
content: {
251+
post: {
252+
getPost,
253+
},
254+
},
255+
},
256+
console: {
257+
content: {
258+
post: {
259+
fetchPostHeadContent,
260+
},
261+
},
262+
},
263+
},
264+
}),
265+
};
266+
267+
await expect(
268+
tryRunPostCommand(
269+
[
270+
"post",
271+
"import-json",
272+
"--raw",
273+
'{"post":{"metadata":{"name":"post-1"},"spec":{"publish":true}},"content":{"raw":"# Halo","content":"<h1>Halo</h1>","rawType":"markdown"}}',
274+
],
275+
runtimeMock as never,
276+
),
277+
).resolves.toBe(true);
278+
279+
const output = stdoutSpy.mock.calls.map((call) => String(call[0])).join("");
280+
expect(output).toContain("Post imported successfully from inline JSON.");
281+
expect(output).toContain("\n\nmetadata.name: post-1");
282+
expect(output).toContain("metadata.name: post-1");
283+
expect(output).toContain("permalink: https://example.com/archives/post-1");
284+
expect(output).toContain("inspect: halo post get post-1");
285+
});
286+
214287
test("tryRunPostCommand rejects invalid inline json", async () => {
215288
const runtimeMock = {
216289
getClientsForOptions: vi.fn(),

src/commands/post/__test__/post-markdown-entry.spec.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,3 +270,90 @@ halo:
270270
await rm(tempDir, { recursive: true, force: true });
271271
}
272272
});
273+
274+
test("tryRunPostCommand prints markdown import summary when permalink is unavailable", async () => {
275+
const stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
276+
277+
const tempDir = await mkdtemp(join(tmpdir(), "halo-post-import-markdown-summary-"));
278+
const filePath = join(tempDir, "hello-halo.md");
279+
280+
try {
281+
await writeFile(
282+
filePath,
283+
`---
284+
title: Hello Halo
285+
---
286+
# Hello Halo
287+
`,
288+
"utf8",
289+
);
290+
291+
const createMyPost = vi.fn().mockResolvedValue({
292+
data: { metadata: { name: "post-1" } },
293+
});
294+
295+
ucPostApiState.implementation = {
296+
createMyPost,
297+
publishMyPost: vi.fn().mockResolvedValue(undefined),
298+
unpublishMyPost: vi.fn().mockResolvedValue(undefined),
299+
};
300+
301+
const getPost = vi.fn().mockResolvedValue({
302+
data: {
303+
metadata: { name: "post-1" },
304+
spec: {
305+
title: "Hello Halo",
306+
slug: "hello-halo",
307+
excerpt: { autoGenerate: true },
308+
cover: "",
309+
categories: [],
310+
tags: [],
311+
publish: false,
312+
},
313+
status: {},
314+
},
315+
});
316+
const fetchPostHeadContent = vi.fn().mockResolvedValue({
317+
data: {
318+
raw: "# Hello Halo",
319+
content: "<h1>Hello Halo</h1>\n",
320+
rawType: "markdown",
321+
},
322+
});
323+
const runtimeMock = {
324+
getClientsForOptions: vi.fn().mockResolvedValue({
325+
profile: { baseUrl: "https://example.com" },
326+
clients: {
327+
axios: {},
328+
core: {
329+
content: {
330+
post: {
331+
getPost,
332+
},
333+
},
334+
},
335+
console: {
336+
content: {
337+
post: {
338+
fetchPostHeadContent,
339+
},
340+
},
341+
},
342+
},
343+
}),
344+
};
345+
346+
await expect(
347+
tryRunPostCommand(["post", "import-markdown", "--file", filePath], runtimeMock as never),
348+
).resolves.toBe(true);
349+
350+
const output = stdoutSpy.mock.calls.map((call) => String(call[0])).join("");
351+
expect(output).toContain(`Markdown post imported successfully from ${filePath}.`);
352+
expect(output).toContain("\n\nmetadata.name: post-1");
353+
expect(output).toContain("metadata.name: post-1");
354+
expect(output).toContain("permalink: (not available until published)");
355+
expect(output).toContain("inspect: halo post get post-1");
356+
} finally {
357+
await rm(tempDir, { recursive: true, force: true });
358+
}
359+
});
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { afterEach, expect, test, vi } from "vitest";
2+
3+
const ucPostApiState = vi.hoisted(() => ({
4+
implementation: {} as Record<string, unknown>,
5+
}));
6+
7+
const promptState = vi.hoisted(() => ({
8+
input: vi.fn(),
9+
confirm: vi.fn(),
10+
checkbox: vi.fn(),
11+
}));
12+
13+
vi.mock("@halo-dev/api-client", async () => {
14+
const actual =
15+
await vi.importActual<typeof import("@halo-dev/api-client")>("@halo-dev/api-client");
16+
17+
return {
18+
...actual,
19+
PostV1alpha1UcApi: vi.fn(function MockPostV1alpha1UcApi() {
20+
return ucPostApiState.implementation;
21+
}),
22+
};
23+
});
24+
25+
vi.mock("@inquirer/prompts", () => ({
26+
input: promptState.input,
27+
confirm: promptState.confirm,
28+
checkbox: promptState.checkbox,
29+
}));
30+
31+
import { tryRunPostCommand } from "../index.js";
32+
33+
const originalStdinTty = process.stdin.isTTY;
34+
const originalStdoutTty = process.stdout.isTTY;
35+
36+
afterEach(() => {
37+
ucPostApiState.implementation = {};
38+
promptState.input.mockReset();
39+
promptState.confirm.mockReset();
40+
promptState.checkbox.mockReset();
41+
vi.restoreAllMocks();
42+
Object.defineProperty(process.stdin, "isTTY", { value: originalStdinTty, configurable: true });
43+
Object.defineProperty(process.stdout, "isTTY", { value: originalStdoutTty, configurable: true });
44+
});
45+
46+
test("tryRunPostCommand prompts title and slug before categories and tags on update", async () => {
47+
Object.defineProperty(process.stdin, "isTTY", { value: true, configurable: true });
48+
Object.defineProperty(process.stdout, "isTTY", { value: true, configurable: true });
49+
vi.spyOn(process.stdout, "write").mockImplementation(() => true);
50+
51+
const order: string[] = [];
52+
53+
promptState.input.mockImplementation(async (config: { message: string }) => {
54+
order.push(config.message);
55+
56+
if (config.message === "Post title") {
57+
return "Updated Halo";
58+
}
59+
60+
if (config.message === "Post slug") {
61+
return "updated-halo";
62+
}
63+
64+
if (config.message === "Post content") {
65+
return "# Updated Halo";
66+
}
67+
68+
return "";
69+
});
70+
71+
promptState.confirm.mockImplementation(async (config: { message: string }) => {
72+
order.push(config.message);
73+
return false;
74+
});
75+
76+
promptState.checkbox.mockImplementation(async (config: { message: string }) => {
77+
order.push(config.message);
78+
return [];
79+
});
80+
81+
const getMyPost = vi
82+
.fn()
83+
.mockResolvedValueOnce({
84+
data: {
85+
metadata: { name: "post-1" },
86+
spec: {
87+
title: "Hello Halo",
88+
slug: "hello-halo",
89+
categories: [],
90+
tags: [],
91+
cover: "",
92+
template: "",
93+
visible: "PUBLIC",
94+
publish: false,
95+
pinned: false,
96+
allowComment: true,
97+
priority: 0,
98+
excerpt: { autoGenerate: true },
99+
},
100+
},
101+
})
102+
.mockResolvedValueOnce({
103+
data: {
104+
metadata: { name: "post-1" },
105+
spec: {
106+
title: "Updated Halo",
107+
slug: "updated-halo",
108+
},
109+
},
110+
});
111+
const getMyPostDraft = vi.fn().mockResolvedValue({
112+
data: {
113+
metadata: { name: "post-1", annotations: {} },
114+
spec: { rawType: "markdown" },
115+
},
116+
});
117+
const updateMyPost = vi.fn().mockResolvedValue({
118+
data: { metadata: { name: "post-1" } },
119+
});
120+
const updateMyPostDraft = vi.fn().mockResolvedValue(undefined);
121+
122+
ucPostApiState.implementation = {
123+
getMyPost,
124+
getMyPostDraft,
125+
updateMyPost,
126+
updateMyPostDraft,
127+
publishMyPost: vi.fn().mockResolvedValue(undefined),
128+
unpublishMyPost: vi.fn().mockResolvedValue(undefined),
129+
};
130+
131+
const runtimeMock = {
132+
getClientsForOptions: vi.fn().mockResolvedValue({
133+
profile: { baseUrl: "https://example.com" },
134+
clients: {
135+
axios: {
136+
get: vi.fn().mockImplementation(async (url: string) => {
137+
if (url.includes("/categories")) {
138+
return { data: { items: [] } };
139+
}
140+
141+
if (url.includes("/tags")) {
142+
return { data: { items: [] } };
143+
}
144+
145+
throw new Error(`Unexpected axios.get url: ${url}`);
146+
}),
147+
post: vi.fn(),
148+
},
149+
},
150+
}),
151+
};
152+
153+
await expect(
154+
tryRunPostCommand(["post", "update", "post-1", "--json"], runtimeMock as never),
155+
).resolves.toBe(true);
156+
157+
expect(order.indexOf("Post title")).toBe(0);
158+
expect(order.indexOf("Post slug")).toBe(1);
159+
expect(order.indexOf("Select categories")).toBeGreaterThan(order.indexOf("Post slug"));
160+
expect(order.indexOf("Select tags")).toBeGreaterThan(order.indexOf("Select categories"));
161+
});

0 commit comments

Comments
 (0)