Files
liqiang b119135836
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
2026-08-20 13:12:50 +00:00

330 lines
11 KiB
Python

"""Tests for both application scenarios."""
import pytest
import time
from pedo.core.models import AccessContext, DataObject, PermissionRule, Operation, PrivilegeType
from pedo.core.store import ObjectStore, PermissionDeniedError, ValidationError, ReferentialIntegrityError
from pedo.scenarios.hiring import register_hiring_types
from pedo.scenarios.project_mgmt import register_project_mgmt_types
DSN = "dbname=pedo_test"
# ── Scenario A: Hiring Pipeline ───────────────────────────────
@pytest.fixture
def hiring_store():
s = ObjectStore(DSN)
s.clear_all()
register_hiring_types(s)
return s
@pytest.fixture
def recruiter():
return AccessContext(user_id="recruiter1", role="recruiter", org_id="acme")
@pytest.fixture
def hiring_manager():
return AccessContext(user_id="hm1", role="hiring_manager", org_id="acme")
@pytest.fixture
def admin():
return AccessContext(user_id="admin1", role="admin", org_id="acme")
@pytest.fixture
def system_ctx():
return AccessContext(user_id="system", role="system", org_id="acme")
def _create_position(store, ctx, status="open"):
return store.create(DataObject(
type_name="position",
content={"title": "Engineer", "department": "Eng", "status": status,
"salary_min": 80000, "salary_max": 150000},
org_id="acme",
), ctx)
def test_candidate_lifecycle(hiring_store, recruiter, hiring_manager, system_ctx):
"""Test the full candidate lifecycle through the state machine."""
pos = _create_position(hiring_store, system_ctx)
# Create candidate (must start as 'applied')
cand = hiring_store.create(DataObject(
type_name="candidate",
content={"name": "Alice", "email": "alice@test.com", "status": "applied",
"position_id": pos.id, "salary_expectation": 100000},
org_id="acme",
), recruiter)
# Valid transitions
hiring_store.update(cand.id, {"status": "screened"}, recruiter)
hiring_store.update(cand.id, {"status": "interviewed"}, recruiter)
hiring_store.update(cand.id, {"status": "offered"}, recruiter)
hiring_store.update(cand.id, {"status": "hired"}, recruiter)
# Process reactions (audit logs)
hiring_store.process_reactions_sync()
logs = hiring_store.get_reaction_log()
assert len(logs) >= 4 # create + 4 status changes
def test_invalid_status_transition(hiring_store, recruiter, system_ctx):
pos = _create_position(hiring_store, system_ctx)
cand = hiring_store.create(DataObject(
type_name="candidate",
content={"name": "Bob", "email": "bob@test.com", "status": "applied",
"position_id": pos.id},
org_id="acme",
), recruiter)
# Skip screened -> directly to offered (invalid)
with pytest.raises(ValidationError, match="Invalid status transition"):
hiring_store.update(cand.id, {"status": "offered"}, recruiter)
def test_closed_position_rejects_candidate(hiring_store, recruiter, system_ctx):
pos = _create_position(hiring_store, system_ctx, status="closed")
with pytest.raises(ValidationError, match="not open"):
hiring_store.create(DataObject(
type_name="candidate",
content={"name": "Charlie", "email": "c@test.com", "status": "applied",
"position_id": pos.id},
org_id="acme",
), recruiter)
def test_salary_out_of_range(hiring_store, recruiter, system_ctx):
pos = _create_position(hiring_store, system_ctx)
with pytest.raises(ValidationError, match="Salary.*outside position range"):
hiring_store.create(DataObject(
type_name="candidate",
content={"name": "Dave", "email": "d@test.com", "status": "applied",
"position_id": pos.id, "salary_expectation": 200000},
org_id="acme",
), recruiter)
def test_hiring_manager_cannot_modify_candidate(hiring_store, recruiter, hiring_manager, system_ctx):
pos = _create_position(hiring_store, system_ctx)
cand = hiring_store.create(DataObject(
type_name="candidate",
content={"name": "Eve", "email": "e@test.com", "status": "applied",
"position_id": pos.id},
org_id="acme",
), recruiter)
# Hiring manager can read but not write
read = hiring_store.get(cand.id, hiring_manager)
assert read is not None
with pytest.raises(PermissionDeniedError):
hiring_store.update(cand.id, {"status": "screened"}, hiring_manager)
def test_recruiter_cannot_see_evaluations(hiring_store, recruiter, hiring_manager, system_ctx):
pos = _create_position(hiring_store, system_ctx)
cand = hiring_store.create(DataObject(
type_name="candidate",
content={"name": "Frank", "email": "f@test.com", "status": "applied",
"position_id": pos.id},
org_id="acme",
), recruiter)
hiring_store.update(cand.id, {"status": "screened"}, recruiter)
interview = hiring_store.create(DataObject(
type_name="interview",
content={"candidate_id": cand.id, "interviewer": "hm1",
"scheduled_at": "2024-01-15", "notes": "", "score": 0},
org_id="acme",
), hiring_manager)
evaluation = hiring_store.create(DataObject(
type_name="evaluation",
content={"interview_id": interview.id, "decision": "proceed", "comments": "Strong candidate"},
org_id="acme",
), hiring_manager)
# Recruiter cannot see evaluation
with pytest.raises(PermissionDeniedError):
hiring_store.get(evaluation.id, recruiter)
# ── Scenario B: Multi-tenant Project Management ───────────────
@pytest.fixture
def pm_store():
s = ObjectStore(DSN)
s.clear_all()
register_project_mgmt_types(s)
return s
@pytest.fixture
def org_admin():
return AccessContext(user_id="admin1", role="org_admin", org_id="tenant_a")
@pytest.fixture
def project_admin():
return AccessContext(user_id="pa1", role="project_admin", org_id="tenant_a")
@pytest.fixture
def member():
return AccessContext(user_id="m1", role="member", org_id="tenant_a")
@pytest.fixture
def other_tenant_member():
return AccessContext(user_id="other1", role="member", org_id="tenant_b")
@pytest.fixture
def guest():
return AccessContext(user_id="guest1", role="guest", org_id="tenant_a")
def test_task_lifecycle(pm_store, project_admin, member):
proj = pm_store.create(DataObject(
type_name="project",
content={"name": "Alpha", "status": "active", "members": ["m1", "pa1"]},
org_id="tenant_a",
), project_admin)
task = pm_store.create(DataObject(
type_name="task",
content={"title": "Build feature", "status": "todo", "priority": "high",
"assignee_id": "m1", "project_id": proj.id},
org_id="tenant_a",
), member)
# Valid transitions
pm_store.update(task.id, {"status": "in_progress"}, member)
pm_store.update(task.id, {"status": "review"}, member)
pm_store.update(task.id, {"status": "done"}, member)
def test_invalid_task_transition(pm_store, project_admin, member):
proj = pm_store.create(DataObject(
type_name="project",
content={"name": "Beta", "status": "active", "members": ["m1", "pa1"]},
org_id="tenant_a",
), project_admin)
task = pm_store.create(DataObject(
type_name="task",
content={"title": "Bug fix", "status": "todo", "priority": "medium",
"project_id": proj.id},
org_id="tenant_a",
), member)
# Can't go from todo -> done (must go through in_progress, review)
with pytest.raises(ValidationError, match="Invalid task transition"):
pm_store.update(task.id, {"status": "done"}, member)
def test_invalid_priority(pm_store, project_admin, member):
proj = pm_store.create(DataObject(
type_name="project",
content={"name": "Gamma", "status": "active", "members": ["m1"]},
org_id="tenant_a",
), project_admin)
with pytest.raises(ValidationError, match="Invalid priority"):
pm_store.create(DataObject(
type_name="task",
content={"title": "Task", "status": "todo", "priority": "URGENT",
"project_id": proj.id},
org_id="tenant_a",
), member)
def test_assignee_must_be_member(pm_store, project_admin, member):
proj = pm_store.create(DataObject(
type_name="project",
content={"name": "Delta", "status": "active", "members": ["m1"]},
org_id="tenant_a",
), project_admin)
with pytest.raises(ValidationError, match="not a member"):
pm_store.create(DataObject(
type_name="task",
content={"title": "Task", "status": "todo", "priority": "low",
"assignee_id": "nonexistent_user", "project_id": proj.id},
org_id="tenant_a",
), member)
def test_tenant_isolation(pm_store, project_admin, other_tenant_member):
proj = pm_store.create(DataObject(
type_name="project",
content={"name": "Secret Project", "status": "active", "members": ["pa1"]},
org_id="tenant_a",
), project_admin)
# Other tenant can't read the project
with pytest.raises(PermissionDeniedError):
pm_store.get(proj.id, other_tenant_member)
def test_guest_cannot_modify_task(pm_store, project_admin, guest):
proj = pm_store.create(DataObject(
type_name="project",
content={"name": "Public", "status": "active", "members": ["pa1", "guest1"]},
org_id="tenant_a",
), project_admin)
task = pm_store.create(DataObject(
type_name="task",
content={"title": "Read-only task", "status": "todo", "priority": "low",
"project_id": proj.id},
org_id="tenant_a",
), project_admin)
# Guest can read
read = pm_store.get(task.id, guest)
assert read is not None
# Guest cannot modify
with pytest.raises(PermissionDeniedError):
pm_store.update(task.id, {"status": "in_progress"}, guest)
def test_temporal_guest_access(pm_store, project_admin):
"""Test that guest access with expired temporal bounds is denied."""
proj = pm_store.create(DataObject(
type_name="project",
content={"name": "Temporal", "status": "active", "members": ["pa1"]},
org_id="tenant_a",
), project_admin)
task = pm_store.create(DataObject(
type_name="task",
content={"title": "Temporal task", "status": "todo", "priority": "low",
"project_id": proj.id},
org_id="tenant_a",
# Override permission rules with temporal bound for guest
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.READ,
{"roles": ["member", "project_admin", "org_admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE,
{"roles": ["member", "project_admin", "org_admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ,
{"role": "guest"},
valid_from=time.time() - 200,
valid_until=time.time() - 100), # expired
],
), project_admin)
expired_guest = AccessContext(user_id="old_guest", role="guest", org_id="tenant_a")
with pytest.raises(PermissionDeniedError):
pm_store.get(task.id, expired_guest)