Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/resources/extensions/gsd/tests/integration/test-isolation.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
13 changes: 13 additions & 0 deletions src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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 });
}
});

Expand Down
21 changes: 21 additions & 0 deletions src/resources/extensions/gsd/tests/stash-pop-gsd-conflict.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
16 changes: 16 additions & 0 deletions src/resources/extensions/gsd/tests/worktree-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 ===");
Expand Down Expand Up @@ -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 });
});
35 changes: 26 additions & 9 deletions src/resources/extensions/gsd/tests/worktree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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");

// With milestone set, getMainBranch returns feature branch
setActiveMilestoneId(repo, "M001");
assert.deepStrictEqual(getMainBranch(repo), "my-feature",
"getMainBranch returns integration branch with 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();

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 });
}
Expand Down
10 changes: 10 additions & 0 deletions src/resources/extensions/gsd/worktree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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().
Expand Down
20 changes: 19 additions & 1 deletion src/tests/rtk-session-stats.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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: {
Expand Down
20 changes: 19 additions & 1 deletion src/tests/rtk.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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");
Expand Down
Loading