Files

35 lines
4.0 KiB
TypeScript

// @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();
});
});