// 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/); }); });