@devory/github 0.0.1 → 0.1.0

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.
@@ -0,0 +1,223 @@
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
+ });
@@ -0,0 +1,187 @@
1
+ /**
2
+ * packages/github/src/test/pr-helpers.test.ts
3
+ *
4
+ * Tests for src/lib/pr-helpers.ts.
5
+ * Run: tsx --test packages/github/src/test/pr-helpers.test.ts
6
+ */
7
+
8
+ import { test, describe } from "node:test";
9
+ import assert from "node:assert/strict";
10
+
11
+ import {
12
+ commitType,
13
+ taskScope,
14
+ buildPrTitle,
15
+ buildPrBody,
16
+ buildPrMetadata,
17
+ } from "../lib/pr-helpers.js";
18
+
19
+ // ── commitType ─────────────────────────────────────────────────────────────
20
+
21
+ describe("commitType", () => {
22
+ test("feature → feat", () => assert.equal(commitType("feature"), "feat"));
23
+ test("feat → feat", () => assert.equal(commitType("feat"), "feat"));
24
+ test("bugfix → fix", () => assert.equal(commitType("bugfix"), "fix"));
25
+ test("bug → fix", () => assert.equal(commitType("bug"), "fix"));
26
+ test("refactor → refactor", () => assert.equal(commitType("refactor"), "refactor"));
27
+ test("chore → chore", () => assert.equal(commitType("chore"), "chore"));
28
+ test("docs → docs", () => assert.equal(commitType("docs"), "docs"));
29
+ test("documentation → docs", () => assert.equal(commitType("documentation"), "docs"));
30
+ test("test → test", () => assert.equal(commitType("test"), "test"));
31
+ test("perf → perf", () => assert.equal(commitType("perf"), "perf"));
32
+ test("subtask → feat", () => assert.equal(commitType("subtask"), "feat"));
33
+ test("unknown → feat", () => assert.equal(commitType("unknown"), "feat"));
34
+ test("undefined → feat", () => assert.equal(commitType(undefined), "feat"));
35
+ test("case-insensitive", () => assert.equal(commitType("FEATURE"), "feat"));
36
+ });
37
+
38
+ // ── taskScope ──────────────────────────────────────────────────────────────
39
+
40
+ describe("taskScope", () => {
41
+ test("uses repo_area first", () => {
42
+ assert.equal(taskScope({ repo_area: "api", lane: "infra", project: "proj" }), "api");
43
+ });
44
+
45
+ test("falls back to lane when no repo_area", () => {
46
+ assert.equal(taskScope({ lane: "infra", project: "proj" }), "infra");
47
+ });
48
+
49
+ test("falls back to project when no repo_area or lane", () => {
50
+ assert.equal(taskScope({ project: "ai-dev-factory" }), "ai-dev-factory");
51
+ });
52
+
53
+ test("defaults to 'core' when all empty", () => {
54
+ assert.equal(taskScope({}), "core");
55
+ });
56
+
57
+ test("trims whitespace", () => {
58
+ assert.equal(taskScope({ repo_area: " api " }), "api");
59
+ });
60
+ });
61
+
62
+ // ── buildPrTitle ───────────────────────────────────────────────────────────
63
+
64
+ describe("buildPrTitle", () => {
65
+ test("builds conventional-commit title", () => {
66
+ const title = buildPrTitle({
67
+ type: "feature",
68
+ project: "ai-dev-factory",
69
+ title: "GitHub Integration MVP",
70
+ });
71
+ assert.equal(title, "feat(ai-dev-factory): GitHub Integration MVP");
72
+ });
73
+
74
+ test("uses repo_area as scope when present", () => {
75
+ const title = buildPrTitle({
76
+ type: "bugfix",
77
+ repo_area: "api",
78
+ title: "Fix null pointer",
79
+ });
80
+ assert.ok(title.startsWith("fix(api):"));
81
+ });
82
+
83
+ test("truncates to 72 chars with ellipsis", () => {
84
+ const longTitle = "A".repeat(100);
85
+ const title = buildPrTitle({ type: "feature", project: "p", title: longTitle });
86
+ assert.ok(title.length <= 72);
87
+ assert.ok(title.endsWith("…"));
88
+ });
89
+
90
+ test("does not truncate short titles", () => {
91
+ const title = buildPrTitle({ type: "chore", project: "proj", title: "short" });
92
+ assert.ok(title.length < 72);
93
+ assert.ok(!title.endsWith("…"));
94
+ });
95
+
96
+ test("handles missing type (defaults to feat)", () => {
97
+ const title = buildPrTitle({ project: "p", title: "My task" });
98
+ assert.ok(title.startsWith("feat("));
99
+ });
100
+
101
+ test("handles missing title", () => {
102
+ const title = buildPrTitle({ type: "feature", project: "p" });
103
+ assert.ok(title.includes("(untitled)"));
104
+ });
105
+ });
106
+
107
+ // ── buildPrBody ────────────────────────────────────────────────────────────
108
+
109
+ describe("buildPrBody", () => {
110
+ const meta = {
111
+ id: "factory-066",
112
+ title: "GitHub Integration MVP",
113
+ project: "ai-dev-factory",
114
+ type: "feature",
115
+ priority: "medium",
116
+ agent: "fullstack-builder",
117
+ depends_on: ["factory-054", "factory-063"],
118
+ verification: ["npm run test", "npm run validate:task -- tasks/backlog/factory-066.md"],
119
+ };
120
+
121
+ test("includes task id in body", () => {
122
+ const body = buildPrBody(meta, "");
123
+ assert.ok(body.includes("factory-066"));
124
+ });
125
+
126
+ test("includes task title in body", () => {
127
+ const body = buildPrBody(meta, "");
128
+ assert.ok(body.includes("GitHub Integration MVP"));
129
+ });
130
+
131
+ test("includes project in body", () => {
132
+ const body = buildPrBody(meta, "");
133
+ assert.ok(body.includes("ai-dev-factory"));
134
+ });
135
+
136
+ test("includes depends_on when present", () => {
137
+ const body = buildPrBody(meta, "");
138
+ assert.ok(body.includes("factory-054"));
139
+ assert.ok(body.includes("factory-063"));
140
+ });
141
+
142
+ test("includes verification commands as checklist items", () => {
143
+ const body = buildPrBody(meta, "");
144
+ assert.ok(body.includes("- [ ] `npm run test`"));
145
+ });
146
+
147
+ test("includes task body content when provided", () => {
148
+ const body = buildPrBody(meta, "## Goal\nDo the thing.");
149
+ assert.ok(body.includes("Do the thing."));
150
+ });
151
+
152
+ test("omits context section when body is empty", () => {
153
+ const body = buildPrBody(meta, "");
154
+ assert.ok(!body.includes("## Context"));
155
+ });
156
+
157
+ test("includes safety footer", () => {
158
+ const body = buildPrBody(meta, "");
159
+ assert.ok(body.includes("human review required before merge"));
160
+ });
161
+
162
+ test("includes Summary section header", () => {
163
+ const body = buildPrBody(meta, "");
164
+ assert.ok(body.includes("## Summary"));
165
+ });
166
+
167
+ test("omits verification section when verification is empty", () => {
168
+ const body = buildPrBody({ ...meta, verification: [] }, "");
169
+ assert.ok(!body.includes("## Verification"));
170
+ });
171
+ });
172
+
173
+ // ── buildPrMetadata ────────────────────────────────────────────────────────
174
+
175
+ describe("buildPrMetadata", () => {
176
+ test("returns object with title and body", () => {
177
+ const result = buildPrMetadata({ type: "feature", project: "p", title: "T" }, "");
178
+ assert.ok(typeof result.title === "string" && result.title.length > 0);
179
+ assert.ok(typeof result.body === "string" && result.body.length > 0);
180
+ });
181
+
182
+ test("title matches buildPrTitle output", () => {
183
+ const meta = { type: "bugfix", project: "proj", title: "Fix bug" };
184
+ const result = buildPrMetadata(meta, "");
185
+ assert.equal(result.title, buildPrTitle(meta));
186
+ });
187
+ });
package/index.js DELETED
@@ -1 +0,0 @@
1
- module.exports = {};