feat(forge): GitHub adapter — second implementation keeps the seam a contract (#2693, milestone 288 step 8)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / integration (push) Successful in 27s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 1m8s
CI & Build / Build & push image (push) Successful in 45s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / integration (push) Successful in 27s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 1m8s
CI & Build / Build & push image (push) Successful in 45s
ForgeAdapter is now a named base class carrying the shared plumbing (host join, error taxonomy, contents decoding, archive, default_branch, latest_commit); GiteaForge keeps its exact behavior and GitHubForge joins with the real differences: api.github.com / GHE /api/v3 host mapping, Bearer auth, a commits call for the provenance stamp (GitHub's contents payload only carries the blob sha), and the codeload tarball redirect. The contract grew latest_commit, and with it the cached-SHA short-circuit in pull-time freshness: a stored provenance commit that still heads the recorded path confirms 'current' without a content transfer — the economy that fits pulls inside GitHub's rate limits; every surprise falls back to the full fetch. Webhook deliveries now also accept X-Hub-Signature-256 (sha256=<hex>); the payload shape was already common. Settings card copy covers both forges' token scopes; the kind selector already flowed from the server. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -104,6 +104,73 @@ async def test_current_with_same_stored_sha_skips_the_write():
|
||||
update.assert_not_called()
|
||||
|
||||
|
||||
async def test_stored_sha_short_circuit_skips_the_content_fetch():
|
||||
"""#2693: when provenance already names a commit and the forge reports no
|
||||
newer commit touching the path, the pull is confirmed current WITHOUT a
|
||||
content transfer — the economy that fits pull-time freshness inside
|
||||
GitHub's rate limits."""
|
||||
calls = []
|
||||
|
||||
def handler(request):
|
||||
calls.append(request.url.path)
|
||||
if request.url.path.endswith("/commits"):
|
||||
return httpx.Response(200, json=[{"sha": SHA}])
|
||||
raise AssertionError("the content fetch should have been skipped")
|
||||
|
||||
update = AsyncMock()
|
||||
data = _data(provenance={"commit_sha": SHA, "fetched_at": "t"})
|
||||
with _patched(_forge_with(handler)), patch.object(svc.notes_svc, "update_note", update):
|
||||
await svc.attach_live_body(_note(), data)
|
||||
await background.drain()
|
||||
assert data["body_source"] == "forge"
|
||||
assert data["body_freshness"] == "current"
|
||||
assert len(calls) == 1
|
||||
update.assert_not_called() # same stamp — nothing to persist
|
||||
|
||||
|
||||
async def test_moved_file_falls_through_to_the_full_fetch():
|
||||
new_sha = "0" * 40
|
||||
|
||||
def handler(request):
|
||||
if request.url.path.endswith("/commits"):
|
||||
return httpx.Response(200, json=[{"sha": new_sha}])
|
||||
return _file_response("prefix\n" + CODE, commit_sha=new_sha)
|
||||
|
||||
saved = {}
|
||||
|
||||
async def fake_update(uid, nid, **fields):
|
||||
saved.update(fields)
|
||||
|
||||
data = _data(provenance={"commit_sha": SHA, "fetched_at": "t"})
|
||||
with _patched(_forge_with(handler)), patch.object(
|
||||
svc.notes_svc, "update_note", fake_update
|
||||
):
|
||||
await svc.attach_live_body(_note(), data)
|
||||
await background.drain()
|
||||
# The file moved but still contains the code — current, with the stamp
|
||||
# advanced by the authoritative full fetch.
|
||||
assert data["body_freshness"] == "current"
|
||||
assert saved["data"]["provenance"]["commit_sha"] == new_sha
|
||||
|
||||
|
||||
async def test_short_circuit_failure_degrades_to_the_full_fetch():
|
||||
"""A forge whose commits endpoint errors must cost nothing: the full
|
||||
fetch stays the authoritative path and the pull behaves as before."""
|
||||
|
||||
def handler(request):
|
||||
if request.url.path.endswith("/commits"):
|
||||
return httpx.Response(500)
|
||||
return _file_response("prefix\n" + CODE)
|
||||
|
||||
update = AsyncMock()
|
||||
data = _data(provenance={"commit_sha": SHA, "fetched_at": "t"})
|
||||
with _patched(_forge_with(handler)), patch.object(svc.notes_svc, "update_note", update):
|
||||
await svc.attach_live_body(_note(), data)
|
||||
await background.drain()
|
||||
assert data["body_freshness"] == "current"
|
||||
update.assert_not_called() # same sha via the full fetch → same-sha skip
|
||||
|
||||
|
||||
async def test_diverged_reports_without_clobbering():
|
||||
forge = _forge_with(lambda r: _file_response("def helper(x):\n return x - 1\n"))
|
||||
update = AsyncMock()
|
||||
|
||||
Reference in New Issue
Block a user