mirror of
https://github.com/langgenius/dify.git
synced 2026-09-02 22:36:15 +08:00
199 lines
6.1 KiB
TypeScript
199 lines
6.1 KiB
TypeScript
import type { CacheAdapter } from "@knowledge/core";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import { createCacheSessionContextRepository } from "./session-context-repository";
|
|
|
|
function createRecordingCache(): CacheAdapter & {
|
|
readonly values: Map<string, Uint8Array>;
|
|
} {
|
|
const values = new Map<string, Uint8Array>();
|
|
|
|
return {
|
|
kind: "memory",
|
|
values,
|
|
delete: async (key) => {
|
|
values.delete(key);
|
|
},
|
|
get: async (key) => {
|
|
const value = values.get(key);
|
|
|
|
return value ? new Uint8Array(value) : null;
|
|
},
|
|
health: async () => true,
|
|
set: async (key, value) => {
|
|
values.set(key, new Uint8Array(value));
|
|
},
|
|
stats: async () => ({
|
|
entries: values.size,
|
|
totalBytes: [...values.values()].reduce((sum, value) => sum + value.byteLength, 0),
|
|
}),
|
|
};
|
|
}
|
|
|
|
describe("createCacheSessionContextRepository", () => {
|
|
it("compensates the cache write when the retrieval lease is lost after set", async () => {
|
|
const cache = createRecordingCache();
|
|
const sessions = createCacheSessionContextRepository({
|
|
cache,
|
|
generateId: () => "session-fenced",
|
|
});
|
|
const assertActive = vi
|
|
.fn<() => Promise<void>>()
|
|
.mockResolvedValueOnce(undefined)
|
|
.mockRejectedValueOnce(new Error("lease lost"));
|
|
|
|
await expect(
|
|
sessions.recordQuery({
|
|
knowledgeSpaceId: "space-a",
|
|
permissionSnapshot: ["read"],
|
|
query: "must be compensated",
|
|
retrievalExecution: { assertActive },
|
|
subjectId: "subject-a",
|
|
tenantId: "tenant-a",
|
|
traceId: "trace-a",
|
|
}),
|
|
).rejects.toThrow("lease lost");
|
|
|
|
expect(assertActive).toHaveBeenCalledTimes(2);
|
|
expect(cache.values.size).toBe(0);
|
|
});
|
|
|
|
it("records bounded cache-backed query context with clone isolation", async () => {
|
|
const cache = createRecordingCache();
|
|
const sessions = createCacheSessionContextRepository({
|
|
cache,
|
|
generateId: () => "session-a",
|
|
maxActiveDocumentIds: 2,
|
|
maxActiveEntityIds: 2,
|
|
maxPreviousQueries: 2,
|
|
now: () => Date.parse("2026-05-11T13:00:00.000Z"),
|
|
ttlMs: 60_000,
|
|
});
|
|
|
|
const first = await sessions.recordQuery({
|
|
activeDocumentIds: ["doc-a", "doc-b", "doc-c"],
|
|
activeEntityIds: ["entity-a", "entity-b", "entity-c"],
|
|
knowledgeSpaceId: "space-a",
|
|
permissionSnapshot: ["read", "write"],
|
|
query: "first question",
|
|
subjectId: "subject-a",
|
|
tenantId: "tenant-a",
|
|
traceId: "trace-a",
|
|
});
|
|
const firstQuery = first.stored.previousQueries[0];
|
|
expect(firstQuery).toBeDefined();
|
|
if (!firstQuery) {
|
|
throw new Error("Expected previous query");
|
|
}
|
|
(firstQuery as { query: string }).query = "mutated";
|
|
const storedKey = [...cache.values.keys()][0];
|
|
expect(storedKey).toContain("space-cache:v2:session-context:tenant:");
|
|
expect(storedKey).toContain(":space:space-a:version:session-context-v1:");
|
|
expect(storedKey).not.toContain("tenant-a");
|
|
|
|
await expect(
|
|
sessions.get({
|
|
knowledgeSpaceId: "space-a",
|
|
sessionId: "session-a",
|
|
subjectId: "subject-a",
|
|
tenantId: "tenant-a",
|
|
}),
|
|
).resolves.toEqual(
|
|
expect.objectContaining({
|
|
activeDocumentIds: ["doc-b", "doc-c"],
|
|
activeEntityIds: ["entity-b", "entity-c"],
|
|
previousQueries: [expect.objectContaining({ query: "first question" })],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("validates bounds and invalidates context when permissions change", async () => {
|
|
expect(() =>
|
|
createCacheSessionContextRepository({
|
|
cache: createRecordingCache(),
|
|
maxPreviousQueries: 0,
|
|
}),
|
|
).toThrow("Session context maxPreviousQueries must be at least 1");
|
|
|
|
const sessions = createCacheSessionContextRepository({
|
|
cache: createRecordingCache(),
|
|
generateId: () => "session-b",
|
|
now: () => Date.parse("2026-05-11T13:00:00.000Z"),
|
|
ttlMs: 60_000,
|
|
});
|
|
await sessions.recordQuery({
|
|
knowledgeSpaceId: "space-a",
|
|
permissionSnapshot: ["read"],
|
|
query: "first question",
|
|
subjectId: "subject-a",
|
|
tenantId: "tenant-a",
|
|
traceId: "trace-a",
|
|
});
|
|
const changed = await sessions.recordQuery({
|
|
knowledgeSpaceId: "space-a",
|
|
permissionSnapshot: ["read", "write"],
|
|
query: "second question",
|
|
sessionId: "session-b",
|
|
subjectId: "subject-a",
|
|
tenantId: "tenant-a",
|
|
traceId: "trace-b",
|
|
});
|
|
|
|
expect(changed.context.permissionInvalidated).toBe(true);
|
|
expect(changed.stored.previousQueries).toEqual([
|
|
expect.objectContaining({ query: "second question" }),
|
|
]);
|
|
});
|
|
|
|
it("records image-only query hashes and keeps validation backward compatible", async () => {
|
|
const sessions = createCacheSessionContextRepository({
|
|
cache: createRecordingCache(),
|
|
generateId: () => "session-images",
|
|
});
|
|
|
|
const result = await sessions.recordQuery({
|
|
knowledgeSpaceId: "space-a",
|
|
permissionSnapshot: ["read"],
|
|
query: "",
|
|
queryImageHashes: [" hash-a ", "hash-a", "hash-b"],
|
|
subjectId: "subject-a",
|
|
tenantId: "tenant-a",
|
|
traceId: "trace-images",
|
|
});
|
|
|
|
expect(result.context.previousQueries).toEqual([]);
|
|
expect(result.stored.previousQueries).toEqual([
|
|
{
|
|
askedAt: expect.any(String),
|
|
query: "",
|
|
queryImageHashes: ["hash-a", "hash-b"],
|
|
traceId: "trace-images",
|
|
},
|
|
]);
|
|
const storedHashes = result.stored.previousQueries[0]?.queryImageHashes;
|
|
expect(storedHashes).toEqual(["hash-a", "hash-b"]);
|
|
|
|
await expect(
|
|
sessions.recordQuery({
|
|
knowledgeSpaceId: "space-a",
|
|
permissionSnapshot: [],
|
|
query: " ",
|
|
subjectId: "subject-a",
|
|
tenantId: "tenant-a",
|
|
traceId: "legacy-empty",
|
|
}),
|
|
).rejects.toThrow("Session context query is required");
|
|
await expect(
|
|
sessions.recordQuery({
|
|
knowledgeSpaceId: "space-a",
|
|
permissionSnapshot: [],
|
|
query: " ",
|
|
queryImageHashes: [],
|
|
subjectId: "subject-a",
|
|
tenantId: "tenant-a",
|
|
traceId: "explicit-empty",
|
|
}),
|
|
).rejects.toThrow("Session context query or queryImageHashes is required");
|
|
});
|
|
});
|