Skip to content

Commit 6252964

Browse files
committed
Associate webstudio output results to workspace
1 parent a06068c commit 6252964

15 files changed

Lines changed: 370 additions & 84 deletions

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ private CliPreparedSession buildPreparedSession(CliRequest request) {
478478
private CliPreparedSession buildPreparedSessionFromWorkspace(CliRequest request) {
479479
String normalizedProfileName = normalizeProfileName(request.argumentAt(0));
480480
Settings profileSettings = CliSettingsLoader.loadProfile(normalizedProfileName);
481+
applyWorkspaceOutputDirectory(profileSettings, normalizedProfileName);
481482
CliSessionTarget sessionTarget = CliSessionTargetResolver.resolve(profileSettings);
482483
PlatformSessionSpecification sessionSpec = PlatformSessionSpecFactory.create(
483484
sessionTarget.operatingSystem(),
@@ -499,6 +500,7 @@ private CliPreparedSession buildPreparedSessionFromExplicitTarget(CliRequest req
499500
: profileName.trim();
500501

501502
Settings profileSettings = CliSettingsLoader.loadProfile(normalizedProfileName);
503+
applyWorkspaceOutputDirectory(profileSettings, normalizedProfileName);
502504
PlatformSessionSpecification sessionSpec = buildSessionSpec(request, profileSettings);
503505
CliProfileConfiguration profileConfiguration = CliProfileConfigurationLoader.load(
504506
normalizedProfileName,
@@ -515,6 +517,10 @@ private String normalizeProfileName(String profileName) {
515517
return profileName.trim();
516518
}
517519

520+
void applyWorkspaceOutputDirectory(Settings settings, String workspaceName) {
521+
settings.set(ConfigTags.OutputDir, "./output/" + normalizeProfileName(workspaceName));
522+
}
523+
518524
private OperatingSystems parseOperatingSystem(String token) {
519525
String normalized = token.toLowerCase(Locale.ROOT);
520526
if ("windows".equals(normalized) || "windows10".equals(normalized)

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@ public void buildSessionSpecForWindowsKeepsTargetInSessionSpec() {
9191
Assert.assertEquals("notepad.exe", sessionSpec.getTarget());
9292
}
9393

94+
@Test
95+
public void applyWorkspaceOutputDirectoryScopesResultsToWorkspace() {
96+
CliDaemonServer server = new CliDaemonServer();
97+
Settings settings = defaultSettings();
98+
99+
server.applyWorkspaceOutputDirectory(settings, "webdriver_generic");
100+
101+
Assert.assertEquals("./output/webdriver_generic", settings.get(ConfigTags.OutputDir));
102+
}
103+
94104
@Test
95105
public void resolveCliTargetFromWebdriverConnector() {
96106
Settings settings = defaultSettings();

docs/architecture_distribution_workspace.md

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ target/install/testar/
3434
windows_generic/
3535
android_generic/
3636
output/
37+
webdriver_generic/
38+
windows_generic/
39+
android_generic/
3740
.runtime/
3841
```
3942

@@ -193,16 +196,40 @@ Executable managed goal files use `.yaml` or `.yml`.
193196

194197
The YAML contract is intentionally structured so WebStudio can validate required fields and agents can consume goal content reliably.
195198

199+
## Workspace-Scoped Output Results
200+
201+
Generated output results are workspace assets.
202+
203+
Each workspace writes and reads results under its own output directory:
204+
205+
```text
206+
target/install/testar/bin/output/{workspace}/
207+
```
208+
209+
Examples:
210+
211+
- `target/install/testar/bin/output/webdriver_generic/`
212+
- `target/install/testar/bin/output/windows_generic/`
213+
- `target/install/testar/bin/output/android_generic/`
214+
215+
Generate and CLI executions share the same runtime root, but they must not mix output results across workspaces.
216+
217+
WebStudio result inspection uses the selected workspace to resolve the output result directory.
218+
219+
Changing the selected workspace changes the visible output results.
220+
221+
This keeps workspace configuration, test goals, generated reports, and state model settings aligned.
222+
196223
## Output Folder Naming
197224

198225
Generated output folders must include the execution mode after the timestamp.
199226

200227
Examples:
201228

202-
- `2026-06-29_13h23m23s_generate_webdriver_parabank_1`
203-
- `2026-06-29_13h23m23s_cli_webdriver_parabank_1`
229+
- `output/webdriver_generic/2026-06-29_13h23m23s_generate_webdriver_parabank_1`
230+
- `output/webdriver_generic/2026-06-29_13h23m23s_cli_webdriver_parabank_1`
204231

205-
The mode token allows WebStudio to present mode-specific result summaries while keeping one shared output folder.
232+
The mode token allows WebStudio to present mode-specific result summaries inside the selected workspace output area.
206233

207234
## Shared Settings
208235

@@ -270,6 +297,6 @@ WebStudio should:
270297
- start CLI sessions with the workspace-driven contract, `startSession <workspace>`
271298
- show warnings when mode-specific entries are ignored
272299
- keep mode-specific controls in their own views
273-
- read generated output results from the shared distribution output folder
300+
- read generated output results from the selected workspace output folder
274301

275302
WebStudio reuses equivalent workspaces across execution modes.

docs/webstudio/WEBSTUDIO_FUNCTIONAL_SPEC.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,19 @@ When CLI reporting is enabled, generated report artifacts must include the final
529529

530530
## Test Results
531531

532-
The Test Results page supports inspection of output folders from the shared TESTAR distribution.
532+
The Test Results page supports inspection of output folders for the selected workspace.
533+
534+
Output results are workspace-scoped artifacts.
535+
536+
WebStudio must resolve result folders from:
537+
538+
- `testar/target/install/testar/bin/output/<workspace>`
539+
540+
Generate and CLI executions both write results for the selected workspace into that workspace output area.
541+
542+
Changing the selected workspace changes the visible Test Results list.
543+
544+
WebStudio must not mix output results from different workspaces in the selected workspace result list.
533545

534546
Output result folder names must include the execution mode token after the timestamp:
535547

@@ -538,8 +550,8 @@ Output result folder names must include the execution mode token after the times
538550

539551
Example names:
540552

541-
- `2026-06-29_13h23m23s_generate_webdriver_parabank_1`
542-
- `2026-06-29_13h23m23s_cli_webdriver_parabank_1`
553+
- `output/webdriver_generic/2026-06-29_13h23m23s_generate_webdriver_parabank_1`
554+
- `output/webdriver_generic/2026-06-29_13h23m23s_cli_webdriver_parabank_1`
543555

544556
### Output Result Selection
545557

@@ -556,6 +568,7 @@ The page must support:
556568

557569
Default behavior:
558570

571+
- output result folders are loaded for the selected workspace
559572
- output result folders are sorted latest first
560573
- selecting an output folder shows a run-level verdict outcome summary by default
561574
- selecting an output folder must not automatically select the first generated file
@@ -594,7 +607,7 @@ The Test Results page must support an empty state.
594607

595608
The empty state is shown when:
596609

597-
- no output result folders exist
610+
- no output result folders exist for the selected workspace
598611
- filters hide all available output result folders
599612
- the last output result folder was deleted
600613

docs/webstudio/WEBSTUDIO_UX_SPEC.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,9 @@ The results page should provide fast visual understanding of output health.
283283
Expected behavior:
284284

285285
- the left panel contains `Output Results`
286-
- `Output Results` reads from the shared Generate and CLI output folder
286+
- `Output Results` reads from the selected workspace output folder
287+
- changing the selected workspace refreshes `Output Results`
288+
- results from other workspaces are not shown in the selected workspace list
287289
- `Output Results` includes sorting by date or name
288290
- `Output Results` includes filtering by the result type
289291
- `Output Results` can be filtered by execution mode: All, Generate, or CLI
@@ -328,7 +330,7 @@ Deletion confirmation behavior:
328330

329331
Empty state behavior:
330332

331-
- no output result folders shows an empty message in the right preview area
333+
- no output result folders for the selected workspace shows an empty message in the right preview area
332334
- filters hiding all output result folders show an empty filtered-results message
333335
- no generated files for a selected output folder shows an empty message in the `Generated Files` panel
334336
- deleting the last output result folder shows the default Test Results empty view

webstudio/frontend/src/App.svelte

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import { clearSelectedSourceState } from "./policyEditorState.js";
1414
import { stateModelWorkspaceDialog } from "./stateModelNavigation.js";
1515
import { objectSnapshot } from "./editorDirtyState.js";
16+
import { resultFileUrl, resultGroupDeleteUrl, resultListUrl } from "./resultApi.js";
1617
1718
const STATE_MODEL_URL = "http://localhost:8090/models";
1819
const CLI_AGENT_SETTING_KEYS = {
@@ -239,6 +240,9 @@
239240
javaCompileResult = null;
240241
resetTestGoalSelection();
241242
testGoalTree = null;
243+
resultsData = null;
244+
selectedResultGroup = null;
245+
selectedResultFile = null;
242246
return;
243247
}
244248
@@ -266,6 +270,8 @@
266270
resetTestGoalSelection();
267271
if (currentPage === "test-goals") {
268272
await loadTestGoalTree(workspaceName);
273+
} else if (currentPage === "results") {
274+
await loadResults(workspaceName);
269275
}
270276
} catch (loadError) {
271277
reportClientError(`Unable to load workspace ${workspaceName}`, loadError);
@@ -1427,9 +1433,16 @@
14271433
);
14281434
}
14291435
1430-
async function loadResults() {
1436+
async function loadResults(workspaceName = selectedWorkspaceName) {
1437+
if (!workspaceName) {
1438+
resultsData = null;
1439+
selectedResultGroup = null;
1440+
selectedResultFile = null;
1441+
return;
1442+
}
1443+
14311444
try {
1432-
resultsData = await loadJson("/api/execution/scriptless/results");
1445+
resultsData = await loadJson(resultListUrl(workspaceName));
14331446
const resultGroups = resultsData.groups || [];
14341447
if (resultGroups.length > 0) {
14351448
await selectResultGroup(resultGroups[resultGroups.length - 1]);
@@ -1760,8 +1773,7 @@
17601773
}
17611774
17621775
try {
1763-
const resultPath = encodeURIComponent(resultFile.path);
1764-
selectedResultFile = await loadJson(`/api/execution/scriptless/results/${resultFile.name}?path=${resultPath}`);
1776+
selectedResultFile = await loadJson(resultFileUrl(selectedWorkspaceName, resultFile));
17651777
} catch (fileError) {
17661778
reportClientError(`Unable to load result file ${resultFile.name}`, fileError);
17671779
}
@@ -1786,9 +1798,8 @@
17861798
}
17871799
17881800
try {
1789-
const resultPath = encodeURIComponent(resultFile.path);
17901801
const currentGroupPath = selectedResultGroup?.path || "";
1791-
const refreshedResults = await loadJson(`/api/execution/scriptless/results/${resultFile.name}?path=${resultPath}`, {
1802+
const refreshedResults = await loadJson(resultFileUrl(selectedWorkspaceName, resultFile), {
17921803
method: "DELETE"
17931804
});
17941805
applyRefreshedResults(refreshedResults, currentGroupPath);
@@ -1803,8 +1814,7 @@
18031814
}
18041815
18051816
try {
1806-
const resultPath = encodeURIComponent(resultGroup.path);
1807-
const refreshedResults = await loadJson(`/api/execution/scriptless/result-groups?path=${resultPath}`, {
1817+
const refreshedResults = await loadJson(resultGroupDeleteUrl(selectedWorkspaceName, resultGroup), {
18081818
method: "DELETE"
18091819
});
18101820
applyRefreshedResults(refreshedResults);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export function resultWorkspaceQuery(workspaceName) {
2+
return `workspace=${encodeURIComponent(workspaceName || "")}`;
3+
}
4+
5+
export function resultListUrl(workspaceName) {
6+
return `/api/execution/scriptless/results?${resultWorkspaceQuery(workspaceName)}`;
7+
}
8+
9+
export function resultFileUrl(workspaceName, resultFile) {
10+
return `/api/execution/scriptless/results/${resultFile.name}?${resultWorkspaceQuery(workspaceName)}&path=${encodeURIComponent(resultFile.path)}`;
11+
}
12+
13+
export function resultGroupDeleteUrl(workspaceName, resultGroup) {
14+
return `/api/execution/scriptless/result-groups?${resultWorkspaceQuery(workspaceName)}&path=${encodeURIComponent(resultGroup.path)}`;
15+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import test from "node:test";
2+
import assert from "node:assert/strict";
3+
import {
4+
resultFileUrl,
5+
resultGroupDeleteUrl,
6+
resultListUrl
7+
} from "../src/resultApi.js";
8+
9+
test("builds workspace-scoped result list URL", () => {
10+
assert.equal(
11+
resultListUrl("webdriver_generic"),
12+
"/api/execution/scriptless/results?workspace=webdriver_generic"
13+
);
14+
});
15+
16+
test("builds workspace-scoped result file URL", () => {
17+
assert.equal(
18+
resultFileUrl("webdriver_generic", {
19+
name: "sequence_1_V001_OK.html",
20+
path: "C:\\output\\webdriver_generic\\run\\reports\\sequence_1_V001_OK.html"
21+
}),
22+
"/api/execution/scriptless/results/sequence_1_V001_OK.html?workspace=webdriver_generic&path=C%3A%5Coutput%5Cwebdriver_generic%5Crun%5Creports%5Csequence_1_V001_OK.html"
23+
);
24+
});
25+
26+
test("builds workspace-scoped result group delete URL", () => {
27+
assert.equal(
28+
resultGroupDeleteUrl("android_generic", {
29+
path: "C:\\output\\android_generic\\run"
30+
}),
31+
"/api/execution/scriptless/result-groups?workspace=android_generic&path=C%3A%5Coutput%5Candroid_generic%5Crun"
32+
);
33+
});

webstudio/src/org/testar/webstudio/api/ExecutionController.java

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -73,25 +73,25 @@ public ExecutionStatusDto stopCliAgentSession() {
7373
return cliExecutionAdapter().stopAgentSession();
7474
}
7575

76-
public ScriptlessResultsDto cliResults() {
77-
return cliExecutionAdapter().cliResults();
76+
public ScriptlessResultsDto cliResults(String workspaceName) {
77+
return cliExecutionAdapter().cliResults(workspaceName);
7878
}
7979

80-
public ResultFileDto cliResultFile(String fileName, String filePath) {
81-
return cliExecutionAdapter().readCliResultFile(fileName, filePath);
80+
public ResultFileDto cliResultFile(String workspaceName, String fileName, String filePath) {
81+
return cliExecutionAdapter().readCliResultFile(workspaceName, fileName, filePath);
8282
}
8383

84-
public ScriptlessResultsDto deleteCliResultFile(String filePath) {
85-
return cliExecutionAdapter().deleteCliResultFile(filePath);
84+
public ScriptlessResultsDto deleteCliResultFile(String workspaceName, String filePath) {
85+
return cliExecutionAdapter().deleteCliResultFile(workspaceName, filePath);
8686
}
8787

88-
public ScriptlessResultsDto deleteCliResultGroup(String groupPath) {
89-
return cliExecutionAdapter().deleteCliResultGroup(groupPath);
88+
public ScriptlessResultsDto deleteCliResultGroup(String workspaceName, String groupPath) {
89+
return cliExecutionAdapter().deleteCliResultGroup(workspaceName, groupPath);
9090
}
9191

92-
public byte[] cliResultAsset(String filePath) {
92+
public byte[] cliResultAsset(String workspaceName, String filePath) {
9393
try {
94-
return Files.readAllBytes(cliExecutionAdapter().resolveCliResultAsset(filePath));
94+
return Files.readAllBytes(cliExecutionAdapter().resolveCliResultAsset(workspaceName, filePath));
9595
} catch (Exception exception) {
9696
throw new IllegalStateException("Unable to read CLI result asset: " + filePath, exception);
9797
}
@@ -105,25 +105,25 @@ public ExecutionStatusDto stopScriptlessRun() {
105105
return scriptlessExecutionAdapter().stop();
106106
}
107107

108-
public ScriptlessResultsDto scriptlessResults() {
109-
return scriptlessExecutionAdapter().scriptlessResults();
108+
public ScriptlessResultsDto scriptlessResults(String workspaceName) {
109+
return scriptlessExecutionAdapter().scriptlessResults(workspaceName);
110110
}
111111

112-
public ResultFileDto scriptlessResultFile(String fileName, String filePath) {
113-
return scriptlessExecutionAdapter().readScriptlessResultFile(fileName, filePath);
112+
public ResultFileDto scriptlessResultFile(String workspaceName, String fileName, String filePath) {
113+
return scriptlessExecutionAdapter().readScriptlessResultFile(workspaceName, fileName, filePath);
114114
}
115115

116-
public ScriptlessResultsDto deleteScriptlessResultFile(String filePath) {
117-
return scriptlessExecutionAdapter().deleteScriptlessResultFile(filePath);
116+
public ScriptlessResultsDto deleteScriptlessResultFile(String workspaceName, String filePath) {
117+
return scriptlessExecutionAdapter().deleteScriptlessResultFile(workspaceName, filePath);
118118
}
119119

120-
public ScriptlessResultsDto deleteScriptlessResultGroup(String groupPath) {
121-
return scriptlessExecutionAdapter().deleteScriptlessResultGroup(groupPath);
120+
public ScriptlessResultsDto deleteScriptlessResultGroup(String workspaceName, String groupPath) {
121+
return scriptlessExecutionAdapter().deleteScriptlessResultGroup(workspaceName, groupPath);
122122
}
123123

124-
public byte[] scriptlessResultAsset(String filePath) {
124+
public byte[] scriptlessResultAsset(String workspaceName, String filePath) {
125125
try {
126-
return Files.readAllBytes(scriptlessExecutionAdapter().resolveScriptlessResultAsset(filePath));
126+
return Files.readAllBytes(scriptlessExecutionAdapter().resolveScriptlessResultAsset(workspaceName, filePath));
127127
} catch (Exception exception) {
128128
throw new IllegalStateException("Unable to read scriptless result asset: " + filePath, exception);
129129
}

0 commit comments

Comments
 (0)