This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new a8f3de30554f chore: camel-jbang - About has the links of the runtime: 
a queue between the routes of an integration is a call or a hand-off, and no In 
or Out (#29600)
a8f3de30554f is described below

commit a8f3de30554f0d8fb86dbfe04bbdcddc06e5a2de
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Nov 11 13:14:50 2026 +0100

    chore: camel-jbang - About has the links of the runtime: a queue between 
the routes of an integration is a call or a hand-off, and no In or Out (#29600)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../ROOT/pages/camel-jbang-tui-observe.adoc        |   4 +-
 .../dsl/jbang/core/commands/tui/AboutView.java     |  47 +++++-
 .../dsl/jbang/core/commands/tui/RuntimeLinks.java  | 181 +++++++++++++++++++++
 .../src/main/resources/tui/help/overview.md        |   6 +-
 .../jbang/core/commands/tui/RuntimeLinksTest.java  | 152 +++++++++++++++++
 5 files changed, 381 insertions(+), 9 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
index b4f00f5c1ec4..8450a29f878e 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
@@ -30,7 +30,9 @@ README, else the descriptions of the routes, else the 
overview an AI wrote in `c
 and *s* the summary of the project.
 * *In*, *Routes* and *Out* say what starts it (a timer or a cron with the 
period or schedule of its properties, its
 profile applied; the operations of an OpenAPI contract the REST DSL serves, 
from the contract), its routes (a route called with `direct` sits under its 
caller with `↳`,
-one handed off with `seda` or a `wireTap` with `⇢`) and what it talks to, with 
the exchanges since it started and a sparkline of the
+one handed off with `seda` or a `wireTap` with `⇢`; for a running integration 
as its route topology has it, so a
+queue or topic of a broker between its own routes is `↳` for a request-reply, 
`⇢` for a hand-off and `on failure` for a
+dead letter queue, and no In or Out unless messages came from outside too) and 
what it talks to, with the exchanges since it started and a sparkline of the
 last 5 minutes. *Up* and *Down* select a line, and *Enter* opens a route 
drilled down on the Diagram, or an endpoint with
 its details on the Endpoint tab.
 * *Last 5m* has the exchanges and how they ended, with the cause that happened 
most, and *What happened* a strip per route
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
index 05bb6aba74dd..25399325e8c7 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
@@ -90,6 +90,8 @@ final class AboutView {
     private final MonitorContext ctx;
     private volatile ProjectOverview.Overview overview;
     private volatile About.Model model;
+    // how the routes of the running integration reach each other; null as the 
sources have it
+    private volatile RuntimeLinks links;
     private volatile String purpose;
     private volatile String purposeFrom;
     private volatile boolean loading;
@@ -130,6 +132,30 @@ final class AboutView {
         this.ctx = ctx;
     }
 
+    /**
+     * The links of the running integration (its route topology, as the 
Diagram has it); null when it does not run, or
+     * does not answer, and the model is as the sources have it.
+     */
+    private RuntimeLinks runtimeLinks(IntegrationInfo info) {
+        if (info.phantom || info.pid == null || !alive(info.pid)) {
+            return null;
+        }
+        try {
+            JsonObject topology = ctx.diagramTopology.load(ctx, info.pid);
+            return topology != null ? RuntimeLinks.of(topology) : null;
+        } catch (RuntimeException e) {
+            return null;
+        }
+    }
+
+    private static boolean alive(String pid) {
+        try {
+            return 
ProcessHandle.of(Long.parseLong(pid)).map(ProcessHandle::isAlive).orElse(false);
+        } catch (NumberFormatException e) {
+            return false;
+        }
+    }
+
     /** Reads the sources of the integration in the background, again when 
they may have changed. */
     private void load(IntegrationInfo info, Consumer<Runnable> onRenderThread) 
{
         Path d = info.directory != null ? Path.of(info.directory) : null;
@@ -152,9 +178,12 @@ final class AboutView {
         Thread t = new Thread(() -> {
             try {
                 ProjectOverview.Overview o = ProjectOverview.analyze(d, 
ArchitectureView.catalog());
-                About.Model m = About.of(d, o);
+                // how the routes reach each other as the runtime has it, a 
hop over a broker included
+                RuntimeLinks rl = runtimeLinks(info);
+                About.Model m = rl != null ? rl.apply(About.of(d, o), info) : 
About.of(d, o);
                 Path r = About.readme(d);
                 onRenderThread.accept(() -> {
+                    links = rl;
                     readme = r;
                     overview = o;
                     model = m;
@@ -1034,7 +1063,8 @@ final class AboutView {
             String how = r.how() == null ? "" : "call".equals(r.how()) ? "↳ " 
: "⇢ ";
             spans.add(Span.raw(" " + "  ".repeat(r.depth()) + how));
             spans.add(Span.styled(r.routeId(), Style.EMPTY.bold()));
-            spans.add(Span.styled("  " + r.from() + " → " + r.steps() + 
(r.steps() == 1 ? " step" : " steps"),
+            spans.add(Span.styled("  " + r.from() + " → " + r.steps() + 
(r.steps() == 1 ? " step" : " steps")
+                                  + ("error".equals(r.how()) ? ", on failure" 
: ""),
                     Theme.muted()));
             Long total = totals.get(r.routeId());
             if (total != null) {
@@ -1046,7 +1076,9 @@ final class AboutView {
 
         // and the ones the integration sent to that its routes do not name (a 
toD, a recipient list)
         List<About.Out> outs = new ArrayList<>(m.out());
-        outs.addAll(ObservedOuts.of(info, m.out()));
+        RuntimeLinks rl = links;
+        // a destination a route of the integration consumes is no system 
outside
+        ObservedOuts.of(info, m.out()).stream().filter(s -> rl == null || 
!rl.consumedHere(s)).forEach(outs::add);
         for (About.Out s : outs) {
             if (spaced && !out.isEmpty()) {
                 out.add(Line.from(Span.raw("")));
@@ -1402,11 +1434,12 @@ final class AboutView {
                     ? "  (the last exchange " + ChaosTab.shortTime((end - 
lastActive) * 1000) + " ago)"
                     : "  (quiet)", Theme.muted()));
         }
-        // what the routes that something starts did since the integration 
started, as their counters say
+        // what the routes that something from outside starts did since the 
integration started, as their counters
+        // say: not a route a queue of the integration itself feeds
         Set<String> entry = new LinkedHashSet<>();
-        ProjectOverview.Overview o = overview;
-        if (o != null) {
-            o.entryPoints().forEach(e -> entry.add(e.route()));
+        About.Model shown = model;
+        if (shown != null) {
+            shown.in().forEach(e -> entry.add(e.route()));
         }
         long total = 0;
         long totalFailed = 0;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RuntimeLinks.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RuntimeLinks.java
new file mode 100644
index 000000000000..8f4b50688f84
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RuntimeLinks.java
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.camel.dsl.jbang.core.commands.ai.About;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * How the routes of a running integration reach each other, from its route 
topology (CAMEL-26823), whatever the
+ * component: a route the sender waits for (direct, a request-reply over a JMS 
queue) is a call, one it hands the
+ * message to (seda, a topic, a wire tap) a hand-off, and one it sends to only 
when it handles a failure (a dead letter
+ * queue) an error path. The sources of the integration name the links by 
their scheme only, and took a hop over a
+ * broker for a message from outside: jms-request-reply had its queues as In 
and Out, and 14 exchanges for 3 orders.
+ */
+final class RuntimeLinks {
+
+    /** How a route reaches another: call, async (a hand-off) or error (only 
when it handles a failure). */
+    record Link(String from, String to, String endpoint, String how) {
+    }
+
+    private static final int MAX_DEPTH = 6;
+
+    private final List<Link> links;
+
+    private RuntimeLinks(List<Link> links) {
+        this.links = links;
+    }
+
+    List<Link> links() {
+        return links;
+    }
+
+    /** The links between different routes in the topology of the 
route-topology console. */
+    static RuntimeLinks of(JsonObject topology) {
+        List<Link> answer = new ArrayList<>();
+        Collection<?> edges = topology != null ? 
topology.getCollection("edges") : null;
+        if (edges != null) {
+            for (Object o : edges) {
+                if (!(o instanceof JsonObject e)) {
+                    continue;
+                }
+                String from = e.getString("fromRouteId");
+                String to = e.getString("toRouteId");
+                String endpoint = e.getString("endpoint");
+                if (from == null || to == null || endpoint == null || 
from.equals(to)) {
+                    continue;
+                }
+                // a hand-off unless the route waits for the answer; an edge 
the TUI adds for the sends the runtime saw
+                // (a recipient list) waits, as a call
+                String how = "error".equals(e.getString("kind")) ? "error"
+                        : Boolean.TRUE.equals(e.get("reply")) || 
Boolean.TRUE.equals(e.get("observed")) ? "call" : "async";
+                answer.add(new Link(from, to, endpoint, how));
+            }
+        }
+        return new RuntimeLinks(answer);
+    }
+
+    /**
+     * The model of the sources with the links of the runtime: the routes 
under the route that reaches them, In without
+     * an endpoint only the integration sends to (the rest of its messages, 
from outside, as +N from outside), and Out
+     * without one only the integration consumes.
+     */
+    About.Model apply(About.Model m, IntegrationInfo info) {
+        if (links.isEmpty()) {
+            return m;
+        }
+        return new About.Model(m.purpose(), m.purposeFrom(), in(m.in(), info), 
routes(m.routes()), out(m.out()));
+    }
+
+    private List<About.In> in(List<About.In> in, IntegrationInfo info) {
+        List<About.In> answer = new ArrayList<>();
+        for (About.In e : in) {
+            String key = DiagramTopology.endpointKey(e.label());
+            Link fed = links.stream()
+                    .filter(l -> l.to().equals(e.route()) && 
key.equals(DiagramTopology.endpointKey(l.endpoint())))
+                    .findFirst().orElse(null);
+            if (fed == null) {
+                answer.add(e);
+                continue;
+            }
+            // what came in beyond what the routes of the integration sent 
there came from outside
+            long outside = hits(info, key, "in", e.route()) - hits(info, key, 
"out", null);
+            if (outside > 0) {
+                answer.add(new About.In(e.kind(), e.label() + "  +" + outside 
+ " from outside", e.route()));
+            }
+        }
+        return answer;
+    }
+
+    private static long hits(IntegrationInfo info, String key, String 
direction, String route) {
+        long answer = 0;
+        for (EndpointInfo ep : info.endpoints) {
+            if (direction.equals(ep.direction) && ep.uri != null && (route == 
null || route.equals(ep.routeId))
+                    && key.equals(DiagramTopology.endpointKey(ep.uri))) {
+                answer += ep.hits;
+            }
+        }
+        return answer;
+    }
+
+    private List<About.Out> out(List<About.Out> out) {
+        return out.stream().filter(o -> !consumedHere(o)).toList();
+    }
+
+    /** Whether the destination of an out is one a route of the integration 
consumes from: a hop, no system outside. */
+    boolean consumedHere(About.Out o) {
+        String key = DiagramTopology.endpointKey(o.uri());
+        return links.stream()
+                .anyMatch(l -> l.from().equals(o.route()) && 
key.equals(DiagramTopology.endpointKey(l.endpoint())));
+    }
+
+    private List<About.RouteLine> routes(List<About.RouteLine> lines) {
+        Map<String, About.RouteLine> byId = new LinkedHashMap<>();
+        lines.forEach(r -> byId.putIfAbsent(r.routeId(), r));
+        Map<String, List<Link>> from = new HashMap<>();
+        Set<String> reached = new LinkedHashSet<>();
+        for (Link l : links) {
+            if (byId.containsKey(l.from()) && byId.containsKey(l.to())) {
+                from.computeIfAbsent(l.from(), k -> new ArrayList<>()).add(l);
+                reached.add(l.to());
+            }
+        }
+        List<About.RouteLine> answer = new ArrayList<>();
+        Set<String> shown = new LinkedHashSet<>();
+        for (String id : byId.keySet()) {
+            if (!reached.contains(id)) {
+                add(byId.get(id), 0, null, byId, from, shown, answer);
+            }
+        }
+        // what is left: routes reached only from a cycle
+        for (String id : byId.keySet()) {
+            add(byId.get(id), 0, null, byId, from, shown, answer);
+        }
+        return answer;
+    }
+
+    private static void add(
+            About.RouteLine r, int depth, String how, Map<String, 
About.RouteLine> byId, Map<String, List<Link>> from,
+            Set<String> shown, List<About.RouteLine> answer) {
+        if (!shown.add(r.routeId()) || depth > MAX_DEPTH) {
+            return;
+        }
+        answer.add(new About.RouteLine(r.routeId(), depth, how, r.from(), 
r.steps()));
+        for (Link l : from.getOrDefault(r.routeId(), List.of())) {
+            add(byId.get(l.to()), depth + 1, l.how(), byId, from, shown, 
answer);
+        }
+    }
+
+    /** How each route that another one reaches is reached: call, async or 
error; none for the others. */
+    Map<String, String> howReached() {
+        Map<String, String> answer = new HashMap<>();
+        for (Link l : links) {
+            // a route called by one and handed to by another: the call keeps 
it inside the exchange of its caller
+            answer.merge(l.to(), l.how(), (a, b) -> "call".equals(a) || 
"call".equals(b) ? "call" : a);
+        }
+        return answer;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
index 7ca5cef11bf8..4ed570918470 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
@@ -25,7 +25,11 @@ happened to it, with no AI:
 - **In, Routes, Out** — what starts it (timers, HTTP, consumers, the
   operations of an OpenAPI contract the REST DSL serves), its
   routes (a route called with `direct:` sits under its caller with `↳`,
-  one handed off with `seda:` or a `wireTap` with `⇢`) and what it talks to, 
from its
+  one handed off with `seda:` or a `wireTap` with `⇢`; for a running
+  integration as its route topology has it, so a queue or topic between
+  its own routes is `↳` for a request-reply, `⇢` for a hand-off, and
+  `on failure` for a dead letter queue, and is no In or Out, unless
+  messages came from outside too: `+5 from outside`) and what it talks to, 
from its
   sources. Each line has the exchanges since it started (for an
   endpoint, the sends to it), and In and Out a sparkline of the last 5
   minutes with the rate now, or `idle`. Out also has the systems the
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RuntimeLinksTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RuntimeLinksTest.java
new file mode 100644
index 000000000000..db541c299065
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RuntimeLinksTest.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.commands.ai.About;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * connect-service/jms-request-reply: order-intake puts each order on a queue, 
reserve-order asks stock-service over JMS
+ * request-reply, publishes the reserved order on a topic for shipping and 
invoicing, and its dead letter channel moves
+ * the order it cannot reserve to a dead letter queue. The sources name the 
links by their scheme only: every queue was
+ * an In and an Out of the integration.
+ */
+class RuntimeLinksTest {
+
+    private static About.Model sources() {
+        List<About.In> in = List.of(
+                new About.In("file", "file:orders", "order-intake"),
+                new About.In("jms", "jms:queue:orders", "reserve-order"),
+                new About.In("jms", "jms:queue:stock", "stock-service"),
+                new About.In("jms", "jms:queue:orders.dead", "dead-orders"),
+                new About.In("jms", "jms:topic:orders.reserved", "shipping"),
+                new About.In("jms", "jms:topic:orders.reserved", "invoicing"));
+        List<About.RouteLine> routes = List.of(
+                new About.RouteLine("order-intake", 0, null, "file", 4),
+                new About.RouteLine("reserve-order", 0, null, "jms", 6),
+                new About.RouteLine("stock-service", 0, null, "jms", 5),
+                new About.RouteLine("dead-orders", 0, null, "jms", 1),
+                new About.RouteLine("shipping", 0, null, "jms", 1),
+                new About.RouteLine("invoicing", 0, null, "jms", 1));
+        List<About.Out> out = List.of(
+                new About.Out("messaging", "jms:queue:orders", "out", 
"order-intake"),
+                new About.Out("messaging", "jms:queue:stock", "in/out", 
"reserve-order"),
+                new About.Out("messaging", "jms:topic:orders.reserved", "out", 
"reserve-order"),
+                new About.Out("messaging", "jms:queue:orders.dead", "out", 
"reserve-order"));
+        return new About.Model("Each order asks a stock service", "from the 
comment", in, routes, out);
+    }
+
+    private static JsonObject topology() {
+        JsonArray edges = new JsonArray();
+        edges.add(edge("order-intake", "reserve-order", "jms:queue:orders", 
"call", false));
+        edges.add(edge("reserve-order", "stock-service", "jms:queue:stock", 
"call", true));
+        edges.add(edge("reserve-order", "shipping", 
"jms:topic:orders.reserved", "call", false));
+        edges.add(edge("reserve-order", "invoicing", 
"jms:topic:orders.reserved", "call", false));
+        edges.add(edge("reserve-order", "dead-orders", 
"jms:queue:orders.dead", "error", false));
+        JsonObject topology = new JsonObject();
+        topology.put("edges", edges);
+        return topology;
+    }
+
+    private static JsonObject edge(String from, String to, String endpoint, 
String kind, boolean reply) {
+        JsonObject e = new JsonObject();
+        e.put("fromRouteId", from);
+        e.put("toRouteId", to);
+        e.put("endpoint", endpoint);
+        e.put("kind", kind);
+        // the runtime writes reply on every edge
+        e.put("reply", reply);
+        return e;
+    }
+
+    @Test
+    void theRoutesAreUnderTheRouteThatReachesThem() {
+        About.Model m = RuntimeLinks.of(topology()).apply(sources(), new 
IntegrationInfo());
+
+        assertThat(m.routes()).extracting(r -> "  ".repeat(r.depth()) + 
(r.how() != null ? r.how() + " " : "") + r.routeId())
+                .containsExactly(
+                        "order-intake",
+                        "  async reserve-order",
+                        "    call stock-service",
+                        "    async shipping",
+                        "    async invoicing",
+                        "    error dead-orders");
+    }
+
+    @Test
+    void inAndOutAreWhatCrossesTheIntegration() {
+        About.Model m = RuntimeLinks.of(topology()).apply(sources(), new 
IntegrationInfo());
+
+        
assertThat(m.in()).extracting(About.In::label).containsExactly("file:orders");
+        assertThat(m.out()).isEmpty();
+    }
+
+    @Test
+    void anOutTheRuntimeSawOnAQueueOfTheIntegrationIsNoSystemOutside() {
+        RuntimeLinks links = RuntimeLinks.of(topology());
+
+        // as the runtime names it, with // after the scheme
+        assertThat(links.consumedHere(new About.Out("messaging", 
"jms://queue:orders", "out", "order-intake"))).isTrue();
+        assertThat(links.consumedHere(new About.Out("messaging", 
"jms://queue:billing", "out", "order-intake"))).isFalse();
+    }
+
+    @Test
+    void aQueueOthersSendToTooStaysAnInWithWhatTheySent() {
+        // 3 orders from order-intake, and 5 more another application put on 
the queue
+        IntegrationInfo info = new IntegrationInfo();
+        info.endpoints.add(endpoint("order-intake", "out", 
"jms://queue:orders", 3));
+        info.endpoints.add(endpoint("reserve-order", "in", 
"jms://queue:orders", 8));
+
+        About.Model m = RuntimeLinks.of(topology()).apply(sources(), info);
+
+        
assertThat(m.in()).extracting(About.In::label).containsExactly("file:orders", 
"jms:queue:orders  +5 from outside");
+    }
+
+    @Test
+    void anEdgeWithoutAnEndpointIsLeftOut() {
+        JsonObject topology = topology();
+        JsonArray edges = (JsonArray) topology.get("edges");
+        edges.add(edge("shipping", "invoicing", null, "call", true));
+
+        
assertThat(RuntimeLinks.of(topology).howReached()).doesNotContainEntry("invoicing",
 "call");
+    }
+
+    @Test
+    void withoutATopologyTheSourcesStay() {
+        About.Model m = sources();
+
+        assertThat(RuntimeLinks.of(null).apply(m, new 
IntegrationInfo())).isSameAs(m);
+        
assertThat(RuntimeLinks.of(topology()).howReached()).containsEntry("stock-service",
 "call")
+                .containsEntry("shipping", 
"async").containsEntry("dead-orders", "error")
+                .doesNotContainKey("order-intake");
+    }
+
+    private static EndpointInfo endpoint(String route, String direction, 
String uri, long hits) {
+        EndpointInfo ep = new EndpointInfo();
+        ep.routeId = route;
+        ep.direction = direction;
+        ep.uri = uri;
+        ep.hits = hits;
+        return ep;
+    }
+}

Reply via email to