diff --git a/actions/azurestorage.js b/actions/azurestorage.js index ea104db1..f75d6000 100644 --- a/actions/azurestorage.js +++ b/actions/azurestorage.js @@ -1384,14 +1384,27 @@ export const getRepsBlobs = async (containerName) => { listOptions )) { const blobClient = containerClient.getBlobClient(blob.name); - //console.log("getreps blob:", blob); - blob.name.split("/")[2].indexOf("_rep.json") > 0 && - blob.name.split("/")[2].indexOf("undefined") < 0 && + + // Filter out soft-deleted/stale tag entries and malformed names. + const namePart = blob.name.split("/")[2] ?? ""; + if ( + namePart.indexOf("_rep.json") <= 0 || + namePart.indexOf("undefined") >= 0 + ) + continue; + + try { + const properties = await blobClient.getProperties(); + blobObj.push({ - "name": blob.name.split("/")[2], + "name": namePart, "path": blob.name, - "size": blobClient.getProperties().contentLength + "size": properties.contentLength }); + } catch (error) { + if (error?.statusCode === 404) continue; + throw error; + } } //console.log("blobObjwwwww:", blobObj); diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index f39569eb..f9151e7f 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -256,3 +256,40 @@ Validation: Follow-ups: - Optional next slice: apply same bounded hash-canonicalization parity to remaining high-sensitivity file routes where mixed encoded/raw callers may exist (`getbloblist`, `getprogressobjblob`) and add regression cases to phase21. + +--- + +### CL-007: TASK22224 getrepsblob stability hotfix after delete representation flow + +date: 2026-03-23 +author: Cline +scope: `actions/azurestorage.js` (`getRepsBlobs`), `tests/phase21/file-handler-contract.test.cjs` +type: change +rationale: Resolve reported runtime 400 (`GET_REPS_BLOB_FAILED`) after delete representation actions, caused by stale soft-deleted blob tag hits during representation blob enumeration. +impact: Prevents transient/stale Azure tag index entries from breaking representation retrieval, improving reliability of post-delete refresh without relaxing route security contracts. +status: completed + +Summary: + +- Hardened `getRepsBlobs(containerName)` in `actions/azurestorage.js`: + - fixed async misuse (`blobClient.getProperties().contentLength` without await) + - added existence/property guard with explicit `await blobClient.getProperties()` + - skips 404s (soft-deleted/stale tag index results) instead of throwing + - preserves behavior for non-404 failures (rethrow for proper error visibility) + - kept existing `_rep.json`/`undefined` name filtering intact +- Added phase21 contract coverage for `getrepsblob` route: + - success payload contract test + - dependency failure contract test (`GET_REPS_BLOB_FAILED`) + +Validation: + +- `node tests/phase21/file-handler-contract.test.cjs` -> pass (25/25) +- `node tests/phase21/api-contract-slice1.test.cjs` -> pass + - helper: 4/4 + - file-handler: 25/25 + - email-handler: 12/12 + - endpoint-handler: 149/149 + +Follow-ups: + +- Optional: add the same stale-tag existence guard pattern to any remaining Azure tag-list readers that still consume `findBlobsByTags` results without property existence verification. diff --git a/tests/phase21/file-handler-contract.test.cjs b/tests/phase21/file-handler-contract.test.cjs index 8b7342c9..dc366e35 100644 --- a/tests/phase21/file-handler-contract.test.cjs +++ b/tests/phase21/file-handler-contract.test.cjs @@ -253,6 +253,60 @@ test("deleteblobrep handler dependency failure returns DELETE_BLOB_REP_FAILED", assert.strictEqual(res.state.jsonBody.error.code, "DELETE_BLOB_REP_FAILED"); }); +test("getrepsblob handler success returns representations payload", async () => { + const mod = loadModule("pages/api/file/getrepsblob.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: () => "&hash=expected", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + consoleLogger: () => {}, + getRepsBlobs: async () => [ + { path: "c/ref_rep.json", name: "ref_rep.json" } + ], + downloadAllRepsFiles: async () => ({ + "@odata.count": 1, + value: [{ id: "r1" }] + }) + }); + + const req = { + query: { container: "c1", hash: "expected" } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + "@odata.count": 1, + value: [{ id: "r1" }] + }); +}); + +test("getrepsblob handler dependency failure returns GET_REPS_BLOB_FAILED", async () => { + const mod = loadModule("pages/api/file/getrepsblob.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: () => "&hash=expected", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + consoleLogger: () => {}, + getRepsBlobs: async () => { + throw new Error("storage failure"); + }, + downloadAllRepsFiles: async () => ({}) + }); + + const req = { + query: { container: "c1", hash: "expected" } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(res.state.jsonBody.error.code, "GET_REPS_BLOB_FAILED"); +}); + test("getbloblist handler returns INVALID_HASH for mismatch", async () => { const mod = loadModule("pages/api/file/getbloblist.js", { nextConnect: createNextConnectMock(),