@vellumai/assistant 0.8.2 → 0.8.3
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/ARCHITECTURE.md +11 -12
- package/docker-entrypoint.sh +13 -1
- package/docker-init-apt-root.sh +79 -6
- package/openapi.yaml +336 -21
- package/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
- package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
- package/src/__tests__/config-get-vision-flag.test.ts +136 -0
- package/src/__tests__/config-loader-backfill.test.ts +115 -18
- package/src/__tests__/context-token-estimator.test.ts +30 -65
- package/src/__tests__/conversation-agent-loop.test.ts +57 -1
- package/src/__tests__/conversation-media-retry.test.ts +19 -8
- package/src/__tests__/conversation-runtime-assembly.test.ts +26 -4
- package/src/__tests__/date-context.test.ts +45 -0
- package/src/__tests__/external-plugin-loader.test.ts +91 -19
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
- package/src/__tests__/guardian-dispatch.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +24 -164
- package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
- package/src/__tests__/host-app-control-proxy.test.ts +241 -0
- package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
- package/src/__tests__/injector-background-turn.test.ts +153 -0
- package/src/__tests__/injector-chain.test.ts +5 -0
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
- package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
- package/src/__tests__/llm-catalog-parity.test.ts +3 -0
- package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
- package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +255 -2
- package/src/__tests__/managed-profile-guard.test.ts +10 -0
- package/src/__tests__/notification-decision-fallback.test.ts +0 -91
- package/src/__tests__/notification-decision-strategy.test.ts +14 -31
- package/src/__tests__/notification-deep-link.test.ts +15 -0
- package/src/__tests__/notification-guardian-path.test.ts +1 -2
- package/src/__tests__/notification-platform-adapter.test.ts +5 -4
- package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
- package/src/__tests__/openai-provider.test.ts +218 -3
- package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
- package/src/__tests__/openrouter-provider-only.test.ts +51 -3
- package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
- package/src/__tests__/platform-proxy-context.test.ts +6 -1
- package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
- package/src/__tests__/plugin-types.test.ts +2 -2
- package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
- package/src/__tests__/system-prompt.test.ts +6 -73
- package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
- package/src/a2a/__tests__/agent-card.test.ts +98 -0
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
- package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
- package/src/a2a/__tests__/task-store.test.ts +246 -0
- package/src/a2a/agent-card.ts +58 -0
- package/src/a2a/feature-gate.ts +8 -0
- package/src/a2a/protocol-constants.ts +21 -0
- package/src/a2a/protocol-errors.ts +50 -0
- package/src/a2a/protocol-types.ts +162 -0
- package/src/a2a/task-store.ts +168 -0
- package/src/agent/loop.ts +167 -18
- package/src/channels/config.ts +9 -0
- package/src/channels/types.ts +14 -0
- package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
- package/src/cli/commands/__tests__/schedules.test.ts +469 -0
- package/src/cli/commands/notifications.ts +65 -35
- package/src/cli/commands/plugins.ts +67 -0
- package/src/cli/commands/schedules.ts +297 -5
- package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
- package/src/cli/lib/install-from-github.ts +8 -9
- package/src/cli/lib/search-plugins.ts +163 -0
- package/src/cli/program.ts +14 -0
- package/src/config/assistant-feature-flags.ts +24 -54
- package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/call-site-defaults.ts +105 -0
- package/src/config/feature-flag-registry.json +21 -29
- package/src/config/llm-resolver.ts +52 -1
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
- package/src/config/schemas/channels.ts +9 -0
- package/src/config/schemas/conversations.ts +10 -0
- package/src/config/schemas/heartbeat.ts +14 -0
- package/src/config/schemas/llm.ts +1 -3
- package/src/config/schemas/memory-retrospective.ts +1 -1
- package/src/config/schemas/memory-v2.ts +4 -4
- package/src/config/schemas/memory.ts +3 -1
- package/src/config/seed-inference-profiles.ts +99 -29
- package/src/context/compactor.ts +72 -12
- package/src/context/token-estimator.ts +32 -34
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
- package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
- package/src/daemon/conversation-agent-loop.ts +29 -2
- package/src/daemon/conversation-runtime-assembly.ts +9 -0
- package/src/daemon/conversation.ts +0 -7
- package/src/daemon/date-context.ts +40 -0
- package/src/daemon/guardian-action-generators.ts +1 -125
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
- package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
- package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
- package/src/daemon/handlers/config-a2a.ts +289 -0
- package/src/daemon/handlers/conversations.ts +1 -0
- package/src/daemon/host-app-control-proxy.ts +69 -18
- package/src/daemon/host-proxy-preactivation.ts +85 -18
- package/src/daemon/lifecycle.ts +49 -61
- package/src/daemon/memory-v2-startup.ts +49 -13
- package/src/daemon/message-types/notifications.ts +21 -0
- package/src/daemon/pkb-reminder-builder.test.ts +10 -53
- package/src/daemon/pkb-reminder-builder.ts +4 -19
- package/src/daemon/process-message.ts +3 -0
- package/src/daemon/skill-memory-refresh.ts +5 -1
- package/src/daemon/wake-target-adapter.ts +2 -0
- package/src/export/__tests__/transcript-formatter.test.ts +121 -0
- package/src/export/transcript-formatter.ts +54 -20
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -0
- package/src/heartbeat/heartbeat-service.ts +34 -191
- package/src/home/__tests__/feed-types.test.ts +40 -0
- package/src/home/feed-types.ts +14 -2
- package/src/ipc/cli-client.ts +147 -45
- package/src/memory/__tests__/conversation-queries.test.ts +220 -0
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
- package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
- package/src/memory/conversation-queries.ts +87 -1
- package/src/memory/conversation-title-service.ts +26 -4
- package/src/memory/db-init.ts +6 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
- package/src/memory/graph/conversation-graph-memory.ts +18 -6
- package/src/memory/graph/tools.ts +6 -37
- package/src/memory/invite-store.ts +53 -0
- package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
- package/src/memory/llm-request-log-store.ts +92 -1
- package/src/memory/memory-retrospective-enqueue.ts +1 -20
- package/src/memory/memory-retrospective-job.ts +33 -6
- package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
- package/src/memory/migrations/251-a2a-tasks.ts +49 -0
- package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/a2a.ts +15 -0
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/inference.ts +2 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
- package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
- package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
- package/src/memory/v2/__tests__/injection.test.ts +190 -3
- package/src/memory/v2/__tests__/static-context.test.ts +12 -1
- package/src/memory/v2/activation-store.ts +14 -16
- package/src/memory/v2/cli-command-content.ts +19 -0
- package/src/memory/v2/cli-command-store.ts +304 -0
- package/src/memory/v2/frontmatter-sweep.ts +7 -1
- package/src/memory/v2/injection.ts +49 -20
- package/src/memory/v2/page-index.ts +38 -13
- package/src/memory/v2/static-context.ts +4 -4
- package/src/memory/v2/types.ts +23 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
- package/src/messaging/providers/a2a/deliver.ts +156 -0
- package/src/messaging/providers/gmail/client.ts +9 -2
- package/src/messaging/providers/index.ts +11 -2
- package/src/notifications/__tests__/broadcaster.test.ts +203 -0
- package/src/notifications/__tests__/decision-engine.test.ts +283 -0
- package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
- package/src/notifications/adapters/macos.ts +12 -2
- package/src/notifications/broadcaster.ts +29 -4
- package/src/notifications/copy-composer.ts +17 -64
- package/src/notifications/decision-engine.ts +111 -44
- package/src/notifications/deterministic-checks.ts +96 -0
- package/src/notifications/emit-signal.ts +1 -0
- package/src/notifications/home-feed-side-effect.ts +85 -6
- package/src/notifications/signal.ts +0 -4
- package/src/notifications/types.ts +8 -0
- package/src/oauth/platform-connection.test.ts +43 -3
- package/src/oauth/platform-connection.ts +13 -4
- package/src/plugins/defaults/injectors.ts +38 -19
- package/src/plugins/external-plugin-loader.ts +82 -10
- package/src/plugins/types.ts +16 -7
- package/src/prompts/__tests__/system-prompt.test.ts +6 -51
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
- package/src/prompts/system-prompt.ts +0 -8
- package/src/prompts/templates/BOOTSTRAP.md +5 -5
- package/src/prompts/templates/system-sections.ts +0 -9
- package/src/providers/__tests__/inference.test.ts +2 -0
- package/src/providers/call-site-routing.ts +24 -6
- package/src/providers/connection-resolution.ts +63 -13
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
- package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
- package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
- package/src/providers/inference/adapter-factory.ts +9 -20
- package/src/providers/inference/auth.ts +12 -0
- package/src/providers/inference/backfill.ts +14 -1
- package/src/providers/inference/connections.ts +85 -5
- package/src/providers/inference/resolve-auth.ts +2 -0
- package/src/providers/model-catalog.ts +199 -244
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
- package/src/providers/openai/chat-completions-provider.ts +159 -6
- package/src/providers/openrouter/client.ts +42 -4
- package/src/providers/platform-proxy/constants.ts +3 -4
- package/src/providers/provider-catalog-visibility.ts +3 -1
- package/src/providers/provider-send-message.ts +27 -12
- package/src/providers/registry.ts +30 -1
- package/src/runtime/agent-wake.ts +61 -1
- package/src/runtime/auth/route-policy.ts +13 -0
- package/src/runtime/http-server.ts +7 -16
- package/src/runtime/http-types.ts +0 -47
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +66 -4
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
- package/src/runtime/routes/channel-availability-routes.ts +5 -0
- package/src/runtime/routes/consolidation-routes.ts +100 -0
- package/src/runtime/routes/conversation-query-routes.ts +70 -11
- package/src/runtime/routes/conversation-routes.ts +7 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +134 -1
- package/src/runtime/routes/integrations/a2a.ts +235 -0
- package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
- package/src/runtime/routes/subagents-routes.ts +41 -0
- package/src/subagent/manager.ts +2 -0
- package/src/tools/memory/register.ts +1 -9
- package/src/tools/registry.ts +2 -2
- package/src/tools/types.ts +37 -2
- package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
- package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
- package/src/runtime/guardian-action-conversation-turn.ts +0 -99
|
@@ -42,6 +42,7 @@ mock.module("../runtime/pending-interactions.js", () => ({
|
|
|
42
42
|
const {
|
|
43
43
|
HostAppControlProxy,
|
|
44
44
|
_getActiveAppControlSession,
|
|
45
|
+
_getConfirmedAppControlSession,
|
|
45
46
|
_resetActiveAppControlSession,
|
|
46
47
|
_setActiveAppControlSession,
|
|
47
48
|
} = await import("../daemon/host-app-control-proxy.js");
|
|
@@ -826,6 +827,246 @@ describe("HostAppControlProxy", () => {
|
|
|
826
827
|
proxy.dispose();
|
|
827
828
|
});
|
|
828
829
|
|
|
830
|
+
test("both-succeed older-arrives-last does not overwrite newer confirmed session", async () => {
|
|
831
|
+
// Two same-conversation starts both succeed, but the older one's
|
|
832
|
+
// `running` response arrives AFTER the newer one's. With a
|
|
833
|
+
// conversation-only ownership guard, the older response would
|
|
834
|
+
// overwrite the newer confirmed session — a latent bug that surfaces
|
|
835
|
+
// on a subsequent rollback. Verify the confirmed pointer stays on the
|
|
836
|
+
// newer session (C), and that a later rollback restores to C, not A.
|
|
837
|
+
const proxy = new HostAppControlProxy("conv-1");
|
|
838
|
+
const ctrl = new AbortController();
|
|
839
|
+
|
|
840
|
+
const pA = proxy.request(
|
|
841
|
+
"app_control_start",
|
|
842
|
+
{ tool: "start", app: "com.example.a" },
|
|
843
|
+
"conv-1",
|
|
844
|
+
ctrl.signal,
|
|
845
|
+
);
|
|
846
|
+
const reqIdA = (sentMessages[0] as Record<string, unknown>)
|
|
847
|
+
.requestId as string;
|
|
848
|
+
|
|
849
|
+
sentMessages.length = 0;
|
|
850
|
+
const pC = proxy.request(
|
|
851
|
+
"app_control_start",
|
|
852
|
+
{ tool: "start", app: "com.example.c" },
|
|
853
|
+
"conv-1",
|
|
854
|
+
ctrl.signal,
|
|
855
|
+
);
|
|
856
|
+
const reqIdC = (sentMessages[0] as Record<string, unknown>)
|
|
857
|
+
.requestId as string;
|
|
858
|
+
expect(_getActiveAppControlSession()?.app).toBe("com.example.c");
|
|
859
|
+
|
|
860
|
+
// C succeeds first → active = C, confirmed = C.
|
|
861
|
+
proxy.resolve(reqIdC, payload({ pngBase64: PNG_A }));
|
|
862
|
+
await pC;
|
|
863
|
+
expect(_getActiveAppControlSession()?.app).toBe("com.example.c");
|
|
864
|
+
expect(_getConfirmedAppControlSession()?.app).toBe("com.example.c");
|
|
865
|
+
|
|
866
|
+
// A succeeds second (older arrives last). The conversation-ownership
|
|
867
|
+
// check passes for A, but A is no longer the live optimistic write
|
|
868
|
+
// and a session is already confirmed — must NOT overwrite confirmed.
|
|
869
|
+
proxy.resolve(reqIdA, payload({ pngBase64: PNG_B }));
|
|
870
|
+
await pA;
|
|
871
|
+
expect(_getConfirmedAppControlSession()?.app).toBe("com.example.c");
|
|
872
|
+
expect(_getActiveAppControlSession()?.app).toBe("com.example.c");
|
|
873
|
+
|
|
874
|
+
// A later restart D that fails must roll back to C (the true
|
|
875
|
+
// confirmed), not to A — guards against the latent-bug case.
|
|
876
|
+
sentMessages.length = 0;
|
|
877
|
+
const pD = proxy.request(
|
|
878
|
+
"app_control_start",
|
|
879
|
+
{ tool: "start", app: "com.example.d" },
|
|
880
|
+
"conv-1",
|
|
881
|
+
ctrl.signal,
|
|
882
|
+
);
|
|
883
|
+
const reqIdD = (sentMessages[0] as Record<string, unknown>)
|
|
884
|
+
.requestId as string;
|
|
885
|
+
proxy.resolve(reqIdD, payload({ state: "missing" }));
|
|
886
|
+
await pD;
|
|
887
|
+
|
|
888
|
+
const session = _getActiveAppControlSession();
|
|
889
|
+
expect(session?.conversationId).toBe("conv-1");
|
|
890
|
+
expect(session?.app).toBe("com.example.c");
|
|
891
|
+
|
|
892
|
+
proxy.dispose();
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
test("rollback after confirmed-A,dispatch-B,dispatch-C,B-confirms,C-fails restores B (Codex P2)", async () => {
|
|
896
|
+
// A is confirmed, then B and C are dispatched. B's `running` arrives
|
|
897
|
+
// while C is the live write; under the dispatch-counter rule that
|
|
898
|
+
// confirmation must promote because B is newer than A. A subsequent
|
|
899
|
+
// failure of C must then roll back to B, not back to A.
|
|
900
|
+
const proxy = new HostAppControlProxy("conv-1");
|
|
901
|
+
const ctrl = new AbortController();
|
|
902
|
+
|
|
903
|
+
const pA = proxy.request(
|
|
904
|
+
"app_control_start",
|
|
905
|
+
{ tool: "start", app: "com.example.a" },
|
|
906
|
+
"conv-1",
|
|
907
|
+
ctrl.signal,
|
|
908
|
+
);
|
|
909
|
+
const reqIdA = (sentMessages[0] as Record<string, unknown>)
|
|
910
|
+
.requestId as string;
|
|
911
|
+
proxy.resolve(reqIdA, payload({ pngBase64: PNG_A }));
|
|
912
|
+
await pA;
|
|
913
|
+
expect(_getConfirmedAppControlSession()?.app).toBe("com.example.a");
|
|
914
|
+
|
|
915
|
+
sentMessages.length = 0;
|
|
916
|
+
const pB = proxy.request(
|
|
917
|
+
"app_control_start",
|
|
918
|
+
{ tool: "start", app: "com.example.b" },
|
|
919
|
+
"conv-1",
|
|
920
|
+
ctrl.signal,
|
|
921
|
+
);
|
|
922
|
+
const reqIdB = (sentMessages[0] as Record<string, unknown>)
|
|
923
|
+
.requestId as string;
|
|
924
|
+
|
|
925
|
+
sentMessages.length = 0;
|
|
926
|
+
const pC = proxy.request(
|
|
927
|
+
"app_control_start",
|
|
928
|
+
{ tool: "start", app: "com.example.c" },
|
|
929
|
+
"conv-1",
|
|
930
|
+
ctrl.signal,
|
|
931
|
+
);
|
|
932
|
+
const reqIdC = (sentMessages[0] as Record<string, unknown>)
|
|
933
|
+
.requestId as string;
|
|
934
|
+
expect(_getActiveAppControlSession()?.app).toBe("com.example.c");
|
|
935
|
+
|
|
936
|
+
// B confirms (newer than A): confirmed should move to B even though
|
|
937
|
+
// C is the live optimistic write.
|
|
938
|
+
proxy.resolve(reqIdB, payload({ pngBase64: PNG_B }));
|
|
939
|
+
await pB;
|
|
940
|
+
expect(_getConfirmedAppControlSession()?.app).toBe("com.example.b");
|
|
941
|
+
expect(_getActiveAppControlSession()?.app).toBe("com.example.c");
|
|
942
|
+
|
|
943
|
+
// C fails → rollback restores to confirmed (B), not A.
|
|
944
|
+
proxy.resolve(reqIdC, payload({ state: "missing" }));
|
|
945
|
+
await pC;
|
|
946
|
+
const session = _getActiveAppControlSession();
|
|
947
|
+
expect(session?.conversationId).toBe("conv-1");
|
|
948
|
+
expect(session?.app).toBe("com.example.b");
|
|
949
|
+
|
|
950
|
+
proxy.dispose();
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
test("three-overlapping-starts A-confirms,B-confirms,C-fails restores B (Devin P2)", async () => {
|
|
954
|
+
// A, B, C dispatched in order with no prior confirmed session.
|
|
955
|
+
// A confirms first (confirmed = A), B confirms second (confirmed
|
|
956
|
+
// must move forward to B because B is newer), C fails → rollback
|
|
957
|
+
// restores to B, not A.
|
|
958
|
+
const proxy = new HostAppControlProxy("conv-1");
|
|
959
|
+
const ctrl = new AbortController();
|
|
960
|
+
|
|
961
|
+
const pA = proxy.request(
|
|
962
|
+
"app_control_start",
|
|
963
|
+
{ tool: "start", app: "com.example.a" },
|
|
964
|
+
"conv-1",
|
|
965
|
+
ctrl.signal,
|
|
966
|
+
);
|
|
967
|
+
const reqIdA = (sentMessages[0] as Record<string, unknown>)
|
|
968
|
+
.requestId as string;
|
|
969
|
+
|
|
970
|
+
sentMessages.length = 0;
|
|
971
|
+
const pB = proxy.request(
|
|
972
|
+
"app_control_start",
|
|
973
|
+
{ tool: "start", app: "com.example.b" },
|
|
974
|
+
"conv-1",
|
|
975
|
+
ctrl.signal,
|
|
976
|
+
);
|
|
977
|
+
const reqIdB = (sentMessages[0] as Record<string, unknown>)
|
|
978
|
+
.requestId as string;
|
|
979
|
+
|
|
980
|
+
sentMessages.length = 0;
|
|
981
|
+
const pC = proxy.request(
|
|
982
|
+
"app_control_start",
|
|
983
|
+
{ tool: "start", app: "com.example.c" },
|
|
984
|
+
"conv-1",
|
|
985
|
+
ctrl.signal,
|
|
986
|
+
);
|
|
987
|
+
const reqIdC = (sentMessages[0] as Record<string, unknown>)
|
|
988
|
+
.requestId as string;
|
|
989
|
+
expect(_getActiveAppControlSession()?.app).toBe("com.example.c");
|
|
990
|
+
|
|
991
|
+
proxy.resolve(reqIdA, payload({ pngBase64: PNG_A }));
|
|
992
|
+
await pA;
|
|
993
|
+
expect(_getConfirmedAppControlSession()?.app).toBe("com.example.a");
|
|
994
|
+
|
|
995
|
+
proxy.resolve(reqIdB, payload({ pngBase64: PNG_B }));
|
|
996
|
+
await pB;
|
|
997
|
+
expect(_getConfirmedAppControlSession()?.app).toBe("com.example.b");
|
|
998
|
+
expect(_getActiveAppControlSession()?.app).toBe("com.example.c");
|
|
999
|
+
|
|
1000
|
+
proxy.resolve(reqIdC, payload({ state: "missing" }));
|
|
1001
|
+
await pC;
|
|
1002
|
+
const session = _getActiveAppControlSession();
|
|
1003
|
+
expect(session?.conversationId).toBe("conv-1");
|
|
1004
|
+
expect(session?.app).toBe("com.example.b");
|
|
1005
|
+
|
|
1006
|
+
proxy.dispose();
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
test("confirmed-A,dispatch-B,dispatch-C,C-fails-first,B-confirms syncs active to B", async () => {
|
|
1010
|
+
// A confirmed, B and C dispatched, C returns `missing` first →
|
|
1011
|
+
// rollback restores active to A. Then B returns `running` →
|
|
1012
|
+
// confirmed advances to B; active must advance alongside it,
|
|
1013
|
+
// otherwise the tool reports B started while
|
|
1014
|
+
// app_control_observe/actions for B target A.
|
|
1015
|
+
const proxy = new HostAppControlProxy("conv-1");
|
|
1016
|
+
const ctrl = new AbortController();
|
|
1017
|
+
|
|
1018
|
+
const pA = proxy.request(
|
|
1019
|
+
"app_control_start",
|
|
1020
|
+
{ tool: "start", app: "com.example.a" },
|
|
1021
|
+
"conv-1",
|
|
1022
|
+
ctrl.signal,
|
|
1023
|
+
);
|
|
1024
|
+
const reqIdA = (sentMessages[0] as Record<string, unknown>)
|
|
1025
|
+
.requestId as string;
|
|
1026
|
+
proxy.resolve(reqIdA, payload({ pngBase64: PNG_A }));
|
|
1027
|
+
await pA;
|
|
1028
|
+
expect(_getConfirmedAppControlSession()?.app).toBe("com.example.a");
|
|
1029
|
+
|
|
1030
|
+
sentMessages.length = 0;
|
|
1031
|
+
const pB = proxy.request(
|
|
1032
|
+
"app_control_start",
|
|
1033
|
+
{ tool: "start", app: "com.example.b" },
|
|
1034
|
+
"conv-1",
|
|
1035
|
+
ctrl.signal,
|
|
1036
|
+
);
|
|
1037
|
+
const reqIdB = (sentMessages[0] as Record<string, unknown>)
|
|
1038
|
+
.requestId as string;
|
|
1039
|
+
|
|
1040
|
+
sentMessages.length = 0;
|
|
1041
|
+
const pC = proxy.request(
|
|
1042
|
+
"app_control_start",
|
|
1043
|
+
{ tool: "start", app: "com.example.c" },
|
|
1044
|
+
"conv-1",
|
|
1045
|
+
ctrl.signal,
|
|
1046
|
+
);
|
|
1047
|
+
const reqIdC = (sentMessages[0] as Record<string, unknown>)
|
|
1048
|
+
.requestId as string;
|
|
1049
|
+
expect(_getActiveAppControlSession()?.app).toBe("com.example.c");
|
|
1050
|
+
|
|
1051
|
+
// C fails first → rollback restores active to confirmed (A).
|
|
1052
|
+
proxy.resolve(reqIdC, payload({ state: "missing" }));
|
|
1053
|
+
await pC;
|
|
1054
|
+
expect(_getActiveAppControlSession()?.app).toBe("com.example.a");
|
|
1055
|
+
expect(_getConfirmedAppControlSession()?.app).toBe("com.example.a");
|
|
1056
|
+
|
|
1057
|
+
// B confirms after C's rollback. Confirmed advances to B AND active
|
|
1058
|
+
// must advance to B too — otherwise non-start tool calls against B
|
|
1059
|
+
// would be rejected because active still targets A.
|
|
1060
|
+
proxy.resolve(reqIdB, payload({ pngBase64: PNG_B }));
|
|
1061
|
+
await pB;
|
|
1062
|
+
expect(_getConfirmedAppControlSession()?.app).toBe("com.example.b");
|
|
1063
|
+
const session = _getActiveAppControlSession();
|
|
1064
|
+
expect(session?.conversationId).toBe("conv-1");
|
|
1065
|
+
expect(session?.app).toBe("com.example.b");
|
|
1066
|
+
|
|
1067
|
+
proxy.dispose();
|
|
1068
|
+
});
|
|
1069
|
+
|
|
829
1070
|
test("first-start failure releases the lock (no prior session to restore)", async () => {
|
|
830
1071
|
const proxy = new HostAppControlProxy("conv-1");
|
|
831
1072
|
const ctrl = new AbortController();
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for `preactivateHostProxySkills
|
|
3
|
-
* in `host-proxy-preactivation.ts`.
|
|
2
|
+
* Tests for `evaluateHostProxyAttachment`, `preactivateHostProxySkills`, and
|
|
3
|
+
* `shouldAttachHostProxyForCapability` in `host-proxy-preactivation.ts`.
|
|
4
4
|
*
|
|
5
5
|
* Covers:
|
|
6
6
|
* - Source interface natively supports capability → preactivate (regression)
|
|
7
7
|
* - Source interface doesn't support but capable client connected → preactivate
|
|
8
8
|
* - Source interface doesn't support and no capable client → don't preactivate
|
|
9
9
|
* - chrome-extension source + capable client connected → don't preactivate (security boundary)
|
|
10
|
+
* - `evaluateHostProxyAttachment` returns the correct `reason` for each branch
|
|
11
|
+
* - `preactivateHostProxySkills` emits one structured log line per call with
|
|
12
|
+
* conversationId, sourceInterface, per-capability decisions, and final
|
|
13
|
+
* preactivatedSkillIds (used by ATL-609-class silent-gate diagnosis)
|
|
10
14
|
*/
|
|
11
15
|
|
|
12
16
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
@@ -26,31 +30,74 @@ mock.module("../runtime/assistant-event-hub.js", () => ({
|
|
|
26
30
|
broadcastMessage: () => {},
|
|
27
31
|
}));
|
|
28
32
|
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Mock the logger so we can assert on the structured info call.
|
|
35
|
+
// `info` calls are pushed into `loggedInfoCalls` for inspection.
|
|
36
|
+
// `child()` is exposed for callers that wrap the logger that way.
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
interface LoggedCall {
|
|
40
|
+
fields: Record<string, unknown>;
|
|
41
|
+
message: string;
|
|
42
|
+
}
|
|
43
|
+
const loggedInfoCalls: LoggedCall[] = [];
|
|
44
|
+
function captureInfo(fields: unknown, message: unknown) {
|
|
45
|
+
loggedInfoCalls.push({
|
|
46
|
+
fields: fields as Record<string, unknown>,
|
|
47
|
+
message: message as string,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
29
51
|
mock.module("../util/logger.js", () => ({
|
|
30
|
-
getLogger: () =>
|
|
31
|
-
|
|
52
|
+
getLogger: () => ({
|
|
53
|
+
info: captureInfo,
|
|
54
|
+
warn: () => {},
|
|
55
|
+
error: () => {},
|
|
56
|
+
debug: () => {},
|
|
57
|
+
trace: () => {},
|
|
58
|
+
fatal: () => {},
|
|
59
|
+
child: () => ({
|
|
60
|
+
info: captureInfo,
|
|
61
|
+
warn: () => {},
|
|
62
|
+
error: () => {},
|
|
63
|
+
debug: () => {},
|
|
64
|
+
trace: () => {},
|
|
65
|
+
fatal: () => {},
|
|
66
|
+
}),
|
|
67
|
+
}),
|
|
32
68
|
}));
|
|
33
69
|
|
|
34
70
|
// ---------------------------------------------------------------------------
|
|
35
|
-
// Imports under test
|
|
71
|
+
// Imports under test
|
|
72
|
+
//
|
|
73
|
+
// Type-only imports are erased at runtime and safe to hoist. The value import
|
|
74
|
+
// of `host-proxy-preactivation` must be dynamic (`await import`) so it
|
|
75
|
+
// resolves AFTER the `mock.module(...)` calls above — otherwise ES module
|
|
76
|
+
// hoisting loads the real logger before the mock registers, the production
|
|
77
|
+
// `const log = getLogger(...)` binds to real pino, and assertions against
|
|
78
|
+
// `loggedInfoCalls` see an empty array. Same pattern as
|
|
79
|
+
// `secret-prompt-log-hygiene.test.ts`.
|
|
36
80
|
// ---------------------------------------------------------------------------
|
|
37
81
|
|
|
38
82
|
import type { HostProxyCapability } from "../channels/types.js";
|
|
39
|
-
import {
|
|
40
|
-
|
|
83
|
+
import type { HostProxyPreactivationTarget } from "../daemon/host-proxy-preactivation.js";
|
|
84
|
+
|
|
85
|
+
const {
|
|
86
|
+
evaluateHostProxyAttachment,
|
|
41
87
|
preactivateHostProxySkills,
|
|
42
88
|
shouldAttachHostProxyForCapability,
|
|
43
|
-
}
|
|
89
|
+
} = await import("../daemon/host-proxy-preactivation.js");
|
|
44
90
|
|
|
45
91
|
// ---------------------------------------------------------------------------
|
|
46
92
|
// Helpers
|
|
47
93
|
// ---------------------------------------------------------------------------
|
|
48
94
|
|
|
49
|
-
function makeTarget(
|
|
50
|
-
|
|
51
|
-
} {
|
|
95
|
+
function makeTarget(
|
|
96
|
+
conversationId = "conv-test",
|
|
97
|
+
): HostProxyPreactivationTarget & { preactivatedSkillIds: string[] } {
|
|
52
98
|
const preactivatedSkillIds: string[] = [];
|
|
53
99
|
return {
|
|
100
|
+
conversationId,
|
|
54
101
|
preactivatedSkillIds,
|
|
55
102
|
addPreactivatedSkillId(id: string) {
|
|
56
103
|
preactivatedSkillIds.push(id);
|
|
@@ -73,6 +120,7 @@ function setCapableClient(
|
|
|
73
120
|
|
|
74
121
|
beforeEach(() => {
|
|
75
122
|
mockClientsByCapability = new Map();
|
|
123
|
+
loggedInfoCalls.length = 0;
|
|
76
124
|
});
|
|
77
125
|
|
|
78
126
|
// ---------------------------------------------------------------------------
|
|
@@ -143,7 +191,10 @@ describe("shouldAttachHostProxyForCapability", () => {
|
|
|
143
191
|
test("returns false for chrome-extension source even when a capable client is connected", () => {
|
|
144
192
|
setCapableClient("host_app_control", true);
|
|
145
193
|
expect(
|
|
146
|
-
shouldAttachHostProxyForCapability(
|
|
194
|
+
shouldAttachHostProxyForCapability(
|
|
195
|
+
"host_app_control",
|
|
196
|
+
"chrome-extension",
|
|
197
|
+
),
|
|
147
198
|
).toBe(false);
|
|
148
199
|
});
|
|
149
200
|
});
|
|
@@ -154,7 +205,7 @@ describe("shouldAttachHostProxyForCapability", () => {
|
|
|
154
205
|
// ---------------------------------------------------------------------------
|
|
155
206
|
|
|
156
207
|
describe("preactivateHostProxySkills", () => {
|
|
157
|
-
test("no
|
|
208
|
+
test("preactivates no skills when sourceInterface is undefined", () => {
|
|
158
209
|
const target = makeTarget();
|
|
159
210
|
preactivateHostProxySkills(target, undefined);
|
|
160
211
|
expect(target.preactivatedSkillIds).toEqual([]);
|
|
@@ -209,3 +260,139 @@ describe("preactivateHostProxySkills", () => {
|
|
|
209
260
|
expect(target.preactivatedSkillIds).toEqual([]);
|
|
210
261
|
});
|
|
211
262
|
});
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// evaluateHostProxyAttachment — reason coverage
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
describe("evaluateHostProxyAttachment", () => {
|
|
269
|
+
test("returns denied_no_interface when sourceInterface is undefined", () => {
|
|
270
|
+
expect(evaluateHostProxyAttachment("host_cu", undefined)).toEqual({
|
|
271
|
+
shouldAttach: false,
|
|
272
|
+
reason: "denied_no_interface",
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("returns native_support for macos + host_cu", () => {
|
|
277
|
+
expect(evaluateHostProxyAttachment("host_cu", "macos")).toEqual({
|
|
278
|
+
shouldAttach: true,
|
|
279
|
+
reason: "native_support",
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("returns denied_chrome_extension for chrome-extension source even when capable clients exist", () => {
|
|
284
|
+
setCapableClient("host_cu", true);
|
|
285
|
+
expect(evaluateHostProxyAttachment("host_cu", "chrome-extension")).toEqual({
|
|
286
|
+
shouldAttach: false,
|
|
287
|
+
reason: "denied_chrome_extension",
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("returns cross_client with clientCount when a capable client is connected", () => {
|
|
292
|
+
setCapableClient("host_cu", true);
|
|
293
|
+
expect(evaluateHostProxyAttachment("host_cu", "web")).toEqual({
|
|
294
|
+
shouldAttach: true,
|
|
295
|
+
reason: "cross_client",
|
|
296
|
+
clientCount: 1,
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("returns denied_no_clients with clientCount 0 when no capable client is connected", () => {
|
|
301
|
+
setCapableClient("host_cu", false);
|
|
302
|
+
expect(evaluateHostProxyAttachment("host_cu", "web")).toEqual({
|
|
303
|
+
shouldAttach: false,
|
|
304
|
+
reason: "denied_no_clients",
|
|
305
|
+
clientCount: 0,
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
// preactivateHostProxySkills — structured logging
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
describe("preactivateHostProxySkills logging", () => {
|
|
315
|
+
test("emits exactly one info log per call", () => {
|
|
316
|
+
const target = makeTarget();
|
|
317
|
+
preactivateHostProxySkills(target, "macos");
|
|
318
|
+
expect(loggedInfoCalls).toHaveLength(1);
|
|
319
|
+
expect(loggedInfoCalls[0].message).toBe(
|
|
320
|
+
"host-proxy preactivation decision",
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("log includes conversationId, sourceInterface, per-capability decisions, and preactivatedSkillIds for macos", () => {
|
|
325
|
+
const target = makeTarget("conv-macos-123");
|
|
326
|
+
preactivateHostProxySkills(target, "macos");
|
|
327
|
+
|
|
328
|
+
expect(loggedInfoCalls).toHaveLength(1);
|
|
329
|
+
const { fields } = loggedInfoCalls[0];
|
|
330
|
+
expect(fields.conversationId).toBe("conv-macos-123");
|
|
331
|
+
expect(fields.sourceInterface).toBe("macos");
|
|
332
|
+
expect(fields.decisions).toEqual({
|
|
333
|
+
host_cu: { shouldAttach: true, reason: "native_support" },
|
|
334
|
+
host_app_control: { shouldAttach: true, reason: "native_support" },
|
|
335
|
+
});
|
|
336
|
+
expect(fields.preactivatedSkillIds).toEqual([
|
|
337
|
+
"computer-use",
|
|
338
|
+
"app-control",
|
|
339
|
+
]);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("log captures denied_no_interface for undefined sourceInterface (silent-gate diagnostic)", () => {
|
|
343
|
+
const target = makeTarget("conv-no-interface");
|
|
344
|
+
preactivateHostProxySkills(target, undefined);
|
|
345
|
+
|
|
346
|
+
expect(loggedInfoCalls).toHaveLength(1);
|
|
347
|
+
const { fields } = loggedInfoCalls[0];
|
|
348
|
+
expect(fields.conversationId).toBe("conv-no-interface");
|
|
349
|
+
expect(fields.sourceInterface).toBeUndefined();
|
|
350
|
+
expect(fields.decisions).toEqual({
|
|
351
|
+
host_cu: { shouldAttach: false, reason: "denied_no_interface" },
|
|
352
|
+
host_app_control: { shouldAttach: false, reason: "denied_no_interface" },
|
|
353
|
+
});
|
|
354
|
+
expect(fields.preactivatedSkillIds).toEqual([]);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("log captures cross_client + clientCount when a web source has a connected host_cu client", () => {
|
|
358
|
+
setCapableClient("host_cu", true);
|
|
359
|
+
const target = makeTarget();
|
|
360
|
+
preactivateHostProxySkills(target, "web");
|
|
361
|
+
|
|
362
|
+
expect(loggedInfoCalls).toHaveLength(1);
|
|
363
|
+
const decisions = loggedInfoCalls[0].fields.decisions as Record<
|
|
364
|
+
string,
|
|
365
|
+
unknown
|
|
366
|
+
>;
|
|
367
|
+
expect(decisions.host_cu).toEqual({
|
|
368
|
+
shouldAttach: true,
|
|
369
|
+
reason: "cross_client",
|
|
370
|
+
clientCount: 1,
|
|
371
|
+
});
|
|
372
|
+
expect(decisions.host_app_control).toEqual({
|
|
373
|
+
shouldAttach: false,
|
|
374
|
+
reason: "denied_no_clients",
|
|
375
|
+
clientCount: 0,
|
|
376
|
+
});
|
|
377
|
+
expect(loggedInfoCalls[0].fields.preactivatedSkillIds).toEqual([
|
|
378
|
+
"computer-use",
|
|
379
|
+
]);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("log captures denied_chrome_extension reason for chrome-extension source", () => {
|
|
383
|
+
setCapableClient("host_cu", true);
|
|
384
|
+
const target = makeTarget();
|
|
385
|
+
preactivateHostProxySkills(target, "chrome-extension");
|
|
386
|
+
|
|
387
|
+
expect(loggedInfoCalls).toHaveLength(1);
|
|
388
|
+
const decisions = loggedInfoCalls[0].fields.decisions as Record<
|
|
389
|
+
string,
|
|
390
|
+
unknown
|
|
391
|
+
>;
|
|
392
|
+
expect(decisions.host_cu).toEqual({
|
|
393
|
+
shouldAttach: false,
|
|
394
|
+
reason: "denied_chrome_extension",
|
|
395
|
+
});
|
|
396
|
+
expect(loggedInfoCalls[0].fields.preactivatedSkillIds).toEqual([]);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
let configBackgroundInjection: string =
|
|
4
|
+
"This is a background turn — your guardian isn't watching. If anything noteworthy comes up, send them a notification so they see it when they're back by invoking the `notifications` skill (`assistant notifications send --message \"...\"`)";
|
|
5
|
+
|
|
6
|
+
const realLoaderForBackgroundTurnTest = await import("../config/loader.js");
|
|
7
|
+
const realGetConfigForBackgroundTurnTest =
|
|
8
|
+
realLoaderForBackgroundTurnTest.getConfig;
|
|
9
|
+
mock.module("../config/loader.js", () => ({
|
|
10
|
+
...realLoaderForBackgroundTurnTest,
|
|
11
|
+
getConfig: () => {
|
|
12
|
+
const real = realGetConfigForBackgroundTurnTest();
|
|
13
|
+
return {
|
|
14
|
+
...real,
|
|
15
|
+
conversations: {
|
|
16
|
+
...real.conversations,
|
|
17
|
+
backgroundInjection: configBackgroundInjection,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
DEFAULT_INJECTOR_ORDER,
|
|
25
|
+
defaultInjectorsPlugin,
|
|
26
|
+
} from "../plugins/defaults/injectors.js";
|
|
27
|
+
import {
|
|
28
|
+
registerPlugin,
|
|
29
|
+
resetPluginRegistryForTests,
|
|
30
|
+
} from "../plugins/registry.js";
|
|
31
|
+
import type { Injector, TurnContext } from "../plugins/types.js";
|
|
32
|
+
|
|
33
|
+
function findInjector(name: string): Injector {
|
|
34
|
+
const injector = defaultInjectorsPlugin.injectors?.find(
|
|
35
|
+
(candidate) => candidate.name === name,
|
|
36
|
+
);
|
|
37
|
+
if (!injector) {
|
|
38
|
+
throw new Error(`injector '${name}' not registered`);
|
|
39
|
+
}
|
|
40
|
+
return injector;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function makeContext(overrides: Partial<TurnContext> = {}): TurnContext {
|
|
44
|
+
return {
|
|
45
|
+
requestId: "req-test",
|
|
46
|
+
conversationId: "conv-test",
|
|
47
|
+
turnIndex: 0,
|
|
48
|
+
trust: { sourceChannel: "vellum", trustClass: "guardian" },
|
|
49
|
+
...overrides,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const backgroundInjector = findInjector("background-turn");
|
|
54
|
+
|
|
55
|
+
const DEFAULT_INJECTION_TEXT =
|
|
56
|
+
"This is a background turn — your guardian isn't watching. If anything noteworthy comes up, send them a notification so they see it when they're back by invoking the `notifications` skill (`assistant notifications send --message \"...\"`)";
|
|
57
|
+
|
|
58
|
+
describe("background-turn injector", () => {
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
resetPluginRegistryForTests();
|
|
61
|
+
registerPlugin(defaultInjectorsPlugin);
|
|
62
|
+
configBackgroundInjection = DEFAULT_INJECTION_TEXT;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("returns null when isBackgroundConversation is false", async () => {
|
|
66
|
+
const result = await backgroundInjector.produce(
|
|
67
|
+
makeContext({
|
|
68
|
+
injectionInputs: {
|
|
69
|
+
isBackgroundConversation: false,
|
|
70
|
+
isNonInteractive: true,
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
expect(result).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("returns null when isBackgroundConversation is unset", async () => {
|
|
78
|
+
const result = await backgroundInjector.produce(
|
|
79
|
+
makeContext({ injectionInputs: { isNonInteractive: true } }),
|
|
80
|
+
);
|
|
81
|
+
expect(result).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("returns null when the guardian is actively connected (interactive turn)", async () => {
|
|
85
|
+
const result = await backgroundInjector.produce(
|
|
86
|
+
makeContext({
|
|
87
|
+
injectionInputs: {
|
|
88
|
+
isBackgroundConversation: true,
|
|
89
|
+
isNonInteractive: false,
|
|
90
|
+
},
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
expect(result).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("returns null when isNonInteractive is unset", async () => {
|
|
97
|
+
const result = await backgroundInjector.produce(
|
|
98
|
+
makeContext({ injectionInputs: { isBackgroundConversation: true } }),
|
|
99
|
+
);
|
|
100
|
+
expect(result).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("wraps configured text in <background_turn> tags when active and non-interactive", async () => {
|
|
104
|
+
const block = await backgroundInjector.produce(
|
|
105
|
+
makeContext({
|
|
106
|
+
injectionInputs: {
|
|
107
|
+
isBackgroundConversation: true,
|
|
108
|
+
isNonInteractive: true,
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect(block).toEqual({
|
|
114
|
+
id: "background-turn",
|
|
115
|
+
text: `<background_turn>\n${DEFAULT_INJECTION_TEXT}\n</background_turn>`,
|
|
116
|
+
placement: "prepend-user-tail",
|
|
117
|
+
});
|
|
118
|
+
expect(backgroundInjector.order).toBe(
|
|
119
|
+
DEFAULT_INJECTOR_ORDER.backgroundTurn,
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("returns null when configured text is the empty string", async () => {
|
|
124
|
+
configBackgroundInjection = "";
|
|
125
|
+
|
|
126
|
+
const result = await backgroundInjector.produce(
|
|
127
|
+
makeContext({
|
|
128
|
+
injectionInputs: {
|
|
129
|
+
isBackgroundConversation: true,
|
|
130
|
+
isNonInteractive: true,
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
expect(result).toBeNull();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("uses operator-configured override text verbatim", async () => {
|
|
138
|
+
configBackgroundInjection = "Custom reminder body.";
|
|
139
|
+
|
|
140
|
+
const block = await backgroundInjector.produce(
|
|
141
|
+
makeContext({
|
|
142
|
+
injectionInputs: {
|
|
143
|
+
isBackgroundConversation: true,
|
|
144
|
+
isNonInteractive: true,
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
expect(block?.text).toBe(
|
|
150
|
+
"<background_turn>\nCustom reminder body.\n</background_turn>",
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
});
|