Files
SnowLuma/packages/onebot/tests/instance-config-lifecycle.test.ts
DrSmoothl dc2ec1235a feat(onebot): backfill cloud history after login
Add an opt-in per-account login history backfill that waits for roster warmup and a 30-second grace period before silently filling local message history. The setting defaults to disabled and applies on the next account lifecycle.

Run one account at a time, prioritize known local gaps, and process at most three groups plus three friends with one 20-message page per session. Failures are logged and never retried during the same login.

Persist reconstructed history without dispatching OneBot events or changing QQ read state. Keep private metadata and event bodies atomic, honor recall tombstones, suppress live-only history side effects, avoid extra media requests, and stop queued, in-flight, or converting work when the account closes.

Share the persistence path with manual history actions and add lifecycle, cancellation, corruption, partial-failure, peer-identity, and storage-atomicity regression coverage. Refs #280.
2026-07-25 23:44:28 +08:00

58 lines
2.3 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { OneBotInstance } from '../src/instance';
import type { NetworkReconcileResult } from '../src/network';
import type { OneBotConfig } from '../src/types';
function config(trigger: string): OneBotConfig {
return {
networks: { httpServers: [], httpClients: [], wsServers: [], wsClients: [] },
statusCommand: { enabled: true, swallow: false, cooldownSeconds: 5, trigger },
historySync: { enabled: false },
notifications: { channelIds: [] },
};
}
describe('OneBotInstance config lifecycle FIFO', () => {
it('keeps ctx.config aligned with each ordered network reconcile', async () => {
const instance = Object.create(OneBotInstance.prototype) as OneBotInstance;
let finishFirst!: () => void;
const firstGate = new Promise<void>((resolve) => { finishFirst = resolve; });
const observations: Array<{ argument: OneBotConfig; current: OneBotConfig }> = [];
const ctx = { config: config('#initial') };
let calls = 0;
const ok: NetworkReconcileResult = { applied: true, errors: [], statuses: [] };
const networkManager = {
compileDesiredPlan: vi.fn(() => []),
reconcile: vi.fn(async (argument: OneBotConfig) => {
observations.push({ argument, current: ctx.config });
calls += 1;
if (calls === 1) await firstGate;
return ok;
}),
};
Object.assign(instance as unknown as Record<string, unknown>, {
uin: '10001',
ctx,
networkManager,
lifecycleTail: Promise.resolve(),
disposeRequested: false,
});
const a = config('#a');
const b = config('#b');
const applyingA = instance.reloadConfig(a);
const applyingB = instance.reloadConfig(b);
await Promise.resolve();
await Promise.resolve();
expect(observations).toHaveLength(1);
expect(observations[0].argument.statusCommand.trigger).toBe('#a');
expect(observations[0].current.statusCommand.trigger).toBe('#a');
finishFirst();
await Promise.all([applyingA, applyingB]);
expect(observations.map((item) => item.argument.statusCommand.trigger)).toEqual(['#a', '#b']);
expect(observations.map((item) => item.current.statusCommand.trigger)).toEqual(['#a', '#b']);
expect(ctx.config.statusCommand.trigger).toBe('#b');
});
});