Files
forge/apps/api/tests/test_projects.py
T
nihalashetty ae67bff5a3 feat: deep-agent canvas, live observability, and multi-environment tooling
Self-hosted platform for building, testing, and shipping LangChain/LangGraph agents. Deep-agent sub-agents on the canvas, a live tracing/observability timeline, auto-provisioned built-in tools with import/export, per-environment tool variables, streamed evaluations, and per-user auth token forwarding.
2026-07-28 01:49:19 +05:30

109 lines
5.5 KiB
Python

"""Project lifecycle tests."""
from __future__ import annotations
from sqlalchemy import func, select
from forge.db.base import SessionLocal
from forge.models import (
Agent,
AuthProvider,
Component,
HandoffRequest,
KbSource,
McpClient,
QaPair,
Run,
Secret,
Span,
Thread,
Tool,
Trace,
Workflow,
)
from forge.services.projects import ProjectService
async def _count(session, model, **where) -> int:
stmt = select(func.count()).select_from(model)
for key, value in where.items():
stmt = stmt.where(getattr(model, key) == value)
return int((await session.execute(stmt)).scalar_one())
async def test_project_counts_are_scoped_to_project():
tenant_id = "tenant_counts"
async with SessionLocal() as session:
proj = await ProjectService.create(session, tenant_id, name="Counts", slug="counts")
other = await ProjectService.create(session, tenant_id, name="Other", slug="other")
session.add_all([
Workflow(tenant_id=tenant_id, project_id=proj.id, name="wf1"),
Workflow(tenant_id=tenant_id, project_id=proj.id, name="wf2"),
Agent(tenant_id=tenant_id, project_id=proj.id, name="a", config={}),
Tool(tenant_id=tenant_id, project_id=proj.id, name="t1", kind="builtin", config={}),
Tool(tenant_id=tenant_id, project_id=proj.id, name="t2", kind="builtin", config={}),
Tool(tenant_id=tenant_id, project_id=proj.id, name="t3", kind="builtin", config={}),
Component(tenant_id=tenant_id, project_id=proj.id, name="card"),
KbSource(tenant_id=tenant_id, project_id=proj.id, kind="text", name="src"),
AuthProvider(tenant_id=tenant_id, project_id=proj.id, name="auth", kind="bearer", config={}),
HandoffRequest(tenant_id=tenant_id, project_id=proj.id, run_id="run-open", status="open"),
HandoffRequest(tenant_id=tenant_id, project_id=proj.id, run_id="run-done", status="answered"),
# Belongs to a different project in the same tenant - must NOT be counted for `proj`.
Tool(tenant_id=tenant_id, project_id=other.id, name="other_tool", kind="builtin", config={}),
Workflow(tenant_id=tenant_id, project_id=other.id, name="other_wf"),
])
await session.commit()
# ProjectService.create auto-provisions the platform built-ins, so each project starts with
# len(BUILTIN_DEFAULTS) tools before the 3 added here. Scoping still holds: `other`'s tool
# (and its own provisioned built-ins) are not counted for `proj`.
from forge.tools.builtin import BUILTIN_DEFAULTS
counts = await ProjectService.counts(session, tenant_id, proj.id)
assert counts == {
"workflows": 2, "agents": 1, "tools": 3 + len(BUILTIN_DEFAULTS),
"components": 1, "knowledge": 1, "auth": 1, "handoffs": 1,
}
# A fresh project is all zeros except the auto-provisioned platform built-ins (never None).
empty = await ProjectService.create(session, tenant_id, name="Empty", slug="empty")
assert await ProjectService.counts(session, tenant_id, empty.id) == {
"workflows": 0, "agents": 0, "tools": len(BUILTIN_DEFAULTS), "components": 0, "knowledge": 0, "auth": 0,
"handoffs": 0,
}
async def test_delete_project_removes_project_scoped_data_and_trace_spans():
tenant_id = "tenant_delete_project"
deleted_threads: list[str] = []
class FakeCheckpointer:
async def adelete_thread(self, thread_id: str) -> None:
deleted_threads.append(thread_id)
async with SessionLocal() as session:
project = await ProjectService.create(session, tenant_id, name="Delete Me", slug="delete-me")
workflow = Workflow(tenant_id=tenant_id, project_id=project.id, name="wf")
thread = Thread(tenant_id=tenant_id, project_id=project.id, workflow_id="wf1", lg_thread_id="lg1")
run = Run(tenant_id=tenant_id, project_id=project.id, workflow_id="wf1", thread_id="thread1")
trace = Trace(tenant_id=tenant_id, project_id=project.id, workflow_id="wf1", run_id="run1", name="trace")
session.add_all([workflow, thread, run, trace])
await session.flush()
session.add_all([
Span(tenant_id=tenant_id, trace_id=trace.id, name="span", kind="node"),
Agent(tenant_id=tenant_id, project_id=project.id, name="agent", config={}),
Tool(tenant_id=tenant_id, project_id=project.id, name="tool", kind="builtin", config={}),
AuthProvider(tenant_id=tenant_id, project_id=project.id, name="auth", kind="bearer", config={}),
Secret(tenant_id=tenant_id, project_id=project.id, name="secret", kind="api_key", encrypted_value=b"x"),
KbSource(tenant_id=tenant_id, project_id=project.id, kind="text", name="source"),
QaPair(tenant_id=tenant_id, project_id=project.id, question="q", answer="a"),
McpClient(tenant_id=tenant_id, project_id=project.id, name="mcp"),
])
await session.commit()
await ProjectService.delete(session, project, checkpointer=FakeCheckpointer())
assert await ProjectService.get(session, tenant_id, project.id) is None
for model in (Workflow, Thread, Run, Trace, Agent, Tool, AuthProvider, Secret, KbSource, QaPair, McpClient):
assert await _count(session, model, project_id=project.id) == 0
assert await _count(session, Span, trace_id=trace.id) == 0
assert deleted_threads == ["lg1"]