chore: v2.8.15 — Oh My Pi login sharing, omp --allow-home, Markdown Preview default, first-login password
- omp-share.js: an admin's switch (S3) runs omp's own auth broker + gateway as the host on loopback; a member's omp pane reaches it as a scoped client keyed by its pane token (clients file = live omp panes only), runs on --model agenthub/<model>, never holds a credential. - omp catalog: --allow-home, so a workspace at the account's home keeps its sessions there (they were recorded under a temp dir and dropped). - Settings → Appearance → Open Markdown as: Preview (default) or Raw, for Files/Changes, the config viewer and a chat's file panel. - First-login password change and password rules, avatar URLs, Artifacts "Mine" includes installed shared skills/plugins, a member's own hook pages.
This commit is contained in:
@@ -2,8 +2,9 @@
|
||||
* @file AccountSettings.test.tsx
|
||||
* @description Settings → Account: shows the account's own information, saves
|
||||
* only a changed display name/email, refuses a password change whose two new
|
||||
* passwords differ, sends the current + new password, asks before signing
|
||||
* the other devices out, and uploads/removes the avatar.
|
||||
* passwords differ, keeps a password that misses a rule from being sent,
|
||||
* sends the current + new password, asks before signing
|
||||
* the other devices out, and uploads/removes the avatar or sets it from a link.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -17,6 +18,7 @@ const me = vi.hoisted(() => ({
|
||||
changePassword: vi.fn(),
|
||||
signOutOthers: vi.fn(),
|
||||
setAvatar: vi.fn(),
|
||||
setAvatarFromUrl: vi.fn(),
|
||||
removeAvatar: vi.fn(),
|
||||
avatarUrl: (at?: string | null) => (at ? `/api/me/avatar?v=${at}` : null),
|
||||
}));
|
||||
@@ -76,6 +78,22 @@ describe("AccountSettings", () => {
|
||||
expect(await screen.findByTestId("user-avatar-initials")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("sets the avatar from a link and closes the field", async () => {
|
||||
const ue = userEvent.setup();
|
||||
const withAvatar = { ...account, user: { ...account.user, avatarAt: "2026-10-21T00:00:00Z" } };
|
||||
me.setAvatarFromUrl.mockResolvedValue(withAvatar);
|
||||
render(<AccountSettings />);
|
||||
await screen.findByTestId("account-card");
|
||||
expect(screen.queryByTestId("account-avatar-url-input")).toBeNull();
|
||||
await ue.click(screen.getByTestId("account-avatar-url-toggle"));
|
||||
await ue.type(screen.getByTestId("account-avatar-url-input"), " https://x.vn/me.png {Enter}");
|
||||
await waitFor(() => expect(me.setAvatarFromUrl).toHaveBeenCalledWith("https://x.vn/me.png"));
|
||||
expect((await screen.findByTestId("user-avatar-img")).getAttribute("src")).toBe(
|
||||
"/api/me/avatar?v=2026-10-21T00:00:00Z"
|
||||
);
|
||||
expect(screen.queryByTestId("account-avatar-url-input")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows the account's own information", async () => {
|
||||
render(<AccountSettings />);
|
||||
const card = await screen.findByTestId("account-card");
|
||||
@@ -114,9 +132,21 @@ describe("AccountSettings", () => {
|
||||
await ue.type(screen.getByLabelText("Current password"), "old-pass-1");
|
||||
await ue.type(screen.getByLabelText("New password"), "new-pass-123");
|
||||
await ue.type(screen.getByLabelText("Confirm new password"), "new-pass-123");
|
||||
// No upper-case letter: the checklist says so and the button stays off.
|
||||
expect(
|
||||
screen
|
||||
.getByTestId("password-rules")
|
||||
.querySelector('[data-rule="upper"]')
|
||||
?.getAttribute("data-ok")
|
||||
).toBe("false");
|
||||
expect(screen.getByRole("button", { name: "Change password" })).toBeDisabled();
|
||||
await ue.clear(screen.getByLabelText("New password"));
|
||||
await ue.clear(screen.getByLabelText("Confirm new password"));
|
||||
await ue.type(screen.getByLabelText("New password"), "New-pass-123");
|
||||
await ue.type(screen.getByLabelText("Confirm new password"), "New-pass-123");
|
||||
await ue.click(screen.getByRole("button", { name: "Change password" }));
|
||||
await waitFor(() =>
|
||||
expect(me.changePassword).toHaveBeenCalledWith("old-pass-1", "new-pass-123")
|
||||
expect(me.changePassword).toHaveBeenCalledWith("old-pass-1", "New-pass-123")
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* @file Tests for OmpSharingSettings — the admin's Oh My Pi login-sharing
|
||||
* switch in Settings → AI tools: hidden in local mode, for a non-admin and
|
||||
* outside S3, and a toggle saves through PUT /api/ai-tools/omp-sharing and
|
||||
* shows the daemons' state.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import "../../../i18n";
|
||||
import { OmpSharingSettings } from "../OmpSharingSettings";
|
||||
import { api, type OmpSharingState } from "../../../lib/api";
|
||||
|
||||
const perms = vi.hoisted(() => ({ isAdmin: true, local: false }));
|
||||
vi.mock("../../AuthGate", () => ({ usePermissions: () => perms }));
|
||||
vi.mock("../../../lib/api", () => ({
|
||||
api: { aiTools: { ompSharing: vi.fn(), setOmpSharing: vi.fn() } },
|
||||
}));
|
||||
|
||||
const get = vi.mocked(api.aiTools.ompSharing);
|
||||
const set = vi.mocked(api.aiTools.setOmpSharing);
|
||||
const off: OmpSharingState = {
|
||||
available: true,
|
||||
enabled: false,
|
||||
installed: true,
|
||||
broker: { running: false, error: null },
|
||||
gateway: { running: false, error: null },
|
||||
};
|
||||
|
||||
describe("OmpSharingSettings", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
perms.isAdmin = true;
|
||||
perms.local = false;
|
||||
});
|
||||
|
||||
it("lets an admin turn sharing on and shows it running", async () => {
|
||||
get.mockResolvedValue(off);
|
||||
set.mockResolvedValue({
|
||||
...off,
|
||||
enabled: true,
|
||||
broker: { running: true },
|
||||
gateway: { running: true, port: 4827 },
|
||||
});
|
||||
render(<OmpSharingSettings />);
|
||||
const sw = await screen.findByRole("switch", { name: "Share this server's Oh My Pi login" });
|
||||
fireEvent.click(sw);
|
||||
await waitFor(() => expect(set).toHaveBeenCalledWith(true));
|
||||
expect(await screen.findByTestId("omp-sharing-status")).toHaveTextContent("Running");
|
||||
});
|
||||
|
||||
it("names a daemon's failure", async () => {
|
||||
get.mockResolvedValue({
|
||||
...off,
|
||||
enabled: true,
|
||||
gateway: { running: false, error: "port 4827 is taken" },
|
||||
});
|
||||
render(<OmpSharingSettings />);
|
||||
expect(await screen.findByTestId("omp-sharing-status")).toHaveTextContent("port 4827 is taken");
|
||||
});
|
||||
|
||||
it("renders nothing outside S3, for a non-admin or in local mode", async () => {
|
||||
get.mockResolvedValue({ ...off, available: false });
|
||||
const r = render(<OmpSharingSettings />);
|
||||
await waitFor(() => expect(get).toHaveBeenCalled());
|
||||
expect(r.container).toBeEmptyDOMElement();
|
||||
r.unmount();
|
||||
perms.isAdmin = false;
|
||||
const r2 = render(<OmpSharingSettings />);
|
||||
expect(r2.container).toBeEmptyDOMElement();
|
||||
expect(get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user