mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
* 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.
307 lines
14 KiB
TypeScript
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();
|
|
}
|
|
}
|
|
},
|
|
);
|
|
},
|
|
);
|