{"record":{"id":"2b9ffd38143d45f1","repo":"redis/node-redis","slug":"no-slots-to-migrate","errorCode":null,"errorMessage":"No slots to migrate","messagePattern":"No slots to migrate","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/test-utils/lib/fault-injector/proxied-fault-injector-cluster.ts","lineNumber":183,"sourceCode":"\n  return {\n    id,\n    host,\n    port: Number(port),\n    proxyPort: Number(proxyPort),\n  };\n};\n\nconst buildSMigratedNotification = (\n  movedSlotsByDestination: Array<{\n    targetNode: { host: string; port: number };\n    slotRanges: string; // e.g., \"0-5460\" or \"0-100,200-300,500\"\n  }>,\n  seqId: number = 1,\n  encoding: \"base64\" | \"raw\" = \"base64\"\n): string => {\n  if (movedSlotsByDestination.length === 0) {\n    throw new Error(\"No slots to migrate\");\n  }\n\n  const entries = movedSlotsByDestination.map(({ targetNode, slotRanges }) => {\n    const hostPort = `${targetNode.host}:${targetNode.port}`;\n    return `*2\\r\\n+${hostPort}\\r\\n+${slotRanges}\\r\\n`;\n  });\n\n  const response = `>3\\r\\n+SMIGRATED\\r\\n:${seqId}\\r\\n*${\n    movedSlotsByDestination.length\n  }\\r\\n${entries.join(\"\")}`;\n\n  return encoding === \"raw\"\n    ? response\n    : Buffer.from(response).toString(encoding);\n};\n\nconst buildClusterSlotsResponse = (\n  nodes: ProxyNode[],","sourceCodeStart":165,"sourceCodeEnd":201,"githubUrl":"https://github.com/redis/node-redis/blob/90fd0652bc3f2a0a1b2f79fa9096b02a86b0ac58/packages/test-utils/lib/fault-injector/proxied-fault-injector-cluster.ts#L165-L201","documentation":"Internal invariant guard inside buildSMigratedNotification — the RESP encoder for the SMIGRATED push notification. It rejects an empty movedSlotsByDestination array because an SMIGRATED notification carrying zero slot-move entries is meaningless. The sole in-repo caller (triggerMigrate) always passes exactly one entry, so this is a defensive check for future callers or direct use of the helper.","triggerScenarios":"Calling buildSMigratedNotification([]) directly, or a future caller that computes the destination/slot list dynamically and passes an empty array because no slots were selected or no destination node was found.","commonSituations":"Refactoring triggerMigrate to build the entries list conditionally (e.g., filtering nodes) such that it can become empty; a new fault-injection scenario reusing the helper without guaranteeing at least one entry.","solutions":["Pass at least one { targetNode, slotRanges } entry to buildSMigratedNotification.","If entries are computed dynamically, guard upstream and skip emitting the SMIGRATED notification entirely when there is nothing to migrate instead of calling the helper with []."],"exampleFix":"// before\nconst entries = pickEntries(); // may be []\nconst notif = buildSMigratedNotification(entries);\n\n// after\nconst entries = pickEntries();\nif (entries.length === 0) return { status: 'success', error: null, output: 'Nothing to migrate' };\nconst notif = buildSMigratedNotification(entries);","handlingStrategy":"validation","validationCode":"function hasMigratedEntries(entries: Array<{ targetNode: { host: string; port: number }; slotRanges: string }>): boolean {\n  return entries.length > 0 && entries.every(e => e.slotRanges.length > 0);\n}\n// only call buildSMigratedNotification when hasMigratedEntries(entries) is true","typeGuard":"const isNonEmptyEntries = (e: unknown): e is Array<{ targetNode: { host: string; port: number }; slotRanges: string }> =>\n  Array.isArray(e) && e.length > 0;","tryCatchPattern":null,"preventionTips":["Guard the caller: skip emitting SMIGRATED when the computed entries list is empty instead of forwarding [] to the helper.","Treat an empty entries list as a no-op success upstream, not as an error path inside the encoder.","If you reuse buildSMigratedNotification in a new scenario, add a unit test for the empty-input contract."],"tags":["proxy","cluster","migration","test-utils","invariant"],"backgroundTag":null,"analyzedSha":"90fd0652bc3f2a0a1b2f79fa9096b02a86b0ac58","analyzedAt":"2026-08-11T15:37:21.243Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}