mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 10:55:31 -06:00
Merge gateway profile foundation into provider profiles UI stack
This commit is contained in:
@@ -1,13 +1,11 @@
|
||||
// Verifies config IO compatibility loading and migration behavior.
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { normalizeCompatibilityConfigValues } from "../commands/doctor/shared/legacy-config-core-migrate.js";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { VERSION } from "../version.js";
|
||||
import { createConfigIO } from "./io.js";
|
||||
import { normalizeExecSafeBinProfilesInConfig } from "./normalize-exec-safe-bin.js";
|
||||
import { withTempHome } from "./test-helpers.js";
|
||||
import type { OpenClawConfig } from "./types.openclaw.js";
|
||||
|
||||
async function writeConfig(
|
||||
home: string,
|
||||
@@ -30,29 +28,6 @@ function createIoForHome(home: string, env: NodeJS.ProcessEnv = {} as NodeJS.Pro
|
||||
}
|
||||
|
||||
describe("config io paths", () => {
|
||||
let whatsappSharedAccessDefaults: unknown;
|
||||
|
||||
beforeAll(() => {
|
||||
const migrated = normalizeCompatibilityConfigValues({
|
||||
channels: {
|
||||
whatsapp: {
|
||||
enabled: true,
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["+15550001111"],
|
||||
groupPolicy: "open",
|
||||
groupAllowFrom: [],
|
||||
accounts: {
|
||||
work: {
|
||||
enabled: true,
|
||||
authDir: "/tmp/wa-work",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig);
|
||||
whatsappSharedAccessDefaults = migrated.config.channels?.whatsapp?.accounts?.default;
|
||||
});
|
||||
|
||||
it("uses ~/.openclaw/openclaw.json when config exists", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configPath = await writeConfig(home, ".openclaw", 19001);
|
||||
@@ -335,13 +310,4 @@ describe("config io paths", () => {
|
||||
});
|
||||
expect(cfg.agents?.list?.[0]?.tools?.exec?.safeBinTrustedDirs).toEqual(["/ops/bin"]);
|
||||
});
|
||||
|
||||
it("moves WhatsApp shared access defaults into accounts.default during runtime compat", () => {
|
||||
expect(whatsappSharedAccessDefaults).toEqual({
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["+15550001111"],
|
||||
groupPolicy: "open",
|
||||
groupAllowFrom: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
const suite = createControlUiE2eSuite({
|
||||
name: "chat Markdown alignment",
|
||||
unavailableMessage: (executablePath) => `Playwright Chromium is unavailable at ${executablePath}`,
|
||||
});
|
||||
|
||||
suite.define(() => {
|
||||
it("aligns Markdown markers and text while containing expanded disclosures", async () => {
|
||||
const longJson = JSON.stringify(
|
||||
Object.fromEntries(
|
||||
Array.from({ length: 40 }, (_, index) => [`field-${index + 1}`, index + 1]),
|
||||
),
|
||||
null,
|
||||
2,
|
||||
);
|
||||
await suite.withPage(
|
||||
{
|
||||
colorScheme: "light",
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
viewport: { height: 800, width: 1180 },
|
||||
},
|
||||
async ({ page }) => {
|
||||
await installMockGateway(page, {
|
||||
historyMessages: [
|
||||
{
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: [
|
||||
"## Alignment check",
|
||||
"",
|
||||
"- Bullet item",
|
||||
"",
|
||||
"1. Numbered item",
|
||||
"",
|
||||
"- [ ] Unchecked task",
|
||||
"",
|
||||
"<details open>",
|
||||
"<summary>More details</summary>",
|
||||
"",
|
||||
"Disclosure body",
|
||||
"</details>",
|
||||
"",
|
||||
"<details>",
|
||||
"<summary>Collapsed details</summary>",
|
||||
"Hidden body",
|
||||
"</details>",
|
||||
"",
|
||||
"```json",
|
||||
longJson,
|
||||
"```",
|
||||
].join("\n"),
|
||||
},
|
||||
],
|
||||
role: "assistant",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await page.goto(`${suite.server.baseUrl}chat`);
|
||||
const markdown = page.locator(".chat-group.assistant .chat-text", {
|
||||
hasText: "Alignment check",
|
||||
});
|
||||
await markdown.waitFor();
|
||||
|
||||
const geometry = await markdown.evaluate((root) => {
|
||||
const textRect = (selector: string) => {
|
||||
const element = root.querySelector(selector);
|
||||
if (!element) {
|
||||
throw new Error(`Missing element for ${selector}`);
|
||||
}
|
||||
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
||||
let text = walker.nextNode();
|
||||
while (text && !text.textContent?.trim()) {
|
||||
text = walker.nextNode();
|
||||
}
|
||||
if (!text) {
|
||||
throw new Error(`Missing text for ${selector}`);
|
||||
}
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(text);
|
||||
return range.getBoundingClientRect();
|
||||
};
|
||||
const checkbox = root.querySelector(".task-list-item-checkbox");
|
||||
const details = root.querySelector("details:not(.json-collapse)[open]");
|
||||
const summary = root.querySelector("details:not(.json-collapse)[open] > summary");
|
||||
if (!checkbox || !details || !summary) {
|
||||
throw new Error("Missing task-list or disclosure markup");
|
||||
}
|
||||
const detailsStyle = getComputedStyle(details);
|
||||
const summaryStyle = getComputedStyle(summary);
|
||||
const checkboxRect = checkbox.getBoundingClientRect();
|
||||
const taskTextRect = textRect(".task-list-item");
|
||||
const collapsedSummary = root.querySelector(
|
||||
"details:not(.json-collapse):not([open]) > summary",
|
||||
);
|
||||
const jsonCollapse = root.querySelector("details.json-collapse");
|
||||
const jsonSummary = root.querySelector("details.json-collapse > summary");
|
||||
const jsonCopy = root.querySelector("details.json-collapse .code-block-copy");
|
||||
if (!collapsedSummary || !jsonCollapse || !jsonSummary || !jsonCopy) {
|
||||
throw new Error("Missing authored or JSON disclosure markup");
|
||||
}
|
||||
const closedChevronStyle = getComputedStyle(collapsedSummary, "::after");
|
||||
const collapsedSummaryRect = collapsedSummary.getBoundingClientRect();
|
||||
const collapsedSummaryTextRect = textRect(
|
||||
"details:not(.json-collapse):not([open]) > summary",
|
||||
);
|
||||
const jsonCollapseStyle = getComputedStyle(jsonCollapse);
|
||||
const jsonSummaryStyle = getComputedStyle(jsonSummary);
|
||||
return {
|
||||
bodyTextX: textRect("details:not(.json-collapse) > p").x,
|
||||
borderInlineStartWidth: detailsStyle.borderInlineStartWidth,
|
||||
bulletTextX: textRect("ul:not(.contains-task-list) > li").x,
|
||||
checkboxGap: taskTextRect.x - checkboxRect.right,
|
||||
checkboxLineCenterDelta:
|
||||
checkboxRect.y + checkboxRect.height / 2 - (taskTextRect.y + taskTextRect.height / 2),
|
||||
checkboxSize: checkboxRect.width,
|
||||
chevronClosedTransform: closedChevronStyle.transform,
|
||||
chevronInlineEnd: closedChevronStyle.insetInlineEnd,
|
||||
chevronTransitionDuration: closedChevronStyle.transitionDuration,
|
||||
chevronWidth: closedChevronStyle.width,
|
||||
collapsedSummaryPaddingInlineEnd: getComputedStyle(collapsedSummary).paddingInlineEnd,
|
||||
collapsedSummaryTextRight: collapsedSummaryTextRect.right,
|
||||
collapsedSummaryTextX: collapsedSummaryTextRect.x,
|
||||
collapsedSummaryRight: collapsedSummaryRect.right,
|
||||
detailsX: details.getBoundingClientRect().x,
|
||||
jsonBorderInlineStartWidth: jsonCollapseStyle.borderInlineStartWidth,
|
||||
jsonCopyFloat: getComputedStyle(jsonCopy).float,
|
||||
jsonDetailsX: jsonCollapse.getBoundingClientRect().x,
|
||||
jsonSummaryDisplay: jsonSummaryStyle.display,
|
||||
jsonSummaryPaddingInlineStart: jsonSummaryStyle.paddingInlineStart,
|
||||
numberedTextX: textRect("ol > li").x,
|
||||
rootX: root.getBoundingClientRect().x,
|
||||
summaryMarginBottom: summaryStyle.marginBottom,
|
||||
summaryTextX: textRect("details[open] > summary").x,
|
||||
taskTextX: taskTextRect.x,
|
||||
};
|
||||
});
|
||||
|
||||
const textStarts = [
|
||||
geometry.bulletTextX,
|
||||
geometry.numberedTextX,
|
||||
geometry.taskTextX,
|
||||
geometry.summaryTextX,
|
||||
geometry.collapsedSummaryTextX,
|
||||
];
|
||||
expect(Math.max(...textStarts) - Math.min(...textStarts)).toBeLessThanOrEqual(1);
|
||||
expect(geometry.checkboxGap).toBeGreaterThanOrEqual(7);
|
||||
expect(geometry.checkboxGap).toBeLessThanOrEqual(9);
|
||||
expect(Math.abs(geometry.checkboxLineCenterDelta)).toBeLessThanOrEqual(1);
|
||||
expect(geometry.checkboxSize).toBe(16);
|
||||
expect(geometry.bodyTextX - geometry.rootX).toBeGreaterThanOrEqual(28);
|
||||
expect(geometry.detailsX).toBeGreaterThan(geometry.rootX);
|
||||
expect(geometry.detailsX).toBeLessThan(geometry.bodyTextX);
|
||||
expect(Number.parseFloat(geometry.borderInlineStartWidth)).toBeGreaterThan(0);
|
||||
expect(Number.parseFloat(geometry.summaryMarginBottom)).toBeGreaterThan(0);
|
||||
expect(Number.parseFloat(geometry.chevronWidth)).toBe(16);
|
||||
expect(Number.parseFloat(geometry.chevronInlineEnd)).toBe(0);
|
||||
expect(Number.parseFloat(geometry.collapsedSummaryPaddingInlineEnd)).toBeGreaterThanOrEqual(
|
||||
24,
|
||||
);
|
||||
expect(geometry.collapsedSummaryRight - geometry.collapsedSummaryTextRight).toBeGreaterThan(
|
||||
24,
|
||||
);
|
||||
expect(geometry.chevronTransitionDuration).not.toBe("0s");
|
||||
expect(Math.abs(geometry.jsonDetailsX - geometry.rootX)).toBeLessThanOrEqual(1);
|
||||
expect(Number.parseFloat(geometry.jsonBorderInlineStartWidth)).toBe(0);
|
||||
expect(geometry.jsonSummaryDisplay).toBe("list-item");
|
||||
expect(Number.parseFloat(geometry.jsonSummaryPaddingInlineStart)).toBe(8);
|
||||
expect(geometry.jsonCopyFloat).toBe("right");
|
||||
|
||||
const collapsedSummary = markdown.locator("summary", { hasText: "Collapsed details" });
|
||||
await collapsedSummary.click();
|
||||
await expect
|
||||
.poll(() =>
|
||||
collapsedSummary.evaluate((summary) => getComputedStyle(summary, "::after").transform),
|
||||
)
|
||||
.not.toBe(geometry.chevronClosedTransform);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves the shared Markdown gutter in RTL transcripts", async () => {
|
||||
await suite.withPage(
|
||||
{
|
||||
colorScheme: "light",
|
||||
locale: "ar",
|
||||
serviceWorkers: "block",
|
||||
viewport: { height: 800, width: 1180 },
|
||||
},
|
||||
async ({ page }) => {
|
||||
await installMockGateway(page, {
|
||||
historyMessages: [
|
||||
{
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: [
|
||||
"## فحص المحاذاة",
|
||||
"",
|
||||
"- عنصر نقطي",
|
||||
"",
|
||||
"1. عنصر مرقم",
|
||||
"",
|
||||
"- [ ] مهمة غير مكتملة",
|
||||
"",
|
||||
"<details open>",
|
||||
"<summary>تفاصيل إضافية</summary>",
|
||||
"",
|
||||
"محتوى التفاصيل",
|
||||
"</details>",
|
||||
].join("\n"),
|
||||
},
|
||||
],
|
||||
role: "assistant",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await page.goto(`${suite.server.baseUrl}chat`);
|
||||
const markdown = page.locator(".chat-group.assistant .chat-text[dir='rtl']", {
|
||||
hasText: "فحص المحاذاة",
|
||||
});
|
||||
await markdown.waitFor();
|
||||
|
||||
const geometry = await markdown.evaluate((root) => {
|
||||
const textRight = (selector: string) => {
|
||||
const element = root.querySelector(selector);
|
||||
if (!element) {
|
||||
throw new Error(`Missing element for ${selector}`);
|
||||
}
|
||||
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
||||
let text = walker.nextNode();
|
||||
while (text && !text.textContent?.trim()) {
|
||||
text = walker.nextNode();
|
||||
}
|
||||
if (!text) {
|
||||
throw new Error(`Missing text for ${selector}`);
|
||||
}
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(text);
|
||||
return range.getBoundingClientRect().right;
|
||||
};
|
||||
const checkbox = root.querySelector(".task-list-item-checkbox");
|
||||
const task = root.querySelector(".task-list-item");
|
||||
const unorderedList = root.querySelector("ul:not(.contains-task-list)");
|
||||
const orderedList = root.querySelector("ol");
|
||||
const summary = root.querySelector("details:not(.json-collapse) > summary");
|
||||
if (!checkbox || !task || !unorderedList || !orderedList || !summary) {
|
||||
throw new Error("Missing RTL Markdown geometry");
|
||||
}
|
||||
const checkboxRect = checkbox.getBoundingClientRect();
|
||||
return {
|
||||
checkboxGap: checkboxRect.left - textRight(".task-list-item"),
|
||||
chevronInlineEnd: getComputedStyle(summary, "::after").insetInlineEnd,
|
||||
orderedPaddingInlineStart: getComputedStyle(orderedList).paddingInlineStart,
|
||||
textStarts: [
|
||||
textRight("ul:not(.contains-task-list) > li"),
|
||||
textRight("ol > li"),
|
||||
textRight(".task-list-item"),
|
||||
textRight("details:not(.json-collapse) > summary"),
|
||||
],
|
||||
unorderedPaddingInlineStart: getComputedStyle(unorderedList).paddingInlineStart,
|
||||
};
|
||||
});
|
||||
|
||||
expect(
|
||||
Math.max(...geometry.textStarts) - Math.min(...geometry.textStarts),
|
||||
).toBeLessThanOrEqual(1);
|
||||
expect(Number.parseFloat(geometry.unorderedPaddingInlineStart)).toBe(32);
|
||||
expect(Number.parseFloat(geometry.orderedPaddingInlineStart)).toBe(32);
|
||||
expect(geometry.checkboxGap).toBeGreaterThanOrEqual(7);
|
||||
expect(geometry.checkboxGap).toBeLessThanOrEqual(9);
|
||||
expect(Number.parseFloat(geometry.chevronInlineEnd)).toBe(0);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
+88
-14
@@ -137,24 +137,103 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chat-text :where(ul, ol) {
|
||||
padding-left: 1.2em;
|
||||
:is(.chat-text, .chat-thinking) {
|
||||
--chat-markdown-indent: var(--space-7);
|
||||
--chat-markdown-marker-gap: var(--space-2);
|
||||
}
|
||||
|
||||
:is(.chat-text, .chat-thinking) :where(ul, ol) {
|
||||
padding-inline-start: var(--chat-markdown-indent);
|
||||
}
|
||||
|
||||
:is(.chat-text, .chat-thinking) :where(details:not(.json-collapse)) {
|
||||
margin-inline-start: var(--space-3);
|
||||
padding-inline-start: calc(var(--chat-markdown-indent) - var(--space-3));
|
||||
}
|
||||
|
||||
.chat-text :where(li + li) {
|
||||
margin-top: 0.4em;
|
||||
}
|
||||
|
||||
/* Hide default marker only for unordered task lists; ordered lists keep numbers */
|
||||
.chat-text :where(ul > .task-list-item),
|
||||
.chat-thinking :where(ul > .task-list-item) {
|
||||
/* Keep bullets, checkboxes, and numbers in one marker column while their text
|
||||
shares the same start edge. */
|
||||
:is(.chat-text, .chat-thinking) :where(ul > li:not(.task-list-item))::marker {
|
||||
content: "";
|
||||
}
|
||||
|
||||
:is(.chat-text, .chat-thinking) :where(ul > .task-list-item) {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.chat-text :where(.task-list-item-checkbox),
|
||||
.chat-thinking :where(.task-list-item-checkbox) {
|
||||
margin-right: 0.4em;
|
||||
vertical-align: middle;
|
||||
:is(.chat-text, .chat-thinking) :where(ol > li)::marker {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
:is(.chat-text, .chat-thinking) :where(ul > li) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:is(.chat-text, .chat-thinking) :where(ul > li:not(.task-list-item))::before {
|
||||
position: absolute;
|
||||
inset-inline-end: calc(100% + var(--chat-markdown-marker-gap));
|
||||
content: "•";
|
||||
inset-block-start: 0;
|
||||
inline-size: calc(var(--chat-markdown-indent) - var(--chat-markdown-marker-gap));
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
/* Authored disclosures align with list text but place their affordance at the
|
||||
row edge; JSON code blocks retain their separate compact presentation. */
|
||||
:is(.chat-text, .chat-thinking) :where(details:not(.json-collapse) > summary) {
|
||||
position: relative;
|
||||
padding-inline-end: var(--space-7);
|
||||
list-style: none;
|
||||
cursor: var(--cursor-action);
|
||||
}
|
||||
|
||||
:is(.chat-text, .chat-thinking)
|
||||
:where(details:not(.json-collapse) > summary)::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:is(.chat-text, .chat-thinking) :where(details:not(.json-collapse) > summary)::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
inset-block-start: calc((1lh - var(--space-4)) / 2);
|
||||
inset-inline-end: 0;
|
||||
inline-size: var(--space-4);
|
||||
block-size: var(--space-4);
|
||||
background: var(--select-chevron) center / var(--space-4) no-repeat;
|
||||
transform: rotate(-90deg);
|
||||
transition: transform var(--duration-normal) var(--ease-out);
|
||||
}
|
||||
|
||||
:is(.chat-text, .chat-thinking) :where(details:not(.json-collapse)[open] > summary)::after {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
:is(.chat-text, .chat-thinking) :where(.task-list-item-checkbox) {
|
||||
position: absolute;
|
||||
inset-block-start: calc((1lh - var(--space-4)) / 2);
|
||||
inset-inline-end: calc(100% + var(--chat-markdown-marker-gap));
|
||||
inline-size: var(--space-4);
|
||||
block-size: var(--space-4);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:is(.chat-text, .chat-thinking) :where(details:not(.json-collapse)[open]) {
|
||||
padding-inline-start: calc(var(--chat-markdown-indent) - var(--space-3) - 1px);
|
||||
border-inline-start: 1px solid var(--border);
|
||||
}
|
||||
|
||||
:is(.chat-text, .chat-thinking) :where(details:not(.json-collapse)[open] > summary) {
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:is(.chat-text, .chat-thinking) :where(details:not(.json-collapse) > summary)::after {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-text :where(a) {
|
||||
@@ -428,11 +507,6 @@
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.chat-text[dir="rtl"] :where(ul, ol) {
|
||||
padding-left: 0;
|
||||
padding-right: 1.5em;
|
||||
}
|
||||
|
||||
.chat-text[dir="rtl"] :where(blockquote) {
|
||||
border-left: none;
|
||||
border-right: 3px solid var(--border);
|
||||
|
||||
Reference in New Issue
Block a user