singularity-forge/src/resources/extensions/gsd/tests/diff-context.test.ts
Jeremy McSpadden 973b8992e5 feat: add GitHub API client, diff-aware context, tiktoken token counting
- Add GitHub API integration via @octokit/rest: createGitHubClient,
  getRepoInfo (parses HTTPS/SSH remotes), createPullRequest,
  getPullRequest, listPullRequestReviews, createIssueComment
- Add diff-aware context module: getRecentlyChangedFiles,
  getChangedFilesWithContext, rankFilesByRelevance — prioritizes
  recently-changed files for context window budget allocation
- Add accurate token counting via tiktoken: countTokens (async),
  countTokensSync, initTokenCounter — falls back to chars/4 heuristic
  when tiktoken is unavailable
- 27 new tests across 3 test files
2026-03-16 13:50:00 -05:00

136 lines
5 KiB
TypeScript

/**
* Unit tests for diff-context.ts — diff-aware context module.
* Tests git-based file discovery and relevance ranking.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { tmpdir } from "node:os";
import { mkdtempSync } from "node:fs";
import { join } from "node:path";
import {
getRecentlyChangedFiles,
getChangedFilesWithContext,
rankFilesByRelevance,
} from "../diff-context.js";
// ─── getRecentlyChangedFiles ────────────────────────────────────────────────
describe("diff-context: getRecentlyChangedFiles", () => {
it("returns an array of file paths in the current git repo", async () => {
// Use the project root — guaranteed to be a git repo
const cwd = process.cwd();
const files = await getRecentlyChangedFiles(cwd);
assert.ok(Array.isArray(files), "should return an array");
// The result may be empty if the repo is totally clean with no recent
// commits, but the function should not throw.
});
it("respects maxFiles option", async () => {
const cwd = process.cwd();
const files = await getRecentlyChangedFiles(cwd, { maxFiles: 3 });
assert.ok(files.length <= 3, "should not exceed maxFiles");
});
it("returns empty array for non-git directory", async () => {
const tmp = mkdtempSync(join(tmpdir(), "diff-ctx-test-"));
const files = await getRecentlyChangedFiles(tmp);
assert.deepStrictEqual(files, [], "should return empty array for non-git dir");
});
it("returns deduplicated paths", async () => {
const cwd = process.cwd();
const files = await getRecentlyChangedFiles(cwd, { maxFiles: 100 });
const unique = new Set(files);
assert.equal(files.length, unique.size, "should have no duplicates");
});
});
// ─── getChangedFilesWithContext ─────────────────────────────────────────────
describe("diff-context: getChangedFilesWithContext", () => {
it("returns array of ChangedFileInfo objects", async () => {
const cwd = process.cwd();
const infos = await getChangedFilesWithContext(cwd);
assert.ok(Array.isArray(infos), "should return an array");
for (const info of infos) {
assert.ok(typeof info.path === "string", "path should be a string");
assert.ok(
["modified", "added", "deleted", "staged"].includes(info.changeType),
`changeType should be valid, got: ${info.changeType}`,
);
if (info.linesChanged !== undefined) {
assert.ok(typeof info.linesChanged === "number", "linesChanged should be a number");
}
}
});
it("returns empty array for non-git directory", async () => {
const tmp = mkdtempSync(join(tmpdir(), "diff-ctx-test2-"));
const infos = await getChangedFilesWithContext(tmp);
assert.deepStrictEqual(infos, [], "should return empty array for non-git dir");
});
});
// ─── rankFilesByRelevance ───────────────────────────────────────────────────
describe("diff-context: rankFilesByRelevance", () => {
it("places changed files before unchanged files", () => {
const allFiles = ["a.ts", "b.ts", "c.ts", "d.ts"];
const changed = ["c.ts", "a.ts"];
const ranked = rankFilesByRelevance(allFiles, changed);
// Changed files come first, sorted by changedFiles priority (c before a)
assert.equal(ranked[0], "c.ts");
assert.equal(ranked[1], "a.ts");
// Unchanged files follow in original order
assert.equal(ranked[2], "b.ts");
assert.equal(ranked[3], "d.ts");
});
it("preserves order of changed files based on changedFiles priority", () => {
const allFiles = ["x.ts", "y.ts", "z.ts", "w.ts"];
const changed = ["z.ts", "x.ts"]; // z has higher priority (index 0)
const ranked = rankFilesByRelevance(allFiles, changed);
assert.equal(ranked[0], "z.ts", "z.ts should be first (higher priority in changedFiles)");
assert.equal(ranked[1], "x.ts", "x.ts should be second");
});
it("returns unchanged files in original order when no changed files match", () => {
const allFiles = ["a.ts", "b.ts", "c.ts"];
const changed = ["x.ts", "y.ts"]; // none match
const ranked = rankFilesByRelevance(allFiles, changed);
assert.deepStrictEqual(ranked, ["a.ts", "b.ts", "c.ts"]);
});
it("handles empty inputs gracefully", () => {
assert.deepStrictEqual(rankFilesByRelevance([], []), []);
assert.deepStrictEqual(rankFilesByRelevance(["a.ts"], []), ["a.ts"]);
assert.deepStrictEqual(rankFilesByRelevance([], ["a.ts"]), []);
});
it("handles all files being changed", () => {
const allFiles = ["a.ts", "b.ts"];
const changed = ["b.ts", "a.ts"];
const ranked = rankFilesByRelevance(allFiles, changed);
// Both are changed, so sorted by changedFiles order: b first, then a
assert.equal(ranked[0], "b.ts");
assert.equal(ranked[1], "a.ts");
assert.equal(ranked.length, 2);
});
});