@jskit-ai/assistant-runtime 0.2.26 → 0.2.28

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.
@@ -2,7 +2,8 @@ import assert from "node:assert/strict";
2
2
  import { mkdir } from "node:fs/promises";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import test from "node:test";
5
- import { chromium, expect } from "@playwright/test";
5
+ import { chromium } from "@playwright/test";
6
+ import { expect } from "../../../tooling/testUtils/browserExpect.mjs";
6
7
  import { createChromiumLaunchOptions, startViteFixture, stopProcess } from "../../../tooling/testUtils/browserFixture.mjs";
7
8
 
8
9
  const fixtureRoot = fileURLToPath(new URL("../fixtures/conversation/", import.meta.url));
@@ -10,9 +11,11 @@ const fixtureRoot = fileURLToPath(new URL("../fixtures/conversation/", import.me
10
11
  test("ready-made assistant preserves streaming, drafts, scope and compact conversation access", {
11
12
  skip: process.env.JSKIT_ASSISTANT_RUNTIME_BROWSER_INTEGRATION !== "1",
12
13
  timeout: 180_000
13
- }, async () => {
14
+ }, async (t) => {
14
15
  const vite = await startViteFixture({ fixtureRoot });
16
+ t.after(() => stopProcess(vite));
15
17
  const browser = await chromium.launch(createChromiumLaunchOptions());
18
+ t.after(() => browser.close());
16
19
  const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
17
20
  const errors = [];
18
21
  page.on("pageerror", (error) => errors.push(error.message));
@@ -138,18 +141,17 @@ test("ready-made assistant preserves streaming, drafts, scope and compact conver
138
141
  assert.deepEqual(errors, []);
139
142
  } catch (error) {
140
143
  throw new Error(`${error.message}\nPage errors: ${JSON.stringify(errors)}\n${vite.readOutput()}`, { cause: error });
141
- } finally {
142
- await browser.close();
143
- await stopProcess(vite);
144
144
  }
145
145
  });
146
146
 
147
147
  test("history restores preserve new typing and reject obsolete pages and failed automatic restores", {
148
148
  skip: process.env.JSKIT_ASSISTANT_RUNTIME_BROWSER_INTEGRATION !== "1",
149
149
  timeout: 60_000
150
- }, async () => {
150
+ }, async (t) => {
151
151
  const vite = await startViteFixture({ fixtureRoot });
152
+ t.after(() => stopProcess(vite));
152
153
  const browser = await chromium.launch(createChromiumLaunchOptions());
154
+ t.after(() => browser.close());
153
155
  const page = await browser.newPage();
154
156
  const input = page.getByRole("textbox", { name: "Message AI assistant" });
155
157
  const send = page.getByRole("button", { name: "Send message", exact: true });
@@ -220,92 +222,89 @@ test("history restores preserve new typing and reject obsolete pages and failed
220
222
  assert.equal(failedRequests, 1, "A failed automatic restore must require explicit selection to retry");
221
223
  } finally {
222
224
  release?.();
223
- await browser.close();
224
- await stopProcess(vite);
225
225
  }
226
226
  });
227
227
 
228
228
  test("page assistant bounds long history in a document layout and preserves drafts when workspace metadata arrives", {
229
229
  skip: process.env.JSKIT_ASSISTANT_RUNTIME_BROWSER_INTEGRATION !== "1",
230
230
  timeout: 60_000
231
- }, async () => {
231
+ }, async (t) => {
232
232
  const vite = await startViteFixture({ fixtureRoot });
233
+ t.after(() => stopProcess(vite));
233
234
  const browser = await chromium.launch(createChromiumLaunchOptions());
235
+ t.after(() => browser.close());
234
236
  const page = await browser.newPage();
235
- try {
236
- await page.goto(`${vite.baseURL}/?page=1`);
237
- await page.getByRole("button", { name: "Conversations", exact: true }).click();
238
- await page.getByRole("dialog", { name: "Conversations", exact: true }).getByText("Saved conversation", { exact: true }).click();
239
- await expect(page.getByText("Saved 1 message 70.", { exact: false })).toBeVisible();
240
- const input = page.getByRole("textbox", { name: "Message AI assistant" });
241
- for (const viewport of [{ width: 390, height: 844 }, { width: 800, height: 800 }, { width: 1280, height: 600 }]) {
242
- await page.setViewportSize(viewport);
243
- await expect.poll(async () => {
244
- const bounds = await input.boundingBox();
245
- return bounds.y + bounds.height;
246
- }).toBeLessThanOrEqual(viewport.height);
247
- assert.ok(await page.getByLabel("Conversation messages", { exact: true }).evaluate(element => element.scrollHeight > element.clientHeight));
248
- }
249
- await input.fill("Keep this draft while workspace details load.");
250
- await input.evaluate(element => { element.focus(); element.setSelectionRange(5, 9); });
251
- await page.getByRole("button", { name: "Load workspace metadata" }).evaluate(element => element.click());
252
- await expect(input).toHaveValue("Keep this draft while workspace details load.");
253
- await expect(input).toBeFocused();
254
- assert.deepEqual(await input.evaluate(element => [element.selectionStart, element.selectionEnd]), [5, 9]);
255
- await expect(page.getByText("Saved 1 message 70.", { exact: false })).toBeVisible();
256
- } finally {
257
- await browser.close();
258
- await stopProcess(vite);
237
+ await page.goto(`${vite.baseURL}/?page=1`);
238
+ await page.getByRole("button", { name: "Conversations", exact: true }).click();
239
+ await page.getByRole("dialog", { name: "Conversations", exact: true }).getByText("Saved conversation", { exact: true }).click();
240
+ await expect(page.getByText("Saved 1 message 70.", { exact: false })).toBeVisible();
241
+ const input = page.getByRole("textbox", { name: "Message AI assistant" });
242
+ for (const viewport of [{ width: 390, height: 844 }, { width: 800, height: 800 }, { width: 1280, height: 600 }]) {
243
+ await page.setViewportSize(viewport);
244
+ await expect.poll(async () => {
245
+ const bounds = await input.boundingBox();
246
+ return bounds.y + bounds.height;
247
+ }).toBeLessThanOrEqual(viewport.height);
248
+ assert.ok(await page.getByLabel("Conversation messages", { exact: true }).evaluate(element => element.scrollHeight > element.clientHeight));
259
249
  }
250
+ await input.fill("Keep this draft while workspace details load.");
251
+ await input.evaluate(element => { element.focus(); element.setSelectionRange(5, 9); });
252
+ await page.getByRole("button", { name: "Load workspace metadata" }).evaluate(element => element.click());
253
+ await expect(input).toHaveValue("Keep this draft while workspace details load.");
254
+ await expect(input).toBeFocused();
255
+ assert.deepEqual(await input.evaluate(element => [element.selectionStart, element.selectionEnd]), [5, 9]);
256
+ await expect(page.getByText("Saved 1 message 70.", { exact: false })).toBeVisible();
260
257
  });
261
258
 
262
259
 
263
260
  test("the ready-made UI streams from the real chat service with selected AI connections and files", {
264
261
  skip: process.env.JSKIT_ASSISTANT_RUNTIME_BROWSER_INTEGRATION !== "1", timeout: 90_000
265
- }, async () => {
262
+ }, async (t) => {
266
263
  const vite = await startViteFixture({ fixtureRoot });
264
+ t.after(() => stopProcess(vite));
267
265
  const browser = await chromium.launch(createChromiumLaunchOptions());
266
+ t.after(() => browser.close());
268
267
  const page = await browser.newPage({ viewport: { width: 1100, height: 800 } });
269
- try {
270
- await page.request.get(`${vite.baseURL}/fixture/service`);
271
- await page.goto(`${vite.baseURL}/?files=1`);
272
- const input = page.getByRole("textbox", { name: "Message AI assistant" });
273
- await expect(input).toBeEnabled();
274
- await page.getByRole("button", { name: "Choose AI", exact: true }).click();
275
- await page.getByRole("button", { name: "Deep model", exact: true }).click();
276
- await page.getByRole("button", { name: "Apply", exact: true }).click();
277
- await page.locator('input[type="file"]').setInputFiles({ name: "notes.txt", mimeType: "text/plain", buffer: Buffer.from("notes") });
278
- await expect(page.getByText("notes.txt", { exact: true })).toBeVisible();
279
- await input.fill("Read the notes");
280
- await input.press("Enter");
281
- await expect(page.getByText("Assistant is working…", { exact: true }).last()).toBeVisible();
282
- await expect(page.getByLabel("Message attachments")).toHaveCount(0);
283
- await expect(page.getByLabel("Attached files")).toBeVisible();
284
- await input.fill("My next draft");
285
- await input.evaluate(element => element.setSelectionRange(3, 7));
286
- await page.request.get(`${vite.baseURL}/fixture/next`);
287
- await expect(page.getByText("Partial answer", { exact: true })).toBeVisible();
288
- await expect(input).toHaveValue("My next draft");
289
- assert.deepEqual(await input.evaluate(element => [element.selectionStart, element.selectionEnd]), [3, 7]);
290
- let state = await (await page.request.get(`${vite.baseURL}/fixture/state`)).json();
291
- assert.equal(state.requests[0].integrationId, "deep");
292
- assert.deepEqual(state.requests[0].attachmentIds, ["fixture-file"]);
293
- assert.equal(state.transcript.filter(message => message.role === "assistant").length, 0);
294
- await page.request.get(`${vite.baseURL}/fixture/next?finish=1`);
295
- await expect(page.getByText("Partial answer completed.", { exact: true })).toHaveCount(1);
296
- await expect(page.getByText("Assistant is working…", { exact: true })).toHaveCount(0);
297
- state = await (await page.request.get(`${vite.baseURL}/fixture/state`)).json();
298
- assert.deepEqual(state.transcript[0].metadata.attachments, [{ attachmentId: "fixture-file", fileName: "notes.txt", size: 5 }]);
299
- assert.equal(state.transcript.filter(message => message.role === "assistant").length, 1);
300
- } finally { await browser.close(); await stopProcess(vite); }
268
+ await page.request.get(`${vite.baseURL}/fixture/service`);
269
+ await page.goto(`${vite.baseURL}/?files=1`);
270
+ const input = page.getByRole("textbox", { name: "Message AI assistant" });
271
+ await expect(input).toBeEnabled();
272
+ await page.getByRole("button", { name: "Choose AI", exact: true }).click();
273
+ await page.getByRole("button", { name: "Deep model", exact: true }).click();
274
+ await page.getByRole("button", { name: "Apply", exact: true }).click();
275
+ await page.locator('input[type="file"]').setInputFiles({ name: "notes.txt", mimeType: "text/plain", buffer: Buffer.from("notes") });
276
+ await expect(page.getByText("notes.txt", { exact: true })).toBeVisible();
277
+ await input.fill("Read the notes");
278
+ await input.press("Enter");
279
+ await expect(page.getByText("Assistant is working…", { exact: true }).last()).toBeVisible();
280
+ await expect(page.getByLabel("Message attachments")).toHaveCount(0);
281
+ await expect(page.getByLabel("Attached files")).toBeVisible();
282
+ await input.fill("My next draft");
283
+ await input.evaluate(element => element.setSelectionRange(3, 7));
284
+ await page.request.get(`${vite.baseURL}/fixture/next`);
285
+ await expect(page.getByText("Partial answer", { exact: true })).toBeVisible();
286
+ await expect(input).toHaveValue("My next draft");
287
+ assert.deepEqual(await input.evaluate(element => [element.selectionStart, element.selectionEnd]), [3, 7]);
288
+ let state = await (await page.request.get(`${vite.baseURL}/fixture/state`)).json();
289
+ assert.equal(state.requests[0].integrationId, "deep");
290
+ assert.deepEqual(state.requests[0].attachmentIds, ["fixture-file"]);
291
+ assert.equal(state.transcript.filter(message => message.role === "assistant").length, 0);
292
+ await page.request.get(`${vite.baseURL}/fixture/next?finish=1`);
293
+ await expect(page.getByText("Partial answer completed.", { exact: true })).toHaveCount(1);
294
+ await expect(page.getByText("Assistant is working…", { exact: true })).toHaveCount(0);
295
+ state = await (await page.request.get(`${vite.baseURL}/fixture/state`)).json();
296
+ assert.deepEqual(state.transcript[0].metadata.attachments, [{ attachmentId: "fixture-file", fileName: "notes.txt", size: 5 }]);
297
+ assert.equal(state.transcript.filter(message => message.role === "assistant").length, 1);
301
298
  });
302
299
 
303
300
  test("pending delivery appears immediately and retries the original request without replacing a newer draft", {
304
301
  skip: process.env.JSKIT_ASSISTANT_RUNTIME_BROWSER_INTEGRATION !== "1",
305
302
  timeout: 60_000
306
- }, async () => {
303
+ }, async (t) => {
307
304
  const vite = await startViteFixture({ fixtureRoot });
305
+ t.after(() => stopProcess(vite));
308
306
  const browser = await chromium.launch(createChromiumLaunchOptions());
307
+ t.after(() => browser.close());
309
308
  const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
310
309
  const held = Promise.withResolvers();
311
310
  const requests = [];
@@ -345,7 +344,5 @@ test("pending delivery appears immediately and retries the original request with
345
344
  assert.deepEqual(errors, []);
346
345
  } finally {
347
346
  held.resolve();
348
- await browser.close();
349
- await stopProcess(vite);
350
347
  }
351
348
  });
@@ -178,7 +178,7 @@ test("registerRoutes forwards the resolved host surface through every direct act
178
178
  params: {
179
179
  conversationId: "conversation_1",
180
180
  surfaceId: "admin",
181
- workspaceSlug: "dogandgroom"
181
+ workspaceSlug: "acme"
182
182
  },
183
183
  query: {
184
184
  limit: 20
@@ -233,7 +233,7 @@ test("registerRoutes builds workspace action input from explicit config and scop
233
233
  },
234
234
  input: {
235
235
  params: {
236
- workspaceSlug: "dogandgroom",
236
+ workspaceSlug: "acme",
237
237
  surfaceId: "admin"
238
238
  },
239
239
  query: {
@@ -259,7 +259,7 @@ test("registerRoutes builds workspace action input from explicit config and scop
259
259
  assert.equal(reply.statusCode, 200);
260
260
  assert.equal(capturedAction?.surface, "admin");
261
261
  assert.equal(capturedAction?.input?.targetSurfaceId, "admin");
262
- assert.equal(capturedAction?.input?.workspaceSlug, "dogandgroom");
262
+ assert.equal(capturedAction?.input?.workspaceSlug, "acme");
263
263
  });
264
264
 
265
265
  test("registerRoutes returns clear AppError payload for pre-stream assistant failures", async () => {
@@ -306,7 +306,7 @@ test("registerRoutes returns clear AppError payload for pre-stream assistant fai
306
306
  },
307
307
  input: {
308
308
  params: {
309
- workspaceSlug: "dogandgroom",
309
+ workspaceSlug: "acme",
310
310
  surfaceId: "admin"
311
311
  },
312
312
  body: {
@@ -330,7 +330,7 @@ test("registerRoutes returns clear AppError payload for pre-stream assistant fai
330
330
  });
331
331
  assert.deepEqual(capturedInput, {
332
332
  targetSurfaceId: "admin",
333
- workspaceSlug: "dogandgroom",
333
+ workspaceSlug: "acme",
334
334
  messageId: "msg_1",
335
335
  input: "hello"
336
336
  });
@@ -349,7 +349,7 @@ test("registerRoutes returns clear AppError payload for pre-stream assistant fai
349
349
  await route.handler({
350
350
  raw: { on() {}, off() {} },
351
351
  headers: { "x-jskit-surface": "admin" },
352
- input: { params: { workspaceSlug: "dogandgroom", surfaceId: "admin" }, body: { messageId: "msg_2", input: "hello" } },
352
+ input: { params: { workspaceSlug: "acme", surfaceId: "admin" }, body: { messageId: "msg_2", input: "hello" } },
353
353
  executeAction: async () => { throw failure; }
354
354
  }, reply);
355
355
  assert.equal(reply.statusCode, status);
@@ -389,7 +389,7 @@ test("chat service uses explicit app config when conversations are listed", asyn
389
389
  {
390
390
  input: {
391
391
  targetSurfaceId: "admin",
392
- workspaceSlug: "dogandgroom"
392
+ workspaceSlug: "acme"
393
393
  },
394
394
  context: {
395
395
  actor: {
@@ -398,14 +398,14 @@ test("chat service uses explicit app config when conversations are listed", asyn
398
398
  },
399
399
  workspace: {
400
400
  id: 7,
401
- slug: "dogandgroom"
401
+ slug: "acme"
402
402
  }
403
403
  }
404
404
  }
405
405
  );
406
406
 
407
407
  assert.equal(response.assistantSurface.targetSurfaceId, "admin");
408
- assert.equal(response.workspace.slug, "dogandgroom");
408
+ assert.equal(response.workspace.slug, "acme");
409
409
  });
410
410
 
411
411
  test("chat service rejects workspace-scoped assistant surfaces when workspace support is unavailable", async () => {
@@ -435,7 +435,7 @@ test("chat service rejects workspace-scoped assistant surfaces when workspace su
435
435
  {
436
436
  input: {
437
437
  targetSurfaceId: "admin",
438
- workspaceSlug: "dogandgroom"
438
+ workspaceSlug: "acme"
439
439
  },
440
440
  context: {
441
441
  actor: {
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
5
5
  import path from "node:path";
6
6
  import knex from "knex";
7
7
  import { AppError } from "@jskit-ai/kernel/server/runtime";
8
+ import { silentLogger } from "@jskit-ai/kernel/server/runtime/logger";
8
9
  import { createConversationTranscript } from "@jskit-ai/assistant-core/server/conversation";
9
10
  import { verifyConversationStorageContract } from "@jskit-ai/assistant-core/testing/conversation-storage";
10
11
  import transcriptsMigration from "../migrations/assistant_transcripts_initial.cjs";
@@ -17,8 +18,12 @@ import { createSqlConversationStorage } from "../src/server/sqlConversationStora
17
18
  import { createAssistantConversationRuntime } from "../src/server/createAssistantRuntime.js";
18
19
  import { createTranscriptService } from "../src/server/services/transcriptService.js";
19
20
  import { createChatService } from "../src/server/services/chatService.js";
21
+ import { NO_PERSON_CONTEXT, createPersonContextPrompt } from "../src/server/support/personContext.js";
20
22
  import { createScriptedModel } from "./support/scriptedModel.js";
21
23
 
24
+ // The chat service of an application that supplies no facts about the person.
25
+ const NO_FACTS = createPersonContextPrompt({ personContext: NO_PERSON_CONTEXT, logger: silentLogger });
26
+
22
27
  async function fixture(t) {
23
28
  const directory = await mkdtemp(path.join(tmpdir(), "assistant-sql-runtime-"));
24
29
  const clients = [];
@@ -178,6 +183,7 @@ test("the SQL facade uses common inference and durable tool rows, and the model'
178
183
  const dependencies = { conversationRuntime: runtime, aiClientFactory, serviceToolCatalog: catalog,
179
184
  transcriptService: createTranscriptService(f), turnRequests: createTurnRequestsRepository(f.db),
180
185
  assistantConfigService: { resolveSystemPrompt: async () => "Retained SQL instructions" },
186
+ personContextPrompt: NO_FACTS,
181
187
  appConfig: { surfaceDefinitions: { assistant: { enabled: true, requiresWorkspace: false } },
182
188
  assistantSurfaces: { assistant: { settingsSurfaceId: "assistant", configScope: "global" } } } };
183
189
  const service = createChatService(dependencies);
@@ -234,6 +240,7 @@ test("the SQL facade replays historical inspection failures with their exact pub
234
240
  const dependencies = { conversationRuntime: runtime, aiClientFactory, serviceToolCatalog: catalog,
235
241
  transcriptService: createTranscriptService(f), turnRequests,
236
242
  assistantConfigService: { resolveSystemPrompt: async () => "Read the supplied context" },
243
+ personContextPrompt: NO_FACTS,
237
244
  appConfig: { surfaceDefinitions: { assistant: { enabled: true, requiresWorkspace: false } },
238
245
  assistantSurfaces: { assistant: { settingsSurfaceId: "assistant", configScope: "global" } } } };
239
246
  const input = { targetSurfaceId: "assistant", conversationId: conversation.id,
@@ -344,6 +351,7 @@ for (const shape of ["orphan-output", "orphan-tool-result", "unconfirmed-tool"])
344
351
  const dependencies = { conversationRuntime: runtime, aiClientFactory, serviceToolCatalog: catalog,
345
352
  transcriptService: createTranscriptService(f), turnRequests,
346
353
  assistantConfigService: { resolveSystemPrompt: async () => "Read only the admitted request" },
354
+ personContextPrompt: NO_FACTS,
347
355
  workspaceScopeSupport: { resolveWorkspace: context => context.requestMeta.resolvedWorkspaceContext.workspace },
348
356
  appConfig: { surfaceDefinitions: {
349
357
  assistant: { enabled: true, requiresWorkspace: true }, other: { enabled: true, requiresWorkspace: true }
@@ -1,8 +1,8 @@
1
1
  /** Repository fixture for the original chat-service unit cases. The production
2
2
  * SQL adapter and common runtime run unchanged over these transactional rows.
3
3
  */
4
- export function createMemoryAssistantRows({ id = "conversation_1", surfaceId = "assistant", messages = [] } = {}) {
5
- let state = { conversation: { id, workspaceId: null, createdByUserId: "1", surfaceId,
4
+ export function createMemoryAssistantRows({ id = "conversation_1", surfaceId = "assistant", workspaceId = null, messages = [] } = {}) {
5
+ let state = { conversation: { id, workspaceId, createdByUserId: "1", surfaceId,
6
6
  title: "New conversation", status: "active", messageCount: 0, metadata: {} }, transcript: { revision: "", summary: null }, messages: [] };
7
7
  let pending = Promise.resolve();
8
8
  const read = options => options?.trx || state;
@@ -6,7 +6,9 @@ import path from "node:path";
6
6
  import knex from "knex";
7
7
  import migration from "../migrations/assistant_turn_requests.cjs";
8
8
  import { createRepository } from "../src/server/repositories/turnRequestsRepository.js";
9
+ import { silentLogger } from "@jskit-ai/kernel/server/runtime/logger";
9
10
  import { createChatService } from "../src/server/services/chatService.js";
11
+ import { NO_PERSON_CONTEXT, createPersonContextPrompt } from "../src/server/support/personContext.js";
10
12
  import { createAssistantConversationRuntime } from "../src/server/createAssistantRuntime.js";
11
13
  import { createMemoryAssistantRows } from "./support/memoryAssistantRows.js";
12
14
  import { createScriptedModel } from "./support/scriptedModel.js";
@@ -59,6 +61,7 @@ function service(repository, { provider, transcript = [] } = {}) {
59
61
  serviceToolCatalog: { limits: { maxToolArgumentBytes: 64 * 1024 }, resolveToolSet: () => ({ tools: [] }),
60
62
  toOpenAiToolSchema: tool => ({ type: "function", function: tool }) },
61
63
  assistantConfigService: { resolveSystemPrompt: async () => "Answer." },
64
+ personContextPrompt: createPersonContextPrompt({ personContext: NO_PERSON_CONTEXT, logger: silentLogger }),
62
65
  appConfig: {
63
66
  surfaceDefinitions: { assistant: { id: "assistant", enabled: true, requiresWorkspace: false, accessPolicyId: "public" } },
64
67
  assistantSurfaces: { assistant: { settingsSurfaceId: "assistant", configScope: "global" } }