mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 02:36:01 -10:00
test(desktop): the mod UI guard, release note and the strip's focus-ring room
This commit is contained in:
1 parent
a062bc13c8
commit
aad4267814
6 files changed
+130
-1
No files matched your search
@@ -144,6 +144,7 @@ jobs:
|
||||
node perf/jump-ends.mjs
|
||||
node perf/modelrows.mjs
|
||||
node perf/explorersize.mjs
|
||||
node perf/mod-ui.mjs
|
||||
node perf/codeeditor-size.mjs
|
||||
node perf/rowhit.mjs
|
||||
node perf/tab-pip.mjs
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
// Claude Code mods in the window: the band a mod draws above the prompt, a question a mod asks, the settings
|
||||
// block (switch, time limit, each mod's state by code) and the install preview's full-trust listing.
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const BASE = process.env.PERF_URL ?? "http://localhost:4399/perf.html";
|
||||
const shotDir = process.env.SHOTS;
|
||||
const fails = [];
|
||||
const check = (name, ok, detail = "") => {
|
||||
console.log(`${ok ? " ok" : "FAIL"} ${name}${detail ? " — " + detail : ""}`);
|
||||
if (!ok) fails.push(name);
|
||||
};
|
||||
|
||||
const band = { kind: "extension_surface", extension: { pluginId: "statusline-clock", surfaceId: "mod-above-prompt", kind: "view", view: { slot: "above_prompt", body: [
|
||||
{ kind: "row", children: [{ kind: "pip", tone: "ok" }, { kind: "text", value: "statusline-clock", tone: "strong" }, { kind: "text", value: "12:30 · 今天还剩 3 个任务", tone: "dim" }, { kind: "button", label: "稍后提醒", actionId: "snooze:5" }] },
|
||||
] } } };
|
||||
const ask = { kind: "extension_surface", extension: { pluginId: "git-guard", surfaceId: "mod-ask", kind: "form", sessionId: "s", generation: 1, form: { title: "git-guard asks", message: "要删除已经合并的分支 old-spike 吗?", fields: [{ key: "answer", label: "回答", kind: "input" }] } } };
|
||||
|
||||
const browser = await chromium.launch();
|
||||
for (const scheme of ["light", "dark"]) {
|
||||
for (const width of [1280, 420]) {
|
||||
const tag = `${scheme} ${width}px`;
|
||||
const page = await browser.newPage({ viewport: { width, height: 1300 }, locale: "zh-CN", colorScheme: scheme, reducedMotion: "reduce" });
|
||||
page.on("pageerror", (e) => fails.push("页面异常: " + e.message));
|
||||
await page.goto(`${BASE}?pref=zh&turns=2`, { waitUntil: "networkidle" });
|
||||
await page.waitForSelector(".app");
|
||||
|
||||
await page.evaluate((ev) => window.__feed(ev), band);
|
||||
await page.getByRole("button", { name: "稍后提醒" }).waitFor();
|
||||
const at = await page.evaluate(() => {
|
||||
const s = document.querySelector(".slotted").getBoundingClientRect();
|
||||
const c = document.querySelector('[aria-label="任务输入"]').getBoundingClientRect();
|
||||
return { above: s.bottom <= c.top + 4, spill: s.right > innerWidth + 1 || document.documentElement.scrollWidth > innerWidth + 1 };
|
||||
});
|
||||
check(`${tag}: the band stands above the text the person types in`, at.above);
|
||||
check(`${tag}: the band does not push the page sideways`, !at.spill);
|
||||
await page.getByRole("button", { name: "稍后提醒" }).focus();
|
||||
check(`${tag}: its button can be reached from the keyboard`, await page.evaluate(() => document.activeElement?.textContent === "稍后提醒"));
|
||||
const ring = await page.evaluate(() => {
|
||||
const b = document.activeElement, rail = document.querySelector(".slotrail");
|
||||
const cs = getComputedStyle(b);
|
||||
const room = Math.min(b.getBoundingClientRect().left - rail.getBoundingClientRect().left, rail.getBoundingClientRect().right - b.getBoundingClientRect().right);
|
||||
return { ring: parseFloat(cs.outlineWidth) + Math.max(0, parseFloat(cs.outlineOffset)), room };
|
||||
});
|
||||
check(`${tag}: the focus ring of the band's button is not cut off by the strip it sits in`, ring.room >= ring.ring - 0.5, `${ring.room}px room for a ${ring.ring}px ring`);
|
||||
await page.evaluate(() => document.activeElement?.blur());
|
||||
if (shotDir) { await page.mouse.move(width - 5, 895); await page.waitForTimeout(300); await page.screenshot({ path: `${shotDir}/${scheme}-${width}-band.png`, clip: { x: 0, y: 760, width, height: 540 } }); }
|
||||
|
||||
await page.evaluate((ev) => window.__feed(ev), ask);
|
||||
await page.waitForSelector('.call[data-k="extension"] .extform');
|
||||
const card = await page.evaluate(() => {
|
||||
const form = document.querySelector(".extform");
|
||||
return { note: form.querySelector(".why")?.textContent ?? "", tag: document.querySelector('.call[data-k="extension"] .tag')?.textContent ?? "" };
|
||||
});
|
||||
check(`${tag}: a mod's question says whose it is and that answering approves nothing`, card.note.includes("git-guard") && card.note.includes("不会批准任何工具调用或权限") && card.tag.includes("git-guard"), card.note);
|
||||
if (shotDir && width === 1280) await page.locator('.call[data-k="extension"]').last().screenshot({ path: `${shotDir}/${scheme}-ask.png` });
|
||||
|
||||
await page.evaluate(() => document.querySelector('[data-action="chrome.settings"]')?.click());
|
||||
await page.waitForSelector(".prefs-nav button");
|
||||
await page.evaluate(() => [...document.querySelectorAll(".prefs-nav button")].find((b) => b.textContent.trim().startsWith("扩展"))?.click());
|
||||
await page.waitForSelector("#set-mods");
|
||||
const off = await page.evaluate(() => {
|
||||
const g = document.getElementById("set-mods");
|
||||
return { checked: g.querySelector('[role="switch"]').getAttribute("aria-checked"), rows: g.querySelectorAll(".modlist li").length, why: [...g.querySelectorAll(".modlist .ds")].filter((e) => e.textContent.includes("没有打开")).length };
|
||||
});
|
||||
check(`${tag}: the switch ships off and every mod says why it is not running`, off.checked === "false" && off.rows === 4 && off.why === 4, `${off.rows} mods, ${off.why} reasons`);
|
||||
await page.locator('#set-mods [role="switch"]').click();
|
||||
await page.waitForFunction(() => document.querySelectorAll("#set-mods .modlist li[data-state='running']").length === 1);
|
||||
const on = await page.evaluate(() => {
|
||||
const g = document.getElementById("set-mods");
|
||||
const r = g.getBoundingClientRect();
|
||||
const states = [...g.querySelectorAll(".modlist li")].map((li) => li.getAttribute("data-state"));
|
||||
return { states, spill: r.right > innerWidth + 1 || g.scrollWidth > g.clientWidth + 1, marks: [...g.querySelectorAll(".modstate")].every((e) => /^\S+\s+\S+/.test(e.textContent.trim())) };
|
||||
});
|
||||
check(`${tag}: each state shows running, disabled, restarting or not running, with a mark as well as a word`, on.states.join() === "running,disabled,restarting,inactive" && on.marks, on.states.join());
|
||||
check(`${tag}: the block does not spill sideways`, !on.spill);
|
||||
if (shotDir) await page.locator("#set-mods").screenshot({ path: `${shotDir}/${scheme}-${width}-settings.png` });
|
||||
|
||||
if (width === 1280) {
|
||||
await page.locator('[data-action="extensions.add"]').first().click();
|
||||
const source = page.locator("textarea, input").filter({ hasNot: page.locator("[aria-label]") }).last();
|
||||
await page.locator('textarea[placeholder*="github.com"], input[placeholder*="github.com"]').first().fill("https://github.com/acme/claude-code mod git-guard");
|
||||
await page.locator('[data-action="extensions.inspect"]').first().click();
|
||||
await page.waitForSelector(".modpreview", { timeout: 10000 });
|
||||
const prev = await page.evaluate(() => {
|
||||
const m = document.querySelector(".modpreview");
|
||||
const text = m.textContent;
|
||||
return { trust: text.includes("完全信任"), static: text.includes("不是保证"), imports: text.includes("看不见"), unserved: text.includes("本应用不会触发的事件"), hash: !!m.querySelector("[title]") };
|
||||
});
|
||||
check(`${tag}: the install preview states full trust, that it is a static listing, hidden imports and unserved events`, prev.trust && prev.static && prev.imports && prev.unserved && prev.hash, JSON.stringify(prev));
|
||||
if (shotDir) await page.locator(".cand").first().screenshot({ path: `${shotDir}/${scheme}-preview.png` });
|
||||
}
|
||||
await page.close();
|
||||
}
|
||||
}
|
||||
await browser.close();
|
||||
if (fails.length) {
|
||||
console.error(`\n${fails.length} failed: ${fails.join("; ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("\nall passed");
|
||||
@@ -1,4 +1,5 @@
|
||||
import { download } from "./download";
|
||||
import { mockModPlan } from "./mock_mods";
|
||||
import type { PluginExport, PluginInstallRequest, PluginPackage, PluginPlan, ScopeLayer, SkillCatalog, SkillEntry, SkillUpdatePlan } from "./port";
|
||||
import { MockMarket } from "./mock_market";
|
||||
|
||||
@@ -102,6 +103,7 @@ export class MockExtensions extends MockMarket {
|
||||
// a hook and connects a server is what the confirmation page exists for.
|
||||
async planPlugin(req: PluginInstallRequest): Promise<PluginPlan> {
|
||||
const source = req.source;
|
||||
if (/\bmod\b/i.test(source)) return mockModPlan(source);
|
||||
return {
|
||||
ok: true, status: "planned", applied: false, source, planId: "high:sha256:mock",
|
||||
actions: [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ModStatus } from "./boundary";
|
||||
import type { ModPreview, PluginPlan } from "./plugin";
|
||||
|
||||
const RUNTIME = { kind: "electron", path: "/Applications/Reasonix Studio.app/Contents/MacOS/Reasonix Studio", version: "v22.12.0" };
|
||||
|
||||
@@ -14,3 +15,26 @@ export function mockMods(enabled: boolean): ModStatus[] {
|
||||
{ plugin: names[3], state: "inactive", code: "mod.runtime_missing", restarts: 0, events: ["turn.complete"], unserved: [] },
|
||||
];
|
||||
}
|
||||
|
||||
export const MOCK_MOD_PREVIEW: ModPreview = {
|
||||
plugin: "git-guard", module: "hooks/index.js", sha256: "9f2c41b07ad35e8c61f0aa42d9be3377c0e1145f8a6d2b9c4e7710d3a5b8c6e2", listed: true,
|
||||
events: [{ name: "tool.check" }, { name: "tool.after" }, { name: "turn.complete" }, { name: "x", dynamic: true }],
|
||||
api: [{ namespace: "process", method: "run" }, { namespace: "fs", method: "readFile" }, { namespace: "ui", method: "toast" }],
|
||||
imports: [{ spec: "./lib/rules.js", kind: "static" }, { spec: "minimatch", kind: "require" }],
|
||||
findings: [{ kind: "computed_member" }],
|
||||
unserved: ["tool.rewrite"],
|
||||
};
|
||||
|
||||
/** The plan a source naming a mod gets: one package whose action carries the
|
||||
* preview of its hooks module. */
|
||||
export function mockModPlan(source: string): PluginPlan {
|
||||
return {
|
||||
ok: true, status: "planned", applied: false, source, planId: "high:sha256:mockmod",
|
||||
actions: [{
|
||||
kind: "plugin", action: "install_plugin_package", status: "planned", riskLevel: "high", name: "git-guard", version: "1.2.0", source,
|
||||
target: "~/.reasonix/plugins/git-guard", manifestKind: "claude-code", skills: [], skillCount: 0, commandCount: 0, hookCount: 0,
|
||||
riskReasons: ["FULL TRUST: ships a Claude Code mod whose code runs on this machine"], mod: MOCK_MOD_PREVIEW,
|
||||
}],
|
||||
next: "Review the plan, then install.",
|
||||
};
|
||||
}
|
||||
@@ -4124,7 +4124,7 @@
|
||||
|
||||
.slotrail {
|
||||
display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
|
||||
padding: 4px 2px 6px; max-height: 84px; overflow-y: auto;
|
||||
padding: 4px 5px 6px; max-height: 84px; overflow-y: auto;
|
||||
}
|
||||
.slotrail .xview { flex-direction: row; align-items: center; gap: 8px }
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
- 新增:设置里可以打开「运行 Claude Code mod」,列出每个 mod 的状态与原因;安装预览写明完全信任与静态清单;mod 可在输入框上方画一条带。关联 #13694。
|
||||
- Added: Settings can turn on Run Claude Code mods and lists each mod's state and why; the install preview states full trust and a static listing; a mod can draw a band above the prompt. Refs #13694.
|
||||
Reference in new issue
Block a user