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
17 changes: 15 additions & 2 deletions packages/core/src/evaluation/workspace/pool-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ export class WorkspacePoolManager {

/**
* Reset an existing slot for reuse:
* 1. Reset repos (git reset --hard {ref} && git clean -fd per repo)
* 1. Reset repos (fetch from origin when resolve=remote, then git reset --hard && git clean per repo)
* 2. Re-copy template files (skip repo directories)
*/
private async resetSlot(
Expand All @@ -373,7 +373,20 @@ export class WorkspacePoolManager {
continue;
}
const ref = repo.checkout?.ref ?? 'HEAD';
await git(['reset', '--hard', ref], { cwd: repoDir });
const resolve = repo.checkout?.resolve ?? 'remote';

// When resolve is 'remote', fetch latest from origin before resetting
// so the pool slot reflects the current remote state at eval start.
if (resolve === 'remote') {
const fetchArgs = ['fetch', 'origin', ref];
if (repo.clone?.depth) {
fetchArgs.splice(1, 0, '--depth', String(repo.clone.depth));
}
await git(fetchArgs, { cwd: repoDir });
await git(['reset', '--hard', 'FETCH_HEAD'], { cwd: repoDir });
} else {
await git(['reset', '--hard', ref], { cwd: repoDir });
}
// strict removes .gitignored files (node_modules, build outputs).
// fast preserves .gitignored files, letting before_all
// build steps survive across pool reuse cycles and avoiding expensive rebuilds.
Expand Down
91 changes: 91 additions & 0 deletions packages/core/test/evaluation/workspace/pool-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,97 @@ describe('WorkspacePoolManager', () => {
});
});

describe('resolve: remote pool reuse', () => {
it('fetches latest from remote on pool reuse when resolve is remote', async () => {
const repoDir = path.join(tmpDir, 'source-repo');
const initialSha = createTestRepo(repoDir, { 'hello.txt': 'v1' });
// Rename default branch to 'main' (git init defaults to 'master')
execSync('git branch -M main', { cwd: repoDir, ...EXEC_OPTS });

const manager = new WorkspacePoolManager(poolRoot);
const repos: RepoConfig[] = [
{
path: './my-repo',
source: { type: 'local', path: repoDir },
checkout: { ref: 'main', resolve: 'remote' },
},
];

// First acquisition — clones at initial commit
const slot1 = await manager.acquireWorkspace({
repos,
maxSlots: 3,
repoManager,
});
expect(slot1.isExisting).toBe(false);
expect(readFileSync(path.join(slot1.path, 'my-repo', 'hello.txt'), 'utf-8')).toBe('v1');
await manager.releaseSlot(slot1);

// Advance the source repo to a new commit
writeFileSync(path.join(repoDir, 'hello.txt'), 'v2');
execSync('git add -A && git commit -m "v2"', { cwd: repoDir, ...EXEC_OPTS });
const newSha = gitExec('git rev-parse HEAD', repoDir);
expect(newSha).not.toBe(initialSha);

// Second acquisition — should reuse slot but fetch latest
const slot2 = await manager.acquireWorkspace({
repos,
maxSlots: 3,
repoManager,
});
expect(slot2.isExisting).toBe(true);
expect(slot2.path).toBe(slot1.path);

// The repo should have the latest content from the source
expect(readFileSync(path.join(slot2.path, 'my-repo', 'hello.txt'), 'utf-8')).toBe('v2');
const slot2Head = gitExec('git rev-parse HEAD', path.join(slot2.path, 'my-repo'));
expect(slot2Head).toBe(newSha);

await manager.releaseSlot(slot2);
});

it('does not fetch from remote when resolve is local', async () => {
const repoDir = path.join(tmpDir, 'source-repo');
createTestRepo(repoDir, { 'hello.txt': 'v1' });
// Rename default branch to 'main' (git init defaults to 'master')
execSync('git branch -M main', { cwd: repoDir, ...EXEC_OPTS });

const manager = new WorkspacePoolManager(poolRoot);
const repos: RepoConfig[] = [
{
path: './my-repo',
source: { type: 'local', path: repoDir },
checkout: { ref: 'main', resolve: 'local' },
},
];

// First acquisition
const slot1 = await manager.acquireWorkspace({
repos,
maxSlots: 3,
repoManager,
});
await manager.releaseSlot(slot1);

// Advance the source repo
writeFileSync(path.join(repoDir, 'hello.txt'), 'v2');
execSync('git add -A && git commit -m "v2"', { cwd: repoDir, ...EXEC_OPTS });

// Second acquisition — should reuse slot WITHOUT fetching
const slot2 = await manager.acquireWorkspace({
repos,
maxSlots: 3,
repoManager,
});
expect(slot2.isExisting).toBe(true);

// The repo should still have the original content (no fetch)
expect(readFileSync(path.join(slot2.path, 'my-repo', 'hello.txt'), 'utf-8')).toBe('v1');

await manager.releaseSlot(slot2);
});
});

describe('releaseSlot', () => {
it('removes lock file', async () => {
const repoDir = path.join(tmpDir, 'source-repo');
Expand Down
Loading