@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
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import type { SwarmTaskNode } from "../swarm/types.js";
|
|
4
|
-
import type {
|
|
5
|
-
SwarmWorkerBackend,
|
|
6
|
-
SwarmWorkerBackendInput,
|
|
7
|
-
} from "../swarm/worker-backend.js";
|
|
8
|
-
import {
|
|
9
|
-
buildWorkerPrompt,
|
|
10
|
-
parseWorkerOutput,
|
|
11
|
-
} from "../swarm/worker-prompts.js";
|
|
12
|
-
import type { WorkerStatusKind } from "../swarm/worker-runner.js";
|
|
13
|
-
import { runWorkerTask } from "../swarm/worker-runner.js";
|
|
14
|
-
|
|
15
|
-
function makeTask(overrides?: Partial<SwarmTaskNode>): SwarmTaskNode {
|
|
16
|
-
return {
|
|
17
|
-
id: "test-task",
|
|
18
|
-
role: "coder",
|
|
19
|
-
objective: "Write a function",
|
|
20
|
-
dependencies: [],
|
|
21
|
-
...overrides,
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function makeBackend(
|
|
26
|
-
overrides?: Partial<SwarmWorkerBackend>,
|
|
27
|
-
): SwarmWorkerBackend {
|
|
28
|
-
return {
|
|
29
|
-
name: "test-backend",
|
|
30
|
-
isAvailable: () => true,
|
|
31
|
-
runTask: async () => ({
|
|
32
|
-
success: true,
|
|
33
|
-
output:
|
|
34
|
-
'```json\n{"summary":"Done","artifacts":[],"issues":[],"nextSteps":[]}\n```',
|
|
35
|
-
durationMs: 100,
|
|
36
|
-
}),
|
|
37
|
-
...overrides,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
describe("runWorkerTask", () => {
|
|
42
|
-
test("returns completed result on success", async () => {
|
|
43
|
-
const result = await runWorkerTask({
|
|
44
|
-
task: makeTask(),
|
|
45
|
-
backend: makeBackend(),
|
|
46
|
-
workingDir: "/tmp",
|
|
47
|
-
timeoutMs: 5000,
|
|
48
|
-
});
|
|
49
|
-
expect(result.status).toBe("completed");
|
|
50
|
-
expect(result.taskId).toBe("test-task");
|
|
51
|
-
expect(result.summary).toBe("Done");
|
|
52
|
-
expect(result.durationMs).toBe(100);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("returns failed result when backend is unavailable", async () => {
|
|
56
|
-
const result = await runWorkerTask({
|
|
57
|
-
task: makeTask(),
|
|
58
|
-
backend: makeBackend({ isAvailable: () => false }),
|
|
59
|
-
workingDir: "/tmp",
|
|
60
|
-
timeoutMs: 5000,
|
|
61
|
-
});
|
|
62
|
-
expect(result.status).toBe("failed");
|
|
63
|
-
expect(result.issues[0]).toContain("unavailable");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("returns failed result when backend returns failure", async () => {
|
|
67
|
-
const result = await runWorkerTask({
|
|
68
|
-
task: makeTask(),
|
|
69
|
-
backend: makeBackend({
|
|
70
|
-
runTask: async () => ({
|
|
71
|
-
success: false,
|
|
72
|
-
output: "Something went wrong",
|
|
73
|
-
failureReason: "timeout",
|
|
74
|
-
durationMs: 900,
|
|
75
|
-
}),
|
|
76
|
-
}),
|
|
77
|
-
workingDir: "/tmp",
|
|
78
|
-
timeoutMs: 5000,
|
|
79
|
-
});
|
|
80
|
-
expect(result.status).toBe("failed");
|
|
81
|
-
expect(result.issues).toContain("timeout");
|
|
82
|
-
expect(result.durationMs).toBe(900);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test("returns failed result when backend throws", async () => {
|
|
86
|
-
const result = await runWorkerTask({
|
|
87
|
-
task: makeTask(),
|
|
88
|
-
backend: makeBackend({
|
|
89
|
-
runTask: async () => {
|
|
90
|
-
throw new Error("Boom");
|
|
91
|
-
},
|
|
92
|
-
}),
|
|
93
|
-
workingDir: "/tmp",
|
|
94
|
-
timeoutMs: 5000,
|
|
95
|
-
});
|
|
96
|
-
expect(result.status).toBe("failed");
|
|
97
|
-
expect(result.summary).toContain("Boom");
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test("emits status callbacks in order", async () => {
|
|
101
|
-
const statuses: WorkerStatusKind[] = [];
|
|
102
|
-
await runWorkerTask({
|
|
103
|
-
task: makeTask(),
|
|
104
|
-
backend: makeBackend(),
|
|
105
|
-
workingDir: "/tmp",
|
|
106
|
-
timeoutMs: 5000,
|
|
107
|
-
onStatus: (_taskId, status) => statuses.push(status),
|
|
108
|
-
});
|
|
109
|
-
expect(statuses).toEqual(["queued", "running", "completed"]);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("emits queued then failed when backend unavailable", async () => {
|
|
113
|
-
const statuses: WorkerStatusKind[] = [];
|
|
114
|
-
await runWorkerTask({
|
|
115
|
-
task: makeTask(),
|
|
116
|
-
backend: makeBackend({ isAvailable: () => false }),
|
|
117
|
-
workingDir: "/tmp",
|
|
118
|
-
timeoutMs: 5000,
|
|
119
|
-
onStatus: (_taskId, status) => statuses.push(status),
|
|
120
|
-
});
|
|
121
|
-
expect(statuses).toEqual(["queued", "failed"]);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("continues execution when onStatus callback throws", async () => {
|
|
125
|
-
const result = await runWorkerTask({
|
|
126
|
-
task: makeTask(),
|
|
127
|
-
backend: makeBackend(),
|
|
128
|
-
workingDir: "/tmp",
|
|
129
|
-
timeoutMs: 5000,
|
|
130
|
-
onStatus: () => {
|
|
131
|
-
throw new Error("status callback failed");
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
expect(result.status).toBe("completed");
|
|
135
|
-
expect(result.summary).toBe("Done");
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test("maps role to correct profile in prompt", async () => {
|
|
139
|
-
let capturedInput: SwarmWorkerBackendInput | null = null;
|
|
140
|
-
await runWorkerTask({
|
|
141
|
-
task: makeTask({ role: "researcher" }),
|
|
142
|
-
backend: makeBackend({
|
|
143
|
-
runTask: async (input) => {
|
|
144
|
-
capturedInput = input;
|
|
145
|
-
return { success: true, output: "ok", durationMs: 50 };
|
|
146
|
-
},
|
|
147
|
-
}),
|
|
148
|
-
workingDir: "/tmp",
|
|
149
|
-
timeoutMs: 5000,
|
|
150
|
-
});
|
|
151
|
-
expect(capturedInput!.profile).toBe("researcher");
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("includes dependency outputs in prompt", async () => {
|
|
155
|
-
let capturedInput: SwarmWorkerBackendInput | null = null;
|
|
156
|
-
await runWorkerTask({
|
|
157
|
-
task: makeTask(),
|
|
158
|
-
dependencyOutputs: [{ taskId: "dep-1", summary: "Research complete" }],
|
|
159
|
-
backend: makeBackend({
|
|
160
|
-
runTask: async (input) => {
|
|
161
|
-
capturedInput = input;
|
|
162
|
-
return { success: true, output: "ok", durationMs: 50 };
|
|
163
|
-
},
|
|
164
|
-
}),
|
|
165
|
-
workingDir: "/tmp",
|
|
166
|
-
timeoutMs: 5000,
|
|
167
|
-
});
|
|
168
|
-
expect(capturedInput!.prompt).toContain("dep-1");
|
|
169
|
-
expect(capturedInput!.prompt).toContain("Research complete");
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
describe("parseWorkerOutput", () => {
|
|
174
|
-
test("parses valid fenced JSON", () => {
|
|
175
|
-
const raw =
|
|
176
|
-
'Some preamble\n```json\n{"summary":"Done","artifacts":["file.ts"],"issues":[],"nextSteps":["test"]}\n```\nSome epilogue';
|
|
177
|
-
const result = parseWorkerOutput(raw);
|
|
178
|
-
expect(result.summary).toBe("Done");
|
|
179
|
-
expect(result.artifacts).toEqual(["file.ts"]);
|
|
180
|
-
expect(result.nextSteps).toEqual(["test"]);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test("falls back to raw summary on invalid JSON", () => {
|
|
184
|
-
const raw = "```json\n{invalid json}\n```";
|
|
185
|
-
const result = parseWorkerOutput(raw);
|
|
186
|
-
expect(result.summary).toBe(raw.slice(0, 500));
|
|
187
|
-
expect(result.artifacts).toEqual([]);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
test("falls back to raw summary when no JSON block", () => {
|
|
191
|
-
const raw = "Just a plain text output without any JSON.";
|
|
192
|
-
const result = parseWorkerOutput(raw);
|
|
193
|
-
expect(result.summary).toBe(raw);
|
|
194
|
-
expect(result.artifacts).toEqual([]);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
test("truncates long raw output to 500 chars", () => {
|
|
198
|
-
const raw = "x".repeat(1000);
|
|
199
|
-
const result = parseWorkerOutput(raw);
|
|
200
|
-
expect(result.summary.length).toBe(500);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test("uses the final fenced JSON block when multiple are present", () => {
|
|
204
|
-
const raw = [
|
|
205
|
-
"```json",
|
|
206
|
-
'{"summary":"intermediate","artifacts":["a.ts"],"issues":[],"nextSteps":[]}',
|
|
207
|
-
"```",
|
|
208
|
-
"",
|
|
209
|
-
"```json",
|
|
210
|
-
'{"summary":"final","artifacts":["b.ts"],"issues":["warn"],"nextSteps":["ship"]}',
|
|
211
|
-
"```",
|
|
212
|
-
].join("\n");
|
|
213
|
-
const result = parseWorkerOutput(raw);
|
|
214
|
-
expect(result.summary).toBe("final");
|
|
215
|
-
expect(result.artifacts).toEqual(["b.ts"]);
|
|
216
|
-
expect(result.issues).toEqual(["warn"]);
|
|
217
|
-
expect(result.nextSteps).toEqual(["ship"]);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
test("skips trailing non-contract JSON and picks the last valid block", () => {
|
|
221
|
-
const raw = [
|
|
222
|
-
"```json",
|
|
223
|
-
'{"summary":"real result","artifacts":["out.ts"],"issues":[],"nextSteps":["deploy"]}',
|
|
224
|
-
"```",
|
|
225
|
-
"",
|
|
226
|
-
"Here is an example config:",
|
|
227
|
-
"```json",
|
|
228
|
-
'{"port":3000,"debug":true}',
|
|
229
|
-
"```",
|
|
230
|
-
].join("\n");
|
|
231
|
-
const result = parseWorkerOutput(raw);
|
|
232
|
-
expect(result.summary).toBe("real result");
|
|
233
|
-
expect(result.artifacts).toEqual(["out.ts"]);
|
|
234
|
-
expect(result.nextSteps).toEqual(["deploy"]);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
test("skips trailing malformed JSON and picks an earlier valid block", () => {
|
|
238
|
-
const raw = [
|
|
239
|
-
"```json",
|
|
240
|
-
'{"summary":"good","artifacts":[],"issues":[],"nextSteps":[]}',
|
|
241
|
-
"```",
|
|
242
|
-
"",
|
|
243
|
-
"```json",
|
|
244
|
-
"{this is not valid json}",
|
|
245
|
-
"```",
|
|
246
|
-
].join("\n");
|
|
247
|
-
const result = parseWorkerOutput(raw);
|
|
248
|
-
expect(result.summary).toBe("good");
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
describe("buildWorkerPrompt", () => {
|
|
253
|
-
test("includes role and objective", () => {
|
|
254
|
-
const prompt = buildWorkerPrompt({
|
|
255
|
-
role: "coder",
|
|
256
|
-
objective: "Build feature X",
|
|
257
|
-
});
|
|
258
|
-
expect(prompt).toContain("coder");
|
|
259
|
-
expect(prompt).toContain("Build feature X");
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
test("includes upstream context when provided", () => {
|
|
263
|
-
const prompt = buildWorkerPrompt({
|
|
264
|
-
role: "coder",
|
|
265
|
-
objective: "Build it",
|
|
266
|
-
upstreamContext: "This is a React project",
|
|
267
|
-
});
|
|
268
|
-
expect(prompt).toContain("This is a React project");
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
test("includes dependency outputs when provided", () => {
|
|
272
|
-
const prompt = buildWorkerPrompt({
|
|
273
|
-
role: "coder",
|
|
274
|
-
objective: "Build it",
|
|
275
|
-
dependencyOutputs: [
|
|
276
|
-
{ taskId: "research", summary: "Found the API docs" },
|
|
277
|
-
],
|
|
278
|
-
});
|
|
279
|
-
expect(prompt).toContain("research");
|
|
280
|
-
expect(prompt).toContain("Found the API docs");
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
test("includes output contract instructions", () => {
|
|
284
|
-
const prompt = buildWorkerPrompt({ role: "coder", objective: "Test" });
|
|
285
|
-
expect(prompt).toContain("```json");
|
|
286
|
-
expect(prompt).toContain("summary");
|
|
287
|
-
});
|
|
288
|
-
});
|
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
discoverCCCommands,
|
|
8
|
-
getCCCommand,
|
|
9
|
-
invalidateCCCommandCache,
|
|
10
|
-
loadCCCommandTemplate,
|
|
11
|
-
} from "../cc-command-registry.js";
|
|
12
|
-
|
|
13
|
-
let tmpDir: string;
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
tmpDir = mkdtempSync(join(tmpdir(), "cc-cmd-test-"));
|
|
17
|
-
invalidateCCCommandCache();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
22
|
-
invalidateCCCommandCache();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
/** Helper to create a .claude/commands/ directory with markdown files. */
|
|
26
|
-
function createCommandsDir(base: string, files: Record<string, string>): void {
|
|
27
|
-
const commandsDir = join(base, ".claude", "commands");
|
|
28
|
-
mkdirSync(commandsDir, { recursive: true });
|
|
29
|
-
for (const [name, content] of Object.entries(files)) {
|
|
30
|
-
writeFileSync(join(commandsDir, name), content, "utf-8");
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
describe("discoverCCCommands", () => {
|
|
35
|
-
test("discovers commands in .claude/commands/", () => {
|
|
36
|
-
createCommandsDir(tmpDir, {
|
|
37
|
-
"hello.md": "# Hello World\nThis is the hello command.",
|
|
38
|
-
"deploy.md": "Deploy the application to production.",
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
const registry = discoverCCCommands(tmpDir);
|
|
42
|
-
expect(registry.entries.size).toBe(2);
|
|
43
|
-
|
|
44
|
-
const hello = registry.entries.get("hello");
|
|
45
|
-
expect(hello).toBeDefined();
|
|
46
|
-
expect(hello!.name).toBe("hello");
|
|
47
|
-
expect(hello!.summary).toBe("Hello World");
|
|
48
|
-
expect(hello!.source).toBe(tmpDir);
|
|
49
|
-
|
|
50
|
-
const deploy = registry.entries.get("deploy");
|
|
51
|
-
expect(deploy).toBeDefined();
|
|
52
|
-
expect(deploy!.name).toBe("deploy");
|
|
53
|
-
expect(deploy!.summary).toBe("Deploy the application to production.");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("child directory commands override parent on name collisions", () => {
|
|
57
|
-
// Create parent commands
|
|
58
|
-
createCommandsDir(tmpDir, {
|
|
59
|
-
"shared.md": "Parent version of shared command.",
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// Create child directory with overriding command
|
|
63
|
-
const childDir = join(tmpDir, "project");
|
|
64
|
-
mkdirSync(childDir, { recursive: true });
|
|
65
|
-
createCommandsDir(childDir, {
|
|
66
|
-
"shared.md": "Child version of shared command.",
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const registry = discoverCCCommands(childDir);
|
|
70
|
-
const shared = registry.entries.get("shared");
|
|
71
|
-
expect(shared).toBeDefined();
|
|
72
|
-
expect(shared!.summary).toBe("Child version of shared command.");
|
|
73
|
-
expect(shared!.source).toBe(childDir);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("invalid filenames are skipped", () => {
|
|
77
|
-
createCommandsDir(tmpDir, {
|
|
78
|
-
"valid-name.md": "A valid command.",
|
|
79
|
-
".hidden.md": "Hidden file should be skipped.",
|
|
80
|
-
"-starts-with-dash.md": "Invalid start character.",
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
const registry = discoverCCCommands(tmpDir);
|
|
84
|
-
expect(registry.entries.size).toBe(1);
|
|
85
|
-
expect(registry.entries.has("valid-name")).toBe(true);
|
|
86
|
-
expect(registry.entries.has(".hidden")).toBe(false);
|
|
87
|
-
expect(registry.entries.has("-starts-with-dash")).toBe(false);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("non-.md files are ignored", () => {
|
|
91
|
-
createCommandsDir(tmpDir, {
|
|
92
|
-
"readme.txt": "Not a markdown file.",
|
|
93
|
-
"command.md": "A real command.",
|
|
94
|
-
"notes.json": "{}",
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
const registry = discoverCCCommands(tmpDir);
|
|
98
|
-
expect(registry.entries.size).toBe(1);
|
|
99
|
-
expect(registry.entries.has("command")).toBe(true);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test("empty directory returns empty registry", () => {
|
|
103
|
-
const commandsDir = join(tmpDir, ".claude", "commands");
|
|
104
|
-
mkdirSync(commandsDir, { recursive: true });
|
|
105
|
-
|
|
106
|
-
const registry = discoverCCCommands(tmpDir);
|
|
107
|
-
expect(registry.entries.size).toBe(0);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test("no .claude/commands/ directory returns empty registry", () => {
|
|
111
|
-
const registry = discoverCCCommands(tmpDir);
|
|
112
|
-
expect(registry.entries.size).toBe(0);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("commands from multiple ancestor levels are merged", () => {
|
|
116
|
-
// Parent has a unique command
|
|
117
|
-
createCommandsDir(tmpDir, {
|
|
118
|
-
"parent-only.md": "Only in parent.",
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// Child has a different command
|
|
122
|
-
const childDir = join(tmpDir, "child");
|
|
123
|
-
mkdirSync(childDir, { recursive: true });
|
|
124
|
-
createCommandsDir(childDir, {
|
|
125
|
-
"child-only.md": "Only in child.",
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
const registry = discoverCCCommands(childDir);
|
|
129
|
-
expect(registry.entries.size).toBe(2);
|
|
130
|
-
expect(registry.entries.has("parent-only")).toBe(true);
|
|
131
|
-
expect(registry.entries.has("child-only")).toBe(true);
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
describe("caching", () => {
|
|
136
|
-
test("cache returns same instance within TTL", () => {
|
|
137
|
-
createCommandsDir(tmpDir, {
|
|
138
|
-
"test.md": "Test command.",
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const first = discoverCCCommands(tmpDir);
|
|
142
|
-
const second = discoverCCCommands(tmpDir);
|
|
143
|
-
expect(first).toBe(second); // same object reference
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("invalidateCCCommandCache forces re-discovery", () => {
|
|
147
|
-
createCommandsDir(tmpDir, {
|
|
148
|
-
"test.md": "Test command.",
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
const first = discoverCCCommands(tmpDir);
|
|
152
|
-
|
|
153
|
-
invalidateCCCommandCache();
|
|
154
|
-
|
|
155
|
-
const second = discoverCCCommands(tmpDir);
|
|
156
|
-
expect(first).not.toBe(second); // different object reference
|
|
157
|
-
expect(second.entries.size).toBe(1);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("expired TTL forces re-discovery", () => {
|
|
161
|
-
createCommandsDir(tmpDir, {
|
|
162
|
-
"test.md": "Test command.",
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// Use a very short TTL
|
|
166
|
-
const first = discoverCCCommands(tmpDir, 0);
|
|
167
|
-
const second = discoverCCCommands(tmpDir, 0);
|
|
168
|
-
expect(first).not.toBe(second); // different object reference due to expired TTL
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
describe("getCCCommand", () => {
|
|
173
|
-
test("looks up command by name (case-insensitive)", () => {
|
|
174
|
-
createCommandsDir(tmpDir, {
|
|
175
|
-
"MyCommand.md": "My command description.",
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
const entry = getCCCommand(tmpDir, "mycommand");
|
|
179
|
-
expect(entry).toBeDefined();
|
|
180
|
-
expect(entry!.name).toBe("MyCommand");
|
|
181
|
-
|
|
182
|
-
const entryUpper = getCCCommand(tmpDir, "MYCOMMAND");
|
|
183
|
-
expect(entryUpper).toBeDefined();
|
|
184
|
-
expect(entryUpper!.name).toBe("MyCommand");
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
test("returns undefined for non-existent command", () => {
|
|
188
|
-
createCommandsDir(tmpDir, {
|
|
189
|
-
"exists.md": "I exist.",
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
const entry = getCCCommand(tmpDir, "nonexistent");
|
|
193
|
-
expect(entry).toBeUndefined();
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
describe("loadCCCommandTemplate", () => {
|
|
198
|
-
test("reads full file content at execution time", () => {
|
|
199
|
-
const fullContent =
|
|
200
|
-
"---\ntitle: Test\n---\n\n# Test Command\n\nThis is the full template body.\n\n## Arguments\n- arg1: required\n- arg2: optional\n";
|
|
201
|
-
createCommandsDir(tmpDir, {
|
|
202
|
-
"test.md": fullContent,
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
const registry = discoverCCCommands(tmpDir);
|
|
206
|
-
const entry = registry.entries.get("test")!;
|
|
207
|
-
expect(entry).toBeDefined();
|
|
208
|
-
|
|
209
|
-
const template = loadCCCommandTemplate(entry);
|
|
210
|
-
expect(template).toBe(fullContent);
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
describe("summary extraction", () => {
|
|
215
|
-
test("skips YAML frontmatter", () => {
|
|
216
|
-
createCommandsDir(tmpDir, {
|
|
217
|
-
"with-frontmatter.md":
|
|
218
|
-
"---\ntitle: My Command\nauthor: test\n---\n\nActual summary line.",
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
const registry = discoverCCCommands(tmpDir);
|
|
222
|
-
const entry = registry.entries.get("with-frontmatter");
|
|
223
|
-
expect(entry).toBeDefined();
|
|
224
|
-
expect(entry!.summary).toBe("Actual summary line.");
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
test("strips heading markers", () => {
|
|
228
|
-
createCommandsDir(tmpDir, {
|
|
229
|
-
"heading.md": "## This is a heading",
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
const registry = discoverCCCommands(tmpDir);
|
|
233
|
-
const entry = registry.entries.get("heading");
|
|
234
|
-
expect(entry).toBeDefined();
|
|
235
|
-
expect(entry!.summary).toBe("This is a heading");
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
test("strips multiple heading levels", () => {
|
|
239
|
-
createCommandsDir(tmpDir, {
|
|
240
|
-
"h1.md": "# H1 Heading",
|
|
241
|
-
"h3.md": "### H3 Heading",
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
const registry = discoverCCCommands(tmpDir);
|
|
245
|
-
expect(registry.entries.get("h1")!.summary).toBe("H1 Heading");
|
|
246
|
-
expect(registry.entries.get("h3")!.summary).toBe("H3 Heading");
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
test("skips empty lines before summary", () => {
|
|
250
|
-
createCommandsDir(tmpDir, {
|
|
251
|
-
"empty-lines.md": "\n\n\nFirst real line.",
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
const registry = discoverCCCommands(tmpDir);
|
|
255
|
-
expect(registry.entries.get("empty-lines")!.summary).toBe(
|
|
256
|
-
"First real line.",
|
|
257
|
-
);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
test("truncates summary to 100 characters", () => {
|
|
261
|
-
const longLine = "A".repeat(150);
|
|
262
|
-
createCommandsDir(tmpDir, {
|
|
263
|
-
"long.md": longLine,
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
const registry = discoverCCCommands(tmpDir);
|
|
267
|
-
const entry = registry.entries.get("long");
|
|
268
|
-
expect(entry).toBeDefined();
|
|
269
|
-
expect(entry!.summary.length).toBe(100);
|
|
270
|
-
expect(entry!.summary).toBe("A".repeat(100));
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
test("handles file with only frontmatter and no content", () => {
|
|
274
|
-
createCommandsDir(tmpDir, {
|
|
275
|
-
"empty-body.md": "---\ntitle: Empty\n---\n",
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
const registry = discoverCCCommands(tmpDir);
|
|
279
|
-
const entry = registry.entries.get("empty-body");
|
|
280
|
-
expect(entry).toBeDefined();
|
|
281
|
-
expect(entry!.summary).toBe("");
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
test("returns empty summary when frontmatter is truncated by partial read", () => {
|
|
285
|
-
// Simulate frontmatter that exceeds SUMMARY_READ_BYTES (1024).
|
|
286
|
-
// The closing --- delimiter will be cut off, causing FRONTMATTER_REGEX to
|
|
287
|
-
// fail. extractSummary should return '' instead of '---'.
|
|
288
|
-
const largeFrontmatter =
|
|
289
|
-
"---\n" + "key: " + "x".repeat(1100) + "\n---\n\nActual summary.";
|
|
290
|
-
createCommandsDir(tmpDir, {
|
|
291
|
-
"big-frontmatter.md": largeFrontmatter,
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
const registry = discoverCCCommands(tmpDir);
|
|
295
|
-
const entry = registry.entries.get("big-frontmatter");
|
|
296
|
-
expect(entry).toBeDefined();
|
|
297
|
-
expect(entry!.summary).toBe("");
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
test("returns empty summary when frontmatter is truncated (CRLF)", () => {
|
|
301
|
-
const largeFrontmatter =
|
|
302
|
-
"---\r\n" + "key: " + "x".repeat(1100) + "\r\n---\r\n\r\nActual summary.";
|
|
303
|
-
createCommandsDir(tmpDir, {
|
|
304
|
-
"big-frontmatter-crlf.md": largeFrontmatter,
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
const registry = discoverCCCommands(tmpDir);
|
|
308
|
-
const entry = registry.entries.get("big-frontmatter-crlf");
|
|
309
|
-
expect(entry).toBeDefined();
|
|
310
|
-
expect(entry!.summary).toBe("");
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
test("returns empty summary when frontmatter is truncated with multibyte UTF-8 characters", () => {
|
|
314
|
-
// When frontmatter contains multibyte UTF-8 characters (e.g., CJK text),
|
|
315
|
-
// the JavaScript string length (UTF-16 code units) is smaller than the
|
|
316
|
-
// byte length. The truncation guard must compare byte length, not
|
|
317
|
-
// string length, against SUMMARY_READ_BYTES (1024).
|
|
318
|
-
//
|
|
319
|
-
// Each CJK character is 3 bytes in UTF-8 but 1 code unit in UTF-16.
|
|
320
|
-
// We need the total byte count to reach 1024 while string length stays
|
|
321
|
-
// well below 1024 to exercise the bug.
|
|
322
|
-
const cjkChars = "\u4e00".repeat(340); // 340 chars * 3 bytes = 1020 bytes
|
|
323
|
-
// '---\n' is 4 bytes, so total = 4 + 1020 = 1024 bytes, but string
|
|
324
|
-
// length = 4 + 340 = 344 chars — well under 1024.
|
|
325
|
-
const truncatedContent = "---\n" + cjkChars;
|
|
326
|
-
createCommandsDir(tmpDir, {
|
|
327
|
-
"multibyte-frontmatter.md": truncatedContent,
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
const registry = discoverCCCommands(tmpDir);
|
|
331
|
-
const entry = registry.entries.get("multibyte-frontmatter");
|
|
332
|
-
expect(entry).toBeDefined();
|
|
333
|
-
// Should return '' because the frontmatter opening delimiter is present
|
|
334
|
-
// but the closing delimiter is missing and the byte length reached the
|
|
335
|
-
// read limit — indicating truncation.
|
|
336
|
-
expect(entry!.summary).toBe("");
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
test("returns summary for small file starting with thematic break ---", () => {
|
|
340
|
-
// A small markdown file that starts with "---" as a thematic break (not
|
|
341
|
-
// frontmatter) should still have its first content line extracted as a
|
|
342
|
-
// summary, rather than being treated as truncated frontmatter.
|
|
343
|
-
createCommandsDir(tmpDir, {
|
|
344
|
-
"thematic-break.md":
|
|
345
|
-
"---\nThis is a valid summary after a thematic break.",
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
const registry = discoverCCCommands(tmpDir);
|
|
349
|
-
const entry = registry.entries.get("thematic-break");
|
|
350
|
-
expect(entry).toBeDefined();
|
|
351
|
-
expect(entry!.summary).toBe(
|
|
352
|
-
"This is a valid summary after a thematic break.",
|
|
353
|
-
);
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
test("handles frontmatter with Windows-style line endings", () => {
|
|
357
|
-
createCommandsDir(tmpDir, {
|
|
358
|
-
"crlf.md": "---\r\ntitle: Test\r\n---\r\n\r\nSummary with CRLF.",
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
const registry = discoverCCCommands(tmpDir);
|
|
362
|
-
const entry = registry.entries.get("crlf");
|
|
363
|
-
expect(entry).toBeDefined();
|
|
364
|
-
expect(entry!.summary).toBe("Summary with CRLF.");
|
|
365
|
-
});
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
describe("command name validation", () => {
|
|
369
|
-
test("accepts valid names with dots, dashes, underscores", () => {
|
|
370
|
-
createCommandsDir(tmpDir, {
|
|
371
|
-
"my-command.md": "Dashed name.",
|
|
372
|
-
"my_command.md": "Underscored name.",
|
|
373
|
-
"my.command.md": "Dotted name.",
|
|
374
|
-
"Command123.md": "Alphanumeric.",
|
|
375
|
-
"a.md": "Single char.",
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
const registry = discoverCCCommands(tmpDir);
|
|
379
|
-
expect(registry.entries.has("my-command")).toBe(true);
|
|
380
|
-
expect(registry.entries.has("my_command")).toBe(true);
|
|
381
|
-
expect(registry.entries.has("my.command")).toBe(true);
|
|
382
|
-
expect(registry.entries.has("command123")).toBe(true);
|
|
383
|
-
expect(registry.entries.has("a")).toBe(true);
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
test("rejects names starting with special characters", () => {
|
|
387
|
-
createCommandsDir(tmpDir, {
|
|
388
|
-
"_start.md": "Starts with underscore.",
|
|
389
|
-
".start.md": "Starts with dot.",
|
|
390
|
-
"-start.md": "Starts with dash.",
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
const registry = discoverCCCommands(tmpDir);
|
|
394
|
-
expect(registry.entries.size).toBe(0);
|
|
395
|
-
});
|
|
396
|
-
});
|