feat(cli): unify gh issue and pr crud
This commit is contained in:
@@ -166,6 +166,16 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
sendJson(res, 200, { ...issue, body: String(parsed.body ?? issue.body), updated_at: "2026-05-20T01:05:00Z" });
|
||||
return;
|
||||
}
|
||||
if (req.method === "POST" && req.url === "/repos/pikasTech/unidesk/issues/20/comments") {
|
||||
const parsed = JSON.parse(body) as JsonRecord;
|
||||
sendJson(res, 201, { id: 9001, body: String(parsed.body ?? ""), html_url: "https://github.com/pikasTech/unidesk/issues/20#issuecomment-9001", user: { login: "tester" }, created_at: "2026-05-20T06:00:00Z", updated_at: "2026-05-20T06:00:00Z" });
|
||||
return;
|
||||
}
|
||||
if (req.method === "DELETE" && req.url === "/repos/pikasTech/unidesk/issues/comments/9001") {
|
||||
res.statusCode = 204;
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
sendJson(res, 404, { message: "not found" });
|
||||
});
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
@@ -302,6 +312,41 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
const staleData = failedDataOf(staleEdit.json ?? {});
|
||||
assertCondition(staleData.degradedReason === "validation-failed", "stale guard should be validation-failed", staleData);
|
||||
|
||||
const appendFile = join(tmp, "append.md");
|
||||
writeFileSync(appendFile, "\n- appended `code`\n| c | d |\n| --- | --- |\n| 3 | 4 |\n", "utf8");
|
||||
const appendDryRun = await runCli(["gh", "issue", "update", "20", "--repo", "pikasTech/unidesk", "--mode", "append", "--body-file", appendFile, "--dry-run"], env);
|
||||
assertCondition(appendDryRun.status === 0, "issue update append dry-run should succeed", appendDryRun.json ?? { stdout: appendDryRun.stdout });
|
||||
const appendData = dataOf(appendDryRun.json ?? {});
|
||||
assertCondition(appendData.command === "issue update", "update command should be primary", appendData);
|
||||
assertCondition(appendData.mode === "append", "append mode should be explicit", appendData);
|
||||
assertCondition(appendData.containsBackticks === true && appendData.containsMarkdownTable === true, "append should preserve markdown signals", appendData);
|
||||
assertCondition(appendData.containsLiteralBackslashN === false, "append should preserve real newlines", appendData);
|
||||
|
||||
const replaceDryRun = await runCli(["gh", "issue", "update", "20", "--repo", "pikasTech/unidesk", "--mode", "replace", "--body-file", safeFile, "--dry-run"], env);
|
||||
assertCondition(replaceDryRun.status === 0, "issue update replace dry-run should succeed", replaceDryRun.json ?? { stdout: replaceDryRun.stdout });
|
||||
const replaceData = dataOf(replaceDryRun.json ?? {});
|
||||
assertCondition(replaceData.command === "issue update" && replaceData.mode === "replace", "replace mode should be explicit", replaceData);
|
||||
|
||||
const commentCreate = await runCli(["gh", "issue", "comment", "create", "20", "--repo", "pikasTech/unidesk", "--body-file", appendFile], env);
|
||||
assertCondition(commentCreate.status === 0, "issue comment create should succeed", commentCreate.json ?? { stdout: commentCreate.stdout });
|
||||
const commentCreateData = dataOf(commentCreate.json ?? {});
|
||||
assertCondition(commentCreateData.command === "issue comment create", "comment create should use CRUD command name", commentCreateData);
|
||||
|
||||
const commentDeleteDryRun = await runCli(["gh", "issue", "comment", "delete", "9001", "--repo", "pikasTech/unidesk", "--dry-run"], env);
|
||||
assertCondition(commentDeleteDryRun.status === 0, "issue comment delete dry-run should succeed", commentDeleteDryRun.json ?? { stdout: commentDeleteDryRun.stdout });
|
||||
const commentDeleteDryRunData = dataOf(commentDeleteDryRun.json ?? {});
|
||||
assertCondition(commentDeleteDryRunData.command === "issue comment delete" && commentDeleteDryRunData.planned === true, "comment delete dry-run should plan DELETE", commentDeleteDryRunData);
|
||||
|
||||
const commentDelete = await runCli(["gh", "issue", "comment", "delete", "9001", "--repo", "pikasTech/unidesk"], env);
|
||||
assertCondition(commentDelete.status === 0, "issue comment delete should succeed", commentDelete.json ?? { stdout: commentDelete.stdout });
|
||||
const commentDeleteData = dataOf(commentDelete.json ?? {});
|
||||
assertCondition(commentDeleteData.deleted === true, "comment delete should report deleted", commentDeleteData);
|
||||
|
||||
const issueDelete = await runCli(["gh", "issue", "delete", "20", "--repo", "pikasTech/unidesk"], env);
|
||||
assertCondition(issueDelete.status !== 0, "issue hard delete should be unsupported", issueDelete.json ?? { stdout: issueDelete.stdout });
|
||||
const issueDeleteData = failedDataOf(issueDelete.json ?? {});
|
||||
assertCondition(issueDeleteData.degradedReason === "unsupported-command", "issue delete should be unsupported-command", issueDeleteData);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
checks: [
|
||||
@@ -317,6 +362,9 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
"dry-run reports old/new body safety and does not PATCH",
|
||||
"multiline Markdown and backticks are not polluted",
|
||||
"expect-updated-at stale write protection blocks PATCH",
|
||||
"issue update replace/append modes preserve Markdown",
|
||||
"issue comment create/delete follows CRUD shape",
|
||||
"issue hard delete is structurally unsupported",
|
||||
],
|
||||
};
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user