feat: AI flow builder, visibility model, dashboard tabs, fork UI (#88)
- AI flow builder: scaffold → branch detail → assemble → review flow - Generate All one-click branch generation with stop/cancel - Regenerate scaffold suggestions button - 3-action review screen: Start Flow, Open in Editor, Build Another - Fix Publish button gated behind !isDirty - Fix visibility column enforcement in tree access filter - Add ?visibility filter and author_name to GET /trees - Dashboard tabbed flows: My Flows / My Team / Public / All - Create button in My Flows tab, window focus reload (stale data fix) - Fork UI with optional reason modal - Fix account_id nullability in User type and schema - Keep is_public and visibility in sync on updates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit was merged in pull request #88.
This commit is contained in:
@@ -124,6 +124,13 @@ class TestReferenceIntegrity:
|
||||
errors = validate_generated_tree(tree)
|
||||
assert any("non-existent child" in e for e in errors)
|
||||
|
||||
def test_action_next_node_id_references_nonexistent_node(self):
|
||||
"""Action next_node_id pointing to a node that doesn't exist anywhere in the tree."""
|
||||
tree = _make_valid_tree()
|
||||
tree["children"][1]["next_node_id"] = "ghost-node"
|
||||
errors = validate_generated_tree(tree)
|
||||
assert any("ghost-node" in e for e in errors)
|
||||
|
||||
def test_duplicate_option_ids(self):
|
||||
tree = _make_valid_tree()
|
||||
tree["options"][0]["id"] = "same"
|
||||
|
||||
@@ -391,3 +391,59 @@ class TestTrees:
|
||||
assert response.status_code == 200
|
||||
trees = response.json()
|
||||
assert isinstance(trees, list)
|
||||
|
||||
|
||||
class TestVisibilityFilter:
|
||||
"""Test that visibility filtering and author_name work correctly."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_private_tree_only_visible_to_author(
|
||||
self, client: AsyncClient, auth_headers: dict, test_user: dict
|
||||
):
|
||||
"""A private tree created by test_user should appear in their own list."""
|
||||
tree_data = {
|
||||
"name": "Private Flow Test",
|
||||
"tree_structure": {"id": "root", "type": "decision", "question": "Q?", "options": [], "children": []},
|
||||
}
|
||||
create_resp = await client.post("/api/v1/trees", json=tree_data, headers=auth_headers)
|
||||
assert create_resp.status_code == 201
|
||||
tree_id = create_resp.json()["id"]
|
||||
|
||||
# Set visibility to private
|
||||
vis_resp = await client.patch(
|
||||
f"/api/v1/trees/{tree_id}/visibility",
|
||||
json={"visibility": "private"},
|
||||
headers=auth_headers
|
||||
)
|
||||
assert vis_resp.status_code == 200
|
||||
|
||||
# Verify it still appears for the author
|
||||
list_resp = await client.get("/api/v1/trees", headers=auth_headers)
|
||||
assert list_resp.status_code == 200
|
||||
ids = [t["id"] for t in list_resp.json()]
|
||||
assert tree_id in ids
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_visibility_query_param_filters_correctly(
|
||||
self, client: AsyncClient, auth_headers: dict
|
||||
):
|
||||
"""?visibility=public should only return trees with visibility='public'."""
|
||||
resp = await client.get("/api/v1/trees?visibility=public", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
trees = resp.json()
|
||||
for tree in trees:
|
||||
assert tree["visibility"] == "public", f"Tree {tree['id']} has visibility={tree['visibility']}"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_author_name_present_in_list_response(
|
||||
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
||||
):
|
||||
"""TreeListResponse should include author_name field."""
|
||||
resp = await client.get("/api/v1/trees", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
trees = resp.json()
|
||||
assert len(trees) >= 1
|
||||
# author_name key should be present (value may be None for system/default trees)
|
||||
assert "author_name" in trees[0]
|
||||
# visibility key should be present
|
||||
assert "visibility" in trees[0]
|
||||
|
||||
Reference in New Issue
Block a user