Harden M8 trust and guidance workflows
This commit is contained in:
@@ -56,7 +56,7 @@ def client(app):
|
||||
def auth_headers(app):
|
||||
"""Create auth headers with a valid token."""
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from cmdforge.registry.db import connect_db
|
||||
|
||||
token = "test-token"
|
||||
@@ -94,7 +94,7 @@ def auth_headers(app):
|
||||
INSERT INTO api_tokens (publisher_id, token_hash, name, created_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
[publisher_id, token_hash, "test-token", datetime.utcnow().isoformat()],
|
||||
[publisher_id, token_hash, "test-token", datetime.now(timezone.utc).isoformat()],
|
||||
)
|
||||
|
||||
# Insert a public approved tool for tests if missing
|
||||
@@ -112,7 +112,7 @@ def auth_headers(app):
|
||||
[
|
||||
"testuser", "tool1", "1.0.0", "Test tool", "Other", "[]",
|
||||
"name: tool1\nversion: 1.0.0\n", "", publisher_id,
|
||||
"public", "approved", datetime.utcnow().isoformat(),
|
||||
"public", "approved", datetime.now(timezone.utc).isoformat(),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -158,6 +158,44 @@ class TestToolApprovedEndpoint:
|
||||
assert data['data']['has_approved_public_version'] is False
|
||||
|
||||
|
||||
@flask_required
|
||||
class TestDeprecationChains:
|
||||
def test_replacement_chain_is_validated_and_returned(self, client, auth_headers):
|
||||
target = client.post(
|
||||
"/api/v1/tools", headers=auth_headers,
|
||||
json={"config": "name: replacement\nversion: 1.0.0\noutput: ok\n"},
|
||||
)
|
||||
assert target.status_code == 201
|
||||
response = client.post(
|
||||
"/api/v1/tools/testuser/tool1/deprecate",
|
||||
headers=auth_headers,
|
||||
json={"deprecated_message": "Moved", "replacement": "replacement"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.get_json()["data"]["replacement_chain"] == [
|
||||
"testuser/replacement"
|
||||
]
|
||||
|
||||
def test_replacement_cycle_is_rejected(self, client, auth_headers):
|
||||
target = client.post(
|
||||
"/api/v1/tools", headers=auth_headers,
|
||||
json={"config": "name: cycle-target\nversion: 1.0.0\noutput: ok\n"},
|
||||
)
|
||||
assert target.status_code == 201
|
||||
assert client.post(
|
||||
"/api/v1/tools/testuser/cycle-target/deprecate",
|
||||
headers=auth_headers,
|
||||
json={"replacement": "tool1"},
|
||||
).status_code == 200
|
||||
response = client.post(
|
||||
"/api/v1/tools/testuser/tool1/deprecate",
|
||||
headers=auth_headers,
|
||||
json={"replacement": "cycle-target"},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert "cycle" in response.get_json()["error"]["message"]
|
||||
|
||||
|
||||
@flask_required
|
||||
class TestPublishPreflightEndpoint:
|
||||
def test_dry_run_includes_shared_preflight(self, client, auth_headers):
|
||||
@@ -180,7 +218,81 @@ class TestPublishPreflightEndpoint:
|
||||
assert response.status_code == 200
|
||||
report = response.get_json()["data"]["preflight"]
|
||||
assert report["errors"] == []
|
||||
assert report["generated_tests"] == []
|
||||
assert report["generated_tests"][0]["state"] == "passed"
|
||||
assert response.get_json()["data"]["quality"]["evidence_coverage"] > 0
|
||||
|
||||
def test_real_publish_persists_immutable_audit(self, client, auth_headers):
|
||||
from cmdforge.registry.db import connect_db
|
||||
|
||||
response = client.post(
|
||||
"/api/v1/tools",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"config": (
|
||||
"name: audited-tool\nversion: 1.0.0\noutput: stable\n"
|
||||
"input_schema: {}\noutput_schema:\n type: string\n"
|
||||
),
|
||||
},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
conn = connect_db()
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT tool_audits.trigger, tool_audits.evidence_json
|
||||
FROM tool_audits JOIN tools ON tools.id = tool_audits.tool_id
|
||||
WHERE tools.owner = 'testuser' AND tools.name = 'audited-tool'
|
||||
"""
|
||||
).fetchall()
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE tools SET moderation_status = 'approved'
|
||||
WHERE owner = 'testuser' AND name = 'audited-tool'
|
||||
"""
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["trigger"] == "publish"
|
||||
assert "contract_conformance" in rows[0]["evidence_json"]
|
||||
search = client.get("/api/v1/tools/search?q=audited")
|
||||
result = search.get_json()["data"][0]
|
||||
assert isinstance(result["quality_score"], int)
|
||||
assert isinstance(result["quality_coverage"], int)
|
||||
assert result["quality_evaluated_at"]
|
||||
|
||||
def test_real_publish_blocks_deterministic_contract_failure(
|
||||
self, client, auth_headers
|
||||
):
|
||||
response = client.post(
|
||||
"/api/v1/tools",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"config": (
|
||||
"name: broken-contract\nversion: 1.0.0\noutput: text\n"
|
||||
"input_schema: {}\noutput_schema:\n type: integer\n"
|
||||
),
|
||||
},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.get_json()["error"]["code"] == "PREFLIGHT_FAILED"
|
||||
|
||||
def test_real_publish_blocks_missing_registry_dependency(
|
||||
self, client, auth_headers
|
||||
):
|
||||
response = client.post(
|
||||
"/api/v1/tools",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"config": (
|
||||
"name: missing-dependency\nversion: 1.0.0\n"
|
||||
"dependencies:\n - absent-tool\n"
|
||||
),
|
||||
},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert "absent-tool" in response.get_json()["error"]["message"]
|
||||
|
||||
def test_invalid_contract_is_rejected(self, client, auth_headers):
|
||||
response = client.post(
|
||||
|
||||
Reference in New Issue
Block a user