Matrix: harden timer-driven tests

This commit is contained in:
Gustavo Madeira Santana
2026-03-09 05:41:31 -04:00
parent 0984eb7c01
commit 2007125244
3 changed files with 99 additions and 21 deletions

View File

@@ -79,6 +79,37 @@ describe("matrix monitor handler pairing account scope", () => {
expect(readAllowFromStore).toHaveBeenCalledTimes(1);
});
it("refreshes the account-scoped allowFrom cache after its ttl expires", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-03-01T10:00:00.000Z"));
try {
const readAllowFromStore = vi.fn(async () => [] as string[]);
const { handler } = createMatrixHandlerTestHarness({
readAllowFromStore,
dmPolicy: "pairing",
buildPairingReply: () => "pairing",
});
const makeEvent = (id: string): MatrixRawEvent =>
createMatrixTextMessageEvent({
eventId: id,
body: "hello",
mentions: { room: true },
});
await handler("!room:example.org", makeEvent("$event1"));
await handler("!room:example.org", makeEvent("$event2"));
expect(readAllowFromStore).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(30_001);
await handler("!room:example.org", makeEvent("$event3"));
expect(readAllowFromStore).toHaveBeenCalledTimes(2);
} finally {
vi.useRealTimers();
}
});
it("sends pairing reminders for pending requests with cooldown", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-03-01T10:00:00.000Z"));

View File

@@ -1,7 +1,7 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { ensureMatrixStartupVerification } from "./startup-verification.js";
function createTempStateDir(): string {
@@ -79,10 +79,6 @@ function createHarness(params?: {
};
}
afterEach(() => {
vi.useRealTimers();
});
describe("ensureMatrixStartupVerification", () => {
it("skips automatic requests when the device is already verified", async () => {
const tempHome = createTempStateDir();
@@ -145,16 +141,15 @@ describe("ensureMatrixStartupVerification", () => {
});
it("respects the startup verification cooldown", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-03-08T12:00:00.000Z"));
const tempHome = createTempStateDir();
const harness = createHarness();
const initialNowMs = Date.parse("2026-03-08T12:00:00.000Z");
await ensureMatrixStartupVerification({
client: harness.client as never,
auth: createAuth(),
accountConfig: {},
stateFilePath: createStateFilePath(tempHome),
nowMs: Date.now(),
nowMs: initialNowMs,
});
expect(harness.client.crypto.requestVerification).toHaveBeenCalledTimes(1);
@@ -163,7 +158,7 @@ describe("ensureMatrixStartupVerification", () => {
auth: createAuth(),
accountConfig: {},
stateFilePath: createStateFilePath(tempHome),
nowMs: Date.now() + 60_000,
nowMs: initialNowMs + 60_000,
});
expect(second.kind).toBe("cooldown");

View File

@@ -40,6 +40,24 @@ describe("matrix thread bindings", () => {
accessToken: "token",
} as const;
function resolveBindingsFilePath() {
return path.join(
resolveMatrixStoragePaths({
...auth,
env: process.env,
}).rootDir,
"thread-bindings.json",
);
}
async function readPersistedLastActivityAt(bindingsPath: string) {
const raw = await fs.readFile(bindingsPath, "utf-8");
const parsed = JSON.parse(raw) as {
bindings?: Array<{ lastActivityAt?: number }>;
};
return parsed.bindings?.[0]?.lastActivityAt;
}
beforeEach(async () => {
stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "matrix-thread-bindings-"));
__testing.resetSessionBindingAdaptersForTests();
@@ -274,6 +292,50 @@ describe("matrix thread bindings", () => {
}
});
it("persists the latest touched activity only after the debounce window", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-03-06T10:00:00.000Z"));
try {
await createMatrixThreadBindingManager({
accountId: "ops",
auth,
client: {} as never,
idleTimeoutMs: 24 * 60 * 60 * 1000,
maxAgeMs: 0,
enableSweeper: false,
});
const binding = await getSessionBindingService().bind({
targetSessionKey: "agent:ops:subagent:child",
targetKind: "subagent",
conversation: {
channel: "matrix",
accountId: "ops",
conversationId: "$thread",
parentConversationId: "!room:example",
},
placement: "current",
});
const bindingsPath = resolveBindingsFilePath();
const originalLastActivityAt = await readPersistedLastActivityAt(bindingsPath);
const firstTouchedAt = Date.parse("2026-03-06T10:05:00.000Z");
const secondTouchedAt = Date.parse("2026-03-06T10:10:00.000Z");
getSessionBindingService().touch(binding.bindingId, firstTouchedAt);
getSessionBindingService().touch(binding.bindingId, secondTouchedAt);
await vi.advanceTimersByTimeAsync(29_000);
expect(await readPersistedLastActivityAt(bindingsPath)).toBe(originalLastActivityAt);
await vi.advanceTimersByTimeAsync(1_000);
await vi.waitFor(async () => {
expect(await readPersistedLastActivityAt(bindingsPath)).toBe(secondTouchedAt);
});
} finally {
vi.useRealTimers();
}
});
it("flushes pending touch persistence on stop", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-03-06T10:00:00.000Z"));
@@ -303,19 +365,9 @@ describe("matrix thread bindings", () => {
manager.stop();
vi.useRealTimers();
const bindingsPath = path.join(
resolveMatrixStoragePaths({
...auth,
env: process.env,
}).rootDir,
"thread-bindings.json",
);
const bindingsPath = resolveBindingsFilePath();
await vi.waitFor(async () => {
const raw = await fs.readFile(bindingsPath, "utf-8");
const parsed = JSON.parse(raw) as {
bindings?: Array<{ lastActivityAt?: number }>;
};
expect(parsed.bindings?.[0]?.lastActivityAt).toBe(touchedAt);
expect(await readPersistedLastActivityAt(bindingsPath)).toBe(touchedAt);
});
} finally {
vi.useRealTimers();