Skip to content

Commit 4936834

Browse files
authored
fix(results): ignore unrelated files during remote sync (#1347)
* fix(results): ignore unrelated files during remote sync * fix(results): scope remote sync commits to result paths
1 parent 3099e5e commit 4936834

2 files changed

Lines changed: 204 additions & 41 deletions

File tree

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

Lines changed: 51 additions & 36 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,53 @@ 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-
});
720-
}
721-
722707
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,
730-
});
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+
}
731735
}
732736

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);
737+
if (inspection.syncStatus === 'dirty') {
738+
await runGit(['add', '--all', '--', RESULTS_REPO_RESULTS_DIR], { cwd: repoDir });
739+
await runGit(
740+
[
741+
'commit',
742+
'-m',
743+
'chore(results): sync local result metadata',
744+
'--',
745+
RESULTS_REPO_RESULTS_DIR,
746+
],
747+
{
748+
cwd: repoDir,
749+
},
750+
);
751+
commitCreated = true;
752+
inspection = await inspectResultsRepoGit(repoDir);
753+
}
739754
}
740755

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

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

Lines changed: 153 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -519,24 +519,172 @@ 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('does not commit unrelated files that were already staged before sync', async () => {
580+
const { remoteDir } = initializeRemoteRepo(rootDir);
581+
const cloneDir = path.join(rootDir, 'results-clone');
582+
const config = createResultsConfig(remoteDir, cloneDir);
583+
584+
await ensureResultsRepoClone(config);
585+
git('git config user.email "test@example.com"', cloneDir);
586+
git('git config user.name "Test User"', cloneDir);
587+
writeFileSync(path.join(cloneDir, 'package.json'), '{"dependencies":{"agentv":"next"}}\n');
588+
git('git add package.json', cloneDir);
589+
590+
const runTimestamp = '2026-05-24T11-30-00-000Z';
591+
const runDir = path.join(
592+
cloneDir,
593+
'.agentv',
594+
'results',
595+
'runs',
596+
'staged-unrelated',
597+
runTimestamp,
598+
);
599+
writeRunArtifacts(runDir, 'staged-unrelated', '2026-05-24T11:30:00.000Z');
600+
601+
const status = await syncResultsRepoForProject(config);
602+
603+
expect(status).toMatchObject({
604+
sync_status: 'clean',
605+
commit_created: true,
606+
push_performed: true,
607+
blocked: false,
608+
});
609+
const remoteFiles = git(`git --git-dir "${remoteDir}" ls-tree -r --name-only main`, rootDir);
610+
expect(remoteFiles).toContain(
611+
`.agentv/results/runs/staged-unrelated/${runTimestamp}/benchmark.json`,
612+
);
613+
expect(remoteFiles).not.toContain('package.json');
614+
expect(git('git status --porcelain', cloneDir)).toContain('A package.json');
615+
}, 20000);
616+
617+
it('fast-forwards remote updates even when unrelated local files are dirty', async () => {
618+
const { remoteDir, seedDir } = initializeRemoteRepo(rootDir);
619+
const cloneDir = path.join(rootDir, 'results-clone');
620+
const config = createResultsConfig(remoteDir, cloneDir);
621+
622+
await ensureResultsRepoClone(config);
623+
writeFileSync(path.join(cloneDir, 'package.json'), '{"dependencies":{"agentv":"next"}}\n');
624+
writeFileSync(path.join(seedDir, 'REMOTE.md'), 'remote update\n');
625+
git('git add REMOTE.md && git commit --quiet -m "remote update"', seedDir);
626+
git('git push --quiet origin main', seedDir);
627+
628+
const status = await syncResultsRepoForProject(config);
629+
630+
expect(status).toMatchObject({
631+
sync_status: 'clean',
632+
pull_performed: true,
633+
push_performed: false,
634+
commit_created: false,
635+
blocked: false,
636+
});
637+
expect(readFileSync(path.join(cloneDir, 'REMOTE.md'), 'utf8')).toBe('remote update\n');
638+
expect(readFileSync(path.join(cloneDir, 'package.json'), 'utf8')).toBe(
639+
'{"dependencies":{"agentv":"next"}}\n',
640+
);
641+
}, 20000);
642+
643+
it('pulls remote updates before pushing local result artifacts with unrelated dirty files', async () => {
644+
const { remoteDir, seedDir } = initializeRemoteRepo(rootDir);
645+
const cloneDir = path.join(rootDir, 'results-clone');
646+
const config = createResultsConfig(remoteDir, cloneDir);
647+
648+
await ensureResultsRepoClone(config);
649+
git('git config user.email "test@example.com"', cloneDir);
650+
git('git config user.name "Test User"', cloneDir);
651+
writeFileSync(path.join(cloneDir, 'package.json'), '{"dependencies":{"agentv":"next"}}\n');
652+
653+
writeFileSync(path.join(seedDir, 'REMOTE.md'), 'remote update\n');
654+
git('git add REMOTE.md && git commit --quiet -m "remote update"', seedDir);
655+
git('git push --quiet origin main', seedDir);
656+
657+
const runTimestamp = '2026-05-24T12-00-00-000Z';
658+
const runDir = path.join(
659+
cloneDir,
660+
'.agentv',
661+
'results',
662+
'runs',
663+
'pulled-then-pushed',
664+
runTimestamp,
665+
);
666+
writeRunArtifacts(runDir, 'pulled-then-pushed', '2026-05-24T12:00:00.000Z');
667+
668+
const status = await syncResultsRepoForProject(config);
669+
670+
expect(status).toMatchObject({
671+
sync_status: 'clean',
672+
pull_performed: true,
673+
push_performed: true,
674+
commit_created: true,
675+
blocked: false,
676+
});
677+
const remoteFiles = git(`git --git-dir "${remoteDir}" ls-tree -r --name-only main`, rootDir);
678+
expect(remoteFiles).toContain('REMOTE.md');
679+
expect(remoteFiles).toContain(
680+
`.agentv/results/runs/pulled-then-pushed/${runTimestamp}/benchmark.json`,
681+
);
682+
expect(remoteFiles).not.toContain('package.json');
683+
expect(readFileSync(path.join(cloneDir, 'package.json'), 'utf8')).toBe(
684+
'{"dependencies":{"agentv":"next"}}\n',
685+
);
686+
}, 20000);
687+
540688
it('blocks diverged committed histories with diff summary', async () => {
541689
const { remoteDir, seedDir } = initializeRemoteRepo(rootDir);
542690
const cloneDir = path.join(rootDir, 'results-clone');

0 commit comments

Comments
 (0)