Skip to content

Commit 70da382

Browse files
author
DavidQ
committed
Block cross-tool session merge before deep preview - PR 11.252. Add merged session naming and persistence flow - PR 11.253.
1 parent 1d0966a commit 70da382

6 files changed

Lines changed: 503 additions & 0 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# PR_11_252 — Cross-Tool Merge Block (Early Validation)
2+
3+
## Summary
4+
Added an early cross-tool validation gate in Workspace V2 Session Merge preview. If selected Session A/B payload `toolId` values differ, preview is blocked immediately with a clear message and both selected tool IDs, before deep merge/conflict preview runs.
5+
6+
## Files Changed
7+
- `tools/workspace-v2/index.js`
8+
- `tests/runtime/V2CrossToolMergeBlock.test.mjs`
9+
10+
## Implementation Details
11+
- Added early check in `computeSelectedSessionMerge()`:
12+
- read `left.payload.toolId` and `right.payload.toolId`
13+
- if both exist and differ:
14+
- block preview
15+
- clear pending preview state (`this.pendingMergePreview = null`)
16+
- keep Preview button available (selection-based enable logic unchanged)
17+
- disable Confirm/Apply via existing state model
18+
- render clear message in merge output:
19+
- `Cross-tool merge is not supported. Select two sessions with the same toolId.`
20+
- `Session A toolId: <...>`
21+
- `Session B toolId: <...>`
22+
- do not render raw merge JSON preview
23+
24+
- Same-tool flow remains unchanged:
25+
- deep merge preview still runs
26+
- conflict summary still works
27+
- conflict-free fresh preview can enable Confirm
28+
29+
## Validation Commands Run
30+
```powershell
31+
node --check tools/workspace-v2/index.js
32+
node --check tests/runtime/V2CrossToolMergeBlock.test.mjs
33+
node --check tests/runtime/V2MergeConflictSummary.test.mjs
34+
node --check tests/runtime/V2ConfirmPreviewEnableState.test.mjs
35+
node tests/runtime/V2CrossToolMergeBlock.test.mjs
36+
node tests/runtime/V2MergeConflictSummary.test.mjs
37+
node tests/runtime/V2ConfirmPreviewEnableState.test.mjs
38+
```
39+
40+
## Validation Results
41+
- `node --check tools/workspace-v2/index.js` -> PASS
42+
- `node --check tests/runtime/V2CrossToolMergeBlock.test.mjs` -> PASS
43+
- `node --check tests/runtime/V2MergeConflictSummary.test.mjs` -> PASS
44+
- `node --check tests/runtime/V2ConfirmPreviewEnableState.test.mjs` -> PASS
45+
- `node tests/runtime/V2CrossToolMergeBlock.test.mjs` -> PASS
46+
- output: `tmp/v2-cross-tool-merge-block-results.json`
47+
- failures: `[]`
48+
- `node tests/runtime/V2MergeConflictSummary.test.mjs` -> PASS
49+
- output: `tmp/v2-merge-conflict-summary-results.json`
50+
- failures: `[]`
51+
- `node tests/runtime/V2ConfirmPreviewEnableState.test.mjs` -> PASS
52+
- output: `tmp/v2-confirm-preview-enable-state-results.json`
53+
- failures: `[]`
54+
55+
## Verified
56+
- cross-tool selections block preview with clear message -> PASS
57+
- cross-tool preview does not render raw merge JSON -> PASS
58+
- cross-tool message includes both toolIds -> PASS
59+
- Confirm/Apply remain disabled after cross-tool block -> PASS
60+
- same-tool selections still run preview -> PASS
61+
- same-tool conflict preview still shows conflict summary -> PASS
62+
- same-tool conflict-free preview still enables Confirm Preview -> PASS
63+
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# PR_11_253 — Merge Target Naming And Output Handling
2+
3+
## Summary
4+
Added post-merge output handling so successful merge results can be explicitly named and saved into Session Library, without auto-saving or overwriting source sessions.
5+
6+
## Files Changed
7+
- `tools/workspace-v2/index.html`
8+
- `tools/workspace-v2/index.js`
9+
- `tests/runtime/V2MergeOutputPersistence.test.mjs`
10+
11+
## Implementation Details
12+
1. Merge output naming UI (merge panel)
13+
- Added under Session Merge:
14+
- label: `Merged Session ID`
15+
- input: `#workspaceV2MergedSessionId`
16+
- button: `Save Merged Session`
17+
- button: `Use in Diff/Merge`
18+
- status line: `#workspaceV2MergedSessionStatus`
19+
20+
2. Post-apply capture (runtime only unless saved)
21+
- After successful `Apply Merge`, merged payload is captured via:
22+
- `setLastMergedSessionResult(appliedPayload, selectedToolId)`
23+
- Default merged ID format:
24+
- `<toolId>-merged-<timestamp>`
25+
- No auto-save to library.
26+
27+
3. Save Merged Session behavior
28+
- `saveMergedSessionResult()` enforces:
29+
- non-empty ID
30+
- duplicate block when ID already exists in `v2-session-library`
31+
- On success:
32+
- saves merged payload into `v2-session-library`
33+
- refreshes Session Library view
34+
- recomputes Diff/Merge inventory through existing library write flow
35+
- status: `Merged session saved.`
36+
- Duplicate status:
37+
- `Session ID already exists. Choose a different ID.`
38+
39+
4. Reuse merged result quickly
40+
- `useMergedSessionInDiffMerge()`:
41+
- keeps merged result runtime-only
42+
- writes merged payload to `sessionStorage` under entered merged ID
43+
- adds recent entry
44+
- syncs Session A/B selection flow for Diff/Merge
45+
46+
## Validation Commands Run
47+
```powershell
48+
node --check tools/workspace-v2/index.js
49+
node --check tests/runtime/V2MergeOutputPersistence.test.mjs
50+
node tests/runtime/V2MergeOutputPersistence.test.mjs
51+
```
52+
53+
## Validation Results
54+
- `node --check tools/workspace-v2/index.js` -> PASS
55+
- `node --check tests/runtime/V2MergeOutputPersistence.test.mjs` -> PASS
56+
- `node tests/runtime/V2MergeOutputPersistence.test.mjs` -> PASS
57+
- output: `tmp/v2-merge-output-persistence-results.json`
58+
- failures: `[]`
59+
60+
## Verified
61+
- merge apply produces capture-ready merged payload -> PASS
62+
- merged payload can be saved with new ID -> PASS
63+
- duplicate ID is blocked -> PASS
64+
- saved merged session appears in Session Library -> PASS
65+
- merged session can be reused in Diff/Merge -> PASS
66+
- not saving leaves library unchanged -> PASS
67+
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import assert from "node:assert/strict";
2+
import fs from "node:fs";
3+
import path from "node:path";
4+
import { execFileSync } from "node:child_process";
5+
import { fileURLToPath, pathToFileURL } from "node:url";
6+
7+
const __filename = fileURLToPath(import.meta.url);
8+
const __dirname = path.dirname(__filename);
9+
const repoRoot = path.resolve(__dirname, "..", "..");
10+
const jsPath = path.join(repoRoot, "tools", "workspace-v2", "index.js");
11+
const resultsPath = path.join(repoRoot, "tmp", "v2-cross-tool-merge-block-results.json");
12+
13+
function checkSyntax(filePath) {
14+
try {
15+
execFileSync(process.execPath, ["--check", filePath], {
16+
cwd: repoRoot,
17+
stdio: ["ignore", "pipe", "pipe"]
18+
});
19+
return { ok: true, error: "" };
20+
} catch (error) {
21+
return { ok: false, error: (error?.stderr || error?.stdout || error?.message || "").toString().trim() };
22+
}
23+
}
24+
25+
function mergeUiState({ canPreviewMerge, previewExists, previewFresh, previewConfirmed, conflictCount }) {
26+
const hasConflicts = previewExists && conflictCount > 0;
27+
const confirmDisabled = !(previewExists && !previewConfirmed && previewFresh && !hasConflicts);
28+
const applyDisabled = !(previewExists && previewConfirmed && previewFresh && !hasConflicts);
29+
return { previewEnabled: canPreviewMerge, confirmDisabled, applyDisabled };
30+
}
31+
32+
function evaluateCrossTool(leftToolId, rightToolId) {
33+
if (leftToolId && rightToolId && leftToolId !== rightToolId) {
34+
return {
35+
blocked: true,
36+
rawJsonRendered: false,
37+
message: [
38+
"Cross-tool merge is not supported. Select two sessions with the same toolId.",
39+
`Session A toolId: ${leftToolId}`,
40+
`Session B toolId: ${rightToolId}`
41+
].join("\n")
42+
};
43+
}
44+
return { blocked: false, rawJsonRendered: true, message: "" };
45+
}
46+
47+
export function run() {
48+
const failures = [];
49+
const jsExists = fs.existsSync(jsPath);
50+
const js = jsExists ? fs.readFileSync(jsPath, "utf8") : "";
51+
const jsSyntax = checkSyntax(jsPath);
52+
const testSyntax = checkSyntax(path.join(repoRoot, "tests", "runtime", "V2CrossToolMergeBlock.test.mjs"));
53+
54+
if (!jsExists) failures.push("Missing tools/workspace-v2/index.js.");
55+
if (!jsSyntax.ok) failures.push("tools/workspace-v2/index.js failed syntax check.");
56+
if (!testSyntax.ok) failures.push("tests/runtime/V2CrossToolMergeBlock.test.mjs failed syntax check.");
57+
58+
const requiredTokens = [
59+
"Cross-tool merge is not supported. Select two sessions with the same toolId.",
60+
"Session A toolId:",
61+
"Session B toolId:",
62+
"if (leftToolId && rightToolId && leftToolId !== rightToolId) {",
63+
"this.pendingMergePreview = null;",
64+
"this.updateMergeSelectionFeedbackAndState();"
65+
];
66+
requiredTokens.forEach((token) => {
67+
if (!js.includes(token)) failures.push(`Missing cross-tool merge block token/text: ${token}`);
68+
});
69+
70+
const crossTool = evaluateCrossTool("palette-manager-v2", "asset-browser-v2");
71+
if (!crossTool.blocked) failures.push("Cross-tool merge should be blocked.");
72+
if (crossTool.rawJsonRendered) failures.push("Cross-tool merge block should not render raw merge JSON.");
73+
if (!crossTool.message.includes("Session A toolId: palette-manager-v2")) failures.push("Cross-tool block must show Session A toolId.");
74+
if (!crossTool.message.includes("Session B toolId: asset-browser-v2")) failures.push("Cross-tool block must show Session B toolId.");
75+
const crossToolState = mergeUiState({ canPreviewMerge: true, previewExists: false, previewFresh: false, previewConfirmed: false, conflictCount: 0 });
76+
if (!crossToolState.previewEnabled) failures.push("Preview button should remain enabled for cross-tool validation message.");
77+
if (!crossToolState.confirmDisabled || !crossToolState.applyDisabled) failures.push("Confirm/Apply should remain disabled after cross-tool block.");
78+
79+
const sameToolConflict = evaluateCrossTool("asset-browser-v2", "asset-browser-v2");
80+
if (sameToolConflict.blocked) failures.push("Same-tool merge should not be blocked by cross-tool guard.");
81+
if (!sameToolConflict.rawJsonRendered) failures.push("Same-tool preview should continue to render merge output.");
82+
const sameToolConflictState = mergeUiState({ canPreviewMerge: true, previewExists: true, previewFresh: true, previewConfirmed: false, conflictCount: 2 });
83+
if (sameToolConflictState.confirmDisabled !== true || sameToolConflictState.applyDisabled !== true) {
84+
failures.push("Same-tool conflict preview should keep Confirm/Apply disabled.");
85+
}
86+
const sameToolConflictFreeState = mergeUiState({ canPreviewMerge: true, previewExists: true, previewFresh: true, previewConfirmed: false, conflictCount: 0 });
87+
if (sameToolConflictFreeState.confirmDisabled) failures.push("Same-tool conflict-free preview should enable Confirm.");
88+
89+
fs.mkdirSync(path.dirname(resultsPath), { recursive: true });
90+
fs.writeFileSync(resultsPath, `${JSON.stringify({
91+
generatedAt: new Date().toISOString(),
92+
failures,
93+
checks: { jsExists, jsSyntax, testSyntax },
94+
scenarios: { crossTool, crossToolState, sameToolConflict, sameToolConflictState, sameToolConflictFreeState }
95+
}, null, 2)}\n`, "utf8");
96+
97+
console.log(`v2 cross-tool-merge-block results: ${resultsPath}`);
98+
assert.equal(failures.length, 0, `V2 cross-tool-merge-block failures: ${failures.join(" | ")}`);
99+
return { failures };
100+
}
101+
102+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
103+
try {
104+
const summary = run();
105+
console.log(JSON.stringify(summary, null, 2));
106+
} catch (error) {
107+
console.error(error);
108+
process.exitCode = 1;
109+
}
110+
}
111+

0 commit comments

Comments
 (0)