Add verification API routes and core client
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -211,3 +211,7 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
coverage/
|
coverage/
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Allow TypeScript library utilities under src/lib
|
||||||
|
!src/lib/
|
||||||
|
!src/lib/**
|
||||||
|
|||||||
@@ -7,10 +7,14 @@ const parsePort = (value: string | undefined, fallback: number): number => {
|
|||||||
return Number.isFinite(parsed) ? parsed : fallback;
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultCoreBaseUrl = process.env.CORE_BASE_URL || "http://localhost:3001";
|
||||||
|
|
||||||
export const env = {
|
export const env = {
|
||||||
PORT: parsePort(process.env.PORT, 8080),
|
PORT: parsePort(process.env.PORT, 8080),
|
||||||
HOST: process.env.HOST || "0.0.0.0",
|
HOST: process.env.HOST || "0.0.0.0",
|
||||||
CORE_BASE_URL: process.env.CORE_BASE_URL || "http://localhost:3001",
|
CORE_BASE_URL: defaultCoreBaseUrl,
|
||||||
|
CORE_VERIFICATION_BASE_URL:
|
||||||
|
process.env.CORE_VERIFICATION_BASE_URL || `${defaultCoreBaseUrl}/internal`,
|
||||||
AGENTS_BASE_URL: process.env.AGENTS_BASE_URL || "http://localhost:3002",
|
AGENTS_BASE_URL: process.env.AGENTS_BASE_URL || "http://localhost:3002",
|
||||||
OPERATOR_BASE_URL: process.env.OPERATOR_BASE_URL || "http://localhost:3003",
|
OPERATOR_BASE_URL: process.env.OPERATOR_BASE_URL || "http://localhost:3003",
|
||||||
SERVICE_VERSION:
|
SERVICE_VERSION:
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export const OS_ROOT = process.env.OS_ROOT || "https://blackroad.systems";
|
|||||||
|
|
||||||
export const CORE_BASE_URL =
|
export const CORE_BASE_URL =
|
||||||
process.env.CORE_BASE_URL || "https://core.blackroad.systems";
|
process.env.CORE_BASE_URL || "https://core.blackroad.systems";
|
||||||
|
export const CORE_VERIFICATION_BASE_URL =
|
||||||
|
process.env.CORE_VERIFICATION_BASE_URL || `${CORE_BASE_URL}/internal`;
|
||||||
export const OPERATOR_BASE_URL =
|
export const OPERATOR_BASE_URL =
|
||||||
process.env.OPERATOR_BASE_URL || "https://operator.blackroad.systems";
|
process.env.OPERATOR_BASE_URL || "https://operator.blackroad.systems";
|
||||||
|
|
||||||
@@ -15,5 +17,6 @@ export const serviceConfig = {
|
|||||||
SERVICE_BASE_URL,
|
SERVICE_BASE_URL,
|
||||||
OS_ROOT,
|
OS_ROOT,
|
||||||
CORE_BASE_URL,
|
CORE_BASE_URL,
|
||||||
|
CORE_VERIFICATION_BASE_URL,
|
||||||
OPERATOR_BASE_URL,
|
OPERATOR_BASE_URL,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import infoRouter from "./routes/info";
|
|||||||
import versionRouter from "./routes/version";
|
import versionRouter from "./routes/version";
|
||||||
import pingRouter from "./routes/v1/ping";
|
import pingRouter from "./routes/v1/ping";
|
||||||
import v1HealthRouter from "./routes/v1/health";
|
import v1HealthRouter from "./routes/v1/health";
|
||||||
|
import verificationRouter from "./routes/v1/verify";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ app.use(infoRouter);
|
|||||||
app.use(versionRouter);
|
app.use(versionRouter);
|
||||||
app.use("/v1", pingRouter);
|
app.use("/v1", pingRouter);
|
||||||
app.use("/v1", v1HealthRouter);
|
app.use("/v1", v1HealthRouter);
|
||||||
|
app.use("/v1", verificationRouter);
|
||||||
|
|
||||||
// Proxy routes
|
// Proxy routes
|
||||||
app.use("/core", createProxyRouter(serviceClients.core));
|
app.use("/core", createProxyRouter(serviceClients.core));
|
||||||
|
|||||||
160
src/lib/coreVerificationClient.ts
Normal file
160
src/lib/coreVerificationClient.ts
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import axios, { AxiosError, AxiosInstance } from "axios";
|
||||||
|
import { env } from "../config/env";
|
||||||
|
|
||||||
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
||||||
|
|
||||||
|
export type VerificationJobPayload = {
|
||||||
|
text: string;
|
||||||
|
source_uri?: string;
|
||||||
|
author_id?: string;
|
||||||
|
claim_hash?: string;
|
||||||
|
domain?: string;
|
||||||
|
policy_id?: string;
|
||||||
|
requested_by?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CoreAssessment = {
|
||||||
|
agent_id?: string;
|
||||||
|
verdict?: string;
|
||||||
|
confidence?: number;
|
||||||
|
created_at?: string;
|
||||||
|
evidence_uris?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CoreSnapshot = {
|
||||||
|
id: string;
|
||||||
|
source_uri?: string;
|
||||||
|
author_id?: string;
|
||||||
|
parent_snapshot_id?: string | null;
|
||||||
|
created_at?: string;
|
||||||
|
ps_sha_infinity?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CoreTruthState = {
|
||||||
|
claim_hash: string;
|
||||||
|
status: string;
|
||||||
|
aggregate_confidence?: number | null;
|
||||||
|
job_ids?: string[];
|
||||||
|
minority_reports?: string[];
|
||||||
|
last_updated?: string;
|
||||||
|
policy_id?: string;
|
||||||
|
domain?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CoreVerificationJob = {
|
||||||
|
job_id: string;
|
||||||
|
snapshot_id?: string;
|
||||||
|
claim_hash?: string;
|
||||||
|
status: string;
|
||||||
|
created_at?: string;
|
||||||
|
domain?: string;
|
||||||
|
policy_id?: string;
|
||||||
|
truth_state?: CoreTruthState | null;
|
||||||
|
snapshot?: CoreSnapshot;
|
||||||
|
assessments?: CoreAssessment[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CoreProvenanceGraph = {
|
||||||
|
snapshot: CoreSnapshot;
|
||||||
|
parent_snapshot?: CoreSnapshot | null;
|
||||||
|
derived_snapshots?: CoreSnapshot[];
|
||||||
|
related_jobs?: { id: string; status?: string }[];
|
||||||
|
ledger_entries?: { id: string; type: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export class CoreVerificationError extends Error {
|
||||||
|
status?: number;
|
||||||
|
data?: any;
|
||||||
|
|
||||||
|
constructor(message: string, status?: number, data?: any) {
|
||||||
|
super(message);
|
||||||
|
this.status = status;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createClient = (): AxiosInstance =>
|
||||||
|
axios.create({
|
||||||
|
baseURL: env.CORE_VERIFICATION_BASE_URL,
|
||||||
|
timeout: DEFAULT_TIMEOUT_MS,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapAxiosError = (error: AxiosError): CoreVerificationError => {
|
||||||
|
const status = error.response?.status;
|
||||||
|
const data = error.response?.data;
|
||||||
|
const message =
|
||||||
|
(data && typeof data === "object" && "message" in data
|
||||||
|
? (data as Record<string, any>).message
|
||||||
|
: undefined) || error.message;
|
||||||
|
|
||||||
|
return new CoreVerificationError(message, status, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface VerificationServiceClient {
|
||||||
|
createVerificationJob(
|
||||||
|
payload: VerificationJobPayload
|
||||||
|
): Promise<CoreVerificationJob>;
|
||||||
|
getVerificationJob(jobId: string): Promise<CoreVerificationJob>;
|
||||||
|
getTruthState(claimHash: string): Promise<CoreTruthState>;
|
||||||
|
getProvenance(snapshotId: string): Promise<CoreProvenanceGraph>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CoreVerificationClient implements VerificationServiceClient {
|
||||||
|
private client: AxiosInstance;
|
||||||
|
|
||||||
|
constructor(client: AxiosInstance = createClient()) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createVerificationJob(
|
||||||
|
payload: VerificationJobPayload
|
||||||
|
): Promise<CoreVerificationJob> {
|
||||||
|
try {
|
||||||
|
const response = await this.client.post("/verification/jobs", payload);
|
||||||
|
return response.data as CoreVerificationJob;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
throw mapAxiosError(error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getVerificationJob(jobId: string): Promise<CoreVerificationJob> {
|
||||||
|
try {
|
||||||
|
const response = await this.client.get(`/verification/jobs/${jobId}`);
|
||||||
|
return response.data as CoreVerificationJob;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
throw mapAxiosError(error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTruthState(claimHash: string): Promise<CoreTruthState> {
|
||||||
|
try {
|
||||||
|
const response = await this.client.get(`/truth/${claimHash}`);
|
||||||
|
return response.data as CoreTruthState;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
throw mapAxiosError(error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProvenance(snapshotId: string): Promise<CoreProvenanceGraph> {
|
||||||
|
try {
|
||||||
|
const response = await this.client.get(`/provenance/${snapshotId}`);
|
||||||
|
return response.data as CoreProvenanceGraph;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
throw mapAxiosError(error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const coreVerificationClient = new CoreVerificationClient();
|
||||||
220
src/routes/v1/verify.ts
Normal file
220
src/routes/v1/verify.ts
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
import { Request, Response, Router } from "express";
|
||||||
|
import {
|
||||||
|
CoreAssessment,
|
||||||
|
CoreProvenanceGraph,
|
||||||
|
CoreSnapshot,
|
||||||
|
CoreTruthState,
|
||||||
|
CoreVerificationJob,
|
||||||
|
CoreVerificationError,
|
||||||
|
VerificationServiceClient,
|
||||||
|
VerificationJobPayload,
|
||||||
|
coreVerificationClient,
|
||||||
|
} from "../../lib/coreVerificationClient";
|
||||||
|
|
||||||
|
const sanitizeAgentId = (agentId?: string): string | undefined => {
|
||||||
|
if (!agentId) return undefined;
|
||||||
|
const parts = agentId.split(":");
|
||||||
|
return parts.length > 1 ? parts.slice(-1)[0] : agentId;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapAssessments = (assessments?: CoreAssessment[]) => {
|
||||||
|
const items = (assessments || []).map((assessment) => ({
|
||||||
|
agent_id: sanitizeAgentId(assessment.agent_id),
|
||||||
|
verdict: assessment.verdict,
|
||||||
|
confidence: assessment.confidence ?? null,
|
||||||
|
created_at: assessment.created_at,
|
||||||
|
evidence_uris: assessment.evidence_uris || [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
count: items.length,
|
||||||
|
items,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapSnapshot = (snapshot?: CoreSnapshot) => {
|
||||||
|
if (!snapshot) return null;
|
||||||
|
return {
|
||||||
|
id: snapshot.id,
|
||||||
|
source_uri: snapshot.source_uri,
|
||||||
|
author_id: snapshot.author_id,
|
||||||
|
parent_snapshot_id: snapshot.parent_snapshot_id,
|
||||||
|
created_at: snapshot.created_at,
|
||||||
|
hash: snapshot.ps_sha_infinity,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapTruthState = (truth?: CoreTruthState | null) => {
|
||||||
|
if (!truth) return null;
|
||||||
|
return {
|
||||||
|
claim_hash: truth.claim_hash,
|
||||||
|
status: truth.status,
|
||||||
|
aggregate_confidence: truth.aggregate_confidence ?? null,
|
||||||
|
job_ids: truth.job_ids || [],
|
||||||
|
minority_reports: truth.minority_reports || [],
|
||||||
|
last_updated: truth.last_updated,
|
||||||
|
policy_id: truth.policy_id,
|
||||||
|
domain: truth.domain,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapJobSummary = (job: CoreVerificationJob) => ({
|
||||||
|
id: job.job_id,
|
||||||
|
snapshot_id: job.snapshot_id || job.snapshot?.id,
|
||||||
|
claim_hash: job.claim_hash || job.truth_state?.claim_hash || null,
|
||||||
|
status: job.status,
|
||||||
|
created_at: job.created_at,
|
||||||
|
policy_id: job.policy_id,
|
||||||
|
domain: job.domain,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapProvenance = (graph: CoreProvenanceGraph) => ({
|
||||||
|
snapshot: mapSnapshot(graph.snapshot),
|
||||||
|
parent_snapshot: mapSnapshot(graph.parent_snapshot || undefined),
|
||||||
|
derived_snapshots: (graph.derived_snapshots || []).map(mapSnapshot),
|
||||||
|
related_jobs: graph.related_jobs || [],
|
||||||
|
ledger_entries: graph.ledger_entries || [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const buildValidationError = (message: string, details?: Record<string, any>) => ({
|
||||||
|
error_code: "INVALID_REQUEST",
|
||||||
|
message,
|
||||||
|
details,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCoreError = (res: Response, error: unknown) => {
|
||||||
|
if (error instanceof CoreVerificationError) {
|
||||||
|
const status = error.status || 502;
|
||||||
|
const payload =
|
||||||
|
error.data && typeof error.data === "object"
|
||||||
|
? error.data
|
||||||
|
: {
|
||||||
|
error_code:
|
||||||
|
status === 404
|
||||||
|
? "VERIFICATION_RESOURCE_NOT_FOUND"
|
||||||
|
: "CORE_VERIFICATION_ERROR",
|
||||||
|
message: error.message,
|
||||||
|
};
|
||||||
|
return res.status(status).json(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
return res.status(502).json({
|
||||||
|
error_code: "CORE_VERIFICATION_ERROR",
|
||||||
|
message: "Core verification service unavailable",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseVerifyRequest = (body: any): VerificationJobPayload | null => {
|
||||||
|
if (!body || typeof body.text !== "string" || !body.text.trim()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: VerificationJobPayload = {
|
||||||
|
text: body.text,
|
||||||
|
};
|
||||||
|
|
||||||
|
const optionalFields: (keyof VerificationJobPayload)[] = [
|
||||||
|
"source_uri",
|
||||||
|
"author_id",
|
||||||
|
"claim_hash",
|
||||||
|
"domain",
|
||||||
|
"policy_id",
|
||||||
|
"requested_by",
|
||||||
|
];
|
||||||
|
|
||||||
|
optionalFields.forEach((field) => {
|
||||||
|
const value = body[field];
|
||||||
|
if (value !== undefined) {
|
||||||
|
payload[field] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createVerificationRouter = (
|
||||||
|
client: VerificationServiceClient = coreVerificationClient
|
||||||
|
) => {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.post("/verify", async (req: Request, res: Response) => {
|
||||||
|
const payload = parseVerifyRequest(req.body);
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json(buildValidationError("`text` is required for verification"));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const job = await client.createVerificationJob(payload);
|
||||||
|
return res.status(202).json({
|
||||||
|
job: mapJobSummary(job),
|
||||||
|
snapshot: mapSnapshot(job.snapshot) || null,
|
||||||
|
truth_state: mapTruthState(job.truth_state),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return handleCoreError(res, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/verify/jobs/:jobId", async (req: Request, res: Response) => {
|
||||||
|
const { jobId } = req.params;
|
||||||
|
if (!jobId) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json(buildValidationError("`job_id` must be provided"));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const job = await client.getVerificationJob(jobId);
|
||||||
|
return res.json({
|
||||||
|
job: mapJobSummary(job),
|
||||||
|
snapshot: mapSnapshot(job.snapshot) || null,
|
||||||
|
assessments: mapAssessments(job.assessments),
|
||||||
|
truth_state: mapTruthState(job.truth_state),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return handleCoreError(res, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/truth/:claimHash", async (req: Request, res: Response) => {
|
||||||
|
const { claimHash } = req.params;
|
||||||
|
if (!claimHash) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json(buildValidationError("`claim_hash` must be provided"));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const truth = await client.getTruthState(claimHash);
|
||||||
|
return res.json(mapTruthState(truth));
|
||||||
|
} catch (error) {
|
||||||
|
return handleCoreError(res, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/provenance/:snapshotId", async (req: Request, res: Response) => {
|
||||||
|
const { snapshotId } = req.params;
|
||||||
|
if (!snapshotId) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json(buildValidationError("`snapshot_id` must be provided"));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const provenance = await client.getProvenance(snapshotId);
|
||||||
|
return res.json(mapProvenance(provenance));
|
||||||
|
} catch (error) {
|
||||||
|
return handleCoreError(res, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
|
|
||||||
|
const verificationRouter = createVerificationRouter();
|
||||||
|
|
||||||
|
export default verificationRouter;
|
||||||
211
tests/verify.test.ts
Normal file
211
tests/verify.test.ts
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import express from "express";
|
||||||
|
import request from "supertest";
|
||||||
|
import {
|
||||||
|
CoreVerificationError,
|
||||||
|
VerificationServiceClient,
|
||||||
|
} from "../src/lib/coreVerificationClient";
|
||||||
|
import { createVerificationRouter } from "../src/routes/v1/verify";
|
||||||
|
|
||||||
|
describe("Verification routes", () => {
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
const mockClient: jest.Mocked<VerificationServiceClient> = {
|
||||||
|
createVerificationJob: jest.fn(),
|
||||||
|
getVerificationJob: jest.fn(),
|
||||||
|
getTruthState: jest.fn(),
|
||||||
|
getProvenance: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
app.use("/v1", createVerificationRouter(mockClient));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("POST /v1/verify", () => {
|
||||||
|
it("creates a verification job", async () => {
|
||||||
|
mockClient.createVerificationJob.mockResolvedValue({
|
||||||
|
job_id: "job-123",
|
||||||
|
snapshot_id: "snap-1",
|
||||||
|
claim_hash: "claim-xyz",
|
||||||
|
status: "pending",
|
||||||
|
created_at: "2024-07-01T00:00:00Z",
|
||||||
|
policy_id: "policy-1",
|
||||||
|
domain: "news",
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app).post("/v1/verify").send({
|
||||||
|
text: "Example text",
|
||||||
|
domain: "news",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(202);
|
||||||
|
expect(response.body).toMatchObject({
|
||||||
|
job: {
|
||||||
|
id: "job-123",
|
||||||
|
snapshot_id: "snap-1",
|
||||||
|
status: "pending",
|
||||||
|
domain: "news",
|
||||||
|
},
|
||||||
|
truth_state: null,
|
||||||
|
});
|
||||||
|
expect(mockClient.createVerificationJob).toHaveBeenCalledWith({
|
||||||
|
text: "Example text",
|
||||||
|
domain: "news",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validates missing text", async () => {
|
||||||
|
const response = await request(app).post("/v1/verify").send({});
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(response.body).toHaveProperty("error_code", "INVALID_REQUEST");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("GET /v1/verify/jobs/:jobId", () => {
|
||||||
|
it("returns job details", async () => {
|
||||||
|
mockClient.getVerificationJob.mockResolvedValue({
|
||||||
|
job_id: "job-123",
|
||||||
|
snapshot_id: "snap-1",
|
||||||
|
claim_hash: "claim-xyz",
|
||||||
|
status: "running",
|
||||||
|
created_at: "2024-07-01T00:00:00Z",
|
||||||
|
assessments: [
|
||||||
|
{
|
||||||
|
agent_id: "agent:alpha",
|
||||||
|
verdict: "confirmed",
|
||||||
|
confidence: 0.92,
|
||||||
|
created_at: "2024-07-01T01:00:00Z",
|
||||||
|
evidence_uris: ["https://example.com"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
truth_state: {
|
||||||
|
claim_hash: "claim-xyz",
|
||||||
|
status: "confirmed",
|
||||||
|
aggregate_confidence: 0.9,
|
||||||
|
job_ids: ["job-123"],
|
||||||
|
minority_reports: [],
|
||||||
|
last_updated: "2024-07-01T02:00:00Z",
|
||||||
|
policy_id: "policy-1",
|
||||||
|
domain: "news",
|
||||||
|
},
|
||||||
|
snapshot: {
|
||||||
|
id: "snap-1",
|
||||||
|
source_uri: "https://example.com/post",
|
||||||
|
author_id: "author-1",
|
||||||
|
parent_snapshot_id: null,
|
||||||
|
created_at: "2024-07-01T00:00:00Z",
|
||||||
|
ps_sha_infinity: "hash123",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app).get("/v1/verify/jobs/job-123");
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.job).toMatchObject({ id: "job-123", status: "running" });
|
||||||
|
expect(response.body.snapshot).toMatchObject({ id: "snap-1", hash: "hash123" });
|
||||||
|
expect(response.body.assessments.count).toBe(1);
|
||||||
|
expect(response.body.truth_state).toMatchObject({ status: "confirmed" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps 404 errors from core", async () => {
|
||||||
|
mockClient.getVerificationJob.mockRejectedValue(
|
||||||
|
new CoreVerificationError("Not found", 404, {
|
||||||
|
error_code: "VERIFICATION_JOB_NOT_FOUND",
|
||||||
|
message: "Verification job not found",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await request(app).get("/v1/verify/jobs/unknown");
|
||||||
|
|
||||||
|
expect(response.status).toBe(404);
|
||||||
|
expect(response.body).toHaveProperty("error_code", "VERIFICATION_JOB_NOT_FOUND");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("GET /v1/truth/:claimHash", () => {
|
||||||
|
it("returns truth state", async () => {
|
||||||
|
mockClient.getTruthState.mockResolvedValue({
|
||||||
|
claim_hash: "claim-xyz",
|
||||||
|
status: "confirmed",
|
||||||
|
aggregate_confidence: 0.8,
|
||||||
|
job_ids: ["job-1"],
|
||||||
|
minority_reports: [],
|
||||||
|
last_updated: "2024-07-01T03:00:00Z",
|
||||||
|
policy_id: "policy-1",
|
||||||
|
domain: "news",
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app).get("/v1/truth/claim-xyz");
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toMatchObject({
|
||||||
|
claim_hash: "claim-xyz",
|
||||||
|
status: "confirmed",
|
||||||
|
aggregate_confidence: 0.8,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 404 for unknown claim", async () => {
|
||||||
|
mockClient.getTruthState.mockRejectedValue(
|
||||||
|
new CoreVerificationError("Unknown claim", 404, {
|
||||||
|
error_code: "TRUTH_STATE_NOT_FOUND",
|
||||||
|
message: "Truth state not found",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await request(app).get("/v1/truth/missing-claim");
|
||||||
|
|
||||||
|
expect(response.status).toBe(404);
|
||||||
|
expect(response.body).toHaveProperty("error_code", "TRUTH_STATE_NOT_FOUND");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("GET /v1/provenance/:snapshotId", () => {
|
||||||
|
it("returns provenance graph", async () => {
|
||||||
|
mockClient.getProvenance.mockResolvedValue({
|
||||||
|
snapshot: {
|
||||||
|
id: "snap-1",
|
||||||
|
source_uri: "https://example.com/post",
|
||||||
|
author_id: "author-1",
|
||||||
|
parent_snapshot_id: null,
|
||||||
|
created_at: "2024-07-01T00:00:00Z",
|
||||||
|
ps_sha_infinity: "hash123",
|
||||||
|
},
|
||||||
|
parent_snapshot: null,
|
||||||
|
derived_snapshots: [
|
||||||
|
{
|
||||||
|
id: "snap-2",
|
||||||
|
parent_snapshot_id: "snap-1",
|
||||||
|
created_at: "2024-07-01T02:00:00Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
related_jobs: [{ id: "job-1", status: "pending" }],
|
||||||
|
ledger_entries: [{ id: "ledger-1", type: "snapshot" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app).get("/v1/provenance/snap-1");
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.snapshot).toMatchObject({ id: "snap-1" });
|
||||||
|
expect(response.body.derived_snapshots[0]).toMatchObject({ id: "snap-2" });
|
||||||
|
expect(response.body.related_jobs[0]).toMatchObject({ id: "job-1" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps missing snapshot to 404", async () => {
|
||||||
|
mockClient.getProvenance.mockRejectedValue(
|
||||||
|
new CoreVerificationError("Not found", 404, {
|
||||||
|
error_code: "SNAPSHOT_NOT_FOUND",
|
||||||
|
message: "Snapshot not found",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await request(app).get("/v1/provenance/unknown");
|
||||||
|
|
||||||
|
expect(response.status).toBe(404);
|
||||||
|
expect(response.body).toHaveProperty("error_code", "SNAPSHOT_NOT_FOUND");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user