@vellumai/assistant 0.10.3 → 0.10.4-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openapi.yaml +73 -56
- package/package.json +1 -1
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
- package/src/__tests__/assistant-stream-state.test.ts +3 -76
- package/src/__tests__/background-workers-disk-pressure.test.ts +4 -2
- package/src/__tests__/channel-approval-routes.test.ts +21 -26
- package/src/__tests__/channel-delivery-store.test.ts +28 -0
- package/src/__tests__/channel-guardian.test.ts +82 -32
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
- package/src/__tests__/channel-reply-delivery.test.ts +6 -2
- package/src/__tests__/compaction-ledger-store.test.ts +128 -0
- package/src/__tests__/config-loader-backfill.test.ts +148 -0
- package/src/__tests__/consult-deadline.test.ts +60 -0
- package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
- package/src/__tests__/contact-store-user-file.test.ts +7 -10
- package/src/__tests__/contacts-relay-reads.test.ts +6 -9
- package/src/__tests__/contacts-write.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -2
- package/src/__tests__/conversation-agent-loop.test.ts +98 -7
- package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
- package/src/__tests__/conversation-error.test.ts +18 -0
- package/src/__tests__/conversation-fork-crud.test.ts +354 -24
- package/src/__tests__/conversation-title-service.test.ts +222 -201
- package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
- package/src/__tests__/delete-propagation.test.ts +5 -3
- package/src/__tests__/dm-backfill.test.ts +6 -4
- package/src/__tests__/emit-signal-routing-intent.test.ts +2 -6
- package/src/__tests__/guardian-binding-drift-heal.test.ts +43 -23
- package/src/__tests__/guardian-dispatch.test.ts +50 -5
- package/src/__tests__/guardian-routing-state.test.ts +6 -10
- package/src/__tests__/helpers/channel-test-adapter.ts +45 -12
- package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
- package/src/__tests__/helpers/mock-logger.ts +1 -0
- package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +87 -10
- package/src/__tests__/invite-redemption-service.test.ts +273 -53
- package/src/__tests__/invite-routes-http.test.ts +34 -0
- package/src/__tests__/invite-service-ipc.test.ts +65 -2
- package/src/__tests__/list-messages-page-latest.test.ts +173 -4
- package/src/__tests__/mcp-config-secret-boundary.test.ts +3 -0
- package/src/__tests__/non-member-access-request.test.ts +15 -13
- package/src/__tests__/onboarding-persona-write.test.ts +52 -22
- package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
- package/src/__tests__/persona-resolver.test.ts +75 -45
- package/src/__tests__/plugin-bootstrap.test.ts +13 -5
- package/src/__tests__/plugin-disabled-state.test.ts +190 -0
- package/src/__tests__/provider-usage-tracking.test.ts +1 -1
- package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
- package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
- package/src/__tests__/reaction-persistence.test.ts +51 -4
- package/src/__tests__/relay-server.test.ts +88 -31
- package/src/__tests__/runtime-attachment-metadata.test.ts +9 -11
- package/src/__tests__/settings-routes.test.ts +32 -0
- package/src/__tests__/slack-block-formatting.test.ts +1 -38
- package/src/__tests__/sse-actor-principal-guardian-source.test.ts +13 -36
- package/src/__tests__/stt-hints.test.ts +6 -3
- package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
- package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
- package/src/__tests__/subagent-role-registry.test.ts +17 -4
- package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
- package/src/__tests__/subagent-tools.test.ts +398 -3
- package/src/__tests__/thread-backfill.test.ts +3 -3
- package/src/__tests__/tool-preview-lifecycle.test.ts +26 -10
- package/src/__tests__/tool-start-timestamp.test.ts +4 -3
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -2
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
- package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
- package/src/__tests__/trusted-contact-verification.test.ts +79 -54
- package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
- package/src/__tests__/voice-invite-redemption.test.ts +183 -20
- package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
- package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +2 -2
- package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
- package/src/agent/loop-exclusive-tool.test.ts +19 -15
- package/src/agent/loop-native-web-search.test.ts +200 -0
- package/src/agent/loop.ts +108 -1
- package/src/api/responses/conversation-message.ts +9 -0
- package/src/approvals/guardian-request-resolvers.ts +16 -4
- package/src/calls/__tests__/relay-setup-router.test.ts +10 -18
- package/src/calls/guardian-dispatch.ts +14 -11
- package/src/calls/inbound-trust-reader.ts +7 -1
- package/src/calls/relay-access-wait.ts +6 -6
- package/src/calls/relay-server.ts +22 -2
- package/src/calls/relay-setup-router.ts +10 -10
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
- package/src/cli/commands/contacts.ts +10 -7
- package/src/cli/commands/memory/__tests__/worker.test.ts +147 -17
- package/src/cli/commands/memory/worker.ts +97 -30
- package/src/cli/commands/plugins.ts +3 -146
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +17 -17
- package/src/cli/lib/__tests__/publish-plugin.test.ts +98 -0
- package/src/cli/lib/publish-plugin.ts +231 -1
- package/src/config/__tests__/sync-gated-profiles.test.ts +5 -7
- package/src/config/bundled-skills/subagent/SKILL.md +16 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
- package/src/config/call-site-defaults.ts +0 -6
- package/src/config/llm-resolver.ts +0 -3
- package/src/config/schemas/call-site-catalog.ts +0 -7
- package/src/config/schemas/heartbeat.ts +2 -5
- package/src/config/schemas/llm.ts +3 -12
- package/src/config/schemas/memory-lifecycle.ts +1 -1
- package/src/config/seed-inference-profiles.ts +76 -35
- package/src/config/sync-gated-profiles.ts +0 -3
- package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +7 -8
- package/src/contacts/__tests__/member-write-relay.test.ts +35 -11
- package/src/contacts/contact-store.ts +27 -237
- package/src/contacts/contacts-write.ts +18 -58
- package/src/contacts/gateway-channel-read.ts +51 -0
- package/src/contacts/member-write-relay.ts +25 -31
- package/src/contacts/types.ts +3 -15
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
- package/src/daemon/conversation-agent-loop-handlers.ts +29 -10
- package/src/daemon/conversation-agent-loop.ts +68 -61
- package/src/daemon/conversation-error.ts +7 -10
- package/src/daemon/conversation-tool-setup.ts +0 -10
- package/src/daemon/conversation.ts +10 -0
- package/src/daemon/external-plugins-bootstrap.ts +8 -2
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -2
- package/src/daemon/handlers/__tests__/config-channels.test.ts +9 -14
- package/src/daemon/handlers/config-channels.ts +14 -29
- package/src/daemon/lifecycle.ts +16 -4
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/heartbeat/heartbeat-service.ts +5 -0
- package/src/home/relationship-state-writer.ts +5 -0
- package/src/memory/__tests__/embedding-cache.test.ts +136 -0
- package/src/memory/compaction-ledger-store.ts +107 -0
- package/src/memory/conversation-crud.ts +136 -61
- package/src/memory/conversation-title-service.ts +173 -24
- package/src/memory/embedding-backend.ts +8 -1
- package/src/memory/embedding-cache.ts +139 -0
- package/src/memory/jobs-worker.ts +75 -29
- package/src/memory/memory-retrospective-job.ts +5 -0
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +27 -5
- package/src/memory/migrations/302-create-compaction-events.ts +107 -0
- package/src/memory/migrations/303-add-conversation-creation-seq.ts +33 -0
- package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +79 -6
- package/src/memory/schema/contacts.ts +6 -2
- package/src/memory/schema/conversations.ts +39 -0
- package/src/memory/steps.ts +1090 -367
- package/src/memory/worker-control.ts +104 -18
- package/src/memory/worker-process.ts +17 -0
- package/src/messaging/channel-binding-metadata.ts +31 -0
- package/src/messaging/channel-binding-schema.ts +51 -0
- package/src/messaging/providers/__tests__/callback-routing.test.ts +45 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +195 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +11 -0
- package/src/messaging/providers/a2a/deliver.ts +5 -1
- package/src/messaging/providers/a2a/transport.ts +10 -0
- package/src/messaging/providers/callback-routing.ts +48 -0
- package/src/messaging/providers/channel-transport.ts +55 -0
- package/src/messaging/providers/index.ts +65 -241
- package/src/messaging/providers/slack/binding-metadata.ts +62 -0
- package/src/messaging/providers/slack/transport.ts +92 -0
- package/src/messaging/providers/telegram-bot/transport.ts +51 -0
- package/src/messaging/providers/whatsapp/transport.ts +38 -0
- package/src/notifications/__tests__/broadcaster.test.ts +0 -8
- package/src/notifications/__tests__/connected-channels.test.ts +8 -36
- package/src/notifications/__tests__/destination-resolver.test.ts +12 -117
- package/src/notifications/destination-resolver.ts +7 -23
- package/src/notifications/emit-signal.ts +5 -11
- package/src/plugins/defaults/index.ts +0 -35
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +11 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +243 -2
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +167 -14
- package/src/plugins/disabled-state.ts +31 -0
- package/src/plugins/registry.ts +55 -12
- package/src/prompts/persona-resolver.ts +43 -11
- package/src/providers/call-site-routing.ts +41 -0
- package/src/providers/provider-send-message.ts +6 -0
- package/src/providers/ratelimit.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/providers/retry.ts +6 -0
- package/src/providers/types.ts +13 -0
- package/src/providers/usage-tracking.ts +6 -0
- package/src/runtime/__tests__/guardian-vellum-migration.test.ts +30 -27
- package/src/runtime/__tests__/local-principal-trust.test.ts +16 -18
- package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
- package/src/runtime/__tests__/trust-verdict-consumer.test.ts +115 -168
- package/src/runtime/access-request-helper.ts +1 -2
- package/src/runtime/actor-trust-resolver.ts +44 -17
- package/src/runtime/anchored-guardian.test.ts +7 -54
- package/src/runtime/anchored-guardian.ts +4 -53
- package/src/runtime/assistant-stream-state.ts +12 -74
- package/src/runtime/channel-reply-delivery.ts +3 -8
- package/src/runtime/guardian-vellum-migration.ts +18 -16
- package/src/runtime/invite-redemption-service.ts +25 -10
- package/src/runtime/local-actor-identity.test.ts +108 -0
- package/src/runtime/local-actor-identity.ts +27 -20
- package/src/runtime/member-verdict-cache.ts +0 -0
- package/src/runtime/routes/__tests__/contact-routes.test.ts +100 -7
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +1 -2
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +2 -1
- package/src/runtime/routes/contact-routes.ts +40 -25
- package/src/runtime/routes/conversation-list-routes.ts +1 -29
- package/src/runtime/routes/conversation-routes.ts +27 -7
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -10
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -8
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
- package/src/runtime/routes/settings-routes.ts +8 -3
- package/src/runtime/services/conversation-serializer.ts +6 -49
- package/src/runtime/slack-block-formatting.ts +0 -15
- package/src/runtime/trust-verdict-consumer.ts +36 -41
- package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
- package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
- package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +17 -39
- package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
- package/src/subagent/index.ts +1 -1
- package/src/subagent/manager.ts +245 -33
- package/src/subagent/types.ts +8 -1
- package/src/tools/registry.ts +10 -3
- package/src/tools/subagent/consult-deadline.ts +49 -0
- package/src/tools/subagent/spawn.ts +234 -5
- package/src/util/logger.ts +9 -0
- package/src/util/platform.ts +14 -0
- package/src/workspace/migrations/031-drop-user-md.ts +232 -148
- package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
- package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
- package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -314
- package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
- package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
- package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
- package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
- package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
- package/src/plugins/defaults/advisor/config.ts +0 -21
- package/src/plugins/defaults/advisor/consult.ts +0 -197
- package/src/plugins/defaults/advisor/context-pack.ts +0 -288
- package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
- package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
- package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
- package/src/plugins/defaults/advisor/package.json +0 -14
- package/src/plugins/defaults/advisor/tools/advisor.ts +0 -92
|
@@ -47,9 +47,8 @@ let nextTrust: ActorTrustContext;
|
|
|
47
47
|
const resolveActorTrustMock = mock(() => nextTrust);
|
|
48
48
|
// Override only `resolveActorTrust`; the real `trust-verdict-consumer` imports
|
|
49
49
|
// `toTrustContext` from this module, so the rest must pass through untouched.
|
|
50
|
-
const actorTrustResolverModule =
|
|
51
|
-
"../../runtime/actor-trust-resolver.js"
|
|
52
|
-
);
|
|
50
|
+
const actorTrustResolverModule =
|
|
51
|
+
await import("../../runtime/actor-trust-resolver.js");
|
|
53
52
|
mock.module("../../runtime/actor-trust-resolver.js", () => ({
|
|
54
53
|
...actorTrustResolverModule,
|
|
55
54
|
resolveActorTrust: resolveActorTrustMock,
|
|
@@ -92,13 +91,7 @@ function makeChannel(overrides: Partial<ContactChannel> = {}): ContactChannel {
|
|
|
92
91
|
address: "+12025550142",
|
|
93
92
|
isPrimary: true,
|
|
94
93
|
externalChatId: null,
|
|
95
|
-
status: "active",
|
|
96
|
-
policy: "allow",
|
|
97
|
-
verifiedAt: null,
|
|
98
|
-
verifiedVia: null,
|
|
99
94
|
inviteId: null,
|
|
100
|
-
revokedReason: null,
|
|
101
|
-
blockedReason: null,
|
|
102
95
|
lastSeenAt: null,
|
|
103
96
|
interactionCount: 0,
|
|
104
97
|
lastInteraction: null,
|
|
@@ -115,13 +108,12 @@ function makeContact(
|
|
|
115
108
|
id: "ct_1",
|
|
116
109
|
displayName: "Test Caller",
|
|
117
110
|
notes: null,
|
|
111
|
+
role: "contact",
|
|
118
112
|
lastInteraction: null,
|
|
119
113
|
interactionCount: 0,
|
|
120
114
|
createdAt: 0,
|
|
121
115
|
updatedAt: 0,
|
|
122
|
-
role: "contact",
|
|
123
116
|
contactType: "human",
|
|
124
|
-
principalId: null,
|
|
125
117
|
userFile: null,
|
|
126
118
|
channels: [],
|
|
127
119
|
...overrides,
|
|
@@ -138,11 +130,11 @@ function makeTrust(
|
|
|
138
130
|
): ActorTrustContext {
|
|
139
131
|
const memberRecord = channel
|
|
140
132
|
? {
|
|
141
|
-
contact: makeContact(
|
|
142
|
-
channel: makeChannel(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
133
|
+
contact: makeContact(),
|
|
134
|
+
channel: makeChannel(),
|
|
135
|
+
status: channel.status,
|
|
136
|
+
policy: channel.policy ?? "allow",
|
|
137
|
+
role: channel.role ?? "contact",
|
|
146
138
|
}
|
|
147
139
|
: null;
|
|
148
140
|
return {
|
|
@@ -404,8 +396,8 @@ function makeVerdict(overrides: Partial<TrustVerdict> = {}): TrustVerdict {
|
|
|
404
396
|
}
|
|
405
397
|
|
|
406
398
|
// A verdict carrying a fully-resolvable member ACL (contactId/channelId + valid
|
|
407
|
-
// known status·policy enums). The
|
|
408
|
-
//
|
|
399
|
+
// known status·policy enums). The verdict path builds a memberRecord from
|
|
400
|
+
// these, so it enforces blocked/revoked/deny.
|
|
409
401
|
function makeMemberVerdict(
|
|
410
402
|
trustClass: TrustVerdict["trustClass"],
|
|
411
403
|
channel: { status: string; policy?: string },
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* 3. Records guardian_action_delivery rows from pipeline delivery results
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
getGuardianDelivery,
|
|
12
|
+
guardianForChannel,
|
|
13
|
+
} from "../contacts/guardian-delivery-reader.js";
|
|
11
14
|
import {
|
|
12
15
|
createCanonicalGuardianRequest,
|
|
13
16
|
listCanonicalGuardianDeliveries,
|
|
@@ -86,16 +89,16 @@ async function dispatchGuardianQuestionInner(
|
|
|
86
89
|
try {
|
|
87
90
|
const expiresAt = Date.now() + getUserConsultationTimeoutMs();
|
|
88
91
|
|
|
89
|
-
// Resolve the request principal from the
|
|
90
|
-
// submitting actor (guardian-action-routes /
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
// request.guardianPrincipalId; sharing this
|
|
94
|
-
// stamped principal == the submitting principal
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
// Resolve the request principal from the gateway guardian delivery — the
|
|
93
|
+
// same source the submitting actor (guardian-action-routes /
|
|
94
|
+
// actor-trust-resolver) resolves, so they cannot diverge.
|
|
95
|
+
// applyCanonicalGuardianDecision requires strict equality with
|
|
96
|
+
// request.guardianPrincipalId; sharing this gateway source guarantees the
|
|
97
|
+
// stamped principal == the submitting principal.
|
|
98
|
+
const guardians = await getGuardianDelivery({ channelTypes: ["vellum"] });
|
|
99
|
+
const guardianPrincipalId = guardians
|
|
100
|
+
? (guardianForChannel(guardians, "vellum")?.principalId ?? undefined)
|
|
101
|
+
: undefined;
|
|
99
102
|
|
|
100
103
|
if (!guardianPrincipalId) {
|
|
101
104
|
log.error(
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
|
|
18
18
|
import type { ChannelId } from "../channels/types.js";
|
|
19
19
|
import { ipcCall } from "../ipc/gateway-client.js";
|
|
20
|
+
import { setMemberVerdict } from "../runtime/member-verdict-cache.js";
|
|
20
21
|
|
|
21
22
|
// Short IPC timeout so the read resolves promptly rather than stalling call
|
|
22
23
|
// setup on a gateway that accepts the socket but hangs.
|
|
@@ -36,7 +37,12 @@ export async function getInboundTrustVerdict(input: {
|
|
|
36
37
|
if (!result) return null;
|
|
37
38
|
|
|
38
39
|
const parsed = TrustVerdictSchema.safeParse(result.verdict);
|
|
39
|
-
|
|
40
|
+
if (!parsed.success) return null;
|
|
41
|
+
|
|
42
|
+
// Single choke point: warm the member-verdict cache so the sync trust
|
|
43
|
+
// fallback resolves the member without a local ACL read.
|
|
44
|
+
setMemberVerdict(input.channelType, input.actorExternalId, parsed.data);
|
|
45
|
+
return parsed.data;
|
|
40
46
|
} catch {
|
|
41
47
|
return null;
|
|
42
48
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { findContactChannel } from "../contacts/contact-store.js";
|
|
10
10
|
import { getCanonicalGuardianRequest } from "../memory/canonical-guardian-store.js";
|
|
11
11
|
import { emitNotificationSignal } from "../notifications/emit-signal.js";
|
|
12
|
+
import { getCachedMemberAcl } from "../runtime/member-verdict-cache.js";
|
|
12
13
|
import { getLogger } from "../util/logger.js";
|
|
13
14
|
import {
|
|
14
15
|
getGuardianWaitUpdateInitialIntervalMs,
|
|
@@ -252,12 +253,11 @@ export function emitAccessRequestCallbackHandoff(
|
|
|
252
253
|
address: fromNumber,
|
|
253
254
|
externalChatId: fromNumber,
|
|
254
255
|
});
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
requesterMemberId = contactResult.channel.id;
|
|
256
|
+
if (contactResult) {
|
|
257
|
+
const acl = getCachedMemberAcl("phone", fromNumber);
|
|
258
|
+
if (acl?.status === "active" && acl.policy === "allow") {
|
|
259
|
+
requesterMemberId = contactResult.channel.id;
|
|
260
|
+
}
|
|
261
261
|
}
|
|
262
262
|
} catch (err) {
|
|
263
263
|
log.warn(
|
|
@@ -13,6 +13,7 @@ import type { ServerWebSocket } from "bun";
|
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
15
|
getGuardianDelivery,
|
|
16
|
+
getGuardianDeliveryFresh,
|
|
16
17
|
voiceGuardianDisplayName,
|
|
17
18
|
} from "../contacts/guardian-delivery-reader.js";
|
|
18
19
|
import { getAssistantName } from "../daemon/identity-helpers.js";
|
|
@@ -601,7 +602,19 @@ export class RelayConnection {
|
|
|
601
602
|
const session = getCallSession(this.callSessionId);
|
|
602
603
|
this.recordSetupBookkeeping(session, msg);
|
|
603
604
|
|
|
604
|
-
|
|
605
|
+
// Prime the guardian displayName and warm the phone-channel guardian-
|
|
606
|
+
// delivery cache before routeSetup's SYNC resolveActorTrust fallback. That
|
|
607
|
+
// fallback reads the IO-free cache snapshot keyed per channel-filter; daemon
|
|
608
|
+
// startup warms only `vellum`, so a cold-process phone call would otherwise
|
|
609
|
+
// misclassify a guardian during a gateway verdict blip. Read fresh: gateway-
|
|
610
|
+
// side binding writes don't invalidate the daemon cache, so a stale empty
|
|
611
|
+
// snapshot from an earlier setup would survive the TTL otherwise. Both are
|
|
612
|
+
// independent IPC reads on different cache keys, so run them concurrently
|
|
613
|
+
// off the sync resolver's hot path.
|
|
614
|
+
await Promise.all([
|
|
615
|
+
this.primeGuardianDisplayName(),
|
|
616
|
+
getGuardianDeliveryFresh({ channelTypes: ["phone"] }),
|
|
617
|
+
]);
|
|
605
618
|
|
|
606
619
|
// Resolve the phone channel's inbound admission floor. The reader fails
|
|
607
620
|
// open to `null` by contract, so a transport hiccup admits the caller.
|
|
@@ -811,7 +824,7 @@ export class RelayConnection {
|
|
|
811
824
|
from,
|
|
812
825
|
trustClass: resolved.actorTrust.trustClass,
|
|
813
826
|
channelId: resolved.actorTrust.memberRecord?.channel.id,
|
|
814
|
-
memberPolicy: resolved.actorTrust.memberRecord?.
|
|
827
|
+
memberPolicy: resolved.actorTrust.memberRecord?.policy,
|
|
815
828
|
});
|
|
816
829
|
this.connectionState = "disconnecting";
|
|
817
830
|
updateCallSession(this.callSessionId, {
|
|
@@ -940,6 +953,13 @@ export class RelayConnection {
|
|
|
940
953
|
});
|
|
941
954
|
}
|
|
942
955
|
|
|
956
|
+
// Warm the phone-channel guardian-delivery cache before the SYNC
|
|
957
|
+
// resolveActorTrust fallback, which reads the IO-free per-channel snapshot
|
|
958
|
+
// that daemon startup leaves cold for `phone`. Read fresh: gateway-side
|
|
959
|
+
// binding writes don't invalidate the daemon cache, so a stale empty
|
|
960
|
+
// snapshot would otherwise survive the TTL and misclassify the guardian.
|
|
961
|
+
await getGuardianDeliveryFresh({ channelTypes: ["phone"] });
|
|
962
|
+
|
|
943
963
|
return toTrustContext(
|
|
944
964
|
resolveActorTrust({
|
|
945
965
|
assistantId,
|
|
@@ -246,7 +246,7 @@ export function routeSetup(ctx: SetupContext): {
|
|
|
246
246
|
? enforceAdmissionPolicy({
|
|
247
247
|
sourceChannel: "phone",
|
|
248
248
|
trustClass: actorTrust.trustClass,
|
|
249
|
-
memberStatus: actorTrust.memberRecord?.
|
|
249
|
+
memberStatus: actorTrust.memberRecord?.status,
|
|
250
250
|
policy: ctx.admissionPolicy!,
|
|
251
251
|
})
|
|
252
252
|
: ({ admitted: true } as const);
|
|
@@ -283,7 +283,7 @@ export function routeSetup(ctx: SetupContext): {
|
|
|
283
283
|
!pendingChallenge
|
|
284
284
|
) {
|
|
285
285
|
// Check for blocked caller
|
|
286
|
-
if (actorTrust.memberRecord?.
|
|
286
|
+
if (actorTrust.memberRecord?.status === "blocked") {
|
|
287
287
|
log.info(
|
|
288
288
|
{
|
|
289
289
|
callSessionId: ctx.callSessionId,
|
|
@@ -366,14 +366,14 @@ export function routeSetup(ctx: SetupContext): {
|
|
|
366
366
|
// gateway and assistant DBs) still get useful guidance instead of
|
|
367
367
|
// the "I don't recognize this number" name-capture script.
|
|
368
368
|
const unverifiedStatuses = new Set(["unverified", "pending"]);
|
|
369
|
-
const
|
|
370
|
-
if (
|
|
369
|
+
const member = actorTrust.memberRecord;
|
|
370
|
+
if (member && unverifiedStatuses.has(member.status)) {
|
|
371
371
|
log.info(
|
|
372
372
|
{
|
|
373
373
|
callSessionId: ctx.callSessionId,
|
|
374
374
|
from: ctx.from,
|
|
375
|
-
channelId:
|
|
376
|
-
channelStatus:
|
|
375
|
+
channelId: member.channel.id,
|
|
376
|
+
channelStatus: member.status,
|
|
377
377
|
},
|
|
378
378
|
"Inbound voice ACL: known but unverified caller — returning verification guidance",
|
|
379
379
|
);
|
|
@@ -382,8 +382,8 @@ export function routeSetup(ctx: SetupContext): {
|
|
|
382
382
|
action: "unverified_caller",
|
|
383
383
|
assistantId,
|
|
384
384
|
fromNumber: ctx.from,
|
|
385
|
-
displayName:
|
|
386
|
-
isGuardian:
|
|
385
|
+
displayName: member.contact.displayName,
|
|
386
|
+
isGuardian: member.role === "guardian",
|
|
387
387
|
},
|
|
388
388
|
resolved,
|
|
389
389
|
};
|
|
@@ -409,7 +409,7 @@ export function routeSetup(ctx: SetupContext): {
|
|
|
409
409
|
}
|
|
410
410
|
|
|
411
411
|
// Members with policy: 'deny'
|
|
412
|
-
if (actorTrust.memberRecord?.
|
|
412
|
+
if (actorTrust.memberRecord?.policy === "deny") {
|
|
413
413
|
log.info(
|
|
414
414
|
{
|
|
415
415
|
callSessionId: ctx.callSessionId,
|
|
@@ -430,7 +430,7 @@ export function routeSetup(ctx: SetupContext): {
|
|
|
430
430
|
}
|
|
431
431
|
|
|
432
432
|
// Members with policy: 'escalate' — live calls can't wait for approval
|
|
433
|
-
if (actorTrust.memberRecord?.
|
|
433
|
+
if (actorTrust.memberRecord?.policy === "escalate") {
|
|
434
434
|
log.info(
|
|
435
435
|
{
|
|
436
436
|
callSessionId: ctx.callSessionId,
|
|
@@ -142,6 +142,7 @@ mock.module("../../../util/logger.js", () => ({
|
|
|
142
142
|
truncateForLog: (value: string) => value,
|
|
143
143
|
pruneOldLogFiles: () => 0,
|
|
144
144
|
LOG_FILE_PATTERN: /^assistant-(\d{4}-\d{2}-\d{2})\.log$/,
|
|
145
|
+
getCurrentLogFilePath: () => "/tmp/test-assistant.log",
|
|
145
146
|
}));
|
|
146
147
|
|
|
147
148
|
const { registerConversationsCommand } = await import("../conversations.js");
|
|
@@ -8,13 +8,16 @@ import { shouldOutputJson, writeOutput } from "../output.js";
|
|
|
8
8
|
// IPC response shapes
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
|
|
11
|
+
// ACL fields (role, status, policy) are gateway-owned and not hydrated by the
|
|
12
|
+
// daemon-native filtered reads (`--query`/`--channel-address`/`--channel-type`),
|
|
13
|
+
// so they are optional here. The unfiltered default read carries them.
|
|
11
14
|
interface ContactChannel {
|
|
12
15
|
id: string;
|
|
13
16
|
contactId: string;
|
|
14
17
|
type: string;
|
|
15
18
|
address: string;
|
|
16
|
-
status
|
|
17
|
-
policy
|
|
19
|
+
status?: string;
|
|
20
|
+
policy?: string;
|
|
18
21
|
isPrimary?: boolean;
|
|
19
22
|
revokedReason?: string | null;
|
|
20
23
|
blockedReason?: string | null;
|
|
@@ -23,7 +26,7 @@ interface ContactChannel {
|
|
|
23
26
|
interface ContactWithChannels {
|
|
24
27
|
id: string;
|
|
25
28
|
displayName: string;
|
|
26
|
-
role
|
|
29
|
+
role?: string;
|
|
27
30
|
contactType: string;
|
|
28
31
|
notes?: string;
|
|
29
32
|
principalId?: string;
|
|
@@ -56,7 +59,7 @@ function formatContactTable(contacts: ContactWithChannels[]): string {
|
|
|
56
59
|
const rows = contacts.map((c) => [
|
|
57
60
|
c.id,
|
|
58
61
|
c.displayName,
|
|
59
|
-
`${c.role}/${c.contactType}`,
|
|
62
|
+
`${c.role ?? "—"}/${c.contactType}`,
|
|
60
63
|
String(c.channels.length),
|
|
61
64
|
]);
|
|
62
65
|
|
|
@@ -81,8 +84,8 @@ function formatChannelTable(channels: ContactChannel[]): string {
|
|
|
81
84
|
const rows = channels.map((ch) => {
|
|
82
85
|
const flags = [
|
|
83
86
|
ch.isPrimary ? "primary" : null,
|
|
84
|
-
ch.status !== "active" ? ch.status : null,
|
|
85
|
-
ch.policy !== "allow" ? ch.policy : null,
|
|
87
|
+
ch.status && ch.status !== "active" ? ch.status : null,
|
|
88
|
+
ch.policy && ch.policy !== "allow" ? ch.policy : null,
|
|
86
89
|
]
|
|
87
90
|
.filter(Boolean)
|
|
88
91
|
.join(", ");
|
|
@@ -124,7 +127,7 @@ function formatContactDetail(
|
|
|
124
127
|
const lines: string[] = [];
|
|
125
128
|
lines.push(`ID: ${c.id}`);
|
|
126
129
|
lines.push(`Display Name: ${c.displayName}`);
|
|
127
|
-
lines.push(`Role: ${c.role}`);
|
|
130
|
+
if (c.role) lines.push(`Role: ${c.role}`);
|
|
128
131
|
lines.push(`Type: ${c.contactType}`);
|
|
129
132
|
if (c.notes) lines.push(`Notes: ${c.notes}`);
|
|
130
133
|
if (c.principalId) lines.push(`Principal: ${c.principalId}`);
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Validates:
|
|
5
5
|
* - Subcommand registration (start, stop, status) under `memory worker`.
|
|
6
|
-
* - `status` reports
|
|
7
|
-
*
|
|
8
|
-
* - `
|
|
9
|
-
*
|
|
6
|
+
* - `status` reports the worker process state, `memory.worker.enabled`, and
|
|
7
|
+
* the synchronous in-process runner via PID/marker-file liveness.
|
|
8
|
+
* - `stop` sends SIGTERM to a live worker and disables the config flag, and
|
|
9
|
+
* still disables the flag (success) when no worker is running.
|
|
10
|
+
* - `start` spawns/reuses the worker process and enables the config flag.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -22,17 +23,51 @@ import { Command } from "commander";
|
|
|
22
23
|
|
|
23
24
|
let tmpDir: string;
|
|
24
25
|
let pidPath: string;
|
|
26
|
+
let markerPath: string;
|
|
25
27
|
let logOutput: string[] = [];
|
|
26
28
|
|
|
29
|
+
/** In-memory stand-in for the on-disk raw config the CLI reads/writes. */
|
|
30
|
+
let rawConfig: Record<string, unknown> = {};
|
|
31
|
+
|
|
27
32
|
/** Records (pid, signal) pairs passed to the mocked process.kill. */
|
|
28
33
|
let killCalls: Array<{ pid: number; signal: string | number }> = [];
|
|
29
34
|
|
|
35
|
+
function workerEnabledFromRaw(): boolean {
|
|
36
|
+
const memory = rawConfig.memory as { worker?: { enabled?: unknown } };
|
|
37
|
+
return memory?.worker?.enabled === true;
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
// ---------------------------------------------------------------------------
|
|
31
41
|
// Mocks
|
|
32
42
|
// ---------------------------------------------------------------------------
|
|
33
43
|
|
|
34
44
|
mock.module("../../../../util/platform.js", () => ({
|
|
35
45
|
getMemoryWorkerPidPath: () => pidPath,
|
|
46
|
+
getMemorySyncRunnerMarkerPath: () => markerPath,
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
mock.module("../../../../config/loader.js", () => ({
|
|
50
|
+
loadRawConfig: () => structuredClone(rawConfig),
|
|
51
|
+
saveRawConfig: (config: Record<string, unknown>) => {
|
|
52
|
+
rawConfig = structuredClone(config);
|
|
53
|
+
},
|
|
54
|
+
getConfigReadOnly: () => ({
|
|
55
|
+
memory: { worker: { enabled: workerEnabledFromRaw() } },
|
|
56
|
+
}),
|
|
57
|
+
setNestedValue: (
|
|
58
|
+
obj: Record<string, unknown>,
|
|
59
|
+
path: string,
|
|
60
|
+
value: unknown,
|
|
61
|
+
) => {
|
|
62
|
+
const keys = path.split(".");
|
|
63
|
+
let cur = obj;
|
|
64
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
65
|
+
const k = keys[i];
|
|
66
|
+
if (cur[k] == null || typeof cur[k] !== "object") cur[k] = {};
|
|
67
|
+
cur = cur[k] as Record<string, unknown>;
|
|
68
|
+
}
|
|
69
|
+
cur[keys[keys.length - 1]] = value;
|
|
70
|
+
},
|
|
36
71
|
}));
|
|
37
72
|
|
|
38
73
|
const capture = (...args: unknown[]) => {
|
|
@@ -47,6 +82,7 @@ const fakeLogger = {
|
|
|
47
82
|
mock.module("../../../../util/logger.js", () => ({
|
|
48
83
|
getLogger: () => fakeLogger,
|
|
49
84
|
getCliLogger: () => fakeLogger,
|
|
85
|
+
getCurrentLogFilePath: () => `${tmpDir}/assistant-test-mock.log`,
|
|
50
86
|
}));
|
|
51
87
|
|
|
52
88
|
// ---------------------------------------------------------------------------
|
|
@@ -134,8 +170,10 @@ function stubProcessKill(livePids: Set<number>): () => void {
|
|
|
134
170
|
beforeEach(() => {
|
|
135
171
|
tmpDir = mkdtempSync(join(tmpdir(), "memory-worker-test-"));
|
|
136
172
|
pidPath = join(tmpDir, "memory-worker.pid");
|
|
173
|
+
markerPath = join(tmpDir, "memory-sync-runner.pid");
|
|
137
174
|
logOutput = [];
|
|
138
175
|
killCalls = [];
|
|
176
|
+
rawConfig = {};
|
|
139
177
|
process.exitCode = 0;
|
|
140
178
|
});
|
|
141
179
|
|
|
@@ -164,7 +202,7 @@ describe("subcommand registration", () => {
|
|
|
164
202
|
// ---------------------------------------------------------------------------
|
|
165
203
|
|
|
166
204
|
describe("memory worker status", () => {
|
|
167
|
-
test("reports not_running when
|
|
205
|
+
test("reports not_running with workerEnabled and syncRunner when nothing is running", async () => {
|
|
168
206
|
const { exitCode, stdout } = await runCommand([
|
|
169
207
|
"memory",
|
|
170
208
|
"worker",
|
|
@@ -172,7 +210,11 @@ describe("memory worker status", () => {
|
|
|
172
210
|
"--json",
|
|
173
211
|
]);
|
|
174
212
|
expect(exitCode).toBe(0);
|
|
175
|
-
expect(JSON.parse(stdout)).toEqual({
|
|
213
|
+
expect(JSON.parse(stdout)).toEqual({
|
|
214
|
+
status: "not_running",
|
|
215
|
+
workerEnabled: false,
|
|
216
|
+
syncRunner: { status: "not_running" },
|
|
217
|
+
});
|
|
176
218
|
});
|
|
177
219
|
|
|
178
220
|
test("reports running when PID file points at a live process", async () => {
|
|
@@ -189,6 +231,45 @@ describe("memory worker status", () => {
|
|
|
189
231
|
expect(JSON.parse(stdout)).toEqual({
|
|
190
232
|
status: "running",
|
|
191
233
|
pid: process.pid,
|
|
234
|
+
workerEnabled: false,
|
|
235
|
+
syncRunner: { status: "not_running" },
|
|
236
|
+
});
|
|
237
|
+
} finally {
|
|
238
|
+
restore();
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("reflects memory.worker.enabled from config", async () => {
|
|
243
|
+
rawConfig = { memory: { worker: { enabled: true } } };
|
|
244
|
+
const restore = stubProcessKill(new Set());
|
|
245
|
+
try {
|
|
246
|
+
const { exitCode, stdout } = await runCommand([
|
|
247
|
+
"memory",
|
|
248
|
+
"worker",
|
|
249
|
+
"status",
|
|
250
|
+
"--json",
|
|
251
|
+
]);
|
|
252
|
+
expect(exitCode).toBe(0);
|
|
253
|
+
expect(JSON.parse(stdout)).toMatchObject({ workerEnabled: true });
|
|
254
|
+
} finally {
|
|
255
|
+
restore();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("reports the synchronous runner as running when its marker is live", async () => {
|
|
260
|
+
writeFileSync(markerPath, String(process.pid));
|
|
261
|
+
const restore = stubProcessKill(new Set([process.pid]));
|
|
262
|
+
try {
|
|
263
|
+
const { exitCode, stdout } = await runCommand([
|
|
264
|
+
"memory",
|
|
265
|
+
"worker",
|
|
266
|
+
"status",
|
|
267
|
+
"--json",
|
|
268
|
+
]);
|
|
269
|
+
expect(exitCode).toBe(0);
|
|
270
|
+
expect(JSON.parse(stdout)).toMatchObject({
|
|
271
|
+
status: "not_running",
|
|
272
|
+
syncRunner: { status: "running", pid: process.pid },
|
|
192
273
|
});
|
|
193
274
|
} finally {
|
|
194
275
|
restore();
|
|
@@ -206,7 +287,11 @@ describe("memory worker status", () => {
|
|
|
206
287
|
"--json",
|
|
207
288
|
]);
|
|
208
289
|
expect(exitCode).toBe(0);
|
|
209
|
-
expect(JSON.parse(stdout)).toEqual({
|
|
290
|
+
expect(JSON.parse(stdout)).toEqual({
|
|
291
|
+
status: "not_running",
|
|
292
|
+
workerEnabled: false,
|
|
293
|
+
syncRunner: { status: "not_running" },
|
|
294
|
+
});
|
|
210
295
|
expect(existsSync(pidPath)).toBe(false);
|
|
211
296
|
} finally {
|
|
212
297
|
restore();
|
|
@@ -219,18 +304,25 @@ describe("memory worker status", () => {
|
|
|
219
304
|
// ---------------------------------------------------------------------------
|
|
220
305
|
|
|
221
306
|
describe("memory worker stop", () => {
|
|
222
|
-
test("
|
|
307
|
+
test("disables the config flag and succeeds when no worker is running", async () => {
|
|
308
|
+
rawConfig = { memory: { worker: { enabled: true } } };
|
|
223
309
|
const { exitCode, stdout } = await runCommand([
|
|
224
310
|
"memory",
|
|
225
311
|
"worker",
|
|
226
312
|
"stop",
|
|
227
313
|
"--json",
|
|
228
314
|
]);
|
|
229
|
-
expect(exitCode).toBe(
|
|
230
|
-
expect(JSON.parse(stdout)).toMatchObject({
|
|
315
|
+
expect(exitCode).toBe(0);
|
|
316
|
+
expect(JSON.parse(stdout)).toMatchObject({
|
|
317
|
+
ok: true,
|
|
318
|
+
workerWasRunning: false,
|
|
319
|
+
workerEnabled: false,
|
|
320
|
+
});
|
|
321
|
+
expect(workerEnabledFromRaw()).toBe(false);
|
|
231
322
|
});
|
|
232
323
|
|
|
233
|
-
test("sends SIGTERM to a running worker", async () => {
|
|
324
|
+
test("sends SIGTERM to a running worker and disables the flag", async () => {
|
|
325
|
+
rawConfig = { memory: { worker: { enabled: true } } };
|
|
234
326
|
writeFileSync(pidPath, String(process.pid));
|
|
235
327
|
const restore = stubProcessKill(new Set([process.pid]));
|
|
236
328
|
try {
|
|
@@ -241,11 +333,16 @@ describe("memory worker stop", () => {
|
|
|
241
333
|
"--json",
|
|
242
334
|
]);
|
|
243
335
|
expect(exitCode).toBe(0);
|
|
244
|
-
expect(JSON.parse(stdout)).toEqual({
|
|
336
|
+
expect(JSON.parse(stdout)).toEqual({
|
|
337
|
+
ok: true,
|
|
338
|
+
pid: process.pid,
|
|
339
|
+
workerEnabled: false,
|
|
340
|
+
});
|
|
245
341
|
expect(killCalls).toContainEqual({
|
|
246
342
|
pid: process.pid,
|
|
247
343
|
signal: "SIGTERM",
|
|
248
344
|
});
|
|
345
|
+
expect(workerEnabledFromRaw()).toBe(false);
|
|
249
346
|
} finally {
|
|
250
347
|
restore();
|
|
251
348
|
}
|
|
@@ -257,7 +354,7 @@ describe("memory worker stop", () => {
|
|
|
257
354
|
// ---------------------------------------------------------------------------
|
|
258
355
|
|
|
259
356
|
describe("memory worker start", () => {
|
|
260
|
-
test("
|
|
357
|
+
test("reuses an already-running worker and enables the flag", async () => {
|
|
261
358
|
writeFileSync(pidPath, String(process.pid));
|
|
262
359
|
const restore = stubProcessKill(new Set([process.pid]));
|
|
263
360
|
try {
|
|
@@ -267,17 +364,20 @@ describe("memory worker start", () => {
|
|
|
267
364
|
"start",
|
|
268
365
|
"--json",
|
|
269
366
|
]);
|
|
270
|
-
expect(exitCode).toBe(
|
|
367
|
+
expect(exitCode).toBe(0);
|
|
271
368
|
expect(JSON.parse(stdout)).toMatchObject({
|
|
272
|
-
ok:
|
|
369
|
+
ok: true,
|
|
273
370
|
pid: process.pid,
|
|
371
|
+
alreadyRunning: true,
|
|
372
|
+
workerEnabled: true,
|
|
274
373
|
});
|
|
374
|
+
expect(workerEnabledFromRaw()).toBe(true);
|
|
275
375
|
} finally {
|
|
276
376
|
restore();
|
|
277
377
|
}
|
|
278
378
|
});
|
|
279
379
|
|
|
280
|
-
test("spawns the worker
|
|
380
|
+
test("spawns the worker, reports its PID, and enables the flag", async () => {
|
|
281
381
|
const restore = stubProcessKill(new Set());
|
|
282
382
|
const originalSpawn = Bun.spawn;
|
|
283
383
|
// Simulate the spawned worker writing its PID file on startup.
|
|
@@ -293,7 +393,37 @@ describe("memory worker start", () => {
|
|
|
293
393
|
"--json",
|
|
294
394
|
]);
|
|
295
395
|
expect(exitCode).toBe(0);
|
|
296
|
-
expect(JSON.parse(stdout)).toMatchObject({
|
|
396
|
+
expect(JSON.parse(stdout)).toMatchObject({
|
|
397
|
+
ok: true,
|
|
398
|
+
pid: 424242,
|
|
399
|
+
workerEnabled: true,
|
|
400
|
+
});
|
|
401
|
+
expect(workerEnabledFromRaw()).toBe(true);
|
|
402
|
+
} finally {
|
|
403
|
+
(Bun as { spawn: typeof Bun.spawn }).spawn = originalSpawn;
|
|
404
|
+
restore();
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("leaves the config flag untouched when the spawn fails", async () => {
|
|
409
|
+
const restore = stubProcessKill(new Set());
|
|
410
|
+
const originalSpawn = Bun.spawn;
|
|
411
|
+
// Spawn returns but never writes a PID file → spawnMemoryWorkerProcess
|
|
412
|
+
// throws after its wait loop, so the flag must stay disabled.
|
|
413
|
+
(Bun as { spawn: typeof Bun.spawn }).spawn = (() => ({
|
|
414
|
+
unref: () => {},
|
|
415
|
+
pid: 0,
|
|
416
|
+
})) as unknown as typeof Bun.spawn;
|
|
417
|
+
try {
|
|
418
|
+
const { exitCode, stdout } = await runCommand([
|
|
419
|
+
"memory",
|
|
420
|
+
"worker",
|
|
421
|
+
"start",
|
|
422
|
+
"--json",
|
|
423
|
+
]);
|
|
424
|
+
expect(exitCode).toBe(1);
|
|
425
|
+
expect(JSON.parse(stdout)).toMatchObject({ ok: false });
|
|
426
|
+
expect(workerEnabledFromRaw()).toBe(false);
|
|
297
427
|
} finally {
|
|
298
428
|
(Bun as { spawn: typeof Bun.spawn }).spawn = originalSpawn;
|
|
299
429
|
restore();
|