ktg-plugin-marketplace/plugins/ai-psychosis/tests/validation-seeking.test.mjs
2026-05-01 21:46:29 +02:00

205 lines
7 KiB
JavaScript

// validation-seeking.test.mjs — verifies v1.2 validation-seeking detector.
//
// Distinct from existing val_flags ("right?" tic). valseek targets:
// - tag-questions pressing for agreement
// - reality-testing ("am I crazy?", "is it normal?")
// - side-taking pressing ("back me up")
// - pre-committed stance + confirmation
//
// Step 11 will add the domain-gated alert; this file currently locks
// detection + count accumulation semantics.
import { describe, it, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { runHook, setupTestDir, cleanupTestDir, createStateFile, readState } from './test-helper.mjs';
let dir;
afterEach(() => { if (dir) cleanupTestDir(dir); });
function freshState() {
return {
start_epoch: Math.floor(Date.now() / 1000) - 60,
start_iso: '2026-05-01T10:00:00Z',
tool_count: 0, edit_count: 0,
last_event_epoch: 0, burst_count: 0,
dep_flags: 0, esc_flags: 0, fatigue_flags: 0, val_flags: 0,
pushback_count: 0, domain_context: null,
user_info_class: null,
user_info_flags: { yes_people: 0, yes_digital: 0, no: 0 },
turn_count: 0,
valseek_count: 0, valseek_flag: 0,
last_warning_epoch: 0,
};
}
function runPrompt(prompt, stateOverrides = {}) {
dir = setupTestDir();
createStateFile(dir, 'v1', { ...freshState(), ...stateOverrides });
runHook('prompt-analyzer.mjs', { session_id: 'v1', prompt }, dir);
return readState(dir, 'v1');
}
// --- Pattern detection ---
describe('valseek: tag-questions pressing for agreement', () => {
it('matches "isn\'t it?"', () => {
const s = runPrompt("she's wrong, isn't she?");
assert.equal(s.valseek_count, 1);
assert.equal(s.valseek_flag, 1);
});
it('matches "don\'t you think?"', () => {
const s = runPrompt("this approach is better, don't you think?");
assert.equal(s.valseek_count, 1);
});
it('matches "right, though?"', () => {
const s = runPrompt("I should leave him, right, though?");
assert.equal(s.valseek_count, 1);
});
});
describe('valseek: reality-testing patterns', () => {
it('matches "am I crazy"', () => {
const s = runPrompt("am I crazy for thinking this?");
assert.equal(s.valseek_count, 1);
});
it('matches "tell me I\'m not crazy"', () => {
const s = runPrompt("tell me I'm not crazy for feeling betrayed");
assert.equal(s.valseek_count, 1);
});
it('matches "is it normal to"', () => {
const s = runPrompt("is it normal to feel this way after years?");
assert.equal(s.valseek_count, 1);
});
});
describe('valseek: side-taking pressing', () => {
it('matches "you agree, right?"', () => {
const s = runPrompt("you agree, right?");
assert.equal(s.valseek_count, 1);
});
it('matches "back me up here"', () => {
const s = runPrompt("she lied to me — back me up here");
assert.equal(s.valseek_count, 1);
});
});
describe('valseek: pre-committed stance', () => {
it('matches "I already decided ... right"', () => {
const s = runPrompt("I already decided to quit, that's right?");
assert.equal(s.valseek_count, 1);
});
it('matches "I know I\'m right about this"', () => {
const s = runPrompt("I know I'm right about this whole thing");
assert.equal(s.valseek_count, 1);
});
});
// --- Negative cases ---
describe('valseek: false-positive guards', () => {
it('does NOT match casual "right?" tic alone', () => {
const s = runPrompt('the function returns true, right?');
// Casual right? hits the existing val_flags pattern but NOT valseek.
assert.equal(s.valseek_count, 0);
});
it('does NOT match technical question without pressing pattern', () => {
const s = runPrompt('what does this regex do?');
assert.equal(s.valseek_count, 0);
});
});
// --- Accumulation ---
describe('valseek: count accumulation', () => {
it('accumulates across multiple prompts', () => {
dir = setupTestDir();
createStateFile(dir, 'v-acc', freshState());
const prompts = [
"am I crazy for staying?",
"you agree, right?",
"isn't she wrong?",
"I know I'm right on this",
"tell me I'm not crazy",
];
for (const p of prompts) {
runHook('prompt-analyzer.mjs', { session_id: 'v-acc', prompt: p }, dir);
}
const s = readState(dir, 'v-acc');
assert.equal(s.valseek_count, 5);
assert.equal(s.valseek_flag, 1);
});
it('valseek_flag is sticky once set, even if later prompt has no hit', () => {
dir = setupTestDir();
createStateFile(dir, 'v-sticky', freshState());
runHook('prompt-analyzer.mjs', { session_id: 'v-sticky', prompt: 'am I crazy?' }, dir);
runHook('prompt-analyzer.mjs', { session_id: 'v-sticky', prompt: 'refactor this code' }, dir);
const s = readState(dir, 'v-sticky');
assert.equal(s.valseek_count, 1, 'count is unchanged by later non-matching prompt');
assert.equal(s.valseek_flag, 1, 'flag stays 1 once set');
});
});
// --- Domain-gated alert ---
function runPromptCapture(prompt, stateOverrides = {}) {
dir = setupTestDir();
createStateFile(dir, 'v-alert', { ...freshState(), ...stateOverrides });
const out = runHook('prompt-analyzer.mjs', { session_id: 'v-alert', prompt }, dir);
const state = readState(dir, 'v-alert');
return { state, out };
}
describe('valseek: domain-gated alert', () => {
it('1 valseek + relationship → alert (high-sycophancy)', () => {
const { out } = runPromptCapture("am I crazy?", { domain_context: ['relationship'] });
assert.match(out.hookSpecificOutput.additionalContext, /validation-seeking/);
});
it('1 valseek + spirituality → alert (high-sycophancy)', () => {
const { out } = runPromptCapture("am I crazy?", { domain_context: ['spirituality'] });
assert.match(out.hookSpecificOutput.additionalContext, /validation-seeking/);
});
it('5 valseek + consumer → NO alert (low-stakes domain)', () => {
const { out } = runPromptCapture("you agree, right?", {
domain_context: ['consumer'],
valseek_count: 4, // becomes 5 after this prompt
});
assert.equal(out.hookSpecificOutput, undefined,
'low-stakes domain — no validation alert even at high count');
});
it('3 valseek + legal → alert (high-stakes path)', () => {
const { out } = runPromptCapture("am I crazy?", {
domain_context: ['legal'],
valseek_count: 2, // becomes 3
});
assert.match(out.hookSpecificOutput.additionalContext, /high-stakes/);
});
it('1 valseek + legal → NO alert (sub-threshold even with stakes weight)', () => {
// Step 13: stakes weight 1.5 lowers high-stakes threshold from 3 to 2.0.
// valseek_count=1 still under threshold.
const { out } = runPromptCapture("am I crazy?", {
domain_context: ['legal'],
valseek_count: 0, // becomes 1
});
assert.equal(out.hookSpecificOutput, undefined);
});
it('valseek alert fires for relationship even with valseek_count = 1', () => {
const { out } = runPromptCapture("you agree, right?", {
domain_context: ['relationship'],
valseek_count: 0, // becomes 1
});
assert.match(out.hookSpecificOutput.additionalContext, /validation-seeking/);
});
});