Files
multica/packages/core/paths/consistency.test.ts
Bohan Jiang 451c46c43f refactor(usage): rename Dashboard → Usage + dynamic per-agent leaderboard (#2511)
The page added in #2462 lived at `/{slug}/dashboard` and was titled
"Dashboard", which collides with the conventional meaning ("personal
landing surface") and doesn't tell new users what the page is for. Its
actual contents — token spend, cost, run time, task counts — map cleanly
onto the OpenAI / Anthropic / Vercel "Usage" surface, so rename to that.

Renames (user-visible)
- Route: `/{slug}/dashboard` → `/{slug}/usage` (web App Router + desktop
  memory router)
- Sidebar entry: label "Dashboard" / "看板" → "Usage" / "用量", icon
  LayoutDashboard → BarChart3 (page header icon swapped in sync)
- Page title in en/zh-Hans
- Reserved-slugs: add `usage` to workspace route segments group;
  `dashboard` stays reserved in the marketing group (back-compat against
  workspace slug collisions + keeps the name free for a future Home page)
- i18n namespace `dashboard` → `usage` across resources-types.ts,
  locales/index.ts, and the moved JSON files
- WORKSPACE_ROUTE_SEGMENTS in editor link-handler
- paths.workspace(slug).dashboard() → .usage(), with matching test
  expectation updates

Per-agent leaderboard polish (`packages/views/dashboard/components/
dashboard-page.tsx`)
- Card title "Cost & run time by agent" → "Leaderboard" with a 4-way
  Segmented control: Tokens / Cost / Time / Tasks
- Active metric drives row order, progress-bar width, and the
  emphasised column header / cell — keeping ranking, visual quantity,
  and column emphasis in lockstep so users always see what's being
  measured
- Default sort = Tokens (most universally meaningful; Cost still one
  click away)
- Project filter dropdown:
  - Show ProjectIcon next to the selected project + each list item;
    FolderKanban as the "All projects" fallback (matches ProjectPicker
    language)
  - alignItemWithTrigger={false} so "All projects" doesn't get pushed
    above the trigger and clipped when the header sits at the top of
    the viewport (was the root cause of "can't re-select All projects"
    once a project was selected)
  - max-h-72 to cap the dropdown when workspaces accrue many projects;
    matches the runtime-detail Select precedent
- Folder name `packages/views/dashboard/*` and `DashboardPage`
  component name intentionally left in place — user-visible rename
  only, no broad code refactor.

Old `/dashboard` routes are not redirected because the page only landed
in #2462 (a few days ago); no real users, external links, or
desktop-tab persistence have settled on it yet.
2026-05-13 14:07:53 +08:00

96 lines
3.3 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { paths, isGlobalPath } from "./paths";
import { RESERVED_SLUGS } from "./reserved-slugs";
// C4 — link-handler's WORKSPACE_ROUTE_SEGMENTS must match paths.workspace's
// parameterless method names. We can't import WORKSPACE_ROUTE_SEGMENTS here
// because link-handler is in packages/views (no inverse import allowed), so
// we hardcode the expected list and assert paths.workspace produces the same
// keys. If you change either, BOTH need to be updated — the test catches drift.
describe("paths.workspace() shape", () => {
it("exposes the expected parameterless workspace route methods", () => {
const ws = paths.workspace("__probe__");
const parameterlessRoutes = Object.entries(ws)
.filter(([, fn]) => typeof fn === "function" && fn.length === 0)
.map(([key]) => key);
expect(new Set(parameterlessRoutes)).toEqual(
new Set([
"root",
"usage",
"issues",
"projects",
"autopilots",
"agents",
"inbox",
"myIssues",
"runtimes",
"skills",
"settings",
]),
);
});
it("each parameterless route emits /{slug}/{segment}", () => {
const ws = paths.workspace("acme");
// Check that none of the parameterless paths embed a leaked literal
// and that their second URL segment matches the method name's kebab-case.
const expectedSegments: Array<[string, string]> = [
["usage", "usage"],
["issues", "issues"],
["projects", "projects"],
["autopilots", "autopilots"],
["agents", "agents"],
["inbox", "inbox"],
["myIssues", "my-issues"],
["runtimes", "runtimes"],
["skills", "skills"],
["settings", "settings"],
];
const wsAsAny = ws as unknown as Record<string, () => string>;
for (const [method, segment] of expectedSegments) {
const fn = wsAsAny[method];
expect(typeof fn).toBe("function");
expect(fn!()).toBe(`/acme/${segment}`);
}
});
});
// C5 — invariants between the global/reserved lists.
describe("global path / reserved slug consistency", () => {
// If a path is "global" (never workspace-scoped), the slug name underlying it
// must be reserved — otherwise a user could create a workspace with that slug
// and shadow the global route's URL space.
//
// GLOBAL_PREFIXES from paths.ts is private — we re-derive the list from
// probing isGlobalPath. Order matters: keep this list in sync with paths.ts.
const globalPrefixes = [
"/login",
"/logout",
"/signup",
"/workspaces/",
"/invite/",
"/auth/",
];
it("isGlobalPath agrees with the canonical global prefix list", () => {
for (const prefix of globalPrefixes) {
expect(isGlobalPath(prefix)).toBe(true);
}
expect(isGlobalPath("/acme/issues")).toBe(false);
expect(isGlobalPath("/")).toBe(false);
});
it("every global prefix's first path segment is a reserved slug", () => {
for (const prefix of globalPrefixes) {
const firstSegment = prefix.split("/").filter(Boolean)[0];
if (!firstSegment) continue;
expect(
RESERVED_SLUGS.has(firstSegment),
`'${firstSegment}' is a global path prefix but not a reserved slug — ` +
`a workspace could be created with this slug and shadow the global route`,
).toBe(true);
}
});
});