@jskit-ai/assistant-runtime 0.2.29 → 0.2.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +6 -3
- package/package.json +16 -10
- package/patterns/assistant-surface/PATTERN.md +5 -2
- package/src/client/components/AssistantSettingsClientElement.vue +5 -3
- package/src/server/AssistantProvider.js +8 -13
- package/src/server/AssistantStoreFeature.js +59 -0
- package/src/server/actions.js +5 -5
- package/src/server/createAssistantRuntime.js +2 -1
- package/src/server/createAssistantSettingsActions.js +17 -9
- package/src/server/index.js +1 -0
- package/src/server/registerConversationSubscriptions.js +5 -2
- package/src/server/registerRoutes.js +11 -8
- package/src/server/registerSettingsRoutes.js +4 -4
- package/src/server/repositories/assistantConfigRepository.js +52 -67
- package/src/server/repositories/assistantResources.js +119 -0
- package/src/server/repositories/conversationsRepository.js +164 -147
- package/src/server/repositories/messagesRepository.js +136 -87
- package/src/server/repositories/repositoryPersistenceUtils.js +11 -22
- package/src/server/repositories/turnRequestsRepository.js +50 -16
- package/src/server/services/assistantConfigService.js +22 -0
- package/src/server/services/chatService.js +15 -7
- package/src/server/services/transcriptService.js +3 -3
- package/src/server/sqlConversationStorage.js +23 -19
- package/src/server/types.js +1 -0
- package/test/assistantIntegrationBoundary.test.js +2 -0
- package/test/assistantSettings.browser.test.js +23 -2
- package/test/assistantSettingsAccess.test.js +46 -1
- package/test/chatServiceLifecycle.test.js +32 -1
- package/test/featureRuntime.test.js +17 -3
- package/test/repositories.mysql.test.js +474 -0
- package/test/repositoryContracts.test.js +13 -13
- package/test/sqlConversationStorage.mysql.test.js +9 -6
- package/test/sqlConversationStorage.test.js +27 -20
- package/test/support/assistantStore.js +39 -0
- package/test/support/memoryAssistantRows.js +11 -4
- package/test/turnRequests.test.js +37 -14
- package/test/usersForeignKeysMigration.test.js +24 -0
|
@@ -11,15 +11,13 @@ import { verifyConversationStorageContract } from "@jskit-ai/assistant-core/test
|
|
|
11
11
|
import transcriptsMigration from "../migrations/assistant_transcripts_initial.cjs";
|
|
12
12
|
import summaryMigration from "../migrations/assistant_transcripts_summary.cjs";
|
|
13
13
|
import claimsMigration from "../migrations/assistant_turn_requests.cjs";
|
|
14
|
-
import { createRepository as createConversationsRepository } from "../src/server/repositories/conversationsRepository.js";
|
|
15
|
-
import { createRepository as createMessagesRepository } from "../src/server/repositories/messagesRepository.js";
|
|
16
|
-
import { createRepository as createTurnRequestsRepository } from "../src/server/repositories/turnRequestsRepository.js";
|
|
17
14
|
import { createSqlConversationStorage } from "../src/server/sqlConversationStorage.js";
|
|
18
15
|
import { createAssistantConversationRuntime } from "../src/server/createAssistantRuntime.js";
|
|
19
16
|
import { createTranscriptService } from "../src/server/services/transcriptService.js";
|
|
20
17
|
import { createChatService } from "../src/server/services/chatService.js";
|
|
21
18
|
import { NO_PERSON_CONTEXT, createPersonContextPrompt } from "../src/server/support/personContext.js";
|
|
22
19
|
import { createScriptedModel } from "./support/scriptedModel.js";
|
|
20
|
+
import { createTestAssistantStore } from "./support/assistantStore.js";
|
|
23
21
|
|
|
24
22
|
// The chat service of an application that supplies no facts about the person.
|
|
25
23
|
const NO_FACTS = createPersonContextPrompt({ personContext: NO_PERSON_CONTEXT, logger: silentLogger });
|
|
@@ -27,18 +25,25 @@ const NO_FACTS = createPersonContextPrompt({ personContext: NO_PERSON_CONTEXT, l
|
|
|
27
25
|
async function fixture(t) {
|
|
28
26
|
const directory = await mkdtemp(path.join(tmpdir(), "assistant-sql-runtime-"));
|
|
29
27
|
const clients = [];
|
|
30
|
-
|
|
28
|
+
const stores = [];
|
|
29
|
+
async function connect() {
|
|
31
30
|
const db = knex({ client: "better-sqlite3", connection: { filename: path.join(directory, "assistant.sqlite") },
|
|
32
31
|
useNullAsDefault: true, pool: { min: 1, max: 1, afterCreate(connection, done) {
|
|
33
32
|
connection.pragma("busy_timeout = 0");
|
|
34
33
|
done(null, connection);
|
|
35
34
|
} } });
|
|
36
35
|
clients.push(db);
|
|
37
|
-
const
|
|
38
|
-
|
|
36
|
+
const store = await createTestAssistantStore(db);
|
|
37
|
+
stores.push(store);
|
|
38
|
+
const repositories = { conversationsRepository: store.conversations, messagesRepository: store.messages };
|
|
39
|
+
return { db, turnRequests: store.turnRequests, ...repositories, storage: createSqlConversationStorage(repositories) };
|
|
39
40
|
}
|
|
40
|
-
t.after(async () => {
|
|
41
|
-
|
|
41
|
+
t.after(async () => {
|
|
42
|
+
for (const store of stores) await store.close();
|
|
43
|
+
for (const client of clients) await client.destroy();
|
|
44
|
+
await rm(directory, { recursive: true, force: true });
|
|
45
|
+
});
|
|
46
|
+
const first = await connect();
|
|
42
47
|
await first.db.schema.createTable("users", table => table.bigInteger("id").primary());
|
|
43
48
|
await first.db("users").insert([{ id: 1 }, { id: 2 }]);
|
|
44
49
|
await transcriptsMigration.up(first.db);
|
|
@@ -100,7 +105,7 @@ test("the existing runtime storage contract uses the original SQL rows and rolls
|
|
|
100
105
|
test("independent SQL connections serialize authored receipt and sequence allocation", async t => {
|
|
101
106
|
const f = await fixture(t);
|
|
102
107
|
const conversation = await f.conversation();
|
|
103
|
-
const second = f.connect();
|
|
108
|
+
const second = await f.connect();
|
|
104
109
|
const transcripts = [f.storage, second.storage].map(storage => createConversationTranscript({ storage }));
|
|
105
110
|
const input = { messageId: "invoice/42:follow-up", text: "Only once" };
|
|
106
111
|
const results = await Promise.all(transcripts.map(transcript => transcript.writeConversationUserMessage(conversation.id, input)));
|
|
@@ -181,7 +186,7 @@ test("the SQL facade uses common inference and durable tool rows, and the model'
|
|
|
181
186
|
const runtime = createAssistantConversationRuntime({ ...f, aiClientFactory, toolCatalog: catalog });
|
|
182
187
|
t.after(() => runtime.close());
|
|
183
188
|
const dependencies = { conversationRuntime: runtime, aiClientFactory, serviceToolCatalog: catalog,
|
|
184
|
-
transcriptService: createTranscriptService(f), turnRequests:
|
|
189
|
+
transcriptService: createTranscriptService(f), turnRequests: f.turnRequests,
|
|
185
190
|
assistantConfigService: { resolveSystemPrompt: async () => "Retained SQL instructions" },
|
|
186
191
|
personContextPrompt: NO_FACTS,
|
|
187
192
|
appConfig: { surfaceDefinitions: { assistant: { enabled: true, requiresWorkspace: false } },
|
|
@@ -236,7 +241,7 @@ test("the SQL facade replays historical inspection failures with their exact pub
|
|
|
236
241
|
executeToolCall() { effects++; assert.fail("Historical inspection cannot invoke application effects"); } };
|
|
237
242
|
const runtime = createAssistantConversationRuntime({ ...f, aiClientFactory, toolCatalog: catalog });
|
|
238
243
|
t.after(() => runtime.close());
|
|
239
|
-
const turnRequests =
|
|
244
|
+
const turnRequests = f.turnRequests;
|
|
240
245
|
const dependencies = { conversationRuntime: runtime, aiClientFactory, serviceToolCatalog: catalog,
|
|
241
246
|
transcriptService: createTranscriptService(f), turnRequests,
|
|
242
247
|
assistantConfigService: { resolveSystemPrompt: async () => "Read the supplied context" },
|
|
@@ -260,7 +265,7 @@ test("the SQL facade replays historical inspection failures with their exact pub
|
|
|
260
265
|
await assert.rejects(createChatService(dependencies).streamChat(input, options), requireInspection);
|
|
261
266
|
const receipt = await f.db("assistant_turn_requests").first();
|
|
262
267
|
assert.equal(receipt.status, "failed");
|
|
263
|
-
const restarted = createChatService({ ...dependencies, turnRequests:
|
|
268
|
+
const restarted = createChatService({ ...dependencies, turnRequests: f.turnRequests });
|
|
264
269
|
await assert.rejects(restarted.streamChat(input, options), requireInspection);
|
|
265
270
|
assert.deepEqual(JSON.parse(receipt.response_json).failure, {
|
|
266
271
|
message: "This conversation has ambiguous historical SQL rows. Inspect the reported rows offline before using the common runtime; no history was changed.",
|
|
@@ -347,7 +352,7 @@ for (const shape of ["orphan-output", "orphan-tool-result", "unconfirmed-tool"])
|
|
|
347
352
|
executeToolCall() { effects++; assert.fail("Retained history cannot repeat an application effect"); } };
|
|
348
353
|
const runtime = createAssistantConversationRuntime({ ...f, aiClientFactory, toolCatalog: catalog });
|
|
349
354
|
t.after(() => runtime.close());
|
|
350
|
-
const turnRequests =
|
|
355
|
+
const turnRequests = f.turnRequests;
|
|
351
356
|
const dependencies = { conversationRuntime: runtime, aiClientFactory, serviceToolCatalog: catalog,
|
|
352
357
|
transcriptService: createTranscriptService(f), turnRequests,
|
|
353
358
|
assistantConfigService: { resolveSystemPrompt: async () => "Read only the admitted request" },
|
|
@@ -376,7 +381,7 @@ for (const shape of ["orphan-output", "orphan-tool-result", "unconfirmed-tool"])
|
|
|
376
381
|
return true;
|
|
377
382
|
});
|
|
378
383
|
const receipt = await f.db("assistant_turn_requests").first();
|
|
379
|
-
const restarted = createChatService({ ...dependencies, turnRequests:
|
|
384
|
+
const restarted = createChatService({ ...dependencies, turnRequests: f.turnRequests });
|
|
380
385
|
await assert.rejects(restarted.streamChat(input, options), error => {
|
|
381
386
|
assert.equal(error.status, 409);
|
|
382
387
|
assert.equal(error.code, "assistant_history_inspection_required");
|
|
@@ -464,7 +469,7 @@ test("a long conversation's rows are read once, then each write appends its rows
|
|
|
464
469
|
const f = await fixture(t);
|
|
465
470
|
const conversation = await f.conversation();
|
|
466
471
|
await writeTurns(f.storage, conversation.id, 300);
|
|
467
|
-
const cold = f.connect();
|
|
472
|
+
const cold = await f.connect();
|
|
468
473
|
let recorded = recordQueries(cold.db);
|
|
469
474
|
assert.equal((await cold.storage.read(conversation.id, transaction => transaction.listTurnIds())).length, 300);
|
|
470
475
|
recorded.stop();
|
|
@@ -485,12 +490,14 @@ test("a long conversation's rows are read once, then each write appends its rows
|
|
|
485
490
|
const write = await measure(() => appendTurn("after-300"));
|
|
486
491
|
assert.equal(write.result, "000301");
|
|
487
492
|
assert.deepEqual(write.queries.filter(readsWholeConversation), []);
|
|
488
|
-
|
|
493
|
+
// BEGIN, the conversation's lock, its last seq, the message's insert and read-back, one update of the conversation, COMMIT;
|
|
494
|
+
// SQLite takes the lock with a no-op write and then reads the row.
|
|
495
|
+
assert.ok(write.queries.length <= 8, `A write issues a constant number of queries, not one per stored row (${write.queries.length})`);
|
|
489
496
|
const read = await measure(() => cold.storage.read(conversation.id, transaction => transaction.readTurn("000301")));
|
|
490
497
|
assert.equal(read.result.user.text, "One more");
|
|
491
498
|
assert.deepEqual(read.result.metadata, { runtime: { status: "running" } });
|
|
492
499
|
assert.deepEqual(read.queries.filter(readsWholeConversation), []);
|
|
493
|
-
assert.ok(read.queries.length <=
|
|
500
|
+
assert.ok(read.queries.length <= 4, `A read issues a constant number of queries (${read.queries.length})`);
|
|
494
501
|
|
|
495
502
|
await writeTurns(cold.storage, conversation.id, 50, "later");
|
|
496
503
|
const longer = await measure(() => appendTurn("after-350"));
|
|
@@ -500,7 +507,7 @@ test("a long conversation's rows are read once, then each write appends its rows
|
|
|
500
507
|
transaction.replaceAssistant("000301", { role: "assistant", messageId: "after-300:reply-1", text: "Answered", at: "2026-02-01T00:00:01.000Z" })));
|
|
501
508
|
assert.deepEqual(answer.queries.filter(readsWholeConversation), []);
|
|
502
509
|
|
|
503
|
-
const fresh = f.connect();
|
|
510
|
+
const fresh = await f.connect();
|
|
504
511
|
const expected = await fresh.storage.read(conversation.id, async transaction =>
|
|
505
512
|
Promise.all((await transaction.listTurnIds()).map(id => transaction.readTurn(id))));
|
|
506
513
|
const cached = await cold.storage.read(conversation.id, async transaction =>
|
|
@@ -512,7 +519,7 @@ test("a long conversation's rows are read once, then each write appends its rows
|
|
|
512
519
|
test("a server sees another server's saves, direct row inserts and conversation updates at once", async t => {
|
|
513
520
|
const f = await fixture(t);
|
|
514
521
|
const conversation = await f.conversation();
|
|
515
|
-
const other = f.connect();
|
|
522
|
+
const other = await f.connect();
|
|
516
523
|
await writeTurns(f.storage, conversation.id, 2);
|
|
517
524
|
assert.deepEqual(await f.storage.read(conversation.id, transaction => transaction.listTurnIds()), ["000001", "000002"]);
|
|
518
525
|
await writeTurns(other.storage, conversation.id, 1, "other");
|
|
@@ -569,7 +576,7 @@ test("the summary and the last turn it covers are saved on the conversation row,
|
|
|
569
576
|
assert.equal(Object.hasOwn(await f.conversationsRepository.findById(conversation.id), "summary"), false,
|
|
570
577
|
"Conversation records sent to the browser do not carry the summary");
|
|
571
578
|
|
|
572
|
-
const restarted = f.connect();
|
|
579
|
+
const restarted = await f.connect();
|
|
573
580
|
const later = await restarted.storage.read(conversation.id, async transaction => {
|
|
574
581
|
const summary = await transaction.readSummary();
|
|
575
582
|
return { summary, turns: await transaction.readTurnsAfter(summary.throughTurnId) };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ActionProvider } from "@jskit-ai/kernel/server/actions";
|
|
2
|
+
import { EventProvider } from "@jskit-ai/kernel/server/runtime";
|
|
3
|
+
import { silentLogger } from "@jskit-ai/kernel/server/runtime/logger";
|
|
4
|
+
import { createCapabilityRuntime, defineProvider } from "@jskit-ai/kernel/shared/capabilities";
|
|
5
|
+
import { ResourceRuntimeProvider } from "@jskit-ai/resource-runtime/server";
|
|
6
|
+
import { AssistantStoreFeature } from "../../src/server/AssistantStoreFeature.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The assistant's repositories on a test's own Knex, as an application gets
|
|
10
|
+
* them: AssistantStoreFeature owning its tables through the resource runtime.
|
|
11
|
+
* `close()` shuts that runtime down; the Knex stays the test's.
|
|
12
|
+
* @param {import("knex").Knex} knex
|
|
13
|
+
*/
|
|
14
|
+
export async function createTestAssistantStore(knex) {
|
|
15
|
+
let repositories = null;
|
|
16
|
+
const runtime = createCapabilityRuntime({
|
|
17
|
+
inputs: { "runtime.logger": silentLogger },
|
|
18
|
+
providers: [
|
|
19
|
+
EventProvider,
|
|
20
|
+
ActionProvider,
|
|
21
|
+
defineProvider({ id: "test.database", provides: { value: "runtime.database" }, setup: () => ({ value: { knex } }) }),
|
|
22
|
+
ResourceRuntimeProvider,
|
|
23
|
+
AssistantStoreFeature,
|
|
24
|
+
defineProvider({
|
|
25
|
+
id: "test.assistant-store",
|
|
26
|
+
requires: { store: "assistant.repositories" },
|
|
27
|
+
setup({ store }) {
|
|
28
|
+
repositories = store;
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
]
|
|
33
|
+
});
|
|
34
|
+
await runtime.start();
|
|
35
|
+
return {
|
|
36
|
+
...repositories,
|
|
37
|
+
close: () => runtime.shutdown()
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -18,14 +18,21 @@ export function createMemoryAssistantRows({ id = "conversation_1", surfaceId = "
|
|
|
18
18
|
Object.assign(target.conversation, structuredClone(patch));
|
|
19
19
|
return structuredClone(target.conversation);
|
|
20
20
|
},
|
|
21
|
-
async
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
async findWithTranscriptState(value, options) {
|
|
22
|
+
if (value !== id) return null;
|
|
23
|
+
const { conversation, transcript } = read(options);
|
|
24
|
+
return structuredClone({ conversation, ...transcript });
|
|
25
|
+
},
|
|
26
|
+
async writeTranscriptState(value, { changes = {}, ...transcript }, options) {
|
|
27
|
+
if (value !== id) return;
|
|
28
|
+
const target = read(options);
|
|
29
|
+
target.transcript = structuredClone(transcript);
|
|
30
|
+
Object.assign(target.conversation, structuredClone(changes));
|
|
24
31
|
},
|
|
25
32
|
transaction(operation) {
|
|
26
33
|
const next = pending.then(async () => {
|
|
27
34
|
const trx = structuredClone(state);
|
|
28
|
-
const value = await operation(trx);
|
|
35
|
+
const value = await operation({ trx });
|
|
29
36
|
state = trx;
|
|
30
37
|
messages.splice(0, messages.length, ...structuredClone(state.messages));
|
|
31
38
|
return value;
|
|
@@ -5,37 +5,60 @@ import { tmpdir } from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import knex from "knex";
|
|
7
7
|
import migration from "../migrations/assistant_turn_requests.cjs";
|
|
8
|
-
import { createRepository } from "../src/server/repositories/turnRequestsRepository.js";
|
|
9
8
|
import { silentLogger } from "@jskit-ai/kernel/server/runtime/logger";
|
|
10
9
|
import { createChatService } from "../src/server/services/chatService.js";
|
|
11
10
|
import { NO_PERSON_CONTEXT, createPersonContextPrompt } from "../src/server/support/personContext.js";
|
|
12
11
|
import { createAssistantConversationRuntime } from "../src/server/createAssistantRuntime.js";
|
|
13
12
|
import { createMemoryAssistantRows } from "./support/memoryAssistantRows.js";
|
|
14
13
|
import { createScriptedModel } from "./support/scriptedModel.js";
|
|
14
|
+
import { createTestAssistantStore } from "./support/assistantStore.js";
|
|
15
15
|
|
|
16
16
|
async function database(t) {
|
|
17
17
|
const dir = await mkdtemp(path.join(tmpdir(), "assistant-requests-"));
|
|
18
18
|
const connections = [];
|
|
19
|
+
const stores = [];
|
|
20
|
+
// Two connections in one process: a connection that waited for SQLite's lock would block the event loop the lock's
|
|
21
|
+
// holder needs to commit, so each finds the database busy at once, and its write's transaction runs again.
|
|
19
22
|
const connect = () => {
|
|
20
|
-
const db = knex({
|
|
23
|
+
const db = knex({
|
|
24
|
+
client: "better-sqlite3",
|
|
25
|
+
connection: { filename: path.join(dir, "requests.db") },
|
|
26
|
+
useNullAsDefault: true,
|
|
27
|
+
pool: {
|
|
28
|
+
afterCreate(connection, done) {
|
|
29
|
+
connection.pragma("busy_timeout = 0");
|
|
30
|
+
done(null, connection);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
21
34
|
connections.push(db);
|
|
22
35
|
return db;
|
|
23
36
|
};
|
|
24
|
-
t.after(async () => {
|
|
37
|
+
t.after(async () => {
|
|
38
|
+
for (const store of stores) await store.close();
|
|
39
|
+
for (const db of connections) await db.destroy();
|
|
40
|
+
await rm(dir, { recursive: true, force: true });
|
|
41
|
+
});
|
|
42
|
+
// The turn requests repository on a connection, as AssistantStoreFeature builds it.
|
|
43
|
+
const createRepository = async (connection) => {
|
|
44
|
+
const store = await createTestAssistantStore(connection);
|
|
45
|
+
stores.push(store);
|
|
46
|
+
return store.turnRequests;
|
|
47
|
+
};
|
|
25
48
|
const db = connect();
|
|
26
49
|
await db.schema.createTable("users", table => table.bigInteger("id").primary());
|
|
27
50
|
await db("users").insert([{ id: 1 }, { id: 2 }]);
|
|
28
51
|
await migration.up(db);
|
|
29
|
-
return { db, connect };
|
|
52
|
+
return { db, connect, createRepository };
|
|
30
53
|
}
|
|
31
54
|
|
|
32
55
|
const scope = { actorUserId: "1", surfaceId: "assistant", workspaceId: null };
|
|
33
56
|
const request = { messageId: "message-1", input: "Do this once." };
|
|
34
57
|
|
|
35
58
|
test("migration preserves existing claims and database uniqueness arbitrates independent connections", async t => {
|
|
36
|
-
const { db, connect } = await database(t);
|
|
37
|
-
const first = createRepository(db);
|
|
38
|
-
const second = createRepository(connect());
|
|
59
|
+
const { db, connect, createRepository } = await database(t);
|
|
60
|
+
const first = await createRepository(db);
|
|
61
|
+
const second = await createRepository(connect());
|
|
39
62
|
const claims = await Promise.all([first.claim(scope, request), second.claim(scope, request)]);
|
|
40
63
|
assert.equal(claims.filter(claim => claim.acquired).length, 1);
|
|
41
64
|
assert.equal((await db("assistant_turn_requests")).length, 1);
|
|
@@ -84,7 +107,7 @@ function run(chat, input = request, { events = [], disconnectAfterCompletion = f
|
|
|
84
107
|
}
|
|
85
108
|
|
|
86
109
|
test("concurrent submissions, lost completion responses and a fresh service replay one provider execution", async t => {
|
|
87
|
-
const { db, connect } = await database(t);
|
|
110
|
+
const { db, connect, createRepository } = await database(t);
|
|
88
111
|
const waiting = Promise.withResolvers();
|
|
89
112
|
const started = Promise.withResolvers();
|
|
90
113
|
let calls = 0;
|
|
@@ -96,11 +119,11 @@ test("concurrent submissions, lost completion responses and a fresh service repl
|
|
|
96
119
|
yield { choices: [{ delta: { content: "Completed once." } }] };
|
|
97
120
|
yield { choices: [{ delta: {}, finish_reason: "stop" }] };
|
|
98
121
|
};
|
|
99
|
-
const first = service(createRepository(db), { provider, transcript });
|
|
122
|
+
const first = service(await createRepository(db), { provider, transcript });
|
|
100
123
|
const pending = run(first, request, { disconnectAfterCompletion: true });
|
|
101
124
|
const failedResponse = assert.rejects(pending, /Response lost/);
|
|
102
125
|
await started.promise;
|
|
103
|
-
const second = service(createRepository(connect()), { provider, transcript });
|
|
126
|
+
const second = service(await createRepository(connect()), { provider, transcript });
|
|
104
127
|
const inFlight = [];
|
|
105
128
|
assert.equal((await run(second, request, { events: inFlight })).status, "unconfirmed");
|
|
106
129
|
assert.equal(inFlight.find(item => item.method === "sendMeta").event.conversationId, "100");
|
|
@@ -108,7 +131,7 @@ test("concurrent submissions, lost completion responses and a fresh service repl
|
|
|
108
131
|
waiting.resolve();
|
|
109
132
|
await failedResponse;
|
|
110
133
|
const replay = [];
|
|
111
|
-
const restarted = service(createRepository(connect()), { provider: () => { throw new Error("Replay must not invoke the provider."); } });
|
|
134
|
+
const restarted = service(await createRepository(connect()), { provider: () => { throw new Error("Replay must not invoke the provider."); } });
|
|
112
135
|
assert.equal((await run(restarted, request, { events: replay })).status, "completed");
|
|
113
136
|
assert.equal(replay.find(item => item.method === "sendAssistantMessage").event.text, "Completed once.");
|
|
114
137
|
assert.equal(calls, 1);
|
|
@@ -117,11 +140,11 @@ test("concurrent submissions, lost completion responses and a fresh service repl
|
|
|
117
140
|
});
|
|
118
141
|
|
|
119
142
|
test("an interrupted claim is never stolen after restart, and provider failures replay without executing again", async t => {
|
|
120
|
-
const { db, connect } = await database(t);
|
|
121
|
-
const repository = createRepository(db);
|
|
143
|
+
const { db, connect, createRepository } = await database(t);
|
|
144
|
+
const repository = await createRepository(db);
|
|
122
145
|
await repository.claim(scope, { ...request, conversationId: null, integrationId: "" });
|
|
123
146
|
let calls = 0;
|
|
124
|
-
const chat = service(createRepository(connect()), { provider: () => { calls++; throw new Error("Provider failed."); } });
|
|
147
|
+
const chat = service(await createRepository(connect()), { provider: () => { calls++; throw new Error("Provider failed."); } });
|
|
125
148
|
const unconfirmed = [];
|
|
126
149
|
assert.equal((await run(chat, request, { events: unconfirmed })).status, "unconfirmed");
|
|
127
150
|
assert.equal(unconfirmed.find(item => item.method === "sendError").event.messageCode, "assistant.runtime.request.unconfirmed");
|
|
@@ -5,6 +5,9 @@ import configMigration from "../migrations/assistant_config_initial.cjs";
|
|
|
5
5
|
import transcriptsMigration from "../migrations/assistant_transcripts_initial.cjs";
|
|
6
6
|
import turnRequestsMigration from "../migrations/assistant_turn_requests.cjs";
|
|
7
7
|
import usersForeignKeysMigration from "../migrations/constraints/assistant_users_foreign_keys.cjs";
|
|
8
|
+
import workspacesForeignKeysMigration from "../migrations/constraints/assistant_workspaces_foreign_keys.cjs";
|
|
9
|
+
import { checkForeignKeys } from "@jskit-ai/resource-runtime/server";
|
|
10
|
+
import * as assistantResources from "../src/server/repositories/assistantResources.js";
|
|
8
11
|
|
|
9
12
|
const USER_KEYS = [
|
|
10
13
|
{ table: "assistant_conversations", from: "created_by_user_id", on_delete: "SET NULL" },
|
|
@@ -44,3 +47,24 @@ test("the assistant tables are created before users exists and get their users k
|
|
|
44
47
|
await usersForeignKeysMigration.down(db);
|
|
45
48
|
assert.deepEqual(await usersKeys(db), []);
|
|
46
49
|
});
|
|
50
|
+
|
|
51
|
+
test("the assistant table resources declare the foreign keys their migrations always create", async (t) => {
|
|
52
|
+
const db = knex({ client: "better-sqlite3", connection: { filename: ":memory:" }, useNullAsDefault: true });
|
|
53
|
+
t.after(() => db.destroy());
|
|
54
|
+
const resources = Object.values(assistantResources);
|
|
55
|
+
const tables = resources.map((resource) => resource.tableName);
|
|
56
|
+
for (const migration of [configMigration, transcriptsMigration, turnRequestsMigration]) await migration.up(db);
|
|
57
|
+
await db.schema.createTable("users", (table) => table.bigInteger("id").primary());
|
|
58
|
+
await usersForeignKeysMigration.up(db);
|
|
59
|
+
assert.deepEqual(await checkForeignKeys({ knex: db, resources, tables }), { missing: [], undeclared: [] });
|
|
60
|
+
|
|
61
|
+
// An application with workspaces also gets the workspace_id keys, which no resource declares: not every
|
|
62
|
+
// application has them.
|
|
63
|
+
await db.schema.createTable("workspaces", (table) => table.bigInteger("id").primary());
|
|
64
|
+
await workspacesForeignKeysMigration.up(db);
|
|
65
|
+
const { missing, undeclared } = await checkForeignKeys({ knex: db, resources, tables });
|
|
66
|
+
assert.deepEqual(missing, []);
|
|
67
|
+
assert.deepEqual(undeclared.map(({ tableName, columns, referencedTableName }) => ({ tableName, columns, referencedTableName })),
|
|
68
|
+
["assistant_config", "assistant_conversations", "assistant_messages"]
|
|
69
|
+
.map((tableName) => ({ tableName, columns: ["workspace_id"], referencedTableName: "workspaces" })));
|
|
70
|
+
});
|