assistant/apps/server/tests/test_execution_profile.py

266 lines
16 KiB
Python

"""Fixed execution contracts; fake browser only, temporary SQLite / dedicated PG."""
from __future__ import annotations
import copy
import hashlib
import json
import os
import sqlite3
import subprocess
import sys
from pathlib import Path
from uuid import uuid4
import pytest
from apps.server.db import PostgresStore, SCHEMA, Store, digest, encoded
from apps.server.learning_contracts import EXECUTION_CONTRACT, EXECUTION_PROFILE, execution_metadata, execution_metadata_matches
from apps.server.tests.test_discovery import item, search_candidate
from apps.server.tests.test_learning import client, initialize, working_candidate
from apps.server.tests.test_postgres import pg_client
from apps.server.tests.test_reading import NOTE_ID, learned_detail
from apps.server.worker import safe_failure_capture, safe_request_observations
META = execution_metadata(EXECUTION_PROFILE)
@pytest.fixture(params=["client", "pg_client"])
def profile_client(request):
return request.getfixturevalue(request.param)
def install_profile_worker(client):
"""Add actual worker echoes, with bounded corruption hooks for each RPC."""
worker = client.worker_reference
previous_call, previous_capture, previous_post = worker.call, worker.capture, client.post
profiles, state = {}, {"method": None, "place": "root", "mutation": None}
def capture(task_id):
result = previous_capture(task_id)
result.update(execution_metadata(profiles.get(task_id)))
return result
async def call(method, params):
if method == "open":
profiles[params["task_id"]] = params.get("execution_profile")
result = await previous_call(method, params)
if method != "close" and not (method == "status" and result.get("status") == "closed"):
result.update(execution_metadata(profiles.get(params["task_id"])))
if state["method"] == method:
target = result if state["place"] == "root" else result[state["place"]]
state["mutation"](target)
return result
def post(url, **kwargs):
if str(url) == "/api/learning/tasks" and "json" in kwargs:
kwargs["json"] = {"execution_profile": EXECUTION_PROFILE, **kwargs["json"]}
return previous_post(url, **kwargs)
worker.capture, worker.call, client.post = capture, call, post
return state, profiles
def raw_facts(store):
return {table: store.rows(f"SELECT * FROM {table} ORDER BY id") for table in ("observations", "learning_steps", "candidates", "validations")}
def test_fixed_descriptor_and_native_metadata_are_exact_not_caller_combinations():
assert META == {"execution_profile": EXECUTION_PROFILE, "execution_contract": EXECUTION_CONTRACT}
assert execution_metadata(None) == {}
assert execution_metadata_matches({}, None)
for value in ("legacy", "", {}, True):
with pytest.raises(ValueError):
execution_metadata(value)
for value in ({}, {"execution_profile": EXECUTION_PROFILE}, META | {"execution_contract": EXECUTION_CONTRACT | {"extra": True}}, META | {"execution_contract": EXECUTION_CONTRACT | {"engine": "browser_use"}}):
assert not execution_metadata_matches(value, EXECUTION_PROFILE)
request = {"method": "POST", "origin": "https://www.xiaohongshu.com", "pathname": "/api/telemetry", "resource_type": "xhr", "automating": True, "blocked": False,
"policy_version": "xhs-page-native-v1", "policy_rule": "native_page_traffic"}
assert safe_request_observations([request])[0]["policy_rule"] == "native_page_traffic"
for mutation in ({"policy_version": "xhs-readonly-ui-experimental-v1"}, {"policy_rule": "collect"}, {"blocked": True}, {"automating": False}):
assert "policy_rule" not in safe_request_observations([request | mutation])[0]
capture = {"url": "http://127.0.0.1/fixture", "elements": [], "screenshot_path": "synthetic.png", **META}
assert execution_metadata_matches(safe_failure_capture(capture), EXECUTION_PROFILE)
assert safe_failure_capture(capture | {"execution_contract": {}}) is None
legacy = {key: value for key, value in capture.items() if key not in META}
assert execution_metadata_matches(safe_failure_capture(legacy), None)
def test_nullable_legacy_and_explicit_profile_roundtrip_do_not_rewrite_history(profile_client):
client = profile_client
initialize(client)
old_task, old_candidate = working_candidate(client)
store = client.app_reference.state.store
facts = raw_facts(store)
assert store.one("SELECT execution_profile FROM learning_tasks WHERE id=$1", (old_task["id"],))["execution_profile"] is None
assert "execution_profile" not in client.get("/api/learning/tasks/" + old_task["id"]).json()
base = {key: old_task[key] for key in ("name", "platform", "entry_url", "goal", "account_key", "parameters")}
null = client.post("/api/learning/tasks", json=base | {"execution_profile": None})
assert null.status_code == 201 and "execution_profile" not in null.json()
for bad in ("legacy", "browser_use", "", {}, True):
assert client.post("/api/learning/tasks", json=base | {"execution_profile": bad}).status_code == 422
install_profile_worker(client)
task, candidate = working_candidate(client)
assert task["execution_profile"] == EXECUTION_PROFILE and "execution_contract" not in task
assert all(candidate["manifest"][key] == value for key, value in META.items())
assert digest(candidate["manifest"]) == candidate["digest"]
endpoint = "/api/learning/candidates/" + candidate["id"]
checked = client.post(endpoint + "/check").json()
assert checked["status"] == "passed" and "execution_profile" not in checked["environment"]
verify = client.post(endpoint + "/verify", json={"expected_outputs": {"results": "建材"}}).json()
assert verify["status"] == "passed" and all(verify["environment"][key] == value for key, value in META.items())
wire = [params for method, params in client.worker_reference.calls if method == "verify"][-1]
assert all(wire[key] == value for key, value in META.items())
detail = client.get("/api/learning/tasks/" + task["id"]).json()
assert all(detail["browser_readiness"][key] == value for key, value in META.items())
assert all(execution_metadata_matches(value, EXECUTION_PROFILE) for value in detail["observations"])
# Neither action nor candidate creation can override a task's contract.
for path, data in (("/browser/action", {"kind": "read", "selector": '[data-testid="result"]', "output_name": "results", "execution_profile": EXECUTION_PROFILE}),
("/candidates", {"execution_profile": EXECUTION_PROFILE})):
assert client.post("/api/learning/tasks/" + task["id"] + path, json=data).status_code == 422
old = client.get("/api/learning/candidates/" + old_candidate["id"]).json()
assert old["digest"] == old_candidate["digest"] and old["manifest"] == old_candidate["manifest"]
current = raw_facts(store)
for table, rows in facts.items():
assert [row for row in current[table] if row["id"] in {item["id"] for item in rows}] == rows
def test_profile_echo_missing_open_status_capture_and_action_never_become_success(profile_client):
client = profile_client
initialize(client)
state, _ = install_profile_worker(client)
task, _ = working_candidate(client)
prefix = "/api/learning/tasks/" + task["id"]
store = client.app_reference.state.store
old_steps = store.rows("SELECT * FROM learning_steps WHERE task_id=$1", (task["id"],))
old_obs = store.one("SELECT count(*) AS n FROM observations WHERE task_id=$1", (task["id"],))["n"]
def strip(value):
value.pop("execution_profile", None)
value.pop("execution_contract", None)
for method, place, path, data in (("capture", "root", "/browser/capture", {}),
("action", "root", "/browser/action", {"kind": "read", "selector": '[data-testid="result"]', "output_name": "results"}),
("action", "before", "/browser/action", {"kind": "read", "selector": '[data-testid="result"]', "output_name": "results"}),
("status", "root", "/browser/capture", {}), ("open", "root", "/browser/open", {})):
state.update(method=None)
assert client.post(prefix + "/browser/open").status_code == 200
assert client.post(prefix + "/browser/ready", json={"identity_label": "测试"}).status_code == 200
state.update(method=method, place=place, mutation=strip)
response = client.post(prefix + path, json=data)
assert response.status_code == 409, (method, place, response.text)
assert not client.app_reference.state.learning.browser_readiness(task["id"])["ready"]
assert store.rows("SELECT * FROM learning_steps WHERE task_id=$1", (task["id"],)) == old_steps
assert store.one("SELECT count(*) AS n FROM observations WHERE task_id=$1", (task["id"],))["n"] == old_obs
def test_new_profile_cannot_relabel_old_evidence_or_accept_mutated_manifest(profile_client):
client = profile_client
initialize(client)
old_task, candidate = working_candidate(client)
service = client.app_reference.state.learning
original = raw_facts(service.store)
service.store.execute("UPDATE learning_tasks SET execution_profile=$1 WHERE id=$2", (EXECUTION_PROFILE, old_task["id"]))
result = client.post("/api/learning/tasks/" + old_task["id"] + "/candidates", json={})
assert result.status_code == 409 and result.json()["detail"]["code"] == "EXECUTION_PROFILE_MISMATCH"
assert service.static_errors(candidate)
for change in ({}, {"execution_profile": "unknown", "execution_contract": EXECUTION_CONTRACT}, META):
changed = copy.deepcopy(candidate)
changed["manifest"].update(change)
changed["digest"] = digest(changed["manifest"])
assert service.static_errors(changed)
assert raw_facts(service.store) == original
def test_verify_bad_actual_profile_or_capture_records_failure_without_usable_outputs(profile_client):
client = profile_client
initialize(client)
state, _ = install_profile_worker(client)
task, candidate = working_candidate(client)
endpoint = "/api/learning/candidates/" + candidate["id"]
assert client.post(endpoint + "/check").json()["status"] == "passed"
for mutate in (lambda result: result.pop("execution_contract"),
lambda result: result.update(execution_profile="unknown"),
lambda result: result["evidence"][0].pop("execution_profile")):
state.update(method=None)
prefix = "/api/learning/tasks/" + task["id"]
client.post(prefix + "/browser/open")
client.post(prefix + "/browser/ready", json={"identity_label": "测试"})
state.update(method="verify", place="root", mutation=mutate)
response = client.post(endpoint + "/verify", json={"expected_outputs": {"results": "建材"}})
assert response.status_code == 200, response.text
result = response.json()
assert result["status"] == "failed" and "EXECUTION_PROFILE_MISMATCH" in result["error_codes"]
assert result["actual_outputs"] == {} and result["evidence"] == []
assert "execution_profile" not in result["environment"]
def test_new_discovery_and_reading_keep_actual_validation_profile_without_upgrading_old_search(profile_client):
client = profile_client
initialize(client)
_, old_search = search_candidate(client, [item("准备装修求推荐靠谱公司")])
old_run = client.post("/api/discovery/runs", json={"search_candidate_id": old_search["id"], "keyword": "装修", "region": "", "max_selected": 1}).json()
assert old_run["status"] == "completed" and "execution_profile" not in old_run
install_profile_worker(client)
_, candidate, _ = learned_detail(client)
result = client.post("/api/discovery/runs/" + old_run["id"] + "/readings", json={"detail_candidate_id": candidate["id"], "note_id": NOTE_ID})
assert result.status_code == 201, result.text
reading = result.json()
assert reading["status"] == "completed" and all(reading[key] == value for key, value in META.items())
assert client.get("/api/discovery/runs/" + old_run["id"]).json() == old_run
_, new_search = search_candidate(client, [item("准备装修求推荐靠谱公司")])
new_run = client.post("/api/discovery/runs", json={"search_candidate_id": new_search["id"], "keyword": "装修", "region": "", "max_selected": 1}).json()
assert new_run["status"] == "completed" and all(new_run[key] == value for key, value in META.items())
def old_sqlite(home):
home.mkdir(parents=True, exist_ok=True)
target = home / "learning.sqlite"
with sqlite3.connect(target) as conn:
conn.executescript(SCHEMA.replace(", execution_profile TEXT", ""))
conn.execute("INSERT INTO operators VALUES(?,?,?,?)", ("owner", "old", "not-used", "2026-10-25"))
conn.execute("INSERT INTO learning_tasks VALUES(?,?,?,?,?,?,?,?,?,?,?,?)", ("old-task", "owner", "old", "demo", "http://127.0.0.1/demo", "", "old", "{}", "draft", None, "2026-10-25", "2026-10-25"))
manifest = {"schema_version": 2, "steps": [], "parameters": {}}
conn.execute("INSERT INTO candidates VALUES(?,?,?,?,?,?,?)", ("old-candidate", "old-task", "old", 1, digest(manifest), encoded(manifest), "2026-10-25"))
return target
def test_sqlite_existing_schema_upgrade_is_idempotent_and_preserves_old_manifest(tmp_path):
target = old_sqlite(tmp_path / "old")
with sqlite3.connect(target) as conn:
original = conn.execute("SELECT digest,manifest_json FROM candidates").fetchall()
store = Store(target.parent)
assert store.one("SELECT execution_profile FROM learning_tasks")["execution_profile"] is None
assert store.one("SELECT version FROM schema_versions WHERE version=10")
assert [(row["digest"], row["manifest_json"]) for row in store.rows("SELECT digest,manifest_json FROM candidates")] == original
assert Store(target.parent).one("SELECT count(*) AS n FROM schema_versions WHERE version=10")["n"] == 1
def test_postgres_existing_column_upgrade_and_old_sqlite_backup_migration(pg_client, tmp_path):
store = pg_client.app_reference.state.store
store.execute("ALTER TABLE learning_tasks DROP COLUMN execution_profile")
upgraded = PostgresStore(store.home, pg_client.test_database_url, schema=pg_client.test_schema)
assert upgraded.one("SELECT count(*) AS n FROM information_schema.columns WHERE table_schema=$1 AND table_name='learning_tasks' AND column_name='execution_profile'", (pg_client.test_schema,))["n"] == 1
assert upgraded.one("SELECT version FROM schema_versions WHERE version=10")
home = tmp_path / "legacy-import"
source = old_sqlite(home)
original_hash = hashlib.sha256(source.read_bytes()).hexdigest()
schema = "assistant_learning_test_" + uuid4().hex
assert pg_client.test_database_url and "/assistant_learning_test" in pg_client.test_database_url
env = {**os.environ, "ASSISTANT_DATABASE_URL": pg_client.test_database_url, "ASSISTANT_DATABASE_SCHEMA": schema, "ASSISTANT_LEARNING_HOME": str(home), "PYTHONUTF8": "1"}
try:
completed = subprocess.run([sys.executable, "-m", "scripts.migrate_learning"], cwd=Path(__file__).resolve().parents[3], env=env, capture_output=True, timeout=60)
assert completed.returncode == 0, "isolated legacy migration failed"
summary = json.loads(completed.stdout)
assert summary["result"] == "copied"
imported = PostgresStore(home, pg_client.test_database_url, schema=schema)
assert imported.one("SELECT execution_profile FROM learning_tasks")["execution_profile"] is None
assert imported.one("SELECT manifest_json FROM candidates")["manifest_json"] == encoded({"schema_version": 2, "steps": [], "parameters": {}})
assert hashlib.sha256(source.read_bytes()).hexdigest() == original_hash
backups = list((home / "backups").glob("*.sqlite"))
assert len(backups) == 1
with sqlite3.connect(backups[0]) as conn:
assert "execution_profile" not in {row[1] for row in conn.execute("PRAGMA table_info(learning_tasks)")}
finally:
import psycopg
from psycopg import sql
with psycopg.connect(pg_client.test_database_url, connect_timeout=5) as conn:
conn.execute(sql.SQL("DROP SCHEMA IF EXISTS {} CASCADE").format(sql.Identifier(schema)))