Skip to content
Merged
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
10 changes: 9 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { homedir } from 'node:os';

/**
* Get the user's home directory (cross-platform).
*
* Delegates to Node's os.homedir() rather than reading process.env.HOME
* directly: on Windows, os.homedir() ignores HOME and resolves via
* USERPROFILE, so it isn't affected by shells (e.g. Git Bash/MSYS) that
* mistranslate a misconfigured HOME into a bare drive root like "C:\\"
* (see EntityProcess/allagents#433).
*/
export function getHomeDir(): string {
return process.env.HOME || process.env.USERPROFILE || '~';
return homedir();
}

/**
Expand Down
6 changes: 4 additions & 2 deletions tests/e2e/user-scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { addUserPlugin, removeUserPlugin, getUserWorkspaceConfig } from '../../s
import { addPlugin } from '../../src/core/workspace-modify.js';
import { syncUserWorkspace, syncWorkspace } from '../../src/core/sync.js';
import { initWorkspace } from '../../src/core/workspace.js';
import { stubHomeDir } from '../helpers/env.js';

// E2E tests require network access and gh authentication.
// They are skipped by default; run with ALLAGENTS_E2E=1 to enable.
Expand All @@ -17,6 +18,7 @@ describe.skipIf(!e2eEnabled)('E2E: user scope vs project scope', () => {
let tempProject: string;
let originalHome: string;
let originalGhConfigDir: string | undefined;
let restoreHomeDir: () => void;

beforeEach(async () => {
tempHome = await mkdtemp(join(tmpdir(), 'allagents-e2e-home-'));
Expand All @@ -29,11 +31,11 @@ describe.skipIf(!e2eEnabled)('E2E: user scope vs project scope', () => {
process.env.GH_CONFIG_DIR = join(originalHome, '.config', 'gh');
}

process.env.HOME = tempHome;
restoreHomeDir = stubHomeDir(tempHome);
});

afterEach(async () => {
process.env.HOME = originalHome;
restoreHomeDir();
if (originalGhConfigDir !== undefined) {
process.env.GH_CONFIG_DIR = originalGhConfigDir;
} else {
Expand Down
22 changes: 22 additions & 0 deletions tests/helpers/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Temporarily override the resolved home directory for a test.
*
* os.homedir() (what src/constants.ts#getHomeDir now delegates to) reads
* $HOME on POSIX and %USERPROFILE% on Windows — never both — so tests that
* only stubbed HOME silently stopped taking effect on Windows. Stubbing both
* keeps tests platform-independent. Returns a restore function that deletes
* (rather than stringifies `undefined` into) any var that wasn't originally set.
*/
export function stubHomeDir(path: string): () => void {
const originalHome = process.env.HOME;
const originalUserProfile = process.env.USERPROFILE;
process.env.HOME = path;
process.env.USERPROFILE = path;

return () => {
if (originalHome === undefined) delete process.env.HOME;
else process.env.HOME = originalHome;
if (originalUserProfile === undefined) delete process.env.USERPROFILE;
else process.env.USERPROFILE = originalUserProfile;
};
}
33 changes: 33 additions & 0 deletions tests/unit/constants.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, test, afterEach } from 'bun:test';
import { getHomeDir } from '../../src/constants.js';

describe('getHomeDir', () => {
const originalHome = process.env.HOME;
const originalUserProfile = process.env.USERPROFILE;

afterEach(() => {
if (originalHome === undefined) delete process.env.HOME;
else process.env.HOME = originalHome;
if (originalUserProfile === undefined) delete process.env.USERPROFILE;
else process.env.USERPROFILE = originalUserProfile;
});

// Windows-only: os.homedir() ignores HOME entirely on win32 (uses USERPROFILE),
// so this needs HOME and USERPROFILE set to *different* values to prove the
// point — stubHomeDir (which sets both to the same path) doesn't apply here.
// On POSIX, os.homedir() reads $HOME directly, so a mistranslated HOME isn't
// a platform-level failure mode there the way it is on Windows.
test.skipIf(process.platform !== 'win32')(
'ignores a HOME env var that has been mistranslated to a bare drive root',
() => {
// Reproduces the Git-Bash/MSYS failure mode: a misconfigured HOME=/c gets
// translated to the literal Windows path "C:\\" for the spawned node process.
// Trusting it would make every user-scope sync operation treat the whole
// drive as "home" (see EntityProcess/allagents#433).
process.env.HOME = 'C:\\';
process.env.USERPROFILE = 'C:\\Users\\realuser';

expect(getHomeDir()).toBe('C:\\Users\\realuser');
},
);
});
8 changes: 4 additions & 4 deletions tests/unit/core/marketplace-add-branch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
import { mkdirSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { stubHomeDir } from '../../helpers/env.js';

// Track clone calls to verify arguments
const cloneCalls: Array<{ url: string; dest: string; ref?: string }> = [];
Expand Down Expand Up @@ -49,19 +50,18 @@ mock.module('simple-git', () => ({
const { addMarketplace, loadRegistry } = await import('../../../src/core/marketplace.js');

describe('addMarketplace branch support', () => {
let originalHome: string | undefined;
let restoreHomeDir: () => void;
let testHome: string;

beforeEach(() => {
originalHome = process.env.HOME;
testHome = join(tmpdir(), `marketplace-add-branch-test-${Date.now()}`);
process.env.HOME = testHome;
restoreHomeDir = stubHomeDir(testHome);
mkdirSync(join(testHome, '.allagents'), { recursive: true });
cloneCalls.length = 0;
});

afterEach(() => {
process.env.HOME = originalHome;
restoreHomeDir();
rmSync(testHome, { recursive: true, force: true });
});

Expand Down
8 changes: 4 additions & 4 deletions tests/unit/core/marketplace-auto-update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { stubHomeDir } from '../../helpers/env.js';

// Track calls
const pullCalls: Array<{ path: string }> = [];
Expand Down Expand Up @@ -55,21 +56,20 @@ const {
} = await import('../../../src/core/marketplace.js');

describe('resolvePluginSpecWithAutoRegister auto-updates marketplace', () => {
let originalHome: string | undefined;
let restoreHomeDir: () => void;
let testHome: string;

beforeEach(() => {
originalHome = process.env.HOME;
testHome = join(tmpdir(), `marketplace-auto-update-test-${Date.now()}`);
process.env.HOME = testHome;
restoreHomeDir = stubHomeDir(testHome);
pullCalls.length = 0;
simpleGitCalls.length = 0;
pullShouldFail = false;
resetUpdatedMarketplaceCache();
});

afterEach(() => {
process.env.HOME = originalHome;
restoreHomeDir();
rmSync(testHome, { recursive: true, force: true });
});

Expand Down
8 changes: 4 additions & 4 deletions tests/unit/core/marketplace-branch-separation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:te
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { stubHomeDir } from '../../helpers/env.js';

// Track cloneTo calls
const cloneToCalls: Array<{ url: string; path: string; branch?: string }> = [];
Expand Down Expand Up @@ -56,21 +57,20 @@ const {
} = await import('../../../src/core/marketplace.js');

describe('branch separation — each branch is a separate marketplace', () => {
let originalHome: string | undefined;
let restoreHomeDir: () => void;
let testHome: string;
let consoleLogSpy: ReturnType<typeof spyOn>;

beforeEach(() => {
originalHome = process.env.HOME;
testHome = join(tmpdir(), `marketplace-branch-sep-test-${Date.now()}`);
process.env.HOME = testHome;
restoreHomeDir = stubHomeDir(testHome);
mkdirSync(join(testHome, '.allagents'), { recursive: true });
cloneToCalls.length = 0;
consoleLogSpy = spyOn(console, 'log').mockImplementation(() => {});
});

afterEach(() => {
process.env.HOME = originalHome;
restoreHomeDir();
rmSync(testHome, { recursive: true, force: true });
consoleLogSpy.mockRestore();
});
Expand Down
8 changes: 4 additions & 4 deletions tests/unit/core/marketplace-dedup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:te
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { stubHomeDir } from '../../helpers/env.js';

// Track cloneTo calls
const cloneToCalls: Array<{ url: string; path: string; branch?: string }> = [];
Expand Down Expand Up @@ -51,15 +52,14 @@ const {
} = await import('../../../src/core/marketplace.js');

describe('marketplace deduplication', () => {
let originalHome: string | undefined;
let restoreHomeDir: () => void;
let testHome: string;
let consoleLogSpy: ReturnType<typeof spyOn>;
let logMessages: string[];

beforeEach(() => {
originalHome = process.env.HOME;
testHome = join(tmpdir(), `marketplace-dedup-test-${Date.now()}`);
process.env.HOME = testHome;
restoreHomeDir = stubHomeDir(testHome);
cloneToCalls.length = 0;
resetAutoRegisterCache();

Expand All @@ -71,7 +71,7 @@ describe('marketplace deduplication', () => {
});

afterEach(() => {
process.env.HOME = originalHome;
restoreHomeDir();
rmSync(testHome, { recursive: true, force: true });
consoleLogSpy.mockRestore();
});
Expand Down
8 changes: 4 additions & 4 deletions tests/unit/core/marketplace-refresh.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { stubHomeDir } from '../../helpers/env.js';

// Track cloneTo calls to verify refresh behavior
const cloneToCalls: Array<{ url: string; path: string; branch?: string }> = [];
Expand Down Expand Up @@ -32,18 +33,17 @@ const { resolvePluginSpecWithAutoRegister } = await import(
);

describe('resolvePluginSpecWithAutoRegister refresh', () => {
let originalHome: string | undefined;
let restoreHomeDir: () => void;
let testHome: string;

beforeEach(() => {
originalHome = process.env.HOME;
testHome = join(tmpdir(), `marketplace-refresh-test-${Date.now()}`);
process.env.HOME = testHome;
restoreHomeDir = stubHomeDir(testHome);
cloneToCalls.length = 0;
});

afterEach(() => {
process.env.HOME = originalHome;
restoreHomeDir();
rmSync(testHome, { recursive: true, force: true });
});

Expand Down
8 changes: 4 additions & 4 deletions tests/unit/core/marketplace-remove-cascade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@ import { removeMarketplace, saveRegistry } from '../../../src/core/marketplace.j
import type { MarketplaceRegistry } from '../../../src/core/marketplace.js';
import { WORKSPACE_CONFIG_FILE } from '../../../src/constants.js';
import type { WorkspaceConfig } from '../../../src/models/workspace-config.js';
import { stubHomeDir } from '../../helpers/env.js';

describe('removeMarketplace cascade', () => {
let testDir: string;
let originalHome: string | undefined;
let restoreHomeDir: () => void;

beforeEach(async () => {
testDir = await mkdtemp(join(tmpdir(), 'allagents-cascade-test-'));
originalHome = process.env.HOME;
process.env.HOME = testDir;
restoreHomeDir = stubHomeDir(testDir);
});

afterEach(async () => {
process.env.HOME = originalHome;
restoreHomeDir();
await rm(testDir, { recursive: true, force: true });
});

Expand Down
22 changes: 10 additions & 12 deletions tests/unit/core/marketplace-scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
import { mkdirSync, writeFileSync, rmSync, readFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { stubHomeDir } from '../../helpers/env.js';

// Mock git module before importing marketplace (needed for addMarketplace tests)
mock.module('../../../src/core/git.js', () => ({
Expand Down Expand Up @@ -344,22 +345,21 @@ describe('scope-aware registry loading and saving', () => {
});

describe('addMarketplace with scope', () => {
let originalHome: string | undefined;
let restoreHomeDir: () => void;
let testHome: string;
let tmpProject: string;

beforeEach(() => {
originalHome = process.env.HOME;
testHome = join(tmpdir(), `marketplace-scope-add-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
process.env.HOME = testHome;
restoreHomeDir = stubHomeDir(testHome);
mkdirSync(join(testHome, '.allagents'), { recursive: true });

tmpProject = join(tmpdir(), `marketplace-scope-project-${Date.now()}-${Math.random().toString(36).slice(2)}`);
mkdirSync(join(tmpProject, '.allagents'), { recursive: true });
});

afterEach(() => {
process.env.HOME = originalHome;
restoreHomeDir();
rmSync(testHome, { recursive: true, force: true });
rmSync(tmpProject, { recursive: true, force: true });
});
Expand Down Expand Up @@ -407,7 +407,7 @@ describe('addMarketplace with scope', () => {
});

describe('removeMarketplace with scope', () => {
let originalHome: string | undefined;
let restoreHomeDir: () => void;
let testHome: string;
let tmpProject: string;
let userRegistryPath: string;
Expand All @@ -420,9 +420,8 @@ describe('removeMarketplace with scope', () => {
});

beforeEach(() => {
originalHome = process.env.HOME;
testHome = join(tmpdir(), `marketplace-scope-remove-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
process.env.HOME = testHome;
restoreHomeDir = stubHomeDir(testHome);
mkdirSync(join(testHome, '.allagents'), { recursive: true });

tmpProject = join(tmpdir(), `marketplace-scope-remove-project-${Date.now()}-${Math.random().toString(36).slice(2)}`);
Expand All @@ -433,7 +432,7 @@ describe('removeMarketplace with scope', () => {
});

afterEach(() => {
process.env.HOME = originalHome;
restoreHomeDir();
rmSync(testHome, { recursive: true, force: true });
rmSync(tmpProject, { recursive: true, force: true });
});
Expand Down Expand Up @@ -529,22 +528,21 @@ describe('removeMarketplace with scope', () => {
});

describe('runtime resolution with merged registries', () => {
let originalHome: string | undefined;
let restoreHomeDir: () => void;
let testHome: string;
let tmpProject: string;

beforeEach(() => {
originalHome = process.env.HOME;
testHome = join(tmpdir(), `marketplace-resolve-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
process.env.HOME = testHome;
restoreHomeDir = stubHomeDir(testHome);
mkdirSync(join(testHome, '.allagents'), { recursive: true });

tmpProject = join(tmpdir(), `marketplace-resolve-project-${Date.now()}-${Math.random().toString(36).slice(2)}`);
mkdirSync(join(tmpProject, '.allagents'), { recursive: true });
});

afterEach(() => {
process.env.HOME = originalHome;
restoreHomeDir();
rmSync(testHome, { recursive: true, force: true });
rmSync(tmpProject, { recursive: true, force: true });
});
Expand Down
Loading