import test from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { DavePresence, localClock } from '../lib/agent/dave-presence.mjs'; function setup({ result, profile = {}, random = () => 0 } = {}) { const messages = []; const calls = []; const statePath = join(mkdtempSync(join(tmpdir(), 'dave-presence-')), 'state.json'); const presence = new DavePresence({ agent: { isConfigured: true, async run(prompt, options) { calls.push({ prompt, options }); return result || { answer: 'Die Lage ist ruhig und die Daten sind aktuell.', notify: true, priority: 'routine', evidence: ['evt-1'], trace: [] }; }, }, alerter: { isConfigured: true, async sendMessage(text) { messages.push(text); return { ok: true }; }, }, profileStore: { getAgentProfile() { return { language: 'de', timezone: 'UTC', quietHours: null, alertPreference: 'all', ...profile }; }, }, getContext: () => '{"status":"healthy"}', getRuntime: () => ({ delta: { summary: { totalChanges: 0, criticalChanges: 0 } } }), statePath, random, config: { enabled: true, maxPerDay: 4, minGapMinutes: 15, minIntervalMinutes: 15, maxIntervalMinutes: 30, idleAfterMinutes: 15, checkIntervalMinutes: 5, timezone: 'UTC', }, }); return { presence, messages, calls, statePath }; } test('dynamic presence sends a grounded message and persists a variable next evaluation', async () => { const { presence, messages, calls } = setup({ random: () => 0.5 }); const now = new Date('2026-07-06T12:00:00Z'); const outcome = await presence.tick(now); assert.deepEqual(outcome, { sent: true, reason: 'sent' }); assert.match(messages[0], /^\[DAVE \/\/ ACTIVE\]/); assert.match(calls[0].prompt, /dynamic presence evaluation, not a fixed scheduled briefing/i); assert.equal(calls[0].options.mode, 'presence'); assert.equal(presence.status().sentToday, 1); assert.equal(presence.status().nextEvaluationAt, '2026-07-06T12:22:30.000Z'); assert.equal((await presence.tick(new Date('2026-07-06T12:05:00Z'))).reason, 'not_due'); }); test('material sweep changes dynamically pull the next evaluation forward', () => { const { presence } = setup(); const now = new Date('2026-07-06T12:00:00Z'); presence.noteUserInteraction(now); assert.equal(presence.status().nextEvaluationAt, '2026-07-06T12:15:00.000Z'); assert.equal(presence.nudge({ summary: { criticalChanges: 1, totalChanges: 1 } }, now), true); assert.equal(presence.status().nextEvaluationAt, '2026-07-06T12:05:00.000Z'); }); test('recent operator activity delays unsolicited conversation', async () => { const { presence, messages } = setup(); const now = new Date('2026-07-06T12:00:00Z'); presence.noteUserInteraction(now); const outcome = await presence.tick(new Date('2026-07-06T12:01:00Z')); assert.equal(outcome.reason, 'not_due'); assert.equal(messages.length, 0); }); test('quiet hours and mutating proposals are suppressed', async () => { const quiet = setup({ profile: { quietHours: '00:00-00:00' } }); assert.equal((await quiet.presence.tick(new Date('2026-07-06T12:00:00Z'))).reason, 'quiet_hours'); assert.equal(quiet.messages.length, 0); const mutation = setup({ result: { answer: 'Confirmation required.', notify: true, pendingAction: { tool: 'trigger_sweep' } } }); assert.equal((await mutation.presence.tick(new Date('2026-07-06T12:00:00Z'))).reason, 'mutation_rejected'); assert.equal(mutation.messages.length, 0); }); test('local clock uses the profile timezone', () => { assert.deepEqual(localClock(new Date('2026-07-06T12:30:00Z'), 'Europe/Berlin'), { day: '2026-07-06', time: '14:30' }); assert.equal(localClock(new Date(), 'Invalid/Timezone'), null); });