mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
* feat(sidebar): top/bottom scroll fade mask (MUL-2150) Apply useScrollFade to SidebarContent so the menu list softly fades into the header / footer when overflowing, matching the existing pattern used in chat list and onboarding steps. Co-authored-by: multica-agent <github@multica.ai> * fix(ui): useScrollFade re-evaluates on content mutations ResizeObserver only fires on the observed element's own box. When a flex / auto-height container's children grow asynchronously (sidebar pinned items loading from TanStack Query, collapsibles expanding), scrollHeight changes but clientHeight does not — mask stayed 'none' until the user scrolled. Add a MutationObserver on childList to recompute fade when content is inserted or removed. Co-authored-by: multica-agent <github@multica.ai> * test(paths): include squads in workspace route consistency check main added the squads parameterless route to paths.workspace() in #2505 but the C4 consistency assertion wasn't updated, turning frontend CI red on every PR. Add 'squads' to both the parameterless-method set and the segment-mapping table. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai>
100 lines
3.4 KiB
TypeScript
100 lines
3.4 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",
|
|
"squads",
|
|
"inbox",
|
|
"myIssues",
|
|
"runtimes",
|
|
"skills",
|
|
"squads",
|
|
"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"],
|
|
["squads", "squads"],
|
|
["inbox", "inbox"],
|
|
["myIssues", "my-issues"],
|
|
["runtimes", "runtimes"],
|
|
["skills", "skills"],
|
|
["squads", "squads"],
|
|
["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);
|
|
}
|
|
});
|
|
});
|