@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.
- package/openapi.yaml +43 -6
- package/package.json +1 -1
- package/src/__tests__/attachment-stored-path-annotation.test.ts +151 -0
- package/src/__tests__/attachments-store.test.ts +37 -1
- package/src/__tests__/attachments.test.ts +53 -0
- package/src/__tests__/channel-reply-delivery.test.ts +214 -5
- package/src/__tests__/config-loader-backfill.test.ts +16 -36
- package/src/__tests__/image-source-path-reinject.test.ts +155 -14
- package/src/__tests__/managed-profile-guard.test.ts +2 -0
- package/src/__tests__/mtime-cache.test.ts +265 -80
- package/src/__tests__/plugin-config-data-migration.test.ts +2 -4
- package/src/__tests__/plugin-effective-enabled-set.test.ts +9 -6
- package/src/__tests__/workspace-migration-125-repoint-managed-connections-to-vellum.test.ts +140 -0
- package/src/agent/attachments.ts +55 -13
- package/src/cli/commands/platform/__tests__/credits.test.ts +111 -0
- package/src/cli/commands/platform/index.ts +86 -4
- package/src/cli/lib/__tests__/confirm-prompt.test.ts +9 -9
- package/src/cli/lib/diff-plugin.ts +7 -3
- package/src/cli/lib/inspect-plugin.ts +28 -10
- package/src/cli/lib/install-from-github.ts +69 -24
- package/src/cli/lib/ipc-params.ts +3 -3
- package/src/cli/lib/list-installed-plugins.ts +11 -5
- package/src/cli/lib/merge-plugin-tree.ts +15 -9
- package/src/cli/lib/plugin-fingerprint.ts +0 -6
- package/src/cli/lib/publish-plugin.ts +1 -1
- package/src/cli/lib/unknown-command.ts +3 -1
- package/src/cli/lib/upgrade-plugin.ts +2 -5
- package/src/config/__tests__/sync-gated-profiles.test.ts +2 -2
- package/src/config/seed-inference-profiles.ts +5 -4
- package/src/daemon/conversation-lifecycle.ts +37 -15
- package/src/daemon/conversation-messaging.ts +56 -28
- package/src/daemon/conversation.ts +2 -2
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +6 -0
- package/src/hooks/__tests__/hook-live-reload.test.ts +110 -96
- package/src/hooks/hook-loader.ts +57 -144
- package/src/hooks/registry.ts +13 -9
- package/src/persistence/attachments-store.ts +2 -1
- package/src/persistence/conversation-crud.ts +24 -0
- package/src/plugin-api/types.ts +4 -1
- package/src/plugins/mtime-cache.ts +347 -33
- package/src/plugins/pipeline.ts +3 -2
- package/src/providers/__tests__/inference.test.ts +42 -31
- package/src/providers/inference/__tests__/connections-label.test.ts +9 -28
- package/src/providers/inference/auth.ts +1 -2
- package/src/providers/inference/backfill.ts +7 -5
- package/src/providers/inference/connections.ts +54 -30
- package/src/runtime/channel-reply-delivery.ts +83 -24
- package/src/runtime/channel-retry-sweep.ts +4 -0
- package/src/runtime/{slack-no-response.ts → no-response.ts} +26 -6
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +43 -3
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +61 -24
- package/src/runtime/routes/conversation-query-routes.ts +18 -1
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +1 -1
- package/src/runtime/routes/inference-provider-connection-routes.ts +4 -3
- package/src/runtime/routes/platform-routes.ts +82 -1
- package/src/runtime/slack-reply-session.ts +1 -1
- package/src/workspace/migrations/125-repoint-managed-connections-to-vellum.ts +99 -0
- 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(
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* hook
|
|
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
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
/**
|
|
83
|
-
async function dispatchOne(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
): Promise<unknown> {
|
|
87
|
-
const entries = await
|
|
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
|
|
93
|
-
test("
|
|
87
|
+
describe("hook reload primitives", () => {
|
|
88
|
+
test("evict + re-import serves fresh hook code, repeatably", async () => {
|
|
94
89
|
const plugin = makePlugin();
|
|
95
|
-
|
|
96
|
-
|
|
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 →
|
|
99
|
-
// second proves a
|
|
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
|
-
|
|
103
|
-
|
|
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("
|
|
107
|
+
test("dispatch serves the same cached function until an eviction", async () => {
|
|
108
108
|
const plugin = makePlugin();
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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("
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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("
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
});
|