feat: publish Agent Studio integrations and setup guide
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import { CmsConnection } from "@agent-studio/harness";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createTestApp } from "./test-app.js";
|
||||
|
||||
describe("CMS API safety", () => {
|
||||
it("exposes only redacted connection status and rejects cross-origin login before contacting CMS", async () => {
|
||||
const cms = new CmsConnection({ publishableKey: "sb_publishable_test", fetch: () => Promise.reject(new Error("Must not reach CMS")) });
|
||||
const { app } = createTestApp({ cmsForSession: () => cms });
|
||||
try {
|
||||
const status = await app.inject({ method: "GET", url: "/api/sessions/ses_1/cms/connection" });
|
||||
expect(status.statusCode).toBe(200);
|
||||
expect(status.json()).toMatchObject({ connection: { connected: false, configured: true } });
|
||||
expect(status.headers["cache-control"]).toBe("no-store");
|
||||
const denied = await app.inject({ method: "POST", url: "/api/sessions/ses_1/cms/connection", headers: { origin: "https://attacker.example", "sec-fetch-site": "cross-site" }, payload: { email: "fixture@example.com", password: "never-expose" } });
|
||||
expect(denied.statusCode).toBe(403);
|
||||
expect(denied.body).not.toContain("never-expose");
|
||||
expect((await app.inject({ method: "GET", url: "/api/sessions/ses_missing/cms/connection" })).statusCode).toBe(404);
|
||||
expect((await app.inject({ method: "GET", url: "/api/sessions/ses_1/cms/dramas" })).json()).toMatchObject({ error: { code: "CMS_LOGIN_REQUIRED" } });
|
||||
} finally { await app.close(); }
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { HarnessError } from "@agent-studio/harness";
|
||||
import { createTestApp } from "./test-app.js";
|
||||
import {mkdtemp,writeFile,rm} from "node:fs/promises";
|
||||
import {tmpdir} from "node:os";
|
||||
import {join} from "node:path";
|
||||
|
||||
describe("private editor frames", () => {
|
||||
it("only serves a registered Session frame and forbids caching", async () => {
|
||||
const { app } = createTestApp({ readEditorFrame: (sessionId, jobId, frameId) => {
|
||||
if (sessionId !== "ses_1" || jobId !== "job_known" || frameId !== "frame_known") throw new HarnessError("FRAME_NOT_FOUND", "Missing frame");
|
||||
return Promise.resolve(Buffer.from([255, 216, 255, 217]));
|
||||
} });
|
||||
try {
|
||||
const response = await app.inject({ method: "GET", url: "/api/sessions/ses_1/editor/job_known/frames/frame_known" });
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.headers["content-type"]).toBe("image/jpeg");
|
||||
expect(response.headers["cache-control"]).toBe("no-store");
|
||||
expect(response.rawPayload).toEqual(Buffer.from([255, 216, 255, 217]));
|
||||
expect((await app.inject({ method: "GET", url: "/api/sessions/ses_missing/editor/job_known/frames/frame_known" })).statusCode).toBe(404);
|
||||
expect((await app.inject({ method: "GET", url: "/api/sessions/ses_1/editor/job_known/frames/secret.env" })).statusCode).toBe(400);
|
||||
} finally { await app.close(); }
|
||||
});
|
||||
});
|
||||
|
||||
it("streams only registered media ranges and keeps review mutations same-origin",async()=>{
|
||||
const root=await mkdtemp(join(tmpdir(),"editor-api-"));const path=join(root,"preview.mp4");await writeFile(path,Buffer.from("0123456789"));
|
||||
const {app}=createTestApp({editorForSession:()=>({workflowView:()=>({jobId:"job_test"}),review:()=>Promise.resolve({saved:true}),video:()=>Promise.resolve({path,size:10})})});
|
||||
try{
|
||||
const range=await app.inject({method:"GET",url:"/api/sessions/ses_1/editor/job_test/plans/plan_test/preview",headers:{range:"bytes=2-5"}});
|
||||
expect(range.statusCode).toBe(206);expect(range.body).toBe("2345");expect(range.headers["content-range"]).toBe("bytes 2-5/10");expect(range.headers["content-type"]).toBe("video/mp4");
|
||||
expect((await app.inject({method:"GET",url:"/api/sessions/ses_1/editor/job_test/plans/plan_test/preview",headers:{range:"bytes=20-30"}})).statusCode).toBe(416);
|
||||
expect((await app.inject({method:"GET",url:"/api/sessions/ses_missing/editor/job_test/plans/plan_test/preview"})).statusCode).toBe(404);
|
||||
expect((await app.inject({method:"POST",url:"/api/sessions/ses_1/editor/job_test/review/selection",headers:{origin:"https://evil.example","content-type":"application/json"},payload:{}})).statusCode).toBeGreaterThanOrEqual(400);
|
||||
expect((await app.inject({method:"POST",url:"/api/sessions/ses_1/editor/job_test/review/selection",payload:{}})).json()).toEqual({saved:true});
|
||||
}finally{await app.close();await rm(root,{recursive:true,force:true});}
|
||||
});
|
||||
@@ -13,10 +13,43 @@ async function fixture() {
|
||||
const image = await attachments.add("ses_1", { name: "pixel.png", mediaType: "image/png", stream: Readable.from(Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAAE0lEQVQImWP4z8DwnwGM/zMwAAAf7gP9qS/A4gAAAABJRU5ErkJggg==", "base64")) });
|
||||
const service = new ImageVideoService({ root: join(root, "tasks"), attachments, remote: {} as never, collect: () => Promise.reject(new Error("No external operations permitted")) });
|
||||
const dependencies = { imageVideo: service, sessionRegistry: { get: (id: string) => id === "ses_1" ? { sessionId: id } : undefined, disposeAll: async () => {} }, skillCatalog: { list: () => [] }, modelStore: {}, attachmentStore: attachments, approvalBroker: {} } as unknown as AppDependencies;
|
||||
return { app: buildApp(dependencies), service, draft: { attachmentId: image.attachmentId, title: "Reference", prompt: "Natural motion and ambient sound", durationSeconds: 10 } };
|
||||
return { app: buildApp(dependencies), service, dependencies, draft: { attachmentId: image.attachmentId, title: "Reference", prompt: "Natural motion and ambient sound", durationSeconds: 10 } };
|
||||
}
|
||||
|
||||
describe("image video local API", () => {
|
||||
it("does not persist a target session that disappeared during continuation", async () => {
|
||||
const { app, service, dependencies, draft } = await fixture();
|
||||
const task = await service.create("ses_1", draft);
|
||||
let lookups = 0;
|
||||
dependencies.sessionRegistry.get = id => id === "ses_new" && lookups++ === 0 ? { sessionId: id } as never : undefined;
|
||||
try {
|
||||
const result = await app.inject({ method: "POST", url: `/api/sessions/ses_new/image-video/tasks/${task.id}/continue`, payload: { confirmed: true, expectedRevision: 0 } });
|
||||
expect(result.statusCode).toBe(404);
|
||||
expect((await service.owned("ses_1", task.id)).revision).toBe(0);
|
||||
} finally { await app.close(); }
|
||||
});
|
||||
it("requires an explicit same-origin confirmation to continue an orphaned task in a live session", async () => {
|
||||
const { app, service, dependencies, draft } = await fixture();
|
||||
const task = await service.create("ses_1", draft);
|
||||
const liveSession = { sessionId: "ses_new" } as never;
|
||||
dependencies.sessionRegistry.get = id => id === "ses_new" ? liveSession : undefined;
|
||||
const url = `/api/sessions/ses_new/image-video/tasks/${task.id}/continue`;
|
||||
const payload = { confirmed: true, expectedRevision: 0 };
|
||||
try {
|
||||
expect((await app.inject({ method: "POST", url, payload: { expectedRevision: 0 } })).statusCode).toBe(400);
|
||||
expect((await app.inject({ method: "POST", url, payload: { ...payload, confirmed: false } })).statusCode).toBe(400);
|
||||
expect((await app.inject({ method: "POST", url, payload, headers: { origin: "https://evil.example" } })).statusCode).toBeGreaterThanOrEqual(400);
|
||||
expect((await app.inject({ method: "POST", url, payload, headers: { "sec-fetch-site": "cross-site" } })).statusCode).toBeGreaterThanOrEqual(400);
|
||||
expect((await app.inject({ method: "POST", url: url.replace("ses_new", "ses_unknown"), payload })).statusCode).toBe(404);
|
||||
expect((await app.inject({ method: "POST", url, payload: { ...payload, prompt: "changed" } })).statusCode).toBe(400);
|
||||
await expect(service.owned("ses_new", task.id)).rejects.toMatchObject({ code: "I2V_TASK_NOT_FOUND" });
|
||||
const result = await app.inject({ method: "POST", url, payload });
|
||||
expect(result.statusCode).toBe(200);
|
||||
expect(result.json<{ task: ImageVideoPublicTask }>().task).toMatchObject({ id: task.id, state: "draft", revision: 1 });
|
||||
expect((await service.owned("ses_new", task.id)).prompt).toBe(draft.prompt);
|
||||
expect((await service.list())).toHaveLength(1);
|
||||
} finally { await app.close(); }
|
||||
});
|
||||
it("creates locally, persists history independently from Sessions and serves registered reference ranges", async () => {
|
||||
const { app, service, draft } = await fixture();
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createTestApp } from "./test-app.js";
|
||||
|
||||
describe("report download API", () => {
|
||||
it("requires a known Session and serves only a fixed report format with sandbox and no-store headers", async () => {
|
||||
const { app } = createTestApp({
|
||||
readReport: (_session, _report, format) => Promise.resolve({ bytes: Buffer.from("<!doctype html><p>report</p>"), contentType: format === "html" ? "text/html" : "application/json" }),
|
||||
});
|
||||
try {
|
||||
const response = await app.inject({ method: "GET", url: "/api/sessions/ses_1/reports/rpt_test/html" });
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body).toContain("<p>report</p>");
|
||||
expect(response.headers["cache-control"]).toBe("no-store");
|
||||
expect(response.headers["content-security-policy"]).toContain("sandbox");
|
||||
expect(response.headers["content-disposition"]).toContain("attachment");
|
||||
expect((await app.inject({ method: "GET", url: "/api/sessions/ses_missing/reports/rpt_test/html" })).statusCode).toBe(404);
|
||||
expect((await app.inject({ method: "GET", url: "/api/sessions/ses_1/reports/rpt_test/env" })).statusCode).toBe(400);
|
||||
} finally { await app.close(); }
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,9 @@
|
||||
import { HarnessError, InMemoryEventStore } from "@agent-studio/harness";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { vi } from "vitest";
|
||||
import { buildApp } from "../src/app.js";
|
||||
import { buildApp, type AppDependencies } from "../src/app.js";
|
||||
|
||||
export function createTestApp(): {
|
||||
export function createTestApp(options: { editorForSession?:AppDependencies["editorForSession"]; readReport?: AppDependencies["readReport"]; readEditorFrame?: AppDependencies["readEditorFrame"]; cmsForSession?: AppDependencies["cmsForSession"] } = {}): {
|
||||
app: FastifyInstance;
|
||||
session: { startRun: ReturnType<typeof vi.fn>; cancelRun: ReturnType<typeof vi.fn> };
|
||||
modelStore: {
|
||||
@@ -60,6 +60,10 @@ export function createTestApp(): {
|
||||
};
|
||||
const eventStore = new InMemoryEventStore();
|
||||
const app = buildApp({
|
||||
...(options.editorForSession===undefined?{}:{editorForSession:options.editorForSession}),
|
||||
...(options.readEditorFrame === undefined ? {} : { readEditorFrame: options.readEditorFrame }),
|
||||
...(options.readReport === undefined ? {} : { readReport: options.readReport }),
|
||||
...(options.cmsForSession === undefined ? {} : { cmsForSession: options.cmsForSession }),
|
||||
sessionRegistry: registry,
|
||||
skillCatalog: { list: () => [{ name: "ads-analysis", description: "Analyze", source: "project", filePath: ".agents/skills/ads-analysis/SKILL.md", diagnostics: [] }] },
|
||||
modelStore,
|
||||
|
||||
Reference in New Issue
Block a user