@devory/github 0.3.0 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,141 +0,0 @@
1
- /**
2
- * packages/github/src/lib/pr-helpers.ts
3
- *
4
- * Pure helpers for generating GitHub PR titles and body text from factory
5
- * task metadata and body content.
6
- *
7
- * No filesystem access. Depends only on @devory/core types.
8
- *
9
- * All functions are deterministic and side-effect free so they can be tested
10
- * in isolation and composed into GitHub Actions, CLI commands, or UI flows.
11
- */
12
-
13
- import type { TaskMeta } from "@devory/core";
14
-
15
- // ---------------------------------------------------------------------------
16
- // Conventional-commit type table (reused for PR titles)
17
- // ---------------------------------------------------------------------------
18
-
19
- const COMMIT_TYPE_MAP: Record<string, string> = {
20
- feature: "feat",
21
- feat: "feat",
22
- bugfix: "fix",
23
- bug: "fix",
24
- fix: "fix",
25
- refactor: "refactor",
26
- chore: "chore",
27
- documentation: "docs",
28
- docs: "docs",
29
- test: "test",
30
- tests: "test",
31
- perf: "perf",
32
- performance: "perf",
33
- subtask: "feat",
34
- };
35
-
36
- /** Map task type to a conventional-commit prefix. Defaults to "feat". */
37
- export function commitType(taskType: string | undefined): string {
38
- const t = (taskType ?? "").toLowerCase().trim();
39
- return COMMIT_TYPE_MAP[t] ?? "feat";
40
- }
41
-
42
- /** Derive a short scope label. Priority: repo_area → lane → project → "core". */
43
- export function taskScope(meta: Partial<TaskMeta>): string {
44
- const repoArea = typeof meta.repo_area === "string" ? meta.repo_area.trim() : "";
45
- const lane = typeof meta.lane === "string" ? meta.lane.trim() : "";
46
- const project = typeof meta.project === "string" ? meta.project.trim() : "";
47
- return repoArea || lane || project || "core";
48
- }
49
-
50
- // ---------------------------------------------------------------------------
51
- // PR title
52
- // ---------------------------------------------------------------------------
53
-
54
- /**
55
- * Build a PR title in conventional-commit format:
56
- * `<type>(<scope>): <task title>`
57
- *
58
- * Truncated to 72 characters to fit GitHub's recommended title length.
59
- */
60
- export function buildPrTitle(meta: Partial<TaskMeta>): string {
61
- const type = commitType(typeof meta.type === "string" ? meta.type : undefined);
62
- const scope = taskScope(meta);
63
- const title = typeof meta.title === "string" ? meta.title.trim() : "(untitled)";
64
- const line = `${type}(${scope}): ${title}`;
65
- return line.length > 72 ? line.slice(0, 71) + "…" : line;
66
- }
67
-
68
- // ---------------------------------------------------------------------------
69
- // PR body
70
- // ---------------------------------------------------------------------------
71
-
72
- export interface PrMetadata {
73
- title: string;
74
- body: string;
75
- }
76
-
77
- /**
78
- * Build a structured PR description from task metadata and task body text.
79
- *
80
- * The body follows GitHub's recommended PR template structure:
81
- * - Summary section (task fields)
82
- * - Context section (task body if present)
83
- * - Verification checklist (from task's verification field)
84
- * - Factory metadata footer
85
- */
86
- export function buildPrBody(meta: Partial<TaskMeta>, taskBody: string): string {
87
- const lines: string[] = [];
88
-
89
- // ── Summary ─────────────────────────────────────────────────────────────
90
- lines.push("## Summary", "");
91
- lines.push(`- **Task:** ${meta.id ?? "(unknown)"} — ${meta.title ?? "(untitled)"}`);
92
- lines.push(`- **Project:** ${meta.project ?? "(unknown)"}`);
93
- lines.push(`- **Type:** ${meta.type ?? "feature"}`);
94
- lines.push(`- **Priority:** ${meta.priority ?? "medium"}`);
95
- lines.push(`- **Agent:** ${meta.agent ?? "(none)"}`);
96
-
97
- const deps = Array.isArray(meta.depends_on) ? meta.depends_on : [];
98
- if (deps.length > 0) {
99
- lines.push(`- **Depends on:** ${deps.join(", ")}`);
100
- }
101
- lines.push("");
102
-
103
- // ── Context ──────────────────────────────────────────────────────────────
104
- const trimmedBody = (taskBody ?? "").trim();
105
- if (trimmedBody) {
106
- lines.push("## Context", "");
107
- // Include up to ~30 lines of the task body to keep the PR description focused
108
- const bodyLines = trimmedBody.split("\n").slice(0, 30);
109
- lines.push(...bodyLines);
110
- lines.push("");
111
- }
112
-
113
- // ── Verification ──────────────────────────────────────────────────────────
114
- const verification = Array.isArray(meta.verification) ? meta.verification : [];
115
- if (verification.length > 0) {
116
- lines.push("## Verification", "");
117
- for (const cmd of verification) {
118
- lines.push(`- [ ] \`${cmd}\``);
119
- }
120
- lines.push("");
121
- }
122
-
123
- // ── Footer ───────────────────────────────────────────────────────────────
124
- lines.push("---");
125
- lines.push("*Generated by Devory · ai-dev-factory — human review required before merge.*");
126
-
127
- return lines.join("\n");
128
- }
129
-
130
- /**
131
- * Build a complete PR metadata object (title + body) from task metadata.
132
- */
133
- export function buildPrMetadata(
134
- meta: Partial<TaskMeta>,
135
- taskBody: string
136
- ): PrMetadata {
137
- return {
138
- title: buildPrTitle(meta),
139
- body: buildPrBody(meta, taskBody),
140
- };
141
- }
@@ -1,158 +0,0 @@
1
- /**
2
- * packages/github/src/test/action-helpers.test.ts
3
- *
4
- * Tests for src/lib/action-helpers.ts.
5
- * Verifies the correct output format without actually writing to env files.
6
- *
7
- * Run: tsx --test packages/github/src/test/action-helpers.test.ts
8
- */
9
-
10
- import { test, describe, before, after } from "node:test";
11
- import assert from "node:assert/strict";
12
- import * as fs from "fs";
13
- import * as os from "os";
14
- import * as path from "path";
15
-
16
- import {
17
- setOutput,
18
- setOutputs,
19
- setEnv,
20
- appendStepSummary,
21
- isGitHubActions,
22
- getRunId,
23
- getRepoSlug,
24
- } from "../lib/action-helpers.js";
25
-
26
- // ── Return value tests (no env files) ─────────────────────────────────────
27
-
28
- describe("setOutput return value", () => {
29
- test("returns name=value string", () => {
30
- const line = setOutput("branch", "feat/factory-066");
31
- assert.equal(line, "branch=feat/factory-066");
32
- });
33
-
34
- test("handles values with spaces", () => {
35
- const line = setOutput("title", "feat(core): My Title");
36
- assert.equal(line, "title=feat(core): My Title");
37
- });
38
- });
39
-
40
- describe("setOutputs return value", () => {
41
- test("returns array of name=value strings", () => {
42
- const lines = setOutputs({ branch: "feat/x", title: "feat(p): T" });
43
- assert.equal(lines.length, 2);
44
- assert.ok(lines.includes("branch=feat/x"));
45
- assert.ok(lines.includes("title=feat(p): T"));
46
- });
47
-
48
- test("returns empty array for empty input", () => {
49
- const lines = setOutputs({});
50
- assert.deepEqual(lines, []);
51
- });
52
- });
53
-
54
- describe("setEnv return value", () => {
55
- test("returns NAME=value string", () => {
56
- const line = setEnv("DEVORY_BRANCH", "feat/my-branch");
57
- assert.equal(line, "DEVORY_BRANCH=feat/my-branch");
58
- });
59
- });
60
-
61
- describe("appendStepSummary return value", () => {
62
- test("returns the markdown passed in", () => {
63
- const md = "# My Summary\nSome content.";
64
- const result = appendStepSummary(md);
65
- assert.equal(result, md);
66
- });
67
- });
68
-
69
- // ── Detection helpers ──────────────────────────────────────────────────────
70
-
71
- describe("isGitHubActions", () => {
72
- test("returns false when GITHUB_ACTIONS is not set", () => {
73
- const saved = process.env.GITHUB_ACTIONS;
74
- delete process.env.GITHUB_ACTIONS;
75
- assert.equal(isGitHubActions(), false);
76
- if (saved !== undefined) process.env.GITHUB_ACTIONS = saved;
77
- });
78
-
79
- test("returns true when GITHUB_ACTIONS=true", () => {
80
- const saved = process.env.GITHUB_ACTIONS;
81
- process.env.GITHUB_ACTIONS = "true";
82
- assert.equal(isGitHubActions(), true);
83
- if (saved !== undefined) process.env.GITHUB_ACTIONS = saved;
84
- else delete process.env.GITHUB_ACTIONS;
85
- });
86
- });
87
-
88
- describe("getRunId", () => {
89
- test("returns null when GITHUB_RUN_ID not set", () => {
90
- const saved = process.env.GITHUB_RUN_ID;
91
- delete process.env.GITHUB_RUN_ID;
92
- assert.equal(getRunId(), null);
93
- if (saved !== undefined) process.env.GITHUB_RUN_ID = saved;
94
- });
95
-
96
- test("returns run ID string when set", () => {
97
- const saved = process.env.GITHUB_RUN_ID;
98
- process.env.GITHUB_RUN_ID = "12345";
99
- assert.equal(getRunId(), "12345");
100
- if (saved !== undefined) process.env.GITHUB_RUN_ID = saved;
101
- else delete process.env.GITHUB_RUN_ID;
102
- });
103
- });
104
-
105
- describe("getRepoSlug", () => {
106
- test("returns null when GITHUB_REPOSITORY not set", () => {
107
- const saved = process.env.GITHUB_REPOSITORY;
108
- delete process.env.GITHUB_REPOSITORY;
109
- assert.equal(getRepoSlug(), null);
110
- if (saved !== undefined) process.env.GITHUB_REPOSITORY = saved;
111
- });
112
-
113
- test("returns owner/repo when set", () => {
114
- const saved = process.env.GITHUB_REPOSITORY;
115
- process.env.GITHUB_REPOSITORY = "devory/ai-dev-factory";
116
- assert.equal(getRepoSlug(), "devory/ai-dev-factory");
117
- if (saved !== undefined) process.env.GITHUB_REPOSITORY = saved;
118
- else delete process.env.GITHUB_REPOSITORY;
119
- });
120
- });
121
-
122
- // ── Live file writing (with GITHUB_OUTPUT set) ─────────────────────────────
123
-
124
- describe("setOutput file writing", () => {
125
- let tmpDir: string;
126
- let outputFile: string;
127
- const originalEnv = process.env.GITHUB_OUTPUT;
128
-
129
- before(() => {
130
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "devory-gh-test-"));
131
- outputFile = path.join(tmpDir, "output");
132
- fs.writeFileSync(outputFile, "", "utf-8");
133
- process.env.GITHUB_OUTPUT = outputFile;
134
- });
135
-
136
- after(() => {
137
- fs.rmSync(tmpDir, { recursive: true, force: true });
138
- if (originalEnv !== undefined) {
139
- process.env.GITHUB_OUTPUT = originalEnv;
140
- } else {
141
- delete process.env.GITHUB_OUTPUT;
142
- }
143
- });
144
-
145
- test("appends name=value to GITHUB_OUTPUT file", () => {
146
- setOutput("my-key", "my-value");
147
- const content = fs.readFileSync(outputFile, "utf-8");
148
- assert.ok(content.includes("my-key=my-value"));
149
- });
150
-
151
- test("multiple outputs are each on their own line", () => {
152
- fs.writeFileSync(outputFile, "", "utf-8"); // reset
153
- setOutput("a", "1");
154
- setOutput("b", "2");
155
- const lines = fs.readFileSync(outputFile, "utf-8").trim().split("\n");
156
- assert.equal(lines.length, 2);
157
- });
158
- });
@@ -1,168 +0,0 @@
1
- /**
2
- * packages/github/src/test/branch-helpers.test.ts
3
- *
4
- * Tests for src/lib/branch-helpers.ts.
5
- * Run: tsx --test packages/github/src/test/branch-helpers.test.ts
6
- */
7
-
8
- import { test, describe } from "node:test";
9
- import assert from "node:assert/strict";
10
-
11
- import {
12
- slugify,
13
- branchPrefix,
14
- buildBranchName,
15
- } from "../lib/branch-helpers.js";
16
-
17
- // ── slugify ────────────────────────────────────────────────────────────────
18
-
19
- describe("slugify", () => {
20
- test("lowercases input", () => {
21
- assert.equal(slugify("Hello World"), "hello-world");
22
- });
23
-
24
- test("replaces spaces with hyphens", () => {
25
- assert.equal(slugify("foo bar baz"), "foo-bar-baz");
26
- });
27
-
28
- test("collapses multiple non-alphanumeric chars to one hyphen", () => {
29
- assert.equal(slugify("foo -- bar"), "foo-bar");
30
- });
31
-
32
- test("strips leading and trailing hyphens", () => {
33
- assert.equal(slugify(" hello "), "hello");
34
- });
35
-
36
- test("handles numbers", () => {
37
- assert.equal(slugify("Factory 066 MVP"), "factory-066-mvp");
38
- });
39
-
40
- test("truncates at maxLen", () => {
41
- const long = "a".repeat(100);
42
- assert.equal(slugify(long, 10).length, 10);
43
- });
44
-
45
- test("empty string returns empty string", () => {
46
- assert.equal(slugify(""), "");
47
- });
48
-
49
- test("strips special characters", () => {
50
- assert.equal(slugify("feat: add @devory/core"), "feat-add-devory-core");
51
- });
52
- });
53
-
54
- // ── branchPrefix ──────────────────────────────────────────────────────────
55
-
56
- describe("branchPrefix", () => {
57
- test("feature → feat", () => assert.equal(branchPrefix("feature"), "feat"));
58
- test("feat → feat", () => assert.equal(branchPrefix("feat"), "feat"));
59
- test("bugfix → fix", () => assert.equal(branchPrefix("bugfix"), "fix"));
60
- test("bug → fix", () => assert.equal(branchPrefix("bug"), "fix"));
61
- test("refactor → refactor", () => assert.equal(branchPrefix("refactor"), "refactor"));
62
- test("chore → chore", () => assert.equal(branchPrefix("chore"), "chore"));
63
- test("docs → docs", () => assert.equal(branchPrefix("docs"), "docs"));
64
- test("documentation → docs", () => assert.equal(branchPrefix("documentation"), "docs"));
65
- test("unknown type defaults to task", () => assert.equal(branchPrefix("unknown"), "task"));
66
- test("undefined defaults to task", () => assert.equal(branchPrefix(undefined), "task"));
67
- test("case-insensitive", () => assert.equal(branchPrefix("FEATURE"), "feat"));
68
- });
69
-
70
- // ── buildBranchName ────────────────────────────────────────────────────────
71
-
72
- describe("buildBranchName — task-meta source", () => {
73
- test("uses meta.branch when present", () => {
74
- const r = buildBranchName({ branch: "task/factory-066-my-feature" });
75
- assert.equal(r.branch, "task/factory-066-my-feature");
76
- assert.equal(r.source, "task-meta");
77
- assert.equal(r.warnings.length, 0);
78
- });
79
-
80
- test("uses meta.branch even if type/id/title are present", () => {
81
- const r = buildBranchName({
82
- branch: "custom/my-branch",
83
- id: "factory-001",
84
- title: "Something else",
85
- type: "feature",
86
- });
87
- assert.equal(r.branch, "custom/my-branch");
88
- assert.equal(r.source, "task-meta");
89
- });
90
-
91
- test("trims whitespace from meta.branch", () => {
92
- const r = buildBranchName({ branch: " feat/some-branch " });
93
- assert.equal(r.branch, "feat/some-branch");
94
- });
95
- });
96
-
97
- describe("buildBranchName — derived source", () => {
98
- test("derives from id and title when no branch field", () => {
99
- const r = buildBranchName({
100
- id: "factory-066",
101
- title: "GitHub Integration MVP",
102
- type: "feature",
103
- });
104
- assert.equal(r.source, "derived");
105
- assert.ok(r.branch.includes("factory-066"));
106
- assert.ok(r.branch.includes("github-integration-mvp"));
107
- });
108
-
109
- test("branch starts with type-based prefix", () => {
110
- const r = buildBranchName({ id: "x", title: "y", type: "bugfix" });
111
- assert.ok(r.branch.startsWith("fix/"));
112
- });
113
-
114
- test("branch starts with 'task/' for unknown type", () => {
115
- const r = buildBranchName({ id: "x", title: "y", type: "unknown" });
116
- assert.ok(r.branch.startsWith("task/"));
117
- });
118
-
119
- test("branch starts with 'task/' when type is missing", () => {
120
- const r = buildBranchName({ id: "x", title: "y" });
121
- assert.ok(r.branch.startsWith("task/"));
122
- });
123
-
124
- test("branch contains id", () => {
125
- const r = buildBranchName({ id: "factory-099", title: "My Task" });
126
- assert.ok(r.branch.includes("factory-099"));
127
- });
128
-
129
- test("branch slug is lowercase", () => {
130
- const r = buildBranchName({ id: "x", title: "Hello World" });
131
- assert.equal(r.branch, r.branch.toLowerCase());
132
- });
133
-
134
- test("handles title with special chars", () => {
135
- const r = buildBranchName({ id: "f-001", title: "Add @devory/core to repo" });
136
- assert.ok(!r.branch.includes("@"));
137
- assert.ok(!r.branch.includes("/devory"));
138
- });
139
-
140
- test("falls back to id-only when title is empty", () => {
141
- const r = buildBranchName({ id: "factory-001", title: "" });
142
- assert.equal(r.source, "derived");
143
- assert.ok(r.branch.includes("factory-001"));
144
- assert.equal(r.warnings.length, 1);
145
- });
146
-
147
- test("returns fallback branch when id and title both empty", () => {
148
- const r = buildBranchName({});
149
- assert.equal(r.branch, "task/unnamed");
150
- assert.ok(r.warnings.length > 0);
151
- });
152
- });
153
-
154
- describe("buildBranchName — branch name format", () => {
155
- test("branch contains no whitespace", () => {
156
- const r = buildBranchName({ id: "factory-066", title: "Multi Word Title Here" });
157
- assert.ok(!/\s/.test(r.branch));
158
- });
159
-
160
- test("branch length is reasonable (under 80 chars)", () => {
161
- const r = buildBranchName({
162
- id: "factory-066",
163
- title: "A very long task title that might overflow the branch name limit",
164
- type: "feature",
165
- });
166
- assert.ok(r.branch.length <= 80);
167
- });
168
- });
@@ -1,223 +0,0 @@
1
- /**
2
- * packages/github/src/test/pr-create.test.ts
3
- *
4
- * Tests for src/lib/pr-create.ts — gated PR creation helper.
5
- * All tests use pure functions; no process spawning.
6
- *
7
- * Run: tsx --test packages/github/src/test/pr-create.test.ts
8
- */
9
-
10
- import { test, describe } from "node:test";
11
- import assert from "node:assert/strict";
12
-
13
- import {
14
- canCreatePr,
15
- prCreateBlockedReason,
16
- buildGhCreateArgs,
17
- createPr,
18
- } from "../lib/pr-create.js";
19
-
20
- const META = {
21
- id: "factory-071",
22
- title: "GitHub PR creation (gated automation)",
23
- project: "ai-dev-factory",
24
- type: "feature" as const,
25
- priority: "high" as const,
26
- status: "review" as const,
27
- repo: ".",
28
- branch: "task/factory-071-github-pr-creation",
29
- };
30
-
31
- const BODY = "## Goal\n\nAdd gated PR creation to the factory.";
32
-
33
- // ---------------------------------------------------------------------------
34
- // canCreatePr
35
- // ---------------------------------------------------------------------------
36
-
37
- describe("canCreatePr", () => {
38
- test("returns true when GITHUB_TOKEN is set", () => {
39
- assert.equal(canCreatePr({ GITHUB_TOKEN: "ghp_test_token" }), true);
40
- });
41
-
42
- test("returns false when GITHUB_TOKEN is absent", () => {
43
- assert.equal(canCreatePr({}), false);
44
- });
45
-
46
- test("returns false when GITHUB_TOKEN is empty string", () => {
47
- assert.equal(canCreatePr({ GITHUB_TOKEN: "" }), false);
48
- });
49
-
50
- test("returns false when GITHUB_TOKEN is whitespace only", () => {
51
- assert.equal(canCreatePr({ GITHUB_TOKEN: " " }), false);
52
- });
53
-
54
- test("returns true for non-empty token with whitespace padding", () => {
55
- // Token itself has content — only the full value is checked
56
- assert.equal(canCreatePr({ GITHUB_TOKEN: " ghp_abc " }), true);
57
- });
58
- });
59
-
60
- // ---------------------------------------------------------------------------
61
- // prCreateBlockedReason
62
- // ---------------------------------------------------------------------------
63
-
64
- describe("prCreateBlockedReason", () => {
65
- const validOptions = {
66
- confirm: true,
67
- branch: "task/factory-071",
68
- env: { GITHUB_TOKEN: "ghp_test" },
69
- };
70
-
71
- test("returns null when all guards pass", () => {
72
- assert.equal(
73
- prCreateBlockedReason(validOptions, { GITHUB_TOKEN: "ghp_test" }),
74
- null
75
- );
76
- });
77
-
78
- test("blocks when confirm is false", () => {
79
- const reason = prCreateBlockedReason(
80
- { ...validOptions, confirm: false },
81
- { GITHUB_TOKEN: "ghp_test" }
82
- );
83
- assert.ok(reason !== null);
84
- assert.ok(reason!.includes("--confirm"));
85
- });
86
-
87
- test("blocks when branch is empty string", () => {
88
- const reason = prCreateBlockedReason(
89
- { ...validOptions, branch: "" },
90
- { GITHUB_TOKEN: "ghp_test" }
91
- );
92
- assert.ok(reason !== null);
93
- assert.ok(reason!.includes("branch"));
94
- });
95
-
96
- test("blocks when branch is whitespace only", () => {
97
- const reason = prCreateBlockedReason(
98
- { ...validOptions, branch: " " },
99
- { GITHUB_TOKEN: "ghp_test" }
100
- );
101
- assert.ok(reason !== null);
102
- assert.ok(reason!.includes("branch"));
103
- });
104
-
105
- test("blocks when GITHUB_TOKEN is absent", () => {
106
- const reason = prCreateBlockedReason(validOptions, {});
107
- assert.ok(reason !== null);
108
- assert.ok(reason!.includes("GITHUB_TOKEN"));
109
- });
110
-
111
- test("confirm guard takes priority over token guard", () => {
112
- const reason = prCreateBlockedReason(
113
- { ...validOptions, confirm: false },
114
- {} // no token
115
- );
116
- assert.ok(reason !== null);
117
- assert.ok(reason!.includes("--confirm"));
118
- });
119
- });
120
-
121
- // ---------------------------------------------------------------------------
122
- // buildGhCreateArgs
123
- // ---------------------------------------------------------------------------
124
-
125
- describe("buildGhCreateArgs", () => {
126
- test("includes 'pr create' subcommand", () => {
127
- const args = buildGhCreateArgs(META, BODY, { branch: "task/factory-071" });
128
- assert.equal(args[0], "pr");
129
- assert.equal(args[1], "create");
130
- });
131
-
132
- test("includes --title derived from meta", () => {
133
- const args = buildGhCreateArgs(META, BODY, { branch: "task/factory-071" });
134
- const i = args.indexOf("--title");
135
- assert.ok(i >= 0);
136
- assert.ok(args[i + 1].includes("GitHub PR creation"));
137
- });
138
-
139
- test("includes --body derived from meta + task body", () => {
140
- const args = buildGhCreateArgs(META, BODY, { branch: "task/factory-071" });
141
- const i = args.indexOf("--body");
142
- assert.ok(i >= 0);
143
- assert.ok(args[i + 1].includes("factory-071"));
144
- });
145
-
146
- test("includes --head with branch name", () => {
147
- const args = buildGhCreateArgs(META, BODY, { branch: "task/factory-071" });
148
- const i = args.indexOf("--head");
149
- assert.ok(i >= 0 && args[i + 1] === "task/factory-071");
150
- });
151
-
152
- test("defaults --base to main", () => {
153
- const args = buildGhCreateArgs(META, BODY, { branch: "task/factory-071" });
154
- const i = args.indexOf("--base");
155
- assert.ok(i >= 0 && args[i + 1] === "main");
156
- });
157
-
158
- test("uses custom --base when provided", () => {
159
- const args = buildGhCreateArgs(META, BODY, { branch: "task/factory-071", base: "develop" });
160
- const i = args.indexOf("--base");
161
- assert.ok(i >= 0 && args[i + 1] === "develop");
162
- });
163
-
164
- test("title follows conventional-commit format", () => {
165
- const args = buildGhCreateArgs(META, BODY, { branch: "task/factory-071" });
166
- const i = args.indexOf("--title");
167
- assert.ok(args[i + 1].startsWith("feat("));
168
- });
169
-
170
- test("body contains summary section", () => {
171
- const args = buildGhCreateArgs(META, BODY, { branch: "task/factory-071" });
172
- const i = args.indexOf("--body");
173
- assert.ok(args[i + 1].includes("## Summary"));
174
- });
175
- });
176
-
177
- // ---------------------------------------------------------------------------
178
- // createPr — guard paths (no process spawning)
179
- // ---------------------------------------------------------------------------
180
-
181
- describe("createPr — guard paths", () => {
182
- test("returns skipped when confirm is false", () => {
183
- const result = createPr(META, BODY, {
184
- confirm: false,
185
- branch: "task/factory-071",
186
- env: { GITHUB_TOKEN: "ghp_test" },
187
- });
188
- assert.equal(result.ok, false);
189
- assert.equal(result.skipped, true);
190
- assert.ok(result.error?.includes("--confirm"));
191
- });
192
-
193
- test("returns skipped when GITHUB_TOKEN is absent", () => {
194
- const result = createPr(META, BODY, {
195
- confirm: true,
196
- branch: "task/factory-071",
197
- env: {},
198
- });
199
- assert.equal(result.ok, false);
200
- assert.equal(result.skipped, true);
201
- assert.ok(result.error?.includes("GITHUB_TOKEN"));
202
- });
203
-
204
- test("returns skipped when branch is empty", () => {
205
- const result = createPr(META, BODY, {
206
- confirm: true,
207
- branch: "",
208
- env: { GITHUB_TOKEN: "ghp_test" },
209
- });
210
- assert.equal(result.ok, false);
211
- assert.equal(result.skipped, true);
212
- assert.ok(result.error?.includes("branch"));
213
- });
214
-
215
- test("skipped result has no prUrl", () => {
216
- const result = createPr(META, BODY, {
217
- confirm: false,
218
- branch: "task/factory-071",
219
- env: { GITHUB_TOKEN: "ghp_test" },
220
- });
221
- assert.equal(result.prUrl, undefined);
222
- });
223
- });