Merge branch 'mai/kahn/phase-5i-phase-a-design' (phase 5i slice A: project filter dim + descendants toggle)

# Conflicts:
#	web/dashboard.go
#	web/server.go
#	web/templates/dashboard_section.tmpl
This commit is contained in:
mAi
2026-05-26 13:29:20 +02:00
17 changed files with 1046 additions and 58 deletions

View File

@@ -294,3 +294,71 @@ func TestMultiParentBothPathsRouteToSameRow(t *testing.T) {
}
}
}
// TestProjectFilterScopesTreeToDescendants verifies the Phase 5i Slice A
// project scope semantics end-to-end: ?project=<path> narrows / to the picked
// item + descendants; ?project_descendants=0 narrows further to the picked
// item alone. Both round-trip through ParseTreeFilter + TreeFilter.Matches +
// the tree handler.
func TestProjectFilterScopesTreeToDescendants(t *testing.T) {
srv, pool := mustServer(t)
defer pool.Close()
h := srv.Routes()
ctx := context.Background()
stamp := strings.ReplaceAll(time.Now().UTC().Format("150405.000"), ".", "")
parentSlug := "p5i-parent-" + stamp
childSlug := "p5i-child-" + stamp
siblingSlug := "p5i-sib-" + stamp
var dev string
if err := pool.QueryRow(ctx, `select id from projax.items where slug='dev' and cardinality(parent_ids)=0`).Scan(&dev); err != nil {
t.Fatalf("dev: %v", err)
}
var parentID, childID, siblingID string
if err := pool.QueryRow(ctx,
`insert into projax.items (kind, title, slug, parent_ids) values (array['project']::text[], 'P5i Parent', $1, ARRAY[$2]::uuid[]) returning id`,
parentSlug, dev).Scan(&parentID); err != nil {
t.Fatalf("seed parent: %v", err)
}
if err := pool.QueryRow(ctx,
`insert into projax.items (kind, title, slug, parent_ids) values (array['project']::text[], 'P5i Child', $1, ARRAY[$2]::uuid[]) returning id`,
childSlug, parentID).Scan(&childID); err != nil {
t.Fatalf("seed child: %v", err)
}
if err := pool.QueryRow(ctx,
`insert into projax.items (kind, title, slug, parent_ids) values (array['project']::text[], 'P5i Sib', $1, ARRAY[$2]::uuid[]) returning id`,
siblingSlug, dev).Scan(&siblingID); err != nil {
t.Fatalf("seed sibling: %v", err)
}
defer pool.Exec(context.Background(), `delete from projax.items where id in ($1,$2,$3)`, childID, parentID, siblingID)
parentPath := "dev." + parentSlug
parentLink := `href="/i/` + parentPath + `"`
childLink := `href="/i/` + parentPath + `.` + childSlug + `"`
siblingLink := `href="/i/dev.` + siblingSlug + `"`
// Descendants on (default): parent + child visible, sibling hidden.
_, withDesc := get(t, h, "/?project="+parentPath)
if !strings.Contains(withDesc, parentLink) {
t.Errorf("?project=%s should show parent row", parentPath)
}
if !strings.Contains(withDesc, childLink) {
t.Errorf("?project=%s should include descendant child row", parentPath)
}
if strings.Contains(withDesc, siblingLink) {
t.Errorf("?project=%s should exclude sibling row", parentPath)
}
// Descendants off: only the picked item, no children.
_, noDesc := get(t, h, "/?project="+parentPath+"&project_descendants=0")
if !strings.Contains(noDesc, parentLink) {
t.Errorf("?project_descendants=0 should still show the picked parent row")
}
if strings.Contains(noDesc, childLink) {
t.Errorf("?project_descendants=0 should hide the child row")
}
if strings.Contains(noDesc, siblingLink) {
t.Errorf("?project_descendants=0 should hide the sibling row")
}
}