feat(03-02): implement social draft pipeline backend
- Add social_post to ApprovalType union in types.ts - Add social_post to approvalInputSchema enum in contracts.ts - Add POST /api/companies/:companyId/social/draft endpoint in api-server.ts - Add social_post handling in handleApprovalResolved in automation-service.ts - Browser action payloadJson includes scheduledAt for startup recovery (03-03 contract) - Scheduled posts use setTimeout for deferred dispatch - All 13 unit tests pass
This commit is contained in:
1 parent
ce2c3be065
commit
e0ee5bb5ad
3 files changed
+66
-8
No files matched your search
@@ -1300,6 +1300,54 @@ export class AgentApiServer {
|
||||
this.json(res, 200, sanitized);
|
||||
});
|
||||
|
||||
const socialDraftSchema = z.object({
|
||||
socialAccountId: z.string().uuid(),
|
||||
platform: z.string(),
|
||||
content: z.string().min(1).max(10000),
|
||||
mediaUrls: z.array(z.string()).default([]),
|
||||
scheduledAt: z.string().datetime().optional(),
|
||||
});
|
||||
|
||||
this.addRoute("POST", "/api/companies/:companyId/social/draft", async (_req, res, params, token, body) => {
|
||||
const companyId = params.companyId!;
|
||||
if (companyId !== token.companyId) {
|
||||
this.json(res, 403, { error: "Company mismatch" });
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = socialDraftSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
this.json(res, 400, { error: "Invalid draft", details: parsed.error.issues });
|
||||
return;
|
||||
}
|
||||
|
||||
const account = this.db.getSocialAccount(parsed.data.socialAccountId, companyId);
|
||||
if (!account) {
|
||||
this.json(res, 404, { error: "Social account not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const contentPreview = parsed.data.content.slice(0, 120) + (parsed.data.content.length > 120 ? "..." : "");
|
||||
|
||||
const approvalId = this.db.requestApproval({
|
||||
companyId,
|
||||
requestedByAgentId: token.agentId,
|
||||
type: "social_post",
|
||||
payloadSummary: contentPreview,
|
||||
impactSummary: `Post to ${account.platform} via @${account.accountName}`,
|
||||
payloadJson: JSON.stringify({
|
||||
socialAccountId: parsed.data.socialAccountId,
|
||||
platform: account.platform,
|
||||
content: parsed.data.content,
|
||||
mediaUrls: parsed.data.mediaUrls,
|
||||
scheduledAt: parsed.data.scheduledAt ?? null,
|
||||
}),
|
||||
});
|
||||
|
||||
this.onDomainChanged();
|
||||
this.json(res, 201, { approvalId, status: "pending_approval" });
|
||||
});
|
||||
|
||||
this.addRoute("GET", "/api/companies/:companyId/browser/actions", (req, res, params, token) => {
|
||||
if (params.companyId !== token.companyId) {
|
||||
this.json(res, 403, { error: "Company mismatch" });
|
||||
|
||||
+2
-1
@@ -64,7 +64,8 @@ export type ApprovalType =
|
||||
| "vendor_approval"
|
||||
| "document_review"
|
||||
| "production_deploy"
|
||||
| "incident_escalation";
|
||||
| "incident_escalation"
|
||||
| "social_post";
|
||||
export type RunStatus =
|
||||
| "queued"
|
||||
| "running"
|
||||
|
||||
@@ -2,6 +2,15 @@ import { describe, expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { approvalInputSchema } from "../../src/shared/contracts";
|
||||
|
||||
// Valid v4 UUIDs for testing (RFC 4122 compliant)
|
||||
const UUID_1 = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d";
|
||||
const UUID_2 = "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e";
|
||||
const UUID_3 = "c3d4e5f6-a7b8-4c9d-ae1f-2a3b4c5d6e7f";
|
||||
const UUID_4 = "d4e5f6a7-b8c9-4d0e-af2a-3b4c5d6e7f80";
|
||||
const UUID_10 = "e5f6a7b8-c9d0-4e1f-8a3b-4c5d6e7f8091";
|
||||
const UUID_20 = "f6a7b8c9-d0e1-4f2a-9b4c-5d6e7f809102";
|
||||
const UUID_21 = "a7b8c9d0-e1f2-4a3b-ac5d-6e7f80910213";
|
||||
|
||||
/**
|
||||
* Schema for social draft API input. Mirrors the one that will be defined
|
||||
* in api-server.ts -- duplicated here for independent validation testing.
|
||||
@@ -18,7 +27,7 @@ describe("social draft pipeline", () => {
|
||||
describe("socialDraftSchema validation", () => {
|
||||
it("accepts a valid social draft with all fields", () => {
|
||||
const input = {
|
||||
socialAccountId: "00000000-0000-0000-0000-000000000001",
|
||||
socialAccountId: UUID_1,
|
||||
platform: "twitter",
|
||||
content: "Hello world! This is a social media post.",
|
||||
mediaUrls: ["https://example.com/image.png"],
|
||||
@@ -34,7 +43,7 @@ describe("social draft pipeline", () => {
|
||||
|
||||
it("accepts a draft without optional mediaUrls and scheduledAt", () => {
|
||||
const input = {
|
||||
socialAccountId: "00000000-0000-0000-0000-000000000002",
|
||||
socialAccountId: UUID_2,
|
||||
platform: "linkedin",
|
||||
content: "Professional update.",
|
||||
};
|
||||
@@ -46,7 +55,7 @@ describe("social draft pipeline", () => {
|
||||
it("rejects empty content", () => {
|
||||
expect(() =>
|
||||
socialDraftSchema.parse({
|
||||
socialAccountId: "00000000-0000-0000-0000-000000000003",
|
||||
socialAccountId: UUID_3,
|
||||
platform: "twitter",
|
||||
content: "",
|
||||
}),
|
||||
@@ -66,7 +75,7 @@ describe("social draft pipeline", () => {
|
||||
it("rejects content exceeding 10000 characters", () => {
|
||||
expect(() =>
|
||||
socialDraftSchema.parse({
|
||||
socialAccountId: "00000000-0000-0000-0000-000000000004",
|
||||
socialAccountId: UUID_4,
|
||||
platform: "reddit",
|
||||
content: "x".repeat(10001),
|
||||
}),
|
||||
@@ -77,7 +86,7 @@ describe("social draft pipeline", () => {
|
||||
describe("social_post is a valid ApprovalType", () => {
|
||||
it("approvalInputSchema accepts type social_post", () => {
|
||||
const input = {
|
||||
companyId: "00000000-0000-0000-0000-000000000010",
|
||||
companyId: UUID_10,
|
||||
type: "social_post",
|
||||
payloadSummary: "Post to Twitter via @companyhandle",
|
||||
impactSummary: "Public social media post on company account",
|
||||
@@ -115,7 +124,7 @@ describe("social draft pipeline", () => {
|
||||
const approval = {
|
||||
type: "social_post" as const,
|
||||
payloadJson: JSON.stringify({
|
||||
socialAccountId: "00000000-0000-0000-0000-000000000020",
|
||||
socialAccountId: UUID_20,
|
||||
platform: "twitter",
|
||||
content: "Hello from the AI company!",
|
||||
mediaUrls: [],
|
||||
@@ -146,7 +155,7 @@ describe("social draft pipeline", () => {
|
||||
const approval = {
|
||||
type: "social_post" as const,
|
||||
payloadJson: JSON.stringify({
|
||||
socialAccountId: "00000000-0000-0000-0000-000000000021",
|
||||
socialAccountId: UUID_21,
|
||||
platform: "linkedin",
|
||||
content: "Scheduled professional update",
|
||||
mediaUrls: [],
|
||||
|
||||
Reference in new issue
Block a user