singularity-forge/src/resources/extensions/github-sync/tests/mapping.test.ts
TÂCHES 7c25036ed9 feat(gsd): GitHub sync extension — auto-sync to Issues, PRs, Milestones (#1603)
* 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>
2026-03-20 10:10:37 -06:00

104 lines
3.3 KiB
TypeScript

import { describe, it, beforeEach, afterEach } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import {
loadSyncMapping,
saveSyncMapping,
createEmptyMapping,
getMilestoneRecord,
getSliceRecord,
getTaskRecord,
getTaskIssueNumber,
setMilestoneRecord,
setSliceRecord,
setTaskRecord,
} from "../mapping.ts";
import type { SyncMapping, MilestoneSyncRecord, SliceSyncRecord, SyncEntityRecord } from "../types.ts";
describe("mapping", () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = mkdtempSync(join(tmpdir(), "gsd-sync-test-"));
mkdirSync(join(tmpDir, ".gsd"), { recursive: true });
});
afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true });
});
it("loadSyncMapping returns null when no file exists", () => {
const result = loadSyncMapping(tmpDir);
assert.equal(result, null);
});
it("round-trips save/load", () => {
const mapping = createEmptyMapping("owner/repo");
saveSyncMapping(tmpDir, mapping);
const loaded = loadSyncMapping(tmpDir);
assert.deepEqual(loaded, mapping);
});
it("createEmptyMapping has correct structure", () => {
const mapping = createEmptyMapping("owner/repo");
assert.equal(mapping.version, 1);
assert.equal(mapping.repo, "owner/repo");
assert.deepEqual(mapping.milestones, {});
assert.deepEqual(mapping.slices, {});
assert.deepEqual(mapping.tasks, {});
});
it("milestone record accessors work", () => {
const mapping = createEmptyMapping("owner/repo");
assert.equal(getMilestoneRecord(mapping, "M001"), null);
const record: MilestoneSyncRecord = {
issueNumber: 42,
ghMilestoneNumber: 1,
lastSyncedAt: "2025-01-01T00:00:00Z",
state: "open",
};
setMilestoneRecord(mapping, "M001", record);
assert.deepEqual(getMilestoneRecord(mapping, "M001"), record);
});
it("slice record accessors work", () => {
const mapping = createEmptyMapping("owner/repo");
assert.equal(getSliceRecord(mapping, "M001", "S01"), null);
const record: SliceSyncRecord = {
issueNumber: 0,
prNumber: 50,
branch: "milestone/M001/S01",
lastSyncedAt: "2025-01-01T00:00:00Z",
state: "open",
};
setSliceRecord(mapping, "M001", "S01", record);
assert.deepEqual(getSliceRecord(mapping, "M001", "S01"), record);
});
it("task record accessors work", () => {
const mapping = createEmptyMapping("owner/repo");
assert.equal(getTaskRecord(mapping, "M001", "S01", "T01"), null);
assert.equal(getTaskIssueNumber(mapping, "M001", "S01", "T01"), null);
const record: SyncEntityRecord = {
issueNumber: 43,
lastSyncedAt: "2025-01-01T00:00:00Z",
state: "open",
};
setTaskRecord(mapping, "M001", "S01", "T01", record);
assert.deepEqual(getTaskRecord(mapping, "M001", "S01", "T01"), record);
assert.equal(getTaskIssueNumber(mapping, "M001", "S01", "T01"), 43);
});
it("rejects mapping with wrong version", () => {
const mapping = createEmptyMapping("owner/repo");
(mapping as any).version = 2;
saveSyncMapping(tmpDir, mapping);
const loaded = loadSyncMapping(tmpDir);
assert.equal(loaded, null);
});
});