@vellumai/assistant 0.11.4-staging.2 → 0.11.4-staging.4
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/AGENTS.md +8 -2
- package/ARCHITECTURE.md +2 -0
- package/docs/architecture/memory.md +15 -0
- package/docs/browser-use-architecture-phase2.md +128 -56
- package/docs/flux-turn-detection-spike.md +11 -6
- package/docs/guardian-request-flow.md +35 -0
- package/knip.json +3 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
- package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
- package/openapi.yaml +136 -76
- package/package.json +1 -1
- package/scripts/write-plugin-api-shim.ts +10 -0
- package/src/__tests__/app-control-flow.test.ts +1 -0
- package/src/__tests__/approval-routes-http.test.ts +2 -2
- package/src/__tests__/assistant-feature-flag-guard.test.ts +25 -3
- package/src/__tests__/channel-setup-panel-ack.test.ts +1 -1
- package/src/__tests__/compaction-events.test.ts +8 -10
- package/src/__tests__/conversation-agent-loop.test.ts +4 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +112 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +209 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +39 -62
- package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +9 -9
- package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +53 -0
- package/src/__tests__/conversation-slash-queue.test.ts +3 -0
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-app-open.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +21 -14
- package/src/__tests__/conversation-surfaces-queued-emit.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-standalone.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-state-update.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-table-action.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -1
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +1 -1
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +1 -1
- package/src/__tests__/cu-unified-flow.test.ts +1 -0
- package/src/__tests__/document-sync-tags.test.ts +0 -75
- package/src/__tests__/file-ops-service.test.ts +163 -30
- package/src/__tests__/filesystem-tools.test.ts +23 -24
- package/src/__tests__/gateway-only-guard.test.ts +2 -5
- package/src/__tests__/host-file-read-tool.test.ts +16 -19
- package/src/__tests__/http-user-message-parity.test.ts +1 -1
- package/src/__tests__/init-feature-flag-overrides.test.ts +49 -0
- package/src/__tests__/managed-skill-lifecycle.test.ts +7 -0
- package/src/__tests__/media-generate-image.test.ts +131 -21
- package/src/__tests__/memory-retrieval-hook.test.ts +94 -2
- package/src/__tests__/plugin-api-webhook-url.test.ts +10 -7
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +7 -3
- package/src/__tests__/proxy-approval-callback.test.ts +1 -0
- package/src/__tests__/qdrant-manager.test.ts +14 -1
- package/src/__tests__/run-due-schedules.test.ts +21 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +187 -18
- package/src/__tests__/schedule-routes.test.ts +23 -0
- package/src/__tests__/schedule-store.test.ts +17 -0
- package/src/__tests__/secret-ingress-http.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +3 -3
- package/src/__tests__/starter-task-flow.test.ts +5 -4
- package/src/__tests__/subagent-fork-prompt-role.test.ts +1 -1
- package/src/__tests__/subagent-spawn-and-await.test.ts +4 -7
- package/src/__tests__/subagent-tool-gate-mode.test.ts +1 -1
- package/src/__tests__/subagent-tools.test.ts +81 -101
- package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +1 -0
- package/src/__tests__/tool-executor.test.ts +5 -1
- package/src/__tests__/ui-choice-copy-surfaces.test.ts +1 -1
- package/src/__tests__/ui-visual-surface.test.ts +1 -1
- package/src/__tests__/ui-voice-picker-surface.test.ts +1 -1
- package/src/__tests__/ui-work-result-surface.test.ts +1 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +5 -3
- package/src/__tests__/voice-session-bridge.test.ts +85 -29
- package/src/acp/session-manager.ts +8 -1
- package/src/api/events/host-file.ts +2 -2
- package/src/api/surfaces.ts +5 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +21 -10
- package/src/calls/__tests__/voice-triage-escalate.test.ts +8 -0
- package/src/calls/voice-session-bridge.ts +44 -25
- package/src/calls/voice-triage-escalate.ts +1 -0
- package/src/cli/bundled-modules.ts +29 -0
- package/src/cli/commands/db/repair.ts +4 -8
- package/src/cli/commands/domain.ts +6 -3
- package/src/cli/commands/email.ts +6 -3
- package/src/cli/commands/keys.ts +8 -3
- package/src/cli/commands/plugins.ts +85 -36
- package/src/cli/commands/schedules.ts +35 -1
- package/src/cli/lib/bundled-marketplace.json +1 -1
- package/src/config/__tests__/balanced-model-experiment.test.ts +278 -0
- package/src/config/assistant-feature-flags.ts +36 -15
- package/src/config/balanced-model-experiment.ts +35 -0
- package/src/config/bundled-skills/image-studio/SKILL.md +5 -4
- package/src/config/bundled-skills/image-studio/TOOLS.json +1 -1
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +101 -0
- package/src/config/bundled-skills/skill-management/TOOLS.json +9 -3
- package/src/config/bundled-skills/subagent/SKILL.md +17 -12
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -4
- package/src/config/call-site-defaults.ts +7 -0
- package/src/config/default-profile-catalog.ts +96 -4
- package/src/config/feature-flag-registry.json +11 -11
- package/src/config/skills.ts +9 -2
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +1 -1
- package/src/daemon/conversation-agent-loop.ts +14 -13
- package/src/daemon/conversation-notifiers.ts +11 -9
- package/src/daemon/conversation-process.ts +0 -27
- package/src/daemon/conversation-runtime-assembly.ts +9 -2
- package/src/daemon/conversation-store.ts +4 -4
- package/src/daemon/conversation-surfaces.ts +27 -13
- package/src/daemon/conversation-tool-setup.ts +2 -4
- package/src/daemon/conversation.ts +104 -51
- package/src/daemon/doordash-steps.ts +2 -2
- package/src/daemon/lifecycle.ts +14 -1
- package/src/daemon/process-message.ts +0 -13
- package/src/daemon/windows-compiled-entry.ts +4 -0
- package/src/documents/document-store.ts +5 -235
- package/src/hooks/types.ts +5 -0
- package/src/ipc/gateway-flag-listener.ts +17 -3
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +118 -0
- package/src/live-voice/live-voice-manager.ts +16 -3
- package/src/live-voice/live-voice-session.ts +63 -9
- package/src/live-voice/windows-compiled-live-voice.ts +4 -0
- package/src/monitoring/control.ts +1 -0
- package/src/monitoring/db-integrity-sample.ts +4 -5
- package/src/notifications/AGENTS.md +2 -0
- package/src/notifications/approval-card-data.ts +33 -0
- package/src/permissions/prompter.ts +1 -5
- package/src/persistence/conversation-queries.ts +66 -16
- package/src/persistence/embeddings/qdrant-manager.ts +84 -49
- package/src/persistence/migrations/360-add-document-workspace-path.ts +5 -14
- package/src/persistence/schema/documents.ts +4 -4
- package/src/plugin-api/constants.ts +8 -0
- package/src/plugin-api/index.ts +5 -1
- package/src/plugin-api/webhook-url.ts +13 -11
- package/src/plugins/defaults/main.ts +6 -7
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +77 -0
- package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +24 -6
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +53 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +4 -4
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +18 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +66 -1
- package/src/plugins/defaults/memory/v3/injector.ts +8 -0
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +23 -9
- package/src/plugins/defaults/memory/worker-control.ts +1 -0
- package/src/plugins/defaults/worker-entrypoints.ts +3 -0
- package/src/plugins/mtime-cache.ts +17 -0
- package/src/prompts/templates/system-sections.ts +0 -7
- package/src/providers/__tests__/context-overflow-error.test.ts +24 -0
- package/src/providers/__tests__/retry-callsite.test.ts +20 -0
- package/src/providers/openai/chat-completions-provider.ts +11 -1
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +11 -6
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +9 -49
- package/src/routes/control.ts +1 -0
- package/src/routes/route-host-client.ts +1 -0
- package/src/runtime/AGENTS.md +16 -17
- package/src/runtime/agent-wake.ts +15 -12
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +170 -1
- package/src/runtime/routes/__tests__/schedule-routes-disarm-reason.test.ts +215 -0
- package/src/runtime/routes/conversation-list-routes.ts +54 -22
- package/src/runtime/routes/conversation-management-routes.ts +2 -3
- package/src/runtime/routes/conversation-routes.ts +11 -13
- package/src/runtime/routes/documents-routes.ts +3 -222
- package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +2 -0
- package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +3 -0
- package/src/runtime/routes/playground/inject-failures.ts +2 -2
- package/src/runtime/routes/playground/reset-circuit.ts +1 -1
- package/src/runtime/routes/schedule-routes.ts +94 -7
- package/src/runtime/routes/workspace-routes.ts +0 -9
- package/src/runtime/routes/workspace-utils.ts +3 -13
- package/src/runtime/services/conversation-serializer.ts +7 -2
- package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +68 -5
- package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +81 -0
- package/src/schedule/plugin-schedule-availability.ts +58 -0
- package/src/schedule/plugin-schedule-declarations.ts +23 -27
- package/src/schedule/plugin-schedule-reconciler.ts +12 -3
- package/src/schedule/schedule-store.ts +5 -1
- package/src/schedule/scheduler.ts +9 -4
- package/src/schedule/worker-control.ts +1 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +26 -15
- package/src/subagent/consult-context.ts +11 -11
- package/src/subagent/consult-prompt.ts +26 -35
- package/src/subagent/manager.ts +20 -37
- package/src/subagent/notify.ts +7 -1
- package/src/subagent/types.ts +15 -13
- package/src/tools/__tests__/tool-input-schemas.test.ts +7 -7
- package/src/tools/acp/spawn.ts +6 -4
- package/src/tools/filesystem/read.ts +27 -10
- package/src/tools/host-filesystem/read.ts +27 -15
- package/src/tools/shared/filesystem/file-ops-service.ts +63 -35
- package/src/tools/shared/filesystem/legacy-read-args.ts +22 -0
- package/src/tools/shared/filesystem/types.ts +5 -5
- package/src/tools/skills/scaffold-managed.ts +25 -7
- package/src/tools/subagent/spawn.ts +28 -88
- package/src/tools/ui-surface/surface-shape-docs.ts +1 -1
- package/src/util/__tests__/worker-process-command.test.ts +37 -0
- package/src/util/logger.ts +16 -0
- package/src/util/worker-process.ts +37 -4
- package/src/windows-compiled-cli.ts +32 -0
- package/src/windows-compiled-entry.ts +4 -0
- package/src/windows-compiled-logger.ts +6 -0
- package/src/windows-compiled-worker-entry.ts +29 -0
- package/src/__tests__/document-workspace-file.test.ts +0 -467
- package/src/daemon/interactive-turn-sender.ts +0 -59
- package/src/subagent/__tests__/consult-transcript.test.ts +0 -184
- package/src/subagent/consult-transcript.ts +0 -90
|
@@ -125,16 +125,19 @@ describe("FileSystemOps symlink handling", () => {
|
|
|
125
125
|
});
|
|
126
126
|
|
|
127
127
|
// ===========================================================================
|
|
128
|
-
// FileSystemOps: read
|
|
128
|
+
// FileSystemOps: read character-window edge cases
|
|
129
129
|
// ===========================================================================
|
|
130
130
|
|
|
131
|
-
describe("FileSystemOps read
|
|
132
|
-
test("
|
|
131
|
+
describe("FileSystemOps read character-window edge cases", () => {
|
|
132
|
+
test("startIndex beyond file length returns empty content", async () => {
|
|
133
133
|
const dir = makeTempDir();
|
|
134
134
|
writeFileSync(join(dir, "short.txt"), "a\nb\nc");
|
|
135
135
|
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
136
136
|
|
|
137
|
-
const result = await ops.readFileSafe({
|
|
137
|
+
const result = await ops.readFileSafe({
|
|
138
|
+
path: "short.txt",
|
|
139
|
+
startIndex: 100,
|
|
140
|
+
});
|
|
138
141
|
expect(result.ok).toBe(true);
|
|
139
142
|
if (!result.ok) {
|
|
140
143
|
return;
|
|
@@ -142,12 +145,12 @@ describe("FileSystemOps read offset/limit edge cases", () => {
|
|
|
142
145
|
expect(result.value.content).toBe("");
|
|
143
146
|
});
|
|
144
147
|
|
|
145
|
-
test("
|
|
148
|
+
test("maxChars of zero returns empty content", async () => {
|
|
146
149
|
const dir = makeTempDir();
|
|
147
150
|
writeFileSync(join(dir, "file.txt"), "a\nb\nc");
|
|
148
151
|
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
149
152
|
|
|
150
|
-
const result = await ops.readFileSafe({ path: "file.txt",
|
|
153
|
+
const result = await ops.readFileSafe({ path: "file.txt", maxChars: 0 });
|
|
151
154
|
expect(result.ok).toBe(true);
|
|
152
155
|
if (!result.ok) {
|
|
153
156
|
return;
|
|
@@ -155,61 +158,57 @@ describe("FileSystemOps read offset/limit edge cases", () => {
|
|
|
155
158
|
expect(result.value.content).toBe("");
|
|
156
159
|
});
|
|
157
160
|
|
|
158
|
-
test("
|
|
161
|
+
test("startIndex=0 reads from the first character (0-indexed)", async () => {
|
|
159
162
|
const dir = makeTempDir();
|
|
160
163
|
writeFileSync(join(dir, "file.txt"), "first\nsecond\nthird");
|
|
161
164
|
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
162
165
|
|
|
163
166
|
const result = await ops.readFileSafe({
|
|
164
167
|
path: "file.txt",
|
|
165
|
-
|
|
166
|
-
|
|
168
|
+
startIndex: 0,
|
|
169
|
+
maxChars: 5,
|
|
167
170
|
});
|
|
168
171
|
expect(result.ok).toBe(true);
|
|
169
172
|
if (!result.ok) {
|
|
170
173
|
return;
|
|
171
174
|
}
|
|
172
|
-
|
|
173
|
-
expect(
|
|
175
|
+
const [body] = result.value.content.split("\n\n[Truncated:");
|
|
176
|
+
expect(body).toBe("first");
|
|
174
177
|
});
|
|
175
178
|
|
|
176
|
-
test("
|
|
179
|
+
test("maxChars exceeding file length returns the rest of the file", async () => {
|
|
177
180
|
const dir = makeTempDir();
|
|
178
181
|
writeFileSync(join(dir, "file.txt"), "a\nb");
|
|
179
182
|
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
180
183
|
|
|
181
184
|
const result = await ops.readFileSafe({
|
|
182
185
|
path: "file.txt",
|
|
183
|
-
|
|
184
|
-
|
|
186
|
+
startIndex: 0,
|
|
187
|
+
maxChars: 1000,
|
|
185
188
|
});
|
|
186
189
|
expect(result.ok).toBe(true);
|
|
187
190
|
if (!result.ok) {
|
|
188
191
|
return;
|
|
189
192
|
}
|
|
190
|
-
expect(result.value.content).
|
|
191
|
-
expect(result.value.content).toContain("b");
|
|
193
|
+
expect(result.value.content).toBe("a\nb");
|
|
192
194
|
});
|
|
193
195
|
|
|
194
|
-
test("
|
|
196
|
+
test("the window starts at startIndex and carries no line numbers", async () => {
|
|
195
197
|
const dir = makeTempDir();
|
|
196
198
|
writeFileSync(join(dir, "file.txt"), "a\nb\nc\nd\ne");
|
|
197
199
|
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
198
200
|
|
|
199
201
|
const result = await ops.readFileSafe({
|
|
200
202
|
path: "file.txt",
|
|
201
|
-
|
|
202
|
-
|
|
203
|
+
startIndex: 4,
|
|
204
|
+
maxChars: 3,
|
|
203
205
|
});
|
|
204
206
|
expect(result.ok).toBe(true);
|
|
205
207
|
if (!result.ok) {
|
|
206
208
|
return;
|
|
207
209
|
}
|
|
208
|
-
|
|
209
|
-
expect(
|
|
210
|
-
expect(result.value.content).toContain("4");
|
|
211
|
-
expect(result.value.content).toContain("c");
|
|
212
|
-
expect(result.value.content).toContain("d");
|
|
210
|
+
const [body] = result.value.content.split("\n\n[Truncated:");
|
|
211
|
+
expect(body).toBe("c\nd");
|
|
213
212
|
});
|
|
214
213
|
});
|
|
215
214
|
|
|
@@ -83,11 +83,8 @@ function isGatewayInternal(filePath: string): boolean {
|
|
|
83
83
|
/** Additional files allowed for the interpolated-port check only (use gateway port, not runtime). */
|
|
84
84
|
const INTERPOLATED_PORT_ALLOWLIST = new Set([
|
|
85
85
|
"clients/macos/src/main/bundle-flow.ts",
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
// class as bundle-flow.ts. Added with #34049 (host proxy for cloud/managed
|
|
89
|
-
// assistants); the allowlist entry was missed there.
|
|
90
|
-
"clients/macos/src/main/host-proxy-router.ts",
|
|
86
|
+
// Desktop clients connect to local assistants through their loopback gateway.
|
|
87
|
+
"packages/electron-desktop/src/host-proxy/router.ts",
|
|
91
88
|
]);
|
|
92
89
|
|
|
93
90
|
/** Shared violation filter: exempt test files, gateway internals, and allowlisted paths. */
|
|
@@ -27,7 +27,7 @@ mock.module("../daemon/host-file-proxy.js", () => ({
|
|
|
27
27
|
}));
|
|
28
28
|
|
|
29
29
|
import { hostFileReadTool } from "../tools/host-filesystem/read.js";
|
|
30
|
-
import {
|
|
30
|
+
import { READ_CHAR_BUDGET } from "../tools/shared/filesystem/file-ops-service.js";
|
|
31
31
|
import type { ToolContext } from "../tools/types.js";
|
|
32
32
|
|
|
33
33
|
const testDirs: string[] = [];
|
|
@@ -77,19 +77,19 @@ describe("host_file_read tool", () => {
|
|
|
77
77
|
expect(result.content).toContain("must be absolute");
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
-
test("reads
|
|
80
|
+
test("reads a character window", async () => {
|
|
81
81
|
const dir = mkdtempSync(join(tmpdir(), "host-file-read-test-"));
|
|
82
82
|
testDirs.push(dir);
|
|
83
83
|
const filePath = join(dir, "sample.txt");
|
|
84
84
|
writeFileSync(filePath, "first\nsecond\nthird\n");
|
|
85
85
|
|
|
86
86
|
const result = await hostFileReadTool.execute(
|
|
87
|
-
{ path: filePath,
|
|
87
|
+
{ path: filePath, start_index: 6, max_chars: 6 },
|
|
88
88
|
makeContext(),
|
|
89
89
|
);
|
|
90
90
|
expect(result.isError).toBe(false);
|
|
91
|
-
expect(result.content).toContain("
|
|
92
|
-
expect(result.content).toContain("
|
|
91
|
+
expect(result.content).toContain("second");
|
|
92
|
+
expect(result.content).not.toContain("first");
|
|
93
93
|
});
|
|
94
94
|
|
|
95
95
|
test("returns error when file does not exist", async () => {
|
|
@@ -128,7 +128,7 @@ describe("host_file_read tool", () => {
|
|
|
128
128
|
expect(result.content).toContain('Invalid input for tool "host_file_read"');
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
-
test("reads
|
|
131
|
+
test("reads the whole file when no window is specified", async () => {
|
|
132
132
|
const dir = mkdtempSync(join(tmpdir(), "host-file-read-test-"));
|
|
133
133
|
testDirs.push(dir);
|
|
134
134
|
const filePath = join(dir, "full.txt");
|
|
@@ -139,9 +139,7 @@ describe("host_file_read tool", () => {
|
|
|
139
139
|
makeContext(),
|
|
140
140
|
);
|
|
141
141
|
expect(result.isError).toBe(false);
|
|
142
|
-
expect(result.content).
|
|
143
|
-
expect(result.content).toContain("2 line2");
|
|
144
|
-
expect(result.content).toContain("3 line3");
|
|
142
|
+
expect(result.content).toBe("line1\nline2\nline3\n");
|
|
145
143
|
});
|
|
146
144
|
|
|
147
145
|
test("handles empty file", async () => {
|
|
@@ -157,20 +155,19 @@ describe("host_file_read tool", () => {
|
|
|
157
155
|
expect(result.isError).toBe(false);
|
|
158
156
|
});
|
|
159
157
|
|
|
160
|
-
test("
|
|
158
|
+
test("start_index is 0-indexed", async () => {
|
|
161
159
|
const dir = mkdtempSync(join(tmpdir(), "host-file-read-test-"));
|
|
162
160
|
testDirs.push(dir);
|
|
163
161
|
const filePath = join(dir, "lines.txt");
|
|
164
162
|
writeFileSync(filePath, "a\nb\nc\nd\ne\n");
|
|
165
163
|
|
|
166
164
|
const result = await hostFileReadTool.execute(
|
|
167
|
-
{ path: filePath,
|
|
165
|
+
{ path: filePath, start_index: 4, max_chars: 1 },
|
|
168
166
|
makeContext(),
|
|
169
167
|
);
|
|
170
168
|
expect(result.isError).toBe(false);
|
|
171
|
-
|
|
172
|
-
expect(
|
|
173
|
-
expect(result.content).not.toContain("4 d");
|
|
169
|
+
const [body] = result.content.split("\n\n[Truncated:");
|
|
170
|
+
expect(body).toBe("c");
|
|
174
171
|
});
|
|
175
172
|
|
|
176
173
|
test("reads a file with symlinks resolved", async () => {
|
|
@@ -228,15 +225,15 @@ describe("host_file_read image support", () => {
|
|
|
228
225
|
expect(result.isError).toBe(false);
|
|
229
226
|
expect(result.contentBlocks).toHaveLength(1);
|
|
230
227
|
// The proxied request carries the resolved default window, so a host read
|
|
231
|
-
// is bounded by the same
|
|
228
|
+
// is bounded by the same budget as a local one rather than streaming a
|
|
232
229
|
// whole file across the bridge.
|
|
233
230
|
expect(requests).toEqual([
|
|
234
231
|
{
|
|
235
232
|
input: {
|
|
236
233
|
operation: "read",
|
|
237
234
|
path: "/host/screenshot.png",
|
|
238
|
-
|
|
239
|
-
|
|
235
|
+
startIndex: undefined,
|
|
236
|
+
maxChars: READ_CHAR_BUDGET,
|
|
240
237
|
},
|
|
241
238
|
conversationId: "test-conversation",
|
|
242
239
|
signal: undefined,
|
|
@@ -306,8 +303,8 @@ describe("host_file_read image support", () => {
|
|
|
306
303
|
);
|
|
307
304
|
|
|
308
305
|
expect(result.isError).toBe(false);
|
|
309
|
-
expect(result.content).toContain("
|
|
310
|
-
expect(result.content).toContain("
|
|
306
|
+
expect(result.content).toContain("hello world");
|
|
307
|
+
expect(result.content).toContain("second line");
|
|
311
308
|
expect((result as any).contentBlocks).toBeUndefined();
|
|
312
309
|
});
|
|
313
310
|
|
|
@@ -189,7 +189,7 @@ const _testAuthContext: AuthContext = {
|
|
|
189
189
|
function makeConversation(overrides: Record<string, unknown> = {}) {
|
|
190
190
|
return {
|
|
191
191
|
setTrustContext: () => {},
|
|
192
|
-
|
|
192
|
+
replayActivityState: () => {},
|
|
193
193
|
setHostBrowserProxy: () => {},
|
|
194
194
|
setHostCuProxy: () => {},
|
|
195
195
|
setHostAppControlProxy: () => {},
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
clearFeatureFlagOverridesCache,
|
|
12
12
|
initFeatureFlagOverrides,
|
|
13
13
|
isAssistantFeatureFlagEnabled,
|
|
14
|
+
refreshOverridesFromGateway,
|
|
14
15
|
} from "../config/assistant-feature-flags.js";
|
|
15
16
|
import { mockGatewayIpc, resetMockGatewayIpc } from "./mock-gateway-ipc.js";
|
|
16
17
|
|
|
@@ -110,3 +111,51 @@ describe("initFeatureFlagOverrides", () => {
|
|
|
110
111
|
expect(isAssistantFeatureFlagEnabled("second-call", config)).toBe(false);
|
|
111
112
|
});
|
|
112
113
|
});
|
|
114
|
+
|
|
115
|
+
describe("refreshOverridesFromGateway", () => {
|
|
116
|
+
it("replaces the cache with the new gateway values", async () => {
|
|
117
|
+
mockGatewayIpc({ "moving-flag": true });
|
|
118
|
+
await initFeatureFlagOverrides();
|
|
119
|
+
|
|
120
|
+
resetMockGatewayIpc();
|
|
121
|
+
mockGatewayIpc({ "moving-flag": false });
|
|
122
|
+
|
|
123
|
+
expect(await refreshOverridesFromGateway()).toBe(true);
|
|
124
|
+
|
|
125
|
+
const config = {} as any;
|
|
126
|
+
expect(isAssistantFeatureFlagEnabled("moving-flag", config)).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("keeps the last known values when the refresh fails", async () => {
|
|
130
|
+
mockGatewayIpc({ "moving-flag": true });
|
|
131
|
+
await initFeatureFlagOverrides();
|
|
132
|
+
|
|
133
|
+
resetMockGatewayIpc();
|
|
134
|
+
mockGatewayIpc(null, { error: true });
|
|
135
|
+
|
|
136
|
+
expect(await refreshOverridesFromGateway()).toBe(false);
|
|
137
|
+
|
|
138
|
+
// A failed refresh must not drop the flag to its registry default: a
|
|
139
|
+
// value the assistant selects behavior from (an experiment arm, a kill
|
|
140
|
+
// switch) would otherwise flip on one timed-out IPC call.
|
|
141
|
+
const config = {} as any;
|
|
142
|
+
expect(isAssistantFeatureFlagEnabled("moving-flag", config)).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("keeps the cache readable for the whole call", async () => {
|
|
146
|
+
mockGatewayIpc({ "moving-flag": true });
|
|
147
|
+
await initFeatureFlagOverrides();
|
|
148
|
+
|
|
149
|
+
resetMockGatewayIpc();
|
|
150
|
+
mockGatewayIpc({ "moving-flag": false });
|
|
151
|
+
|
|
152
|
+
const config = {} as any;
|
|
153
|
+
const inFlight = refreshOverridesFromGateway();
|
|
154
|
+
// A sync read landing mid-refresh sees the previous values, never an
|
|
155
|
+
// empty cache.
|
|
156
|
+
expect(isAssistantFeatureFlagEnabled("moving-flag", config)).toBe(true);
|
|
157
|
+
|
|
158
|
+
await inFlight;
|
|
159
|
+
expect(isAssistantFeatureFlagEnabled("moving-flag", config)).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -47,6 +47,9 @@ import { skillLoadTool } from "../tools/skills/load.js";
|
|
|
47
47
|
import { executeScaffoldManagedSkill } from "../tools/skills/scaffold-managed.js";
|
|
48
48
|
import type { ToolContext } from "../tools/types.js";
|
|
49
49
|
|
|
50
|
+
/** scaffold_managed_skill requires activation hints; a fixed set for tests. */
|
|
51
|
+
const HINTS = ["user asks to run the lifecycle test procedure"];
|
|
52
|
+
|
|
50
53
|
function makeContext(): ToolContext {
|
|
51
54
|
return {
|
|
52
55
|
workingDir: "/tmp",
|
|
@@ -126,6 +129,7 @@ Run the custom lifecycle verification procedure.
|
|
|
126
129
|
name: "Lifecycle Test",
|
|
127
130
|
description: "Integration test skill.",
|
|
128
131
|
body_markdown: "Run the lifecycle test procedure.",
|
|
132
|
+
activation_hints: HINTS,
|
|
129
133
|
emoji: "🧪",
|
|
130
134
|
},
|
|
131
135
|
makeContext(),
|
|
@@ -185,6 +189,7 @@ Run the custom lifecycle verification procedure.
|
|
|
185
189
|
name: "V1",
|
|
186
190
|
description: "Version 1.",
|
|
187
191
|
body_markdown: "Original body.",
|
|
192
|
+
activation_hints: HINTS,
|
|
188
193
|
},
|
|
189
194
|
ctx,
|
|
190
195
|
);
|
|
@@ -196,6 +201,7 @@ Run the custom lifecycle verification procedure.
|
|
|
196
201
|
name: "V2",
|
|
197
202
|
description: "Version 2.",
|
|
198
203
|
body_markdown: "Updated body.",
|
|
204
|
+
activation_hints: HINTS,
|
|
199
205
|
overwrite: true,
|
|
200
206
|
},
|
|
201
207
|
ctx,
|
|
@@ -241,6 +247,7 @@ Run the custom lifecycle verification procedure.
|
|
|
241
247
|
description: "Created from scaffold.",
|
|
242
248
|
body_markdown:
|
|
243
249
|
"This skill was dynamically created.\n\nRun: `echo chain-test-ok`",
|
|
250
|
+
activation_hints: HINTS,
|
|
244
251
|
},
|
|
245
252
|
ctx,
|
|
246
253
|
);
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
symlinkSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "fs";
|
|
11
|
+
import { tmpdir } from "os";
|
|
4
12
|
import { dirname, join } from "path";
|
|
5
13
|
|
|
6
14
|
import { __resetRegistryForTesting, getTool } from "../tools/registry.js";
|
|
@@ -97,8 +105,23 @@ import { run } from "../config/bundled-skills/image-studio/tools/media-generate-
|
|
|
97
105
|
// Clean up after this file to prevent contamination of later test files.
|
|
98
106
|
afterAll(() => {
|
|
99
107
|
__resetRegistryForTesting();
|
|
108
|
+
for (const dir of tempWorkingDirs) {
|
|
109
|
+
rmSync(dir, { recursive: true, force: true });
|
|
110
|
+
}
|
|
100
111
|
});
|
|
101
112
|
|
|
113
|
+
const tempWorkingDirs: string[] = [];
|
|
114
|
+
|
|
115
|
+
/** Build a ToolContext with a fresh temp working directory. */
|
|
116
|
+
function makeContext(): ToolContext {
|
|
117
|
+
const workingDir = mkdtempSync(join(tmpdir(), "media-gen-test-"));
|
|
118
|
+
tempWorkingDirs.push(workingDir);
|
|
119
|
+
return {
|
|
120
|
+
conversationId: "conv-123",
|
|
121
|
+
workingDir,
|
|
122
|
+
} as unknown as ToolContext;
|
|
123
|
+
}
|
|
124
|
+
|
|
102
125
|
const CONFIG_DIR = join(
|
|
103
126
|
dirname(import.meta.dirname!),
|
|
104
127
|
"config",
|
|
@@ -128,12 +151,10 @@ beforeEach(() => {
|
|
|
128
151
|
platformBaseUrl: "",
|
|
129
152
|
assistantApiKey: "",
|
|
130
153
|
};
|
|
154
|
+
fakeContext = makeContext();
|
|
131
155
|
});
|
|
132
156
|
|
|
133
|
-
|
|
134
|
-
conversationId: "conv-123",
|
|
135
|
-
workingDir: "/tmp",
|
|
136
|
-
} as unknown as ToolContext;
|
|
157
|
+
let fakeContext: ToolContext;
|
|
137
158
|
|
|
138
159
|
describe("image-studio skill script wrapper", () => {
|
|
139
160
|
test("exports a run function without registering media_generate_image in the tool registry", async () => {
|
|
@@ -431,28 +452,117 @@ describe("image-studio skill script wrapper", () => {
|
|
|
431
452
|
});
|
|
432
453
|
|
|
433
454
|
test("reads source images from file paths on disk", async () => {
|
|
434
|
-
// Write a temp image file inside the workspace
|
|
435
|
-
const tmpPath = join(
|
|
455
|
+
// Write a temp image file inside the workspace
|
|
456
|
+
const tmpPath = join(fakeContext.workingDir, "test-source-image.png");
|
|
436
457
|
const pngBytes = Buffer.from(
|
|
437
458
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
|
|
438
459
|
"base64",
|
|
439
460
|
);
|
|
440
461
|
await Bun.write(tmpPath, pngBytes);
|
|
441
462
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
463
|
+
const result = await run(
|
|
464
|
+
{ prompt: "edit this", mode: "edit", source_paths: [tmpPath] },
|
|
465
|
+
fakeContext,
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
expect(result.isError).toBe(false);
|
|
469
|
+
expect(result.content).toContain("Generated 1 image");
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("saves the image into media/generated and returns the embed instruction", async () => {
|
|
473
|
+
const result = await run({ prompt: "a sunset" }, fakeContext);
|
|
474
|
+
|
|
475
|
+
expect(result.isError).toBe(false);
|
|
476
|
+
expect(result.content).toContain("Saved to media/generated/a-sunset.png");
|
|
477
|
+
expect(result.content).toContain(
|
|
478
|
+
"",
|
|
479
|
+
);
|
|
480
|
+
const saved = Bun.file(
|
|
481
|
+
join(fakeContext.workingDir, "media/generated/a-sunset.png"),
|
|
482
|
+
);
|
|
483
|
+
expect(await saved.exists()).toBe(true);
|
|
484
|
+
const bytes = Buffer.from(await saved.arrayBuffer());
|
|
485
|
+
expect(bytes.equals(Buffer.from("generated-data", "base64"))).toBe(true);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test("uses the image title for the filename when present", async () => {
|
|
489
|
+
mockGenerateResult = {
|
|
490
|
+
images: [
|
|
491
|
+
{
|
|
492
|
+
mimeType: "image/png",
|
|
493
|
+
dataBase64: "img1",
|
|
494
|
+
title: "Twilight Gremlin!",
|
|
495
|
+
} as never,
|
|
496
|
+
],
|
|
497
|
+
text: "",
|
|
498
|
+
resolvedModel: "gemini-3.1-flash-image-preview",
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const result = await run({ prompt: "a pond creature" }, fakeContext);
|
|
502
|
+
|
|
503
|
+
expect(result.isError).toBe(false);
|
|
504
|
+
expect(result.content).toContain(
|
|
505
|
+
"Saved to media/generated/twilight-gremlin.png",
|
|
506
|
+
);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test("gives each variant a distinct filename", async () => {
|
|
510
|
+
mockGenerateResult = {
|
|
511
|
+
images: [
|
|
512
|
+
{ mimeType: "image/png", dataBase64: "img1" },
|
|
513
|
+
{ mimeType: "image/png", dataBase64: "img2" },
|
|
514
|
+
],
|
|
515
|
+
text: undefined as unknown as string,
|
|
516
|
+
resolvedModel: "gemini-3.1-flash-image-preview",
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const result = await run({ prompt: "a robot", variants: 2 }, fakeContext);
|
|
520
|
+
|
|
521
|
+
expect(result.isError).toBe(false);
|
|
522
|
+
expect(result.content).toContain("- media/generated/a-robot.png");
|
|
523
|
+
expect(result.content).toContain("- media/generated/a-robot-2.png");
|
|
524
|
+
expect(
|
|
525
|
+
await Bun.file(
|
|
526
|
+
join(fakeContext.workingDir, "media/generated/a-robot-2.png"),
|
|
527
|
+
).exists(),
|
|
528
|
+
).toBe(true);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test("suffixes the filename when a previous generation already used it", async () => {
|
|
532
|
+
const first = await run({ prompt: "a fox" }, fakeContext);
|
|
533
|
+
expect(first.content).toContain("Saved to media/generated/a-fox.png");
|
|
534
|
+
|
|
535
|
+
const second = await run({ prompt: "a fox" }, fakeContext);
|
|
536
|
+
expect(second.content).toContain("Saved to media/generated/a-fox-2.png");
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test("refuses to write through a symlink that escapes the workspace", async () => {
|
|
540
|
+
const outside = mkdtempSync(join(tmpdir(), "media-gen-outside-"));
|
|
541
|
+
tempWorkingDirs.push(outside);
|
|
542
|
+
symlinkSync(outside, join(fakeContext.workingDir, "media"));
|
|
543
|
+
|
|
544
|
+
const result = await run({ prompt: "a cat" }, fakeContext);
|
|
545
|
+
|
|
546
|
+
expect(result.isError).toBe(false);
|
|
547
|
+
expect(result.content).toContain("Could not save to the workspace");
|
|
548
|
+
expect(result.content).not.toContain("vellum://workspace/");
|
|
549
|
+
expect(readdirSync(outside)).toEqual([]);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test("falls back to inline-only when the workspace write fails", async () => {
|
|
553
|
+
// Occupy the media path with a regular file so the directory creation
|
|
554
|
+
// under it fails.
|
|
555
|
+
writeFileSync(join(fakeContext.workingDir, "media"), "not a directory");
|
|
556
|
+
|
|
557
|
+
const result = await run({ prompt: "a cat" }, fakeContext);
|
|
558
|
+
|
|
559
|
+
expect(result.isError).toBe(false);
|
|
560
|
+
expect(result.content).toContain("Could not save to the workspace");
|
|
561
|
+
expect(result.content).toContain(
|
|
562
|
+
"will be attached to your reply automatically",
|
|
563
|
+
);
|
|
564
|
+
expect(result.content).not.toContain("vellum://workspace/");
|
|
565
|
+
expect(result.contentBlocks).toHaveLength(1);
|
|
456
566
|
});
|
|
457
567
|
|
|
458
568
|
test("returns error when all source_paths are invalid", async () => {
|
|
@@ -64,9 +64,13 @@ mock.module("../runtime/assistant-event-hub.js", () => ({
|
|
|
64
64
|
broadcastMessage: broadcastMessageMock,
|
|
65
65
|
}));
|
|
66
66
|
|
|
67
|
-
import
|
|
67
|
+
import {
|
|
68
|
+
type UserPromptSubmitContext,
|
|
69
|
+
VOICE_ESCALATION_CONTINUATION_MESSAGE_KIND,
|
|
70
|
+
} from "@vellumai/plugin-api";
|
|
68
71
|
|
|
69
72
|
import type { AssistantEvent } from "../api/index.js";
|
|
73
|
+
import { ESCALATION_CONTINUATION_CONTENT } from "../calls/voice-triage-escalate.js";
|
|
70
74
|
import type { AssistantConfig } from "../config/schema.js";
|
|
71
75
|
import type { Conversation } from "../daemon/conversation.js";
|
|
72
76
|
import type { QdrantSparseVector } from "../persistence/embeddings/qdrant-client.js";
|
|
@@ -74,6 +78,7 @@ import type { ConversationGraphMemory } from "../plugins/defaults/memory/graph/c
|
|
|
74
78
|
import userPromptSubmitMemoryRetrieval from "../plugins/defaults/memory/hooks/user-prompt-submit.js";
|
|
75
79
|
import type { Message } from "../providers/types.js";
|
|
76
80
|
import { asConversation } from "./helpers/mock-conversation.js";
|
|
81
|
+
import { setConfig } from "./helpers/set-config.js";
|
|
77
82
|
|
|
78
83
|
/** Canonical metrics payload the graph retriever attaches to a real hit. */
|
|
79
84
|
function makeMetrics() {
|
|
@@ -175,13 +180,14 @@ function installConversation(
|
|
|
175
180
|
trusted?: boolean;
|
|
176
181
|
abortController?: AbortController;
|
|
177
182
|
},
|
|
178
|
-
):
|
|
183
|
+
): Conversation {
|
|
179
184
|
currentTrustClass = opts?.trusted === false ? "unknown" : "guardian";
|
|
180
185
|
currentConversation = asConversation({
|
|
181
186
|
graphMemory,
|
|
182
187
|
trustContext: undefined,
|
|
183
188
|
abortController: opts?.abortController ?? new AbortController(),
|
|
184
189
|
});
|
|
190
|
+
return currentConversation;
|
|
185
191
|
}
|
|
186
192
|
|
|
187
193
|
beforeEach(() => {
|
|
@@ -192,9 +198,95 @@ beforeEach(() => {
|
|
|
192
198
|
broadcastMessageMock.mockReset();
|
|
193
199
|
currentConversation = undefined;
|
|
194
200
|
currentTrustClass = "guardian";
|
|
201
|
+
setConfig("memory", { enabled: true, v3: { live: false } });
|
|
195
202
|
});
|
|
196
203
|
|
|
197
204
|
describe("user-prompt-submit hook (memory retrieval)", () => {
|
|
205
|
+
test("voice front door skips serial legacy retrieval", async () => {
|
|
206
|
+
const { memory, prepareMemoryMock, recordPkbQueryVectorsMock } =
|
|
207
|
+
makeFakeGraphMemory();
|
|
208
|
+
const conversation = installConversation(memory, { trusted: true });
|
|
209
|
+
conversation.currentCallSite = "voiceFrontDoor";
|
|
210
|
+
const ctx = makeHookCtx({ conversationId: "conv-voice" });
|
|
211
|
+
|
|
212
|
+
await userPromptSubmitMemoryRetrieval(ctx);
|
|
213
|
+
|
|
214
|
+
expect(prepareMemoryMock).not.toHaveBeenCalled();
|
|
215
|
+
expect(recordPkbQueryVectorsMock).toHaveBeenCalledWith(
|
|
216
|
+
undefined,
|
|
217
|
+
undefined,
|
|
218
|
+
);
|
|
219
|
+
expect(applyRuntimeInjectionsMock).toHaveBeenCalledTimes(1);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("voice escalation continuation routes legacy retrieval on the preceding user message", async () => {
|
|
223
|
+
const { memory, prepareMemoryMock } = makeFakeGraphMemory();
|
|
224
|
+
installConversation(memory, { trusted: true });
|
|
225
|
+
const latestMessages: Message[] = [
|
|
226
|
+
{
|
|
227
|
+
role: "user",
|
|
228
|
+
content: [
|
|
229
|
+
{ type: "text", text: "<info>\ncarried context\n</info>" },
|
|
230
|
+
{
|
|
231
|
+
type: "text",
|
|
232
|
+
text: "<turn_context>\nturn context\n</turn_context>",
|
|
233
|
+
},
|
|
234
|
+
{ type: "text", text: "What is my preferred editor?" },
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
role: "assistant",
|
|
239
|
+
content: [{ type: "text", text: "Let me check that." }],
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
role: "user",
|
|
243
|
+
content: [{ type: "text", text: "Continue the answer." }],
|
|
244
|
+
},
|
|
245
|
+
];
|
|
246
|
+
const ctx = makeHookCtx({
|
|
247
|
+
latestMessages,
|
|
248
|
+
isHiddenPrompt: true,
|
|
249
|
+
prompt: ESCALATION_CONTINUATION_CONTENT,
|
|
250
|
+
messageKind: VOICE_ESCALATION_CONTINUATION_MESSAGE_KIND,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
await userPromptSubmitMemoryRetrieval(ctx);
|
|
254
|
+
|
|
255
|
+
expect(prepareMemoryMock).toHaveBeenCalledTimes(1);
|
|
256
|
+
expect(prepareMemoryMock.mock.calls[0]?.[0]).toBe(latestMessages);
|
|
257
|
+
expect(prepareMemoryMock.mock.calls[0]?.[4]).toEqual([
|
|
258
|
+
{
|
|
259
|
+
role: "user",
|
|
260
|
+
content: [{ type: "text", text: "What is my preferred editor?" }],
|
|
261
|
+
},
|
|
262
|
+
]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("generic hidden prompts route legacy retrieval on their own content", async () => {
|
|
266
|
+
const { memory, prepareMemoryMock } = makeFakeGraphMemory();
|
|
267
|
+
installConversation(memory, { trusted: true });
|
|
268
|
+
const latestMessages: Message[] = [
|
|
269
|
+
{
|
|
270
|
+
role: "user",
|
|
271
|
+
content: [{ type: "text", text: "Visible caller message" }],
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
role: "user",
|
|
275
|
+
content: [{ type: "text", text: "Hidden prompt content" }],
|
|
276
|
+
},
|
|
277
|
+
];
|
|
278
|
+
const ctx = makeHookCtx({
|
|
279
|
+
latestMessages,
|
|
280
|
+
isHiddenPrompt: true,
|
|
281
|
+
prompt: "Hidden prompt content",
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
await userPromptSubmitMemoryRetrieval(ctx);
|
|
285
|
+
|
|
286
|
+
expect(prepareMemoryMock).toHaveBeenCalledTimes(1);
|
|
287
|
+
expect(prepareMemoryMock.mock.calls[0]?.[4]).toBe(latestMessages);
|
|
288
|
+
});
|
|
289
|
+
|
|
198
290
|
test("adopts the injected run messages when the actor is trusted", async () => {
|
|
199
291
|
const injected: Message[] = [
|
|
200
292
|
{ role: "user", content: [{ type: "text", text: "injected" }] },
|