baa-conductor


baa-conductor / packages / artifact-db / src
codex@macbookpro  ·  2026-04-01

index.test.js

  1import assert from "node:assert/strict";
  2import {
  3  existsSync,
  4  mkdirSync,
  5  mkdtempSync,
  6  readFileSync,
  7  rmSync
  8} from "node:fs";
  9import { tmpdir } from "node:os";
 10import { join } from "node:path";
 11import { DatabaseSync } from "node:sqlite";
 12import test from "node:test";
 13
 14import {
 15  ARTIFACT_DB_FILENAME,
 16  ARTIFACTS_DIRNAME,
 17  ArtifactStore
 18} from "../dist/index.js";
 19
 20const STORE_SOURCE = readFileSync(new URL("./store.ts", import.meta.url), "utf8");
 21
 22function extractStoreSql(name) {
 23  const marker = `const ${name} = \``;
 24  const start = STORE_SOURCE.indexOf(marker);
 25  assert.notEqual(start, -1, `Could not find ${name} in store.ts`);
 26
 27  const sqlStart = start + marker.length;
 28  const sqlEnd = STORE_SOURCE.indexOf("`;", sqlStart);
 29  assert.notEqual(sqlEnd, -1, `Could not parse ${name} from store.ts`);
 30
 31  return STORE_SOURCE.slice(sqlStart, sqlEnd);
 32}
 33
 34function getStoreDb(store) {
 35  const db = Reflect.get(store, "db");
 36  assert.ok(db instanceof DatabaseSync);
 37  return db;
 38}
 39
 40test("ArtifactStore writes message, execution, session, and index artifacts synchronously", async () => {
 41  const rootDir = mkdtempSync(join(tmpdir(), "artifact-db-test-"));
 42  const stateDir = join(rootDir, "state");
 43  const databasePath = join(stateDir, ARTIFACT_DB_FILENAME);
 44  const artifactDir = join(stateDir, ARTIFACTS_DIRNAME);
 45  const store = new ArtifactStore({
 46    artifactDir,
 47    databasePath,
 48    publicBaseUrl: "https://conductor.makefile.so"
 49  });
 50
 51  try {
 52    const message = await store.insertMessage({
 53      conversationId: "conv_123",
 54      id: "msg_123",
 55      observedAt: Date.UTC(2026, 2, 28, 8, 9, 0),
 56      organizationId: "org_123",
 57      pageTitle: "Claude",
 58      pageUrl: "https://claude.ai/chat/conv_123",
 59      platform: "claude",
 60      rawText: "完整消息内容\n第二行",
 61      role: "assistant"
 62    });
 63    const execution = await store.insertExecution({
 64      executedAt: Date.UTC(2026, 2, 28, 8, 10, 0),
 65      instructionId: "inst_123",
 66      messageId: message.id,
 67      params: {
 68        command: "pnpm test"
 69      },
 70      paramsKind: "body",
 71      resultData: {
 72        exit_code: 0,
 73        stdout: "all good"
 74      },
 75      resultOk: true,
 76      target: "conductor",
 77      tool: "exec"
 78    });
 79    const [session] = await store.getLatestSessions(1);
 80
 81    assert.ok(session);
 82    assert.equal(session.conversationId, "conv_123");
 83    assert.equal(session.executionCount, 1);
 84    assert.equal(session.messageCount, 1);
 85    assert.equal(session.platform, "claude");
 86
 87    assert.equal(existsSync(databasePath), true);
 88    assert.equal(existsSync(join(artifactDir, "msg", "msg_123.txt")), true);
 89    assert.equal(existsSync(join(artifactDir, "msg", "msg_123.json")), true);
 90    assert.equal(existsSync(join(artifactDir, "exec", "inst_123.txt")), true);
 91    assert.equal(existsSync(join(artifactDir, "exec", "inst_123.json")), true);
 92    assert.equal(existsSync(join(artifactDir, session.staticPath)), true);
 93    assert.equal(existsSync(join(artifactDir, session.staticPath.replace(/\.txt$/u, ".json"))), true);
 94    assert.equal(existsSync(join(artifactDir, "session", "latest.txt")), true);
 95    assert.equal(existsSync(join(artifactDir, "session", "latest.json")), true);
 96
 97    assert.match(readFileSync(join(artifactDir, "msg", "msg_123.txt"), "utf8"), /kind: message/u);
 98    assert.match(readFileSync(join(artifactDir, "msg", "msg_123.txt"), "utf8"), /完整消息内容/u);
 99    assert.match(
100      readFileSync(join(artifactDir, "msg", "msg_123.json"), "utf8"),
101      /https:\/\/conductor\.makefile\.so\/artifact\/msg\/msg_123\.txt/u
102    );
103    assert.match(readFileSync(join(artifactDir, "exec", "inst_123.txt"), "utf8"), /params:/u);
104    assert.match(readFileSync(join(artifactDir, "exec", "inst_123.txt"), "utf8"), /pnpm test/u);
105    assert.match(readFileSync(join(artifactDir, session.staticPath), "utf8"), /### message msg_123/u);
106    assert.match(readFileSync(join(artifactDir, session.staticPath), "utf8"), /### execution inst_123/u);
107    assert.match(readFileSync(join(artifactDir, "session", "latest.txt"), "utf8"), new RegExp(session.id, "u"));
108    assert.match(
109      readFileSync(join(artifactDir, "session", "latest.json"), "utf8"),
110      new RegExp(`https://conductor\\.makefile\\.so/artifact/session/${session.id}\\.txt`, "u")
111    );
112
113    assert.deepEqual(await store.getMessage(message.id), message);
114    assert.deepEqual(await store.getExecution(execution.instructionId), execution);
115    assert.deepEqual(await store.listMessages({ conversationId: "conv_123" }), [message]);
116    assert.deepEqual(await store.listExecutions({ messageId: message.id }), [execution]);
117    assert.deepEqual(await store.listSessions({ platform: "claude" }), [session]);
118  } finally {
119    store.close();
120    rmSync(rootDir, {
121      force: true,
122      recursive: true
123    });
124  }
125});
126
127test("ArtifactStore scans messages by cursor and settle cutoff without rescanning older rows", async () => {
128  const rootDir = mkdtempSync(join(tmpdir(), "artifact-db-message-scan-test-"));
129  const stateDir = join(rootDir, "state");
130  const databasePath = join(stateDir, ARTIFACT_DB_FILENAME);
131  const artifactDir = join(stateDir, ARTIFACTS_DIRNAME);
132  const store = new ArtifactStore({
133    artifactDir,
134    databasePath
135  });
136
137  try {
138    const first = await store.insertMessage({
139      id: "msg_scan_001",
140      observedAt: Date.UTC(2026, 2, 28, 9, 0, 0),
141      platform: "claude",
142      rawText: "first assistant message",
143      role: "assistant"
144    });
145    await store.insertMessage({
146      id: "msg_scan_user",
147      observedAt: Date.UTC(2026, 2, 28, 9, 0, 30),
148      platform: "claude",
149      rawText: "interleaved user message",
150      role: "user"
151    });
152    const second = await store.insertMessage({
153      id: "msg_scan_002",
154      observedAt: Date.UTC(2026, 2, 28, 9, 1, 0),
155      platform: "claude",
156      rawText: "second assistant message",
157      role: "assistant"
158    });
159    const third = await store.insertMessage({
160      id: "msg_scan_003",
161      observedAt: Date.UTC(2026, 2, 28, 9, 2, 0),
162      platform: "claude",
163      rawText: "third assistant message",
164      role: "assistant"
165    });
166
167    assert.deepEqual(
168      await store.scanMessages({
169        limit: 10,
170        observedAtLte: second.observedAt,
171        role: "assistant"
172      }),
173      [first, second]
174    );
175    assert.deepEqual(
176      await store.scanMessages({
177        after: {
178          id: first.id,
179          observedAt: first.observedAt
180        },
181        limit: 10,
182        observedAtLte: third.observedAt,
183        role: "assistant"
184      }),
185      [second, third]
186    );
187    assert.deepEqual(
188      await store.scanMessages({
189        after: {
190          id: second.id,
191          observedAt: second.observedAt
192        },
193        limit: 10,
194        observedAtLte: second.observedAt,
195        role: "assistant"
196      }),
197      []
198    );
199  } finally {
200    store.close();
201    rmSync(rootDir, {
202      force: true,
203      recursive: true
204    });
205  }
206});
207
208test("ArtifactStore persists renewal storage records and enqueues sync payloads", async () => {
209  const rootDir = mkdtempSync(join(tmpdir(), "artifact-db-renewal-test-"));
210  const stateDir = join(rootDir, "state");
211  const databasePath = join(stateDir, ARTIFACT_DB_FILENAME);
212  const artifactDir = join(stateDir, ARTIFACTS_DIRNAME);
213  const store = new ArtifactStore({
214    artifactDir,
215    databasePath
216  });
217  const syncRecords = [];
218  store.setSyncQueue({
219    enqueueSyncRecord(input) {
220      syncRecords.push(input);
221    }
222  });
223
224  try {
225    const message = await store.insertMessage({
226      conversationId: "conv_renew_123",
227      id: "msg_renew_123",
228      observedAt: Date.UTC(2026, 2, 28, 9, 0, 0),
229      platform: "claude",
230      rawText: "续命候选消息",
231      role: "assistant"
232    });
233
234    const localConversation = await store.upsertLocalConversation({
235      automationStatus: "auto",
236      cooldownUntil: Date.UTC(2026, 2, 28, 9, 5, 0),
237      lastMessageAt: message.observedAt,
238      lastMessageId: message.id,
239      localConversationId: "lc_123",
240      platform: "claude",
241      summary: "Initial summary",
242      title: "Renewal thread"
243    });
244    const pausedConversation = await store.upsertLocalConversation({
245      automationStatus: "paused",
246      localConversationId: localConversation.localConversationId,
247      pausedAt: Date.UTC(2026, 2, 28, 9, 6, 0),
248      platform: "claude"
249    });
250
251    const conversationLink = await store.upsertConversationLink({
252      clientId: "firefox-claude",
253      linkId: "link_123",
254      localConversationId: localConversation.localConversationId,
255      observedAt: Date.UTC(2026, 2, 28, 9, 1, 0),
256      pageTitle: "Claude",
257      pageUrl: "https://claude.ai/chat/conv_renew_123",
258      platform: "claude",
259      remoteConversationId: "conv_renew_123",
260      routeParams: { conversationId: "conv_renew_123" },
261      routePath: "/chat/conv_renew_123",
262      routePattern: "/chat/:conversationId",
263      targetId: "firefox-claude",
264      targetKind: "browser.proxy_delivery",
265      targetPayload: { clientId: "firefox-claude" }
266    });
267    const updatedConversationLink = await store.upsertConversationLink({
268      linkId: "link_ignore_new_id",
269      localConversationId: localConversation.localConversationId,
270      observedAt: Date.UTC(2026, 2, 28, 9, 2, 0),
271      pageTitle: "Claude Updated",
272      platform: "claude",
273      remoteConversationId: "conv_renew_123"
274    });
275
276    const dueJob = await store.insertRenewalJob({
277      jobId: "job_123",
278      localConversationId: localConversation.localConversationId,
279      logPath: "logs/renewal/2026-03-28.jsonl",
280      maxAttempts: 5,
281      messageId: message.id,
282      nextAttemptAt: Date.UTC(2026, 2, 28, 9, 3, 0),
283      payload: "[renew] keepalive",
284      payloadKind: "text",
285      targetSnapshot: {
286        clientId: "firefox-claude",
287        pageUrl: "https://claude.ai/chat/conv_renew_123"
288      }
289    });
290    const runningJob = await store.updateRenewalJob({
291      attemptCount: 1,
292      jobId: dueJob.jobId,
293      lastAttemptAt: Date.UTC(2026, 2, 28, 9, 4, 0),
294      nextAttemptAt: null,
295      startedAt: Date.UTC(2026, 2, 28, 9, 4, 0),
296      status: "running"
297    });
298
299    assert.deepEqual(await store.getLocalConversation(localConversation.localConversationId), pausedConversation);
300    assert.deepEqual(await store.getConversationLink(conversationLink.linkId), updatedConversationLink);
301    assert.deepEqual(
302      await store.findConversationLinkByRemoteConversation("claude", "conv_renew_123"),
303      updatedConversationLink
304    );
305    assert.deepEqual(await store.getRenewalJob(dueJob.jobId), runningJob);
306
307    assert.equal(localConversation.automationStatus, "auto");
308    assert.equal(pausedConversation.automationStatus, "paused");
309    assert.equal(pausedConversation.summary, "Initial summary");
310    assert.equal(updatedConversationLink.linkId, "link_123");
311    assert.equal(updatedConversationLink.clientId, "firefox-claude");
312    assert.equal(updatedConversationLink.pageTitle, "Claude Updated");
313    assert.equal(runningJob.status, "running");
314    assert.equal(runningJob.attemptCount, 1);
315    assert.equal(runningJob.nextAttemptAt, null);
316    assert.match(runningJob.targetSnapshot, /firefox-claude/u);
317
318    assert.deepEqual(await store.listLocalConversations({ automationStatus: "paused" }), [pausedConversation]);
319    assert.deepEqual(
320      await store.listConversationLinks({ localConversationId: localConversation.localConversationId }),
321      [updatedConversationLink]
322    );
323    assert.deepEqual(await store.listRenewalJobs({ status: "running" }), [runningJob]);
324    assert.deepEqual(
325      syncRecords.map((record) => [record.tableName, record.operation, record.recordId]),
326      [
327        ["messages", "insert", "msg_renew_123"],
328        ["local_conversations", "insert", "lc_123"],
329        ["local_conversations", "insert", "lc_123"],
330        ["conversation_links", "insert", "link_123"],
331        ["conversation_links", "insert", "link_123"],
332        ["renewal_jobs", "insert", "job_123"],
333        ["renewal_jobs", "update", "job_123"]
334      ]
335    );
336  } finally {
337    store.close();
338    rmSync(rootDir, {
339      force: true,
340      recursive: true
341    });
342  }
343});
344
345test("ArtifactStore persists browser request policy state and enqueues sync payloads", async () => {
346  const rootDir = mkdtempSync(join(tmpdir(), "artifact-db-browser-policy-state-test-"));
347  const stateDir = join(rootDir, "state");
348  const databasePath = join(stateDir, ARTIFACT_DB_FILENAME);
349  const artifactDir = join(stateDir, ARTIFACTS_DIRNAME);
350  const store = new ArtifactStore({
351    artifactDir,
352    databasePath
353  });
354  const syncRecords = [];
355  store.setSyncQueue({
356    enqueueSyncRecord(input) {
357      syncRecords.push(input);
358    }
359  });
360
361  try {
362    const persisted = await store.upsertBrowserRequestPolicyState({
363      stateKey: "global",
364      updatedAt: Date.UTC(2026, 2, 28, 9, 30, 0),
365      valueJson: JSON.stringify({
366        platforms: [
367          {
368            dispatches: [Date.UTC(2026, 2, 28, 9, 29, 0)],
369            lastDispatchedAt: Date.UTC(2026, 2, 28, 9, 29, 0),
370            platform: "claude"
371          }
372        ],
373        targets: [],
374        version: 1
375      })
376    });
377
378    assert.deepEqual(await store.getBrowserRequestPolicyState("global"), persisted);
379    assert.deepEqual(
380      syncRecords.map((record) => [record.tableName, record.operation, record.recordId]),
381      [["browser_request_policy_state", "update", "global"]]
382    );
383  } finally {
384    store.close();
385    rmSync(rootDir, {
386      force: true,
387      recursive: true
388    });
389  }
390});
391
392test("ArtifactStore runtime recovery clears stale execution locks and requeues running renewal jobs", async () => {
393  const rootDir = mkdtempSync(join(tmpdir(), "artifact-db-runtime-recovery-test-"));
394  const stateDir = join(rootDir, "state");
395  const databasePath = join(stateDir, ARTIFACT_DB_FILENAME);
396  const artifactDir = join(stateDir, ARTIFACTS_DIRNAME);
397  const store = new ArtifactStore({
398    artifactDir,
399    databasePath
400  });
401  const syncRecords = [];
402  store.setSyncQueue({
403    enqueueSyncRecord(input) {
404      syncRecords.push(input);
405    }
406  });
407
408  try {
409    const message = await store.insertMessage({
410      conversationId: "conv_recovery",
411      id: "msg_recovery",
412      observedAt: Date.UTC(2026, 2, 28, 10, 0, 0),
413      platform: "claude",
414      rawText: "recovery message",
415      role: "assistant"
416    });
417    await store.upsertLocalConversation({
418      automationStatus: "auto",
419      executionState: "renewal_running",
420      localConversationId: "lc_recovery_busy",
421      platform: "claude",
422      updatedAt: Date.UTC(2026, 2, 28, 10, 1, 0)
423    });
424    await store.upsertLocalConversation({
425      automationStatus: "auto",
426      executionState: "instruction_running",
427      localConversationId: "lc_recovery_instruction",
428      platform: "claude",
429      updatedAt: Date.UTC(2026, 2, 28, 10, 1, 30)
430    });
431    await store.insertRenewalJob({
432      attemptCount: 1,
433      createdAt: Date.UTC(2026, 2, 28, 10, 2, 0),
434      jobId: "job_recovery_running",
435      localConversationId: "lc_recovery_busy",
436      messageId: message.id,
437      nextAttemptAt: null,
438      payload: "[renew]",
439      startedAt: Date.UTC(2026, 2, 28, 10, 2, 0),
440      status: "running",
441      updatedAt: Date.UTC(2026, 2, 28, 10, 2, 0)
442    });
443
444    const recovered = await store.recoverAutomationRuntimeState({
445      now: Date.UTC(2026, 2, 28, 10, 3, 0),
446      renewalRecoveryDelayMs: 45_000
447    });
448
449    const recoveredBusyConversation = await store.getLocalConversation("lc_recovery_busy");
450    const recoveredInstructionConversation = await store.getLocalConversation("lc_recovery_instruction");
451    const recoveredJob = await store.getRenewalJob("job_recovery_running");
452
453    assert.deepEqual(recovered, {
454      recoveredExecutionStateCount: 2,
455      recoveryNextAttemptAt: Date.UTC(2026, 2, 28, 10, 3, 45),
456      requeuedRenewalJobCount: 1
457    });
458    assert.equal(recoveredBusyConversation?.executionState, "idle");
459    assert.equal(recoveredInstructionConversation?.executionState, "idle");
460    assert.equal(recoveredJob?.status, "pending");
461    assert.equal(recoveredJob?.startedAt, null);
462    assert.equal(recoveredJob?.finishedAt, null);
463    assert.equal(recoveredJob?.nextAttemptAt, Date.UTC(2026, 2, 28, 10, 3, 45));
464    assert.deepEqual(
465      syncRecords
466        .filter((record) => record.operation === "update")
467        .map((record) => [record.tableName, record.recordId])
468        .sort(),
469      [
470        ["local_conversations", "lc_recovery_busy"],
471        ["local_conversations", "lc_recovery_instruction"],
472        ["renewal_jobs", "job_recovery_running"]
473      ]
474    );
475  } finally {
476    store.close();
477    rmSync(rootDir, {
478      force: true,
479      recursive: true
480    });
481  }
482});
483
484test("ArtifactStore UPSERT SQL preserves created_at for renewal storage rows on conflict", async () => {
485  const rootDir = mkdtempSync(join(tmpdir(), "artifact-db-created-at-upsert-test-"));
486  const stateDir = join(rootDir, "state");
487  const databasePath = join(stateDir, ARTIFACT_DB_FILENAME);
488  const artifactDir = join(stateDir, ARTIFACTS_DIRNAME);
489  const store = new ArtifactStore({
490    artifactDir,
491    databasePath
492  });
493
494  try {
495    const db = getStoreDb(store);
496    const upsertLocalConversationSql = extractStoreSql("UPSERT_LOCAL_CONVERSATION_SQL");
497    const upsertConversationLinkSql = extractStoreSql("UPSERT_CONVERSATION_LINK_SQL");
498    const upsertRenewalJobSql = extractStoreSql("UPSERT_RENEWAL_JOB_SQL");
499
500    await store.upsertLocalConversation({
501      automationStatus: "manual",
502      createdAt: 100,
503      localConversationId: "lc_created_at",
504      platform: "claude",
505      updatedAt: 110
506    });
507    db.prepare(upsertLocalConversationSql).run(
508      "lc_created_at",
509      "claude",
510      "auto",
511      "auto",
512      null,
513      null,
514      "idle",
515      0,
516      0,
517      0,
518      null,
519      null,
520      "Updated title",
521      "Updated summary",
522      null,
523      null,
524      null,
525      null,
526      999,
527      220
528    );
529
530    const storedConversation = await store.getLocalConversation("lc_created_at");
531    assert.ok(storedConversation);
532    assert.equal(storedConversation.createdAt, 100);
533    assert.equal(storedConversation.updatedAt, 220);
534    assert.equal(storedConversation.automationStatus, "auto");
535    assert.equal(storedConversation.title, "Updated title");
536
537    await store.upsertConversationLink({
538      createdAt: 200,
539      linkId: "link_created_at",
540      localConversationId: "lc_created_at",
541      observedAt: 300,
542      pageTitle: "Before",
543      platform: "claude",
544      remoteConversationId: "conv_created_at",
545      updatedAt: 310
546    });
547    db.prepare(upsertConversationLinkSql).run(
548      "link_created_at",
549      "lc_created_at",
550      "claude",
551      "conv_created_at",
552      "firefox-claude",
553      "https://claude.ai/chat/conv_created_at",
554      "After",
555      "/chat/conv_created_at",
556      "/chat/:conversationId",
557      "{\"conversationId\":\"conv_created_at\"}",
558      "browser.proxy_delivery",
559      "tab:1",
560      "{\"tabId\":1}",
561      0,
562      400,
563      999,
564      410
565    );
566
567    const storedLink = await store.getConversationLink("link_created_at");
568    assert.ok(storedLink);
569    assert.equal(storedLink.createdAt, 200);
570    assert.equal(storedLink.updatedAt, 410);
571    assert.equal(storedLink.pageTitle, "After");
572    assert.equal(storedLink.isActive, false);
573
574    const message = await store.insertMessage({
575      conversationId: "conv_created_at",
576      createdAt: 500,
577      id: "msg_created_at",
578      observedAt: 500,
579      platform: "claude",
580      rawText: "续命消息",
581      role: "assistant"
582    });
583    await store.insertRenewalJob({
584      createdAt: 600,
585      jobId: "job_created_at",
586      localConversationId: "lc_created_at",
587      messageId: message.id,
588      payload: "[renew] ping",
589      targetSnapshot: {
590        clientId: "firefox-claude"
591      },
592      updatedAt: 610
593    });
594    db.prepare(upsertRenewalJobSql).run(
595      "job_created_at",
596      "lc_created_at",
597      message.id,
598      "running",
599      "[renew] pong",
600      "text",
601      "{\"clientId\":\"firefox-claude\",\"targetId\":\"tab:1\"}",
602      1,
603      3,
604      null,
605      700,
606      "temporary failure",
607      "logs/renewal/created-at.jsonl",
608      700,
609      null,
610      999,
611      710
612    );
613
614    const storedJob = await store.getRenewalJob("job_created_at");
615    assert.ok(storedJob);
616    assert.equal(storedJob.createdAt, 600);
617    assert.equal(storedJob.updatedAt, 710);
618    assert.equal(storedJob.status, "running");
619    assert.equal(storedJob.payload, "[renew] pong");
620    assert.match(storedJob.targetSnapshot, /tab:1/u);
621  } finally {
622    store.close();
623    rmSync(rootDir, {
624      force: true,
625      recursive: true
626    });
627  }
628});
629
630test("ArtifactStore findConversationLinkByRemoteConversation only returns active links", async () => {
631  const rootDir = mkdtempSync(join(tmpdir(), "artifact-db-renewal-active-link-test-"));
632  const stateDir = join(rootDir, "state");
633  const databasePath = join(stateDir, ARTIFACT_DB_FILENAME);
634  const artifactDir = join(stateDir, ARTIFACTS_DIRNAME);
635  const store = new ArtifactStore({
636    artifactDir,
637    databasePath
638  });
639
640  try {
641    await store.upsertLocalConversation({
642      localConversationId: "lc_active_link_123",
643      platform: "chatgpt"
644    });
645    const activeLink = await store.upsertConversationLink({
646      linkId: "link_active_link_123",
647      localConversationId: "lc_active_link_123",
648      observedAt: Date.UTC(2026, 2, 30, 8, 0, 0),
649      platform: "chatgpt",
650      remoteConversationId: "conv_active_link_123"
651    });
652
653    assert.deepEqual(
654      await store.findConversationLinkByRemoteConversation("chatgpt", "conv_active_link_123"),
655      activeLink
656    );
657
658    const inactiveLink = await store.upsertConversationLink({
659      isActive: false,
660      linkId: activeLink.linkId,
661      localConversationId: activeLink.localConversationId,
662      observedAt: activeLink.observedAt,
663      platform: activeLink.platform,
664      updatedAt: Date.UTC(2026, 2, 30, 8, 1, 0)
665    });
666
667    assert.deepEqual(
668      await store.listConversationLinks({
669        isActive: false,
670        platform: "chatgpt",
671        remoteConversationId: "conv_active_link_123"
672      }),
673      [inactiveLink]
674    );
675    assert.equal(
676      await store.findConversationLinkByRemoteConversation("chatgpt", "conv_active_link_123"),
677      null
678    );
679  } finally {
680    store.close();
681    rmSync(rootDir, {
682      force: true,
683      recursive: true
684    });
685  }
686});
687
688test("ArtifactStore listConversationLinks supports exact renewal identity filters", async () => {
689  const rootDir = mkdtempSync(join(tmpdir(), "artifact-db-link-filter-test-"));
690  const stateDir = join(rootDir, "state");
691  const databasePath = join(stateDir, ARTIFACT_DB_FILENAME);
692  const artifactDir = join(stateDir, ARTIFACTS_DIRNAME);
693  const store = new ArtifactStore({
694    artifactDir,
695    databasePath
696  });
697  const observedAt = Date.UTC(2026, 2, 30, 8, 5, 0);
698
699  try {
700    await store.upsertLocalConversation({
701      localConversationId: "lc_filter_1",
702      platform: "chatgpt"
703    });
704    await store.upsertLocalConversation({
705      localConversationId: "lc_filter_2",
706      platform: "chatgpt"
707    });
708
709    const matchingLink = await store.upsertConversationLink({
710      clientId: "firefox-chatgpt",
711      linkId: "link_filter_match",
712      localConversationId: "lc_filter_1",
713      observedAt,
714      pageTitle: "Target Thread",
715      pageUrl: "https://chatgpt.com/c/conv-filter-match",
716      platform: "chatgpt",
717      remoteConversationId: "conv-filter-match",
718      routePath: "/c/conv-filter-match",
719      routePattern: "/c/:conversationId",
720      targetId: "tab:7",
721      updatedAt: observedAt
722    });
723    await store.upsertConversationLink({
724      clientId: "firefox-chatgpt",
725      linkId: "link_filter_other",
726      localConversationId: "lc_filter_2",
727      observedAt: observedAt + 1_000,
728      pageTitle: "Other Thread",
729      pageUrl: "https://chatgpt.com/c/conv-filter-other",
730      platform: "chatgpt",
731      remoteConversationId: "conv-filter-other",
732      routePath: "/c/conv-filter-other",
733      routePattern: "/c/:conversationId",
734      targetId: "tab:8",
735      updatedAt: observedAt + 1_000
736    });
737
738    assert.deepEqual(
739      await store.listConversationLinks({ pageTitle: "Target Thread", platform: "chatgpt" }),
740      [matchingLink]
741    );
742    assert.deepEqual(
743      await store.listConversationLinks({ pageUrl: "https://chatgpt.com/c/conv-filter-match", platform: "chatgpt" }),
744      [matchingLink]
745    );
746    assert.deepEqual(
747      await store.listConversationLinks({ platform: "chatgpt", routePath: "/c/conv-filter-match" }),
748      [matchingLink]
749    );
750    assert.deepEqual(
751      await store.listConversationLinks({ platform: "chatgpt", targetId: "tab:7" }),
752      [matchingLink]
753    );
754  } finally {
755    store.close();
756    rmSync(rootDir, {
757      force: true,
758      recursive: true
759    });
760  }
761});
762
763test("ArtifactStore reuses the same null-remote conversation link for repeated route upserts", async () => {
764  const rootDir = mkdtempSync(join(tmpdir(), "artifact-db-null-remote-upsert-test-"));
765  const stateDir = join(rootDir, "state");
766  const databasePath = join(stateDir, ARTIFACT_DB_FILENAME);
767  const artifactDir = join(stateDir, ARTIFACTS_DIRNAME);
768  const store = new ArtifactStore({
769    artifactDir,
770    databasePath
771  });
772  const firstObservedAt = Date.UTC(2026, 2, 30, 18, 0, 0);
773
774  try {
775    await store.upsertLocalConversation({
776      localConversationId: "lc_null_remote_1",
777      platform: "gemini"
778    });
779    await store.upsertLocalConversation({
780      localConversationId: "lc_null_remote_2",
781      platform: "gemini"
782    });
783
784    const firstLink = await store.upsertConversationLink({
785      clientId: "firefox-gemini",
786      linkId: "link_null_remote_1",
787      localConversationId: "lc_null_remote_1",
788      observedAt: firstObservedAt,
789      pageTitle: "Gemini",
790      platform: "gemini",
791      routePath: "/app",
792      routePattern: "/app",
793      targetKind: "browser.proxy_delivery"
794    });
795    const secondLink = await store.upsertConversationLink({
796      clientId: "firefox-gemini",
797      linkId: "link_null_remote_2",
798      localConversationId: "lc_null_remote_2",
799      observedAt: firstObservedAt + 60_000,
800      pageTitle: "Gemini Updated",
801      platform: "gemini",
802      routePath: "/app",
803      routePattern: "/app",
804      targetKind: "browser.proxy_delivery"
805    });
806
807    assert.equal(firstLink.linkId, "link_null_remote_1");
808    assert.equal(secondLink.linkId, "link_null_remote_1");
809    assert.equal(secondLink.localConversationId, "lc_null_remote_2");
810    assert.equal(secondLink.pageTitle, "Gemini Updated");
811    assert.equal(
812      (await store.listConversationLinks({
813        localConversationId: "lc_null_remote_1"
814      })).length,
815      0
816    );
817    assert.deepEqual(await store.listConversationLinks({ platform: "gemini" }), [secondLink]);
818  } finally {
819    store.close();
820    rmSync(rootDir, {
821      force: true,
822      recursive: true
823    });
824  }
825});
826
827test("ArtifactStore migrates duplicate null-remote conversation links into one canonical route record", async () => {
828  const rootDir = mkdtempSync(join(tmpdir(), "artifact-db-null-remote-migration-test-"));
829  const stateDir = join(rootDir, "state");
830  mkdirSync(stateDir, {
831    recursive: true
832  });
833  const databasePath = join(stateDir, ARTIFACT_DB_FILENAME);
834  const artifactDir = join(stateDir, ARTIFACTS_DIRNAME);
835  const bootstrapDb = new DatabaseSync(databasePath);
836  let bootstrapClosed = false;
837
838  try {
839    bootstrapDb.exec(`
840      CREATE TABLE IF NOT EXISTS local_conversations (
841        local_conversation_id TEXT PRIMARY KEY,
842        platform              TEXT NOT NULL,
843        automation_status     TEXT NOT NULL DEFAULT 'manual',
844        title                 TEXT,
845        summary               TEXT,
846        last_message_id       TEXT,
847        last_message_at       INTEGER,
848        cooldown_until        INTEGER,
849        paused_at             INTEGER,
850        created_at            INTEGER NOT NULL,
851        updated_at            INTEGER NOT NULL
852      );
853
854      CREATE TABLE IF NOT EXISTS conversation_links (
855        link_id                TEXT PRIMARY KEY,
856        local_conversation_id  TEXT NOT NULL,
857        platform               TEXT NOT NULL,
858        remote_conversation_id TEXT,
859        client_id              TEXT,
860        page_url               TEXT,
861        page_title             TEXT,
862        route_path             TEXT,
863        route_pattern          TEXT,
864        route_params           TEXT,
865        target_kind            TEXT,
866        target_id              TEXT,
867        target_payload         TEXT,
868        is_active              INTEGER NOT NULL DEFAULT 1,
869        observed_at            INTEGER NOT NULL,
870        created_at             INTEGER NOT NULL,
871        updated_at             INTEGER NOT NULL
872      );
873
874      CREATE UNIQUE INDEX IF NOT EXISTS idx_conversation_links_remote
875        ON conversation_links(platform, remote_conversation_id);
876    `);
877    bootstrapDb.exec(`
878      INSERT INTO local_conversations (
879        local_conversation_id,
880        platform,
881        automation_status,
882        created_at,
883        updated_at
884      ) VALUES
885        ('lc_migration_null_remote_1', 'gemini', 'manual', 1000, 1000),
886        ('lc_migration_null_remote_2', 'gemini', 'manual', 2000, 2000);
887
888      INSERT INTO conversation_links (
889        link_id,
890        local_conversation_id,
891        platform,
892        remote_conversation_id,
893        client_id,
894        page_url,
895        page_title,
896        route_path,
897        route_pattern,
898        route_params,
899        target_kind,
900        target_id,
901        target_payload,
902        is_active,
903        observed_at,
904        created_at,
905        updated_at
906      ) VALUES
907        (
908          'link_migration_null_remote_older',
909          'lc_migration_null_remote_1',
910          'gemini',
911          NULL,
912          'firefox-gemini',
913          'https://gemini.google.com/app?first=1',
914          'Gemini Older',
915          '/app',
916          '/app',
917          NULL,
918          'browser.proxy_delivery',
919          'tab:1',
920          NULL,
921          1,
922          1000,
923          1000,
924          1000
925        ),
926        (
927          'link_migration_null_remote_newer',
928          'lc_migration_null_remote_2',
929          'gemini',
930          NULL,
931          'firefox-gemini',
932          'https://gemini.google.com/app?second=1',
933          'Gemini Newer',
934          '/app',
935          '/app',
936          NULL,
937          'browser.proxy_delivery',
938          'tab:2',
939          NULL,
940          1,
941          2000,
942          2000,
943          2000
944        );
945    `);
946    bootstrapDb.close();
947    bootstrapClosed = true;
948
949    const store = new ArtifactStore({
950      artifactDir,
951      databasePath
952    });
953
954    try {
955      const links = await store.listConversationLinks({ platform: "gemini" });
956
957      assert.equal(links.length, 1);
958      assert.equal(links[0].linkId, "link_migration_null_remote_newer");
959      assert.equal(links[0].localConversationId, "lc_migration_null_remote_2");
960      assert.equal(links[0].createdAt, 1000);
961      assert.equal(links[0].routePath, "/app");
962      assert.equal(links[0].pageTitle, "Gemini Newer");
963
964      const reused = await store.upsertConversationLink({
965        clientId: "firefox-gemini",
966        linkId: "link_migration_null_remote_reused",
967        localConversationId: "lc_migration_null_remote_2",
968        observedAt: 3000,
969        platform: "gemini",
970        routePath: "/app",
971        routePattern: "/app"
972      });
973
974      assert.equal(reused.linkId, "link_migration_null_remote_newer");
975      assert.equal((await store.listConversationLinks({ platform: "gemini" })).length, 1);
976    } finally {
977      store.close();
978    }
979  } finally {
980    if (bootstrapClosed === false) {
981      bootstrapDb.close();
982    }
983    rmSync(rootDir, {
984      force: true,
985      recursive: true
986    });
987  }
988});