Skip to content

Commit 178fc29

Browse files
author
DavidQ
committed
Normalize Session Inspector V2 tool objects into workspace data and dirty sections - PR_26128_020-session-inspector-v2-data-dirty-model
1 parent cb736fc commit 178fc29

12 files changed

Lines changed: 311 additions & 83 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Playwright Session Inspector V2 Data Dirty Model
2+
3+
## Command
4+
`npm run test:workspace-v2`
5+
6+
## Result
7+
- Passed: 15/15
8+
- Runtime: about 1.4 minutes
9+
10+
## Targeted Coverage
11+
- Workspace Manager V2 writes normalized tool sessions with `schema`, `workspace`, `data`, and `dirty`.
12+
- Repeated workspace/repo/game context lives under `workspace`.
13+
- Actual tool payload lives under `data`.
14+
- Dirty tracking defaults to the clean object.
15+
- Session Inspector V2 shows JSON, Data, and Dirty views.
16+
- Session Inspector V2 State and Schema labels/controls are absent.
17+
- JSON shows the full selected object.
18+
- Data shows only selected `data`.
19+
- Dirty shows only selected `dirty`.
20+
- JSON, Data, Dirty, Controls, Filters, Entries, and Status accordions all open and close.
21+
- Split `workspace.tools.<tool-id>.schema` and `workspace.tools.<tool-id>.state` keys are not recreated.
22+
23+
## Skipped
24+
- Full samples smoke test was skipped by request. The changed surface is covered by `tests/playwright/tools/WorkspaceManagerV2.spec.mjs`.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Session Inspector V2 Data Dirty Model
2+
3+
## Scope
4+
- Replaced the normalized tool object model with:
5+
- `schema`
6+
- `workspace`
7+
- `data`
8+
- `dirty`
9+
- Moved repeated workspace/repo/game context into `workspace`.
10+
- Moved actual tool payload data into `data`.
11+
- Added clean dirty tracking defaults:
12+
- `isDirty: false`
13+
- `reason: null`
14+
- `changedAt: null`
15+
- `changedKeys: []`
16+
- Removed Session Inspector V2 State UI.
17+
- Kept Schema out of the Session Inspector V2 UI.
18+
- Added Session Inspector V2 views:
19+
- JSON: full stored object
20+
- Data: selected item `data`
21+
- Dirty: selected item `dirty`
22+
- Removed the Workspace Manager V2 Session Inspector tile subtitle `Session storage inspector`.
23+
24+
## Implementation Notes
25+
- Workspace Manager V2 still writes one session key per tool: `workspace.tools.<tool-id>`.
26+
- Workspace Manager V2 still preserves `workspace.repo.reference`.
27+
- Preview Generator V2 reads workspace session context from `workspace.tools.preview-generator-v2.workspace`.
28+
- Preview Generator V2 image generation behavior was not changed.
29+
- Data and Dirty views show actionable empty states when the selected storage item has no matching section.
30+
- Per-tile Delete and Delete All behavior were preserved.
31+
32+
## Guardrails
33+
- No cross-tool communication was added.
34+
- No `.schema` or `.state` split keys were recreated.
35+
- No sample JSON was modified.
36+
- No roadmap content was modified.
37+
38+
## Validation
39+
- Passed `npm run test:workspace-v2` with 15/15 tests.
40+
- Verified normalized tool objects use `schema`, `workspace`, `data`, and `dirty`.
41+
- Verified repeated workspace context is not stored under `state`.
42+
- Verified actual tool payload is stored under `data`.
43+
- Verified dirty defaults to clean.
44+
- Verified Session Inspector V2 has JSON, Data, and Dirty views.
45+
- Verified State and Schema controls are absent from Session Inspector V2 UI.
46+
- Verified JSON/Data/Dirty accordion behavior does not conflict with other accordions.
47+
48+
## Skipped
49+
- Full samples smoke test was skipped because this PR is scoped to Workspace Manager V2 session hydration and Session Inspector V2 display of normalized browser storage. The requested Workspace V2 Playwright suite covers the changed behavior.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 129 additions & 34 deletions
Large diffs are not rendered by default.

tools/preview-generator-v2/PreviewGeneratorV2App.js

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -126,31 +126,31 @@ function readWorkspaceRepoReference() {
126126
return { ok: true, reference: { ...reference, displayName } };
127127
}
128128

129-
function readWorkspacePreviewGeneratorState(manifest) {
129+
function readWorkspacePreviewGeneratorWorkspace(manifest) {
130130
const result = readSessionJson(WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY);
131131
if (!result.ok) {
132132
return result;
133133
}
134-
if (!isPlainObject(result.value.state)) {
135-
return { ok: false, message: `${WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY}.state must contain a JSON object.` };
134+
if (!isPlainObject(result.value.workspace)) {
135+
return { ok: false, message: `${WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY}.workspace must contain a JSON object.` };
136136
}
137-
const state = result.value.state;
138-
if (state.source !== "workspace-manager-v2") {
139-
return { ok: false, message: `${WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY}.state.source must be workspace-manager-v2.` };
137+
const workspace = result.value.workspace;
138+
if (workspace.source !== "workspace-manager-v2") {
139+
return { ok: false, message: `${WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY}.workspace.source must be workspace-manager-v2.` };
140140
}
141-
if (state.toolId !== PREVIEW_GENERATOR_V2_TOOL_KEY) {
142-
return { ok: false, message: `${WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY}.state.toolId must be ${PREVIEW_GENERATOR_V2_TOOL_KEY}.` };
141+
if (workspace.toolId !== PREVIEW_GENERATOR_V2_TOOL_KEY) {
142+
return { ok: false, message: `${WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY}.workspace.toolId must be ${PREVIEW_GENERATOR_V2_TOOL_KEY}.` };
143143
}
144-
if (state.repoReferenceKey !== WORKSPACE_REPO_REFERENCE_SESSION_KEY) {
145-
return { ok: false, message: `${WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY}.state.repoReferenceKey must be ${WORKSPACE_REPO_REFERENCE_SESSION_KEY}.` };
144+
if (workspace.repoReferenceKey !== WORKSPACE_REPO_REFERENCE_SESSION_KEY) {
145+
return { ok: false, message: `${WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY}.workspace.repoReferenceKey must be ${WORKSPACE_REPO_REFERENCE_SESSION_KEY}.` };
146146
}
147-
if (state.gameId !== manifest.gameId) {
148-
return { ok: false, message: `${WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY}.state.gameId must match manifest.gameId ${manifest.gameId}.` };
147+
if (workspace.gameId !== manifest.gameId) {
148+
return { ok: false, message: `${WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY}.workspace.gameId must match manifest.gameId ${manifest.gameId}.` };
149149
}
150-
if (state.gameRoot !== manifest.gameRoot || state.assetsPath !== manifest.assetsPath) {
151-
return { ok: false, message: `${WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY}.state gameRoot/assetsPath must match the workspace manifest.` };
150+
if (workspace.gameRoot !== manifest.gameRoot || workspace.assetsPath !== manifest.assetsPath) {
151+
return { ok: false, message: `${WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY}.workspace gameRoot/assetsPath must match the workspace manifest.` };
152152
}
153-
return { ok: true, state };
153+
return { ok: true, workspace };
154154
}
155155

156156
function isWorkspaceManifest(value) {
@@ -902,9 +902,9 @@ class PreviewGeneratorV2App {
902902
if (!repoReferenceResult.ok) {
903903
return { ok: false, message: repoReferenceResult.message };
904904
}
905-
const stateResult = readWorkspacePreviewGeneratorState(manifest);
906-
if (!stateResult.ok) {
907-
return { ok: false, message: stateResult.message };
905+
const workspaceResult = readWorkspacePreviewGeneratorWorkspace(manifest);
906+
if (!workspaceResult.ok) {
907+
return { ok: false, message: workspaceResult.message };
908908
}
909909
const repoReference = repoReferenceResult.reference;
910910
if (!repoRootNameMatches(repoReference.displayName, manifest.repoRoot)) {
@@ -922,7 +922,7 @@ class PreviewGeneratorV2App {
922922
if (!repoValidation.ok) {
923923
return repoValidation;
924924
}
925-
return { ok: true, handle, repoReference, state: stateResult.state };
925+
return { ok: true, handle, repoReference, workspace: workspaceResult.workspace };
926926
}
927927

928928
async handleExecute() {
@@ -1118,7 +1118,7 @@ class PreviewGeneratorV2App {
11181118
ui.setRepoDestinationDisplayName(repoDisplayName);
11191119
workspaceLaunchHydrated = true;
11201120
logger.log(`OK Workspace repo session reference loaded from ${WORKSPACE_REPO_REFERENCE_SESSION_KEY} for ${repoDisplayName}.`);
1121-
logger.log(`OK Workspace tool session state loaded from ${WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY}.`);
1121+
logger.log(`OK Workspace tool session workspace context loaded from ${WORKSPACE_PREVIEW_GENERATOR_SESSION_KEY}.`);
11221122
logger.log("Workspace launch repo context resolved from session storage; independent repo selection is not required.");
11231123
this.syncGeneratePreviewButton();
11241124
}

tools/session-inspector-v2/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ Session Inspector V2 is a first-class tool for inspecting and clearing current-o
1313
- Seed a storage key in the current origin.
1414
- Refresh the tool and verify the key appears in the Entries list.
1515
- Select an entry and confirm the JSON panel shows the full stored value.
16-
- Select a normalized workspace tool entry and confirm the State panel shows only its state section.
16+
- Select a normalized workspace tool entry and confirm the Data panel shows only its data section.
17+
- Select a normalized workspace tool entry and confirm the Dirty panel shows only its dirty-tracking section.
1718
- Use per-entry Delete or Delete All and verify the Entries list and status log update immediately.

tools/session-inspector-v2/index.html

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ <h2 class="tools-platform-frame__eyebrow">Session storage visibility</h2>
9595
</section>
9696
</section>
9797

98-
<aside class="session-inspector-v2__panel session-inspector-v2__panel--right tool-shell-common__fullscreen-panel tool-shell-common__fullscreen-side-panel tool-shell-common__fullscreen-panel-right" aria-label="Session JSON and state">
98+
<aside class="session-inspector-v2__panel session-inspector-v2__panel--right tool-shell-common__fullscreen-panel tool-shell-common__fullscreen-side-panel tool-shell-common__fullscreen-panel-right" aria-label="Session JSON, data, and dirty tracking">
9999
<section class="accordion-v2 session-inspector-v2__accordion session-inspector-v2__accordion--fill is-open" data-accordion-v2-open="true">
100100
<div class="accordion-v2__header session-inspector-v2__json-accordion-header" role="button" tabindex="0" aria-expanded="true" aria-controls="sessionInspectorV2JsonContent">
101101
<span>JSON</span>
@@ -110,12 +110,22 @@ <h2 class="tools-platform-frame__eyebrow">Session storage visibility</h2>
110110
</section>
111111

112112
<section class="accordion-v2 session-inspector-v2__accordion session-inspector-v2__accordion--fill is-open" data-accordion-v2-open="true">
113-
<div class="accordion-v2__header session-inspector-v2__state-accordion-header" role="button" tabindex="0" aria-expanded="true" aria-controls="sessionInspectorV2StateContent">
114-
<span>State</span>
113+
<div class="accordion-v2__header session-inspector-v2__data-accordion-header" role="button" tabindex="0" aria-expanded="true" aria-controls="sessionInspectorV2DataContent">
114+
<span>Data</span>
115115
<span class="accordion-v2__icon" aria-hidden="true">+</span>
116116
</div>
117-
<div id="sessionInspectorV2StateContent" class="accordion-v2__content">
118-
<pre id="sessionInspectorV2StateOutput" class="session-inspector-v2__output">Select a normalized tool entry with a top-level state section.</pre>
117+
<div id="sessionInspectorV2DataContent" class="accordion-v2__content">
118+
<pre id="sessionInspectorV2DataOutput" class="session-inspector-v2__output">Select a normalized tool entry with a top-level data section.</pre>
119+
</div>
120+
</section>
121+
122+
<section class="accordion-v2 session-inspector-v2__accordion session-inspector-v2__accordion--fill is-open" data-accordion-v2-open="true">
123+
<div class="accordion-v2__header session-inspector-v2__dirty-accordion-header" role="button" tabindex="0" aria-expanded="true" aria-controls="sessionInspectorV2DirtyContent">
124+
<span>Dirty</span>
125+
<span class="accordion-v2__icon" aria-hidden="true">+</span>
126+
</div>
127+
<div id="sessionInspectorV2DirtyContent" class="accordion-v2__content">
128+
<pre id="sessionInspectorV2DirtyOutput" class="session-inspector-v2__output">Select a normalized tool entry with a top-level dirty section.</pre>
119129
</div>
120130
</section>
121131

tools/session-inspector-v2/js/SessionInspectorV2App.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,31 @@ export class SessionInspectorV2App {
22
constructor({
33
accordions,
44
copyJsonButton,
5+
data,
56
deleteAllButton,
7+
dirty,
68
entryList,
79
filters,
810
json,
911
refreshButton,
1012
returnToWorkspaceButton,
1113
runtimeContract,
12-
state,
1314
statusLog,
1415
storageService,
1516
windowRef = window
1617
}) {
1718
this.accordions = accordions;
1819
this.copyJsonButton = copyJsonButton;
20+
this.data = data;
1921
this.deleteAllButton = deleteAllButton;
22+
this.dirty = dirty;
2023
this.entries = [];
2124
this.entryList = entryList;
2225
this.filters = filters;
2326
this.json = json;
2427
this.refreshButton = refreshButton;
2528
this.returnToWorkspaceButton = returnToWorkspaceButton;
2629
this.runtimeContract = runtimeContract || { storageAccess: "read-only" };
27-
this.state = state;
2830
this.statusLog = statusLog;
2931
this.storageService = storageService;
3032
this.selectedId = "";
@@ -66,7 +68,8 @@ export class SessionInspectorV2App {
6668
this.entryList.render(this.entries, this.selectedId);
6769
const selectedEntry = this.entries.find((entry) => entry.id === this.selectedId);
6870
this.json.render(selectedEntry);
69-
this.state.render(selectedEntry);
71+
this.data.render(selectedEntry);
72+
this.dirty.render(selectedEntry);
7073
this.filters.setSummary(this.summaryCounts());
7174
if (!silent) {
7275
this.statusLog.ok(`Loaded ${this.entries.length} matching storage entries.`);
@@ -85,7 +88,8 @@ export class SessionInspectorV2App {
8588
this.entryList.render(this.entries, this.selectedId);
8689
const entry = this.entries.find((candidate) => candidate.id === entryId);
8790
this.json.render(entry);
88-
this.state.render(entry);
91+
this.data.render(entry);
92+
this.dirty.render(entry);
8993
if (entry) {
9094
this.statusLog.info(`Selected ${entry.storageType}:${entry.key}.`);
9195
}

tools/session-inspector-v2/js/bootstrap.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { SessionInspectorV2App } from "./SessionInspectorV2App.js";
22
import { AccordionSection } from "./controls/AccordionSection.js";
3+
import { DataControl } from "./controls/DataControl.js";
4+
import { DirtyControl } from "./controls/DirtyControl.js";
35
import { EntryListControl } from "./controls/EntryListControl.js";
46
import { FilterControl } from "./controls/FilterControl.js";
57
import { JsonControl } from "./controls/JsonControl.js";
6-
import { StateControl } from "./controls/StateControl.js";
78
import { StatusLogControl } from "./controls/StatusLogControl.js";
89
import { sessionInspectorV2RuntimeContract } from "./services/SessionInspectorV2RuntimeContract.js";
910
import { SessionInspectorV2StorageService } from "./services/SessionInspectorV2StorageService.js";
@@ -20,9 +21,15 @@ window.addEventListener("DOMContentLoaded", () => {
2021
const app = new SessionInspectorV2App({
2122
accordions: Array.from(document.querySelectorAll(".accordion-v2"), (section) => new AccordionSection(section)),
2223
copyJsonButton: requireElement("#copySessionInspectorV2JsonButton"),
24+
data: new DataControl({
25+
output: requireElement("#sessionInspectorV2DataOutput")
26+
}),
2327
json: new JsonControl({
2428
output: requireElement("#sessionInspectorV2JsonOutput")
2529
}),
30+
dirty: new DirtyControl({
31+
output: requireElement("#sessionInspectorV2DirtyOutput")
32+
}),
2633
deleteAllButton: requireElement("#deleteAllSessionInspectorV2Button"),
2734
entryList: new EntryListControl({
2835
container: requireElement("#sessionInspectorV2EntryList")
@@ -35,9 +42,6 @@ window.addEventListener("DOMContentLoaded", () => {
3542
refreshButton: requireElement("#refreshSessionInspectorV2Button"),
3643
returnToWorkspaceButton: requireElement("#returnToWorkspaceButton"),
3744
runtimeContract: sessionInspectorV2RuntimeContract(),
38-
state: new StateControl({
39-
output: requireElement("#sessionInspectorV2StateOutput")
40-
}),
4145
statusLog: new StatusLogControl({
4246
clearButton: requireElement("#clearSessionInspectorV2StatusButton"),
4347
output: requireElement("#statusLog")
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export class DataControl {
2+
constructor({ output }) {
3+
this.output = output;
4+
}
5+
6+
clear() {
7+
this.output.textContent = "Select a normalized tool entry with a top-level data section.";
8+
}
9+
10+
render(entry) {
11+
if (!entry) {
12+
this.clear();
13+
return;
14+
}
15+
const value = entry.parseOk ? entry.parsedValue : null;
16+
if (!value || typeof value !== "object" || Array.isArray(value) || !Object.prototype.hasOwnProperty.call(value, "data")) {
17+
this.output.textContent = `No data section is present for ${entry.storageType}:${entry.key}. Select a normalized tool entry with a top-level data section.`;
18+
return;
19+
}
20+
this.output.textContent = JSON.stringify(value.data, null, 2);
21+
}
22+
}

tools/session-inspector-v2/js/controls/StateControl.js renamed to tools/session-inspector-v2/js/controls/DirtyControl.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
export class StateControl {
1+
export class DirtyControl {
22
constructor({ output }) {
33
this.output = output;
44
}
55

66
clear() {
7-
this.output.textContent = "Select a normalized tool entry with a top-level state section.";
7+
this.output.textContent = "Select a normalized tool entry with a top-level dirty section.";
88
}
99

1010
render(entry) {
@@ -13,10 +13,10 @@ export class StateControl {
1313
return;
1414
}
1515
const value = entry.parseOk ? entry.parsedValue : null;
16-
if (!value || typeof value !== "object" || Array.isArray(value) || !Object.prototype.hasOwnProperty.call(value, "state")) {
17-
this.output.textContent = `No state section is present for ${entry.storageType}:${entry.key}. Select a normalized tool entry with a top-level state section.`;
16+
if (!value || typeof value !== "object" || Array.isArray(value) || !Object.prototype.hasOwnProperty.call(value, "dirty")) {
17+
this.output.textContent = `No dirty section is present for ${entry.storageType}:${entry.key}. Select a normalized tool entry with a top-level dirty section.`;
1818
return;
1919
}
20-
this.output.textContent = JSON.stringify(value.state, null, 2);
20+
this.output.textContent = JSON.stringify(value.dirty, null, 2);
2121
}
2222
}

0 commit comments

Comments
 (0)