vellum 0.2.0 → 0.2.2
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/README.md +15 -2
- package/bun.lock +5 -2
- package/package.json +4 -2
- package/scripts/capture-x-graphql.ts +562 -0
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
- package/scripts/test.sh +5 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +161 -34
- package/src/__tests__/account-registry.test.ts +2 -1
- package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
- package/src/__tests__/app-bundler.test.ts +12 -33
- package/src/__tests__/asset-materialize-tool.test.ts +16 -15
- package/src/__tests__/asset-search-tool.test.ts +23 -22
- package/src/__tests__/attachments-store.test.ts +56 -127
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
- package/src/__tests__/browser-skill-endstate.test.ts +5 -8
- package/src/__tests__/call-bridge.test.ts +385 -0
- package/src/__tests__/call-constants.test.ts +40 -0
- package/src/__tests__/call-orchestrator.test.ts +454 -0
- package/src/__tests__/call-recovery.test.ts +518 -0
- package/src/__tests__/call-routes-http.test.ts +459 -0
- package/src/__tests__/call-state-machine.test.ts +143 -0
- package/src/__tests__/call-state.test.ts +133 -0
- package/src/__tests__/call-store.test.ts +691 -0
- package/src/__tests__/cli-discover.test.ts +1 -1
- package/src/__tests__/commit-message-enrichment-service.test.ts +550 -0
- package/src/__tests__/compaction.benchmark.test.ts +176 -0
- package/src/__tests__/computer-use-tools.test.ts +250 -0
- package/src/__tests__/config-schema.test.ts +348 -3
- package/src/__tests__/conflict-store.test.ts +2 -1
- package/src/__tests__/contacts-tools.test.ts +331 -0
- package/src/__tests__/conversation-store.test.ts +30 -32
- package/src/__tests__/credential-security-invariants.test.ts +4 -0
- package/src/__tests__/date-context.test.ts +373 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
- package/src/__tests__/doordash-session.test.ts +9 -0
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
- package/src/__tests__/followup-tools.test.ts +303 -0
- package/src/__tests__/handlers-twitter-config.test.ts +718 -0
- package/src/__tests__/intent-routing.test.ts +64 -57
- package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
- package/src/__tests__/ipc-snapshot.test.ts +96 -28
- package/src/__tests__/llm-usage-store.test.ts +3 -8
- package/src/__tests__/media-generate-image.test.ts +1 -1
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
- package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
- package/src/__tests__/playbook-tools.test.ts +342 -0
- package/src/__tests__/profile-compiler.test.ts +2 -1
- package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
- package/src/__tests__/recurrence-engine.test.ts +69 -0
- package/src/__tests__/recurrence-types.test.ts +71 -0
- package/src/__tests__/registry.test.ts +17 -10
- package/src/__tests__/relay-server.test.ts +633 -0
- package/src/__tests__/reminder-store.test.ts +6 -3
- package/src/__tests__/reminder.test.ts +43 -77
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +222 -0
- package/src/__tests__/run-orchestrator.test.ts +7 -7
- package/src/__tests__/runtime-attachment-metadata.test.ts +19 -20
- package/src/__tests__/runtime-runs-http.test.ts +5 -23
- package/src/__tests__/runtime-runs.test.ts +11 -11
- package/src/__tests__/schedule-store.test.ts +482 -0
- package/src/__tests__/schedule-tools.test.ts +700 -0
- package/src/__tests__/scheduler-recurrence.test.ts +329 -0
- package/src/__tests__/server-history-render.test.ts +14 -13
- package/src/__tests__/session-error.test.ts +28 -0
- package/src/__tests__/session-init.benchmark.test.ts +462 -0
- package/src/__tests__/session-queue.test.ts +89 -16
- package/src/__tests__/session-runtime-assembly.test.ts +161 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
- package/src/__tests__/signup-e2e.test.ts +2 -1
- package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
- package/src/__tests__/skill-script-runner.test.ts +159 -0
- package/src/__tests__/speaker-identification.test.ts +52 -0
- package/src/__tests__/subagent-manager-notify.test.ts +42 -10
- package/src/__tests__/subagent-tools.test.ts +141 -41
- package/src/__tests__/task-compiler.test.ts +2 -1
- package/src/__tests__/task-runner.test.ts +2 -1
- package/src/__tests__/task-scheduler.test.ts +2 -1
- package/src/__tests__/task-tools.test.ts +49 -56
- package/src/__tests__/tool-audit-listener.test.ts +1 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
- package/src/__tests__/tool-executor.test.ts +13 -17
- package/src/__tests__/turn-commit.test.ts +273 -2
- package/src/__tests__/twilio-provider.test.ts +143 -0
- package/src/__tests__/twilio-routes.test.ts +789 -0
- package/src/__tests__/twitter-auth-handler.test.ts +581 -0
- package/src/__tests__/view-image-tool.test.ts +217 -0
- package/src/__tests__/workspace-git-service.test.ts +403 -0
- package/src/__tests__/workspace-heartbeat-service.test.ts +141 -2
- package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
- package/src/bundler/app-bundler.ts +35 -14
- package/src/calls/call-bridge.ts +95 -0
- package/src/calls/call-constants.ts +48 -0
- package/src/calls/call-domain.ts +276 -0
- package/src/calls/call-orchestrator.ts +390 -0
- package/src/calls/call-recovery.ts +207 -0
- package/src/calls/call-state-machine.ts +68 -0
- package/src/calls/call-state.ts +64 -0
- package/src/calls/call-store.ts +416 -0
- package/src/calls/relay-server.ts +335 -0
- package/src/calls/speaker-identification.ts +213 -0
- package/src/calls/twilio-config.ts +34 -0
- package/src/calls/twilio-provider.ts +173 -0
- package/src/calls/twilio-routes.ts +250 -0
- package/src/calls/types.ts +37 -0
- package/src/calls/voice-provider.ts +14 -0
- package/src/cli/config-commands.ts +334 -0
- package/src/cli/core-commands.ts +776 -0
- package/src/cli/doordash.ts +256 -25
- package/src/cli/ipc-client.ts +82 -0
- package/src/cli/map.ts +246 -0
- package/src/cli/twitter.ts +575 -0
- package/src/cli.ts +7 -5
- package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
- package/src/commands/cc-command-registry.ts +209 -0
- package/src/config/bundled-skills/contacts/SKILL.md +39 -0
- package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
- package/src/config/bundled-skills/document/SKILL.md +18 -0
- package/src/config/bundled-skills/document/TOOLS.json +53 -0
- package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
- package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
- package/src/config/bundled-skills/doordash/SKILL.md +163 -0
- package/src/config/bundled-skills/followups/SKILL.md +32 -0
- package/src/config/bundled-skills/followups/TOOLS.json +100 -0
- package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -24
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
- package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
- package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
- package/src/config/bundled-skills/reminder/SKILL.md +20 -0
- package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
- package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
- package/src/config/bundled-skills/schedule/SKILL.md +74 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
- package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
- package/src/config/bundled-skills/subagent/SKILL.md +25 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
- package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
- package/src/config/bundled-skills/tasks/SKILL.md +28 -0
- package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
- package/src/config/bundled-skills/twitter/SKILL.md +134 -0
- package/src/config/bundled-skills/watcher/SKILL.md +27 -0
- package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
- package/src/config/defaults.ts +44 -0
- package/src/config/loader.ts +4 -1
- package/src/config/schema.ts +218 -1
- package/src/config/system-prompt.ts +100 -6
- package/src/config/templates/IDENTITY.md +7 -0
- package/src/config/types.ts +5 -0
- package/src/contacts/contact-store.ts +4 -4
- package/src/daemon/assistant-attachments.ts +10 -0
- package/src/daemon/classifier.ts +3 -1
- package/src/daemon/computer-use-session.ts +3 -1
- package/src/daemon/date-context.ts +136 -0
- package/src/daemon/handlers/apps.ts +16 -1
- package/src/daemon/handlers/browser.ts +54 -0
- package/src/daemon/handlers/computer-use.ts +7 -1
- package/src/daemon/handlers/config.ts +192 -4
- package/src/daemon/handlers/diagnostics.ts +5 -1
- package/src/daemon/handlers/documents.ts +18 -29
- package/src/daemon/handlers/home-base.ts +5 -1
- package/src/daemon/handlers/index.ts +40 -271
- package/src/daemon/handlers/misc.ts +9 -1
- package/src/daemon/handlers/publish.ts +6 -1
- package/src/daemon/handlers/sessions.ts +65 -12
- package/src/daemon/handlers/shared.ts +36 -1
- package/src/daemon/handlers/signing.ts +37 -0
- package/src/daemon/handlers/skills.ts +20 -6
- package/src/daemon/handlers/subagents.ts +8 -3
- package/src/daemon/handlers/twitter-auth.ts +169 -0
- package/src/daemon/handlers/work-items.ts +495 -39
- package/src/daemon/ipc-contract-inventory.json +40 -4
- package/src/daemon/ipc-contract.ts +185 -37
- package/src/daemon/ipc-protocol.ts +7 -2
- package/src/daemon/lifecycle.ts +48 -5
- package/src/daemon/main.ts +10 -4
- package/src/daemon/ride-shotgun-handler.ts +74 -10
- package/src/daemon/server.ts +144 -29
- package/src/daemon/session-agent-loop.ts +887 -0
- package/src/daemon/session-attachments.ts +28 -5
- package/src/daemon/session-error.ts +24 -3
- package/src/daemon/session-lifecycle.ts +147 -0
- package/src/daemon/session-media-retry.ts +147 -0
- package/src/daemon/session-messaging.ts +145 -0
- package/src/daemon/session-notifiers.ts +164 -0
- package/src/daemon/session-process.ts +2 -2
- package/src/daemon/session-queue-manager.ts +1 -0
- package/src/daemon/session-runtime-assembly.ts +52 -0
- package/src/daemon/session-skill-tools.ts +124 -5
- package/src/daemon/session-slash.ts +3 -0
- package/src/daemon/session-surfaces.ts +77 -2
- package/src/daemon/session-tool-setup.ts +222 -2
- package/src/daemon/session-usage.ts +0 -2
- package/src/daemon/session.ts +114 -1365
- package/src/daemon/video-thumbnail.ts +60 -0
- package/src/doordash/client.ts +121 -27
- package/src/doordash/queries.ts +1 -2
- package/src/export/formatter.ts +3 -1
- package/src/followups/followup-store.ts +4 -2
- package/src/followups/types.ts +6 -0
- package/src/hooks/templates.ts +1 -1
- package/src/index.ts +32 -1151
- package/src/media/gemini-image-service.ts +1 -1
- package/src/memory/attachments-store.ts +28 -83
- package/src/memory/channel-delivery-store.ts +7 -21
- package/src/memory/clarification-resolver.ts +6 -5
- package/src/memory/contradiction-checker.ts +3 -2
- package/src/memory/conversation-key-store.ts +10 -29
- package/src/memory/conversation-store.ts +2 -1
- package/src/memory/db.ts +362 -2
- package/src/memory/entity-extractor.ts +6 -3
- package/src/memory/items-extractor.ts +5 -4
- package/src/memory/jobs-store.ts +3 -2
- package/src/memory/llm-usage-store.ts +1 -2
- package/src/memory/runs-store.ts +1 -2
- package/src/memory/schema.ts +65 -2
- package/src/messaging/style-analyzer.ts +3 -2
- package/src/messaging/thread-summarizer.ts +8 -12
- package/src/messaging/triage-engine.ts +4 -2
- package/src/providers/openrouter/client.ts +20 -0
- package/src/providers/registry.ts +8 -0
- package/src/runtime/http-server.ts +277 -25
- package/src/runtime/http-types.ts +0 -2
- package/src/runtime/routes/attachment-routes.ts +5 -6
- package/src/runtime/routes/call-routes.ts +140 -0
- package/src/runtime/routes/channel-routes.ts +12 -19
- package/src/runtime/routes/conversation-routes.ts +5 -9
- package/src/runtime/routes/run-routes.ts +4 -8
- package/src/runtime/run-orchestrator.ts +39 -6
- package/src/schedule/recurrence-engine.ts +138 -0
- package/src/schedule/recurrence-types.ts +67 -0
- package/src/schedule/schedule-store.ts +102 -57
- package/src/schedule/scheduler.ts +9 -6
- package/src/security/oauth2.ts +29 -4
- package/src/security/secret-allowlist.ts +46 -0
- package/src/skills/clawhub.ts +1 -1
- package/src/subagent/manager.ts +40 -8
- package/src/swarm/backend-claude-code.ts +64 -9
- package/src/swarm/worker-prompts.ts +2 -1
- package/src/tasks/SPEC.md +34 -28
- package/src/tasks/ephemeral-permissions.ts +16 -7
- package/src/tasks/task-compiler.ts +5 -4
- package/src/tasks/task-runner.ts +10 -5
- package/src/tasks/task-scheduler.ts +1 -1
- package/src/tasks/tool-sanitizer.ts +36 -0
- package/src/tools/assets/search.ts +4 -4
- package/src/tools/browser/api-map.ts +220 -0
- package/src/tools/browser/auto-navigate.ts +270 -0
- package/src/tools/browser/browser-execution.ts +2 -1
- package/src/tools/browser/browser-manager.ts +2 -2
- package/src/tools/browser/network-recorder.ts +5 -4
- package/src/tools/browser/x-auto-navigate.ts +207 -0
- package/src/tools/calls/call-end.ts +67 -0
- package/src/tools/calls/call-start.ts +73 -0
- package/src/tools/calls/call-status.ts +81 -0
- package/src/tools/claude-code/claude-code.ts +77 -11
- package/src/tools/contacts/contact-merge.ts +46 -78
- package/src/tools/contacts/contact-search.ts +35 -79
- package/src/tools/contacts/contact-upsert.ts +35 -108
- package/src/tools/credentials/vault.ts +21 -5
- package/src/tools/document/document-tool.ts +71 -144
- package/src/tools/executor.ts +129 -10
- package/src/tools/followups/followup_create.ts +46 -88
- package/src/tools/followups/followup_list.ts +34 -74
- package/src/tools/followups/followup_resolve.ts +31 -66
- package/src/tools/host-terminal/cli-discover.ts +2 -1
- package/src/tools/host-terminal/host-shell.ts +10 -0
- package/src/tools/memory/handlers.ts +5 -4
- package/src/tools/network/__tests__/web-search.test.ts +427 -0
- package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
- package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
- package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
- package/src/tools/network/web-fetch.ts +18 -6
- package/src/tools/playbooks/index.ts +4 -5
- package/src/tools/playbooks/playbook-create.ts +3 -47
- package/src/tools/playbooks/playbook-delete.ts +1 -25
- package/src/tools/playbooks/playbook-list.ts +1 -28
- package/src/tools/playbooks/playbook-update.ts +3 -51
- package/src/tools/registry.ts +2 -4
- package/src/tools/reminder/reminder.ts +5 -78
- package/src/tools/schedule/create.ts +69 -74
- package/src/tools/schedule/delete.ts +21 -47
- package/src/tools/schedule/list.ts +55 -74
- package/src/tools/schedule/update.ts +77 -84
- package/src/tools/subagent/abort.ts +29 -58
- package/src/tools/subagent/message.ts +30 -63
- package/src/tools/subagent/read.ts +53 -84
- package/src/tools/subagent/spawn.ts +43 -82
- package/src/tools/subagent/status.ts +42 -71
- package/src/tools/swarm/delegate.ts +2 -1
- package/src/tools/tasks/index.ts +8 -6
- package/src/tools/tasks/task-delete.ts +69 -56
- package/src/tools/tasks/task-list.ts +31 -52
- package/src/tools/tasks/task-run.ts +74 -102
- package/src/tools/tasks/task-save.ts +33 -65
- package/src/tools/tasks/work-item-enqueue.ts +192 -134
- package/src/tools/tasks/work-item-list.ts +33 -78
- package/src/tools/tasks/work-item-remove.ts +60 -0
- package/src/tools/tasks/work-item-update.ts +114 -0
- package/src/tools/terminal/backends/native.ts +3 -1
- package/src/tools/tool-manifest.ts +20 -74
- package/src/tools/types.ts +6 -0
- package/src/tools/ui-surface/definitions.ts +6 -1
- package/src/tools/watch/screen-watch.ts +3 -1
- package/src/tools/watcher/create.ts +52 -98
- package/src/tools/watcher/delete.ts +20 -46
- package/src/tools/watcher/digest.ts +36 -70
- package/src/tools/watcher/list.ts +49 -79
- package/src/tools/watcher/update.ts +45 -91
- package/src/twitter/client.ts +690 -0
- package/src/twitter/session.ts +91 -0
- package/src/usage/types.ts +0 -1
- package/src/util/truncate.ts +6 -0
- package/src/watcher/providers/slack.ts +2 -1
- package/src/watcher/watcher-store.ts +3 -2
- package/src/work-items/work-item-store.ts +236 -2
- package/src/workspace/commit-message-enrichment-service.ts +284 -0
- package/src/workspace/commit-message-provider.ts +95 -0
- package/src/workspace/git-service.ts +272 -52
- package/src/workspace/heartbeat-service.ts +70 -13
- package/src/workspace/provider-commit-message-generator.ts +242 -0
- package/src/workspace/turn-commit.ts +100 -51
- package/src/tools/contacts/index.ts +0 -4
- package/src/tools/document/index.ts +0 -5
- package/src/tools/followups/index.ts +0 -3
- package/src/tools/subagent/index.ts +0 -5
- /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
6
|
+
import type { CommitContext } from '../workspace/commit-message-provider.js';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
CommitEnrichmentService,
|
|
10
|
+
_resetEnrichmentService,
|
|
11
|
+
} from '../workspace/commit-message-enrichment-service.js';
|
|
12
|
+
import { WorkspaceGitService, _resetGitServiceRegistry } from '../workspace/git-service.js';
|
|
13
|
+
|
|
14
|
+
describe('CommitEnrichmentService', () => {
|
|
15
|
+
let testDir: string;
|
|
16
|
+
let gitService: WorkspaceGitService;
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
testDir = join(tmpdir(), `vellum-enrichment-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
20
|
+
mkdirSync(testDir, { recursive: true });
|
|
21
|
+
_resetGitServiceRegistry();
|
|
22
|
+
_resetEnrichmentService();
|
|
23
|
+
|
|
24
|
+
gitService = new WorkspaceGitService(testDir);
|
|
25
|
+
await gitService.ensureInitialized();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
if (existsSync(testDir)) {
|
|
30
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
function makeContext(overrides?: Partial<CommitContext>): CommitContext {
|
|
35
|
+
return {
|
|
36
|
+
workspaceDir: testDir,
|
|
37
|
+
trigger: 'turn',
|
|
38
|
+
sessionId: 'sess_test',
|
|
39
|
+
turnNumber: 1,
|
|
40
|
+
changedFiles: ['file.txt'],
|
|
41
|
+
timestampMs: Date.now(),
|
|
42
|
+
...overrides,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function createCommit(): Promise<string> {
|
|
47
|
+
writeFileSync(join(testDir, `file-${Date.now()}.txt`), 'content');
|
|
48
|
+
await gitService.commitChanges('test commit');
|
|
49
|
+
return await gitService.getHeadHash();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function waitForDrain(service: CommitEnrichmentService, timeoutMs = 5000): Promise<void> {
|
|
53
|
+
const started = Date.now();
|
|
54
|
+
while (service._getQueueSize() > 0 || service._getActiveWorkers() > 0) {
|
|
55
|
+
if (Date.now() - started > timeoutMs) {
|
|
56
|
+
throw new Error(`Timed out waiting for enrichment queue to drain after ${timeoutMs}ms`);
|
|
57
|
+
}
|
|
58
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
test('enqueue and execute writes git note on success', async () => {
|
|
63
|
+
const commitHash = await createCommit();
|
|
64
|
+
const service = new CommitEnrichmentService({
|
|
65
|
+
maxQueueSize: 10,
|
|
66
|
+
maxConcurrency: 1,
|
|
67
|
+
jobTimeoutMs: 5000,
|
|
68
|
+
maxRetries: 0,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
service.enqueue({
|
|
72
|
+
workspaceDir: testDir,
|
|
73
|
+
commitHash,
|
|
74
|
+
context: makeContext(),
|
|
75
|
+
gitService,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Wait for async processing
|
|
79
|
+
await service.shutdown();
|
|
80
|
+
|
|
81
|
+
// Verify git note was written
|
|
82
|
+
const noteContent = execFileSync('git', ['notes', '--ref=vellum', 'show', commitHash], {
|
|
83
|
+
cwd: testDir,
|
|
84
|
+
encoding: 'utf-8',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const note = JSON.parse(noteContent);
|
|
88
|
+
expect(note.enriched).toBe(true);
|
|
89
|
+
expect(note.trigger).toBe('turn');
|
|
90
|
+
expect(note.sessionId).toBe('sess_test');
|
|
91
|
+
expect(note.turnNumber).toBe(1);
|
|
92
|
+
expect(note.filesChanged).toBe(1);
|
|
93
|
+
expect(service._getSucceededCount()).toBe(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('queue overflow drops oldest job', async () => {
|
|
97
|
+
const service = new CommitEnrichmentService({
|
|
98
|
+
maxQueueSize: 2,
|
|
99
|
+
maxConcurrency: 1,
|
|
100
|
+
jobTimeoutMs: 30000,
|
|
101
|
+
maxRetries: 0,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const hash1 = await createCommit();
|
|
105
|
+
const hash2 = await createCommit();
|
|
106
|
+
const hash3 = await createCommit();
|
|
107
|
+
|
|
108
|
+
// Enqueue 3 jobs — hash1 starts immediately (active worker),
|
|
109
|
+
// hash2 goes to queue (size=1), hash3 goes to queue (size=2), no overflow drop.
|
|
110
|
+
service.enqueue({ workspaceDir: testDir, commitHash: hash1, context: makeContext(), gitService });
|
|
111
|
+
service.enqueue({ workspaceDir: testDir, commitHash: hash2, context: makeContext(), gitService });
|
|
112
|
+
service.enqueue({ workspaceDir: testDir, commitHash: hash3, context: makeContext(), gitService });
|
|
113
|
+
|
|
114
|
+
// No overflow drops — queue size 2 can hold 2 pending while 1 is active
|
|
115
|
+
expect(service._getDroppedCount()).toBe(0);
|
|
116
|
+
expect(service._getQueueSize()).toBe(2);
|
|
117
|
+
|
|
118
|
+
// Shutdown discards the 2 pending jobs
|
|
119
|
+
await service.shutdown();
|
|
120
|
+
expect(service._getDroppedCount()).toBe(2);
|
|
121
|
+
expect(service._getSucceededCount()).toBe(1);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('queue overflow actually drops when truly full', async () => {
|
|
125
|
+
// Create a service where the worker is slow
|
|
126
|
+
const service = new CommitEnrichmentService({
|
|
127
|
+
maxQueueSize: 1,
|
|
128
|
+
maxConcurrency: 1,
|
|
129
|
+
jobTimeoutMs: 30000,
|
|
130
|
+
maxRetries: 0,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const hash1 = await createCommit();
|
|
134
|
+
const hash2 = await createCommit();
|
|
135
|
+
const hash3 = await createCommit();
|
|
136
|
+
|
|
137
|
+
// hash1 starts processing immediately (active worker = 1, queue empty)
|
|
138
|
+
// hash2 goes to queue (queue size = 1)
|
|
139
|
+
// hash3 tries to go to queue but it's full → drops hash2, adds hash3
|
|
140
|
+
service.enqueue({ workspaceDir: testDir, commitHash: hash1, context: makeContext(), gitService });
|
|
141
|
+
service.enqueue({ workspaceDir: testDir, commitHash: hash2, context: makeContext(), gitService });
|
|
142
|
+
service.enqueue({ workspaceDir: testDir, commitHash: hash3, context: makeContext(), gitService });
|
|
143
|
+
|
|
144
|
+
expect(service._getDroppedCount()).toBe(1);
|
|
145
|
+
|
|
146
|
+
await service.shutdown();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('fire-and-forget enqueue does not block caller', async () => {
|
|
150
|
+
const commitHash = await createCommit();
|
|
151
|
+
const service = new CommitEnrichmentService({
|
|
152
|
+
maxQueueSize: 10,
|
|
153
|
+
maxConcurrency: 1,
|
|
154
|
+
jobTimeoutMs: 5000,
|
|
155
|
+
maxRetries: 0,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const start = Date.now();
|
|
159
|
+
service.enqueue({
|
|
160
|
+
workspaceDir: testDir,
|
|
161
|
+
commitHash,
|
|
162
|
+
context: makeContext(),
|
|
163
|
+
gitService,
|
|
164
|
+
});
|
|
165
|
+
const elapsed = Date.now() - start;
|
|
166
|
+
|
|
167
|
+
// enqueue should return immediately (< 50ms)
|
|
168
|
+
expect(elapsed).toBeLessThan(50);
|
|
169
|
+
|
|
170
|
+
await service.shutdown();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('graceful shutdown drains in-flight and discards pending', async () => {
|
|
174
|
+
const hash1 = await createCommit();
|
|
175
|
+
const hash2 = await createCommit();
|
|
176
|
+
|
|
177
|
+
const service = new CommitEnrichmentService({
|
|
178
|
+
maxQueueSize: 10,
|
|
179
|
+
maxConcurrency: 1,
|
|
180
|
+
jobTimeoutMs: 5000,
|
|
181
|
+
maxRetries: 0,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
service.enqueue({ workspaceDir: testDir, commitHash: hash1, context: makeContext(), gitService });
|
|
185
|
+
service.enqueue({ workspaceDir: testDir, commitHash: hash2, context: makeContext(), gitService });
|
|
186
|
+
|
|
187
|
+
// Shutdown should complete without hanging
|
|
188
|
+
await service.shutdown();
|
|
189
|
+
|
|
190
|
+
// The first job was in-flight and should complete. The second was pending
|
|
191
|
+
// and should be discarded, counted as dropped.
|
|
192
|
+
expect(service._getSucceededCount()).toBe(1);
|
|
193
|
+
expect(service._getDroppedCount()).toBe(1);
|
|
194
|
+
expect(service._getQueueSize()).toBe(0);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('shutdown discards all pending jobs and counts them as dropped', async () => {
|
|
198
|
+
// Use maxConcurrency 1 so only one job starts processing; the rest stay pending.
|
|
199
|
+
const hashes: string[] = [];
|
|
200
|
+
for (let i = 0; i < 5; i++) {
|
|
201
|
+
hashes.push(await createCommit());
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const service = new CommitEnrichmentService({
|
|
205
|
+
maxQueueSize: 10,
|
|
206
|
+
maxConcurrency: 1,
|
|
207
|
+
jobTimeoutMs: 5000,
|
|
208
|
+
maxRetries: 0,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
for (const hash of hashes) {
|
|
212
|
+
service.enqueue({ workspaceDir: testDir, commitHash: hash, context: makeContext(), gitService });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// First job is in-flight, remaining 4 are pending
|
|
216
|
+
await service.shutdown();
|
|
217
|
+
|
|
218
|
+
// In-flight job completes, pending jobs are discarded
|
|
219
|
+
expect(service._getSucceededCount()).toBe(1);
|
|
220
|
+
expect(service._getDroppedCount()).toBe(4);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('shutdown does not cause concurrency spike', async () => {
|
|
224
|
+
const hashes: string[] = [];
|
|
225
|
+
for (let i = 0; i < 3; i++) {
|
|
226
|
+
hashes.push(await createCommit());
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const service = new CommitEnrichmentService({
|
|
230
|
+
maxQueueSize: 10,
|
|
231
|
+
maxConcurrency: 1,
|
|
232
|
+
jobTimeoutMs: 5000,
|
|
233
|
+
maxRetries: 0,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
for (const hash of hashes) {
|
|
237
|
+
service.enqueue({ workspaceDir: testDir, commitHash: hash, context: makeContext(), gitService });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await service.shutdown();
|
|
241
|
+
|
|
242
|
+
// Active workers should be 0 after shutdown
|
|
243
|
+
expect(service._getActiveWorkers()).toBe(0);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('discards jobs enqueued after shutdown', async () => {
|
|
247
|
+
const commitHash = await createCommit();
|
|
248
|
+
const service = new CommitEnrichmentService({
|
|
249
|
+
maxQueueSize: 10,
|
|
250
|
+
maxConcurrency: 1,
|
|
251
|
+
jobTimeoutMs: 5000,
|
|
252
|
+
maxRetries: 0,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
await service.shutdown();
|
|
256
|
+
|
|
257
|
+
// Enqueue after shutdown should be silently discarded
|
|
258
|
+
service.enqueue({
|
|
259
|
+
workspaceDir: testDir,
|
|
260
|
+
commitHash,
|
|
261
|
+
context: makeContext(),
|
|
262
|
+
gitService,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
expect(service._getQueueSize()).toBe(0);
|
|
266
|
+
expect(service._getSucceededCount()).toBe(0);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('multiple successful enrichments write separate git notes', async () => {
|
|
270
|
+
const hash1 = await createCommit();
|
|
271
|
+
const hash2 = await createCommit();
|
|
272
|
+
|
|
273
|
+
const service = new CommitEnrichmentService({
|
|
274
|
+
maxQueueSize: 10,
|
|
275
|
+
maxConcurrency: 1,
|
|
276
|
+
jobTimeoutMs: 5000,
|
|
277
|
+
maxRetries: 0,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
service.enqueue({
|
|
281
|
+
workspaceDir: testDir,
|
|
282
|
+
commitHash: hash1,
|
|
283
|
+
context: makeContext({ turnNumber: 1 }),
|
|
284
|
+
gitService,
|
|
285
|
+
});
|
|
286
|
+
service.enqueue({
|
|
287
|
+
workspaceDir: testDir,
|
|
288
|
+
commitHash: hash2,
|
|
289
|
+
context: makeContext({ turnNumber: 2 }),
|
|
290
|
+
gitService,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Wait for queue to drain before shutdown (avoids discarding pending jobs)
|
|
294
|
+
await waitForDrain(service, 5000);
|
|
295
|
+
await service.shutdown();
|
|
296
|
+
|
|
297
|
+
// Both notes should exist
|
|
298
|
+
const note1 = JSON.parse(execFileSync('git', ['notes', '--ref=vellum', 'show', hash1], {
|
|
299
|
+
cwd: testDir, encoding: 'utf-8',
|
|
300
|
+
}));
|
|
301
|
+
const note2 = JSON.parse(execFileSync('git', ['notes', '--ref=vellum', 'show', hash2], {
|
|
302
|
+
cwd: testDir, encoding: 'utf-8',
|
|
303
|
+
}));
|
|
304
|
+
|
|
305
|
+
expect(note1.turnNumber).toBe(1);
|
|
306
|
+
expect(note2.turnNumber).toBe(2);
|
|
307
|
+
expect(service._getSucceededCount()).toBe(2);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test('job timeout triggers retry with backoff then fails after max retries', async () => {
|
|
311
|
+
// Use a very short timeout so the real git notes write times out
|
|
312
|
+
const service = new CommitEnrichmentService({
|
|
313
|
+
maxQueueSize: 10,
|
|
314
|
+
maxConcurrency: 1,
|
|
315
|
+
jobTimeoutMs: 1, // 1ms timeout — will always time out
|
|
316
|
+
maxRetries: 2,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const commitHash = await createCommit();
|
|
320
|
+
service.enqueue({
|
|
321
|
+
workspaceDir: testDir,
|
|
322
|
+
commitHash,
|
|
323
|
+
context: makeContext(),
|
|
324
|
+
gitService,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Wait for all retries to complete (initial + 2 retries, with backoff)
|
|
328
|
+
// Backoff: 1s after attempt 1, 2s after attempt 2 = ~3s total
|
|
329
|
+
// But since the job itself is very fast to time out, total time is dominated by backoff
|
|
330
|
+
await waitForDrain(service, 10000);
|
|
331
|
+
await service.shutdown();
|
|
332
|
+
|
|
333
|
+
// After 1 initial attempt + 2 retries (3 total), the job should be counted as failed
|
|
334
|
+
expect(service._getFailedCount()).toBe(1);
|
|
335
|
+
expect(service._getSucceededCount()).toBe(0);
|
|
336
|
+
}, 15000); // Allow up to 15s for backoff delays
|
|
337
|
+
|
|
338
|
+
test('queue overflow drop behavior is deterministic', async () => {
|
|
339
|
+
// With maxQueueSize=2 and maxConcurrency=1:
|
|
340
|
+
// - Job A starts processing immediately (in-flight)
|
|
341
|
+
// - Job B enters queue (size=1)
|
|
342
|
+
// - Job C enters queue (size=2)
|
|
343
|
+
// - Job D overflows: drops oldest (B), adds D → queue has [C, D]
|
|
344
|
+
// - Job E overflows: drops oldest (C), adds E → queue has [D, E]
|
|
345
|
+
const service = new CommitEnrichmentService({
|
|
346
|
+
maxQueueSize: 2,
|
|
347
|
+
maxConcurrency: 1,
|
|
348
|
+
jobTimeoutMs: 30000,
|
|
349
|
+
maxRetries: 0,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const hashA = await createCommit();
|
|
353
|
+
const hashB = await createCommit();
|
|
354
|
+
const hashC = await createCommit();
|
|
355
|
+
const hashD = await createCommit();
|
|
356
|
+
const hashE = await createCommit();
|
|
357
|
+
|
|
358
|
+
service.enqueue({ workspaceDir: testDir, commitHash: hashA, context: makeContext({ turnNumber: 1 }), gitService });
|
|
359
|
+
service.enqueue({ workspaceDir: testDir, commitHash: hashB, context: makeContext({ turnNumber: 2 }), gitService });
|
|
360
|
+
service.enqueue({ workspaceDir: testDir, commitHash: hashC, context: makeContext({ turnNumber: 3 }), gitService });
|
|
361
|
+
// No drops yet: A is in-flight, B and C in queue (size=2)
|
|
362
|
+
expect(service._getDroppedCount()).toBe(0);
|
|
363
|
+
|
|
364
|
+
service.enqueue({ workspaceDir: testDir, commitHash: hashD, context: makeContext({ turnNumber: 4 }), gitService });
|
|
365
|
+
// Queue was full (2), so oldest (B) was dropped
|
|
366
|
+
expect(service._getDroppedCount()).toBe(1);
|
|
367
|
+
|
|
368
|
+
service.enqueue({ workspaceDir: testDir, commitHash: hashE, context: makeContext({ turnNumber: 5 }), gitService });
|
|
369
|
+
// Queue was full again (2), so oldest (C) was dropped
|
|
370
|
+
expect(service._getDroppedCount()).toBe(2);
|
|
371
|
+
|
|
372
|
+
// Queue should have exactly 2 items: D and E
|
|
373
|
+
expect(service._getQueueSize()).toBe(2);
|
|
374
|
+
|
|
375
|
+
await service.shutdown();
|
|
376
|
+
|
|
377
|
+
// A was in-flight and completed; D and E were pending and discarded at shutdown
|
|
378
|
+
expect(service._getSucceededCount()).toBe(1);
|
|
379
|
+
// 2 overflow drops + 2 shutdown discards = 4 total
|
|
380
|
+
expect(service._getDroppedCount()).toBe(4);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test('timed-out enrichment work is cancelled via AbortSignal', async () => {
|
|
384
|
+
// Track whether the slow enrichment work actually ran to completion
|
|
385
|
+
let enrichmentCompleted = false;
|
|
386
|
+
const commitHash = await createCommit();
|
|
387
|
+
|
|
388
|
+
const service = new CommitEnrichmentService({
|
|
389
|
+
maxQueueSize: 10,
|
|
390
|
+
maxConcurrency: 1,
|
|
391
|
+
jobTimeoutMs: 50, // Very short timeout
|
|
392
|
+
maxRetries: 0,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Monkey-patch writeNote to simulate slow work that respects the abort signal.
|
|
396
|
+
// The real writeNote now passes the signal to execFileAsync which kills the
|
|
397
|
+
// child process on abort. This mock replicates that behavior by rejecting
|
|
398
|
+
// when the signal fires.
|
|
399
|
+
const originalWriteNote = gitService.writeNote.bind(gitService);
|
|
400
|
+
gitService.writeNote = async (_hash: string, _note: string, signal?: AbortSignal) => {
|
|
401
|
+
// Simulate slow work that is cancellable via AbortSignal
|
|
402
|
+
await new Promise<void>((resolve, reject) => {
|
|
403
|
+
const timer = setTimeout(() => {
|
|
404
|
+
enrichmentCompleted = true;
|
|
405
|
+
resolve();
|
|
406
|
+
}, 2000);
|
|
407
|
+
signal?.addEventListener('abort', () => {
|
|
408
|
+
clearTimeout(timer);
|
|
409
|
+
reject(new Error('aborted'));
|
|
410
|
+
}, { once: true });
|
|
411
|
+
});
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
service.enqueue({
|
|
415
|
+
workspaceDir: testDir,
|
|
416
|
+
commitHash,
|
|
417
|
+
context: makeContext(),
|
|
418
|
+
gitService,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
await waitForDrain(service, 5000);
|
|
422
|
+
await service.shutdown();
|
|
423
|
+
|
|
424
|
+
// Allow any zombie work to settle — if abort didn't work, the 2s timer
|
|
425
|
+
// would still be running and would set enrichmentCompleted=true. Wait
|
|
426
|
+
// longer than the 2000ms mock delay to reliably catch the regression.
|
|
427
|
+
await new Promise(resolve => setTimeout(resolve, 2500));
|
|
428
|
+
|
|
429
|
+
// The job should have timed out and been counted as failed
|
|
430
|
+
expect(service._getFailedCount()).toBe(1);
|
|
431
|
+
expect(service._getSucceededCount()).toBe(0);
|
|
432
|
+
// The slow enrichment work should NOT have completed since the signal was aborted
|
|
433
|
+
expect(enrichmentCompleted).toBe(false);
|
|
434
|
+
|
|
435
|
+
// Restore original
|
|
436
|
+
gitService.writeNote = originalWriteNote;
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test('shutdown does not hang on timed-out jobs', async () => {
|
|
440
|
+
const commitHash = await createCommit();
|
|
441
|
+
|
|
442
|
+
const service = new CommitEnrichmentService({
|
|
443
|
+
maxQueueSize: 10,
|
|
444
|
+
maxConcurrency: 1,
|
|
445
|
+
jobTimeoutMs: 50, // Short timeout
|
|
446
|
+
maxRetries: 0,
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// Make writeNote artificially slow so the job will always time out.
|
|
450
|
+
// The mock respects the abort signal so the subprocess is killed on timeout.
|
|
451
|
+
const originalWriteNote = gitService.writeNote.bind(gitService);
|
|
452
|
+
gitService.writeNote = async (_hash: string, _note: string, signal?: AbortSignal) => {
|
|
453
|
+
await new Promise<void>((resolve, reject) => {
|
|
454
|
+
const timer = setTimeout(resolve, 5000);
|
|
455
|
+
signal?.addEventListener('abort', () => {
|
|
456
|
+
clearTimeout(timer);
|
|
457
|
+
reject(new Error('aborted'));
|
|
458
|
+
}, { once: true });
|
|
459
|
+
});
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
service.enqueue({
|
|
463
|
+
workspaceDir: testDir,
|
|
464
|
+
commitHash,
|
|
465
|
+
context: makeContext(),
|
|
466
|
+
gitService,
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Shutdown should complete promptly, not hang for 5s waiting on the slow writeNote
|
|
470
|
+
const shutdownStart = Date.now();
|
|
471
|
+
await service.shutdown();
|
|
472
|
+
const shutdownElapsed = Date.now() - shutdownStart;
|
|
473
|
+
|
|
474
|
+
// Shutdown should complete well under the 5s slow-work duration
|
|
475
|
+
expect(shutdownElapsed).toBeLessThan(3000);
|
|
476
|
+
expect(service._getFailedCount()).toBe(1);
|
|
477
|
+
|
|
478
|
+
gitService.writeNote = originalWriteNote;
|
|
479
|
+
}, 10000);
|
|
480
|
+
|
|
481
|
+
test('abort signal is triggered on non-timeout errors before retry', async () => {
|
|
482
|
+
const commitHash = await createCommit();
|
|
483
|
+
|
|
484
|
+
const service = new CommitEnrichmentService({
|
|
485
|
+
maxQueueSize: 10,
|
|
486
|
+
maxConcurrency: 1,
|
|
487
|
+
jobTimeoutMs: 5000,
|
|
488
|
+
maxRetries: 0,
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// Make writeNote throw an error and observe whether the signal gets aborted
|
|
492
|
+
const originalWriteNote = gitService.writeNote.bind(gitService);
|
|
493
|
+
gitService.writeNote = async (_hash: string, _note: string) => {
|
|
494
|
+
// Set up a listener on the abort controller's signal to track abortion.
|
|
495
|
+
// We access the signal indirectly by throwing, which triggers the catch
|
|
496
|
+
// block in executeJob where controller.abort() is called.
|
|
497
|
+
throw new Error('Simulated writeNote failure');
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
service.enqueue({
|
|
501
|
+
workspaceDir: testDir,
|
|
502
|
+
commitHash,
|
|
503
|
+
context: makeContext(),
|
|
504
|
+
gitService,
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
await waitForDrain(service, 5000);
|
|
508
|
+
await service.shutdown();
|
|
509
|
+
|
|
510
|
+
// The job should have failed (no retries configured)
|
|
511
|
+
expect(service._getFailedCount()).toBe(1);
|
|
512
|
+
expect(service._getSucceededCount()).toBe(0);
|
|
513
|
+
|
|
514
|
+
gitService.writeNote = originalWriteNote;
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test('enqueue is fire-and-forget and never throws even when called rapidly', async () => {
|
|
518
|
+
const service = new CommitEnrichmentService({
|
|
519
|
+
maxQueueSize: 3,
|
|
520
|
+
maxConcurrency: 1,
|
|
521
|
+
jobTimeoutMs: 5000,
|
|
522
|
+
maxRetries: 0,
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
const hashes: string[] = [];
|
|
526
|
+
for (let i = 0; i < 5; i++) {
|
|
527
|
+
hashes.push(await createCommit());
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Rapidly enqueue more jobs than the queue can hold — must never throw
|
|
531
|
+
const fn = () => {
|
|
532
|
+
for (const hash of hashes) {
|
|
533
|
+
service.enqueue({
|
|
534
|
+
workspaceDir: testDir,
|
|
535
|
+
commitHash: hash,
|
|
536
|
+
context: makeContext(),
|
|
537
|
+
gitService,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
expect(fn).not.toThrow();
|
|
543
|
+
|
|
544
|
+
// Some jobs should have been dropped due to overflow (queue size 3, 1 in-flight)
|
|
545
|
+
// 5 jobs: 1 in-flight + 3 queue + 1 overflow = at least 1 drop
|
|
546
|
+
expect(service._getDroppedCount()).toBeGreaterThanOrEqual(1);
|
|
547
|
+
|
|
548
|
+
await service.shutdown();
|
|
549
|
+
});
|
|
550
|
+
});
|