|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +const { describe, it, before } = require('node:test'); |
| 4 | +const assert = require('node:assert/strict'); |
| 5 | +const path = require('path'); |
| 6 | +const fs = require('fs'); |
| 7 | + |
| 8 | +const { |
| 9 | + loadAgentDefinition, |
| 10 | + PACKAGE_ROOT, |
| 11 | + AGENTS_DIR, |
| 12 | + WORKFLOWS_DIR, |
| 13 | + STEP_PATTERN, |
| 14 | +} = require('./helpers'); |
| 15 | + |
| 16 | +const NOAH_ID = 'production-intelligence-specialist'; |
| 17 | +const NOAH_WORKFLOW_NAMES = ['signal-interpretation', 'behavior-analysis', 'production-monitoring']; |
| 18 | + |
| 19 | +// ─── P0 Noah: Activation Sequence (FR7) ──────────────────────── |
| 20 | + |
| 21 | +describe('P0 Noah: Activation Sequence', () => { |
| 22 | + let def; |
| 23 | + let rawContent; |
| 24 | + |
| 25 | + before(() => { |
| 26 | + def = loadAgentDefinition(NOAH_ID); |
| 27 | + rawContent = fs.readFileSync(path.join(AGENTS_DIR, `${NOAH_ID}.md`), 'utf8'); |
| 28 | + }); |
| 29 | + |
| 30 | + it('persona role contains "Signal Interpretation"', () => { |
| 31 | + assert.ok( |
| 32 | + def.persona.role.includes('Signal Interpretation'), |
| 33 | + `Noah (${NOAH_ID}): persona role should contain "Signal Interpretation", got "${def.persona.role}"` |
| 34 | + ); |
| 35 | + }); |
| 36 | + |
| 37 | + it('persona identity references the Sensitize stream', () => { |
| 38 | + assert.ok( |
| 39 | + def.persona.identity.includes('Sensitize'), |
| 40 | + `Noah (${NOAH_ID}): persona identity should reference "Sensitize" stream` |
| 41 | + ); |
| 42 | + }); |
| 43 | + |
| 44 | + it('persona communication_style contains characteristic phrase', () => { |
| 45 | + const style = def.persona.communication_style; |
| 46 | + assert.ok( |
| 47 | + style.includes('The signal indicates'), |
| 48 | + `Noah (${NOAH_ID}): communication_style should contain "The signal indicates"` |
| 49 | + ); |
| 50 | + }); |
| 51 | + |
| 52 | + it('has exactly 7 menu items with expected cmd triggers', () => { |
| 53 | + assert.equal( |
| 54 | + def.menuItems.length, |
| 55 | + 7, |
| 56 | + `Noah (${NOAH_ID}): expected 7 menu items, got ${def.menuItems.length}` |
| 57 | + ); |
| 58 | + const expectedTriggers = ['MH', 'CH', 'SI', 'BA', 'MO', 'PM', 'DA']; |
| 59 | + for (const trigger of expectedTriggers) { |
| 60 | + const found = def.menuItems.some(item => item.cmd.startsWith(trigger)); |
| 61 | + assert.ok( |
| 62 | + found, |
| 63 | + `Noah (${NOAH_ID}): missing menu item with cmd starting with "${trigger}"` |
| 64 | + ); |
| 65 | + } |
| 66 | + }); |
| 67 | + |
| 68 | + it('exec-path menu items reference existing files on disk', () => { |
| 69 | + const execRegex = /<item\s[^>]*exec="([^"]+)"[^>]*>/g; |
| 70 | + const execPaths = []; |
| 71 | + let m; |
| 72 | + while ((m = execRegex.exec(rawContent)) !== null) { |
| 73 | + execPaths.push(m[1]); |
| 74 | + } |
| 75 | + assert.ok( |
| 76 | + execPaths.length >= 4, |
| 77 | + `Noah (${NOAH_ID}): expected at least 4 exec paths, found ${execPaths.length}` |
| 78 | + ); |
| 79 | + for (const execPath of execPaths) { |
| 80 | + const resolved = execPath.replace(/\{project-root\}/g, PACKAGE_ROOT); |
| 81 | + assert.ok( |
| 82 | + fs.existsSync(resolved), |
| 83 | + `Noah (${NOAH_ID}): exec path not found on disk: ${resolved}` |
| 84 | + ); |
| 85 | + } |
| 86 | + }); |
| 87 | + |
| 88 | + it('activation step 2 references config.yaml loading with error handling', () => { |
| 89 | + const step2Match = rawContent.match(/<step n="2">([\s\S]*?)<step n="3">/); |
| 90 | + assert.ok( |
| 91 | + step2Match, |
| 92 | + `Noah (${NOAH_ID}): could not extract activation step 2 content` |
| 93 | + ); |
| 94 | + assert.ok( |
| 95 | + step2Match[1].includes('config.yaml'), |
| 96 | + `Noah (${NOAH_ID}): step 2 should reference "config.yaml" loading` |
| 97 | + ); |
| 98 | + assert.ok( |
| 99 | + step2Match[1].includes('Configuration Error'), |
| 100 | + `Noah (${NOAH_ID}): step 2 should contain "Configuration Error" handling` |
| 101 | + ); |
| 102 | + }); |
| 103 | + |
| 104 | + it('rules section has at least 5 rules', () => { |
| 105 | + const ruleMatches = rawContent.match(/<r>/g); |
| 106 | + const ruleCount = ruleMatches ? ruleMatches.length : 0; |
| 107 | + assert.ok( |
| 108 | + ruleCount >= 5, |
| 109 | + `Noah (${NOAH_ID}): expected at least 5 rules, found ${ruleCount}` |
| 110 | + ); |
| 111 | + }); |
| 112 | +}); |
| 113 | + |
| 114 | +// ─── P0 Noah: Workflow Execution Output (FR8) ────────────────── |
| 115 | + |
| 116 | +describe('P0 Noah: Workflow Execution Output', () => { |
| 117 | + for (const wfName of NOAH_WORKFLOW_NAMES) { |
| 118 | + describe(`${wfName}`, () => { |
| 119 | + const wfDir = path.join(WORKFLOWS_DIR, wfName); |
| 120 | + const stepsDir = path.join(wfDir, 'steps'); |
| 121 | + let stepFiles; |
| 122 | + |
| 123 | + before(() => { |
| 124 | + assert.ok( |
| 125 | + fs.existsSync(stepsDir) && fs.statSync(stepsDir).isDirectory(), |
| 126 | + `Noah (${NOAH_ID}): ${wfName}/steps/ directory not found at ${stepsDir}` |
| 127 | + ); |
| 128 | + stepFiles = fs.readdirSync(stepsDir) |
| 129 | + .filter(f => STEP_PATTERN.test(f)) |
| 130 | + .sort(); |
| 131 | + }); |
| 132 | + |
| 133 | + it('workflow.md contains type, description, and author fields', () => { |
| 134 | + const wfContent = fs.readFileSync(path.join(wfDir, 'workflow.md'), 'utf8'); |
| 135 | + assert.ok( |
| 136 | + wfContent.includes('type:'), |
| 137 | + `Noah (${NOAH_ID}): ${wfName}/workflow.md missing "type:" field` |
| 138 | + ); |
| 139 | + assert.ok( |
| 140 | + wfContent.includes('description:'), |
| 141 | + `Noah (${NOAH_ID}): ${wfName}/workflow.md missing "description:" field` |
| 142 | + ); |
| 143 | + assert.ok( |
| 144 | + wfContent.includes('author:'), |
| 145 | + `Noah (${NOAH_ID}): ${wfName}/workflow.md missing "author:" field` |
| 146 | + ); |
| 147 | + }); |
| 148 | + |
| 149 | + // Noah's workflows have NO template files — skip template tests |
| 150 | + |
| 151 | + it('steps reference the next step in sequence', () => { |
| 152 | + assert.ok( |
| 153 | + stepFiles.length >= 2, |
| 154 | + `Noah (${NOAH_ID}): ${wfName} needs at least 2 steps for cross-reference check, found ${stepFiles.length}` |
| 155 | + ); |
| 156 | + for (let i = 0; i < stepFiles.length - 1; i++) { |
| 157 | + const stepContent = fs.readFileSync(path.join(stepsDir, stepFiles[i]), 'utf8'); |
| 158 | + const nextNum = String(i + 2).padStart(2, '0'); |
| 159 | + assert.ok( |
| 160 | + stepContent.includes(`step-${nextNum}`), |
| 161 | + `Noah (${NOAH_ID}): ${wfName}/${stepFiles[i]} should reference step-${nextNum}` |
| 162 | + ); |
| 163 | + } |
| 164 | + }); |
| 165 | + |
| 166 | + it('final step contains synthesize/artifact content', () => { |
| 167 | + const lastFile = stepFiles[stepFiles.length - 1]; |
| 168 | + assert.ok( |
| 169 | + lastFile, |
| 170 | + `Noah (${NOAH_ID}): ${wfName} has no step files to check for synthesize content` |
| 171 | + ); |
| 172 | + const content = fs.readFileSync(path.join(stepsDir, lastFile), 'utf8').toLowerCase(); |
| 173 | + const hasSynthesize = content.includes('synthesize') || |
| 174 | + content.includes('artifact') || |
| 175 | + content.includes('final'); |
| 176 | + assert.ok( |
| 177 | + hasSynthesize, |
| 178 | + `Noah (${NOAH_ID}): ${wfName}/${lastFile} should contain synthesize/artifact/final content` |
| 179 | + ); |
| 180 | + }); |
| 181 | + |
| 182 | + it('final step suggests next workflows', () => { |
| 183 | + const lastFile = stepFiles[stepFiles.length - 1]; |
| 184 | + assert.ok( |
| 185 | + lastFile, |
| 186 | + `Noah (${NOAH_ID}): ${wfName} has no step files to check for next workflow suggestions` |
| 187 | + ); |
| 188 | + const content = fs.readFileSync(path.join(stepsDir, lastFile), 'utf8'); |
| 189 | + const hasHandoff = content.includes('next') || content.includes('Next') || |
| 190 | + content.includes('Suggest') || content.includes('suggest'); |
| 191 | + assert.ok( |
| 192 | + hasHandoff, |
| 193 | + `Noah (${NOAH_ID}): ${wfName}/${lastFile} should suggest next workflows` |
| 194 | + ); |
| 195 | + }); |
| 196 | + |
| 197 | + it('has exactly 5 step files', () => { |
| 198 | + assert.equal( |
| 199 | + stepFiles.length, |
| 200 | + 5, |
| 201 | + `Noah (${NOAH_ID}): ${wfName} expected 5 steps, got ${stepFiles.length}` |
| 202 | + ); |
| 203 | + }); |
| 204 | + }); |
| 205 | + } |
| 206 | +}); |
0 commit comments