|
| 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 | +} = require('./helpers'); |
| 14 | +const { |
| 15 | + AGENTS, |
| 16 | + WORKFLOWS, |
| 17 | + AGENT_IDS, |
| 18 | + AGENT_FILES, |
| 19 | + WAVE3_WORKFLOW_NAMES, |
| 20 | +} = require('../../scripts/update/lib/agent-registry'); |
| 21 | + |
| 22 | +const WADE_ID = 'lean-experiments-specialist'; |
| 23 | +const WADE_WORKFLOW_NAMES = ['mvp', 'lean-experiment', 'proof-of-concept', 'proof-of-value']; |
| 24 | +const MVP_WORKFLOW = 'mvp'; |
| 25 | +const STEP_PATTERN = /^step-\d{2}(-[^.]+)?\.md$/; |
| 26 | + |
| 27 | +// ─── P0 Wade: Activation Sequence (FR7) ──────────────────────── |
| 28 | + |
| 29 | +describe('P0 Wade: Activation Sequence', () => { |
| 30 | + let def; |
| 31 | + let rawContent; |
| 32 | + |
| 33 | + before(() => { |
| 34 | + def = loadAgentDefinition(WADE_ID); |
| 35 | + rawContent = fs.readFileSync(path.join(AGENTS_DIR, `${WADE_ID}.md`), 'utf8'); |
| 36 | + }); |
| 37 | + |
| 38 | + it('persona role contains "Validated Learning Expert"', () => { |
| 39 | + assert.ok( |
| 40 | + def.persona.role.includes('Validated Learning Expert'), |
| 41 | + `Wade (${WADE_ID}): persona role should contain "Validated Learning Expert", got "${def.persona.role}"` |
| 42 | + ); |
| 43 | + }); |
| 44 | + |
| 45 | + it('persona identity references the Externalize stream', () => { |
| 46 | + assert.ok( |
| 47 | + def.persona.identity.includes('Externalize'), |
| 48 | + `Wade (${WADE_ID}): persona identity should reference "Externalize" stream` |
| 49 | + ); |
| 50 | + }); |
| 51 | + |
| 52 | + it('persona communication_style contains characteristic phrase', () => { |
| 53 | + const style = def.persona.communication_style; |
| 54 | + const hasPhrase = style.includes('riskiest assumption') || style.includes('smallest experiment'); |
| 55 | + assert.ok( |
| 56 | + hasPhrase, |
| 57 | + `Wade (${WADE_ID}): communication_style should contain "riskiest assumption" or "smallest experiment"` |
| 58 | + ); |
| 59 | + }); |
| 60 | + |
| 61 | + it('has exactly 9 menu items with expected cmd triggers', () => { |
| 62 | + assert.equal( |
| 63 | + def.menuItems.length, |
| 64 | + 9, |
| 65 | + `Wade (${WADE_ID}): expected 9 menu items, got ${def.menuItems.length}` |
| 66 | + ); |
| 67 | + const expectedTriggers = ['MH', 'CH', 'ME', 'LE', 'PC', 'PV', 'VE', 'PM', 'DA']; |
| 68 | + for (const trigger of expectedTriggers) { |
| 69 | + const found = def.menuItems.some(item => item.cmd.startsWith(trigger)); |
| 70 | + assert.ok( |
| 71 | + found, |
| 72 | + `Wade (${WADE_ID}): missing menu item with cmd starting with "${trigger}"` |
| 73 | + ); |
| 74 | + } |
| 75 | + }); |
| 76 | + |
| 77 | + it('exec-path menu items reference existing files on disk', () => { |
| 78 | + const execRegex = /<item\s[^>]*exec="([^"]+)"[^>]*>/g; |
| 79 | + const execPaths = []; |
| 80 | + let m; |
| 81 | + while ((m = execRegex.exec(rawContent)) !== null) { |
| 82 | + execPaths.push(m[1]); |
| 83 | + } |
| 84 | + assert.ok( |
| 85 | + execPaths.length >= 6, |
| 86 | + `Wade (${WADE_ID}): expected at least 6 exec paths, found ${execPaths.length}` |
| 87 | + ); |
| 88 | + for (const execPath of execPaths) { |
| 89 | + const resolved = execPath.replace(/\{project-root\}/g, PACKAGE_ROOT); |
| 90 | + assert.ok( |
| 91 | + fs.existsSync(resolved), |
| 92 | + `Wade (${WADE_ID}): exec path not found on disk: ${resolved}` |
| 93 | + ); |
| 94 | + } |
| 95 | + }); |
| 96 | + |
| 97 | + it('activation step 2 references config.yaml loading with error handling', () => { |
| 98 | + const step2Match = rawContent.match(/<step n="2">([\s\S]*?)<step n="3">/); |
| 99 | + assert.ok( |
| 100 | + step2Match, |
| 101 | + `Wade (${WADE_ID}): could not extract activation step 2 content` |
| 102 | + ); |
| 103 | + assert.ok( |
| 104 | + step2Match[1].includes('config.yaml'), |
| 105 | + `Wade (${WADE_ID}): step 2 should reference "config.yaml" loading` |
| 106 | + ); |
| 107 | + assert.ok( |
| 108 | + step2Match[1].includes('Configuration Error'), |
| 109 | + `Wade (${WADE_ID}): step 2 should contain "Configuration Error" handling` |
| 110 | + ); |
| 111 | + }); |
| 112 | + |
| 113 | + it('rules section has at least 5 rules', () => { |
| 114 | + const ruleMatches = rawContent.match(/<r>/g); |
| 115 | + const ruleCount = ruleMatches ? ruleMatches.length : 0; |
| 116 | + assert.ok( |
| 117 | + ruleCount >= 5, |
| 118 | + `Wade (${WADE_ID}): expected at least 5 rules, found ${ruleCount}` |
| 119 | + ); |
| 120 | + }); |
| 121 | +}); |
| 122 | + |
| 123 | +// ─── P0 Wade: Workflow Execution Output (FR8) ────────────────── |
| 124 | + |
| 125 | +describe('P0 Wade: Workflow Execution Output', () => { |
| 126 | + for (const wfName of WADE_WORKFLOW_NAMES) { |
| 127 | + describe(`${wfName}`, () => { |
| 128 | + const wfDir = path.join(WORKFLOWS_DIR, wfName); |
| 129 | + const stepsDir = path.join(wfDir, 'steps'); |
| 130 | + let stepFiles; |
| 131 | + |
| 132 | + before(() => { |
| 133 | + assert.ok( |
| 134 | + fs.existsSync(stepsDir) && fs.statSync(stepsDir).isDirectory(), |
| 135 | + `Wade (${WADE_ID}): ${wfName}/steps/ directory not found at ${stepsDir}` |
| 136 | + ); |
| 137 | + stepFiles = fs.readdirSync(stepsDir) |
| 138 | + .filter(f => STEP_PATTERN.test(f)) |
| 139 | + .sort(); |
| 140 | + }); |
| 141 | + |
| 142 | + it('workflow.md contains type, description, and author fields', () => { |
| 143 | + const wfContent = fs.readFileSync(path.join(wfDir, 'workflow.md'), 'utf8'); |
| 144 | + assert.ok( |
| 145 | + wfContent.includes('type:'), |
| 146 | + `Wade (${WADE_ID}): ${wfName}/workflow.md missing "type:" field` |
| 147 | + ); |
| 148 | + assert.ok( |
| 149 | + wfContent.includes('description:'), |
| 150 | + `Wade (${WADE_ID}): ${wfName}/workflow.md missing "description:" field` |
| 151 | + ); |
| 152 | + assert.ok( |
| 153 | + wfContent.includes('author:'), |
| 154 | + `Wade (${WADE_ID}): ${wfName}/workflow.md missing "author:" field` |
| 155 | + ); |
| 156 | + }); |
| 157 | + |
| 158 | + it('template file exists', () => { |
| 159 | + const templatePath = path.join(wfDir, `${wfName}.template.md`); |
| 160 | + assert.ok( |
| 161 | + fs.existsSync(templatePath), |
| 162 | + `Wade (${WADE_ID}): ${wfName} template not found at ${templatePath}` |
| 163 | + ); |
| 164 | + }); |
| 165 | + |
| 166 | + it('template file contains placeholder variables', () => { |
| 167 | + const templatePath = path.join(wfDir, `${wfName}.template.md`); |
| 168 | + const content = fs.readFileSync(templatePath, 'utf8'); |
| 169 | + const placeholderPattern = /\{[a-z][-a-z]*\}/; |
| 170 | + assert.ok( |
| 171 | + placeholderPattern.test(content), |
| 172 | + `Wade (${WADE_ID}): ${wfName} template should contain {variable-name} placeholders` |
| 173 | + ); |
| 174 | + }); |
| 175 | + |
| 176 | + // Cross-reference and synthesize tests run ONLY for MVP. |
| 177 | + // Wade's other 3 workflows (lean-experiment, proof-of-concept, proof-of-value) |
| 178 | + // have placeholder stub steps that lack cross-references and synthesize content. |
| 179 | + if (wfName === MVP_WORKFLOW) { |
| 180 | + it('steps 01-05 reference the next step in sequence', () => { |
| 181 | + assert.ok( |
| 182 | + stepFiles.length >= 2, |
| 183 | + `Wade (${WADE_ID}): ${wfName} needs at least 2 steps for cross-reference check, found ${stepFiles.length}` |
| 184 | + ); |
| 185 | + for (let i = 0; i < stepFiles.length - 1; i++) { |
| 186 | + const stepContent = fs.readFileSync(path.join(stepsDir, stepFiles[i]), 'utf8'); |
| 187 | + const nextNum = String(i + 2).padStart(2, '0'); |
| 188 | + assert.ok( |
| 189 | + stepContent.includes(`step-${nextNum}`), |
| 190 | + `Wade (${WADE_ID}): ${wfName}/${stepFiles[i]} should reference step-${nextNum}` |
| 191 | + ); |
| 192 | + } |
| 193 | + }); |
| 194 | + |
| 195 | + it('final step contains synthesize/artifact content', () => { |
| 196 | + const lastFile = stepFiles[stepFiles.length - 1]; |
| 197 | + assert.ok( |
| 198 | + lastFile, |
| 199 | + `Wade (${WADE_ID}): ${wfName} has no step files to check for synthesize content` |
| 200 | + ); |
| 201 | + const content = fs.readFileSync(path.join(stepsDir, lastFile), 'utf8').toLowerCase(); |
| 202 | + const hasSynthesize = content.includes('synthesize') || |
| 203 | + content.includes('artifact') || |
| 204 | + content.includes('final'); |
| 205 | + assert.ok( |
| 206 | + hasSynthesize, |
| 207 | + `Wade (${WADE_ID}): ${wfName}/${lastFile} should contain synthesize/artifact/final content` |
| 208 | + ); |
| 209 | + }); |
| 210 | + } |
| 211 | + |
| 212 | + it('final step suggests next workflows', () => { |
| 213 | + const lastFile = stepFiles[stepFiles.length - 1]; |
| 214 | + assert.ok( |
| 215 | + lastFile, |
| 216 | + `Wade (${WADE_ID}): ${wfName} has no step files to check for next workflow suggestions` |
| 217 | + ); |
| 218 | + const content = fs.readFileSync(path.join(stepsDir, lastFile), 'utf8'); |
| 219 | + const hasHandoff = content.includes('next') || content.includes('Next') || |
| 220 | + content.includes('Suggest') || content.includes('suggest'); |
| 221 | + assert.ok( |
| 222 | + hasHandoff, |
| 223 | + `Wade (${WADE_ID}): ${wfName}/${lastFile} should suggest next workflows` |
| 224 | + ); |
| 225 | + }); |
| 226 | + |
| 227 | + it('has exactly 6 step files', () => { |
| 228 | + assert.equal( |
| 229 | + stepFiles.length, |
| 230 | + 6, |
| 231 | + `Wade (${WADE_ID}): ${wfName} expected 6 steps, got ${stepFiles.length}` |
| 232 | + ); |
| 233 | + }); |
| 234 | + }); |
| 235 | + } |
| 236 | +}); |
| 237 | + |
| 238 | +// ─── P0 Wade: Infrastructure Integration (FR10) ──────────────── |
| 239 | + |
| 240 | +describe('P0 Wade: Infrastructure Integration', () => { |
| 241 | + let wadeRegistry; |
| 242 | + let def; |
| 243 | + |
| 244 | + before(() => { |
| 245 | + wadeRegistry = AGENTS.find(a => a.id === WADE_ID); |
| 246 | + def = loadAgentDefinition(WADE_ID); |
| 247 | + }); |
| 248 | + |
| 249 | + it('registry entry exists for Wade', () => { |
| 250 | + assert.ok( |
| 251 | + wadeRegistry, |
| 252 | + `Wade (${WADE_ID}): registry entry not found in AGENTS array` |
| 253 | + ); |
| 254 | + }); |
| 255 | + |
| 256 | + it('registry persona has all 4 fields as non-empty strings', () => { |
| 257 | + const fields = ['role', 'identity', 'communication_style', 'expertise']; |
| 258 | + for (const field of fields) { |
| 259 | + assert.ok( |
| 260 | + typeof wadeRegistry.persona[field] === 'string' && wadeRegistry.persona[field].length > 0, |
| 261 | + `Wade (${WADE_ID}): registry persona.${field} should be a non-empty string` |
| 262 | + ); |
| 263 | + } |
| 264 | + }); |
| 265 | + |
| 266 | + it('registry stream is "Externalize"', () => { |
| 267 | + assert.equal( |
| 268 | + wadeRegistry.stream, |
| 269 | + 'Externalize', |
| 270 | + `Wade (${WADE_ID}): registry stream should be "Externalize", got "${wadeRegistry.stream}"` |
| 271 | + ); |
| 272 | + }); |
| 273 | + |
| 274 | + it('WORKFLOWS contains exactly 4 entries for Wade with correct names', () => { |
| 275 | + const wadeWorkflows = WORKFLOWS.filter(w => w.agent === WADE_ID); |
| 276 | + assert.equal( |
| 277 | + wadeWorkflows.length, |
| 278 | + 4, |
| 279 | + `Wade (${WADE_ID}): expected 4 workflows, got ${wadeWorkflows.length}` |
| 280 | + ); |
| 281 | + const names = wadeWorkflows.map(w => w.name); |
| 282 | + assert.deepStrictEqual( |
| 283 | + names, |
| 284 | + WADE_WORKFLOW_NAMES, |
| 285 | + `Wade (${WADE_ID}): workflow names mismatch` |
| 286 | + ); |
| 287 | + }); |
| 288 | + |
| 289 | + it('Wade ID appears in derived AGENT_IDS', () => { |
| 290 | + assert.ok( |
| 291 | + AGENT_IDS.includes(WADE_ID), |
| 292 | + `Wade (${WADE_ID}): ID not found in AGENT_IDS` |
| 293 | + ); |
| 294 | + }); |
| 295 | + |
| 296 | + it('Wade file appears in derived AGENT_FILES', () => { |
| 297 | + assert.ok( |
| 298 | + AGENT_FILES.includes(`${WADE_ID}.md`), |
| 299 | + `Wade (${WADE_ID}): file not found in AGENT_FILES` |
| 300 | + ); |
| 301 | + }); |
| 302 | + |
| 303 | + it('Wade workflows are NOT in WAVE3_WORKFLOW_NAMES', () => { |
| 304 | + const registryNames = WORKFLOWS.filter(w => w.agent === WADE_ID).map(w => w.name); |
| 305 | + for (const wfName of registryNames) { |
| 306 | + assert.ok( |
| 307 | + !WAVE3_WORKFLOW_NAMES.has(wfName), |
| 308 | + `Wade (${WADE_ID}): workflow "${wfName}" should NOT be in WAVE3_WORKFLOW_NAMES (Wave 1 agent)` |
| 309 | + ); |
| 310 | + } |
| 311 | + }); |
| 312 | + |
| 313 | + it('persona cross-validation: registry and agent file roles share "Validated Learning" keyword', () => { |
| 314 | + assert.ok( |
| 315 | + wadeRegistry.persona.role.includes('Validated Learning'), |
| 316 | + `Wade (${WADE_ID}): registry persona.role should contain "Validated Learning"` |
| 317 | + ); |
| 318 | + assert.ok( |
| 319 | + def.persona.role.includes('Validated Learning'), |
| 320 | + `Wade (${WADE_ID}): agent file <role> should contain "Validated Learning"` |
| 321 | + ); |
| 322 | + }); |
| 323 | + |
| 324 | + it('persona cross-validation: communication styles share "validated learning" pattern', () => { |
| 325 | + assert.ok( |
| 326 | + wadeRegistry.persona.communication_style.includes('validated learning'), |
| 327 | + `Wade (${WADE_ID}): registry communication_style should contain "validated learning"` |
| 328 | + ); |
| 329 | + assert.ok( |
| 330 | + def.persona.communication_style.includes('validated learning'), |
| 331 | + `Wade (${WADE_ID}): agent file <communication_style> should contain "validated learning"` |
| 332 | + ); |
| 333 | + }); |
| 334 | + |
| 335 | + it('agent tag name matches registry name exactly', () => { |
| 336 | + assert.equal( |
| 337 | + def.agentAttrs.name, |
| 338 | + wadeRegistry.name, |
| 339 | + `Wade (${WADE_ID}): agent tag name "${def.agentAttrs.name}" should match registry name "${wadeRegistry.name}"` |
| 340 | + ); |
| 341 | + }); |
| 342 | + |
| 343 | + it('agent tag icon matches registry icon', () => { |
| 344 | + assert.equal( |
| 345 | + def.agentAttrs.icon, |
| 346 | + wadeRegistry.icon, |
| 347 | + `Wade (${WADE_ID}): agent tag icon should match registry icon` |
| 348 | + ); |
| 349 | + }); |
| 350 | +}); |
0 commit comments