Brand kits are validated through saveBrandKit and return their warnings; manual metrics refit the teaser model. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NLSCTnhge4umMnemGXWr1w
390 lines
14 KiB
TypeScript
390 lines
14 KiB
TypeScript
import type { MediaInferenceProvider } from "@rakazo/adapter-kit";
|
|
import type {
|
|
MediaActor,
|
|
MediaIngestService,
|
|
MediaMarketingService,
|
|
MediaPrivacyService,
|
|
MediaRenderService,
|
|
MediaServiceDeps,
|
|
MediaServices,
|
|
} from "@rakazo/adapters";
|
|
import type { PlatformProfile } from "@rakazo/contracts";
|
|
import { mediaRpcShapes } from "@rakazo/contracts";
|
|
import type { MediaRepos, PrismaClient } from "@rakazo/db";
|
|
|
|
/*
|
|
* In-memory stand-ins for the media service slices, for API tests only. They
|
|
* record what the router hands them and do the minimum through the real
|
|
* repositories; nothing here renders, detects, or publishes.
|
|
*/
|
|
|
|
export interface MediaServiceCall {
|
|
service: "ingest" | "privacy" | "render" | "marketing";
|
|
method: string;
|
|
actor: MediaActor;
|
|
input: unknown;
|
|
}
|
|
|
|
export interface FakeMediaServicesOptions {
|
|
repos: MediaRepos;
|
|
mediaRoot: string;
|
|
prisma?: PrismaClient;
|
|
inference?: MediaInferenceProvider | null;
|
|
profiles?: PlatformProfile[];
|
|
now?: () => Date;
|
|
/** Render id → absolute output path the fake render service reports. */
|
|
renderOutputs?: Map<string, string>;
|
|
onUploadCompleted?: (
|
|
actor: MediaActor,
|
|
input: { assetId: string; tempPath: string; sha256: string; fileName: string },
|
|
) => Promise<void>;
|
|
}
|
|
|
|
export function fakeMediaServices(options: FakeMediaServicesOptions): {
|
|
services: MediaServices;
|
|
calls: MediaServiceCall[];
|
|
} {
|
|
const calls: MediaServiceCall[] = [];
|
|
const { repos } = options;
|
|
const record = (
|
|
service: MediaServiceCall["service"],
|
|
method: string,
|
|
actor: MediaActor,
|
|
input: unknown,
|
|
) => {
|
|
calls.push({ service, method, actor: { spaceId: actor.spaceId, userId: actor.userId }, input });
|
|
};
|
|
|
|
const ingest: MediaIngestService = {
|
|
async pushItems(actor, input) {
|
|
record("ingest", "pushItems", actor, input);
|
|
return { created: 0, updated: 0, unchanged: 0, deleted: 0 };
|
|
},
|
|
async requestSync(actor, sourceId) {
|
|
record("ingest", "requestSync", actor, sourceId);
|
|
},
|
|
async uploadCompleted(actor, input) {
|
|
record("ingest", "uploadCompleted", actor, input);
|
|
await options.onUploadCompleted?.(actor, input);
|
|
},
|
|
async recluster(actor) {
|
|
record("ingest", "recluster", actor, null);
|
|
},
|
|
async removeSource(actor, sourceId) {
|
|
record("ingest", "removeSource", actor, sourceId);
|
|
await repos.removeSource(actor.spaceId, sourceId);
|
|
},
|
|
};
|
|
|
|
const privacy: MediaPrivacyService = {
|
|
async prepareSources() {
|
|
throw new Error("prepareSources is not part of the API fakes");
|
|
},
|
|
async gate() {
|
|
throw new Error("gate is not part of the API fakes");
|
|
},
|
|
async createMark(actor, input) {
|
|
record("privacy", "createMark", actor, input);
|
|
return repos.createMark(actor.spaceId, input);
|
|
},
|
|
async updateMark(actor, input) {
|
|
record("privacy", "updateMark", actor, input);
|
|
const { markId, ...patch } = input;
|
|
return repos.updateMark(actor.spaceId, markId, patch);
|
|
},
|
|
async removePerson(actor, personId) {
|
|
record("privacy", "removePerson", actor, personId);
|
|
await repos.removePersonCompletely(actor.spaceId, personId);
|
|
},
|
|
};
|
|
|
|
const render: MediaRenderService = {
|
|
async start(actor, input) {
|
|
record("render", "start", actor, input);
|
|
return repos.createRender(actor.spaceId, { ...input, createdBy: actor.userId });
|
|
},
|
|
async cancel(actor, renderId) {
|
|
record("render", "cancel", actor, renderId);
|
|
return repos.cancelRender(actor.spaceId, renderId);
|
|
},
|
|
async outputPath(actor, renderId, fileName) {
|
|
record("render", "outputPath", actor, { renderId, fileName });
|
|
const file = options.renderOutputs?.get(renderId);
|
|
return file?.endsWith(`/${fileName}`) ? file : null;
|
|
},
|
|
};
|
|
|
|
const marketing: MediaMarketingService = {
|
|
async autoPlan(actor, input) {
|
|
record("marketing", "autoPlan", actor, input);
|
|
return [];
|
|
},
|
|
async submitPlan(actor, input) {
|
|
record("marketing", "submitPlan", actor, input);
|
|
return { ok: false, errors: [] };
|
|
},
|
|
async introVariants(actor, input) {
|
|
record("marketing", "introVariants", actor, input);
|
|
return [];
|
|
},
|
|
async thumbnails(actor, input) {
|
|
record("marketing", "thumbnails", actor, input);
|
|
return [];
|
|
},
|
|
async copy(actor, input) {
|
|
record("marketing", "copy", actor, input);
|
|
const render = await repos.getRender(actor.spaceId, input.renderId);
|
|
return {
|
|
profileId: render.profileId,
|
|
language: input.language,
|
|
title: "Title",
|
|
hook: "Hook",
|
|
caption: "Caption",
|
|
hashtags: [],
|
|
issues: [],
|
|
};
|
|
},
|
|
async createPublication(actor, input) {
|
|
record("marketing", "createPublication", actor, input);
|
|
return repos.createPublication(actor.spaceId, actor.userId, {
|
|
...input,
|
|
trackingUrl: null,
|
|
scheduledAt: input.scheduledAt ? new Date(input.scheduledAt) : null,
|
|
});
|
|
},
|
|
async bundle(actor, publicationId) {
|
|
record("marketing", "bundle", actor, publicationId);
|
|
return repos.markPublicationBundled(actor.spaceId, publicationId, null);
|
|
},
|
|
async cancelPublication(actor, publicationId) {
|
|
record("marketing", "cancelPublication", actor, publicationId);
|
|
return repos.cancelPublication(actor.spaceId, publicationId);
|
|
},
|
|
async importMetricsCsv(actor, input) {
|
|
record("marketing", "importMetricsCsv", actor, input);
|
|
return { imported: 0, skipped: [] };
|
|
},
|
|
};
|
|
|
|
const deps = {
|
|
prisma: options.prisma,
|
|
repos,
|
|
jobs: { publish: async () => undefined },
|
|
inference: options.inference ?? null,
|
|
renderer: {
|
|
id: "fake",
|
|
hardwareEncoders: async () => ["h264_fake"],
|
|
render: async () => {
|
|
throw new Error("render is not part of the API fakes");
|
|
},
|
|
},
|
|
motion: { id: "fake", templates: () => [], render: async () => ({ path: "", hasAlpha: true }) },
|
|
publisher: { id: "fake", publish: async () => ({ status: "bundled", externalUrl: null }) },
|
|
shortener: null,
|
|
sources: {},
|
|
mediaRoot: options.mediaRoot,
|
|
profiles: async () => ({
|
|
profiles: new Map(
|
|
(options.profiles ?? []).map((profile) => [
|
|
profile.id,
|
|
{ profile, origin: "builtin" as const },
|
|
]),
|
|
),
|
|
errors: [],
|
|
}),
|
|
artifacts: {},
|
|
model: { generate: async () => null },
|
|
publish: async () => undefined,
|
|
now: options.now ?? (() => new Date()),
|
|
} as unknown as MediaServiceDeps;
|
|
|
|
return { services: { deps, ingest, privacy, render, marketing }, calls };
|
|
}
|
|
|
|
/** Every RPC path of the media contract, e.g. "sources/list". */
|
|
export function mediaProcedurePaths(): string[] {
|
|
const paths: string[] = [];
|
|
const walk = (node: Record<string, unknown>, prefix: string[]) => {
|
|
for (const [key, value] of Object.entries(node)) {
|
|
const child = value as Record<string, unknown>;
|
|
if ("input" in child && "output" in child) paths.push([...prefix, key].join("/"));
|
|
else walk(child, [...prefix, key]);
|
|
}
|
|
};
|
|
walk(mediaRpcShapes, []);
|
|
return paths;
|
|
}
|
|
|
|
export interface MediaSampleIds {
|
|
sourceId: string;
|
|
assetId: string;
|
|
secondAssetId: string;
|
|
segmentId: string;
|
|
trackId: string;
|
|
personId: string;
|
|
markId: string;
|
|
collectionId: string;
|
|
brandKitId: string;
|
|
projectId: string;
|
|
planId: string;
|
|
renderId: string;
|
|
publicationId: string;
|
|
mappingId: string;
|
|
uploadId: string;
|
|
}
|
|
|
|
/**
|
|
* How a procedure must treat another space's ids: refuse them as not found,
|
|
* answer only from the caller's own space, or be covered by a dedicated test.
|
|
*/
|
|
export type ForeignExpectation = "not_found" | "scoped" | "dedicated";
|
|
|
|
/** A valid input for every media procedure, built from one space's ids. */
|
|
export function sampleMediaInputs(
|
|
ids: MediaSampleIds,
|
|
kit: unknown,
|
|
): Record<string, { input: unknown; foreign: ForeignExpectation }> {
|
|
const at = "2026-09-01T10:00:00.000Z";
|
|
return {
|
|
capabilities: { input: {}, foreign: "scoped" },
|
|
"sources/list": { input: {}, foreign: "scoped" },
|
|
"sources/createFolder": { input: { label: "Footage", path: "/" }, foreign: "dedicated" },
|
|
"sources/registerClient": {
|
|
input: { kind: "ios_library", label: "Phone", deviceId: "device-sample" },
|
|
foreign: "scoped",
|
|
},
|
|
"sources/pushItems": {
|
|
input: { sourceId: ids.sourceId, items: [], syncToken: null, complete: true },
|
|
foreign: "not_found",
|
|
},
|
|
"sources/sync": { input: { sourceId: ids.sourceId }, foreign: "not_found" },
|
|
"sources/remove": { input: { sourceId: ids.sourceId }, foreign: "not_found" },
|
|
"assets/search": { input: {}, foreign: "scoped" },
|
|
"assets/get": { input: { assetId: ids.assetId }, foreign: "not_found" },
|
|
"assets/setPaidOnly": {
|
|
input: { segmentIds: [ids.segmentId], paidOnly: true },
|
|
foreign: "not_found",
|
|
},
|
|
"assets/requestUpload": { input: { assetIds: [ids.assetId] }, foreign: "not_found" },
|
|
"uploads/begin": {
|
|
input: {
|
|
assetId: ids.secondAssetId,
|
|
sizeBytes: 10,
|
|
sha256: "a".repeat(64),
|
|
fileName: "clip.mp4",
|
|
},
|
|
foreign: "not_found",
|
|
},
|
|
"uploads/status": { input: { uploadId: ids.uploadId }, foreign: "not_found" },
|
|
"uploads/complete": { input: { uploadId: ids.uploadId }, foreign: "not_found" },
|
|
"collections/list": { input: {}, foreign: "scoped" },
|
|
"collections/get": { input: { collectionId: ids.collectionId }, foreign: "not_found" },
|
|
"collections/rename": {
|
|
input: { collectionId: ids.collectionId, title: "Renamed" },
|
|
foreign: "not_found",
|
|
},
|
|
"collections/merge": {
|
|
input: { collectionIds: [ids.collectionId, `${ids.collectionId}x`] },
|
|
foreign: "not_found",
|
|
},
|
|
"collections/split": {
|
|
input: { collectionId: ids.collectionId, atAssetId: ids.assetId },
|
|
foreign: "not_found",
|
|
},
|
|
"collections/createManual": {
|
|
input: { title: "Picks", assetIds: [ids.assetId] },
|
|
foreign: "not_found",
|
|
},
|
|
"collections/recluster": { input: {}, foreign: "scoped" },
|
|
"people/list": { input: {}, foreign: "scoped" },
|
|
"people/update": { input: { personId: ids.personId, displayName: "X" }, foreign: "not_found" },
|
|
"people/merge": {
|
|
input: { personIds: [ids.personId, `${ids.personId}x`] },
|
|
foreign: "not_found",
|
|
},
|
|
"people/assignTrack": { input: { trackId: ids.trackId, personId: null }, foreign: "not_found" },
|
|
"people/recordConsent": {
|
|
input: {
|
|
personId: ids.personId,
|
|
consentArtifactId: null,
|
|
consentRecordedAt: at,
|
|
ageVerificationMethod: "platform_verified",
|
|
ageVerifiedAt: at,
|
|
},
|
|
foreign: "not_found",
|
|
},
|
|
"people/remove": { input: { personId: ids.personId }, foreign: "not_found" },
|
|
"marks/list": { input: { assetId: ids.assetId }, foreign: "scoped" },
|
|
"marks/create": {
|
|
input: { kind: "tattoo", assetId: ids.assetId, prompt: { tMs: 0 }, treatment: "blur" },
|
|
foreign: "not_found",
|
|
},
|
|
"marks/update": { input: { markId: ids.markId, label: "x" }, foreign: "not_found" },
|
|
"marks/remove": { input: { markId: ids.markId }, foreign: "not_found" },
|
|
"brandKits/list": { input: {}, foreign: "scoped" },
|
|
"brandKits/upsert": { input: { id: ids.brandKitId, kit }, foreign: "not_found" },
|
|
"brandKits/remove": { input: { id: ids.brandKitId }, foreign: "not_found" },
|
|
"brandKits/templates": { input: {}, foreign: "scoped" },
|
|
"brandKits/introVariants": { input: { id: ids.brandKitId }, foreign: "not_found" },
|
|
"profiles/list": { input: {}, foreign: "scoped" },
|
|
"profiles/get": { input: { profileId: "example-short-vertical" }, foreign: "scoped" },
|
|
"projects/list": { input: {}, foreign: "scoped" },
|
|
"projects/get": { input: { projectId: ids.projectId }, foreign: "not_found" },
|
|
"projects/create": {
|
|
input: {
|
|
title: "Teaser",
|
|
goal: "teaser",
|
|
collectionIds: [ids.collectionId],
|
|
profileIds: ["example-short-vertical"],
|
|
},
|
|
foreign: "not_found",
|
|
},
|
|
"projects/update": { input: { projectId: ids.projectId, title: "X" }, foreign: "not_found" },
|
|
"projects/remove": { input: { projectId: ids.projectId }, foreign: "not_found" },
|
|
"projects/autoPlan": { input: { projectId: ids.projectId }, foreign: "not_found" },
|
|
"plans/submit": { input: { projectId: ids.projectId, edl: {} }, foreign: "not_found" },
|
|
"renders/start": { input: { planId: ids.planId, quality: "draft" }, foreign: "not_found" },
|
|
"renders/get": { input: { renderId: ids.renderId }, foreign: "not_found" },
|
|
"renders/cancel": { input: { renderId: ids.renderId }, foreign: "not_found" },
|
|
"renders/thumbnails": { input: { renderId: ids.renderId }, foreign: "not_found" },
|
|
"marketing/copy": { input: { renderId: ids.renderId }, foreign: "not_found" },
|
|
"publications/list": { input: { projectId: ids.projectId }, foreign: "scoped" },
|
|
"publications/create": { input: { renderId: ids.renderId }, foreign: "not_found" },
|
|
"publications/bundle": { input: { publicationId: ids.publicationId }, foreign: "not_found" },
|
|
"publications/markPublished": {
|
|
input: { publicationId: ids.publicationId, externalUrl: null },
|
|
foreign: "not_found",
|
|
},
|
|
"publications/cancel": { input: { publicationId: ids.publicationId }, foreign: "not_found" },
|
|
"metrics/mappings": { input: {}, foreign: "scoped" },
|
|
"metrics/saveMapping": {
|
|
input: {
|
|
id: ids.mappingId,
|
|
name: "Imported",
|
|
columns: { publication: "id", observedAt: "date" },
|
|
publicationKey: "id",
|
|
},
|
|
foreign: "not_found",
|
|
},
|
|
"metrics/importCsv": {
|
|
input: { mappingId: ids.mappingId, csv: "id,date" },
|
|
foreign: "not_found",
|
|
},
|
|
"metrics/record": {
|
|
input: {
|
|
publicationId: ids.publicationId,
|
|
observedAt: at,
|
|
views: 1,
|
|
clicks: 0,
|
|
conversions: 0,
|
|
revenueCents: 0,
|
|
currency: "EUR",
|
|
},
|
|
foreign: "not_found",
|
|
},
|
|
"metrics/funnel": { input: { projectId: ids.projectId }, foreign: "not_found" },
|
|
"metrics/segments": { input: { projectId: ids.projectId }, foreign: "not_found" },
|
|
"metrics/resetTeaserModel": { input: {}, foreign: "scoped" },
|
|
};
|
|
}
|