mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
Progressive disclosure, final stage: with lazyTools on (default), the eleven browser_* tool schemas stay out of the system prompt — the browser-skill catalog entry is the sole advertisement. One successful skill invocation (model tool call via tools/result, or a /browser-skill user gesture via the session append feed) registers the suite for the process lifetime, idempotent on repeats; session resume is covered by scanning durable events for a past tool/call+tool/result pair (or skill-invocation message) on session entry and at apply time. lazyTools: false keeps the legacy always-on registration. Investigation (recorded in the ticket): tools/result carries normalized name/arguments with an isError discriminant; dsh 0.1 has no official global visibility switch (ctx.tools.restrict is agent-scoped and throws from a plain context), making conditional registration the intended pattern; per-step assembly picks the new suite up on the next step, and visibility changes are first-class inputs of tool-skill's catalog digest. registerTools now returns the combined disposer via a prototype-preserving tracking overlay; the skill prelude tells the model the browser_* tools unlock on skill load. Tests: +8 (hidden-before/reveal/idempotent/gesture/history hit+miss/dispose/ scanner pairing/two-state apply wiring). 124/124 green.
95 lines
3.4 KiB
TypeScript
95 lines
3.4 KiB
TypeScript
// Unload-cleanup ownership discipline: dispose must stop exactly the sessions
|
|
// this plugin created — never referenced/foreign sessions on the shared
|
|
// daemon — and a stale handle (already stopped elsewhere, daemon restarted)
|
|
// must not abort the remaining stops.
|
|
|
|
import type { ToolDefinition, ToolRunContext } from "@deepseek-ai/dsh-tools";
|
|
import { describe, expect, it } from "vitest";
|
|
import { apply } from "../src/index";
|
|
import type { BskRunOptions, BskRunResult } from "../src/runner";
|
|
|
|
interface FakeCall {
|
|
args: string[];
|
|
}
|
|
|
|
function ok(payload: unknown): BskRunResult {
|
|
return { code: 0, stdout: JSON.stringify(payload), stderr: "", timedOut: false, aborted: false };
|
|
}
|
|
|
|
function fail(message: string): BskRunResult {
|
|
return {
|
|
code: 4,
|
|
stdout: JSON.stringify({ code: "session_not_found", message }),
|
|
stderr: "",
|
|
timedOut: false,
|
|
aborted: false,
|
|
};
|
|
}
|
|
|
|
function makeExec(): ToolRunContext {
|
|
return {
|
|
callId: "t",
|
|
name: "t",
|
|
signal: new AbortController().signal,
|
|
} as unknown as ToolRunContext;
|
|
}
|
|
|
|
describe("dispose cleanup ownership", () => {
|
|
it("stops only plugin-owned sessions and tolerates stale handles", async () => {
|
|
const calls: FakeCall[] = [];
|
|
const startReplies = [
|
|
{ session_id: "own1", browser_instance_id: "b1" },
|
|
{ session_id: "own2", browser_instance_id: "b1" },
|
|
];
|
|
const runner = {
|
|
async run(args: string[], _options: BskRunOptions = {}): Promise<BskRunResult> {
|
|
calls.push({ args });
|
|
const joined = args.join(" ");
|
|
if (joined.startsWith("status")) return ok({});
|
|
if (joined.startsWith("session start")) return ok(startReplies.shift());
|
|
if (joined.startsWith("snapshot")) {
|
|
return ok({ text: "x", ref_count: 1, tab_id: 7, truncated: false });
|
|
}
|
|
if (joined === "session stop own1") return ok({});
|
|
if (joined === "session stop own2") {
|
|
// Stale handle: already stopped by someone else / daemon restarted.
|
|
return fail("session not found");
|
|
}
|
|
return fail(`unexpected command: ${joined}`);
|
|
},
|
|
killAll() {},
|
|
killFor: () => 0,
|
|
};
|
|
const tools = new Map<string, ToolDefinition>();
|
|
const disposers: Array<() => Promise<void>> = [];
|
|
const ctx = {
|
|
tools: { register: (def: ToolDefinition) => tools.set(def.name, def) },
|
|
get: () => undefined,
|
|
// No webServer in this harness: the inject callback never runs.
|
|
inject: () => {},
|
|
effect: (fn: () => () => Promise<void>) => {
|
|
disposers.push(fn());
|
|
},
|
|
};
|
|
apply(ctx as never, { maxSessions: 5, lazyTools: false }, { runnerFactory: () => runner });
|
|
|
|
const start = tools.get("browser_session_start");
|
|
const snapshot = tools.get("browser_snapshot");
|
|
if (start === undefined || snapshot === undefined) throw new Error("tools not registered");
|
|
await start.execute({}, makeExec());
|
|
await start.execute({}, makeExec());
|
|
// A foreign session cannot even be referenced any more.
|
|
await expect(snapshot.execute({ session: "ext9" }, makeExec())).rejects.toThrow(
|
|
/does not belong to this plugin/,
|
|
);
|
|
|
|
await Promise.all(disposers.map((dispose) => dispose()));
|
|
|
|
const stops = calls
|
|
.map((call) => call.args.join(" "))
|
|
.filter((joined) => joined.startsWith("session stop"));
|
|
expect(stops.sort()).toEqual(["session stop own1", "session stop own2"]);
|
|
expect(stops.some((joined) => joined.includes("ext9"))).toBe(false);
|
|
});
|
|
});
|