@vellumai/assistant 0.11.4-staging.1 → 0.11.4-staging.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/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/knip.json +3 -0
- package/openapi.yaml +135 -75
- 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__/byok-default-profile-ensure.test.ts +15 -0
- 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-notifiers-provenance.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +99 -0
- 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-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__/gateway-only-guard.test.ts +2 -5
- 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__/ui-choice-copy-surfaces.test.ts +1 -1
- package/src/__tests__/ui-shape-teaching.test.ts +33 -0
- package/src/__tests__/ui-visual-surface.test.ts +1 -1
- package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
- package/src/__tests__/ui-work-result-surface.test.ts +1 -1
- package/src/__tests__/voice-config-update.test.ts +40 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +5 -3
- package/src/__tests__/voice-session-bridge.test.ts +85 -29
- package/src/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -0
- package/src/acp/session-manager.ts +8 -1
- package/src/api/surfaces.ts +12 -3
- 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/call-controller.ts +16 -7
- 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/settings/tools/navigate-settings-tab.test.ts +65 -0
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
- package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
- 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 +97 -5
- 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-store.ts +4 -4
- package/src/daemon/conversation-surfaces.ts +54 -18
- package/src/daemon/conversation-tool-setup.ts +2 -4
- package/src/daemon/conversation.ts +81 -44
- package/src/daemon/doordash-steps.ts +2 -2
- package/src/daemon/lifecycle.ts +14 -1
- package/src/daemon/process-message.ts +0 -23
- 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/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/vision-support.test.ts +1 -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/model-catalog.ts +3 -3
- package/src/providers/model-intents.ts +2 -2
- package/src/providers/openai/chat-completions-provider.ts +16 -7
- 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 +8 -6
- package/src/tools/acp/spawn.ts +6 -4
- 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 +12 -1
- package/src/tts/__tests__/reasoning-tag-filter.test.ts +15 -0
- package/src/tts/reasoning-tag-filter.ts +11 -32
- package/src/util/__tests__/worker-process-command.test.ts +37 -0
- package/src/util/logger.ts +16 -0
- package/src/util/think-tag-stream.ts +95 -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/workspace/byok-default-profile-ensure.ts +15 -6
- package/src/workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/document-workspace-file.test.ts +0 -467
- package/src/subagent/__tests__/consult-transcript.test.ts +0 -184
- package/src/subagent/consult-transcript.ts +0 -90
|
@@ -2988,6 +2988,9 @@ describe("Conversation host attachment directives", () => {
|
|
|
2988
2988
|
attachments: [],
|
|
2989
2989
|
onEvent: (e) => events.push(e),
|
|
2990
2990
|
requestId: "req-1",
|
|
2991
|
+
// A host attachment read prompts only when a human is present to
|
|
2992
|
+
// answer; presence is declared per turn.
|
|
2993
|
+
isInteractive: true,
|
|
2991
2994
|
});
|
|
2992
2995
|
await waitForPendingRun(1);
|
|
2993
2996
|
|
|
@@ -3058,6 +3061,9 @@ describe("Conversation host attachment directives", () => {
|
|
|
3058
3061
|
attachments: [],
|
|
3059
3062
|
onEvent: (e) => events.push(e),
|
|
3060
3063
|
requestId: "req-1",
|
|
3064
|
+
// A host attachment read prompts only when a human is present to
|
|
3065
|
+
// answer; presence is declared per turn.
|
|
3066
|
+
isInteractive: true,
|
|
3061
3067
|
});
|
|
3062
3068
|
await waitForPendingRun(1);
|
|
3063
3069
|
|
|
@@ -3923,3 +3929,96 @@ describe("subagent notification user_message_echo suppression", () => {
|
|
|
3923
3929
|
await new Promise((r) => setTimeout(r, 10));
|
|
3924
3930
|
});
|
|
3925
3931
|
});
|
|
3932
|
+
|
|
3933
|
+
describe("drained turns deliver through the conversation sink", () => {
|
|
3934
|
+
beforeEach(() => {
|
|
3935
|
+
pendingRuns = [];
|
|
3936
|
+
});
|
|
3937
|
+
|
|
3938
|
+
test("a confirmation prompt raised in a drained interactive turn reaches the sink", async () => {
|
|
3939
|
+
// The sink is fixed for the conversation's life, so an interactive turn
|
|
3940
|
+
// drained from the queue delivers exactly like a live one: no per-turn
|
|
3941
|
+
// rebinding, nothing for the previous turn's cleanup to clobber. This is
|
|
3942
|
+
// the invariant behind the acp_spawn permission-prompt timeouts (a queued
|
|
3943
|
+
// interactive turn's confirmation_request used to be emitted into a
|
|
3944
|
+
// reset no-op sender and hang until the permission timeout).
|
|
3945
|
+
const sink: AssistantEvent[] = [];
|
|
3946
|
+
const conversation = makeConversation((msg) => sink.push(msg));
|
|
3947
|
+
await conversation.loadFromDb();
|
|
3948
|
+
|
|
3949
|
+
const p1 = conversation.processMessage({
|
|
3950
|
+
content: "msg-1",
|
|
3951
|
+
attachments: [],
|
|
3952
|
+
onEvent: () => {},
|
|
3953
|
+
requestId: "req-1",
|
|
3954
|
+
});
|
|
3955
|
+
await waitForPendingRun(1);
|
|
3956
|
+
|
|
3957
|
+
conversation.enqueueMessage({
|
|
3958
|
+
content: "msg-2",
|
|
3959
|
+
requestId: "req-2",
|
|
3960
|
+
onEvent: () => {},
|
|
3961
|
+
isInteractive: true,
|
|
3962
|
+
});
|
|
3963
|
+
|
|
3964
|
+
await resolveRun(0);
|
|
3965
|
+
await p1;
|
|
3966
|
+
await waitForPendingRun(2);
|
|
3967
|
+
|
|
3968
|
+
// The drained turn is in flight and interactive: presence reads a human.
|
|
3969
|
+
expect(conversation.hasNoClient).toBe(false);
|
|
3970
|
+
|
|
3971
|
+
// A prompt raised now, mid-drained-turn, must land on the sink.
|
|
3972
|
+
void conversation.prompter.prompt(
|
|
3973
|
+
"acp_spawn",
|
|
3974
|
+
{ agent: "claude", task: "t" },
|
|
3975
|
+
"high",
|
|
3976
|
+
[],
|
|
3977
|
+
[],
|
|
3978
|
+
undefined,
|
|
3979
|
+
conversation.conversationId,
|
|
3980
|
+
"host",
|
|
3981
|
+
false,
|
|
3982
|
+
undefined,
|
|
3983
|
+
"tool-use-1",
|
|
3984
|
+
);
|
|
3985
|
+
const prompt = sink.find((e) => e.type === "confirmation_request");
|
|
3986
|
+
expect(prompt).toBeDefined();
|
|
3987
|
+
expect((prompt as { toolName: string }).toolName).toBe("acp_spawn");
|
|
3988
|
+
// Settle it so the prompt's timer does not outlive the test.
|
|
3989
|
+
conversation.prompter.denyAllPending();
|
|
3990
|
+
|
|
3991
|
+
await resolveRun(1);
|
|
3992
|
+
// Turn over: nothing is in flight, so no human is asserted present.
|
|
3993
|
+
await waitForCondition(() => conversation.hasNoClient);
|
|
3994
|
+
});
|
|
3995
|
+
|
|
3996
|
+
test("a drained non-interactive turn asserts no human present", async () => {
|
|
3997
|
+
const conversation = makeConversation();
|
|
3998
|
+
await conversation.loadFromDb();
|
|
3999
|
+
|
|
4000
|
+
const p1 = conversation.processMessage({
|
|
4001
|
+
content: "msg-1",
|
|
4002
|
+
attachments: [],
|
|
4003
|
+
onEvent: () => {},
|
|
4004
|
+
requestId: "req-1",
|
|
4005
|
+
});
|
|
4006
|
+
await waitForPendingRun(1);
|
|
4007
|
+
|
|
4008
|
+
conversation.enqueueMessage({
|
|
4009
|
+
content: "msg-2",
|
|
4010
|
+
requestId: "req-2",
|
|
4011
|
+
onEvent: () => {},
|
|
4012
|
+
isInteractive: false,
|
|
4013
|
+
});
|
|
4014
|
+
|
|
4015
|
+
await resolveRun(0);
|
|
4016
|
+
await p1;
|
|
4017
|
+
await waitForPendingRun(2);
|
|
4018
|
+
|
|
4019
|
+
expect(conversation.hasNoClient).toBe(true);
|
|
4020
|
+
|
|
4021
|
+
await resolveRun(1);
|
|
4022
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
4023
|
+
});
|
|
4024
|
+
});
|
|
@@ -149,7 +149,7 @@ function createFakeConversation(conversationId: string): Conversation {
|
|
|
149
149
|
return this.turnInterfaceContext;
|
|
150
150
|
},
|
|
151
151
|
ensureActorScopedHistory: async () => {},
|
|
152
|
-
|
|
152
|
+
replayActivityState: () => {},
|
|
153
153
|
|
|
154
154
|
setHostCuProxy(this: { hostCuProxy: unknown }, proxy: unknown) {
|
|
155
155
|
this.hostCuProxy = proxy;
|
|
@@ -158,7 +158,7 @@ function makeConversation() {
|
|
|
158
158
|
currentRequestId: undefined,
|
|
159
159
|
queue: { length: 0 },
|
|
160
160
|
setTrustContext: () => {},
|
|
161
|
-
|
|
161
|
+
replayActivityState: () => {},
|
|
162
162
|
emitConfirmationStateChanged: () => {},
|
|
163
163
|
emitActivityState: () => {},
|
|
164
164
|
setTurnChannelContext: () => {},
|
|
@@ -163,7 +163,7 @@ describe("handleSendMessage guardian reply interception", () => {
|
|
|
163
163
|
const runAgentLoop = mock(async () => undefined);
|
|
164
164
|
const session = {
|
|
165
165
|
setTrustContext: () => {},
|
|
166
|
-
|
|
166
|
+
replayActivityState: () => {},
|
|
167
167
|
emitConfirmationStateChanged: () => {},
|
|
168
168
|
emitActivityState: () => {},
|
|
169
169
|
setTurnChannelContext: () => {},
|
|
@@ -253,7 +253,7 @@ describe("handleSendMessage guardian reply interception", () => {
|
|
|
253
253
|
const runAgentLoop = mock(async () => undefined);
|
|
254
254
|
const session = {
|
|
255
255
|
setTrustContext: () => {},
|
|
256
|
-
|
|
256
|
+
replayActivityState: () => {},
|
|
257
257
|
emitConfirmationStateChanged: () => {},
|
|
258
258
|
emitActivityState: () => {},
|
|
259
259
|
setTurnChannelContext: () => {},
|
|
@@ -331,7 +331,7 @@ describe("handleSendMessage guardian reply interception", () => {
|
|
|
331
331
|
const runAgentLoop = mock(async () => undefined);
|
|
332
332
|
const session = {
|
|
333
333
|
setTrustContext: () => {},
|
|
334
|
-
|
|
334
|
+
replayActivityState: () => {},
|
|
335
335
|
emitConfirmationStateChanged: () => {},
|
|
336
336
|
emitActivityState: () => {},
|
|
337
337
|
setTurnChannelContext: () => {},
|
|
@@ -419,7 +419,7 @@ describe("handleSendMessage guardian reply interception", () => {
|
|
|
419
419
|
const runAgentLoop = mock(async () => undefined);
|
|
420
420
|
const session = {
|
|
421
421
|
setTrustContext: () => {},
|
|
422
|
-
|
|
422
|
+
replayActivityState: () => {},
|
|
423
423
|
emitConfirmationStateChanged: () => {},
|
|
424
424
|
emitActivityState: () => {},
|
|
425
425
|
setTurnChannelContext: () => {},
|
|
@@ -507,7 +507,7 @@ describe("handleSendMessage guardian reply interception", () => {
|
|
|
507
507
|
const runAgentLoop = mock(async () => undefined);
|
|
508
508
|
const session = {
|
|
509
509
|
setTrustContext: () => {},
|
|
510
|
-
|
|
510
|
+
replayActivityState: () => {},
|
|
511
511
|
emitConfirmationStateChanged: () => {},
|
|
512
512
|
emitActivityState: () => {},
|
|
513
513
|
setTurnChannelContext: () => {},
|
|
@@ -590,7 +590,7 @@ describe("handleSendMessage guardian reply interception", () => {
|
|
|
590
590
|
const runAgentLoop = mock(async () => undefined);
|
|
591
591
|
const session = {
|
|
592
592
|
setTrustContext: () => {},
|
|
593
|
-
|
|
593
|
+
replayActivityState: () => {},
|
|
594
594
|
emitConfirmationStateChanged: () => {},
|
|
595
595
|
emitActivityState: () => {},
|
|
596
596
|
setTurnChannelContext: () => {},
|
|
@@ -667,7 +667,7 @@ describe("handleSendMessage guardian reply interception", () => {
|
|
|
667
667
|
const runAgentLoop = mock(async () => undefined);
|
|
668
668
|
const session = {
|
|
669
669
|
setTrustContext: () => {},
|
|
670
|
-
|
|
670
|
+
replayActivityState: () => {},
|
|
671
671
|
emitConfirmationStateChanged: () => {},
|
|
672
672
|
emitActivityState: () => {},
|
|
673
673
|
setTurnChannelContext: () => {},
|
|
@@ -746,7 +746,7 @@ describe("handleSendMessage guardian reply interception", () => {
|
|
|
746
746
|
const runAgentLoop = mock(async () => undefined);
|
|
747
747
|
const session = {
|
|
748
748
|
setTrustContext: () => {},
|
|
749
|
-
|
|
749
|
+
replayActivityState: () => {},
|
|
750
750
|
emitConfirmationStateChanged: () => {},
|
|
751
751
|
emitActivityState: () => {},
|
|
752
752
|
setTurnChannelContext: () => {},
|
|
@@ -826,7 +826,7 @@ describe("handleSendMessage guardian reply interception", () => {
|
|
|
826
826
|
const runAgentLoop = mock(async () => undefined);
|
|
827
827
|
const session = {
|
|
828
828
|
setTrustContext: () => {},
|
|
829
|
-
|
|
829
|
+
replayActivityState: () => {},
|
|
830
830
|
emitConfirmationStateChanged: () => {},
|
|
831
831
|
emitActivityState: () => {},
|
|
832
832
|
setTurnChannelContext: () => {},
|
|
@@ -169,7 +169,7 @@ function makeConversationWithPendingConfirmation(
|
|
|
169
169
|
setTrustContext(this: { trustContext: unknown }, ctx: unknown) {
|
|
170
170
|
this.trustContext = ctx;
|
|
171
171
|
},
|
|
172
|
-
|
|
172
|
+
replayActivityState: () => {},
|
|
173
173
|
emitConfirmationStateChanged: () => {},
|
|
174
174
|
emitActivityState: () => {},
|
|
175
175
|
setTurnChannelContext: () => {},
|
|
@@ -233,7 +233,7 @@ function makeConversation() {
|
|
|
233
233
|
currentRequestId: undefined,
|
|
234
234
|
queue: { length: 0 },
|
|
235
235
|
setTrustContext: () => {},
|
|
236
|
-
|
|
236
|
+
replayActivityState: () => {},
|
|
237
237
|
emitConfirmationStateChanged: () => {},
|
|
238
238
|
emitActivityState: () => {},
|
|
239
239
|
setTurnChannelContext: () => {},
|
|
@@ -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(
|
|
@@ -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. */
|
|
@@ -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
|
+
});
|