102 lines
4.3 KiB
JavaScript
102 lines
4.3 KiB
JavaScript
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 \/\/ AKTIV\]/);
|
|
assert.match(messages[0], /Belege:/);
|
|
assert.match(calls[0].prompt, /dynamic presence evaluation, not a fixed scheduled briefing/i);
|
|
assert.equal(calls[0].options.mode, 'presence');
|
|
assert.equal(calls[0].options.preferredLanguage, 'de');
|
|
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);
|
|
});
|
|
|
|
test('invalid profile timezone falls back to configured operational timezone', async () => {
|
|
const { presence, messages, calls } = setup({ profile: { timezone: 'not-a-timezone' } });
|
|
const outcome = await presence.tick(new Date('2026-07-06T12:00:00Z'));
|
|
assert.equal(outcome.reason, 'sent');
|
|
assert.equal(messages.length, 1);
|
|
assert.match(calls[0].prompt, /timezone: UTC/);
|
|
});
|