mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-22 15:58:11 +00:00
clawdbot-13a: migrate bundled session store callers
This commit is contained in:
@@ -5,7 +5,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { applyModelOverrideToSessionEntry } from "openclaw/plugin-sdk/model-session-runtime";
|
||||
import type { ResolvedAgentRoute } from "openclaw/plugin-sdk/routing";
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { resolveStorePath, updateSessionStore } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { patchSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { withTimeout } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import type { ButtonInteraction, StringSelectMenuInteraction } from "../internal/discord.js";
|
||||
import {
|
||||
@@ -42,34 +42,39 @@ async function persistDiscordModelPickerOverride(params: {
|
||||
agentId: params.route.agentId,
|
||||
});
|
||||
let persisted = false;
|
||||
await updateSessionStore(storePath, (store) => {
|
||||
const entry = store[params.route.sessionKey] ?? {
|
||||
await patchSessionEntry({
|
||||
storePath,
|
||||
sessionKey: params.route.sessionKey,
|
||||
fallbackEntry: {
|
||||
sessionId: randomUUID(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
store[params.route.sessionKey] = entry;
|
||||
persisted =
|
||||
applyModelOverrideToSessionEntry({
|
||||
entry,
|
||||
selection: {
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
isDefault: params.isDefault,
|
||||
},
|
||||
markLiveSwitchPending: true,
|
||||
}).updated || persisted;
|
||||
const runtime = params.runtime?.trim();
|
||||
if (runtime && runtime !== "auto" && runtime !== "default") {
|
||||
if (entry.agentRuntimeOverride !== runtime) {
|
||||
entry.agentRuntimeOverride = runtime;
|
||||
},
|
||||
replaceEntry: true,
|
||||
update: (entry) => {
|
||||
persisted =
|
||||
applyModelOverrideToSessionEntry({
|
||||
entry,
|
||||
selection: {
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
isDefault: params.isDefault,
|
||||
},
|
||||
markLiveSwitchPending: true,
|
||||
}).updated || persisted;
|
||||
const runtime = params.runtime?.trim();
|
||||
if (runtime && runtime !== "auto" && runtime !== "default") {
|
||||
if (entry.agentRuntimeOverride !== runtime) {
|
||||
entry.agentRuntimeOverride = runtime;
|
||||
delete entry.agentHarnessId;
|
||||
persisted = true;
|
||||
}
|
||||
} else if (runtime && entry.agentRuntimeOverride) {
|
||||
delete entry.agentRuntimeOverride;
|
||||
delete entry.agentHarnessId;
|
||||
persisted = true;
|
||||
}
|
||||
} else if (runtime && entry.agentRuntimeOverride) {
|
||||
delete entry.agentRuntimeOverride;
|
||||
delete entry.agentHarnessId;
|
||||
persisted = true;
|
||||
}
|
||||
return entry;
|
||||
},
|
||||
});
|
||||
return persisted;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import * as globalsModule from "openclaw/plugin-sdk/runtime-env";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveStorePath,
|
||||
saveSessionStore,
|
||||
upsertSessionEntry,
|
||||
} from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import * as commandTextModule from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
@@ -814,8 +814,10 @@ describe("Discord model picker interactions", () => {
|
||||
});
|
||||
const modelCommand = createModelCommandDefinition();
|
||||
const storePath = resolveStorePath(context.cfg.session?.store, { agentId: "worker" });
|
||||
await saveSessionStore(storePath, {
|
||||
"agent:worker:subagent:bound": {
|
||||
await upsertSessionEntry({
|
||||
storePath,
|
||||
sessionKey: "agent:worker:subagent:bound",
|
||||
entry: {
|
||||
updatedAt: Date.now(),
|
||||
sessionId: "bound-session",
|
||||
},
|
||||
@@ -864,8 +866,10 @@ describe("Discord model picker interactions", () => {
|
||||
const pickerData = createDefaultModelPickerData();
|
||||
const modelCommand = createModelCommandDefinition();
|
||||
const storePath = resolveStorePath(context.cfg.session?.store, { agentId: "worker" });
|
||||
await saveSessionStore(storePath, {
|
||||
"agent:worker:subagent:bound": {
|
||||
await upsertSessionEntry({
|
||||
storePath,
|
||||
sessionKey: "agent:worker:subagent:bound",
|
||||
entry: {
|
||||
updatedAt: Date.now(),
|
||||
sessionId: "bound-session",
|
||||
},
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const hoisted = vi.hoisted(() => {
|
||||
const updateSessionStore = vi.fn();
|
||||
const listSessionEntries = vi.fn();
|
||||
const patchSessionEntry = vi.fn();
|
||||
const resolveStorePath = vi.fn(() => "/tmp/openclaw-sessions.json");
|
||||
return { updateSessionStore, resolveStorePath };
|
||||
return { listSessionEntries, patchSessionEntry, resolveStorePath };
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/session-store-runtime", async () => {
|
||||
@@ -13,16 +14,37 @@ vi.mock("openclaw/plugin-sdk/session-store-runtime", async () => {
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
updateSessionStore: hoisted.updateSessionStore,
|
||||
listSessionEntries: hoisted.listSessionEntries,
|
||||
patchSessionEntry: hoisted.patchSessionEntry,
|
||||
resolveStorePath: hoisted.resolveStorePath,
|
||||
};
|
||||
});
|
||||
|
||||
let closeDiscordThreadSessions: typeof import("./thread-session-close.js").closeDiscordThreadSessions;
|
||||
|
||||
function setupStore(store: Record<string, { updatedAt: number }>) {
|
||||
hoisted.updateSessionStore.mockImplementation(
|
||||
async (_storePath: string, mutator: (s: typeof store) => unknown) => mutator(store),
|
||||
function setupStore(store: Record<string, { sessionId?: string; updatedAt: number }>) {
|
||||
hoisted.listSessionEntries.mockImplementation(() =>
|
||||
Object.entries(store).map(([sessionKey, entry]) => ({ sessionKey, entry })),
|
||||
);
|
||||
hoisted.patchSessionEntry.mockImplementation(
|
||||
async (params: {
|
||||
sessionKey: string;
|
||||
update: (entry: {
|
||||
sessionId?: string;
|
||||
updatedAt: number;
|
||||
}) => { sessionId?: string; updatedAt: number } | null;
|
||||
}) => {
|
||||
const entry = store[params.sessionKey];
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
const next = params.update({ ...entry });
|
||||
if (!next) {
|
||||
return entry;
|
||||
}
|
||||
store[params.sessionKey] = next;
|
||||
return next;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,7 +60,8 @@ describe("closeDiscordThreadSessions", () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
hoisted.updateSessionStore.mockClear();
|
||||
hoisted.listSessionEntries.mockReset();
|
||||
hoisted.patchSessionEntry.mockReset();
|
||||
hoisted.resolveStorePath.mockClear();
|
||||
hoisted.resolveStorePath.mockReturnValue("/tmp/openclaw-sessions.json");
|
||||
});
|
||||
@@ -143,7 +166,8 @@ describe("closeDiscordThreadSessions", () => {
|
||||
});
|
||||
|
||||
expect(count).toBe(0);
|
||||
expect(hoisted.updateSessionStore).not.toHaveBeenCalled();
|
||||
expect(hoisted.listSessionEntries).not.toHaveBeenCalled();
|
||||
expect(hoisted.patchSessionEntry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not recount sessions that were already reset", async () => {
|
||||
@@ -164,6 +188,35 @@ describe("closeDiscordThreadSessions", () => {
|
||||
expect(store[UNMATCHED_KEY].updatedAt).toBe(1_700_000_000_001);
|
||||
});
|
||||
|
||||
it("does not reset a matching session that changed after the list snapshot", async () => {
|
||||
const store = {
|
||||
[MATCHED_KEY]: {
|
||||
sessionId: "fresh-session",
|
||||
updatedAt: 2_000,
|
||||
},
|
||||
};
|
||||
setupStore(store);
|
||||
hoisted.listSessionEntries.mockReturnValue([
|
||||
{
|
||||
sessionKey: MATCHED_KEY,
|
||||
entry: {
|
||||
sessionId: "old-session",
|
||||
updatedAt: 1_000,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const count = await closeDiscordThreadSessions({
|
||||
cfg: {},
|
||||
accountId: "default",
|
||||
threadId: THREAD_ID,
|
||||
});
|
||||
|
||||
expect(count).toBe(0);
|
||||
expect(store[MATCHED_KEY].updatedAt).toBe(2_000);
|
||||
expect(store[MATCHED_KEY].sessionId).toBe("fresh-session");
|
||||
});
|
||||
|
||||
it("resolves the store path using cfg.session.store and accountId", async () => {
|
||||
const store = {};
|
||||
setupStore(store);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// Discord plugin module implements thread session close behavior.
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { resolveStorePath, updateSessionStore } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import {
|
||||
listSessionEntries,
|
||||
patchSessionEntry,
|
||||
resolveStorePath,
|
||||
} from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
|
||||
/**
|
||||
@@ -44,21 +48,32 @@ export async function closeDiscordThreadSessions(params: {
|
||||
|
||||
let resetCount = 0;
|
||||
|
||||
await updateSessionStore(storePath, (store) => {
|
||||
for (const [key, entry] of Object.entries(store)) {
|
||||
if (!entry || !sessionKeyContainsThreadId(key)) {
|
||||
continue;
|
||||
}
|
||||
if (entry.updatedAt === 0) {
|
||||
continue;
|
||||
}
|
||||
// Setting updatedAt to 0 signals that this session is stale.
|
||||
// evaluateSessionFreshness will create a new session on the next message.
|
||||
entry.updatedAt = 0;
|
||||
for (const { sessionKey, entry } of listSessionEntries({ storePath })) {
|
||||
if (!sessionKeyContainsThreadId(sessionKey) || entry.updatedAt === 0) {
|
||||
continue;
|
||||
}
|
||||
// Setting updatedAt to 0 signals that this session is stale.
|
||||
// evaluateSessionFreshness will create a new session on the next message.
|
||||
let resetEntry = false;
|
||||
await patchSessionEntry({
|
||||
storePath,
|
||||
sessionKey,
|
||||
replaceEntry: true,
|
||||
update: (current) => {
|
||||
if (current.updatedAt === 0) {
|
||||
return null;
|
||||
}
|
||||
if (current.updatedAt !== entry.updatedAt || current.sessionId !== entry.sessionId) {
|
||||
return null;
|
||||
}
|
||||
resetEntry = true;
|
||||
return { ...current, updatedAt: 0 };
|
||||
},
|
||||
});
|
||||
if (resetEntry) {
|
||||
resetCount += 1;
|
||||
}
|
||||
return resetCount;
|
||||
});
|
||||
}
|
||||
|
||||
return resetCount;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,12 @@ import { dispatchReplyWithBufferedBlockDispatcher } from "openclaw/plugin-sdk/re
|
||||
import { resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
|
||||
import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
|
||||
import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { readSessionUpdatedAt, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import {
|
||||
getSessionEntry,
|
||||
listSessionEntries,
|
||||
readSessionUpdatedAt,
|
||||
resolveStorePath,
|
||||
} from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { loadSessionStore } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { listSkillCommandsForAgents } from "openclaw/plugin-sdk/skill-commands-runtime";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/system-event-runtime";
|
||||
@@ -31,6 +36,8 @@ import { wasSentByBot } from "./sent-message-cache.js";
|
||||
export type TelegramBotDeps = {
|
||||
getRuntimeConfig: typeof getRuntimeConfig;
|
||||
resolveStorePath: typeof resolveStorePath;
|
||||
getSessionEntry?: typeof getSessionEntry;
|
||||
listSessionEntries?: typeof listSessionEntries;
|
||||
loadSessionStore?: typeof loadSessionStore;
|
||||
readSessionUpdatedAt?: typeof readSessionUpdatedAt;
|
||||
recordInboundSession?: typeof recordInboundSession;
|
||||
@@ -64,6 +71,12 @@ export const defaultTelegramBotDeps: TelegramBotDeps = {
|
||||
get resolveStorePath() {
|
||||
return resolveStorePath;
|
||||
},
|
||||
get getSessionEntry() {
|
||||
return getSessionEntry;
|
||||
},
|
||||
get listSessionEntries() {
|
||||
return listSessionEntries;
|
||||
},
|
||||
get readChannelAllowFromStore() {
|
||||
return readChannelAllowFromStore;
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Telegram plugin module implements bot handlers behavior.
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { Message, ReactionTypeEmoji } from "grammy/types";
|
||||
import { parseExecApprovalCommandText } from "openclaw/plugin-sdk/approval-reply-runtime";
|
||||
import { resolveChannelConfigWrites } from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
@@ -36,9 +37,9 @@ import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
|
||||
import { resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
|
||||
import { danger, logVerbose, warn } from "openclaw/plugin-sdk/runtime-env";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveSessionStoreEntry,
|
||||
updateSessionStore,
|
||||
getSessionEntry,
|
||||
listSessionEntries,
|
||||
patchSessionEntry,
|
||||
} from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { expandTelegramAllowFromWithAccessGroups } from "./access-groups.js";
|
||||
@@ -666,7 +667,7 @@ export const registerTelegramHandlers = ({
|
||||
runtimeCfg?: OpenClawConfig;
|
||||
}): {
|
||||
agentId: string;
|
||||
sessionEntry: ReturnType<typeof resolveSessionStoreEntry>["existing"];
|
||||
sessionEntry: ReturnType<typeof getSessionEntry>;
|
||||
sessionKey: string;
|
||||
model?: string;
|
||||
} => {
|
||||
@@ -708,8 +709,12 @@ export const registerTelegramHandlers = ({
|
||||
const storePath = telegramDeps.resolveStorePath(runtimeCfg.session?.store, {
|
||||
agentId: route.agentId,
|
||||
});
|
||||
const store = (telegramDeps.loadSessionStore ?? loadSessionStore)(storePath);
|
||||
const entry = resolveSessionStoreEntry({ store, sessionKey }).existing;
|
||||
const entry = (telegramDeps.getSessionEntry ?? getSessionEntry)({ storePath, sessionKey });
|
||||
const store = Object.fromEntries(
|
||||
(telegramDeps.listSessionEntries ?? listSessionEntries)({ storePath }).map(
|
||||
({ sessionKey: key, entry: value }) => [key, value],
|
||||
),
|
||||
);
|
||||
const storedOverride = resolveStoredModelOverride({
|
||||
sessionEntry: entry,
|
||||
sessionStore: store,
|
||||
@@ -2716,18 +2721,25 @@ export const registerTelegramHandlers = ({
|
||||
selection.model === resolvedDefault.model;
|
||||
|
||||
try {
|
||||
await updateSessionStore(storePath, (store) => {
|
||||
const sessionKey = sessionState.sessionKey;
|
||||
const entry = store[sessionKey] ?? {};
|
||||
store[sessionKey] = entry;
|
||||
applyModelOverrideToSessionEntry({
|
||||
entry,
|
||||
selection: {
|
||||
provider: selection.provider,
|
||||
model: selection.model,
|
||||
isDefault: isDefaultSelection,
|
||||
},
|
||||
});
|
||||
await patchSessionEntry({
|
||||
storePath,
|
||||
sessionKey: sessionState.sessionKey,
|
||||
fallbackEntry: {
|
||||
sessionId: randomUUID(),
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
replaceEntry: true,
|
||||
update: (entry) => {
|
||||
applyModelOverrideToSessionEntry({
|
||||
entry,
|
||||
selection: {
|
||||
provider: selection.provider,
|
||||
model: selection.model,
|
||||
isDefault: isDefaultSelection,
|
||||
},
|
||||
});
|
||||
return entry;
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
throw new TelegramRetryableCallbackError(err);
|
||||
|
||||
@@ -5391,8 +5391,8 @@ describe("createTelegramBot", () => {
|
||||
await dispatch(0);
|
||||
};
|
||||
|
||||
const updateSessionStoreSpy = vi.spyOn(sessionStoreRuntime, "updateSessionStore");
|
||||
updateSessionStoreSpy.mockRejectedValueOnce(new Error("session store boom"));
|
||||
const patchSessionEntrySpy = vi.spyOn(sessionStoreRuntime, "patchSessionEntry");
|
||||
patchSessionEntrySpy.mockRejectedValueOnce(new Error("session store boom"));
|
||||
|
||||
const ctx = {
|
||||
update: { update_id: 890 },
|
||||
@@ -5414,7 +5414,7 @@ describe("createTelegramBot", () => {
|
||||
await expect(runMiddlewareChain(ctx)).rejects.toThrow("session store boom");
|
||||
await runMiddlewareChain(ctx);
|
||||
} finally {
|
||||
updateSessionStoreSpy.mockRestore();
|
||||
patchSessionEntrySpy.mockRestore();
|
||||
}
|
||||
|
||||
expect(editMessageTextSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// Telegram plugin module implements bot behavior.
|
||||
import { getSessionEntry, listSessionEntries } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import {
|
||||
createTelegramBotCore,
|
||||
getTelegramSequentialKey,
|
||||
setTelegramBotRuntimeForTest,
|
||||
} from "./bot-core.js";
|
||||
import { defaultTelegramBotDeps } from "./bot-deps.js";
|
||||
import { defaultTelegramBotDeps, type TelegramBotDeps } from "./bot-deps.js";
|
||||
import type { TelegramBotOptions } from "./bot.types.js";
|
||||
|
||||
export type { TelegramBotOptions } from "./bot.types.js";
|
||||
@@ -16,6 +17,39 @@ export function createTelegramBot(
|
||||
): ReturnType<typeof createTelegramBotCore> {
|
||||
return createTelegramBotCore({
|
||||
...opts,
|
||||
telegramDeps: opts.telegramDeps ?? defaultTelegramBotDeps,
|
||||
telegramDeps: withTelegramSessionAccessorDeps(opts.telegramDeps ?? defaultTelegramBotDeps),
|
||||
});
|
||||
}
|
||||
|
||||
function withTelegramSessionAccessorDeps(deps: TelegramBotDeps): TelegramBotDeps {
|
||||
if (!deps.loadSessionStore) {
|
||||
return {
|
||||
...deps,
|
||||
getSessionEntry: deps.getSessionEntry ?? getSessionEntry,
|
||||
listSessionEntries: deps.listSessionEntries ?? listSessionEntries,
|
||||
};
|
||||
}
|
||||
|
||||
const listInjectedEntries = (
|
||||
scope: Parameters<NonNullable<TelegramBotDeps["listSessionEntries"]>>[0] = {},
|
||||
) => {
|
||||
const storePath =
|
||||
scope.storePath ?? deps.resolveStorePath(undefined, { agentId: scope.agentId });
|
||||
return Object.entries(deps.loadSessionStore?.(storePath) ?? {}).map(([sessionKey, entry]) => ({
|
||||
sessionKey,
|
||||
entry,
|
||||
}));
|
||||
};
|
||||
|
||||
return {
|
||||
...deps,
|
||||
// Existing Telegram tests and custom deps inject loadSessionStore; expose
|
||||
// the same data through the accessor seam consumed by migrated handlers.
|
||||
getSessionEntry:
|
||||
deps.getSessionEntry ??
|
||||
((scope) =>
|
||||
listInjectedEntries(scope).find(({ sessionKey }) => sessionKey === scope.sessionKey)
|
||||
?.entry),
|
||||
listSessionEntries: deps.listSessionEntries ?? listInjectedEntries,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { normalizeMainKey } from "openclaw/plugin-sdk/routing";
|
||||
import { saveSessionStore } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { upsertSessionEntry } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { withTempDir } from "openclaw/plugin-sdk/test-env";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createTestWebInboundMessage } from "../inbound/test-message.test-helper.js";
|
||||
@@ -277,8 +277,10 @@ describe("getSessionSnapshot", () => {
|
||||
const storePath = path.join(root, "sessions.json");
|
||||
const sessionKey = "agent:main:whatsapp:dm:s1";
|
||||
|
||||
await saveSessionStore(storePath, {
|
||||
[sessionKey]: {
|
||||
await upsertSessionEntry({
|
||||
storePath,
|
||||
sessionKey,
|
||||
entry: {
|
||||
sessionId: "snapshot-session",
|
||||
updatedAt: new Date(2026, 0, 18, 3, 30, 0).getTime(),
|
||||
lastChannel: "whatsapp",
|
||||
|
||||
@@ -18,6 +18,11 @@ const legacyReaderNames = new Set([
|
||||
"readSessionStoreReadOnly",
|
||||
"resolveSessionStoreEntry",
|
||||
]);
|
||||
const legacyWholeStoreAccessNames = new Set([
|
||||
...legacyReaderNames,
|
||||
"saveSessionStore",
|
||||
"updateSessionStore",
|
||||
]);
|
||||
|
||||
export const migratedSessionAccessorFiles = new Set([
|
||||
"src/commands/export-trajectory.ts",
|
||||
@@ -36,10 +41,27 @@ export const migratedSessionAccessorFiles = new Set([
|
||||
"src/infra/outbound/message-action-tts.ts",
|
||||
]);
|
||||
|
||||
export const migratedBundledPluginSessionAccessorFiles = new Set([
|
||||
"extensions/discord/src/monitor/native-command-model-picker-apply.ts",
|
||||
"extensions/discord/src/monitor/thread-session-close.ts",
|
||||
"extensions/telegram/src/bot-handlers.runtime.ts",
|
||||
]);
|
||||
|
||||
function normalizeRelativePath(filePath) {
|
||||
return filePath.replaceAll(path.sep, "/");
|
||||
}
|
||||
|
||||
function legacyNamesForFile(fileName) {
|
||||
const normalized = normalizeRelativePath(fileName);
|
||||
if (
|
||||
fileName === "source.ts" ||
|
||||
[...migratedBundledPluginSessionAccessorFiles].some((filePath) => normalized.endsWith(filePath))
|
||||
) {
|
||||
return legacyWholeStoreAccessNames;
|
||||
}
|
||||
return legacyReaderNames;
|
||||
}
|
||||
|
||||
function propertyAccessName(expression) {
|
||||
const unwrapped = unwrapExpression(expression);
|
||||
if (ts.isIdentifier(unwrapped)) {
|
||||
@@ -66,6 +88,7 @@ function bindingName(node) {
|
||||
|
||||
export function findSessionAccessorBoundaryViolations(content, fileName = "source.ts") {
|
||||
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
|
||||
const legacyNames = legacyNamesForFile(fileName);
|
||||
const violations = [];
|
||||
|
||||
const visit = (node) => {
|
||||
@@ -74,10 +97,10 @@ export function findSessionAccessorBoundaryViolations(content, fileName = "sourc
|
||||
if (namedBindings && ts.isNamedImports(namedBindings)) {
|
||||
for (const specifier of namedBindings.elements) {
|
||||
const importedName = specifier.propertyName?.text ?? specifier.name.text;
|
||||
if (legacyReaderNames.has(importedName)) {
|
||||
if (legacyNames.has(importedName)) {
|
||||
violations.push({
|
||||
line: toLine(sourceFile, specifier),
|
||||
reason: `imports legacy session store reader "${importedName}"`,
|
||||
reason: `imports legacy session store access "${importedName}"`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -86,29 +109,29 @@ export function findSessionAccessorBoundaryViolations(content, fileName = "sourc
|
||||
|
||||
if (ts.isBindingElement(node)) {
|
||||
const name = bindingName(node);
|
||||
if (name && legacyReaderNames.has(name)) {
|
||||
if (name && legacyNames.has(name)) {
|
||||
violations.push({
|
||||
line: toLine(sourceFile, node),
|
||||
reason: `aliases legacy session store reader "${name}"`,
|
||||
reason: `aliases legacy session store access "${name}"`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (ts.isPropertyAccessExpression(node) && legacyReaderNames.has(node.name.text)) {
|
||||
if (ts.isPropertyAccessExpression(node) && legacyNames.has(node.name.text)) {
|
||||
violations.push({
|
||||
line: toLine(sourceFile, node.name),
|
||||
reason: `references legacy session store reader "${node.name.text}"`,
|
||||
reason: `references legacy session store access "${node.name.text}"`,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
ts.isElementAccessExpression(node) &&
|
||||
ts.isStringLiteral(node.argumentExpression) &&
|
||||
legacyReaderNames.has(node.argumentExpression.text)
|
||||
legacyNames.has(node.argumentExpression.text)
|
||||
) {
|
||||
violations.push({
|
||||
line: toLine(sourceFile, node.argumentExpression),
|
||||
reason: `references legacy session store reader "${node.argumentExpression.text}"`,
|
||||
reason: `references legacy session store access "${node.argumentExpression.text}"`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -116,12 +139,12 @@ export function findSessionAccessorBoundaryViolations(content, fileName = "sourc
|
||||
const calleeName = propertyAccessName(node.expression);
|
||||
if (
|
||||
calleeName &&
|
||||
legacyReaderNames.has(calleeName) &&
|
||||
legacyNames.has(calleeName) &&
|
||||
ts.isIdentifier(unwrapExpression(node.expression))
|
||||
) {
|
||||
violations.push({
|
||||
line: toLine(sourceFile, node.expression),
|
||||
reason: `calls legacy session store reader "${calleeName}"`,
|
||||
reason: `calls legacy session store access "${calleeName}"`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -136,6 +159,8 @@ export function findSessionAccessorBoundaryViolations(content, fileName = "sourc
|
||||
export async function main() {
|
||||
const repoRoot = resolveRepoRoot(import.meta.url);
|
||||
const sourceRoots = resolveSourceRoots(repoRoot, [
|
||||
"extensions/discord/src/monitor",
|
||||
"extensions/telegram/src",
|
||||
"src/commands",
|
||||
"src/config/sessions",
|
||||
"src/cron",
|
||||
@@ -145,8 +170,13 @@ export async function main() {
|
||||
const violations = await collectFileViolations({
|
||||
repoRoot,
|
||||
sourceRoots,
|
||||
skipFile: (filePath) =>
|
||||
!migratedSessionAccessorFiles.has(normalizeRelativePath(path.relative(repoRoot, filePath))),
|
||||
skipFile: (filePath) => {
|
||||
const relativePath = normalizeRelativePath(path.relative(repoRoot, filePath));
|
||||
return (
|
||||
!migratedSessionAccessorFiles.has(relativePath) &&
|
||||
!migratedBundledPluginSessionAccessorFiles.has(relativePath)
|
||||
);
|
||||
},
|
||||
findViolations: findSessionAccessorBoundaryViolations,
|
||||
});
|
||||
|
||||
@@ -155,12 +185,12 @@ export async function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error("Found legacy session store reader usage in session-accessor migrated files:");
|
||||
console.error("Found legacy session store access usage in session-accessor migrated files:");
|
||||
for (const violation of violations) {
|
||||
console.error(`- ${violation.path}:${violation.line}: ${violation.reason}`);
|
||||
}
|
||||
console.error(
|
||||
"Use src/config/sessions/session-accessor.ts helpers for migrated read/projection paths. Expand this ratchet only after a slice migrates more files.",
|
||||
"Use src/config/sessions/session-accessor.ts helpers for migrated paths. Expand this ratchet only after a slice migrates more files.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
findSessionAccessorBoundaryViolations,
|
||||
migratedBundledPluginSessionAccessorFiles,
|
||||
migratedSessionAccessorFiles,
|
||||
} from "../../scripts/check-session-accessor-boundary.mjs";
|
||||
|
||||
@@ -26,6 +27,16 @@ describe("session accessor boundary guard", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("ratchets only the bundled plugin files migrated by this slice", () => {
|
||||
expect(migratedBundledPluginSessionAccessorFiles).toEqual(
|
||||
new Set([
|
||||
"extensions/discord/src/monitor/native-command-model-picker-apply.ts",
|
||||
"extensions/discord/src/monitor/thread-session-close.ts",
|
||||
"extensions/telegram/src/bot-handlers.runtime.ts",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("flags legacy reader imports", () => {
|
||||
expect(
|
||||
findSessionAccessorBoundaryViolations(`
|
||||
@@ -33,14 +44,14 @@ describe("session accessor boundary guard", () => {
|
||||
import { readSessionEntry, readSessionStoreReadOnly } from "../config/sessions/store-load.js";
|
||||
`),
|
||||
).toEqual([
|
||||
{ line: 2, reason: 'imports legacy session store reader "loadSessionStore"' },
|
||||
{ line: 2, reason: 'imports legacy session store reader "readSessionEntries"' },
|
||||
{ line: 3, reason: 'imports legacy session store reader "readSessionEntry"' },
|
||||
{ line: 3, reason: 'imports legacy session store reader "readSessionStoreReadOnly"' },
|
||||
{ line: 2, reason: 'imports legacy session store access "loadSessionStore"' },
|
||||
{ line: 2, reason: 'imports legacy session store access "readSessionEntries"' },
|
||||
{ line: 3, reason: 'imports legacy session store access "readSessionEntry"' },
|
||||
{ line: 3, reason: 'imports legacy session store access "readSessionStoreReadOnly"' },
|
||||
]);
|
||||
});
|
||||
|
||||
it("flags direct and namespace legacy reader calls", () => {
|
||||
it("flags direct and namespace legacy access calls", () => {
|
||||
expect(
|
||||
findSessionAccessorBoundaryViolations(`
|
||||
loadSessionStore(storePath);
|
||||
@@ -50,11 +61,11 @@ describe("session accessor boundary guard", () => {
|
||||
resolveSessionStoreEntry({ store, sessionKey });
|
||||
`),
|
||||
).toEqual([
|
||||
{ line: 2, reason: 'calls legacy session store reader "loadSessionStore"' },
|
||||
{ line: 3, reason: 'references legacy session store reader "readSessionEntries"' },
|
||||
{ line: 4, reason: 'references legacy session store reader "loadSessionStore"' },
|
||||
{ line: 5, reason: 'calls legacy session store reader "readSessionStoreReadOnly"' },
|
||||
{ line: 6, reason: 'calls legacy session store reader "resolveSessionStoreEntry"' },
|
||||
{ line: 2, reason: 'calls legacy session store access "loadSessionStore"' },
|
||||
{ line: 3, reason: 'references legacy session store access "readSessionEntries"' },
|
||||
{ line: 4, reason: 'references legacy session store access "loadSessionStore"' },
|
||||
{ line: 5, reason: 'calls legacy session store access "readSessionStoreReadOnly"' },
|
||||
{ line: 6, reason: 'calls legacy session store access "resolveSessionStoreEntry"' },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -66,9 +77,24 @@ describe("session accessor boundary guard", () => {
|
||||
const { loadSessionStore } = sessions;
|
||||
`),
|
||||
).toEqual([
|
||||
{ line: 2, reason: 'references legacy session store reader "loadSessionStore"' },
|
||||
{ line: 3, reason: 'aliases legacy session store reader "readSessionEntries"' },
|
||||
{ line: 4, reason: 'aliases legacy session store reader "loadSessionStore"' },
|
||||
{ line: 2, reason: 'references legacy session store access "loadSessionStore"' },
|
||||
{ line: 3, reason: 'aliases legacy session store access "readSessionEntries"' },
|
||||
{ line: 4, reason: 'aliases legacy session store access "loadSessionStore"' },
|
||||
]);
|
||||
});
|
||||
|
||||
it("flags legacy whole-store writes", () => {
|
||||
expect(
|
||||
findSessionAccessorBoundaryViolations(`
|
||||
import { saveSessionStore, updateSessionStore } from "../config/sessions.js";
|
||||
saveSessionStore(storePath, store);
|
||||
updateSessionStore(storePath, update);
|
||||
`),
|
||||
).toEqual([
|
||||
{ line: 2, reason: 'imports legacy session store access "saveSessionStore"' },
|
||||
{ line: 2, reason: 'imports legacy session store access "updateSessionStore"' },
|
||||
{ line: 3, reason: 'calls legacy session store access "saveSessionStore"' },
|
||||
{ line: 4, reason: 'calls legacy session store access "updateSessionStore"' },
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user