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
64 changes: 64 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
build:
name: Build
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json
- run: bun install --frozen-lockfile --ignore-scripts
- run: bun run build

typecheck:
name: Typecheck
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json
- run: bun install --frozen-lockfile --ignore-scripts
- run: bun run typecheck

lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json
- run: bun install --frozen-lockfile --ignore-scripts
- run: bun run lint

test:
name: Test
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json
- run: bun install --frozen-lockfile --ignore-scripts
- run: bun run test
21 changes: 0 additions & 21 deletions .pre-commit-config.yaml

This file was deleted.

4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,15 @@ git worktree remove ../allagents.worktrees/<name>
## Tech Stack & Tools
- Runtime and package manager: Bun
- Language: TypeScript
- Tests: `bun:test` plus shell integration tests
- Tests: `bun:test` unit and E2E tests
- Lint/format: Biome

## Testing & Verification

### Core Commands
- Build: `bun run build`
- Unit tests: `bun test`
- Integration tests: `bun run test:integration`
- E2E tests: `bun run test:e2e`
- Typecheck: `bun run typecheck`
- Lint: `bun run lint`

Expand Down
19 changes: 18 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,26 @@ bun run dev update
- PR explains what changed and why
- Tests are updated when relevant
- No unrelated refactors in the same PR
- Pre-push hooks pass (`lint`, `typecheck`, `test` run automatically on push)
- Run focused local checks while developing and the relevant broad checks before pushing
- Manual E2E test: build the CLI (`bun run build`) and run against a temp workspace

GitHub Actions is the authoritative broad quality gate. It runs build, typecheck,
lint, and the full test suite as separate checks on pull requests and `main`.
Repository maintainers should configure the `Build`, `Typecheck`, `Lint`, and
`Test` checks as required in the `main` branch protection rules after this
workflow lands. You can run the same checks locally when needed:

```bash
bun run build
bun run typecheck
bun run lint
bun run test
```

Older clones may still have the previously generated prek pre-push hook. If a
push still invokes prek, inspect `.git/hooks/pre-push` and remove it only when
it is the generated prek hook; preserve any custom hook content.

## Workflow

- Branch from `main`
Expand Down
59 changes: 0 additions & 59 deletions bun.lock

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@
"test": "bun test",
"test:watch": "bun test --watch",
"test:coverage": "bun test --coverage",
"test:integration": "bats tests/integration/*.bats",
"test:e2e": "bun test tests/e2e",
"typecheck": "tsc --noEmit",
"lint": "biome lint src",
"lint:fix": "biome lint --write src",
"format": "biome format --write src",
"check": "biome check src",
"check:fix": "biome check --write src",
"prepare": "bun run build && (test -d .git && bunx prek install -t pre-push || true)",
"prepare": "bun run build",
"release": "bun run scripts/release.ts",
"release:next": "bun run scripts/release.ts next",
"release:finalize": "bun run scripts/release.ts finalize",
Expand Down Expand Up @@ -68,7 +68,6 @@
},
"devDependencies": {
"@biomejs/biome": "^1.9.0",
"@j178/prek": "^0.3.0",
"@types/bun": "latest",
"@types/js-yaml": "^4.0.9",
"@types/micromatch": "^4.0.10",
Expand Down
5 changes: 4 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import { homedir } from 'node:os';
* (see EntityProcess/allagents#433).
*/
export function getHomeDir(): string {
return homedir();
// Bun caches os.homedir() for the process, so tests cannot isolate user state
// by mutating HOME/USERPROFILE. This deliberately named override belongs to
// AllAgents tests; production continues to use the Windows-safe os.homedir().
return process.env.ALLAGENTS_TEST_HOME || homedir();
}

/**
Expand Down
22 changes: 9 additions & 13 deletions tests/helpers/env.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
/**
* 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.
* Bun caches os.homedir() for the process, so changing HOME or USERPROFILE
* cannot isolate tests reliably. getHomeDir() owns this explicitly test-only
* override while continuing to use os.homedir() in production. The restore
* function supports nested stubs and deletes an override that was originally
* absent.
*/
export function stubHomeDir(path: string): () => void {
const originalHome = process.env.HOME;
const originalUserProfile = process.env.USERPROFILE;
process.env.HOME = path;
process.env.USERPROFILE = path;
const originalTestHome = process.env.ALLAGENTS_TEST_HOME;
process.env.ALLAGENTS_TEST_HOME = 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;
if (originalTestHome === undefined) delete process.env.ALLAGENTS_TEST_HOME;
else process.env.ALLAGENTS_TEST_HOME = originalTestHome;
};
}
16 changes: 16 additions & 0 deletions tests/unit/constants.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { describe, expect, test, afterEach } from 'bun:test';
import { homedir } from 'node:os';
import { getHomeDir } from '../../src/constants.js';
import { stubHomeDir } from '../helpers/env.js';

describe('getHomeDir', () => {
const originalHome = process.env.HOME;
Expand All @@ -12,6 +14,20 @@ describe('getHomeDir', () => {
else process.env.USERPROFILE = originalUserProfile;
});

test('uses and restores the explicit AllAgents test home override', () => {
// Resolve os.homedir() first to reproduce Bun's process-level cache.
const realHome = homedir();
const restoreHomeDir = stubHomeDir('/tmp/allagents-isolated-home');

try {
expect(getHomeDir()).toBe('/tmp/allagents-isolated-home');
} finally {
restoreHomeDir();
}

expect(getHomeDir()).toBe(realHome);
});

// 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.
Expand Down
14 changes: 7 additions & 7 deletions tests/unit/core/status-both-scopes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ import { stubHomeDir } from '../../helpers/env.js';
describe('workspace status - both scopes', () => {
let testDir: string;
let restoreHomeDir: () => void;
let restoreScopedHomeDir: (() => void) | undefined;

beforeEach(async () => {
testDir = await mkdtemp(join(tmpdir(), 'allagents-status-test-'));
restoreHomeDir = stubHomeDir(testDir);
restoreScopedHomeDir = undefined;
});

afterEach(async () => {
restoreScopedHomeDir?.();
restoreHomeDir();
await rm(testDir, { recursive: true, force: true });
});
Expand Down Expand Up @@ -50,11 +53,8 @@ describe('workspace status - both scopes', () => {

it('should include userPlugins in status result', async () => {
// Use a separate HOME so user config doesn't overlap with project dir.
// Discarding the returned restore fn is intentional: the outer afterEach's
// restoreHomeDir() already unwinds straight back to the pre-suite original
// regardless of how many times HOME/USERPROFILE were reassigned in between.
const homeDir = await mkdtemp(join(tmpdir(), 'allagents-status-home-'));
stubHomeDir(homeDir);
restoreScopedHomeDir = stubHomeDir(homeDir);

const projectPlugin = await createLocalPlugin('project-plugin');
const userPlugin = await createLocalPlugin('user-plugin');
Expand Down Expand Up @@ -85,7 +85,7 @@ describe('workspace status - both scopes', () => {
it('should fall back to user plugins when no project workspace exists', async () => {
// Use a separate HOME so user config doesn't overlap with project dir
const homeDir = await mkdtemp(join(tmpdir(), 'allagents-status-home-'));
stubHomeDir(homeDir);
restoreScopedHomeDir = stubHomeDir(homeDir);

const userPlugin = await createLocalPlugin('user-plugin');
const allagentsDir = join(homeDir, '.allagents');
Expand Down Expand Up @@ -129,7 +129,7 @@ describe('workspace status - both scopes', () => {
it('should show empty userPlugins when no user config exists', async () => {
// Use a separate HOME dir so there's no user config
const separateHome = await mkdtemp(join(tmpdir(), 'allagents-status-home-'));
stubHomeDir(separateHome);
restoreScopedHomeDir = stubHomeDir(separateHome);

const projectPlugin = await createLocalPlugin('project-plugin');

Expand All @@ -150,7 +150,7 @@ describe('workspace status - both scopes', () => {
// Regression: `skills add <github-blob-url>` clones into `<owner>-<repo>@<branch>`,
// but `workspace status` looked up `<owner>-<repo>` and reported "not cached".
const homeDir = await mkdtemp(join(tmpdir(), 'allagents-status-home-'));
process.env.HOME = homeDir;
restoreScopedHomeDir = stubHomeDir(homeDir);

const branchedCache = join(
homeDir,
Expand Down
22 changes: 10 additions & 12 deletions tests/unit/core/workspace-cache-seed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,27 @@ import { tmpdir } from 'node:os';
import { seedCacheFromClone } from '../../../src/core/workspace.js';
import { getPluginCachePath } from '../../../src/utils/plugin-path.js';
import { getMarketplacesDir } from '../../../src/core/marketplace.js';
import { stubHomeDir } from '../../helpers/env.js';

describe('seedCacheFromClone', () => {
let tempCloneDir: string;
let tempHomeDir: string;
let restoreHomeDir: () => void;

beforeEach(async () => {
tempCloneDir = await mkdtemp(join(tmpdir(), 'allagents-clone-'));
tempHomeDir = await mkdtemp(join(tmpdir(), 'allagents-home-'));
restoreHomeDir = stubHomeDir(tempHomeDir);
// Add a marker file to verify the clone was copied
await writeFile(join(tempCloneDir, 'marker.txt'), 'cloned-content');
});

afterEach(async () => {
if (existsSync(tempCloneDir)) {
await rm(tempCloneDir, { recursive: true, force: true });
}
// Clean up any cache directories that were seeded
const pluginCachePath = getPluginCachePath('test-owner', 'test-repo', 'main');
if (existsSync(pluginCachePath)) {
await rm(pluginCachePath, { recursive: true, force: true });
}
const marketplaceCachePath = join(getMarketplacesDir(), 'test-repo');
if (existsSync(marketplaceCachePath)) {
await rm(marketplaceCachePath, { recursive: true, force: true });
}
restoreHomeDir();
await Promise.all([
rm(tempCloneDir, { recursive: true, force: true }),
rm(tempHomeDir, { recursive: true, force: true }),
]);
});

it('should seed both plugin and marketplace caches', async () => {
Expand Down