Files
ai-job-search/tests/test_check_upstream_updates.py
T
Oscar Madera 72bbe00529 fix(check_upstream_updates): warn when check falls back to a fork's own origin (#265)
On a fork without an 'upstream' remote, the checker silently fell back to 'origin' (the fork itself) and still printed '[OK] All framework files are up to date with upstream!', a false positive: the fork is always up to date with itself, so upstream updates were never reported. This is exactly the setup CONTRIBUTING.md recommends for forks.

Now, when the fallback remote does not point at the ai-job-search template repo, the script warns that the comparison is fork-vs-self and prints the command to add the template as a remote. The final OK line now names the ref it actually compared against.

Tests (new tests/test_check_upstream_updates.py, three scenarios) fail on master and pass with the fix.
2026-08-01 22:00:49 +02:00

120 lines
4.8 KiB
Python

import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
SCRIPT = REPO_ROOT / "tools" / "check_upstream_updates.py"
TEMPLATE_URL = "https://github.com/MadsLorentzen/ai-job-search.git"
FORK_URL = "https://github.com/octocat/ai-job-search.git"
FRAMEWORK_FILES = [
".claude/skills/job-application-assistant/01-candidate-profile.md",
".claude/skills/job-application-assistant/02-behavioral-profile.md",
".claude/skills/job-application-assistant/03-writing-style.md",
".claude/skills/job-application-assistant/04-job-evaluation.md",
".claude/skills/job-application-assistant/05-cv-templates.md",
".claude/skills/job-application-assistant/06-cover-letter-templates.md",
".claude/skills/job-application-assistant/07-interview-prep.md",
".claude/skills/job-application-assistant/08-application-forms.md",
".claude/skills/job-application-assistant/SKILL.md",
"AGENTS.md",
]
FRONTMATTER = "---\nframework_version: 1.0.0\n---\n"
class UpstreamCheckerRepoFixture(unittest.TestCase):
def setUp(self):
self.root = Path(tempfile.mkdtemp())
self.addCleanup(shutil.rmtree, self.root, ignore_errors=True)
tools = self.root / "tools"
tools.mkdir()
shutil.copy(SCRIPT, tools / "check_upstream_updates.py")
for rel in FRAMEWORK_FILES:
path = self.root / rel
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(FRONTMATTER, encoding="utf-8")
subprocess.run(["git", "init", "-b", "master"], cwd=self.root, check=True, capture_output=True)
subprocess.run(["git", "config", "user.name", "Test"], cwd=self.root, check=True, capture_output=True)
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=self.root, check=True, capture_output=True)
subprocess.run(["git", "add", "-A"], cwd=self.root, check=True, capture_output=True)
subprocess.run(["git", "commit", "-m", "init"], cwd=self.root, check=True, capture_output=True)
def add_remote(self, name: str, url: str) -> None:
subprocess.run(["git", "remote", "add", name, url], cwd=self.root, check=True, capture_output=True)
def materialize_remote_ref(self, name: str) -> None:
subprocess.run(
["git", "update-ref", f"refs/remotes/{name}/master", "HEAD"],
cwd=self.root,
check=True,
capture_output=True,
)
def run_checker(self, *args) -> subprocess.CompletedProcess:
return subprocess.run(
[sys.executable, str(self.root / "tools" / "check_upstream_updates.py"), "--no-fetch", *args],
cwd=self.root,
capture_output=True,
text=True,
)
class ForkWithoutUpstreamRemoteTests(UpstreamCheckerRepoFixture):
def setUp(self):
super().setUp()
self.add_remote("origin", FORK_URL)
self.materialize_remote_ref("origin")
def test_fork_fallback_warns_that_check_is_against_own_fork(self):
result = self.run_checker("--remote", "upstream")
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("Falling back to 'origin'", result.stdout)
self.assertIn("does not point to the ai-job-search template repo", result.stdout)
self.assertNotIn("up to date with upstream!", result.stdout)
self.assertIn("up to date with origin/master", result.stdout)
class DirectCloneFallbackTests(UpstreamCheckerRepoFixture):
def setUp(self):
super().setUp()
self.add_remote("origin", TEMPLATE_URL)
self.materialize_remote_ref("origin")
def test_clone_of_template_falls_back_without_fork_warning(self):
result = self.run_checker("--remote", "upstream")
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("Falling back to 'origin'", result.stdout)
self.assertNotIn("does not point to the ai-job-search template repo", result.stdout)
self.assertIn("up to date with origin/master", result.stdout)
class UpstreamRemotePresentTests(UpstreamCheckerRepoFixture):
def setUp(self):
super().setUp()
self.add_remote("origin", FORK_URL)
self.add_remote("upstream", TEMPLATE_URL)
self.materialize_remote_ref("upstream")
def test_explicit_upstream_remote_is_used_without_warning(self):
result = self.run_checker()
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertNotIn("Falling back to 'origin'", result.stdout)
self.assertNotIn("does not point to the ai-job-search template repo", result.stdout)
self.assertIn("up to date with upstream/master", result.stdout)
if __name__ == "__main__":
unittest.main()