{"record":{"id":"80da301ed7cb45be","repo":"KeygraphHQ/shannon","slug":"template-must-be-a-non-empty-string","errorCode":null,"errorMessage":"Template must be a non-empty string","messagePattern":"Template must be a non-empty string","errorType":"validation","errorClass":"PentestError","httpStatus":null,"severity":"error","filePath":"apps/worker/src/services/prompt-manager.ts","lineNumber":318,"sourceCode":"\n  if (auth.credentials?.totp_secret) {\n    lines.push('- MFA: TOTP enabled');\n  }\n\n  return lines.join('\\n');\n}\n\n// Pure function: Variable interpolation\nasync function interpolateVariables(\n  template: string,\n  variables: PromptVariables,\n  config: DistributedConfig | null = null,\n  logger: ActivityLogger,\n  promptsBaseDir: string = PROMPTS_DIR,\n): Promise<string> {\n  try {\n    if (!template || typeof template !== 'string') {\n      throw new PentestError('Template must be a non-empty string', 'validation', false, {\n        templateType: typeof template,\n        templateLength: template?.length,\n      });\n    }\n\n    if (!variables || !variables.webUrl || !variables.repoPath) {\n      throw new PentestError('Variables must include webUrl and repoPath', 'validation', false, {\n        variables: Object.keys(variables || {}),\n      });\n    }\n\n    // replaceLiteral is used for all value insertions so config values that\n    // contain `$&`/`$$`/`$1`/etc. aren't mangled as replacement patterns.\n    let result = template;\n    result = replaceLiteral(result, /{{WEB_URL}}/g, variables.webUrl);\n    result = replaceLiteral(result, /{{REPO_PATH}}/g, variables.repoPath);\n    result = replaceLiteral(result, /{{PLAYWRIGHT_SESSION}}/g, variables.PLAYWRIGHT_SESSION || 'agent1');\n    result = replaceLiteral(result, /{{AUTH_CONTEXT}}/g, buildAuthContext(config));","sourceCodeStart":300,"sourceCodeEnd":336,"githubUrl":"https://github.com/KeygraphHQ/shannon/blob/1ae0a142f8525410a688f0309fd003cc5b1d92de/apps/worker/src/services/prompt-manager.ts#L300-L336","documentation":"First validation guard in interpolateVariables: the template argument must be a non-empty string. If template is falsy (null, undefined, '') or not a string (number, object), this throws before any substitution. Category 'validation', non-retryable. It is an internal-contract guard; loadPrompt reads the file into a string before calling, so a hit indicates the read returned empty/non-string or an internal caller misused the function.","triggerScenarios":"interpolateVariables is called with a template that is undefined/null/'' — e.g. an empty prompt file was read (0 bytes), a programmatic caller passed a non-string, or fs.readFile returned an unexpected type. The guard fires before webUrl/repoPath are even checked.","commonSituations":"A prompt file under apps/worker/prompts/ is empty (truncated by a failed save). A custom promptDir points at a zero-byte file. An internal refactor calls interpolateVariables with a computed value that evaluated to undefined.","solutions":["Inspect the prompt file named in the surrounding loadPrompt call and confirm it is non-empty and valid UTF-8.","If the file is empty or truncated, restore it from the repo (apps/worker/prompts/<name>.txt) or git.","Check the context.templateType and context.templateLength fields on the thrown PentestError to confirm the bad input.","If calling interpolateVariables directly, ensure the template argument is a string read from a file, not an optional/undefined value."],"exampleFix":"// before: empty prompt file read into template\n//   const template = await fs.readFile(missingPromptPath, 'utf8'); // ''\n//   await interpolateVariables(template, vars, ...);\n// after: guard the read, or restore the file\n//   const template = await fs.readFile(promptPath, 'utf8');\n//   if (!template) throw new Error(`Empty prompt: ${promptPath}`);","handlingStrategy":"validation","validationCode":"// Ensure the template is a non-empty string before interpolating\nfunction isValidTemplate(t: unknown): t is string {\n  return typeof t === 'string' && t.length > 0;\n}\nif (!isValidTemplate(template)) {\n  throw new Error(`Invalid prompt template: type=${typeof template}, len=${(template as any)?.length}`);\n}","typeGuard":"function isNonEmptyString(v: unknown): v is string {\n  return typeof v === 'string' && v.length > 0;\n}","tryCatchPattern":"try {\n  await interpolateVariables(template, vars, config, logger);\n} catch (e) {\n  if (e instanceof PentestError && /Template must be a non-empty string/.test(e.message)) {\n    // the prompt file is empty/corrupt — restore from git before retrying\n    await restorePromptFromGit(promptName);\n  }\n  throw e;\n}","preventionTips":["Treat an empty prompt file as a build/CI failure, not a runtime recoverable state.","When calling interpolateVariables directly, assert the template is a non-empty string first.","Version-control prompt files and restore truncated files from git.","Add a startup self-test that every prompt in the registry is non-empty."],"tags":["prompt","validation","typescript","internal-contract"],"backgroundTag":null,"analyzedSha":"1ae0a142f8525410a688f0309fd003cc5b1d92de","analyzedAt":"2026-08-12T17:40:03.583Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}