



















@@ -12,6 +12,27 @@ import type {
1212} from "openclaw/plugin-sdk/runtime-doctor";
1313import { afterEach, beforeEach, describe, expect, it } from "vitest";
1414import { stateMigrations } from "./doctor-contract-api.js";
15+import {
16+buildMSTeamsConversationStateKey,
17+MSTEAMS_CONVERSATIONS_NAMESPACE,
18+type MSTeamsLegacyConversationStoreData,
19+} from "./src/conversation-store-state.js";
20+import type { StoredConversationReference } from "./src/conversation-store.js";
21+import {
22+buildMSTeamsPollStateKey,
23+buildMSTeamsPollVoteBucketKey,
24+MSTEAMS_POLL_VOTE_BUCKETS_NAMESPACE,
25+MSTEAMS_POLLS_NAMESPACE,
26+selectMSTeamsPollVoteBucket,
27+type MSTeamsPoll,
28+type StoredMSTeamsPoll,
29+type StoredMSTeamsPollVoteBucket,
30+} from "./src/polls.js";
31+import {
32+makeMSTeamsSsoTokenStoreKey,
33+MSTEAMS_SSO_TOKENS_NAMESPACE,
34+type MSTeamsSsoStoredToken,
35+} from "./src/sso-token-store.js";
15361637function createDoctorContext(env: NodeJS.ProcessEnv): PluginDoctorStateMigrationContext {
1738return {
@@ -32,6 +53,14 @@ function learningStoreKey(storePath: string, sessionKey: string): string {
3253return createHash("sha256").update(`${storePath}\0${sessionKey}`, "utf8").digest("hex");
3354}
345556+function migrationById(id: string) {
57+const migration = stateMigrations.find((entry) => entry.id === id);
58+if (!migration) {
59+throw new Error(`missing migration ${id}`);
60+}
61+return migration;
62+}
63+3564describe("msteams doctor state migration", () => {
3665let stateDir = "";
3766let env: NodeJS.ProcessEnv;
@@ -46,6 +75,187 @@ describe("msteams doctor state migration", () => {
4675await fs.rm(stateDir, { recursive: true, force: true });
4776});
487778+it("imports legacy conversations into plugin state", async () => {
79+const filePath = path.join(stateDir, "msteams-conversations.json");
80+const ref: StoredConversationReference = {
81+conversation: { id: "19:conv@thread.tacv2" },
82+channelId: "msteams",
83+serviceUrl: "https://service.example.com",
84+user: { id: "user-1" },
85+};
86+await fs.writeFile(
87+filePath,
88+`${JSON.stringify({
89+ version: 1,
90+ conversations: {
91+ "19:conv@thread.tacv2": ref,
92+ },
93+ } satisfies MSTeamsLegacyConversationStoreData)}\n`,
94+);
95+96+const migration = migrationById("msteams-conversations-json-to-plugin-state");
97+const context = createDoctorContext(env);
98+await expect(
99+migration.detectLegacyState({
100+config: {},
101+ env,
102+ stateDir,
103+oauthDir: path.join(stateDir, "oauth"),
104+ context,
105+}),
106+).resolves.toMatchObject({
107+preview: [expect.stringContaining("Microsoft Teams conversations")],
108+});
109+110+const result = await migration.migrateLegacyState({
111+config: {},
112+ env,
113+ stateDir,
114+oauthDir: path.join(stateDir, "oauth"),
115+ context,
116+});
117+118+expect(result.warnings).toEqual([]);
119+expect(result.changes).toEqual([
120+expect.stringContaining("Migrated 1 Microsoft Teams conversation entry"),
121+expect.stringContaining("Archived Microsoft Teams conversation legacy source"),
122+]);
123+await expect(fs.access(filePath)).rejects.toThrow();
124+await expect(fs.access(`${filePath}.migrated`)).resolves.toBeUndefined();
125+const store = context.openPluginStateKeyedStore<StoredConversationReference>({
126+namespace: MSTEAMS_CONVERSATIONS_NAMESPACE,
127+maxEntries: 2000,
128+});
129+await expect(
130+store.lookup(buildMSTeamsConversationStateKey("19:conv@thread.tacv2")),
131+).resolves.toMatchObject({
132+conversation: { id: "19:conv@thread.tacv2" },
133+user: { id: "user-1" },
134+});
135+});
136+137+it("imports legacy polls and vote buckets into plugin state", async () => {
138+const filePath = path.join(stateDir, "msteams-polls.json");
139+const poll: MSTeamsPoll = {
140+id: "poll-legacy",
141+question: "Lunch?",
142+options: ["Pizza", "Sushi"],
143+maxSelections: 1,
144+createdAt: new Date().toISOString(),
145+votes: {
146+"user-legacy": ["0"],
147+"user-new": ["1"],
148+},
149+};
150+await fs.writeFile(
151+filePath,
152+`${JSON.stringify({
153+ version: 1,
154+ polls: {
155+ "poll-legacy": poll,
156+ },
157+ })}\n`,
158+);
159+const context = createDoctorContext(env);
160+const voteBucketStore = context.openPluginStateKeyedStore<StoredMSTeamsPollVoteBucket>({
161+namespace: MSTEAMS_POLL_VOTE_BUCKETS_NAMESPACE,
162+maxEntries: 32_032,
163+});
164+const legacyBucket = selectMSTeamsPollVoteBucket("poll-legacy", "user-legacy");
165+await voteBucketStore.register(buildMSTeamsPollVoteBucketKey("poll-legacy", legacyBucket), {
166+pollId: "poll-legacy",
167+bucket: legacyBucket,
168+votes: { "user-legacy": ["1"] },
169+updatedAt: poll.createdAt,
170+});
171+172+const migration = migrationById("msteams-polls-json-to-plugin-state");
173+const result = await migration.migrateLegacyState({
174+config: {},
175+ env,
176+ stateDir,
177+oauthDir: path.join(stateDir, "oauth"),
178+ context,
179+});
180+181+expect(result.warnings).toEqual([]);
182+expect(result.changes).toEqual([
183+expect.stringContaining("Migrated 1 Microsoft Teams poll entry"),
184+expect.stringContaining("Archived Microsoft Teams poll legacy source"),
185+]);
186+const pollStore = context.openPluginStateKeyedStore<StoredMSTeamsPoll>({
187+namespace: MSTEAMS_POLLS_NAMESPACE,
188+maxEntries: 2000,
189+});
190+await expect(pollStore.lookup(buildMSTeamsPollStateKey("poll-legacy"))).resolves.toMatchObject({
191+id: "poll-legacy",
192+question: "Lunch?",
193+});
194+const newBucket = selectMSTeamsPollVoteBucket("poll-legacy", "user-new");
195+await expect(
196+voteBucketStore.lookup(buildMSTeamsPollVoteBucketKey("poll-legacy", legacyBucket)),
197+).resolves.toMatchObject({
198+votes: { "user-legacy": ["1"] },
199+});
200+await expect(
201+voteBucketStore.lookup(buildMSTeamsPollVoteBucketKey("poll-legacy", newBucket)),
202+).resolves.toMatchObject({
203+votes: { "user-new": ["1"] },
204+});
205+await expect(fs.access(`${filePath}.migrated`)).resolves.toBeUndefined();
206+});
207+208+it("imports legacy SSO tokens into the existing plugin-state token namespace", async () => {
209+const filePath = path.join(stateDir, "msteams-sso-tokens.json");
210+const token: MSTeamsSsoStoredToken = {
211+connectionName: "conn::alpha",
212+userId: "user::one",
213+token: "test-token-value",
214+updatedAt: "2026-04-10T00:00:00.000Z",
215+};
216+await fs.writeFile(
217+filePath,
218+`${JSON.stringify({
219+ version: 1,
220+ tokens: {
221+ "legacy::wrong-key": token,
222+ },
223+ })}\n`,
224+);
225+226+const migration = migrationById("msteams-sso-tokens-json-to-plugin-state");
227+const context = createDoctorContext(env);
228+const result = await migration.migrateLegacyState({
229+config: {},
230+ env,
231+ stateDir,
232+oauthDir: path.join(stateDir, "oauth"),
233+ context,
234+});
235+236+expect(result.warnings).toEqual([]);
237+expect(result.changes).toEqual([
238+expect.stringContaining("Migrated 1 Microsoft Teams SSO token entry"),
239+expect.stringContaining("Archived Microsoft Teams SSO-token legacy source"),
240+]);
241+const store = context.openPluginStateKeyedStore<MSTeamsSsoStoredToken>({
242+namespace: MSTEAMS_SSO_TOKENS_NAMESPACE,
243+maxEntries: 5000,
244+});
245+await expect(
246+store.lookup(makeMSTeamsSsoTokenStoreKey("conn::alpha", "user::one")),
247+).resolves.toEqual(token);
248+expect(result.changes.join("\n")).not.toContain(token.token);
249+expect(result.warnings.join("\n")).not.toContain(token.token);
250+await expect(fs.access(`${filePath}.migrated`)).resolves.toBeUndefined();
251+});
252+253+it("does not register a doctor migration for pending-upload cache files", () => {
254+expect(stateMigrations.map((migration) => migration.id)).not.toContain(
255+"msteams-pending-uploads-json-to-plugin-state",
256+);
257+});
258+49259it("imports legacy feedback learnings into plugin state", async () => {
50260const agentStoreTemplate = path.join(stateDir, "agents", "{agentId}", "sessions");
51261const mainStorePath = path.join(stateDir, "agents", "main", "sessions");
@@ -69,7 +279,7 @@ describe("msteams doctor state migration", () => {
69279await fs.writeFile(encodedSourcePath, JSON.stringify(["Be concise", "Use examples"]));
70280await fs.writeFile(sanitizedSourcePath, JSON.stringify(["Prefer cards for channel feedback"]));
7128172-const migration = stateMigrations[0];
282+const migration = migrationById("msteams-feedback-learnings-json-to-plugin-state");
73283const context = createDoctorContext(env);
74284await context
75285.openPluginStateKeyedStore({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。