Skip to content

Commit 4d376f7

Browse files
committed
Add CLI verdicts when stopping a test session
1 parent e171c5b commit 4d376f7

10 files changed

Lines changed: 273 additions & 18 deletions

File tree

agent/resources/skills/testar-cli/SKILL.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ Treat the CLI distribution as an operational environment, not as a source-code w
3838
- `executeAction click <semanticText>`
3939
- `executeAction type <semanticText> <inputText>`
4040
- `executeAction select <semanticText> <value>`
41-
- `stopSession`
41+
- `stopSession LLM_COMPLETE <reason>`
42+
- `stopSession LLM_INVALID <reason>`
4243
- `shutdownDaemon`
4344

4445
## Semantic action matching
@@ -68,6 +69,19 @@ Use short meaningful text that is likely to appear in the action description, fo
6869
6. Stop the session when the goal is finished.
6970
7. Shutdown the daemon after stopping the session.
7071

72+
## Test goal verdicts
73+
74+
Agent CLI executions must finish each test goal with exactly one explicit verdict command:
75+
76+
- `stopSession LLM_COMPLETE <reason>`
77+
- `stopSession LLM_INVALID <reason>`
78+
79+
Use `LLM_COMPLETE` only when the requested test goal was completed and the expected result was verified with CLI evidence.
80+
81+
Use `LLM_INVALID` when the expected result is not verified, a bug or invalid state is observed, the goal cannot be completed, or the available evidence is not reliable enough to declare completion.
82+
83+
The `<reason>` must briefly explain the final decision using observed CLI evidence.
84+
7185
## Resilience and retry behavior rules
7286

7387
Some SUTs may need time to start, load screens, populate state information, or expose derived actions after an action is executed.
@@ -100,7 +114,7 @@ Workspace-driven Windows testing:
100114
- `testar-cli.bat getDerivedActions`
101115
- `testar-cli.bat executeAction click Open`
102116
- `testar-cli.bat executeAction type Editor Writing_text_in_Notepad`
103-
- `testar-cli.bat stopSession`
117+
- `testar-cli.bat stopSession LLM_COMPLETE "Notepad opened and text was entered successfully."`
104118
- `testar-cli.bat shutdownDaemon`
105119

106120
Workspace-driven WebDriver testing:
@@ -113,7 +127,7 @@ Workspace-driven WebDriver testing:
113127
- `testar-cli.bat executeAction click a_about`
114128
- `testar-cli.bat executeAction type input_contact This_is_a_message`
115129
- `testar-cli.bat executeAction select select_amount 99999`
116-
- `testar-cli.bat stopSession`
130+
- `testar-cli.bat stopSession LLM_INVALID "There is no confirmation that the transaction was sent successfully."`
117131
- `testar-cli.bat shutdownDaemon`
118132

119133
Workspace-driven Android testing:
@@ -125,7 +139,7 @@ Workspace-driven Android testing:
125139
- `testar-cli.bat getDerivedActions`
126140
- `testar-cli.bat executeAction click Graphics`
127141
- `testar-cli.bat executeAction type username Writing_text_in_Android`
128-
- `testar-cli.bat stopSession`
142+
- `testar-cli.bat stopSession LLM_COMPLETE "The expected Android state was verified."`
129143
- `testar-cli.bat shutdownDaemon`
130144

131145
Explicit standalone startup wihtout workspace:

cli/src/org/testar/cli/CliDaemonServer.java

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.testar.core.state.State;
2727
import org.testar.core.state.Widget;
2828
import org.testar.core.tag.Tags;
29+
import org.testar.core.verdict.Verdict;
2930
import org.testar.cli.profile.CliProfileConfiguration;
3031
import org.testar.cli.profile.CliProfileConfigurationLoader;
3132
import org.testar.plugin.OperatingSystems;
@@ -105,7 +106,7 @@ private CliResponse handle(CliRequest request) {
105106
case EXECUTE_ACTION:
106107
return executeAction(request);
107108
case STOP_SESSION:
108-
return stopSession();
109+
return stopSession(request);
109110
case SHUTDOWN_DAEMON:
110111
return shutdownDaemon();
111112
case HELP:
@@ -125,9 +126,6 @@ private CliResponse startSession(CliRequest request) {
125126
lines.add("platform=" + sessionSpec.getOperatingSystem().name().toLowerCase(Locale.ROOT));
126127
lines.add("pid=" + activeSession.system().get(Tags.PID, -1L));
127128
lines.add("desc=" + activeSession.system().get(Tags.Desc, ""));
128-
for (String warning : preparedSession.profileConfiguration().compatibilityWarnings()) {
129-
lines.add("warning=" + warning);
130-
}
131129
return new CliResponse(0, lines);
132130
} catch (RuntimeException exception) {
133131
return new CliResponse(1, List.of("startSession failed: " + exception.getMessage()));
@@ -209,15 +207,22 @@ private CliResponse executeAction(CliRequest request) {
209207
}
210208
}
211209

212-
private CliResponse stopSession() {
210+
private CliResponse stopSession(CliRequest request) {
213211
try {
212+
List<Verdict> finalVerdicts = finalVerdictsFromStopRequest(request);
214213
PlatformSession session = requireActiveSession();
215214
long pid = session.system().get(Tags.PID, -1L);
216-
session.stopSystem();
217-
closeActiveSession();
215+
try {
216+
session.stopSystem(finalVerdicts);
217+
} finally {
218+
closeStoppedSession(session);
219+
}
220+
Verdict finalVerdict = finalVerdicts.get(0);
218221
return new CliResponse(0, List.of(
219222
"systemStopped",
220-
"pid=" + pid
223+
"pid=" + pid,
224+
"verdict=" + finalVerdict.verdictSeverityTitle(),
225+
"info=" + finalVerdict.info()
221226
));
222227
} catch (RuntimeException exception) {
223228
return new CliResponse(1, List.of("stopSession failed: " + exception.getMessage()));
@@ -271,6 +276,18 @@ private synchronized void closeActiveSession() {
271276
}
272277
}
273278

279+
private synchronized void closeStoppedSession(PlatformSession session) {
280+
try {
281+
session.close();
282+
} finally {
283+
if (activeSession == session) {
284+
activeSession = null;
285+
activeSessionSpec = null;
286+
activePolicyConfiguration = PolicySessionConfiguration.defaults();
287+
}
288+
}
289+
}
290+
274291
private void shutdownIfRequested() {
275292
if (!shutdownRequested) {
276293
return;
@@ -400,6 +417,42 @@ private OperatingSystems parseOperatingSystem(String token) {
400417
throw new IllegalArgumentException("Unsupported platform token: " + token);
401418
}
402419

420+
List<Verdict> finalVerdictsFromStopRequest(CliRequest request) {
421+
String verdictName = request.argumentAt(0);
422+
if (verdictName == null || verdictName.isBlank()) {
423+
return List.of(Verdict.OK);
424+
}
425+
426+
Verdict.Severity severity = parseFinalVerdictSeverity(verdictName);
427+
String info = buildFinalVerdictInfo(request, severity);
428+
return List.of(new Verdict(severity, info));
429+
}
430+
431+
private Verdict.Severity parseFinalVerdictSeverity(String verdictName) {
432+
String normalizedVerdictName = verdictName.trim().toUpperCase(Locale.ROOT);
433+
if (Verdict.Severity.LLM_COMPLETE.name().equals(normalizedVerdictName)) {
434+
return Verdict.Severity.LLM_COMPLETE;
435+
}
436+
if (Verdict.Severity.LLM_INVALID.name().equals(normalizedVerdictName)) {
437+
return Verdict.Severity.LLM_INVALID;
438+
}
439+
440+
throw new IllegalArgumentException(
441+
"Unsupported stopSession verdict: " + verdictName + ". Expected LLM_COMPLETE or LLM_INVALID."
442+
);
443+
}
444+
445+
private String buildFinalVerdictInfo(CliRequest request, Verdict.Severity severity) {
446+
List<String> arguments = request.getArguments();
447+
if (arguments.size() <= 1) {
448+
return severity == Verdict.Severity.LLM_COMPLETE
449+
? "Agent completed the test goal."
450+
: "Agent marked the test goal as invalid.";
451+
}
452+
453+
return String.join(" ", arguments.subList(1, arguments.size())).trim();
454+
}
455+
403456
private List<String> describeStateWidgets(State state) {
404457
List<String> descriptions = new ArrayList<>();
405458
for (Widget widget : state) {

cli/src/org/testar/cli/CliRunner.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ private void printHelp() {
5757
output.println(" executeAction click <semanticText>");
5858
output.println(" executeAction type <semanticText> <inputText>");
5959
output.println(" executeAction select <semanticText> <value>");
60-
output.println(" stopSession");
60+
output.println(" stopSession [LLM_COMPLETE|LLM_INVALID <reason>]");
6161
output.println(" shutdownDaemon");
6262
}
6363

cli/test/org/testar/cli/CliDaemonServerTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.testar.config.ConfigTags;
99
import org.testar.config.settings.Settings;
1010
import org.testar.config.settings.SettingsDefaults;
11+
import org.testar.core.verdict.Verdict;
1112
import org.testar.plugin.OperatingSystems;
1213
import org.testar.plugin.configuration.PlatformSessionSpecification;
1314

@@ -141,6 +142,60 @@ public void resolveCliTargetRejectsDesktopAttachConnector() {
141142
}
142143
}
143144

145+
@Test
146+
public void stopSessionWithoutVerdictDefaultsToOk() {
147+
CliDaemonServer server = new CliDaemonServer();
148+
149+
List<Verdict> verdicts = server.finalVerdictsFromStopRequest(
150+
CliRequest.of(CliCommand.STOP_SESSION, List.of())
151+
);
152+
153+
Assert.assertEquals(1, verdicts.size());
154+
Assert.assertEquals(Verdict.OK, verdicts.get(0));
155+
}
156+
157+
@Test
158+
public void stopSessionAcceptsLlmCompleteWithReason() {
159+
CliDaemonServer server = new CliDaemonServer();
160+
161+
List<Verdict> verdicts = server.finalVerdictsFromStopRequest(
162+
CliRequest.of(CliCommand.STOP_SESSION, List.of("LLM_COMPLETE", "Login", "verified"))
163+
);
164+
165+
Assert.assertEquals(1, verdicts.size());
166+
Assert.assertEquals(Verdict.Severity.LLM_COMPLETE.getValue(), verdicts.get(0).severity(), 0.0);
167+
Assert.assertEquals("Login verified", verdicts.get(0).info());
168+
}
169+
170+
@Test
171+
public void stopSessionAcceptsLlmInvalidWithReason() {
172+
CliDaemonServer server = new CliDaemonServer();
173+
174+
List<Verdict> verdicts = server.finalVerdictsFromStopRequest(
175+
CliRequest.of(CliCommand.STOP_SESSION, List.of("LLM_INVALID", "Welcome message was not shown"))
176+
);
177+
178+
Assert.assertEquals(1, verdicts.size());
179+
Assert.assertEquals(Verdict.Severity.LLM_INVALID.getValue(), verdicts.get(0).severity(), 0.0);
180+
Assert.assertEquals("Welcome message was not shown", verdicts.get(0).info());
181+
}
182+
183+
@Test
184+
public void stopSessionRejectsUnsupportedFinalVerdict() {
185+
CliDaemonServer server = new CliDaemonServer();
186+
187+
try {
188+
server.finalVerdictsFromStopRequest(
189+
CliRequest.of(CliCommand.STOP_SESSION, List.of("WARNING", "Not valid for agent finalization"))
190+
);
191+
Assert.fail("Expected unsupported stopSession verdict failure");
192+
} catch (IllegalArgumentException exception) {
193+
Assert.assertTrue(exception.getMessage().contains("Unsupported stopSession verdict"));
194+
Assert.assertTrue(exception.getMessage().contains("LLM_COMPLETE"));
195+
Assert.assertTrue(exception.getMessage().contains("LLM_INVALID"));
196+
}
197+
}
198+
144199
private static Settings defaultSettings() {
145200
Properties properties = new Properties();
146201
properties.setProperty(ConfigTags.SUTConnectorValue.name(), "notepad.exe");

docs/architecture_distribution_workspace.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,36 @@ This prevents CLI from failing only because unrelated scriptless-only Java files
142142

143143
Oracle composer entries are scriptless-only.
144144

145-
CLI agent runs should use the agent as the oracle for LLM-complete verdicts, such as `LLM_COMPLETE_VALID` and `LLM_COMPLETE_INVALID`.
145+
CLI agent runs should use the agent as the oracle for LLM verdicts, such as `LLM_COMPLETE` and `LLM_INVALID`.
146+
147+
## CLI Session Verdict Contract
148+
149+
`stopSession` is the CLI session finalization command.
150+
151+
Manual CLI execution may use:
152+
153+
```text
154+
stopSession
155+
```
156+
157+
Plain `stopSession` defaults the final session verdict to `OK`.
158+
159+
Agent CLI execution must finish each test goal with an explicit LLM verdict:
160+
161+
```text
162+
stopSession LLM_COMPLETE <reason>
163+
stopSession LLM_INVALID <reason>
164+
```
165+
166+
`LLM_COMPLETE` means the agent completed the requested test goal and verified the expected result with CLI evidence.
167+
168+
`LLM_INVALID` means the agent reached an invalid result, found a bug, could not verify the expected result, or could not complete the goal with reliable CLI evidence.
169+
170+
The reason is free text and should explain the final decision.
171+
172+
If reporting is enabled, the final CLI verdict must be written to the generated report artifacts.
173+
174+
The `testar-cli` skill documentation is the operational source that tells agents how to use this contract.
146175

147176
## Shared Settings
148177

docs/webstudio/WEBSTUDIO_FUNCTIONAL_SPEC.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,27 @@ Dialog actions:
360360
- `Discard`: restore the last persisted Agent CLI settings in the CLI panel, then continue the pending action
361361
- `Cancel`: keep the user in CLI mode and abort the pending action
362362

363+
### CLI Verdict Finalization
364+
365+
Manual CLI execution may stop a session with plain `stopSession`.
366+
367+
Plain `stopSession` must finalize the session with an `OK` verdict.
368+
369+
Agent CLI execution must stop a session with an explicit LLM verdict:
370+
371+
- `stopSession LLM_COMPLETE <reason>`
372+
- `stopSession LLM_INVALID <reason>`
373+
374+
`LLM_COMPLETE` means the agent completed the test goal and verified the expected result.
375+
376+
`LLM_INVALID` means the agent found an invalid result, could not verify the expected result, or could not complete the goal with reliable evidence.
377+
378+
WebStudio must include this finalization rule in the Agent CLI prompt contract.
379+
380+
If an Agent CLI execution ends without an explicit LLM verdict, WebStudio should treat the execution as incomplete or invalid and surface that status to the user.
381+
382+
When CLI reporting is enabled, generated report artifacts must include the final CLI verdict and reason.
383+
363384
## Test Results
364385

365386
The Test Results page supports inspection of output folders from the shared TESTAR distribution.

plugin/src/org/testar/plugin/DefaultPlatformSession.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.testar.core.action.resolver.ResolvedAction;
1515
import org.testar.core.state.SUT;
1616
import org.testar.core.state.State;
17+
import org.testar.core.verdict.Verdict;
1718
import org.testar.plugin.reporting.SessionReportingManager;
1819

1920
final class DefaultPlatformSession implements PlatformSession {
@@ -88,7 +89,7 @@ public boolean executeAction(Action action) {
8889
}
8990

9091
@Override
91-
public void stopSystem() {
92+
public void stopSystem(List<Verdict> finalVerdicts) {
9293
try {
9394
State state = services.stateService().getState(system);
9495
if (reportingEnabled) {
@@ -104,7 +105,7 @@ public void stopSystem() {
104105
services.systemService().stopSystem(system);
105106
} finally {
106107
if (reportingEnabled) {
107-
sessionReportingManager.finish();
108+
sessionReportingManager.finish(finalVerdicts);
108109
}
109110
}
110111
}

plugin/src/org/testar/plugin/PlatformSession.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66

77
package org.testar.plugin;
88

9-
import java.util.Set;
109
import java.util.List;
10+
import java.util.Set;
1111

1212
import org.testar.core.action.Action;
1313
import org.testar.core.action.resolver.ResolvedAction;
1414
import org.testar.core.state.SUT;
1515
import org.testar.core.state.State;
16+
import org.testar.core.verdict.Verdict;
1617

1718
/**
1819
* Live platform session holding the started system plus the composed services
@@ -30,7 +31,11 @@ public interface PlatformSession extends AutoCloseable {
3031

3132
boolean executeAction(Action action);
3233

33-
void stopSystem();
34+
default void stopSystem() {
35+
stopSystem(List.of(Verdict.OK));
36+
}
37+
38+
void stopSystem(List<Verdict> finalVerdicts);
3439

3540
@Override
3641
void close();

0 commit comments

Comments
 (0)