mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-25 02:41:18 +00:00
* refactor(matrix): retire pre-2026.4 legacy crypto and flat-storage migrations * refactor(cli): drop pre-tsdown daemon-cli dist compat shim from the build * refactor(diffs): remove deprecated image*/format tool params and output aliases * refactor(ui): drop pre-gateway-scoped localStorage readers and legacy theme map * refactor: remove assorted pre-2026.4 compat shims and deprecated aliases * fix(discord): map legacy thread-binding fields in the doctor JSON import * fix(msteams): keep bot read fallback for legacy imported conversation rows * test(msteams): cover legacy bot-only imported conversation references * fix(ui): keep channel-prefixed session key display fallback * chore: refresh native i18n inventory and build-docker step count * chore(matrix): drop now-unused migration-config module * chore: re-pin plugin SDK public export budget after compat removals * chore: refresh native i18n inventory after rebase * chore: re-pin plugin SDK callable budget and refresh i18n inventory after rebase
266 lines
9.1 KiB
TypeScript
266 lines
9.1 KiB
TypeScript
// Msteams tests cover conversation store.shared plugin behavior.
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { resetPluginStateStoreForTests } from "openclaw/plugin-sdk/plugin-state-test-runtime";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
findPreferredDmConversationByUserId,
|
|
mergeStoredConversationReference,
|
|
normalizeStoredConversationId,
|
|
toConversationStoreEntries,
|
|
} from "./conversation-store-helpers.js";
|
|
import { createMSTeamsConversationStoreState } from "./conversation-store-state.js";
|
|
import type {
|
|
MSTeamsConversationStore,
|
|
MSTeamsConversationStoreEntry,
|
|
StoredConversationReference,
|
|
} from "./conversation-store.js";
|
|
import { setMSTeamsRuntime } from "./runtime.js";
|
|
import { msteamsRuntimeStub } from "./test-support/runtime.js";
|
|
|
|
type StoreFactory = {
|
|
name: string;
|
|
createStore: () => Promise<MSTeamsConversationStore>;
|
|
};
|
|
|
|
function createMemoryConversationStore(
|
|
initial: MSTeamsConversationStoreEntry[] = [],
|
|
): MSTeamsConversationStore {
|
|
const map = new Map<string, StoredConversationReference>();
|
|
for (const { conversationId, reference } of initial) {
|
|
map.set(normalizeStoredConversationId(conversationId), reference);
|
|
}
|
|
|
|
const findPreferredDmByUserId = async (
|
|
id: string,
|
|
): Promise<MSTeamsConversationStoreEntry | null> =>
|
|
findPreferredDmConversationByUserId(toConversationStoreEntries(map.entries()), id);
|
|
|
|
return {
|
|
upsert: async (conversationId, reference) => {
|
|
const normalizedId = normalizeStoredConversationId(conversationId);
|
|
map.set(
|
|
normalizedId,
|
|
mergeStoredConversationReference(
|
|
map.get(normalizedId),
|
|
reference,
|
|
new Date().toISOString(),
|
|
),
|
|
);
|
|
},
|
|
get: async (conversationId) => map.get(normalizeStoredConversationId(conversationId)) ?? null,
|
|
list: async () => toConversationStoreEntries(map.entries()),
|
|
remove: async (conversationId) => map.delete(normalizeStoredConversationId(conversationId)),
|
|
findPreferredDmByUserId,
|
|
};
|
|
}
|
|
|
|
const storeFactories: StoreFactory[] = [
|
|
{
|
|
name: "state",
|
|
createStore: async () => {
|
|
const stateDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "openclaw-msteams-store-"));
|
|
return createMSTeamsConversationStoreState({
|
|
env: { ...process.env, OPENCLAW_STATE_DIR: stateDir },
|
|
ttlMs: 60_000,
|
|
});
|
|
},
|
|
},
|
|
{
|
|
name: "memory",
|
|
createStore: async () => createMemoryConversationStore(),
|
|
},
|
|
];
|
|
|
|
describe.each(storeFactories)("msteams conversation store ($name)", ({ createStore }) => {
|
|
beforeEach(() => {
|
|
resetPluginStateStoreForTests();
|
|
setMSTeamsRuntime(msteamsRuntimeStub);
|
|
});
|
|
|
|
it("normalizes conversation ids consistently", async () => {
|
|
const store = await createStore();
|
|
|
|
await store.upsert("conv-norm;messageid=123", {
|
|
conversation: { id: "conv-norm" },
|
|
channelId: "msteams",
|
|
serviceUrl: "https://service.example.com",
|
|
user: { id: "u1" },
|
|
});
|
|
|
|
const normalized = await store.get("conv-norm");
|
|
expect(normalized).toEqual({
|
|
conversation: { id: "conv-norm" },
|
|
channelId: "msteams",
|
|
serviceUrl: "https://service.example.com",
|
|
user: { id: "u1" },
|
|
lastSeenAt: normalized?.lastSeenAt,
|
|
});
|
|
expect(typeof normalized?.lastSeenAt).toBe("string");
|
|
await expect(store.remove("conv-norm")).resolves.toBe(true);
|
|
await expect(store.get("conv-norm;messageid=123")).resolves.toBeNull();
|
|
});
|
|
|
|
it("upserts, lists, removes, and resolves users by both AAD and Bot Framework ids", async () => {
|
|
const store = await createStore();
|
|
|
|
vi.useFakeTimers();
|
|
try {
|
|
vi.setSystemTime(new Date("2026-03-25T20:00:00.000Z"));
|
|
await store.upsert("conv-a", {
|
|
conversation: { id: "conv-a" },
|
|
channelId: "msteams",
|
|
serviceUrl: "https://service.example.com",
|
|
user: { id: "user-a", aadObjectId: "aad-a", name: "Alice" },
|
|
});
|
|
|
|
vi.setSystemTime(new Date("2026-03-25T20:00:30.000Z"));
|
|
await store.upsert("conv-b", {
|
|
conversation: { id: "conv-b" },
|
|
channelId: "msteams",
|
|
serviceUrl: "https://service.example.com",
|
|
user: { id: "user-b", aadObjectId: "aad-b", name: "Bob" },
|
|
});
|
|
|
|
await expect(store.get("conv-a")).resolves.toEqual({
|
|
conversation: { id: "conv-a" },
|
|
channelId: "msteams",
|
|
serviceUrl: "https://service.example.com",
|
|
user: { id: "user-a", aadObjectId: "aad-a", name: "Alice" },
|
|
lastSeenAt: "2026-03-25T20:00:00.000Z",
|
|
});
|
|
|
|
await expect(store.list()).resolves.toEqual([
|
|
{
|
|
conversationId: "conv-a",
|
|
reference: {
|
|
conversation: { id: "conv-a" },
|
|
channelId: "msteams",
|
|
serviceUrl: "https://service.example.com",
|
|
user: { id: "user-a", aadObjectId: "aad-a", name: "Alice" },
|
|
lastSeenAt: "2026-03-25T20:00:00.000Z",
|
|
},
|
|
},
|
|
{
|
|
conversationId: "conv-b",
|
|
reference: {
|
|
conversation: { id: "conv-b" },
|
|
channelId: "msteams",
|
|
serviceUrl: "https://service.example.com",
|
|
user: { id: "user-b", aadObjectId: "aad-b", name: "Bob" },
|
|
lastSeenAt: "2026-03-25T20:00:30.000Z",
|
|
},
|
|
},
|
|
]);
|
|
|
|
await expect(store.findPreferredDmByUserId(" aad-b ")).resolves.toEqual({
|
|
conversationId: "conv-b",
|
|
reference: {
|
|
conversation: { id: "conv-b" },
|
|
channelId: "msteams",
|
|
serviceUrl: "https://service.example.com",
|
|
user: { id: "user-b", aadObjectId: "aad-b", name: "Bob" },
|
|
lastSeenAt: "2026-03-25T20:00:30.000Z",
|
|
},
|
|
});
|
|
await expect(store.findPreferredDmByUserId("user-a")).resolves.toEqual({
|
|
conversationId: "conv-a",
|
|
reference: {
|
|
conversation: { id: "conv-a" },
|
|
channelId: "msteams",
|
|
serviceUrl: "https://service.example.com",
|
|
user: { id: "user-a", aadObjectId: "aad-a", name: "Alice" },
|
|
lastSeenAt: "2026-03-25T20:00:00.000Z",
|
|
},
|
|
});
|
|
await expect(store.findPreferredDmByUserId(" ")).resolves.toBeNull();
|
|
|
|
await expect(store.remove("conv-a")).resolves.toBe(true);
|
|
await expect(store.get("conv-a")).resolves.toBeNull();
|
|
await expect(store.remove("missing")).resolves.toBe(false);
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("preserves existing timezone when upsert omits timezone", async () => {
|
|
const store = await createStore();
|
|
|
|
vi.useFakeTimers();
|
|
try {
|
|
vi.setSystemTime(new Date("2026-03-25T20:00:00.000Z"));
|
|
await store.upsert("conv-tz", {
|
|
conversation: { id: "conv-tz" },
|
|
channelId: "msteams",
|
|
serviceUrl: "https://service.example.com",
|
|
user: { id: "u1" },
|
|
timezone: "Europe/London",
|
|
});
|
|
|
|
vi.setSystemTime(new Date("2026-03-25T20:01:00.000Z"));
|
|
await store.upsert("conv-tz", {
|
|
conversation: { id: "conv-tz" },
|
|
channelId: "msteams",
|
|
serviceUrl: "https://service.example.com",
|
|
user: { id: "u1" },
|
|
});
|
|
|
|
await expect(store.get("conv-tz")).resolves.toEqual({
|
|
conversation: { id: "conv-tz" },
|
|
channelId: "msteams",
|
|
serviceUrl: "https://service.example.com",
|
|
user: { id: "u1" },
|
|
timezone: "Europe/London",
|
|
lastSeenAt: "2026-03-25T20:01:00.000Z",
|
|
});
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("prefers the freshest personal conversation for repeated upserts of the same user", async () => {
|
|
const store = await createStore();
|
|
|
|
vi.useFakeTimers();
|
|
try {
|
|
vi.setSystemTime(new Date("2026-03-25T20:00:00.000Z"));
|
|
await store.upsert("dm-old", {
|
|
conversation: { id: "dm-old", conversationType: "personal" },
|
|
channelId: "msteams",
|
|
serviceUrl: "https://service.example.com",
|
|
user: { id: "user-shared-old", aadObjectId: "aad-shared", name: "Old DM" },
|
|
});
|
|
|
|
vi.setSystemTime(new Date("2026-03-25T20:30:00.000Z"));
|
|
await store.upsert("group-shared", {
|
|
conversation: { id: "group-shared", conversationType: "groupChat" },
|
|
channelId: "msteams",
|
|
serviceUrl: "https://service.example.com",
|
|
user: { id: "user-shared-group", aadObjectId: "aad-shared", name: "Group" },
|
|
});
|
|
|
|
vi.setSystemTime(new Date("2026-03-25T21:00:00.000Z"));
|
|
await store.upsert("dm-new", {
|
|
conversation: { id: "dm-new", conversationType: "personal" },
|
|
channelId: "msteams",
|
|
serviceUrl: "https://service.example.com",
|
|
user: { id: "user-shared-new", aadObjectId: "aad-shared", name: "New DM" },
|
|
});
|
|
|
|
await expect(store.findPreferredDmByUserId("aad-shared")).resolves.toEqual({
|
|
conversationId: "dm-new",
|
|
reference: {
|
|
conversation: { id: "dm-new", conversationType: "personal" },
|
|
channelId: "msteams",
|
|
serviceUrl: "https://service.example.com",
|
|
user: { id: "user-shared-new", aadObjectId: "aad-shared", name: "New DM" },
|
|
lastSeenAt: "2026-03-25T21:00:00.000Z",
|
|
},
|
|
});
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
});
|