Skip to content

Commit 050f487

Browse files
committed
test(coverage): cover deterministic status helpers
Keep the coverage gate stable in CI by exercising parser and markdown formatting branches without depending on authenticated GitHub API responses.
1 parent e9f21c0 commit 050f487

6 files changed

Lines changed: 264 additions & 62 deletions

File tree

src/server/module-pin-hint-tool.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
22

33
import {
44
buildGoPseudoVersion,
5+
formatModulePinHintMarkdown,
56
formatPseudoVersionDate,
67
registerModulePinHintTool,
78
} from "./module-pin-hint-tool.js";
@@ -62,6 +63,26 @@ describe("buildGoPseudoVersion", () => {
6263
});
6364
});
6465

66+
describe("formatModulePinHintMarkdown", () => {
67+
test("renders the pseudo-version and go.mod snippet", () => {
68+
const text = formatModulePinHintMarkdown({
69+
owner: "Rethunk-AI",
70+
repo: "rethunk-github-mcp",
71+
ref: "main",
72+
resolvedSha: "0123456789abcdef0123456789abcdef01234567",
73+
committerDate: "2026-04-26T20:00:00Z",
74+
goPseudoVersion: "v0.0.0-20260426200000-0123456789ab",
75+
});
76+
77+
expect(text).toContain("# Go Pseudo-Version: Rethunk-AI/rethunk-github-mcp");
78+
expect(text).toContain("**Ref:** `main`");
79+
expect(text).toContain("**SHA:** `0123456789abcdef0123456789abcdef01234567`");
80+
expect(text).toContain(
81+
"require github.com/Rethunk-AI/rethunk-github-mcp v0.0.0-20260426200000-0123456789ab",
82+
);
83+
});
84+
});
85+
6586
// ---------------------------------------------------------------------------
6687
// registerModulePinHintTool execute paths (via captureTool)
6788
// ---------------------------------------------------------------------------

src/server/module-pin-hint-tool.ts

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,35 @@ export function buildGoPseudoVersion(committedDate: string, fullSha: string): st
3434
return `v0.0.0-${ts}-${sha12}`;
3535
}
3636

37+
export interface ModulePinHintResult {
38+
owner: string;
39+
repo: string;
40+
ref: string;
41+
resolvedSha: string;
42+
committerDate: string;
43+
goPseudoVersion: string;
44+
}
45+
46+
export function formatModulePinHintMarkdown(result: ModulePinHintResult): string {
47+
return [
48+
`# Go Pseudo-Version: ${result.owner}/${result.repo}`,
49+
"",
50+
`**Ref:** \`${result.ref}\``,
51+
`**SHA:** \`${result.resolvedSha}\``,
52+
`**Committed:** ${result.committerDate}`,
53+
"",
54+
"## Pseudo-version",
55+
"```",
56+
result.goPseudoVersion,
57+
"```",
58+
"",
59+
"## go.mod snippet",
60+
"```go",
61+
`require github.com/${result.owner}/${result.repo} ${result.goPseudoVersion}`,
62+
"```",
63+
].join("\n");
64+
}
65+
3766
// ---------------------------------------------------------------------------
3867
// GraphQL
3968
// ---------------------------------------------------------------------------
@@ -133,7 +162,7 @@ export function registerModulePinHintTool(server: FastMCP): void {
133162

134163
const goPseudoVersion = buildGoPseudoVersion(commit.committedDate, commit.oid);
135164

136-
const result = {
165+
const result: ModulePinHintResult = {
137166
owner,
138167
repo,
139168
ref: commit.resolvedRef,
@@ -144,26 +173,7 @@ export function registerModulePinHintTool(server: FastMCP): void {
144173

145174
if (args.format === "json") return jsonRespond(result);
146175

147-
// Markdown
148-
const lines = [
149-
`# Go Pseudo-Version: ${owner}/${repo}`,
150-
"",
151-
`**Ref:** \`${commit.resolvedRef}\``,
152-
`**SHA:** \`${commit.oid}\``,
153-
`**Committed:** ${commit.committedDate}`,
154-
"",
155-
"## Pseudo-version",
156-
"```",
157-
goPseudoVersion,
158-
"```",
159-
"",
160-
"## go.mod snippet",
161-
"```go",
162-
`require github.com/${owner}/${repo} ${goPseudoVersion}`,
163-
"```",
164-
];
165-
166-
return lines.join("\n");
176+
return formatModulePinHintMarkdown(result);
167177
} catch (err) {
168178
return errorRespond(classifyError(err));
169179
}

src/server/pin-drift-tool.test.ts

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
33
import { tmpdir } from "node:os";
44
import { join } from "node:path";
55

6-
import { registerPinDriftTool } from "./pin-drift-tool.js";
6+
import {
7+
parseGoMod,
8+
parsePackageJson,
9+
parseVersionsEnv,
10+
registerPinDriftTool,
11+
} from "./pin-drift-tool.js";
712
import { captureTool } from "./test-harness.js";
813

914
// ---------------------------------------------------------------------------
@@ -218,6 +223,49 @@ replace github.com/Rethunk-Tech/satcom => github.com/Rethunk-Tech/satcom v0.0.0-
218223
});
219224
});
220225

226+
describe("parseGoMod source parser", () => {
227+
test("reads real go.mod files and reports pins plus ambiguous replacements", () => {
228+
const dir = _makeTmpDir();
229+
_writeFile(
230+
dir,
231+
"go.mod",
232+
`
233+
module example.com/myapp
234+
235+
go 1.21
236+
237+
require (
238+
github.com/Rethunk-Tech/satcom v0.0.0-20260401000000-aaaaaaaaaaaa
239+
)
240+
241+
replace github.com/foo/bar => github.com/Rethunk-AI/bar v1.2.3
242+
replace github.com/foo/baz => github.com/Rethunk-AI/baz main
243+
`,
244+
);
245+
246+
const { pins, skipped } = parseGoMod(dir);
247+
248+
expect(pins).toContainEqual({
249+
source: "go.mod",
250+
owner: "Rethunk-AI",
251+
repo: "bar",
252+
pinnedRef: "v1.2.3",
253+
});
254+
expect(pins).toContainEqual({
255+
source: "go.mod",
256+
owner: "Rethunk-Tech",
257+
repo: "satcom",
258+
pinnedRef: "aaaaaaaaaaaa",
259+
});
260+
expect(skipped).toContainEqual({
261+
source: "go.mod",
262+
key: "replace github.com/Rethunk-AI/baz",
263+
value: "main",
264+
reason: "ambiguous_ref",
265+
});
266+
});
267+
});
268+
221269
describe("parsePackageJson", () => {
222270
test("parses GitHub shorthand dep", () => {
223271
const { pins } = parsePackageJsonFixture(
@@ -256,6 +304,48 @@ describe("parsePackageJson", () => {
256304
});
257305
});
258306

307+
describe("parsePackageJson source parser", () => {
308+
test("reads dependencies and devDependencies from package.json", () => {
309+
const dir = _makeTmpDir();
310+
_writeFile(
311+
dir,
312+
"package.json",
313+
JSON.stringify({
314+
dependencies: {
315+
"short-lib": "owner/repo#abc123def456",
316+
npm: "^1.0.0",
317+
},
318+
devDependencies: {
319+
"url-lib": "https://github.com/Rethunk-AI/some-lib.git#877f8d94448e",
320+
},
321+
}),
322+
);
323+
324+
const { pins, skipped } = parsePackageJson(dir);
325+
326+
expect(skipped).toEqual([]);
327+
expect(pins).toContainEqual({
328+
source: "package.json",
329+
owner: "owner",
330+
repo: "repo",
331+
pinnedRef: "abc123def456",
332+
});
333+
expect(pins).toContainEqual({
334+
source: "package.json",
335+
owner: "Rethunk-AI",
336+
repo: "some-lib",
337+
pinnedRef: "877f8d94448e",
338+
});
339+
});
340+
341+
test("returns no pins for malformed package.json", () => {
342+
const dir = _makeTmpDir();
343+
_writeFile(dir, "package.json", "{not json");
344+
345+
expect(parsePackageJson(dir)).toEqual({ pins: [], skipped: [] });
346+
});
347+
});
348+
259349
describe("parseVersionsEnv", () => {
260350
test("marks 40-char SHA _REF keys as ambiguous_repo", () => {
261351
const text = `BASTION_SATCOM_REF=877f8d94448e8cc843e83409dd0a59bb73562e45\n`;
@@ -287,6 +377,30 @@ describe("parseVersionsEnv", () => {
287377
});
288378
});
289379

380+
describe("parseVersionsEnv source parser", () => {
381+
test("reads SHA-like refs from scripts/versions.env", () => {
382+
const dir = _makeTmpDir();
383+
_writeFile(
384+
dir,
385+
"scripts/versions.env",
386+
`
387+
SATCOM_REF=0123456789abcdef0123456789abcdef01234567
388+
SATCOM_BRANCH=main
389+
MFA_VERSION=v1.2.3
390+
`,
391+
);
392+
393+
expect(parseVersionsEnv(dir).skipped).toEqual([
394+
{
395+
source: "scripts/versions.env",
396+
key: "SATCOM_REF",
397+
value: "0123456789abcdef0123456789abcdef01234567",
398+
reason: "ambiguous_repo",
399+
},
400+
]);
401+
});
402+
});
403+
290404
// ---------------------------------------------------------------------------
291405
// pin_drift tool integration (via captureTool)
292406
//

src/server/pin-drift-tool.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { FormatSchema } from "./schemas.js";
1414
// Types
1515
// ---------------------------------------------------------------------------
1616

17-
type PinSource = "go.mod" | ".gitmodules" | "scripts/versions.env" | "package.json";
17+
export type PinSource = "go.mod" | ".gitmodules" | "scripts/versions.env" | "package.json";
1818

1919
interface PinEntry {
2020
source: PinSource;
@@ -32,7 +32,7 @@ interface PinEntry {
3232
error?: McpErrorEnvelope;
3333
}
3434

35-
interface SkippedEntry {
35+
export interface SkippedEntry {
3636
source: PinSource;
3737
key: string;
3838
value: string;
@@ -65,15 +65,15 @@ export function pseudoVersionSha(version: string): string | undefined {
6565
return m?.[1];
6666
}
6767

68-
interface RawPin {
68+
export interface RawPin {
6969
source: PinSource;
7070
owner: string;
7171
repo: string;
7272
pinnedRef: string; // SHA or branch/tag
7373
}
7474

7575
/** Parse go.mod for replace directives and pseudo-version requires pointing at GitHub. */
76-
function parseGoMod(localPath: string): { pins: RawPin[]; skipped: SkippedEntry[] } {
76+
export function parseGoMod(localPath: string): { pins: RawPin[]; skipped: SkippedEntry[] } {
7777
const goModPath = join(localPath, "go.mod");
7878
if (!existsSync(goModPath)) return { pins: [], skipped: [] };
7979

@@ -196,7 +196,7 @@ function parseGitModules(localPath: string): { pins: RawPin[]; skipped: SkippedE
196196
}
197197

198198
/** Parse scripts/versions.env for KEY=VALUE lines where value is a 40-char hex SHA. */
199-
function parseVersionsEnv(localPath: string): { skipped: SkippedEntry[] } {
199+
export function parseVersionsEnv(localPath: string): { skipped: SkippedEntry[] } {
200200
const envPath = join(localPath, "scripts", "versions.env");
201201
if (!existsSync(envPath)) return { skipped: [] };
202202

@@ -223,7 +223,7 @@ function parseVersionsEnv(localPath: string): { skipped: SkippedEntry[] } {
223223
}
224224

225225
/** Parse package.json for dependencies pinned to GitHub URLs. */
226-
function parsePackageJson(localPath: string): { pins: RawPin[]; skipped: SkippedEntry[] } {
226+
export function parsePackageJson(localPath: string): { pins: RawPin[]; skipped: SkippedEntry[] } {
227227
const pkgPath = join(localPath, "package.json");
228228
if (!existsSync(pkgPath)) return { pins: [], skipped: [] };
229229

src/server/repo-status-tool.test.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, test } from "bun:test";
22

3-
import { registerRepoStatusTool } from "./repo-status-tool.js";
3+
import { formatRepoStatusMarkdown, registerRepoStatusTool } from "./repo-status-tool.js";
44
import { MAX_REPOS_PER_REQUEST } from "./schemas.js";
55
import { captureTool } from "./test-harness.js";
66
import { timeAgo } from "./utils.js";
@@ -39,6 +39,62 @@ describe("timeAgo", () => {
3939
});
4040
});
4141

42+
describe("formatRepoStatusMarkdown", () => {
43+
test("renders failing CI, draft PRs, and local state", () => {
44+
const text = formatRepoStatusMarkdown([
45+
{
46+
owner: "Rethunk-AI",
47+
repo: "rethunk-github-mcp",
48+
defaultBranch: "main",
49+
latestCommit: {
50+
sha7: "abc1234",
51+
message: "tighten status output",
52+
author: "damon",
53+
date: "2h ago",
54+
},
55+
ci: {
56+
status: "failure",
57+
failedChecks: [{ name: "Unit tests", conclusion: "FAILURE" }],
58+
},
59+
openPRs: 3,
60+
draftPRs: 1,
61+
openIssues: 5,
62+
local: { branch: "main", dirty: 0, ahead: 1, behind: 0 },
63+
},
64+
]);
65+
66+
expect(text).toContain("## Rethunk-AI/rethunk-github-mcp (main)");
67+
expect(text).toContain("Latest: `abc1234` tighten status output");
68+
expect(text).toContain("CI: failing: Unit tests");
69+
expect(text).toContain("PRs: 3 open (1 draft) · Issues: 5 open");
70+
expect(text).toContain("[Local: main, 0 dirty, 1 ahead / 0 behind]");
71+
});
72+
73+
test("renders errors and missing CI", () => {
74+
const text = formatRepoStatusMarkdown([
75+
{
76+
owner: "Rethunk-AI",
77+
repo: "missing",
78+
error: {
79+
code: "NOT_FOUND",
80+
message: "Repository not found.",
81+
retryable: false,
82+
},
83+
},
84+
{
85+
owner: "Rethunk-AI",
86+
repo: "no-ci",
87+
openPRs: 0,
88+
openIssues: 0,
89+
},
90+
]);
91+
92+
expect(text).toContain("Error (NOT_FOUND): Repository not found.");
93+
expect(text).toContain("## Rethunk-AI/no-ci (?)");
94+
expect(text).toContain("CI: not configured");
95+
});
96+
});
97+
4298
// ---------------------------------------------------------------------------
4399
// repo_status tool integration (via captureTool)
44100
//

0 commit comments

Comments
 (0)