{"record":{"id":"06a940e7f29e87a6","repo":"jackwener/OpenCLI","slug":"expected-array-of-summary-rows-got-typeof-rows","errorCode":null,"errorMessage":"expected array of summary rows, got ${typeof rows} (contract drift?)","messagePattern":"expected array of summary rows, got (.+?) \\(contract drift\\?\\)","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/slock/unread-summary.js","lineNumber":45,"sourceCode":"      if (sres.status === 401) return { kind: 'auth', detail: '/servers/ returned 401' };\n      if (!sres.ok) return { kind: 'http', status: sres.status, where:'/servers/' };\n      const servers = await sres.json();\n      const ures = await fetch('${SLOCK_API_BASE}/servers/unread-summary', { credentials:'include', headers });\n      if (ures.status === 401) return { kind: 'auth', detail: '/servers/unread-summary returned 401' };\n      if (!ures.ok) return { kind: 'http', status: ures.status, where:'/servers/unread-summary' };\n      const summary = await ures.json();\n      const byId = {};\n      (Array.isArray(servers) ? servers : []).forEach((s) => { if (s && s.id) byId[s.id] = s; });\n      const rows = (Array.isArray(summary) ? summary : []).map((u) => {\n        const s = byId[u.serverId] || {};\n        return { serverId: u.serverId, slug: s.slug || '', name: s.name || '', unreadCount: u.unreadCount || 0 };\n      });\n      return { kind: 'ok', rows };\n    `;\n    const result = await page.evaluate(`(async () => { ${snippet} })()`);\n    const rows = dispatchEvaluateResult(result);\n    if (!Array.isArray(rows)) {\n      throw new CommandExecutionError(`expected array of summary rows, got ${typeof rows} (contract drift?)`);\n    }\n    return rows;\n  },\n});\n","sourceCodeStart":27,"sourceCodeEnd":50,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/slock/unread-summary.js#L27-L50","documentation":"unread-summary dispatches the page.evaluate result and expects an array of summary rows; if the result is not an array it throws this CommandExecutionError labeling it contract drift. Like thread-list, it refuses to proceed when the response shape doesn't match the documented rows contract.","triggerScenarios":"The unread summary endpoint returns an object envelope ({rows:[...]} or {summary:{...}}), an error object that survived dispatchEvaluateResult, or a non-JSON success body instead of a top-level array.","commonSituations":"Server upgrade changing the response envelope; a 200-wrapped error from a gateway; querying against a different server (`--server`) whose API version differs.","solutions":["Print the raw result and typeof to see the actual envelope.","Unwrap the envelope in the snippet or dispatchEvaluateResult (e.g. `res.rows ?? res`).","Align CLI and server versions.","Verify auth/base URL so real errors aren't surfacing as malformed success payloads."],"exampleFix":"// before\nconst rows = dispatchEvaluateResult(result);\nif (!Array.isArray(rows)) throw new CommandExecutionError(...);\n// after\nconst raw = dispatchEvaluateResult(result);\nconst rows = Array.isArray(raw) ? raw : (raw?.rows ?? []);","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"function isSummaryRows(d) { return Array.isArray(d) || Array.isArray(d?.rows); }","tryCatchPattern":"try {\n  const rows = await cli.run(['unread-summary']);\n} catch (e) {\n  if (String(e.message).includes('expected array of summary rows')) {\n    console.error('unread-summary envelope changed — inspect raw payload / API version');\n  } else throw e;\n}","preventionTips":["Keep server and CLI versions in sync","Assert the rows array shape in tests against the live API","Check for 200-wrapped gateway errors when shapes drift"],"tags":["contract-drift","type-mismatch","api-response"],"backgroundTag":"api-response-contract-drift","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}