/**
* Tests for atlas_inventory.ts — pre-Phase-2 manifest builder.
*
* Each describe block targets one detection function plus the shared
* generate / persist / load round-trip.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import * as fs from "node:fs";
import * as path from "node:path";
import {
detectProjectMetadata,
detectOrmEntities,
detectRestEndpoints,
detectCodeClients,
discoverShippedRestProfiles,
generateInventory,
inventoryPath,
persistInventory,
loadInventory,
loadCustomRestProfiles,
loadRestProfile,
resolveRestProfiles,
main,
_readEcosystemRestEnabled,
_readEcosystemCrossServiceEnabled,
resolveCrossService,
type CodeInventory,
type RestProfile,
} from "../atlas_inventory.js";
import { makeTmpPath, cleanupTmpPath } from "./fixtures.js";
import { buildServiceGraph } from "../cross_service_edges.js";
const VALID_RUN_ID = "2026-05-01T10-30-00";
// ── detectProjectMetadata ──────────────────────────────────────────
describe("detectProjectMetadata", () => {
let tmpPath: string;
beforeEach(() => {
tmpPath = makeTmpPath("atlas-inv-meta-");
});
afterEach(() => {
cleanupTmpPath(tmpPath);
});
it("returns unknown / empty for a repo with no manifest", () => {
const meta = detectProjectMetadata(tmpPath);
expect(meta.name).toBe("");
expect(meta.version).toBe("");
expect(meta.language).toBe("unknown");
expect(meta.runtime).toBe("");
expect(meta.manifests_seen).toEqual([]);
});
it("reads name + version from package.json (Node)", () => {
fs.writeFileSync(
path.join(tmpPath, "package.json"),
JSON.stringify({
name: "my-app",
version: "1.2.3",
engines: { node: ">=20.0.0 <21.0.0" },
}),
);
const meta = detectProjectMetadata(tmpPath);
expect(meta.name).toBe("my-app");
expect(meta.version).toBe("1.2.3");
expect(meta.language).toBe("typescript");
expect(meta.runtime).toBe("node@20");
expect(meta.manifests_seen).toEqual(["package.json"]);
});
it("falls back to plain `node` when engines.node is absent", () => {
fs.writeFileSync(
path.join(tmpPath, "package.json"),
JSON.stringify({ name: "x", version: "0.1.0" }),
);
const meta = detectProjectMetadata(tmpPath);
expect(meta.runtime).toBe("node");
});
it("reads PEP 621 [project] from pyproject.toml", () => {
fs.writeFileSync(
path.join(tmpPath, "pyproject.toml"),
`[project]
name = "py-app"
version = "0.5.0"
`,
);
const meta = detectProjectMetadata(tmpPath);
expect(meta.name).toBe("py-app");
expect(meta.version).toBe("0.5.0");
expect(meta.language).toBe("python");
expect(meta.runtime).toBe("python");
expect(meta.manifests_seen).toEqual(["pyproject.toml"]);
});
it("falls back to [tool.poetry] when [project] is absent", () => {
fs.writeFileSync(
path.join(tmpPath, "pyproject.toml"),
`[tool.poetry]
name = "poetry-app"
version = "2.0.0"
`,
);
const meta = detectProjectMetadata(tmpPath);
expect(meta.name).toBe("poetry-app");
expect(meta.version).toBe("2.0.0");
});
it("reads module + go version from go.mod", () => {
fs.writeFileSync(
path.join(tmpPath, "go.mod"),
`module github.com/example/proj
go 1.22
require example/dep v1.0.0
`,
);
const meta = detectProjectMetadata(tmpPath);
expect(meta.name).toBe("github.com/example/proj");
expect(meta.language).toBe("go");
expect(meta.runtime).toBe("go@1.22");
expect(meta.manifests_seen).toEqual(["go.mod"]);
});
it("reads [package] from Cargo.toml", () => {
fs.writeFileSync(
path.join(tmpPath, "Cargo.toml"),
`[package]
name = "rust-app"
version = "3.1.4"
rust-version = "1.70"
`,
);
const meta = detectProjectMetadata(tmpPath);
expect(meta.name).toBe("rust-app");
expect(meta.version).toBe("3.1.4");
expect(meta.language).toBe("rust");
expect(meta.runtime).toBe("rust@1.70");
});
it("priorities package.json over later manifests when both exist", () => {
fs.writeFileSync(
path.join(tmpPath, "package.json"),
JSON.stringify({ name: "node-app", version: "1.0.0" }),
);
fs.writeFileSync(
path.join(tmpPath, "pyproject.toml"),
`[project]
name = "py-app"
version = "0.1.0"
`,
);
const meta = detectProjectMetadata(tmpPath);
expect(meta.name).toBe("node-app");
expect(meta.version).toBe("1.0.0");
expect(meta.language).toBe("typescript");
// Both manifests seen and reported.
expect(meta.manifests_seen).toEqual(["package.json", "pyproject.toml"]);
});
it("appends a note when a manifest is unparseable", () => {
fs.writeFileSync(path.join(tmpPath, "package.json"), "{not json");
const notes: string[] = [];
const meta = detectProjectMetadata(tmpPath, notes);
expect(meta.manifests_seen).toEqual([]);
expect(notes.some((n) => n.startsWith("package.json unparseable"))).toBe(
true,
);
});
it("detects Java/Maven project metadata from pom.xml (parent-skipped)", () => {
const root = makeTmpPath("java-meta");
fs.writeFileSync(path.join(root, "pom.xml"), `
spring-boot-starter-parent2.7.11
intake1.4.0
17
`);
const meta = detectProjectMetadata(root);
expect(meta.name).toBe("intake");
expect(meta.version).toBe("1.4.0");
expect(meta.language).toBe("java");
expect(meta.runtime).toBe("java@17");
expect(meta.manifests_seen).toContain("pom.xml");
cleanupTmpPath(root);
});
});
// ── detectOrmEntities ──────────────────────────────────────────────
describe("detectOrmEntities", () => {
let tmpPath: string;
beforeEach(() => {
tmpPath = makeTmpPath("atlas-inv-orm-");
});
afterEach(() => {
cleanupTmpPath(tmpPath);
});
it("returns empty when no ORM markers are present", () => {
fs.writeFileSync(path.join(tmpPath, "x.ts"), "console.log('hi');\n");
expect(detectOrmEntities(tmpPath)).toEqual([]);
});
it("extracts SQLAlchemy entities from a tmp Python file", () => {
fs.mkdirSync(path.join(tmpPath, "models"), { recursive: true });
fs.writeFileSync(
path.join(tmpPath, "models", "user.py"),
`from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
email = Column(String)
`,
);
const entities = detectOrmEntities(tmpPath);
expect(entities.length).toBeGreaterThan(0);
const user = entities.find((e) => e.class_name === "User");
expect(user).toBeTruthy();
expect(user!.profile).toBe("sqlalchemy");
expect(user!.table_name).toBe("users");
expect(user!.source_file.endsWith("models/user.py")).toBe(true);
});
});
// ── detectRestEndpoints ────────────────────────────────────────────
describe("detectRestEndpoints", () => {
let tmpPath: string;
beforeEach(() => {
tmpPath = makeTmpPath("atlas-inv-rest-");
});
afterEach(() => {
cleanupTmpPath(tmpPath);
});
it("loads the Express profile from disk", () => {
const profile = loadRestProfile("express");
expect(profile).not.toBeNull();
expect(profile?.name).toBe("express");
expect(profile?.endpoint_extraction.patterns.length).toBeGreaterThan(0);
});
it("returns null on a missing profile", () => {
expect(loadRestProfile("nonexistent")).toBeNull();
});
it("extracts Express routes from a tmp file with marker import", () => {
fs.mkdirSync(path.join(tmpPath, "src"), { recursive: true });
fs.writeFileSync(
path.join(tmpPath, "src", "routes.ts"),
`import express from "express";
const app = express();
app.get('/api/users', (req, res) => res.json([]));
app.post("/api/users", (req, res) => res.status(201).end());
router.put('/api/users/:id', (req, res) => res.end());
const ignored = "app.delete('/x')"; // string literal, but still matches by regex
`,
);
const profile = loadRestProfile("express")!;
const eps = detectRestEndpoints(tmpPath, [profile]);
const tuples = eps.map((e) => `${e.method} ${e.path}`);
expect(tuples).toContain("GET /api/users");
expect(tuples).toContain("POST /api/users");
expect(tuples).toContain("PUT /api/users/:id");
// line numbers 1-indexed
const get = eps.find((e) => e.method === "GET" && e.path === "/api/users");
expect(get?.line).toBe(3);
});
it("skips files without an Express marker import", () => {
fs.mkdirSync(path.join(tmpPath, "src"), { recursive: true });
fs.writeFileSync(
path.join(tmpPath, "src", "routes.ts"),
`app.get('/api/users', (req, res) => res.json([]));\n`,
);
const profile = loadRestProfile("express")!;
expect(detectRestEndpoints(tmpPath, [profile])).toEqual([]);
});
it("returns empty when an empty profile list is passed", () => {
expect(detectRestEndpoints(tmpPath, [])).toEqual([]);
});
});
// ── discoverShippedRestProfiles + per-profile fixtures ─────────────
describe("discoverShippedRestProfiles", () => {
it("returns all eighteen shipped profiles, sorted", () => {
const names = discoverShippedRestProfiles();
expect(names).toEqual([
"actix",
"aspnet",
"django",
"echo",
"express",
"fastapi",
"fastify",
"flask",
"gin",
"hono",
"koa",
"laravel",
"phoenix",
"rails",
"rocket",
"slim",
"spring",
"vapor",
]);
});
});
describe("Django profile", () => {
let tmpPath: string;
beforeEach(() => { tmpPath = makeTmpPath("atlas-inv-django-"); });
afterEach(() => { cleanupTmpPath(tmpPath); });
it("loads cleanly from disk", () => {
const p = loadRestProfile("django");
expect(p).not.toBeNull();
expect(p?.language).toBe("python");
});
it("extracts URLconf entries (path / re_path)", () => {
fs.writeFileSync(
path.join(tmpPath, "urls.py"),
`from django.urls import path, re_path
from . import views
urlpatterns = [
path("api/users/", views.UserList.as_view(), name="user-list"),
path("api/users//", views.UserDetail.as_view()),
re_path(r"^api/legacy/$", views.legacy_handler),
]
`,
);
const profile = loadRestProfile("django")!;
const eps = detectRestEndpoints(tmpPath, [profile]);
const urls = eps.map((e) => e.path).sort();
expect(urls).toContain("api/users/");
expect(urls).toContain("api/users//");
expect(urls).toContain("^api/legacy/$");
});
it("skips Python files without a django.urls import", () => {
fs.writeFileSync(
path.join(tmpPath, "tasks.py"),
`path("/internal/", lambda: None)\n`,
);
const profile = loadRestProfile("django")!;
expect(detectRestEndpoints(tmpPath, [profile])).toEqual([]);
});
});
describe("Flask profile", () => {
let tmpPath: string;
beforeEach(() => { tmpPath = makeTmpPath("atlas-inv-flask-"); });
afterEach(() => { cleanupTmpPath(tmpPath); });
it("loads cleanly from disk", () => {
const p = loadRestProfile("flask");
expect(p).not.toBeNull();
expect(p?.language).toBe("python");
});
it("extracts @app.route(methods=), @app. shorthand, and bare @app.route default-GET", () => {
fs.writeFileSync(
path.join(tmpPath, "app.py"),
`from flask import Flask
app = Flask(__name__)
@app.route("/api/users", methods=["GET"])
def list_users(): pass
@app.route('/api/users', methods=['POST'])
def create_user(): pass
@app.get("/api/users/")
def show(user_id): pass
@app.delete('/api/users/')
def remove(user_id): pass
@app.route("/api/health")
def health(): pass
@app.route('/api/version')
def version(): pass
`,
);
const profile = loadRestProfile("flask")!;
const eps = detectRestEndpoints(tmpPath, [profile]);
const tuples = eps.map((e) => `${e.method} ${e.path}`);
expect(tuples).toContain("GET /api/users");
expect(tuples).toContain("POST /api/users");
expect(tuples).toContain("GET /api/users/");
expect(tuples).toContain("DELETE /api/users/");
expect(tuples).toContain("GET /api/health");
expect(tuples).toContain("GET /api/version");
});
it("does not double-count the explicit-methods= form via the bare-route pattern", () => {
fs.writeFileSync(
path.join(tmpPath, "app.py"),
`from flask import Flask
app = Flask(__name__)
@app.route("/api/users", methods=["POST"])
def create_user(): pass
`,
);
const profile = loadRestProfile("flask")!;
const eps = detectRestEndpoints(tmpPath, [profile]);
expect(eps).toHaveLength(1);
expect(eps[0]?.method).toBe("POST");
expect(eps[0]?.path).toBe("/api/users");
});
});
describe("ASP.NET profile", () => {
let tmpPath: string;
beforeEach(() => { tmpPath = makeTmpPath("atlas-inv-aspnet-"); });
afterEach(() => { cleanupTmpPath(tmpPath); });
it("loads cleanly from disk", () => {
const p = loadRestProfile("aspnet");
expect(p).not.toBeNull();
expect(p?.language).toBe("csharp");
});
it("extracts [HttpVerb] attribute routes (absolute paths)", () => {
fs.mkdirSync(path.join(tmpPath, "Controllers"), { recursive: true });
fs.writeFileSync(
path.join(tmpPath, "Controllers", "UsersController.cs"),
`using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpGet("/api/users")]
public IActionResult List() => Ok();
[HttpPost("/api/users")]
public IActionResult Create() => Created("/", null);
[HttpDelete("/api/users/{id:int}")]
public IActionResult Delete(int id) => NoContent();
}
`,
);
const profile = loadRestProfile("aspnet")!;
const eps = detectRestEndpoints(tmpPath, [profile]);
const tuples = eps.map((e) => `${e.method} ${e.path}`);
expect(tuples).toContain("GET /api/users");
expect(tuples).toContain("POST /api/users");
expect(tuples).toContain("DELETE /api/users/{id:int}");
});
it("prepends class-level [Route(...)] prefix to relative paths", () => {
fs.mkdirSync(path.join(tmpPath, "Controllers"), { recursive: true });
fs.writeFileSync(
path.join(tmpPath, "Controllers", "OrdersController.cs"),
`using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
[HttpGet]
public IActionResult List() => Ok();
[HttpGet("{id:int}")]
public IActionResult Get(int id) => Ok();
[HttpPost]
public IActionResult Create() => Created("/", null);
[HttpDelete("{id:int}")]
public IActionResult Delete(int id) => NoContent();
}
`,
);
const profile = loadRestProfile("aspnet")!;
const eps = detectRestEndpoints(tmpPath, [profile]);
const tuples = eps.map((e) => `${e.method} ${e.path}`);
expect(tuples).toContain("GET api/orders");
expect(tuples).toContain("GET api/orders/{id:int}");
expect(tuples).toContain("POST api/orders");
expect(tuples).toContain("DELETE api/orders/{id:int}");
});
it("expands the [controller] token in the class-level [Route]", () => {
fs.mkdirSync(path.join(tmpPath, "Controllers"), { recursive: true });
fs.writeFileSync(
path.join(tmpPath, "Controllers", "AuthController.cs"),
`using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
[HttpPost("login")]
public IActionResult Login() => Ok();
[HttpPost("logout")]
public IActionResult Logout() => Ok();
}
`,
);
const profile = loadRestProfile("aspnet")!;
const eps = detectRestEndpoints(tmpPath, [profile]);
const tuples = eps.map((e) => `${e.method} ${e.path}`);
expect(tuples).toContain("POST api/auth/login");
expect(tuples).toContain("POST api/auth/logout");
});
it("absolute paths in [HttpVerb] bypass the class-level prefix", () => {
fs.mkdirSync(path.join(tmpPath, "Controllers"), { recursive: true });
fs.writeFileSync(
path.join(tmpPath, "Controllers", "MixedController.cs"),
`using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/mixed")]
public class MixedController : ControllerBase
{
[HttpGet]
public IActionResult List() => Ok();
[HttpGet("/healthz")]
public IActionResult Health() => Ok();
}
`,
);
const profile = loadRestProfile("aspnet")!;
const eps = detectRestEndpoints(tmpPath, [profile]);
const tuples = eps.map((e) => `${e.method} ${e.path}`);
expect(tuples).toContain("GET api/mixed");
expect(tuples).toContain("GET /healthz");
});
it("falls back to relative path when the class has no [Route]", () => {
fs.mkdirSync(path.join(tmpPath, "Controllers"), { recursive: true });
fs.writeFileSync(
path.join(tmpPath, "Controllers", "StatusController.cs"),
`using Microsoft.AspNetCore.Mvc;
[ApiController]
public class StatusController : ControllerBase
{
[HttpGet("/healthz")]
public IActionResult Health() => Ok();
}
`,
);
const profile = loadRestProfile("aspnet")!;
const eps = detectRestEndpoints(tmpPath, [profile]);
const tuples = eps.map((e) => `${e.method} ${e.path}`);
expect(tuples).toEqual(["GET /healthz"]);
});
});
describe("Gin profile", () => {
let tmpPath: string;
beforeEach(() => { tmpPath = makeTmpPath("atlas-inv-gin-"); });
afterEach(() => { cleanupTmpPath(tmpPath); });
it("loads cleanly from disk", () => {
const p = loadRestProfile("gin");
expect(p).not.toBeNull();
expect(p?.language).toBe("go");
});
it("extracts r.GET / r.POST etc. routes", () => {
fs.writeFileSync(
path.join(tmpPath, "main.go"),
`package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/api/users", listUsers)
r.POST("/api/users", createUser)
r.PUT("/api/users/:id", updateUser)
api := r.Group("/v2")
api.DELETE("/api/users/:id", deleteUser)
r.Run()
}
`,
);
const profile = loadRestProfile("gin")!;
const eps = detectRestEndpoints(tmpPath, [profile]);
const tuples = eps.map((e) => `${e.method} ${e.path}`);
expect(tuples).toContain("GET /api/users");
expect(tuples).toContain("POST /api/users");
expect(tuples).toContain("PUT /api/users/:id");
expect(tuples).toContain("DELETE /api/users/:id");
});
});
describe("Laravel profile", () => {
let tmpPath: string;
beforeEach(() => { tmpPath = makeTmpPath("atlas-inv-laravel-"); });
afterEach(() => { cleanupTmpPath(tmpPath); });
it("loads cleanly from disk", () => {
const p = loadRestProfile("laravel");
expect(p).not.toBeNull();
expect(p?.language).toBe("php");
});
it("extracts Route::verb declarations", () => {
fs.mkdirSync(path.join(tmpPath, "routes"), { recursive: true });
fs.writeFileSync(
path.join(tmpPath, "routes", "api.php"),
` `${e.method} ${e.path}`);
expect(tuples).toContain("GET /api/users");
expect(tuples).toContain("POST /api/users");
expect(tuples).toContain("PUT /api/users/{user}");
expect(tuples).toContain("DELETE /api/users/{user}");
});
});
describe("Hono profile", () => {
let tmpPath: string;
beforeEach(() => { tmpPath = makeTmpPath("atlas-inv-hono-"); });
afterEach(() => { cleanupTmpPath(tmpPath); });
it("loads cleanly from disk", () => {
const p = loadRestProfile("hono");
expect(p).not.toBeNull();
expect(p?.language).toBe("typescript");
});
it("extracts Hono routes from a file with the hono import", () => {
fs.writeFileSync(
path.join(tmpPath, "app.ts"),
`import { Hono } from "hono";
const app = new Hono();
app.get('/api/users', (c) => c.json([]));
app.post("/api/users", (c) => c.json({}, 201));
app.delete(\`/api/users/:id\`, (c) => c.text(""));
`,
);
const profile = loadRestProfile("hono")!;
const eps = detectRestEndpoints(tmpPath, [profile]);
const tuples = eps.map((e) => `${e.method} ${e.path}`);
expect(tuples).toContain("GET /api/users");
expect(tuples).toContain("POST /api/users");
expect(tuples).toContain("DELETE /api/users/:id");
});
it("does NOT match files that import express, not hono", () => {
fs.writeFileSync(
path.join(tmpPath, "app.ts"),
`import express from "express";
const app = express();
app.get('/x', () => {});
`,
);
const profile = loadRestProfile("hono")!;
expect(detectRestEndpoints(tmpPath, [profile])).toEqual([]);
});
it("does NOT extract .use() middleware as a route", () => {
fs.writeFileSync(
path.join(tmpPath, "app.ts"),
`import { Hono } from "hono";
import { logger } from "hono/logger";
const app = new Hono();
app.use('/api/*', logger());
app.use('*', async (c, next) => { await next(); });
app.get('/api/users', (c) => c.json([]));
app.post('/api/users', (c) => c.json({}, 201));
`,
);
const profile = loadRestProfile("hono")!;
const eps = detectRestEndpoints(tmpPath, [profile]);
const tuples = eps.map((e) => `${e.method} ${e.path}`);
expect(tuples).toEqual(["GET /api/users", "POST /api/users"]);
});
});
describe("FastAPI profile", () => {
let tmpPath: string;
beforeEach(() => {
tmpPath = makeTmpPath("atlas-inv-fastapi-");
});
afterEach(() => {
cleanupTmpPath(tmpPath);
});
it("loads cleanly from disk", () => {
const p = loadRestProfile("fastapi");
expect(p).not.toBeNull();
expect(p?.language).toBe("python");
});
it("extracts decorator-style FastAPI routes", () => {
fs.writeFileSync(
path.join(tmpPath, "main.py"),
`from fastapi import FastAPI
app = FastAPI()
@app.get("/api/users")
async def list_users():
return []
@app.post('/api/users')
async def create_user():
return {}
@router.put(f"/api/users/{user_id}")
async def update_user():
return {}
`,
);
const profile = loadRestProfile("fastapi")!;
const eps = detectRestEndpoints(tmpPath, [profile]);
const tuples = eps.map((e) => `${e.method} ${e.path}`);
expect(tuples).toContain("GET /api/users");
expect(tuples).toContain("POST /api/users");
expect(tuples.find((t) => t.startsWith("PUT "))).toBeTruthy();
});
it("skips Python files without a fastapi import", () => {
fs.writeFileSync(
path.join(tmpPath, "tasks.py"),
`@app.get("/internal")\ndef handler(): pass\n`,
);
const profile = loadRestProfile("fastapi")!;
expect(detectRestEndpoints(tmpPath, [profile])).toEqual([]);
});
});
describe("Spring profile", () => {
let tmpPath: string;
beforeEach(() => {
tmpPath = makeTmpPath("atlas-inv-spring-");
});
afterEach(() => {
cleanupTmpPath(tmpPath);
});
it("loads cleanly from disk", () => {
const p = loadRestProfile("spring");
expect(p).not.toBeNull();
expect(p?.language).toBe("java");
});
it("extracts @VerbMapping-style Spring routes", () => {
fs.mkdirSync(path.join(tmpPath, "src"), { recursive: true });
fs.writeFileSync(
path.join(tmpPath, "src", "UserController.java"),
`import org.springframework.web.bind.annotation.*;
@RestController
public class UserController {
@GetMapping("/api/users")
public List list() { return null; }
@PostMapping(value = "/api/users")
public User create() { return null; }
@PutMapping(path = "/api/users/{id}")
public User update() { return null; }
}
`,
);
const profile = loadRestProfile("spring")!;
const eps = detectRestEndpoints(tmpPath, [profile]);
const tuples = eps.map((e) => `${e.method} ${e.path}`);
expect(tuples).toContain("GET /api/users");
expect(tuples).toContain("POST /api/users");
expect(tuples).toContain("PUT /api/users/{id}");
});
});
describe("Rails profile", () => {
let tmpPath: string;
beforeEach(() => {
tmpPath = makeTmpPath("atlas-inv-rails-");
});
afterEach(() => {
cleanupTmpPath(tmpPath);
});
it("loads cleanly from disk", () => {
const p = loadRestProfile("rails");
expect(p).not.toBeNull();
expect(p?.language).toBe("ruby");
});
it("extracts routes from config/routes.rb verb declarations", () => {
fs.mkdirSync(path.join(tmpPath, "config"), { recursive: true });
fs.writeFileSync(
path.join(tmpPath, "config", "routes.rb"),
`Rails.application.routes.draw do
get "/api/users", to: "users#index"
post '/api/users', to: 'users#create'
put "/api/users/:id", to: "users#update"
delete '/api/users/:id', to: 'users#destroy'
resources :posts # block — intentionally NOT extracted in v1
end
`,
);
const profile = loadRestProfile("rails")!;
const eps = detectRestEndpoints(tmpPath, [profile]);
const tuples = eps.map((e) => `${e.method} ${e.path}`);
expect(tuples).toContain("GET /api/users");
expect(tuples).toContain("POST /api/users");
expect(tuples).toContain("PUT /api/users/:id");
expect(tuples).toContain("DELETE /api/users/:id");
});
});
describe("Fastify profile", () => {
let tmpPath: string;
beforeEach(() => { tmpPath = makeTmpPath("atlas-inv-fastify-"); });
afterEach(() => { cleanupTmpPath(tmpPath); });
it("loads cleanly from disk", () => {
const p = loadRestProfile("fastify");
expect(p).not.toBeNull();
expect(p?.language).toBe("typescript");
});
it("extracts fastify.* / app.* / server.* verb routes", () => {
fs.writeFileSync(
path.join(tmpPath, "server.ts"),
`import Fastify from 'fastify';
const fastify = Fastify();
fastify.get('/api/users', async () => []);
fastify.post("/api/users", async () => ({}));
app.put(\`/api/users/:id\`, async () => ({}));
server.delete('/api/users/:id', async () => undefined);
`,
);
const profile = loadRestProfile("fastify")!;
const eps = detectRestEndpoints(tmpPath, [profile]);
const tuples = eps.map((e) => `${e.method} ${e.path}`);
expect(tuples).toContain("GET /api/users");
expect(tuples).toContain("POST /api/users");
expect(tuples).toContain("PUT /api/users/:id");
expect(tuples).toContain("DELETE /api/users/:id");
const get = eps.find((e) => e.method === "GET")!;
expect(get.line).toBe(3);
});
it("skips files without a fastify import", () => {
fs.writeFileSync(
path.join(tmpPath, "x.ts"),
`app.get('/api/users', async () => []);\n`,
);
const profile = loadRestProfile("fastify")!;
expect(detectRestEndpoints(tmpPath, [profile])).toEqual([]);
});
});
describe("Koa profile", () => {
let tmpPath: string;
beforeEach(() => { tmpPath = makeTmpPath("atlas-inv-koa-"); });
afterEach(() => { cleanupTmpPath(tmpPath); });
it("loads cleanly from disk", () => {
const p = loadRestProfile("koa");
expect(p).not.toBeNull();
expect(p?.language).toBe("typescript");
});
it("extracts router.* / api.* / app.* verb routes", () => {
fs.writeFileSync(
path.join(tmpPath, "routes.ts"),
`import Router from "@koa/router";
const router = new Router();
router.get('/api/users', (ctx) => {});
router.post("/api/users", (ctx) => {});
api.put(\`/api/users/:id\`, (ctx) => {});
app.delete('/api/users/:id', (ctx) => {});
`,
);
const profile = loadRestProfile("koa")!;
const eps = detectRestEndpoints(tmpPath, [profile]);
const tuples = eps.map((e) => `${e.method} ${e.path}`);
expect(tuples).toContain("GET /api/users");
expect(tuples).toContain("POST /api/users");
expect(tuples).toContain("PUT /api/users/:id");
expect(tuples).toContain("DELETE /api/users/:id");
const get = eps.find((e) => e.method === "GET")!;
expect(get.line).toBe(3);
});
it("skips files without a koa-router import", () => {
fs.writeFileSync(
path.join(tmpPath, "x.ts"),
`router.get('/api/users', (ctx) => {});\n`,
);
const profile = loadRestProfile("koa")!;
expect(detectRestEndpoints(tmpPath, [profile])).toEqual([]);
});
});
describe("Echo profile", () => {
let tmpPath: string;
beforeEach(() => { tmpPath = makeTmpPath("atlas-inv-echo-"); });
afterEach(() => { cleanupTmpPath(tmpPath); });
it("loads cleanly from disk", () => {
const p = loadRestProfile("echo");
expect(p).not.toBeNull();
expect(p?.language).toBe("go");
});
it("extracts e.* / g.* SHOUTING-CASE verb routes", () => {
fs.writeFileSync(
path.join(tmpPath, "main.go"),
`package main
import "github.com/labstack/echo/v4"
func main() {
e := echo.New()
e.GET("/api/users", listUsers)
e.POST("/api/users", createUser)
g := e.Group("/v2")
g.PUT("/api/users/:id", updateUser)
api.DELETE("/api/users/:id", deleteUser)
e.Start(":8080")
}
`,
);
const profile = loadRestProfile("echo")!;
const eps = detectRestEndpoints(tmpPath, [profile]);
const tuples = eps.map((e) => `${e.method} ${e.path}`);
expect(tuples).toContain("GET /api/users");
expect(tuples).toContain("POST /api/users");
expect(tuples).toContain("PUT /api/users/:id");
expect(tuples).toContain("DELETE /api/users/:id");
});
});
describe("Rocket profile", () => {
let tmpPath: string;
beforeEach(() => { tmpPath = makeTmpPath("atlas-inv-rocket-"); });
afterEach(() => { cleanupTmpPath(tmpPath); });
it("loads cleanly from disk", () => {
const p = loadRestProfile("rocket");
expect(p).not.toBeNull();
expect(p?.language).toBe("rust");
});
it("extracts #[verb(\"/path\")] attribute-macro routes", () => {
fs.writeFileSync(
path.join(tmpPath, "main.rs"),
`#[macro_use] extern crate rocket;
use rocket::*;
#[get("/api/users")]
fn list() -> &'static str { "" }
#[post("/api/users")]
fn create() {}
#[put("/api/users/")]
fn update() {}
#[delete("/api/users/")]
fn destroy() {}
`,
);
const profile = loadRestProfile("rocket")!;
const eps = detectRestEndpoints(tmpPath, [profile]);
const tuples = eps.map((e) => `${e.method} ${e.path}`);
expect(tuples).toContain("GET /api/users");
expect(tuples).toContain("POST /api/users");
expect(tuples).toContain("PUT /api/users/");
expect(tuples).toContain("DELETE /api/users/");
const get = eps.find((e) => e.method === "GET")!;
expect(get.line).toBe(4);
});
});
describe("Actix profile", () => {
let tmpPath: string;
beforeEach(() => { tmpPath = makeTmpPath("atlas-inv-actix-"); });
afterEach(() => { cleanupTmpPath(tmpPath); });
it("loads cleanly from disk", () => {
const p = loadRestProfile("actix");
expect(p).not.toBeNull();
expect(p?.language).toBe("rust");
});
it("extracts .route(\"/path\", web::verb()) registration routes", () => {
fs.writeFileSync(
path.join(tmpPath, "main.rs"),
`use actix_web::{web, App, HttpServer};
fn main() {
let app = App::new()
.route("/api/users", web::get().to(list_users))
.route("/api/users", web::post().to(create_user))
.route("/api/users/{id}", web::put().to(update_user))
.route("/api/users/{id}", web::delete().to(delete_user));
}
`,
);
const profile = loadRestProfile("actix")!;
const eps = detectRestEndpoints(tmpPath, [profile]);
const tuples = eps.map((e) => `${e.method} ${e.path}`);
expect(tuples).toContain("GET /api/users");
expect(tuples).toContain("POST /api/users");
expect(tuples).toContain("PUT /api/users/{id}");
expect(tuples).toContain("DELETE /api/users/{id}");
});
it("skips Rust files without an actix_web import", () => {
fs.writeFileSync(
path.join(tmpPath, "x.rs"),
`app.route("/x", web::get().to(h));\n`,
);
const profile = loadRestProfile("actix")!;
expect(detectRestEndpoints(tmpPath, [profile])).toEqual([]);
});
});
describe("Slim profile", () => {
let tmpPath: string;
beforeEach(() => { tmpPath = makeTmpPath("atlas-inv-slim-"); });
afterEach(() => { cleanupTmpPath(tmpPath); });
it("loads cleanly from disk", () => {
const p = loadRestProfile("slim");
expect(p).not.toBeNull();
expect(p?.language).toBe("php");
});
it("extracts $app->verb('/path') routes", () => {
fs.writeFileSync(
path.join(tmpPath, "index.php"),
`get('/api/users', function ($req, $res) {});
$app->post("/api/users", function ($req, $res) {});
$app->put('/api/users/{id}', function ($req, $res) {});
$app->delete("/api/users/{id}", function ($req, $res) {});
`,
);
const profile = loadRestProfile("slim")!;
const eps = detectRestEndpoints(tmpPath, [profile]);
const tuples = eps.map((e) => `${e.method} ${e.path}`);
expect(tuples).toContain("GET /api/users");
expect(tuples).toContain("POST /api/users");
expect(tuples).toContain("PUT /api/users/{id}");
expect(tuples).toContain("DELETE /api/users/{id}");
const get = eps.find((e) => e.method === "GET")!;
expect(get.line).toBe(6);
});
});
describe("Phoenix profile", () => {
let tmpPath: string;
beforeEach(() => { tmpPath = makeTmpPath("atlas-inv-phoenix-"); });
afterEach(() => { cleanupTmpPath(tmpPath); });
it("loads cleanly from disk", () => {
const p = loadRestProfile("phoenix");
expect(p).not.toBeNull();
expect(p?.language).toBe("elixir");
});
it("extracts router-block verb declarations", () => {
fs.writeFileSync(
path.join(tmpPath, "router.ex"),
`defmodule MyAppWeb.Router do
use Phoenix.Router
scope "/api", MyAppWeb do
get "/users", UserController, :index
post "/users", UserController, :create
put "/users/:id", UserController, :update
delete "/users/:id", UserController, :delete
end
end
`,
);
const profile = loadRestProfile("phoenix")!;
const eps = detectRestEndpoints(tmpPath, [profile]);
const tuples = eps.map((e) => `${e.method} ${e.path}`);
expect(tuples).toContain("GET /users");
expect(tuples).toContain("POST /users");
expect(tuples).toContain("PUT /users/:id");
expect(tuples).toContain("DELETE /users/:id");
});
it("skips Elixir files without a Phoenix.Router marker", () => {
fs.writeFileSync(
path.join(tmpPath, "x.ex"),
`get "/internal", Handler, :run\n`,
);
const profile = loadRestProfile("phoenix")!;
expect(detectRestEndpoints(tmpPath, [profile])).toEqual([]);
});
});
describe("Vapor profile", () => {
let tmpPath: string;
beforeEach(() => { tmpPath = makeTmpPath("atlas-inv-vapor-"); });
afterEach(() => { cleanupTmpPath(tmpPath); });
it("loads cleanly from disk", () => {
const p = loadRestProfile("vapor");
expect(p).not.toBeNull();
expect(p?.language).toBe("swift");
});
it("extracts app.* / routes.* / grouped.* verb routes", () => {
fs.writeFileSync(
path.join(tmpPath, "routes.swift"),
`import Vapor
func routes(_ app: Application) throws {
app.get("users") { req in [] }
app.post("users") { req in "" }
routes.put("users/:id") { req in "" }
let grouped = app.grouped("v2")
grouped.delete("users/:id") { req in "" }
}
`,
);
const profile = loadRestProfile("vapor")!;
const eps = detectRestEndpoints(tmpPath, [profile]);
const tuples = eps.map((e) => `${e.method} ${e.path}`);
expect(tuples).toContain("GET users");
expect(tuples).toContain("POST users");
expect(tuples).toContain("PUT users/:id");
expect(tuples).toContain("DELETE users/:id");
});
it("skips Swift files without a Vapor marker", () => {
fs.writeFileSync(
path.join(tmpPath, "x.swift"),
`app.get("internal") { req in [] }\n`,
);
const profile = loadRestProfile("vapor")!;
expect(detectRestEndpoints(tmpPath, [profile])).toEqual([]);
});
});
// ── resolveRestProfiles ─────────────────────────────────────────────
describe("resolveRestProfiles", () => {
let tmpPath: string;
let configPath: string;
beforeEach(() => {
tmpPath = makeTmpPath("atlas-inv-resolve-");
configPath = path.join(tmpPath, "wiki.config.yaml");
});
afterEach(() => {
cleanupTmpPath(tmpPath);
});
it("returns all eighteen shipped profiles when no name list is given", () => {
const out = resolveRestProfiles({});
const names = out.map((p) => p.name).sort();
expect(names).toEqual([
"actix",
"aspnet",
"django",
"echo",
"express",
"fastapi",
"fastify",
"flask",
"gin",
"hono",
"koa",
"laravel",
"phoenix",
"rails",
"rocket",
"slim",
"spring",
"vapor",
]);
});
it("filters to the named subset when profileNames is given", () => {
const out = resolveRestProfiles({ profileNames: ["express", "fastapi"] });
const names = out.map((p) => p.name).sort();
expect(names).toEqual(["express", "fastapi"]);
});
it("silently drops unknown profile names", () => {
const out = resolveRestProfiles({ profileNames: ["express", "nonexistent"] });
expect(out.map((p) => p.name)).toEqual(["express"]);
});
it("loads custom profiles from wiki.config.yaml (default-set mode)", () => {
fs.writeFileSync(
configPath,
`wiki:
name: x
ecosystem:
rest:
custom_profiles:
- name: hono
language: typescript
detection:
file_patterns: ["**/*.ts"]
markers:
- pattern: 'from "hono"'
endpoint_extraction:
patterns:
- regex: "app\\\\.(get|post)\\\\(['\\"]([^'\\"]+)"
method_group: 1
path_group: 2
`,
);
const out = resolveRestProfiles({ wikiConfigPath: configPath });
const names = out.map((p) => p.name);
expect(names).toContain("hono");
expect(names).toContain("express"); // shipped
expect(names).toContain("fastapi"); // shipped
});
it("custom profile wins on name collision with shipped", () => {
fs.writeFileSync(
configPath,
`wiki:
name: x
ecosystem:
rest:
custom_profiles:
- name: express
language: typescript
description: "OVERRIDDEN-EXPRESS"
detection:
file_patterns: ["**/*.ts"]
markers:
- pattern: 'from "express"'
endpoint_extraction:
patterns:
- regex: "app\\\\.(get)\\\\(['\\"]([^'\\"]+)"
method_group: 1
path_group: 2
`,
);
const out = resolveRestProfiles({ wikiConfigPath: configPath });
const express = out.find((p) => p.name === "express")!;
expect(express.description).toBe("OVERRIDDEN-EXPRESS");
});
it("returns shipped-only when wiki.config.yaml has no custom_profiles", () => {
fs.writeFileSync(configPath, `wiki:\n name: x\n`);
const out = resolveRestProfiles({ wikiConfigPath: configPath });
expect(out.map((p) => p.name).sort()).toEqual([
"actix",
"aspnet",
"django",
"echo",
"express",
"fastapi",
"fastify",
"flask",
"gin",
"hono",
"koa",
"laravel",
"phoenix",
"rails",
"rocket",
"slim",
"spring",
"vapor",
]);
});
it("ignores malformed custom_profiles entries silently", () => {
fs.writeFileSync(
configPath,
`ecosystem:
rest:
custom_profiles:
- name: ok
language: typescript
detection:
file_patterns: ["**/*.ts"]
markers: []
endpoint_extraction:
patterns: []
- this-is-not-a-profile
- name: 42
language: invalid
`,
);
const out = resolveRestProfiles({ wikiConfigPath: configPath });
const names = out.map((p) => p.name);
expect(names).toContain("ok");
expect(names).not.toContain("42");
});
});
// ── loadCustomRestProfiles ─────────────────────────────────────────
describe("loadCustomRestProfiles", () => {
let tmpPath: string;
beforeEach(() => {
tmpPath = makeTmpPath("atlas-inv-custom-");
});
afterEach(() => {
cleanupTmpPath(tmpPath);
});
it("returns [] when the config file is missing", () => {
expect(loadCustomRestProfiles(path.join(tmpPath, "missing.yaml"))).toEqual([]);
});
it("returns [] when the YAML is malformed", () => {
const p = path.join(tmpPath, "bad.yaml");
fs.writeFileSync(p, "{not: yaml::: ::");
expect(loadCustomRestProfiles(p)).toEqual([]);
});
it("returns [] when ecosystem.rest is absent", () => {
const p = path.join(tmpPath, "config.yaml");
fs.writeFileSync(p, "wiki:\n name: x\n");
expect(loadCustomRestProfiles(p)).toEqual([]);
});
});
// ── Dedup across profiles ──────────────────────────────────────────
describe("dedup across profiles", () => {
let tmpPath: string;
beforeEach(() => {
tmpPath = makeTmpPath("atlas-inv-dedup-");
});
afterEach(() => {
cleanupTmpPath(tmpPath);
});
it("does not double-count an endpoint matched by two profiles", () => {
// Custom profile that overlaps with Express on `app.get('/x', ...)`.
const customProfile: RestProfile = {
name: "custom-overlap",
language: "typescript",
detection: {
file_patterns: ["**/*.ts"],
markers: [{ pattern: 'from "express"', type: "import" }],
},
endpoint_extraction: {
patterns: [
{
regex: "app\\.(get|post)\\(['\"]([^'\"]+)['\"]",
method_group: 1,
path_group: 2,
},
],
},
};
fs.writeFileSync(
path.join(tmpPath, "routes.ts"),
`import express from "express";\nconst app = express();\napp.get('/x', () => {});\n`,
);
const expressProfile = loadRestProfile("express")!;
const eps = detectRestEndpoints(tmpPath, [expressProfile, customProfile]);
const xRoutes = eps.filter((e) => e.path === "/x" && e.method === "GET");
expect(xRoutes).toHaveLength(1);
// First profile (express) wins on the framework field.
expect(xRoutes[0]!.framework).toBe("express");
});
});
// ── detectCodeClients ──────────────────────────────────────────────
describe("detectCodeClients", () => {
let tmpPath: string;
beforeEach(() => {
tmpPath = makeTmpPath("atlas-inv-clients-");
});
afterEach(() => {
cleanupTmpPath(tmpPath);
});
it("finds gather() callsites", () => {
fs.writeFileSync(
path.join(tmpPath, "main.ts"),
`import { gather } from "narai-primitives";
const x = await gather({ prompt: "hi", consumer: "doc-wiki" });
`,
);
const clients = detectCodeClients(tmpPath);
expect(clients.length).toBeGreaterThan(0);
expect(clients.some((c) => c.kind === "gather" && c.line === 2)).toBe(true);
});
it("finds fetchWithCaps() callsites", () => {
fs.writeFileSync(
path.join(tmpPath, "main.ts"),
`import { fetchWithCaps } from "narai-primitives/toolkit";
const r = await fetchWithCaps("https://example.com");
`,
);
const clients = detectCodeClients(tmpPath);
expect(clients.some((c) => c.kind === "fetchWithCaps")).toBe(true);
});
it("does not match string-literal occurrences without parens", () => {
fs.writeFileSync(
path.join(tmpPath, "main.ts"),
`// gather is documented elsewhere\nconsole.log("gather");\n`,
);
expect(detectCodeClients(tmpPath)).toEqual([]);
});
it("returns empty when no source files match", () => {
fs.writeFileSync(path.join(tmpPath, "README.md"), "no code here");
expect(detectCodeClients(tmpPath)).toEqual([]);
});
});
// ── generateInventory + persist + load round-trip ──────────────────
describe("inventory round-trip", () => {
let tmpPath: string;
let wikiRoot: string;
let repoRoot: string;
beforeEach(() => {
tmpPath = makeTmpPath("atlas-inv-roundtrip-");
wikiRoot = path.join(tmpPath, "wiki-root");
repoRoot = path.join(tmpPath, "repo-root");
fs.mkdirSync(wikiRoot, { recursive: true });
fs.mkdirSync(repoRoot, { recursive: true });
fs.writeFileSync(
path.join(repoRoot, "package.json"),
JSON.stringify({ name: "demo", version: "0.1.0" }),
);
});
afterEach(() => {
cleanupTmpPath(tmpPath);
});
it("generates a manifest with the canonical shape", () => {
const inv = generateInventory(repoRoot, VALID_RUN_ID);
expect(inv.atlas_run_id).toBe(VALID_RUN_ID);
expect(inv.repo_root).toBe(path.resolve(repoRoot));
expect(inv.project_metadata.name).toBe("demo");
expect(inv.project_metadata.version).toBe("0.1.0");
expect(Array.isArray(inv.orm_entities)).toBe(true);
expect(Array.isArray(inv.rest_endpoints)).toBe(true);
expect(Array.isArray(inv.code_clients)).toBe(true);
expect(typeof inv.stats.duration_ms).toBe("number");
expect(inv.generated_at).toMatch(/^\d{4}-\d{2}-\d{2}T/);
});
it("respects enableOrm / enableRest flags", () => {
fs.writeFileSync(
path.join(repoRoot, "routes.ts"),
`import express from "express";\nconst app = express();\napp.get('/x', () => {});\n`,
);
const offRest = generateInventory(repoRoot, VALID_RUN_ID, {
enableRest: false,
});
expect(offRest.rest_endpoints).toEqual([]);
const onRest = generateInventory(repoRoot, VALID_RUN_ID, {
enableRest: true,
});
expect(onRest.rest_endpoints.length).toBeGreaterThan(0);
});
it("persists to canonical path and round-trips via loadInventory", () => {
const inv = generateInventory(repoRoot, VALID_RUN_ID);
const target = persistInventory(wikiRoot, inv);
expect(target).toBe(inventoryPath(wikiRoot, VALID_RUN_ID));
expect(fs.existsSync(target)).toBe(true);
const loaded = loadInventory(wikiRoot, VALID_RUN_ID);
expect(loaded).toEqual(inv);
});
it("loadInventory returns null for a missing run-id", () => {
expect(loadInventory(wikiRoot, "2099-01-01T00-00-00")).toBeNull();
});
it("loadInventory returns null on shape mismatch (mid-file run-id swap)", () => {
const inv = generateInventory(repoRoot, VALID_RUN_ID);
persistInventory(wikiRoot, inv);
const target = inventoryPath(wikiRoot, VALID_RUN_ID);
const raw = JSON.parse(fs.readFileSync(target, "utf-8")) as Record;
raw["atlas_run_id"] = "2099-01-01T00-00-00";
fs.writeFileSync(target, JSON.stringify(raw));
expect(loadInventory(wikiRoot, VALID_RUN_ID)).toBeNull();
});
it("loadInventory returns null on malformed JSON", () => {
const target = inventoryPath(wikiRoot, VALID_RUN_ID);
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, "{not json");
expect(loadInventory(wikiRoot, VALID_RUN_ID)).toBeNull();
});
});
describe("_readEcosystemRestEnabled", () => {
let tmpPath: string;
beforeEach(() => { tmpPath = makeTmpPath("atlas-inv-eco-rest-"); });
afterEach(() => { cleanupTmpPath(tmpPath); });
it("returns true when wiki.config.yaml has ecosystem.rest.enabled: true", () => {
fs.writeFileSync(
path.join(tmpPath, "wiki.config.yaml"),
"ecosystem:\n rest:\n enabled: true\n",
);
expect(_readEcosystemRestEnabled(tmpPath)).toBe(true);
});
it("returns false when wiki.config.yaml has ecosystem.rest.enabled: false", () => {
fs.writeFileSync(
path.join(tmpPath, "wiki.config.yaml"),
"ecosystem:\n rest:\n enabled: false\n",
);
expect(_readEcosystemRestEnabled(tmpPath)).toBe(false);
});
it("returns false when wiki.config.yaml is missing", () => {
expect(_readEcosystemRestEnabled(tmpPath)).toBe(false);
});
it("returns false when wiki.config.yaml is malformed", () => {
fs.writeFileSync(
path.join(tmpPath, "wiki.config.yaml"),
"ecosystem: : :\n not yaml [",
);
expect(_readEcosystemRestEnabled(tmpPath)).toBe(false);
});
it("returns false when wiki.config.yaml omits the ecosystem.rest key", () => {
fs.writeFileSync(
path.join(tmpPath, "wiki.config.yaml"),
"ecosystem:\n orm:\n cross_validate_against_db: true\n",
);
expect(_readEcosystemRestEnabled(tmpPath)).toBe(false);
});
it("falls back to wiki/wiki.config.yaml when the root copy is missing", () => {
fs.mkdirSync(path.join(tmpPath, "wiki"), { recursive: true });
fs.writeFileSync(
path.join(tmpPath, "wiki", "wiki.config.yaml"),
"ecosystem:\n rest:\n enabled: true\n",
);
expect(_readEcosystemRestEnabled(tmpPath)).toBe(true);
});
});
// ── multi-service inventory (A5) ──────────────────────────────────
describe("generateInventory cross-service", () => {
it("emits a per-service inventory with owner-tagged rest endpoints", () => {
const root = makeTmpPath("multi-svc");
// service A (java) with a controller
fs.mkdirSync(path.join(root, "svc-a", "src"), { recursive: true });
fs.writeFileSync(path.join(root, "svc-a", "pom.xml"), "svc-a");
fs.writeFileSync(path.join(root, "svc-a", "src", "C.java"),
'@RestController class C { @GetMapping("/a/ping") String p(){return "";} }');
// service B (node)
fs.mkdirSync(path.join(root, "svc-b"), { recursive: true });
fs.writeFileSync(path.join(root, "svc-b", "package.json"), '{"name":"svc-b"}');
const inv = generateInventory(root, "2026-06-07T10-00-00", { enableRest: true, enableCrossService: true });
const ids = inv.services.map((s) => s.identity.id).sort();
expect(ids).toEqual(["svc-a", "svc-b"]);
const a = inv.services.find((s) => s.identity.id === "svc-a")!;
expect(a.rest_endpoints.some((e) => e.path === "/a/ping")).toBe(true);
// back-compat: single-project still works (no services discovered)
const single = makeTmpPath("single");
fs.writeFileSync(path.join(single, "package.json"), '{"name":"x"}');
const inv2 = generateInventory(single, "2026-06-07T10-00-01", {});
expect(Array.isArray(inv2.services)).toBe(true); // present, may be []
cleanupTmpPath(root); cleanupTmpPath(single);
});
it("populates http_clients and queue_endpoints per service with correct owner-tagging (B6)", () => {
const root = makeTmpPath("multi-svc-b6");
// service A (java): a Feign client + a RabbitMQ listener
fs.mkdirSync(path.join(root, "svc-a", "src"), { recursive: true });
fs.writeFileSync(path.join(root, "svc-a", "pom.xml"),
"svc-a");
fs.writeFileSync(
path.join(root, "svc-a", "src", "BillingClient.java"),
`import org.springframework.cloud.openfeign.FeignClient;\n@FeignClient(name = "payments-service", path = "/billing")\ninterface BillingClient {\n @GetMapping("/invoices")\n List list();\n}`,
);
fs.writeFileSync(
path.join(root, "svc-a", "src", "EventConsumer.java"),
`import org.springframework.amqp.rabbit.annotation.RabbitListener;\n@RabbitListener(queues = "events")\nvoid handle(String msg) {}`,
);
// service B (node): a BullMQ queue (no Feign, no AMQP)
fs.mkdirSync(path.join(root, "svc-b"), { recursive: true });
fs.writeFileSync(path.join(root, "svc-b", "package.json"), '{"name":"svc-b"}');
fs.writeFileSync(
path.join(root, "svc-b", "worker.ts"),
`import { Worker } from 'bullmq';\nconst w = new Worker('invoice-jobs', async job => {});`,
);
const inv = generateInventory(root, "2026-06-07T10-00-02", {
enableRest: true,
enableCrossService: true,
});
const a = inv.services.find((s) => s.identity.id === "svc-a")!;
const b = inv.services.find((s) => s.identity.id === "svc-b")!;
expect(a).toBeDefined();
expect(b).toBeDefined();
// svc-a owns the Feign client
expect(a.http_clients.length).toBeGreaterThan(0);
expect(a.http_clients.every((e) => e.file.startsWith("svc-a/"))).toBe(true);
// svc-a owns the RabbitMQ consumer
expect(a.queue_endpoints.length).toBeGreaterThan(0);
expect(a.queue_endpoints.find((e) => e.queue_name === "events" && e.role === "consumer")).toBeTruthy();
expect(a.queue_endpoints.every((e) => e.file.startsWith("svc-a/"))).toBe(true);
// svc-b owns the BullMQ consumer
expect(b.queue_endpoints.length).toBeGreaterThan(0);
expect(b.queue_endpoints.find((e) => e.queue_name === "invoice-jobs" && e.role === "consumer")).toBeTruthy();
expect(b.queue_endpoints.every((e) => e.file.startsWith("svc-b/"))).toBe(true);
// svc-b has no Feign clients
expect(b.http_clients.filter((e) => e.framework === "feign")).toHaveLength(0);
// the scope-out note is present
expect(inv.notes.some((n) => n.includes("cross-service queue detection"))).toBe(true);
cleanupTmpPath(root);
});
it("resolves a Feign ${prop.url} target through application.yml → host → service (BUG 2)", () => {
const root = makeTmpPath("multi-svc-feign-prop");
// svc-a: a Feign client whose url is a property reference; the property is
// defined in svc-a's application.yml and points at the settings-service
// host (note the abbreviation: property key says "config", host says
// "configuration"). Aliases alone cannot bridge this — only property→url→host.
fs.mkdirSync(path.join(root, "svc-a", "src"), { recursive: true });
fs.mkdirSync(path.join(root, "svc-a", "src", "main", "resources"), { recursive: true });
fs.writeFileSync(path.join(root, "svc-a", "pom.xml"),
"svc-a");
fs.writeFileSync(
path.join(root, "svc-a", "src", "ConfigClient.java"),
`import org.springframework.cloud.openfeign.FeignClient;\n@FeignClient(name = "x", url = "\${config-service.url}")\ninterface ConfigClient {\n @GetMapping("/api/configuration/clients")\n List list();\n}`,
);
fs.writeFileSync(
path.join(root, "svc-a", "src", "main", "resources", "application.yml"),
"config-service:\n url: http://settings-svc/api/configuration\n",
);
// settings-service: exposes the matching endpoint. Its A2-derived
// aliases include "configuration", so the host settings-svc
// (strip app-/-service → configuration) resolves here.
fs.mkdirSync(path.join(root, "settings-service", "src"), { recursive: true });
fs.writeFileSync(path.join(root, "settings-service", "pom.xml"),
"settings-service");
fs.writeFileSync(
path.join(root, "settings-service", "src", "Ctrl.java"),
'@RestController class Ctrl { @GetMapping("/api/configuration/clients") String l(){return "";} }',
);
const inv = generateInventory(root, "2026-06-07T10-00-03", {
enableRest: true,
enableCrossService: true,
});
const a = inv.services.find((s) => s.identity.id === "svc-a")!;
expect(a).toBeDefined();
// The Feign client's ${prop} target_ref was resolved against svc-a's yml.
const client = a.http_clients.find((c) => c.target_ref.startsWith("${"));
expect(client).toBeDefined();
expect(client!.resolved_target).toBe("http://settings-svc/api/configuration");
const g = buildServiceGraph(inv);
const calls = g.edges.find(
(e) => e.kind === "calls" && e.from_service === "svc-a" && e.to_service === "settings-service",
);
expect(calls).toBeTruthy();
});
});
describe("main() honors ecosystem.rest.enabled", () => {
let tmpPath: string;
let stdoutSpy: ReturnType;
let stderrSpy: ReturnType;
beforeEach(() => {
tmpPath = makeTmpPath("atlas-inv-main-eco-");
// The CLI writes the manifest to stdout; suppress it during tests.
stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
});
afterEach(() => {
stdoutSpy.mockRestore();
stderrSpy.mockRestore();
cleanupTmpPath(tmpPath);
});
function writeFlaskFile(): void {
fs.writeFileSync(
path.join(tmpPath, "app.py"),
`from flask import Flask
app = Flask(__name__)
@app.route("/api/health")
def health(): pass
`,
);
}
function readManifest(runId: string): CodeInventory {
const manifest = loadInventory(tmpPath, runId);
if (!manifest) throw new Error("manifest not found");
return manifest;
}
it("enables REST detection when wiki.config.yaml has ecosystem.rest.enabled: true (no CLI flag)", () => {
writeFlaskFile();
fs.writeFileSync(
path.join(tmpPath, "wiki.config.yaml"),
"ecosystem:\n rest:\n enabled: true\n",
);
const runId = "2026-05-03T00-00-00";
const exit = main([
"generate",
"--wiki-root", tmpPath,
"--repo-root", tmpPath,
"--run-id", runId,
]);
expect(exit).toBe(0);
const manifest = readManifest(runId);
expect(manifest.rest_endpoints.length).toBeGreaterThan(0);
expect(manifest.rest_endpoints[0]?.method).toBe("GET");
expect(manifest.rest_endpoints[0]?.path).toBe("/api/health");
});
it("leaves REST detection off when wiki.config.yaml is absent and no --enable-rest", () => {
writeFlaskFile();
const runId = "2026-05-03T00-00-01";
const exit = main([
"generate",
"--wiki-root", tmpPath,
"--repo-root", tmpPath,
"--run-id", runId,
]);
expect(exit).toBe(0);
expect(readManifest(runId).rest_endpoints).toEqual([]);
});
it("--enable-rest forces detection even when config disables it", () => {
writeFlaskFile();
fs.writeFileSync(
path.join(tmpPath, "wiki.config.yaml"),
"ecosystem:\n rest:\n enabled: false\n",
);
const runId = "2026-05-03T00-00-02";
const exit = main([
"generate",
"--wiki-root", tmpPath,
"--repo-root", tmpPath,
"--run-id", runId,
"--enable-rest",
]);
expect(exit).toBe(0);
expect(readManifest(runId).rest_endpoints.length).toBeGreaterThan(0);
});
});
// ── resolveCrossService: the single shared precedence resolver ────────────
describe("resolveCrossService precedence matrix", () => {
// Shorthand: build opts with sensible defaults so each case states only the
// dimensions it exercises.
const r = (o: Partial[0]>): boolean =>
resolveCrossService({
fromCliEnable: false,
fromCliDisable: false,
config: undefined,
serviceCount: 0,
...o,
});
it("--no-cross-service forces OFF, beating config:true (the codex P2 case)", () => {
expect(r({ fromCliDisable: true, config: true, serviceCount: 5 })).toBe(false);
});
it("--no-cross-service forces OFF, beating --cross-service (both flags → off)", () => {
expect(r({ fromCliDisable: true, fromCliEnable: true, config: true, serviceCount: 5 })).toBe(false);
});
it("--no-cross-service forces OFF even with ≥2 services and no config", () => {
expect(r({ fromCliDisable: true, serviceCount: 9 })).toBe(false);
});
it("--cross-service forces ON, beating config:false", () => {
expect(r({ fromCliEnable: true, config: false, serviceCount: 0 })).toBe(true);
});
it("--cross-service forces ON even on a monolith (serviceCount 0)", () => {
expect(r({ fromCliEnable: true, serviceCount: 0 })).toBe(true);
});
it("config:true (no flags) → ON, ignoring service count", () => {
expect(r({ config: true, serviceCount: 0 })).toBe(true);
});
it("config:false (no flags) → OFF, ignoring service count", () => {
expect(r({ config: false, serviceCount: 9 })).toBe(false);
});
it("AUTO (config absent, no flags): ON iff ≥2 services", () => {
expect(r({ config: undefined, serviceCount: 2 })).toBe(true);
expect(r({ config: undefined, serviceCount: 1 })).toBe(false);
expect(r({ config: undefined, serviceCount: 0 })).toBe(false);
});
});
// ── cross-service AUTO resolution (default-on when ≥2 services) ────────────
describe("main() cross-service AUTO resolution", () => {
let tmpPath: string;
let stdoutSpy: ReturnType;
let stderrSpy: ReturnType;
beforeEach(() => {
tmpPath = makeTmpPath("atlas-inv-cs-auto-");
stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
});
afterEach(() => {
stdoutSpy.mockRestore();
stderrSpy.mockRestore();
cleanupTmpPath(tmpPath);
});
/** A two-service repo (svc-a java + svc-b node) — ≥2 real services. */
function writeMultiService(): void {
fs.mkdirSync(path.join(tmpPath, "svc-a", "src"), { recursive: true });
fs.writeFileSync(path.join(tmpPath, "svc-a", "pom.xml"), "svc-a");
fs.writeFileSync(
path.join(tmpPath, "svc-a", "src", "C.java"),
'@RestController class C { @GetMapping("/a/ping") String p(){return "";} }',
);
fs.mkdirSync(path.join(tmpPath, "svc-b"), { recursive: true });
fs.writeFileSync(path.join(tmpPath, "svc-b", "package.json"), '{"name":"svc-b"}');
}
/** A single-project monolith — 0/1 real services. */
function writeMonolith(): void {
fs.writeFileSync(path.join(tmpPath, "package.json"), '{"name":"x"}');
}
function readManifest(runId: string): CodeInventory {
const manifest = loadInventory(tmpPath, runId);
if (!manifest) throw new Error("manifest not found");
return manifest;
}
function run(runId: string, ...extra: string[]): number {
return main([
"generate",
"--wiki-root", tmpPath,
"--repo-root", tmpPath,
"--run-id", runId,
...extra,
]);
}
it("AUTO enables cross-service when ≥2 real services are discovered (no flag, no config)", () => {
writeMultiService();
const runId = "2026-06-09T00-00-00";
expect(run(runId)).toBe(0);
const ids = readManifest(runId).services.map((s) => s.identity.id).sort();
expect(ids).toEqual(["svc-a", "svc-b"]);
});
it("AUTO disables cross-service for a monolith (0/1 services)", () => {
writeMonolith();
const runId = "2026-06-09T00-00-01";
expect(run(runId)).toBe(0);
expect(readManifest(runId).services).toEqual([]);
});
it("--no-cross-service suppresses even when ≥2 services are present", () => {
writeMultiService();
const runId = "2026-06-09T00-00-02";
expect(run(runId, "--no-cross-service")).toBe(0);
expect(readManifest(runId).services).toEqual([]);
});
it("--cross-service forces cross-service even when config disables it", () => {
writeMultiService();
fs.writeFileSync(
path.join(tmpPath, "wiki.config.yaml"),
"ecosystem:\n cross_service:\n enabled: false\n",
);
const runId = "2026-06-09T00-00-03";
expect(run(runId, "--cross-service")).toBe(0);
const ids = readManifest(runId).services.map((s) => s.identity.id).sort();
expect(ids).toEqual(["svc-a", "svc-b"]);
});
it("config enabled:false suppresses cross-service even with ≥2 services", () => {
writeMultiService();
fs.writeFileSync(
path.join(tmpPath, "wiki.config.yaml"),
"ecosystem:\n cross_service:\n enabled: false\n",
);
const runId = "2026-06-09T00-00-04";
expect(run(runId)).toBe(0);
expect(readManifest(runId).services).toEqual([]);
});
it("config enabled:true forces cross-service even on a monolith (services may still be empty via no-scaffolding gate downstream)", () => {
// On a monolith, discovery finds 0/1 services; config:true still flips the
// generate flag on, but there is nothing to populate — services stays [].
writeMonolith();
fs.writeFileSync(
path.join(tmpPath, "wiki.config.yaml"),
"ecosystem:\n cross_service:\n enabled: true\n",
);
const runId = "2026-06-09T00-00-05";
expect(run(runId)).toBe(0);
expect(readManifest(runId).services).toEqual([]);
});
it("--no-cross-service beats config enabled:true (hard suppress)", () => {
writeMultiService();
fs.writeFileSync(
path.join(tmpPath, "wiki.config.yaml"),
"ecosystem:\n cross_service:\n enabled: true\n",
);
const runId = "2026-06-09T00-00-06";
expect(run(runId, "--no-cross-service")).toBe(0);
expect(readManifest(runId).services).toEqual([]);
});
// The resolved decision is PERSISTED into the manifest (the single source of
// truth Phase 4/7 read). It must match resolveCrossService across the matrix.
it("persists cross_service_enabled matching the resolved decision (AUTO-on, ≥2 services)", () => {
writeMultiService();
const runId = "2026-06-09T00-00-10";
expect(run(runId)).toBe(0);
expect(readManifest(runId).cross_service_enabled).toBe(true);
});
it("persists cross_service_enabled=false for a monolith (AUTO-off)", () => {
writeMonolith();
const runId = "2026-06-09T00-00-11";
expect(run(runId)).toBe(0);
expect(readManifest(runId).cross_service_enabled).toBe(false);
});
it("persists cross_service_enabled=false under --no-cross-service even with ≥2 services", () => {
writeMultiService();
const runId = "2026-06-09T00-00-12";
expect(run(runId, "--no-cross-service")).toBe(0);
expect(readManifest(runId).cross_service_enabled).toBe(false);
});
it("persists cross_service_enabled=true under --cross-service even with config false", () => {
writeMultiService();
fs.writeFileSync(path.join(tmpPath, "wiki.config.yaml"), "ecosystem:\n cross_service:\n enabled: false\n");
const runId = "2026-06-09T00-00-13";
expect(run(runId, "--cross-service")).toBe(0);
expect(readManifest(runId).cross_service_enabled).toBe(true);
});
it("persists cross_service_enabled honoring config true / false", () => {
writeMultiService();
fs.writeFileSync(path.join(tmpPath, "wiki.config.yaml"), "ecosystem:\n cross_service:\n enabled: false\n");
const runIdFalse = "2026-06-09T00-00-14";
expect(run(runIdFalse)).toBe(0);
expect(readManifest(runIdFalse).cross_service_enabled).toBe(false);
fs.writeFileSync(path.join(tmpPath, "wiki.config.yaml"), "ecosystem:\n cross_service:\n enabled: true\n");
const runIdTrue = "2026-06-09T00-00-15";
expect(run(runIdTrue)).toBe(0);
expect(readManifest(runIdTrue).cross_service_enabled).toBe(true);
});
});
// ── resolved-cross-service printer: deterministic Phase-7 signal ──────────
describe("main() resolved-cross-service printer", () => {
let tmpPath: string;
let stdout: string[];
let stdoutSpy: ReturnType;
let stderrSpy: ReturnType;
beforeEach(() => {
tmpPath = makeTmpPath("atlas-inv-rcs-");
stdout = [];
stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation((c) => { stdout.push(c.toString()); return true; });
stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
});
afterEach(() => {
stdoutSpy.mockRestore();
stderrSpy.mockRestore();
cleanupTmpPath(tmpPath);
});
function writeMultiService(): void {
fs.mkdirSync(path.join(tmpPath, "svc-a", "src"), { recursive: true });
fs.writeFileSync(path.join(tmpPath, "svc-a", "pom.xml"), "svc-a");
fs.writeFileSync(path.join(tmpPath, "svc-a", "src", "C.java"), '@RestController class C { @GetMapping("/a/ping") String p(){return "";} }');
fs.mkdirSync(path.join(tmpPath, "svc-b"), { recursive: true });
fs.writeFileSync(path.join(tmpPath, "svc-b", "package.json"), '{"name":"svc-b"}');
}
function gen(runId: string, ...extra: string[]): void {
const exit = main(["generate", "--wiki-root", tmpPath, "--repo-root", tmpPath, "--run-id", runId, ...extra]);
expect(exit).toBe(0);
stdout.length = 0; // discard generate's manifest JSON
}
function printResolved(runId: string): string {
const exit = main(["resolved-cross-service", "--wiki-root", tmpPath, "--run-id", runId]);
expect(exit).toBe(0);
return stdout.join("").trim();
}
it("prints true after an AUTO-on (≥2 services) generate", () => {
writeMultiService();
const runId = "2026-06-09T01-00-00";
gen(runId);
expect(printResolved(runId)).toBe("true");
});
it("prints false after a --no-cross-service generate (even with ≥2 services)", () => {
writeMultiService();
const runId = "2026-06-09T01-00-01";
gen(runId, "--no-cross-service");
expect(printResolved(runId)).toBe("false");
});
it("prints false when the manifest is missing", () => {
expect(printResolved("2026-06-09T01-00-02")).toBe("false");
});
it("requires --run-id (exit 2)", () => {
const exit = main(["resolved-cross-service", "--wiki-root", tmpPath]);
expect(exit).toBe(2);
});
});
// ── B7b: external_sources bucket + connector cross-reference ──────────────
describe("generateInventory external_sources (B7b)", () => {
it("populates external_sources for a service with a DB datasource URL", () => {
const root = makeTmpPath("ext-src-b7b");
// A service with a Spring datasource config containing a postgresql URL.
fs.mkdirSync(path.join(root, "common-service", "src", "main", "resources"), { recursive: true });
fs.writeFileSync(
path.join(root, "common-service", "pom.xml"),
"common-service",
);
fs.writeFileSync(
path.join(root, "common-service", "src", "main", "resources", "application.yml"),
"spring:\n datasource:\n url: jdbc:postgresql://pg/appdb\n",
);
// Write a .connectors/config.yaml enabling the db connector.
const connDir = path.join(root, ".connectors");
fs.mkdirSync(connDir, { recursive: true });
const connConfigPath = path.join(connDir, "config.yaml");
fs.writeFileSync(connConfigPath, "connectors:\n db:\n enabled: true\n");
const inv = generateInventory(root, "2026-06-07T11-00-00", {
enableCrossService: true,
connectorsConfigPath: connConfigPath,
});
const svc = inv.services.find((s) => s.identity.id === "common-service");
expect(svc).toBeDefined();
const dbEntry = svc!.external_sources.find((e) => e.kind === "database");
expect(dbEntry).toBeDefined();
expect(dbEntry!.connector_id).toBe("db");
expect(dbEntry!.configured).toBe(true);
cleanupTmpPath(root);
});
it("marks configured:false when db connector is NOT enabled in config", () => {
const root = makeTmpPath("ext-src-b7b-uncfg");
fs.mkdirSync(path.join(root, "common-service", "src", "main", "resources"), { recursive: true });
fs.writeFileSync(
path.join(root, "common-service", "pom.xml"),
"common-service",
);
fs.writeFileSync(
path.join(root, "common-service", "src", "main", "resources", "application.yml"),
"spring:\n datasource:\n url: jdbc:postgresql://pg/appdb\n",
);
// No connectorsConfigPath provided → configured defaults to false.
const inv = generateInventory(root, "2026-06-07T11-00-01", {
enableCrossService: true,
// connectorsConfigPath deliberately omitted
});
const svc = inv.services.find((s) => s.identity.id === "common-service");
expect(svc).toBeDefined();
const dbEntry = svc!.external_sources.find((e) => e.kind === "database");
expect(dbEntry).toBeDefined();
expect(dbEntry!.connector_id).toBe("db");
expect(dbEntry!.configured).toBe(false);
cleanupTmpPath(root);
});
it("external_sources is empty when enableCrossService is false", () => {
const root = makeTmpPath("ext-src-no-cs");
fs.writeFileSync(path.join(root, "package.json"), '{"name":"x"}');
const inv = generateInventory(root, "2026-06-07T11-00-02", { enableCrossService: false });
// services array is empty, so there are no external_sources to check —
// but the top-level inventory still shouldn't have the key.
expect(inv.services).toEqual([]);
cleanupTmpPath(root);
});
});
// ── B7b-perservice: per-service detectExternalSources walk (PR #58 review) ──
describe("generateInventory external_sources per-service scan (B7b-perservice)", () => {
it("detects datasource entries for BOTH services in a 2-service repo", () => {
const root = makeTmpPath("ext-src-perservice");
// Service A: jdbc:postgresql
fs.mkdirSync(path.join(root, "svc-a", "src", "main", "resources"), { recursive: true });
fs.writeFileSync(
path.join(root, "svc-a", "pom.xml"),
"svc-a",
);
fs.writeFileSync(
path.join(root, "svc-a", "src", "main", "resources", "application.yml"),
"spring:\n datasource:\n url: jdbc:postgresql://pg-a/svc_a_db\n",
);
// Service B: jdbc:mysql
fs.mkdirSync(path.join(root, "svc-b", "src", "main", "resources"), { recursive: true });
fs.writeFileSync(
path.join(root, "svc-b", "pom.xml"),
"svc-b",
);
fs.writeFileSync(
path.join(root, "svc-b", "src", "main", "resources", "application.yml"),
"spring:\n datasource:\n url: jdbc:mysql://mysql-b/svc_b_db\n",
);
const inv = generateInventory(root, "2026-06-08T10-00-00", {
enableCrossService: true,
});
const svcA = inv.services.find((s) => s.identity.id === "svc-a");
const svcB = inv.services.find((s) => s.identity.id === "svc-b");
expect(svcA).toBeDefined();
expect(svcB).toBeDefined();
// Both services must have a database entry — proving per-service scanning.
const dbA = svcA!.external_sources.find((e) => e.kind === "database");
const dbB = svcB!.external_sources.find((e) => e.kind === "database");
expect(dbA).toBeDefined();
expect(dbB).toBeDefined();
// Entries are owner-tagged to the right service (file path starts with svc root).
expect(dbA!.file.startsWith("svc-a/")).toBe(true);
expect(dbB!.file.startsWith("svc-b/")).toBe(true);
cleanupTmpPath(root);
});
});
// ── Fix 1: per-service queue-const isolation + shared-library resolution ──
describe("generateInventory cross-service queue-name resolution (Fix 1)", () => {
it("resolves same-named queue constants per-service, not repo-wide (no cross-bleed)", () => {
const root = makeTmpPath("ctx-isolate");
for (const [svc, val] of [["svc-a", "a-queue"], ["svc-b", "b-queue"]] as const) {
fs.mkdirSync(path.join(root, svc, "src"), { recursive: true });
fs.writeFileSync(path.join(root, svc, "pom.xml"), `${svc}`);
fs.writeFileSync(path.join(root, svc, "src", "C.java"),
`public static final String Q = "${val}";\n@RabbitListener(queues = Q)\nvoid h(Dto m){}`);
}
const inv = generateInventory(root, "2026-06-07T10-00-00", { enableCrossService: true });
const a = inv.services.find((s) => s.identity.id === "svc-a")!;
const b = inv.services.find((s) => s.identity.id === "svc-b")!;
expect(a.queue_endpoints.find((e) => e.role === "consumer")?.queue_name).toBe("a-queue");
expect(b.queue_endpoints.find((e) => e.role === "consumer")?.queue_name).toBe("b-queue");
cleanupTmpPath(root);
});
it("resolves a shared-library queue constant for a consuming service", () => {
const root = makeTmpPath("ctx-sharedlib");
fs.mkdirSync(path.join(root, "shared", "msg", "src"), { recursive: true });
fs.writeFileSync(path.join(root, "shared", "msg", "pom.xml"),
"com.x.sharedmsg");
fs.writeFileSync(path.join(root, "shared", "msg", "src", "Q.java"),
'public static final String SHARED_Q = "shared-events";');
fs.mkdirSync(path.join(root, "svc-a", "src"), { recursive: true });
fs.writeFileSync(path.join(root, "svc-a", "pom.xml"), "svc-a");
fs.writeFileSync(path.join(root, "svc-a", "src", "L.java"),
'import com.x.shared.msg.Q;\n@RabbitListener(queues = SHARED_Q)\nvoid h(Dto m){}');
const inv = generateInventory(root, "2026-06-07T10-00-01", { enableCrossService: true });
const a = inv.services.find((s) => s.identity.id === "svc-a")!;
expect(a.queue_endpoints.find((e) => e.role === "consumer")?.queue_name).toBe("shared-events");
cleanupTmpPath(root);
});
it("enableCrossService implies REST detection so client->endpoint calls edges work (frontend->backend)", () => {
const root = makeTmpPath("xs-implies-rest");
// Vue frontend calling a backend endpoint via the gateway path
fs.mkdirSync(path.join(root, "admin-ui", "src"), { recursive: true });
fs.writeFileSync(path.join(root, "admin-ui", "package.json"), '{"name":"admin-ui","dependencies":{"vue":"^3"}}');
fs.writeFileSync(path.join(root, "admin-ui", "src", "api.ts"),
'import axios from "axios";\nawait axios.get("/api/payments/invoices/42");');
// Backend exposing the endpoint
fs.mkdirSync(path.join(root, "payments-module", "src"), { recursive: true });
fs.writeFileSync(path.join(root, "payments-module", "pom.xml"), "payments-module");
fs.writeFileSync(path.join(root, "payments-module", "src", "R.java"),
'@RestController class R { @GetMapping("/api/payments/invoices/{id}") String g(){return"";} }');
// NOTE: enableRest is NOT passed — enableCrossService must imply it.
const inv = generateInventory(root, "2026-06-07T10-00-02", { enableCrossService: true });
const billing = inv.services.find((s) => s.identity.id === "payments-module")!;
expect(billing.rest_endpoints.length).toBeGreaterThan(0); // REST ran despite enableRest unset
const g = buildServiceGraph(inv);
const fe = g.edges.find((e) => e.kind === "calls" && e.from_service === "admin-ui" && e.to_service === "payments-module");
expect(fe).toBeTruthy(); // frontend -> backend edge present
expect(fe!.confidence).toBe("high");
cleanupTmpPath(root);
});
});
// ── library_deps + auth_issuer inventory fields (Task C2b) ────────────
describe("generateInventory library_deps + auth_issuer fields (C2b)", () => {
it("populates library_deps from pom.xml blocks matching discovered library ids", () => {
const root = makeTmpPath("c2b-libdeps");
// Discovered library: shared/common-model with artifactId common-model
fs.mkdirSync(path.join(root, "shared", "common-model", "src"), { recursive: true });
fs.writeFileSync(
path.join(root, "shared", "common-model", "pom.xml"),
`
com.x.shared
common-model
1.0
jar
`,
);
// svc-a depends on common-model
fs.mkdirSync(path.join(root, "svc-a", "src", "main", "resources"), { recursive: true });
fs.writeFileSync(
path.join(root, "svc-a", "pom.xml"),
`
svc-a
com.x.shared
common-model
`,
);
fs.writeFileSync(
path.join(root, "svc-a", "src", "main", "resources", "application.yml"),
`spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.com/
`,
);
const inv = generateInventory(root, "2026-06-07T10-00-00", { enableCrossService: true });
const svcA = inv.services.find((s) => s.identity.id === "svc-a");
expect(svcA).toBeDefined();
expect(svcA!.library_deps).toContain("common-model");
expect(svcA!.auth_issuer).toBe("https://auth.example.com/");
// The library itself should have empty library_deps (it has no pom dependency on itself)
const lib = inv.services.find((s) => s.identity.id === "common-model");
expect(lib).toBeDefined();
expect(lib!.library_deps).toEqual([]);
cleanupTmpPath(root);
});
it("auth_issuer falls back to issuer_uri (underscore) property key", () => {
const root = makeTmpPath("c2b-issuer-underscore");
fs.mkdirSync(path.join(root, "svc-b", "src", "main", "resources"), { recursive: true });
fs.writeFileSync(path.join(root, "svc-b", "pom.xml"), "svc-b");
// Use dots-mapped as underscores in properties file
fs.writeFileSync(
path.join(root, "svc-b", "src", "main", "resources", "application.properties"),
"spring.security.oauth2.resourceserver.jwt.issuer-uri=https://example.okta.com/\n",
);
const inv = generateInventory(root, "2026-06-07T10-00-01", { enableCrossService: true });
const svcB = inv.services.find((s) => s.identity.id === "svc-b");
expect(svcB).toBeDefined();
expect(svcB!.auth_issuer).toBe("https://example.okta.com/");
cleanupTmpPath(root);
});
it("library_deps is empty when service pom has no matching library dependency", () => {
const root = makeTmpPath("c2b-no-deps");
fs.mkdirSync(path.join(root, "svc-c", "src"), { recursive: true });
fs.writeFileSync(
path.join(root, "svc-c", "pom.xml"),
`
svc-c
org.external
some-lib
`,
);
const inv = generateInventory(root, "2026-06-07T10-00-02", { enableCrossService: true });
const svcC = inv.services.find((s) => s.identity.id === "svc-c");
expect(svcC).toBeDefined();
expect(svcC!.library_deps).toEqual([]);
cleanupTmpPath(root);
});
});
// ── nested-pom library_deps (PR #58 Finding 2) ───────────────────
it("reads library_deps from a nested-pom service (build-subdir layout)", () => {
const root = makeTmpPath("nested-libdeps");
// a discovered library
fs.mkdirSync(path.join(root, "shared", "common-model"), { recursive: true });
fs.writeFileSync(path.join(root, "shared", "common-model", "pom.xml"),
"com.x.sharedcommon-model");
// a nested-pom service: pom is in feed/project/, not feed/
fs.mkdirSync(path.join(root, "feed", "project", "src"), { recursive: true });
fs.writeFileSync(path.join(root, "feed", "project", "pom.xml"),
"feedcom.x.sharedcommon-model");
const inv = generateInventory(root, "2026-06-08T10-00-00", { enableCrossService: true });
const feed = inv.services.find((s) => s.identity.id === "feed")!;
expect(feed).toBeDefined();
expect(feed.library_deps).toContain("common-model");
cleanupTmpPath(root);
});
// ── _readEcosystemCrossServiceEnabled ─────────────────────────────
describe("_readEcosystemCrossServiceEnabled", () => {
it("reads ecosystem.cross_service.enabled from wiki.config.yaml", () => {
const root = makeTmpPath("xs-flag");
fs.writeFileSync(path.join(root, "wiki.config.yaml"), "ecosystem:\n cross_service:\n enabled: true\n");
expect(_readEcosystemCrossServiceEnabled(root)).toBe(true);
const root2 = makeTmpPath("xs-flag2");
fs.writeFileSync(path.join(root2, "wiki.config.yaml"), "ecosystem:\n rest:\n enabled: true\n");
expect(_readEcosystemCrossServiceEnabled(root2)).toBe(false); // absent → false
cleanupTmpPath(root); cleanupTmpPath(root2);
});
it("CLI --cross-service runs cross-service detection (services populated)", () => {
const root = makeTmpPath("xs-cli-repo");
const wiki = makeTmpPath("xs-cli-wiki");
fs.mkdirSync(path.join(root, "svc-a", "src"), { recursive: true });
fs.writeFileSync(path.join(root, "svc-a", "pom.xml"), "svc-a");
fs.writeFileSync(path.join(root, "svc-a", "src", "R.java"), '@RestController class R { @GetMapping("/a/x") String g(){return"";} }');
const code = main(["generate", "--wiki-root", wiki, "--repo-root", root, "--run-id", "2026-06-07T10-00-00", "--cross-service"]);
expect(code).toBe(0);
const inv = loadInventory(wiki, "2026-06-07T10-00-00")!;
expect(inv.services.length).toBeGreaterThan(0); // cross-service ran
cleanupTmpPath(root); cleanupTmpPath(wiki);
});
});