clawdbot-13a: migrate bundled session store callers

This commit is contained in:
Josh Lehman
2026-06-01 08:10:21 -07:00
parent 5caa1dbc82
commit 7975bc06ac
11 changed files with 299 additions and 105 deletions

View File

@@ -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;
}

View File

@@ -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",
},

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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;
},

View File

@@ -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);

View File

@@ -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);

View File

@@ -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,
};
}

View File

@@ -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",

View File

@@ -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);
}

View File

@@ -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"' },
]);
});