diff --git a/.github/workflows/dead-code.yml b/.github/workflows/dead-code.yml new file mode 100644 index 0000000..f9303e8 --- /dev/null +++ b/.github/workflows/dead-code.yml @@ -0,0 +1,19 @@ +name: Dead code +on: + pull_request: + push: + branches: [main] + workflow_dispatch: +permissions: + contents: read +jobs: + dead-code: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - uses: astral-sh/setup-uv@v7 + - run: bash scripts/check-dead-code.sh diff --git a/.github/workflows/product-validation.yml b/.github/workflows/product-validation.yml new file mode 100644 index 0000000..e040c78 --- /dev/null +++ b/.github/workflows/product-validation.yml @@ -0,0 +1,41 @@ +name: Product validation +on: + pull_request: + push: + branches: [main] + workflow_dispatch: +permissions: + contents: read +jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - uses: actions/setup-node@v4 + with: + node-version: '22' + - run: python3 scripts/validate-poc.py + - name: Preserve exact-commit validation receipt + if: always() + run: | + python3 - <<'PY' + import json, os + from pathlib import Path + result = Path('.evidence/result.json') + if not result.is_file(): + raise SystemExit('Required exact-commit receipt is missing') + data = json.loads(result.read_text()) + text = '## Product validation\n\n```json\n' + json.dumps(data, indent=2) + '\n```\n' + with open(os.environ['GITHUB_STEP_SUMMARY'], 'a') as summary: + summary.write(text) + print(text) + for validator in data['validators']: + if validator['status'] != 'passed': + print(Path(validator['log']).read_text()) + PY diff --git a/.gitignore b/.gitignore index 0c11895..192492c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ __pycache__/ *.pyc .env .alpha/ +.evidence/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 14154af..f84ec36 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,4 +4,4 @@ Start with the user outcome, a reproducible example, and acceptance checks. Keep Open an issue or draft PR with expected and actual behavior, source revision, runtime, a minimal reproduction, and redacted check results. Never include account caches, private tickets, customer data, or credentials. Native integration checks require your own documented host prerequisites and account; simulations must be labeled. -For source changes, run Python compilation and the relevant synthetic demo checks. Add a focused regression test for changed behavior. The maintainer decides merges and releases. This experimental alpha has no production-support commitment. +For source changes, run `python3 scripts/validate-poc.py` and `bash scripts/check-dead-code.sh` (the latter uses pinned Vulture 2.16 through `uvx`). Validation covers controller regression tests, Python compilation, and the preserved synthetic demo, with a receipt under `.evidence`. A dirty checkout is labeled blocked for exact-commit validation; commit the reviewed change and rerun before publication. Add a focused regression test for changed behavior. The maintainer decides merges and releases. This experimental alpha has no production-support commitment. diff --git a/README.md b/README.md index a8278f4..248c114 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,8 @@ Pi's full profile uses its existing extensions. Other agents keep their own tool ## Support and license +This is the canonical development repository for Autonomous Development. Controller tests, source packaging, operating services, and the preserved synthetic demo are maintained here. Run `python3 scripts/validate-poc.py` and `bash scripts/check-dead-code.sh` before opening a pull request; GitHub runs these checks on pull requests and on `main`. + Maintained by Hudson Aikins / IntelIP. Use [issues](https://github.com/IntelIP/autonomous-development/issues) for redacted bugs and reproducible examples. No response-time or production-support commitment is made. See [SECURITY.md](SECURITY.md) for sensitive reports. Project source and the five explicitly identified bundled worker files use [Apache License 2.0](LICENSE). The export contains no private ticket records, account caches, or private repository history. diff --git a/controller/autonomous-development-pilot@.service b/controller/autonomous-development-pilot@.service new file mode 100644 index 0000000..2895411 --- /dev/null +++ b/controller/autonomous-development-pilot@.service @@ -0,0 +1,18 @@ +[Unit] +Description=Autonomous Development supervised approved packet %i +After=network-online.target docker.service +Wants=network-online.target +Requires=docker.service + +[Service] +Type=oneshot +User=root +SetLoginEnvironment=yes +Environment=PATH=/usr/local/bin:/usr/bin:/bin +WorkingDirectory=/opt/AutonomousDevelopment +ExecStartPre=/usr/bin/python3 /opt/AutonomousDevelopment/controller/control.py recover +ExecStart=/usr/bin/python3 /opt/AutonomousDevelopment/controller/engineering.py run --pilot --packet /var/lib/autonomous-development/queue/%i.json +ExecStopPost=/usr/bin/python3 /opt/AutonomousDevelopment/controller/control.py recover +TimeoutStartSec=3670 +KillMode=control-group +UMask=0077 diff --git a/controller/autonomous-development.service b/controller/autonomous-development.service new file mode 100644 index 0000000..483e775 --- /dev/null +++ b/controller/autonomous-development.service @@ -0,0 +1,21 @@ +[Unit] +Description=Autonomous Development approved story runner +After=network-online.target docker.service +Wants=network-online.target +Requires=docker.service + +[Service] +Type=oneshot +User=root +SetLoginEnvironment=yes +Environment=PATH=/usr/local/bin:/usr/bin:/bin +WorkingDirectory=/opt/AutonomousDevelopment +ExecStartPre=/usr/bin/python3 /opt/AutonomousDevelopment/controller/control.py recover +ExecStart=/usr/bin/python3 /opt/AutonomousDevelopment/controller/control.py scheduled +ExecStopPost=/usr/bin/python3 /opt/AutonomousDevelopment/controller/control.py recover +TimeoutStartSec=3670 +KillMode=control-group +UMask=0077 + +[Install] +WantedBy=multi-user.target diff --git a/controller/autonomous-development.timer b/controller/autonomous-development.timer new file mode 100644 index 0000000..56a7d53 --- /dev/null +++ b/controller/autonomous-development.timer @@ -0,0 +1,11 @@ +[Unit] +Description=Check approved Autonomous Development queue + +[Timer] +OnBootSec=2min +OnCalendar=*-*-* *:0/15:00 America/New_York +Persistent=true +Unit=autonomous-development.service + +[Install] +WantedBy=timers.target diff --git a/docs/published-source-demo.md b/docs/published-source-demo.md index ed1a2c7..494361f 100644 --- a/docs/published-source-demo.md +++ b/docs/published-source-demo.md @@ -8,7 +8,7 @@ The published alpha.3 controller and a freshly built generic Codex worker comple - Run: `bdb59c6b615c63bf`, September 30, 02:01:20–02:02:22 UTC. - Independent check: `python3 checks.py`, exit 0. Empty, positive, and mixed-sign examples passed. - Lead review: approved commit `4e77b8d42fe474c0a83e84e25f4c5eea8f776add`, no findings. -- Result: [draft PR #1](https://github.com/IntelIP/autonomous-development-alpha-demo/pull/1), one line changed in `sample.py`. Human merge authority retained. +- Result: a draft PR with one line changed in `sample.py`, subsequently merged by the operator on September 29 at 10:40 PM Eastern. The [fixture, repair diff, and historical receipt](../tests/fixtures/published-demo/receipt.json) preserve the result in this repository. Human merge authority retained. ## Operator repairs before success diff --git a/scripts/check-dead-code.sh b/scripts/check-dead-code.sh new file mode 100644 index 0000000..6f3ec6b --- /dev/null +++ b/scripts/check-dead-code.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +py_files=() +while IFS= read -r -d '' file; do py_files+=("$file"); done < <(git ls-files --cached --others --exclude-standard -z -- '*.py') +if ((${#py_files[@]})); then uvx --from vulture==2.16 vulture "${py_files[@]}" --min-confidence 90; fi diff --git a/scripts/package-alpha.py b/scripts/package-alpha.py index ac386da..2453316 100644 --- a/scripts/package-alpha.py +++ b/scripts/package-alpha.py @@ -19,6 +19,15 @@ 'README.md', 'CONTRIBUTING.md', 'SECURITY.md', 'docs/releases.md', 'docs/executable-tickets.md', 'docs/engineering.md', 'docs/overnight-pilot.md', 'controller/overnight.py', 'controller/autonomous-development-overnight@.service', + 'controller/autonomous-development.service', 'controller/autonomous-development.timer', + 'controller/autonomous-development-pilot@.service', + 'scripts/validate-poc.py', 'scripts/check-dead-code.sh', 'tabellio.validation.json', + '.github/workflows/product-validation.yml', '.github/workflows/dead-code.yml', + 'tests/test_control.py', 'tests/test_engineering.py', 'tests/test_overnight.py', + 'tests/test_package.py', 'tests/test_pi_inference.py', 'tests/test_portable_runtime.py', + 'tests/test_recovery.py', 'tests/lead_gateway.test.mjs', + 'tests/fixtures/published-demo/sample.py', 'tests/fixtures/published-demo/checks.py', + 'tests/fixtures/published-demo/repair.patch', 'tests/fixtures/published-demo/receipt.json', ) WORKER = ( 'agent/run-worker.py', 'agent/worker_protocol.py', 'agent/Dockerfile.runtime', diff --git a/scripts/validate-poc.py b/scripts/validate-poc.py new file mode 100644 index 0000000..11bc18c --- /dev/null +++ b/scripts/validate-poc.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +"""Persist each static validator invocation, including failed commands.""" +import json +from pathlib import Path +import subprocess +import time + +root=Path(__file__).resolve().parents[1] +head=subprocess.check_output(['git','rev-parse','HEAD'],cwd=root,text=True).strip() +dirty=bool(subprocess.check_output(['git','status','--porcelain'],cwd=root,text=True).strip()) +manifest=json.loads((root/'tabellio.validation.json').read_text()) +results=[] +for validator in manifest['validators']: + start=time.time() + path=root/validator['evidence']['path'];path.parent.mkdir(parents=True,exist_ok=True) + try: + result=subprocess.run(validator['argv'],cwd=root/validator['cwd'],capture_output=True,text=True,timeout=validator['timeoutMs']/1000) + log=result.stdout+'\n'+result.stderr;status='passed' if result.returncode==0 else 'failed' + except Exception as error:log=str(error);status='blocked' + path.with_suffix('.log').write_text(log) + evidence={'validatorId':validator['id'],'headSha':head,'sourceDirty':dirty,'status':status,'elapsedSeconds':time.time()-start,'log':str(path.with_suffix('.log').relative_to(root))} + path.write_text(json.dumps(evidence,indent=2)+'\n');results.append(evidence) + print(validator['id'],status) +status='failed' if any(r['status']=='failed' for r in results) else 'blocked' if dirty or any(r['status']=='blocked' for r in results) else 'passed' +(root/'.evidence/result.json').write_text(json.dumps({'headSha':head,'sourceDirty':dirty,'status':status,'reason':'Uncommitted source changes; exact-commit validation requires a clean checkout' if dirty and status=='blocked' else None,'validators':results},indent=2)+'\n') +raise SystemExit(0 if status=='passed' else 1) diff --git a/tabellio.validation.json b/tabellio.validation.json new file mode 100644 index 0000000..28c7963 --- /dev/null +++ b/tabellio.validation.json @@ -0,0 +1,97 @@ +{ + "schemaVersion": "tabellio-validation/v0.2", + "id": "autonomous-development-consolidation", + "requireEntireCheckpoint": false, + "failFast": true, + "acceptance": { + "id": "single-maintained-public-repository", + "source": "README.md", + "risk": "medium", + "outcomes": [ + "Controller regression and authority checks remain runnable in the public source", + "The historical synthetic repair and its checks remain reproducible locally", + "Source releases preserve the maintenance tools and checks" + ], + "invariants": [ + "Human approval of exact execution packets", + "Human merge and deployment authority", + "No private ticket or account-cache publication", + "No model calls or paid validation runs" + ], + "forbiddenOutcomes": [ + "Lost outstanding website work", + "Silent approval changes", + "Deletion before runtime and hosted checks pass" + ], + "requiredValidatorTypes": [ + "static" + ] + }, + "validators": [ + { + "id": "controller-contract", + "type": "static", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tests", + "-p", + "test_*.py", + "-v" + ], + "cwd": ".", + "timeoutMs": 180000, + "required": true, + "evidence": { + "path": ".evidence/controller-contract.json" + }, + "policy": { + "metricThresholds": [], + "maxCostUsd": null, + "requireCostTelemetry": false + } + }, + { + "id": "source-compilation", + "type": "static", + "argv": [ + "python3", + "-c", + "import pathlib,py_compile; [py_compile.compile(str(p),doraise=True) for name in ('controller','agent','scripts') for p in pathlib.Path(name).rglob('*.py')]" + ], + "cwd": ".", + "timeoutMs": 60000, + "required": true, + "evidence": { + "path": ".evidence/source-compilation.json" + }, + "policy": { + "metricThresholds": [], + "maxCostUsd": null, + "requireCostTelemetry": false + } + }, + { + "id": "preserved-demo", + "type": "static", + "argv": [ + "python3", + "tests/fixtures/published-demo/checks.py" + ], + "cwd": ".", + "timeoutMs": 30000, + "required": true, + "evidence": { + "path": ".evidence/preserved-demo.json" + }, + "policy": { + "metricThresholds": [], + "maxCostUsd": null, + "requireCostTelemetry": false + } + } + ] +} diff --git a/tests/fixtures/published-demo/checks.py b/tests/fixtures/published-demo/checks.py new file mode 100644 index 0000000..629ff36 --- /dev/null +++ b/tests/fixtures/published-demo/checks.py @@ -0,0 +1,6 @@ +import json +from pathlib import Path +from sample import summarize +assert summarize([]) == {"count": 0, "total": 0} +assert summarize([1, 2, 3]) == {"count": 3, "total": 6} +assert summarize([-2, 5]) == {"count": 2, "total": 3} diff --git a/tests/fixtures/published-demo/receipt.json b/tests/fixtures/published-demo/receipt.json new file mode 100644 index 0000000..6c9b5ac --- /dev/null +++ b/tests/fixtures/published-demo/receipt.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 1, + "classification": "historical supervised synthetic workflow", + "sourceRepository": "IntelIP/autonomous-development-alpha-demo", + "seedCommit": "27d73c9fbd580df35a2a69aa2fb2044e0563426f", + "candidateCommit": "4e77b8d42fe474c0a83e84e25f4c5eea8f776add", + "mergedCommit": "5ed0e14fd4e57b6ea63d4026bbe8ffd3a77fc2bc", + "createdAt": "2026-09-30T02:02:22Z", + "mergedAt": "2026-09-30T02:40:59Z", + "originalPullRequestUrl": "https://github.com/IntelIP/autonomous-development-alpha-demo/pull/1", + "runId": "bdb59c6b615c63bf", + "controllerSource": "8a6c8f029fedb0947c080dbbe3641e0b2411a60b", + "worker": { + "runtime": "Codex", + "version": "0.155.1", + "model": "gpt-6-sol", + "profile": "minimal" + }, + "leadReview": { + "approved": true, + "headSha": "4e77b8d42fe474c0a83e84e25f4c5eea8f776add" + }, + "checks": { + "command": [ + "python3", + "checks.py" + ], + "exitCode": 0, + "cases": [ + "empty", + "positive integers", + "mixed-sign integers" + ] + }, + "limitations": [ + "Existing authenticated Windows WSL host", + "Operator setup repairs required", + "Synthetic example; not a fresh-host or repeated-reliability claim" + ], + "recordSource": "Published source demo record and exported GitHub PR metadata" +} diff --git a/tests/fixtures/published-demo/repair.patch b/tests/fixtures/published-demo/repair.patch new file mode 100644 index 0000000..ac74a54 --- /dev/null +++ b/tests/fixtures/published-demo/repair.patch @@ -0,0 +1,13 @@ +diff --git a/sample.py b/sample.py +index d396595..de60d26 100644 +--- a/sample.py ++++ b/sample.py +@@ -2,7 +2,7 @@ + import json + + def summarize(values): +- return {"count": len(values), "total": len(values)} ++ return {"count": len(values), "total": sum(values)} + + if __name__ == "__main__": + print(json.dumps(summarize([1, 2, 3]), sort_keys=True)) diff --git a/tests/fixtures/published-demo/sample.py b/tests/fixtures/published-demo/sample.py new file mode 100644 index 0000000..de60d26 --- /dev/null +++ b/tests/fixtures/published-demo/sample.py @@ -0,0 +1,8 @@ +"""Demonstration data: summarize a list of integers.""" +import json + +def summarize(values): + return {"count": len(values), "total": sum(values)} + +if __name__ == "__main__": + print(json.dumps(summarize([1, 2, 3]), sort_keys=True)) diff --git a/tests/lead_gateway.test.mjs b/tests/lead_gateway.test.mjs new file mode 100644 index 0000000..9706fee --- /dev/null +++ b/tests/lead_gateway.test.mjs @@ -0,0 +1,51 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { runLead, gatewayOptions } from '../controller/lead_gateway.mjs'; + +test('large prompt stays intact and acceptance is saved before completion', async () => { + const request = { requestId: 'request-1', agent: 'lead', prompt: '💡'.repeat(40000), deadlineMs: Date.now() + 5000 }; + const saved = [], calls = []; + const result = { result: { payloads: [{ text: '{"approved":true}' }] } }; + const actual = await runLead(request, null, (callbacks) => ({ + start: () => callbacks.onHelloOk({ policy: { maxPayload: 512 * 1024 } }), + stop: () => callbacks.onClose(), + request: async (method, params, opts) => { + calls.push(method); + assert.equal(params.message, request.prompt); + opts.onAccepted({ runId: 'native-run-1' }); + assert.equal(saved[0].runId, 'native-run-1'); + return result; + }, + }), (value) => saved.push(value)); + assert.deepEqual(actual, result); + assert.deepEqual(calls, ['agent']); + assert.equal(saved[1].status, 'completed'); +}); + +test('reconnect waits for the accepted run without submitting another turn', async () => { + const calls = [], saved = []; + await assert.rejects(runLead({ requestId: 'r', deadlineMs: Date.now() + 5000 }, { runId: 'existing' }, (callbacks) => ({ + start: () => callbacks.onHelloOk({}), stop() {}, + request: async (method, params) => { calls.push(method); assert.equal(params.runId, 'existing'); return { status: 'ok' }; }, + }), (value) => saved.push(value)), /no replay/); + assert.deepEqual(calls, ['agent.wait']); + assert.equal(saved[0].waitOutcome.status, 'ok'); +}); + +test('gateway payload limit blocks dispatch before any agent starts', async () => { + let calls = 0; + await assert.rejects(runLead({ requestId: 'r', agent: 'lead', prompt: 'x'.repeat(200), deadlineMs: Date.now() + 5000 }, null, (callbacks) => ({ + start: () => callbacks.onHelloOk({ policy: { maxPayload: 100 } }), stop() {}, request: async () => { calls++; }, + }), () => {}), /payload limit/); + assert.equal(calls, 0); +}); + + +test('gateway defaults and authentication preserve local runtime settings', () => { + for (const config of [{}, { gateway: null }, { gateway: { auth: null } }]) { + assert.deepEqual(gatewayOptions(config), { url: 'ws://127.0.0.1:18789', token: undefined, password: undefined }); + } + assert.deepEqual(gatewayOptions({ gateway: { port: 12345, auth: { token: 'fixture-token', password: 'fixture-password' } } }), { + url: 'ws://127.0.0.1:12345', token: 'fixture-token', password: 'fixture-password', + }); +}); diff --git a/tests/test_control.py b/tests/test_control.py new file mode 100644 index 0000000..364e161 --- /dev/null +++ b/tests/test_control.py @@ -0,0 +1,54 @@ +import datetime as dt +import importlib.util +from pathlib import Path +import unittest +import tempfile +import shutil +from unittest.mock import patch + +spec=importlib.util.spec_from_file_location('control',Path(__file__).parents[1]/'controller/control.py') +c=importlib.util.module_from_spec(spec);spec.loader.exec_module(c) + +class Contract(unittest.TestCase): + def setUp(self): + self.temporary=tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.root=Path(self.temporary.name) + sources=[] + for name in c.SOURCES: + path=self.root/name;path.parent.mkdir(parents=True,exist_ok=True) + body=('Synthetic acceptance reference: '+name+'\n').encode() + path.write_bytes(body) + sources.append({'id':path.stem,'path':name,'owner':'test fixture','revision':'1','sha256':c.digest(body)}) + (self.root/'docs/sources.json').write_text(c.json.dumps({'sources':sources})) + + def test_window_dst_and_cutoff(self): + for stamp,active in [('2026-09-26T01:59:00+00:00',False),('2026-09-26T02:00:00+00:00',True),('2026-09-26T10:00:00+00:00',False),('2026-12-26T03:00:00+00:00',True)]: + self.assertEqual(c.window(dt.datetime.fromisoformat(stamp))[0],active) + + def test_lead_cannot_expand_or_overlap(self): + plan={'assignments':[{'id':key,'path':value,'instructions':'Improve the hero and supporting copy with concrete product detail. Explain the intended operating window, review artifact and human merge decision. Preserve the existing schema.','sourceIds':['product','claims','architecture','design']} for key,value in c.OWNERS.items()]} + self.assertEqual(c.validate_plan(plan),plan) + plan['assignments'][0]['path']='../../etc/passwd' + with self.assertRaises(ValueError):c.validate_plan(plan) + + def test_required_references(self): + self.assertEqual(len(c.references(self.root)),4) + + def test_packet_staleness_and_approval(self): + story={'id':'4da1b8be-7b33-5ce2-8f9f-07ab7d43076e','project_id':c.PROJECT,'updated_at':'2026-09-26','title':'Copy improvement','description':'Acceptance: improve the two approved copy surfaces.'} + with patch.object(c,'git',return_value='a'*40): + packet=c.compile_packet(story,root=self.root) + self.assertEqual(packet,c.compile_packet(story,root=self.root)) + with self.assertRaisesRegex(ValueError,'approval'):c.validate_packet(packet,root=self.root,current=story) + packet['approval']={'owner':'Hudson Aikins','approvedAt':'2026-09-26','packetDigest':c.digest(c.json.dumps(packet,sort_keys=True).encode())} + c.validate_packet(packet,root=self.root,current=story) + with self.assertRaisesRegex(ValueError,'Stale issue'):c.validate_packet(packet,root=self.root,current=dict(story,updated_at='newer')) + packet['limits']={'workers':3} + with self.assertRaisesRegex(ValueError,'authority'):c.validate_packet(packet,root=self.root,current=story) + with tempfile.TemporaryDirectory() as directory: + root=Path(directory);shutil.copytree(self.root/'docs',root/'docs') + (root/'docs/product.md').write_text('Unapproved replacement') + with self.assertRaisesRegex(ValueError,'Stale or missing'):c.references(root) + +if __name__=='__main__':unittest.main() diff --git a/tests/test_engineering.py b/tests/test_engineering.py new file mode 100644 index 0000000..aadf7ab --- /dev/null +++ b/tests/test_engineering.py @@ -0,0 +1,222 @@ +import copy +import importlib.util +import json +import os +from pathlib import Path +import sys +import subprocess +import tempfile +import time +import unittest +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).parents[1] / 'controller')) +import engineering as e + + +class EngineeringContract(unittest.TestCase): + def test_lead_profile_rejects_wrong_model_without_fallback(self): + profile = {'agent': 'autonomous-lead-codex', 'provider': 'pi-subscription', 'model': 'gpt-6-sol'} + envelope = {'result': {'payloads': [{'text': '{"approved":true}'}], + 'meta': {'agentMeta': {'provider': 'inference', 'model': 'gpt-oss-pi:latest'}}}} + with tempfile.TemporaryDirectory() as temp: + with patch.object(e.c, 'gateway_request', return_value=json.dumps(envelope)) as command: + with self.assertRaisesRegex(ValueError, 'no fallback'): + e.c.lead('Review', Path(temp), 'review', time.time() + 60, profile=profile) + self.assertEqual(command.call_count, 1) + self.assertEqual(profile['agent'], command.call_args.args[4]) + envelope['result']['meta']['agentMeta'] = {'provider': profile['provider'], 'model': profile['model']} + with patch.object(e.c, 'gateway_request', return_value=json.dumps(envelope)): + self.assertEqual(e.c.lead('Review', Path(temp), 'review', time.time() + 60, profile=profile), {'approved': True}) + + def test_review_rejects_stale_or_unanchored_findings(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp); (root / 'code.go').write_text('func pathsAlias() {}\n') + decision = {'headSha': 'a' * 40, 'approved': False, 'summary': 'Defect found', + 'findings': [{'path': 'code.go', 'line': 1, 'quote': 'func pathsAlias()', 'behavior': 'Specific incorrect behavior', 'severity': 'major'}]} + e.validate_review(decision, 'a' * 40, root) + for change in [lambda r: r.update(headSha='b' * 40), lambda r: r.update(findings=[]), + lambda r: r['findings'][0].update(quote='ConfidenceHigh'), + lambda r: r['findings'][0].update(path='../outside.go')]: + bad = copy.deepcopy(decision); change(bad) + with self.assertRaises(ValueError): e.validate_review(bad, 'a' * 40, root) + + def test_review_ignores_quote_indentation_but_keeps_source_text_and_line(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp); (root / 'code.go').write_text('\treturn value\nother line\n') + decision = {'headSha': 'a' * 40, 'approved': False, 'summary': 'Defect found', + 'findings': [{'path': 'code.go', 'line': 1, 'quote': ' return value ', + 'behavior': 'Specific incorrect behavior', 'severity': 'major'}]} + e.validate_review(decision, 'a' * 40, root) + for field, value in [('quote', 'return different'), ('line', 2)]: + bad = copy.deepcopy(decision); bad['findings'][0][field] = value + with self.assertRaises(ValueError): e.validate_review(bad, 'a' * 40, root) + + def ticket(self, contract): + return {'schemaVersion': 1, 'outcome': 'Explain cost variance to an engineer', + 'acceptance': [{'id': 'variance', 'scenario': 'Actual exceeds forecast', 'expected': 'Variance reconciles', 'check': 0}], + 'exclusions': ['No deployment'], 'dependencies': [], + 'interfaces': [{'owners': list(contract['scopes']), 'contract': 'Both engineers use the same explicit input and output contract.'}], + 'execution': contract, 'limits': e.c.LIMITS, + 'completion': {'success': 'draft-pr', 'failure': 'blocked-report', 'merge': 'human'}} + + def test_ticket_separates_history_and_rejects_missing_contracts(self): + ticket = self.ticket(self.contract(Path('/tmp/product'))) + def description(value): + return 'Historical failure: eight tests failed.\n```autonomous-ticket\n' + json.dumps(value) + '\n```\nLater history.' + parsed = e.tickets.extract(description(ticket), e.validate_contract, e.c.LIMITS) + self.assertNotIn('Historical failure', e.tickets.acceptance(parsed)) + for change in [lambda t: t.update(interfaces=[]), lambda t: t.update(acceptance=[]), + lambda t: t['acceptance'][0].update(check=7), lambda t: t['completion'].update(merge='automatic'), + lambda t: t['interfaces'][0].update(owners=['implementation'])]: + bad = copy.deepcopy(ticket); change(bad) + with self.assertRaises(ValueError): + e.tickets.extract(description(bad), e.validate_contract, e.c.LIMITS) + with self.assertRaises(ValueError): + e.tickets.extract('Implement something useful.', e.validate_contract, e.c.LIMITS) + + def test_lead_json_retry_is_strict_bounded_and_recorded(self): + malformed = json.dumps({'payloads': [{'text': '{"assignments":[{"instructions":"unterminated'}]}) + valid = json.dumps({'payloads': [{'text': '{"assignments": []}'}]}) + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + with patch.object(e.c, 'gateway_request', side_effect=[malformed, valid]) as command: + self.assertEqual(e.c.lead('Return assignments.', root, 'plan', time.time() + 60), {'assignments': []}) + self.assertEqual(command.call_count, 2) + self.assertIn('invalid JSON', command.call_args.args[0]) + self.assertEqual((root / 'plan-raw.json').read_text().strip(), malformed) + self.assertEqual((root / 'plan-json-retry-raw.json').read_text().strip(), valid) + with patch.object(e.c, 'gateway_request', return_value=malformed) as command: + with self.assertRaises(json.JSONDecodeError): + e.c.lead('Return assignments.', root, 'bad', time.time() + 60) + self.assertEqual(command.call_count, 2) + with patch.object(e.c, 'gateway_request', return_value=malformed) as command, \ + patch.object(e.c.time, 'time', side_effect=[100, 100, 161]): + with self.assertRaises(TimeoutError): + e.c.lead('Return assignments.', root, 'expired', 160) + self.assertEqual(command.call_count, 1) + + def test_lead_transport_preserves_large_prompt_as_data(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + prompt = '\U0001f4a1' * 40000 + ' $(printf injected) `printf injected`\nend' + envelope = {'payloads': [{'text': '{"approved":true}'}]} + def invoke(argv, **_kwargs): + self.assertTrue(all(len(arg.encode()) < 32768 for arg in argv)) + self.assertNotIn(prompt, argv) + request = json.loads((root / 'transport-test-gateway/request.json').read_text()) + self.assertEqual(request['prompt'], prompt) + if 'download' in argv: + Path(argv[-1]).write_text(json.dumps({'requestId': request['requestId'], 'result': envelope})) + return json.dumps(envelope) + with patch.object(e.c, 'command', side_effect=invoke): + self.assertEqual(e.c.lead(prompt, root, 'transport-test', time.time() + 60), {'approved': True}) + with patch.object(e.c, 'command') as command: + self.assertEqual(e.c.lead(prompt, root, 'transport-test', time.time() + 60), {'approved': True}) + command.assert_not_called() + with patch.object(e.c, 'command') as command: + with self.assertRaisesRegex(ValueError, 'context budget'): + e.c.lead('\U0001f4a1' * 140000, root, 'too-large', time.time() + 60) + command.assert_not_called() + + def test_gateway_client_lifecycle(self): + subprocess.run(['node', '--test', str(e.c.ROOT / 'tests/lead_gateway.test.mjs')], check=True, capture_output=True, text=True) + + def test_resume_preserves_product_authority_and_repair_budget(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp); run = 'a' * 16; directory = root / 'runs' / run + directory.mkdir(parents=True) + packet = {'contract': self.contract(root), 'imageId': 'sha256:original', 'controllerSources': {'controller': 'old'}} + previous = {'status': 'blocked', 'packet': packet, 'checks': [], + 'workers': [{'id': key, 'attempt': 0} for key in packet['contract']['scopes']]} + receipt = directory / 'receipt.json'; receipt.write_text(json.dumps(previous)) + updated = dict(packet, controllerSources={'controller': 'fixed'}) + with patch.object(e.c, 'STATE', root): + self.assertEqual(e.resume_receipt(run, updated), previous) + with self.assertRaisesRegex(ValueError, 'authority'): + e.resume_receipt(run, dict(updated, imageId='sha256:changed')) + (directory / 'lead-repair-0.json').write_text('{}') + with self.assertRaisesRegex(ValueError, 'initial assignments'): + e.resume_receipt(run, updated) + + def contract(self, root): + return {'repositoryPath': str(root), 'githubRepository': 'IntelIP/example', 'baseBranch': 'main', 'projectId': 'a' * 36, + 'sources': ['README.md'], 'scopes': {'implementation': {'files': ['runtime/review.py'], 'directories': []}, + 'tests': {'files': [], 'directories': ['tests']}}, 'setup': [], 'checks': [['python3', '-m', 'unittest']], + 'stackRoot': str(root), 'workerImage': 'agent-stack:test', 'requiredArtifacts': []} + + def test_contract_rejects_overlap_escape_missing_checks_and_extra_authority(self): + with tempfile.TemporaryDirectory() as temp: + contract = self.contract(Path(temp)) + e.validate_contract(contract) + for mutate in [lambda c: c['scopes']['tests'].update(directories=['runtime']), + lambda c: c['scopes']['tests'].update(directories=['../outside']), + lambda c: c.update(checks=[]), lambda c: c.update(autoMerge=True)]: + bad = copy.deepcopy(contract); mutate(bad) + with self.assertRaises(ValueError): e.validate_contract(bad) + + def test_lead_cannot_change_scope_or_drop_an_engineer(self): + packet = {'contract': self.contract(Path('/tmp/product'))} + plan = {'assignments': [{'id': key, 'instructions': 'Implement the assigned product behavior from the approved references. Preserve existing contracts and edge cases. Add focused tests and report the exact commands and results.'} for key in packet['contract']['scopes']]} + self.assertEqual(e.validate_plan(plan, packet), plan) + bad = copy.deepcopy(plan); bad['assignments'][0]['path'] = '/etc/passwd' + with self.assertRaises(ValueError): e.validate_plan(bad, packet) + bad = copy.deepcopy(plan); bad['assignments'].pop() + with self.assertRaises(ValueError): e.validate_plan(bad, packet) + + def test_packet_invalidates_changed_issue_image_and_driver(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + for name in ['README.md', *e.DRIVER_FILES]: + path = root / name; path.parent.mkdir(parents=True, exist_ok=True); path.write_text('source') + contract = self.contract(root) + ticket = self.ticket(contract) + item = {'id': 'b' * 36, 'project_id': contract['projectId'], 'updated_at': 'revision-1', 'title': 'Real feature', + 'description': '```autonomous-ticket\n' + json.dumps(ticket) + '\n```'} + def git(*args, **_kwargs): return 'a' * 40 if args[0] == 'rev-parse' else '' + with patch.object(e, 'repo_git', side_effect=git), patch.object(e.c, 'command', return_value='sha256:' + '1' * 64): + packet = e.compile_packet(contract, item) + packet['approval'] = {'approvedBy': 'owner', 'packetDigest': e.c.digest(json.dumps(packet, sort_keys=True).encode())} + e.validate_packet(packet, item) + with self.assertRaisesRegex(ValueError, 'stale'): e.validate_packet(packet, dict(item, updated_at='revision-2')) + (root / e.DRIVER_FILES[0]).write_text('changed runtime') + with self.assertRaisesRegex(ValueError, 'stale'): e.validate_packet(packet, item) + (root / e.DRIVER_FILES[0]).write_text('source') + with patch.object(e.c, 'command', return_value='sha256:' + '2' * 64): + with self.assertRaisesRegex(ValueError, 'stale'): e.validate_packet(packet, item) + + def test_packet_checks_live_dependency_before_approval(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + for name in ['README.md', *e.DRIVER_FILES]: + path = root / name; path.parent.mkdir(parents=True, exist_ok=True); path.write_text('source') + contract = self.contract(root); ticket = self.ticket(contract) + ticket['dependencies'] = ['d' * 36] + item = {'id': 'b' * 36, 'project_id': contract['projectId'], 'updated_at': 'revision-1', 'title': 'Real feature', + 'description': '```autonomous-ticket\n' + json.dumps(ticket) + '\n```'} + def git(*args, **_kwargs): return 'a' * 40 if args[0] == 'rev-parse' else '' + with patch.object(e, 'repo_git', side_effect=git), patch.object(e.c, 'command', return_value='sha256:image'), \ + patch.object(e, 'story', return_value={'updated_at': 'dependency-1', 'completed_at': '2026-01-01'}) as dependency: + packet = e.compile_packet(None, item) + packet['approval'] = {'approvedBy': 'owner', 'packetDigest': e.c.digest(json.dumps(packet, sort_keys=True).encode())} + e.validate_packet(packet, item) + dependency.return_value = {'updated_at': 'dependency-2', 'completed_at': None} + with self.assertRaisesRegex(ValueError, 'Dependency is not complete'): + e.validate_packet(packet, item) + + def test_ownership_does_not_match_neighboring_directory(self): + scope = {'files': ['runtime/review.py'], 'directories': ['tests']} + self.assertTrue(e.owns('tests/test_review.py', scope)) + self.assertFalse(e.owns('tests-other/test_review.py', scope)) + self.assertFalse(e.owns('runtime/unrelated.py', scope)) + + def test_cleanup_targets_only_the_interrupted_assignment(self): + with patch.object(e.c, 'command', side_effect=['container-1\n', '']) as command: + e.cleanup('a' * 16, 'implementation') + query = command.call_args_list[0].args[0] + self.assertIn('label=ad.run=' + 'a' * 16, query) + self.assertIn('label=ad.assignment=implementation', query) + self.assertEqual(command.call_args_list[1].args[0][-3:], ['rm', '-f', 'container-1']) + + +if __name__ == '__main__': unittest.main() diff --git a/tests/test_overnight.py b/tests/test_overnight.py new file mode 100644 index 0000000..3b45ea9 --- /dev/null +++ b/tests/test_overnight.py @@ -0,0 +1,30 @@ +import datetime as dt +import json +from pathlib import Path +import sys +import tempfile +import unittest +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'controller')) +import overnight + + +class OvernightTests(unittest.TestCase): + def test_dated_admission_never_bypasses_engineering_window(self): + with tempfile.TemporaryDirectory() as directory: + packet = Path(directory) / 'packet.json' + packet.write_text(json.dumps({'approved': 'fixture'})) + schedule = {'packet': str(packet), 'startAt': '2026-09-29T22:00:00-04:00', + 'latestStartAt': '2026-09-29T23:00:00-04:00'} + with patch.object(overnight.e, 'execute', return_value={'status': 'blocked'}) as execute: + for stamp, status in [('2026-09-29T21:59:59-04:00', 'not-started'), + ('2026-09-29T23:00:00-04:00', 'expired'), + ('2026-09-30T22:00:00-04:00', 'expired')]: + self.assertEqual(overnight.run(schedule, dt.datetime.fromisoformat(stamp))['status'], status) + execute.assert_not_called() + self.assertEqual(overnight.run(schedule, dt.datetime.fromisoformat(schedule['startAt']))['status'], 'blocked') + execute.assert_called_once_with({'approved': 'fixture'}) + for invalid in ('2026-09-29T23:00:00', '2026-09-30T23:00:00-04:00'): + with self.assertRaises(ValueError): + overnight.run(dict(schedule, latestStartAt=invalid)) diff --git a/tests/test_package.py b/tests/test_package.py new file mode 100644 index 0000000..10cde4c --- /dev/null +++ b/tests/test_package.py @@ -0,0 +1,54 @@ +import hashlib +import importlib.util +import json +from pathlib import Path +import tarfile +import tempfile +import unittest +from unittest.mock import patch + + +class PackageTests(unittest.TestCase): + def test_source_bundle_keeps_checks_services_and_demo(self): + root=Path(__file__).resolve().parents[1] + spec=importlib.util.spec_from_file_location('alpha_source_package',root/'scripts/package-alpha.py') + module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module) + with tempfile.TemporaryDirectory() as temporary: + archive=Path(temporary)/'source.tar.gz' + manifest=module.package(root,archive,version='0.1.0-alpha.3') + required={'tests/test_engineering.py','tests/test_control.py','tests/lead_gateway.test.mjs', + 'tests/fixtures/published-demo/receipt.json','scripts/validate-poc.py', + '.github/workflows/product-validation.yml','controller/autonomous-development.timer'} + self.assertTrue(required.issubset(manifest['files'])) + with tarfile.open(archive) as bundle: + for name,digest in manifest['files'].items(): + self.assertEqual(hashlib.sha256(bundle.extractfile('autonomous-development-alpha/'+name).read()).hexdigest(),digest) + + def test_archive_is_reproducible_and_excludes_unlisted_files(self): + spec = importlib.util.spec_from_file_location('alpha_package', Path(__file__).parents[1] / 'scripts/package-alpha.py') + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + controller, worker = root / 'controller', root / 'worker' + for directory, names in ((controller, module.CONTROLLER), (worker, module.WORKER)): + for name in names: + path = directory / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(name + '\n') + (directory / 'auth.json').write_text('must never ship') + a, b = root / 'a.tar.gz', root / 'b.tar.gz' + with patch.object(module, 'ROOT', controller), patch.object(module, 'source_revision', return_value={'commit': 'a' * 40, 'dirty': False}): + manifest = module.package(worker, a) + module.package(worker, b) + with self.assertRaises(ValueError): + module.package(worker, b, 'latest') + self.assertEqual(a.read_bytes(), b.read_bytes()) + with tarfile.open(a) as archive: + prefix = 'autonomous-development-alpha/' + self.assertEqual(set(archive.getnames()), {prefix + name for name in manifest['files']} | {prefix + 'MANIFEST.json'}) + saved = json.load(archive.extractfile(prefix + 'MANIFEST.json')) + self.assertEqual(saved, manifest) + for name, digest in manifest['files'].items(): + self.assertEqual(hashlib.sha256(archive.extractfile(prefix + name).read()).hexdigest(), digest) + self.assertEqual(archive.extractfile(prefix + 'README.md').read(), b'README.md\n') diff --git a/tests/test_pi_inference.py b/tests/test_pi_inference.py new file mode 100644 index 0000000..5e4e219 --- /dev/null +++ b/tests/test_pi_inference.py @@ -0,0 +1,52 @@ +import json +from pathlib import Path +import sys +import threading +import unittest +from urllib.error import HTTPError +from urllib.request import Request, urlopen +from unittest.mock import patch +from http.server import ThreadingHTTPServer + +sys.path.insert(0, str(Path(__file__).parents[1] / 'controller')) +import pi_inference as bridge + + +class SubscriptionBridge(unittest.TestCase): + def test_auth_tool_boundary_stream_and_provider_failure(self): + server = ThreadingHTTPServer(('127.0.0.1', 0), bridge.handler('x' * 32, Path('/existing/auth'), 'test-image')) + thread = threading.Thread(target=server.serve_forever, daemon=True); thread.start() + url = f'http://127.0.0.1:{server.server_port}/v1/chat/completions' + def request(payload, authorized=True): + return urlopen(Request(url, data=json.dumps(payload).encode(), headers={ + 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + ('x' * 32 if authorized else 'wrong')}), timeout=2) + body = {'model': bridge.MODEL, 'messages': [{'role': 'user', 'content': 'Return OK'}], 'stream': True} + try: + with patch.object(bridge, 'infer', return_value=('OK', {'total_tokens': 2})) as infer: + for payload, authorized, code in [(body, False, 401), (dict(body, tools=[{}]), True, 400), (dict(body, model='other'), True, 400)]: + with self.assertRaises(HTTPError) as error: + request(payload, authorized) + self.assertEqual(error.exception.code, code) + infer.assert_not_called() + with request(body) as response: + content = response.read().decode() + self.assertIn('"content": "OK"', content); self.assertIn('data: [DONE]', content) + with request(dict(body, tools=[{'type': 'function', 'function': {'name': 'tool_call'}}])) as response: + self.assertIn('data: [DONE]', response.read().decode()) + self.assertEqual(infer.call_args.args[0], body['messages']) + infer.side_effect = RuntimeError('upstream') + with self.assertRaises(HTTPError) as error: + request(body) + self.assertEqual(error.exception.code, 502) + finally: + server.shutdown(); server.server_close(); thread.join() + + def test_completed_pi_response_required(self): + event = {'type': 'message_end', 'message': {'role': 'assistant', 'stopReason': 'stop', + 'content': [{'type': 'text', 'text': 'OK'}], 'usage': {'input': 3, 'cacheRead': 2, 'output': 1, 'totalTokens': 6}}} + self.assertEqual(bridge.completion(json.dumps(event)), ('OK', {'prompt_tokens': 5, 'completion_tokens': 1, 'total_tokens': 6})) + event['message']['stopReason'] = 'error' + with self.assertRaises(ValueError): bridge.completion(json.dumps(event)) + + +if __name__ == '__main__': unittest.main() diff --git a/tests/test_portable_runtime.py b/tests/test_portable_runtime.py new file mode 100644 index 0000000..65882fb --- /dev/null +++ b/tests/test_portable_runtime.py @@ -0,0 +1,53 @@ +import copy +import importlib.util +import subprocess +import json +import os +from pathlib import Path +import sys +import tempfile +import unittest +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).parents[1] / 'controller')) +import engineering as e + + +class PortableRuntime(unittest.TestCase): + def test_local_ticket_revision_changes_with_content(self): + with tempfile.TemporaryDirectory() as temp: + path = Path(temp) / 'tickets.json' + identity = 'a' * 36 + item = {'id': identity, 'project_id': 'project', 'title': 'Task', 'description': 'Before'} + path.write_text(json.dumps({identity: item})) + with patch.dict(os.environ, {'AD_TICKETS_FILE': str(path)}): + before = e.story(identity, 'project') + item['description'] = 'After' + path.write_text(json.dumps({identity: item})) + after = e.story(identity, 'project') + self.assertNotEqual(before['updated_at'], after['updated_at']) + with self.assertRaises(ValueError): e.story(identity, 'other-project') + + def test_demo_contracts_and_seeded_failures(self): + spec = importlib.util.spec_from_file_location('demo', Path(__file__).parents[1] / 'scripts/prepare-demo.py') + demo = importlib.util.module_from_spec(spec); spec.loader.exec_module(demo) + with tempfile.TemporaryDirectory() as temp: + for workflow in ('bugfix', 'docs'): + root = Path(temp) / workflow + info = demo.prepare(root, 'example/demo', Path(temp), 'demo:1', 'codex', '', workflow) + with patch.dict(os.environ, {'AD_TICKETS_FILE': info['ticketFile']}): + item = e.story(info['issueId'], info['projectId']) + ticket = e.tickets.extract(item['description'], e.validate_contract, e.c.LIMITS) + e.validate_contract(ticket['execution']) + self.assertNotIn('workerModel', ticket['execution']) + self.assertNotEqual(subprocess.run(['python3', 'checks.py'], cwd=root, capture_output=True).returncode, 0) + + def test_runtime_choice_is_part_of_approved_authority(self): + packet = {'contract': {'workerAgent': 'pi'}, 'approval': {'approvedBy': 'operator'}} + before = e.approval_digest(packet) + changed = copy.deepcopy(packet) + changed['contract']['workerAgent'] = 'codex' + self.assertNotEqual(before, e.approval_digest(changed)) + + +if __name__ == '__main__': unittest.main() diff --git a/tests/test_recovery.py b/tests/test_recovery.py new file mode 100644 index 0000000..7eb9b98 --- /dev/null +++ b/tests/test_recovery.py @@ -0,0 +1,220 @@ +"""Recovery boundaries: retain authority, reuse commits, and avoid duplicate PRs.""" +import datetime as dt +import copy +import json +from pathlib import Path +import subprocess +import sys +import tempfile +import time +import unittest +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).parents[1] / 'controller')) +import engineering as e + + +class Recovery(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.state = Path(self.temp.name) + self.state_patch = patch.object(e.c, 'STATE', self.state) + self.state_patch.start(); self.addCleanup(self.state_patch.stop) + self.run = 'a' * 16 + self.directory = self.state / 'runs' / self.run + self.tree = self.state / 'engineering-workspaces' / self.run / 'integration-0' + self.tree.mkdir(parents=True) + subprocess.run(['git', 'init', '-q', str(self.tree)], check=True) + (self.tree / 'source.txt').write_text('original\n') + e.repo_git('add', 'source.txt', cwd=self.tree) + e.repo_git('-c', 'user.name=Test', '-c', 'user.email=test@localhost', 'commit', '-qm', 'base', cwd=self.tree) + self.sha = e.repo_git('rev-parse', 'HEAD', cwd=self.tree) + self.packet = {'schemaVersion': 2, 'recoveryPolicy': e.RECOVERY_POLICY, + 'issueId': 'test-issue', 'contract': {'requiredArtifacts': [], 'projectId': 'project', + 'githubRepository': 'IntelIP/example', 'baseBranch': 'main'}} + self.report = {'passed': True, 'artifacts': {}} + self.review = {'headSha': self.sha, 'approved': True, 'findings': [], 'summary': 'Test response'} + self.receipt = {'runId': self.run, 'status': 'running', 'packet': self.packet, + 'startedAt': dt.datetime.now(dt.timezone.utc).isoformat(), 'workers': [], 'checks': [], + 'infrastructureRetries': 0} + e.checkpoint(self.receipt, self.tree, self.directory, 'validate') + + def test_supplemental_repair_binds_source_scope_and_runs_only_once(self): + self.packet.update(baseSha=self.sha, leadProfile={'agent': 'test'}) + self.packet['contract'].update(repositoryPath=str(self.tree), sources=['source.txt'], + scopes={'cli': {'files': ['source.txt'], 'directories': []}, + 'engine': {'files': ['other.txt'], 'directories': []}}) + self.receipt['status'] = 'blocked' + e.c.save(self.directory / 'receipt.json', self.receipt) + repair = {'sourceRun': self.run, 'scope': 'cli', 'headSha': self.sha} + packet = dict(self.packet, approval={'approvedBy': 'owner', 'repair': repair}) + original_digest = e.approval_digest(packet) + packet['approval']['repair'] = dict(repair, scope='engine') + self.assertNotEqual(original_digest, e.approval_digest(packet)) + packet['approval']['repair'] = repair + packet['approval']['packetDigest'] = original_digest + self.assertEqual(e.repair_source(repair, packet)[0], self.tree) + (self.state / 'auth').mkdir(); (self.state / 'auth/auth.json').write_text('{}') + assignment = {'id': 'cli', 'instructions': 'Fix the reported behavior and add the missing regression while preserving all existing interfaces and checks. ' * 2} + def clone(root, dest, branch, base, **_): + dest.parent.mkdir(parents=True, exist_ok=True) + subprocess.run(['git', 'clone', '-q', str(root), str(dest)], check=True) + e.repo_git('config', 'user.name', 'Test', cwd=dest) + e.repo_git('config', 'user.email', 'test@localhost', cwd=dest) + e.repo_git('checkout', '-b', branch, base, cwd=dest) + def worker(task, _packet, tree, *_): + (tree / 'source.txt').write_text('repaired\n') + e.repo_git('add', 'source.txt', cwd=tree) + e.repo_git('-c', 'user.name=Test', '-c', 'user.email=test@localhost', 'commit', '-qm', 'repair', cwd=tree) + return {'id': task['id'], 'attempt': 0, 'commit': e.repo_git('rev-parse', 'HEAD', cwd=tree), + 'branch': e.repo_git('branch', '--show-current', cwd=tree), 'paths': ['source.txt']} + with patch.object(e, 'validate_packet'), patch.object(e, 'story', return_value={}), \ + patch.object(e.c, 'recover'), patch.object(e.c, 'lead', return_value={'assignments': [assignment]}) as lead, \ + patch.object(e, 'clone', side_effect=clone), patch.object(e, 'worker', side_effect=worker) as engineer, \ + patch.object(e, 'check', return_value={'passed': False}), patch.object(e, 'cleanup'), \ + patch.object(e, 'publish_candidate') as publish: + result = e.execute(packet, pilot=True) + self.assertEqual(result['status'], 'blocked') + self.assertIn('no second coding invocation', result['reason']) + engineer.assert_called_once(); lead.assert_called_once(); publish.assert_not_called() + self.assertIn('Active assignment scope IDs: ["cli"]', lead.call_args.args[0]) + with self.assertRaisesRegex(ValueError, 'assignment authority'): + e.validate_plan({'assignments': [assignment, dict(assignment, id='engine')]}, packet, {'cli'}) + self.assertEqual(e.repo_git('rev-parse', 'HEAD', cwd=self.tree), self.sha) + + def test_review_repair_shares_budget_and_changes_review_identity(self): + self.packet.update(baseSha=self.sha, leadProfile={}) + self.packet['contract'].update(repositoryPath=str(self.tree), sources=['source.txt'], + scopes={'cli': {'files': ['source.txt'], 'directories': []}}) + (self.state / 'auth').mkdir() + (self.state / 'auth/auth.json').write_text('{}') + assignment = {'id': 'cli', 'instructions': 'Implement only the approved source file and preserve the interface. Add the required focused regression, repair the identified behavior, and report the result.'} + def clone(root, dest, branch, base, **_): + dest.parent.mkdir(parents=True, exist_ok=True) + subprocess.run(['git', 'clone', '-q', str(root), str(dest)], check=True) + e.repo_git('config', 'user.name', 'Test', cwd=dest) + e.repo_git('config', 'user.email', 'test@localhost', cwd=dest) + e.repo_git('checkout', '-b', branch, base, cwd=dest) + def worker(task, _packet, tree, _directory, _deadline, attempt): + (tree / 'source.txt').write_text('repair-' + str(attempt) + '\n') + e.repo_git('add', 'source.txt', cwd=tree) + e.repo_git('commit', '-qm', 'worker', cwd=tree) + return {'id': task['id'], 'attempt': attempt, 'commit': e.repo_git('rev-parse', 'HEAD', cwd=tree), + 'branch': e.repo_git('branch', '--show-current', cwd=tree), 'paths': ['source.txt']} + for last_approved in (False, True): + packet = dict(self.packet, approval={}, scenario=last_approved) + phases = [] + def review(_packet, tree, _directory, _report, _deadline, phase='lead-review'): + phases.append(phase) + approved = last_approved and len(phases) == 2 + return {'headSha': e.repo_git('rev-parse', 'HEAD', cwd=tree), 'approved': approved, + 'summary': 'Fixture review', 'findings': [] if approved else [{'path': 'source.txt', + 'line': 1, 'quote': (tree / 'source.txt').read_text().strip(), 'severity': 'major', 'behavior': 'Required case still fails'}]} + def publish(receipt, *_): receipt.update(status='draft-pr-created', prUrl='fixture://draft') + with patch.object(e, 'validate_packet'), patch.object(e, 'story', return_value={}), \ + patch.object(e.c, 'recover'), patch.object(e.c, 'lead', side_effect=lambda *_, **__: copy.deepcopy({'assignments': [assignment]})), \ + patch.object(e, 'clone', side_effect=clone), patch.object(e, 'worker', side_effect=worker) as engineer, \ + patch.object(e, 'check', return_value=self.report), patch.object(e, 'cleanup'), \ + patch.object(e, 'review_candidate', side_effect=review), patch.object(e, 'publish_candidate', side_effect=publish) as publication: + result = e.execute(packet, pilot=True) + self.assertEqual(engineer.call_count, 2, result.get('reason')) + self.assertEqual(phases, ['lead-review-0', 'lead-review-1']) + self.assertEqual(result['status'], 'draft-pr-created' if last_approved else 'blocked') + self.assertEqual(publication.call_count, int(last_approved)) + self.assertEqual([w['attempt'] for w in result['workers']], [0, 1]) + + def test_interrupted_validation_resumes_without_replaying_workers(self): + with patch.object(e.c, 'command', return_value=''): + e.c.recover() + saved = json.loads((self.directory / 'receipt.json').read_text()) + self.assertEqual(saved['status'], 'interrupted') + def publish(receipt, *_): + receipt.update(status='draft-pr-created', headSha=self.sha, prUrl='test://draft') + with patch.object(e, 'worker') as worker, patch.object(e, 'check', return_value=self.report) as check, \ + patch.object(e, 'review_candidate', return_value=self.review), \ + patch.object(e, 'publish_candidate', side_effect=publish), patch.object(e, 'cleanup'), \ + patch.object(e, 'validate_packet'), patch.object(e, 'story', return_value={}): + result = e.resume_candidate(self.packet, saved, self.directory, time.time() + 60) + self.assertEqual(result['status'], 'draft-pr-created') + self.assertEqual(result['infrastructureRetries'], 1) + self.assertEqual(result['startedAt'], self.receipt['startedAt']) + self.assertEqual(e.repo_git('rev-parse', 'HEAD', cwd=self.tree), self.sha) + worker.assert_not_called(); check.assert_called_once() + + def test_changed_candidate_and_expired_deadline_cannot_publish(self): + (self.tree / 'source.txt').write_text('changed\n') + with patch.object(e, 'publish_candidate') as publish: + with self.assertRaisesRegex(ValueError, 'changed'): + e.finish_candidate(self.receipt, self.directory, time.time() + 60) + with self.assertRaisesRegex(ValueError, 'deadline'): + e.finish_candidate(self.receipt, self.directory, time.time() - 1) + publish.assert_not_called() + + def test_retry_budget_does_not_reset_and_worker_interruption_blocks(self): + self.receipt['infrastructureRetries'] = 1 + with patch.object(e, 'finish_candidate') as finish: + result = e.resume_candidate(self.packet, self.receipt, self.directory, time.time() + 60) + self.assertEqual(result['status'], 'blocked'); finish.assert_not_called() + self.receipt.update(status='running', infrastructureRetries=0) + self.receipt.pop('checkpoint') + e.c.save(self.directory / 'receipt.json', self.receipt) + with patch.object(e.c, 'command', return_value=''): + e.c.recover() + self.assertEqual(json.loads((self.directory / 'receipt.json').read_text())['status'], 'blocked') + + def test_existing_exact_pr_is_reconciled_without_another_push_or_create(self): + e.checkpoint(self.receipt, self.tree, self.directory, 'publish', self.report) + existing = [{'url': 'https://github.com/IntelIP/example/pull/1', 'headRefOid': self.sha, + 'baseRefName': 'main', 'state': 'OPEN'}] + original_git = e.repo_git + calls = [] + def git(*args, **kwargs): + calls.append(args) + return original_git(*args, **kwargs) + original_command = e.c.command + def command(args, **kwargs): + if 'pr' in args: + self.assertEqual(args[1:3], ['pr', 'list']) + return json.dumps(existing) + return original_command(args, **kwargs) + with patch.object(e, 'repo_git', side_effect=git), patch.object(e.c, 'command', side_effect=command): + e.publish_candidate(self.receipt, self.tree, self.directory) + self.assertEqual(self.receipt['prUrl'], existing[0]['url']) + self.assertFalse(any('push' in args for args in calls)) + + def test_ownership_cannot_escape_and_code_failure_is_not_infrastructure_retry(self): + with patch.object(e.c, 'command') as command: + with self.assertRaises(ValueError): + e.prepare_workspace(self.state) + command.assert_not_called() + e.record_failure(self.receipt, ValueError('Lead review declined result'), time.time() + 60) + self.assertEqual(self.receipt['status'], 'blocked') + e.record_failure(self.receipt, TimeoutError('Connection interrupted'), time.time() + 60) + self.assertEqual(self.receipt['status'], 'retryable') + + def test_worker_transport_failure_is_not_a_code_repair(self): + packet = dict(self.packet, issueTitle='Test', imageId='image', ticket={'interfaces': []}, acceptance='{}') + packet['contract'] = dict(packet['contract'], scopes={'engine': {'files': ['source.txt'], 'directories': []}}, + stackRoot='/unused', sources=['source.txt'], setup=[], checks=[]) + with patch.object(e.subprocess, 'run', side_effect=TimeoutError('transport lost')), patch.object(e, 'cleanup'): + with self.assertRaisesRegex(e.WorkerInfrastructureError, 'no automatic replay'): + e.worker({'id': 'engine', 'instructions': 'Test'}, packet, self.tree, self.directory, time.time() + 60, 0) + + def test_stale_queue_admission_preserves_saved_work(self): + identity = e.c.digest(json.dumps(self.packet, sort_keys=True).encode())[:16] + saved = dict(self.receipt, runId=identity, status='interrupted', workers=[{'commit': self.sha}]) + path = self.state / 'runs' / identity / 'receipt.json' + e.c.save(path, saved) + e.c.save(self.state / 'queue' / 'ticket.json', self.packet) + with patch.object(sys, 'argv', ['control.py', 'tick']), patch.object(e.c, 'window', return_value=(True, 60)), \ + patch.object(e, 'execute', side_effect=ValueError('Stale ticket')): + e.c.main() + result = json.loads(path.read_text()) + self.assertEqual(result['status'], 'blocked') + self.assertEqual(result['workers'], saved['workers']) + self.assertEqual(result['checkpoint'], saved['checkpoint']) + + +if __name__ == '__main__': + unittest.main()