@holdyourvoice/hyv 3.6.0 → 4.0.0
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/Readme.md +93 -154
- package/dist/ai-editor-rules.js +1 -0
- package/dist/ai-editor.js +23 -33
- package/dist/analysis.js +33 -0
- package/dist/cli/agents.js +88 -0
- package/dist/cli/checks.js +226 -0
- package/dist/cli/io.js +141 -0
- package/dist/cli/lifecycle.js +162 -0
- package/dist/cli/profiles.js +246 -0
- package/dist/cli/rewriting.js +111 -0
- package/dist/cli.js +5 -974
- package/dist/copy-spec.js +6 -6
- package/dist/editorial-packs.js +4 -3
- package/dist/fact-linter.js +45 -27
- package/dist/hidden-text.js +6 -6
- package/dist/hold-your-voice.mcpb +0 -0
- package/dist/internal.js +8 -0
- package/dist/judgment-task.js +10 -14
- package/dist/learning.js +77 -71
- package/dist/lifecycle-adapter.js +2 -5
- package/dist/local-eval.js +16 -31
- package/dist/mcp-server.js +272 -0
- package/dist/mcp-tools.js +10 -19
- package/dist/mcp.js +2 -267
- package/dist/pipeline.js +18 -105
- package/dist/profile-compose.js +18 -11
- package/dist/profile-score.js +6 -6
- package/dist/profile.js +17 -19
- package/dist/provenance-status.js +1 -3
- package/dist/rebuild-task.js +18 -52
- package/dist/recomposition.js +18 -13
- package/dist/rewrite-prompt.js +82 -0
- package/dist/rewrite-response.js +27 -0
- package/dist/rewrite-task.js +73 -98
- package/dist/rule-allowances.js +6 -10
- package/dist/semantic-review.js +71 -47
- package/dist/stage1-evaluation.js +29 -14
- package/dist/strict-quality.js +10 -18
- package/dist/version.js +1 -1
- package/dist/voice-dna.js +34 -21
- package/dist/writing-examples.js +2 -7
- package/package.json +6 -3
- package/skills/hyv-prepare-judgment/SKILL.md +4 -0
- package/dist/agents/catalog.test.js +0 -60
- package/dist/agents/emit.test.js +0 -64
- package/dist/agents/load.test.js +0 -149
- package/dist/ai-editor.test.js +0 -265
- package/dist/approval-capability.test.js +0 -52
- package/dist/approval-context.test.js +0 -38
- package/dist/backtest.test.js +0 -20
- package/dist/benchmark.test.js +0 -328
- package/dist/canonical-json.test.js +0 -24
- package/dist/cli.test.js +0 -731
- package/dist/editorial-packs.test.js +0 -94
- package/dist/fact-linter.test.js +0 -85
- package/dist/hidden-text.test.js +0 -26
- package/dist/hygiene.test.js +0 -83
- package/dist/judgment-task.test.js +0 -162
- package/dist/learning.test.js +0 -325
- package/dist/lifecycle-adapter.test.js +0 -56
- package/dist/local-eval.test.js +0 -20
- package/dist/logic-linter-corpus.test.js +0 -22
- package/dist/logic-linter.test.js +0 -39
- package/dist/mcp-tools.test.js +0 -286
- package/dist/mcp.test.js +0 -312
- package/dist/mirror-refs.test.js +0 -63
- package/dist/pipeline.test.js +0 -247
- package/dist/preservation.test.js +0 -22
- package/dist/production-gates.test.js +0 -34
- package/dist/profile-compose.test.js +0 -32
- package/dist/profile-score.test.js +0 -22
- package/dist/profile-watch.test.js +0 -23
- package/dist/profile.test.js +0 -141
- package/dist/provenance-status.test.js +0 -22
- package/dist/rebuild-task.test.js +0 -206
- package/dist/recomposition.test.js +0 -34
- package/dist/release-audit.test.js +0 -292
- package/dist/rewrite-task.test.js +0 -166
- package/dist/rule-allowances.test.js +0 -17
- package/dist/rule-reconciliation.test.js +0 -50
- package/dist/sample-ingest.test.js +0 -52
- package/dist/semantic-review.test.js +0 -101
- package/dist/stage1-dry-run.test.js +0 -39
- package/dist/stage1-evaluation.test.js +0 -184
- package/dist/stage1-human-packet.test.js +0 -102
- package/dist/stage1-schema-contract.test.js +0 -95
- package/dist/stage2-human-packet.test.js +0 -81
- package/dist/strict-quality.test.js +0 -62
- package/dist/text-provenance.feature.test.js +0 -45
- package/dist/text.test.js +0 -16
- package/dist/voice-dna.test.js +0 -121
- package/dist/writing-examples.test.js +0 -35
package/dist/mcp-tools.test.js
DELETED
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { createHash, generateKeyPairSync, sign } from 'node:crypto';
|
|
3
|
-
import { mkdtempSync, rmSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { join } from 'node:path';
|
|
6
|
-
import test from 'node:test';
|
|
7
|
-
import { analyzeBatchForMcp, analyzeForMcp, applyHiddenTextPolicyForMcp, applyRebuildForMcp, applyRewriteForMcp, buildProfileForMcp, clearLearningForMcp, finalOutputCheckForMcp, finalizeLifecycleForMcp, inspectHiddenTextForMcp, inspectHygieneForMcp, inspectLearningForMcp, inspectLifecycleForMcp, logicLintForMcp, patternsForMcp, prepareJudgmentForMcp, prepareLifecycleForMcp, prepareRebuildForMcp, prepareRewriteForMcp, ratifyLearningForMcp, recordApprovedLearningForMcp, recordLearningForMcp, rebuildWriterRequestForMcp, reduceJudgmentForMcp, rewritePromptForMcp, scoreHeldoutForMcp, strictCheckForMcp, submitSemanticVerdictForMcp, supersedeLearningForMcp, validateFinalApprovalForMcp, verifyCopySpecForMcp, verifyForMcp } from './mcp-tools.js';
|
|
8
|
-
import { canonicalJson } from './canonical-json.js';
|
|
9
|
-
import { findWritingExamplesForMcp } from './mcp-tools.js';
|
|
10
|
-
import { backtestForMcp } from './mcp-tools.js';
|
|
11
|
-
const profile = buildProfileForMcp(['I write clearly. I keep the useful detail.', 'I make the call. Then I explain the trade-off.'], ['leverage']);
|
|
12
|
-
const profileJson = JSON.stringify(profile);
|
|
13
|
-
test('builds a portable profile for MCP without files', () => {
|
|
14
|
-
assert.equal(profile.version, '2');
|
|
15
|
-
assert.equal(profile.sampleCount, 2);
|
|
16
|
-
});
|
|
17
|
-
test('keeps the dual-engine analysis shape through MCP tools', () => {
|
|
18
|
-
const result = analyzeForMcp('I leverage a clear plan.\u200B', profileJson);
|
|
19
|
-
assert.equal(result.voiceDna.engine, 'voice_dna');
|
|
20
|
-
assert.equal(result.aiEditor.engine, 'ai_editor');
|
|
21
|
-
assert.equal(result.hygiene.suspiciousCount, 1);
|
|
22
|
-
});
|
|
23
|
-
test('exposes the strict local quality gate through MCP helpers', () => {
|
|
24
|
-
const report = strictCheckForMcp('I leverage a clear plan.', profileJson, ['one.', 'two.']);
|
|
25
|
-
assert.equal(report.disposition, 'blocked');
|
|
26
|
-
assert.ok(report.findings.some((finding) => finding.id === 'strict.profile.version'));
|
|
27
|
-
});
|
|
28
|
-
test('scores explicit held-out samples through MCP without storing them', () => {
|
|
29
|
-
const samples = [
|
|
30
|
-
'I write a direct note about the launch. The owner checks the evidence before we ship. The next step stays clear and small.',
|
|
31
|
-
'I name the trade-off before I make a decision. We keep the mechanism visible for the person doing the work. The release has one owner.',
|
|
32
|
-
'I start from evidence in the issue. Then I explain the constraint and choose a concrete next step. The team checks the result.',
|
|
33
|
-
];
|
|
34
|
-
const result = scoreHeldoutForMcp(samples[0], JSON.stringify(buildProfileForMcp(samples)), samples);
|
|
35
|
-
assert.equal(result.version, '1');
|
|
36
|
-
assert.equal(result.selfSimilarity?.ceiling, 100);
|
|
37
|
-
});
|
|
38
|
-
test('runs an isolated backtest without returning target or candidate prose', () => {
|
|
39
|
-
const samples = ['I write direct evidence for the operator doing the release work today.', 'The owner checks each rollback and names the next step for production.', 'The report keeps the mechanism visible and gives the team one concrete action.'];
|
|
40
|
-
const report = backtestForMcp('Explain the rollout owner.', 'The owner checks rollback.', 'The owner checks rollback.', JSON.stringify(buildProfileForMcp(samples)), samples);
|
|
41
|
-
assert.equal(report.preservation.score, 100);
|
|
42
|
-
assert.equal(JSON.stringify(report).includes('The owner checks rollback.'), false);
|
|
43
|
-
});
|
|
44
|
-
test('finds and injects redacted local writing examples without storing an index', () => {
|
|
45
|
-
const examples = [{ basename: 'email.md', text: 'The retry queue stays local; owner@example.com receives the review.' }];
|
|
46
|
-
const found = findWritingExamplesForMcp('The retry queue needs review.', examples);
|
|
47
|
-
assert.equal(found[0]?.source, 'email.md');
|
|
48
|
-
assert.match(found[0]?.text ?? '', /REDACTED:EMAIL/);
|
|
49
|
-
const prompt = rewritePromptForMcp('The retry queue needs review.', profileJson, {}, undefined, examples);
|
|
50
|
-
assert.match(prompt.prompt, /Approved local writing examples/);
|
|
51
|
-
assert.doesNotMatch(prompt.prompt, /owner@example\.com/);
|
|
52
|
-
});
|
|
53
|
-
test('inspects Unicode hygiene through MCP without a voice profile', () => {
|
|
54
|
-
const result = inspectHygieneForMcp('one\u200Btwo\u00A0three');
|
|
55
|
-
assert.equal(result.suspiciousCount, 2);
|
|
56
|
-
assert.equal(result.fixableCount, 0);
|
|
57
|
-
});
|
|
58
|
-
test('exposes the deterministic logic gate through MCP helpers', () => {
|
|
59
|
-
const report = logicLintForMcp('The service records cache keys. Each build writes its cache key. Espresso machines use dual boilers for stable temperature control. The service uses cache keys during incident review.');
|
|
60
|
-
assert.equal(report.passed, false);
|
|
61
|
-
assert.equal(report.findings[0]?.kind, 'topic_drift');
|
|
62
|
-
});
|
|
63
|
-
test('applies only explicit hidden-text removals through MCP', () => {
|
|
64
|
-
const policy = JSON.stringify({ version: '1', name: 'minimal-text-control-cleanup', approvedRemovals: ['ascii_control'], acknowledgement: 'Removes only listed non-semantic controls; all other findings remain review-only.' });
|
|
65
|
-
const inspected = inspectHiddenTextForMcp('one\u0007two\uFEFFthree\u200D', policy);
|
|
66
|
-
assert.deepEqual(inspected.proposedChanges.map((change) => change.codepoint), ['U+0007']);
|
|
67
|
-
const applied = applyHiddenTextPolicyForMcp('one\u0007two\uFEFFthree\u200D', policy);
|
|
68
|
-
assert.equal(applied.output, 'onetwo\uFEFFthree\u200D');
|
|
69
|
-
assert.equal(applied.idempotent, true);
|
|
70
|
-
});
|
|
71
|
-
test('gates exact final output through MCP without a voice profile', () => {
|
|
72
|
-
const accepted = finalOutputCheckForMcp('exact output');
|
|
73
|
-
assert.equal(accepted.accepted && accepted.output, 'exact output');
|
|
74
|
-
const rejected = finalOutputCheckForMcp('hidden\u200Boutput');
|
|
75
|
-
assert.equal(rejected.accepted, false);
|
|
76
|
-
assert.equal('output' in rejected, false);
|
|
77
|
-
});
|
|
78
|
-
test('accepts optional WritingBrief context and exposes batch findings through MCP helpers', () => {
|
|
79
|
-
const brief = JSON.stringify({
|
|
80
|
-
version: '1', audience: 'founders', intent: 'start a discussion', format: 'social', evidenceStatus: 'unverified',
|
|
81
|
-
argumentMap: { observation: 'Founders repeat vague advice.', mechanism: 'The advice skips the work.', consequence: 'Readers cannot act.', readerValue: 'Avoid a vague post.' },
|
|
82
|
-
});
|
|
83
|
-
const analysis = analyzeForMcp('A pattern I keep seeing in founder posts is vague advice.', profileJson, brief);
|
|
84
|
-
assert.ok(analysis.editorial?.findings.some((item) => item.id === 'editorial.social.generic-opener'));
|
|
85
|
-
assert.ok(analysis.editorial?.findings.some((item) => item.id === 'editorial.evidence.unverified'));
|
|
86
|
-
const batch = analyzeBatchForMcp(['The launch needs a clear owner.', 'The launch needs a clear owner.']);
|
|
87
|
-
assert.equal(batch.findings.length, 2);
|
|
88
|
-
});
|
|
89
|
-
test('keeps MCP verification read-only', () => {
|
|
90
|
-
const root = mkdtempSync(join(tmpdir(), 'holdyourvoice-mcp-'));
|
|
91
|
-
try {
|
|
92
|
-
const result = verifyForMcp('I leverage the answer with useful detail and clear mechanism.', 'I use the answer with useful detail and clear mechanism.', profileJson);
|
|
93
|
-
const brief = rewritePromptForMcp('I leverage a clear plan.', profileJson, { root });
|
|
94
|
-
assert.match(brief.prompt, /Tier 0/);
|
|
95
|
-
assert.doesNotMatch(brief.prompt, /Learned local preferences/);
|
|
96
|
-
assert.equal(result.preservationScore >= 70, true);
|
|
97
|
-
assert.equal('learning' in result, false);
|
|
98
|
-
assert.deepEqual(inspectLearningForMcp(profileJson, { root }), []);
|
|
99
|
-
}
|
|
100
|
-
finally {
|
|
101
|
-
rmSync(root, { recursive: true, force: true });
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
test('prepares, submits, and inspects the normal lifecycle through MCP helpers', () => {
|
|
105
|
-
const deterministicBase = { version: '1', verificationKind: 'standard', passed: true, analysisVersion: '2', rulesetVersion: '3.2.0', preservationMetricVersion: 'legacy-set-v1', preservationScore: 100, sourceHash: '4'.repeat(64), candidateHash: '5'.repeat(64), profileId: 'founder.primary', profileRevisionDigest: '6'.repeat(64), regressionKeys: [] };
|
|
106
|
-
const deterministic = { ...deterministicBase, artifactFingerprint: createHash('sha256').update(`hyv:deterministic-verification:v1\0${canonicalJson(deterministicBase)}`).digest('hex') };
|
|
107
|
-
const binding = { rewriteTaskFingerprint: '1'.repeat(64), rewriteResponseFingerprint: '2'.repeat(64), deterministicArtifactFingerprint: deterministic.artifactFingerprint, sourceHash: deterministic.sourceHash, candidateHash: deterministic.candidateHash, profileId: deterministic.profileId, profileRevisionDigest: deterministic.profileRevisionDigest, rulesetVersion: deterministic.rulesetVersion, schemaVersion: '1' };
|
|
108
|
-
const receipt = { version: '1', taskFingerprint: binding.rewriteTaskFingerprint, responseFingerprint: binding.rewriteResponseFingerprint, adapterIds: [], replacementSentenceIds: [1] };
|
|
109
|
-
const prepared = prepareLifecycleForMcp(JSON.stringify(deterministic), JSON.stringify(binding), JSON.stringify(receipt), 'normal', ['action_change']);
|
|
110
|
-
const context = { now: 0, trustStore: { version: '1', audience: '@holdyourvoice/hyv', maxCapabilityLifetimeSeconds: 1, keys: [] }, authorizedSemanticEvaluatorIds: { normal: ['reviewer-1'], highAssurance: [] }, authorizedHumanFinalizerIds: [] };
|
|
111
|
-
const submitted = submitSemanticVerdictForMcp(JSON.stringify(prepared.artifact), JSON.stringify(prepared.task), 'reviewer-1', JSON.stringify({ approved: true, violations: [] }), context);
|
|
112
|
-
assert.equal(submitted.ok && submitted.artifact.status, 'ready_for_human_review');
|
|
113
|
-
assert.equal(inspectLifecycleForMcp(JSON.stringify(submitted.ok && submitted.artifact)).status, 'ready_for_human_review');
|
|
114
|
-
assert.throws(() => prepareLifecycleForMcp(JSON.stringify(deterministic), JSON.stringify(binding), JSON.stringify(receipt), 'high_assurance', ['action_change']), /trusted embedding/);
|
|
115
|
-
const forgedTask = { ...prepared.task, allowedViolations: ['unsupported_claim'] };
|
|
116
|
-
assert.throws(() => submitSemanticVerdictForMcp(JSON.stringify(prepared.artifact), JSON.stringify(forgedTask), 'reviewer-1', JSON.stringify({ approved: false, violations: ['unsupported_claim'] }), context));
|
|
117
|
-
});
|
|
118
|
-
test('validates and finalizes signed approval with helper/core parity and no bearer output', () => {
|
|
119
|
-
const deterministicBase = { version: '1', verificationKind: 'standard', passed: true, analysisVersion: '2', rulesetVersion: '3.2.0', preservationMetricVersion: 'legacy-set-v1', preservationScore: 100, sourceHash: '4'.repeat(64), candidateHash: '5'.repeat(64), profileId: 'founder.primary', profileRevisionDigest: '6'.repeat(64), regressionKeys: [] };
|
|
120
|
-
const deterministic = { ...deterministicBase, artifactFingerprint: createHash('sha256').update(`hyv:deterministic-verification:v1\0${canonicalJson(deterministicBase)}`).digest('hex') };
|
|
121
|
-
const binding = { rewriteTaskFingerprint: '1'.repeat(64), rewriteResponseFingerprint: '2'.repeat(64), deterministicArtifactFingerprint: deterministic.artifactFingerprint, sourceHash: deterministic.sourceHash, candidateHash: deterministic.candidateHash, profileId: deterministic.profileId, profileRevisionDigest: deterministic.profileRevisionDigest, rulesetVersion: deterministic.rulesetVersion, schemaVersion: '1' };
|
|
122
|
-
const receipt = { version: '1', taskFingerprint: binding.rewriteTaskFingerprint, responseFingerprint: binding.rewriteResponseFingerprint, adapterIds: [], replacementSentenceIds: [1] };
|
|
123
|
-
const prepared = prepareLifecycleForMcp(JSON.stringify(deterministic), JSON.stringify(binding), JSON.stringify(receipt), 'normal', ['action_change']);
|
|
124
|
-
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
125
|
-
const trustStore = { version: '1', audience: '@holdyourvoice/hyv', maxCapabilityLifetimeSeconds: 300, keys: [{ issuer: 'host', keyId: 'k1', publicKeySpki: publicKey.export({ type: 'spki', format: 'der' }).toString('base64url'), status: 'active' }] };
|
|
126
|
-
const context = { now: 150, trustStore, authorizedSemanticEvaluatorIds: { normal: ['reviewer-1'], highAssurance: [] }, authorizedHumanFinalizerIds: ['human-1'] };
|
|
127
|
-
const submitted = submitSemanticVerdictForMcp(JSON.stringify(prepared.artifact), JSON.stringify(prepared.task), 'reviewer-1', JSON.stringify({ approved: true, violations: [] }), context);
|
|
128
|
-
assert.equal(submitted.ok, true);
|
|
129
|
-
if (!submitted.ok)
|
|
130
|
-
return;
|
|
131
|
-
const claims = { version: '1', purpose: 'hyv.final-approval', issuer: 'host', audience: '@holdyourvoice/hyv', subjectArtifactFingerprint: submitted.artifact.artifactFingerprint, sourceHash: binding.sourceHash, candidateHash: binding.candidateHash, profileId: binding.profileId, profileRevisionDigest: binding.profileRevisionDigest, keyId: 'k1', issuedAt: 100, notBefore: 100, expiresAt: 200, nonce: 'mcp-secret-nonce' };
|
|
132
|
-
const payload = Buffer.from(canonicalJson(claims));
|
|
133
|
-
const capability = { payload: payload.toString('base64url'), signature: sign(null, payload, privateKey).toString('base64url') };
|
|
134
|
-
const validated = validateFinalApprovalForMcp(JSON.stringify(submitted.artifact), canonicalJson(capability), context);
|
|
135
|
-
assert.equal(validated.ok, true);
|
|
136
|
-
const finalized = finalizeLifecycleForMcp(JSON.stringify(submitted.artifact), JSON.stringify({ evaluatorId: 'human-1', decision: 'approve' }), context, canonicalJson(capability));
|
|
137
|
-
assert.equal(finalized.ok && finalized.artifact.status, 'approved');
|
|
138
|
-
assert.throws(() => finalizeLifecycleForMcp(JSON.stringify(submitted.artifact), JSON.stringify({ evaluatorId: 'human-1', decision: 'reject' }), context, canonicalJson(capability)), /does not accept/);
|
|
139
|
-
assert.doesNotMatch(JSON.stringify({ validated, finalized }), /mcp-secret-nonce|payload|signature/);
|
|
140
|
-
});
|
|
141
|
-
test('records approved learning once and treats an exact replay as a no-op', () => {
|
|
142
|
-
const root = mkdtempSync(join(tmpdir(), 'holdyourvoice-mcp-approved-'));
|
|
143
|
-
const previousHome = process.env.HYV_HOME;
|
|
144
|
-
process.env.HYV_HOME = root;
|
|
145
|
-
try {
|
|
146
|
-
const source = 'I leverage the answer with useful detail and clear mechanism.';
|
|
147
|
-
const candidate = 'I use the answer with useful detail and clear mechanism.';
|
|
148
|
-
const rewriteTask = prepareRewriteForMcp(source, profileJson);
|
|
149
|
-
const evaluation = applyRewriteForMcp(JSON.stringify(rewriteTask), JSON.stringify({
|
|
150
|
-
version: '1', taskFingerprint: rewriteTask.fingerprint, replacements: [{ sentenceId: 1, text: candidate }],
|
|
151
|
-
}), profileJson);
|
|
152
|
-
assert.equal(evaluation.status, 'needs_semantic_review');
|
|
153
|
-
const prepared = prepareLifecycleForMcp(JSON.stringify(evaluation.deterministicArtifact), JSON.stringify(evaluation.lifecycleBinding), JSON.stringify(evaluation.receipt), 'normal', []);
|
|
154
|
-
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
155
|
-
const trustStore = { version: '1', audience: '@holdyourvoice/hyv', maxCapabilityLifetimeSeconds: 300, keys: [{ issuer: 'host', keyId: 'k1', publicKeySpki: publicKey.export({ type: 'spki', format: 'der' }).toString('base64url'), status: 'active' }] };
|
|
156
|
-
const context = { now: 150, trustStore, authorizedSemanticEvaluatorIds: { normal: ['reviewer-1'], highAssurance: [] }, authorizedHumanFinalizerIds: ['human-1'] };
|
|
157
|
-
const submitted = submitSemanticVerdictForMcp(JSON.stringify(prepared.artifact), JSON.stringify(prepared.task), 'reviewer-1', JSON.stringify({ approved: true, violations: [] }), context);
|
|
158
|
-
assert.equal(submitted.ok, true);
|
|
159
|
-
if (!submitted.ok)
|
|
160
|
-
return;
|
|
161
|
-
const binding = submitted.artifact.binding;
|
|
162
|
-
const claims = { version: '1', purpose: 'hyv.final-approval', issuer: 'host', audience: '@holdyourvoice/hyv', subjectArtifactFingerprint: submitted.artifact.artifactFingerprint, sourceHash: binding.sourceHash, candidateHash: binding.candidateHash, profileId: binding.profileId, profileRevisionDigest: binding.profileRevisionDigest, keyId: 'k1', issuedAt: 100, notBefore: 100, expiresAt: 200, nonce: 'approved-learning-secret' };
|
|
163
|
-
const payload = Buffer.from(canonicalJson(claims));
|
|
164
|
-
const capability = canonicalJson({ payload: payload.toString('base64url'), signature: sign(null, payload, privateKey).toString('base64url') });
|
|
165
|
-
const decision = JSON.stringify({ evaluatorId: 'human-1', decision: 'approve' });
|
|
166
|
-
const finalized = finalizeLifecycleForMcp(JSON.stringify(submitted.artifact), decision, context, capability);
|
|
167
|
-
assert.equal(finalized.ok, true);
|
|
168
|
-
if (!finalized.ok)
|
|
169
|
-
return;
|
|
170
|
-
const request = { readyJson: JSON.stringify(submitted.artifact), approvedJson: JSON.stringify(finalized.artifact), source, candidate, profileJson, decisionJson: decision, capabilityJson: capability, context };
|
|
171
|
-
assert.equal(recordApprovedLearningForMcp(request), 'recorded');
|
|
172
|
-
assert.equal(recordApprovedLearningForMcp(request), 'nothing_to_learn');
|
|
173
|
-
const stored = JSON.stringify(inspectLearningForMcp(profileJson, { root }));
|
|
174
|
-
assert.equal(JSON.parse(stored).length, 1);
|
|
175
|
-
assert.doesNotMatch(stored, /approved-learning-secret|payload|signature|I leverage|I use/);
|
|
176
|
-
}
|
|
177
|
-
finally {
|
|
178
|
-
if (previousHome === undefined)
|
|
179
|
-
delete process.env.HYV_HOME;
|
|
180
|
-
else
|
|
181
|
-
process.env.HYV_HOME = previousHome;
|
|
182
|
-
rmSync(root, { recursive: true, force: true });
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
test('exposes text-free learning inspection and explicit mutations through MCP helpers', () => {
|
|
186
|
-
const root = mkdtempSync(join(tmpdir(), 'holdyourvoice-mcp-learning-'));
|
|
187
|
-
const unsigned = { ...profile, version: '3', id: 'founder.test', revision: 1, provenance: { source: 'test', rights: 'test', createdAt: '2026-08-13T00:00:00.000Z' }, rulePolicy: {}, fingerprint: { contractionRate: 0, sentenceLengthDistribution: { short: 1, medium: 0, long: 0 }, bulletRate: 0, enDashRate: 0 }, tolerances: { contractionRate: { absolute: 0.1, calibrated: false }, sentenceLengthDistribution: { absolute: 0.1, calibrated: false }, bulletRate: { absolute: 0.1, calibrated: false }, enDashRate: { absolute: 0.1, calibrated: false } }, metricFixtures: { contractionRate: ['test'], sentenceLengthDistribution: ['test'], bulletRate: ['test'], enDashRate: ['test'] } };
|
|
188
|
-
const canonical = (value) => Array.isArray(value) ? `[${value.map(canonical).join(',')}]` : value && typeof value === 'object' ? `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}` : JSON.stringify(value);
|
|
189
|
-
const profileV3 = { ...unsigned, revisionDigest: createHash('sha256').update(canonical(unsigned)).digest('hex') };
|
|
190
|
-
const v3Json = JSON.stringify(profileV3);
|
|
191
|
-
try {
|
|
192
|
-
const recorded = recordLearningForMcp(v3Json, 'Keep the mechanism concrete.', { root, mutationId: 'm1', authority: 'founder', provenance: 'editor-review', weight: 2, compatibility: 'exact' });
|
|
193
|
-
assert.equal(recorded.status, 'recorded');
|
|
194
|
-
assert.equal(recordLearningForMcp(v3Json, 'Different instruction.', { root, mutationId: 'm1' }).status, 'conflict');
|
|
195
|
-
const inspection = inspectLearningForMcp(v3Json, { root });
|
|
196
|
-
assert.equal(inspection[0]?.eventType, 'instruction');
|
|
197
|
-
assert.equal(JSON.stringify(inspection).includes('Keep the mechanism concrete.'), false);
|
|
198
|
-
assert.equal(ratifyLearningForMcp(v3Json, recorded.eventId, { root, mutationId: 'm2', authority: 'founder' }).status, 'recorded');
|
|
199
|
-
assert.equal(supersedeLearningForMcp(v3Json, recorded.eventId, { root, mutationId: 'm3', authority: 'founder' }).status, 'recorded');
|
|
200
|
-
assert.equal(clearLearningForMcp(v3Json, { root }).cleared, true);
|
|
201
|
-
}
|
|
202
|
-
finally {
|
|
203
|
-
rmSync(root, { recursive: true, force: true });
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
test('exposes the executable pattern IDs through MCP tools', () => {
|
|
207
|
-
const catalog = patternsForMcp();
|
|
208
|
-
assert.ok(catalog.rules.some((rule) => rule.id === 'ai.leverage'));
|
|
209
|
-
});
|
|
210
|
-
test('fails closed on changed CopySpec claims through MCP tools', () => {
|
|
211
|
-
const result = verifyCopySpecForMcp('The launch is on 14 August.', 'The launch is next month.', profileJson, JSON.stringify({
|
|
212
|
-
version: '1', audience: 'operators', intent: 'explain', channel: 'email',
|
|
213
|
-
claims: [{ id: 'launch-date', text: 'The launch is on 14 August.', evidence: 'Release calendar.' }],
|
|
214
|
-
}));
|
|
215
|
-
assert.equal(result.passed, false);
|
|
216
|
-
assert.equal(result.claims.failures[0]?.code, 'missing_immutable_claim');
|
|
217
|
-
assert.equal('learning' in result, false);
|
|
218
|
-
});
|
|
219
|
-
test('allows declared CopySpec atoms to survive a split MCP rewrite', () => {
|
|
220
|
-
const spec = JSON.stringify({
|
|
221
|
-
version: '1', audience: 'operators', intent: 'explain', channel: 'email',
|
|
222
|
-
claims: [{ id: 'model-size', text: 'Kimi K2.6 has 600 GB of INT4 weights.', atoms: ['Kimi K2.6 uses INT4 weights', 'payload is 600 GB'], evidence: 'Technical report.' }],
|
|
223
|
-
});
|
|
224
|
-
const preserved = verifyCopySpecForMcp('Kimi K2.6 has 600 GB of INT4 weights.', 'Kimi K2.6 uses INT4 weights. The payload is 600 GB.', profileJson, spec);
|
|
225
|
-
assert.equal(preserved.claims.passed, true);
|
|
226
|
-
const missing = verifyCopySpecForMcp('Kimi K2.6 has 600 GB of INT4 weights.', 'Kimi K2.6 uses INT4 weights.', profileJson, spec);
|
|
227
|
-
assert.deepEqual(missing.claims.failures.map((failure) => failure.code), ['missing_immutable_atom']);
|
|
228
|
-
});
|
|
229
|
-
test('prepares and applies the rewrite task through MCP helpers', () => {
|
|
230
|
-
const task = prepareRewriteForMcp('I leverage the answer with useful detail and clear mechanism.', profileJson);
|
|
231
|
-
const result = applyRewriteForMcp(JSON.stringify(task), JSON.stringify({
|
|
232
|
-
version: '1', taskFingerprint: task.fingerprint, replacements: [{ sentenceId: 1, text: 'I use the answer with useful detail and clear mechanism.' }],
|
|
233
|
-
}), profileJson);
|
|
234
|
-
assert.equal(result.status, 'needs_semantic_review');
|
|
235
|
-
assert.equal(result.candidate, 'I use the answer with useful detail and clear mechanism.');
|
|
236
|
-
});
|
|
237
|
-
test('prepares and reduces judgment envelopes through MCP helpers', () => {
|
|
238
|
-
const draft = 'I leverage the answer.';
|
|
239
|
-
const envelopes = ['triage', 'argument', 'form'].map((kind) => {
|
|
240
|
-
const task = prepareJudgmentForMcp('pre-edit', kind, draft, profileJson);
|
|
241
|
-
return {
|
|
242
|
-
version: '1', stage: 'pre-edit', judgmentType: kind, taskFingerprint: task.taskFingerprint,
|
|
243
|
-
bindings: { ...task.bindings, evaluatorId: 'writer.1' }, findings: [], decision: 'SHIP',
|
|
244
|
-
};
|
|
245
|
-
});
|
|
246
|
-
assert.equal(reduceJudgmentForMcp(JSON.stringify(envelopes)).decision, 'SHIP');
|
|
247
|
-
});
|
|
248
|
-
test('prepares and evaluates authorized rebuild through MCP helpers', () => {
|
|
249
|
-
const draft = 'I leverage the answer. The launch is on 14 August.';
|
|
250
|
-
const envelopes = ['triage', 'argument', 'form'].map((kind) => {
|
|
251
|
-
const task = prepareJudgmentForMcp('pre-edit', kind, draft, profileJson);
|
|
252
|
-
return {
|
|
253
|
-
version: '1', stage: 'pre-edit', judgmentType: kind, taskFingerprint: task.taskFingerprint,
|
|
254
|
-
bindings: { ...task.bindings, evaluatorId: 'writer.1' }, findings: kind === 'argument' ? [] : [], decision: kind === 'argument' ? 'REBUILD' : 'SHIP',
|
|
255
|
-
};
|
|
256
|
-
});
|
|
257
|
-
const reduction = reduceJudgmentForMcp(JSON.stringify(envelopes));
|
|
258
|
-
assert.equal(reduction.decision, 'REBUILD');
|
|
259
|
-
if (!('recommendationFingerprint' in reduction))
|
|
260
|
-
throw new Error('expected a pre-edit rebuild recommendation');
|
|
261
|
-
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
262
|
-
const trustStore = { version: '1', audience: '@holdyourvoice/hyv', maxCapabilityLifetimeSeconds: 300, keys: [{ issuer: 'host.example', keyId: 'key-1', publicKeySpki: publicKey.export({ format: 'der', type: 'spki' }).toString('base64url'), status: 'active' }] };
|
|
263
|
-
const sourceHash = createHash('sha256').update(draft).digest('hex');
|
|
264
|
-
const identity = `legacy-v2:${createHash('sha256').update(canonicalJson(profile)).digest('hex')}`;
|
|
265
|
-
const claims = {
|
|
266
|
-
version: '1', purpose: 'hyv.rebuild-authorization', issuer: 'host.example', audience: '@holdyourvoice/hyv',
|
|
267
|
-
subjectArtifactFingerprint: reduction.recommendationFingerprint, sourceHash, candidateHash: sourceHash,
|
|
268
|
-
profileId: identity, profileRevisionDigest: identity, keyId: 'key-1', issuedAt: 100, notBefore: 100, expiresAt: 200, nonce: 'mcp-rebuild',
|
|
269
|
-
};
|
|
270
|
-
const payload = Buffer.from(canonicalJson(claims));
|
|
271
|
-
const capability = { payload: payload.toString('base64url'), signature: sign(null, payload, privateKey).toString('base64url') };
|
|
272
|
-
const copySpec = JSON.stringify({ version: '1', audience: 'operators', intent: 'explain', channel: 'email', claims: [{ id: 'launch-date', text: 'The launch is on 14 August.', evidence: 'Release calendar, 7 August.' }] });
|
|
273
|
-
const context = { now: 150, trustStore, authorizedSemanticEvaluatorIds: { normal: [], highAssurance: [] }, authorizedHumanFinalizerIds: [] };
|
|
274
|
-
const policy = JSON.stringify({ version: '1', mode: 'meaning-first', lexicalResidual: { ngramSize: 5, maxSharedNgramFraction: 0, maxLongestSharedRunTokens: 4 }, acknowledgement: 'Measures shared wording only; does not detect or prove removal of a watermark.' });
|
|
275
|
-
const task = prepareRebuildForMcp(draft, profileJson, JSON.stringify(reduction), copySpec, JSON.stringify(capability), context, undefined, policy);
|
|
276
|
-
assert.doesNotMatch(task.prompt, /I leverage the answer/);
|
|
277
|
-
const writerRequest = rebuildWriterRequestForMcp(JSON.stringify(task));
|
|
278
|
-
assert.doesNotMatch(JSON.stringify(writerRequest), /I leverage the answer|capability|profile/i);
|
|
279
|
-
const result = applyRebuildForMcp(JSON.stringify(task), JSON.stringify({
|
|
280
|
-
version: '1', mode: 'REBUILD', taskFingerprint: task.fingerprint,
|
|
281
|
-
candidate: 'Ship planning now treats one calendar fact as fixed. The launch is on 14 August. Every other sentence in this note is new operational language for the release desk.',
|
|
282
|
-
}), profileJson, JSON.stringify(capability), context);
|
|
283
|
-
assert.equal(result.status, 'needs_semantic_review');
|
|
284
|
-
assert.equal(result.receipt.mode, 'REBUILD');
|
|
285
|
-
assert.equal(result.receipt.lexicalResidual?.passed, true);
|
|
286
|
-
});
|
package/dist/mcp.test.js
DELETED
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { createHash, generateKeyPairSync, sign } from 'node:crypto';
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
4
|
-
import { once } from 'node:events';
|
|
5
|
-
import { chmodSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
6
|
-
import { tmpdir } from 'node:os';
|
|
7
|
-
import { join } from 'node:path';
|
|
8
|
-
import test from 'node:test';
|
|
9
|
-
import { profileFingerprint } from './learning.js';
|
|
10
|
-
import { buildProfile } from './voice-dna.js';
|
|
11
|
-
import { canonicalJson } from './canonical-json.js';
|
|
12
|
-
import { applyRewriteForMcp, prepareLifecycleForMcp, prepareRewriteForMcp } from './mcp-tools.js';
|
|
13
|
-
function profileV3Json() {
|
|
14
|
-
const profile = buildProfile(['I write plainly. I name the work.', 'I keep the mechanism clear. I avoid filler.'], ['leverage']);
|
|
15
|
-
const unsigned = { ...profile, version: '3', id: 'founder.test', revision: 1, provenance: { source: 'test', rights: 'test', createdAt: '2026-08-13T00:00:00.000Z' }, rulePolicy: {}, fingerprint: { contractionRate: 0, sentenceLengthDistribution: { short: 1, medium: 0, long: 0 }, bulletRate: 0, enDashRate: 0 }, tolerances: { contractionRate: { absolute: 0.1, calibrated: false }, sentenceLengthDistribution: { absolute: 0.1, calibrated: false }, bulletRate: { absolute: 0.1, calibrated: false }, enDashRate: { absolute: 0.1, calibrated: false } }, metricFixtures: { contractionRate: ['test'], sentenceLengthDistribution: ['test'], bulletRate: ['test'], enDashRate: ['test'] } };
|
|
16
|
-
const canonical = (value) => Array.isArray(value) ? `[${value.map(canonical).join(',')}]` : value && typeof value === 'object' ? `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}` : JSON.stringify(value);
|
|
17
|
-
return JSON.stringify({ ...unsigned, revisionDigest: createHash('sha256').update(canonical(unsigned)).digest('hex') });
|
|
18
|
-
}
|
|
19
|
-
async function callMcp(tools, env = process.env) {
|
|
20
|
-
const server = spawn(process.execPath, [new URL('./cli.js', import.meta.url).pathname, 'mcp'], { stdio: ['pipe', 'pipe', 'pipe'], env });
|
|
21
|
-
let stdout = '';
|
|
22
|
-
let stderr = '';
|
|
23
|
-
server.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
24
|
-
server.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
25
|
-
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'test', version: '1.0.0' } } })}\n`);
|
|
26
|
-
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized', params: {} })}\n`);
|
|
27
|
-
tools.forEach((tool, index) => server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: index + 2, method: 'tools/call', params: tool })}\n`));
|
|
28
|
-
server.stdin.end();
|
|
29
|
-
const [code] = await once(server, 'close');
|
|
30
|
-
assert.equal(code, 0);
|
|
31
|
-
assert.equal(stderr, '');
|
|
32
|
-
return stdout.trim().split('\n').map((line) => JSON.parse(line)).filter((response) => response.id >= 2).sort((a, b) => a.id - b.id);
|
|
33
|
-
}
|
|
34
|
-
function toolPayload(response) {
|
|
35
|
-
return JSON.parse(response.result?.content?.[0]?.text ?? '{}');
|
|
36
|
-
}
|
|
37
|
-
function installedContextEnvironment(root, context) {
|
|
38
|
-
const home = join(root, 'home');
|
|
39
|
-
const config = join(home, '.config', 'holdyourvoice');
|
|
40
|
-
mkdirSync(config, { recursive: true, mode: 0o700 });
|
|
41
|
-
const contextPath = join(config, 'approval-context.json');
|
|
42
|
-
writeFileSync(contextPath, canonicalJson(context), { mode: 0o600 });
|
|
43
|
-
chmodSync(contextPath, 0o600);
|
|
44
|
-
const fakeOs = join(root, 'fake-os.mjs');
|
|
45
|
-
const hooks = join(root, 'hooks.mjs');
|
|
46
|
-
const register = join(root, 'register.mjs');
|
|
47
|
-
writeFileSync(fakeOs, `import * as actual from 'node:os'; export const userInfo = () => ({ ...actual.userInfo(), homedir: process.env.HYV_TEST_HOME });\n`);
|
|
48
|
-
writeFileSync(hooks, `export async function resolve(specifier, context, nextResolve) { if (specifier === 'node:os' && context.parentURL?.endsWith('/approval-context.js')) return { url: new URL('./fake-os.mjs', import.meta.url).href, shortCircuit: true }; return nextResolve(specifier, context); }\n`);
|
|
49
|
-
writeFileSync(register, `import { register } from 'node:module'; register(new URL('./hooks.mjs', import.meta.url));\n`);
|
|
50
|
-
return { ...process.env, NODE_NO_WARNINGS: '1', NODE_OPTIONS: `--import=${register}`, HYV_TEST_HOME: home };
|
|
51
|
-
}
|
|
52
|
-
test('serves local Claude tools over stdio', async () => {
|
|
53
|
-
const server = spawn(process.execPath, [new URL('./cli.js', import.meta.url).pathname, 'mcp'], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
54
|
-
let stdout = '';
|
|
55
|
-
let stderr = '';
|
|
56
|
-
server.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
57
|
-
server.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
58
|
-
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'test', version: '1.0.0' } } })}\n`);
|
|
59
|
-
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized', params: {} })}\n`);
|
|
60
|
-
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} })}\n`);
|
|
61
|
-
server.stdin.end();
|
|
62
|
-
const [code] = await once(server, 'close');
|
|
63
|
-
assert.equal(stderr, '');
|
|
64
|
-
assert.equal(code, 0);
|
|
65
|
-
const responses = stdout.trim().split('\n').map((line) => JSON.parse(line));
|
|
66
|
-
const tools = responses.find((response) => response.id === 2)?.result?.tools;
|
|
67
|
-
assert.deepEqual(tools?.map((tool) => tool.name), ['hyv_build_profile', 'hyv_profile_assess', 'hyv_analyze', 'hyv_strict_check', 'hyv_score', 'hyv_backtest', 'hyv_evaluate_local', 'hyv_hygiene', 'hyv_inspect_hidden_text', 'hyv_apply_hidden_text_policy', 'hyv_final_check', 'hyv_delivery_check', 'hyv_fact_lint', 'hyv_mcp_capabilities', 'hyv_logic_lint', 'hyv_rewrite_prompt', 'hyv_find_writing_examples', 'hyv_prepare_rewrite', 'hyv_apply_rewrite', 'hyv_prepare_judgment', 'hyv_reduce_judgment', 'hyv_verify', 'hyv_verify_copy_spec', 'hyv_batch_analyze', 'hyv_patterns', 'hyv_learning_inspect', 'hyv_learning_record', 'hyv_learning_ratify', 'hyv_learning_supersede', 'hyv_learning_migrate', 'hyv_learning_clear', 'hyv_lifecycle_prepare_semantic', 'hyv_lifecycle_submit_verdict', 'hyv_lifecycle_inspect', 'hyv_lifecycle_finalize']);
|
|
68
|
-
assert.ok(tools?.filter((tool) => !['hyv_verify', 'hyv_verify_copy_spec', 'hyv_apply_hidden_text_policy', 'hyv_learning_record', 'hyv_learning_ratify', 'hyv_learning_supersede', 'hyv_learning_migrate', 'hyv_learning_clear'].includes(tool.name)).every((tool) => tool.annotations?.readOnlyHint));
|
|
69
|
-
assert.equal(tools?.find((tool) => tool.name === 'hyv_verify')?.annotations?.readOnlyHint, true);
|
|
70
|
-
assert.equal(tools?.find((tool) => tool.name === 'hyv_verify_copy_spec')?.annotations?.readOnlyHint, true);
|
|
71
|
-
assert.equal(tools?.some((tool) => tool.name === 'hyv_lifecycle_validate_final_approval'), false);
|
|
72
|
-
assert.equal(tools?.some((tool) => tool.name === 'hyv_learning_record_approved'), false);
|
|
73
|
-
assert.equal(tools?.some((tool) => tool.name === 'hyv_prepare_rebuild'), false);
|
|
74
|
-
assert.equal(tools?.some((tool) => tool.name === 'hyv_apply_rebuild'), false);
|
|
75
|
-
assert.equal('capability_json' in (tools?.find((tool) => tool.name === 'hyv_lifecycle_finalize')?.inputSchema?.properties ?? {}), false);
|
|
76
|
-
assert.equal(tools?.find((tool) => tool.name === 'hyv_learning_inspect')?.annotations?.readOnlyHint, true);
|
|
77
|
-
assert.equal(tools?.find((tool) => tool.name === 'hyv_inspect_hidden_text')?.annotations?.readOnlyHint, true);
|
|
78
|
-
assert.equal(tools?.find((tool) => tool.name === 'hyv_logic_lint')?.annotations?.readOnlyHint, true);
|
|
79
|
-
assert.equal(tools?.find((tool) => tool.name === 'hyv_fact_lint')?.annotations?.readOnlyHint, true);
|
|
80
|
-
assert.equal(tools?.find((tool) => tool.name === 'hyv_mcp_capabilities')?.annotations?.readOnlyHint, true);
|
|
81
|
-
assert.equal(tools?.find((tool) => tool.name === 'hyv_apply_hidden_text_policy')?.annotations?.readOnlyHint, false);
|
|
82
|
-
assert.equal(tools?.find((tool) => tool.name === 'hyv_learning_clear')?.annotations?.readOnlyHint, false);
|
|
83
|
-
assert.deepEqual(tools?.filter((tool) => tool.name.startsWith('hyv_learning_')).map((tool) => [tool.name, tool.annotations?.readOnlyHint, tool.annotations?.destructiveHint]), [
|
|
84
|
-
['hyv_learning_inspect', true, undefined], ['hyv_learning_record', false, false], ['hyv_learning_ratify', false, false],
|
|
85
|
-
['hyv_learning_supersede', false, true], ['hyv_learning_migrate', false, false], ['hyv_learning_clear', false, true],
|
|
86
|
-
]);
|
|
87
|
-
});
|
|
88
|
-
test('registers capability tools only with host redaction attestation', async () => {
|
|
89
|
-
const server = spawn(process.execPath, [new URL('./cli.js', import.meta.url).pathname, 'mcp'], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, HYV_MCP_SENSITIVE_INPUT_REDACTION: '1' } });
|
|
90
|
-
let stdout = '';
|
|
91
|
-
let stderr = '';
|
|
92
|
-
server.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
93
|
-
server.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
94
|
-
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'test', version: '1.0.0' } } })}\n`);
|
|
95
|
-
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized', params: {} })}\n`);
|
|
96
|
-
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} })}\n`);
|
|
97
|
-
server.stdin.end();
|
|
98
|
-
const [code] = await once(server, 'close');
|
|
99
|
-
assert.equal(code, 0);
|
|
100
|
-
assert.equal(stderr, '');
|
|
101
|
-
const response = stdout.trim().split('\n').map((line) => JSON.parse(line)).find((item) => item.id === 2);
|
|
102
|
-
const names = response.result.tools.map((tool) => tool.name);
|
|
103
|
-
assert.equal(names.length, 40);
|
|
104
|
-
assert.ok(names.includes('hyv_lifecycle_validate_final_approval'));
|
|
105
|
-
assert.ok(names.includes('hyv_learning_record_approved'));
|
|
106
|
-
assert.ok(names.includes('hyv_prepare_rebuild'));
|
|
107
|
-
assert.ok(names.includes('hyv_apply_rebuild'));
|
|
108
|
-
assert.ok(names.includes('hyv_rebuild_writer_request'));
|
|
109
|
-
const finalize = response.result.tools.find((tool) => tool.name === 'hyv_lifecycle_finalize');
|
|
110
|
-
assert.equal('capability_json' in finalize.inputSchema.properties, true);
|
|
111
|
-
});
|
|
112
|
-
test('redacts rebuild prepare transport failures', async () => {
|
|
113
|
-
const secret = 'rebuild-prepare-secret-nonce';
|
|
114
|
-
const responses = await callMcp([{ name: 'hyv_prepare_rebuild', arguments: { draft: 'x', profile_json: '{}', reduction_json: '{}', copy_spec_json: '{}', capability_json: JSON.stringify({ payload: secret, signature: secret }) } }], { ...process.env, HYV_MCP_SENSITIVE_INPUT_REDACTION: '1' });
|
|
115
|
-
const serialized = JSON.stringify(responses[0]);
|
|
116
|
-
assert.equal(responses[0]?.result?.isError, true);
|
|
117
|
-
assert.doesNotMatch(serialized, new RegExp(secret));
|
|
118
|
-
assert.match(serialized, /Rebuild preparation failed/);
|
|
119
|
-
});
|
|
120
|
-
test('redacts rebuild apply transport failures', async () => {
|
|
121
|
-
const secret = 'rebuild-apply-secret-nonce';
|
|
122
|
-
const responses = await callMcp([{ name: 'hyv_apply_rebuild', arguments: { task_json: '{}', response_json: '{}', profile_json: '{}', capability_json: JSON.stringify({ payload: secret, signature: secret }) } }], { ...process.env, HYV_MCP_SENSITIVE_INPUT_REDACTION: '1' });
|
|
123
|
-
const serialized = JSON.stringify(responses[0]);
|
|
124
|
-
assert.equal(responses[0]?.result?.isError, true);
|
|
125
|
-
assert.doesNotMatch(serialized, new RegExp(secret));
|
|
126
|
-
assert.match(serialized, /Rebuild application failed/);
|
|
127
|
-
});
|
|
128
|
-
test('redacts capability-bearing transport failures', async () => {
|
|
129
|
-
const secret = 'transport-secret-nonce';
|
|
130
|
-
const responses = await callMcp([{ name: 'hyv_lifecycle_validate_final_approval', arguments: { artifact_json: '{}', capability_json: JSON.stringify({ payload: secret, signature: secret }) } }], { ...process.env, HYV_MCP_SENSITIVE_INPUT_REDACTION: '1' });
|
|
131
|
-
const serialized = JSON.stringify(responses[0]);
|
|
132
|
-
assert.equal(responses[0]?.result?.isError, true);
|
|
133
|
-
assert.doesNotMatch(serialized, new RegExp(secret));
|
|
134
|
-
assert.match(serialized, /Capability validation failed/);
|
|
135
|
-
});
|
|
136
|
-
test('runs registered lifecycle preparation and inspection over stdio', async () => {
|
|
137
|
-
const base = { version: '1', verificationKind: 'standard', passed: true, analysisVersion: '2', rulesetVersion: '3.2.0', preservationMetricVersion: 'legacy-set-v1', preservationScore: 100, sourceHash: '4'.repeat(64), candidateHash: '5'.repeat(64), profileId: 'founder.primary', profileRevisionDigest: '6'.repeat(64), regressionKeys: [] };
|
|
138
|
-
const deterministic = { ...base, artifactFingerprint: createHash('sha256').update(`hyv:deterministic-verification:v1\0${canonicalJson(base)}`).digest('hex') };
|
|
139
|
-
const binding = { rewriteTaskFingerprint: '1'.repeat(64), rewriteResponseFingerprint: '2'.repeat(64), deterministicArtifactFingerprint: deterministic.artifactFingerprint, sourceHash: deterministic.sourceHash, candidateHash: deterministic.candidateHash, profileId: deterministic.profileId, profileRevisionDigest: deterministic.profileRevisionDigest, rulesetVersion: deterministic.rulesetVersion, schemaVersion: '1' };
|
|
140
|
-
const receipt = { version: '1', taskFingerprint: binding.rewriteTaskFingerprint, responseFingerprint: binding.rewriteResponseFingerprint, adapterIds: [], replacementSentenceIds: [1] };
|
|
141
|
-
const preparedResponse = await callMcp([{ name: 'hyv_lifecycle_prepare_semantic', arguments: { deterministic_json: JSON.stringify(deterministic), binding_json: JSON.stringify(binding), receipt_json: JSON.stringify(receipt), policy: 'normal', allowed_violations: ['action_change'] } }], { ...process.env, HYV_MCP_SENSITIVE_INPUT_REDACTION: '' });
|
|
142
|
-
const prepared = toolPayload(preparedResponse[0]);
|
|
143
|
-
assert.equal(prepared.artifact.status, 'needs_semantic_review');
|
|
144
|
-
const inspectedResponse = await callMcp([{ name: 'hyv_lifecycle_inspect', arguments: { artifact_json: JSON.stringify(prepared.artifact) } }]);
|
|
145
|
-
const inspected = toolPayload(inspectedResponse[0]);
|
|
146
|
-
assert.equal(inspected.artifactFingerprint, prepared.artifact.artifactFingerprint);
|
|
147
|
-
assert.doesNotMatch(JSON.stringify(inspected), /sourceHash|candidateHash|payload|signature|nonce/);
|
|
148
|
-
});
|
|
149
|
-
test('runs signed lifecycle and approved-learning replay through registered stdio tools', async () => {
|
|
150
|
-
const root = mkdtempSync(join(tmpdir(), 'holdyourvoice-mcp-approved-'));
|
|
151
|
-
try {
|
|
152
|
-
const source = 'I leverage the answer with useful detail and clear mechanism.';
|
|
153
|
-
const candidate = 'I use the answer with useful detail and clear mechanism.';
|
|
154
|
-
const profile = buildProfile(['I write plainly. I name the work.', 'I keep the mechanism clear. I avoid filler.'], ['leverage']);
|
|
155
|
-
const profileJson = JSON.stringify(profile);
|
|
156
|
-
const rewriteTask = prepareRewriteForMcp(source, profileJson);
|
|
157
|
-
const evaluation = applyRewriteForMcp(JSON.stringify(rewriteTask), JSON.stringify({ version: '1', taskFingerprint: rewriteTask.fingerprint, replacements: [{ sentenceId: 1, text: candidate }] }), profileJson);
|
|
158
|
-
assert.equal(evaluation.status, 'needs_semantic_review');
|
|
159
|
-
const prepared = prepareLifecycleForMcp(JSON.stringify(evaluation.deterministicArtifact), JSON.stringify(evaluation.lifecycleBinding), JSON.stringify(evaluation.receipt), 'normal', []);
|
|
160
|
-
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
161
|
-
const now = Math.floor(Date.now() / 1000);
|
|
162
|
-
const trustStore = { version: '1', audience: '@holdyourvoice/hyv', maxCapabilityLifetimeSeconds: 300, keys: [{ issuer: 'test-host', keyId: 'test-key', publicKeySpki: publicKey.export({ type: 'spki', format: 'der' }).toString('base64url'), status: 'active' }] };
|
|
163
|
-
const context = { now: 0, trustStore, authorizedSemanticEvaluatorIds: { normal: ['test-reviewer'], highAssurance: [] }, authorizedHumanFinalizerIds: ['test-human'] };
|
|
164
|
-
const env = { ...installedContextEnvironment(root, context), HYV_HOME: join(root, 'state'), HYV_MCP_SENSITIVE_INPUT_REDACTION: '1' };
|
|
165
|
-
const submittedResponse = await callMcp([{ name: 'hyv_lifecycle_submit_verdict', arguments: { artifact_json: JSON.stringify(prepared.artifact), task_json: JSON.stringify(prepared.task), evaluator_id: 'test-reviewer', verdict_json: JSON.stringify({ approved: true, violations: [] }) } }], env);
|
|
166
|
-
const ready = toolPayload(submittedResponse[0]);
|
|
167
|
-
assert.equal(ready.status, 'ready_for_human_review');
|
|
168
|
-
const rejectedResponse = await callMcp([{ name: 'hyv_lifecycle_finalize', arguments: { artifact_json: JSON.stringify(ready), decision_json: JSON.stringify({ evaluatorId: 'test-human', decision: 'reject' }) } }], { ...env, HYV_MCP_SENSITIVE_INPUT_REDACTION: '' });
|
|
169
|
-
const rejected = toolPayload(rejectedResponse[0]);
|
|
170
|
-
assert.equal(rejected.status, 'needs_escalation');
|
|
171
|
-
assert.equal(rejected.reason, 'human_rejection');
|
|
172
|
-
const binding = ready.binding;
|
|
173
|
-
const claims = { version: '1', purpose: 'hyv.final-approval', issuer: 'test-host', audience: '@holdyourvoice/hyv', subjectArtifactFingerprint: ready.artifactFingerprint, sourceHash: binding.sourceHash, candidateHash: binding.candidateHash, profileId: binding.profileId, profileRevisionDigest: binding.profileRevisionDigest, keyId: 'test-key', issuedAt: now - 1, notBefore: now - 1, expiresAt: now + 120, nonce: 'registered-secret-nonce' };
|
|
174
|
-
const payload = Buffer.from(canonicalJson(claims));
|
|
175
|
-
const capability = canonicalJson({ payload: payload.toString('base64url'), signature: sign(null, payload, privateKey).toString('base64url') });
|
|
176
|
-
const decision = JSON.stringify({ evaluatorId: 'test-human', decision: 'approve' });
|
|
177
|
-
const approvalResponses = await callMcp([
|
|
178
|
-
{ name: 'hyv_lifecycle_validate_final_approval', arguments: { artifact_json: JSON.stringify(ready), capability_json: capability } },
|
|
179
|
-
{ name: 'hyv_lifecycle_finalize', arguments: { artifact_json: JSON.stringify(ready), decision_json: decision, capability_json: capability } },
|
|
180
|
-
], env);
|
|
181
|
-
assert.equal(toolPayload(approvalResponses[0]).ok, true);
|
|
182
|
-
const approved = toolPayload(approvalResponses[1]);
|
|
183
|
-
assert.equal(approved.status, 'approved');
|
|
184
|
-
const learningArguments = { ready_json: JSON.stringify(ready), approved_json: JSON.stringify(approved), original: source, candidate, profile_json: profileJson, decision_json: decision, capability_json: capability };
|
|
185
|
-
const learned = await callMcp([
|
|
186
|
-
{ name: 'hyv_learning_record_approved', arguments: learningArguments },
|
|
187
|
-
{ name: 'hyv_learning_record_approved', arguments: learningArguments },
|
|
188
|
-
], env);
|
|
189
|
-
assert.equal(toolPayload(learned[0]).status, 'recorded');
|
|
190
|
-
assert.equal(toolPayload(learned[1]).status, 'nothing_to_learn');
|
|
191
|
-
const serialized = JSON.stringify({ submittedResponse, rejectedResponse, approvalResponses, learned });
|
|
192
|
-
assert.doesNotMatch(serialized, /registered-secret-nonce|payload|signature|I leverage|I use/);
|
|
193
|
-
}
|
|
194
|
-
finally {
|
|
195
|
-
rmSync(root, { recursive: true, force: true });
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
test('runs the registered MCP learning lifecycle and rejects invalid profile versions', async () => {
|
|
199
|
-
const root = mkdtempSync(join(tmpdir(), 'holdyourvoice-mcp-lifecycle-'));
|
|
200
|
-
try {
|
|
201
|
-
const env = { ...process.env, HYV_HOME: root };
|
|
202
|
-
const v2Json = JSON.stringify(buildProfile(['I write plainly. I name the work.', 'I keep the mechanism clear. I avoid filler.'], ['leverage']));
|
|
203
|
-
const v3Json = profileV3Json();
|
|
204
|
-
const first = await callMcp([
|
|
205
|
-
{ name: 'hyv_learning_record', arguments: { profile_json: v3Json, instruction: 'Keep the mechanism concrete.', mutation_id: 'm1', authority: 'founder', provenance: 'review', weight: 2, compatibility: 'exact' } },
|
|
206
|
-
{ name: 'hyv_learning_inspect', arguments: { profile_json: v3Json } },
|
|
207
|
-
{ name: 'hyv_learning_record', arguments: { profile_json: v2Json, instruction: 'Keep the evidence named.', mutation_id: 'legacy-1' } },
|
|
208
|
-
{ name: 'hyv_learning_migrate', arguments: { source_profile_json: v2Json, target_profile_json: v3Json, mutation_id: 'migration-1' } },
|
|
209
|
-
{ name: 'hyv_learning_ratify', arguments: { profile_json: v2Json, event_id: 'invalid' } },
|
|
210
|
-
{ name: 'hyv_learning_migrate', arguments: { source_profile_json: v3Json, target_profile_json: v2Json } },
|
|
211
|
-
{ name: 'hyv_learning_record', arguments: { profile_json: v3Json, instruction: 'Invalid schema.', mutation_id: 'm'.repeat(201) } },
|
|
212
|
-
], env);
|
|
213
|
-
const recorded = toolPayload(first[0]);
|
|
214
|
-
assert.equal(recorded.status, 'recorded');
|
|
215
|
-
assert.equal(toolPayload(first[1])[0].eventType, 'instruction');
|
|
216
|
-
assert.equal(toolPayload(first[2]).status, 'recorded');
|
|
217
|
-
assert.equal(toolPayload(first[3]).status, 'recorded');
|
|
218
|
-
assert.equal(first[4]?.result?.isError, true);
|
|
219
|
-
assert.equal(first[5]?.result?.isError, true);
|
|
220
|
-
assert.equal(first[6]?.result?.isError, true);
|
|
221
|
-
const second = await callMcp([
|
|
222
|
-
{ name: 'hyv_learning_record', arguments: { profile_json: v3Json, instruction: 'Different instruction.', mutation_id: 'm1' } },
|
|
223
|
-
{ name: 'hyv_learning_ratify', arguments: { profile_json: v3Json, event_id: recorded.eventId, mutation_id: 'm2', authority: 'founder' } },
|
|
224
|
-
{ name: 'hyv_learning_supersede', arguments: { profile_json: v3Json, event_id: recorded.eventId, mutation_id: 'm3', authority: 'founder' } },
|
|
225
|
-
{ name: 'hyv_learning_clear', arguments: { profile_json: v3Json } },
|
|
226
|
-
{ name: 'hyv_learning_inspect', arguments: { profile_json: v3Json } },
|
|
227
|
-
], env);
|
|
228
|
-
assert.equal(toolPayload(second[0]).status, 'conflict');
|
|
229
|
-
assert.equal(toolPayload(second[1]).status, 'recorded');
|
|
230
|
-
assert.equal(toolPayload(second[2]).status, 'recorded');
|
|
231
|
-
assert.equal(toolPayload(second[3]).cleared, true);
|
|
232
|
-
assert.deepEqual(toolPayload(second[4]), []);
|
|
233
|
-
}
|
|
234
|
-
finally {
|
|
235
|
-
rmSync(root, { recursive: true, force: true });
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
test('accepts empty text for profile-free hygiene inspection', async () => {
|
|
239
|
-
const server = spawn(process.execPath, [new URL('./cli.js', import.meta.url).pathname, 'mcp'], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
240
|
-
let stdout = '';
|
|
241
|
-
server.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
242
|
-
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'test', version: '1.0.0' } } })}\n`);
|
|
243
|
-
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized', params: {} })}\n`);
|
|
244
|
-
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'hyv_hygiene', arguments: { draft: '' } } })}\n`);
|
|
245
|
-
server.stdin.end();
|
|
246
|
-
const [code] = await once(server, 'close');
|
|
247
|
-
assert.equal(code, 0);
|
|
248
|
-
const responses = stdout.trim().split('\n').map((line) => JSON.parse(line));
|
|
249
|
-
const report = JSON.parse(responses.find((response) => response.id === 2)?.result?.content?.[0]?.text ?? '{}');
|
|
250
|
-
assert.equal(report.suspiciousCount, 0);
|
|
251
|
-
});
|
|
252
|
-
test('gates exact final output through the registered profile-free MCP tool', async () => {
|
|
253
|
-
const server = spawn(process.execPath, [new URL('./cli.js', import.meta.url).pathname, 'mcp'], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
254
|
-
let stdout = '';
|
|
255
|
-
server.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
256
|
-
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'test', version: '1.0.0' } } })}\n`);
|
|
257
|
-
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized', params: {} })}\n`);
|
|
258
|
-
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'hyv_final_check', arguments: { text: 'Exact output.' } } })}\n`);
|
|
259
|
-
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: 'hyv_final_check', arguments: { text: 'Hidden\u200B output.' } } })}\n`);
|
|
260
|
-
server.stdin.end();
|
|
261
|
-
const [code] = await once(server, 'close');
|
|
262
|
-
assert.equal(code, 0);
|
|
263
|
-
const responses = stdout.trim().split('\n').map((line) => JSON.parse(line));
|
|
264
|
-
const accepted = JSON.parse(responses.find((response) => response.id === 2)?.result?.content?.[0]?.text ?? '{}');
|
|
265
|
-
const rejected = JSON.parse(responses.find((response) => response.id === 3)?.result?.content?.[0]?.text ?? '{}');
|
|
266
|
-
assert.equal(accepted.accepted, true);
|
|
267
|
-
assert.equal(accepted.output, 'Exact output.');
|
|
268
|
-
assert.equal(rejected.accepted, false);
|
|
269
|
-
assert.equal('output' in rejected, false);
|
|
270
|
-
});
|
|
271
|
-
test('keeps registered MCP verification read-only', async () => {
|
|
272
|
-
const root = mkdtempSync(join(tmpdir(), 'holdyourvoice-mcp-server-'));
|
|
273
|
-
try {
|
|
274
|
-
const profile = buildProfile(['I write plainly. I name the work.', 'I keep the mechanism clear. I avoid filler.'], ['leverage']);
|
|
275
|
-
const server = spawn(process.execPath, [new URL('./cli.js', import.meta.url).pathname, 'mcp'], {
|
|
276
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
277
|
-
env: { ...process.env, HYV_HOME: root },
|
|
278
|
-
});
|
|
279
|
-
let stdout = '';
|
|
280
|
-
let stderr = '';
|
|
281
|
-
server.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
282
|
-
server.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
283
|
-
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'test', version: '1.0.0' } } })}\n`);
|
|
284
|
-
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized', params: {} })}\n`);
|
|
285
|
-
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'hyv_verify', arguments: { original: 'I leverage the answer with useful detail and clear mechanism.', candidate: 'I use the answer with useful detail and clear mechanism.', profile_json: JSON.stringify(profile) } } })}\n`);
|
|
286
|
-
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: 'hyv_rewrite_prompt', arguments: { draft: 'I use the answer with useful detail and clear mechanism.', profile_json: JSON.stringify(profile) } } })}\n`);
|
|
287
|
-
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 4, method: 'tools/call', params: { name: 'hyv_analyze', arguments: { draft: 'A pattern I keep seeing in founder posts is vague advice.\u200B', profile_json: JSON.stringify(profile), writing_brief_json: JSON.stringify({ version: '1', audience: 'founders', intent: 'start a discussion', format: 'social' }) } } })}\n`);
|
|
288
|
-
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 5, method: 'tools/call', params: { name: 'hyv_batch_analyze', arguments: { drafts: ['The launch needs a clear owner.', 'The launch needs a clear owner.'] } } })}\n`);
|
|
289
|
-
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 6, method: 'tools/call', params: { name: 'hyv_analyze', arguments: { draft: 'Plain draft.', profile_json: JSON.stringify(profile), writing_brief_json: '{' } } })}\n`);
|
|
290
|
-
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 7, method: 'tools/call', params: { name: 'hyv_hygiene', arguments: { draft: 'Plain\u200B draft.' } } })}\n`);
|
|
291
|
-
server.stdin.end();
|
|
292
|
-
const [code] = await once(server, 'close');
|
|
293
|
-
assert.equal(stderr, '');
|
|
294
|
-
assert.equal(code, 0);
|
|
295
|
-
const responses = stdout.trim().split('\n').map((line) => JSON.parse(line));
|
|
296
|
-
const prompt = JSON.parse(responses.find((response) => response.id === 3)?.result?.content?.[0]?.text ?? '{}').prompt;
|
|
297
|
-
const contextual = JSON.parse(responses.find((response) => response.id === 4)?.result?.content?.[0]?.text ?? '{}');
|
|
298
|
-
const batch = JSON.parse(responses.find((response) => response.id === 5)?.result?.content?.[0]?.text ?? '{}');
|
|
299
|
-
const malformed = responses.find((response) => response.id === 6)?.result;
|
|
300
|
-
const hygiene = JSON.parse(responses.find((response) => response.id === 7)?.result?.content?.[0]?.text ?? '{}');
|
|
301
|
-
assert.doesNotMatch(prompt, /Learned local preferences/);
|
|
302
|
-
assert.equal(contextual.editorial.findings[0].id, 'editorial.social.generic-opener');
|
|
303
|
-
assert.equal(contextual.hygiene.suspiciousCount, 1);
|
|
304
|
-
assert.deepEqual(batch.findings.map((finding) => finding.id), ['batch.repeated-opening', 'batch.repeated-ending']);
|
|
305
|
-
assert.equal(malformed?.isError, true);
|
|
306
|
-
assert.equal(hygiene.suspiciousCount, 1);
|
|
307
|
-
assert.equal(existsSync(join(root, 'learning', `${profileFingerprint(profile)}.jsonl`)), false);
|
|
308
|
-
}
|
|
309
|
-
finally {
|
|
310
|
-
rmSync(root, { recursive: true, force: true });
|
|
311
|
-
}
|
|
312
|
-
});
|