feat(FN-2449): merge fusion/fn-2449
This commit is contained in:
@@ -181,6 +181,110 @@ describe("ChatStore", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("findLatestActiveSessionForTarget", () => {
|
||||
it("returns newest exact model match for model-specific targets", async () => {
|
||||
const olderModelMatch = createTestSession(store, {
|
||||
agentId: "agent-lookup",
|
||||
projectId: "proj-1",
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-4o",
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
const newestModelMatch = createTestSession(store, {
|
||||
agentId: "agent-lookup",
|
||||
projectId: "proj-1",
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-4o",
|
||||
});
|
||||
|
||||
createTestSession(store, {
|
||||
agentId: "agent-lookup",
|
||||
projectId: "proj-1",
|
||||
modelProvider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
});
|
||||
|
||||
const found = store.findLatestActiveSessionForTarget({
|
||||
projectId: "proj-1",
|
||||
agentId: "agent-lookup",
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-4o",
|
||||
});
|
||||
|
||||
expect(found?.id).toBe(newestModelMatch.id);
|
||||
expect(found?.id).not.toBe(olderModelMatch.id);
|
||||
});
|
||||
|
||||
it("prefers model-less session for agent-only targets", async () => {
|
||||
const modelSpecific = createTestSession(store, {
|
||||
agentId: "agent-lookup",
|
||||
projectId: "proj-1",
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-4o",
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
const modelLess = createTestSession(store, {
|
||||
agentId: "agent-lookup",
|
||||
projectId: "proj-1",
|
||||
});
|
||||
|
||||
const found = store.findLatestActiveSessionForTarget({
|
||||
projectId: "proj-1",
|
||||
agentId: "agent-lookup",
|
||||
});
|
||||
|
||||
expect(found?.id).toBe(modelLess.id);
|
||||
expect(found?.id).not.toBe(modelSpecific.id);
|
||||
});
|
||||
|
||||
it("falls back to newest agent session when no model-less session exists", async () => {
|
||||
createTestSession(store, {
|
||||
agentId: "agent-lookup",
|
||||
projectId: "proj-1",
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-4o-mini",
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
const newestModelSpecific = createTestSession(store, {
|
||||
agentId: "agent-lookup",
|
||||
projectId: "proj-1",
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-4o",
|
||||
});
|
||||
|
||||
const found = store.findLatestActiveSessionForTarget({
|
||||
projectId: "proj-1",
|
||||
agentId: "agent-lookup",
|
||||
});
|
||||
|
||||
expect(found?.id).toBe(newestModelSpecific.id);
|
||||
});
|
||||
|
||||
it("returns undefined when there is no matching active session", () => {
|
||||
createTestSession(store, {
|
||||
agentId: "agent-lookup",
|
||||
projectId: "proj-1",
|
||||
});
|
||||
|
||||
const found = store.findLatestActiveSessionForTarget({
|
||||
projectId: "proj-2",
|
||||
agentId: "agent-lookup",
|
||||
});
|
||||
|
||||
expect(found).toBeUndefined();
|
||||
});
|
||||
|
||||
it("throws for inconsistent model-provider query pairs", () => {
|
||||
expect(() =>
|
||||
store.findLatestActiveSessionForTarget({
|
||||
projectId: "proj-1",
|
||||
agentId: "agent-lookup",
|
||||
modelProvider: "openai",
|
||||
}),
|
||||
).toThrow("modelProvider and modelId must both be provided together, or neither");
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateSession", () => {
|
||||
it("updates title and bumps updatedAt", async () => {
|
||||
const session = createTestSession(store);
|
||||
|
||||
@@ -203,6 +203,74 @@ export class ChatStore extends EventEmitter<ChatStoreEvents> {
|
||||
return (rows as unknown as ChatSessionRow[]).map((row) => this.rowToSession(row));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the newest active session for a specific quick-chat target.
|
||||
*
|
||||
* Matching semantics:
|
||||
* - model target (`modelProvider` + `modelId`): exact agent+model match
|
||||
* - agent target (no model): prefer model-less sessions, then newest agent session fallback
|
||||
*/
|
||||
findLatestActiveSessionForTarget(options: {
|
||||
agentId: string;
|
||||
projectId?: string;
|
||||
modelProvider?: string;
|
||||
modelId?: string;
|
||||
}): ChatSession | undefined {
|
||||
const normalizedAgentId = options.agentId.trim();
|
||||
if (!normalizedAgentId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalizedProvider = options.modelProvider?.trim();
|
||||
const normalizedModelId = options.modelId?.trim();
|
||||
|
||||
if ((normalizedProvider && !normalizedModelId) || (!normalizedProvider && normalizedModelId)) {
|
||||
throw new Error("modelProvider and modelId must both be provided together, or neither");
|
||||
}
|
||||
|
||||
const whereClauses: string[] = ["status = ?", "agentId = ?"];
|
||||
const baseParams: string[] = ["active", normalizedAgentId];
|
||||
|
||||
if (options.projectId && options.projectId.trim()) {
|
||||
whereClauses.push("projectId = ?");
|
||||
baseParams.push(options.projectId.trim());
|
||||
}
|
||||
|
||||
const baseWhereSql = whereClauses.join(" AND ");
|
||||
|
||||
if (normalizedProvider && normalizedModelId) {
|
||||
const row = this.db.prepare(`
|
||||
SELECT * FROM chat_sessions
|
||||
WHERE ${baseWhereSql} AND modelProvider = ? AND modelId = ?
|
||||
ORDER BY updatedAt DESC
|
||||
LIMIT 1
|
||||
`).get(...baseParams, normalizedProvider, normalizedModelId) as ChatSessionRow | undefined;
|
||||
return row ? this.rowToSession(row) : undefined;
|
||||
}
|
||||
|
||||
const modelLessRow = this.db.prepare(`
|
||||
SELECT * FROM chat_sessions
|
||||
WHERE ${baseWhereSql}
|
||||
AND COALESCE(TRIM(modelProvider), '') = ''
|
||||
AND COALESCE(TRIM(modelId), '') = ''
|
||||
ORDER BY updatedAt DESC
|
||||
LIMIT 1
|
||||
`).get(...baseParams) as ChatSessionRow | undefined;
|
||||
|
||||
if (modelLessRow) {
|
||||
return this.rowToSession(modelLessRow);
|
||||
}
|
||||
|
||||
const fallbackRow = this.db.prepare(`
|
||||
SELECT * FROM chat_sessions
|
||||
WHERE ${baseWhereSql}
|
||||
ORDER BY updatedAt DESC
|
||||
LIMIT 1
|
||||
`).get(...baseParams) as ChatSessionRow | undefined;
|
||||
|
||||
return fallbackRow ? this.rowToSession(fallbackRow) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a chat session.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user