mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-16 06:39:01 +02:00
* fix(routing): rename /new-workspace to /workspaces/new + extend reserved slug list
Two related changes:
1. Rename the global workspace-creation route from /new-workspace to
/workspaces/new. The hyphenated word-group `new-workspace` is a
common user workspace name (last deploy was blocked by a real user
with exactly this slug). Industry consensus from auditing Linear,
Vercel, Notion, Slack, GitHub: zero major SaaS uses hyphenated
word-group root routes — they all use single words or `/{noun}/{verb}`
pairs. Reserving the noun `workspaces` automatically protects the
entire `/workspaces/*` subtree, so future workspace-related routes
(`/workspaces/{id}/edit`, `/workspaces/{id}/billing`, etc.) need no
additional reserved slugs or audit migrations.
2. Extend the reserved slug list to cover the minimal set recommended by
the URL-design audit: full auth flow vocab, RFC 2142 mailbox names
(postmaster, abuse, noreply...), hostname confusables (mail, ftp,
static, cdn...), and likely-future platform routes (docs, support,
status, legal, privacy, terms, security, etc.). Production data
audit confirmed zero conflicts for every newly added slug, so
migration 047 (the safety net) passes cleanly.
Slugs intentionally NOT added despite being in scope of the audit:
admin, multica, new, setup, www. Each has one production workspace
already using it; adding them now would block deploy. They will be
handled in a follow-up PR via owner outreach + targeted rename.
Also adds a CLAUDE.md convention rule: new global routes MUST use a
single word or `/{noun}/{verb}` pair, never hyphenated word groups.
This prevents the pattern from regenerating itself.
This PR does NOT resolve the currently-blocked prd deploy — that requires
the existing `slug='new-workspace'` workspace (owner: Dhruv Raina) to be
renamed by ops. After that workspace is renamed and migration 046 passes,
this PR's migration 047 will also pass on its first run.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* review: drop migration 046, sweep stale comments, drive reserved test from map
Address code review on PR #1188:
1. Delete migration 046 (audit_new_workspace_slug). It audits "new-workspace"
which is no longer a reserved slug after this PR's rename. Removing 046
has an unexpected upside: it directly unblocks the currently-stuck prd
deploy. Migration 046 had never successfully applied (it was the source
of the deploy block); the audit-only nature means down-rollback is a
no-op. The user workspace previously caught by 046 (slug='new-workspace',
owner: Dhruv Raina) is now safe — `new-workspace` is no longer reserved,
so the slug correctly resolves to that workspace and the global route
`/workspaces/new` doesn't shadow it.
2. Refactor workspace_test.go to drive its reserved-slug list from the
reservedSlugs map directly via `for slug := range reservedSlugs`. The
previous hand-copied list was already drifting (40-ish entries vs 58 in
the map). Now drift is impossible.
3. Sweep ~10 stale `/new-workspace` references in code comments to
`/workspaces/new`. Comments only — runtime unchanged. The references
in reserved-slugs.ts/workspace_reserved_slugs.go and CLAUDE.md are
intentionally kept as anti-pattern examples ("don't add hyphenated
word-group root routes like /new-workspace").
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
94 lines
3.3 KiB
TypeScript
94 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",
|
|
"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]> = [
|
|
["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);
|
|
}
|
|
});
|
|
});
|