Skip to content

Commit 57e0b55

Browse files
author
Mark Pollack
committed
Framework smoke matrix: the Spring WebFlux agent cells
The Spring program runs as a reactive web application with --web reactive: Reactor Netty on server.port with the SDK's WebFlux host (acp-http-webflux) routed at /acp. smoke.json gains a spring-webflux framework that reuses programs/spring with that flag, for http and ws, and GenConfigs learns two optional framework keys: agentArgs (flags the program's agent.sh gets before --transport) and agentName (the @AcpAgent name the initialize step expects). Generated: fw-spring-webflux-agent-http, fw-spring-webflux-agent-ws and load-10-fw-spring-webflux.
1 parent 10681f7 commit 57e0b55

9 files changed

Lines changed: 260 additions & 19 deletions

File tree

‎integration-testing/GenConfigs.java‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,11 @@ void validateSmoke(Set<String> ids, Set<String> langs, Set<String> transports) {
558558
if (!Files.exists(root.resolve(f.getValue().path("dir").asText()).resolve("launch/agent.sh"))) {
559559
problems.add("smoke.json: " + f.getKey() + ".dir has no launch/agent.sh");
560560
}
561+
for (String key : List.of("agentArgs", "agentName")) {
562+
if (f.getValue().has(key) && !f.getValue().path(key).isTextual()) {
563+
problems.add("smoke.json: " + f.getKey() + "." + key + " must be a string");
564+
}
565+
}
561566
});
562567
}
563568

@@ -604,6 +609,12 @@ void generateSmoke(Map<String, String> out) throws Exception {
604609
}
605610
}
606611

612+
/** The framework program's extra agent flags (smoke.json agentArgs), with a leading space, or none. */
613+
static String agentArgs(JsonNode fw) {
614+
String args = fw.path("agentArgs").asText("").trim();
615+
return args.isEmpty() ? "" : " " + args;
616+
}
617+
607618
String fwBuild(JsonNode fw) {
608619
return "IT_ROOT=\"${ROOT}\" MVNW=\"${MVNW}\" ACP_VERSION=\"${ACP_VERSION}\" CACHE=\"${CACHE}\" ${ROOT}/"
609620
+ fw.path("dir").asText() + "/launch/build.sh";
@@ -640,7 +651,8 @@ ObjectNode smokeCell(String name, String framework, JsonNode fw, String peer, St
640651
a.put("role", "server");
641652
a.put("dir", "${ROOT}/" + agentDir);
642653
a.put("build", agentBuild);
643-
a.put("run", "${ROOT}/" + agentDir + "/launch/agent.sh --transport " + transport + " --port ${PORT}");
654+
a.put("run", "${ROOT}/" + agentDir + "/launch/agent.sh" + (agentSide ? agentArgs(fw) : "") + " --transport "
655+
+ transport + " --port ${PORT}");
644656
ObjectNode aenv = a.putObject("env");
645657
env.forEach(aenv::put);
646658
ObjectNode ready = a.putObject("ready");
@@ -662,7 +674,8 @@ ObjectNode smokeCell(String name, String framework, JsonNode fw, String peer, St
662674
ObjectNode cenv = cl.putObject("env");
663675
cenv.put("STEPS", String.join(",", ids));
664676
if (stdio) {
665-
cenv.put("AGENT_CMD", "${ROOT}/" + agentDir + "/launch/agent.sh --transport stdio");
677+
cenv.put("AGENT_CMD", "${ROOT}/" + agentDir + "/launch/agent.sh" + (agentSide ? agentArgs(fw) : "")
678+
+ " --transport stdio");
666679
}
667680
env.forEach(cenv::put);
668681
cl.put("timeoutSec", clientTimeout);
@@ -683,7 +696,7 @@ ObjectNode smokeCell(String name, String framework, JsonNode fw, String peer, St
683696
putLines(as, "forbiddenOutput", forbidden);
684697
Map<String, Set<String>> patterns = new LinkedHashMap<>();
685698
patterns.put("client", new LinkedHashSet<>(List.of("STEP init\\.initialize PASS .*\"name\":\"interop-"
686-
+ framework + "-agent\"")));
699+
+ fw.path("agentName").asText(framework) + "-agent\"")));
687700
putLines(as, "requiredPatterns", patterns);
688701
}
689702
ArrayNode checks = as.putArray("checks");
@@ -712,7 +725,8 @@ ObjectNode smokeLoad(String name, String framework, JsonNode fw) {
712725
a.put("role", "server");
713726
a.put("dir", "${ROOT}/" + fw.path("dir").asText());
714727
a.put("build", fwBuild(fw));
715-
a.put("run", "${ROOT}/" + fw.path("dir").asText() + "/launch/agent.sh --transport http --port ${PORT}");
728+
a.put("run", "${ROOT}/" + fw.path("dir").asText() + "/launch/agent.sh" + agentArgs(fw)
729+
+ " --transport http --port ${PORT}");
716730
a.putObject("env").put("JAVA_OPTS", load.path("serverJavaOpts").asText("-Xmx512m"));
717731
ObjectNode ready = a.putObject("ready");
718732
ready.put("line", "READY");

‎integration-testing/README.md‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ There are four kinds of scenarios:
2020
- **Conformance scenarios**, `configs/conf-java-*.json`: the raw JSON-RPC driver
2121
(`programs/raw`) against the Java agent and client, generated by `programs/raw/gen_conf.py`.
2222
- **Framework smoke cells**, `configs/fw-*.json` and `configs/load-10-fw-*.json`: the SDK hosted
23-
by Spring Boot, Micronaut and Quarkus against the TypeScript SDK, generated from `smoke.json` by
23+
by Spring Boot (servlet and WebFlux), Micronaut and Quarkus against the TypeScript SDK, generated from `smoke.json` by
2424
`GenConfigs.java` ([Framework smoke matrix](#framework-smoke-matrix)).
2525
- **Load scenarios**, `configs/load-*.json`, hand-written (except `load-10-fw-*`).
2626

@@ -158,13 +158,17 @@ the commit being released (`framework-smoke.yml`, or the local command below), n
158158
| Framework | Agent transports | Client | Program |
159159
|---|---|---|---|
160160
| Spring Boot 4.1 | stdio (`spring.main.keep-alive=true`), HTTP and WebSocket (the SDK servlet, `acp-http-servlet`, on `server.port` in a servlet web application) | HTTP | [programs/spring](programs/spring) |
161+
| Spring Boot 4.1, WebFlux (`spring-webflux`) | HTTP and WebSocket (the SDK's WebFlux host, `acp-http-webflux`, routed on `server.port` by Reactor Netty in a reactive web application) | none | [programs/spring](programs/spring), `--web reactive` |
161162
| Micronaut 4 | stdio, HTTP, WebSocket (the SDK listener) | HTTP | [programs/micronaut](programs/micronaut) |
162163
| Quarkus | stdio, HTTP, WebSocket (the Quarkus HTTP server); one package per build-time transport | HTTP | [programs/quarkus](programs/quarkus) |
163164

164165
The agent handlers ([programs/framework/.../SmokeAgent.java](programs/framework/src/main/java/interop/framework/SmokeAgent.java))
165166
and the client steps ([SmokeClient.java](programs/framework/src/main/java/interop/framework/SmokeClient.java))
166167
are shared sources that every framework program compiles in; each framework adds only an
167-
`@AcpAgent` subclass with its own bean annotation and a main class. To run the whole matrix,
168+
`@AcpAgent` subclass with its own bean annotation and a main class. A framework entry in
169+
`smoke.json` may reuse another's program with `agentArgs` (flags its `agent.sh` gets before
170+
`--transport`, as `spring-webflux` runs `programs/spring` with `--web reactive`) and `agentName`
171+
(the `@AcpAgent` name the `init.initialize` step expects, `interop-<agentName>-agent`). To run the whole matrix,
168172
locally or against another branch:
169173

170174
```bash
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"name": "fw-spring-webflux-agent-http",
3+
"description": "typescript client -> spring-webflux-hosted java agent over http, 14 smoke steps (generated from smoke.json)",
4+
"timeoutSec": 370,
5+
"peers": [
6+
"typescript-sdk"
7+
],
8+
"processes": [
9+
{
10+
"name": "agent",
11+
"language": "java",
12+
"role": "server",
13+
"dir": "${ROOT}/programs/spring",
14+
"build": "IT_ROOT=\"${ROOT}\" MVNW=\"${MVNW}\" ACP_VERSION=\"${ACP_VERSION}\" CACHE=\"${CACHE}\" ${ROOT}/programs/spring/launch/build.sh",
15+
"run": "${ROOT}/programs/spring/launch/agent.sh --web reactive --transport http --port ${PORT}",
16+
"env": {
17+
"TS_SDK": "${peer.typescript-sdk}"
18+
},
19+
"ready": {
20+
"line": "READY",
21+
"timeoutSec": 90
22+
}
23+
},
24+
{
25+
"name": "client",
26+
"language": "typescript",
27+
"role": "client",
28+
"dir": "${ROOT}/programs/node",
29+
"build": "IT_ROOT=\"${ROOT}\" MVNW=\"${MVNW}\" ACP_VERSION=\"${ACP_VERSION}\" CACHE=\"${CACHE}\" TS_SDK=\"${peer.typescript-sdk}\" ${ROOT}/programs/node/launch/build.sh",
30+
"run": "${ROOT}/programs/node/launch/client.sh --transport http --url http://127.0.0.1:${PORT}/acp",
31+
"env": {
32+
"STEPS": "init.initialize,init.agent-capabilities,init.agent-info,session.new,session.load,session.multi,update.agent_message_chunk,perm.selected,elicit.form,cancel.prompt,ext.client-request,error.method-not-found,http.reconnect,conn.close",
33+
"TS_SDK": "${peer.typescript-sdk}"
34+
},
35+
"timeoutSec": 190
36+
}
37+
],
38+
"assertions": {
39+
"requiredOutput": {
40+
"agent": [
41+
"STEP agent.perm.selected PASS",
42+
"STEP agent.elicit.form PASS",
43+
"STEP agent.cancel.prompt PASS"
44+
]
45+
},
46+
"forbiddenOutput": {
47+
"agent": [
48+
" ERROR ",
49+
" WARN "
50+
]
51+
},
52+
"requiredPatterns": {
53+
"client": [
54+
"STEP init\\.initialize PASS .*\"name\":\"interop-spring-agent\""
55+
]
56+
},
57+
"checks": [
58+
"client RESULT pass == 14",
59+
"client RESULT fail == 0"
60+
],
61+
"expectedFailures": [],
62+
"report": {
63+
"client": [
64+
"TIMEOUT"
65+
]
66+
}
67+
},
68+
"comment": "GENERATED by GenConfigs.java from smoke.json, steps.json and matrix.json; do not edit, regenerate."
69+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"name": "fw-spring-webflux-agent-ws",
3+
"description": "typescript client -> spring-webflux-hosted java agent over ws, 13 smoke steps (generated from smoke.json)",
4+
"timeoutSec": 365,
5+
"peers": [
6+
"typescript-sdk"
7+
],
8+
"processes": [
9+
{
10+
"name": "agent",
11+
"language": "java",
12+
"role": "server",
13+
"dir": "${ROOT}/programs/spring",
14+
"build": "IT_ROOT=\"${ROOT}\" MVNW=\"${MVNW}\" ACP_VERSION=\"${ACP_VERSION}\" CACHE=\"${CACHE}\" ${ROOT}/programs/spring/launch/build.sh",
15+
"run": "${ROOT}/programs/spring/launch/agent.sh --web reactive --transport ws --port ${PORT}",
16+
"env": {
17+
"TS_SDK": "${peer.typescript-sdk}"
18+
},
19+
"ready": {
20+
"line": "READY",
21+
"timeoutSec": 90
22+
}
23+
},
24+
{
25+
"name": "client",
26+
"language": "typescript",
27+
"role": "client",
28+
"dir": "${ROOT}/programs/node",
29+
"build": "IT_ROOT=\"${ROOT}\" MVNW=\"${MVNW}\" ACP_VERSION=\"${ACP_VERSION}\" CACHE=\"${CACHE}\" TS_SDK=\"${peer.typescript-sdk}\" ${ROOT}/programs/node/launch/build.sh",
30+
"run": "${ROOT}/programs/node/launch/client.sh --transport ws --url ws://127.0.0.1:${PORT}/acp",
31+
"env": {
32+
"STEPS": "init.initialize,init.agent-capabilities,init.agent-info,session.new,session.load,session.multi,update.agent_message_chunk,perm.selected,elicit.form,cancel.prompt,ext.client-request,error.method-not-found,conn.close",
33+
"TS_SDK": "${peer.typescript-sdk}"
34+
},
35+
"timeoutSec": 185
36+
}
37+
],
38+
"assertions": {
39+
"requiredOutput": {
40+
"agent": [
41+
"STEP agent.perm.selected PASS",
42+
"STEP agent.elicit.form PASS",
43+
"STEP agent.cancel.prompt PASS"
44+
]
45+
},
46+
"forbiddenOutput": {
47+
"agent": [
48+
" ERROR ",
49+
" WARN "
50+
]
51+
},
52+
"requiredPatterns": {
53+
"client": [
54+
"STEP init\\.initialize PASS .*\"name\":\"interop-spring-agent\""
55+
]
56+
},
57+
"checks": [
58+
"client RESULT pass == 13",
59+
"client RESULT fail == 0"
60+
],
61+
"expectedFailures": [],
62+
"report": {
63+
"client": [
64+
"TIMEOUT"
65+
]
66+
}
67+
},
68+
"comment": "GENERATED by GenConfigs.java from smoke.json, steps.json and matrix.json; do not edit, regenerate."
69+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"name": "load-10-fw-spring-webflux",
3+
"description": "10 connections x 10 prompts against the spring-webflux-hosted agent over http, two streamed updates each (generated from smoke.json)",
4+
"timeoutSec": 420,
5+
"processes": [
6+
{
7+
"name": "server",
8+
"language": "java",
9+
"role": "server",
10+
"dir": "${ROOT}/programs/spring",
11+
"build": "IT_ROOT=\"${ROOT}\" MVNW=\"${MVNW}\" ACP_VERSION=\"${ACP_VERSION}\" CACHE=\"${CACHE}\" ${ROOT}/programs/spring/launch/build.sh",
12+
"run": "${ROOT}/programs/spring/launch/agent.sh --web reactive --transport http --port ${PORT}",
13+
"env": {
14+
"JAVA_OPTS": "-Xmx512m"
15+
},
16+
"ready": {
17+
"line": "READY",
18+
"timeoutSec": 90
19+
}
20+
},
21+
{
22+
"name": "clients",
23+
"language": "java",
24+
"role": "client",
25+
"dir": "${ROOT}/programs/java",
26+
"build": "IT_ROOT=\"${ROOT}\" MVNW=\"${MVNW}\" ACP_VERSION=\"${ACP_VERSION}\" CACHE=\"${CACHE}\" ${ROOT}/programs/java/launch/build.sh",
27+
"run": "java -Xmx1g -Dacp.client.level=INFO -cp target/classes:$(cat target/classpath.txt) load.LoadClients http://127.0.0.1:${PORT}/acp 10 10 perclient",
28+
"timeoutSec": 300
29+
}
30+
],
31+
"assertions": {
32+
"requiredOutput": {
33+
"clients": [
34+
"PEAK_REACHED",
35+
"CLOSED"
36+
]
37+
},
38+
"forbiddenOutput": {
39+
"clients": [
40+
"ERROR ",
41+
"CLOSE_ERROR"
42+
],
43+
"server": [
44+
" ERROR "
45+
]
46+
},
47+
"checks": [
48+
"clients RESULT errors == 0",
49+
"clients RESULT ok == expected",
50+
"clients RESULT expected == 100",
51+
"clients RESULT updates == 2*ok",
52+
"clients RESULT close_errors == 0"
53+
]
54+
},
55+
"comment": "GENERATED by GenConfigs.java from smoke.json, steps.json and matrix.json; do not edit, regenerate."
56+
}

‎integration-testing/programs/spring/launch/agent.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env bash
2-
# Starts the Spring Boot smoke agent: --transport stdio | --transport http|ws --port <port>.
2+
# Starts the Spring Boot smoke agent: [--web servlet|reactive] --transport stdio | --transport http|ws --port <port>.
33
# Launcher contract: integration-testing/README.md, "Contracts". Prints nothing itself: on stdio,
44
# stdout is the protocol stream (the application logs to stderr). JAVA_OPTS, if set, is passed on.
55
set -euo pipefail

‎integration-testing/programs/spring/pom.xml‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
<!--
33
The Spring Boot programs of the framework smoke cells (integration-testing/smoke.json): an
44
@AcpAgent bean served by acp-spring-boot-starter (interop.spring.agent: stdio, or Streamable HTTP
5-
on the servlet the starter mounts on server.port), and an ACP client bean
5+
and WebSocket on server.port, through the servlet in a servlet web application or the WebFlux
6+
host in a reactive one), and an ACP client bean
67
the starter builds from spring.acp.client.* (interop.spring.client). The agent's handlers and the
78
client's steps are the shared sources of programs/framework. Not a module of the SDK reactor:
89
launch/build.sh builds it against the SDK installed from this checkout (-Dacp.version) and the
@@ -54,6 +55,17 @@
5455
<artifactId>acp-http-servlet</artifactId>
5556
<version>${acp.version}</version>
5657
</dependency>
58+
<!-- With the reactive web flag, a WebFlux application: the SDK's WebFlux host routed on the
59+
application's own Reactor Netty server (server.port), Streamable HTTP and WebSocket -->
60+
<dependency>
61+
<groupId>org.springframework.boot</groupId>
62+
<artifactId>spring-boot-starter-webflux</artifactId>
63+
</dependency>
64+
<dependency>
65+
<groupId>com.agentclientprotocol</groupId>
66+
<artifactId>acp-http-webflux</artifactId>
67+
<version>${acp.version}</version>
68+
</dependency>
5769
</dependencies>
5870

5971
<build>

‎integration-testing/programs/spring/src/main/java/interop/spring/agent/SpringAgentApp.java‎

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,37 @@
1010
/**
1111
* The launcher contract (integration-testing/README.md, "Contracts") over a Spring Boot 4
1212
* application whose {@code @AcpAgent} bean ({@link SpringSmokeAgent}) acp-spring-boot-starter
13-
* serves: {@code --transport stdio}, or {@code --transport http --port <port>}, which runs a
14-
* servlet web application on {@code server.port} with the SDK's Streamable HTTP servlet mounted at
15-
* {@code /acp}, and prints {@code READY <port>} once it accepts connections. This class only maps
16-
* the flags onto Spring properties. Over stdio the application is not a web application, is kept
13+
* serves: {@code --transport stdio}, or {@code --transport http|ws --port <port>}, which runs a
14+
* web application on {@code server.port} with the endpoint at {@code /acp} (Streamable HTTP and
15+
* the WebSocket upgrade on that one path) and prints {@code READY <port>} once it accepts
16+
* connections. {@code --web servlet} (the default) is a servlet web application with the SDK's
17+
* servlet ({@code acp-http-servlet}) on Tomcat; {@code --web reactive} a WebFlux application with
18+
* the SDK's WebFlux host ({@code acp-http-webflux}) on Reactor Netty. This class only maps the
19+
* flags onto Spring properties. Over stdio the application is not a web application, is kept
1720
* alive by {@code spring.main.keep-alive} and exits when its input ends
1821
* ({@code spring.acp.agent.shutdown-on-transport-end}); over HTTP it runs until SIGTERM.
19-
*
20-
* <p>{@code --transport ws} is refused: the servlet takes no WebSocket upgrades on
21-
* {@code server.port} yet (TODO: the WebSocket cell, smoke.json "pendingAgentTransports").
2222
*/
2323
@SpringBootApplication
2424
public class SpringAgentApp {
2525

2626
public static void main(String[] args) {
2727
String transport = null;
2828
String port = "0";
29+
String web = "servlet";
2930
for (int i = 0; i < args.length; i++) {
3031
switch (args[i]) {
3132
case "--transport" -> transport = value(args, ++i);
3233
case "--port" -> port = value(args, ++i);
34+
case "--web" -> web = value(args, ++i);
3335
default -> usage("unknown flag " + args[i]);
3436
}
3537
}
3638
if (transport == null) {
3739
usage("--transport is required");
3840
}
41+
if (!"servlet".equals(web) && !"reactive".equals(web)) {
42+
usage("unsupported --web " + web + " (servlet or reactive)");
43+
}
3944
boolean stdio = "stdio".equals(transport);
4045
if (!stdio && !"http".equals(transport) && !"ws".equals(transport)) {
4146
usage("unsupported transport " + transport + " (the Spring agent serves stdio, http and ws)");
@@ -48,10 +53,10 @@ public static void main(String[] args) {
4853
properties.put("spring.acp.agent.transport.type", "stdio");
4954
}
5055
else {
51-
properties.put("spring.main.web-application-type", "servlet");
56+
properties.put("spring.main.web-application-type", web);
5257
properties.put("server.address", "127.0.0.1");
5358
properties.put("server.port", port);
54-
// http and ws are one endpoint: the servlet takes the WebSocket upgrade on its path.
59+
// http and ws are one endpoint: the host takes the WebSocket upgrade on its path.
5560
properties.put("spring.acp.agent.transport.type", "ws".equals(transport) ? "websocket" : "http");
5661
properties.put("spring.acp.agent.transport.http.path", "/acp");
5762
}
@@ -74,7 +79,7 @@ private static String value(String[] args, int i) {
7479

7580
private static void usage(String problem) {
7681
System.err.println(problem);
77-
System.err.println("usage: agent.sh --transport stdio | --transport http|ws --port <port>");
82+
System.err.println("usage: agent.sh [--web servlet|reactive] --transport stdio | --transport http|ws --port <port>");
7883
System.exit(2);
7984
}
8085

0 commit comments

Comments
 (0)