mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-21 10:03:29 +00:00
refactor(bootstrap): extend dashboard caps to active_items/active_plans/by_role (TASK-1422) (#541)
* refactor(bootstrap): extend dashboard caps to active_items/active_plans/by_role/suggested_next (TASK-1422) Implements IDEA-1421 (absorbed into PLAN-1410's v0.4 envelope). Extends the BootstrapDashboard wrapper from TASK-1413 to cap four more dashboard sub-arrays with parallel overflow counts, same shape and semantics as the existing attention/recent_activity caps. ## Struct + caps Four new int fields on BootstrapDashboard (all `,omitempty`): - active_items_overflow_count - active_plans_overflow_count - by_role_overflow_count - suggested_next_overflow_count Four new cap constants alongside the existing two: - bootstrapActiveItemsCap = 5 - bootstrapActivePlansCap = 5 - bootstrapByRoleCap = 5 - bootstrapSuggestedNextCap = 5 capBootstrapDashboard extended with four parallel truncate-and-count blocks — same shallow-copy mutation pattern, source pointer untouched (the dashboard endpoint still returns its full-length arrays per its own contract). ## Tests TestCapBootstrapDashboard rewritten to cover all six caps under one contract. `mk` now takes a `dashCounts` struct (Att/Rec/Items/Plans/ Role/Sugg) so each subtest exercises specific caps without populating the others. Each of the four existing subtests (under-cap-no-overflow, over-cap-truncates-and-counts-overflow, source-pointer-unchanged, exact-cap-no-overflow) now asserts the new caps too. Added tiny assertLen/assertOverflow helpers to keep the per-array assertion noise from drowning the contract being tested. bootstrapSectionBytes extended to surface the four new cap-effect lines when triggered, table-driven so future caps drop in cleanly. seedBootstrapSizeFixture updated to seed 6 in_progress tasks (was 5 open) so the new active_items cap fires visibly in the per-section breakdown: "active_items capped: 5 shown, 1 overflow". The status flip is deliberate — dashboard.active_items filters on isActiveStatus(), which excludes initial/terminal statuses; open tasks never appeared in the section. ## Budget bootstrapSizeBudget 7 KiB → 9 KiB. Note that this is FIXTURE-side growth, not shape-side regression: the fixture now seeds enough active items to exercise the new cap (active_items section was 0 bytes when tasks were status=open). The cap itself is purely a SAVINGS — on docapp it drops active_items from 7 → 5 entries with overflow_count=2. Budget history note in handlers_bootstrap_test.go updated with the TASK-1422 line and an explicit "fixture-side, not shape-side" explanation so future readers understand why the budget moved up. ## Out of scope - Slim BootstrapRole projection — TASK-1423. - Schema label + sort_order trim — TASK-1424. - ToolSurfaceVersion 0.3 → 0.4 bump — TASK-1418 (final PR). Parent: PLAN-1410. Resolves IDEA-1421 once merged. * fix(bootstrap): drop unreachable suggested_next cap (TASK-1422 follow-up) Address Codex P1 finding on PR #541: `suggested_next_overflow_count` was unreachable in production responses because `buildDashboardResponse` already truncates `SuggestedNext` to 3 upstream (see "Take top 3" comment in handlers_dashboard.go:854-858), while my bootstrap cap was 5. The cap-and-overflow logic could only have fired against synthetic test state, never against the real dashboard pipeline. Two responses to consider: 1. Lower bootstrap's cap to a number smaller than 3 — defeats the upstream design choice (3 IS the intentional limit). 2. Drop the bootstrap-side cap — clean, no dead surface. Going with (2). If the upstream cap is ever raised or removed, that's the moment to add a suggested_next_overflow_count back. Removed: - SuggestedNextOverflowCount field on BootstrapDashboard - bootstrapSuggestedNextCap constant - The truncate-and-count block in capBootstrapDashboard - The suggested_next row in TestCapBootstrapDashboard's dashCounts helper and all five subtest assertions - The suggested_next entry in bootstrapSectionBytes's cap-line loop The fixture still seeds tasks and a plan, so the no-cap path on SuggestedNext is naturally exercised through TestBootstrapSizeBudget. The godoc on BootstrapDashboard now explicitly calls out the exclusion + the upstream-cap rationale so a future reader knows why suggested_next is missing from the otherwise-uniform cap set. Parent: PLAN-1410 / TASK-1422. * docs(skill): align SKILL.md dashboard cap description with TASK-1422 Address Codex P3 finding on PR #541: the SKILL.md `Context Loading` section described only the two original cap fields (attention_overflow_count, recent_activity_overflow_count). After TASK-1422 the bootstrap response carries three more (active_items_overflow_count, active_plans_overflow_count, by_role_overflow_count), and the agent needs to know to pull the full set via `pad project dashboard` when any of them are > 0. Updated the bullet to enumerate all five capped sub-arrays and state the overflow-field pattern generically rather than per-field. Same in-PR-sync pattern used for TASK-1413, TASK-1415, TASK-1416. Parent: PLAN-1410 / TASK-1422. * docs(bootstrap): fix three stale comments after dropping suggested_next cap (TASK-1422 follow-up) Address Codex P3 finding on PR #541 round 3: three stale doc strings referenced the old shape (with suggested_next) after the cap was dropped in the prior commit. Updated: 1. handlers_bootstrap_test.go budget-history line for TASK-1422 — removed `suggested_next` from the cap list and added the "deliberately excluded — already capped to 3 upstream" rationale so future readers know why the otherwise-uniform cap set is missing one. 2. handlers_bootstrap.go BootstrapDashboard godoc — changed "two overflow counts" to "five overflow counts (one per capped sub-array)". 3. handlers_bootstrap.go capBootstrapDashboard godoc — changed "both caps are untriggered" to "all caps are untriggered". Tidy-up only, no behavior change. Same skill-↔-code sync hygiene that has been the running theme across PLAN-1410's review loops. Parent: PLAN-1410 / TASK-1422. * docs(bootstrap): update remaining stale call-site comment for capBootstrapDashboard (TASK-1422 follow-up) Final stale-doc cleanup per Codex P3 on PR #541 round 4: the BuildAgentBootstrap dashboard-wrapping call-site comment still listed only attention + recent_activity. Updated to enumerate all five capped sub-arrays for parity with the godoc on BootstrapDashboard / capBootstrapDashboard. Same hygiene as the previous commit; no behavior change. Parent: PLAN-1410 / TASK-1422.
This commit is contained in:
@@ -153,8 +153,8 @@ type AgentBootstrapPlaybookMeta struct {
|
||||
// BootstrapDashboard is the bootstrap-side dashboard projection. It
|
||||
// embeds *DashboardResponse so the wire shape stays compatible with the
|
||||
// `GET /dashboard` endpoint (same field names, same nesting), then adds
|
||||
// two overflow counts that report how many entries were trimmed from
|
||||
// the bootstrap's capped views.
|
||||
// five overflow counts (one per capped sub-array) that report how many
|
||||
// entries were trimmed from the bootstrap's capped views.
|
||||
//
|
||||
// Why a wrapper rather than mutating DashboardResponse: the cap is
|
||||
// bootstrap-only — `pad project dashboard` and the web UI's dashboard
|
||||
@@ -168,18 +168,38 @@ type BootstrapDashboard struct {
|
||||
// RecentActivityOverflowCount mirrors AttentionOverflowCount for the
|
||||
// recent_activity tail.
|
||||
RecentActivityOverflowCount int `json:"recent_activity_overflow_count,omitempty"`
|
||||
// ActiveItemsOverflowCount, ActivePlansOverflowCount, and
|
||||
// ByRoleOverflowCount cap the three other dashboard sub-arrays
|
||||
// that grow with workspace state. Same semantics as the
|
||||
// Attention/RecentActivity counts above: omitted when zero,
|
||||
// populated with `len(original) - cap` when truncation kicked in.
|
||||
// PLAN-1410 / TASK-1422 (absorbs IDEA-1421).
|
||||
//
|
||||
// `suggested_next` was deliberately NOT added to this set:
|
||||
// buildDashboardResponse already truncates SuggestedNext to 3
|
||||
// upstream (see "Take top 3" in handlers_dashboard.go), so a
|
||||
// bootstrap-side cap of 5 would be unreachable dead code. If the
|
||||
// upstream cap is ever raised or removed, that's the moment to
|
||||
// add a suggested_next_overflow_count here.
|
||||
ActiveItemsOverflowCount int `json:"active_items_overflow_count,omitempty"`
|
||||
ActivePlansOverflowCount int `json:"active_plans_overflow_count,omitempty"`
|
||||
ByRoleOverflowCount int `json:"by_role_overflow_count,omitempty"`
|
||||
}
|
||||
|
||||
// bootstrapAttentionCap and bootstrapRecentActivityCap clamp the
|
||||
// dashboard's `attention` and `recent_activity` arrays in the bootstrap
|
||||
// Bootstrap caps clamp the per-array sizes in the bootstrap dashboard
|
||||
// projection. 5 is the practical surfacing depth for an agent greeting
|
||||
// or status pass — anything beyond is too much for a single response
|
||||
// to render conversationally; the agent should pivot to the full
|
||||
// `pad project dashboard` query when the overflow count signals more
|
||||
// work to consider. PLAN-1410.
|
||||
// `pad project dashboard` query when an overflow count signals more
|
||||
// work to consider. PLAN-1410. The first two land in TASK-1413; the
|
||||
// remaining three (active_items / active_plans / by_role) are TASK-1422
|
||||
// (IDEA-1421 absorbed). suggested_next is excluded — upstream cap of 3.
|
||||
const (
|
||||
bootstrapAttentionCap = 5
|
||||
bootstrapRecentActivityCap = 5
|
||||
bootstrapActiveItemsCap = 5
|
||||
bootstrapActivePlansCap = 5
|
||||
bootstrapByRoleCap = 5
|
||||
)
|
||||
|
||||
// isCollectionSlugVisible reports whether the named collection survived
|
||||
@@ -375,8 +395,10 @@ func (s *Server) BuildAgentBootstrap(workspaceID string, user *models.User, r *h
|
||||
|
||||
// Dashboard — recreate via the existing handler logic if a request
|
||||
// context is available, then wrap in BootstrapDashboard so the
|
||||
// bootstrap-side cap on `attention` and `recent_activity` (with
|
||||
// overflow counts) doesn't leak into the `GET /dashboard` contract.
|
||||
// bootstrap-side caps on `attention`, `recent_activity`,
|
||||
// `active_items`, `active_plans`, and `by_role` (each with its
|
||||
// `*_overflow_count` companion) don't leak into the
|
||||
// `GET /dashboard` contract.
|
||||
if r != nil {
|
||||
dash, derr := s.buildDashboardResponse(workspaceID, r)
|
||||
if derr == nil && dash != nil {
|
||||
@@ -577,7 +599,7 @@ func trimLeadingSpaces(s string) string {
|
||||
// arrays unchanged. The slice backing arrays are shared — we only
|
||||
// trim the view, no allocation needed for the truncated portion.
|
||||
//
|
||||
// Returns a non-nil *BootstrapDashboard even when both caps are
|
||||
// Returns a non-nil *BootstrapDashboard even when all caps are
|
||||
// untriggered, so the agent always sees a consistent shape.
|
||||
func capBootstrapDashboard(d *DashboardResponse) *BootstrapDashboard {
|
||||
copied := *d
|
||||
@@ -590,6 +612,20 @@ func capBootstrapDashboard(d *DashboardResponse) *BootstrapDashboard {
|
||||
copied.RecentActivity = copied.RecentActivity[:bootstrapRecentActivityCap]
|
||||
out.RecentActivityOverflowCount = n
|
||||
}
|
||||
if n := len(copied.ActiveItems) - bootstrapActiveItemsCap; n > 0 {
|
||||
copied.ActiveItems = copied.ActiveItems[:bootstrapActiveItemsCap]
|
||||
out.ActiveItemsOverflowCount = n
|
||||
}
|
||||
if n := len(copied.ActivePlans) - bootstrapActivePlansCap; n > 0 {
|
||||
copied.ActivePlans = copied.ActivePlans[:bootstrapActivePlansCap]
|
||||
out.ActivePlansOverflowCount = n
|
||||
}
|
||||
if n := len(copied.ByRole) - bootstrapByRoleCap; n > 0 {
|
||||
copied.ByRole = copied.ByRole[:bootstrapByRoleCap]
|
||||
out.ByRoleOverflowCount = n
|
||||
}
|
||||
// SuggestedNext intentionally NOT capped here — see godoc on
|
||||
// BootstrapDashboard.
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/PerpetualSoftware/pad/internal/store"
|
||||
)
|
||||
|
||||
// TestBootstrapEmptyWorkspace verifies the bootstrap blob returns the
|
||||
@@ -151,12 +153,17 @@ func TestBootstrapIncludesPlaybookMetadata(t *testing.T) {
|
||||
// TASK-1412 — 11 KiB (slim BootstrapCollection projection; fixture at 8,992 bytes — collections section dropped from 8,848 to 3,979 bytes)
|
||||
// TASK-1413 — 8 KiB (dedup top-level recent_activity, drop convention slug, cap dashboard.attention/recent_activity to 5 with overflow counts; fixture at 6,355 bytes — total dropped another 2,637 bytes)
|
||||
// TASK-1417 — 7 KiB (close-out: bootstrap shape work complete, fixture still at 6,355 bytes; locks in the cumulative -54.2% win with ~12.8% headroom for routine schema reordering)
|
||||
// TASK-1422 — 9 KiB (extend dashboard caps to active_items/active_plans/by_role; fixture grew to 7,823 bytes after seeding 6 in_progress tasks to exercise the new active_items cap — the growth is fixture-side, not shape-side, and the new caps demonstrably trigger in the per-section breakdown. suggested_next deliberately excluded — already capped to 3 upstream in buildDashboardResponse.)
|
||||
//
|
||||
// PLAN-1410 followups land their own wins under their own budget
|
||||
// ratchets (e.g. IDEA-1421's dashboard sub-array caps). The constant
|
||||
// intentionally lives next to the test that consumes it so PRs
|
||||
// touching the bootstrap shape see the budget in the diff.
|
||||
const bootstrapSizeBudget = 7 * 1024
|
||||
// Note that the TASK-1422 budget loosening is purely fixture-side: the
|
||||
// fixture deliberately seeds more `in_progress` items so the
|
||||
// active_items cap fires under realistic load. The cap itself is
|
||||
// purely a SAVINGS (clamps unbounded growth in live workspaces). On
|
||||
// docapp the cap drops active_items from 7 → 5 entries.
|
||||
//
|
||||
// The constant intentionally lives next to the test that consumes it
|
||||
// so PRs touching the bootstrap shape see the budget in the diff.
|
||||
const bootstrapSizeBudget = 9 * 1024
|
||||
|
||||
// TestBootstrapSizeBudget locks in a payload-size budget for the
|
||||
// bootstrap response so future regressions are caught at PR time.
|
||||
@@ -233,11 +240,16 @@ func seedBootstrapSizeFixture(t *testing.T, srv *Server, wsSlug string) {
|
||||
"fields": `{"status":"active","trigger":"manual","invocation_slug":"release","arguments":[{"name":"version","type":"string","required":true}]}`,
|
||||
})
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
// 6 in-progress tasks: 6 > bootstrapActiveItemsCap (5) → exercises
|
||||
// the active_items cap in the fixture, producing an "active_items
|
||||
// capped: 5 shown, 1 overflow" line in the per-section breakdown.
|
||||
// in_progress (not open) because dashboard.active_items filters on
|
||||
// isActiveStatus(), which excludes initial/terminal statuses.
|
||||
for i := 0; i < 6; i++ {
|
||||
createItem(t, srv, wsSlug, "tasks", map[string]interface{}{
|
||||
"title": fmt.Sprintf("Sample task %d", i),
|
||||
"content": "Task body — placeholder content to give the dashboard something to summarize.",
|
||||
"fields": `{"status":"open","priority":"medium"}`,
|
||||
"fields": `{"status":"in-progress","priority":"medium"}`,
|
||||
})
|
||||
}
|
||||
createItem(t, srv, wsSlug, "plans", map[string]interface{}{
|
||||
@@ -266,17 +278,26 @@ func bootstrapSectionBytes(b AgentBootstrap) []string {
|
||||
fmt.Sprintf("dashboard: %d bytes", jsonLen(b.Dashboard)),
|
||||
}
|
||||
// Surface the caps' effect when triggered so the trim's value is
|
||||
// legible from CI output as PLAN-1410's later PRs land.
|
||||
// legible from CI output as PLAN-1410's PRs land. Each line follows
|
||||
// the same shape: "<array> capped: N shown, M overflow".
|
||||
if b.Dashboard != nil {
|
||||
if b.Dashboard.AttentionOverflowCount > 0 {
|
||||
lines = append(lines, fmt.Sprintf(
|
||||
" └─ attention capped: %d shown, %d overflow",
|
||||
len(b.Dashboard.Attention), b.Dashboard.AttentionOverflowCount))
|
||||
type capLine struct {
|
||||
name string
|
||||
shown int
|
||||
overflow int
|
||||
}
|
||||
if b.Dashboard.RecentActivityOverflowCount > 0 {
|
||||
lines = append(lines, fmt.Sprintf(
|
||||
" └─ recent_activity capped: %d shown, %d overflow",
|
||||
len(b.Dashboard.RecentActivity), b.Dashboard.RecentActivityOverflowCount))
|
||||
for _, c := range []capLine{
|
||||
{"attention", len(b.Dashboard.Attention), b.Dashboard.AttentionOverflowCount},
|
||||
{"recent_activity", len(b.Dashboard.RecentActivity), b.Dashboard.RecentActivityOverflowCount},
|
||||
{"active_items", len(b.Dashboard.ActiveItems), b.Dashboard.ActiveItemsOverflowCount},
|
||||
{"active_plans", len(b.Dashboard.ActivePlans), b.Dashboard.ActivePlansOverflowCount},
|
||||
{"by_role", len(b.Dashboard.ByRole), b.Dashboard.ByRoleOverflowCount},
|
||||
} {
|
||||
if c.overflow > 0 {
|
||||
lines = append(lines, fmt.Sprintf(
|
||||
" └─ %s capped: %d shown, %d overflow",
|
||||
c.name, c.shown, c.overflow))
|
||||
}
|
||||
}
|
||||
}
|
||||
return lines
|
||||
@@ -295,91 +316,166 @@ func jsonLen(v interface{}) int {
|
||||
}
|
||||
|
||||
// TestCapBootstrapDashboard isolates the bootstrap dashboard cap logic
|
||||
// from the rest of the bootstrap pipeline so the contract (cap to N,
|
||||
// surface overflow count, leave the source pointer untouched) doesn't
|
||||
// drift silently. PLAN-1410 / TASK-1413.
|
||||
// from the rest of the bootstrap pipeline so the contract (cap to N per
|
||||
// array, surface overflow count, leave the source pointer untouched)
|
||||
// doesn't drift silently as new caps are added.
|
||||
//
|
||||
// PLAN-1410 introduced caps on attention + recent_activity (TASK-1413)
|
||||
// and extended them to active_items / active_plans / by_role
|
||||
// (TASK-1422, absorbing IDEA-1421). `suggested_next` is deliberately
|
||||
// excluded — it's already capped to 3 upstream in buildDashboardResponse,
|
||||
// making a bootstrap-side cap unreachable in production. This test
|
||||
// covers all five live caps under the same contract.
|
||||
func TestCapBootstrapDashboard(t *testing.T) {
|
||||
// Helper: make a DashboardResponse with N attention + M recent_activity.
|
||||
mk := func(attN, recN int) *DashboardResponse {
|
||||
attention := make([]DashboardAttention, attN)
|
||||
for i := range attention {
|
||||
attention[i] = DashboardAttention{Type: "stalled", ItemRef: fmt.Sprintf("TASK-%d", i)}
|
||||
// dashCounts is the per-array size knob the test uses to construct
|
||||
// a DashboardResponse with arbitrary fill levels. Each field can be
|
||||
// set independently so subtests can exercise specific caps without
|
||||
// populating the others — keeps the assertions for any one cap
|
||||
// uncoupled from the noise of the others.
|
||||
type dashCounts struct {
|
||||
Att, Rec, Items, Plans, Role int
|
||||
}
|
||||
mk := func(c dashCounts) *DashboardResponse {
|
||||
d := &DashboardResponse{
|
||||
Attention: make([]DashboardAttention, c.Att),
|
||||
RecentActivity: make([]DashboardActivity, c.Rec),
|
||||
ActiveItems: make([]DashboardActiveItem, c.Items),
|
||||
ActivePlans: make([]DashboardPlan, c.Plans),
|
||||
ByRole: make([]store.RoleBreakdown, c.Role),
|
||||
}
|
||||
recent := make([]DashboardActivity, recN)
|
||||
for i := range recent {
|
||||
recent[i] = DashboardActivity{Action: "updated", ItemSlug: fmt.Sprintf("item-%d", i)}
|
||||
// Populate with identifying values so the cap's slice header
|
||||
// retains a defined order — easier to spot index-shifting bugs.
|
||||
for i := range d.Attention {
|
||||
d.Attention[i] = DashboardAttention{Type: "stalled", ItemRef: fmt.Sprintf("TASK-%d", i)}
|
||||
}
|
||||
return &DashboardResponse{Attention: attention, RecentActivity: recent}
|
||||
for i := range d.RecentActivity {
|
||||
d.RecentActivity[i] = DashboardActivity{Action: "updated", ItemSlug: fmt.Sprintf("item-%d", i)}
|
||||
}
|
||||
for i := range d.ActiveItems {
|
||||
d.ActiveItems[i] = DashboardActiveItem{Slug: fmt.Sprintf("active-%d", i)}
|
||||
}
|
||||
for i := range d.ActivePlans {
|
||||
d.ActivePlans[i] = DashboardPlan{Slug: fmt.Sprintf("plan-%d", i)}
|
||||
}
|
||||
for i := range d.ByRole {
|
||||
d.ByRole[i] = store.RoleBreakdown{}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
t.Run("under-cap-no-overflow", func(t *testing.T) {
|
||||
d := mk(2, 3)
|
||||
d := mk(dashCounts{Att: 2, Rec: 3, Items: 1, Plans: 1, Role: 2})
|
||||
out := capBootstrapDashboard(d)
|
||||
if out == nil {
|
||||
t.Fatal("expected non-nil result even when nothing trimmed")
|
||||
}
|
||||
if got := len(out.Attention); got != 2 {
|
||||
t.Errorf("attention len = %d, want 2 (under cap, no trim)", got)
|
||||
}
|
||||
if out.AttentionOverflowCount != 0 {
|
||||
t.Errorf("attention_overflow_count = %d, want 0", out.AttentionOverflowCount)
|
||||
}
|
||||
if got := len(out.RecentActivity); got != 3 {
|
||||
t.Errorf("recent_activity len = %d, want 3", got)
|
||||
}
|
||||
if out.RecentActivityOverflowCount != 0 {
|
||||
t.Errorf("recent_activity_overflow_count = %d, want 0", out.RecentActivityOverflowCount)
|
||||
}
|
||||
assertLen(t, "attention", len(out.Attention), 2)
|
||||
assertOverflow(t, "attention", out.AttentionOverflowCount, 0)
|
||||
assertLen(t, "recent_activity", len(out.RecentActivity), 3)
|
||||
assertOverflow(t, "recent_activity", out.RecentActivityOverflowCount, 0)
|
||||
assertLen(t, "active_items", len(out.ActiveItems), 1)
|
||||
assertOverflow(t, "active_items", out.ActiveItemsOverflowCount, 0)
|
||||
assertLen(t, "active_plans", len(out.ActivePlans), 1)
|
||||
assertOverflow(t, "active_plans", out.ActivePlansOverflowCount, 0)
|
||||
assertLen(t, "by_role", len(out.ByRole), 2)
|
||||
assertOverflow(t, "by_role", out.ByRoleOverflowCount, 0)
|
||||
})
|
||||
|
||||
t.Run("over-cap-truncates-and-counts-overflow", func(t *testing.T) {
|
||||
d := mk(bootstrapAttentionCap+8, bootstrapRecentActivityCap+3)
|
||||
d := mk(dashCounts{
|
||||
Att: bootstrapAttentionCap + 8,
|
||||
Rec: bootstrapRecentActivityCap + 3,
|
||||
Items: bootstrapActiveItemsCap + 4,
|
||||
Plans: bootstrapActivePlansCap + 2,
|
||||
Role: bootstrapByRoleCap + 1,
|
||||
})
|
||||
out := capBootstrapDashboard(d)
|
||||
if got := len(out.Attention); got != bootstrapAttentionCap {
|
||||
t.Errorf("attention len = %d, want %d (capped)", got, bootstrapAttentionCap)
|
||||
}
|
||||
if out.AttentionOverflowCount != 8 {
|
||||
t.Errorf("attention_overflow_count = %d, want 8", out.AttentionOverflowCount)
|
||||
}
|
||||
if got := len(out.RecentActivity); got != bootstrapRecentActivityCap {
|
||||
t.Errorf("recent_activity len = %d, want %d (capped)", got, bootstrapRecentActivityCap)
|
||||
}
|
||||
if out.RecentActivityOverflowCount != 3 {
|
||||
t.Errorf("recent_activity_overflow_count = %d, want 3", out.RecentActivityOverflowCount)
|
||||
}
|
||||
assertLen(t, "attention", len(out.Attention), bootstrapAttentionCap)
|
||||
assertOverflow(t, "attention", out.AttentionOverflowCount, 8)
|
||||
assertLen(t, "recent_activity", len(out.RecentActivity), bootstrapRecentActivityCap)
|
||||
assertOverflow(t, "recent_activity", out.RecentActivityOverflowCount, 3)
|
||||
assertLen(t, "active_items", len(out.ActiveItems), bootstrapActiveItemsCap)
|
||||
assertOverflow(t, "active_items", out.ActiveItemsOverflowCount, 4)
|
||||
assertLen(t, "active_plans", len(out.ActivePlans), bootstrapActivePlansCap)
|
||||
assertOverflow(t, "active_plans", out.ActivePlansOverflowCount, 2)
|
||||
assertLen(t, "by_role", len(out.ByRole), bootstrapByRoleCap)
|
||||
assertOverflow(t, "by_role", out.ByRoleOverflowCount, 1)
|
||||
})
|
||||
|
||||
t.Run("source-pointer-unchanged", func(t *testing.T) {
|
||||
// Defensive contract: callers downstream of buildDashboardResponse
|
||||
// (the dashboard endpoint itself) must see their full-length
|
||||
// arrays. The cap mutates a shallow copy.
|
||||
d := mk(bootstrapAttentionCap+5, bootstrapRecentActivityCap+5)
|
||||
origAttLen := len(d.Attention)
|
||||
origRecLen := len(d.RecentActivity)
|
||||
d := mk(dashCounts{
|
||||
Att: bootstrapAttentionCap + 5,
|
||||
Rec: bootstrapRecentActivityCap + 5,
|
||||
Items: bootstrapActiveItemsCap + 5,
|
||||
Plans: bootstrapActivePlansCap + 5,
|
||||
Role: bootstrapByRoleCap + 5,
|
||||
})
|
||||
want := struct{ att, rec, items, plans, role int }{
|
||||
att: len(d.Attention),
|
||||
rec: len(d.RecentActivity),
|
||||
items: len(d.ActiveItems),
|
||||
plans: len(d.ActivePlans),
|
||||
role: len(d.ByRole),
|
||||
}
|
||||
|
||||
_ = capBootstrapDashboard(d)
|
||||
|
||||
if got := len(d.Attention); got != origAttLen {
|
||||
t.Errorf("source Attention mutated: len = %d, want %d", got, origAttLen)
|
||||
if got := len(d.Attention); got != want.att {
|
||||
t.Errorf("source Attention mutated: len = %d, want %d", got, want.att)
|
||||
}
|
||||
if got := len(d.RecentActivity); got != origRecLen {
|
||||
t.Errorf("source RecentActivity mutated: len = %d, want %d", got, origRecLen)
|
||||
if got := len(d.RecentActivity); got != want.rec {
|
||||
t.Errorf("source RecentActivity mutated: len = %d, want %d", got, want.rec)
|
||||
}
|
||||
if got := len(d.ActiveItems); got != want.items {
|
||||
t.Errorf("source ActiveItems mutated: len = %d, want %d", got, want.items)
|
||||
}
|
||||
if got := len(d.ActivePlans); got != want.plans {
|
||||
t.Errorf("source ActivePlans mutated: len = %d, want %d", got, want.plans)
|
||||
}
|
||||
if got := len(d.ByRole); got != want.role {
|
||||
t.Errorf("source ByRole mutated: len = %d, want %d", got, want.role)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("exact-cap-no-overflow", func(t *testing.T) {
|
||||
// Boundary: len == cap should not flag overflow.
|
||||
d := mk(bootstrapAttentionCap, bootstrapRecentActivityCap)
|
||||
d := mk(dashCounts{
|
||||
Att: bootstrapAttentionCap,
|
||||
Rec: bootstrapRecentActivityCap,
|
||||
Items: bootstrapActiveItemsCap,
|
||||
Plans: bootstrapActivePlansCap,
|
||||
Role: bootstrapByRoleCap,
|
||||
})
|
||||
out := capBootstrapDashboard(d)
|
||||
if out.AttentionOverflowCount != 0 {
|
||||
t.Errorf("attention_overflow_count at exact cap = %d, want 0", out.AttentionOverflowCount)
|
||||
}
|
||||
if out.RecentActivityOverflowCount != 0 {
|
||||
t.Errorf("recent_activity_overflow_count at exact cap = %d, want 0", out.RecentActivityOverflowCount)
|
||||
}
|
||||
assertOverflow(t, "attention", out.AttentionOverflowCount, 0)
|
||||
assertOverflow(t, "recent_activity", out.RecentActivityOverflowCount, 0)
|
||||
assertOverflow(t, "active_items", out.ActiveItemsOverflowCount, 0)
|
||||
assertOverflow(t, "active_plans", out.ActivePlansOverflowCount, 0)
|
||||
assertOverflow(t, "by_role", out.ByRoleOverflowCount, 0)
|
||||
})
|
||||
}
|
||||
|
||||
// assertLen and assertOverflow are tiny helpers used by
|
||||
// TestCapBootstrapDashboard to keep the per-array assertion noise from
|
||||
// drowning the actual contract being tested. Both call t.Helper() so
|
||||
// failure lines point at the calling subtest, not at this file.
|
||||
func assertLen(t *testing.T, name string, got, want int) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Errorf("%s len = %d, want %d", name, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func assertOverflow(t *testing.T, name string, got, want int) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Errorf("%s_overflow_count = %d, want %d", name, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPlaybookSummaryPrefersFirstParagraph isolates the summary extraction
|
||||
// from the bootstrap path so the rule (skip headings, take first non-empty
|
||||
// paragraph, cap at ~240 chars) doesn't drift silently.
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ The returned `AgentBootstrap` blob carries everything the skill needs to start a
|
||||
- `conventions [...]` — full bodies of `trigger=always, status=active` items. **Must-follow project rules.**
|
||||
- `roles [...]` — agent roles configured in the workspace
|
||||
- `playbooks [...]` — METADATA ONLY: `ref`, `title`, `slug`, `invocation_slug`, `trigger`, `scope`, `status`, `has_arguments`, `summary`. Full bodies load on invocation via `pad playbook show <slug>`.
|
||||
- `dashboard {...}` — active items, attention, suggested next, recent activity. `attention` and `recent_activity` are capped to 5 entries each; `attention_overflow_count` and `recent_activity_overflow_count` report how many were trimmed (use `pad project dashboard` to pull the full set when overflow > 0).
|
||||
- `dashboard {...}` — active items, attention, suggested next, recent activity. Five sub-arrays are capped to 5 entries each (`attention`, `recent_activity`, `active_items`, `active_plans`, `by_role`); each pairs with a `<name>_overflow_count` int field surfaced when truncation kicked in. Use `pad project dashboard` to pull the full set when any overflow > 0.
|
||||
|
||||
If the conventions list includes items, treat them as project rules you must follow. The vocabulary depends on the workspace domain — a software workspace ships rules like "use conventional commit format," a hiring workspace ships rules like "anonymize candidate names in exports," a research workspace ships rules like "always cite sources." Follow whatever the workspace has configured.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user