feat: appove and go
This commit is contained in:
@@ -29,9 +29,79 @@ class Result:
|
||||
|
||||
|
||||
class GoalPatchWorkflowTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Keep unit tests independent from production route-health settings.
|
||||
# Tests that exercise preflight mock model_preflight explicitly.
|
||||
self.environment = patch.dict(
|
||||
os.environ, {"CASAN_GOAL_MODEL_PREFLIGHT": "0"}, clear=False,
|
||||
)
|
||||
self.environment.start()
|
||||
self.addCleanup(self.environment.stop)
|
||||
|
||||
def test_vietnamese_completion_goal_is_write_intent(self):
|
||||
self.assertTrue(ORCHESTRATOR.requests_side_effect("Hoàn thành component KeyResultDetail với form update progress đầy đủ"))
|
||||
|
||||
def test_phase_labeled_backend_completion_is_write_intent(self):
|
||||
objective = """PHASE 1 — Hoàn thiện backend Objective và Key Result cho ứng dụng OKR.
|
||||
|
||||
Chỉ làm việc trong phạm vi apps/okr/backend.
|
||||
Tạo patch nhưng không tự apply. Chờ Independent Reviewer phê duyệt.
|
||||
"""
|
||||
self.assertTrue(ORCHESTRATOR.requests_side_effect(objective))
|
||||
|
||||
def test_read_only_backend_audit_is_not_write_intent(self):
|
||||
objective = "Rà soát backend và liệt kê các file có rủi ro. Không sửa code, không tạo patch."
|
||||
self.assertFalse(ORCHESTRATOR.requests_side_effect(objective))
|
||||
|
||||
def test_scoped_negative_constraint_does_not_cancel_write_intent(self):
|
||||
objective = """Hoàn thiện tính nguyên tử của luồng cập nhật tiến độ Key Result trong ứng dụng OKR.
|
||||
|
||||
Không thay đổi API contract. Chỉ trả về unified git diff và chờ Independent Reviewer phê duyệt.
|
||||
"""
|
||||
self.assertTrue(ORCHESTRATOR.requests_side_effect(objective))
|
||||
|
||||
def test_valid_worker_patch_still_reaches_human_approval_when_h3_is_unavailable(self):
|
||||
diff = "diff --git a/apps/okr/backend/a.ts b/apps/okr/backend/a.ts\n--- a/apps/okr/backend/a.ts\n+++ b/apps/okr/backend/a.ts\n@@ -1 +1 @@\n-a\n+b\n"
|
||||
manifest = {"file_count": 1, "characters": 10, "truncated": False, "bundle_sha256": "context-sha"}
|
||||
artifact = {"path": ".specify/state/goals/default/job.patch", "sha256": "patch-sha", "files": ["apps/okr/backend/a.ts"], "status": "awaiting_approval"}
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = os.path.join(directory, "job.json")
|
||||
with open(path, "w", encoding="utf-8") as handle:
|
||||
json.dump({
|
||||
"id": "goal-1", "goal": "Hoàn thiện backend nhưng không thay đổi API contract.",
|
||||
"project": "AINative_OKR_CASAN4", "actor": "owner", "local_provider": "ollama",
|
||||
"cloud_provider": "omniroute", "workspace": {"context_roots": ["apps/okr/backend"]},
|
||||
"stages": [],
|
||||
}, handle)
|
||||
with (
|
||||
patch.dict(os.environ, {"CASAN_GOAL_LOCAL_MODEL": "ollama:worker", "CASAN_GOAL_CLOUD_MODEL": "openai-compatible:reviewer"}, clear=False),
|
||||
patch.object(ORCHESTRATOR, "scan", side_effect=lambda text, mode: (True, text)),
|
||||
patch.object(ORCHESTRATOR, "build_context", return_value=("snapshot", manifest, "manifest.json")),
|
||||
patch.object(ORCHESTRATOR, "call_model", return_value=(True, diff, {}, "ok")),
|
||||
patch.object(ORCHESTRATOR, "validate_write_output", return_value=diff),
|
||||
patch.object(ORCHESTRATOR, "run_reviewer_chain", return_value=(False, "", {}, "model_output_empty", {"provider": "ollama", "model": "ollama:worker"})),
|
||||
patch.object(ORCHESTRATOR, "validate_and_store_patch", return_value=artifact),
|
||||
patch.object(ORCHESTRATOR, "submit_side_effect", return_value={"id": "AP-1", "status": "pending", "action": "goal.workspace.execute"}),
|
||||
patch.object(ORCHESTRATOR, "emit"), patch.object(ORCHESTRATOR, "metric"),
|
||||
patch.object(ORCHESTRATOR, "audit", return_value="audit-sha"),
|
||||
):
|
||||
exit_code = ORCHESTRATOR.run(path)
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
job = json.load(handle)
|
||||
self.assertEqual(exit_code, 0)
|
||||
self.assertEqual(job["status"], "requires_approval")
|
||||
self.assertEqual(job["approval"]["id"], "AP-1")
|
||||
self.assertIn("Automated H3 review was unavailable", job["result"])
|
||||
self.assertNotIn("independently reviewed", job["result"])
|
||||
|
||||
def test_source_code_delete_method_is_not_promoted_to_security_block(self):
|
||||
source = "export class Service { async delete(id: number) { return this.repo.delete({ where: { id } }); } }"
|
||||
with tempfile.TemporaryDirectory() as directory, \
|
||||
patch.dict(os.environ, {"CASAN_STATE_ROOT": directory, "CASAN_SECURITY_STRICT": "0"}, clear=False):
|
||||
allowed, safe_source = ORCHESTRATOR.scan(source, "input")
|
||||
self.assertTrue(allowed)
|
||||
self.assertIn("repo.delete", safe_source)
|
||||
|
||||
def test_extract_patch_requires_unified_diff(self):
|
||||
with self.assertRaisesRegex(ValueError, "goal_patch_missing"):
|
||||
ORCHESTRATOR.extract_patch("implementation plan only")
|
||||
@@ -60,6 +130,31 @@ class GoalPatchWorkflowTests(unittest.TestCase):
|
||||
self.assertIn("@@ -1,1 +1,1 @@", normalized)
|
||||
self.assertEqual(check.call_count, 2)
|
||||
|
||||
def test_patch_fragment_error_triggers_deterministic_hunk_recount(self):
|
||||
wrong_counts = (
|
||||
"diff --git a/a b/a\n--- a/a\n+++ b/a\n"
|
||||
"@@ -1,5 +1,5 @@\n-old\n+new\n"
|
||||
"@@ -10,7 +10,7 @@\n-tail-old\n+tail-new\n"
|
||||
)
|
||||
with patch.object(
|
||||
ORCHESTRATOR,
|
||||
"validate_patch_check",
|
||||
side_effect=[ValueError("goal_patch_check_failed:error: patch fragment without header at line 7: @@ -10,7 +10,7 @@"), None],
|
||||
) as check:
|
||||
normalized = ORCHESTRATOR.validate_write_output(wrong_counts)
|
||||
self.assertIn("@@ -1,1 +1,1 @@", normalized)
|
||||
self.assertIn("@@ -10,1 +10,1 @@", normalized)
|
||||
self.assertEqual(check.call_count, 2)
|
||||
|
||||
def test_patch_fragment_error_adds_recount_instruction_to_model_repair(self):
|
||||
with (
|
||||
patch.dict(os.environ, {"CASAN_GOAL_PATCH_REPAIR_ATTEMPTS": "1", "CASAN_GOAL_PATCH_REPAIR_MODELS": "openai:gpt"}, clear=False),
|
||||
patch.object(ORCHESTRATOR, "model_preflight", return_value=(True, "ok")),
|
||||
patch.object(ORCHESTRATOR, "call_model", return_value=(False, "", {}, "stopped")) as call,
|
||||
):
|
||||
ORCHESTRATOR.repair_write_output("openai:gpt", "original", "invalid", "goal_patch_check_failed:patch fragment without header")
|
||||
self.assertIn("Recompute every `@@ -old,count +new,count @@` header", call.call_args.args[1])
|
||||
|
||||
def test_truncated_replacement_is_not_reinterpreted_as_deletion(self):
|
||||
truncated = "diff --git a/a b/a\n--- a/a\n+++ b/a\n@@ -1,1 +1,1 @@\n-old\n"
|
||||
self.assertEqual(ORCHESTRATOR.normalize_unified_diff(truncated), truncated)
|
||||
@@ -80,6 +175,50 @@ class GoalPatchWorkflowTests(unittest.TestCase):
|
||||
self.assertEqual(manifest["files"][0]["characters"], len(raw.strip()))
|
||||
self.assertFalse(manifest["files"][0]["truncated"])
|
||||
|
||||
def test_explicit_scope_paths_rank_source_ahead_of_general_architecture(self):
|
||||
project = ORCHESTRATOR.registered_project("AINative_OKR_CASAN4")
|
||||
objective = """PHASE 1 — Hoàn thiện backend Objective và Key Result.
|
||||
Nguồn sự thật:
|
||||
- docs/technical_architecture.md
|
||||
- apps/okr/domain/input/okr-requirement.md
|
||||
Chỉ làm việc trong:
|
||||
- apps/okr/backend/src/objectives/**
|
||||
- apps/okr/backend/src/key-results/**
|
||||
- apps/okr/backend/prisma/schema.prisma
|
||||
Tạo patch nhưng không tự apply.
|
||||
"""
|
||||
ranked = ORCHESTRATOR.context_candidates(project, objective)
|
||||
top_paths = [row[1] for row in ranked[:10]]
|
||||
self.assertTrue(any(path.startswith("apps/okr/backend/src/objectives/") for path in top_paths))
|
||||
self.assertTrue(any(path.startswith("apps/okr/backend/src/key-results/") for path in top_paths))
|
||||
self.assertIn("apps/okr/backend/prisma/schema.prisma", top_paths)
|
||||
self.assertNotIn("docs/technical_architecture.md", top_paths)
|
||||
self.assertNotIn("apps/okr/domain/input/okr-requirement.md", top_paths)
|
||||
|
||||
def test_explicit_only_scope_excludes_unrequested_context_files(self):
|
||||
goal = """Hoàn thiện backend.
|
||||
Chỉ được đọc và thay đổi:
|
||||
apps/okr/backend/src/key-results/key-results.service.ts
|
||||
apps/okr/backend/test/services.test.ts
|
||||
Không sửa bất kỳ file nào khác.
|
||||
"""
|
||||
candidates = [
|
||||
(2000, "apps/okr/backend/src/key-results/key-results.service.ts", "service"),
|
||||
(2000, "apps/okr/backend/test/services.test.ts", "tests"),
|
||||
(50, "apps/okr/backend/src/objectives/objectives.service.ts", "unrequested"),
|
||||
]
|
||||
project = {"domain": "OKR", "domain_root": "apps/okr", "roots": []}
|
||||
with tempfile.TemporaryDirectory() as directory, \
|
||||
patch.object(ORCHESTRATOR, "registered_project", return_value=project), \
|
||||
patch.object(ORCHESTRATOR, "context_candidates", return_value=candidates), \
|
||||
patch.object(ORCHESTRATOR, "scan", side_effect=lambda text, mode: (True, text)):
|
||||
bundle, manifest, _ = ORCHESTRATOR.build_context(os.path.join(directory, "goal.json"), "okr", goal, True)
|
||||
self.assertEqual([row["path"] for row in manifest["files"]], [
|
||||
"apps/okr/backend/src/key-results/key-results.service.ts",
|
||||
"apps/okr/backend/test/services.test.ts",
|
||||
])
|
||||
self.assertNotIn("unrequested", bundle)
|
||||
|
||||
def test_invalid_write_output_gets_one_bounded_repair_attempt(self):
|
||||
repaired = "```diff\ndiff --git a/apps/okr/frontend/a.ts b/apps/okr/frontend/a.ts\n--- a/apps/okr/frontend/a.ts\n+++ b/apps/okr/frontend/a.ts\n@@ -1 +1 @@\n-a\n+b\n```"
|
||||
with patch.dict(os.environ, {"CASAN_GOAL_PATCH_REPAIR_ATTEMPTS": "1"}, clear=False), \
|
||||
@@ -114,6 +253,53 @@ class GoalPatchWorkflowTests(unittest.TestCase):
|
||||
self.assertEqual(call.call_args.args[0], "openai:gpt-4o-mini")
|
||||
self.assertEqual(call.call_args.kwargs["max_output_tokens"], ORCHESTRATOR.patch_output_tokens())
|
||||
|
||||
def test_repair_skips_model_that_fails_generation_preflight(self):
|
||||
repaired = "diff --git a/apps/okr/frontend/a.ts b/apps/okr/frontend/a.ts\n--- a/apps/okr/frontend/a.ts\n+++ b/apps/okr/frontend/a.ts\n@@ -1 +1 @@\n-a\n+b\n"
|
||||
with (
|
||||
patch.dict(os.environ, {"CASAN_GOAL_PATCH_REPAIR_ATTEMPTS": "2", "CASAN_GOAL_PATCH_REPAIR_MODELS": "openai-compatible:auto/coding,ollama:ornith:9b"}, clear=False),
|
||||
patch.object(ORCHESTRATOR, "model_preflight", side_effect=[(False, "gateway_503"), (True, "ok")]),
|
||||
patch.object(ORCHESTRATOR, "call_model", return_value=(True, repaired, {}, "ok")) as call,
|
||||
patch.object(ORCHESTRATOR, "validate_write_output", return_value=repaired),
|
||||
):
|
||||
ok, _, usage, reason = ORCHESTRATOR.repair_write_output("ollama:ornith:9b", "original", "invalid")
|
||||
self.assertTrue(ok)
|
||||
self.assertEqual(reason, "ok")
|
||||
self.assertEqual(call.call_count, 1)
|
||||
self.assertEqual(call.call_args.args[0], "ollama:ornith:9b")
|
||||
self.assertIn("model_preflight_failed", usage["repair_attempts"][0]["reason"])
|
||||
|
||||
def test_logged_in_account_worker_is_first_patch_repair_candidate(self):
|
||||
repaired = "diff --git a/apps/okr/frontend/a.ts b/apps/okr/frontend/a.ts\n--- a/apps/okr/frontend/a.ts\n+++ b/apps/okr/frontend/a.ts\n@@ -1 +1 @@\n-a\n+b\n"
|
||||
with (
|
||||
patch.dict(os.environ, {"CASAN_GOAL_PATCH_REPAIR_ATTEMPTS": "2", "CASAN_GOAL_PATCH_REPAIR_MODELS": "openai-compatible:auto/coding,ollama:ornith:9b"}, clear=False),
|
||||
patch.object(ORCHESTRATOR, "model_preflight", return_value=(True, "ok")),
|
||||
patch.object(ORCHESTRATOR, "call_account_model", return_value=(True, repaired, {}, "ok")) as account_call,
|
||||
patch.object(ORCHESTRATOR, "call_model") as routed_call,
|
||||
patch.object(ORCHESTRATOR, "validate_write_output", return_value=repaired),
|
||||
):
|
||||
ok, _, usage, reason = ORCHESTRATOR.repair_write_output("account:codex", "original", "invalid")
|
||||
self.assertTrue(ok)
|
||||
self.assertEqual(reason, "ok")
|
||||
self.assertEqual(account_call.call_args.args[0], "codex")
|
||||
routed_call.assert_not_called()
|
||||
self.assertEqual(usage["repair_attempts"][0]["provider"], "codex-account")
|
||||
|
||||
def test_account_codex_gets_corrective_pass_before_other_routes(self):
|
||||
repaired = "diff --git a/apps/okr/frontend/a.ts b/apps/okr/frontend/a.ts\n--- a/apps/okr/frontend/a.ts\n+++ b/apps/okr/frontend/a.ts\n@@ -1 +1 @@\n-a\n+b\n"
|
||||
with (
|
||||
patch.dict(os.environ, {"CASAN_GOAL_PATCH_REPAIR_ATTEMPTS": "3", "CASAN_GOAL_PATCH_REPAIR_MODELS": "openai-compatible:auto/coding,ollama:ornith:9b"}, clear=False),
|
||||
patch.object(ORCHESTRATOR, "model_preflight", return_value=(True, "ok")),
|
||||
patch.object(ORCHESTRATOR, "call_account_model", side_effect=[(True, "corrupt", {}, "ok"), (True, repaired, {}, "ok")]) as account_call,
|
||||
patch.object(ORCHESTRATOR, "call_model") as routed_call,
|
||||
patch.object(ORCHESTRATOR, "validate_write_output", side_effect=[ValueError("goal_patch_check_failed:patch fragment without header"), repaired]),
|
||||
):
|
||||
ok, _, usage, reason = ORCHESTRATOR.repair_write_output("account:codex", "original", "invalid")
|
||||
self.assertTrue(ok)
|
||||
self.assertEqual(reason, "ok")
|
||||
self.assertEqual(account_call.call_count, 2)
|
||||
routed_call.assert_not_called()
|
||||
self.assertEqual([row["model"] for row in usage["repair_attempts"]], ["account:codex", "account:codex"])
|
||||
|
||||
def test_repair_tries_next_stronger_candidate_when_first_patch_is_corrupt(self):
|
||||
repaired = "diff --git a/apps/okr/frontend/a.ts b/apps/okr/frontend/a.ts\n--- a/apps/okr/frontend/a.ts\n+++ b/apps/okr/frontend/a.ts\n@@ -1 +1 @@\n-a\n+b\n"
|
||||
with (
|
||||
@@ -178,6 +364,16 @@ class GoalPatchWorkflowTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError, "goal_patch_outside_workspace"):
|
||||
ORCHESTRATOR.validate_and_store_patch(path, job, content)
|
||||
|
||||
def test_patch_outside_explicit_file_scope_is_denied(self):
|
||||
job = {
|
||||
"goal": "Chỉ được đọc và thay đổi:\napps/okr/backend/allowed.ts\nKhông sửa bất kỳ file nào khác.",
|
||||
"workspace": {"context_roots": ["apps/okr/backend"]},
|
||||
}
|
||||
content = "diff --git a/apps/okr/backend/other.ts b/apps/okr/backend/other.ts\n--- a/apps/okr/backend/other.ts\n+++ b/apps/okr/backend/other.ts\n@@ -1 +1 @@\n-a\n+b\n"
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
with self.assertRaisesRegex(ValueError, "goal_patch_outside_workspace"):
|
||||
ORCHESTRATOR.validate_and_store_patch(os.path.join(directory, "job.json"), job, content)
|
||||
|
||||
def test_patch_rename_source_outside_workspace_is_denied(self):
|
||||
job = {"workspace": {"context_roots": ["apps/okr/frontend"]}}
|
||||
content = "diff --git a/package.json b/apps/okr/frontend/package.json\nsimilarity index 100%\nrename from package.json\nrename to apps/okr/frontend/package.json\n"
|
||||
@@ -199,9 +395,20 @@ class GoalPatchWorkflowTests(unittest.TestCase):
|
||||
|
||||
def test_frontend_patch_runs_build_and_tests(self):
|
||||
commands = EXECUTOR.verification_commands(["apps/okr/frontend/src/pages/KeyResultDetail.tsx"])
|
||||
self.assertFalse(any(command[:2] == ["git", "diff"] for command in commands))
|
||||
self.assertIn(["npm", "run", "build", "-w", "@ainative-okr/frontend"], commands)
|
||||
self.assertIn(["npm", "test", "-w", "@ainative-okr/frontend"], commands)
|
||||
|
||||
def test_executor_can_retry_only_a_verified_rollback_failure(self):
|
||||
artifact = {"path": "job.patch"}
|
||||
self.assertTrue(EXECUTOR.ready_for_apply({"status": "requires_approval", "patch_artifact": artifact}))
|
||||
self.assertTrue(EXECUTOR.ready_for_apply({
|
||||
"status": "failed",
|
||||
"error": "GOAL_APPLY_VERIFICATION_FAILED_ROLLED_BACK",
|
||||
"patch_artifact": artifact,
|
||||
}))
|
||||
self.assertFalse(EXECUTOR.ready_for_apply({"status": "failed", "error": "OTHER", "patch_artifact": artifact}))
|
||||
|
||||
def test_service_desk_patch_uses_service_desk_manifest_commands(self):
|
||||
commands = EXECUTOR.verification_commands(["apps/service-desk/src/ticket.js"])
|
||||
self.assertIn(["node", "--check", "apps/service-desk/src/ticket.js"], commands)
|
||||
|
||||
Reference in New Issue
Block a user