@vellumai/assistant 0.10.6-staging.1 → 0.10.6

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.
Files changed (58) hide show
  1. package/openapi.yaml +43 -6
  2. package/package.json +1 -1
  3. package/src/__tests__/attachment-stored-path-annotation.test.ts +151 -0
  4. package/src/__tests__/attachments-store.test.ts +37 -1
  5. package/src/__tests__/attachments.test.ts +53 -0
  6. package/src/__tests__/channel-reply-delivery.test.ts +214 -5
  7. package/src/__tests__/config-loader-backfill.test.ts +16 -36
  8. package/src/__tests__/image-source-path-reinject.test.ts +155 -14
  9. package/src/__tests__/managed-profile-guard.test.ts +2 -0
  10. package/src/__tests__/mtime-cache.test.ts +265 -80
  11. package/src/__tests__/plugin-config-data-migration.test.ts +2 -4
  12. package/src/__tests__/plugin-effective-enabled-set.test.ts +9 -6
  13. package/src/__tests__/workspace-migration-125-repoint-managed-connections-to-vellum.test.ts +140 -0
  14. package/src/agent/attachments.ts +55 -13
  15. package/src/cli/commands/platform/__tests__/credits.test.ts +111 -0
  16. package/src/cli/commands/platform/index.ts +86 -4
  17. package/src/cli/lib/__tests__/confirm-prompt.test.ts +9 -9
  18. package/src/cli/lib/diff-plugin.ts +7 -3
  19. package/src/cli/lib/inspect-plugin.ts +28 -10
  20. package/src/cli/lib/install-from-github.ts +69 -24
  21. package/src/cli/lib/ipc-params.ts +3 -3
  22. package/src/cli/lib/list-installed-plugins.ts +11 -5
  23. package/src/cli/lib/merge-plugin-tree.ts +15 -9
  24. package/src/cli/lib/plugin-fingerprint.ts +0 -6
  25. package/src/cli/lib/publish-plugin.ts +1 -1
  26. package/src/cli/lib/unknown-command.ts +3 -1
  27. package/src/cli/lib/upgrade-plugin.ts +2 -5
  28. package/src/config/__tests__/sync-gated-profiles.test.ts +2 -2
  29. package/src/config/seed-inference-profiles.ts +5 -4
  30. package/src/daemon/conversation-lifecycle.ts +37 -15
  31. package/src/daemon/conversation-messaging.ts +56 -28
  32. package/src/daemon/conversation.ts +2 -2
  33. package/src/heartbeat/__tests__/heartbeat-service.test.ts +6 -0
  34. package/src/hooks/__tests__/hook-live-reload.test.ts +110 -96
  35. package/src/hooks/hook-loader.ts +57 -144
  36. package/src/hooks/registry.ts +13 -9
  37. package/src/persistence/attachments-store.ts +2 -1
  38. package/src/persistence/conversation-crud.ts +24 -0
  39. package/src/plugin-api/types.ts +4 -1
  40. package/src/plugins/mtime-cache.ts +347 -33
  41. package/src/plugins/pipeline.ts +3 -2
  42. package/src/providers/__tests__/inference.test.ts +42 -31
  43. package/src/providers/inference/__tests__/connections-label.test.ts +9 -28
  44. package/src/providers/inference/auth.ts +1 -2
  45. package/src/providers/inference/backfill.ts +7 -5
  46. package/src/providers/inference/connections.ts +54 -30
  47. package/src/runtime/channel-reply-delivery.ts +83 -24
  48. package/src/runtime/channel-retry-sweep.ts +4 -0
  49. package/src/runtime/{slack-no-response.ts → no-response.ts} +26 -6
  50. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +43 -3
  51. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +61 -24
  52. package/src/runtime/routes/conversation-query-routes.ts +18 -1
  53. package/src/runtime/routes/inbound-stages/background-dispatch.ts +1 -1
  54. package/src/runtime/routes/inference-provider-connection-routes.ts +4 -3
  55. package/src/runtime/routes/platform-routes.ts +82 -1
  56. package/src/runtime/slack-reply-session.ts +1 -1
  57. package/src/workspace/migrations/125-repoint-managed-connections-to-vellum.ts +99 -0
  58. package/src/workspace/migrations/registry.ts +2 -0
@@ -31,16 +31,19 @@ import {
31
31
  attachInlineAttachmentToMessage,
32
32
  attachmentExists,
33
33
  AttachmentUploadError,
34
+ getFilePathForAttachment,
34
35
  linkAttachmentToMessage,
35
36
  validateAttachmentUpload,
36
37
  } from "../persistence/attachments-store.js";
37
38
  import {
38
39
  addMessage,
40
+ extractAttachmentStoredPaths,
39
41
  extractImageSourcePaths,
40
42
  getConversation,
41
43
  provenanceFromTrustContext,
42
44
  setConversationOriginChannelIfUnset,
43
45
  setConversationOriginInterfaceIfUnset,
46
+ updateMessageMetadata,
44
47
  } from "../persistence/conversation-crud.js";
45
48
  import {
46
49
  syncMessageToDisk,
@@ -484,30 +487,18 @@ export async function persistQueuedMessageBody(
484
487
  displayContent,
485
488
  clientMessageId,
486
489
  } = options;
487
- const attachmentInputs = attachments.map((attachment) => ({
488
- id: attachment.id,
489
- filename: attachment.filename,
490
- mimeType: attachment.mimeType,
491
- data: attachment.data,
492
- extractedText: attachment.extractedText,
493
- filePath: attachment.filePath,
494
- }));
495
- const cleanMessage = createUserMessage(content, attachmentInputs);
496
- const llmMessage = enrichMessageWithSourcePaths(
497
- cleanMessage,
498
- attachmentInputs,
499
- );
500
- log.info(
501
- {
502
- requestId,
503
- contentBlockTypes: Array.isArray(llmMessage.content)
504
- ? llmMessage.content.map((b) => b.type)
505
- : typeof llmMessage.content,
506
- attachmentCount: attachments.length,
507
- },
508
- "persistUserMessage: content blocks being sent to model",
490
+ const attachmentInputs: MessageAttachmentInput[] = attachments.map(
491
+ (attachment) => ({
492
+ id: attachment.id,
493
+ filename: attachment.filename,
494
+ mimeType: attachment.mimeType,
495
+ data: attachment.data,
496
+ extractedText: attachment.extractedText,
497
+ filePath: attachment.filePath,
498
+ }),
509
499
  );
510
- ctx.messages.push(llmMessage);
500
+ const cleanMessage = createUserMessage(content, attachmentInputs);
501
+ let pushedToHistory = false;
511
502
 
512
503
  try {
513
504
  const turnCtx =
@@ -579,7 +570,6 @@ export async function persistQueuedMessageBody(
579
570
  );
580
571
 
581
572
  if (persistedUserMessage.deduplicated) {
582
- ctx.messages.pop();
583
573
  return { id: persistedUserMessage.id, deduplicated: true };
584
574
  }
585
575
 
@@ -608,7 +598,11 @@ export async function persistQueuedMessageBody(
608
598
  throw new Error("Failed to persist user message");
609
599
  }
610
600
 
611
- // Index user attachments in the attachments table for later retrieval.
601
+ // Index user attachments in the attachments table for later retrieval,
602
+ // capturing the resolved stored path of each linked attachment. Name
603
+ // collisions in the conversation's attachments/ dir get a -2/-3 suffix,
604
+ // so the stored path — not the original filename — is the only reliable
605
+ // on-disk handle for the file.
612
606
  for (let i = 0; i < attachments.length; i++) {
613
607
  const a = attachments[i];
614
608
  try {
@@ -617,7 +611,13 @@ export async function persistQueuedMessageBody(
617
611
  // re-uploading. This handles the case where data is empty because
618
612
  // the attachment content lives on disk.
619
613
  if (a.id && attachmentExists(a.id)) {
620
- linkAttachmentToMessage(persistedUserMessage.id, a.id, i);
614
+ const scopedAttachmentId = linkAttachmentToMessage(
615
+ persistedUserMessage.id,
616
+ a.id,
617
+ i,
618
+ );
619
+ attachmentInputs[i].storedPath =
620
+ getFilePathForAttachment(scopedAttachmentId) ?? undefined;
621
621
  continue;
622
622
  }
623
623
 
@@ -631,7 +631,7 @@ export async function persistQueuedMessageBody(
631
631
  );
632
632
  continue;
633
633
  }
634
- attachInlineAttachmentToMessage(
634
+ const stored = attachInlineAttachmentToMessage(
635
635
  persistedUserMessage.id,
636
636
  i,
637
637
  a.filename,
@@ -639,6 +639,7 @@ export async function persistQueuedMessageBody(
639
639
  a.data,
640
640
  { sourcePath: a.filePath, normalizeImage: true },
641
641
  );
642
+ attachmentInputs[i].storedPath = stored.filePath;
642
643
  } catch (err) {
643
644
  if (err instanceof AttachmentUploadError) {
644
645
  log.warn(
@@ -654,6 +655,31 @@ export async function persistQueuedMessageBody(
654
655
  }
655
656
  }
656
657
 
658
+ // Persist the resolved paths so history reloads can rebuild the same
659
+ // annotation block the in-memory message carries below.
660
+ const attachmentStoredPaths =
661
+ extractAttachmentStoredPaths(attachmentInputs);
662
+ if (attachmentStoredPaths) {
663
+ updateMessageMetadata(persistedUserMessage.id, { attachmentStoredPaths });
664
+ }
665
+
666
+ const llmMessage = enrichMessageWithSourcePaths(
667
+ cleanMessage,
668
+ attachmentInputs,
669
+ );
670
+ log.info(
671
+ {
672
+ requestId,
673
+ contentBlockTypes: Array.isArray(llmMessage.content)
674
+ ? llmMessage.content.map((b) => b.type)
675
+ : typeof llmMessage.content,
676
+ attachmentCount: attachments.length,
677
+ },
678
+ "persistUserMessage: content blocks being sent to model",
679
+ );
680
+ ctx.messages.push(llmMessage);
681
+ pushedToHistory = true;
682
+
657
683
  // Sync the persisted user message (with attachments) to the disk view
658
684
  const conv = getConversation(ctx.conversationId);
659
685
  if (conv) {
@@ -666,7 +692,9 @@ export async function persistQueuedMessageBody(
666
692
 
667
693
  return { id: persistedUserMessage.id, deduplicated: false };
668
694
  } catch (err) {
669
- ctx.messages.pop();
695
+ if (pushedToHistory) {
696
+ ctx.messages.pop();
697
+ }
670
698
  throw err;
671
699
  }
672
700
  }
@@ -117,7 +117,7 @@ import { isToolResultBlock, undo as undoImpl } from "./conversation-history.js";
117
117
  import {
118
118
  abortConversation,
119
119
  disposeConversation,
120
- reinjectImageSourcePaths,
120
+ reinjectAttachmentPathAnnotations,
121
121
  } from "./conversation-lifecycle.js";
122
122
  import type {
123
123
  EnqueueMessageOptions,
@@ -1044,7 +1044,7 @@ export class Conversation {
1044
1044
  content = [{ type: "text", text: m.content }];
1045
1045
  }
1046
1046
 
1047
- content = reinjectImageSourcePaths(content, role, m.metadata);
1047
+ content = reinjectAttachmentPathAnnotations(content, role, m.metadata);
1048
1048
 
1049
1049
  // Re-inject persisted injection blocks from metadata so it survives
1050
1050
  // conversation reloads (eviction, restart, fork).
@@ -29,8 +29,14 @@ mock.module("../../runtime/assistant-event-hub.js", () => ({
29
29
  // Stub workspace prompt reads so the heartbeat service doesn't try to
30
30
  // read real workspace files. Use a fallback for early module-load calls
31
31
  // (e.g. AuthSessionCache constructor) before beforeEach sets workspaceDir.
32
+ // The mock spreads the real module so exports it doesn't override (imported
33
+ // by transitive dependencies of the service under test) keep their real,
34
+ // preload-temp-dir-scoped implementations instead of vanishing — a missing
35
+ // export is an import-time SyntaxError for the whole test file.
36
+ const realPlatform = await import("../../util/platform.js");
32
37
  const fallbackDir = join(tmpdir(), "vellum-hb-svc-fallback");
33
38
  mock.module("../../util/platform.js", () => ({
39
+ ...realPlatform,
34
40
  getWorkspaceDir: () => workspaceDir ?? fallbackDir,
35
41
  getWorkspacePromptPath: (name: string) =>
36
42
  join(workspaceDir ?? fallbackDir, name),
@@ -1,36 +1,36 @@
1
1
  /**
2
- * End-to-end coverage for hook live-reload through the hook loader: an
3
- * edited hook file takes effect on the next dispatch, without a daemon
4
- * restart.
2
+ * Pins the eviction primitives hook live-reload is built on, driven through
3
+ * the production import machinery (`preImportHooksDir` →
4
+ * `collectUserHookEntries`), one layer below the sentinel-driven reconcile
5
+ * that orchestrates them in production (see `../../plugins/mtime-cache.ts`
6
+ * and its end-to-end suite).
5
7
  *
6
- * The loader keys each hook on its source file's mtime; when the mtime
7
- * moves it evicts the module from the runtime registry (`evictModule` in
8
- * `../../plugins/surface-import.ts`) and re-imports, so the fresh content is
9
- * what dispatches. These tests drive the public collection API against real
10
- * hook files on disk, which also makes them the regression guard for the
11
- * Bun behavior the eviction is built on — `require.cache` deletion
12
- * invalidating dynamic `import()` is intended per
13
- * github.com/oven-sh/bun/discussions/10162 but not spec-guaranteed, and a
14
- * Bun upgrade that breaks it must fail here rather than silently degrade
15
- * hook edits back to restart-required.
8
+ * Two invariants live here:
9
+ *
10
+ * 1. **The Bun primitive.** Deleting a file's `require.cache` entry makes
11
+ * the next dynamic `import()` re-evaluate it from disk. This is
12
+ * Node-compat surface, intended per
13
+ * github.com/oven-sh/bun/discussions/10162 but not spec-guaranteed — a
14
+ * Bun upgrade that breaks it must fail here rather than silently degrade
15
+ * hook edits back to restart-required.
16
+ *
17
+ * 2. **Whole-owner eviction.** Evicting only a hook's own module re-binds
18
+ * the re-imported hook to its stale cached helpers. The reconcile sweeps
19
+ * every path in the plugin for exactly this reason, and this suite pins
20
+ * the failure mode that rule prevents.
16
21
  */
17
22
 
18
- import {
19
- mkdirSync,
20
- mkdtempSync,
21
- rmSync,
22
- unlinkSync,
23
- utimesSync,
24
- writeFileSync,
25
- } from "node:fs";
23
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
26
24
  import { tmpdir } from "node:os";
27
25
  import { join } from "node:path";
28
26
  import { afterAll, beforeEach, describe, expect, test } from "bun:test";
29
27
 
28
+ import { evictModule } from "../../plugins/surface-import.js";
30
29
  import type { HookEntry } from "../../plugins/types.js";
31
- import { getWorkspaceHooksDir } from "../../util/platform.js";
32
30
  import {
33
31
  collectUserHookEntries,
32
+ evictHooksForOwner,
33
+ preImportHooksDir,
34
34
  resetHookCacheForTests,
35
35
  } from "../hook-loader.js";
36
36
 
@@ -46,108 +46,122 @@ beforeEach(() => {
46
46
  resetHookCacheForTests();
47
47
  });
48
48
 
49
- /**
50
- * Each write gets an explicitly bumped, strictly increasing mtime: two
51
- * writes can land inside the same filesystem-timestamp granule, which the
52
- * loader's mtime check would read as "unchanged". Real edits arrive seconds
53
- * apart; the tests must not race that granularity.
54
- */
55
- let mtimeSeq = 1_750_000_000;
56
- function writeHookFile(hooksDir: string, marker: string): string {
57
- const path = join(hooksDir, `${HOOK_NAME}.ts`);
58
- writeFileSync(path, `export default () => ${JSON.stringify(marker)};\n`);
59
- const stamp = new Date(++mtimeSeq * 1000);
60
- utimesSync(path, stamp, stamp);
61
- return path;
62
- }
63
-
64
49
  /** Fresh plugin fixture: `<root>/<name>/hooks/`, unique name per call. */
65
50
  let pluginSeq = 0;
66
51
  function makePlugin(): { dir: string; hooksDir: string; name: string } {
67
52
  const name = `reload-plugin-${++pluginSeq}`;
68
53
  const dir = join(root, name);
69
54
  const hooksDir = join(dir, "hooks");
55
+ mkdirSync(join(dir, "lib"), { recursive: true });
70
56
  mkdirSync(hooksDir, { recursive: true });
71
57
  return { dir, hooksDir, name };
72
58
  }
73
59
 
74
- /** Collect the hook chain for a plugin, exactly as dispatch does. */
75
- async function collect(
76
- pluginDir: string,
77
- pluginName: string,
78
- ): Promise<HookEntry[]> {
79
- return collectUserHookEntries(HOOK_NAME, [[pluginDir, pluginName]]);
60
+ /**
61
+ * Redeploy an owner's hooks the way the reconcile does: drop its cache
62
+ * entries, sweep the given module paths, and re-import from disk.
63
+ */
64
+ async function redeploy(
65
+ plugin: { hooksDir: string; name: string },
66
+ modulePaths: string[],
67
+ ): Promise<void> {
68
+ evictHooksForOwner(plugin.name);
69
+ for (const path of modulePaths) {
70
+ evictModule(path);
71
+ }
72
+ await preImportHooksDir(plugin.hooksDir, plugin.name);
80
73
  }
81
74
 
82
- /** Run the single expected hook in the chain and return its result. */
83
- async function dispatchOne(
84
- pluginDir: string,
85
- pluginName: string,
86
- ): Promise<unknown> {
87
- const entries = await collect(pluginDir, pluginName);
75
+ /** Collect and run the single expected hook, returning its result. */
76
+ async function dispatchOne(plugin: {
77
+ dir: string;
78
+ name: string;
79
+ }): Promise<unknown> {
80
+ const entries: HookEntry[] = await collectUserHookEntries(HOOK_NAME, [
81
+ [plugin.dir, plugin.name],
82
+ ]);
88
83
  expect(entries).toHaveLength(1);
89
84
  return (entries[0]!.fn as () => unknown)();
90
85
  }
91
86
 
92
- describe("hook live-reload", () => {
93
- test("an edited hook file takes effect on the next dispatch, repeatably", async () => {
87
+ describe("hook reload primitives", () => {
88
+ test("evict + re-import serves fresh hook code, repeatably", async () => {
94
89
  const plugin = makePlugin();
95
- writeHookFile(plugin.hooksDir, "v1");
96
- expect(await dispatchOne(plugin.dir, plugin.name)).toBe("v1");
90
+ const hookPath = join(plugin.hooksDir, `${HOOK_NAME}.ts`);
91
+ writeFileSync(hookPath, `export default () => "v1";\n`);
92
+ await preImportHooksDir(plugin.hooksDir, plugin.name);
93
+ expect(await dispatchOne(plugin)).toBe("v1");
97
94
 
98
- // Two edit → dispatch cycles: the first proves reload works at all, the
99
- // second proves a reloaded hook is itself reloadable (edits keep landing
100
- // turn after turn, not just once).
95
+ // Two edit → redeploy cycles: the first proves the primitive, the
96
+ // second proves a re-imported module is itself evictable again.
101
97
  for (const marker of ["v2", "v3"]) {
102
- writeHookFile(plugin.hooksDir, marker);
103
- expect(await dispatchOne(plugin.dir, plugin.name)).toBe(marker);
98
+ writeFileSync(
99
+ hookPath,
100
+ `export default () => ${JSON.stringify(marker)};\n`,
101
+ );
102
+ await redeploy(plugin, [hookPath]);
103
+ expect(await dispatchOne(plugin)).toBe(marker);
104
104
  }
105
105
  });
106
106
 
107
- test("an unchanged hook file is served from the cache, not re-imported", async () => {
107
+ test("dispatch serves the same cached function until an eviction", async () => {
108
108
  const plugin = makePlugin();
109
- writeHookFile(plugin.hooksDir, "stable");
110
-
111
- const first = await collect(plugin.dir, plugin.name);
112
- const second = await collect(plugin.dir, plugin.name);
113
-
114
- // Same function instance means the cache hit — dispatch after dispatch
115
- // costs a stat, not an import.
109
+ writeFileSync(
110
+ join(plugin.hooksDir, `${HOOK_NAME}.ts`),
111
+ `export default () => "stable";\n`,
112
+ );
113
+ await preImportHooksDir(plugin.hooksDir, plugin.name);
114
+
115
+ const first = await collectUserHookEntries(HOOK_NAME, [
116
+ [plugin.dir, plugin.name],
117
+ ]);
118
+ const second = await collectUserHookEntries(HOOK_NAME, [
119
+ [plugin.dir, plugin.name],
120
+ ]);
121
+
122
+ // Same function instance: dispatch is a map lookup, not an import.
116
123
  expect(second[0]!.fn).toBe(first[0]!.fn);
117
124
  });
118
125
 
119
- test("a deleted hook stops dispatching; recreating it serves the new content", async () => {
126
+ test("evicting only the hook re-binds it to a stale helper; the full sweep does not", async () => {
120
127
  const plugin = makePlugin();
121
- const path = writeHookFile(plugin.hooksDir, "original");
122
- expect(await dispatchOne(plugin.dir, plugin.name)).toBe("original");
123
-
124
- unlinkSync(path);
125
- expect(await collect(plugin.dir, plugin.name)).toHaveLength(0);
126
-
127
- // The recreated file must dispatch its own content, not the module the
128
- // registry cached for the deleted original.
129
- writeHookFile(plugin.hooksDir, "recreated");
130
- expect(await dispatchOne(plugin.dir, plugin.name)).toBe("recreated");
128
+ const helperPath = join(plugin.dir, "lib", "helper.ts");
129
+ const hookPath = join(plugin.hooksDir, `${HOOK_NAME}.ts`);
130
+ writeFileSync(helperPath, `export const value = "h1";\n`);
131
+ writeFileSync(
132
+ hookPath,
133
+ `import { value } from "../lib/helper.ts";\nexport default () => value;\n`,
134
+ );
135
+ await preImportHooksDir(plugin.hooksDir, plugin.name);
136
+ expect(await dispatchOne(plugin)).toBe("h1");
137
+
138
+ writeFileSync(helperPath, `export const value = "h2";\n`);
139
+
140
+ // Partial eviction — the hook file only. The re-evaluated hook binds to
141
+ // the helper module still cached at h1: the version-skew failure mode
142
+ // whole-plugin sweeping exists to prevent.
143
+ await redeploy(plugin, [hookPath]);
144
+ expect(await dispatchOne(plugin)).toBe("h1");
145
+
146
+ // The full sweep (hook + helper, as the reconcile's eviction list would
147
+ // carry) brings the edit through.
148
+ await redeploy(plugin, [hookPath, helperPath]);
149
+ expect(await dispatchOne(plugin)).toBe("h2");
131
150
  });
132
151
 
133
- test("standalone workspace hooks reload the same way", async () => {
134
- const wsHooksDir = getWorkspaceHooksDir();
135
- mkdirSync(wsHooksDir, { recursive: true });
136
- const path = writeHookFile(wsHooksDir, "ws-v1");
137
-
138
- try {
139
- const before = await collectUserHookEntries(HOOK_NAME, []);
140
- expect(before).toHaveLength(1);
141
- expect(before[0]!.owner.kind).toBe("workspace");
142
- expect((before[0]!.fn as () => unknown)()).toBe("ws-v1");
143
-
144
- writeHookFile(wsHooksDir, "ws-v2");
145
- const after = await collectUserHookEntries(HOOK_NAME, []);
146
- expect((after[0]!.fn as () => unknown)()).toBe("ws-v2");
147
- } finally {
148
- // The workspace hooks dir is shared per-process test state — leave it
149
- // the way this test found it.
150
- unlinkSync(path);
151
- }
152
+ test("re-import after owner eviction drops hooks whose files are gone", async () => {
153
+ const plugin = makePlugin();
154
+ const hookPath = join(plugin.hooksDir, `${HOOK_NAME}.ts`);
155
+ writeFileSync(hookPath, `export default () => "here";\n`);
156
+ await preImportHooksDir(plugin.hooksDir, plugin.name);
157
+ expect(await dispatchOne(plugin)).toBe("here");
158
+
159
+ rmSync(hookPath);
160
+ await redeploy(plugin, [hookPath]);
161
+
162
+ const entries = await collectUserHookEntries(HOOK_NAME, [
163
+ [plugin.dir, plugin.name],
164
+ ]);
165
+ expect(entries).toHaveLength(0);
152
166
  });
153
167
  });