Skip to content

Commit d721063

Browse files
committed
fix(results): ignore unrelated files during remote sync
1 parent 499db1f commit d721063

2 files changed

Lines changed: 156 additions & 40 deletions

File tree

packages/core/src/evaluation/results-repo.ts

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,8 @@ async function inspectResultsRepoGit(repoDir: string): Promise<ResultsRepoGitIns
453453
cwd: repoDir,
454454
check: false,
455455
});
456-
const { dirtyPaths, conflictedPaths } = parseGitPorcelainPaths(porcelain);
456+
const { dirtyPaths: allDirtyPaths, conflictedPaths } = parseGitPorcelainPaths(porcelain);
457+
const dirtyPaths = allDirtyPaths.filter(isSafeResultsRepoPath);
457458
const { ahead = 0, behind = 0 } = await getAheadBehind(repoDir, upstream);
458459
const inProgressConflict = await hasInProgressGitConflict(repoDir);
459460

@@ -518,9 +519,6 @@ function lastErrorForGitInspection(
518519
if (status.auto_push === false) {
519520
return 'Results repo has uncommitted changes and auto_push is disabled';
520521
}
521-
if (!areSafeResultsRepoPaths(inspection.dirtyPaths)) {
522-
return 'Results repo has non-results working tree changes';
523-
}
524522
}
525523

526524
return undefined;
@@ -562,13 +560,12 @@ function withActionFlags(
562560
};
563561
}
564562

563+
function isSafeResultsRepoPath(p: string): boolean {
564+
return p === RESULTS_REPO_RESULTS_DIR || p.startsWith(`${RESULTS_REPO_RESULTS_DIR}/`);
565+
}
566+
565567
function areSafeResultsRepoPaths(paths: readonly string[]): boolean {
566-
return (
567-
paths.length > 0 &&
568-
paths.every(
569-
(p) => p === RESULTS_REPO_RESULTS_DIR || p.startsWith(`${RESULTS_REPO_RESULTS_DIR}/`),
570-
)
571-
);
568+
return paths.length > 0 && paths.every(isSafeResultsRepoPath);
572569
}
573570

574571
async function getAheadPaths(
@@ -707,35 +704,44 @@ export async function syncResultsRepoForProject(config: ResultsConfig): Promise<
707704
);
708705
}
709706

710-
if (!areSafeResultsRepoPaths(inspection.dirtyPaths)) {
711-
const status = withGitInspection(getResultsRepoStatus(normalized), inspection);
712-
updateStatusFile(normalized, {
713-
last_error: 'Results repo has non-results working tree changes',
714-
});
715-
return withBlockedStatus(status, 'Results repo has non-results working tree changes', {
716-
pullPerformed,
717-
pushPerformed,
718-
commitCreated,
719-
});
707+
if ((inspection.behind ?? 0) > 0) {
708+
if (!inspection.upstream) {
709+
const status = withGitInspection(getResultsRepoStatus(normalized), inspection);
710+
updateStatusFile(normalized, {
711+
last_error: 'Results repo has no upstream branch to pull from',
712+
});
713+
return withBlockedStatus(status, 'Results repo has no upstream branch to pull from', {
714+
pullPerformed,
715+
pushPerformed,
716+
commitCreated,
717+
});
718+
}
719+
720+
try {
721+
await runGit(['merge', '--ff-only', inspection.upstream], { cwd: repoDir });
722+
pullPerformed = true;
723+
inspection = await inspectResultsRepoGit(repoDir);
724+
} catch (error) {
725+
inspection = await inspectResultsRepoGit(repoDir);
726+
const status = withGitInspection(getResultsRepoStatus(normalized), inspection);
727+
const reason = `Results repo could not be fast-forwarded: ${getStatusMessage(error)}`;
728+
updateStatusFile(normalized, { last_error: reason });
729+
return withBlockedStatus(status, reason, {
730+
pullPerformed,
731+
pushPerformed,
732+
commitCreated,
733+
});
734+
}
720735
}
721736

722-
if ((inspection.behind ?? 0) > 0) {
723-
const status = withGitInspection(getResultsRepoStatus(normalized), inspection);
724-
const reason = 'Results repo has uncommitted result changes and remote changes';
725-
updateStatusFile(normalized, { last_error: reason });
726-
return withBlockedStatus(status, reason, {
727-
pullPerformed,
728-
pushPerformed,
729-
commitCreated,
737+
if (inspection.syncStatus === 'dirty') {
738+
await runGit(['add', '--all', '--', RESULTS_REPO_RESULTS_DIR], { cwd: repoDir });
739+
await runGit(['commit', '-m', 'chore(results): sync local result metadata'], {
740+
cwd: repoDir,
730741
});
742+
commitCreated = true;
743+
inspection = await inspectResultsRepoGit(repoDir);
731744
}
732-
733-
await runGit(['add', '--all', '--', RESULTS_REPO_RESULTS_DIR], { cwd: repoDir });
734-
await runGit(['commit', '-m', 'chore(results): sync local result metadata'], {
735-
cwd: repoDir,
736-
});
737-
commitCreated = true;
738-
inspection = await inspectResultsRepoGit(repoDir);
739745
}
740746

741747
if (inspection.syncStatus === 'diverged') {

packages/core/test/evaluation/results-repo.test.ts

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -519,24 +519,134 @@ describe('results repo write path', () => {
519519
);
520520
}, 20000);
521521

522-
it('blocks dirty non-results changes with git summaries instead of resetting', async () => {
522+
it('ignores dirty non-results files when reporting project sync status', async () => {
523523
const { remoteDir } = initializeRemoteRepo(rootDir);
524524
const cloneDir = path.join(rootDir, 'results-clone');
525525
const config = createResultsConfig(remoteDir, cloneDir);
526526

527527
await ensureResultsRepoClone(config);
528528
writeFileSync(path.join(cloneDir, 'NOTES.md'), 'do not auto-push me\n');
529529

530+
await expect(getResultsRepoSyncStatus(config)).resolves.toMatchObject({
531+
sync_status: 'clean',
532+
dirty_paths: [],
533+
last_error: undefined,
534+
});
535+
530536
const status = await syncResultsRepoForProject(config);
531537

532-
expect(status.sync_status).toBe('dirty');
533-
expect(status.blocked).toBe(true);
534-
expect(status.block_reason).toContain('non-results');
535-
expect(status.dirty_paths).toEqual(['NOTES.md']);
538+
expect(status.sync_status).toBe('clean');
539+
expect(status.blocked).toBe(false);
540+
expect(status.dirty_paths).toEqual([]);
536541
expect(status.git_status).toContain('NOTES.md');
537542
expect(readFileSync(path.join(cloneDir, 'NOTES.md'), 'utf8')).toBe('do not auto-push me\n');
538543
}, 20000);
539544

545+
it('commits and pushes dirty result artifacts while leaving unrelated files untracked', async () => {
546+
const { remoteDir } = initializeRemoteRepo(rootDir);
547+
const cloneDir = path.join(rootDir, 'results-clone');
548+
const config = createResultsConfig(remoteDir, cloneDir);
549+
550+
await ensureResultsRepoClone(config);
551+
git('git config user.email "test@example.com"', cloneDir);
552+
git('git config user.name "Test User"', cloneDir);
553+
writeFileSync(path.join(cloneDir, 'package.json'), '{"dependencies":{"agentv":"next"}}\n');
554+
555+
const runTimestamp = '2026-05-24T11-00-00-000Z';
556+
const runDir = path.join(cloneDir, '.agentv', 'results', 'runs', 'safe-run', runTimestamp);
557+
writeRunArtifacts(runDir, 'safe-run', '2026-05-24T11:00:00.000Z');
558+
559+
const status = await syncResultsRepoForProject(config);
560+
561+
expect(status).toMatchObject({
562+
sync_status: 'clean',
563+
commit_created: true,
564+
push_performed: true,
565+
blocked: false,
566+
});
567+
expect(status.dirty_paths).toEqual([]);
568+
expect(git(`git --git-dir "${remoteDir}" ls-tree -r --name-only main`, rootDir)).toContain(
569+
`.agentv/results/runs/safe-run/${runTimestamp}/benchmark.json`,
570+
);
571+
expect(git(`git --git-dir "${remoteDir}" ls-tree -r --name-only main`, rootDir)).not.toContain(
572+
'package.json',
573+
);
574+
expect(readFileSync(path.join(cloneDir, 'package.json'), 'utf8')).toBe(
575+
'{"dependencies":{"agentv":"next"}}\n',
576+
);
577+
}, 20000);
578+
579+
it('fast-forwards remote updates even when unrelated local files are dirty', async () => {
580+
const { remoteDir, seedDir } = initializeRemoteRepo(rootDir);
581+
const cloneDir = path.join(rootDir, 'results-clone');
582+
const config = createResultsConfig(remoteDir, cloneDir);
583+
584+
await ensureResultsRepoClone(config);
585+
writeFileSync(path.join(cloneDir, 'package.json'), '{"dependencies":{"agentv":"next"}}\n');
586+
writeFileSync(path.join(seedDir, 'REMOTE.md'), 'remote update\n');
587+
git('git add REMOTE.md && git commit --quiet -m "remote update"', seedDir);
588+
git('git push --quiet origin main', seedDir);
589+
590+
const status = await syncResultsRepoForProject(config);
591+
592+
expect(status).toMatchObject({
593+
sync_status: 'clean',
594+
pull_performed: true,
595+
push_performed: false,
596+
commit_created: false,
597+
blocked: false,
598+
});
599+
expect(readFileSync(path.join(cloneDir, 'REMOTE.md'), 'utf8')).toBe('remote update\n');
600+
expect(readFileSync(path.join(cloneDir, 'package.json'), 'utf8')).toBe(
601+
'{"dependencies":{"agentv":"next"}}\n',
602+
);
603+
}, 20000);
604+
605+
it('pulls remote updates before pushing local result artifacts with unrelated dirty files', async () => {
606+
const { remoteDir, seedDir } = initializeRemoteRepo(rootDir);
607+
const cloneDir = path.join(rootDir, 'results-clone');
608+
const config = createResultsConfig(remoteDir, cloneDir);
609+
610+
await ensureResultsRepoClone(config);
611+
git('git config user.email "test@example.com"', cloneDir);
612+
git('git config user.name "Test User"', cloneDir);
613+
writeFileSync(path.join(cloneDir, 'package.json'), '{"dependencies":{"agentv":"next"}}\n');
614+
615+
writeFileSync(path.join(seedDir, 'REMOTE.md'), 'remote update\n');
616+
git('git add REMOTE.md && git commit --quiet -m "remote update"', seedDir);
617+
git('git push --quiet origin main', seedDir);
618+
619+
const runTimestamp = '2026-05-24T12-00-00-000Z';
620+
const runDir = path.join(
621+
cloneDir,
622+
'.agentv',
623+
'results',
624+
'runs',
625+
'pulled-then-pushed',
626+
runTimestamp,
627+
);
628+
writeRunArtifacts(runDir, 'pulled-then-pushed', '2026-05-24T12:00:00.000Z');
629+
630+
const status = await syncResultsRepoForProject(config);
631+
632+
expect(status).toMatchObject({
633+
sync_status: 'clean',
634+
pull_performed: true,
635+
push_performed: true,
636+
commit_created: true,
637+
blocked: false,
638+
});
639+
const remoteFiles = git(`git --git-dir "${remoteDir}" ls-tree -r --name-only main`, rootDir);
640+
expect(remoteFiles).toContain('REMOTE.md');
641+
expect(remoteFiles).toContain(
642+
`.agentv/results/runs/pulled-then-pushed/${runTimestamp}/benchmark.json`,
643+
);
644+
expect(remoteFiles).not.toContain('package.json');
645+
expect(readFileSync(path.join(cloneDir, 'package.json'), 'utf8')).toBe(
646+
'{"dependencies":{"agentv":"next"}}\n',
647+
);
648+
}, 20000);
649+
540650
it('blocks diverged committed histories with diff summary', async () => {
541651
const { remoteDir, seedDir } = initializeRemoteRepo(rootDir);
542652
const cloneDir = path.join(rootDir, 'results-clone');

0 commit comments

Comments
 (0)