Skip to content

Commit e02baf6

Browse files
author
DavidQ
committed
Complete Asset Browser V2 strict payload rendering and invalid input rejection - PR_11_313
1 parent e2b1885 commit e02baf6

8 files changed

Lines changed: 230 additions & 291 deletions

File tree

docs/dev/codex_commands.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,11 @@ PR_11_312
116116
```bash
117117
npx @openai/codex run --model gpt-5.3-codex --reasoning medium "Implement PR_11_312 ..."
118118
```
119+
120+
121+
---
122+
PR_11_313
123+
124+
```bash
125+
npx @openai/codex run --model gpt-5.3-codex --reasoning medium "Implement PR_11_313 ..."
126+
```

docs/dev/commit_comment.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Enforce strict payloadJson-only contract and visible invalid-state rejection across V2 tools - PR 11.312
1+
Complete Asset Browser V2 strict payloadJson catalog handling with explicit valid-empty and invalid legacy hint rejection - PR 11.313
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# PR_11_313 Report
2+
3+
## Purpose
4+
Complete Asset Browser V2 strict JSON behavior with explicit valid-empty handling and strict rejection of invalid/legacy payload fields.
5+
6+
## Files Changed
7+
- `tools/asset-browser-v2/index.js`
8+
- `tests/runtime/V2AssetBrowserStrictJson.test.mjs`
9+
- `docs/pr/PR_11_313_ASSET_BROWSER_V2_STRICT_JSON_BEHAVIOR/PLAN_PR.md`
10+
- `docs/pr/PR_11_313_ASSET_BROWSER_V2_STRICT_JSON_BEHAVIOR/BUILD_PR.md`
11+
- `docs/dev/reports/PR_11_313_report.md`
12+
- `docs/dev/codex_commands.md`
13+
- `docs/dev/commit_comment.txt`
14+
15+
## Implementation Summary
16+
- Enforced strict rejection of legacy future-hint fields:
17+
- `payloadJson.importName`
18+
- `payloadJson.importDestination`
19+
- Kept catalog render sourcing strict to validated `payloadJson.assetCatalog.entries`.
20+
- Added explicit UI empty message for valid catalog with zero entries:
21+
- `Asset catalog is valid but empty. Add assets to payloadJson.assetCatalog.entries and relaunch Asset Browser V2.`
22+
- Added targeted runtime test coverage for strict JSON behavior and failure modes.
23+
24+
## Validation Commands Run
25+
- `node --check tools/asset-browser-v2/index.js` -> **PASS**
26+
- `node --check tests/runtime/V2AssetBrowserStrictJson.test.mjs` -> **PASS**
27+
- `node tests/runtime/V2AssetBrowserStrictJson.test.mjs` -> **PASS**
28+
- `node tests/runtime/LaunchSmokeAllEntries.test.mjs --samples --sample-range=1505-1505` -> **PASS**
29+
30+
## Targeted Test Notes
31+
- Strict runtime test confirms:
32+
- Valid asset catalog payload renders (`VALID_ENTRIES`).
33+
- Valid empty asset catalog is explicit (`VALID_EMPTY`).
34+
- Invalid payload and invalid entries are rejected (`INVALID`).
35+
- Legacy future-hint fields are rejected.
36+
- Sample launch smoke `1505` passed as the targeted Asset Browser launch/sample validation.
37+
38+
## Full Samples Smoke
39+
- **Skipped intentionally**.
40+
- Reason: PR scope is isolated to Asset Browser V2 strict payload behavior; targeted runtime checks plus targeted sample `1505` launch smoke provide direct coverage without requiring full-samples execution.

docs/dev/reports/launch_smoke_report.md

Lines changed: 3 additions & 289 deletions
Large diffs are not rendered by default.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# BUILD_PR_11_313
2+
3+
## Implementation
4+
- Updated `asset-browser-v2` session contract handling to reject legacy `payloadJson.importName` / `payloadJson.importDestination` fields.
5+
- Preserved strict render path from validated `payloadJson.assetCatalog.entries` only.
6+
- Added explicit valid-empty catalog message when catalog is valid but has zero entries.
7+
- Added targeted runtime validation test:
8+
- `tests/runtime/V2AssetBrowserStrictJson.test.mjs`
9+
10+
## Validation
11+
- `node --check tools/asset-browser-v2/index.js`
12+
- `node --check tests/runtime/V2AssetBrowserStrictJson.test.mjs`
13+
- `node tests/runtime/V2AssetBrowserStrictJson.test.mjs`
14+
- `node tests/runtime/LaunchSmokeAllEntries.test.mjs --samples --sample-range=1505-1505`
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# PLAN_PR_11_313
2+
3+
## Purpose
4+
Complete Asset Browser V2 strict JSON behavior so it renders only validated `payloadJson.assetCatalog`, shows explicit valid-empty state, and rejects invalid/legacy payload shapes visibly.
5+
6+
## Scope
7+
- `tools/asset-browser-v2/index.js`
8+
- `tests/runtime/V2AssetBrowserStrictJson.test.mjs`
9+
- `docs/dev/reports/PR_11_313_report.md`
10+
- `docs/dev/codex_commands.md`
11+
- `docs/dev/commit_comment.txt`
12+
13+
## Steps
14+
1. Tighten Asset Browser V2 session validation to reject legacy future-hint fields (`importName`, `importDestination`) and require `payloadJson.assetCatalog`.
15+
2. Keep rendering sourced only from validated asset catalog entries.
16+
3. Make valid-empty catalog behavior explicit on-screen with actionable text.
17+
4. Add a focused runtime test for valid entries, valid empty catalog, and invalid payload cases.
18+
5. Run targeted syntax/runtime validation plus targeted sample launch smoke for sample `1505`.
19+
6. Record evidence and package the PR delta zip.
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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 fixturePath = path.join(repoRoot, "tests", "fixtures", "v2-tools", "asset-browser-v2.json");
11+
const toolJsPath = path.join(repoRoot, "tools", "asset-browser-v2", "index.js");
12+
const resultsPath = path.join(repoRoot, "tmp", "pr_11_313_asset_browser_results.json");
13+
14+
function cloneJson(value) {
15+
return JSON.parse(JSON.stringify(value));
16+
}
17+
18+
function validateAssetBrowserSession(sessionContext) {
19+
if (!sessionContext || typeof sessionContext !== "object" || Array.isArray(sessionContext)) {
20+
return { state: "INVALID", message: "Session context is invalid." };
21+
}
22+
if (sessionContext.version !== "v2") {
23+
return { state: "INVALID", message: "Unsupported session version." };
24+
}
25+
if (typeof sessionContext.toolId !== "string" || sessionContext.toolId.trim() !== "asset-browser-v2") {
26+
return { state: "INVALID", message: "Expected toolId 'asset-browser-v2'." };
27+
}
28+
if (!sessionContext.payloadJson || typeof sessionContext.payloadJson !== "object" || Array.isArray(sessionContext.payloadJson)) {
29+
return { state: "INVALID", message: "Expected payloadJson object." };
30+
}
31+
if (typeof sessionContext.payloadJson.importName === "string" || typeof sessionContext.payloadJson.importDestination === "string") {
32+
return { state: "INVALID", message: "Legacy importName/importDestination is not allowed." };
33+
}
34+
if (!sessionContext.payloadJson.assetCatalog || typeof sessionContext.payloadJson.assetCatalog !== "object" || Array.isArray(sessionContext.payloadJson.assetCatalog)) {
35+
return { state: "INVALID", message: "Expected payloadJson.assetCatalog." };
36+
}
37+
if (typeof sessionContext.payloadJson.assetCatalog.name !== "string" || !sessionContext.payloadJson.assetCatalog.name.trim()) {
38+
return { state: "INVALID", message: "Expected assetCatalog.name." };
39+
}
40+
if (!Array.isArray(sessionContext.payloadJson.assetCatalog.entries)) {
41+
return { state: "INVALID", message: "Expected assetCatalog.entries[]." };
42+
}
43+
if (sessionContext.payloadJson.assetCatalog.entries.some((entry) =>
44+
!entry ||
45+
typeof entry !== "object" ||
46+
Array.isArray(entry) ||
47+
typeof entry.id !== "string" ||
48+
!entry.id.trim() ||
49+
typeof entry.label !== "string" ||
50+
!entry.label.trim() ||
51+
typeof entry.kind !== "string" ||
52+
!entry.kind.trim() ||
53+
typeof entry.path !== "string" ||
54+
!entry.path.trim()
55+
)) {
56+
return { state: "INVALID", message: "Each entry must include id, label, kind, and path." };
57+
}
58+
if (sessionContext.payloadJson.assetCatalog.entries.length === 0) {
59+
return { state: "VALID_EMPTY", message: "Valid catalog with zero assets." };
60+
}
61+
return { state: "VALID_ENTRIES", message: "Valid catalog with renderable assets." };
62+
}
63+
64+
function checkSyntax(filePath) {
65+
execFileSync(process.execPath, ["--check", filePath], {
66+
cwd: repoRoot,
67+
stdio: ["ignore", "pipe", "pipe"]
68+
});
69+
}
70+
71+
export function run() {
72+
checkSyntax(toolJsPath);
73+
const fixtureJson = JSON.parse(fs.readFileSync(fixturePath, "utf8"));
74+
const toolSource = fs.readFileSync(toolJsPath, "utf8");
75+
const validWithEntries = validateAssetBrowserSession(fixtureJson.sessionContext);
76+
77+
const validEmptyContext = cloneJson(fixtureJson.sessionContext);
78+
validEmptyContext.payloadJson.assetCatalog.entries = [];
79+
const validEmpty = validateAssetBrowserSession(validEmptyContext);
80+
81+
const invalidPayloadContext = cloneJson(fixtureJson.sessionContext);
82+
delete invalidPayloadContext.payloadJson;
83+
const invalidPayload = validateAssetBrowserSession(invalidPayloadContext);
84+
85+
const invalidLegacyHintContext = cloneJson(fixtureJson.sessionContext);
86+
invalidLegacyHintContext.payloadJson.importName = "future-import";
87+
const invalidLegacyHint = validateAssetBrowserSession(invalidLegacyHintContext);
88+
89+
const invalidEntryContext = cloneJson(fixtureJson.sessionContext);
90+
invalidEntryContext.payloadJson.assetCatalog.entries[0] = {
91+
id: "broken",
92+
label: "Broken",
93+
kind: "image"
94+
};
95+
const invalidEntry = validateAssetBrowserSession(invalidEntryContext);
96+
97+
const summary = {
98+
generatedAt: new Date().toISOString(),
99+
checks: {
100+
hasPayloadCatalogValidation: toolSource.includes("payloadJson.assetCatalog"),
101+
hasLegacyFutureHintRejection: toolSource.includes("importName/importDestination"),
102+
hasEmptyValidStateMessage: toolSource.includes("Asset catalog is valid but empty")
103+
},
104+
cases: {
105+
validWithEntries,
106+
validEmpty,
107+
invalidPayload,
108+
invalidLegacyHint,
109+
invalidEntry
110+
}
111+
};
112+
113+
fs.mkdirSync(path.dirname(resultsPath), { recursive: true });
114+
fs.writeFileSync(resultsPath, `${JSON.stringify(summary, null, 2)}\n`, "utf8");
115+
116+
assert.equal(summary.checks.hasPayloadCatalogValidation, true, "Asset Browser V2 must validate payloadJson.assetCatalog.");
117+
assert.equal(summary.checks.hasLegacyFutureHintRejection, true, "Asset Browser V2 must reject legacy future hint fields.");
118+
assert.equal(summary.checks.hasEmptyValidStateMessage, true, "Asset Browser V2 must expose an explicit empty valid-catalog message.");
119+
assert.equal(validWithEntries.state, "VALID_ENTRIES");
120+
assert.equal(validEmpty.state, "VALID_EMPTY");
121+
assert.equal(invalidPayload.state, "INVALID");
122+
assert.equal(invalidLegacyHint.state, "INVALID");
123+
assert.equal(invalidEntry.state, "INVALID");
124+
return summary;
125+
}
126+
127+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
128+
try {
129+
const summary = run();
130+
console.log(JSON.stringify(summary, null, 2));
131+
} catch (error) {
132+
console.error(error);
133+
process.exitCode = 1;
134+
}
135+
}

tools/asset-browser-v2/index.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,10 @@ class AssetBrowserV2 {
178178
this.renderError("Asset Browser V2 session data is invalid. Expected payloadJson.assetCatalog.");
179179
return;
180180
}
181+
if (typeof versionCheck.payload.payloadJson.importName === "string" || typeof versionCheck.payload.payloadJson.importDestination === "string") {
182+
this.renderError("Asset Browser V2 session data is invalid. Remove payloadJson.importName/importDestination. Load assets from payloadJson.assetCatalog only.");
183+
return;
184+
}
181185
this.renderCatalog(versionCheck.payload.payloadJson.assetCatalog, versionCheck.payload);
182186
}
183187

@@ -218,7 +222,12 @@ class AssetBrowserV2 {
218222
document.getElementById("assetBrowserV2State").textContent = assetCatalog.entries.length === 0
219223
? "Asset Browser V2 loaded a valid session asset catalog with zero entries."
220224
: "Asset Browser V2 loaded the session asset catalog.";
221-
document.getElementById("assetBrowserV2EmptyState").hidden = true;
225+
if (assetCatalog.entries.length === 0) {
226+
document.getElementById("assetBrowserV2EmptyState").textContent = "Asset catalog is valid but empty. Add assets to payloadJson.assetCatalog.entries and relaunch Asset Browser V2.";
227+
document.getElementById("assetBrowserV2EmptyState").hidden = false;
228+
} else {
229+
document.getElementById("assetBrowserV2EmptyState").hidden = true;
230+
}
222231
document.getElementById("assetBrowserV2InvalidState").hidden = true;
223232
document.getElementById("assetBrowserV2ValidState").hidden = false;
224233

0 commit comments

Comments
 (0)