* feat(gsd): GitHub sync extension — auto-sync lifecycle events to Issues, PRs, Milestones Standalone opt-in extension at src/resources/extensions/github-sync/ that syncs GSD lifecycle events to GitHub as a presentation layer. Local .gsd/ files remain source of truth; GitHub is fire-and-forget. Lifecycle mapping: - plan-milestone → GH Milestone + tracking Issue (roadmap body) - plan-slice → slice branch + draft PR + task sub-issues - execute-task → summary comment + close task issue + Resolves #N commit - complete-slice → mark PR ready + squash-merge into milestone branch - complete-milestone → close GH Milestone + tracking issue GSD core changes (minimal): - preferences: add `github` config key with validation and merge logic - auto-post-unit: single dynamic import integration point after auto-commit - git-service: `issueNumber` field on TaskCommitContext for Resolves #N trailer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: strict TS casts for SummaryFrontmatter and GitHubSyncConfig CI tsconfig requires double-cast through unknown for interfaces without index signatures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
110 lines
3.4 KiB
TypeScript
110 lines
3.4 KiB
TypeScript
import { describe, it } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
formatMilestoneIssueBody,
|
|
formatSlicePRBody,
|
|
formatTaskIssueBody,
|
|
formatSummaryComment,
|
|
} from "../templates.ts";
|
|
|
|
describe("templates", () => {
|
|
describe("formatMilestoneIssueBody", () => {
|
|
it("includes title and vision", () => {
|
|
const body = formatMilestoneIssueBody({
|
|
id: "M001",
|
|
title: "Build Auth",
|
|
vision: "Secure authentication for all users",
|
|
});
|
|
assert.ok(body.includes("M001: Build Auth"));
|
|
assert.ok(body.includes("Secure authentication"));
|
|
});
|
|
|
|
it("renders success criteria as checkboxes", () => {
|
|
const body = formatMilestoneIssueBody({
|
|
id: "M001",
|
|
title: "Auth",
|
|
successCriteria: ["Users can log in", "OAuth works"],
|
|
});
|
|
assert.ok(body.includes("- [ ] Users can log in"));
|
|
assert.ok(body.includes("- [ ] OAuth works"));
|
|
});
|
|
|
|
it("renders slice table", () => {
|
|
const body = formatMilestoneIssueBody({
|
|
id: "M001",
|
|
title: "Auth",
|
|
slices: [
|
|
{ id: "S01", title: "Core types", taskCount: 3 },
|
|
{ id: "S02", title: "OAuth", taskCount: 5 },
|
|
],
|
|
});
|
|
assert.ok(body.includes("| S01 | Core types | 3 |"));
|
|
assert.ok(body.includes("| S02 | OAuth | 5 |"));
|
|
});
|
|
});
|
|
|
|
describe("formatSlicePRBody", () => {
|
|
it("includes goal and must-haves", () => {
|
|
const body = formatSlicePRBody({
|
|
id: "S01",
|
|
title: "Core Auth Types",
|
|
goal: "Define all auth types",
|
|
mustHaves: ["User type", "Session type"],
|
|
});
|
|
assert.ok(body.includes("Define all auth types"));
|
|
assert.ok(body.includes("- User type"));
|
|
assert.ok(body.includes("- Session type"));
|
|
});
|
|
|
|
it("renders task checklist with issue links", () => {
|
|
const body = formatSlicePRBody({
|
|
id: "S01",
|
|
title: "Auth",
|
|
tasks: [
|
|
{ id: "T01", title: "Types", issueNumber: 43 },
|
|
{ id: "T02", title: "Schema" },
|
|
],
|
|
});
|
|
assert.ok(body.includes("- [ ] T01: Types (#43)"));
|
|
assert.ok(body.includes("- [ ] T02: Schema"));
|
|
assert.ok(!body.includes("T02: Schema (#"));
|
|
});
|
|
});
|
|
|
|
describe("formatTaskIssueBody", () => {
|
|
it("includes files and verification", () => {
|
|
const body = formatTaskIssueBody({
|
|
id: "T01",
|
|
title: "Add types",
|
|
files: ["src/types.ts"],
|
|
verifyCriteria: ["Types compile"],
|
|
});
|
|
assert.ok(body.includes("`src/types.ts`"));
|
|
assert.ok(body.includes("- [ ] Types compile"));
|
|
});
|
|
});
|
|
|
|
describe("formatSummaryComment", () => {
|
|
it("includes one-liner and body", () => {
|
|
const comment = formatSummaryComment({
|
|
oneLiner: "Added retry logic",
|
|
body: "Implemented exponential backoff",
|
|
});
|
|
assert.ok(comment.includes("**Summary:** Added retry logic"));
|
|
assert.ok(comment.includes("Implemented exponential backoff"));
|
|
});
|
|
|
|
it("wraps frontmatter in details block", () => {
|
|
const comment = formatSummaryComment({
|
|
frontmatter: { duration: "45m", key_files: ["a.ts"] },
|
|
});
|
|
assert.ok(comment.includes("<details>"));
|
|
assert.ok(comment.includes("duration:"));
|
|
});
|
|
|
|
it("handles empty data gracefully", () => {
|
|
const comment = formatSummaryComment({});
|
|
assert.equal(typeof comment, "string");
|
|
});
|
|
});
|
|
});
|