mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
feat(dsh-plugin-browserskill): publish the BSK skill through ctx.skills
The plugin exposed only tools; the BrowserSkill agent skill never reached the harness's <available_skills> catalog. Register it as an embedded runtime skill (progressive disclosure: catalog entry resident, body on skill-tool invocation) via the official ctx.skills seam — silent no-op when the composition lacks it. Content is build-time assembled so there is one source of truth: skill/prelude.md (committed, dsh-specific: tool<->CLI map, owned-session semantics, plugin-only overrides) + the canonical repo-root skill/SKILL.md verbatim (the same file crates/bsk-cli/build.rs mirrors; frontmatter stripped, name/description supplied by the registration). Generated into src/skill-content.generated.ts (gitignored) — registration and every pre-step snapshot are pure in-memory reads: no disk, no process, no daemon. Scripts: build:skill chained into build/test/typecheck (same pattern as build:css); biome excludes the generated artifacts. Tests: +3 (catalog content, in-memory weight, silent degradation). 116/116 green.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
// BSK skill injection: registration wiring, catalog content, progressive-load
|
||||
// weight, and silent degradation without the skill seam.
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { registerBskSkill } from "../src/skill";
|
||||
|
||||
function fakeCtx(skills?: unknown) {
|
||||
return { get: (key: string) => (key === "skills" ? skills : undefined) } as never;
|
||||
}
|
||||
|
||||
describe("registerBskSkill", () => {
|
||||
it("registers the catalog entry with the assembled body", () => {
|
||||
const registrations: { skill: Record<string, unknown>; disposer: () => void }[] = [];
|
||||
const skills = {
|
||||
register(skill: Record<string, unknown>) {
|
||||
const disposer = () => {
|
||||
skill.__disposed = true;
|
||||
};
|
||||
registrations.push({ skill, disposer });
|
||||
return disposer;
|
||||
},
|
||||
};
|
||||
const unregister = registerBskSkill(fakeCtx(skills));
|
||||
expect(registrations).toHaveLength(1);
|
||||
const skill = registrations[0].skill;
|
||||
expect(skill.name).toBe("browser-skill");
|
||||
expect(typeof skill.description).toBe("string");
|
||||
expect(String(skill.description)).toContain("browser_*");
|
||||
// body = dsh prelude + canonical CLI skill body
|
||||
const content = String(skill.content);
|
||||
expect(content).toContain("browser_observe");
|
||||
expect(content).toContain("Owned sessions only");
|
||||
expect(content).toContain("Mandatory workflow");
|
||||
expect(content).toContain("Refs invalidate after navigation");
|
||||
expect(content.length).toBeGreaterThan(10_000);
|
||||
expect(skill.source).toBe("bundled");
|
||||
// the original YAML frontmatter must not leak into the body
|
||||
expect(content.includes("name: browser-skill\ndescription:")).toBe(false);
|
||||
// disposer passthrough
|
||||
unregister();
|
||||
expect(skill.__disposed).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps one in-memory copy: repeated reads share the same content", () => {
|
||||
let captured: Record<string, unknown> | undefined;
|
||||
const skills = {
|
||||
register(skill: Record<string, unknown>) {
|
||||
captured = skill;
|
||||
return () => {};
|
||||
},
|
||||
};
|
||||
registerBskSkill(fakeCtx(skills));
|
||||
// Pre-step catalog snapshots re-read the registration: the body must be
|
||||
// the identical in-memory string every time (no reload, no copy).
|
||||
expect(captured?.content).toBe(captured?.content);
|
||||
expect(typeof captured?.content).toBe("string");
|
||||
});
|
||||
|
||||
it("degrades silently when the skills seam is absent or foreign", () => {
|
||||
expect(() => registerBskSkill(fakeCtx())).not.toThrow();
|
||||
expect(() => registerBskSkill(fakeCtx(null))).not.toThrow();
|
||||
expect(() => registerBskSkill(fakeCtx({}))).not.toThrow();
|
||||
const disposer = registerBskSkill(fakeCtx());
|
||||
expect(typeof disposer).toBe("function");
|
||||
expect(() => disposer()).not.toThrow();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user