b0cd18d797
Квирк 2: новый stripQuotedSpans делает детектор stdout/stderr-редиректа кавычко-осознанным — `>` / `2>` ВНУТРИ кавыченного аргумента (текст коммита с <email>, "2>1") больше не ложно-блокируется; настоящие редиректы (оператор вне кавычек) блокируются как прежде. RED→GREEN, существующие redirect/cd-app кейсы целы. 1A: убрана реклама мёртвых override-фраз (findOverride — заглушка v4, фразы не работают): баннер enforce-prompt-injection (каждый UserPromptSubmit) + block-сообщения enforce-verify-before-push / coverage-verify / memory-coverage / tdd-gate (×3). Каждый фикс залочен негативным тестом. Сознательно НЕ делали: калибровку 6 судьи (читать чат-контекст) и ослабление exact-match approve (квирк 3) — это рубежи защиты, их трогать нельзя. Регрессия vitest tools-only: 1989 passed | 2 skipped (verify через npx vitest run --root app --config vitest.config.tools.mjs). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
90 lines
3.0 KiB
JavaScript
90 lines
3.0 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import { decide } from './enforce-memory-coverage.mjs';
|
|
|
|
function entries(userPrompt, assistantText) {
|
|
const out = [];
|
|
if (userPrompt) out.push({ message: { role: 'user', content: userPrompt } });
|
|
if (assistantText) out.push({ message: { role: 'assistant', content: [{ type: 'text', text: assistantText }] } });
|
|
return out;
|
|
}
|
|
|
|
describe('enforce-memory-coverage / decide', () => {
|
|
it('allows non-memory paths regardless of coverage', () => {
|
|
const r = decide({
|
|
toolName: 'Write',
|
|
filePath: 'tools/foo.mjs',
|
|
transcriptEntries: entries('do it', 'coverage: skill:tdd'),
|
|
});
|
|
expect(r.block).toBe(false);
|
|
});
|
|
|
|
it('blocks memory path with TDD coverage (stale)', () => {
|
|
const r = decide({
|
|
toolName: 'Edit',
|
|
filePath: 'C:\\Users\\x\\.claude\\projects\\proj\\memory\\foo.md',
|
|
transcriptEntries: entries('do', 'coverage: skill:superpowers:test-driven-development'),
|
|
});
|
|
expect(r.block).toBe(true);
|
|
expect(r.message).toMatch(/memory-sync/);
|
|
// 1A (2026-05-31): не рекламировать мёртвую override-фразу (findOverride — заглушка v4).
|
|
expect(r.message).not.toMatch(/Override:/);
|
|
expect(r.message).not.toMatch(/memory dump/);
|
|
});
|
|
|
|
it('blocks memory path with no coverage at all', () => {
|
|
const r = decide({
|
|
toolName: 'Write',
|
|
filePath: '/Users/x/.claude/projects/p/memory/x.md',
|
|
transcriptEntries: entries('do', 'no coverage line here'),
|
|
});
|
|
expect(r.block).toBe(true);
|
|
expect(r.message).toMatch(/NONE/);
|
|
});
|
|
|
|
it('allows memory path with direct:memory-sync coverage', () => {
|
|
const r = decide({
|
|
toolName: 'Edit',
|
|
filePath: 'C:\\Users\\x\\.claude\\projects\\proj\\memory\\foo.md',
|
|
transcriptEntries: entries('do', 'coverage: direct:memory-sync\nок'),
|
|
});
|
|
expect(r.block).toBe(false);
|
|
});
|
|
|
|
it('allows memory path with skill:memory-something coverage', () => {
|
|
const r = decide({
|
|
toolName: 'Edit',
|
|
filePath: '/x/.claude/projects/p/memory/foo.md',
|
|
transcriptEntries: entries('do', 'coverage: skill:memory-coordinator'),
|
|
});
|
|
expect(r.block).toBe(false);
|
|
});
|
|
|
|
it('allows memory path when override phrase present', () => {
|
|
const r = decide({
|
|
toolName: 'Write',
|
|
filePath: '/x/.claude/projects/p/memory/foo.md',
|
|
transcriptEntries: entries('memory dump please', 'no coverage'),
|
|
override: { phrase: 'memory dump', suppresses: ['memory-sync-coverage'] },
|
|
});
|
|
expect(r.block).toBe(false);
|
|
});
|
|
|
|
it('skips non-Edit/Write/MultiEdit tools', () => {
|
|
const r = decide({
|
|
toolName: 'Bash',
|
|
filePath: 'memory/x.md',
|
|
transcriptEntries: entries('do', 'no coverage'),
|
|
});
|
|
expect(r.block).toBe(false);
|
|
});
|
|
|
|
it('matches MEMORY.md anywhere', () => {
|
|
const r = decide({
|
|
toolName: 'Edit',
|
|
filePath: '/whatever/MEMORY.md',
|
|
transcriptEntries: entries('do', 'coverage: skill:tdd'),
|
|
});
|
|
expect(r.block).toBe(true);
|
|
});
|
|
});
|