@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
|
@@ -4024,6 +4024,59 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
4024
4024
|
expect(allText).not.toContain("private guardian-only context");
|
|
4025
4025
|
});
|
|
4026
4026
|
|
|
4027
|
+
// Slack builds provider history from rows rather than `this.messages`, so a
|
|
4028
|
+
// guardian card dropped only at `Conversation.loadFromDb` would still reach
|
|
4029
|
+
// the model here -- the channel the incident happened on.
|
|
4030
|
+
test("loadSlackChronologicalContext drops guardian card rows", () => {
|
|
4031
|
+
const caps: ChannelCapabilities = {
|
|
4032
|
+
channel: "slack",
|
|
4033
|
+
dashboardCapable: false,
|
|
4034
|
+
supportsDynamicUi: false,
|
|
4035
|
+
supportsVoiceInput: false,
|
|
4036
|
+
chatType: "channel",
|
|
4037
|
+
};
|
|
4038
|
+
const cardRow: MessageRow = {
|
|
4039
|
+
id: "m-card",
|
|
4040
|
+
conversationId: "conv-1",
|
|
4041
|
+
role: "assistant",
|
|
4042
|
+
content: [
|
|
4043
|
+
{
|
|
4044
|
+
type: "ui_surface",
|
|
4045
|
+
surfaceId: "tool-approval-req-1",
|
|
4046
|
+
surfaceType: "card",
|
|
4047
|
+
title: "Tool Approval",
|
|
4048
|
+
data: {},
|
|
4049
|
+
display: "inline",
|
|
4050
|
+
},
|
|
4051
|
+
{ type: "text", text: "Tool Approval card body" },
|
|
4052
|
+
],
|
|
4053
|
+
createdAt: 1700000035_000,
|
|
4054
|
+
metadata: null,
|
|
4055
|
+
clientMessageId: null,
|
|
4056
|
+
finalized: 1,
|
|
4057
|
+
} as MessageRow;
|
|
4058
|
+
const rows: MessageRow[] = [
|
|
4059
|
+
userRow({
|
|
4060
|
+
id: "asked",
|
|
4061
|
+
createdAt: 1700000030_000,
|
|
4062
|
+
text: "please run it",
|
|
4063
|
+
slackMeta: buildSlackMeta({ channelTs: T2, displayName: "carol" }),
|
|
4064
|
+
}),
|
|
4065
|
+
cardRow,
|
|
4066
|
+
];
|
|
4067
|
+
|
|
4068
|
+
const result = loadSlackChronologicalContext("conv-1", caps, {
|
|
4069
|
+
loader: () => rows,
|
|
4070
|
+
trustClass: "guardian",
|
|
4071
|
+
});
|
|
4072
|
+
|
|
4073
|
+
expect(result).not.toBeNull();
|
|
4074
|
+
const renderedText = JSON.stringify(result!.messages);
|
|
4075
|
+
expect(renderedText).toContain("please run it");
|
|
4076
|
+
expect(renderedText).not.toContain("Tool Approval card body");
|
|
4077
|
+
expect(renderedText).not.toContain("tool-approval-req-1");
|
|
4078
|
+
});
|
|
4079
|
+
|
|
4027
4080
|
test("loadSlackChronologicalContext preserves summary and filters by Slack watermark", () => {
|
|
4028
4081
|
const caps: ChannelCapabilities = {
|
|
4029
4082
|
channel: "slack",
|
|
@@ -62,6 +62,9 @@ mock.module("../persistence/conversation-crud.js", () => ({
|
|
|
62
62
|
getLastUserTimestampBefore: () => 0,
|
|
63
63
|
reserveMessage: mock(async () => ({ id: "msg-reserve" })),
|
|
64
64
|
updateMessageContent: mock(() => {}),
|
|
65
|
+
// The drain records the snapshot-stream anchor after each echo. Mocked so a
|
|
66
|
+
// sink that stamps seqs (the hub) cannot send the real recorder to SQLite.
|
|
67
|
+
recordConversationPersistedSeq: () => {},
|
|
65
68
|
}));
|
|
66
69
|
|
|
67
70
|
mock.module("../persistence/conversation-queries.js", () => ({
|
|
@@ -43,6 +43,7 @@ function makeContext(sent: AssistantEvent[] = []): Conversation & {
|
|
|
43
43
|
return asConversation({
|
|
44
44
|
conversationId: "conv-1",
|
|
45
45
|
sendToClient: (msg: AssistantEvent) => sent.push(msg),
|
|
46
|
+
emit: (msg: AssistantEvent) => sent.push(msg),
|
|
46
47
|
pendingSurfaceActions: new Map<string, { surfaceType: SurfaceType }>(),
|
|
47
48
|
lastSurfaceAction: new Map<
|
|
48
49
|
string,
|
|
@@ -69,6 +69,7 @@ function makeContext(
|
|
|
69
69
|
return {
|
|
70
70
|
conversationId,
|
|
71
71
|
sendToClient: (msg: AssistantEvent) => sent.push(msg),
|
|
72
|
+
emit: (msg: AssistantEvent) => sent.push(msg),
|
|
72
73
|
pendingSurfaceActions: new Map<string, { surfaceType: SurfaceType }>(),
|
|
73
74
|
lastSurfaceAction: new Map<
|
|
74
75
|
string,
|
|
@@ -29,7 +29,7 @@ function makeContext(
|
|
|
29
29
|
channelCapabilities:
|
|
30
30
|
options.channelCapabilities &&
|
|
31
31
|
mockChannelCapabilities(options.channelCapabilities),
|
|
32
|
-
|
|
32
|
+
emit: (msg) => sent.push(msg),
|
|
33
33
|
pendingSurfaceActions: new Map<string, { surfaceType: SurfaceType }>(),
|
|
34
34
|
lastSurfaceAction: new Map<
|
|
35
35
|
string,
|
|
@@ -57,7 +57,7 @@ import { asConversation } from "./helpers/mock-conversation.js";
|
|
|
57
57
|
function makeContext(sent: AssistantEvent[] = []): Conversation {
|
|
58
58
|
return asConversation({
|
|
59
59
|
conversationId: "conv-persist-1",
|
|
60
|
-
|
|
60
|
+
emit: (msg) => sent.push(msg),
|
|
61
61
|
pendingSurfaceActions: new Map<string, { surfaceType: SurfaceType }>(),
|
|
62
62
|
lastSurfaceAction: new Map<
|
|
63
63
|
string,
|
|
@@ -124,13 +124,17 @@ type EnqueueResult = { queued: boolean; requestId: string; rejected?: boolean };
|
|
|
124
124
|
|
|
125
125
|
function makeContext(
|
|
126
126
|
enqueueResult: EnqueueResult = { queued: false, requestId: "req-1" },
|
|
127
|
-
overrides: {
|
|
127
|
+
overrides: {
|
|
128
|
+
trustContext?: TrustContext;
|
|
129
|
+
queueDepth?: number;
|
|
130
|
+
emit?: (msg: AssistantEvent) => void;
|
|
131
|
+
} = {},
|
|
128
132
|
): Conversation & { enqueuedContents: string[] } {
|
|
129
133
|
const enqueuedContents: string[] = [];
|
|
130
134
|
return asConversation({
|
|
131
135
|
conversationId: CONVERSATION_ID,
|
|
132
136
|
trustContext: overrides.trustContext ?? GUARDIAN_TRUST,
|
|
133
|
-
|
|
137
|
+
emit: overrides.emit ?? (() => {}),
|
|
134
138
|
pendingSurfaceActions: new Map<string, { surfaceType: SurfaceType }>(),
|
|
135
139
|
lastSurfaceAction: new Map<
|
|
136
140
|
string,
|
|
@@ -885,11 +889,12 @@ describe("surface completion when the persisted write does not land", () => {
|
|
|
885
889
|
test("a ui_dismiss completion whose write throws stays retryable", async () => {
|
|
886
890
|
const surfaceId = "surface-dismiss-persist-throws-1";
|
|
887
891
|
seedSurfaceRow(surfaceId, "choice");
|
|
888
|
-
const ctx = makeContext();
|
|
889
892
|
const sentToClient: AssistantEvent[] = [];
|
|
890
|
-
ctx
|
|
891
|
-
|
|
892
|
-
|
|
893
|
+
const ctx = makeContext(undefined, {
|
|
894
|
+
emit: (msg) => {
|
|
895
|
+
sentToClient.push(msg);
|
|
896
|
+
},
|
|
897
|
+
});
|
|
893
898
|
seedAnsweredSurface(ctx, surfaceId);
|
|
894
899
|
persistError = new Error("database is locked");
|
|
895
900
|
|
|
@@ -919,11 +924,12 @@ describe("surface completion when the persisted write does not land", () => {
|
|
|
919
924
|
test("a ui_dismiss completion that lands clears the live state", async () => {
|
|
920
925
|
const surfaceId = "surface-dismiss-persist-lands-1";
|
|
921
926
|
seedSurfaceRow(surfaceId, "choice");
|
|
922
|
-
const ctx = makeContext();
|
|
923
927
|
const sentToClient: AssistantEvent[] = [];
|
|
924
|
-
ctx
|
|
925
|
-
|
|
926
|
-
|
|
928
|
+
const ctx = makeContext(undefined, {
|
|
929
|
+
emit: (msg) => {
|
|
930
|
+
sentToClient.push(msg);
|
|
931
|
+
},
|
|
932
|
+
});
|
|
927
933
|
seedAnsweredSurface(ctx, surfaceId);
|
|
928
934
|
|
|
929
935
|
const result = await surfaceProxyResolver(ctx, "ui_dismiss", {
|
|
@@ -942,11 +948,12 @@ describe("surface completion when the persisted write does not land", () => {
|
|
|
942
948
|
test("a ui_dismiss with no recorded action clears the live state even when the write throws", async () => {
|
|
943
949
|
const surfaceId = "surface-dismiss-no-action-1";
|
|
944
950
|
seedSurfaceRow(surfaceId, "card");
|
|
945
|
-
const ctx = makeContext();
|
|
946
951
|
const sentToClient: AssistantEvent[] = [];
|
|
947
|
-
ctx
|
|
948
|
-
|
|
949
|
-
|
|
952
|
+
const ctx = makeContext(undefined, {
|
|
953
|
+
emit: (msg) => {
|
|
954
|
+
sentToClient.push(msg);
|
|
955
|
+
},
|
|
956
|
+
});
|
|
950
957
|
ctx.pendingSurfaceActions.set(surfaceId, { surfaceType: "card" });
|
|
951
958
|
ctx.surfaceState.set(surfaceId, { surfaceType: "card", data: {} });
|
|
952
959
|
persistError = new Error("database is locked");
|
|
@@ -22,6 +22,7 @@ function makeQueuedContext(): Conversation {
|
|
|
22
22
|
return asConversation({
|
|
23
23
|
conversationId: CONV_ID,
|
|
24
24
|
sendToClient: () => {},
|
|
25
|
+
emit: () => {},
|
|
25
26
|
pendingSurfaceActions: new Map<string, { surfaceType: SurfaceType }>(),
|
|
26
27
|
lastSurfaceAction: new Map<
|
|
27
28
|
string,
|
|
@@ -65,6 +65,7 @@ function createMockContext(
|
|
|
65
65
|
})
|
|
66
66
|
: undefined,
|
|
67
67
|
sendToClient: (msg: AssistantEvent) => sentMessages.push(msg),
|
|
68
|
+
emit: (msg: AssistantEvent) => sentMessages.push(msg),
|
|
68
69
|
pendingSurfaceActions: new Map(),
|
|
69
70
|
lastSurfaceAction: new Map(),
|
|
70
71
|
surfaceState: new Map(),
|
|
@@ -53,6 +53,7 @@ function createMockContext(
|
|
|
53
53
|
})
|
|
54
54
|
: undefined,
|
|
55
55
|
sendToClient: (msg: AssistantEvent) => sentMessages.push(msg),
|
|
56
|
+
emit: (msg: AssistantEvent) => sentMessages.push(msg),
|
|
56
57
|
pendingSurfaceActions: new Map(),
|
|
57
58
|
lastSurfaceAction: new Map(),
|
|
58
59
|
surfaceState: new Map(),
|
|
@@ -26,7 +26,7 @@ function makeContext(opts?: { sent?: AssistantEvent[] }): Conversation & {
|
|
|
26
26
|
|
|
27
27
|
return asConversation({
|
|
28
28
|
conversationId: "test-session",
|
|
29
|
-
|
|
29
|
+
emit: (msg) => sent.push(msg),
|
|
30
30
|
pendingSurfaceActions: new Map<string, { surfaceType: SurfaceType }>(),
|
|
31
31
|
lastSurfaceAction: new Map<
|
|
32
32
|
string,
|
|
@@ -49,7 +49,7 @@ function makeContext(): Conversation & {
|
|
|
49
49
|
|
|
50
50
|
return asConversation({
|
|
51
51
|
conversationId: "test-convo",
|
|
52
|
-
|
|
52
|
+
emit: (msg) => sentMessages.push(msg),
|
|
53
53
|
pendingSurfaceActions: new Map<string, { surfaceType: SurfaceType }>(),
|
|
54
54
|
lastSurfaceAction: new Map<
|
|
55
55
|
string,
|
|
@@ -26,7 +26,7 @@ function makeContext(
|
|
|
26
26
|
conversationId: "session-1",
|
|
27
27
|
channelCapabilities:
|
|
28
28
|
channelCapabilities && mockChannelCapabilities(channelCapabilities),
|
|
29
|
-
|
|
29
|
+
emit: (msg) => sent.push(msg),
|
|
30
30
|
pendingSurfaceActions: new Map<string, { surfaceType: SurfaceType }>(),
|
|
31
31
|
lastSurfaceAction: new Map<
|
|
32
32
|
string,
|
|
@@ -94,7 +94,7 @@ function makeCtx(overrides: Partial<Conversation> = {}): Conversation {
|
|
|
94
94
|
workingDir: "/tmp/test",
|
|
95
95
|
getTurnActorPrincipalId: () => undefined,
|
|
96
96
|
abortController: null,
|
|
97
|
-
|
|
97
|
+
emit: mock(() => {}),
|
|
98
98
|
pendingSurfaceActions: new Map(),
|
|
99
99
|
lastSurfaceAction: new Map(),
|
|
100
100
|
surfaceState: new Map(),
|
|
@@ -101,7 +101,7 @@ function makeCtx(overrides: Partial<Conversation> = {}): Conversation {
|
|
|
101
101
|
workingDir: "/tmp/test",
|
|
102
102
|
getTurnActorPrincipalId: () => undefined,
|
|
103
103
|
abortController: null,
|
|
104
|
-
|
|
104
|
+
emit: mock(() => {}),
|
|
105
105
|
pendingSurfaceActions: new Map(),
|
|
106
106
|
lastSurfaceAction: new Map(),
|
|
107
107
|
surfaceState: new Map(),
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
* the Library and the per-conversation assets list converge on every surface.
|
|
4
4
|
* Read routes and rejected writes must stay silent.
|
|
5
5
|
*/
|
|
6
|
-
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { join } from "node:path";
|
|
8
6
|
import { afterAll, beforeEach, describe, expect, test } from "bun:test";
|
|
9
7
|
|
|
10
8
|
import type { AssistantEventEnvelope } from "../api/index.js";
|
|
@@ -24,9 +22,6 @@ import { resetDbForTesting } from "./db-test-helpers.js";
|
|
|
24
22
|
|
|
25
23
|
await initializeDb();
|
|
26
24
|
|
|
27
|
-
const workspaceDir = process.env.VELLUM_WORKSPACE_DIR!;
|
|
28
|
-
const notesDir = join(workspaceDir, "sync-notes");
|
|
29
|
-
|
|
30
25
|
const CONVERSATION_ID = "conv-doc-sync";
|
|
31
26
|
const OTHER_CONVERSATION_ID = "conv-doc-sync-other";
|
|
32
27
|
const MISSING_CONVERSATION_ID = "conv-doc-sync-missing";
|
|
@@ -110,15 +105,6 @@ async function saveViaRoute(
|
|
|
110
105
|
});
|
|
111
106
|
}
|
|
112
107
|
|
|
113
|
-
async function openWorkspaceFile(
|
|
114
|
-
path: string,
|
|
115
|
-
conversationId = CONVERSATION_ID,
|
|
116
|
-
): Promise<unknown> {
|
|
117
|
-
return await invoke("documentForWorkspaceFile", {
|
|
118
|
-
body: { path, conversationId },
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
108
|
async function linkViaRoute(
|
|
123
109
|
surfaceId: string,
|
|
124
110
|
conversationId: string,
|
|
@@ -136,12 +122,9 @@ beforeEach(() => {
|
|
|
136
122
|
db.run("DELETE FROM conversations");
|
|
137
123
|
createConversation({ id: CONVERSATION_ID });
|
|
138
124
|
createConversation({ id: OTHER_CONVERSATION_ID });
|
|
139
|
-
rmSync(notesDir, { recursive: true, force: true });
|
|
140
|
-
mkdirSync(notesDir, { recursive: true });
|
|
141
125
|
});
|
|
142
126
|
|
|
143
127
|
afterAll(() => {
|
|
144
|
-
rmSync(notesDir, { recursive: true, force: true });
|
|
145
128
|
resetDbForTesting();
|
|
146
129
|
});
|
|
147
130
|
|
|
@@ -220,67 +203,9 @@ describe("document write routes publish documents:list", () => {
|
|
|
220
203
|
expect(events).toHaveLength(1);
|
|
221
204
|
expect("originClientId" in events[0].message).toBe(false);
|
|
222
205
|
});
|
|
223
|
-
|
|
224
|
-
test("binding a workspace file publishes without an origin so the caller is not suppressed", async () => {
|
|
225
|
-
// `viewer-store.loadWorkspaceFileDocument` creates the row and invalidates
|
|
226
|
-
// nothing, so the client that opened the file is the one that most needs
|
|
227
|
-
// the invalidation.
|
|
228
|
-
writeFileSync(join(notesDir, "plan.md"), "# Plan");
|
|
229
|
-
|
|
230
|
-
const events = await captureSyncEvents(() =>
|
|
231
|
-
invoke("documentForWorkspaceFile", {
|
|
232
|
-
body: { path: "sync-notes/plan.md", conversationId: CONVERSATION_ID },
|
|
233
|
-
headers: { "x-vellum-client-id": "client-abc" },
|
|
234
|
-
}),
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
expect(events).toHaveLength(1);
|
|
238
|
-
expect("originClientId" in events[0].message).toBe(false);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
test("binding a workspace file to a new document publishes", async () => {
|
|
242
|
-
writeFileSync(join(notesDir, "plan.md"), "# Plan");
|
|
243
|
-
|
|
244
|
-
const events = await captureSyncEvents(() =>
|
|
245
|
-
openWorkspaceFile("sync-notes/plan.md"),
|
|
246
|
-
);
|
|
247
|
-
expectDocumentsChanged(events);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
test("reopening the file from another conversation publishes the new link", async () => {
|
|
251
|
-
writeFileSync(join(notesDir, "plan.md"), "# Plan");
|
|
252
|
-
await openWorkspaceFile("sync-notes/plan.md");
|
|
253
|
-
|
|
254
|
-
const events = await captureSyncEvents(() =>
|
|
255
|
-
openWorkspaceFile("sync-notes/plan.md", OTHER_CONVERSATION_ID),
|
|
256
|
-
);
|
|
257
|
-
expectDocumentsChanged(events);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
test("refreshing a document whose file changed on disk publishes", async () => {
|
|
261
|
-
const filePath = join(notesDir, "plan.md");
|
|
262
|
-
writeFileSync(filePath, "# Plan");
|
|
263
|
-
await openWorkspaceFile("sync-notes/plan.md");
|
|
264
|
-
writeFileSync(filePath, "# Plan\n\nedited outside the editor");
|
|
265
|
-
|
|
266
|
-
const events = await captureSyncEvents(() =>
|
|
267
|
-
openWorkspaceFile("sync-notes/plan.md"),
|
|
268
|
-
);
|
|
269
|
-
expectDocumentsChanged(events);
|
|
270
|
-
});
|
|
271
206
|
});
|
|
272
207
|
|
|
273
208
|
describe("document routes that change nothing the list shows stay silent", () => {
|
|
274
|
-
test("reopening an unchanged, already-linked file publishes nothing", async () => {
|
|
275
|
-
writeFileSync(join(notesDir, "plan.md"), "# Plan");
|
|
276
|
-
await openWorkspaceFile("sync-notes/plan.md");
|
|
277
|
-
|
|
278
|
-
const events = await captureSyncEvents(() =>
|
|
279
|
-
openWorkspaceFile("sync-notes/plan.md"),
|
|
280
|
-
);
|
|
281
|
-
expect(events).toEqual([]);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
209
|
test("a save rejected by validation publishes nothing", async () => {
|
|
285
210
|
const events = await captureSyncEvents(async () => {
|
|
286
211
|
await expect(saveViaRoute({ title: undefined })).rejects.toThrow(
|
|
@@ -10,10 +10,11 @@ import { tmpdir } from "node:os";
|
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
12
12
|
|
|
13
|
+
import { THRESHOLD_CHARS } from "../context/post-turn-tool-result-truncation.js";
|
|
13
14
|
import {
|
|
14
|
-
DEFAULT_READ_LINE_LIMIT,
|
|
15
15
|
FileSystemOps,
|
|
16
16
|
type PathPolicy,
|
|
17
|
+
READ_CHAR_BUDGET,
|
|
17
18
|
} from "../tools/shared/filesystem/file-ops-service.js";
|
|
18
19
|
import { sandboxPolicy } from "../tools/shared/filesystem/path-policy.js";
|
|
19
20
|
|
|
@@ -111,34 +112,28 @@ describe("FileSystemOps.readFileSafe", () => {
|
|
|
111
112
|
expect(result.error.code).toBe("PATH_OUT_OF_BOUNDS");
|
|
112
113
|
});
|
|
113
114
|
|
|
114
|
-
test("respects
|
|
115
|
+
test("respects startIndex and maxChars", async () => {
|
|
115
116
|
const dir = makeTempDir();
|
|
116
117
|
writeFileSync(join(dir, "lines.txt"), "a\nb\nc\nd\ne\n");
|
|
117
118
|
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
118
119
|
|
|
119
120
|
const result = await ops.readFileSafe({
|
|
120
121
|
path: "lines.txt",
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
startIndex: 2,
|
|
123
|
+
maxChars: 3,
|
|
123
124
|
});
|
|
124
125
|
expect(result.ok).toBe(true);
|
|
125
126
|
if (!result.ok) {
|
|
126
127
|
return;
|
|
127
128
|
}
|
|
128
|
-
// Assert on the numbered lines rather than bare letters: the truncation
|
|
129
|
-
// notice is prose, so a bare `toContain("d")` matches its wording.
|
|
130
129
|
const [body] = result.value.content.split("\n\n[Truncated:");
|
|
131
|
-
expect(body).
|
|
132
|
-
expect(body).toContain(" 3 c");
|
|
133
|
-
expect(body).not.toContain(" 1 a");
|
|
134
|
-
expect(body).not.toContain(" 4 d");
|
|
130
|
+
expect(body).toBe("b\nc");
|
|
135
131
|
});
|
|
136
132
|
|
|
137
|
-
test("caps an unbounded read at the
|
|
133
|
+
test("caps an unbounded read at the character budget and says so", async () => {
|
|
138
134
|
const dir = makeTempDir();
|
|
139
|
-
const total =
|
|
140
|
-
|
|
141
|
-
writeFileSync(join(dir, "big.txt"), lines.join("\n"));
|
|
135
|
+
const total = READ_CHAR_BUDGET + 500;
|
|
136
|
+
writeFileSync(join(dir, "big.txt"), "x".repeat(total));
|
|
142
137
|
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
143
138
|
|
|
144
139
|
const result = await ops.readFileSafe({ path: "big.txt" });
|
|
@@ -146,36 +141,74 @@ describe("FileSystemOps.readFileSafe", () => {
|
|
|
146
141
|
if (!result.ok) {
|
|
147
142
|
return;
|
|
148
143
|
}
|
|
149
|
-
const
|
|
150
|
-
expect(
|
|
151
|
-
expect(content).
|
|
152
|
-
|
|
153
|
-
`[Truncated: showing through line ${DEFAULT_READ_LINE_LIMIT} of ${total}. Read on with offset=${DEFAULT_READ_LINE_LIMIT + 1}`,
|
|
144
|
+
const [body] = result.value.content.split("\n\n[Truncated:");
|
|
145
|
+
expect(body).toHaveLength(READ_CHAR_BUDGET);
|
|
146
|
+
expect(result.value.content).toContain(
|
|
147
|
+
`[Truncated: characters 0-${READ_CHAR_BUDGET} of ${total}. Read on with start_index=${READ_CHAR_BUDGET}.]`,
|
|
154
148
|
);
|
|
155
149
|
});
|
|
156
150
|
|
|
157
|
-
test("
|
|
151
|
+
test("a maxChars above the budget is clamped to it", async () => {
|
|
158
152
|
const dir = makeTempDir();
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
(_, i) => `line${i + 1}`,
|
|
162
|
-
);
|
|
163
|
-
writeFileSync(join(dir, "big.txt"), lines.join("\n"));
|
|
153
|
+
const total = READ_CHAR_BUDGET + 500;
|
|
154
|
+
writeFileSync(join(dir, "big.txt"), "x".repeat(total));
|
|
164
155
|
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
165
156
|
|
|
166
157
|
const result = await ops.readFileSafe({
|
|
167
158
|
path: "big.txt",
|
|
168
|
-
|
|
159
|
+
maxChars: total,
|
|
169
160
|
});
|
|
170
161
|
expect(result.ok).toBe(true);
|
|
171
162
|
if (!result.ok) {
|
|
172
163
|
return;
|
|
173
164
|
}
|
|
174
|
-
|
|
175
|
-
expect(
|
|
165
|
+
const [body] = result.value.content.split("\n\n[Truncated:");
|
|
166
|
+
expect(body).toHaveLength(READ_CHAR_BUDGET);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("a maxChars below the budget is honored as given", async () => {
|
|
170
|
+
const dir = makeTempDir();
|
|
171
|
+
writeFileSync(join(dir, "big.txt"), "x".repeat(500));
|
|
172
|
+
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
173
|
+
|
|
174
|
+
const result = await ops.readFileSafe({ path: "big.txt", maxChars: 10 });
|
|
175
|
+
expect(result.ok).toBe(true);
|
|
176
|
+
if (!result.ok) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const [body] = result.value.content.split("\n\n[Truncated:");
|
|
180
|
+
expect(body).toHaveLength(10);
|
|
181
|
+
expect(result.value.content).toContain(
|
|
182
|
+
"[Truncated: characters 0-10 of 500. Read on with start_index=10.]",
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("paging with the offset from a notice returns the next window", async () => {
|
|
187
|
+
const dir = makeTempDir();
|
|
188
|
+
writeFileSync(join(dir, "big.txt"), "abcdefghij");
|
|
189
|
+
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
190
|
+
|
|
191
|
+
const first = await ops.readFileSafe({ path: "big.txt", maxChars: 4 });
|
|
192
|
+
expect(first.ok).toBe(true);
|
|
193
|
+
if (!first.ok) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
expect(first.value.content).toContain("start_index=4");
|
|
197
|
+
|
|
198
|
+
const second = await ops.readFileSafe({
|
|
199
|
+
path: "big.txt",
|
|
200
|
+
startIndex: 4,
|
|
201
|
+
maxChars: 4,
|
|
202
|
+
});
|
|
203
|
+
expect(second.ok).toBe(true);
|
|
204
|
+
if (!second.ok) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const [body] = second.value.content.split("\n\n[Truncated:");
|
|
208
|
+
expect(body).toBe("efgh");
|
|
176
209
|
});
|
|
177
210
|
|
|
178
|
-
test("a read that reaches the
|
|
211
|
+
test("a read that reaches the end carries no truncation notice", async () => {
|
|
179
212
|
const dir = makeTempDir();
|
|
180
213
|
writeFileSync(join(dir, "small.txt"), "a\nb\nc");
|
|
181
214
|
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
@@ -193,13 +226,113 @@ describe("FileSystemOps.readFileSafe", () => {
|
|
|
193
226
|
writeFileSync(join(dir, "small.txt"), "a\nb\nc");
|
|
194
227
|
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
195
228
|
|
|
196
|
-
const result = await ops.readFileSafe({
|
|
229
|
+
const result = await ops.readFileSafe({
|
|
230
|
+
path: "small.txt",
|
|
231
|
+
startIndex: 100,
|
|
232
|
+
});
|
|
197
233
|
expect(result.ok).toBe(true);
|
|
198
234
|
if (!result.ok) {
|
|
199
235
|
return;
|
|
200
236
|
}
|
|
201
237
|
expect(result.value.content).not.toContain("[Truncated:");
|
|
202
238
|
});
|
|
239
|
+
|
|
240
|
+
test("a window boundary inside a surrogate pair does not corrupt it", async () => {
|
|
241
|
+
const dir = makeTempDir();
|
|
242
|
+
// "ab" + a 2-unit emoji + "cd": a maxChars of 3 would split the pair.
|
|
243
|
+
writeFileSync(join(dir, "emoji.txt"), `ab\u{1F600}cd`);
|
|
244
|
+
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
245
|
+
|
|
246
|
+
const result = await ops.readFileSafe({ path: "emoji.txt", maxChars: 3 });
|
|
247
|
+
expect(result.ok).toBe(true);
|
|
248
|
+
if (!result.ok) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const [body] = result.value.content.split("\n\n[Truncated:");
|
|
252
|
+
expect(body).toBe("ab");
|
|
253
|
+
expect(body).not.toContain("\uFFFD");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("paging across a surrogate pair reassembles the whole file", async () => {
|
|
257
|
+
const dir = makeTempDir();
|
|
258
|
+
const original = `ab\u{1F600}cd`;
|
|
259
|
+
writeFileSync(join(dir, "emoji.txt"), original);
|
|
260
|
+
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
261
|
+
|
|
262
|
+
let assembled = "";
|
|
263
|
+
let startIndex = 0;
|
|
264
|
+
for (let i = 0; i < 10; i++) {
|
|
265
|
+
const result = await ops.readFileSafe({
|
|
266
|
+
path: "emoji.txt",
|
|
267
|
+
startIndex,
|
|
268
|
+
maxChars: 3,
|
|
269
|
+
});
|
|
270
|
+
expect(result.ok).toBe(true);
|
|
271
|
+
if (!result.ok) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const [body] = result.value.content.split("\n\n[Truncated:");
|
|
275
|
+
assembled += body;
|
|
276
|
+
const match = /start_index=(\d+)/.exec(result.value.content);
|
|
277
|
+
if (match?.[1] === undefined) {
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
startIndex = Number(match[1]);
|
|
281
|
+
}
|
|
282
|
+
expect(assembled).toBe(original);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("a lone-low-surrogate startIndex backs up onto the whole pair", async () => {
|
|
286
|
+
const dir = makeTempDir();
|
|
287
|
+
writeFileSync(join(dir, "emoji.txt"), `ab\u{1F600}cd`);
|
|
288
|
+
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
289
|
+
|
|
290
|
+
// Index 3 (0-indexed) is the low half of the emoji.
|
|
291
|
+
const result = await ops.readFileSafe({
|
|
292
|
+
path: "emoji.txt",
|
|
293
|
+
startIndex: 3,
|
|
294
|
+
maxChars: 2,
|
|
295
|
+
});
|
|
296
|
+
expect(result.ok).toBe(true);
|
|
297
|
+
if (!result.ok) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const [body] = result.value.content.split("\n\n[Truncated:");
|
|
301
|
+
expect(body).toBe(`\u{1F600}`);
|
|
302
|
+
expect(body).not.toContain("\uFFFD");
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("legacy offset/limit are rejected rather than silently ignored", async () => {
|
|
306
|
+
const { fileReadTool } = await import("../tools/filesystem/read.js");
|
|
307
|
+
const dir = makeTempDir();
|
|
308
|
+
writeFileSync(join(dir, "big.txt"), "x".repeat(500));
|
|
309
|
+
|
|
310
|
+
const result = await fileReadTool.execute(
|
|
311
|
+
{ path: join(dir, "big.txt"), offset: 2001, limit: 2000 },
|
|
312
|
+
{ workingDir: dir } as never,
|
|
313
|
+
);
|
|
314
|
+
expect(result.isError).toBe(true);
|
|
315
|
+
expect(result.content).toContain("no longer takes");
|
|
316
|
+
expect(result.content).toContain("start_index");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("legacy fields alongside current ones do not trigger the guard", async () => {
|
|
320
|
+
const { fileReadTool } = await import("../tools/filesystem/read.js");
|
|
321
|
+
const dir = makeTempDir();
|
|
322
|
+
writeFileSync(join(dir, "big.txt"), "abcdefghij");
|
|
323
|
+
|
|
324
|
+
const result = await fileReadTool.execute(
|
|
325
|
+
{ path: join(dir, "big.txt"), offset: 1, max_chars: 4 },
|
|
326
|
+
{ workingDir: dir } as never,
|
|
327
|
+
);
|
|
328
|
+
expect(result.isError).toBe(false);
|
|
329
|
+
const [body] = result.content.split("\n\n[Truncated:");
|
|
330
|
+
expect(body).toBe("abcd");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("the read budget stays under the tool-result spool threshold", () => {
|
|
334
|
+
expect(READ_CHAR_BUDGET).toBeLessThan(THRESHOLD_CHARS);
|
|
335
|
+
});
|
|
203
336
|
});
|
|
204
337
|
|
|
205
338
|
// ---------------------------------------------------------------------------
|