fix(dsh-plugin-browserskill): send JSON content-type on overlay interrupt

The observation HTTP fence rejects POSTs without application/json, so the
Interrupt button always got 403.
This commit is contained in:
BB-fat
2026-08-18 09:36:15 +00:00
parent 8bae85127a
commit fea66ce404
2 changed files with 20 additions and 9 deletions
@@ -20,7 +20,7 @@ export interface EventSourceLike {
export interface ObservationClientDeps { export interface ObservationClientDeps {
fetchFn: ( fetchFn: (
url: string, url: string,
init?: { method?: string; body?: string }, init?: { method?: string; body?: string; headers?: Record<string, string> },
) => Promise<{ ) => Promise<{
ok: boolean; ok: boolean;
json(): Promise<unknown>; json(): Promise<unknown>;
@@ -282,6 +282,7 @@ export class ObservationClientStore {
try { try {
const res = await this.deps.fetchFn(INTERRUPT_URL, { const res = await this.deps.fetchFn(INTERRUPT_URL, {
method: "POST", method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(sessionId === undefined ? {} : { sessionId }), body: JSON.stringify(sessionId === undefined ? {} : { sessionId }),
}); });
if (!res.ok) return false; if (!res.ok) return false;
@@ -9,15 +9,23 @@ const OBS_IDLE: SessionObservation = { sessionId: "s1", action: "idle", since: 1
const OBS_BUSY: SessionObservation = { sessionId: "s1", action: "clicking", since: 2000 }; const OBS_BUSY: SessionObservation = { sessionId: "s1", action: "clicking", since: 2000 };
function harness(state: SessionObservation[] = []) { function harness(state: SessionObservation[] = []) {
const fetches: { url: string; init?: { method?: string; body?: string } }[] = []; const fetches: {
url: string;
init?: { method?: string; body?: string; headers?: Record<string, string> };
}[] = [];
const esInstances: EventSourceLike[] = []; const esInstances: EventSourceLike[] = [];
const fetchFn = vi.fn(async (url: string, init?: { method?: string; body?: string }) => { const fetchFn = vi.fn(
fetches.push({ url, init }); async (
if (url === "/bsk-observation/state") { url: string,
return { ok: true, json: async () => ({ sessions: state }) }; init?: { method?: string; body?: string; headers?: Record<string, string> },
} ) => {
return { ok: true, json: async () => ({ interrupted: true }) }; fetches.push({ url, init });
}); if (url === "/bsk-observation/state") {
return { ok: true, json: async () => ({ sessions: state }) };
}
return { ok: true, json: async () => ({ interrupted: true }) };
},
);
const eventSourceFactory = (url: string) => { const eventSourceFactory = (url: string) => {
const es: EventSourceLike = { onmessage: null, close: vi.fn() }; const es: EventSourceLike = { onmessage: null, close: vi.fn() };
esInstances.push(es); esInstances.push(es);
@@ -89,7 +97,9 @@ describe("ObservationClientStore", () => {
expect(await store.interrupt()).toBe(true); expect(await store.interrupt()).toBe(true);
expect(await store.interrupt("s1")).toBe(true); expect(await store.interrupt("s1")).toBe(true);
const calls = fetches.filter((f) => f.url === "/bsk-observation/interrupt"); const calls = fetches.filter((f) => f.url === "/bsk-observation/interrupt");
expect(calls[0].init?.headers?.["content-type"]).toBe("application/json");
expect(JSON.parse(calls[0].init?.body ?? "")).toEqual({}); expect(JSON.parse(calls[0].init?.body ?? "")).toEqual({});
expect(calls[1].init?.headers?.["content-type"]).toBe("application/json");
expect(JSON.parse(calls[1].init?.body ?? "")).toEqual({ sessionId: "s1" }); expect(JSON.parse(calls[1].init?.body ?? "")).toEqual({ sessionId: "s1" });
}); });