|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +const { describe, it } = require('node:test'); |
| 4 | +const assert = require('node:assert/strict'); |
| 5 | +const fs = require('fs'); |
| 6 | +const path = require('path'); |
| 7 | + |
| 8 | +const { discoverAgents, VORTEX_DIR, STEP_PATTERN } = require('./helpers'); |
| 9 | + |
| 10 | +// ─── Constants ────────────────────────────────────────────────── |
| 11 | + |
| 12 | +const COMPASS_REF = path.join(VORTEX_DIR, 'compass-routing-reference.md'); |
| 13 | + |
| 14 | +// Workflows that intentionally have NO compass table |
| 15 | +const COMPASS_EXEMPT = ['vortex-navigation']; |
| 16 | + |
| 17 | +// ─── Dynamic Discovery (NFR5) ─────────────────────────────────── |
| 18 | + |
| 19 | +const agents = discoverAgents(); |
| 20 | + |
| 21 | +const allWorkflows = agents.flatMap(agent => |
| 22 | + agent.workflowNames.map((name, i) => ({ |
| 23 | + name, |
| 24 | + dir: agent.workflowDirs[i], |
| 25 | + agentName: agent.name, |
| 26 | + agentId: agent.id, |
| 27 | + })) |
| 28 | +); |
| 29 | + |
| 30 | +const agentNameMap = new Map( |
| 31 | + agents.map(a => [a.name.toLowerCase(), a]) |
| 32 | +); |
| 33 | + |
| 34 | +const registeredWorkflowNames = new Set(allWorkflows.map(w => w.name.toLowerCase())); |
| 35 | + |
| 36 | +// ─── Parsing Utilities ────────────────────────────────────────── |
| 37 | + |
| 38 | +/** |
| 39 | + * Find the final (highest-numbered) step file in a workflow directory. |
| 40 | + * Compass sections always appear in the last step file. |
| 41 | + */ |
| 42 | +function findFinalStepFile(workflowDir) { |
| 43 | + const stepsDir = path.join(workflowDir, 'steps'); |
| 44 | + if (!fs.existsSync(stepsDir)) return null; |
| 45 | + const stepFiles = fs.readdirSync(stepsDir) |
| 46 | + .filter(f => STEP_PATTERN.test(f)) |
| 47 | + .sort(); |
| 48 | + return stepFiles.length > 0 |
| 49 | + ? path.join(stepsDir, stepFiles[stepFiles.length - 1]) |
| 50 | + : null; |
| 51 | +} |
| 52 | + |
| 53 | +/** |
| 54 | + * Extract the ## Vortex Compass section from step file content. |
| 55 | + * Returns section content as string, or null if no compass section. |
| 56 | + */ |
| 57 | +function extractCompassSection(content) { |
| 58 | + const match = content.match(/## Vortex Compass\n([\s\S]*?)(?=\n## [^#]|$)/); |
| 59 | + return match ? match[0] : null; |
| 60 | +} |
| 61 | + |
| 62 | +/** |
| 63 | + * Parse a compass markdown table into structured data. |
| 64 | + * Returns { headers, rows, hasFooter } or null if no valid table. |
| 65 | + */ |
| 66 | +function parseCompassTable(compassContent) { |
| 67 | + const lines = compassContent.split('\n'); |
| 68 | + const tableLines = []; |
| 69 | + let inTable = false; |
| 70 | + |
| 71 | + for (const line of lines) { |
| 72 | + const trimmed = line.trim(); |
| 73 | + if (trimmed.startsWith('|') && trimmed.endsWith('|')) { |
| 74 | + inTable = true; |
| 75 | + tableLines.push(trimmed); |
| 76 | + } else if (inTable) { |
| 77 | + break; // End of table |
| 78 | + } |
| 79 | + } |
| 80 | + |
| 81 | + if (tableLines.length < 3) return null; // Need header + separator + at least 1 row |
| 82 | + |
| 83 | + // Parse header row |
| 84 | + const headerCells = tableLines[0] |
| 85 | + .split('|') |
| 86 | + .filter(c => c.trim() !== '') |
| 87 | + .map(c => c.trim()); |
| 88 | + |
| 89 | + // Skip separator row (index 1), parse data rows |
| 90 | + const rows = []; |
| 91 | + for (let i = 2; i < tableLines.length; i++) { |
| 92 | + const cells = tableLines[i] |
| 93 | + .split('|') |
| 94 | + .filter(c => c.trim() !== '') |
| 95 | + .map(c => c.trim()); |
| 96 | + // Skip separator rows |
| 97 | + if (/^[\s-|]+$/.test(tableLines[i].replace(/\|/g, ''))) continue; |
| 98 | + if (cells.length >= 4) { |
| 99 | + rows.push({ |
| 100 | + condition: cells[0], |
| 101 | + workflow: cells[1], |
| 102 | + agent: cells[2], |
| 103 | + why: cells[3], |
| 104 | + }); |
| 105 | + } |
| 106 | + } |
| 107 | + |
| 108 | + // Check for footer containing "evidence-based recommendations" (scoped before ### subheadings) |
| 109 | + const mainContent = compassContent.split(/\n###/)[0]; |
| 110 | + const hasFooter = mainContent |
| 111 | + .toLowerCase() |
| 112 | + .includes('evidence-based recommendations'); |
| 113 | + |
| 114 | + return { headers: headerCells, rows, hasFooter }; |
| 115 | +} |
| 116 | + |
| 117 | +/** |
| 118 | + * Extract the lowercase agent name from a compass table Agent cell. |
| 119 | + * e.g., "Mila 🔬" → "mila" |
| 120 | + */ |
| 121 | +function extractAgentName(agentCell) { |
| 122 | + const match = agentCell.match(/^(\w+)/); |
| 123 | + return match ? match[1].toLowerCase() : null; |
| 124 | +} |
| 125 | + |
| 126 | +// ─── Test Suite: Compass Table Presence (AC: 3, 6, 7) ────────── |
| 127 | + |
| 128 | +describe('Compass Table Presence', () => { |
| 129 | + const nonExemptWorkflows = allWorkflows.filter( |
| 130 | + w => !COMPASS_EXEMPT.includes(w.name) |
| 131 | + ); |
| 132 | + const exemptWorkflows = allWorkflows.filter( |
| 133 | + w => COMPASS_EXEMPT.includes(w.name) |
| 134 | + ); |
| 135 | + |
| 136 | + // 3.1: Vacuous pass guard |
| 137 | + it('at least 20 workflows have compass tables', () => { |
| 138 | + let compassCount = 0; |
| 139 | + for (const wf of nonExemptWorkflows) { |
| 140 | + const stepFile = findFinalStepFile(wf.dir); |
| 141 | + if (stepFile) { |
| 142 | + const content = fs.readFileSync(stepFile, 'utf8'); |
| 143 | + if (extractCompassSection(content)) compassCount++; |
| 144 | + } |
| 145 | + } |
| 146 | + assert.ok( |
| 147 | + compassCount >= 20, |
| 148 | + `Expected at least 20 workflows with compass tables, found ${compassCount}` |
| 149 | + ); |
| 150 | + }); |
| 151 | + |
| 152 | + // 3.2: Per non-exempt workflow: final step has compass heading |
| 153 | + for (const wf of nonExemptWorkflows) { |
| 154 | + it(`${wf.name} (${wf.agentName}): final step has ## Vortex Compass`, () => { |
| 155 | + const stepFile = findFinalStepFile(wf.dir); |
| 156 | + assert.ok( |
| 157 | + stepFile, |
| 158 | + `${wf.name} (${wf.agentName}): no step files found in ${wf.dir}/steps/` |
| 159 | + ); |
| 160 | + const content = fs.readFileSync(stepFile, 'utf8'); |
| 161 | + const compass = extractCompassSection(content); |
| 162 | + assert.ok( |
| 163 | + compass, |
| 164 | + `${wf.name} (${wf.agentName}): final step ${path.basename(stepFile)} missing ## Vortex Compass section` |
| 165 | + ); |
| 166 | + }); |
| 167 | + } |
| 168 | + |
| 169 | + // 3.3: Per exempt workflow: confirm NO compass heading |
| 170 | + for (const wf of exemptWorkflows) { |
| 171 | + it(`${wf.name} (${wf.agentName}): exempt workflow has NO compass section`, () => { |
| 172 | + const stepFile = findFinalStepFile(wf.dir); |
| 173 | + if (!stepFile) return; // No step files — exempt is correct |
| 174 | + const content = fs.readFileSync(stepFile, 'utf8'); |
| 175 | + const compass = extractCompassSection(content); |
| 176 | + assert.strictEqual( |
| 177 | + compass, |
| 178 | + null, |
| 179 | + `${wf.name} (${wf.agentName}): exempt workflow should NOT have ## Vortex Compass section but one was found` |
| 180 | + ); |
| 181 | + }); |
| 182 | + } |
| 183 | +}); |
| 184 | + |
| 185 | +// ─── Test Suite: Compass Table Format & Routing (AC: 1,2,3,4,6,7) ── |
| 186 | + |
| 187 | +describe('Compass Table Format & Routing Validity', () => { |
| 188 | + const nonExemptWorkflows = allWorkflows.filter( |
| 189 | + w => !COMPASS_EXEMPT.includes(w.name) |
| 190 | + ); |
| 191 | + |
| 192 | + for (const wf of nonExemptWorkflows) { |
| 193 | + describe(`${wf.name} (${wf.agentName})`, () => { |
| 194 | + let compass = null; |
| 195 | + let parsed = null; |
| 196 | + |
| 197 | + // Load compass data once per workflow (try-catch so tests fail gracefully) |
| 198 | + const stepFile = findFinalStepFile(wf.dir); |
| 199 | + try { |
| 200 | + if (stepFile) { |
| 201 | + const content = fs.readFileSync(stepFile, 'utf8'); |
| 202 | + compass = extractCompassSection(content); |
| 203 | + if (compass) parsed = parseCompassTable(compass); |
| 204 | + } |
| 205 | + } catch (_) { /* compass/parsed remain null — tests will fail with diagnostics */ } |
| 206 | + |
| 207 | + // 4.1: 4-column headers match expected (case-insensitive) |
| 208 | + it('compass table has correct 4-column headers', () => { |
| 209 | + assert.ok(parsed, `${wf.name}: no valid compass table found`); |
| 210 | + const expectedHeaders = [ |
| 211 | + 'if you learned...', |
| 212 | + 'consider next...', |
| 213 | + 'agent', |
| 214 | + 'why', |
| 215 | + ]; |
| 216 | + const actualHeaders = parsed.headers.map(h => h.toLowerCase()); |
| 217 | + assert.deepStrictEqual( |
| 218 | + actualHeaders, |
| 219 | + expectedHeaders, |
| 220 | + `${wf.name} (${wf.agentName}): compass table headers mismatch — expected ${JSON.stringify(expectedHeaders)}, got ${JSON.stringify(actualHeaders)}` |
| 221 | + ); |
| 222 | + }); |
| 223 | + |
| 224 | + // 4.2: 2-3 data rows |
| 225 | + it('compass table has 2-3 data rows', () => { |
| 226 | + assert.ok(parsed, `${wf.name}: no valid compass table found`); |
| 227 | + assert.ok( |
| 228 | + parsed.rows.length >= 2 && parsed.rows.length <= 3, |
| 229 | + `${wf.name} (${wf.agentName}): compass table should have 2-3 rows, found ${parsed.rows.length}` |
| 230 | + ); |
| 231 | + }); |
| 232 | + |
| 233 | + // 4.3: Footer contains "evidence-based recommendations" |
| 234 | + it('compass section has evidence-based recommendations footer', () => { |
| 235 | + assert.ok(compass, `${wf.name}: no compass section found`); |
| 236 | + assert.ok( |
| 237 | + parsed && parsed.hasFooter, |
| 238 | + `${wf.name} (${wf.agentName}): compass section missing footer with "evidence-based recommendations"` |
| 239 | + ); |
| 240 | + }); |
| 241 | + |
| 242 | + // 4.4: Agent references match registered agents |
| 243 | + it('all agent references match registered agents', () => { |
| 244 | + assert.ok(parsed, `${wf.name}: no valid compass table found`); |
| 245 | + for (let i = 0; i < parsed.rows.length; i++) { |
| 246 | + const row = parsed.rows[i]; |
| 247 | + const agentRef = extractAgentName(row.agent); |
| 248 | + assert.ok( |
| 249 | + agentRef && agentNameMap.has(agentRef), |
| 250 | + `${wf.name} (${wf.agentName}): compass row ${i + 1} references unknown agent '${agentRef}' — registered: [${[...agentNameMap.keys()].join(', ')}]` |
| 251 | + ); |
| 252 | + } |
| 253 | + }); |
| 254 | + |
| 255 | + // 4.5: Workflow references match registered workflows |
| 256 | + it('all workflow references match registered workflows', () => { |
| 257 | + assert.ok(parsed, `${wf.name}: no valid compass table found`); |
| 258 | + for (let i = 0; i < parsed.rows.length; i++) { |
| 259 | + const row = parsed.rows[i]; |
| 260 | + const wfRef = row.workflow.trim().toLowerCase(); |
| 261 | + assert.ok( |
| 262 | + registeredWorkflowNames.has(wfRef), |
| 263 | + `${wf.name} (${wf.agentName}): compass row ${i + 1} references unknown workflow '${wfRef}' — registered: [${[...registeredWorkflowNames].join(', ')}]` |
| 264 | + ); |
| 265 | + } |
| 266 | + }); |
| 267 | + }); |
| 268 | + } |
| 269 | +}); |
| 270 | + |
| 271 | +// ─── Test Suite: Cross-Reference Integrity (AC: 1, 2, 3, 5) ──── |
| 272 | + |
| 273 | +describe('Cross-Reference Integrity', () => { |
| 274 | + // 5.1: Vacuous pass guard — routing reference exists |
| 275 | + it('compass-routing-reference.md exists and is non-empty', () => { |
| 276 | + assert.ok( |
| 277 | + fs.existsSync(COMPASS_REF), |
| 278 | + `compass-routing-reference.md not found at ${COMPASS_REF}` |
| 279 | + ); |
| 280 | + const content = fs.readFileSync(COMPASS_REF, 'utf8'); |
| 281 | + assert.ok( |
| 282 | + content.trim().length > 0, |
| 283 | + 'compass-routing-reference.md exists but is empty' |
| 284 | + ); |
| 285 | + }); |
| 286 | + |
| 287 | + // 5.2: Reference mentions all 7 agent short names |
| 288 | + it('compass routing reference mentions all registered agent names', () => { |
| 289 | + const content = fs.readFileSync(COMPASS_REF, 'utf8').toLowerCase(); |
| 290 | + for (const [agentName] of agentNameMap) { |
| 291 | + assert.ok( |
| 292 | + content.includes(agentName), |
| 293 | + `compass-routing-reference.md does not mention agent '${agentName}' — registered agents: [${[...agentNameMap.keys()].join(', ')}]` |
| 294 | + ); |
| 295 | + } |
| 296 | + }); |
| 297 | + |
| 298 | + // 5.3: Reference mentions all 22 workflow names |
| 299 | + it('compass routing reference mentions all registered workflow names', () => { |
| 300 | + const content = fs.readFileSync(COMPASS_REF, 'utf8').toLowerCase(); |
| 301 | + const missing = []; |
| 302 | + for (const wfName of registeredWorkflowNames) { |
| 303 | + if (!content.includes(wfName.toLowerCase())) { |
| 304 | + missing.push(wfName); |
| 305 | + } |
| 306 | + } |
| 307 | + assert.strictEqual( |
| 308 | + missing.length, |
| 309 | + 0, |
| 310 | + `compass-routing-reference.md missing ${missing.length} workflow(s): [${missing.join(', ')}] — total registered: ${registeredWorkflowNames.size}` |
| 311 | + ); |
| 312 | + }); |
| 313 | + |
| 314 | + // 5.4: Per agent — all workflowDirs exist as filesystem directories |
| 315 | + for (const agent of agents) { |
| 316 | + for (let i = 0; i < agent.workflowDirs.length; i++) { |
| 317 | + const wfDir = agent.workflowDirs[i]; |
| 318 | + const wfName = agent.workflowNames[i]; |
| 319 | + it(`${agent.name}: workflow directory exists for '${wfName}'`, () => { |
| 320 | + assert.ok( |
| 321 | + fs.existsSync(wfDir) && fs.statSync(wfDir).isDirectory(), |
| 322 | + `${agent.name} (${agent.id}): registry says workflow '${wfName}' exists but directory not found at ${wfDir}` |
| 323 | + ); |
| 324 | + }); |
| 325 | + } |
| 326 | + } |
| 327 | + |
| 328 | + // 5.5: Per workflow with compass — routing targets reference at least one different agent's workflow |
| 329 | + const nonExemptWorkflows = allWorkflows.filter( |
| 330 | + w => !COMPASS_EXEMPT.includes(w.name) |
| 331 | + ); |
| 332 | + |
| 333 | + for (const wf of nonExemptWorkflows) { |
| 334 | + it(`${wf.name} (${wf.agentName}): compass routes to at least one different agent's workflow`, () => { |
| 335 | + const stepFile = findFinalStepFile(wf.dir); |
| 336 | + assert.ok(stepFile, `${wf.name}: no step files found`); |
| 337 | + const content = fs.readFileSync(stepFile, 'utf8'); |
| 338 | + const compass = extractCompassSection(content); |
| 339 | + assert.ok(compass, `${wf.name}: no compass section found`); |
| 340 | + const parsed = parseCompassTable(compass); |
| 341 | + assert.ok(parsed, `${wf.name}: could not parse compass table`); |
| 342 | + |
| 343 | + const ownerAgent = wf.agentName.toLowerCase(); |
| 344 | + const hasCrossAgentRoute = parsed.rows.some(row => { |
| 345 | + const targetAgent = extractAgentName(row.agent); |
| 346 | + return targetAgent && targetAgent !== ownerAgent; |
| 347 | + }); |
| 348 | + |
| 349 | + assert.ok( |
| 350 | + hasCrossAgentRoute, |
| 351 | + `${wf.name} (${wf.agentName}): compass table only routes to own agent — expected at least one cross-agent route` |
| 352 | + ); |
| 353 | + }); |
| 354 | + } |
| 355 | +}); |
0 commit comments