@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,483 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
3
|
+
*
|
|
4
|
+
* Phase 118-05 Plan 05 -- mva-option-router tests.
|
|
5
|
+
*
|
|
6
|
+
* 17 tests covering:
|
|
7
|
+
* - 3 option paths (1 = stay_in_just_talk, 2 = phase_119_stub, 3 = invoke_challenge_assumptions)
|
|
8
|
+
* - Telemetry emission (mva_option_selected with sentence_sha256 + option_id + time_to_click_ms)
|
|
9
|
+
* - Operator transitions (option 1 -> JUST_TALK; option 2 -> no transition; option 3 -> METHODOLOGY)
|
|
10
|
+
* - Edge cases: invalid option (no telemetry), missing side-file, brief still rendering
|
|
11
|
+
* - resolveCurrentSha8 (CRITICAL-3 part 2 wire): present / absent / staleness pass-through
|
|
12
|
+
* - File-inspection tests for commands/mva-option.md + skills/mva-pipeline/SKILL.md (Tests 13-16)
|
|
13
|
+
* - End-to-end integration: /mos:mva-option 2 with no sha argument resolves from state.json (Test 17)
|
|
14
|
+
*
|
|
15
|
+
* Hermetic temp-HOME isolation: every test uses fs.mkdtempSync for HOME and
|
|
16
|
+
* cleans up after. process.env.HOME is restored on cleanup.
|
|
17
|
+
*
|
|
18
|
+
* Pure CJS, node built-ins only.
|
|
19
|
+
*/
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const test = require('node:test');
|
|
23
|
+
const assert = require('node:assert/strict');
|
|
24
|
+
const fs = require('node:fs');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
const os = require('node:os');
|
|
27
|
+
const { execFileSync } = require('node:child_process');
|
|
28
|
+
|
|
29
|
+
const PLUGIN_ROOT = path.resolve(__dirname, '..', '..');
|
|
30
|
+
const ROUTER_PATH = path.join(PLUGIN_ROOT, 'lib', 'core', 'mva-option-router.cjs');
|
|
31
|
+
const SKILL_PATH = path.join(PLUGIN_ROOT, 'skills', 'mva-pipeline', 'SKILL.md');
|
|
32
|
+
const COMMAND_PATH = path.join(PLUGIN_ROOT, 'commands', 'mva-option.md');
|
|
33
|
+
|
|
34
|
+
// ---------- Test fixtures ----------
|
|
35
|
+
|
|
36
|
+
const FIXTURE_SHA8 = 'ab3c1234';
|
|
37
|
+
const FIXTURE_SHA256 = FIXTURE_SHA8 + 'a'.repeat(56); // 64 chars total
|
|
38
|
+
|
|
39
|
+
function makeTempHome() {
|
|
40
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'mva-router-home-'));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function cleanupHome(homeDir, savedHome) {
|
|
44
|
+
if (savedHome === undefined) {
|
|
45
|
+
delete process.env.HOME;
|
|
46
|
+
} else {
|
|
47
|
+
process.env.HOME = savedHome;
|
|
48
|
+
}
|
|
49
|
+
try { fs.rmSync(homeDir, { recursive: true, force: true }); } catch (_) {}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function writeSideFile(homeDir, sha8, sha256, extra) {
|
|
53
|
+
const dir = path.join(homeDir, '.mindrian', 'mva', 'briefs');
|
|
54
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
55
|
+
const body = Object.assign({
|
|
56
|
+
sha256: sha256,
|
|
57
|
+
sha8: sha8,
|
|
58
|
+
timestamp: new Date().toISOString(),
|
|
59
|
+
results: [
|
|
60
|
+
{ agent_id: 'brain_similar', status: 'ok', payload: { summary: 'three precedents found' } },
|
|
61
|
+
],
|
|
62
|
+
}, extra || {});
|
|
63
|
+
fs.writeFileSync(path.join(dir, sha8 + '.json'), JSON.stringify(body), 'utf8');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function writeStateJson(homeDir, sha8, sha256, renderedAtMs, vercelUrl) {
|
|
67
|
+
const dir = path.join(homeDir, '.mindrian', 'mva');
|
|
68
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
69
|
+
fs.writeFileSync(path.join(dir, 'state.json'), JSON.stringify({
|
|
70
|
+
current_sha8: sha8,
|
|
71
|
+
current_sha256: sha256,
|
|
72
|
+
rendered_at_ms: renderedAtMs,
|
|
73
|
+
vercel_url: vercelUrl === undefined ? null : vercelUrl,
|
|
74
|
+
}), 'utf8');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function writeBriefRenderedEvent(homeDir, sha256, renderedAtIso) {
|
|
78
|
+
const dir = path.join(homeDir, '.mindrian', 'telemetry', 'v1.13');
|
|
79
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
80
|
+
const line = JSON.stringify({
|
|
81
|
+
event: 'mva_brief_rendered',
|
|
82
|
+
timestamp: renderedAtIso || new Date().toISOString(),
|
|
83
|
+
session_id: 'default',
|
|
84
|
+
sentence_sha256: sha256,
|
|
85
|
+
total_duration_ms: 12000,
|
|
86
|
+
agent_count_ok: 5,
|
|
87
|
+
agent_count_failed: 1,
|
|
88
|
+
}) + '\n';
|
|
89
|
+
fs.appendFileSync(path.join(dir, 'mva.jsonl'), line, 'utf8');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readTelemetryLines(homeDir) {
|
|
93
|
+
const p = path.join(homeDir, '.mindrian', 'telemetry', 'v1.13', 'mva.jsonl');
|
|
94
|
+
if (!fs.existsSync(p)) return [];
|
|
95
|
+
return fs.readFileSync(p, 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function freshRequire(modulePath) {
|
|
99
|
+
delete require.cache[require.resolve(modulePath)];
|
|
100
|
+
// Also reset the telemetry module (it caches paths via env at call time but
|
|
101
|
+
// the module itself is stateless; still flush for hygiene).
|
|
102
|
+
try { delete require.cache[require.resolve(path.join(PLUGIN_ROOT, 'lib', 'core', 'mva-telemetry.cjs'))]; } catch (_) {}
|
|
103
|
+
return require(modulePath);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------- Test 1: routeOption(1) -> stay_in_just_talk ----------
|
|
107
|
+
|
|
108
|
+
test('Test 1: routeOption(1, sha8) returns stay_in_just_talk + emits telemetry', async () => {
|
|
109
|
+
const home = makeTempHome();
|
|
110
|
+
const savedHome = process.env.HOME;
|
|
111
|
+
process.env.HOME = home;
|
|
112
|
+
try {
|
|
113
|
+
writeSideFile(home, FIXTURE_SHA8, FIXTURE_SHA256);
|
|
114
|
+
const renderedAt = new Date(Date.now() - 1500).toISOString();
|
|
115
|
+
writeBriefRenderedEvent(home, FIXTURE_SHA256, renderedAt);
|
|
116
|
+
|
|
117
|
+
const router = freshRequire(ROUTER_PATH);
|
|
118
|
+
const result = await router.routeOption(1, FIXTURE_SHA8, { roomDir: home });
|
|
119
|
+
|
|
120
|
+
assert.equal(result.ok, true);
|
|
121
|
+
assert.equal(result.action, 'stay_in_just_talk');
|
|
122
|
+
assert.equal(result.next_state, 'JUST_TALK');
|
|
123
|
+
assert.ok(typeof result.message === 'string' && result.message.length > 0);
|
|
124
|
+
assert.ok(result.time_to_click_ms >= 1000 && result.time_to_click_ms < 60000);
|
|
125
|
+
|
|
126
|
+
const events = readTelemetryLines(home);
|
|
127
|
+
const opt = events.find(e => e.event === 'mva_option_selected');
|
|
128
|
+
assert.ok(opt, 'mva_option_selected event must be emitted');
|
|
129
|
+
assert.equal(opt.sentence_sha256, FIXTURE_SHA256);
|
|
130
|
+
assert.equal(opt.option_id, 1);
|
|
131
|
+
assert.ok(typeof opt.time_to_click_ms === 'number');
|
|
132
|
+
} finally {
|
|
133
|
+
cleanupHome(home, savedHome);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ---------- Test 2: routeOption(2) -> phase_119_stub ----------
|
|
138
|
+
|
|
139
|
+
test('Test 2: routeOption(2, sha8) returns phase_119_stub + STUB_MESSAGE_119 + telemetry', async () => {
|
|
140
|
+
const home = makeTempHome();
|
|
141
|
+
const savedHome = process.env.HOME;
|
|
142
|
+
process.env.HOME = home;
|
|
143
|
+
try {
|
|
144
|
+
writeSideFile(home, FIXTURE_SHA8, FIXTURE_SHA256);
|
|
145
|
+
writeBriefRenderedEvent(home, FIXTURE_SHA256, new Date(Date.now() - 500).toISOString());
|
|
146
|
+
|
|
147
|
+
const router = freshRequire(ROUTER_PATH);
|
|
148
|
+
const result = await router.routeOption(2, FIXTURE_SHA8, { roomDir: home });
|
|
149
|
+
|
|
150
|
+
assert.equal(result.ok, true);
|
|
151
|
+
assert.equal(result.action, 'phase_119_stub');
|
|
152
|
+
assert.equal(result.message, router.STUB_MESSAGE_119, 'message must be the verbatim stub');
|
|
153
|
+
assert.ok(router.STUB_MESSAGE_119.includes('Phase 119'), 'stub must mention Phase 119');
|
|
154
|
+
assert.ok(router.STUB_MESSAGE_119.includes('beta.18'), 'stub must mention beta.18');
|
|
155
|
+
|
|
156
|
+
const events = readTelemetryLines(home);
|
|
157
|
+
const opt = events.find(e => e.event === 'mva_option_selected');
|
|
158
|
+
assert.ok(opt, 'option-2 stub still emits telemetry (OQ18)');
|
|
159
|
+
assert.equal(opt.option_id, 2);
|
|
160
|
+
} finally {
|
|
161
|
+
cleanupHome(home, savedHome);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ---------- Test 3: routeOption(3) -> invoke_challenge_assumptions ----------
|
|
166
|
+
|
|
167
|
+
test('Test 3: routeOption(3, sha8) returns invoke_challenge_assumptions + METHODOLOGY transition', async () => {
|
|
168
|
+
const home = makeTempHome();
|
|
169
|
+
const savedHome = process.env.HOME;
|
|
170
|
+
process.env.HOME = home;
|
|
171
|
+
try {
|
|
172
|
+
writeSideFile(home, FIXTURE_SHA8, FIXTURE_SHA256);
|
|
173
|
+
writeBriefRenderedEvent(home, FIXTURE_SHA256, new Date(Date.now() - 800).toISOString());
|
|
174
|
+
|
|
175
|
+
const router = freshRequire(ROUTER_PATH);
|
|
176
|
+
const result = await router.routeOption(3, FIXTURE_SHA8, { roomDir: home });
|
|
177
|
+
|
|
178
|
+
assert.equal(result.ok, true);
|
|
179
|
+
assert.equal(result.action, 'invoke_challenge_assumptions');
|
|
180
|
+
assert.equal(result.next_state, 'METHODOLOGY');
|
|
181
|
+
assert.equal(result.invoke_command, '/mos:challenge-assumptions --from-brief ' + FIXTURE_SHA8);
|
|
182
|
+
|
|
183
|
+
const events = readTelemetryLines(home);
|
|
184
|
+
const opt = events.find(e => e.event === 'mva_option_selected');
|
|
185
|
+
assert.ok(opt);
|
|
186
|
+
assert.equal(opt.option_id, 3);
|
|
187
|
+
} finally {
|
|
188
|
+
cleanupHome(home, savedHome);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ---------- Test 4: missing side-file -> brief_not_found, no telemetry ----------
|
|
193
|
+
|
|
194
|
+
test('Test 4: missing side-file -> brief_not_found error, no telemetry emitted', async () => {
|
|
195
|
+
const home = makeTempHome();
|
|
196
|
+
const savedHome = process.env.HOME;
|
|
197
|
+
process.env.HOME = home;
|
|
198
|
+
try {
|
|
199
|
+
// Side-file is intentionally NOT created. Brief-rendered event also absent.
|
|
200
|
+
const router = freshRequire(ROUTER_PATH);
|
|
201
|
+
const result = await router.routeOption(1, 'deadbeef', { roomDir: home });
|
|
202
|
+
|
|
203
|
+
assert.equal(result.ok, false);
|
|
204
|
+
assert.equal(result.error, 'brief_not_found');
|
|
205
|
+
assert.ok(typeof result.message === 'string' && result.message.length > 0);
|
|
206
|
+
|
|
207
|
+
const events = readTelemetryLines(home);
|
|
208
|
+
const optEvents = events.filter(e => e.event === 'mva_option_selected');
|
|
209
|
+
assert.equal(optEvents.length, 0, 'no telemetry on error path');
|
|
210
|
+
} finally {
|
|
211
|
+
cleanupHome(home, savedHome);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ---------- Test 5: brief still rendering -> friendly message ----------
|
|
216
|
+
|
|
217
|
+
test('Test 5: side-file present but no mva_brief_rendered event -> brief_still_rendering', async () => {
|
|
218
|
+
const home = makeTempHome();
|
|
219
|
+
const savedHome = process.env.HOME;
|
|
220
|
+
process.env.HOME = home;
|
|
221
|
+
try {
|
|
222
|
+
writeSideFile(home, FIXTURE_SHA8, FIXTURE_SHA256);
|
|
223
|
+
// Do NOT write the mva_brief_rendered event (the pipeline is still streaming).
|
|
224
|
+
|
|
225
|
+
const router = freshRequire(ROUTER_PATH);
|
|
226
|
+
const result = await router.routeOption(1, FIXTURE_SHA8, { roomDir: home });
|
|
227
|
+
|
|
228
|
+
assert.equal(result.ok, false);
|
|
229
|
+
assert.equal(result.error, 'brief_still_rendering');
|
|
230
|
+
assert.match(result.message, /still rendering/);
|
|
231
|
+
|
|
232
|
+
const events = readTelemetryLines(home);
|
|
233
|
+
const optEvents = events.filter(e => e.event === 'mva_option_selected');
|
|
234
|
+
assert.equal(optEvents.length, 0, 'no telemetry while rendering');
|
|
235
|
+
} finally {
|
|
236
|
+
cleanupHome(home, savedHome);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ---------- Test 6: time_to_click_ms computed from mva_brief_rendered ts ----------
|
|
241
|
+
|
|
242
|
+
test('Test 6: time_to_click_ms is approximately (now - mva_brief_rendered.timestamp)', async () => {
|
|
243
|
+
const home = makeTempHome();
|
|
244
|
+
const savedHome = process.env.HOME;
|
|
245
|
+
process.env.HOME = home;
|
|
246
|
+
try {
|
|
247
|
+
writeSideFile(home, FIXTURE_SHA8, FIXTURE_SHA256);
|
|
248
|
+
// Brief rendered 5 seconds ago.
|
|
249
|
+
const renderedAt = new Date(Date.now() - 5000).toISOString();
|
|
250
|
+
writeBriefRenderedEvent(home, FIXTURE_SHA256, renderedAt);
|
|
251
|
+
|
|
252
|
+
const router = freshRequire(ROUTER_PATH);
|
|
253
|
+
const result = await router.routeOption(1, FIXTURE_SHA8, { roomDir: home });
|
|
254
|
+
|
|
255
|
+
assert.equal(result.ok, true);
|
|
256
|
+
// Allow generous slack: 4500..7000ms
|
|
257
|
+
assert.ok(result.time_to_click_ms >= 4500 && result.time_to_click_ms <= 7000,
|
|
258
|
+
'time_to_click_ms should be ~5000, got: ' + result.time_to_click_ms);
|
|
259
|
+
|
|
260
|
+
const events = readTelemetryLines(home);
|
|
261
|
+
const opt = events.find(e => e.event === 'mva_option_selected');
|
|
262
|
+
assert.ok(opt.time_to_click_ms >= 4500 && opt.time_to_click_ms <= 7000);
|
|
263
|
+
} finally {
|
|
264
|
+
cleanupHome(home, savedHome);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// ---------- Test 7: em-dash sweep on rendered messages ----------
|
|
269
|
+
|
|
270
|
+
test('Test 7: STUB_MESSAGE_119 + OPTION_BEHAVIOR narratives are em-dash-free', () => {
|
|
271
|
+
const router = require(ROUTER_PATH);
|
|
272
|
+
const emDash = '—';
|
|
273
|
+
|
|
274
|
+
assert.ok(!router.STUB_MESSAGE_119.includes(emDash), 'STUB_MESSAGE_119 must not contain em-dash');
|
|
275
|
+
for (const key of [1, 2, 3]) {
|
|
276
|
+
const b = router.OPTION_BEHAVIOR[key];
|
|
277
|
+
assert.ok(!b.narrative.includes(emDash),
|
|
278
|
+
'OPTION_BEHAVIOR[' + key + '].narrative must not contain em-dash');
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// ---------- Test 8: invalid option (99) -> no state transition, no telemetry ----------
|
|
283
|
+
|
|
284
|
+
test('Test 8: routeOption(99) returns invalid_option, no telemetry, no state change', async () => {
|
|
285
|
+
const home = makeTempHome();
|
|
286
|
+
const savedHome = process.env.HOME;
|
|
287
|
+
process.env.HOME = home;
|
|
288
|
+
try {
|
|
289
|
+
writeSideFile(home, FIXTURE_SHA8, FIXTURE_SHA256);
|
|
290
|
+
writeBriefRenderedEvent(home, FIXTURE_SHA256, new Date(Date.now() - 100).toISOString());
|
|
291
|
+
|
|
292
|
+
const router = freshRequire(ROUTER_PATH);
|
|
293
|
+
const result = await router.routeOption(99, FIXTURE_SHA8, { roomDir: home });
|
|
294
|
+
|
|
295
|
+
assert.equal(result.ok, false);
|
|
296
|
+
assert.equal(result.error, 'invalid_option');
|
|
297
|
+
|
|
298
|
+
const events = readTelemetryLines(home);
|
|
299
|
+
const optEvents = events.filter(e => e.event === 'mva_option_selected');
|
|
300
|
+
assert.equal(optEvents.length, 0);
|
|
301
|
+
} finally {
|
|
302
|
+
cleanupHome(home, savedHome);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// ---------- Test 9: OPTION_BEHAVIOR is a frozen object documenting each contract ----------
|
|
307
|
+
|
|
308
|
+
test('Test 9: OPTION_BEHAVIOR is Object.frozen with the 3 option contracts', () => {
|
|
309
|
+
const router = require(ROUTER_PATH);
|
|
310
|
+
assert.ok(Object.isFrozen(router.OPTION_BEHAVIOR));
|
|
311
|
+
for (const id of [1, 2, 3]) {
|
|
312
|
+
assert.ok(router.OPTION_BEHAVIOR[id], 'OPTION_BEHAVIOR[' + id + '] missing');
|
|
313
|
+
assert.ok(typeof router.OPTION_BEHAVIOR[id].action === 'string');
|
|
314
|
+
assert.ok(typeof router.OPTION_BEHAVIOR[id].narrative === 'string');
|
|
315
|
+
}
|
|
316
|
+
assert.equal(router.OPTION_BEHAVIOR[1].action, 'stay_in_just_talk');
|
|
317
|
+
assert.equal(router.OPTION_BEHAVIOR[2].action, 'phase_119_stub');
|
|
318
|
+
assert.equal(router.OPTION_BEHAVIOR[3].action, 'invoke_challenge_assumptions');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// ---------- Test 10: resolveCurrentSha8 reads ~/.mindrian/mva/state.json (CRITICAL-3 part 2) ----------
|
|
322
|
+
|
|
323
|
+
test('Test 10: resolveCurrentSha8 reads state.json + integration with routeOption(2) no-arg flow', async () => {
|
|
324
|
+
const home = makeTempHome();
|
|
325
|
+
const savedHome = process.env.HOME;
|
|
326
|
+
process.env.HOME = home;
|
|
327
|
+
try {
|
|
328
|
+
writeStateJson(home, FIXTURE_SHA8, FIXTURE_SHA256, Date.now() - 2000, 'https://mos-brief-ab3c1234.vercel.app');
|
|
329
|
+
writeSideFile(home, FIXTURE_SHA8, FIXTURE_SHA256);
|
|
330
|
+
writeBriefRenderedEvent(home, FIXTURE_SHA256, new Date(Date.now() - 2000).toISOString());
|
|
331
|
+
|
|
332
|
+
const router = freshRequire(ROUTER_PATH);
|
|
333
|
+
const sha8 = router.resolveCurrentSha8();
|
|
334
|
+
assert.equal(sha8, FIXTURE_SHA8);
|
|
335
|
+
|
|
336
|
+
// Integration: simulate /mos:mva-option 2 with no explicit sha.
|
|
337
|
+
const result = await router.routeOption(2, sha8, { roomDir: home });
|
|
338
|
+
assert.equal(result.ok, true);
|
|
339
|
+
assert.equal(result.action, 'phase_119_stub');
|
|
340
|
+
|
|
341
|
+
const events = readTelemetryLines(home);
|
|
342
|
+
const opt = events.find(e => e.event === 'mva_option_selected');
|
|
343
|
+
assert.ok(opt);
|
|
344
|
+
assert.equal(opt.sentence_sha256, FIXTURE_SHA256, 'telemetry carries resolved sha256 from state.json');
|
|
345
|
+
} finally {
|
|
346
|
+
cleanupHome(home, savedHome);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// ---------- Test 11: resolveCurrentSha8 returns null when state.json absent ----------
|
|
351
|
+
|
|
352
|
+
test('Test 11: resolveCurrentSha8 returns null on missing state.json (fresh-install / Hebrew refusal)', () => {
|
|
353
|
+
const home = makeTempHome();
|
|
354
|
+
const savedHome = process.env.HOME;
|
|
355
|
+
process.env.HOME = home;
|
|
356
|
+
try {
|
|
357
|
+
// No state.json written.
|
|
358
|
+
const router = freshRequire(ROUTER_PATH);
|
|
359
|
+
const sha8 = router.resolveCurrentSha8();
|
|
360
|
+
assert.equal(sha8, null);
|
|
361
|
+
} finally {
|
|
362
|
+
cleanupHome(home, savedHome);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// ---------- Test 12: resolveCurrentSha8 staleness pass-through (returns sha8 regardless of age) ----------
|
|
367
|
+
|
|
368
|
+
test('Test 12: resolveCurrentSha8 returns sha8 even when rendered_at_ms is old (staleness is router-level concern)', () => {
|
|
369
|
+
const home = makeTempHome();
|
|
370
|
+
const savedHome = process.env.HOME;
|
|
371
|
+
process.env.HOME = home;
|
|
372
|
+
try {
|
|
373
|
+
// rendered_at_ms = 1 hour ago (stale by any reasonable threshold)
|
|
374
|
+
const oneHourAgo = Date.now() - 60 * 60 * 1000;
|
|
375
|
+
writeStateJson(home, FIXTURE_SHA8, FIXTURE_SHA256, oneHourAgo, null);
|
|
376
|
+
|
|
377
|
+
const router = freshRequire(ROUTER_PATH);
|
|
378
|
+
const sha8 = router.resolveCurrentSha8();
|
|
379
|
+
assert.equal(sha8, FIXTURE_SHA8, 'resolveCurrentSha8 returns sha8 regardless of age; staleness checked downstream');
|
|
380
|
+
} finally {
|
|
381
|
+
cleanupHome(home, savedHome);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// ---------- Test 13: commands/mva-option.md frontmatter ----------
|
|
386
|
+
|
|
387
|
+
test('Test 13: commands/mva-option.md exists with required frontmatter', () => {
|
|
388
|
+
assert.ok(fs.existsSync(COMMAND_PATH), 'commands/mva-option.md must exist');
|
|
389
|
+
const body = fs.readFileSync(COMMAND_PATH, 'utf8');
|
|
390
|
+
assert.match(body, /^---[\s\S]*?name:\s*mva-option/m);
|
|
391
|
+
assert.match(body, /argument-hint:\s*<1\|2\|3>\s*\[<sha8>\]/);
|
|
392
|
+
assert.match(body, /allowed-tools:\s*Bash/);
|
|
393
|
+
assert.match(body, /interactive_first_reward:\s*--none/);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// ---------- Test 14: command body documents resolveCurrentSha8 auto-discovery ----------
|
|
397
|
+
|
|
398
|
+
test('Test 14: commands/mva-option.md body documents the resolveCurrentSha8 auto-discovery path', () => {
|
|
399
|
+
const body = fs.readFileSync(COMMAND_PATH, 'utf8');
|
|
400
|
+
assert.match(body, /resolveCurrentSha8/);
|
|
401
|
+
assert.match(body, /state\.json/);
|
|
402
|
+
// Auto-resolution explanation must appear.
|
|
403
|
+
assert.match(body, /auto-discover|auto-resolve|omitted/i);
|
|
404
|
+
// Must reference the router module.
|
|
405
|
+
assert.match(body, /mva-option-router/);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// ---------- Test 15: SKILL.md has the routing section ----------
|
|
409
|
+
|
|
410
|
+
test('Test 15: skills/mva-pipeline/SKILL.md has "Routing the 3-option footer" section', () => {
|
|
411
|
+
assert.ok(fs.existsSync(SKILL_PATH));
|
|
412
|
+
const body = fs.readFileSync(SKILL_PATH, 'utf8');
|
|
413
|
+
assert.match(body, /## Routing the 3-option footer/);
|
|
414
|
+
// Must list the 3 options + the recognition rule
|
|
415
|
+
assert.match(body, /Option 1.*[Jj]ust tell me/);
|
|
416
|
+
assert.match(body, /Option 2.*Build a room|invest/);
|
|
417
|
+
assert.match(body, /Option 3.*Challenge me|Devil['']s Advocate/);
|
|
418
|
+
// Must reference mva-option command
|
|
419
|
+
assert.match(body, /mva-option/);
|
|
420
|
+
// Must document brief-still-rendering edge case
|
|
421
|
+
assert.match(body, /still rendering/);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// ---------- Test 16: em-dash sweep on both files ----------
|
|
425
|
+
|
|
426
|
+
test('Test 16: commands/mva-option.md + skills/mva-pipeline/SKILL.md are em-dash-free', () => {
|
|
427
|
+
const emDash = '—';
|
|
428
|
+
const cmd = fs.readFileSync(COMMAND_PATH, 'utf8');
|
|
429
|
+
const skill = fs.readFileSync(SKILL_PATH, 'utf8');
|
|
430
|
+
assert.ok(!cmd.includes(emDash), 'commands/mva-option.md must not contain em-dash');
|
|
431
|
+
assert.ok(!skill.includes(emDash), 'skills/mva-pipeline/SKILL.md must not contain em-dash');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// ---------- Test 17: E2E -- /mos:mva-option 2 with no sha arg via documented Bash invocation ----------
|
|
435
|
+
|
|
436
|
+
test('Test 17: end-to-end -- routeOption(2) without explicit sha auto-resolves from state.json', async () => {
|
|
437
|
+
const home = makeTempHome();
|
|
438
|
+
const savedHome = process.env.HOME;
|
|
439
|
+
process.env.HOME = home;
|
|
440
|
+
try {
|
|
441
|
+
// Setup as if Plan 118-03 + 118-04 had just run:
|
|
442
|
+
// - state.json written by 118-03 orchestrator
|
|
443
|
+
// - side-file written by 118-04 deck builder
|
|
444
|
+
// - mva_brief_rendered event emitted by 118-03 telemetry
|
|
445
|
+
writeStateJson(home, FIXTURE_SHA8, FIXTURE_SHA256, Date.now() - 3000, 'https://mos-brief-ab3c1234.vercel.app');
|
|
446
|
+
writeSideFile(home, FIXTURE_SHA8, FIXTURE_SHA256);
|
|
447
|
+
writeBriefRenderedEvent(home, FIXTURE_SHA256, new Date(Date.now() - 3000).toISOString());
|
|
448
|
+
|
|
449
|
+
const router = freshRequire(ROUTER_PATH);
|
|
450
|
+
|
|
451
|
+
// The wrapper pattern documented in commands/mva-option.md when sha8 omitted:
|
|
452
|
+
// const sha = r.resolveCurrentSha8();
|
|
453
|
+
// if (!sha) { surface no_current_brief }
|
|
454
|
+
// else r.routeOption(N, sha)
|
|
455
|
+
const sha = router.resolveCurrentSha8();
|
|
456
|
+
assert.ok(sha, 'resolveCurrentSha8 must succeed in E2E setup');
|
|
457
|
+
|
|
458
|
+
const result = await router.routeOption(2, sha, { roomDir: home });
|
|
459
|
+
assert.equal(result.ok, true);
|
|
460
|
+
assert.equal(result.action, 'phase_119_stub');
|
|
461
|
+
|
|
462
|
+
const events = readTelemetryLines(home);
|
|
463
|
+
const opt = events.find(e => e.event === 'mva_option_selected');
|
|
464
|
+
assert.ok(opt, 'E2E flow must emit mva_option_selected telemetry');
|
|
465
|
+
assert.equal(opt.option_id, 2);
|
|
466
|
+
assert.equal(opt.sentence_sha256, FIXTURE_SHA256);
|
|
467
|
+
assert.ok(typeof opt.time_to_click_ms === 'number');
|
|
468
|
+
assert.ok(opt.time_to_click_ms >= 2500 && opt.time_to_click_ms <= 5000,
|
|
469
|
+
'E2E time_to_click_ms should be ~3000, got: ' + opt.time_to_click_ms);
|
|
470
|
+
} finally {
|
|
471
|
+
cleanupHome(home, savedHome);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// ---------- Canon Part 8 source-sweep ----------
|
|
476
|
+
|
|
477
|
+
test('Canon Part 8: forbidden-token sweep on router source (no raw_sentence / MVA_SENTENCE)', () => {
|
|
478
|
+
const src = fs.readFileSync(ROUTER_PATH, 'utf8');
|
|
479
|
+
// Strip comments first so we can document forbidden patterns in JSDoc.
|
|
480
|
+
const stripped = src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '');
|
|
481
|
+
assert.ok(!/raw_sentence/.test(stripped), 'router source must not reference raw_sentence');
|
|
482
|
+
assert.ok(!/MVA_SENTENCE/.test(stripped), 'router source must not reference MVA_SENTENCE');
|
|
483
|
+
});
|