// @vitest-environment jsdom import { describe, expect, it, vi } from "vitest"; import { HarnessClientStore } from "../src/assistant/HarnessClientStore.js"; import { toolkit } from "../src/assistant/useHarnessRuntime.js"; describe("web harness adapter",()=>{ it("registers the analyzer and Skill-owned Word renderer",()=>{ expect(Object.keys(toolkit)).toEqual(expect.arrayContaining(["mock_ads_metrics","analyze_uploaded_report","build_daily_brief"])); expect(Object.keys(toolkit)).not.toContain("save_report_draft"); }); it("never writes an API key to Web Storage",async()=>{ const store=new HarnessClientStore(); Object.defineProperty(store,"state",{value:{...store.getSnapshot(),sessionId:"s"},writable:true}); const fetchMock=vi.spyOn(globalThis,"fetch").mockResolvedValue(new Response(JSON.stringify({model:{source:"session",configured:true,providerId:"openai",modelId:"gpt-5",supportsImages:false,keyHint:"••••cret"}}),{status:200,headers:{"Content-Type":"application/json"}})); const key="sk-training-super-secret"; await store.configureModel({providerId:"openai",api:"openai-responses",apiKey:key,modelId:"gpt-5",supportsImages:false}); expect(localStorage.length).toBe(0); expect(sessionStorage.length).toBe(0); expect(store.getSnapshot().model).not.toHaveProperty("apiKey"); fetchMock.mockRestore(); }); it("turns browser fetch failures into an actionable server message",async()=>{ const store=new HarnessClientStore(); const fetchMock=vi.spyOn(globalThis,"fetch").mockRejectedValue(new TypeError("Failed to fetch")); await expect(store.request("/health")).rejects.toThrow("默认端口 3001"); fetchMock.mockRestore(); }); it("recreates an expired in-memory session and retries the model request",async()=>{ const store=new HarnessClientStore(); Object.defineProperty(store,"state",{value:{...store.getSnapshot(),sessionId:"old"},writable:true}); Object.defineProperty(store,"connect",{value:vi.fn()}); const json=(value:unknown,status=200)=>new Response(JSON.stringify(value),{status,headers:{"Content-Type":"application/json"}}); const fetchMock=vi.spyOn(globalThis,"fetch") .mockResolvedValueOnce(json({error:{code:"SESSION_NOT_FOUND",message:"Session not found"}},404)) .mockResolvedValueOnce(json({sessionId:"fresh"},201)) .mockResolvedValueOnce(json({model:{source:"missing",configured:false}})) .mockResolvedValueOnce(json({model:{source:"session",configured:true,providerId:"openai",modelId:"gpt-5",supportsImages:false}})); await store.configureModel({providerId:"openai",api:"openai-responses",apiKey:"secret",modelId:"gpt-5",supportsImages:false}); expect(store.getSnapshot().sessionId).toBe("fresh"); expect(store.getSnapshot().model.configured).toBe(true); expect(fetchMock.mock.calls[3]?.[0]).toBe("/api/sessions/fresh/model-config"); fetchMock.mockRestore(); }); it("creates a session before model actions instead of requesting sessions/undefined",async()=>{ const store=new HarnessClientStore(); Object.defineProperty(store,"connect",{value:vi.fn()}); const json=(value:unknown,status=200)=>new Response(JSON.stringify(value),{status,headers:{"Content-Type":"application/json"}}); const fetchMock=vi.spyOn(globalThis,"fetch") .mockResolvedValueOnce(json({sessionId:"created"},201)) .mockResolvedValueOnce(json({skills:[]})) .mockResolvedValueOnce(json({model:{source:"missing",configured:false}})) .mockResolvedValueOnce(json({profile:null})) .mockResolvedValueOnce(json({model:{source:"session",configured:true,providerId:"openai",modelId:"gpt-5",supportsImages:false}})); await store.configureModel({providerId:"openai",api:"openai-responses",apiKey:"secret",modelId:"gpt-5",supportsImages:false}); expect(fetchMock.mock.calls.map(([url])=>String(url))).not.toContain(expect.stringContaining("/undefined/")); expect(fetchMock.mock.calls[0]?.[1]?.headers).not.toHaveProperty("Content-Type"); expect(fetchMock.mock.calls[4]?.[0]).toBe("/api/sessions/created/model-config"); fetchMock.mockRestore(); }); });