@mindrian_os/install 1.13.0-beta.14 → 1.13.0-beta.17
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.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +15 -0
- package/commands/file-meeting.md +2 -0
- package/commands/grade.md +2 -0
- package/commands/mva-brief.md +56 -0
- package/commands/mva-option.md +89 -0
- package/commands/new-project.md +2 -0
- package/commands/onboard.md +2 -0
- package/hooks/hooks.json +9 -0
- package/lib/agents/mva/brain-classic-traps.cjs +77 -0
- package/lib/agents/mva/brain-cross-domain.cjs +79 -0
- package/lib/agents/mva/brain-similar-ventures.cjs +93 -0
- package/lib/agents/mva/dashboard-graph-neighborhood.cjs +72 -0
- package/lib/agents/mva/index.cjs +42 -0
- package/lib/agents/mva/six-hats-red-black.cjs +137 -0
- package/lib/agents/mva/tavily-funding-scan.cjs +147 -0
- package/lib/agents/mva/test-all-six-agents.cjs +467 -0
- package/lib/conversation/operator.cjs +64 -0
- package/lib/conversation/operator.test.cjs +160 -0
- package/lib/core/cache-prune.cjs +114 -8
- package/lib/core/install-state.cjs +242 -0
- package/lib/core/mva-agent-contract.cjs +170 -0
- package/lib/core/mva-agent-contract.test.cjs +169 -0
- package/lib/core/mva-budget.cjs +75 -0
- package/lib/core/mva-budget.test.cjs +68 -0
- package/lib/core/mva-classifier.cjs +370 -0
- package/lib/core/mva-classifier.test.cjs +248 -0
- package/lib/core/mva-deck-builder.cjs +452 -0
- package/lib/core/mva-deck-builder.test.cjs +287 -0
- package/lib/core/mva-detect.smoke.test.cjs +197 -0
- package/lib/core/mva-dispatcher.cjs +110 -0
- package/lib/core/mva-dispatcher.test.cjs +216 -0
- package/lib/core/mva-option-router.cjs +292 -0
- package/lib/core/mva-option-router.test.cjs +483 -0
- package/lib/core/mva-orchestrator.cjs +324 -0
- package/lib/core/mva-orchestrator.test.cjs +908 -0
- package/lib/core/mva-progressive-renderer.cjs +194 -0
- package/lib/core/mva-progressive-renderer.test.cjs +157 -0
- package/lib/core/mva-rule-linter.cjs +213 -0
- package/lib/core/mva-rule-linter.test.cjs +336 -0
- package/lib/core/mva-state.cjs +159 -0
- package/lib/core/mva-telemetry.cjs +170 -0
- package/lib/core/mva-telemetry.test.cjs +196 -0
- package/lib/core/mva-vercel-deploy.cjs +168 -0
- package/lib/core/mva-vercel-deploy.test.cjs +239 -0
- package/lib/core/navigation/dashboard-helpers.cjs +145 -0
- package/lib/core/navigation.cjs +11 -0
- package/lib/core/resolve-vercel-key.cjs +107 -0
- package/lib/core/resolve-vercel-key.test.cjs +137 -0
- package/lib/memory/run-feynman-tests.cjs +27 -0
- package/package.json +1 -1
- package/skills/mva-pipeline/SKILL.md +129 -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
|
+
});
|