diff --git a/plugins/power-pages/scripts/tests/validate-serverlogic.test.js b/plugins/power-pages/scripts/tests/validate-serverlogic.test.js index fff243f2f..ee0046e2a 100644 --- a/plugins/power-pages/scripts/tests/validate-serverlogic.test.js +++ b/plugins/power-pages/scripts/tests/validate-serverlogic.test.js @@ -228,3 +228,42 @@ test('yml name mismatch is flagged', (t) => { assert.equal(result.status, 2); assert.match(result.stderr, /does not match folder name/); }); + +test('startswith( inside string literal is flagged (with-pattern)', (t) => { + const projectRoot = createTempProject(t); + setupProject(projectRoot); + const startsWithJs = `function get() { + try { + Server.Logger.Log("test-endpoint GET called"); + var query = "$filter=startswith(name,'INV-')"; + return JSON.stringify({ status: "success", query: query }); + } catch (err) { + Server.Logger.Error("test-endpoint GET failed: " + err.message); + return JSON.stringify({ status: "error", message: err.message }); + } +}`; + writeServerLogic(projectRoot, 'test-endpoint', startsWithJs, VALID_YML); + + const result = runValidator(projectRoot); + assert.equal(result.status, 2); + assert.match(result.stderr, /contains the substring 'with\('/); +}); + +test('split startswith( workaround passes validation', (t) => { + const projectRoot = createTempProject(t); + setupProject(projectRoot); + const splitJs = `function get() { + try { + Server.Logger.Log("test-endpoint GET called"); + var query = "$filter=startswith" + "(name,'INV-')"; + return JSON.stringify({ status: "success", query: query }); + } catch (err) { + Server.Logger.Error("test-endpoint GET failed: " + err.message); + return JSON.stringify({ status: "error", message: err.message }); + } +}`; + writeServerLogic(projectRoot, 'test-endpoint', splitJs, VALID_YML); + + const result = runValidator(projectRoot); + assert.equal(result.status, 0, result.stderr); +}); diff --git a/plugins/power-pages/skills/add-server-logic/SKILL.md b/plugins/power-pages/skills/add-server-logic/SKILL.md index b607d15d1..c08b77f5c 100644 --- a/plugins/power-pages/skills/add-server-logic/SKILL.md +++ b/plugins/power-pages/skills/add-server-logic/SKILL.md @@ -432,6 +432,26 @@ Repeat this step for each approved server logic item. Create or update `` in diagnostics, and the function silently falls through without executing user code. + +| Pattern | Regex | Caveat | +|---------|-------|--------| +| JavaScript `with` statement | `with\s*\(` | The regex matches the substring `with(` **anywhere** in the file — including inside string literals and inside other identifiers. OData filter functions like `startswith(`, `endswith(`, and `groupwith(` will trip it because they end with `with(`. | + +**Workaround for OData functions** — split the literal so `with(` is not contiguous in source: + +```javascript +// ❌ Triggers validator: "startswith(" contains the substring "with(" +var query = "$filter=startswith(name,'INV-')"; + +// ✅ Split the literal — server still receives "startswith(name,...)" +var query = "$filter=startswith" + "(name,'INV-')"; +``` + +The same trick applies to `endswith(`, `groupwith(`, and any other identifier that ends with `with(`. + #### Code Template ```javascript @@ -1112,6 +1132,7 @@ Provide testing instructions: Use the frontend integration reference from Phase 9 for the exact calling pattern that matches the site's stack. 5. **Check diagnostics** — Server.Logger output can be viewed in Power Pages design studio diagnostics +6. **If the endpoint returns an error or unexpected response** — see [Troubleshooting Server Logic Execution Errors](#troubleshooting-server-logic-execution-errors) for the Playwright + `X-Ms-UserTrace` debugging flow **Output**: Code validated, API URL provided, test guidance given @@ -1188,6 +1209,46 @@ After deployment (or if skipped), remind the user: --- +## Troubleshooting Server Logic Execution Errors + +When a deployed server logic endpoint returns an error or unexpected response, the underlying cause is usually hidden inside the `X-Ms-UserTrace` response header — a base64-encoded blob containing the runtime diagnostic logs. The Power Pages Edge browser extension shows the same data, but inspecting the response header is the fastest path when iterating against a live site. + +Use this flow whenever a server logic call fails or returns a different response than expected: + +### 1. Open the Live Site in a Browser via Playwright + +Use the Playwright MCP tools to drive the site: + +1. Navigate to the deployed site URL (the `websiteUrl` returned by `/activate-site` or shown in the Power Pages admin center). +2. Ask the user to sign in if the endpoint requires authentication and wait for confirmation. +3. Trigger the action that calls the failing server logic endpoint (click the button, submit the form, etc.) — or call the endpoint directly with `fetch()`. + +### 2. Capture the Network Response + +Use `mcp__plugin_power-pages_playwright__browser_network_requests` to list network activity, then locate the request to `/_api/serverlogics/`. Note: + +- The HTTP status code (e.g., 200, 400, 500) +- The response body (often a generic error or empty payload when validation fails) +- **Most importantly: the `X-Ms-UserTrace` response header** — this is where the actual diagnostic logs live + +If `browser_network_requests` does not surface the response headers directly, fall back to `mcp__plugin_power-pages_playwright__browser_evaluate` and read the headers from a `fetch()` call: + +```javascript +const res = await fetch('/_api/serverlogics/', { method: 'GET', credentials: 'include' }); +const trace = res.headers.get('X-Ms-UserTrace'); +return { status: res.status, body: await res.text(), trace }; +``` + +### 3. Decode the `X-Ms-UserTrace` Header + +The header value is base64-encoded JSON. Decode it. + +The decoded payload contains the diagnostic log entries — including the actual error message, the prohibited pattern (if script validation failed). + +After fixing, redeploy via `/deploy-site` and restart the site so the change is picked up immediately. + +--- + ## Important Notes ### Throughout All Phases diff --git a/plugins/power-pages/skills/add-server-logic/scripts/validate-serverlogic.js b/plugins/power-pages/skills/add-server-logic/scripts/validate-serverlogic.js index f68e00dc6..aa975c310 100644 --- a/plugins/power-pages/skills/add-server-logic/scripts/validate-serverlogic.js +++ b/plugins/power-pages/skills/add-server-logic/scripts/validate-serverlogic.js @@ -202,6 +202,24 @@ runValidation((cwd) => { if (/(?:async\s+)?function\s+delete\s*\(/m.test(strippedContent)) { errors.push(`${dirName}.js: uses 'function delete()' — 'delete' is a reserved word, use 'del' instead`); } + + // Check: no 'with(' substring anywhere in the raw source. + // The Power Pages server validator rejects scripts matching /with\s*\(/ to block + // the JavaScript `with` statement, but the regex also matches the substring inside + // identifiers like `startswith(`, `endswith(`, and `groupwith(` — even inside string + // literals — which causes runtime error: "Script validation failed: prohibited + // pattern found - Pattern: with\s*\(". Run this on the raw content (not stripped) + // because the server validator does the same. + const withMatches = [...content.matchAll(/with\s*\(/g)]; + if (withMatches.length > 0) { + const lineNumbers = withMatches.map(m => content.slice(0, m.index).split('\n').length); + errors.push( + `${dirName}.js: contains the substring 'with(' on line(s) ${lineNumbers.join(', ')} — ` + + `the Power Pages server validator's regex /with\\s*\\(/ blocks this even inside string literals ` + + `(e.g., OData functions like startswith(, endswith(). Split the literal so 'with(' is not contiguous, ` + + `e.g., \`"startswith" + "(crd50_name,..."\` instead of \`"startswith(crd50_name,..."\`.` + ); + } } if (errors.length > 0) {