Initial Daily Campaign Agent release
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
import { mkdtemp } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import sharp from "sharp";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { MemoryAttachmentStore } from "../src/attachments.js";
|
||||
import { SafeWorkspace } from "../src/safe-workspace.js";
|
||||
async function stream(buffer: Buffer) { return { async *[Symbol.asyncIterator]() { yield buffer; } }; }
|
||||
describe("attachments", () => {
|
||||
async function setup(max=1024*1024) { const root=await mkdtemp(path.join(os.tmpdir(),"asm-att-")); const ws=new SafeWorkspace(root); await ws.init(); return { ws, store:new MemoryAttachmentStore(()=>ws,max) }; }
|
||||
for (const [format,mime,ext] of [["jpeg","image/jpeg","jpg"],["png","image/png","png"],["webp","image/webp","webp"]] as const) it(`accepts ${format}`, async () => { const {ws,store}=await setup(); const buffer=await sharp({create:{width:8,height:6,channels:3,background:"red"}})[format]().toBuffer(); const item=await store.add("s",{filename:`a.${ext}`,mimetype:mime,stream:await stream(buffer)}); expect([item.width,item.height,item.mediaType]).toEqual([8,6,mime]); await ws.remove(); });
|
||||
it("rejects spoofed MIME, SVG and oversized input", async () => { const {ws,store}=await setup(); const png=await sharp({create:{width:2,height:2,channels:3,background:"red"}}).png().toBuffer(); await expect(store.add("s",{filename:"a.jpg",mimetype:"image/jpeg",stream:await stream(png)})).rejects.toThrow(/内容/); await expect(store.add("s",{filename:"x.svg",mimetype:"image/svg+xml",stream:await stream(Buffer.from("<svg/>"))})).rejects.toThrow(/仅支持/); const tiny=new MemoryAttachmentStore(()=>ws,10); await expect(tiny.add("s",{filename:"a.png",mimetype:"image/png",stream:await stream(png)})).rejects.toThrow(/大小/); await ws.remove(); });
|
||||
it("isolates session lookup and removes files", async () => { const {ws,store}=await setup(); const png=await sharp({create:{width:2,height:2,channels:3,background:"red"}}).png().toBuffer(); const item=await store.add("one",{filename:"a.png",mimetype:"image/png",stream:await stream(png)}); expect(store.get("two",item.attachmentId)).toBeUndefined(); await store.removeAll("one"); expect(store.get("one",item.attachmentId)).toBeUndefined(); await ws.remove(); });
|
||||
it("accepts UTF-8 CSV and TSV as spreadsheet attachments", async () => { const {ws,store}=await setup(); const csv=await store.add("s",{filename:"日报.csv",mimetype:"text/csv",stream:await stream(Buffer.from("日期,Ad,消耗\n2026-09-15,A,12\n"))}); const tsv=await store.add("s",{filename:"report.tsv",mimetype:"text/plain",stream:await stream(Buffer.from("date\tad\tspend\n2026-09-15\tA\t12\n"))}); expect(csv).toMatchObject({kind:"spreadsheet",mediaType:"text/csv",name:"日报.csv"}); expect(tsv).toMatchObject({kind:"spreadsheet",mediaType:"text/tab-separated-values"}); await ws.remove(); });
|
||||
it("accepts non-standard spreadsheet MIME after content validation", async () => { const {ws,store}=await setup(); const csv=await store.add("s",{filename:"wps-export.csv",mimetype:"application/x-wps-office.csv",stream:await stream(Buffer.from("date,ad,spend\n2026-09-15,A,12\n"))}); expect(csv).toMatchObject({kind:"spreadsheet",mediaType:"text/csv"}); await ws.remove(); });
|
||||
it("rejects legacy XLS, invalid UTF-8 and spoofed XLSX", async () => { const {ws,store}=await setup(); await expect(store.add("s",{filename:"old.xls",mimetype:"application/vnd.ms-excel",stream:await stream(Buffer.from("x"))})).rejects.toThrow(/另存为 .xlsx/); await expect(store.add("s",{filename:"bad.csv",mimetype:"text/csv",stream:await stream(Buffer.from([0xff,0xfe]))})).rejects.toThrow(/UTF-8/); await expect(store.add("s",{filename:"fake.xlsx",mimetype:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",stream:await stream(Buffer.from("not a zip"))})).rejects.toThrow(/有效的 XLSX/); await ws.remove(); });
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { MemoryApprovalBroker } from "../src/approval.js";
|
||||
import { MemoryEventStore } from "../src/event-store.js";
|
||||
|
||||
describe("event store", () => {
|
||||
it("allocates monotonic per-session sequences", () => { const store = new MemoryEventStore(); const a = store.append("s", { type: "run.started", runId: "r", payload: { runId: "r" } }); const b = store.append("s", { type: "run.completed", runId: "r", payload: { runId: "r" } }); expect([a.sequence,b.sequence]).toEqual([1,2]); });
|
||||
it("replays only after cursor and notifies subscribers", () => { const store = new MemoryEventStore(); const listener = vi.fn(); const off = store.subscribe("s", listener); const event = store.append("s", { type: "session.started", payload: { cwd: "safe" } }); off(); store.append("s", { type: "session.ended", payload: { reason: "x" } }); expect(listener).toHaveBeenCalledTimes(1); expect(store.listAfter("s", { eventId: event.eventId })).toHaveLength(1); });
|
||||
it("keeps a bounded history", () => { const store = new MemoryEventStore(2); for (let i=0;i<3;i++) store.append("s", { type: "run.started", runId: `r${i}`, payload: { runId: `r${i}` } }); expect(store.listAfter("s").map((e)=>e.sequence)).toEqual([2,3]); });
|
||||
});
|
||||
|
||||
describe("approval broker", () => {
|
||||
const input = { sessionId: "s", runId: "r", toolCallId: "t", toolName: "save_report_draft", arguments: { body: "draft", apiKey: "secret" }, riskLevel: "medium" as const };
|
||||
it("approves and redacts arguments", async () => { let required = ""; const broker = new MemoryApprovalBroker(1000, undefined, (request) => { required=request.approvalId; expect(request.arguments).toMatchObject({ apiKey: "[REDACTED]" }); }); const pending = broker.request(input, new AbortController().signal); broker.approve(required); expect((await pending).decision).toBe("approved"); });
|
||||
it("denies and is idempotent for the same decision", async () => { let id=""; const broker = new MemoryApprovalBroker(1000, undefined, (r)=>{id=r.approvalId}); const pending=broker.request(input,new AbortController().signal); const first=broker.deny(id); expect(broker.deny(id)).toEqual(first); expect((await pending).decision).toBe("denied"); });
|
||||
it("rejects a conflicting decision", () => { let id=""; const broker=new MemoryApprovalBroker(1000,undefined,(r)=>{id=r.approvalId}); void broker.request(input,new AbortController().signal); broker.deny(id); expect(()=>broker.approve(id)).toThrow(/already resolved/i); });
|
||||
it("expires", async () => { let id=""; const broker=new MemoryApprovalBroker(5,undefined,(r)=>{id=r.approvalId}); const result=await broker.request(input,new AbortController().signal); expect(id).toMatch(/^apr_/); expect(result.decision).toBe("expired"); });
|
||||
it("cancels on abort", async () => { const controller=new AbortController(); const broker=new MemoryApprovalBroker(1000); const result=broker.request(input,controller.signal); controller.abort(); expect((await result).decision).toBe("cancelled"); });
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { access, mkdtemp } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import sharp from "sharp";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { MemoryApprovalBroker } from "../src/approval.js";
|
||||
import { MemoryAttachmentStore } from "../src/attachments.js";
|
||||
import { MemoryEventStore } from "../src/event-store.js";
|
||||
import { MemoryModelConnectionStore } from "../src/model-store.js";
|
||||
import { HarnessSession, MISSING_INPUT_POLICY, type AgentSessionPort } from "../src/session.js";
|
||||
import { SkillCatalog } from "../src/skills.js";
|
||||
import { DAILY_REPORT_PROMPT, type AccountProfile } from "@agent-studio/shared";
|
||||
|
||||
const projectRoot=path.resolve(import.meta.dirname,"../../..");
|
||||
async function fixture(supportsImages=false, accountProfile?: AccountProfile) {
|
||||
const temp=await mkdtemp(path.join(os.tmpdir(),"asm-session-")); const sessionsRoot=path.join(temp,"sessions"); const events=new MemoryEventStore(); const models=new MemoryModelConnectionStore();
|
||||
await models.configure("s",{providerId:"openai",api:"openai-responses",apiKey:"sk-fake-testing-key",modelId:"gpt-5",supportsImages});
|
||||
const approvals=new MemoryApprovalBroker(1000); const sessionRef:{current?:HarnessSession}={}; const attachments=new MemoryAttachmentStore(()=>sessionRef.current!.workspace); const skills=await SkillCatalog.create(projectRoot,path.join(temp,"agent"),"");
|
||||
let resolvePrompt!:()=>void; const prompt=new Promise<void>((resolve)=>{resolvePrompt=resolve}); const port:AgentSessionPort={prompt:vi.fn(()=>prompt),abort:vi.fn(async()=>resolvePrompt()),subscribe:vi.fn(()=>()=>undefined),dispose:vi.fn()};
|
||||
const session=new HarnessSession({id:"s",projectRoot,sessionsRoot,agentDir:path.join(temp,"agent"),fixturePath:path.join(projectRoot,"fixtures/ads-account.json"),eventStore:events,approvals,models,attachments,skills,getAccountProfile:()=>accountProfile,runTimeoutMs:10000,maxImages:4,fakeFactory:async()=>port}); sessionRef.current=session; await session.init();
|
||||
return {temp,session,events,attachments,port,resolvePrompt};
|
||||
}
|
||||
describe("HarnessSession lifecycle",()=>{
|
||||
it("defines a global clarification gate for required Skill and template inputs",()=>{ expect(MISSING_INPUT_POLICY).toContain("ask the user a concise, explicit clarification question"); expect(MISSING_INPUT_POLICY).toContain("Do not leave blank fields"); expect(MISSING_INPUT_POLICY).toContain("template"); });
|
||||
it("rejects concurrent runs, cancels, unsubscribes and disposes",async()=>{ const x=await fixture(); const run=await x.session.start({text:"one",attachmentIds:[]}); expect(run).toMatch(/^run_/); await expect(x.session.start({text:"two",attachmentIds:[]})).rejects.toMatchObject({code:"SESSION_BUSY"}); await x.session.cancel("user"); expect(x.port.abort).toHaveBeenCalled(); await x.session.dispose(); expect(x.port.dispose).toHaveBeenCalled(); await expect(access(path.join(x.temp,"sessions","s"))).rejects.toThrow(); expect(x.events.listAfter("s").some((e)=>e.type==="run.cancelled"&&e.payload.reason==="user")).toBe(true); });
|
||||
it("maps normalized images to Pi ImageContent and never events base64",async()=>{ const x=await fixture(true); const png=await sharp({create:{width:3,height:2,channels:3,background:"cyan"}}).png().toBuffer(); const upload={filename:"shot.png",mimetype:"image/png",stream:{async *[Symbol.asyncIterator](){yield png}}}; const attachment=await x.attachments.add("s",upload); await x.session.start({text:"看图",attachmentIds:[attachment.attachmentId]}); expect(x.port.prompt).toHaveBeenCalledWith("看图",{images:[expect.objectContaining({type:"image",mimeType:"image/png",data:expect.any(String)})]}); expect(JSON.stringify(x.events.listAfter("s"))).not.toContain(png.toString("base64")); x.resolvePrompt(); await x.session.dispose(); });
|
||||
it("rejects images for text-only models",async()=>{ const x=await fixture(false); const png=await sharp({create:{width:2,height:2,channels:3,background:"red"}}).png().toBuffer(); const item=await x.attachments.add("s",{filename:"a.png",mimetype:"image/png",stream:{async *[Symbol.asyncIterator](){yield png}}}); await expect(x.session.start({text:"",attachmentIds:[item.attachmentId]})).rejects.toMatchObject({code:"MODEL_DOES_NOT_SUPPORT_IMAGES",status:422}); await x.session.dispose(); });
|
||||
it("does not start the quick daily-report workflow without a spreadsheet",async()=>{ const x=await fixture(false); await expect(x.session.start({text:DAILY_REPORT_PROMPT,attachmentIds:[]})).rejects.toMatchObject({code:"REPORT_ATTACHMENT_REQUIRED",status:422}); expect(x.port.prompt).not.toHaveBeenCalled(); await x.session.dispose(); });
|
||||
it("accepts spreadsheets on text-only models and gives the agent a controlled attachment reference",async()=>{ const x=await fixture(false); const csv=Buffer.from("date,ad,spend,installs,d1 retained\n2026-09-15,A,12,3,1\n"); const item=await x.attachments.add("s",{filename:"daily.csv",mimetype:"text/csv",stream:{async *[Symbol.asyncIterator](){yield csv}}}); await x.session.start({text:"分析日报",attachmentIds:[item.attachmentId]}); expect(x.port.prompt).toHaveBeenCalledWith(expect.stringContaining(`attachmentId: ${item.attachmentId}`),undefined); expect(x.port.prompt).toHaveBeenCalledWith(expect.stringContaining("analyze_uploaded_report"),undefined); expect(x.port.prompt).toHaveBeenCalledWith(expect.stringContaining("向用户发起一次清晰、合并后的澄清"),undefined); x.resolvePrompt(); await x.session.dispose(); });
|
||||
it("requires the Skill-owned Word renderer after spreadsheet analysis",async()=>{ const x=await fixture(false); const csv=Buffer.from("date,ad,spend,installs,d1 retained\n2026-09-15,A,12,3,1\n"); const item=await x.attachments.add("s",{filename:"daily.csv",mimetype:"text/csv",stream:{async *[Symbol.asyncIterator](){yield csv}}}); await x.session.start({text:"分析日报",attachmentIds:[item.attachmentId]}); expect(x.port.prompt).toHaveBeenCalledWith(expect.stringContaining("必须调用 build_daily_brief"),undefined); x.resolvePrompt(); await x.session.dispose(); });
|
||||
it("keeps the spreadsheet workflow across a clarification reply",async()=>{ const x=await fixture(false); const csv=Buffer.from("date,ad,spend,installs,d1 retained\n2026-09-15,A,12,3,1\n"); const item=await x.attachments.add("s",{filename:"daily.csv",mimetype:"text/csv",stream:{async *[Symbol.asyncIterator](){yield csv}}}); await x.session.start({text:"分析日报",attachmentIds:[item.attachmentId]}); x.resolvePrompt(); await vi.waitFor(()=>expect(x.session.state).toBe("idle")); await x.session.start({text:"上限,账户级",attachmentIds:[]}); expect(x.port.prompt).toHaveBeenLastCalledWith(expect.stringContaining(`attachmentId: ${item.attachmentId}`),undefined); expect(x.port.prompt).toHaveBeenLastCalledWith(expect.stringContaining("上一轮澄清尚未完成"),undefined); expect(x.port.prompt).toHaveBeenLastCalledWith(expect.stringContaining("必须调用 build_daily_brief"),undefined); await x.session.dispose(); });
|
||||
it("injects the persistent account KPI card and forbids repeat questions",async()=>{ const profile:AccountProfile={accountName:"蚂蚁",analysisDatePolicy:"yesterday",retentionLagDays:1,kpis:[{metric:"CPI",role:"primary",formula:"spend / installs",direction:"lower",target:"≤ 4",scope:"账户级",maturityRule:"当日完整"}],pacing:{enabled:false}}; const x=await fixture(false,profile); const csv=Buffer.from("date,ad,spend,installs,d1 retained\n2026-09-15,A,12,3,1\n"); const item=await x.attachments.add("s",{filename:"daily.csv",mimetype:"text/csv",stream:{async *[Symbol.asyncIterator](){yield csv}}}); await x.session.start({text:"生成日报",attachmentIds:[item.attachmentId]}); expect(x.port.prompt).toHaveBeenCalledWith(expect.stringContaining('"accountName":"蚂蚁"'),undefined); expect(x.port.prompt).toHaveBeenCalledWith(expect.stringContaining("不得再次询问这些字段"),undefined); expect(x.port.prompt).toHaveBeenCalledWith(expect.stringContaining("pacing.enabled=false"),undefined); x.resolvePrompt(); await x.session.dispose(); });
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { access, mkdtemp, stat } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { SkillCatalog } from "../src/skills.js";
|
||||
import { MemoryModelConnectionStore, validateBaseUrl } from "../src/model-store.js";
|
||||
import { adaptPiEvent } from "../src/pi-event-adapter.js";
|
||||
import { MemoryEventStore } from "../src/event-store.js";
|
||||
|
||||
const projectRoot=path.resolve(import.meta.dirname,"../../..");
|
||||
describe("skill catalog", () => {
|
||||
it("exposes only the vendored daily campaign skill", async () => { const catalog=await SkillCatalog.create(projectRoot,await mkdtemp(path.join(os.tmpdir(),"asm-agent-")),path.join(os.tmpdir(),"must-not-load")); expect(catalog.list().map((s)=>s.name)).toEqual(["daily-campaign-triage"]); expect(catalog.list()[0]?.source).toBe("built-in"); });
|
||||
it("matches only the built-in canonical SKILL path", async () => { const catalog=await SkillCatalog.create(projectRoot,await mkdtemp(path.join(os.tmpdir(),"asm-agent-")),""); const matched=await catalog.matchRead(path.join(projectRoot,".agents/skills/daily-campaign-triage/SKILL.md"),projectRoot); expect(matched?.name).toBe("daily-campaign-triage"); expect(await catalog.matchRead(path.join(projectRoot,".agents/skills/ads-analysis/SKILL.md"),projectRoot)).toBeUndefined(); });
|
||||
});
|
||||
describe("base URL guard", () => {
|
||||
it("allows https public URLs", async () => expect((await validateBaseUrl("https://example.com/v1")).hostname).toBe("example.com"));
|
||||
it("rejects credentials, dangerous protocols and metadata", async () => { await expect(validateBaseUrl("https://user:pass@example.com")).rejects.toThrow(/用户信息/); await expect(validateBaseUrl("file:///etc/passwd")).rejects.toThrow(/HTTPS/); await expect(validateBaseUrl("https://169.254.169.254/latest")).rejects.toThrow(/metadata|内网/); });
|
||||
});
|
||||
describe("persistent model config",()=>{
|
||||
it("survives store recreation, preserves a blank key and clears only explicitly",async()=>{ const root=await mkdtemp(path.join(os.tmpdir(),"asm-model-")); const file=path.join(root,"private","model.json"); const first=new MemoryModelConnectionStore(false,file); await first.configure("s",{providerId:"openai",api:"openai-responses",apiKey:"sk-persistent-secret",modelId:"gpt-5",supportsImages:false}); expect((await stat(file)).mode&0o777).toBe(0o600); const second=new MemoryModelConnectionStore(false,file); await second.loadPersistent(); expect((await second.applyPersistent("next"))?.configured).toBe(true); await second.configure("next",{providerId:"openai",api:"openai-responses",modelId:"gpt-5.1",supportsImages:false}); expect(second.getRedacted("next")?.configured).toBe(true); await second.clearCredentials("next"); await expect(access(file)).rejects.toThrow(); });
|
||||
});
|
||||
describe("Pi event adapter", () => {
|
||||
it("coalesces deltas and ignores unknown events", () => { const store=new MemoryEventStore(); const context={sessionId:"s",runId:()=>"r",eventStore:store,assistantText:new Map<string,string>()}; adaptPiEvent({type:"unknown"},context); adaptPiEvent({type:"message_update",messageId:"m",assistantMessageEvent:{type:"text_delta",delta:"你"}},context); adaptPiEvent({type:"message_update",messageId:"m",assistantMessageEvent:{type:"text_delta",delta:"好"}},context); adaptPiEvent({type:"message_end",message:{id:"m",role:"assistant",content:[{type:"text",text:"你好"}],stopReason:"stop"}},context); expect(store.listAfter("s")).toHaveLength(2); expect(context.assistantText.get("m")).toBe("你好"); });
|
||||
it("assigns distinct fallback IDs to assistant turns around tools",()=>{ const store=new MemoryEventStore(); const context={sessionId:"s",runId:()=>"r",eventStore:store,assistantText:new Map<string,string>(),assistantState:{nextMessageNumber:1,pendingDelta:""}}; for(const text of ["准备分析","最终报告"]){ adaptPiEvent({type:"message_update",assistantMessageEvent:{type:"text_delta",delta:text}},context); adaptPiEvent({type:"message_end",message:{role:"assistant",content:[{type:"text",text}],stopReason:"stop"}},context); } const completed=store.listAfter("s").filter((event)=>event.type==="assistant.completed"); expect(completed.map((event)=>event.payload.messageId)).toEqual(["assistant_r_1","assistant_r_2"]); });
|
||||
it("recovers final text and exposes provider errors",()=>{ const store=new MemoryEventStore(); const errors:string[]=[]; const context={sessionId:"s",runId:()=>"r",eventStore:store,assistantText:new Map<string,string>(),onModelError:(message:string)=>errors.push(message)}; adaptPiEvent({type:"message_end",message:{role:"assistant",content:[{type:"text",text:"完整回复"}],stopReason:"stop"}},context); adaptPiEvent({type:"message_end",message:{role:"assistant",content:[],stopReason:"error",errorMessage:"Bad model id"}},context); expect(store.listAfter("s").some((event)=>event.type==="assistant.delta"&&event.payload.delta==="完整回复")).toBe(true); expect(errors).toEqual(["Bad model id"]); });
|
||||
it("treats an empty final response as a visible model error",()=>{ const errors:string[]=[]; adaptPiEvent({type:"message_end",message:{role:"assistant",content:[],stopReason:"stop"}},{sessionId:"s",runId:()=>"r",eventStore:new MemoryEventStore(),assistantText:new Map(),onModelError:(message)=>errors.push(message)}); expect(errors[0]).toMatch(/空响应/); });
|
||||
it("sanitizes tool arguments", () => { const store=new MemoryEventStore(); adaptPiEvent({type:"tool_execution_start",toolCallId:"t",toolName:"x",args:{apiKey:"secret"}},{sessionId:"s",runId:()=>"r",eventStore:store,assistantText:new Map()}); expect(JSON.stringify(store.listAfter("s"))).not.toContain("secret"); });
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { mkdtemp, readFile, symlink } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { SafeWorkspace } from "../src/safe-workspace.js";
|
||||
import { MemoryAttachmentStore } from "../src/attachments.js";
|
||||
import { calculateAdsMetrics, createAnalyzeUploadedReportTool, createDailyBriefTool, createMockAdsMetricsTool } from "../src/tools.js";
|
||||
|
||||
const projectRoot = path.resolve(import.meta.dirname, "../../..");
|
||||
describe("SafeWorkspace", () => {
|
||||
it("writes only fixed report.md", async () => { const root=await mkdtemp(path.join(os.tmpdir(),"asm-workspace-")); const ws=new SafeWorkspace(root); await ws.init(); await ws.writeReport("正文","报告"); expect(await readFile(path.join(root,"report.md"),"utf8")).toContain("# 报告"); await ws.remove(); });
|
||||
it("rejects dot-dot and absolute escapes", async () => { const root=await mkdtemp(path.join(os.tmpdir(),"asm-workspace-")); const ws=new SafeWorkspace(root); await ws.init(); await expect(ws.resolve("../outside",true)).rejects.toThrow(/outside/); await expect(ws.resolve("/etc/passwd")).rejects.toThrow(/outside/); await ws.remove(); });
|
||||
it("rejects symlink parents", async () => { const root=await mkdtemp(path.join(os.tmpdir(),"asm-workspace-")); const ws=new SafeWorkspace(root); await ws.init(); await symlink(os.tmpdir(),path.join(root,"link")); await expect(ws.resolve("link/file",true)).rejects.toThrow(/Symbolic/); await ws.remove(); });
|
||||
});
|
||||
describe("mock ads tool", () => {
|
||||
const fixture=path.join(projectRoot,"fixtures/ads-account.json");
|
||||
it("computes stable rounded metrics", async () => { const result=await calculateAdsMetrics(fixture,"demo-account",7); expect(result.impressions).toBe(256500); expect(result.ctr).toBeGreaterThan(2); expect(result.campaigns).toHaveLength(3); });
|
||||
it("rejects unknown account and invalid days", async () => { await expect(calculateAdsMetrics(fixture,"unknown",7)).rejects.toThrow(/Unknown/); await expect(calculateAdsMetrics(fixture,"demo-account",31)).rejects.toThrow(/1 to 30/); });
|
||||
it("uses a strict TypeBox schema", () => { const tool=createMockAdsMetricsTool(fixture); expect(tool.parameters).toMatchObject({ type:"object", additionalProperties:false }); });
|
||||
});
|
||||
describe("uploaded report tool", () => {
|
||||
it("runs the bundled analyzer and returns compact evidence", async () => { const root=await mkdtemp(path.join(os.tmpdir(),"asm-analysis-")); const ws=new SafeWorkspace(root); await ws.init(); const store=new MemoryAttachmentStore(()=>ws); const source=await readFile(path.join(projectRoot,".agents/skills/daily-campaign-triage/tests/fixtures/sample-report.csv")); const attachment=await store.add("s",{filename:"sample-report.csv",mimetype:"text/csv",stream:{async *[Symbol.asyncIterator](){yield source}}}); const tool=createAnalyzeUploadedReportTool(projectRoot,"s",ws,store); const result=await tool.execute("call",{attachmentId:attachment.attachmentId,analysisDate:"2026-08-25",retentionLagDays:1}); expect(result.details).toMatchObject({contract_version:"1.0-compact",source:{row_count:11},dates:{analysis_date:"2026-08-25"},selection:{total_ads:2,included_ads:2}}); expect(JSON.stringify(result).length).toBeLessThan(30_000); await ws.remove(); });
|
||||
});
|
||||
describe("Skill Word report generation",()=>{
|
||||
it("uses daily-campaign-triage build_brief.py and its dated four-section contract",async()=>{ const root=await mkdtemp(path.join(os.tmpdir(),"asm-word-")); const ws=new SafeWorkspace(root); await ws.init(); let created: unknown; const tool=createDailyBriefTool(projectRoot,"s",ws,(artifact)=>{created=artifact}); const brief=JSON.parse(await readFile(path.join(projectRoot,".agents/skills/daily-campaign-triage/examples/brief.example.json"),"utf8")); await tool.execute("call",brief); const name="2026-08-25-campaign-triage.docx"; const file=await readFile(path.join(root,name)); expect(created).toMatchObject({name,downloadUrl:`/api/sessions/s/artifacts/${name}`}); expect(file.subarray(0,2).toString()).toBe("PK"); expect(file.includes(Buffer.from("word/document.xml"))).toBe(true); await ws.remove(); });
|
||||
});
|
||||
Reference in New Issue
Block a user