feat: publish Agent Studio integrations and setup guide

This commit is contained in:
Wenxin Duan
2026-09-16 16:32:16 +08:00
parent 2ad089d68a
commit 05e6060490
106 changed files with 4942 additions and 198 deletions
@@ -0,0 +1,27 @@
import {describe,it,expect} from "vitest";
import {assertAnalysisBinding} from "../src/media/analysis-binding.js";
const expected={analysisId:"analysis_real",dramaId:"drama_real",fetchedAt:"2026-09-14T00:00:00Z",title:"Actual title",synopsis:"Actual CMS synopsis",tags:["Mystery"],episodeCount:5,
sources:[{episodeId:"e1",episodeNo:1,duration:30,subtitle:"WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nWhere is she?",frames:[{id:"f1",time:2}]},{episodeId:"e3",episodeNo:3,duration:30,subtitle:"WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nFind her.",frames:[{id:"f3",time:2}]},{episodeId:"e5",episodeNo:5,duration:30,subtitle:"WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nWho did this?",frames:[{id:"f5",time:2}]}]};
function analysis(){return {analysis_id:"analysis_real",drama:{drama_id:"drama_real",title:"Actual title",synopsis:"Actual CMS synopsis",tags:["Mystery"],episode_count:5},cms_source:{fetched_at:expected.fetchedAt},evidence:{
frame_evidence:[{episode:1,episode_id:"e1",frame_id:"f1",time:2,claim:"A person stands"},{episode:3,episode_id:"e3",frame_id:"f3",time:2,claim:"A person speaks"}],
subtitle_evidence:[{episode:1,episode_id:"e1",story_position:"opening",quote:"Where is she?"},{episode:3,episode_id:"e3",story_position:"middle",quote:"Find her."},{episode:5,episode_id:"e5",story_position:"late",quote:"Who did this?"}]}};}
describe("analysis evidence binding",()=>{
it("accepts only the actual job identity, CMS record and registered frame/subtitle evidence",()=>{
expect(()=>assertAnalysisBinding(analysis(),expected)).not.toThrow();
const changed=analysis();changed.analysis_id="analysis_other";
expect(()=>assertAnalysisBinding(changed,expected)).toThrow();
changed.analysis_id="analysis_real";changed.drama.synopsis="Model invented synopsis";
expect(()=>assertAnalysisBinding(changed,expected)).toThrow();
});
it("rejects invented frames, mismatched timestamps, duplicate frames and invented subtitle quotes",()=>{
const unknown=analysis();unknown.evidence.frame_evidence[0]!.frame_id="unknown";
expect(()=>assertAnalysisBinding(unknown,expected)).toThrow();
const timestamp=analysis();timestamp.evidence.frame_evidence[0]!.time=29;
expect(()=>assertAnalysisBinding(timestamp,expected)).toThrow();
const duplicate=analysis();duplicate.evidence.frame_evidence[1]=duplicate.evidence.frame_evidence[0]!;
expect(()=>assertAnalysisBinding(duplicate,expected)).toThrow();
const quote=analysis();quote.evidence.subtitle_evidence[0]!.quote="Invented dialogue";
expect(()=>assertAnalysisBinding(quote,expected)).toThrow();
});
});
@@ -0,0 +1,102 @@
import { describe, expect, it } from "vitest";
import { CmsConnection } from "../src/cms/cms-connection.js";
const dramaId = "11111111-1111-4111-8111-111111111111";
const episodeId = "22222222-2222-4222-8222-222222222222";
const userId = "33333333-3333-4333-8333-333333333333";
const token = "test-access-secret";
function server(role = "admin") {
const requests: { url: URL; init: RequestInit | undefined }[] = [];
const fetcher: typeof fetch = (input, init) => {
const url = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
requests.push({ url, init });
const user = { id: userId, aud: "authenticated", role: "authenticated", email: "fixture@example.com", app_metadata: { role }, user_metadata: { role: "admin" }, created_at: "2026-01-01T00:00:00Z" };
let data: unknown;
if (url.pathname === "/auth/v1/token") data = { access_token: token, refresh_token: "refresh-secret", token_type: "bearer", expires_in: 3600, user };
else if (url.pathname === "/auth/v1/user") data = user;
else if (url.pathname === "/rest/v1/drama") data = [{ id: dramaId, title: "Test drama", synopsis: "Fixture only", language: "en", total_episodes: 2, status: "published" }];
else if (url.pathname === "/rest/v1/episode") data = [{ id: episodeId, drama_id: dramaId, episode_no: 1, title: "One", duration_seconds: 90, status: "published" }];
else if (url.pathname === "/rest/v1/drama_category") data = [{drama_id:dramaId,category:{name:"Mystery"}}];
else if (url.pathname === "/rest/v1/episode_source") data = [{ episode_id: episodeId, provider: "fixture", playback_path: "private/fixture.m3u8" }];
else if (url.pathname === "/rest/v1/subtitle_track") data = [{ episode_id: episodeId, locale: "en", path: "private/fixture.srt" }];
else if (url.pathname.endsWith("/subtitles/en.vtt")) return Promise.resolve(new Response("WEBVTT\n\n00:00:00.000 --> 00:00:01.000\nFixture", { headers: { "content-type": "text/vtt" } }));
else return Promise.resolve(new Response("unexpected destination", { status: 500 }));
return Promise.resolve(new Response(JSON.stringify(data), { headers: { "content-type": "application/json" } }));
};
return { requests, fetcher };
}
describe("CMS connection", () => {
it("binds signing to the selected drama and keeps it on the fixed CMS admin route", async () => {
const fixture = server();
const cms = new CmsConnection({publishableKey:"sb_publishable_test",fetch:async(input,init)=>{
const url=new URL(typeof input==="string"?input:input instanceof URL?input.href:input.url);
if(url.pathname.endsWith(`/cms/playback/${episodeId}`)) {
expect(url.origin).toBe("https://swave.pencil-stub.com");
expect(new Headers(init?.headers).get("authorization")).toBe(`Bearer ${token}`);
return new Response(JSON.stringify({url:"https://video.pencil-stub.com/fixture/master.m3u8?auth_key=private",expires_at:new Date(Date.now()+600000).toISOString()}));
}
return fixture.fetcher(input,init);
}});
await cms.login("fixture@example.com","secret");
await expect(cms.playback(dramaId,userId)).rejects.toMatchObject({code:"CMS_EPISODE_NOT_FOUND"});
expect((await cms.playback(dramaId,episodeId)).url).toContain("https://video.pencil-stub.com/");
expect(JSON.stringify(cms.status())).not.toContain("auth_key");
});
it("blocks content before login and never sends credentials to a model-facing status", async () => {
const { fetcher } = server();
const cms = new CmsConnection({ publishableKey: "sb_publishable_test", fetch: fetcher });
await expect(cms.listDramas("", 0)).rejects.toMatchObject({ code: "CMS_LOGIN_REQUIRED" });
await cms.login("fixture@example.com", "password-secret");
expect(cms.status()).toMatchObject({ configured: true, connected: true });
expect(JSON.stringify(cms.status())).not.toMatch(/password-secret|test-access-secret|refresh-secret|fixture@example/);
expect((await cms.listDramas("", 0)).dramas[0]?.title).toBe("Test drama");
cms.clear();
expect(cms.status().connected).toBe(false);
await expect(cms.listDramas("", 0)).rejects.toMatchObject({ code: "CMS_LOGIN_REQUIRED" });
});
it("rejects user-editable administrator claims and discards the session", async () => {
const { fetcher } = server("viewer");
const cms = new CmsConnection({ publishableKey: "sb_publishable_test", fetch: fetcher });
await expect(cms.login("fixture@example.com", "secret")).rejects.toMatchObject({ code: "CMS_ADMIN_REQUIRED" });
expect(cms.status().connected).toBe(false);
});
it("reports incomplete inventory without leaking playback or subtitle paths", async () => {
const { fetcher } = server();
const cms = new CmsConnection({ publishableKey: "sb_publishable_test", fetch: fetcher });
await cms.login("fixture@example.com", "secret");
const inventory = await cms.inventory(dramaId);
expect(inventory.complete).toBe(false);
expect(inventory.drama.tags).toEqual(["Mystery"]);
expect(inventory.missingEpisodeNumbers).toEqual([2]);
expect(inventory.episodes[0]).toMatchObject({ episodeNo: 1, hasSource: true, subtitleLocales: ["en"] });
expect(JSON.stringify(inventory)).not.toContain("private/");
expect(inventory.fetchedAt).toMatch(/^\d{4}-/);
});
it("binds subtitle access to an actual episode in the chosen drama", async () => {
const { fetcher, requests } = server();
const cms = new CmsConnection({ publishableKey: "sb_publishable_test", fetch: fetcher });
await cms.login("fixture@example.com", "secret");
await expect(cms.subtitle(dramaId, userId, "en")).rejects.toMatchObject({ code: "CMS_EPISODE_NOT_FOUND" });
expect((await cms.subtitle(dramaId, episodeId, "en")).text).toContain("WEBVTT");
const subtitleRequest = requests.find((request) => request.url.pathname.endsWith(".vtt"));
expect(subtitleRequest?.url.origin).toBe("https://swave.pencil-stub.com");
expect(subtitleRequest?.init?.redirect).toBe("error");
expect(new Headers(subtitleRequest?.init?.headers).get("authorization")).toBe(`Bearer ${token}`);
});
it("rejects a late login result after the connection was cleared", async () => {
const { fetcher } = server();
let release: (() => void) | undefined;
const gate = new Promise<void>((resolve) => { release = resolve; });
const cms = new CmsConnection({ publishableKey: "sb_publishable_test", fetch: async (input, init) => { await gate; return fetcher(input, init); } });
const pending = cms.login("fixture@example.com", "secret");
cms.clear();
release?.();
await expect(pending).rejects.toMatchObject({ code: "CMS_LOGIN_CANCELLED" });
expect(cms.status().connected).toBe(false);
});
});
@@ -0,0 +1,18 @@
import {describe,it,expect} from "vitest";
import {EditorService} from "../src/media/editor-service.js";
import {CmsConnection} from "../src/cms/cms-connection.js";
describe("editor job boundary",()=>{
it("requires CMS login before preparing media and rejects unknown jobs",async()=>{
const service=new EditorService({cms:new CmsConnection({publishableKey:""}),runtimeRoot:"/unused/editor-tests",sessionId:"ses_test"});
await expect(service.prepare({dramaId:"06aa396d-175a-48ed-ad76-02786ef4d0a9",episodeNumbers:[1,3,5]},new AbortController().signal)).rejects.toMatchObject({code:"CMS_LOGIN_REQUIRED"});
expect(()=>service.status("job_unknown")).toThrow();
await expect(service.submitAnalysis("job_unknown",{},new AbortController().signal)).rejects.toMatchObject({code:"EDITOR_JOB_NOT_FOUND"});
expect(()=>service.workflowView("job_unknown")).toThrow();
await expect(service.review("job_unknown","selection",{})).rejects.toMatchObject({code:"EDITOR_JOB_NOT_FOUND"});
});
it("rejects duplicate episode requests and oversized preparation scope before fetching",async()=>{
const service=new EditorService({cms:new CmsConnection({publishableKey:""}),runtimeRoot:"/unused/editor-tests",sessionId:"ses_test"});
for(const numbers of [[1,1],[1,2,3,4,5,6],[]])await expect(service.prepare({dramaId:"06aa396d-175a-48ed-ad76-02786ef4d0a9",episodeNumbers:numbers},new AbortController().signal)).rejects.toThrow();
});
});
@@ -0,0 +1,9 @@
import {it,expect} from "vitest";
import {cutSubtitles} from "../src/media/editor-subtitles.js";
it("maps original English cues to the selected timeline without invented text",()=>{
const result=cutSubtitles([{subtitle:"WEBVTT\n\n00:00:02.000 --> 00:00:04.000\nA &amp; B.\n\n00:00:07.000 --> 00:00:09.000\nDo not clip this line.\n",sourceStart:1,sourceEnd:8,targetStart:0,segmentId:"one"}]);
expect(result.srt).toContain("00:00:01,000 --> 00:00:03,000\nA & B.");
expect(result.boundaryWarnings).toEqual(["one"]);
expect(result.srt).toContain("00:00:06,000 --> 00:00:07,000");
expect(()=>cutSubtitles([{subtitle:"invalid",sourceStart:0,sourceEnd:10,targetStart:0,segmentId:"one"}])).toThrow();
});
@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { decideToolPolicy } from "../src/tools/tool-policy.js";
describe("editor content approval", () => {
it("requires approval before exposing licensed CMS content to the model", () => {
expect(decideToolPolicy("cms_connection_status")).toBe("allow");
expect(decideToolPolicy("editor_job_status")).toBe("allow");
expect(decideToolPolicy("submit_drama_analysis")).toBe("allow");
expect(decideToolPolicy("propose_drama_selection")).toBe("allow");
expect(decideToolPolicy("submit_editor_plan")).toBe("allow");
expect(decideToolPolicy("render_editor_preview")).toBe("ask");
expect(decideToolPolicy("approve_editor_review")).toBe("deny");
expect(decideToolPolicy("prepare_drama_sources")).toBe("ask");
expect(decideToolPolicy("read_drama_frames")).toBe("ask");
for (const name of ["cms_list_dramas", "cms_drama_inventory", "cms_read_subtitle"]) expect(decideToolPolicy(name)).toBe("ask");
expect(decideToolPolicy("cms_login")).toBe("deny");
expect(decideToolPolicy("render_exact")).toBe("deny");
});
});
@@ -0,0 +1,56 @@
import {it,expect} from "vitest";
import {mkdtemp,rm,writeFile,readFile} from "node:fs/promises";
import {tmpdir,homedir} from "node:os";
import {join} from "node:path";
import {createHash} from "node:crypto";
import {EditorWorkflow} from "../src/media/editor-workflow.js";
import {SkillRunner} from "../src/media/skill-runner.js";
import {analysisFixture} from "./fixtures/editor-analysis.js";
import {planFixture} from "./fixtures/editor-plan.js";
import {mediaRuntime,runMedia} from "../src/media/media-worker.js";
it("requires source review before accepting a plan and never treats model PASS as human review",async()=>{
const root=await mkdtemp(join(tmpdir(),"workflow-"));
try{
await writeFile(join(root,"target-drama-analysis.json"),JSON.stringify({withheld_information:["The answer"]}));
const workflow=new EditorWorkflow({root,sessionId:"ses_test",jobId:"job_test",analysisId:"analysis_test",dramaId:"drama_test",sources:[{episodeId:"ep_test",episodeNo:1,source:join(root,"source.mp4"),duration:20,sha256:"abc",subtitle:"WEBVTT"}],runner:new SkillRunner({skillRoot:"/unused",pythonExecutable:"/unused"})});
await expect(workflow.submitPlan({},new AbortController().signal)).rejects.toMatchObject({code:"SELECTION_REVIEW_REQUIRED"});
const selected=await workflow.proposeSelection({segments:[{id:"one",episodeId:"ep_test",start:0,end:10,reason:"A complete question and reaction"}]});
expect(selected.selection?.reviewed).toBe(false);
expect(JSON.stringify(selected)).not.toContain(root);
await expect(workflow.review("selection",{planHash:selected.selection?.hash,pass:true})).rejects.toThrow();
await expect(workflow.render(new AbortController().signal)).rejects.toMatchObject({code:"PLAN_REVIEW_REQUIRED"});
await expect(workflow.proposeSelection({segments:[{id:"one",episodeId:"foreign",start:0,end:10,reason:"Invalid source"}]})).rejects.toMatchObject({code:"SELECTION_INVALID"});
}finally{await rm(root,{recursive:true,force:true});}
});
it("binds human-reviewed selection to original validators and renders only a REVIEW artifact",async()=>{
const root=await mkdtemp(join(tmpdir(),"workflow-render-"));const signal=new AbortController().signal;
try{
const runtime=await mediaRuntime();const source=join(root,"source.mp4");
await runMedia(runtime.ffmpeg,["-v","error","-f","lavfi","-i","testsrc2=size=180x320:rate=24","-f","lavfi","-i","sine=frequency=440:sample_rate=48000","-t","11","-c:v","libx264","-c:a","aac",source],root,signal);
await writeFile(join(root,"target-drama-analysis.json"),JSON.stringify(analysisFixture()));
const original=await readFile(source);
const workflow=new EditorWorkflow({root,sessionId:"ses_test",jobId:"job_test",analysisId:"analysis_fixture",dramaId:"drama_fixture",sources:[{episodeId:"ep_test",episodeNo:1,source,duration:11,sha256:createHash("sha256").update(original).digest("hex"),subtitle:"WEBVTT\n\n00:00:01.000 --> 00:00:04.000\nSynthetic test caption.\n"}],runner:new SkillRunner({skillRoot:join(homedir(),".codex/skills/swads-short-drama-editor"),pythonExecutable:join(homedir(),".cache/codex-runtimes/codex-primary-runtime/dependencies/python/bin/python3")})});
const selected=await workflow.proposeSelection({segments:[{id:"one",episodeId:"ep_test",start:0,end:10,reason:"Synthetic complete test sequence"}]});
const evidence="Synthetic engineering review; not a licensed drama clearance.";
await workflow.review("selection",{planHash:selected.selection!.hash,reviewer:"Test fixture",policyCheckedAt:new Date().toISOString(),policyPageUpdated:"Test fixture policy",pictureAudioReviewed:true,noUnresolvedFindings:true,segmentEvidence:[{segmentId:"one",pictureEvidence:evidence,audioEvidence:evidence,subtitleEvidence:evidence}]});
const unverified=planFixture();unverified.referenceCandidates.candidates[0]!.evidence.metrics={views:0,likes:0};
const pendingReference=await workflow.submitPlan(unverified,signal);
expect(pendingReference.pass).toBe(true);
await expect(workflow.review("plan",{planHash:workflow.view().plan!.hash,reviewer:"Test fixture",policyCheckedAt:new Date().toISOString(),pictureAudioReviewed:true,referenceReviewed:true,storyReviewed:true,packagingReviewed:true,noUnresolvedFindings:true,segmentEvidence:[{segmentId:"one",observation:evidence}]})).rejects.toMatchObject({code:"VERIFIED_REFERENCE_REQUIRED"});
const result=await workflow.submitPlan(planFixture(),signal);
expect(result.pass).toBe(true);
const plan=workflow.view().plan!;
await expect(workflow.render(signal)).rejects.toMatchObject({code:"PLAN_REVIEW_REQUIRED"});
await workflow.review("plan",{planHash:plan.hash,reviewer:"Test fixture",policyCheckedAt:new Date().toISOString(),pictureAudioReviewed:true,referenceReviewed:true,storyReviewed:true,packagingReviewed:true,noUnresolvedFindings:true,segmentEvidence:[{segmentId:"one",observation:evidence}]});
await writeFile(source,"changed after review");
await expect(workflow.render(signal)).rejects.toMatchObject({code:"REVIEWED_SOURCE_CHANGED"});
await writeFile(source,original);
const rendered=await workflow.render(signal);
expect(rendered.preview?.duration).toBeCloseTo(10,1);expect(rendered.preview?.reviewStatus).toBe("REVIEW");
expect(JSON.stringify(rendered)).not.toContain(root);
const bytes=await readFile(workflow.previewPath(plan.id));expect(bytes.byteLength).toBeGreaterThan(1000);
expect(rendered.humanReviewPassed).toBe(false);
}finally{await rm(root,{recursive:true,force:true});}
},60_000);
+9
View File
@@ -0,0 +1,9 @@
// Synthetic engineering fixture, never used as a real drama diagnosis.
export function analysisFixture(){return {
schema_version:"2.0",analysis_id:"analysis_fixture",analysis_status:"ready",
drama:{drama_id:"drama_fixture",title:"Synthetic validation fixture",synopsis:"Test-only conflict with an unresolved question.",tags:["Test"],episode_count:3},cms_source:{fetched_at:"2026-09-14T00:00:00Z"},
genre:{primary:"family evidence dispute mystery",secondary:["family conflict"],anti_genres:["comedy","romantic payoff"],reason:"Synthetic test only",evidence:[{source_type:"synopsis",claim:"Fixture synopsis"},{source_type:"subtitle",claim:"Fixture subtitle"},{source_type:"frame",claim:"Fixture frame"}]},
core_promise:"Resolve the fixture dispute",primary_hook:{question:"Who left the test note?",promise:"An unresolved dispute"},relationship_conflict:{protagonist:"A",opposition:"B",surface_conflict:"A note",deeper_conflict:"Trust"},character_engine:{misery:["Uncertainty"],power:["Search"],desire:"An answer"},emotional_pattern:["doubt","discovery","uncertainty"],
available_story_beats:[1,2,3].map(episode=>({episode,time_ranges:["00:00:00-00:00:10"],beat:"Synthetic test"})),
evidence:{synopsis_used:true,synopsis_source:"Fixture",subtitle_evidence:["opening","middle","late"].map((story_position,index)=>({episode:index+1,story_position,time_range:"00:00:00-00:00:10",claim:"Synthetic"})),frame_evidence:[{episode:1,time_range:"00:00:01",claim:"Synthetic pattern"},{episode:2,time_range:"00:00:02",claim:"Synthetic pattern"}],conflicts:[],conflicts_resolved:true},must_explain:["The note"],withheld_information:["The author"],confidence:0.8,
};}
+15
View File
@@ -0,0 +1,15 @@
// Synthetic data for isolated engineering tests only. Not production reference evidence.
export function planFixture(){
const binding={analysis_id:"analysis_fixture",drama_id:"drama_fixture"};
const shared={target_binding:binding,withheld_information:["The author"]};
const questions=["Who left this test note?","Why was the test note hidden?","Will the note resolve this dispute?","How will they find the author?"];
return {
recipe:{schema_version:"2.0",...binding,duration_strategy:"content_led",withheld_information:shared.withheld_information,reference_id:"ref_fixture",reference_evidence_tier:"A",ending:{cutoff_reason:"Synthetic ending with reaction",before_withheld_reveal:true},segments:[{id:"one",hook_id:"hook_1",episode_id:"ep_test",episode:1,source_start:0,source_end:10,target_start:0,target_end:10,semantic_role:"Synthetic question",continuity_group:"scene",source_gap_before:0,confidence:"high",reveal_risk:"low",cut_reason:"Test unit complete"}]},
hookMap:{...shared,mainline_question:questions[0],beats:["beat","phase","mainline"].map((hook_level,index)=>({id:`hook_${String(index+1)}`,target_start:index*3,target_end:index===2?10:(index+1)*3,hook_level,audience_question_before:questions[index],information_revealed:["A test clue"],revealed_categories:["evidence"],question_resolved:"A clue exists",new_question:questions[index+1],expectation_a:null,actual_b:null,hook_c:null,withheld_information:shared.withheld_information,evidence:"Synthetic span"})),phase_hooks:[{id:"phase",beat_ids:["hook_1","hook_2"],stage_payoff:"The clue was found",escalated_question:questions[2]}],final_hook:{beat_id:"hook_3",cutoff_after_complete_action:true,reaction_retained:true,answer_withheld:true},final_unanswered_question:questions[3]},
strategy:{...shared,narrative_structure:{mode:"linear",reason:"Synthetic causal sequence for engineering verification"},opening:{mode:"original_footage",voiceover_authorized:false},first_30_seconds:{central_promise:"Test clue"},character_amplification:{primary:"power",evidence:["Fixture"]},event_density:{major_events:["Test clue"]}},
referenceCandidates:{candidates:[{id:"ref_fixture",platform:"Synthetic",reference_type:"short_drama_ad",origin_name:"Test-only publisher",origin_evidence_url:"https://example.com/title",playable_url:"https://example.com/video",evidence_url:"https://example.com/evidence",captured_at:new Date().toISOString(),full_video_access:true,evidence:{metrics:{views:10,likes:1}},genre_match:1,hook_match:1,replicability:1,market_recency:1}]},
referenceAnalysis:{...shared,reference_id:"ref_fixture",structure:"Synthetic question then partial answer"},
referenceRecipe:{...shared,reference_id:"ref_fixture",beats:[{role:"Synthetic question"}]},
continuity:{...shared,checks:["Synthetic continuous shot"],findings:[]},
};
}
+25
View File
@@ -0,0 +1,25 @@
import { describe, expect, it } from "vitest";
import { parseHls, resolveHlsAsset } from "../src/media/hls.js";
describe("bounded HLS inputs", () => {
it("preserves CDN-issued per-path signatures without accepting unrelated parameters", () => {
const base = new URL("https://cdn.example/episode/master.m3u8?auth_key=123-0-0-abcdef");
expect(resolveHlsAsset(base, "child.m3u8?auth_key=124-0-0-123abc", "/episode/").search).toBe("?auth_key=124-0-0-123abc");
for (const uri of ["child.m3u8?auth_key=124-0-0-123abc&token=secret", "child.m3u8?auth_key=oops", "child.m3u8?auth_key=124-0-0-123abc&auth_key=124-0-0-123abc"]) expect(() => resolveHlsAsset(base, uri, "/episode/")).toThrow();
});
it("parses finite TS playlists and preserves exact durations", () => {
expect(parseHls("#EXTM3U\n#EXTINF:4.5,\none.ts\n#EXTINF:2,\ntwo.ts\n#EXT-X-ENDLIST\n")).toEqual({kind:"media",segments:[{uri:"one.ts",duration:4.5},{uri:"two.ts",duration:2}],duration:6.5});
});
it("rejects incomplete, encrypted and unsupported source structures", () => {
for (const text of ["not hls", "#EXTM3U\n#EXTINF:3,\none.ts", "#EXTM3U\n#EXT-X-KEY:METHOD=AES-128\n#EXTINF:3,\none.ts\n#EXT-X-ENDLIST", "#EXTM3U\n#EXT-X-MAP:URI=init.mp4\n#EXTINF:3,\none.m4s\n#EXT-X-ENDLIST", "#EXTM3U\n#EXTINF:-3,\none.ts\n#EXT-X-ENDLIST"]) expect(()=>parseHls(text)).toThrow();
});
it("recognizes a simple master but refuses separate audio renditions", () => {
expect(parseHls("#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=400000\nlow/list.m3u8")).toEqual({kind:"master",variants:["low/list.m3u8"]});
expect(()=>parseHls('#EXTM3U\n#EXT-X-MEDIA:TYPE=AUDIO,URI="audio.m3u8"\n#EXT-X-STREAM-INF:BANDWIDTH=1\nvideo.m3u8')).toThrow();
});
it("keeps signing parameters only within the signed episode's path and origin", () => {
const base = new URL("https://cdn.example/video/episode/master.m3u8?auth_key=fixture");
expect(resolveHlsAsset(base,"low/0001.ts",base.pathname.slice(0,base.pathname.lastIndexOf('/')+1)).href).toBe("https://cdn.example/video/episode/low/0001.ts?auth_key=fixture");
for (const uri of ["https://attacker.example/a.ts","../other.ts","file:///tmp/a.ts","//127.0.0.1/a.ts","%2e%2e/other.ts","one.ts?token=other"]) expect(()=>resolveHlsAsset(base,uri,"/video/episode/")).toThrow();
});
});
@@ -0,0 +1,37 @@
import { expect, it } from "vitest";
import { mkdtemp, readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import * as media from "../src/image-video/media.js";
import { mediaRuntime, runMedia } from "../src/media/media-worker.js";
it("trims H3's small frame overrun to the requested duration at original speed, preserving the source and rejecting short or excessive output", async () => {
const directory = await mkdtemp(join(tmpdir(), "i2v-duration-"));
const { ffmpeg, ffprobe } = await mediaRuntime();
const signal = new AbortController().signal;
const make = async (name: string, seconds: string) => {
const path = join(directory, name);
await runMedia(ffmpeg, ["-v", "error", "-f", "lavfi", "-i", "testsrc2=size=90x160:rate=24", "-f", "lavfi", "-i", "sine=frequency=440:sample_rate=32000", "-t", seconds, "-c:v", "libx264", "-preset", "ultrafast", "-c:a", "aac", "-n", path], directory, signal);
return path;
};
const source = await make("original.mp4", "1.125");
const original = await readFile(source);
const output = await media.conformImageVideoDuration(source, directory, 1);
const probe = JSON.parse(await runMedia(ffprobe, ["-v", "error", "-show_streams", "-of", "json", output], directory, signal)) as { streams: { codec_type: string; duration: string; nb_frames: string; avg_frame_rate: string }[] };
const video = probe.streams.find(s => s.codec_type === "video")!;
expect(Number(video.duration)).toBe(1);
expect(video.nb_frames).toBe("24");
expect(video.avg_frame_rate).toBe("24/1");
expect(Number(probe.streams.find(s => s.codec_type === "audio")!.duration)).toBeCloseTo(1, 2);
expect(await readFile(source)).toEqual(original);
// Actual H3 2-second response is 56 frames (17k+5), not 48 frames.
const gridSource = await make("h3-2s.mp4", "2.333333");
const gridOriginal = await readFile(gridSource);
const gridOutput = await media.conformImageVideoDuration(gridSource, directory, 2);
const gridProbe = JSON.parse(await runMedia(ffprobe, ["-v", "error", "-select_streams", "v:0", "-show_entries", "stream=duration,nb_frames,avg_frame_rate", "-of", "json", gridOutput], directory, signal)) as { streams: { duration: string; nb_frames: string; avg_frame_rate: string }[] };
expect(gridProbe.streams[0]).toMatchObject({ duration: "2.000000", nb_frames: "48", avg_frame_rate: "24/1" });
expect(await readFile(gridSource)).toEqual(gridOriginal);
await expect(media.conformImageVideoDuration(output, directory, 1)).resolves.toBe(output);
await expect(media.conformImageVideoDuration(await make("short.mp4", "0.875"), directory, 1)).rejects.toBeDefined();
await expect(media.conformImageVideoDuration(await make("long.mp4", "1.5"), directory, 1)).rejects.toBeDefined();
}, 30_000);
@@ -0,0 +1,107 @@
import { afterEach, expect, it, vi } from "vitest";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { ImageVideoMcpRemote, imageVideoResponse } from "../src/image-video/mcp.js";
import type { ImageVideoRecord } from "../src/image-video/contracts.js";
import { mkdtemp, mkdir, readdir, readFile, stat } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
vi.mock("node:dns/promises", () => ({ lookup: () => Promise.resolve([{ address: "8.8.8.8", family: 4 }]) }));
afterEach(() => vi.restoreAllMocks());
it("accepts the live reused-image receipt only when hash, type and size match the frozen reference", async () => {
vi.spyOn(Client.prototype, "connect").mockResolvedValue();
vi.spyOn(Client.prototype, "listTools").mockResolvedValue({ tools: ["swads_whoami", "creative_upload_from_url", "creative_import_status", "video_h3_submit", "video_job_status"].map(name => ({ name, inputSchema: { type: "object", properties: Object.fromEntries(["tenant_id", "platform", "platforms", "media_kind", "review_status", "asset_role", "ratio", "prompt", "file_name", "content_type", "size_bytes", "content_sha256"].map(key => [key, {}])) } })) });
const file = { file_id: "file_1", filename: "i2v_test.jpeg", stored_path: "https://cdn.example/image.jpeg", object_key: "image.jpeg", asset_url: "https://cdn.example/image.jpeg", mime_type: "image/jpeg", size_bytes: 9, attachment_kind: "image", creative_factory_asset_id: "existing_asset", creative_factory_request_id: "request_1", asset_metadata: { content_sha256: "a".repeat(64) } };
let reusedFile: unknown = file;
vi.spyOn(Client.prototype, "callTool").mockImplementation(request => Promise.resolve({ content: [], structuredContent: request.name === "swads_whoami" ? { access: { current_account: { tenant_id: "tenant", platform: "tiktok" } } } : { upload_id: "du-reuse-1", object_key: "image.jpeg", upload_url: "", public_url: "https://cdn.example/image.jpeg", method: "PUT", headers: {}, expires_in_seconds: 0, reused: true, reused_file: reusedFile } }));
const remote = new ImageVideoMcpRemote({ token: "test" });
const record = { id: "i2v_test", identity: { tenantId: "tenant", platform: "tiktok" }, draft: { prompt: "waves" }, referenceType: "image/jpeg", referenceHash: "a".repeat(64) } as ImageVideoRecord;
await expect(remote.initialize(record, Buffer.from("reference"))).resolves.toEqual({ reused: true, assetId: "existing_asset" });
for (const changed of [{ asset_metadata: { content_sha256: "b".repeat(64) } }, { mime_type: "image/png" }, { size_bytes: 10 }, { creative_factory_asset_id: "" }]) {
reusedFile = { ...file, ...changed };
await expect(remote.initialize(record, Buffer.from("reference"))).rejects.toBeDefined();
}
});
it("collects the registered job's storage_url MP4 and ignores JSON artifacts, rejecting foreign or conflicting receipts", async () => {
vi.spyOn(Client.prototype, "connect").mockResolvedValue();
vi.spyOn(Client.prototype, "listTools").mockResolvedValue({ tools: ["swads_whoami", "creative_upload_from_url", "creative_import_status", "video_h3_submit", "video_job_status"].map(name => ({ name, inputSchema: { type: "object", properties: { job_id: {} } } })) });
const video = { artifact_id: "artifact_video", job_id: "job_1", org_id: "org", asset_id: null, version_id: null, artifact_kind: "video", content_type: "video/mp4", storage_url: "https://cdn.example/video.mp4", object_key: "video.mp4", json_payload: null, metadata: {}, created_at: "2026-09-15T00:00:00Z" };
let result: Record<string, unknown> = { job_id: "job_1", status: "succeeded", artifacts: [video, { ...video, artifact_id: "artifact_json", artifact_kind: "metadata", content_type: "application/json", storage_url: null, object_key: null, json_payload: { provider: "h3" } }] };
vi.spyOn(Client.prototype, "callTool").mockImplementation(request => Promise.resolve({ content: [], structuredContent: request.name === "swads_whoami" ? { access: { current_account: { tenant_id: "tenant", platform: "tiktok" } } } : result }));
const remote = new ImageVideoMcpRemote({ token: "test" });
const record = { jobId: "job_1" } as ImageVideoRecord;
await expect(remote.status(record)).resolves.toEqual({ state: "succeeded", artifactUrl: "https://cdn.example/video.mp4" });
result = { job_id: "job_1", status: "succeeded", artifacts: [{ ...video, job_id: "job_other" }] };
await expect(remote.status(record)).rejects.toBeDefined();
result = { job_id: "job_1", status: "succeeded", artifacts: [{ ...video, url: "https://cdn.example/other.mp4" }] };
await expect(remote.status(record)).rejects.toBeDefined();
});
it("submits within the gateway key budget and deduplicates the same frozen request without merging different requests", async () => {
vi.spyOn(Client.prototype, "connect").mockResolvedValue();
vi.spyOn(Client.prototype, "listTools").mockResolvedValue({ tools: ["swads_whoami", "creative_upload_from_url", "creative_import_status", "video_h3_submit", "video_job_status"].map(name => ({ name, inputSchema: { type: "object", properties: Object.fromEntries(["prompt", "title", "reference_asset_ids", "aspect_ratio", "duration_seconds", "resolution", "h3_mode", "idempotency_key", "skill_id"].map(key => [key, {}])) } })) });
const jobs = new Map<string, string>();
vi.spyOn(Client.prototype, "callTool").mockImplementation(request => {
if (request.name === "swads_whoami") return Promise.resolve({ content: [], structuredContent: { access: { current_account: { tenant_id: "tenant", platform: "tiktok" } } } });
const key = String(request.arguments?.idempotency_key);
if (key.length > 87) return Promise.resolve({ content: [], structuredContent: { error_code: "invalid_arguments", detail: "idempotency_key must be at most 87 characters" } });
if (!jobs.has(key)) jobs.set(key, `job_${jobs.size + 1}`);
return Promise.resolve({ content: [], structuredContent: { job_id: jobs.get(key) } });
});
const remote = new ImageVideoMcpRemote({ token: "test" });
const record = { id: "i2v_11111111-1111-1111-1111-111111111111", requestHash: "a".repeat(64), draft: { prompt: "waves", title: "test", durationSeconds: 10 }, assetId: "asset_1" } as ImageVideoRecord;
await expect(remote.submit(record)).resolves.toEqual({ jobId: "job_1" });
await expect(remote.submit(record)).resolves.toEqual({ jobId: "job_1" });
await expect(remote.submit({ ...record, requestHash: "b".repeat(64) })).resolves.toEqual({ jobId: "job_2" });
await expect(remote.submit({ ...record, id: "i2v_22222222-2222-2222-2222-222222222222" })).resolves.toEqual({ jobId: "job_3" });
});
it("uploads with the frozen prompt and ratio required by the live begin/finish contract", async () => {
vi.spyOn(Client.prototype, "connect").mockResolvedValue();
const properties = Object.fromEntries(["tenant_id", "platform", "platforms", "media_kind", "review_status", "asset_role", "ratio", "prompt", "file_name", "content_type", "size_bytes", "content_sha256", "upload_id", "object_key"].map(key => [key, {}]));
vi.spyOn(Client.prototype, "listTools").mockResolvedValue({ tools: ["swads_whoami", "creative_upload_from_url", "creative_import_status", "video_h3_submit", "video_job_status"].map(name => ({ name, inputSchema: { type: "object", properties } })) });
vi.spyOn(Client.prototype, "callTool").mockImplementation(request => {
const args = request.arguments;
const result = request.name === "swads_whoami" ? { access: { current_account: { tenant_id: "tenant", platform: "tiktok" } } }
: args?.ratio !== "9:16" || args.prompt !== "Subtle waves and breeze" ? { error_code: "invalid_arguments", detail: "push begin needs: ratio, prompt" }
: args.file_name !== "i2v_test.jpeg" || args.content_type !== "image/jpeg" || args.size_bytes !== 9 ? { error_code: "invalid_arguments", detail: "push finish needs: file_name, content_type, size_bytes" }
: args.upload_id ? { status: "finalizing" } : { upload_id: "upload_1", object_key: "image.jpeg", upload_url: "https://public.example/put?signature=private" };
return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(result) }] });
});
const remote = new ImageVideoMcpRemote({ token: "test" });
const record = { id: "i2v_test", identity: { tenantId: "tenant", platform: "tiktok" }, draft: { prompt: "Subtle waves and breeze" }, referenceType: "image/jpeg", referenceHash: "a".repeat(64) } as ImageVideoRecord;
const upload = await remote.initialize(record, Buffer.from("reference"));
if ("reused" in upload) throw new Error("Expected a new upload ticket");
expect(upload).toEqual({ uploadId: "upload_1", objectKey: "image.jpeg", uploadUrl: "https://public.example/put?signature=private" });
await expect(remote.finalize({ ...record, upload }, Buffer.from("reference"))).resolves.toBeUndefined();
});
it("recognizes error_code failures even when MCP isError is false without exposing remote details", () => {
expect(() => imageVideoResponse({ isError: false, content: [{ type: "text", text: JSON.stringify({ error_code: "invalid_arguments", detail: "token=private https://private.example/?signature=secret" }) }] })).toThrow(expect.objectContaining({ code: "I2V_REMOTE_REJECTED", message: "SW Ads rejected the request (invalid_arguments)" }));
});
it("retains an unparseable submission response privately before rejecting it, without a second submit", async () => {
const root = await mkdtemp(join(tmpdir(), "i2v-receipt-"));
const id = "i2v_11111111-1111-1111-1111-111111111111";
await mkdir(join(root, id), { mode: 0o700 });
vi.spyOn(Client.prototype, "connect").mockResolvedValue();
vi.spyOn(Client.prototype, "listTools").mockResolvedValue({ tools: ["swads_whoami", "creative_upload_from_url", "creative_import_status", "video_h3_submit", "video_job_status"].map(name => ({ name, inputSchema: { type: "object", properties: Object.fromEntries(["prompt", "title", "reference_asset_ids", "aspect_ratio", "duration_seconds", "resolution", "h3_mode", "idempotency_key", "skill_id"].map(key => [key, {}])) } })) });
let submissions = 0;
const raw = { content: [{ type: "text" as const, text: '{"unexpected_job":{"id":"recoverable_job"},"private_url":"https://public.example/?signature=secret"}' }] };
vi.spyOn(Client.prototype, "callTool").mockImplementation(request => {
if (request.name === "swads_whoami") return Promise.resolve({ structuredContent: { access: { current_account: { tenant_id: "tenant", platform: "tiktok" } } }, content: [] });
submissions++; return Promise.resolve(raw);
});
const remote = new ImageVideoMcpRemote({ token: "private-token", receiptRoot: root });
const record = { id, revision: 4, requestHash: "a".repeat(64), draft: { prompt: "waves", title: "test", durationSeconds: 10 }, assetId: "asset_1" } as ImageVideoRecord;
await expect(remote.submit(record)).rejects.toBeDefined();
const files = await readdir(join(root, id));
expect(files).toHaveLength(1);
const file = join(root, id, files[0]!);
expect(JSON.parse(await readFile(file, "utf8"))).toMatchObject({ id, revision: 4, requestHash: "a".repeat(64), operation: "submit", response: raw });
expect((await stat(file)).mode & 0o777).toBe(0o600);
expect(await readFile(file, "utf8")).not.toContain("private-token");
expect(submissions).toBe(1);
});
@@ -29,6 +29,62 @@ async function fixture() {
}
describe("durable image video tasks", () => {
it("continues an orphaned uploaded task locally without losing receipts or automatically submitting", async () => {
const f = await fixture(); const task = await f.service.create("ses_owner", f.draft);
await f.service.upload("ses_owner", task.id); await f.service.refresh(task.id);
const path = join(f.options.root, task.id, "record.json");
const before = JSON.parse(await readFile(path, "utf8")) as ImageVideoRecord;
f.calls.length = 0;
const restored = new harness.ImageVideoService(f.options);
const result = await restored.continueTask("ses_new", task.id, before.revision, () => false, () => {});
expect(result).toMatchObject({ id: task.id, state: "awaiting_submission", revision: before.revision + 1 });
expect(f.calls).toEqual([]);
const after = JSON.parse(await readFile(path, "utf8")) as ImageVideoRecord;
expect(after).toMatchObject({ ownerSessionId: "ses_new", draft: before.draft, referenceHash: before.referenceHash, requestHash: before.requestHash, upload: before.upload, assetId: "asset_1" });
expect(after.continuations).toHaveLength(1);
expect(after.continuations?.[0]).toMatchObject({ fromSessionId: "ses_owner", toSessionId: "ses_new", revision: before.revision });
expect(Number.isFinite(Date.parse(after.continuations?.[0]?.at ?? ""))).toBe(true);
await expect(restored.owned("ses_owner", task.id)).rejects.toMatchObject({ code: "I2V_TASK_NOT_FOUND" });
expect((await restored.owned("ses_new", task.id)).id).toBe(task.id);
await restored.submit("ses_new", task.id);
expect(f.calls).toEqual(["identity", "submit"]);
expect(JSON.stringify(result)).not.toMatch(/ownerSessionId|continuations|uploadUrl/);
});
it("rejects active-owner takeover, stale confirmation, uncertain state and concurrent continuation", async () => {
const f = await fixture(); const task = await f.service.create("ses_owner", f.draft);
await expect(f.service.continueTask("ses_new", task.id, 0, () => true, () => {})).rejects.toMatchObject({ code: "I2V_OWNER_ACTIVE" });
await expect(f.service.continueTask("ses_new", task.id, 99, () => false, () => {})).rejects.toMatchObject({ code: "I2V_STALE_REVISION" });
const attempts = await Promise.allSettled([f.service.continueTask("ses_new", task.id, 0, () => false, () => {}), f.service.continueTask("ses_other", task.id, 0, () => false, () => {})]);
expect(attempts.map(a => a.status)).toEqual(["fulfilled", "rejected"]);
const path = join(f.options.root, task.id, "record.json");
const record = JSON.parse(await readFile(path, "utf8")) as ImageVideoRecord;
for (const state of ["unknown_upload", "unknown_submission", "failed", "queued"] as const) {
await writeFile(path, JSON.stringify({ ...record, state }));
await expect(f.service.continueTask("ses_new", task.id, record.revision, () => false, () => {})).rejects.toMatchObject({ code: "I2V_CONTINUE_BLOCKED" });
expect((JSON.parse(await readFile(path, "utf8")) as ImageVideoRecord).state).toBe(state);
}
expect(f.calls).toEqual([]);
});
it("reuses a verified existing image without PUT, finalize or automatic generation and persists it across restart", async () => {
const f = await fixture(); const task = await f.service.create("ses_owner", f.draft);
f.remote.initialize = () => { f.calls.push("initialize"); return Promise.resolve({ reused: true as const, assetId: "existing_asset" }); };
expect((await f.service.upload("ses_owner", task.id)).state).toBe("awaiting_submission");
expect(f.calls).toEqual(["identity", "initialize"]);
const restored = new harness.ImageVideoService(f.options);
expect((await restored.view(task.id)).state).toBe("awaiting_submission");
expect((await restored.submit("ses_owner", task.id)).jobId).toBe("remote_job_1");
expect(f.calls).not.toContain("put"); expect(f.calls).not.toContain("finalize");
});
it("persists a sanitized failure stage while keeping rejected uploads quarantined", async () => {
const f = await fixture(); const task = await f.service.create("ses_owner", f.draft);
f.remote.initialize = () => Promise.reject(new harness.HarnessError("I2V_REMOTE_REJECTED", "secret signed URL"));
const result = await f.service.upload("ses_owner", task.id);
expect(result).toMatchObject({ state: "unknown_upload" });
expect(result.error).toContain("initializing_upload/I2V_REMOTE_REJECTED");
expect(result.error).not.toContain("secret");
expect((await new harness.ImageVideoService(f.options).view(task.id)).error).toBe(result.error);
expect(f.calls).not.toContain("put");
});
it("copies a local reference without transmitting it and hides private bindings", async () => {
const f = await fixture(); const task = await f.service.create("ses_owner", f.draft);
expect(f.calls).toEqual([]); expect(task.state).toBe("draft");
@@ -65,6 +121,16 @@ describe("durable image video tasks", () => {
record.state = "submitting"; await writeFile(file, JSON.stringify(record));
expect((await new harness.ImageVideoService(f.options).view(task.id)).state).toBe("unknown_submission");
});
it("records a typed submission rejection without exposing provider details or retrying", async () => {
const f = await fixture(); const task = await f.service.create("ses_owner", f.draft);
await f.service.upload("ses_owner", task.id); await f.service.refresh(task.id);
f.remote.submit = () => Promise.reject(new harness.HarnessError("I2V_REMOTE_REJECTED", "secret upstream detail"));
const result = await f.service.submit("ses_owner", task.id);
expect(result.error).toContain("submitting/I2V_REMOTE_REJECTED");
expect(result.error).not.toContain("secret");
expect(result.state).toBe("unknown_submission");
await expect(f.service.submit("ses_owner", task.id)).rejects.toMatchObject({ code: "I2V_UNKNOWN_SUBMISSION" });
});
it("never reinitializes uncertain uploads and blocks stale persisted revisions", async () => {
const f = await fixture(); const task = await f.service.create("ses_owner", f.draft);
f.remote.initialize = () => { f.calls.push("initialize"); return Promise.reject(new Error("timeout")); };
@@ -0,0 +1,28 @@
import { afterEach, describe, expect, it } from "vitest";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { downloadHls } from "../src/media/media-worker.js";
const roots:string[]=[];
afterEach(async()=>{for(const root of roots.splice(0)) await rm(root,{recursive:true,force:true});});
describe("private source download",()=>{
it("localizes a signed VOD playlist without leaking credentials to disk",async()=>{
const root=await mkdtemp(join(tmpdir(),"hls-test-"));roots.push(root);
const requests:URL[]=[];
const fetcher:typeof fetch=(input,init)=>{
const url=new URL(typeof input==="string"?input:input instanceof URL?input.href:input.url);requests.push(url);
expect(init?.redirect).toBe("error");expect(new Headers(init?.headers).has("authorization")).toBe(false);
return Promise.resolve(new Response(url.pathname.endsWith(".m3u8")?"#EXTM3U\n#EXTINF:2,\n0.ts\n#EXT-X-ENDLIST\n":new Uint8Array([0x47,0,0,0])));
};
const result=await downloadHls(new URL("https://video.pencil-stub.com/episode/master.m3u8?auth_key=private"),root,new AbortController().signal,{remaining:1024},fetcher);
expect(result.duration).toBe(2);
const playlist=await readFile(result.path,"utf8");
expect(playlist).toContain("segment-0.ts");expect(playlist).not.toMatch(/private|https:|auth_key/);
expect(requests.every(url=>url.origin==="https://video.pencil-stub.com"&&url.search==="?auth_key=private")).toBe(true);
});
it("enforces byte quota before persisting oversized segments",async()=>{
const root=await mkdtemp(join(tmpdir(),"hls-test-"));roots.push(root);
await expect(downloadHls(new URL("https://video.pencil-stub.com/e/a.m3u8"),root,new AbortController().signal,{remaining:1},()=>Promise.resolve(new Response("#EXTM3U\n#EXTINF:2,\na.ts\n#EXT-X-ENDLIST")))).rejects.toMatchObject({code:"MEDIA_BYTE_LIMIT"});
});
});
@@ -0,0 +1,34 @@
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import * as harness from "../src/index.js";
const roots: string[] = [];
afterEach(async () => { for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true }); });
describe("Minis report service", () => {
it("requires captured query evidence and returns real HTML with an explicit PDF failure", async () => {
const root = await mkdtemp(join(tmpdir(), "minis-report-test-")); roots.push(root);
const skill = join(root, "skill");
await mkdir(join(skill, "scripts"), { recursive: true });
await writeFile(join(skill, "scripts/generate_report.py"), 'import sys,json,html\nfrom pathlib import Path\nd=json.loads(Path(sys.argv[1]).read_text())\nPath(sys.argv[2]).write_text("<!doctype html><h1>"+html.escape(d["title"])+"</h1>")\n');
const Constructor = Reflect.get(harness, "MinisReportService");
expect(Constructor).toBeDefined();
const service = new Constructor({ workspace: await harness.SafeWorkspace.create(root), sessionId: "ses_test", skillRoot: skill, chromeExecutable: "/missing/chrome", pythonExecutable: "/usr/bin/python3" });
const data = { title: "渲染验收(人工测试数据)", report_type: "daily", period: "2026-09-01", comparison_period: "2026-08-31", currency: "USD", timezone: "UTC", data_status: "测试", conclusion: "仅测试排版,不是经营数据", cards: [], issues: [] };
await expect(service.generate({ data, source_query_ids: [] })).rejects.toThrow("query");
const queryId = service.recordQuery({ date_from: "2026-09-01" }, { content: [{ type: "text", text: "test-only" }] });
await expect(service.generate({ data, source_query_ids: ["unknown"] })).rejects.toThrow("query");
const output = await service.generate({ data, source_query_ids: [queryId] });
expect(output.pdfStatus).toBe("failed");
expect(output.files.map((f: {format: string}) => f.format)).toEqual(["json", "html"]);
expect(output.files[1]?.url).toMatch(/^\/api\/sessions\/ses_test\/reports\/rpt_[\w-]+\/html$/);
const html = await service.read(output.reportId, "html");
expect(html.bytes.toString()).toContain("人工测试数据");
await expect(service.read(output.reportId, "../../.env")).rejects.toThrow();
const json = JSON.parse((await service.read(output.reportId, "json")).bytes.toString()) as { source_queries: Array<{queryId: string}> };
expect(json.source_queries[0]?.queryId).toBe(queryId);
expect(await readFile(join(skill, "scripts/generate_report.py"), "utf8")).not.toContain("SWADS_MCP_TOKEN");
});
});
@@ -2,6 +2,45 @@ import { describe, expect, it, vi } from "vitest";
import * as harness from "../src/index.js";
describe("PiEventAdapter", () => {
it.each([
{ stopReason: "stop", content: [], code: "MODEL_EMPTY_RESPONSE" },
{ stopReason: "stop", content: [{ type: "thinking", thinking: "private reasoning" }], code: "MODEL_EMPTY_RESPONSE" },
{ stopReason: "length", content: [{ type: "text", text: "partial output" }], code: "MODEL_OUTPUT_LIMIT" },
])("rejects incomplete assistant replies ($stopReason, $code)", async ({ stopReason, content, code }) => {
const store = new harness.InMemoryEventStore();
const adapter = new harness.PiEventAdapter({ sessionId: "ses_1", runId: "run_1", eventStore: store });
await expect(adapter.observe({ type: "message_end", message: { role: "assistant", stopReason, content } })).rejects.toMatchObject({ code });
expect(store.listAfter("ses_1").events).toHaveLength(0);
});
it("keeps licensed frame bytes and subtitles out of event history", async () => {
const store = new harness.InMemoryEventStore();
const adapter = new harness.PiEventAdapter({ sessionId: "ses_1", runId: "run_1", eventStore: store });
await adapter.observe({ type: "tool_execution_end", toolName: "read_drama_frames", toolCallId: "call_1", isError: false,
result: { content: [{ type: "image", data: "PRIVATE_IMAGE_BYTES", mimeType: "image/jpeg" }, { type: "text", text: "PRIVATE_SUBTITLES" }], details: { jobId: "job_1", imageCount: 9 } } });
expect(store.listAfter("ses_1").events[0]?.payload).toMatchObject({ result: { jobId: "job_1", imageCount: 9 } });
expect(JSON.stringify(store.listAfter("ses_1"))).not.toContain("PRIVATE_");
});
it("does not announce completion between tool turns or turn provider errors into success", async () => {
const store = new harness.InMemoryEventStore();
const adapter = new harness.PiEventAdapter({ sessionId: "ses_1", runId: "run_1", eventStore: store });
await adapter.observe({ type: "message_end", message: { role: "assistant", stopReason: "toolUse" } });
expect(store.listAfter("ses_1").events).toHaveLength(0);
await expect(adapter.observe({ type: "message_end", message: { role: "assistant", stopReason: "error", errorMessage: "provider secret" } })).rejects.toThrow("Model request failed");
expect(store.listAfter("ses_1").events).toHaveLength(0);
});
it("unwraps real Pi custom-tool details for the metric card", async () => {
const store = new harness.InMemoryEventStore();
const adapter = new harness.PiEventAdapter({ sessionId: "ses_1", runId: "run_1", eventStore: store });
await adapter.observe({
type: "tool_execution_end", toolName: "mock_ads_metrics", toolCallId: "call_1", isError: false,
result: { content: [{ type: "text", text: '{"spend":120,"revenue":360,"roas":3}' }], details: { spend: 120, revenue: 360, roas: 3 } },
});
expect(store.listAfter("ses_1").events[0]?.payload).toEqual({
toolCallId: "call_1", toolName: "mock_ads_metrics", result: { spend: 120, revenue: 360, roas: 3 },
});
});
it("maps assistant deltas and completion with one stable message id", async () => {
const store = new harness.InMemoryEventStore();
const Adapter = Reflect.get(harness, "PiEventAdapter") as unknown as new (options: Record<string, unknown>) => {
@@ -0,0 +1,22 @@
import {describe,it,expect} from "vitest";
import {bindRecipe} from "../src/media/recipe-binding.js";
const source={episodeId:"episode_real",episodeNo:1,source:"/private/authorized/episode.mp4",duration:20};
function recipe(){return {schema_version:"2.0",analysis_id:"analysis_real",drama_id:"drama_real",duration_strategy:"content_led",withheld_information:["An answer"],ending:{cutoff_reason:"Question remains",before_withheld_reveal:true},segments:[{id:"one",hook_id:"hook",episode_id:"episode_real",episode:1,source:"/etc/passwd",source_start:1,source_end:11,target_start:0,target_end:10,semantic_role:"Question",continuity_group:"scene",source_gap_before:0,confidence:"high",reveal_risk:"low",cut_reason:"Complete exchange"}]};}
const binding={analysisId:"analysis_real",dramaId:"drama_real",analysisPath:"/private/job/target-drama-analysis.json",withheldInformation:["An answer"],sources:[source]};
describe("authorized ten-second recipe",()=>{
it("resolves source and analysis paths on the server instead of accepting model-supplied paths",()=>{
const bound=bindRecipe(recipe(),binding);
expect(bound.segments[0]?.source).toBe(source.source);expect(bound.target_analysis_path).toBe(binding.analysisPath);
});
it("rejects wrong identities, wrong episodes, out-of-source ranges and non-frame-aligned cuts",()=>{
const foreign=recipe();foreign.drama_id="other";expect(()=>bindRecipe(foreign,binding)).toThrow();
const episode=recipe();episode.segments[0]!.episode_id="other";expect(()=>bindRecipe(episode,binding)).toThrow();
const range=recipe();range.segments[0]!.source_end=30;expect(()=>bindRecipe(range,binding)).toThrow();
const boundary=recipe();boundary.segments[0]!.source_start=1.01;expect(()=>bindRecipe(boundary,binding)).toThrow();
const reveal=recipe();reveal.withheld_information=["Different answer"];expect(()=>bindRecipe(reveal,binding)).toThrow();
});
it("rejects non-ten-second output and arbitrary subtitle input paths",()=>{
const long=recipe();long.segments[0]!.source_end=12;long.segments[0]!.target_end=11;expect(()=>bindRecipe(long,binding)).toThrow();
expect(()=>bindRecipe({...recipe(),subtitle_path:"/private/secret.srt"},binding)).toThrow();
});
});
@@ -0,0 +1,12 @@
import {describe,it,expect} from "vitest";
import {assertSelectionReview} from "../src/media/review-ledger.js";
const plan={hash:"abc123",segmentIds:["one"]};
const review={planHash:"abc123",reviewer:"Local user",policyCheckedAt:new Date().toISOString(),pictureAudioReviewed:true,referenceReviewed:true,storyReviewed:true,packagingReviewed:true,noUnresolvedFindings:true,segmentEvidence:[{segmentId:"one",observation:"Reviewed the complete selected span with original audio; nonsexual conversation."}]};
describe("selection review gate",()=>{
it("requires every real review and exact immutable plan hash",()=>{
expect(()=>assertSelectionReview(review,plan)).not.toThrow();
for(const field of ["pictureAudioReviewed","referenceReviewed","storyReviewed","packagingReviewed","noUnresolvedFindings"])expect(()=>assertSelectionReview({...review,[field]:false},plan)).toThrow();
expect(()=>assertSelectionReview({...review,planHash:"changed"},plan)).toThrow();
expect(()=>assertSelectionReview({...review,segmentEvidence:[]},plan)).toThrow();
});
});
@@ -0,0 +1,43 @@
import {it,expect} from "vitest";
import {mkdtemp,rm,writeFile} from "node:fs/promises";
import {tmpdir,homedir} from "node:os";
import {join} from "node:path";
import {SkillRunner} from "../src/media/skill-runner.js";
import {mediaRuntime,runMedia} from "../src/media/media-worker.js";
import {analysisFixture} from "./fixtures/editor-analysis.js";
it.each(["analysis","recipe","hook"] as const)("runs the original %s validator and returns its real failures, never a caller PASS flag",async(kind)=>{
const root=await mkdtemp(join(tmpdir(),"original-validator-"));
try{
const runner=new SkillRunner({skillRoot:join(homedir(),".codex/skills/swads-short-drama-editor"),pythonExecutable:join(homedir(),".cache/codex-runtimes/codex-primary-runtime/dependencies/python/bin/python3")});
const input=join(root,"analysis.json");await writeFile(input,JSON.stringify({pass:true}));
const result=await runner.validate(kind,input,root,new AbortController().signal);
expect(result.pass).toBe(false);expect(result.issues.length).toBeGreaterThan(0);
expect(JSON.stringify(result)).not.toContain(root);
}finally{await rm(root,{recursive:true,force:true});}
});
it("keeps references blocked when only unverified evidence is supplied",async()=>{
const root=await mkdtemp(join(tmpdir(),"original-reference-"));
try{
const analysis=join(root,"target-drama-analysis.json");await writeFile(analysis,JSON.stringify(analysisFixture()));
const input=join(root,"candidates.json");await writeFile(input,JSON.stringify({target_analysis_path:analysis,analysis_id:"analysis_fixture",drama_id:"drama_fixture",candidates:[{id:"r1",full_video_access:true,reference_type:"short_drama_ad",origin_name:"Fixture",origin_evidence_url:"https://example.com/title",playable_url:"https://example.com/video",evidence_url:"https://example.com/metrics",platform:"fixture",captured_at:"2026-09-14T00:00:00Z",genre_match:1,hook_match:1,replicability:1,market_recency:1}]}));
const runner=new SkillRunner({skillRoot:join(homedir(),".codex/skills/swads-short-drama-editor"),pythonExecutable:join(homedir(),".cache/codex-runtimes/codex-primary-runtime/dependencies/python/bin/python3")});
const result=await runner.scoreReferences(input,root,new AbortController().signal);
expect(result.status).toBe("blocked_unverified_reference");expect(result.selected_reference).toBeNull();
}finally{await rm(root,{recursive:true,force:true});}
});
it("renders an actual 10-second H264/AAC file using the original renderer and reports actual QA",async()=>{
const root=await mkdtemp(join(tmpdir(),"original-render-"));const signal=new AbortController().signal;
try{
const runtime=await mediaRuntime();const source=join(root,"source.mp4");
await runMedia(runtime.ffmpeg,["-v","error","-f","lavfi","-i","testsrc2=size=180x320:rate=24","-f","lavfi","-i","sine=frequency=440:sample_rate=48000","-t","11","-c:v","libx264","-c:a","aac",source],root,signal);
const analysis=join(root,"target-drama-analysis.json");await writeFile(analysis,JSON.stringify(analysisFixture()));
const recipe=join(root,"recipe.json");await writeFile(recipe,JSON.stringify({schema_version:"2.0",analysis_id:"analysis_fixture",drama_id:"drama_fixture",target_analysis_path:analysis,withheld_information:["The author"],duration_strategy:"content_led",ending:{cutoff_reason:"Test",before_withheld_reveal:true},segments:[{id:"one",hook_id:"fixture",source,episode:1,source_start:0,source_end:10,target_start:0,target_end:10,semantic_role:"Test",continuity_group:"scene",source_gap_before:0,confidence:"high",reveal_risk:"low",cut_reason:"Synthetic test"}]}));
const runner=new SkillRunner({skillRoot:join(homedir(),".codex/skills/swads-short-drama-editor"),pythonExecutable:join(homedir(),".cache/codex-runtimes/codex-primary-runtime/dependencies/python/bin/python3")});
const result=await runner.render(recipe,root,signal);
expect(result.duration).toBeCloseTo(10,1);expect(result.sha256).toMatch(/^[a-f0-9]{64}$/);
expect(result.checks.complete_decode).toBe(true);expect(result.checks.h264_aac).toBe(true);expect(result.checks.portrait_1080x1920).toBe(true);expect(result.reviewStatus).toBe("REVIEW");
}finally{await rm(root,{recursive:true,force:true});}
},60_000);
+56 -1
View File
@@ -1,4 +1,4 @@
import { access, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { access, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { delimiter, join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
@@ -52,6 +52,61 @@ function catalogConstructor(): CatalogConstructor {
}
describe("SkillCatalog", () => {
it("keeps the editor's new content-review reference readable in the isolated Skill snapshot", async () => {
const original = await createSkill(".agents/skills", "swads-short-drama-editor");
const baseDir = join(root, ".agents/skills/swads-short-drama-editor");
await mkdir(join(baseDir, "references"));
await mkdir(join(baseDir, "scripts"));
for (const resource of harness.EDITOR_RESOURCES) await writeFile(join(baseDir, resource), "fixture resource");
await writeFile(join(baseDir, "references/tiktok-adult-content-policy.md"), "fixture selection and final review gate");
const catalog = new harness.SkillCatalog({ cwd: root, agentDir, resources: { [original]: [...harness.EDITOR_RESOURCES] } });
await catalog.reload();
const sessionRoot = join(root, "session");
await mkdir(sessionRoot);
const snapshot = await catalog.createSessionSnapshot(sessionRoot);
const editor = snapshot.skills.find((skill) => skill.name === "swads-short-drama-editor")!;
const workspace = await harness.SafeWorkspace.create(sessionRoot);
const policy = await workspace.resolveReadable(join(editor.baseDir, "references/tiktok-adult-content-policy.md"));
expect(await readFile(policy, "utf8")).toBe("fixture selection and final review gate");
});
it("copies only approved resources with relative links intact and rejects escaped resources", async () => {
const original = await createSkill(".agents/skills", "diagnosis");
const baseDir = join(root, ".agents/skills/diagnosis");
await mkdir(join(baseDir, "references"));
await writeFile(join(baseDir, "references/metrics.md"), "metric contract");
await writeFile(join(baseDir, ".env"), "do not copy");
const catalog = new harness.SkillCatalog({ cwd: root, agentDir,
resources: { [original]: ["references/metrics.md"] },
});
await catalog.reload();
const sessionRoot = join(root, "session");
await mkdir(sessionRoot);
const snapshot = await catalog.createSessionSnapshot(sessionRoot);
const copied = snapshot.skills.find((skill) => skill.name === "diagnosis")!;
expect(await readFile(join(copied.baseDir, "references/metrics.md"), "utf8")).toBe("metric contract");
await expect(access(join(copied.baseDir, ".env"))).rejects.toThrow();
await rm(join(baseDir, "references/metrics.md"));
await symlink(join(baseDir, ".env"), join(baseDir, "references/metrics.md"));
await expect(catalog.createSessionSnapshot(sessionRoot)).rejects.toThrow();
});
it("makes discovered Skill bodies readable inside the Session without admitting project paths", async () => {
const original = await createSkill(".agents/skills", "ads-analysis");
const sessionRoot = join(root, "session");
await mkdir(sessionRoot);
const catalog = new harness.SkillCatalog({ cwd: root, agentDir });
await catalog.reload();
const snapshot = await catalog.createSessionSnapshot(sessionRoot);
const workspace = await harness.SafeWorkspace.create(sessionRoot);
const copied = snapshot.skills.find((skill) => skill.name === "ads-analysis")!;
await expect(workspace.resolveReadable(copied.filePath)).resolves.toBe(copied.filePath);
expect(await readFile(copied.filePath, "utf8")).toContain("Full secret skill body.");
expect(snapshot.index.find(copied.filePath)?.name).toBe("ads-analysis");
expect(snapshot.index.find(original)).toBeUndefined();
await expect(workspace.resolveReadable(original)).rejects.toThrow();
});
it("discovers .agents, .pi, and delimiter-separated extra roots through Pi", async () => {
await createSkill(".agents/skills", "ads-analysis");
await createSkill(".pi/skills", "report-writer");
+38
View File
@@ -0,0 +1,38 @@
import { describe, expect, it } from "vitest";
import * as harness from "../src/index.js";
describe("swads MCP bridge", () => {
it("performs MCP initialization, sends credentials only in headers and blocks non-allowlisted tools", async () => {
const requests: Array<{ method: string; authorization: string | null }> = [];
const fetcher: typeof fetch = async (_url, init) => {
await Promise.resolve();
const headers = new Headers(init?.headers);
if (init?.method === "GET") return new Response(null, { status: 405 });
if (typeof init?.body !== "string") throw new Error("Expected JSON-RPC body");
const message = JSON.parse(init.body) as { method: string; id?: number; params?: { name?: string } };
requests.push({ method: message.method, authorization: headers.get("authorization") });
if (message.id === undefined) return new Response(null, { status: 202 });
const result = message.method === "initialize"
? { protocolVersion: "2025-03-26", capabilities: { tools: {} }, serverInfo: { name: "test", version: "1" } }
: { content: [{ type: "text", text: '{"metric":"spend","value":7}' }], structuredContent: { metric: "spend", value: 7 } };
return Response.json({ jsonrpc: "2.0", id: message.id, result });
};
const Constructor = Reflect.get(harness, "SwadsMcpClient");
expect(Constructor).toBeDefined();
const client = new Constructor({ token: "test-only-token", fetch: fetcher });
await expect(client.call("campaign_delete", {})).rejects.toThrow("not allowed");
expect(requests).toHaveLength(0);
const result = await client.call("metrics_catalog", {});
expect(result.structuredContent).toEqual({ metric: "spend", value: 7 });
expect(requests.filter((r) => r.method === "tools/call")).toEqual([{ method: "tools/call", authorization: "Bearer test-only-token" }]);
expect(JSON.stringify(result)).not.toContain("test-only-token");
await client.close();
});
it("fails closed without credentials", async () => {
const Constructor = Reflect.get(harness, "SwadsMcpClient");
expect(Constructor).toBeDefined();
const client = new Constructor({ token: "" });
await expect(client.call("metrics_catalog", {})).rejects.toThrow("SWADS_MCP_TOKEN");
});
});
+8
View File
@@ -44,6 +44,14 @@ describe("mock_ads_metrics", () => {
});
describe("tool policy", () => {
it("requires approval for real analytics and report generation but never allows arbitrary MCP mutations", () => {
expect(decideToolPolicy("swads_whoami")).toBe("allow");
expect(decideToolPolicy("metrics_catalog")).toBe("allow");
expect(decideToolPolicy("metrics_semantic_query")).toBe("ask");
expect(decideToolPolicy("generate_minis_report")).toBe("ask");
expect(decideToolPolicy("campaign_update")).toBe("deny");
});
it("allows only the explicit safe set, asks for report save, and denies unknown tools", () => {
expect(["read", "grep", "find", "ls", "mock_ads_metrics"].map(decideToolPolicy)).toEqual([
"allow", "allow", "allow", "allow", "allow",