@mindrian_os/install 1.13.0-beta.16 → 1.13.0-beta.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +36 -0
  3. package/commands/act.md +1 -0
  4. package/commands/admin.md +1 -0
  5. package/commands/analyze-needs.md +2 -0
  6. package/commands/analyze-systems.md +2 -0
  7. package/commands/analyze-timing.md +2 -0
  8. package/commands/auto-explore.md +2 -0
  9. package/commands/beautiful-question.md +2 -0
  10. package/commands/brain-derive.md +2 -0
  11. package/commands/build-knowledge.md +2 -0
  12. package/commands/build-thesis.md +2 -0
  13. package/commands/causal.md +2 -0
  14. package/commands/challenge-assumptions.md +2 -0
  15. package/commands/compare-ventures.md +2 -0
  16. package/commands/dashboard.md +2 -1
  17. package/commands/deep-grade.md +2 -0
  18. package/commands/diagnose.md +21 -1
  19. package/commands/diagnostics.md +14 -3
  20. package/commands/doctor.md +4 -1
  21. package/commands/dogfood-flush.md +92 -0
  22. package/commands/dominant-designs.md +2 -0
  23. package/commands/explain-decision.md +2 -0
  24. package/commands/explore-domains.md +2 -0
  25. package/commands/explore-futures.md +2 -0
  26. package/commands/explore-trends.md +2 -0
  27. package/commands/export.md +1 -0
  28. package/commands/feynman-timeline-refresh.md +2 -0
  29. package/commands/file-meeting.md +4 -0
  30. package/commands/find-analogies.md +1 -0
  31. package/commands/find-bottlenecks.md +2 -0
  32. package/commands/find-connections.md +2 -0
  33. package/commands/funding.md +1 -0
  34. package/commands/grade.md +4 -0
  35. package/commands/graph.md +1 -0
  36. package/commands/hat-briefing.md +1 -0
  37. package/commands/heal.md +22 -170
  38. package/commands/help.md +54 -334
  39. package/commands/hmi-status.md +23 -144
  40. package/commands/jtbd.md +1 -0
  41. package/commands/leadership.md +2 -0
  42. package/commands/lean-canvas.md +2 -0
  43. package/commands/macro-trends.md +2 -0
  44. package/commands/map-unknowns.md +2 -0
  45. package/commands/memory.md +1 -0
  46. package/commands/models.md +1 -0
  47. package/commands/mos-reason.md +2 -0
  48. package/commands/mos.md +139 -0
  49. package/commands/mullins.md +2 -0
  50. package/commands/mva-brief.md +58 -0
  51. package/commands/mva-option.md +91 -0
  52. package/commands/new-project.md +4 -0
  53. package/commands/onboard.md +22 -7
  54. package/commands/operator.md +1 -0
  55. package/commands/opportunities.md +1 -0
  56. package/commands/organize.md +22 -469
  57. package/commands/persona.md +1 -0
  58. package/commands/pipeline.md +2 -0
  59. package/commands/present.md +1 -0
  60. package/commands/publish.md +2 -0
  61. package/commands/query.md +24 -102
  62. package/commands/radar.md +2 -0
  63. package/commands/reanalyze.md +1 -0
  64. package/commands/research.md +2 -0
  65. package/commands/room.md +2 -0
  66. package/commands/rooms.md +1 -0
  67. package/commands/root-cause.md +2 -0
  68. package/commands/rs-experts.md +1 -0
  69. package/commands/rs-explain.md +1 -0
  70. package/commands/rs-fetch.md +1 -0
  71. package/commands/rs-thesis.md +1 -0
  72. package/commands/scenario-plan.md +2 -0
  73. package/commands/scheduled-tasks.md +1 -0
  74. package/commands/score-innovation.md +2 -0
  75. package/commands/scout.md +1 -0
  76. package/commands/setup.md +2 -0
  77. package/commands/snapshot.md +2 -0
  78. package/commands/speakers.md +1 -0
  79. package/commands/splash.md +5 -2
  80. package/commands/status.md +1 -0
  81. package/commands/structure-argument.md +2 -0
  82. package/commands/suggest-next.md +2 -0
  83. package/commands/systems-thinking.md +2 -0
  84. package/commands/think-hats.md +2 -0
  85. package/commands/update.md +2 -0
  86. package/commands/user-needs.md +2 -0
  87. package/commands/validate.md +2 -0
  88. package/commands/value-proposition.md +2 -0
  89. package/commands/vault.md +2 -0
  90. package/commands/visualize.md +24 -29
  91. package/commands/whitespace.md +2 -1
  92. package/commands/wiki.md +1 -0
  93. package/hooks/hooks.json +31 -88
  94. package/lib/agents/auto-explore-agent.cjs +82 -0
  95. package/lib/agents/mva/brain-classic-traps.cjs +77 -0
  96. package/lib/agents/mva/brain-cross-domain.cjs +79 -0
  97. package/lib/agents/mva/brain-similar-ventures.cjs +93 -0
  98. package/lib/agents/mva/dashboard-graph-neighborhood.cjs +72 -0
  99. package/lib/agents/mva/index.cjs +42 -0
  100. package/lib/agents/mva/six-hats-red-black.cjs +137 -0
  101. package/lib/agents/mva/tavily-funding-scan.cjs +147 -0
  102. package/lib/agents/mva/test-all-six-agents.cjs +467 -0
  103. package/lib/conversation/operator.cjs +64 -0
  104. package/lib/conversation/operator.test.cjs +160 -0
  105. package/lib/core/breakthrough/canary.cjs +134 -0
  106. package/lib/core/breakthrough/canary.test.cjs +136 -0
  107. package/lib/core/breakthrough/detectors.cjs +359 -0
  108. package/lib/core/breakthrough/detectors.test.cjs +333 -0
  109. package/lib/core/breakthrough/ethics-fence.cjs +127 -0
  110. package/lib/core/breakthrough/ethics-fence.test.cjs +178 -0
  111. package/lib/core/breakthrough/resurfacing.cjs +150 -0
  112. package/lib/core/breakthrough/resurfacing.test.cjs +233 -0
  113. package/lib/core/breakthrough/review-queue.cjs +154 -0
  114. package/lib/core/breakthrough/review-queue.test.cjs +160 -0
  115. package/lib/core/breakthrough/scanner-d17-d18.test.cjs +229 -0
  116. package/lib/core/breakthrough/scanner.cjs +426 -0
  117. package/lib/core/breakthrough/scanner.test.cjs +267 -0
  118. package/lib/core/breakthrough/schema.cjs +164 -0
  119. package/lib/core/breakthrough/schema.test.cjs +256 -0
  120. package/lib/core/breakthrough/scoring.cjs +293 -0
  121. package/lib/core/breakthrough/scoring.test.cjs +423 -0
  122. package/lib/core/breakthrough/verb-dispatch.cjs +221 -0
  123. package/lib/core/breakthrough/verb-dispatch.test.cjs +185 -0
  124. package/lib/core/breakthrough/voice-scaffold.cjs +247 -0
  125. package/lib/core/breakthrough/voice-scaffold.test.cjs +251 -0
  126. package/lib/core/first-touch-version-stamper.cjs +113 -0
  127. package/lib/core/larry-thinness-acknowledgment.cjs +64 -0
  128. package/lib/core/larry-thinness-acknowledgment.test.cjs +97 -0
  129. package/lib/core/llm-name-suggester.cjs +194 -0
  130. package/lib/core/llm-name-suggester.test.cjs +132 -0
  131. package/lib/core/mva-agent-contract.cjs +170 -0
  132. package/lib/core/mva-agent-contract.test.cjs +169 -0
  133. package/lib/core/mva-budget.cjs +75 -0
  134. package/lib/core/mva-budget.test.cjs +68 -0
  135. package/lib/core/mva-classifier.cjs +370 -0
  136. package/lib/core/mva-classifier.test.cjs +248 -0
  137. package/lib/core/mva-deck-builder.cjs +452 -0
  138. package/lib/core/mva-deck-builder.test.cjs +287 -0
  139. package/lib/core/mva-detect.smoke.test.cjs +197 -0
  140. package/lib/core/mva-dispatcher.cjs +110 -0
  141. package/lib/core/mva-dispatcher.test.cjs +216 -0
  142. package/lib/core/mva-option-router.cjs +292 -0
  143. package/lib/core/mva-option-router.test.cjs +483 -0
  144. package/lib/core/mva-orchestrator.cjs +365 -0
  145. package/lib/core/mva-orchestrator.test.cjs +908 -0
  146. package/lib/core/mva-progressive-renderer.cjs +194 -0
  147. package/lib/core/mva-progressive-renderer.test.cjs +157 -0
  148. package/lib/core/mva-rule-linter.cjs +213 -0
  149. package/lib/core/mva-rule-linter.test.cjs +336 -0
  150. package/lib/core/mva-state.cjs +159 -0
  151. package/lib/core/mva-telemetry.cjs +58 -0
  152. package/lib/core/mva-telemetry.test.cjs +196 -0
  153. package/lib/core/mva-vercel-deploy.cjs +168 -0
  154. package/lib/core/mva-vercel-deploy.test.cjs +239 -0
  155. package/lib/core/navigation/dashboard-helpers.cjs +145 -0
  156. package/lib/core/navigation/edges.cjs +35 -0
  157. package/lib/core/navigation/memory-events.cjs +126 -0
  158. package/lib/core/navigation.cjs +11 -0
  159. package/lib/core/resolve-vercel-key.cjs +107 -0
  160. package/lib/core/resolve-vercel-key.test.cjs +137 -0
  161. package/lib/core/room-auto-create.cjs +318 -0
  162. package/lib/core/room-auto-create.test.cjs +198 -0
  163. package/lib/core/room-discard-cascade.cjs +225 -0
  164. package/lib/core/room-discard-cascade.test.cjs +135 -0
  165. package/lib/core/room-name-validator.cjs +132 -0
  166. package/lib/core/room-name-validator.test.cjs +156 -0
  167. package/lib/core/room-naming-selector.cjs +357 -0
  168. package/lib/core/room-naming-selector.test.cjs +277 -0
  169. package/lib/core/room-receipt-emit.cjs +63 -0
  170. package/lib/core/room-skeleton-scaffold.cjs +315 -0
  171. package/lib/core/room-skeleton-scaffold.test.cjs +291 -0
  172. package/lib/core/stale-copy-scanner.cjs +190 -0
  173. package/lib/core/state-aware-router.cjs +78 -0
  174. package/lib/core/telemetry/schema.cjs +168 -0
  175. package/lib/core/telemetry/schema.test.cjs +124 -0
  176. package/lib/core/telemetry/validator.cjs +197 -0
  177. package/lib/core/telemetry/validator.test.cjs +188 -0
  178. package/lib/core/telemetry/writer.cjs +141 -0
  179. package/lib/core/telemetry/writer.test.cjs +331 -0
  180. package/lib/core/terminal-capability.cjs +88 -0
  181. package/lib/core/venture-shape-nudge.cjs +163 -0
  182. package/lib/core/venture-shape-nudge.test.cjs +161 -0
  183. package/lib/core/visual-ops.cjs +70 -2
  184. package/lib/hmi/selector-dispatcher.cjs +90 -1
  185. package/lib/hmi/shape-f7-breakthrough-renderer.cjs +222 -0
  186. package/lib/hmi/shape-f7-breakthrough-renderer.test.cjs +233 -0
  187. package/lib/memory/body-shape-coverage.test.cjs +268 -0
  188. package/lib/memory/doctor-deprecation-surface.test.cjs +185 -0
  189. package/lib/memory/first-touch-version.test.cjs +198 -0
  190. package/lib/memory/help-coverage.test.cjs +108 -0
  191. package/lib/memory/help-renderer.test.cjs +145 -0
  192. package/lib/memory/palette-consistency.test.cjs +127 -0
  193. package/lib/memory/pending-tension-store.cjs +80 -0
  194. package/lib/memory/render-v2-disposition.test.cjs +199 -0
  195. package/lib/memory/run-feynman-tests.cjs +240 -0
  196. package/lib/memory/sessionstart-coordinator.test.cjs +446 -0
  197. package/lib/memory/skill-vs-code-drift.test.cjs +257 -0
  198. package/lib/memory/soft-alias.test.cjs +144 -0
  199. package/lib/memory/stale-copy-scanner.test.cjs +291 -0
  200. package/lib/memory/state-aware-router.test.cjs +90 -0
  201. package/lib/memory/statusline-two-row.test.cjs +338 -0
  202. package/lib/memory/terminal-capability.test.cjs +155 -0
  203. package/lib/render/ROOM.md +74 -22
  204. package/lib/sessionstart/budget-compressor.cjs +130 -0
  205. package/lib/sessionstart/contributor-interface.cjs +134 -0
  206. package/lib/sessionstart/contributor-isolator.cjs +128 -0
  207. package/lib/sessionstart/precedence-ladder.cjs +47 -0
  208. package/lib/statusline/governing-thought-truncator.cjs +45 -0
  209. package/lib/statusline/two-row-renderer.cjs +186 -0
  210. package/lib/statusline/version-resolver.cjs +81 -0
  211. package/package.json +1 -1
  212. package/references/visual/ROOM.md +55 -0
  213. package/references/visual/palette.json +54 -0
  214. package/skills/larry-personality/SKILL.md +34 -0
  215. package/skills/mva-pipeline/SKILL.md +129 -0
  216. package/skills/ui-system/SKILL.md +109 -1
  217. package/skills/ui-system/rules/dual-palette.md +156 -0
  218. package/skills/ui-system/rules/glyph-disambiguation.md +171 -0
  219. package/skills/ui-system/rules/shape-f-zero-and-six.md +169 -0
@@ -0,0 +1,908 @@
1
+ /*
2
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
3
+ *
4
+ * Phase 118-03 Plan 03 Task 2 -- mva-orchestrator tests.
5
+ *
6
+ * End-to-end orchestrator tests with all 6 agents MOCKED via require-cache
7
+ * manipulation. Verifies:
8
+ * - State transitions (markRunning at start, markComplete at end)
9
+ * - Telemetry events fire in the right order with the right schemas
10
+ * - Hebrew short-circuit skips the dispatcher and the state.json manifest
11
+ * - All-fail emits the sharp-question fallback + mva_pipeline_failed
12
+ * - state.json manifest atomically written after mva_brief_rendered
13
+ * - The 3-option footer always closes the rendered output (except Hebrew)
14
+ * - Canon Part 8: orchestrator code does not destructure forbidden fields
15
+ * - scripts/mva-run.cjs CLI smoke test writes rendered output to stdout
16
+ *
17
+ * Tests mock modules via require.cache injection BEFORE requiring the
18
+ * orchestrator. Each test creates a hermetic temp HOME so state.json + jsonl
19
+ * writes do not pollute the real filesystem.
20
+ *
21
+ * Pure CJS, node built-ins only. Run via `node --test`.
22
+ */
23
+ 'use strict';
24
+
25
+ const test = require('node:test');
26
+ const assert = require('node:assert');
27
+ const fs = require('node:fs');
28
+ const path = require('node:path');
29
+ const os = require('node:os');
30
+ const { spawnSync } = require('node:child_process');
31
+
32
+ const SHA256_SAMPLE = 'a'.repeat(64);
33
+
34
+ // -------------------- helpers --------------------
35
+
36
+ function mkTmpHome() {
37
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'mva-orchestrator-test-'));
38
+ }
39
+
40
+ function rmTmpHome(tmpHome) {
41
+ try { fs.rmSync(tmpHome, { recursive: true, force: true }); } catch (_e) {}
42
+ }
43
+
44
+ /**
45
+ * Install module mocks into require.cache so the orchestrator picks them up.
46
+ * The orchestrator under test require()s:
47
+ * - ./mva-state.cjs
48
+ * - ./mva-progressive-renderer.cjs (we use the real one)
49
+ * - ./mva-telemetry.cjs (we use the real one)
50
+ * - ./mva-dispatcher.cjs (mocked: replaces dispatch async-generator)
51
+ * - ../agents/mva/index.cjs (mocked: provides ALL_AGENTS)
52
+ */
53
+ function withMocks({ pending, dispatchResults, ALL_AGENTS }, fn) {
54
+ // Resolve module paths
55
+ const stateP = require.resolve('./mva-state.cjs');
56
+ const dispP = require.resolve('./mva-dispatcher.cjs');
57
+ // Use require.resolve.paths to compute the agents path
58
+ const agentsP = path.resolve(__dirname, '..', 'agents', 'mva', 'index.cjs');
59
+
60
+ // Tracking calls
61
+ const calls = {
62
+ markRunning: 0,
63
+ markComplete: 0,
64
+ readPending: 0,
65
+ pending: pending
66
+ };
67
+
68
+ // Save current cache entries
69
+ const prevState = require.cache[stateP];
70
+ const prevDisp = require.cache[dispP];
71
+ const prevAgents = require.cache[agentsP];
72
+
73
+ // Install mocks
74
+ require.cache[stateP] = {
75
+ id: stateP,
76
+ filename: stateP,
77
+ loaded: true,
78
+ exports: {
79
+ readPending: () => { calls.readPending++; return pending; },
80
+ markRunning: () => { calls.markRunning++; },
81
+ markComplete: () => { calls.markComplete++; },
82
+ // Other functions are unused by the orchestrator under test
83
+ }
84
+ };
85
+
86
+ require.cache[dispP] = {
87
+ id: dispP,
88
+ filename: dispP,
89
+ loaded: true,
90
+ exports: {
91
+ // async generator that yields each result in order
92
+ dispatch: async function* () {
93
+ for (const r of (dispatchResults || [])) {
94
+ yield r;
95
+ }
96
+ },
97
+ dispatchToArray: async () => (dispatchResults || []).slice(),
98
+ }
99
+ };
100
+
101
+ // Mock agents module. Ensure the directory exists (require.cache can hold
102
+ // entries for non-existent files; node uses the cache by id-resolution).
103
+ try {
104
+ fs.mkdirSync(path.dirname(agentsP), { recursive: true });
105
+ } catch (_e) {}
106
+
107
+ require.cache[agentsP] = {
108
+ id: agentsP,
109
+ filename: agentsP,
110
+ loaded: true,
111
+ exports: {
112
+ ALL_AGENTS: ALL_AGENTS || []
113
+ }
114
+ };
115
+
116
+ // Also clear orchestrator cache so it rebinds to fresh mocks
117
+ const orchP = require.resolve('./mva-orchestrator.cjs');
118
+ delete require.cache[orchP];
119
+
120
+ try {
121
+ return fn(calls);
122
+ } finally {
123
+ // Restore
124
+ if (prevState) require.cache[stateP] = prevState; else delete require.cache[stateP];
125
+ if (prevDisp) require.cache[dispP] = prevDisp; else delete require.cache[dispP];
126
+ if (prevAgents) require.cache[agentsP] = prevAgents; else delete require.cache[agentsP];
127
+ delete require.cache[orchP];
128
+ }
129
+ }
130
+
131
+ function readJsonl(home) {
132
+ const p = path.join(home, '.mindrian', 'telemetry', 'v1.13', 'mva.jsonl');
133
+ if (!fs.existsSync(p)) return [];
134
+ return fs.readFileSync(p, 'utf8').split('\n').filter((l) => l.length > 0).map((l) => JSON.parse(l));
135
+ }
136
+
137
+ function readStateJson(home) {
138
+ const p = path.join(home, '.mindrian', 'mva', 'state.json');
139
+ if (!fs.existsSync(p)) return null;
140
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
141
+ }
142
+
143
+ // -------------------- tests --------------------
144
+
145
+ test('orchestrator Test 1 -- 6 ok agents stream, full telemetry, footer present', async () => {
146
+ const tmpHome = mkTmpHome();
147
+ const prevHome = process.env.HOME;
148
+ process.env.HOME = tmpHome;
149
+
150
+ try {
151
+ const dispatchResults = [
152
+ { agent_id: 'brain_similar', status: 'ok', duration_ms: 10, payload: { summary_line: 'Found 3 ventures' } },
153
+ { agent_id: 'brain_cross_domain', status: 'ok', duration_ms: 20, payload: { summary_line: 'Cross-domain analog' } },
154
+ { agent_id: 'brain_classic_traps', status: 'ok', duration_ms: 30, payload: { summary_line: 'Classic trap: freemium' } },
155
+ { agent_id: 'tavily_funding', status: 'ok', duration_ms: 40, payload: { summary_line: 'Tnufa track active' } },
156
+ { agent_id: 'six_hats_red_black', status: 'ok', duration_ms: 50, payload: { summary_line: 'One question: how' } },
157
+ { agent_id: 'dashboard_graph', status: 'ok', duration_ms: 60, payload: { summary_line: 'Room nodes pre-rendered' } },
158
+ ];
159
+
160
+ await withMocks({
161
+ pending: { sentence_sha256: SHA256_SAMPLE, classifier_source: 'heuristic' },
162
+ dispatchResults,
163
+ ALL_AGENTS: [{ id: 'a', fn: async () => null }] // unused (dispatch is mocked)
164
+ }, async (calls) => {
165
+ const { runPipeline } = require('./mva-orchestrator.cjs');
166
+ const t0 = Date.now();
167
+ const outcome = await runPipeline({});
168
+ const elapsed = Date.now() - t0;
169
+
170
+ assert.strictEqual(calls.markRunning, 1, 'markRunning called once');
171
+ assert.strictEqual(calls.markComplete, 1, 'markComplete called once');
172
+ assert.strictEqual(outcome.results.length, 6);
173
+ assert.ok(outcome.rendered.includes('What now?'), 'footer must be present');
174
+ assert.ok(outcome.rendered.includes('[brain]'), 'brain label present');
175
+ assert.equal(outcome.rendered.match(/—/), null, 'no em-dashes in rendered output');
176
+ assert.ok(elapsed < 1000, `wall-clock ${elapsed}ms must be < 1s`);
177
+
178
+ // Telemetry events
179
+ const events = readJsonl(tmpHome);
180
+ const types = events.map((e) => e.event);
181
+ assert.ok(types.includes('mva_pipeline_started'), 'pipeline_started fired');
182
+ assert.strictEqual(types.filter((t) => t === 'mva_agent_returned').length, 6, '6 agent_returned events');
183
+ assert.ok(types.includes('mva_brief_rendered'), 'brief_rendered fired');
184
+
185
+ // CRITICAL: mva_brief_rendered carries total_duration_ms (not duration_ms)
186
+ const rendered = events.find((e) => e.event === 'mva_brief_rendered');
187
+ assert.ok(typeof rendered.total_duration_ms === 'number', 'must have total_duration_ms');
188
+ assert.strictEqual(rendered.duration_ms, undefined, 'must NOT have duration_ms');
189
+ assert.strictEqual(rendered.agent_count_ok, 6);
190
+ assert.strictEqual(rendered.agent_count_failed, 0);
191
+ });
192
+ } finally {
193
+ process.env.HOME = prevHome;
194
+ rmTmpHome(tmpHome);
195
+ }
196
+ });
197
+
198
+ test('orchestrator Test 2 -- 6 error agents trigger sharp-question + pipeline_failed', async () => {
199
+ const tmpHome = mkTmpHome();
200
+ const prevHome = process.env.HOME;
201
+ process.env.HOME = tmpHome;
202
+
203
+ try {
204
+ const dispatchResults = Array.from({ length: 6 }, (_, i) => ({
205
+ agent_id: `agent_${i}`,
206
+ status: 'error',
207
+ duration_ms: 10,
208
+ error: 'forced fail'
209
+ }));
210
+
211
+ await withMocks({
212
+ pending: { sentence_sha256: SHA256_SAMPLE, classifier_source: 'heuristic' },
213
+ dispatchResults,
214
+ ALL_AGENTS: []
215
+ }, async (calls) => {
216
+ const { runPipeline } = require('./mva-orchestrator.cjs');
217
+ const outcome = await runPipeline({});
218
+
219
+ assert.strictEqual(calls.markComplete, 1);
220
+ assert.ok(outcome.rendered.includes("I didn't find precedents"), 'sharp-question fallback rendered');
221
+
222
+ const events = readJsonl(tmpHome);
223
+ const types = events.map((e) => e.event);
224
+ assert.strictEqual(types.filter((t) => t === 'mva_agent_returned').length, 6);
225
+ assert.ok(types.includes('mva_brief_rendered'), 'brief_rendered still fires');
226
+ assert.ok(types.includes('mva_pipeline_failed'), 'pipeline_failed fires on all-fail');
227
+
228
+ const failed = events.find((e) => e.event === 'mva_pipeline_failed');
229
+ assert.ok(typeof failed.total_duration_ms === 'number');
230
+ });
231
+ } finally {
232
+ process.env.HOME = prevHome;
233
+ rmTmpHome(tmpHome);
234
+ }
235
+ });
236
+
237
+ test('orchestrator Test 3 -- 3 ok + 3 timeout renders footer with mixed results', async () => {
238
+ const tmpHome = mkTmpHome();
239
+ const prevHome = process.env.HOME;
240
+ process.env.HOME = tmpHome;
241
+
242
+ try {
243
+ const dispatchResults = [
244
+ { agent_id: 'brain_similar', status: 'ok', duration_ms: 10, payload: { summary_line: 'X' } },
245
+ { agent_id: 'brain_cross_domain', status: 'ok', duration_ms: 20, payload: { summary_line: 'Y' } },
246
+ { agent_id: 'brain_classic_traps', status: 'timeout', duration_ms: 45000 },
247
+ { agent_id: 'tavily_funding', status: 'ok', duration_ms: 30, payload: { summary_line: 'Z' } },
248
+ { agent_id: 'six_hats_red_black', status: 'timeout', duration_ms: 45000 },
249
+ { agent_id: 'dashboard_graph', status: 'timeout', duration_ms: 45000 },
250
+ ];
251
+
252
+ await withMocks({
253
+ pending: { sentence_sha256: SHA256_SAMPLE, classifier_source: 'heuristic' },
254
+ dispatchResults,
255
+ ALL_AGENTS: []
256
+ }, async () => {
257
+ const { runPipeline } = require('./mva-orchestrator.cjs');
258
+ const outcome = await runPipeline({});
259
+
260
+ assert.strictEqual(outcome.results.length, 6);
261
+ assert.ok(outcome.rendered.includes('What now?'), 'footer present');
262
+ assert.ok(/still in progress/.test(outcome.rendered), 'timeout placeholders rendered');
263
+
264
+ const events = readJsonl(tmpHome);
265
+ const rendered = events.find((e) => e.event === 'mva_brief_rendered');
266
+ assert.strictEqual(rendered.agent_count_ok, 3);
267
+ assert.strictEqual(rendered.agent_count_failed, 3);
268
+ });
269
+ } finally {
270
+ process.env.HOME = prevHome;
271
+ rmTmpHome(tmpHome);
272
+ }
273
+ });
274
+
275
+ test('orchestrator Test 4 -- Hebrew refusal short-circuits dispatcher and state.json', async () => {
276
+ const tmpHome = mkTmpHome();
277
+ const prevHome = process.env.HOME;
278
+ process.env.HOME = tmpHome;
279
+
280
+ try {
281
+ let dispatchCalled = false;
282
+
283
+ await withMocks({
284
+ pending: { sentence_sha256: SHA256_SAMPLE, classifier_source: 'language_detect', hebrew_refusal: true, locale: 'he' },
285
+ dispatchResults: [], // would be empty anyway
286
+ ALL_AGENTS: []
287
+ }, async (calls) => {
288
+ // Re-install a dispatcher that flips a flag if called
289
+ const dispP = require.resolve('./mva-dispatcher.cjs');
290
+ require.cache[dispP] = {
291
+ id: dispP,
292
+ filename: dispP,
293
+ loaded: true,
294
+ exports: {
295
+ dispatch: async function* () { dispatchCalled = true; },
296
+ dispatchToArray: async () => { dispatchCalled = true; return []; }
297
+ }
298
+ };
299
+ // Bust orchestrator cache
300
+ delete require.cache[require.resolve('./mva-orchestrator.cjs')];
301
+
302
+ const { runPipeline } = require('./mva-orchestrator.cjs');
303
+ const outcome = await runPipeline({});
304
+
305
+ assert.strictEqual(dispatchCalled, false, 'dispatcher must not be called on Hebrew');
306
+ assert.ok(outcome.rendered.includes('Hebrew') || /[֐-׿]/.test(outcome.rendered), 'Hebrew refusal rendered');
307
+ assert.strictEqual(calls.markComplete, 1, 'markComplete still called');
308
+ // state.json manifest must NOT be written on Hebrew path
309
+ const manifest = readStateJson(tmpHome);
310
+ assert.strictEqual(manifest, null, 'state.json must NOT exist on Hebrew refusal');
311
+ });
312
+ } finally {
313
+ process.env.HOME = prevHome;
314
+ rmTmpHome(tmpHome);
315
+ }
316
+ });
317
+
318
+ test('orchestrator Test 5 -- Canon Part 8 source-grep: no forbidden field destructuring', () => {
319
+ const src = fs.readFileSync(path.join(__dirname, 'mva-orchestrator.cjs'), 'utf8');
320
+ // Strip comments to avoid documentation false positives
321
+ const noBlock = src.replace(/\/\*[\s\S]*?\*\//g, '');
322
+ const noLine = noBlock.replace(/\/\/[^\n]*/g, '');
323
+
324
+ // Forbidden tokens
325
+ const forbidden = [
326
+ /\.sentence\b/,
327
+ /\.prompt\b/,
328
+ /\.raw_sentence\b/,
329
+ /\.raw_text\b/,
330
+ /MVA_SENTENCE/,
331
+ /brain_query/,
332
+ /mcp__brain_/,
333
+ ];
334
+ for (const re of forbidden) {
335
+ assert.equal(re.test(noLine), false, `forbidden pattern ${re} present in orchestrator source`);
336
+ }
337
+ });
338
+
339
+ test('orchestrator Test 6 -- footer text exact verbatim', async () => {
340
+ const tmpHome = mkTmpHome();
341
+ const prevHome = process.env.HOME;
342
+ process.env.HOME = tmpHome;
343
+
344
+ try {
345
+ const dispatchResults = [
346
+ { agent_id: 'brain_similar', status: 'ok', duration_ms: 10, payload: { summary_line: 'X' } },
347
+ ];
348
+
349
+ await withMocks({
350
+ pending: { sentence_sha256: SHA256_SAMPLE, classifier_source: 'heuristic' },
351
+ dispatchResults,
352
+ ALL_AGENTS: []
353
+ }, async () => {
354
+ const { runPipeline } = require('./mva-orchestrator.cjs');
355
+ const outcome = await runPipeline({});
356
+ assert.ok(outcome.rendered.includes("Just tell me what's new"));
357
+ assert.ok(outcome.rendered.includes("Build a room around this"));
358
+ assert.ok(outcome.rendered.includes("Challenge me -- Devil's Advocate"));
359
+ assert.equal(outcome.rendered.match(/—/), null, 'orchestrator output em-dash-free');
360
+ });
361
+ } finally {
362
+ process.env.HOME = prevHome;
363
+ rmTmpHome(tmpHome);
364
+ }
365
+ });
366
+
367
+ test('orchestrator Test 6b (CRITICAL-3 wire) -- state.json manifest atomically written', async () => {
368
+ const tmpHome = mkTmpHome();
369
+ const prevHome = process.env.HOME;
370
+ process.env.HOME = tmpHome;
371
+
372
+ try {
373
+ const sha256 = 'b'.repeat(64);
374
+ const dispatchResults = [
375
+ { agent_id: 'brain_similar', status: 'ok', duration_ms: 10, payload: { summary_line: 'X' } },
376
+ ];
377
+
378
+ await withMocks({
379
+ pending: { sentence_sha256: sha256, classifier_source: 'heuristic' },
380
+ dispatchResults,
381
+ ALL_AGENTS: []
382
+ }, async () => {
383
+ const { runPipeline } = require('./mva-orchestrator.cjs');
384
+ const tBefore = Date.now();
385
+ await runPipeline({});
386
+
387
+ const manifest = readStateJson(tmpHome);
388
+ assert.ok(manifest !== null, 'state.json must exist');
389
+ assert.strictEqual(manifest.current_sha8, sha256.slice(0, 8));
390
+ assert.strictEqual(manifest.current_sha256, sha256);
391
+ // Plan 118-04 contract update: vercel_url is no longer null at this stage.
392
+ // The Plan 118-03 baseline had no deploy wired; Plan 118-04 wires
393
+ // deployDeck and writes either the deploy URL OR the file:// fallback.
394
+ // In this test path (no VERCEL_TOKEN, no mock), it falls back locally.
395
+ assert.ok(typeof manifest.vercel_url === 'string' && manifest.vercel_url.length > 0,
396
+ 'Plan 118-04: vercel_url is filled (real URL or file:// fallback)');
397
+ assert.ok(typeof manifest.rendered_at_ms === 'number');
398
+ assert.ok(manifest.rendered_at_ms >= tBefore && manifest.rendered_at_ms - tBefore < 5000,
399
+ 'rendered_at_ms within 5s of test start');
400
+ });
401
+ } finally {
402
+ process.env.HOME = prevHome;
403
+ rmTmpHome(tmpHome);
404
+ }
405
+ });
406
+
407
+ test('orchestrator Test 7 -- scripts/mva-run.cjs smoke test exits 0 with no pending', () => {
408
+ // With no pending state, the script should exit 0 and print nothing
409
+ // (or only the header). We exercise the no-pending path.
410
+ const tmpHome = mkTmpHome();
411
+ const env = Object.assign({}, process.env, { HOME: tmpHome });
412
+
413
+ try {
414
+ const scriptPath = path.resolve(__dirname, '..', '..', 'scripts', 'mva-run.cjs');
415
+ assert.ok(fs.existsSync(scriptPath), `script must exist: ${scriptPath}`);
416
+ const r = spawnSync('node', [scriptPath], { env, encoding: 'utf8', timeout: 10000 });
417
+ assert.strictEqual(r.status, 0, `script must exit 0, got ${r.status}; stderr: ${r.stderr}`);
418
+ } finally {
419
+ rmTmpHome(tmpHome);
420
+ }
421
+ });
422
+
423
+ // -------------------- Task 3 -- skill + command static-file checks --------------------
424
+
425
+ test('orchestrator Test 8 -- skills/mva-pipeline/SKILL.md exists with required frontmatter', () => {
426
+ const skillPath = path.resolve(__dirname, '..', '..', 'skills', 'mva-pipeline', 'SKILL.md');
427
+ assert.ok(fs.existsSync(skillPath), `SKILL.md must exist: ${skillPath}`);
428
+ const src = fs.readFileSync(skillPath, 'utf8');
429
+ assert.ok(/^---/.test(src.trim()), 'must start with YAML frontmatter');
430
+ // Required frontmatter keys (linter contract for Plan 118-06)
431
+ assert.ok(/\bname:\s*mva-pipeline\b/.test(src), 'frontmatter must declare name: mva-pipeline');
432
+ assert.ok(/\bdescription:/.test(src), 'frontmatter must declare description');
433
+ assert.ok(/\binteractive_first_reward:\s*instant_brief\b/.test(src),
434
+ 'frontmatter must declare interactive_first_reward: instant_brief (Plan 118-06 linter contract)');
435
+ // State-file hint
436
+ assert.ok(/~\/\.mindrian\/mva\//.test(src), 'must reference the state file path');
437
+ });
438
+
439
+ test('orchestrator Test 9 -- commands/mva-brief.md exists with required frontmatter + body', () => {
440
+ const cmdPath = path.resolve(__dirname, '..', '..', 'commands', 'mva-brief.md');
441
+ assert.ok(fs.existsSync(cmdPath), `mva-brief.md must exist: ${cmdPath}`);
442
+ const src = fs.readFileSync(cmdPath, 'utf8');
443
+ assert.ok(/^---/.test(src.trim()), 'must start with YAML frontmatter');
444
+ assert.ok(/\bname:\s*mva-brief\b/.test(src), 'frontmatter must declare name: mva-brief');
445
+ assert.ok(/\bdescription:/.test(src), 'frontmatter must declare description');
446
+ assert.ok(/\bargument-hint:/.test(src), 'frontmatter must declare argument-hint');
447
+ assert.ok(/\ballowed-tools:\s*Bash\b/.test(src), 'frontmatter must declare allowed-tools: Bash');
448
+ assert.ok(/\binteractive_first_reward:\s*instant_brief\b/.test(src),
449
+ 'frontmatter must declare interactive_first_reward: instant_brief');
450
+ // Body must instruct invocation
451
+ assert.ok(/scripts\/mva-run\.cjs/.test(src), 'body must reference scripts/mva-run.cjs');
452
+ assert.ok(/Bash/i.test(src), 'body must mention Bash invocation');
453
+ });
454
+
455
+ test('orchestrator Test 10 -- both skill + command reference scripts/mva-run.cjs', () => {
456
+ const skill = fs.readFileSync(
457
+ path.resolve(__dirname, '..', '..', 'skills', 'mva-pipeline', 'SKILL.md'), 'utf8');
458
+ const cmd = fs.readFileSync(
459
+ path.resolve(__dirname, '..', '..', 'commands', 'mva-brief.md'), 'utf8');
460
+ assert.ok(/scripts\/mva-run\.cjs/.test(skill), 'SKILL.md must reference scripts/mva-run.cjs');
461
+ assert.ok(/scripts\/mva-run\.cjs/.test(cmd), 'mva-brief.md must reference scripts/mva-run.cjs');
462
+ });
463
+
464
+ test('orchestrator Test 11 -- SKILL.md has explicit GUIDED-voice DO-NOT section + em-dash-free', () => {
465
+ const skill = fs.readFileSync(
466
+ path.resolve(__dirname, '..', '..', 'skills', 'mva-pipeline', 'SKILL.md'), 'utf8');
467
+ assert.ok(/\bWhat NOT to do\b/i.test(skill) || /\bDo NOT\b/i.test(skill),
468
+ 'SKILL.md must have explicit do-not section');
469
+ assert.ok(/commentary/i.test(skill), 'must warn against commentary');
470
+ assert.ok(/footer/i.test(skill), 'must warn against skipping footer');
471
+ // Em-dash discipline
472
+ assert.equal(skill.match(/—/), null, 'SKILL.md must have no em-dashes');
473
+
474
+ const cmd = fs.readFileSync(
475
+ path.resolve(__dirname, '..', '..', 'commands', 'mva-brief.md'), 'utf8');
476
+ assert.equal(cmd.match(/—/), null, 'mva-brief.md must have no em-dashes');
477
+ });
478
+
479
+ // -------------------- Plan 118-04 -- deck + Vercel deploy integration --------------------
480
+
481
+ /**
482
+ * withMocks-extended for Plan 118-04 -- also mocks deck-builder + vercel-deploy
483
+ * so we can inspect what the orchestrator passes to them and what it returns
484
+ * to the renderer.
485
+ */
486
+ function withMocksDeck({ pending, dispatchResults, deployResult, captureBuildDeckCalls }, fn) {
487
+ const stateP = require.resolve('./mva-state.cjs');
488
+ const dispP = require.resolve('./mva-dispatcher.cjs');
489
+ const agentsP = path.resolve(__dirname, '..', 'agents', 'mva', 'index.cjs');
490
+ const deckP = require.resolve('./mva-deck-builder.cjs');
491
+ const deployP = require.resolve('./mva-vercel-deploy.cjs');
492
+
493
+ const calls = {
494
+ markRunning: 0,
495
+ markComplete: 0,
496
+ readPending: 0,
497
+ buildDeck: 0,
498
+ deployDeck: 0,
499
+ buildDeckArgs: [],
500
+ deployDeckArgs: [],
501
+ };
502
+
503
+ const prevState = require.cache[stateP];
504
+ const prevDisp = require.cache[dispP];
505
+ const prevAgents = require.cache[agentsP];
506
+ const prevDeck = require.cache[deckP];
507
+ const prevDeploy = require.cache[deployP];
508
+
509
+ require.cache[stateP] = {
510
+ id: stateP, filename: stateP, loaded: true,
511
+ exports: {
512
+ readPending: () => { calls.readPending++; return pending; },
513
+ markRunning: () => { calls.markRunning++; },
514
+ markComplete: () => { calls.markComplete++; },
515
+ }
516
+ };
517
+
518
+ require.cache[dispP] = {
519
+ id: dispP, filename: dispP, loaded: true,
520
+ exports: {
521
+ dispatch: async function* () { for (const r of (dispatchResults || [])) yield r; },
522
+ dispatchToArray: async () => (dispatchResults || []).slice(),
523
+ }
524
+ };
525
+
526
+ try { fs.mkdirSync(path.dirname(agentsP), { recursive: true }); } catch (_e) {}
527
+ require.cache[agentsP] = {
528
+ id: agentsP, filename: agentsP, loaded: true,
529
+ exports: { ALL_AGENTS: [] }
530
+ };
531
+
532
+ require.cache[deckP] = {
533
+ id: deckP, filename: deckP, loaded: true,
534
+ exports: {
535
+ buildDeck: (outcome) => {
536
+ calls.buildDeck++;
537
+ if (captureBuildDeckCalls) calls.buildDeckArgs.push(outcome);
538
+ return '<!DOCTYPE html><html><body>STUB-DECK</body></html>';
539
+ },
540
+ buildSlide: () => '<article></article>',
541
+ DECK_PALETTE: {},
542
+ }
543
+ };
544
+
545
+ require.cache[deployP] = {
546
+ id: deployP, filename: deployP, loaded: true,
547
+ exports: {
548
+ deployDeck: async (html, sha8) => {
549
+ calls.deployDeck++;
550
+ calls.deployDeckArgs.push({ html, sha8 });
551
+ return deployResult || { url: 'https://mos-brief-' + sha8 + '-x.vercel.app', deploy_duration_ms: 100 };
552
+ },
553
+ FALLBACK_DIR: path.join(os.homedir(), '.mindrian', 'mva', 'briefs'),
554
+ }
555
+ };
556
+
557
+ const orchP = require.resolve('./mva-orchestrator.cjs');
558
+ delete require.cache[orchP];
559
+
560
+ // CRITICAL: fn is async; we must await it inside the try so caches stay
561
+ // mocked for the entire await chain. The original Plan 118-03 withMocks
562
+ // helper had the same bug but never tripped it because Plan 118-03's tests
563
+ // did not lazy-require modules during await. Plan 118-04 DOES (the deck
564
+ // builder + vercel-deploy are lazy-required inside runPipeline), so this
565
+ // must be an async wrapper.
566
+ return Promise.resolve()
567
+ .then(() => fn(calls))
568
+ .finally(() => {
569
+ if (prevState) require.cache[stateP] = prevState; else delete require.cache[stateP];
570
+ if (prevDisp) require.cache[dispP] = prevDisp; else delete require.cache[dispP];
571
+ if (prevAgents) require.cache[agentsP] = prevAgents; else delete require.cache[agentsP];
572
+ if (prevDeck) require.cache[deckP] = prevDeck; else delete require.cache[deckP];
573
+ if (prevDeploy) require.cache[deployP] = prevDeploy; else delete require.cache[deployP];
574
+ delete require.cache[orchP];
575
+ });
576
+ }
577
+
578
+ test('orchestrator Test 12 (118-04) -- buildDeck + deployDeck called; outcome has deck_url; mva_brief_deployed fires', async () => {
579
+ const tmpHome = mkTmpHome();
580
+ const prevHome = process.env.HOME;
581
+ process.env.HOME = tmpHome;
582
+ try {
583
+ const sha256 = 'c'.repeat(64);
584
+ const dispatchResults = [
585
+ { agent_id: 'brain_similar', status: 'ok', duration_ms: 10, payload: { summary_line: 'X' } },
586
+ { agent_id: 'brain_cross_domain', status: 'ok', duration_ms: 20, payload: { summary_line: 'Y' } },
587
+ ];
588
+ await withMocksDeck({
589
+ pending: { sentence_sha256: sha256, classifier_source: 'heuristic' },
590
+ dispatchResults,
591
+ deployResult: { url: 'https://mos-brief-cccccccc-foo.vercel.app', deploy_duration_ms: 120 },
592
+ }, async (calls) => {
593
+ const { runPipeline } = require('./mva-orchestrator.cjs');
594
+ const outcome = await runPipeline({});
595
+
596
+ assert.strictEqual(calls.buildDeck, 1, 'buildDeck called once');
597
+ assert.strictEqual(calls.deployDeck, 1, 'deployDeck called once');
598
+ assert.strictEqual(outcome.deck_url, 'https://mos-brief-cccccccc-foo.vercel.app');
599
+
600
+ const events = readJsonl(tmpHome);
601
+ const deployed = events.find((e) => e.event === 'mva_brief_deployed');
602
+ assert.ok(deployed, 'mva_brief_deployed must fire');
603
+ assert.strictEqual(deployed.vercel_subdomain_hash, sha256.slice(0, 8));
604
+ assert.ok(typeof deployed.deploy_duration_ms === 'number');
605
+ assert.strictEqual(deployed.status, 'ok');
606
+ });
607
+ } finally {
608
+ process.env.HOME = prevHome;
609
+ rmTmpHome(tmpHome);
610
+ }
611
+ });
612
+
613
+ test('orchestrator Test 13 (118-04) -- fallback path: deploy returns fallback_path -> deck_url is file:// URL', async () => {
614
+ const tmpHome = mkTmpHome();
615
+ const prevHome = process.env.HOME;
616
+ process.env.HOME = tmpHome;
617
+ try {
618
+ const sha256 = 'd'.repeat(64);
619
+ const dispatchResults = [
620
+ { agent_id: 'brain_similar', status: 'ok', duration_ms: 10, payload: { summary_line: 'X' } },
621
+ ];
622
+ const fallbackPath = path.join(tmpHome, '.mindrian', 'mva', 'briefs', 'dddddddd.html');
623
+ await withMocksDeck({
624
+ pending: { sentence_sha256: sha256, classifier_source: 'heuristic' },
625
+ dispatchResults,
626
+ deployResult: { error: 'vercel_unavailable', fallback_path: fallbackPath, deploy_duration_ms: 5 },
627
+ }, async (calls) => {
628
+ const { runPipeline } = require('./mva-orchestrator.cjs');
629
+ const outcome = await runPipeline({});
630
+
631
+ assert.strictEqual(calls.deployDeck, 1);
632
+ assert.ok(typeof outcome.deck_url === 'string');
633
+ assert.ok(outcome.deck_url.startsWith('file://'), 'deck_url must be file:// URL on fallback');
634
+ assert.ok(outcome.deck_url.endsWith(fallbackPath));
635
+
636
+ const events = readJsonl(tmpHome);
637
+ const deployed = events.find((e) => e.event === 'mva_brief_deployed');
638
+ assert.ok(deployed);
639
+ assert.strictEqual(deployed.status, 'fallback');
640
+ });
641
+ } finally {
642
+ process.env.HOME = prevHome;
643
+ rmTmpHome(tmpHome);
644
+ }
645
+ });
646
+
647
+ test('orchestrator Test 14 (118-04) -- URL line appears AFTER agent blocks BEFORE 3-option footer', async () => {
648
+ const tmpHome = mkTmpHome();
649
+ const prevHome = process.env.HOME;
650
+ process.env.HOME = tmpHome;
651
+ try {
652
+ const sha256 = 'e'.repeat(64);
653
+ const dispatchResults = [
654
+ { agent_id: 'brain_similar', status: 'ok', duration_ms: 10, payload: { summary_line: 'AAAA' } },
655
+ { agent_id: 'brain_cross_domain', status: 'ok', duration_ms: 20, payload: { summary_line: 'BBBB' } },
656
+ ];
657
+ await withMocksDeck({
658
+ pending: { sentence_sha256: sha256, classifier_source: 'heuristic' },
659
+ dispatchResults,
660
+ deployResult: { url: 'https://mos-brief-eeeeeeee-z.vercel.app', deploy_duration_ms: 100 },
661
+ }, async () => {
662
+ const { runPipeline } = require('./mva-orchestrator.cjs');
663
+ const outcome = await runPipeline({});
664
+
665
+ // Order check: AAAA appears before URL line which appears before "What now?"
666
+ const rendered = outcome.rendered;
667
+ const idxAAAA = rendered.indexOf('AAAA');
668
+ const idxUrl = rendered.indexOf('Your Feynman deck:');
669
+ const idxFooter = rendered.indexOf('What now?');
670
+ assert.ok(idxAAAA >= 0, 'first agent block present');
671
+ assert.ok(idxUrl > idxAAAA, 'URL line appears AFTER agent blocks');
672
+ assert.ok(idxFooter > idxUrl, 'footer appears AFTER URL line');
673
+ assert.ok(rendered.includes('https://mos-brief-eeeeeeee-z.vercel.app'),
674
+ 'rendered output includes the deploy URL');
675
+ });
676
+ } finally {
677
+ process.env.HOME = prevHome;
678
+ rmTmpHome(tmpHome);
679
+ }
680
+ });
681
+
682
+ test('orchestrator Test 15 (118-04) -- Hebrew refusal short-circuits BEFORE buildDeck/deployDeck', async () => {
683
+ const tmpHome = mkTmpHome();
684
+ const prevHome = process.env.HOME;
685
+ process.env.HOME = tmpHome;
686
+ try {
687
+ await withMocksDeck({
688
+ pending: { sentence_sha256: SHA256_SAMPLE, hebrew_refusal: true, locale: 'he' },
689
+ dispatchResults: [],
690
+ deployResult: { url: 'should-not-be-called' },
691
+ }, async (calls) => {
692
+ const { runPipeline } = require('./mva-orchestrator.cjs');
693
+ const outcome = await runPipeline({});
694
+
695
+ assert.strictEqual(calls.buildDeck, 0, 'buildDeck must NOT be called on Hebrew');
696
+ assert.strictEqual(calls.deployDeck, 0, 'deployDeck must NOT be called on Hebrew');
697
+ assert.ok(/Hebrew/.test(outcome.rendered) || /[֐-׿]/.test(outcome.rendered),
698
+ 'Hebrew refusal rendered');
699
+ // state.json manifest still must NOT be written
700
+ const manifest = readStateJson(tmpHome);
701
+ assert.strictEqual(manifest, null, 'state.json must NOT exist on Hebrew refusal');
702
+ });
703
+ } finally {
704
+ process.env.HOME = prevHome;
705
+ rmTmpHome(tmpHome);
706
+ }
707
+ });
708
+
709
+ test('orchestrator Test 16 (118-04) -- side-file ~/.mindrian/mva/briefs/<sha8>.json written for Plan 118-05', async () => {
710
+ const tmpHome = mkTmpHome();
711
+ const prevHome = process.env.HOME;
712
+ process.env.HOME = tmpHome;
713
+ try {
714
+ const sha256 = 'f'.repeat(64);
715
+ const sha8 = sha256.slice(0, 8);
716
+ const dispatchResults = [
717
+ { agent_id: 'brain_similar', status: 'ok', duration_ms: 10, payload: { summary_line: 'X' } },
718
+ { agent_id: 'brain_cross_domain', status: 'ok', duration_ms: 20, payload: { summary_line: 'Y' } },
719
+ ];
720
+ await withMocksDeck({
721
+ pending: { sentence_sha256: sha256, classifier_source: 'heuristic' },
722
+ dispatchResults,
723
+ deployResult: { url: 'https://mos-brief-ffffffff-q.vercel.app', deploy_duration_ms: 110 },
724
+ }, async () => {
725
+ const { runPipeline } = require('./mva-orchestrator.cjs');
726
+ await runPipeline({});
727
+
728
+ const sideFile = path.join(tmpHome, '.mindrian', 'mva', 'briefs', sha8 + '.json');
729
+ assert.ok(fs.existsSync(sideFile), 'side-file must exist at ' + sideFile);
730
+ const data = JSON.parse(fs.readFileSync(sideFile, 'utf8'));
731
+ assert.strictEqual(data.sha256, sha256);
732
+ assert.strictEqual(data.sha8, sha8);
733
+ assert.ok(typeof data.timestamp === 'string');
734
+ assert.ok(Array.isArray(data.results));
735
+ assert.strictEqual(data.results.length, 2);
736
+ });
737
+ } finally {
738
+ process.env.HOME = prevHome;
739
+ rmTmpHome(tmpHome);
740
+ }
741
+ });
742
+
743
+ test('orchestrator Test 17 (118-04) -- end-to-end wall-clock < 1500ms with mocked deploy', async () => {
744
+ const tmpHome = mkTmpHome();
745
+ const prevHome = process.env.HOME;
746
+ process.env.HOME = tmpHome;
747
+ try {
748
+ const dispatchResults = Array.from({ length: 6 }, (_, i) => ({
749
+ agent_id: 'agent_' + i,
750
+ status: 'ok',
751
+ duration_ms: 50,
752
+ payload: { summary_line: 'result ' + i },
753
+ }));
754
+ await withMocksDeck({
755
+ pending: { sentence_sha256: SHA256_SAMPLE, classifier_source: 'heuristic' },
756
+ dispatchResults,
757
+ deployResult: { url: 'https://mos-brief-aaaaaaaa-z.vercel.app', deploy_duration_ms: 200 },
758
+ }, async () => {
759
+ const { runPipeline } = require('./mva-orchestrator.cjs');
760
+ const t0 = Date.now();
761
+ const outcome = await runPipeline({});
762
+ const wall = Date.now() - t0;
763
+
764
+ assert.strictEqual(outcome.results.length, 6);
765
+ assert.ok(outcome.deck_url);
766
+ assert.ok(wall < 1500, 'wall-clock ' + wall + 'ms must be < 1500ms');
767
+ });
768
+ } finally {
769
+ process.env.HOME = prevHome;
770
+ rmTmpHome(tmpHome);
771
+ }
772
+ });
773
+
774
+ test('orchestrator Test 18 (118-04) -- deploy exception does NOT fail the pipeline', async () => {
775
+ const tmpHome = mkTmpHome();
776
+ const prevHome = process.env.HOME;
777
+ process.env.HOME = tmpHome;
778
+ try {
779
+ const sha256 = '1'.repeat(64);
780
+ const dispatchResults = [
781
+ { agent_id: 'brain_similar', status: 'ok', duration_ms: 10, payload: { summary_line: 'X' } },
782
+ ];
783
+
784
+ // Mock deployDeck to throw -- the orchestrator's try/catch must absorb it.
785
+ const stateP = require.resolve('./mva-state.cjs');
786
+ const dispP = require.resolve('./mva-dispatcher.cjs');
787
+ const deckP = require.resolve('./mva-deck-builder.cjs');
788
+ const deployP = require.resolve('./mva-vercel-deploy.cjs');
789
+
790
+ const prevState = require.cache[stateP];
791
+ const prevDisp = require.cache[dispP];
792
+ const prevDeck = require.cache[deckP];
793
+ const prevDeploy = require.cache[deployP];
794
+
795
+ require.cache[stateP] = {
796
+ id: stateP, filename: stateP, loaded: true,
797
+ exports: {
798
+ readPending: () => ({ sentence_sha256: sha256 }),
799
+ markRunning: () => {},
800
+ markComplete: () => {},
801
+ }
802
+ };
803
+ require.cache[dispP] = {
804
+ id: dispP, filename: dispP, loaded: true,
805
+ exports: {
806
+ dispatch: async function* () { for (const r of dispatchResults) yield r; },
807
+ dispatchToArray: async () => dispatchResults,
808
+ }
809
+ };
810
+ require.cache[deckP] = {
811
+ id: deckP, filename: deckP, loaded: true,
812
+ exports: {
813
+ buildDeck: () => '<html></html>',
814
+ buildSlide: () => '',
815
+ DECK_PALETTE: {},
816
+ }
817
+ };
818
+ require.cache[deployP] = {
819
+ id: deployP, filename: deployP, loaded: true,
820
+ exports: {
821
+ deployDeck: async () => { throw new Error('explosive failure'); },
822
+ FALLBACK_DIR: '',
823
+ }
824
+ };
825
+
826
+ const orchP = require.resolve('./mva-orchestrator.cjs');
827
+ delete require.cache[orchP];
828
+
829
+ try {
830
+ const { runPipeline } = require('./mva-orchestrator.cjs');
831
+ // Should NOT throw
832
+ const outcome = await runPipeline({});
833
+ assert.ok(outcome, 'pipeline returned outcome even after deploy explosion');
834
+ assert.ok(outcome.rendered.includes('What now?'), 'footer still rendered');
835
+ // mva_brief_deployed should have fired with status: 'error'
836
+ const events = readJsonl(tmpHome);
837
+ const deployed = events.find((e) => e.event === 'mva_brief_deployed');
838
+ assert.ok(deployed, 'mva_brief_deployed event still fires on exception');
839
+ assert.strictEqual(deployed.status, 'error');
840
+ assert.ok(typeof deployed.error_short === 'string');
841
+ } finally {
842
+ if (prevState) require.cache[stateP] = prevState; else delete require.cache[stateP];
843
+ if (prevDisp) require.cache[dispP] = prevDisp; else delete require.cache[dispP];
844
+ if (prevDeck) require.cache[deckP] = prevDeck; else delete require.cache[deckP];
845
+ if (prevDeploy) require.cache[deployP] = prevDeploy; else delete require.cache[deployP];
846
+ delete require.cache[orchP];
847
+ }
848
+ } finally {
849
+ process.env.HOME = prevHome;
850
+ rmTmpHome(tmpHome);
851
+ }
852
+ });
853
+
854
+ test('orchestrator Test 19 (118-04) -- all-fail path skips deploy (no deck URL needed for sharp-question)', async () => {
855
+ const tmpHome = mkTmpHome();
856
+ const prevHome = process.env.HOME;
857
+ process.env.HOME = tmpHome;
858
+ try {
859
+ const dispatchResults = Array.from({ length: 6 }, (_, i) => ({
860
+ agent_id: 'agent_' + i, status: 'error', duration_ms: 10, error: 'fail',
861
+ }));
862
+ await withMocksDeck({
863
+ pending: { sentence_sha256: SHA256_SAMPLE, classifier_source: 'heuristic' },
864
+ dispatchResults,
865
+ deployResult: { url: 'should-not-be-called' },
866
+ }, async (calls) => {
867
+ const { runPipeline } = require('./mva-orchestrator.cjs');
868
+ const outcome = await runPipeline({});
869
+
870
+ assert.strictEqual(calls.buildDeck, 0, 'buildDeck must NOT be called on all-fail');
871
+ assert.strictEqual(calls.deployDeck, 0, 'deployDeck must NOT be called on all-fail');
872
+ assert.ok(outcome.rendered.includes("didn't find precedents"), 'sharp-question rendered');
873
+ assert.strictEqual(outcome.deck_url, null, 'deck_url is null on all-fail');
874
+ });
875
+ } finally {
876
+ process.env.HOME = prevHome;
877
+ rmTmpHome(tmpHome);
878
+ }
879
+ });
880
+
881
+ test('orchestrator Test 20 (118-04) -- state.json vercel_url updated after successful deploy', async () => {
882
+ const tmpHome = mkTmpHome();
883
+ const prevHome = process.env.HOME;
884
+ process.env.HOME = tmpHome;
885
+ try {
886
+ const sha256 = '2'.repeat(64);
887
+ const dispatchResults = [
888
+ { agent_id: 'brain_similar', status: 'ok', duration_ms: 10, payload: { summary_line: 'X' } },
889
+ ];
890
+ const url = 'https://mos-brief-22222222-mock.vercel.app';
891
+ await withMocksDeck({
892
+ pending: { sentence_sha256: sha256, classifier_source: 'heuristic' },
893
+ dispatchResults,
894
+ deployResult: { url, deploy_duration_ms: 100 },
895
+ }, async () => {
896
+ const { runPipeline } = require('./mva-orchestrator.cjs');
897
+ await runPipeline({});
898
+
899
+ const manifest = readStateJson(tmpHome);
900
+ assert.ok(manifest, 'state.json must exist after deploy');
901
+ assert.strictEqual(manifest.current_sha8, sha256.slice(0, 8));
902
+ assert.strictEqual(manifest.vercel_url, url, 'state.json vercel_url MUST be the real URL');
903
+ });
904
+ } finally {
905
+ process.env.HOME = prevHome;
906
+ rmTmpHome(tmpHome);
907
+ }
908
+ });