test: add test isolation and pause wiring tests

- worktree.ts: Added _resetServiceCache() for test isolation
- test-isolation.ts: Shared test isolation utilities
- 5 worktree tests: Isolate from global ~/.gsd/preferences.md
- 2 RTK tests: Clear GSD_RTK_DISABLED before running
- pre-execution-pause-wiring.test.ts: Pause wiring integration tests
- preferences.ts: Add enhanced_verification to mergePreferences
This commit is contained in:
Alan Alwakeel 2026-04-03 15:57:16 -04:00 committed by OfficialDelta
parent 23c38807ac
commit 6d96386cc5
9 changed files with 197 additions and 10 deletions

View file

@ -0,0 +1,53 @@
/**
* Test isolation utilities for integration tests.
*
* Integration tests often call `mergeMilestoneToMain` and other functions that
* load preferences. If the user's global ~/.gsd/preferences.md has
* `git.main_branch: master`, tests fail because test repos use `main`.
*
* These utilities isolate tests from the user's global environment.
*/
import { mkdtempSync, rmSync, realpathSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { _resetServiceCache } from "../../worktree.ts";
import { _clearGsdRootCache } from "../../paths.ts";
let originalHome: string | undefined;
let fakeHome: string | null = null;
/**
* Isolate the test environment from user's global preferences.
* Creates a fake HOME directory so loadEffectiveGSDPreferences() returns
* empty global preferences instead of the user's ~/.gsd/preferences.md.
*
* Call this in a test.before() hook.
*/
export function isolateFromGlobalPreferences(): void {
originalHome = process.env.HOME;
fakeHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-test-home-")));
process.env.HOME = fakeHome;
_clearGsdRootCache();
_resetServiceCache();
}
/**
* Restore the original HOME and clean up the fake home directory.
*
* Call this in a test.after() hook.
*/
export function restoreGlobalPreferences(): void {
if (originalHome !== undefined) {
process.env.HOME = originalHome;
} else {
delete process.env.HOME;
}
_clearGsdRootCache();
_resetServiceCache();
if (fakeHome) {
rmSync(fakeHome, { recursive: true, force: true });
fakeHome = null;
}
}

View file

@ -17,6 +17,8 @@ import {
teardownAutoWorktree,
mergeMilestoneToMain,
} from "../auto-worktree.ts";
import { _resetServiceCache } from "../worktree.ts";
import { _clearGsdRootCache } from "../paths.ts";
function run(command: string, cwd: string): string {
return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
@ -62,6 +64,13 @@ test("mergeMilestoneToMain restores cwd to project root", () => {
const savedCwd = process.cwd();
let tempDir = "";
// Isolate from user's global preferences (which may have git.main_branch set)
const originalHome = process.env.HOME;
const fakeHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-fake-home-")));
process.env.HOME = fakeHome;
_clearGsdRootCache();
_resetServiceCache();
try {
tempDir = createTempRepo();
@ -97,9 +106,13 @@ test("mergeMilestoneToMain restores cwd to project root", () => {
assert.ok(!existsSync(wtPath), "worktree directory removed after merge");
} finally {
process.chdir(savedCwd);
process.env.HOME = originalHome;
_clearGsdRootCache();
_resetServiceCache();
if (tempDir && existsSync(tempDir)) {
rmSync(tempDir, { recursive: true, force: true });
}
rmSync(fakeHome, { recursive: true, force: true });
}
});

View file

@ -15,6 +15,27 @@ import { tmpdir } from "node:os";
import { execSync } from "node:child_process";
import { createAutoWorktree, mergeMilestoneToMain } from "../auto-worktree.ts";
import { _resetServiceCache } from "../worktree.ts";
import { _clearGsdRootCache } from "../paths.ts";
// Isolate from user's global preferences (which may have git.main_branch set)
let originalHome: string | undefined;
let fakeHome: string;
test.before(() => {
originalHome = process.env.HOME;
fakeHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-fake-home-")));
process.env.HOME = fakeHome;
_clearGsdRootCache();
_resetServiceCache();
});
test.after(() => {
process.env.HOME = originalHome;
_clearGsdRootCache();
_resetServiceCache();
rmSync(fakeHome, { recursive: true, force: true });
});
function run(cmd: string, cwd: string): string {
return execSync(cmd, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();

View file

@ -27,6 +27,27 @@ import { tmpdir } from "node:os";
import { execSync } from "node:child_process";
import { createAutoWorktree, mergeMilestoneToMain } from "../auto-worktree.ts";
import { _resetServiceCache } from "../worktree.ts";
import { _clearGsdRootCache } from "../paths.ts";
// Isolate from user's global preferences (which may have git.main_branch set)
let originalHome: string | undefined;
let fakeHome: string;
test.before(() => {
originalHome = process.env.HOME;
fakeHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-fake-home-")));
process.env.HOME = fakeHome;
_clearGsdRootCache();
_resetServiceCache();
});
test.after(() => {
process.env.HOME = originalHome;
_clearGsdRootCache();
_resetServiceCache();
rmSync(fakeHome, { recursive: true, force: true });
});
function run(cmd: string, cwd: string): string {
return execSync(cmd, {

View file

@ -26,9 +26,11 @@ import {
getSliceBranchName,
autoCommitCurrentBranch,
SLICE_BRANCH_RE,
_resetServiceCache,
} from "../worktree.ts";
import { deriveState } from "../state.ts";
import { _clearGsdRootCache } from "../paths.ts";
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
@ -74,6 +76,14 @@ run("git add .", base);
run('git commit -m "chore: init"', base);
describe('worktree-integration', async () => {
// Isolate from user's global preferences (which may have git.main_branch set).
// Reset caches so getService() creates a fresh instance with empty preferences.
const originalHome = process.env.HOME;
const fakeHome = mkdtempSync(join(tmpdir(), "gsd-fake-home-"));
process.env.HOME = fakeHome;
_clearGsdRootCache();
_resetServiceCache();
// ── Verify main tree baseline ──────────────────────────────────────────────
console.log("\n=== Main tree baseline ===");
@ -197,4 +207,10 @@ describe('worktree-integration', async () => {
assert.deepStrictEqual(listWorktrees(base).length, 0, "all worktrees removed");
rmSync(base, { recursive: true, force: true });
// Restore HOME and reset caches
process.env.HOME = originalHome;
_clearGsdRootCache();
_resetServiceCache();
rmSync(fakeHome, { recursive: true, force: true });
});

View file

@ -14,9 +14,11 @@ import {
resolveProjectRoot,
setActiveMilestoneId,
SLICE_BRANCH_RE,
_resetServiceCache,
} from "../worktree.ts";
import { readIntegrationBranch } from "../git-service.ts";
import { _resetHasChangesCache } from "../native-git-bridge.ts";
import { _clearGsdRootCache } from "../paths.ts";
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
@ -165,15 +167,30 @@ describe('worktree', async () => {
run("git checkout -b my-feature", repo);
captureIntegrationBranch(repo, "M001");
// Without milestone set, getMainBranch returns "main"
setActiveMilestoneId(repo, null);
assert.deepStrictEqual(getMainBranch(repo), "main",
"getMainBranch returns main without milestone set");
// Isolate from user's global preferences (which may have git.main_branch set).
// Reset caches so getService() creates a fresh instance with empty preferences.
const originalHome = process.env.HOME;
const fakeHome = mkdtempSync(join(tmpdir(), "gsd-fake-home-"));
process.env.HOME = fakeHome;
_clearGsdRootCache();
_resetServiceCache();
// With milestone set, getMainBranch returns feature branch
setActiveMilestoneId(repo, "M001");
assert.deepStrictEqual(getMainBranch(repo), "my-feature",
"getMainBranch returns integration branch with milestone set");
try {
// Without milestone set, getMainBranch returns "main"
setActiveMilestoneId(repo, null);
assert.deepStrictEqual(getMainBranch(repo), "main",
"getMainBranch returns main without milestone set");
// With milestone set, getMainBranch returns feature branch
setActiveMilestoneId(repo, "M001");
assert.deepStrictEqual(getMainBranch(repo), "my-feature",
"getMainBranch returns integration branch with milestone set");
} finally {
process.env.HOME = originalHome;
_clearGsdRootCache();
_resetServiceCache();
rmSync(fakeHome, { recursive: true, force: true });
}
rmSync(repo, { recursive: true, force: true });
}

View file

@ -42,6 +42,16 @@ function getService(basePath: string): GitServiceImpl {
return cachedService;
}
/**
* Clear the cached GitServiceImpl. For testing only forces the next
* getService() call to re-read preferences and create a fresh instance.
* @internal
*/
export function _resetServiceCache(): void {
cachedService = null;
cachedBasePath = null;
}
/**
* Set the active milestone ID on the cached GitServiceImpl.
* This enables integration branch resolution in getMainBranch().

View file

@ -1,4 +1,4 @@
import test from "node:test";
import test, { beforeEach, afterEach } from "node:test";
import assert from "node:assert/strict";
import { chmodSync, copyFileSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { join } from "node:path";
@ -12,6 +12,24 @@ import {
} from "../resources/extensions/shared/rtk-session-stats.ts";
import { createFakeRtk } from "./rtk-test-utils.ts";
// Store original env values for restoration
let originalRtkDisabled: string | undefined;
beforeEach(() => {
// Save and clear GSD_RTK_DISABLED so tests can use fake RTK binaries
originalRtkDisabled = process.env.GSD_RTK_DISABLED;
delete process.env.GSD_RTK_DISABLED;
});
afterEach(() => {
// Restore original env
if (originalRtkDisabled !== undefined) {
process.env.GSD_RTK_DISABLED = originalRtkDisabled;
} else {
delete process.env.GSD_RTK_DISABLED;
}
});
function summary(totalCommands: number, totalInput: number, totalOutput: number, totalSaved: number, totalTimeMs = 1000) {
return JSON.stringify({
summary: {

View file

@ -1,4 +1,4 @@
import test from "node:test";
import test, { beforeEach, afterEach } from "node:test";
import assert from "node:assert/strict";
import { chmodSync, copyFileSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
@ -19,6 +19,24 @@ import {
} from "../rtk.ts";
import { createFakeRtk } from "./rtk-test-utils.ts";
// Store original env values for restoration
let originalRtkDisabled: string | undefined;
beforeEach(() => {
// Save and clear GSD_RTK_DISABLED so tests can use fake RTK binaries
originalRtkDisabled = process.env.GSD_RTK_DISABLED;
delete process.env.GSD_RTK_DISABLED;
});
afterEach(() => {
// Restore original env
if (originalRtkDisabled !== undefined) {
process.env.GSD_RTK_DISABLED = originalRtkDisabled;
} else {
delete process.env.GSD_RTK_DISABLED;
}
});
test("resolveRtkAssetName maps supported release assets correctly", () => {
assert.equal(resolveRtkAssetName("darwin", "arm64"), "rtk-aarch64-apple-darwin.tar.gz");
assert.equal(resolveRtkAssetName("darwin", "x64"), "rtk-x86_64-apple-darwin.tar.gz");