mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-25 22:16:19 +00:00
* feat(authz): gate composer send on runs:create (Phase 4 follow-up, #4063) Mirrors #5294's stop gating on the send side: both chat routes pass canCreateRuns (from PERMISSIONS.RUNS_CREATE, lockstep with the backend enum) into the shared composer. The gate sits at the top of submitThreadMessage — the single choke point every composer entry (submit button, Enter, goal-set-triggered run) funnels through — and denies with a toast plus a rejected promise so PromptInput keeps the text. The idle submit button is disabled and explains the boundary via conditionally-spread aria-label/title (startTurnUnavailable, en/zh); while streaming the button stays the runs:cancel stop affordance. Also removes the unreachable kind === "stop" branch in handleSubmit (flagged during #5294's review): the Enter path early-returns with the streaming toast before the classifier runs. * fix(frontend): reject denied goal starts before saving state --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
81 lines
2.6 KiB
TypeScript
81 lines
2.6 KiB
TypeScript
import { describe, expect, it } from "@rstest/core";
|
|
|
|
import { hasPermission, PERMISSIONS } from "@/core/auth/permissions";
|
|
import { userSchema } from "@/core/auth/types";
|
|
|
|
describe("hasPermission", () => {
|
|
it("grants a permission present in the resolved list", () => {
|
|
expect(
|
|
hasPermission(
|
|
{ permissions: ["threads:read", "threads:delete"] },
|
|
PERMISSIONS.THREADS_DELETE,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("denies a permission absent from the resolved list", () => {
|
|
expect(
|
|
hasPermission(
|
|
{ permissions: ["threads:read"] },
|
|
PERMISSIONS.THREADS_DELETE,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("treats an absent permissions field as permissive (pre-Phase-4 backend)", () => {
|
|
// A mixed old-backend/new-frontend deploy must not hide actions the
|
|
// caller can still perform — the Gateway route guards stay the
|
|
// enforcement point, the UI field is advisory only.
|
|
expect(hasPermission({}, PERMISSIONS.RUNS_CANCEL)).toBe(true);
|
|
});
|
|
|
|
it("treats a null permissions field as permissive", () => {
|
|
// Credential-creation responses (register/initialize) serialize null;
|
|
// they never advertise an empty grant set.
|
|
expect(hasPermission({ permissions: null }, PERMISSIONS.RUNS_CANCEL)).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("treats a not-yet-loaded user as permissive", () => {
|
|
expect(hasPermission(null, PERMISSIONS.THREADS_DELETE)).toBe(true);
|
|
expect(hasPermission(undefined, PERMISSIONS.THREADS_DELETE)).toBe(true);
|
|
});
|
|
|
|
it("maps the runs:create composer gate onto the /me permission list", () => {
|
|
expect(PERMISSIONS.RUNS_CREATE).toBe("runs:create");
|
|
expect(
|
|
hasPermission({ permissions: ["runs:create"] }, PERMISSIONS.RUNS_CREATE),
|
|
).toBe(true);
|
|
expect(
|
|
hasPermission({ permissions: ["runs:read"] }, PERMISSIONS.RUNS_CREATE),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("userSchema permissions field", () => {
|
|
const baseUser = {
|
|
id: "user-1",
|
|
email: "user@example.test",
|
|
system_role: "user" as const,
|
|
};
|
|
|
|
it("parses a /me payload that carries effective permissions", () => {
|
|
const parsed = userSchema.parse({
|
|
...baseUser,
|
|
permissions: ["threads:read", "runs:cancel"],
|
|
});
|
|
expect(parsed.permissions).toEqual(["threads:read", "runs:cancel"]);
|
|
});
|
|
|
|
it("parses a /me payload from a pre-Phase-4 backend (field absent)", () => {
|
|
const parsed = userSchema.parse(baseUser);
|
|
expect(parsed.permissions).toBeUndefined();
|
|
});
|
|
|
|
it("parses a credential-creation payload that carries null", () => {
|
|
const parsed = userSchema.parse({ ...baseUser, permissions: null });
|
|
expect(parsed.permissions).toBeNull();
|
|
});
|
|
});
|