mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
clawdbot-d02.1.9.1.31: add sessions.create lifecycle seam (#93691)
This commit is contained in:
@@ -34,6 +34,13 @@ const legacyTranscriptWriterNames = new Set([
|
||||
"emitSessionTranscriptUpdate",
|
||||
"rewriteTranscriptEntriesInSessionFile",
|
||||
]);
|
||||
const sessionCreateLifecycleWriterNames = new Set([
|
||||
"applySessionStoreEntryPatch",
|
||||
"saveSessionStore",
|
||||
"updateSessionStore",
|
||||
"updateSessionStoreEntry",
|
||||
"ensureSessionTranscriptFile",
|
||||
]);
|
||||
|
||||
export const migratedSessionAccessorFiles = new Set([
|
||||
"src/agents/embedded-agent-runner/compaction-successor-transcript.ts",
|
||||
@@ -238,6 +245,39 @@ export function findTranscriptWriterBoundaryViolations(content, fileName = "sour
|
||||
);
|
||||
}
|
||||
|
||||
export function findGatewaySessionCreateLifecycleViolations(content, fileName = "source.ts") {
|
||||
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
|
||||
const violations = [];
|
||||
|
||||
const visitCreateHandler = (node) => {
|
||||
if (ts.isCallExpression(node)) {
|
||||
const calleeName = propertyAccessName(node.expression);
|
||||
if (calleeName && sessionCreateLifecycleWriterNames.has(calleeName)) {
|
||||
violations.push({
|
||||
line: toLine(sourceFile, node.expression),
|
||||
reason: `calls legacy sessions.create lifecycle writer "${calleeName}"`,
|
||||
});
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visitCreateHandler);
|
||||
};
|
||||
|
||||
const visit = (node) => {
|
||||
if (
|
||||
ts.isPropertyAssignment(node) &&
|
||||
ts.isStringLiteralLike(node.name) &&
|
||||
node.name.text === "sessions.create"
|
||||
) {
|
||||
visitCreateHandler(node.initializer);
|
||||
return;
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
|
||||
visit(sourceFile);
|
||||
return violations;
|
||||
}
|
||||
|
||||
export async function main() {
|
||||
const repoRoot = resolveRepoRoot(import.meta.url);
|
||||
const readSourceRoots = resolveSourceRoots(repoRoot, [
|
||||
@@ -287,7 +327,20 @@ export async function main() {
|
||||
!migratedTranscriptWriterFiles.has(normalizeRelativePath(path.relative(repoRoot, filePath))),
|
||||
findViolations: findTranscriptWriterBoundaryViolations,
|
||||
});
|
||||
const violations = [...readViolations, ...writeViolations, ...transcriptWriterViolations];
|
||||
const sessionCreateLifecycleViolations = await collectFileViolations({
|
||||
repoRoot,
|
||||
sourceRoots: resolveSourceRoots(repoRoot, ["src/gateway/server-methods"]),
|
||||
skipFile: (filePath) =>
|
||||
normalizeRelativePath(path.relative(repoRoot, filePath)) !==
|
||||
"src/gateway/server-methods/sessions.ts",
|
||||
findViolations: findGatewaySessionCreateLifecycleViolations,
|
||||
});
|
||||
const violations = [
|
||||
...readViolations,
|
||||
...writeViolations,
|
||||
...transcriptWriterViolations,
|
||||
...sessionCreateLifecycleViolations,
|
||||
];
|
||||
|
||||
if (violations.length === 0) {
|
||||
console.log("session accessor boundary guard passed.");
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
appendTranscriptMessage,
|
||||
appendTranscriptEvent,
|
||||
cleanupSessionLifecycleArtifacts,
|
||||
createSessionEntryWithTranscript,
|
||||
listSessionEntries,
|
||||
loadExactSessionEntry,
|
||||
loadSessionEntry,
|
||||
@@ -93,6 +94,57 @@ describe("session accessor file-backed seam", () => {
|
||||
expect(loadSessionEntry(scope)?.sessionId).toBe(inserted?.sessionId);
|
||||
});
|
||||
|
||||
it("creates entries with initialized transcripts and normalized sessionFile metadata", async () => {
|
||||
const scope = {
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:main",
|
||||
storePath,
|
||||
};
|
||||
|
||||
const created = await createSessionEntryWithTranscript(scope, ({ sessionEntries }) => {
|
||||
expect(sessionEntries).toEqual({});
|
||||
return {
|
||||
ok: true,
|
||||
entry: {
|
||||
sessionId: "session-1",
|
||||
updatedAt: 10,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
expect(created.ok).toBe(true);
|
||||
if (!created.ok) {
|
||||
throw new Error("expected session creation to succeed");
|
||||
}
|
||||
expect(path.basename(created.sessionFile)).toBe("session-1.jsonl");
|
||||
expect(created.entry.sessionFile).toBe(created.sessionFile);
|
||||
});
|
||||
|
||||
it("rolls back the entry when transcript initialization fails", async () => {
|
||||
const scope = {
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:main",
|
||||
storePath,
|
||||
};
|
||||
fs.writeFileSync(path.join(tempDir, "blocked"), "not a directory", "utf8");
|
||||
|
||||
const created = await createSessionEntryWithTranscript(scope, () => ({
|
||||
ok: true,
|
||||
entry: {
|
||||
sessionFile: "blocked/session-1.jsonl",
|
||||
sessionId: "session-1",
|
||||
updatedAt: 10,
|
||||
},
|
||||
}));
|
||||
|
||||
expect(created).toMatchObject({
|
||||
ok: false,
|
||||
phase: "transcript",
|
||||
});
|
||||
expect(loadSessionEntry(scope)).toBeUndefined();
|
||||
expect(loadSessionStore(storePath, { skipCache: true })[scope.sessionKey]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("can borrow cached entry objects for read-only hot paths", async () => {
|
||||
const scope = {
|
||||
clone: false,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {
|
||||
acquireSessionWriteLock,
|
||||
resolveSessionWriteLockOptions,
|
||||
} from "../../agents/session-write-lock.js";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
|
||||
import { emitSessionTranscriptUpdate } from "../../sessions/transcript-events.js";
|
||||
import type { SessionTranscriptUpdate } from "../../sessions/transcript-events.js";
|
||||
@@ -26,6 +28,7 @@ import {
|
||||
patchSessionEntry as patchFileSessionEntry,
|
||||
readSessionUpdatedAt as readFileSessionUpdatedAt,
|
||||
resolveSessionStoreEntry,
|
||||
updateSessionStore,
|
||||
updateSessionStoreEntry as updateFileSessionStoreEntry,
|
||||
type SessionLifecycleArtifactCleanupParams,
|
||||
type SessionLifecycleArtifactCleanupResult,
|
||||
@@ -40,6 +43,7 @@ import {
|
||||
withSessionTranscriptAppendQueue,
|
||||
} from "./transcript-append.js";
|
||||
import { resolveSessionTranscriptFile } from "./transcript-file-resolve.js";
|
||||
import { createSessionTranscriptHeader } from "./transcript-header.js";
|
||||
import { streamSessionTranscriptLines } from "./transcript-stream.js";
|
||||
import {
|
||||
type OwnedSessionTranscriptPublishedEntry,
|
||||
@@ -243,6 +247,26 @@ export type SessionEntryPatchContext = {
|
||||
existingEntry?: SessionEntry;
|
||||
};
|
||||
|
||||
export type SessionEntryCreateWithTranscriptContext = {
|
||||
/** Current entry under the requested key before creation, if any. */
|
||||
existingEntry?: SessionEntry;
|
||||
/** Current entries snapshot for validation rules such as label uniqueness. */
|
||||
sessionEntries: Record<string, SessionEntry>;
|
||||
};
|
||||
|
||||
export type SessionEntryCreateWithTranscriptResult<TError = string> =
|
||||
| { ok: true; entry: SessionEntry; sessionFile: string }
|
||||
| { ok: false; error: TError; phase: "entry" }
|
||||
| { ok: false; error: string; phase: "transcript" };
|
||||
|
||||
export type SessionEntryCreateWithTranscriptPrepareResult<TError = string> =
|
||||
| { ok: true; entry: SessionEntry }
|
||||
| { ok: false; error: TError };
|
||||
|
||||
type CreatedSessionTranscriptResult =
|
||||
| { ok: true; sessionFile: string }
|
||||
| { ok: false; error: string; phase: "transcript" };
|
||||
|
||||
export type { SessionLifecycleArtifactCleanupParams, SessionLifecycleArtifactCleanupResult };
|
||||
|
||||
/** Returns the entry for a canonical or alias session key, if one exists. */
|
||||
@@ -349,6 +373,100 @@ export async function patchSessionEntry(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or updates one session entry and initializes its transcript header as
|
||||
* one storage-sized lifecycle operation. File-backed storage still writes JSON
|
||||
* plus JSONL, but callers no longer compose entry write, header creation,
|
||||
* rollback, and normalized sessionFile persistence themselves.
|
||||
*/
|
||||
export async function createSessionEntryWithTranscript<TError = string>(
|
||||
scope: SessionAccessScope,
|
||||
createEntry: (
|
||||
context: SessionEntryCreateWithTranscriptContext,
|
||||
) =>
|
||||
| Promise<SessionEntryCreateWithTranscriptPrepareResult<TError>>
|
||||
| SessionEntryCreateWithTranscriptPrepareResult<TError>,
|
||||
): Promise<SessionEntryCreateWithTranscriptResult<TError>> {
|
||||
const storePath = resolveAccessStorePath(scope);
|
||||
return await updateSessionStore(storePath, async (store) => {
|
||||
const resolved = resolveSessionStoreEntry({ store, sessionKey: scope.sessionKey });
|
||||
const created = await createEntry({
|
||||
existingEntry: resolved.existing ? { ...resolved.existing } : undefined,
|
||||
sessionEntries: cloneSessionEntries(store),
|
||||
});
|
||||
if (!created.ok) {
|
||||
return { ok: false, error: created.error, phase: "entry" };
|
||||
}
|
||||
|
||||
const ensured = ensureCreatedSessionTranscript({
|
||||
agentId: scope.agentId,
|
||||
entry: created.entry,
|
||||
storePath,
|
||||
});
|
||||
if (!ensured.ok) {
|
||||
delete store[resolved.normalizedKey];
|
||||
return ensured;
|
||||
}
|
||||
|
||||
const entry =
|
||||
created.entry.sessionFile === ensured.sessionFile
|
||||
? created.entry
|
||||
: {
|
||||
...created.entry,
|
||||
sessionFile: ensured.sessionFile,
|
||||
};
|
||||
store[resolved.normalizedKey] = entry;
|
||||
for (const legacyKey of resolved.legacyKeys) {
|
||||
delete store[legacyKey];
|
||||
}
|
||||
return { ok: true, entry, sessionFile: ensured.sessionFile };
|
||||
});
|
||||
}
|
||||
|
||||
function cloneSessionEntries(store: Record<string, SessionEntry>): Record<string, SessionEntry> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(store).map(([sessionKey, entry]) => [sessionKey, { ...entry }]),
|
||||
);
|
||||
}
|
||||
|
||||
// File-backed creation resolves the concrete transcript artifact and writes the
|
||||
// header before the store mutation is saved; SQLite adapters implement this as
|
||||
// the same lifecycle operation without exposing rollback details to callers.
|
||||
function ensureCreatedSessionTranscript(params: {
|
||||
agentId?: string;
|
||||
entry: SessionEntry;
|
||||
storePath: string;
|
||||
}): CreatedSessionTranscriptResult {
|
||||
try {
|
||||
const sessionFile = resolveSessionFilePath(
|
||||
params.entry.sessionId,
|
||||
params.entry.sessionFile ? { sessionFile: params.entry.sessionFile } : undefined,
|
||||
{
|
||||
agentId: params.agentId,
|
||||
sessionsDir: path.dirname(path.resolve(params.storePath)),
|
||||
},
|
||||
);
|
||||
if (!fs.existsSync(sessionFile)) {
|
||||
fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
sessionFile,
|
||||
`${JSON.stringify(createSessionTranscriptHeader({ sessionId: params.entry.sessionId }))}\n`,
|
||||
{
|
||||
encoding: "utf-8",
|
||||
mode: 0o600,
|
||||
},
|
||||
);
|
||||
}
|
||||
return { ok: true, sessionFile };
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
error: formatErrorMessage(err),
|
||||
phase: "transcript",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates an existing entry only; returns null when the session is absent. */
|
||||
export async function updateSessionEntry(
|
||||
scope: SessionAccessScope,
|
||||
|
||||
@@ -52,14 +52,12 @@ import {
|
||||
runSessionsCleanup,
|
||||
serializeSessionCleanupResult,
|
||||
resolveMainSessionKey,
|
||||
resolveSessionFilePath,
|
||||
resolveSessionFilePathOptions,
|
||||
listConfiguredSessionStoreAgentIds,
|
||||
type SessionEntry,
|
||||
updateSessionStore,
|
||||
} from "../../config/sessions.js";
|
||||
import { resolveAgentMainSessionKey } from "../../config/sessions/main-session.js";
|
||||
import { CURRENT_SESSION_VERSION } from "../../config/sessions/version.js";
|
||||
import { createSessionEntryWithTranscript } from "../../config/sessions/session-accessor.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import {
|
||||
createInternalHookEvent,
|
||||
@@ -497,44 +495,6 @@ async function createAgentMainSessionForSend(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function ensureSessionTranscriptFile(params: {
|
||||
sessionId: string;
|
||||
storePath: string;
|
||||
sessionFile?: string;
|
||||
agentId: string;
|
||||
}): { ok: true; transcriptPath: string } | { ok: false; error: string } {
|
||||
try {
|
||||
const transcriptPath = resolveSessionFilePath(
|
||||
params.sessionId,
|
||||
params.sessionFile ? { sessionFile: params.sessionFile } : undefined,
|
||||
resolveSessionFilePathOptions({
|
||||
storePath: params.storePath,
|
||||
agentId: params.agentId,
|
||||
}),
|
||||
);
|
||||
if (!fs.existsSync(transcriptPath)) {
|
||||
fs.mkdirSync(path.dirname(transcriptPath), { recursive: true });
|
||||
const header = {
|
||||
type: "session",
|
||||
version: CURRENT_SESSION_VERSION,
|
||||
id: params.sessionId,
|
||||
timestamp: new Date().toISOString(),
|
||||
cwd: process.cwd(),
|
||||
};
|
||||
fs.writeFileSync(transcriptPath, `${JSON.stringify(header)}\n`, {
|
||||
encoding: "utf-8",
|
||||
mode: 0o600,
|
||||
});
|
||||
}
|
||||
return { ok: true, transcriptPath };
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
error: formatErrorMessage(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAbortSessionKey(params: {
|
||||
context: Pick<GatewayRequestContext, "chatAbortControllers">;
|
||||
requestedKey: string;
|
||||
@@ -1511,76 +1471,56 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
||||
: buildDashboardSessionKey(agentId);
|
||||
const target = resolveGatewaySessionStoreTarget({ cfg, key, agentId });
|
||||
const targetAgentId = target.agentId;
|
||||
const created = await updateSessionStore(target.storePath, async (store) => {
|
||||
const patched = await applySessionsPatchToStore({
|
||||
cfg,
|
||||
store,
|
||||
storeKey: target.canonicalKey,
|
||||
const created = await createSessionEntryWithTranscript(
|
||||
{
|
||||
agentId: targetAgentId,
|
||||
patch: {
|
||||
key: target.canonicalKey,
|
||||
label: normalizeOptionalString(p.label),
|
||||
model: normalizeOptionalString(p.model),
|
||||
},
|
||||
loadGatewayModelCatalog: context.loadGatewayModelCatalog,
|
||||
});
|
||||
if (!patched.ok || !canonicalParentSessionKey) {
|
||||
return patched;
|
||||
}
|
||||
const inheritedSelection = normalizeOptionalString(p.model)
|
||||
? {}
|
||||
: inheritSessionRuntimeSelection(parentSessionEntry);
|
||||
const nextEntry: SessionEntry = {
|
||||
...patched.entry,
|
||||
...inheritedSelection,
|
||||
parentSessionKey: canonicalParentSessionKey,
|
||||
};
|
||||
store[target.canonicalKey] = nextEntry;
|
||||
return {
|
||||
...patched,
|
||||
entry: nextEntry,
|
||||
};
|
||||
});
|
||||
sessionKey: target.canonicalKey,
|
||||
storePath: target.storePath,
|
||||
},
|
||||
async ({ sessionEntries }) => {
|
||||
const patched = await applySessionsPatchToStore({
|
||||
cfg,
|
||||
store: sessionEntries,
|
||||
storeKey: target.canonicalKey,
|
||||
agentId: targetAgentId,
|
||||
patch: {
|
||||
key: target.canonicalKey,
|
||||
label: normalizeOptionalString(p.label),
|
||||
model: normalizeOptionalString(p.model),
|
||||
},
|
||||
loadGatewayModelCatalog: context.loadGatewayModelCatalog,
|
||||
});
|
||||
if (!patched.ok || !canonicalParentSessionKey) {
|
||||
return patched;
|
||||
}
|
||||
const inheritedSelection = normalizeOptionalString(p.model)
|
||||
? {}
|
||||
: inheritSessionRuntimeSelection(parentSessionEntry);
|
||||
const nextEntry: SessionEntry = {
|
||||
...patched.entry,
|
||||
...inheritedSelection,
|
||||
parentSessionKey: canonicalParentSessionKey,
|
||||
};
|
||||
return {
|
||||
...patched,
|
||||
entry: nextEntry,
|
||||
};
|
||||
},
|
||||
);
|
||||
if (!created.ok) {
|
||||
respond(false, undefined, created.error);
|
||||
return;
|
||||
}
|
||||
const ensured = ensureSessionTranscriptFile({
|
||||
sessionId: created.entry.sessionId,
|
||||
storePath: target.storePath,
|
||||
sessionFile: created.entry.sessionFile,
|
||||
agentId: targetAgentId,
|
||||
});
|
||||
if (!ensured.ok) {
|
||||
await updateSessionStore(target.storePath, (store) => {
|
||||
delete store[target.canonicalKey];
|
||||
});
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.UNAVAILABLE, `failed to create session transcript: ${ensured.error}`),
|
||||
created.phase === "transcript"
|
||||
? errorShape(
|
||||
ErrorCodes.UNAVAILABLE,
|
||||
`failed to create session transcript: ${created.error}`,
|
||||
)
|
||||
: created.error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const createdEntry =
|
||||
created.entry.sessionFile === ensured.transcriptPath
|
||||
? created.entry
|
||||
: {
|
||||
...created.entry,
|
||||
sessionFile: ensured.transcriptPath,
|
||||
};
|
||||
if (createdEntry !== created.entry) {
|
||||
await updateSessionStore(target.storePath, (store) => {
|
||||
const existing = store[target.canonicalKey];
|
||||
if (existing) {
|
||||
store[target.canonicalKey] = {
|
||||
...existing,
|
||||
sessionFile: ensured.transcriptPath,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
const createdEntry = created.entry;
|
||||
|
||||
const initialMessage = resolveOptionalInitialSessionMessage(p);
|
||||
let runPayload: Record<string, unknown> | undefined;
|
||||
|
||||
@@ -82,6 +82,7 @@ test("sessions.create stores dashboard session model and parent linkage, and cre
|
||||
expect(sessionFile).toBe(rawStore[key]?.sessionFile);
|
||||
|
||||
const transcriptPath = path.join(dir, `${created.payload?.sessionId}.jsonl`);
|
||||
await expect(fs.realpath(sessionFile)).resolves.toBe(await fs.realpath(transcriptPath));
|
||||
const transcript = await fs.readFile(transcriptPath, "utf-8");
|
||||
const [headerLine] = transcript.trim().split(/\r?\n/, 1);
|
||||
const header = JSON.parse(headerLine) as { type?: string; id?: string };
|
||||
@@ -259,6 +260,40 @@ test("sessions.create replaces a dead main entry with a fresh session id", async
|
||||
}
|
||||
});
|
||||
|
||||
test("sessions.create rolls back the entry when transcript initialization fails", async () => {
|
||||
const { dir, storePath } = await createSessionStoreDir();
|
||||
testState.agentsConfig = { list: [{ id: "ops", default: true }] };
|
||||
const blockerPath = path.join(dir, "blocked");
|
||||
await fs.writeFile(blockerPath, "not a directory", "utf-8");
|
||||
try {
|
||||
await writeSessionStore({
|
||||
agentId: "ops",
|
||||
entries: {
|
||||
main: {
|
||||
sessionFile: "blocked/session-1.jsonl",
|
||||
sessionId: "session-1",
|
||||
updatedAt: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const created = await directSessionReq("sessions.create", {
|
||||
key: "main",
|
||||
agentId: "ops",
|
||||
});
|
||||
|
||||
expect(created.ok).toBe(false);
|
||||
expect((created.error as { code?: string } | undefined)?.code).toBe("UNAVAILABLE");
|
||||
expect((created.error as { message?: string } | undefined)?.message ?? "").toContain(
|
||||
"failed to create session transcript:",
|
||||
);
|
||||
const rawStore = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record<string, unknown>;
|
||||
expect(rawStore["agent:ops:main"]).toBeUndefined();
|
||||
} finally {
|
||||
testState.agentsConfig = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
test("sessions.create preserves global and unknown sentinel keys", async () => {
|
||||
const { storePath } = await createSessionStoreDir();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
findGatewaySessionCreateLifecycleViolations,
|
||||
findSessionAccessorBoundaryViolations,
|
||||
findSessionAccessorWriteBoundaryViolations,
|
||||
findTranscriptWriterBoundaryViolations,
|
||||
@@ -239,6 +240,40 @@ describe("session accessor boundary guard", () => {
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("flags legacy writers inside the gateway sessions.create lifecycle", () => {
|
||||
expect(
|
||||
findGatewaySessionCreateLifecycleViolations(`
|
||||
const handlers = {
|
||||
"sessions.create": async () => {
|
||||
await updateSessionStore(storePath, () => undefined);
|
||||
ensureSessionTranscriptFile(params);
|
||||
},
|
||||
"sessions.patch": async () => {
|
||||
await updateSessionStore(storePath, () => undefined);
|
||||
},
|
||||
};
|
||||
`),
|
||||
).toEqual([
|
||||
{ line: 4, reason: 'calls legacy sessions.create lifecycle writer "updateSessionStore"' },
|
||||
{
|
||||
line: 5,
|
||||
reason: 'calls legacy sessions.create lifecycle writer "ensureSessionTranscriptFile"',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("allows the gateway sessions.create lifecycle accessor seam", () => {
|
||||
expect(
|
||||
findGatewaySessionCreateLifecycleViolations(`
|
||||
const handlers = {
|
||||
"sessions.create": async () => {
|
||||
await createSessionEntryWithTranscript(scope, createEntry);
|
||||
},
|
||||
};
|
||||
`),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("ignores comments and strings that describe legacy readers", () => {
|
||||
expect(
|
||||
findSessionAccessorBoundaryViolations(`
|
||||
|
||||
Reference in New Issue
Block a user