Files
waggle-os/packages/hive-mind-hooks-hermes/tests/compact-on-stop.test.ts
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

170 lines
6.2 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
compactStatePath,
isCompactEnabled,
maybeCompactOnStop,
readLastCompactTs,
resolveWindowMs,
writeLastCompactTs,
} from '../src/compact-on-stop.js';
import { makeMockBridge } from './hooks/_test-helpers.js';
import type { HookContext } from '@waggle/hive-mind-hooks-core';
import type { Logger } from '@waggle/hive-mind-shim-core';
import type { MockBridge } from './hooks/_test-helpers.js';
const FLAG = 'WAGGLE_HERMES_COMPACT_ON_STOP';
const WINDOW_ENV = 'WAGGLE_HERMES_COMPACT_WINDOW_MIN';
const MINUTE_MS = 60_000;
function makeLogger(): Logger {
return {
debug: () => {},
info: () => {},
warn: () => {},
error: () => {},
} as unknown as Logger;
}
function makeCtx(bridge: MockBridge): HookContext {
return { bridge, logger: makeLogger() };
}
describe('compact-on-stop unit', () => {
let home: string;
beforeEach(async () => {
home = await mkdtemp(join(tmpdir(), 'hmher-compact-'));
});
afterEach(async () => {
vi.unstubAllEnvs();
await rm(home, { recursive: true, force: true });
});
// case 1
it('flag off → cleanupFrames NOT called and no state file written', async () => {
vi.stubEnv(FLAG, '');
const bridge = makeMockBridge();
await maybeCompactOnStop(makeCtx(bridge), { now: () => 1_000_000, home });
expect(bridge.cleanupFrames).not.toHaveBeenCalled();
const last = await readLastCompactTs(compactStatePath(home));
expect(last).toBeUndefined();
});
// case 2
it('flag on, no prior timestamp → cleanupFrames called once and state file holds now', async () => {
vi.stubEnv(FLAG, '1');
const bridge = makeMockBridge();
const now = 5_000_000;
await maybeCompactOnStop(makeCtx(bridge), { now: () => now, home });
expect(bridge.cleanupFrames).toHaveBeenCalledTimes(1);
const last = await readLastCompactTs(compactStatePath(home));
expect(last).toBe(now);
});
// case 3
it('flag on, last = now - 1min, window 10min → NOT called (inside window)', async () => {
vi.stubEnv(FLAG, '1');
const bridge = makeMockBridge();
const now = 10_000_000;
await writeLastCompactTs(compactStatePath(home), now - 1 * MINUTE_MS);
await maybeCompactOnStop(makeCtx(bridge), { now: () => now, home, windowMs: 10 * MINUTE_MS });
expect(bridge.cleanupFrames).not.toHaveBeenCalled();
// timestamp untouched
const last = await readLastCompactTs(compactStatePath(home));
expect(last).toBe(now - 1 * MINUTE_MS);
});
// case 4
it('flag on, last = now - 11min, window 10min → called and timestamp updated to now', async () => {
vi.stubEnv(FLAG, '1');
const bridge = makeMockBridge();
const now = 20_000_000;
await writeLastCompactTs(compactStatePath(home), now - 11 * MINUTE_MS);
await maybeCompactOnStop(makeCtx(bridge), { now: () => now, home, windowMs: 10 * MINUTE_MS });
expect(bridge.cleanupFrames).toHaveBeenCalledTimes(1);
const last = await readLastCompactTs(compactStatePath(home));
expect(last).toBe(now);
});
// case 5
it('flag on, cleanupFrames rejects → resolves (no throw) and state file NOT updated', async () => {
vi.stubEnv(FLAG, '1');
const bridge = makeMockBridge();
bridge.cleanupFrames.mockRejectedValueOnce(new Error('cli unreachable'));
const now = 30_000_000;
await expect(
maybeCompactOnStop(makeCtx(bridge), { now: () => now, home }),
).resolves.toBeUndefined();
const last = await readLastCompactTs(compactStatePath(home));
expect(last).toBeUndefined();
});
// case 6
it('flag on, state-file write fails (home is a FILE) → resolves, no throw, cleanupFrames still attempted', async () => {
vi.stubEnv(FLAG, '1');
const homeFile = join(home, 'home-as-file');
await writeFile(homeFile, 'not a dir');
const bridge = makeMockBridge();
const now = 40_000_000;
await expect(
maybeCompactOnStop(makeCtx(bridge), { now: () => now, home: homeFile }),
).resolves.toBeUndefined();
expect(bridge.cleanupFrames).toHaveBeenCalledTimes(1);
});
// case 7
it('WAGGLE_HERMES_COMPACT_WINDOW_MIN=5 honored; compactWindowMs opt overrides env', () => {
vi.stubEnv(WINDOW_ENV, '5');
expect(resolveWindowMs(process.env)).toBe(5 * MINUTE_MS);
// opt override wins over env
expect(resolveWindowMs(process.env, 2 * MINUTE_MS)).toBe(2 * MINUTE_MS);
});
it('resolveWindowMs default is 10min when env unset/invalid', () => {
expect(resolveWindowMs({})).toBe(600_000);
expect(resolveWindowMs({ [WINDOW_ENV]: 'garbage' })).toBe(600_000);
expect(resolveWindowMs({ [WINDOW_ENV]: '0' })).toBe(600_000);
expect(resolveWindowMs({ [WINDOW_ENV]: '-3' })).toBe(600_000);
});
// case 8
it('isCompactEnabled truth table', () => {
expect(isCompactEnabled({})).toBe(false);
expect(isCompactEnabled({ [FLAG]: '' })).toBe(false);
expect(isCompactEnabled({ [FLAG]: '0' })).toBe(false);
expect(isCompactEnabled({ [FLAG]: 'false' })).toBe(false);
expect(isCompactEnabled({ [FLAG]: 'FALSE' })).toBe(false);
expect(isCompactEnabled({ [FLAG]: '1' })).toBe(true);
expect(isCompactEnabled({ [FLAG]: 'true' })).toBe(true);
expect(isCompactEnabled({ [FLAG]: 'yes' })).toBe(true);
});
it('compactStatePath lives under the hermes dir', () => {
const p = compactStatePath(home);
expect(p).toBe(join(home, '.hermes', '.hive-mind-last-compact'));
});
it('readLastCompactTs returns undefined on garbage content', async () => {
const p = compactStatePath(home);
await writeLastCompactTs(p, 123);
// overwrite with garbage
await writeFile(p, 'not-a-number');
expect(await readLastCompactTs(p)).toBeUndefined();
});
it('readLastCompactTs returns undefined when file is missing', async () => {
expect(await readLastCompactTs(join(home, 'does-not-exist'))).toBeUndefined();
});
it('writeLastCompactTs mkdirs the parent recursively then writes', async () => {
const p = compactStatePath(home); // parent .hermes does not exist yet
await writeLastCompactTs(p, 777);
const raw = await readFile(p, 'utf-8');
expect(raw).toBe('777');
});
});