mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
fix(dsh-plugin-browserskill): address review findings (leaks, trust fence, queue race)
Blocking: - B1: delete screenshot scratch PNGs — observation frames reuse one fixed per-session path and unlink after every read (finally); browser_screenshot unlinks once the bytes are in the attachment store (kept only when the file itself is the model-facing artifact). - B2: replicate dsh's browser-trust fence on /bsk-observation/*: loopback Host only, Origin must match Host, sec-fetch-site: cross-site refused, POST requires application/json; README documents the loopback trust premise and the 0.0.0.0 warning. Major: - M1: KeyedExecutor tail chains previous+task (allSettled) so a task aborted while queued cannot release the next one into the running session. - M2: client tracks the live frame per session — replacing/removing/reset revokes the old blob URL at once; loads settling after replacement never resurrect it. Minor: drop replaced thumbRefs (+on remove); beginAction fires inside the queue (no label overwrite while queued, no idle flash); half-initialized session cleanup stops through the queue with one retry; SSE (re)open refetches /state (+ snapshot flag renamed subscribed); install probe uses --version (no daemon spawn); emulate mobile documents the width+height requirement (the daemon refuses it alone — verified). Nit: single BskRunOptions declaration; tails map entry dropped on drain; SSE cleanup on res close; popOut catches requestWindow rejections; resize handle gains keyboard control (arrows, 16px steps, aria value attrs). Tests: +14 (fence rules, scratch lifecycle, queue race, blob revocation, instrumentation timing, SSE resync). 113/113 green.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
// KeyedExecutor: per-key FIFO guarantees, including the abort-while-queued
|
||||
// race that used to release the tail early (letting a later task overlap the
|
||||
// one still running on the same session).
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { KeyedExecutor } from "../src/queue";
|
||||
|
||||
describe("KeyedExecutor", () => {
|
||||
it("runs same-key tasks in FIFO order", async () => {
|
||||
const queue = new KeyedExecutor();
|
||||
const order: string[] = [];
|
||||
const gate = new Promise<void>((resolve) => setTimeout(resolve, 5));
|
||||
await Promise.all([
|
||||
queue.run("k", async () => {
|
||||
await gate;
|
||||
order.push("a");
|
||||
}),
|
||||
queue.run("k", async () => {
|
||||
order.push("b");
|
||||
}),
|
||||
queue.run("k", async () => {
|
||||
order.push("c");
|
||||
}),
|
||||
]);
|
||||
expect(order).toEqual(["a", "b", "c"]);
|
||||
});
|
||||
|
||||
it("a task aborted while queued does not let the next one overlap the runner", async () => {
|
||||
const queue = new KeyedExecutor();
|
||||
let releaseA!: () => void;
|
||||
const aRuns = new Promise<void>((resolve) => {
|
||||
releaseA = resolve;
|
||||
});
|
||||
const events: string[] = [];
|
||||
|
||||
// A holds the session until releaseA.
|
||||
const a = queue.run("s1", async () => {
|
||||
events.push("a:start");
|
||||
await aRuns;
|
||||
events.push("a:end");
|
||||
});
|
||||
// B is queued behind A and aborted before A finishes.
|
||||
const abortB = new AbortController();
|
||||
const b = queue.run(
|
||||
"s1",
|
||||
async () => {
|
||||
events.push("b:start");
|
||||
},
|
||||
abortB.signal,
|
||||
);
|
||||
// C is queued behind B.
|
||||
const c = queue.run("s1", async () => {
|
||||
events.push("c:start");
|
||||
});
|
||||
|
||||
abortB.abort();
|
||||
await expect(b).rejects.toMatchObject({ name: "AbortError" });
|
||||
// Give C every chance to (wrongly) start while A is still in flight.
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
expect(events).toEqual(["a:start"]);
|
||||
|
||||
releaseA();
|
||||
await a;
|
||||
await c;
|
||||
expect(events).toEqual(["a:start", "a:end", "c:start"]);
|
||||
});
|
||||
|
||||
it("a failed task never strands the queue", async () => {
|
||||
const queue = new KeyedExecutor();
|
||||
await expect(
|
||||
queue.run("k", async () => {
|
||||
throw new Error("boom");
|
||||
}),
|
||||
).rejects.toThrow("boom");
|
||||
await expect(queue.run("k", async () => "ok")).resolves.toBe("ok");
|
||||
});
|
||||
|
||||
it("drops the tail entry once the queue drains", async () => {
|
||||
const queue = new KeyedExecutor();
|
||||
const internals = queue as unknown as { tails: Map<string, Promise<void>> };
|
||||
await queue.run("k", async () => {});
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
expect(internals.tails.size).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user