openclaw/test/transcripts-tool.discord-lifecycle.integration.test.ts
Peter Steinberger 7972c35315
perf(sessions): reduce recovery ownership-check allocations (#160831)
* perf(sessions): reduce recovery ownership-check allocations

Move repeated recovery currency reads to compact revision-owned worker facts and carry live admission through lifecycle effects, native writes, and Browser cleanup. Preserve replacement, rollback, missing-store, and accepted-work settlement contracts. Large-entry Testbox checks allocate 83–85% less on the main thread; tiny entries and total process CPU retain worker-dispatch overhead.

* fix(sessions): preserve superseded timing no-ops

Keep obsolete timing callbacks silent before replacement planning while retaining final write-authority checks. Extract captured-source types into a dependency leaf and migrate the recovery, admission, and cleanup fixtures to their current owners. The exact architecture and root test-type gates pass, with 256 focused tests and clean independent review.
2026-09-29 03:45:31 +00:00

307 lines
14 KiB
TypeScript

import path from "node:path";
import {
discordVoiceTranscriptsSourceProvider,
loadDiscordVoiceTestHarness,
setDiscordTranscriptsVoiceManager,
} from "../extensions/discord/test-api.js";
import { createTranscriptsTool } from "../src/agents/tools/transcripts-tool.js";
import * as workerAdmission from "../src/infra/sqlite-worker-store.js";
import { createPluginMetadataSnapshotFixture } from "../src/plugins/plugin-metadata.test-support.js";
import { createEmptyPluginRegistry } from "../src/plugins/registry-empty.js";
import { withPluginRuntimeGenerationScope } from "../src/plugins/runtime/generation-scope.js";
import {
closeOpenClawStateDatabaseAsync,
closeOpenClawStateDatabaseForTest,
} from "../src/state/openclaw-state-db.js";
import { TranscriptsStore } from "../src/transcripts/store.js";
import { createDeferred } from "./helpers/promise.js";
import { createTempDirTracker } from "./helpers/temp-dir.js";
const { defineDiscordVoiceTests } = await loadDiscordVoiceTestHarness();
defineDiscordVoiceTests(
({
expect,
expectDefined,
it,
vi,
createClientWithMember,
createManager,
makeVoiceConfig,
getSessionEntry,
receiveRecordedSpeech,
expectConnectedStatus,
requireRecord,
lastRealtimeBridgeParams,
beginSpeakerTurn,
}) => {
it.each(["manager destruction", "completed", "error", "interleaved finalization"] as const)(
"retires replaced captures and handles %s without stopping a Discord replacement",
async (scenario) => {
const terminal = scenario === "interleaved finalization" ? "manager destruction" : scenario;
const tempDirs = createTempDirTracker();
const stateDir = tempDirs.make("discord-transcripts-replacement-");
const accountId = "transcript-replacement";
const discordConfig = makeVoiceConfig(
{ mode: terminal === "manager destruction" ? "stt-tts" : "agent-proxy" },
{ token: "test-token", groupPolicy: "open", allowFrom: ["discord:u-speaker"] },
);
const config = {
agents: { defaults: { utilityModel: "" } },
transcripts: { enabled: true },
channels: { discord: { accounts: { [accountId]: discordConfig } } },
};
const manager = createManager(
discordConfig,
createClientWithMember("u-speaker", "Speaker", "0001"),
config,
accountId,
);
const registry = createEmptyPluginRegistry();
registry.transcriptSourceProviders.push({
pluginId: "discord",
source: "discord/transcripts-source-api.ts",
provider: discordVoiceTranscriptsSourceProvider,
});
const metadataSnapshot = createPluginMetadataSnapshotFixture({
plugins: [{ id: "discord", contracts: { transcriptSourceProviders: ["discord-voice"] } }],
});
const tool = createTranscriptsTool({
config,
stateDir,
agentId: "transcript-replacement",
caller: { kind: "operator", source: "local" },
});
const execute = (params: Record<string, unknown>) =>
withPluginRuntimeGenerationScope({ metadataSnapshot, pluginRegistry: registry }, () =>
tool.execute("transcripts", params),
);
const store = new TranscriptsStore(path.join(stateDir, "transcripts"), {
env: { ...process.env, OPENCLAW_STATE_DIR: stateDir },
});
const source = { providerId: "discord-voice", accountId, guildId: "g1", channelId: "1001" };
const providerStop = vi.spyOn(discordVoiceTranscriptsSourceProvider, "stop");
setDiscordTranscriptsVoiceManager({ accountId, manager });
const finalizerReady = createDeferred();
let resumeFinalizer: (() => void) | undefined;
let finalizerCompletion: Promise<string> | undefined;
const createAdmission = workerAdmission.createSqliteWorkerWriteAdmission;
const summaryWrite = vi.spyOn(TranscriptsStore.prototype, "writeSummary");
TranscriptsStore.prototype.writeSummary = function (
this: TranscriptsStore,
...args: Parameters<TranscriptsStore["writeSummary"]>
) {
if (
scenario !== "interleaved finalization" ||
args[1].sessionId !== "first" ||
finalizerCompletion
) {
return summaryWrite.apply(this, args);
}
const completion = createDeferred<string>();
finalizerCompletion = completion.promise;
resumeFinalizer = () => {
resumeFinalizer = undefined;
void summaryWrite.apply(this, args).then(completion.resolve, completion.reject);
};
finalizerReady.resolve();
return completion.promise;
};
const admission = vi
.spyOn(workerAdmission, "createSqliteWorkerWriteAdmission")
.mockImplementation((assertCurrent, locations) =>
createAdmission((request) => {
// Grant the replacement transaction before its predecessor's finalizer contends.
if (resumeFinalizer) {
queueMicrotask(() => resumeFinalizer?.());
}
assertCurrent(request);
}, locations),
);
try {
await expect(
execute({ action: "start", sessionId: "first", ...source }),
).resolves.toMatchObject({
details: { sessionId: "first", providerId: "discord-voice", accountId },
});
const record = (text: string) =>
receiveRecordedSpeech(manager, text, getSessionEntry(manager), "u-speaker");
await record("Keep the original historical note.");
await expect(
execute({ action: "start", sessionId: "second", ...source }),
).resolves.toMatchObject({
details: { sessionId: "second", providerId: "discord-voice", accountId },
});
if (scenario === "interleaved finalization") {
await finalizerReady.promise;
}
await record("This belongs only to the replacement.");
await finalizerCompletion;
const first = expectDefined(await store.readSession("first"), "first capture");
const second = expectDefined(await store.readSession("second"), "second capture");
const secondTexts = ["This belongs only to the replacement."];
expect(first.source).toMatchObject(source);
expect(second.source).toEqual(first.source);
expect(
(await store.readUtterancesForSession(first)).map((utterance) => utterance.text),
).toEqual(["Keep the original historical note."]);
expect(
(await store.readUtterancesForSession(second)).map((utterance) => utterance.text),
).toEqual(secondTexts);
expectConnectedStatus(manager, "1001");
expect(providerStop).not.toHaveBeenCalled();
const status = await execute({ action: "status" });
const active = requireRecord(status.details, "transcript status").active;
expect(Array.isArray(active)).toBe(true);
if (!Array.isArray(active)) {
throw new Error("expected transcript status active sessions");
}
// Keep the stale-status failure first, then exercise historical recovery as well.
expect
.soft(active.map((capture) => requireRecord(capture, "active capture").sessionId))
.toEqual(["second"]);
await vi.waitFor(async () => {
expect((await store.readSession("first"))?.stoppedAt).toEqual(expect.any(String));
});
await execute({ action: "stop", sessionId: "first" });
await expect(execute({ action: "summarize", sessionId: "first" })).resolves.toMatchObject(
{
details: { summary: { sessionId: "first", utteranceCount: 1 } },
},
);
expect.soft(providerStop).not.toHaveBeenCalled();
expectConnectedStatus(manager, "1001");
await expect(execute({ action: "status" })).resolves.toMatchObject({
details: { active: [expect.objectContaining({ sessionId: "second" })] },
});
if (terminal !== "manager destruction") {
await expect(manager.join({ guildId: "g1", channelId: "1001" })).resolves.toMatchObject(
{
ok: true,
},
);
}
const provider =
terminal === "manager destruction" ? undefined : lastRealtimeBridgeParams();
const speaker = { userId: "u-speaker", speakerLabel: "Speaker", senderIsOwner: false };
const turn = provider ? beginSpeakerTurn(getSessionEntry(manager), speaker) : undefined;
if (provider) {
expectDefined(
provider.onClose,
"provider terminal callback",
)(terminal === "error" ? "error" : "completed");
expectConnectedStatus(manager, "1001");
await expect(execute({ action: "status" })).resolves.toMatchObject({
details: {
active: [expect.objectContaining({ sessionId: "second" })],
pendingFinalization: [],
},
});
const ongoing = expectDefined(await store.readSession("second"), "ongoing capture");
expect(ongoing.stoppedAt).toBeUndefined();
beginSpeakerTurn(getSessionEntry(manager), speaker);
const recovered = expectDefined(lastRealtimeBridgeParams(), "recovered speaker");
expect(recovered).not.toBe(provider);
provider.onTranscript?.("user", "Late text from the retired speaker.", true);
const recoveredText = "The same capture continues after reconnect.";
secondTexts.push(recoveredText);
recovered.onTranscript?.("user", recoveredText, true);
await record(recoveredText);
await vi.waitFor(async () => {
expect(
(await store.readUtterancesForSession(second)).map((utterance) => utterance.text),
).toEqual(secondTexts);
});
expect(providerStop).not.toHaveBeenCalled();
await expect(manager.leave({ guildId: "g1" })).resolves.toMatchObject({ ok: true });
} else {
await manager.destroy();
}
expect(manager.status()).toEqual([]);
await expect(execute({ action: "status" })).resolves.toMatchObject({
details: { active: [expect.objectContaining({ sessionId: "second" })] },
});
expect((await store.readSession("second"))?.stoppedAt).toBeUndefined();
expect(providerStop).not.toHaveBeenCalled();
await execute({ action: "stop", sessionId: "second" });
await expect(execute({ action: "status" })).resolves.toMatchObject({
details: { active: [], pendingFinalization: [] },
});
expect(await store.readSession("second")).toMatchObject({
stoppedAt: expect.any(String),
});
expect(providerStop).toHaveBeenCalledOnce();
if (provider) {
const stoppedSecond = expectDefined(await store.readSession("second"), "ended capture");
await expect(
execute({ action: "start", sessionId: "third", ...source }),
).resolves.toMatchObject({
details: { sessionId: "third", providerId: "discord-voice", accountId },
});
await expect(manager.join({ guildId: "g1", channelId: "1001" })).resolves.toMatchObject(
{ ok: true },
);
turn?.close();
provider.onClose?.("error");
provider.onReady?.();
provider.onTranscript?.("user", "Late text from the retired provider.", true);
await execute({ action: "stop", sessionId: "second" });
expect(providerStop).toHaveBeenCalledOnce();
expectConnectedStatus(manager, "1001");
await expect(execute({ action: "status" })).resolves.toMatchObject({
details: {
active: [expect.objectContaining({ sessionId: "third" })],
pendingFinalization: [],
},
});
expect(await store.readSession("second")).toMatchObject({
stoppedAt: stoppedSecond.stoppedAt,
});
expect(
(await store.readUtterancesForSession(stoppedSecond)).map(
(utterance) => utterance.text,
),
).toEqual(secondTexts);
const third = expectDefined(await store.readSession("third"), "replacement capture");
expect(await store.readUtterancesForSession(third)).toEqual([]);
}
} finally {
summaryWrite.mockRestore();
resumeFinalizer?.();
await Promise.allSettled([finalizerCompletion]);
admission.mockRestore();
try {
for (const capture of await discordVoiceTranscriptsSourceProvider.status!(source)) {
if (capture.sessionId) {
await execute({ action: "stop", sessionId: capture.sessionId });
}
}
await manager.destroy();
await vi.waitFor(async () => {
await expect(execute({ action: "status" })).resolves.toMatchObject({
details: { active: [], pendingFinalization: [] },
});
});
} finally {
setDiscordTranscriptsVoiceManager({
accountId,
manager: null,
expectedManager: manager,
});
providerStop.mockRestore();
await closeOpenClawStateDatabaseAsync();
closeOpenClawStateDatabaseForTest();
tempDirs.cleanup();
}
}
},
);
},
);