@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.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +36 -0
- package/commands/act.md +1 -0
- package/commands/admin.md +1 -0
- package/commands/analyze-needs.md +2 -0
- package/commands/analyze-systems.md +2 -0
- package/commands/analyze-timing.md +2 -0
- package/commands/auto-explore.md +2 -0
- package/commands/beautiful-question.md +2 -0
- package/commands/brain-derive.md +2 -0
- package/commands/build-knowledge.md +2 -0
- package/commands/build-thesis.md +2 -0
- package/commands/causal.md +2 -0
- package/commands/challenge-assumptions.md +2 -0
- package/commands/compare-ventures.md +2 -0
- package/commands/dashboard.md +2 -1
- package/commands/deep-grade.md +2 -0
- package/commands/diagnose.md +21 -1
- package/commands/diagnostics.md +14 -3
- package/commands/doctor.md +4 -1
- package/commands/dogfood-flush.md +92 -0
- package/commands/dominant-designs.md +2 -0
- package/commands/explain-decision.md +2 -0
- package/commands/explore-domains.md +2 -0
- package/commands/explore-futures.md +2 -0
- package/commands/explore-trends.md +2 -0
- package/commands/export.md +1 -0
- package/commands/feynman-timeline-refresh.md +2 -0
- package/commands/file-meeting.md +4 -0
- package/commands/find-analogies.md +1 -0
- package/commands/find-bottlenecks.md +2 -0
- package/commands/find-connections.md +2 -0
- package/commands/funding.md +1 -0
- package/commands/grade.md +4 -0
- package/commands/graph.md +1 -0
- package/commands/hat-briefing.md +1 -0
- package/commands/heal.md +22 -170
- package/commands/help.md +54 -334
- package/commands/hmi-status.md +23 -144
- package/commands/jtbd.md +1 -0
- package/commands/leadership.md +2 -0
- package/commands/lean-canvas.md +2 -0
- package/commands/macro-trends.md +2 -0
- package/commands/map-unknowns.md +2 -0
- package/commands/memory.md +1 -0
- package/commands/models.md +1 -0
- package/commands/mos-reason.md +2 -0
- package/commands/mos.md +139 -0
- package/commands/mullins.md +2 -0
- package/commands/mva-brief.md +58 -0
- package/commands/mva-option.md +91 -0
- package/commands/new-project.md +4 -0
- package/commands/onboard.md +22 -7
- package/commands/operator.md +1 -0
- package/commands/opportunities.md +1 -0
- package/commands/organize.md +22 -469
- package/commands/persona.md +1 -0
- package/commands/pipeline.md +2 -0
- package/commands/present.md +1 -0
- package/commands/publish.md +2 -0
- package/commands/query.md +24 -102
- package/commands/radar.md +2 -0
- package/commands/reanalyze.md +1 -0
- package/commands/research.md +2 -0
- package/commands/room.md +2 -0
- package/commands/rooms.md +1 -0
- package/commands/root-cause.md +2 -0
- package/commands/rs-experts.md +1 -0
- package/commands/rs-explain.md +1 -0
- package/commands/rs-fetch.md +1 -0
- package/commands/rs-thesis.md +1 -0
- package/commands/scenario-plan.md +2 -0
- package/commands/scheduled-tasks.md +1 -0
- package/commands/score-innovation.md +2 -0
- package/commands/scout.md +1 -0
- package/commands/setup.md +2 -0
- package/commands/snapshot.md +2 -0
- package/commands/speakers.md +1 -0
- package/commands/splash.md +5 -2
- package/commands/status.md +1 -0
- package/commands/structure-argument.md +2 -0
- package/commands/suggest-next.md +2 -0
- package/commands/systems-thinking.md +2 -0
- package/commands/think-hats.md +2 -0
- package/commands/update.md +2 -0
- package/commands/user-needs.md +2 -0
- package/commands/validate.md +2 -0
- package/commands/value-proposition.md +2 -0
- package/commands/vault.md +2 -0
- package/commands/visualize.md +24 -29
- package/commands/whitespace.md +2 -1
- package/commands/wiki.md +1 -0
- package/hooks/hooks.json +31 -88
- package/lib/agents/auto-explore-agent.cjs +82 -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/breakthrough/canary.cjs +134 -0
- package/lib/core/breakthrough/canary.test.cjs +136 -0
- package/lib/core/breakthrough/detectors.cjs +359 -0
- package/lib/core/breakthrough/detectors.test.cjs +333 -0
- package/lib/core/breakthrough/ethics-fence.cjs +127 -0
- package/lib/core/breakthrough/ethics-fence.test.cjs +178 -0
- package/lib/core/breakthrough/resurfacing.cjs +150 -0
- package/lib/core/breakthrough/resurfacing.test.cjs +233 -0
- package/lib/core/breakthrough/review-queue.cjs +154 -0
- package/lib/core/breakthrough/review-queue.test.cjs +160 -0
- package/lib/core/breakthrough/scanner-d17-d18.test.cjs +229 -0
- package/lib/core/breakthrough/scanner.cjs +426 -0
- package/lib/core/breakthrough/scanner.test.cjs +267 -0
- package/lib/core/breakthrough/schema.cjs +164 -0
- package/lib/core/breakthrough/schema.test.cjs +256 -0
- package/lib/core/breakthrough/scoring.cjs +293 -0
- package/lib/core/breakthrough/scoring.test.cjs +423 -0
- package/lib/core/breakthrough/verb-dispatch.cjs +221 -0
- package/lib/core/breakthrough/verb-dispatch.test.cjs +185 -0
- package/lib/core/breakthrough/voice-scaffold.cjs +247 -0
- package/lib/core/breakthrough/voice-scaffold.test.cjs +251 -0
- package/lib/core/first-touch-version-stamper.cjs +113 -0
- package/lib/core/larry-thinness-acknowledgment.cjs +64 -0
- package/lib/core/larry-thinness-acknowledgment.test.cjs +97 -0
- package/lib/core/llm-name-suggester.cjs +194 -0
- package/lib/core/llm-name-suggester.test.cjs +132 -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 +365 -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 +58 -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/edges.cjs +35 -0
- package/lib/core/navigation/memory-events.cjs +126 -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/core/room-auto-create.cjs +318 -0
- package/lib/core/room-auto-create.test.cjs +198 -0
- package/lib/core/room-discard-cascade.cjs +225 -0
- package/lib/core/room-discard-cascade.test.cjs +135 -0
- package/lib/core/room-name-validator.cjs +132 -0
- package/lib/core/room-name-validator.test.cjs +156 -0
- package/lib/core/room-naming-selector.cjs +357 -0
- package/lib/core/room-naming-selector.test.cjs +277 -0
- package/lib/core/room-receipt-emit.cjs +63 -0
- package/lib/core/room-skeleton-scaffold.cjs +315 -0
- package/lib/core/room-skeleton-scaffold.test.cjs +291 -0
- package/lib/core/stale-copy-scanner.cjs +190 -0
- package/lib/core/state-aware-router.cjs +78 -0
- package/lib/core/telemetry/schema.cjs +168 -0
- package/lib/core/telemetry/schema.test.cjs +124 -0
- package/lib/core/telemetry/validator.cjs +197 -0
- package/lib/core/telemetry/validator.test.cjs +188 -0
- package/lib/core/telemetry/writer.cjs +141 -0
- package/lib/core/telemetry/writer.test.cjs +331 -0
- package/lib/core/terminal-capability.cjs +88 -0
- package/lib/core/venture-shape-nudge.cjs +163 -0
- package/lib/core/venture-shape-nudge.test.cjs +161 -0
- package/lib/core/visual-ops.cjs +70 -2
- package/lib/hmi/selector-dispatcher.cjs +90 -1
- package/lib/hmi/shape-f7-breakthrough-renderer.cjs +222 -0
- package/lib/hmi/shape-f7-breakthrough-renderer.test.cjs +233 -0
- package/lib/memory/body-shape-coverage.test.cjs +268 -0
- package/lib/memory/doctor-deprecation-surface.test.cjs +185 -0
- package/lib/memory/first-touch-version.test.cjs +198 -0
- package/lib/memory/help-coverage.test.cjs +108 -0
- package/lib/memory/help-renderer.test.cjs +145 -0
- package/lib/memory/palette-consistency.test.cjs +127 -0
- package/lib/memory/pending-tension-store.cjs +80 -0
- package/lib/memory/render-v2-disposition.test.cjs +199 -0
- package/lib/memory/run-feynman-tests.cjs +240 -0
- package/lib/memory/sessionstart-coordinator.test.cjs +446 -0
- package/lib/memory/skill-vs-code-drift.test.cjs +257 -0
- package/lib/memory/soft-alias.test.cjs +144 -0
- package/lib/memory/stale-copy-scanner.test.cjs +291 -0
- package/lib/memory/state-aware-router.test.cjs +90 -0
- package/lib/memory/statusline-two-row.test.cjs +338 -0
- package/lib/memory/terminal-capability.test.cjs +155 -0
- package/lib/render/ROOM.md +74 -22
- package/lib/sessionstart/budget-compressor.cjs +130 -0
- package/lib/sessionstart/contributor-interface.cjs +134 -0
- package/lib/sessionstart/contributor-isolator.cjs +128 -0
- package/lib/sessionstart/precedence-ladder.cjs +47 -0
- package/lib/statusline/governing-thought-truncator.cjs +45 -0
- package/lib/statusline/two-row-renderer.cjs +186 -0
- package/lib/statusline/version-resolver.cjs +81 -0
- package/package.json +1 -1
- package/references/visual/ROOM.md +55 -0
- package/references/visual/palette.json +54 -0
- package/skills/larry-personality/SKILL.md +34 -0
- package/skills/mva-pipeline/SKILL.md +129 -0
- package/skills/ui-system/SKILL.md +109 -1
- package/skills/ui-system/rules/dual-palette.md +156 -0
- package/skills/ui-system/rules/glyph-disambiguation.md +171 -0
- package/skills/ui-system/rules/shape-f-zero-and-six.md +169 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
3
|
+
*
|
|
4
|
+
* Phase 118-03 Plan 03 Task 1 -- mva-telemetry tests.
|
|
5
|
+
*
|
|
6
|
+
* Verifies atomic JSONL append to ~/.mindrian/telemetry/v1.13/mva.jsonl,
|
|
7
|
+
* schema validation that rejects raw user content, per-event ALLOWED_FIELDS
|
|
8
|
+
* export with mva_brief_rendered carrying 'total_duration_ms' (NOT 'duration_ms'),
|
|
9
|
+
* and concurrent-emit safety.
|
|
10
|
+
*
|
|
11
|
+
* Pure CJS, node built-ins only. Run via `node --test`.
|
|
12
|
+
*/
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const test = require('node:test');
|
|
16
|
+
const assert = require('node:assert');
|
|
17
|
+
const fs = require('node:fs');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
const os = require('node:os');
|
|
20
|
+
const crypto = require('node:crypto');
|
|
21
|
+
|
|
22
|
+
// Use a hermetic temp HOME to isolate writes. Must be set BEFORE require so the
|
|
23
|
+
// module's paths resolve against the temp HOME at module-load time. Restored
|
|
24
|
+
// after each test via the cleanup helper.
|
|
25
|
+
|
|
26
|
+
function mkTmpHome() {
|
|
27
|
+
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'mva-telemetry-test-'));
|
|
28
|
+
return tmpHome;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function rmTmpHome(tmpHome) {
|
|
32
|
+
try { fs.rmSync(tmpHome, { recursive: true, force: true }); } catch (_e) {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Pre-set HOME so the require below resolves paths against the temp dir.
|
|
36
|
+
// Each test gets its own subdir via emit() path resolution (already env-aware).
|
|
37
|
+
|
|
38
|
+
function freshTelemetry() {
|
|
39
|
+
// Re-require with cleared cache so module constants resolve against the
|
|
40
|
+
// current process.env.HOME.
|
|
41
|
+
delete require.cache[require.resolve('./mva-telemetry.cjs')];
|
|
42
|
+
return require('./mva-telemetry.cjs');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const SHA256_SAMPLE = crypto.createHash('sha256').update('hello world').digest('hex');
|
|
46
|
+
|
|
47
|
+
test('telemetry Test 10 -- emit appends a valid JSON line to mva.jsonl', () => {
|
|
48
|
+
const tmpHome = mkTmpHome();
|
|
49
|
+
const prevHome = process.env.HOME;
|
|
50
|
+
process.env.HOME = tmpHome;
|
|
51
|
+
try {
|
|
52
|
+
const telemetry = freshTelemetry();
|
|
53
|
+
telemetry.emit('mva_pipeline_started', { sentence_sha256: SHA256_SAMPLE });
|
|
54
|
+
const jsonlPath = path.join(tmpHome, '.mindrian', 'telemetry', 'v1.13', 'mva.jsonl');
|
|
55
|
+
assert.ok(fs.existsSync(jsonlPath), 'jsonl file must exist');
|
|
56
|
+
const content = fs.readFileSync(jsonlPath, 'utf8');
|
|
57
|
+
const lines = content.split('\n').filter((l) => l.length > 0);
|
|
58
|
+
assert.strictEqual(lines.length, 1, 'must have exactly 1 line');
|
|
59
|
+
const parsed = JSON.parse(lines[0]);
|
|
60
|
+
assert.strictEqual(parsed.event, 'mva_pipeline_started');
|
|
61
|
+
assert.strictEqual(parsed.sentence_sha256, SHA256_SAMPLE);
|
|
62
|
+
assert.ok(typeof parsed.timestamp === 'string', 'must have ISO timestamp');
|
|
63
|
+
} finally {
|
|
64
|
+
process.env.HOME = prevHome;
|
|
65
|
+
rmTmpHome(tmpHome);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('telemetry Test 11 -- validateEventPayload rejects long string fields (raw content suspicion)', () => {
|
|
70
|
+
const tmpHome = mkTmpHome();
|
|
71
|
+
const prevHome = process.env.HOME;
|
|
72
|
+
process.env.HOME = tmpHome;
|
|
73
|
+
try {
|
|
74
|
+
const telemetry = freshTelemetry();
|
|
75
|
+
// error_short is explicitly allowed at <= 60 chars
|
|
76
|
+
const longErrorShort = 'a'.repeat(120);
|
|
77
|
+
const v1 = telemetry.validateEventPayload('mva_agent_returned', {
|
|
78
|
+
sentence_sha256: SHA256_SAMPLE,
|
|
79
|
+
agent_id: 'brain_similar',
|
|
80
|
+
duration_ms: 100,
|
|
81
|
+
status: 'error',
|
|
82
|
+
error_short: longErrorShort
|
|
83
|
+
});
|
|
84
|
+
assert.strictEqual(v1.ok, false, 'long error_short must be rejected');
|
|
85
|
+
|
|
86
|
+
// Bare string field over 64 chars (not error_short, not sha256)
|
|
87
|
+
const v2 = telemetry.validateEventPayload('mva_agent_returned', {
|
|
88
|
+
sentence_sha256: SHA256_SAMPLE,
|
|
89
|
+
agent_id: 'a'.repeat(120),
|
|
90
|
+
duration_ms: 100,
|
|
91
|
+
status: 'ok'
|
|
92
|
+
});
|
|
93
|
+
assert.strictEqual(v2.ok, false, 'long agent_id must be rejected');
|
|
94
|
+
} finally {
|
|
95
|
+
process.env.HOME = prevHome;
|
|
96
|
+
rmTmpHome(tmpHome);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('telemetry Test 12 -- validateEventPayload rejects fields not in schema', () => {
|
|
101
|
+
const tmpHome = mkTmpHome();
|
|
102
|
+
const prevHome = process.env.HOME;
|
|
103
|
+
process.env.HOME = tmpHome;
|
|
104
|
+
try {
|
|
105
|
+
const telemetry = freshTelemetry();
|
|
106
|
+
// 'sentence' is NOT in any ALLOWED_FIELDS entry; only sentence_sha256 is allowed
|
|
107
|
+
const v = telemetry.validateEventPayload('mva_pipeline_started', {
|
|
108
|
+
sentence_sha256: SHA256_SAMPLE,
|
|
109
|
+
sentence: 'I have a venture idea' // forbidden field name (sentence-related)
|
|
110
|
+
});
|
|
111
|
+
assert.strictEqual(v.ok, false, 'sentence field must be rejected');
|
|
112
|
+
|
|
113
|
+
// 'raw_text' should also be rejected
|
|
114
|
+
const v2 = telemetry.validateEventPayload('mva_pipeline_started', {
|
|
115
|
+
sentence_sha256: SHA256_SAMPLE,
|
|
116
|
+
raw_text: 'something'
|
|
117
|
+
});
|
|
118
|
+
assert.strictEqual(v2.ok, false, 'raw_text field must be rejected');
|
|
119
|
+
} finally {
|
|
120
|
+
process.env.HOME = prevHome;
|
|
121
|
+
rmTmpHome(tmpHome);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('telemetry Test 13 -- EVENT_TYPES + ALLOWED_FIELDS frozen, mva_brief_rendered uses total_duration_ms', () => {
|
|
126
|
+
const tmpHome = mkTmpHome();
|
|
127
|
+
const prevHome = process.env.HOME;
|
|
128
|
+
process.env.HOME = tmpHome;
|
|
129
|
+
try {
|
|
130
|
+
const telemetry = freshTelemetry();
|
|
131
|
+
assert.ok(Object.isFrozen(telemetry.EVENT_TYPES), 'EVENT_TYPES must be frozen');
|
|
132
|
+
assert.ok(Object.isFrozen(telemetry.ALLOWED_FIELDS), 'ALLOWED_FIELDS must be frozen');
|
|
133
|
+
|
|
134
|
+
// The 6 event types from OQ8
|
|
135
|
+
const expected = [
|
|
136
|
+
'mva_pipeline_started',
|
|
137
|
+
'mva_agent_returned',
|
|
138
|
+
'mva_brief_rendered',
|
|
139
|
+
'mva_option_selected',
|
|
140
|
+
'mva_brief_deployed',
|
|
141
|
+
'mva_pipeline_failed'
|
|
142
|
+
];
|
|
143
|
+
for (const e of expected) {
|
|
144
|
+
assert.ok(telemetry.EVENT_TYPES.includes(e), `EVENT_TYPES must include ${e}`);
|
|
145
|
+
assert.ok(Array.isArray(telemetry.ALLOWED_FIELDS[e]), `ALLOWED_FIELDS must list ${e}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The CRITICAL invariant: mva_brief_rendered uses 'total_duration_ms', NOT 'duration_ms'
|
|
149
|
+
const brendered = telemetry.ALLOWED_FIELDS.mva_brief_rendered;
|
|
150
|
+
assert.ok(brendered.includes('total_duration_ms'), 'must list total_duration_ms');
|
|
151
|
+
assert.equal(brendered.includes('duration_ms'), false, 'must NOT list duration_ms');
|
|
152
|
+
|
|
153
|
+
// Sanity: mva_agent_returned still uses duration_ms (it's the per-agent variant)
|
|
154
|
+
assert.ok(telemetry.ALLOWED_FIELDS.mva_agent_returned.includes('duration_ms'));
|
|
155
|
+
} finally {
|
|
156
|
+
process.env.HOME = prevHome;
|
|
157
|
+
rmTmpHome(tmpHome);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('telemetry Test 14 -- concurrent emits produce well-formed JSONL lines', async () => {
|
|
162
|
+
const tmpHome = mkTmpHome();
|
|
163
|
+
const prevHome = process.env.HOME;
|
|
164
|
+
process.env.HOME = tmpHome;
|
|
165
|
+
try {
|
|
166
|
+
const telemetry = freshTelemetry();
|
|
167
|
+
// Fire 6 concurrent emits (simulating 6 parallel agents)
|
|
168
|
+
const promises = Array.from({ length: 6 }, (_, i) => {
|
|
169
|
+
return new Promise((resolve) => {
|
|
170
|
+
setImmediate(() => {
|
|
171
|
+
telemetry.emit('mva_agent_returned', {
|
|
172
|
+
sentence_sha256: SHA256_SAMPLE,
|
|
173
|
+
agent_id: `agent_${i}`,
|
|
174
|
+
duration_ms: 100 + i,
|
|
175
|
+
status: 'ok'
|
|
176
|
+
});
|
|
177
|
+
resolve();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
await Promise.all(promises);
|
|
182
|
+
|
|
183
|
+
const jsonlPath = path.join(tmpHome, '.mindrian', 'telemetry', 'v1.13', 'mva.jsonl');
|
|
184
|
+
const content = fs.readFileSync(jsonlPath, 'utf8');
|
|
185
|
+
const lines = content.split('\n').filter((l) => l.length > 0);
|
|
186
|
+
assert.strictEqual(lines.length, 6, 'must have 6 lines');
|
|
187
|
+
for (const line of lines) {
|
|
188
|
+
const parsed = JSON.parse(line); // throws if torn
|
|
189
|
+
assert.strictEqual(parsed.event, 'mva_agent_returned');
|
|
190
|
+
assert.ok(typeof parsed.agent_id === 'string');
|
|
191
|
+
}
|
|
192
|
+
} finally {
|
|
193
|
+
process.env.HOME = prevHome;
|
|
194
|
+
rmTmpHome(tmpHome);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
3
|
+
*
|
|
4
|
+
* Phase 118-04 Plan 04 Task 1 -- mva-vercel-deploy.
|
|
5
|
+
*
|
|
6
|
+
* Vercel REST API direct deploy of the MVA Feynman deck HTML. Per LD2
|
|
7
|
+
* (Locked Decision 2 in 118-CONTEXT.md): NO `vercel` CLI dependency, NO
|
|
8
|
+
* `@vercel/client` SDK. Direct fetch() calls.
|
|
9
|
+
*
|
|
10
|
+
* Public surface:
|
|
11
|
+
* deployDeck(html: string, sha8: string) -> Promise<DeployResult>
|
|
12
|
+
* FALLBACK_DIR: string (~/.mindrian/mva/briefs)
|
|
13
|
+
*
|
|
14
|
+
* DeployResult:
|
|
15
|
+
* On success: { url: 'https://<subdomain>.vercel.app', deploy_duration_ms }
|
|
16
|
+
* On failure: { error: <code>, fallback_path, deploy_duration_ms,
|
|
17
|
+
* status?, error_short? }
|
|
18
|
+
*
|
|
19
|
+
* Failure codes:
|
|
20
|
+
* - 'vercel_unavailable' -- no VERCEL_TOKEN configured
|
|
21
|
+
* - 'vercel_api_error' -- API returned non-2xx (status field carries code)
|
|
22
|
+
* - 'vercel_exception' -- fetch threw / AbortController fired
|
|
23
|
+
*
|
|
24
|
+
* On any failure: writes the HTML to FALLBACK_DIR/<sha8>.html as the
|
|
25
|
+
* shareable receipt (the user still gets a deck, just locally).
|
|
26
|
+
*
|
|
27
|
+
* LD2 wire details:
|
|
28
|
+
* POST https://api.vercel.com/v13/deployments
|
|
29
|
+
* Authorization: Bearer <VERCEL_TOKEN>
|
|
30
|
+
* Content-Type: application/json
|
|
31
|
+
* Body: { name: 'mos-brief-<sha8>', files: [{ file: 'index.html',
|
|
32
|
+
* data: base64(html), encoding: 'base64' }],
|
|
33
|
+
* projectSettings: { framework: null }, target: 'production' }
|
|
34
|
+
*
|
|
35
|
+
* Timeout: 5 seconds via AbortController (the source spec's <3s budget plus
|
|
36
|
+
* safety headroom; the overall MVA budget is 45s and this fits with ample
|
|
37
|
+
* margin).
|
|
38
|
+
*
|
|
39
|
+
* Canon Part 8: the function sees ONLY `html` (already sanitized at build
|
|
40
|
+
* time by mva-deck-builder) and `sha8` (a hash of a hash). No raw sentence
|
|
41
|
+
* fields, no MVA_SENTENCE reads, no .sentence access. The Vercel request
|
|
42
|
+
* body uses base64-encoded HTML in the request payload; the URL is the bare
|
|
43
|
+
* endpoint. No user content in URLs or query strings.
|
|
44
|
+
*
|
|
45
|
+
* Pure CJS, node built-ins only (fs, path, os) plus the resolver module.
|
|
46
|
+
*/
|
|
47
|
+
'use strict';
|
|
48
|
+
|
|
49
|
+
const fs = require('node:fs');
|
|
50
|
+
const path = require('node:path');
|
|
51
|
+
const os = require('node:os');
|
|
52
|
+
const { resolveVercelKey } = require('./resolve-vercel-key.cjs');
|
|
53
|
+
|
|
54
|
+
const VERCEL_API_URL = 'https://api.vercel.com/v13/deployments';
|
|
55
|
+
const DEPLOY_TIMEOUT_MS = 5000;
|
|
56
|
+
|
|
57
|
+
function _homeDir() {
|
|
58
|
+
return process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** @type {string} Resolved at call-time so HOME overrides in tests work. */
|
|
62
|
+
const FALLBACK_DIR = path.join(_homeDir(), '.mindrian', 'mva', 'briefs');
|
|
63
|
+
|
|
64
|
+
function _fallbackDir() {
|
|
65
|
+
return path.join(_homeDir(), '.mindrian', 'mva', 'briefs');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Write the rendered HTML to the local fallback path so the user still has a
|
|
70
|
+
* shareable artifact even when Vercel is unreachable / unconfigured.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} html the rendered deck HTML
|
|
73
|
+
* @param {string} sha8 the 8-char hash identifier
|
|
74
|
+
* @returns {string} absolute fallback path
|
|
75
|
+
*/
|
|
76
|
+
function _writeFallback(html, sha8) {
|
|
77
|
+
const dir = _fallbackDir();
|
|
78
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
79
|
+
const fallbackPath = path.join(dir, sha8 + '.html');
|
|
80
|
+
fs.writeFileSync(fallbackPath, html, 'utf8');
|
|
81
|
+
return fallbackPath;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* deployDeck -- the one-shot Vercel deploy with local fallback.
|
|
86
|
+
*
|
|
87
|
+
* Best-effort by design: the rendered terminal brief is already in the user's
|
|
88
|
+
* scrollback by the time we're called; a deploy failure is a degraded reward
|
|
89
|
+
* (local file instead of public URL), not a fatal error.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} html full HTML document; will be base64-encoded for transit
|
|
92
|
+
* @param {string} sha8 8-char hash identifier (first 8 chars of sentence_sha256)
|
|
93
|
+
* @returns {Promise<{
|
|
94
|
+
* url?: string,
|
|
95
|
+
* error?: 'vercel_unavailable' | 'vercel_api_error' | 'vercel_exception',
|
|
96
|
+
* status?: number,
|
|
97
|
+
* error_short?: string,
|
|
98
|
+
* fallback_path?: string,
|
|
99
|
+
* deploy_duration_ms: number
|
|
100
|
+
* }>}
|
|
101
|
+
*/
|
|
102
|
+
async function deployDeck(html, sha8) {
|
|
103
|
+
const t0 = Date.now();
|
|
104
|
+
const key = resolveVercelKey();
|
|
105
|
+
if (!key) {
|
|
106
|
+
const fallback_path = _writeFallback(html, sha8);
|
|
107
|
+
return {
|
|
108
|
+
error: 'vercel_unavailable',
|
|
109
|
+
fallback_path,
|
|
110
|
+
deploy_duration_ms: Date.now() - t0,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const ctl = new AbortController();
|
|
115
|
+
const timer = setTimeout(() => ctl.abort(), DEPLOY_TIMEOUT_MS);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const response = await fetch(VERCEL_API_URL, {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: {
|
|
121
|
+
'Content-Type': 'application/json',
|
|
122
|
+
'Authorization': 'Bearer ' + key,
|
|
123
|
+
},
|
|
124
|
+
body: JSON.stringify({
|
|
125
|
+
name: 'mos-brief-' + sha8,
|
|
126
|
+
files: [{
|
|
127
|
+
file: 'index.html',
|
|
128
|
+
data: Buffer.from(html, 'utf8').toString('base64'),
|
|
129
|
+
encoding: 'base64',
|
|
130
|
+
}],
|
|
131
|
+
projectSettings: { framework: null },
|
|
132
|
+
target: 'production',
|
|
133
|
+
}),
|
|
134
|
+
signal: ctl.signal,
|
|
135
|
+
});
|
|
136
|
+
clearTimeout(timer);
|
|
137
|
+
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
const fallback_path = _writeFallback(html, sha8);
|
|
140
|
+
return {
|
|
141
|
+
error: 'vercel_api_error',
|
|
142
|
+
status: response.status,
|
|
143
|
+
fallback_path,
|
|
144
|
+
deploy_duration_ms: Date.now() - t0,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const data = await response.json();
|
|
149
|
+
return {
|
|
150
|
+
url: 'https://' + data.url,
|
|
151
|
+
deploy_duration_ms: Date.now() - t0,
|
|
152
|
+
};
|
|
153
|
+
} catch (e) {
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
const fallback_path = _writeFallback(html, sha8);
|
|
156
|
+
return {
|
|
157
|
+
error: 'vercel_exception',
|
|
158
|
+
error_short: String((e && e.message) || e).slice(0, 60),
|
|
159
|
+
fallback_path,
|
|
160
|
+
deploy_duration_ms: Date.now() - t0,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = {
|
|
166
|
+
deployDeck,
|
|
167
|
+
FALLBACK_DIR,
|
|
168
|
+
};
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
3
|
+
*
|
|
4
|
+
* Phase 118-04 Plan 04 Task 1 -- mva-vercel-deploy tests.
|
|
5
|
+
*
|
|
6
|
+
* Verifies the Vercel REST API direct-deploy path with fallback to local file.
|
|
7
|
+
* Mocks global.fetch via monkey-patch; uses tmp HOME for fallback paths.
|
|
8
|
+
*
|
|
9
|
+
* Canon Part 8 invariants:
|
|
10
|
+
* - The deploy module only sees `html` (already sanitized) and `sha8` (a hash).
|
|
11
|
+
* - It NEVER receives or transmits the raw sentence.
|
|
12
|
+
* - The request body is base64-encoded HTML; no raw user content in URLs.
|
|
13
|
+
*
|
|
14
|
+
* Locked Decision 2 (LD2):
|
|
15
|
+
* - Direct REST API to https://api.vercel.com/v13/deployments.
|
|
16
|
+
* - 5-second AbortController timeout.
|
|
17
|
+
* - Subdomain shape: mos-brief-<sha8>.
|
|
18
|
+
*/
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const test = require('node:test');
|
|
22
|
+
const assert = require('node:assert/strict');
|
|
23
|
+
const fs = require('node:fs');
|
|
24
|
+
const os = require('node:os');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
|
|
27
|
+
function _mkTmpHome() {
|
|
28
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'mva-vercel-deploy-test-'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function _cleanup(dir) {
|
|
32
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function _freshRequire() {
|
|
36
|
+
const p = require.resolve('./mva-vercel-deploy.cjs');
|
|
37
|
+
delete require.cache[p];
|
|
38
|
+
// Also bust resolve-vercel-key in case the test mutates env
|
|
39
|
+
const rp = require.resolve('./resolve-vercel-key.cjs');
|
|
40
|
+
delete require.cache[rp];
|
|
41
|
+
return require('./mva-vercel-deploy.cjs');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test('Test 6: no VERCEL_TOKEN -> writes fallback file + returns error envelope', async () => {
|
|
45
|
+
const home = _mkTmpHome();
|
|
46
|
+
const prev = process.env.VERCEL_TOKEN;
|
|
47
|
+
const prevHome = process.env.HOME;
|
|
48
|
+
delete process.env.VERCEL_TOKEN;
|
|
49
|
+
process.env.HOME = home;
|
|
50
|
+
try {
|
|
51
|
+
const { deployDeck } = _freshRequire();
|
|
52
|
+
const result = await deployDeck('<!DOCTYPE html><html><body>test</body></html>', 'abcd1234');
|
|
53
|
+
assert.equal(result.error, 'vercel_unavailable');
|
|
54
|
+
assert.equal(typeof result.fallback_path, 'string');
|
|
55
|
+
assert.ok(result.fallback_path.endsWith(path.join('.mindrian', 'mva', 'briefs', 'abcd1234.html')));
|
|
56
|
+
assert.equal(typeof result.deploy_duration_ms, 'number');
|
|
57
|
+
assert.ok(result.deploy_duration_ms >= 0);
|
|
58
|
+
// Fallback HTML written to disk
|
|
59
|
+
const written = fs.readFileSync(result.fallback_path, 'utf8');
|
|
60
|
+
assert.ok(written.includes('<body>test</body>'));
|
|
61
|
+
} finally {
|
|
62
|
+
if (prev === undefined) { delete process.env.VERCEL_TOKEN; } else { process.env.VERCEL_TOKEN = prev; }
|
|
63
|
+
if (prevHome === undefined) { delete process.env.HOME; } else { process.env.HOME = prevHome; }
|
|
64
|
+
_cleanup(home);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('Test 7: mocked Vercel API returns 200 -> deployDeck returns https URL', async () => {
|
|
69
|
+
const home = _mkTmpHome();
|
|
70
|
+
const prev = process.env.VERCEL_TOKEN;
|
|
71
|
+
const prevHome = process.env.HOME;
|
|
72
|
+
const prevFetch = global.fetch;
|
|
73
|
+
process.env.VERCEL_TOKEN = 'test-token';
|
|
74
|
+
process.env.HOME = home;
|
|
75
|
+
global.fetch = async (_url, _opts) => ({
|
|
76
|
+
ok: true,
|
|
77
|
+
status: 200,
|
|
78
|
+
json: async () => ({ url: 'mos-brief-abcd-xyz.vercel.app', readyState: 'READY' })
|
|
79
|
+
});
|
|
80
|
+
try {
|
|
81
|
+
const { deployDeck } = _freshRequire();
|
|
82
|
+
const result = await deployDeck('<!DOCTYPE html><html></html>', 'abcd1234');
|
|
83
|
+
assert.equal(result.url, 'https://mos-brief-abcd-xyz.vercel.app');
|
|
84
|
+
assert.equal(typeof result.deploy_duration_ms, 'number');
|
|
85
|
+
assert.ok(result.deploy_duration_ms >= 0);
|
|
86
|
+
assert.equal(result.error, undefined);
|
|
87
|
+
assert.equal(result.fallback_path, undefined);
|
|
88
|
+
} finally {
|
|
89
|
+
if (prev === undefined) { delete process.env.VERCEL_TOKEN; } else { process.env.VERCEL_TOKEN = prev; }
|
|
90
|
+
if (prevHome === undefined) { delete process.env.HOME; } else { process.env.HOME = prevHome; }
|
|
91
|
+
if (prevFetch === undefined) { delete global.fetch; } else { global.fetch = prevFetch; }
|
|
92
|
+
_cleanup(home);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('Test 8: Vercel API 5xx -> falls back to local file with vercel_api_error', async () => {
|
|
97
|
+
const home = _mkTmpHome();
|
|
98
|
+
const prev = process.env.VERCEL_TOKEN;
|
|
99
|
+
const prevHome = process.env.HOME;
|
|
100
|
+
const prevFetch = global.fetch;
|
|
101
|
+
process.env.VERCEL_TOKEN = 'test-token';
|
|
102
|
+
process.env.HOME = home;
|
|
103
|
+
global.fetch = async () => ({
|
|
104
|
+
ok: false,
|
|
105
|
+
status: 503,
|
|
106
|
+
json: async () => ({ error: { message: 'server unavailable' } })
|
|
107
|
+
});
|
|
108
|
+
try {
|
|
109
|
+
const { deployDeck } = _freshRequire();
|
|
110
|
+
const result = await deployDeck('<html></html>', 'beef5678');
|
|
111
|
+
assert.equal(result.error, 'vercel_api_error');
|
|
112
|
+
assert.equal(result.status, 503);
|
|
113
|
+
assert.ok(result.fallback_path.endsWith(path.join('.mindrian', 'mva', 'briefs', 'beef5678.html')));
|
|
114
|
+
assert.equal(typeof result.deploy_duration_ms, 'number');
|
|
115
|
+
assert.ok(fs.existsSync(result.fallback_path));
|
|
116
|
+
} finally {
|
|
117
|
+
if (prev === undefined) { delete process.env.VERCEL_TOKEN; } else { process.env.VERCEL_TOKEN = prev; }
|
|
118
|
+
if (prevHome === undefined) { delete process.env.HOME; } else { process.env.HOME = prevHome; }
|
|
119
|
+
if (prevFetch === undefined) { delete global.fetch; } else { global.fetch = prevFetch; }
|
|
120
|
+
_cleanup(home);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('Test 9: AbortController times out after 5s on hang -> fallback under 5500ms', async () => {
|
|
125
|
+
const home = _mkTmpHome();
|
|
126
|
+
const prev = process.env.VERCEL_TOKEN;
|
|
127
|
+
const prevHome = process.env.HOME;
|
|
128
|
+
const prevFetch = global.fetch;
|
|
129
|
+
process.env.VERCEL_TOKEN = 'test-token';
|
|
130
|
+
process.env.HOME = home;
|
|
131
|
+
// Hanging fetch: never resolves, but respects AbortSignal
|
|
132
|
+
global.fetch = (_url, opts) => new Promise((_resolve, reject) => {
|
|
133
|
+
if (opts && opts.signal) {
|
|
134
|
+
opts.signal.addEventListener('abort', () => {
|
|
135
|
+
const err = new Error('aborted');
|
|
136
|
+
err.name = 'AbortError';
|
|
137
|
+
reject(err);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
try {
|
|
142
|
+
const { deployDeck } = _freshRequire();
|
|
143
|
+
const t0 = Date.now();
|
|
144
|
+
const result = await deployDeck('<html></html>', 'cafe9012');
|
|
145
|
+
const wall = Date.now() - t0;
|
|
146
|
+
// Should fall back (any error envelope, fallback_path populated)
|
|
147
|
+
assert.ok(result.fallback_path);
|
|
148
|
+
assert.ok(['vercel_exception', 'vercel_api_error', 'vercel_unavailable'].includes(result.error));
|
|
149
|
+
assert.ok(wall < 5500, 'wall-clock should be under 5.5s; got ' + wall);
|
|
150
|
+
} finally {
|
|
151
|
+
if (prev === undefined) { delete process.env.VERCEL_TOKEN; } else { process.env.VERCEL_TOKEN = prev; }
|
|
152
|
+
if (prevHome === undefined) { delete process.env.HOME; } else { process.env.HOME = prevHome; }
|
|
153
|
+
if (prevFetch === undefined) { delete global.fetch; } else { global.fetch = prevFetch; }
|
|
154
|
+
_cleanup(home);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('Test 10: deploy request body.name === mos-brief-<sha8>', async () => {
|
|
159
|
+
const home = _mkTmpHome();
|
|
160
|
+
const prev = process.env.VERCEL_TOKEN;
|
|
161
|
+
const prevHome = process.env.HOME;
|
|
162
|
+
const prevFetch = global.fetch;
|
|
163
|
+
process.env.VERCEL_TOKEN = 'test-token';
|
|
164
|
+
process.env.HOME = home;
|
|
165
|
+
let capturedBody = null;
|
|
166
|
+
global.fetch = async (_url, opts) => {
|
|
167
|
+
capturedBody = JSON.parse(opts.body);
|
|
168
|
+
return {
|
|
169
|
+
ok: true,
|
|
170
|
+
status: 200,
|
|
171
|
+
json: async () => ({ url: 'mos-brief-deadbeef-1.vercel.app', readyState: 'READY' })
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
try {
|
|
175
|
+
const { deployDeck } = _freshRequire();
|
|
176
|
+
await deployDeck('<html></html>', 'deadbeef');
|
|
177
|
+
assert.ok(capturedBody);
|
|
178
|
+
assert.equal(capturedBody.name, 'mos-brief-deadbeef');
|
|
179
|
+
assert.ok(Array.isArray(capturedBody.files));
|
|
180
|
+
assert.equal(capturedBody.files.length, 1);
|
|
181
|
+
assert.equal(capturedBody.files[0].file, 'index.html');
|
|
182
|
+
assert.equal(capturedBody.files[0].encoding, 'base64');
|
|
183
|
+
} finally {
|
|
184
|
+
if (prev === undefined) { delete process.env.VERCEL_TOKEN; } else { process.env.VERCEL_TOKEN = prev; }
|
|
185
|
+
if (prevHome === undefined) { delete process.env.HOME; } else { process.env.HOME = prevHome; }
|
|
186
|
+
if (prevFetch === undefined) { delete global.fetch; } else { global.fetch = prevFetch; }
|
|
187
|
+
_cleanup(home);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('Test 11: HTML body is base64-encoded in request body, NOT in URL', async () => {
|
|
192
|
+
const home = _mkTmpHome();
|
|
193
|
+
const prev = process.env.VERCEL_TOKEN;
|
|
194
|
+
const prevHome = process.env.HOME;
|
|
195
|
+
const prevFetch = global.fetch;
|
|
196
|
+
process.env.VERCEL_TOKEN = 'test-token';
|
|
197
|
+
process.env.HOME = home;
|
|
198
|
+
let capturedUrl = null;
|
|
199
|
+
let capturedBody = null;
|
|
200
|
+
global.fetch = async (url, opts) => {
|
|
201
|
+
capturedUrl = url;
|
|
202
|
+
capturedBody = JSON.parse(opts.body);
|
|
203
|
+
return {
|
|
204
|
+
ok: true,
|
|
205
|
+
status: 200,
|
|
206
|
+
json: async () => ({ url: 'mos-brief-aaaa1111-1.vercel.app', readyState: 'READY' })
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
try {
|
|
210
|
+
const { deployDeck } = _freshRequire();
|
|
211
|
+
const html = '<!DOCTYPE html><html><body>HELLO</body></html>';
|
|
212
|
+
await deployDeck(html, 'aaaa1111');
|
|
213
|
+
// URL is the bare endpoint, no encoded HTML in it
|
|
214
|
+
assert.equal(capturedUrl, 'https://api.vercel.com/v13/deployments');
|
|
215
|
+
// Body file content is base64 of the HTML
|
|
216
|
+
const expectedBase64 = Buffer.from(html, 'utf8').toString('base64');
|
|
217
|
+
assert.equal(capturedBody.files[0].data, expectedBase64);
|
|
218
|
+
// And: the URL does NOT contain "HELLO" or any HTML literal
|
|
219
|
+
assert.ok(!String(capturedUrl).includes('HELLO'));
|
|
220
|
+
assert.ok(!String(capturedUrl).includes('<body>'));
|
|
221
|
+
} finally {
|
|
222
|
+
if (prev === undefined) { delete process.env.VERCEL_TOKEN; } else { process.env.VERCEL_TOKEN = prev; }
|
|
223
|
+
if (prevHome === undefined) { delete process.env.HOME; } else { process.env.HOME = prevHome; }
|
|
224
|
+
if (prevFetch === undefined) { delete global.fetch; } else { global.fetch = prevFetch; }
|
|
225
|
+
_cleanup(home);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('Bonus: Canon Part 8 source grep -- no raw-sentence references in deploy module', () => {
|
|
230
|
+
const code = fs.readFileSync(path.join(__dirname, 'mva-vercel-deploy.cjs'), 'utf8');
|
|
231
|
+
// Strip comments before grepping (mirrors Plan 118-03 source-purity pattern)
|
|
232
|
+
const stripped = code.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
|
|
233
|
+
assert.equal(stripped.match(/MVA_SENTENCE/), null,
|
|
234
|
+
'mva-vercel-deploy.cjs must not reference MVA_SENTENCE');
|
|
235
|
+
assert.equal(stripped.match(/\.sentence\b/), null,
|
|
236
|
+
'mva-vercel-deploy.cjs must not read .sentence');
|
|
237
|
+
assert.equal(stripped.match(/raw_sentence/), null,
|
|
238
|
+
'mva-vercel-deploy.cjs must not reference raw_sentence');
|
|
239
|
+
});
|