{"path":"oracle/src/__tests__/index.test.ts","size":14832,"locked":false,"binary":false,"truncated":false,"content":"import { describe, it, expect, vi, beforeEach, beforeAll, afterAll } from \"vitest\";\nimport { PublicKey } from \"@solana/web3.js\";\nimport BN from \"bn.js\";\nimport request from \"supertest\";\nimport express, { Request, Response, NextFunction } from \"express\";\nimport { store } from \"../store\";\nimport { GigEscrow, StoredJob, SubmissionArtifact, ConsensusResult } from \"../types\";\n\n// ─── HELPERS ──────────────────────────────────────────────────────────────────\n\nfunction makeEscrow(overrides: Partial<GigEscrow> = {}): GigEscrow {\n  return {\n    client: PublicKey.default,\n    freelancer: PublicKey.default,\n    oracle: PublicKey.default,\n    amount: new BN(2_000_000_000),\n    jobId: \"http-test-001\",\n    status: { pending: {} },\n    escrowBump: 255,\n    vaultBump: 254,\n    ...overrides,\n  };\n}\n\n// ─── BUILD A MINIMAL EXPRESS APP ──────────────────────────────────────────────\n// We recreate the route handlers from index.ts without the bootstrap/sentry/listener\n// dependencies. This tests the HTTP layer in isolation.\n\nfunction buildTestApp(\n  mockRunConsensus?: (job: StoredJob, artifact: SubmissionArtifact) => Promise<ConsensusResult>,\n  mockReleasePayment?: (job: StoredJob) => Promise<string>,\n  mockCancelJob?: (job: StoredJob) => Promise<string>\n) {\n  const app = express();\n  app.use(express.json());\n\n  app.get(\"/health\", (_req: Request, res: Response) => {\n    res.json({\n      status: \"ok\",\n      pendingJobs: store.size(),\n      timestamp: new Date().toISOString(),\n    });\n  });\n\n  app.get(\"/jobs/:jobId\", (req: Request, res: Response) => {\n    const { jobId } = req.params;\n    const job = store.get(jobId);\n\n    if (!job) {\n      res.status(404).json({ error: \"Job not found in oracle store\" });\n      return;\n    }\n\n    res.json({\n      jobId,\n      escrowPubkey: job.escrowPubkey.toBase58(),\n      client: job.escrow.client.toBase58(),\n      freelancer: job.escrow.freelancer.toBase58(),\n      amount: job.escrow.amount.toString(),\n      status: job.escrow.status,\n      detectedAt: job.detectedAt,\n    });\n  });\n\n  app.post(\"/submit\", async (req: Request, res: Response) => {\n    const body = req.body;\n\n    if (\n      !body.jobId ||\n      !body.description ||\n      !Array.isArray(body.acceptanceCriteria) ||\n      body.acceptanceCriteria.length === 0 ||\n      !body.deliverable ||\n      !body.deliverableType\n    ) {\n      res.status(400).json({\n        success: false,\n        jobId: body.jobId ?? \"\",\n        error: \"Missing required fields: jobId, description, acceptanceCriteria, deliverable, deliverableType\",\n      });\n      return;\n    }\n\n    const job = store.get(body.jobId);\n\n    if (!job) {\n      res.status(404).json({\n        success: false,\n        jobId: body.jobId,\n        error: \"Job not found. Either the job does not exist, has already been resolved, or the oracle has not yet detected the on-chain event. If just initialized, wait a few seconds and retry.\",\n      });\n      return;\n    }\n\n    if (!(\"pending\" in job.escrow.status)) {\n      res.status(409).json({\n        success: false,\n        jobId: body.jobId,\n        error: \"Job is no longer in Pending state. It has already been resolved.\",\n      });\n      return;\n    }\n\n    const artifact: SubmissionArtifact = {\n      jobId: body.jobId,\n      description: body.description,\n      acceptanceCriteria: body.acceptanceCriteria,\n      deliverable: body.deliverable,\n      deliverableType: body.deliverableType,\n      submittedAt: Date.now(),\n    };\n\n    try {\n      const result = await mockRunConsensus!(job, artifact);\n      let txSig: string | undefined;\n\n      if (result.outcome === \"RELEASE\") {\n        txSig = await mockReleasePayment!(job);\n        store.remove(body.jobId);\n      } else if (result.outcome === \"REFUND\") {\n        txSig = await mockCancelJob!(job);\n        store.remove(body.jobId);\n      }\n\n      res.json({\n        success: true,\n        jobId: body.jobId,\n        outcome: result.outcome,\n        txSig,\n      });\n    } catch (err: unknown) {\n      const message = err instanceof Error ? err.message : String(err);\n      res.status(500).json({\n        success: false,\n        jobId: body.jobId,\n        error: `Internal oracle error: ${message}`,\n      });\n    }\n  });\n\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {\n    res.status(500).json({ error: \"Internal server error\" });\n  });\n\n  return app;\n}\n\n// ─── TEST SUITE ───────────────────────────────────────────────────────────────\n\ndescribe(\"HTTP endpoints\", () => {\n  beforeEach(() => {\n    // Clear the store\n    for (const job of store.allPending()) {\n      store.remove(job.escrow.jobId);\n    }\n  });\n\n  // ── /health ───────────────────────────────────────────────────────────────\n\n  describe(\"GET /health\", () => {\n    it(\"returns 200 with status ok and pending job count\", async () => {\n      const app = buildTestApp();\n\n      const res = await request(app).get(\"/health\");\n\n      expect(res.status).toBe(200);\n      expect(res.body.status).toBe(\"ok\");\n      expect(res.body.pendingJobs).toBe(0);\n      expect(res.body.timestamp).toBeDefined();\n    });\n\n    it(\"reflects pending job count\", async () => {\n      store.upsert(\n        \"health-job-1\",\n        PublicKey.default,\n        makeEscrow({ jobId: \"health-job-1\" })\n      );\n\n      const app = buildTestApp();\n      const res = await request(app).get(\"/health\");\n\n      expect(res.body.pendingJobs).toBe(1);\n      store.remove(\"health-job-1\");\n    });\n  });\n\n  // ── /jobs/:jobId ──────────────────────────────────────────────────────────\n\n  describe(\"GET /jobs/:jobId\", () => {\n    it(\"returns 404 for unknown job\", async () => {\n      const app = buildTestApp();\n      const res = await request(app).get(\"/jobs/unknown-id\");\n\n      expect(res.status).toBe(404);\n      expect(res.body.error).toContain(\"Job not found\");\n    });\n\n    it(\"returns job details for existing job\", async () => {\n      const escrow = makeEscrow({ jobId: \"job-detail-1\" });\n      store.upsert(\"job-detail-1\", PublicKey.default, escrow);\n\n      const app = buildTestApp();\n      const res = await request(app).get(\"/jobs/job-detail-1\");\n\n      expect(res.status).toBe(200);\n      expect(res.body.jobId).toBe(\"job-detail-1\");\n      expect(res.body.escrowPubkey).toBe(PublicKey.default.toBase58());\n      expect(res.body.amount).toBe(\"2000000000\");\n      expect(res.body.status).toEqual({ pending: {} });\n\n      store.remove(\"job-detail-1\");\n    });\n  });\n\n  // ── POST /submit ──────────────────────────────────────────────────────────\n\n  describe(\"POST /submit\", () => {\n    it(\"returns 400 when required fields are missing\", async () => {\n      const app = buildTestApp();\n      const res = await request(app).post(\"/submit\").send({\n        jobId: \"test\",\n        // missing other fields\n      });\n\n      expect(res.status).toBe(400);\n      expect(res.body.success).toBe(false);\n      expect(res.body.error).toContain(\"Missing required fields\");\n    });\n\n    it(\"returns 400 when acceptanceCriteria is empty\", async () => {\n      const app = buildTestApp();\n      const res = await request(app).post(\"/submit\").send({\n        jobId: \"test\",\n        description: \"Build something\",\n        acceptanceCriteria: [],\n        deliverable: \"https://example.com\",\n        deliverableType: \"url\",\n      });\n\n      expect(res.status).toBe(400);\n      expect(res.body.success).toBe(false);\n    });\n\n    it(\"returns 404 when job is not in store\", async () => {\n      const app = buildTestApp();\n      const res = await request(app).post(\"/submit\").send({\n        jobId: \"not-in-store\",\n        description: \"Build something\",\n        acceptanceCriteria: [\"Responsive\"],\n        deliverable: \"https://example.com\",\n        deliverableType: \"url\",\n      });\n\n      expect(res.status).toBe(404);\n      expect(res.body.success).toBe(false);\n      expect(res.body.error).toContain(\"Job not found\");\n    });\n\n    it(\"returns 409 when job is not pending\", async () => {\n      store.upsert(\n        \"completed-job\",\n        PublicKey.default,\n        makeEscrow({ jobId: \"completed-job\", status: { completed: {} } })\n      );\n\n      const app = buildTestApp();\n      const res = await request(app).post(\"/submit\").send({\n        jobId: \"completed-job\",\n        description: \"Build something\",\n        acceptanceCriteria: [\"Responsive\"],\n        deliverable: \"https://example.com\",\n        deliverableType: \"url\",\n      });\n\n      expect(res.status).toBe(409);\n      expect(res.body.success).toBe(false);\n      expect(res.body.error).toContain(\"no longer in Pending state\");\n      store.remove(\"completed-job\");\n    });\n\n    it(\"returns RELEASE outcome and removes job from store\", async () => {\n      store.upsert(\n        \"release-me\",\n        PublicKey.default,\n        makeEscrow({ jobId: \"release-me\" })\n      );\n\n      const mockConsensus = vi.fn().mockResolvedValue({\n        outcome: \"RELEASE\",\n        geminiVerdict: {\n          model: \"gemini\",\n          verdict: \"APPROVED\",\n          confidence: 0.95,\n          reasoning: \"Good\",\n          criteriaMet: [\"A\"],\n          criteriaFailed: [],\n        },\n        claudeVerdict: {\n          model: \"claude\",\n          verdict: \"APPROVED\",\n          confidence: 0.90,\n          reasoning: \"Good\",\n          criteriaMet: [\"A\"],\n          criteriaFailed: [],\n        },\n        reasoning: \"Both approved\",\n        processedAt: Date.now(),\n      });\n      const mockRelease = vi.fn().mockResolvedValue(\"tx-sig-release-123\");\n\n      const app = buildTestApp(mockConsensus, mockRelease);\n      const res = await request(app).post(\"/submit\").send({\n        jobId: \"release-me\",\n        description: \"Build something\",\n        acceptanceCriteria: [\"Responsive\"],\n        deliverable: \"https://example.com\",\n        deliverableType: \"url\",\n      });\n\n      expect(res.status).toBe(200);\n      expect(res.body.success).toBe(true);\n      expect(res.body.outcome).toBe(\"RELEASE\");\n      expect(res.body.txSig).toBe(\"tx-sig-release-123\");\n      expect(store.get(\"release-me\")).toBeUndefined();\n    });\n\n    it(\"returns REFUND outcome and removes job from store\", async () => {\n      store.upsert(\n        \"refund-me\",\n        PublicKey.default,\n        makeEscrow({ jobId: \"refund-me\" })\n      );\n\n      const mockConsensus = vi.fn().mockResolvedValue({\n        outcome: \"REFUND\",\n        geminiVerdict: {\n          model: \"gemini\",\n          verdict: \"REJECTED\",\n          confidence: 0.85,\n          reasoning: \"Bad\",\n          criteriaMet: [],\n          criteriaFailed: [\"A\"],\n        },\n        claudeVerdict: {\n          model: \"claude\",\n          verdict: \"REJECTED\",\n          confidence: 0.80,\n          reasoning: \"Bad\",\n          criteriaMet: [],\n          criteriaFailed: [\"A\"],\n        },\n        reasoning: \"Both rejected\",\n        processedAt: Date.now(),\n      });\n      const mockCancel = vi.fn().mockResolvedValue(\"tx-sig-cancel-456\");\n\n      const app = buildTestApp(mockConsensus, undefined, mockCancel);\n      const res = await request(app).post(\"/submit\").send({\n        jobId: \"refund-me\",\n        description: \"Build something\",\n        acceptanceCriteria: [\"Responsive\"],\n        deliverable: \"https://example.com\",\n        deliverableType: \"url\",\n      });\n\n      expect(res.status).toBe(200);\n      expect(res.body.success).toBe(true);\n      expect(res.body.outcome).toBe(\"REFUND\");\n      expect(res.body.txSig).toBe(\"tx-sig-cancel-456\");\n      expect(store.get(\"refund-me\")).toBeUndefined();\n    });\n\n    it(\"returns ESCALATE outcome without on-chain action\", async () => {\n      store.upsert(\n        \"escalate-me\",\n        PublicKey.default,\n        makeEscrow({ jobId: \"escalate-me\" })\n      );\n\n      const mockConsensus = vi.fn().mockResolvedValue({\n        outcome: \"ESCALATE\",\n        geminiVerdict: {\n          model: \"gemini\",\n          verdict: \"APPROVED\",\n          confidence: 0.95,\n          reasoning: \"Good\",\n          criteriaMet: [\"A\"],\n          criteriaFailed: [],\n        },\n        claudeVerdict: {\n          model: \"claude\",\n          verdict: \"REJECTED\",\n          confidence: 0.85,\n          reasoning: \"Bad\",\n          criteriaMet: [],\n          criteriaFailed: [\"A\"],\n        },\n        reasoning: \"Divergent\",\n        processedAt: Date.now(),\n      });\n\n      const app = buildTestApp(mockConsensus);\n      const res = await request(app).post(\"/submit\").send({\n        jobId: \"escalate-me\",\n        description: \"Build something\",\n        acceptanceCriteria: [\"Responsive\"],\n        deliverable: \"https://example.com\",\n        deliverableType: \"url\",\n      });\n\n      expect(res.status).toBe(200);\n      expect(res.body.success).toBe(true);\n      expect(res.body.outcome).toBe(\"ESCALATE\");\n      expect(res.body.txSig).toBeUndefined();\n      // Job should still be in store (not removed on ESCALATE)\n      expect(store.get(\"escalate-me\")).toBeDefined();\n      store.remove(\"escalate-me\");\n    });\n\n    it(\"returns 500 when consensus throws an error\", async () => {\n      store.upsert(\n        \"error-job\",\n        PublicKey.default,\n        makeEscrow({ jobId: \"error-job\" })\n      );\n\n      const mockConsensus = vi.fn().mockRejectedValue(new Error(\"AI service unavailable\"));\n\n      const app = buildTestApp(mockConsensus);\n      const res = await request(app).post(\"/submit\").send({\n        jobId: \"error-job\",\n        description: \"Build something\",\n        acceptanceCriteria: [\"Responsive\"],\n        deliverable: \"https://example.com\",\n        deliverableType: \"url\",\n      });\n\n      expect(res.status).toBe(500);\n      expect(res.body.success).toBe(false);\n      expect(res.body.error).toContain(\"AI service unavailable\");\n      store.remove(\"error-job\");\n    });\n\n    it(\"returns 400 when jobId is missing from body\", async () => {\n      const app = buildTestApp();\n      const res = await request(app).post(\"/submit\").send({\n        description: \"Build something\",\n        acceptanceCriteria: [\"Responsive\"],\n        deliverable: \"https://example.com\",\n        deliverableType: \"url\",\n      });\n\n      expect(res.status).toBe(400);\n      expect(res.body.jobId).toBe(\"\");\n    });\n  });\n});\n"}