@vellumai/assistant 0.5.6 → 0.5.7
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/.env.example +16 -2
- package/ARCHITECTURE.md +6 -75
- package/Dockerfile +1 -1
- package/README.md +0 -2
- package/bun.lock +0 -414
- package/docs/architecture/keychain-broker.md +45 -240
- package/docs/architecture/security.md +0 -17
- package/docs/credential-execution-service.md +2 -2
- package/node_modules/@vellumai/ces-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +119 -0
- package/node_modules/@vellumai/credential-storage/package.json +1 -0
- package/node_modules/@vellumai/egress-proxy/package.json +1 -0
- package/package.json +2 -3
- package/src/__tests__/actor-token-service.test.ts +0 -114
- package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
- package/src/__tests__/browser-skill-endstate.test.ts +6 -5
- package/src/__tests__/btw-routes.test.ts +0 -39
- package/src/__tests__/call-domain.test.ts +0 -128
- package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
- package/src/__tests__/channel-approval-routes.test.ts +0 -5
- package/src/__tests__/channel-readiness-service.test.ts +1 -60
- package/src/__tests__/checker.test.ts +4 -2
- package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
- package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
- package/src/__tests__/conversation-skill-tools.test.ts +0 -54
- package/src/__tests__/conversation-title-service.test.ts +87 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +28 -14
- package/src/__tests__/credential-execution-managed-contract.test.ts +33 -18
- package/src/__tests__/credential-security-e2e.test.ts +0 -66
- package/src/__tests__/credential-security-invariants.test.ts +4 -45
- package/src/__tests__/credentials-cli.test.ts +78 -0
- package/src/__tests__/db-migration-rollback.test.ts +2015 -1
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
- package/src/__tests__/guardian-routing-state.test.ts +0 -5
- package/src/__tests__/host-shell-tool.test.ts +6 -7
- package/src/__tests__/http-user-message-parity.test.ts +3 -103
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
- package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
- package/src/__tests__/intent-routing.test.ts +0 -13
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
- package/src/__tests__/keychain-broker-client.test.ts +161 -22
- package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
- package/src/__tests__/migration-export-http.test.ts +2 -2
- package/src/__tests__/migration-import-commit-http.test.ts +2 -2
- package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
- package/src/__tests__/migration-validate-http.test.ts +2 -2
- package/src/__tests__/non-member-access-request.test.ts +0 -5
- package/src/__tests__/notification-decision-fallback.test.ts +4 -0
- package/src/__tests__/notification-decision-identity.test.ts +4 -0
- package/src/__tests__/permission-types.test.ts +1 -0
- package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
- package/src/__tests__/qdrant-manager.test.ts +28 -2
- package/src/__tests__/registry.test.ts +0 -6
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
- package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
- package/src/__tests__/secure-keys.test.ts +83 -263
- package/src/__tests__/shell-identity.test.ts +96 -6
- package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
- package/src/__tests__/skill-feature-flags.test.ts +46 -45
- package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
- package/src/__tests__/skill-load-inline-command.test.ts +8 -12
- package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
- package/src/__tests__/skill-load-tool.test.ts +0 -2
- package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
- package/src/__tests__/skills.test.ts +0 -2
- package/src/__tests__/slack-inbound-verification.test.ts +0 -4
- package/src/__tests__/suggestion-routes.test.ts +1 -32
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
- package/src/__tests__/update-bulletin.test.ts +0 -2
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -6
- package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +218 -0
- package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
- package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
- package/src/calls/audio-store.test.ts +97 -0
- package/src/calls/audio-store.ts +205 -0
- package/src/calls/call-controller.ts +85 -7
- package/src/calls/call-domain.ts +3 -0
- package/src/calls/call-store.ts +10 -3
- package/src/calls/fish-audio-client.ts +117 -0
- package/src/calls/relay-server.ts +27 -0
- package/src/calls/twilio-routes.ts +2 -1
- package/src/calls/types.ts +1 -0
- package/src/calls/voice-ingress-preflight.ts +0 -42
- package/src/calls/voice-quality.ts +26 -5
- package/src/calls/voice-session-bridge.ts +6 -12
- package/src/cli/commands/config.ts +1 -4
- package/src/cli/commands/credentials.ts +34 -4
- package/src/cli/commands/oauth/index.ts +7 -0
- package/src/cli/commands/oauth/platform.ts +179 -0
- package/src/cli/commands/platform.ts +3 -3
- package/src/config/assistant-feature-flags.ts +186 -5
- package/src/config/bundled-skills/messaging/SKILL.md +5 -5
- package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
- package/src/config/bundled-skills/settings/TOOLS.json +2 -2
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
- package/src/config/bundled-tool-registry.ts +1 -11
- package/src/config/env-registry.ts +1 -1
- package/src/config/env.ts +8 -14
- package/src/config/feature-flag-registry.json +48 -8
- package/src/config/loader.ts +98 -31
- package/src/config/schema.ts +4 -13
- package/src/config/schemas/calls.ts +13 -0
- package/src/config/schemas/fish-audio.ts +39 -0
- package/src/config/schemas/security.ts +0 -4
- package/src/config/types.ts +0 -1
- package/src/contacts/contact-store.ts +39 -0
- package/src/contacts/types.ts +2 -0
- package/src/credential-execution/approval-bridge.ts +1 -0
- package/src/credential-execution/executable-discovery.ts +28 -4
- package/src/credential-execution/feature-gates.ts +16 -0
- package/src/credential-execution/process-manager.ts +38 -0
- package/src/daemon/assistant-attachments.ts +9 -0
- package/src/daemon/config-watcher.ts +5 -0
- package/src/daemon/conversation-tool-setup.ts +0 -105
- package/src/daemon/conversation.ts +10 -1
- package/src/daemon/handlers/config-vercel.ts +92 -0
- package/src/daemon/handlers/skills.ts +2 -15
- package/src/daemon/install-symlink.ts +195 -0
- package/src/daemon/lifecycle.ts +227 -51
- package/src/daemon/message-types/conversations.ts +3 -4
- package/src/daemon/message-types/diagnostics.ts +3 -22
- package/src/daemon/message-types/messages.ts +0 -2
- package/src/daemon/message-types/upgrades.ts +8 -0
- package/src/daemon/server.ts +30 -92
- package/src/events/domain-events.ts +2 -1
- package/src/inbound/platform-callback-registration.ts +3 -3
- package/src/instrument.ts +8 -5
- package/src/memory/conversation-title-service.ts +50 -1
- package/src/memory/db-init.ts +12 -0
- package/src/memory/items-extractor.ts +15 -1
- package/src/memory/job-handlers/conversation-starters.ts +4 -1
- package/src/memory/jobs-store.ts +30 -5
- package/src/memory/jobs-worker.ts +31 -7
- package/src/memory/migrations/001-job-deferrals.ts +19 -0
- package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
- package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
- package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
- package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
- package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
- package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
- package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
- package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
- package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
- package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
- package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
- package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
- package/src/memory/migrations/116-messages-fts.ts +106 -1
- package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
- package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
- package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
- package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
- package/src/memory/migrations/141-rename-verification-table.ts +54 -0
- package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
- package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
- package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
- package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
- package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
- package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
- package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
- package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
- package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
- package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
- package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
- package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
- package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
- package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/migrations/registry.ts +90 -0
- package/src/memory/migrations/validate-migration-state.ts +137 -11
- package/src/memory/qdrant-circuit-breaker.ts +9 -0
- package/src/memory/qdrant-manager.ts +64 -7
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/contacts.ts +1 -0
- package/src/notifications/decision-engine.ts +4 -1
- package/src/oauth/connection-resolver.ts +6 -4
- package/src/permissions/checker.ts +0 -38
- package/src/permissions/shell-identity.ts +76 -22
- package/src/permissions/types.ts +4 -2
- package/src/platform/client.ts +35 -7
- package/src/prompts/persona-resolver.ts +138 -0
- package/src/prompts/system-prompt.ts +36 -4
- package/src/prompts/templates/users/default.md +1 -0
- package/src/providers/registry.ts +27 -40
- package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
- package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
- package/src/runtime/auth/external-assistant-id.ts +13 -59
- package/src/runtime/auth/route-policy.ts +15 -1
- package/src/runtime/auth/token-service.ts +43 -138
- package/src/runtime/channel-readiness-service.ts +1 -16
- package/src/runtime/http-server.ts +27 -2
- package/src/runtime/middleware/error-handler.ts +1 -9
- package/src/runtime/routes/audio-routes.ts +40 -0
- package/src/runtime/routes/btw-routes.ts +0 -17
- package/src/runtime/routes/conversation-query-routes.ts +63 -1
- package/src/runtime/routes/conversation-routes.ts +4 -44
- package/src/runtime/routes/diagnostics-routes.ts +1 -477
- package/src/runtime/routes/identity-routes.ts +18 -29
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
- package/src/runtime/routes/integrations/vercel.ts +89 -0
- package/src/runtime/routes/log-export-routes.ts +5 -0
- package/src/runtime/routes/memory-item-routes.ts +24 -6
- package/src/runtime/routes/migration-rollback-routes.ts +209 -0
- package/src/runtime/routes/migration-routes.ts +17 -1
- package/src/runtime/routes/notification-routes.ts +58 -0
- package/src/runtime/routes/schedule-routes.ts +65 -0
- package/src/runtime/routes/settings-routes.ts +41 -1
- package/src/runtime/routes/tts-routes.ts +86 -0
- package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
- package/src/runtime/routes/workspace-commit-routes.ts +62 -0
- package/src/runtime/routes/workspace-routes.test.ts +22 -1
- package/src/runtime/routes/workspace-routes.ts +1 -1
- package/src/runtime/routes/workspace-utils.ts +86 -2
- package/src/security/ces-credential-client.ts +59 -22
- package/src/security/ces-rpc-credential-backend.ts +85 -0
- package/src/security/credential-backend.ts +12 -88
- package/src/security/keychain-broker-client.ts +10 -2
- package/src/security/secure-keys.ts +94 -113
- package/src/skills/catalog-install.ts +13 -7
- package/src/telemetry/usage-telemetry-reporter.ts +4 -2
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/executor.ts +0 -4
- package/src/tools/network/script-proxy/session-manager.ts +19 -4
- package/src/tools/network/web-fetch.ts +3 -1
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/types.ts +0 -8
- package/src/util/errors.ts +0 -12
- package/src/util/platform.ts +3 -50
- package/src/workspace/git-service.ts +5 -2
- package/src/workspace/migrations/001-avatar-rename.ts +15 -0
- package/src/workspace/migrations/003-seed-device-id.ts +17 -1
- package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
- package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
- package/src/workspace/migrations/006-services-config.ts +49 -0
- package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
- package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
- package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
- package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
- package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
- package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
- package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
- package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
- package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
- package/src/workspace/migrations/017-seed-persona-dirs.ts +95 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +23 -1
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/workspace/migrations/runner.ts +106 -2
- package/src/workspace/migrations/types.ts +4 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
- package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
- package/src/__tests__/diagnostics-export.test.ts +0 -288
- package/src/__tests__/local-gateway-health.test.ts +0 -209
- package/src/__tests__/secret-ingress-handler.test.ts +0 -120
- package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
- package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
- package/src/__tests__/swarm-orchestrator.test.ts +0 -463
- package/src/__tests__/swarm-plan-validator.test.ts +0 -384
- package/src/__tests__/swarm-recursion.test.ts +0 -197
- package/src/__tests__/swarm-router-planner.test.ts +0 -234
- package/src/__tests__/swarm-tool.test.ts +0 -185
- package/src/__tests__/swarm-worker-backend.test.ts +0 -144
- package/src/__tests__/swarm-worker-runner.test.ts +0 -288
- package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
- package/src/commands/cc-command-registry.ts +0 -248
- package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
- package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
- package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
- package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
- package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
- package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
- package/src/config/schemas/swarm.ts +0 -82
- package/src/logfire.ts +0 -135
- package/src/runtime/local-gateway-health.ts +0 -275
- package/src/security/secret-ingress.ts +0 -68
- package/src/swarm/backend-claude-code.ts +0 -225
- package/src/swarm/checkpoint.ts +0 -137
- package/src/swarm/graph-utils.ts +0 -53
- package/src/swarm/index.ts +0 -55
- package/src/swarm/limits.ts +0 -66
- package/src/swarm/orchestrator.ts +0 -424
- package/src/swarm/plan-validator.ts +0 -117
- package/src/swarm/router-planner.ts +0 -162
- package/src/swarm/router-prompts.ts +0 -39
- package/src/swarm/synthesizer.ts +0 -81
- package/src/swarm/types.ts +0 -72
- package/src/swarm/worker-backend.ts +0 -131
- package/src/swarm/worker-prompts.ts +0 -80
- package/src/swarm/worker-runner.ts +0 -170
- package/src/tools/claude-code/claude-code.ts +0 -610
- package/src/tools/swarm/delegate.ts +0 -205
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
afterAll,
|
|
6
|
+
beforeAll,
|
|
7
|
+
beforeEach,
|
|
8
|
+
describe,
|
|
9
|
+
expect,
|
|
10
|
+
mock,
|
|
11
|
+
test,
|
|
12
|
+
} from "bun:test";
|
|
13
|
+
|
|
14
|
+
const testDir = mkdtempSync(join(tmpdir(), "jobs-store-qdrant-breaker-"));
|
|
15
|
+
|
|
16
|
+
mock.module("../util/platform.js", () => ({
|
|
17
|
+
getDataDir: () => testDir,
|
|
18
|
+
isMacOS: () => process.platform === "darwin",
|
|
19
|
+
isLinux: () => process.platform === "linux",
|
|
20
|
+
isWindows: () => process.platform === "win32",
|
|
21
|
+
getPidPath: () => join(testDir, "test.pid"),
|
|
22
|
+
getDbPath: () => join(testDir, "test.db"),
|
|
23
|
+
getLogPath: () => join(testDir, "test.log"),
|
|
24
|
+
ensureDataDir: () => {},
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
mock.module("../util/logger.js", () => ({
|
|
28
|
+
getLogger: () =>
|
|
29
|
+
new Proxy({} as Record<string, unknown>, {
|
|
30
|
+
get: () => () => {},
|
|
31
|
+
}),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
mock.module("../config/loader.js", () => ({
|
|
35
|
+
loadConfig: () => ({}),
|
|
36
|
+
getConfig: () => ({}),
|
|
37
|
+
invalidateConfigCache: () => {},
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
import { getDb, initializeDb, resetDb } from "../memory/db.js";
|
|
41
|
+
import {
|
|
42
|
+
claimMemoryJobs,
|
|
43
|
+
enqueueMemoryJob,
|
|
44
|
+
type MemoryJobType,
|
|
45
|
+
} from "../memory/jobs-store.js";
|
|
46
|
+
import {
|
|
47
|
+
_resetQdrantBreaker,
|
|
48
|
+
withQdrantBreaker,
|
|
49
|
+
} from "../memory/qdrant-circuit-breaker.js";
|
|
50
|
+
|
|
51
|
+
describe("claimMemoryJobs with Qdrant circuit breaker", () => {
|
|
52
|
+
beforeAll(() => {
|
|
53
|
+
initializeDb();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
const db = getDb();
|
|
58
|
+
db.run("DELETE FROM memory_jobs");
|
|
59
|
+
_resetQdrantBreaker();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
afterAll(() => {
|
|
63
|
+
resetDb();
|
|
64
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("claims embed jobs when circuit breaker is closed (healthy)", () => {
|
|
68
|
+
enqueueMemoryJob("embed_segment", { segmentId: "seg-1" });
|
|
69
|
+
enqueueMemoryJob("embed_item", { itemId: "item-1" });
|
|
70
|
+
enqueueMemoryJob("extract_items", { conversationId: "conv-1" });
|
|
71
|
+
|
|
72
|
+
const claimed = claimMemoryJobs(10);
|
|
73
|
+
const types = claimed.map((j) => j.type);
|
|
74
|
+
|
|
75
|
+
expect(types).toContain("embed_segment");
|
|
76
|
+
expect(types).toContain("embed_item");
|
|
77
|
+
expect(types).toContain("extract_items");
|
|
78
|
+
expect(claimed).toHaveLength(3);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("skips embed jobs when circuit breaker is open", async () => {
|
|
82
|
+
// Trip the circuit breaker by recording 5 consecutive failures
|
|
83
|
+
for (let i = 0; i < 5; i++) {
|
|
84
|
+
try {
|
|
85
|
+
await withQdrantBreaker(async () => {
|
|
86
|
+
throw new Error("simulated qdrant failure");
|
|
87
|
+
});
|
|
88
|
+
} catch {
|
|
89
|
+
// expected
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
enqueueMemoryJob("embed_segment", { segmentId: "seg-1" });
|
|
94
|
+
enqueueMemoryJob("embed_item", { itemId: "item-1" });
|
|
95
|
+
enqueueMemoryJob("embed_summary", { summaryId: "sum-1" });
|
|
96
|
+
enqueueMemoryJob("extract_items", { conversationId: "conv-1" });
|
|
97
|
+
enqueueMemoryJob("build_conversation_summary", {
|
|
98
|
+
conversationId: "conv-1",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const claimed = claimMemoryJobs(10);
|
|
102
|
+
const types = claimed.map((j) => j.type);
|
|
103
|
+
|
|
104
|
+
// Only non-embed jobs should be claimed
|
|
105
|
+
expect(types).toContain("extract_items");
|
|
106
|
+
expect(types).toContain("build_conversation_summary");
|
|
107
|
+
expect(types).not.toContain("embed_segment");
|
|
108
|
+
expect(types).not.toContain("embed_item");
|
|
109
|
+
expect(types).not.toContain("embed_summary");
|
|
110
|
+
expect(claimed).toHaveLength(2);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("resumes claiming embed jobs after circuit breaker closes", async () => {
|
|
114
|
+
// Trip the circuit breaker
|
|
115
|
+
for (let i = 0; i < 5; i++) {
|
|
116
|
+
try {
|
|
117
|
+
await withQdrantBreaker(async () => {
|
|
118
|
+
throw new Error("simulated qdrant failure");
|
|
119
|
+
});
|
|
120
|
+
} catch {
|
|
121
|
+
// expected
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Verify embed jobs are skipped while open
|
|
126
|
+
enqueueMemoryJob("embed_segment", { segmentId: "seg-1" });
|
|
127
|
+
enqueueMemoryJob("extract_items", { conversationId: "conv-1" });
|
|
128
|
+
|
|
129
|
+
const claimedWhileOpen = claimMemoryJobs(10);
|
|
130
|
+
expect(claimedWhileOpen.map((j) => j.type)).not.toContain("embed_segment");
|
|
131
|
+
|
|
132
|
+
// Reset breaker (simulates successful probe closing the circuit)
|
|
133
|
+
_resetQdrantBreaker();
|
|
134
|
+
|
|
135
|
+
// Re-enqueue an embed job (the previous one is now "running")
|
|
136
|
+
enqueueMemoryJob("embed_item", { itemId: "item-2" });
|
|
137
|
+
|
|
138
|
+
const claimedAfterClose = claimMemoryJobs(10);
|
|
139
|
+
const types = claimedAfterClose.map((j) => j.type);
|
|
140
|
+
|
|
141
|
+
expect(types).toContain("embed_item");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("all embed job types are skipped when breaker is open", async () => {
|
|
145
|
+
const embedTypes: MemoryJobType[] = [
|
|
146
|
+
"embed_segment",
|
|
147
|
+
"embed_item",
|
|
148
|
+
"embed_summary",
|
|
149
|
+
"embed_media",
|
|
150
|
+
"embed_attachment",
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
// Trip the circuit breaker
|
|
154
|
+
for (let i = 0; i < 5; i++) {
|
|
155
|
+
try {
|
|
156
|
+
await withQdrantBreaker(async () => {
|
|
157
|
+
throw new Error("simulated qdrant failure");
|
|
158
|
+
});
|
|
159
|
+
} catch {
|
|
160
|
+
// expected
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Enqueue one of each embed type
|
|
165
|
+
for (const type of embedTypes) {
|
|
166
|
+
enqueueMemoryJob(type, { id: `test-${type}` });
|
|
167
|
+
}
|
|
168
|
+
// Also enqueue a non-embed job
|
|
169
|
+
enqueueMemoryJob("extract_entities", { conversationId: "conv-1" });
|
|
170
|
+
|
|
171
|
+
const claimed = claimMemoryJobs(20);
|
|
172
|
+
const types = claimed.map((j) => j.type);
|
|
173
|
+
|
|
174
|
+
// Only the non-embed job should be claimed
|
|
175
|
+
expect(claimed).toHaveLength(1);
|
|
176
|
+
expect(types).toEqual(["extract_entities"]);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -537,8 +537,8 @@ describe("keychain-broker-client", () => {
|
|
|
537
537
|
writeFileSync(SOCKET_PATH, "");
|
|
538
538
|
expect(client.isAvailable()).toBe(false);
|
|
539
539
|
|
|
540
|
-
// Advance time past the first cooldown (
|
|
541
|
-
fakeNow +=
|
|
540
|
+
// Advance time past the first cooldown (5s)
|
|
541
|
+
fakeNow += 5_001;
|
|
542
542
|
expect(client.isAvailable()).toBe(true);
|
|
543
543
|
|
|
544
544
|
// Now start a real broker and verify the client reconnects
|
|
@@ -558,8 +558,8 @@ describe("keychain-broker-client", () => {
|
|
|
558
558
|
const client = createBrokerClient();
|
|
559
559
|
await client.ping();
|
|
560
560
|
|
|
561
|
-
// Advance past first cooldown (
|
|
562
|
-
fakeNow +=
|
|
561
|
+
// Advance past first cooldown (5s)
|
|
562
|
+
fakeNow += 5_001;
|
|
563
563
|
|
|
564
564
|
// Start broker — reconnection should succeed and reset counters
|
|
565
565
|
const broker = createMockBroker();
|
|
@@ -578,15 +578,15 @@ describe("keychain-broker-client", () => {
|
|
|
578
578
|
rmSync(SOCKET_PATH, { force: true });
|
|
579
579
|
|
|
580
580
|
// This new failure should start from the beginning of the cooldown
|
|
581
|
-
// schedule (
|
|
581
|
+
// schedule (5s), not escalated.
|
|
582
582
|
await client.ping();
|
|
583
583
|
|
|
584
|
-
// Verify cooldown is back to
|
|
584
|
+
// Verify cooldown is back to 5s (not 15s)
|
|
585
585
|
writeFileSync(SOCKET_PATH, "");
|
|
586
586
|
expect(client.isAvailable()).toBe(false);
|
|
587
587
|
|
|
588
|
-
//
|
|
589
|
-
fakeNow +=
|
|
588
|
+
// 5s should be enough to clear cooldown
|
|
589
|
+
fakeNow += 5_001;
|
|
590
590
|
expect(client.isAvailable()).toBe(true);
|
|
591
591
|
}, 15_000);
|
|
592
592
|
|
|
@@ -594,60 +594,73 @@ describe("keychain-broker-client", () => {
|
|
|
594
594
|
const client = createBrokerClient();
|
|
595
595
|
|
|
596
596
|
// First failure round: two attempts (first + immediate retry) ->
|
|
597
|
-
// consecutiveFailures=2, cooldown index = max(2-2,0) = 0 ->
|
|
597
|
+
// consecutiveFailures=2, cooldown index = max(2-2,0) = 0 -> 5s.
|
|
598
598
|
await client.ping();
|
|
599
599
|
|
|
600
600
|
writeFileSync(SOCKET_PATH, "");
|
|
601
601
|
expect(client.isAvailable()).toBe(false);
|
|
602
602
|
|
|
603
|
-
//
|
|
604
|
-
fakeNow +=
|
|
603
|
+
// 5s should clear the first cooldown
|
|
604
|
+
fakeNow += 5_001;
|
|
605
605
|
expect(client.isAvailable()).toBe(true);
|
|
606
606
|
|
|
607
607
|
// Remove socket to trigger another failure. After cooldown elapses,
|
|
608
608
|
// ensureConnected clears unavailableSince and tries connect().
|
|
609
609
|
// This failure increments consecutiveFailures to 3 (no immediate retry
|
|
610
610
|
// since consecutiveFailures > 1 after increment).
|
|
611
|
-
// Cooldown index = max(3-2,0) = 1 ->
|
|
611
|
+
// Cooldown index = max(3-2,0) = 1 -> 15s.
|
|
612
612
|
rmSync(SOCKET_PATH, { force: true });
|
|
613
613
|
await client.ping();
|
|
614
614
|
|
|
615
615
|
writeFileSync(SOCKET_PATH, "");
|
|
616
616
|
expect(client.isAvailable()).toBe(false);
|
|
617
617
|
|
|
618
|
-
fakeNow +=
|
|
619
|
-
expect(client.isAvailable()).toBe(false); //
|
|
618
|
+
fakeNow += 5_001;
|
|
619
|
+
expect(client.isAvailable()).toBe(false); // 5s not enough
|
|
620
620
|
|
|
621
|
-
fakeNow +=
|
|
621
|
+
fakeNow += 10_000; // total 15_001ms since this cooldown started
|
|
622
622
|
expect(client.isAvailable()).toBe(true);
|
|
623
623
|
|
|
624
|
-
// Another failure -> consecutiveFailures=4, index = max(4-2,0) = 2 ->
|
|
624
|
+
// Another failure -> consecutiveFailures=4, index = max(4-2,0) = 2 -> 30s
|
|
625
625
|
rmSync(SOCKET_PATH, { force: true });
|
|
626
626
|
await client.ping();
|
|
627
627
|
|
|
628
628
|
writeFileSync(SOCKET_PATH, "");
|
|
629
629
|
expect(client.isAvailable()).toBe(false);
|
|
630
630
|
|
|
631
|
-
fakeNow +=
|
|
631
|
+
fakeNow += 15_001;
|
|
632
|
+
expect(client.isAvailable()).toBe(false);
|
|
633
|
+
|
|
634
|
+
fakeNow += 15_000; // total 30_001ms
|
|
635
|
+
expect(client.isAvailable()).toBe(true);
|
|
636
|
+
|
|
637
|
+
// Another failure -> consecutiveFailures=5, index = max(5-2,0) = 3 -> 60s
|
|
638
|
+
rmSync(SOCKET_PATH, { force: true });
|
|
639
|
+
await client.ping();
|
|
640
|
+
|
|
641
|
+
writeFileSync(SOCKET_PATH, "");
|
|
642
|
+
expect(client.isAvailable()).toBe(false);
|
|
643
|
+
|
|
644
|
+
fakeNow += 30_001;
|
|
632
645
|
expect(client.isAvailable()).toBe(false);
|
|
633
646
|
|
|
634
|
-
fakeNow +=
|
|
647
|
+
fakeNow += 30_000; // total 60_001ms
|
|
635
648
|
expect(client.isAvailable()).toBe(true);
|
|
636
649
|
|
|
637
|
-
// Another failure -> consecutiveFailures=
|
|
650
|
+
// Another failure -> consecutiveFailures=6, index = min(max(6-2,0), 4) = 4 -> 300s (5min)
|
|
638
651
|
rmSync(SOCKET_PATH, { force: true });
|
|
639
652
|
await client.ping();
|
|
640
653
|
|
|
641
654
|
writeFileSync(SOCKET_PATH, "");
|
|
642
655
|
expect(client.isAvailable()).toBe(false);
|
|
643
656
|
|
|
644
|
-
fakeNow +=
|
|
657
|
+
fakeNow += 60_001;
|
|
645
658
|
expect(client.isAvailable()).toBe(false);
|
|
646
659
|
|
|
647
|
-
fakeNow +=
|
|
660
|
+
fakeNow += 240_000; // total 300_001ms
|
|
648
661
|
expect(client.isAvailable()).toBe(true);
|
|
649
662
|
|
|
650
|
-
// Another failure -> consecutiveFailures=
|
|
663
|
+
// Another failure -> consecutiveFailures=7, index = min(max(7-2,0), 4) = 4 -> 300s (capped)
|
|
651
664
|
rmSync(SOCKET_PATH, { force: true });
|
|
652
665
|
await client.ping();
|
|
653
666
|
|
|
@@ -658,4 +671,130 @@ describe("keychain-broker-client", () => {
|
|
|
658
671
|
expect(client.isAvailable()).toBe(true);
|
|
659
672
|
});
|
|
660
673
|
});
|
|
674
|
+
|
|
675
|
+
// -----------------------------------------------------------------------
|
|
676
|
+
// Connect timeout
|
|
677
|
+
// -----------------------------------------------------------------------
|
|
678
|
+
describe("connect timeout", () => {
|
|
679
|
+
let stopFn: (() => Promise<void>) | null = null;
|
|
680
|
+
|
|
681
|
+
beforeEach(() => {
|
|
682
|
+
writeFileSync(TOKEN_PATH, TEST_TOKEN);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
afterEach(async () => {
|
|
686
|
+
if (stopFn) {
|
|
687
|
+
await stopFn();
|
|
688
|
+
stopFn = null;
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
test("rejects connect within 3 seconds when broker is unresponsive", async () => {
|
|
693
|
+
// Create a server that accepts connections but never responds
|
|
694
|
+
// (simulates an unresponsive broker process).
|
|
695
|
+
const activeConns = new Set<import("node:net").Socket>();
|
|
696
|
+
const server = createServer((conn) => {
|
|
697
|
+
activeConns.add(conn);
|
|
698
|
+
conn.on("close", () => activeConns.delete(conn));
|
|
699
|
+
// Accept connection but do nothing — no data, no close
|
|
700
|
+
});
|
|
701
|
+
await new Promise<void>((resolve) => {
|
|
702
|
+
server.listen(SOCKET_PATH, () => resolve());
|
|
703
|
+
});
|
|
704
|
+
stopFn = () =>
|
|
705
|
+
new Promise<void>((resolve) => {
|
|
706
|
+
for (const conn of activeConns) conn.destroy();
|
|
707
|
+
activeConns.clear();
|
|
708
|
+
server.close(() => resolve());
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
const client = createBrokerClient();
|
|
712
|
+
const start = Date.now();
|
|
713
|
+
const result = await client.ping();
|
|
714
|
+
const elapsed = Date.now() - start;
|
|
715
|
+
|
|
716
|
+
// Should return null (graceful fallback) and not hang indefinitely.
|
|
717
|
+
// The connect timeout is 3s; allow some slack but it should be well
|
|
718
|
+
// under 10s (the old behavior would hang for REQUEST_TIMEOUT_MS * retries).
|
|
719
|
+
expect(result).toBeNull();
|
|
720
|
+
expect(elapsed).toBeLessThan(10_000);
|
|
721
|
+
}, 15_000);
|
|
722
|
+
|
|
723
|
+
test("successful connect clears the connect timer", async () => {
|
|
724
|
+
// Normal broker that responds to pings — verifies the timer is cleared
|
|
725
|
+
// and doesn't fire after a successful connection.
|
|
726
|
+
const broker = createMockBroker();
|
|
727
|
+
broker.setHandler(() => ({ ok: true, result: { pong: true } }));
|
|
728
|
+
await broker.start();
|
|
729
|
+
stopFn = () => broker.stop();
|
|
730
|
+
|
|
731
|
+
const client = createBrokerClient();
|
|
732
|
+
const result = await client.ping();
|
|
733
|
+
expect(result).toEqual({ pong: true });
|
|
734
|
+
|
|
735
|
+
// Wait a bit past the connect timeout to ensure no stale timer fires
|
|
736
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
737
|
+
|
|
738
|
+
// Client should still work fine
|
|
739
|
+
const result2 = await client.ping();
|
|
740
|
+
expect(result2).toEqual({ pong: true });
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
// -----------------------------------------------------------------------
|
|
745
|
+
// Reduced initial cooldown
|
|
746
|
+
// -----------------------------------------------------------------------
|
|
747
|
+
describe("reduced initial cooldown", () => {
|
|
748
|
+
const originalDateNow = Date.now;
|
|
749
|
+
let fakeNow: number;
|
|
750
|
+
|
|
751
|
+
beforeEach(() => {
|
|
752
|
+
fakeNow = originalDateNow.call(Date);
|
|
753
|
+
Date.now = () => fakeNow;
|
|
754
|
+
writeFileSync(TOKEN_PATH, TEST_TOKEN);
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
afterEach(() => {
|
|
758
|
+
Date.now = originalDateNow;
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
test("first cooldown is 5 seconds, not 30 seconds", async () => {
|
|
762
|
+
const client = createBrokerClient();
|
|
763
|
+
|
|
764
|
+
// Trigger two connection failures (first + immediate retry)
|
|
765
|
+
await client.ping();
|
|
766
|
+
|
|
767
|
+
writeFileSync(SOCKET_PATH, "");
|
|
768
|
+
|
|
769
|
+
// Should still be in cooldown at 4 seconds
|
|
770
|
+
fakeNow += 4_000;
|
|
771
|
+
expect(client.isAvailable()).toBe(false);
|
|
772
|
+
|
|
773
|
+
// Should be available after 5 seconds
|
|
774
|
+
fakeNow += 1_001;
|
|
775
|
+
expect(client.isAvailable()).toBe(true);
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
test("second cooldown is 15 seconds", async () => {
|
|
779
|
+
const client = createBrokerClient();
|
|
780
|
+
|
|
781
|
+
// First failure round -> cooldown 5s
|
|
782
|
+
await client.ping();
|
|
783
|
+
|
|
784
|
+
// Clear first cooldown
|
|
785
|
+
fakeNow += 5_001;
|
|
786
|
+
|
|
787
|
+
// Second failure -> cooldown 15s
|
|
788
|
+
await client.ping();
|
|
789
|
+
|
|
790
|
+
writeFileSync(SOCKET_PATH, "");
|
|
791
|
+
expect(client.isAvailable()).toBe(false);
|
|
792
|
+
|
|
793
|
+
fakeNow += 14_000;
|
|
794
|
+
expect(client.isAvailable()).toBe(false);
|
|
795
|
+
|
|
796
|
+
fakeNow += 1_001;
|
|
797
|
+
expect(client.isAvailable()).toBe(true);
|
|
798
|
+
});
|
|
799
|
+
});
|
|
661
800
|
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for adaptive poll interval backoff in the memory jobs worker.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that when no jobs are claimable, the poll interval doubles each
|
|
5
|
+
* tick (1.5s -> 3s -> 6s -> ... -> 30s cap), and resets to 1.5s when work
|
|
6
|
+
* is found.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
9
|
+
|
|
10
|
+
// ── Mocks (must precede imports of tested module) ──────────────────
|
|
11
|
+
|
|
12
|
+
mock.module("../util/platform.js", () => ({
|
|
13
|
+
getDataDir: () => "/tmp/test-backoff",
|
|
14
|
+
isMacOS: () => false,
|
|
15
|
+
isLinux: () => true,
|
|
16
|
+
isWindows: () => false,
|
|
17
|
+
getPidPath: () => "/tmp/test-backoff/test.pid",
|
|
18
|
+
getDbPath: () => "/tmp/test-backoff/test.db",
|
|
19
|
+
getLogPath: () => "/tmp/test-backoff/test.log",
|
|
20
|
+
ensureDataDir: () => {},
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
mock.module("../util/logger.js", () => ({
|
|
24
|
+
getLogger: () =>
|
|
25
|
+
new Proxy({} as Record<string, unknown>, {
|
|
26
|
+
get: () => () => {},
|
|
27
|
+
}),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
// Mock config — memory disabled so runMemoryJobsOnce returns 0 immediately
|
|
31
|
+
mock.module("../config/loader.js", () => ({
|
|
32
|
+
getConfig: () => ({
|
|
33
|
+
memory: { enabled: false },
|
|
34
|
+
}),
|
|
35
|
+
loadConfig: () => ({
|
|
36
|
+
memory: { enabled: false },
|
|
37
|
+
}),
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
// Mock jobs-store (accesses DB)
|
|
41
|
+
mock.module("../memory/jobs-store.js", () => ({
|
|
42
|
+
resetRunningJobsToPending: () => 0,
|
|
43
|
+
claimMemoryJobs: () => [],
|
|
44
|
+
completeMemoryJob: () => {},
|
|
45
|
+
deferMemoryJob: () => "deferred",
|
|
46
|
+
failMemoryJob: () => {},
|
|
47
|
+
failStalledJobs: () => 0,
|
|
48
|
+
enqueueCleanupStaleSupersededItemsJob: () => null,
|
|
49
|
+
enqueuePruneOldConversationsJob: () => null,
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
// Mock db.js (rawRun used in sweepStaleItems)
|
|
53
|
+
mock.module("../memory/db.js", () => ({
|
|
54
|
+
rawRun: () => 0,
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
import {
|
|
58
|
+
POLL_INTERVAL_MAX_MS,
|
|
59
|
+
POLL_INTERVAL_MIN_MS,
|
|
60
|
+
startMemoryJobsWorker,
|
|
61
|
+
} from "../memory/jobs-worker.js";
|
|
62
|
+
|
|
63
|
+
describe("memory jobs worker adaptive poll interval", () => {
|
|
64
|
+
test("exports expected poll interval constants", () => {
|
|
65
|
+
expect(POLL_INTERVAL_MIN_MS).toBe(1_500);
|
|
66
|
+
expect(POLL_INTERVAL_MAX_MS).toBe(30_000);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("backoff sequence doubles from min to max then caps", () => {
|
|
70
|
+
// Verify the math: starting at 1500, doubling each step, capped at 30000
|
|
71
|
+
const intervals: number[] = [];
|
|
72
|
+
let current = POLL_INTERVAL_MIN_MS;
|
|
73
|
+
for (let i = 0; i < 10; i++) {
|
|
74
|
+
intervals.push(current);
|
|
75
|
+
current = Math.min(current * 2, POLL_INTERVAL_MAX_MS);
|
|
76
|
+
}
|
|
77
|
+
expect(intervals).toEqual([
|
|
78
|
+
1_500, // tick 1
|
|
79
|
+
3_000, // tick 2
|
|
80
|
+
6_000, // tick 3
|
|
81
|
+
12_000, // tick 4
|
|
82
|
+
24_000, // tick 5
|
|
83
|
+
30_000, // tick 6 (capped)
|
|
84
|
+
30_000, // stays capped
|
|
85
|
+
30_000,
|
|
86
|
+
30_000,
|
|
87
|
+
30_000,
|
|
88
|
+
]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("worker schedules setTimeout with increasing intervals when idle", async () => {
|
|
92
|
+
const timeoutDelays: number[] = [];
|
|
93
|
+
const originalSetTimeout = globalThis.setTimeout;
|
|
94
|
+
const originalClearTimeout = globalThis.clearTimeout;
|
|
95
|
+
|
|
96
|
+
// Collect pending timer callbacks so we can fire them manually
|
|
97
|
+
const pendingCallbacks: Array<() => void> = [];
|
|
98
|
+
|
|
99
|
+
globalThis.setTimeout = ((fn: () => void, delay?: number) => {
|
|
100
|
+
if (delay !== undefined && delay >= POLL_INTERVAL_MIN_MS) {
|
|
101
|
+
timeoutDelays.push(delay);
|
|
102
|
+
pendingCallbacks.push(fn);
|
|
103
|
+
}
|
|
104
|
+
return 999 as unknown as ReturnType<typeof setTimeout>;
|
|
105
|
+
}) as typeof setTimeout;
|
|
106
|
+
globalThis.clearTimeout = (() => {}) as typeof clearTimeout;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const worker = startMemoryJobsWorker();
|
|
110
|
+
|
|
111
|
+
// Wait for the initial tick() promise to settle
|
|
112
|
+
await new Promise((resolve) => originalSetTimeout(resolve, 20));
|
|
113
|
+
|
|
114
|
+
// Fire pending timer callbacks to advance through the backoff sequence.
|
|
115
|
+
// Each callback triggers tick() which is async, so we await a microtask
|
|
116
|
+
// after each to let the promise chain settle and schedule the next timer.
|
|
117
|
+
for (let i = 0; i < 6; i++) {
|
|
118
|
+
const cb = pendingCallbacks.shift();
|
|
119
|
+
if (cb) {
|
|
120
|
+
cb();
|
|
121
|
+
await new Promise((resolve) => originalSetTimeout(resolve, 20));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
worker.stop();
|
|
126
|
+
|
|
127
|
+
// We should have captured several setTimeout calls with increasing delays
|
|
128
|
+
expect(timeoutDelays.length).toBeGreaterThanOrEqual(4);
|
|
129
|
+
|
|
130
|
+
// Intervals should be non-decreasing (backoff)
|
|
131
|
+
for (let i = 1; i < timeoutDelays.length; i++) {
|
|
132
|
+
expect(timeoutDelays[i]).toBeGreaterThanOrEqual(timeoutDelays[i - 1]!);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// All intervals within bounds
|
|
136
|
+
for (const delay of timeoutDelays) {
|
|
137
|
+
expect(delay).toBeGreaterThanOrEqual(POLL_INTERVAL_MIN_MS);
|
|
138
|
+
expect(delay).toBeLessThanOrEqual(POLL_INTERVAL_MAX_MS);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Should eventually reach the cap
|
|
142
|
+
expect(timeoutDelays[timeoutDelays.length - 1]).toBe(
|
|
143
|
+
POLL_INTERVAL_MAX_MS,
|
|
144
|
+
);
|
|
145
|
+
} finally {
|
|
146
|
+
globalThis.setTimeout = originalSetTimeout;
|
|
147
|
+
globalThis.clearTimeout = originalClearTimeout;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -527,9 +527,9 @@ describe("integration: existing routes unaffected", () => {
|
|
|
527
527
|
});
|
|
528
528
|
|
|
529
529
|
test("GET /v1/health still works (not intercepted by migration routes)", async () => {
|
|
530
|
-
const {
|
|
530
|
+
const { handleDetailedHealth } =
|
|
531
531
|
await import("../runtime/routes/identity-routes.js");
|
|
532
|
-
const res =
|
|
532
|
+
const res = handleDetailedHealth();
|
|
533
533
|
const body = (await res.json()) as Record<string, unknown>;
|
|
534
534
|
|
|
535
535
|
expect(res.status).toBe(200);
|
|
@@ -939,9 +939,9 @@ describe("route policy registration", () => {
|
|
|
939
939
|
|
|
940
940
|
describe("integration: existing routes unaffected", () => {
|
|
941
941
|
test("GET /v1/health still works", async () => {
|
|
942
|
-
const {
|
|
942
|
+
const { handleDetailedHealth } =
|
|
943
943
|
await import("../runtime/routes/identity-routes.js");
|
|
944
|
-
const res =
|
|
944
|
+
const res = handleDetailedHealth();
|
|
945
945
|
const body = (await res.json()) as Record<string, unknown>;
|
|
946
946
|
|
|
947
947
|
expect(res.status).toBe(200);
|
|
@@ -792,9 +792,9 @@ describe("route policy registration", () => {
|
|
|
792
792
|
|
|
793
793
|
describe("integration: existing routes unaffected", () => {
|
|
794
794
|
test("GET /v1/health still works", async () => {
|
|
795
|
-
const {
|
|
795
|
+
const { handleDetailedHealth } =
|
|
796
796
|
await import("../runtime/routes/identity-routes.js");
|
|
797
|
-
const res =
|
|
797
|
+
const res = handleDetailedHealth();
|
|
798
798
|
const body = (await res.json()) as Record<string, unknown>;
|
|
799
799
|
|
|
800
800
|
expect(res.status).toBe(200);
|
|
@@ -684,9 +684,9 @@ describe("route policy registration", () => {
|
|
|
684
684
|
|
|
685
685
|
describe("integration: existing routes unaffected", () => {
|
|
686
686
|
test("GET /v1/health still works (not intercepted by migration routes)", async () => {
|
|
687
|
-
const {
|
|
687
|
+
const { handleDetailedHealth } =
|
|
688
688
|
await import("../runtime/routes/identity-routes.js");
|
|
689
|
-
const res =
|
|
689
|
+
const res = handleDetailedHealth();
|
|
690
690
|
const body = (await res.json()) as Record<string, unknown>;
|
|
691
691
|
|
|
692
692
|
expect(res.status).toBe(200);
|
|
@@ -37,11 +37,6 @@ mock.module("../util/logger.js", () => ({
|
|
|
37
37
|
}),
|
|
38
38
|
}));
|
|
39
39
|
|
|
40
|
-
// Mock security check to always pass
|
|
41
|
-
mock.module("../security/secret-ingress.js", () => ({
|
|
42
|
-
checkIngressForSecrets: () => ({ blocked: false }),
|
|
43
|
-
}));
|
|
44
|
-
|
|
45
40
|
mock.module("../config/env.js", () => ({
|
|
46
41
|
isHttpAuthDisabled: () => true,
|
|
47
42
|
getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
|
|
@@ -34,6 +34,10 @@ mock.module("../notifications/conversation-candidates.js", () => ({
|
|
|
34
34
|
serializeCandidatesForPrompt: () => undefined,
|
|
35
35
|
}));
|
|
36
36
|
|
|
37
|
+
mock.module("../prompts/persona-resolver.js", () => ({
|
|
38
|
+
resolveGuardianPersona: () => null,
|
|
39
|
+
}));
|
|
40
|
+
|
|
37
41
|
let configuredProvider: { sendMessage: () => Promise<unknown> } | null = null;
|
|
38
42
|
let extractedToolUse: unknown = null;
|
|
39
43
|
|
|
@@ -36,6 +36,10 @@ mock.module("../notifications/conversation-candidates.js", () => ({
|
|
|
36
36
|
serializeCandidatesForPrompt: () => undefined,
|
|
37
37
|
}));
|
|
38
38
|
|
|
39
|
+
mock.module("../prompts/persona-resolver.js", () => ({
|
|
40
|
+
resolveGuardianPersona: () => null,
|
|
41
|
+
}));
|
|
42
|
+
|
|
39
43
|
// ── Identity context mock ─────────────────────────────────────────────
|
|
40
44
|
|
|
41
45
|
let mockIdentityContext: string | null = null;
|