/** * cross_service_e2e.test.ts — multi-language (Java/Node/Python/Go) end-to-end test. * * Builds a tiny 6-service repo fixture in a tmp dir, runs the full pipeline: * generateInventory → buildServiceGraph → writeCrossServicePages * and asserts cross-service edges + pages are correct across all languages. * * Services: * orders-service Java/Maven — Feign client → payments-service, @RabbitListener consumer, datasource * payments-service Java/Maven — REST endpoint /api/payments/charge/{id}, AMQP producer, JPA entity * web-ui Node/TS — axios.get("/api/orders/123") frontend → orders-service * notifier Python — requests.get("http://orders-service/api/orders/1") * gateway Go — http.Get("http://payments-service/api/payments/charge/1") * shared/common-model Java — shared library (groupId com.x.shared) */ import { describe, it, expect, afterAll } from "vitest"; import * as fs from "node:fs"; import * as path from "node:path"; import { makeTmpPath, cleanupTmpPath } from "./fixtures.js"; import { generateInventory } from "../atlas_inventory.js"; import { buildServiceGraph } from "../cross_service_edges.js"; import type { ServiceEdge } from "../cross_service_edges.js"; import { writeCrossServicePages } from "../cross_service_pages.js"; // ── Fixture builder ────────────────────────────────────────────────── function buildFixture(root: string): void { const mk = (rel: string) => fs.mkdirSync(path.join(root, rel), { recursive: true }); const write = (rel: string, content: string) => fs.writeFileSync(path.join(root, rel), content, "utf-8"); // ── orders-service (Java/Maven) ──────────────────────────────────── mk("orders-service/src/main/java/com/example/orders"); mk("orders-service/src/main/resources"); write("orders-service/pom.xml", ` com.example orders-service 1.0.0 com.x.shared common-model 1.0.0 org.springframework.cloud spring-cloud-starter-openfeign org.springframework.amqp spring-rabbit `); write("orders-service/src/main/resources/application.yml", `spring: application: name: orders-service datasource: url: jdbc:postgresql://pg/orders rabbitmq: host: localhost payments-service: url: http://payments-svc/api/payments `); write( "orders-service/src/main/java/com/example/orders/OrdersController.java", `package com.example.orders; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.GetMapping; @RestController public class OrdersController { @GetMapping("/api/orders/{id}") public Order getOrder(@PathVariable String id) { return new Order(); } }`, ); // Feign client: url = "${payments-service.url}" → resolved → http://payments-svc/api/payments // resolveTargetService strips "app-" → matches "payments-service" write( "orders-service/src/main/java/com/example/orders/BillingFeignClient.java", `package com.example.orders; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; @FeignClient(name = "billing-client", url = "\${payments-service.url}") public interface BillingFeignClient { @GetMapping("/charge/{id}") ChargeDto charge(@PathVariable String id); }`, ); // RabbitMQ consumer write( "orders-service/src/main/java/com/example/orders/OrderEventConsumer.java", `package com.example.orders; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @Component public class OrderEventConsumer { @RabbitListener(queues = "order-events") public void handleOrderEvent(Object event) { // process } }`, ); // ── payments-service (Java/Maven) ────────────────────────────────── mk("payments-service/src/main/java/com/example/billing"); mk("payments-service/src/main/resources"); write("payments-service/pom.xml", ` com.example payments-service 1.0.0 com.x.shared common-model 1.0.0 org.springframework.amqp spring-rabbit `); write("payments-service/src/main/resources/application.yml", `spring: application: name: payments-service `); write( "payments-service/src/main/java/com/example/billing/BillingController.java", `package com.example.billing; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.GetMapping; @RestController public class BillingController { @GetMapping("/api/payments/charge/{id}") public Charge chargeById(@PathVariable String id) { return new Charge(); } }`, ); // AMQP producer write( "payments-service/src/main/java/com/example/billing/OrderEventPublisher.java", `package com.example.billing; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.stereotype.Component; @Component public class OrderEventPublisher { private final RabbitTemplate rabbitTemplate; public void publishOrderEvent(Object event) { rabbitTemplate.convertAndSend("order-events", event); } }`, ); // JPA entity with name + schema (name must come first to match the JPA profile's table_pattern). // Profile regex: @Table\s*\(\s*name\s*=\s*"(\w+)"(?:.*?schema\s*=\s*"(\w+)")? write( "payments-service/src/main/java/com/example/billing/Charge.java", `package com.example.billing; import javax.persistence.Entity; import javax.persistence.Table; @Entity @Table(name = "charge", schema = "billing") public class Charge { @javax.persistence.Id private Long id; private String amount; }`, ); // ── web-ui (Node/TypeScript frontend) ──────────────────────────── mk("web-ui/src"); write("web-ui/package.json", `{ "name": "web-ui", "version": "0.1.0", "dependencies": { "vue": "^3.0.0", "axios": "^1.0.0" } }`); // axios.get with a plain literal URL (no ${} — required by the profile's regex) write( "web-ui/src/api.ts", `import axios from "axios"; export async function fetchOrder(id: string) { const res = await axios.get("/api/orders/123"); return res.data; }`, ); // ── notifier (Python) ──────────────────────────────────────────── mk("notifier"); write("notifier/pyproject.toml", `[project] name = "notifier" version = "0.1.0" `); // requests.get with k8s-style host: host label = "orders-service" → direct id match write( "notifier/app.py", `import requests def notify_order(order_id): resp = requests.get("http://orders-service/api/orders/1") return resp.json() `, ); // ── gateway (Go) ───────────────────────────────────────────────── mk("gateway"); write("gateway/go.mod", `module gateway go 1.21 `); // net/http client: http.Get with k8s-style host write( "gateway/main.go", `package main import ( "net/http" ) func main() { resp, err := http.Get("http://payments-service/api/payments/charge/1") if err != nil { panic(err) } defer resp.Body.Close() } `, ); // ── shared/common-model (Java library) ────────────────────────── mk("shared/common-model/src/main/java/com/x/shared"); write("shared/common-model/pom.xml", ` com.x.shared common-model 1.0.0 `); write( "shared/common-model/src/main/java/com/x/shared/Order.java", `package com.x.shared; public class Order { private String id; private String status; }`, ); } // ── Test suite ──────────────────────────────────────────────────────── describe("cross-service end-to-end (multi-language)", () => { const root = makeTmpPath("cs-e2e-"); const wiki = makeTmpPath("cs-e2e-wiki-"); const runId = "2026-06-07T10-00-00"; buildFixture(root); const inv = generateInventory(root, runId, { enableCrossService: true }); const g = buildServiceGraph(inv); afterAll(() => { cleanupTmpPath(root); cleanupTmpPath(wiki); }); // ── Service discovery ───────────────────────────────────────────── it("discovers all 6 services", () => { expect(inv.services).toHaveLength(6); const ids = inv.services.map((s) => s.identity.id); expect(ids).toContain("orders-service"); expect(ids).toContain("payments-service"); expect(ids).toContain("web-ui"); expect(ids).toContain("notifier"); expect(ids).toContain("gateway"); expect(ids).toContain("common-model"); }); it("classifies web-ui as frontend", () => { const webUi = inv.services.find((s) => s.identity.id === "web-ui"); expect(webUi?.identity.kind).toBe("frontend"); }); it("classifies common-model as library", () => { const lib = inv.services.find((s) => s.identity.id === "common-model"); expect(lib?.identity.kind).toBe("library"); }); it("classifies orders-service, payments-service, notifier, gateway as service", () => { for (const id of ["orders-service", "payments-service", "notifier", "gateway"]) { const svc = inv.services.find((s) => s.identity.id === id); expect(svc?.identity.kind, `${id} should be service`).toBe("service"); } }); // ── Cross-language calls edges ──────────────────────────────────── it("emits calls edge: orders-service → payments-service (Java Feign via ${payments-service.url} property resolution)", () => { const edge = g.edges.find( (e): e is ServiceEdge => e.kind === "calls" && e.from_service === "orders-service" && e.to_service === "payments-service", ); expect(edge, "orders-service → payments-service calls edge").toBeDefined(); }); it("emits calls edge: web-ui → orders-service (Node/Vue axios → Java backend)", () => { const edge = g.edges.find( (e): e is ServiceEdge => e.kind === "calls" && e.from_service === "web-ui" && e.to_service === "orders-service", ); expect(edge, "web-ui → orders-service calls edge").toBeDefined(); }); it("emits calls edge: notifier → orders-service (Python requests → Java backend via k8s host)", () => { const edge = g.edges.find( (e): e is ServiceEdge => e.kind === "calls" && e.from_service === "notifier" && e.to_service === "orders-service", ); expect(edge, "notifier → orders-service calls edge").toBeDefined(); }); it("emits calls edge: gateway → payments-service (Go net/http → Java backend via k8s host)", () => { const edge = g.edges.find( (e): e is ServiceEdge => e.kind === "calls" && e.from_service === "gateway" && e.to_service === "payments-service", ); expect(edge, "gateway → payments-service calls edge").toBeDefined(); }); // ── Queue produces/consumes ─────────────────────────────────────── it("emits produces edge: payments-service → queue:order-events", () => { const edge = g.edges.find( (e) => e.kind === "produces" && e.from_service === "payments-service" && e.to_service === "queue:order-events", ); expect(edge, "payments-service produces order-events").toBeDefined(); }); it("emits consumes edge: queue:order-events → orders-service", () => { const edge = g.edges.find( (e) => e.kind === "consumes" && e.from_service === "queue:order-events" && e.to_service === "orders-service", ); expect(edge, "orders-service consumes order-events").toBeDefined(); }); // ── Library dependency ──────────────────────────────────────────── it("emits depends_on edge: orders-service → common-model", () => { const edge = g.edges.find( (e) => e.kind === "depends_on" && e.from_service === "orders-service" && e.to_service === "common-model", ); expect(edge, "orders-service depends_on common-model").toBeDefined(); }); // ── External source (DB datasource) ────────────────────────────── it("emits external_source edge: orders-service → ext:db (JDBC datasource)", () => { const edge = g.edges.find( (e) => e.kind === "external_source" && e.from_service === "orders-service" && e.to_service === "ext:db", ); expect(edge, "orders-service external_source ext:db").toBeDefined(); }); // ── ORM reads_table ─────────────────────────────────────────────── it("emits reads_table edge: payments-service → table:billing.charge (JPA entity)", () => { const edge = g.edges.find( (e) => e.kind === "reads_table" && e.from_service === "payments-service" && e.to_service === "table:billing.charge", ); expect(edge, "payments-service reads_table billing.charge").toBeDefined(); }); // ── Pages written ───────────────────────────────────────────────── it("writeCrossServicePages writes all 6 pages and client-registry mentions key services", () => { const written = writeCrossServicePages(wiki, inv, g); expect(written).toHaveLength(6); // All expected page slugs exist const slugs = written.map((p) => path.basename(p, ".md")); expect(slugs).toContain("service-map"); expect(slugs).toContain("client-registry"); expect(slugs).toContain("queue-registry"); expect(slugs).toContain("database-traces"); expect(slugs).toContain("shared-libraries"); expect(slugs).toContain("service-dependencies"); // client-registry.md mentions payments-service as a target and // web-ui / notifier / gateway as cross-language sources const registryPath = written.find((p) => p.includes("client-registry"))!; const registryContent = fs.readFileSync(registryPath, "utf-8"); expect(registryContent).toContain("payments-service"); expect(registryContent).toContain("web-ui"); expect(registryContent).toContain("notifier"); expect(registryContent).toContain("gateway"); }); });