discoclaw 0.1.0
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/.context/README.md +42 -0
- package/.context/architecture.md +58 -0
- package/.context/bot-setup.md +24 -0
- package/.context/dev.md +230 -0
- package/.context/discord.md +144 -0
- package/.context/memory.md +257 -0
- package/.context/ops.md +59 -0
- package/.context/pa-safety.md +47 -0
- package/.context/pa.md +118 -0
- package/.context/project.md +43 -0
- package/.context/runtime.md +253 -0
- package/.context/tasks.md +71 -0
- package/.context/tools.md +75 -0
- package/.env.example +88 -0
- package/.env.example.full +378 -0
- package/LICENSE +21 -0
- package/README.md +220 -0
- package/dist/beads/auto-tag.js +2 -0
- package/dist/beads/auto-tag.test.js +62 -0
- package/dist/beads/bd-cli.js +9 -0
- package/dist/beads/bd-cli.test.js +495 -0
- package/dist/beads/bead-hooks-cli.js +149 -0
- package/dist/beads/bead-sync-cli.js +5 -0
- package/dist/beads/bead-sync-cli.test.js +72 -0
- package/dist/beads/bead-sync-coordinator.js +4 -0
- package/dist/beads/bead-sync-coordinator.test.js +239 -0
- package/dist/beads/bead-sync-watcher.js +2 -0
- package/dist/beads/bead-sync-watcher.test.js +96 -0
- package/dist/beads/bead-sync.js +7 -0
- package/dist/beads/bead-sync.test.js +876 -0
- package/dist/beads/bead-thread-cache.js +8 -0
- package/dist/beads/bead-thread-cache.test.js +91 -0
- package/dist/beads/discord-sync.js +18 -0
- package/dist/beads/discord-sync.test.js +782 -0
- package/dist/beads/find-bead-by-thread.test.js +36 -0
- package/dist/beads/forum-guard.js +2 -0
- package/dist/beads/forum-guard.test.js +204 -0
- package/dist/beads/initialize.js +3 -0
- package/dist/beads/initialize.test.js +304 -0
- package/dist/beads/types.js +10 -0
- package/dist/cli/daemon-installer.js +225 -0
- package/dist/cli/daemon-installer.test.js +289 -0
- package/dist/cli/index.js +42 -0
- package/dist/cli/init-wizard.js +374 -0
- package/dist/cli/init-wizard.test.js +191 -0
- package/dist/config.js +385 -0
- package/dist/config.test.js +589 -0
- package/dist/cron/auto-tag.js +100 -0
- package/dist/cron/auto-tag.test.js +91 -0
- package/dist/cron/cadence.js +74 -0
- package/dist/cron/cadence.test.js +53 -0
- package/dist/cron/cron-sync-coordinator.js +66 -0
- package/dist/cron/cron-sync-coordinator.test.js +118 -0
- package/dist/cron/cron-sync.js +165 -0
- package/dist/cron/cron-sync.test.js +228 -0
- package/dist/cron/cron-tag-map-watcher.js +128 -0
- package/dist/cron/cron-tag-map-watcher.test.js +155 -0
- package/dist/cron/default-timezone.js +23 -0
- package/dist/cron/default-timezone.test.js +30 -0
- package/dist/cron/discord-sync.js +205 -0
- package/dist/cron/discord-sync.test.js +353 -0
- package/dist/cron/executor.js +303 -0
- package/dist/cron/executor.test.js +614 -0
- package/dist/cron/forum-sync.js +347 -0
- package/dist/cron/forum-sync.test.js +539 -0
- package/dist/cron/job-lock.js +164 -0
- package/dist/cron/job-lock.test.js +178 -0
- package/dist/cron/parser.js +68 -0
- package/dist/cron/parser.test.js +115 -0
- package/dist/cron/run-control.js +24 -0
- package/dist/cron/run-control.test.js +27 -0
- package/dist/cron/run-stats.js +265 -0
- package/dist/cron/run-stats.test.js +160 -0
- package/dist/cron/scheduler.js +97 -0
- package/dist/cron/scheduler.test.js +112 -0
- package/dist/cron/tag-map.js +47 -0
- package/dist/cron/tag-map.test.js +64 -0
- package/dist/cron/types.js +1 -0
- package/dist/discoclaw-plan-format.test.js +137 -0
- package/dist/discoclaw-recipe-format.test.js +137 -0
- package/dist/discord/abort-registry.js +70 -0
- package/dist/discord/action-categories.js +36 -0
- package/dist/discord/action-types.js +1 -0
- package/dist/discord/action-utils.js +58 -0
- package/dist/discord/action-utils.test.js +58 -0
- package/dist/discord/actions-beads.js +1 -0
- package/dist/discord/actions-beads.test.js +372 -0
- package/dist/discord/actions-bot-profile.js +107 -0
- package/dist/discord/actions-bot-profile.test.js +138 -0
- package/dist/discord/actions-channels.js +427 -0
- package/dist/discord/actions-channels.test.js +697 -0
- package/dist/discord/actions-config.js +173 -0
- package/dist/discord/actions-config.test.js +322 -0
- package/dist/discord/actions-crons.js +586 -0
- package/dist/discord/actions-crons.test.js +499 -0
- package/dist/discord/actions-defer.js +60 -0
- package/dist/discord/actions-defer.test.js +134 -0
- package/dist/discord/actions-forge.js +134 -0
- package/dist/discord/actions-forge.test.js +206 -0
- package/dist/discord/actions-guild.js +301 -0
- package/dist/discord/actions-guild.test.js +386 -0
- package/dist/discord/actions-memory.js +106 -0
- package/dist/discord/actions-memory.test.js +248 -0
- package/dist/discord/actions-messaging.js +401 -0
- package/dist/discord/actions-messaging.test.js +738 -0
- package/dist/discord/actions-moderation.js +65 -0
- package/dist/discord/actions-moderation.test.js +88 -0
- package/dist/discord/actions-plan.js +445 -0
- package/dist/discord/actions-plan.test.js +610 -0
- package/dist/discord/actions-poll.js +38 -0
- package/dist/discord/actions-poll.test.js +93 -0
- package/dist/discord/actions-tasks.js +3 -0
- package/dist/discord/actions-tasks.test.js +418 -0
- package/dist/discord/actions.js +600 -0
- package/dist/discord/actions.test.js +522 -0
- package/dist/discord/allowed-mentions.js +3 -0
- package/dist/discord/allowed-mentions.test.js +17 -0
- package/dist/discord/allowlist.js +29 -0
- package/dist/discord/allowlist.test.js +24 -0
- package/dist/discord/audit-handler.js +191 -0
- package/dist/discord/audit-handler.test.js +361 -0
- package/dist/discord/bot.js +141 -0
- package/dist/discord/channel-context.js +181 -0
- package/dist/discord/defer-scheduler.js +45 -0
- package/dist/discord/destructive-confirmation.js +128 -0
- package/dist/discord/destructive-confirmation.test.js +49 -0
- package/dist/discord/discord-plan-auto-implement.test.js +18 -0
- package/dist/discord/durable-memory.js +145 -0
- package/dist/discord/durable-memory.test.js +281 -0
- package/dist/discord/durable-write-queue.js +4 -0
- package/dist/discord/file-download.js +308 -0
- package/dist/discord/file-download.test.js +303 -0
- package/dist/discord/forge-audit-verdict.js +140 -0
- package/dist/discord/forge-auto-implement.js +80 -0
- package/dist/discord/forge-auto-implement.test.js +110 -0
- package/dist/discord/forge-commands.js +698 -0
- package/dist/discord/forge-commands.test.js +1606 -0
- package/dist/discord/forge-plan-registry.js +68 -0
- package/dist/discord/forge-plan-registry.test.js +127 -0
- package/dist/discord/forum-count-sync.js +130 -0
- package/dist/discord/forum-count-sync.test.js +200 -0
- package/dist/discord/health-command.js +98 -0
- package/dist/discord/health-command.test.js +195 -0
- package/dist/discord/help-command.js +22 -0
- package/dist/discord/help-command.test.js +49 -0
- package/dist/discord/image-download.js +201 -0
- package/dist/discord/image-download.test.js +499 -0
- package/dist/discord/inflight-replies.js +228 -0
- package/dist/discord/inflight-replies.test.js +295 -0
- package/dist/discord/json-extract.js +110 -0
- package/dist/discord/keyed-queue.js +22 -0
- package/dist/discord/memory-commands.js +85 -0
- package/dist/discord/memory-commands.test.js +159 -0
- package/dist/discord/memory-timing.integration.test.js +159 -0
- package/dist/discord/message-coordinator.js +2347 -0
- package/dist/discord/message-coordinator.onboarding.test.js +183 -0
- package/dist/discord/message-coordinator.plan-run.test.js +264 -0
- package/dist/discord/message-history.js +53 -0
- package/dist/discord/message-history.test.js +95 -0
- package/dist/discord/models-command.js +59 -0
- package/dist/discord/models-command.test.js +150 -0
- package/dist/discord/nickname.test.js +76 -0
- package/dist/discord/onboarding-completion.js +55 -0
- package/dist/discord/onboarding-completion.test.js +176 -0
- package/dist/discord/output-common.js +178 -0
- package/dist/discord/output-common.test.js +198 -0
- package/dist/discord/output-utils.js +156 -0
- package/dist/discord/parse-identity-name.test.js +129 -0
- package/dist/discord/plan-commands.js +612 -0
- package/dist/discord/plan-commands.test.js +1622 -0
- package/dist/discord/plan-manager.js +1491 -0
- package/dist/discord/plan-manager.test.js +2380 -0
- package/dist/discord/plan-parser.js +110 -0
- package/dist/discord/plan-parser.test.js +63 -0
- package/dist/discord/plan-run-phase-start.js +20 -0
- package/dist/discord/plan-run-phase-start.test.js +29 -0
- package/dist/discord/platform-message.js +45 -0
- package/dist/discord/platform-message.test.js +110 -0
- package/dist/discord/prompt-common.js +240 -0
- package/dist/discord/prompt-common.test.js +423 -0
- package/dist/discord/reaction-handler.js +691 -0
- package/dist/discord/reaction-handler.test.js +1574 -0
- package/dist/discord/reaction-prompts.js +118 -0
- package/dist/discord/reaction-prompts.test.js +253 -0
- package/dist/discord/reply-reference.js +66 -0
- package/dist/discord/reply-reference.test.js +125 -0
- package/dist/discord/restart-command.js +143 -0
- package/dist/discord/restart-command.test.js +196 -0
- package/dist/discord/runtime-utils.js +43 -0
- package/dist/discord/runtime-utils.test.js +112 -0
- package/dist/discord/session-key.js +7 -0
- package/dist/discord/session-key.test.js +13 -0
- package/dist/discord/shortterm-memory.js +166 -0
- package/dist/discord/shortterm-memory.test.js +345 -0
- package/dist/discord/shutdown-context.js +122 -0
- package/dist/discord/shutdown-context.test.js +279 -0
- package/dist/discord/startup-profile.test.js +214 -0
- package/dist/discord/status-channel.js +190 -0
- package/dist/discord/status-channel.test.js +282 -0
- package/dist/discord/status-command.js +206 -0
- package/dist/discord/status-command.test.js +341 -0
- package/dist/discord/streaming-progress.js +107 -0
- package/dist/discord/streaming-progress.test.js +93 -0
- package/dist/discord/summarizer.js +89 -0
- package/dist/discord/summarizer.test.js +245 -0
- package/dist/discord/system-bootstrap.js +396 -0
- package/dist/discord/system-bootstrap.test.js +724 -0
- package/dist/discord/thread-context.js +169 -0
- package/dist/discord/thread-context.test.js +386 -0
- package/dist/discord/tool-aware-queue.js +116 -0
- package/dist/discord/tool-aware-queue.test.js +180 -0
- package/dist/discord/update-command.js +127 -0
- package/dist/discord/update-command.test.js +275 -0
- package/dist/discord/user-errors.js +40 -0
- package/dist/discord/user-errors.test.js +31 -0
- package/dist/discord/user-turn-to-durable.js +111 -0
- package/dist/discord/user-turn-to-durable.test.js +273 -0
- package/dist/discord-followup.test.js +677 -0
- package/dist/discord.channel-context.test.js +95 -0
- package/dist/discord.fail-closed.test.js +199 -0
- package/dist/discord.health-command.integration.test.js +140 -0
- package/dist/discord.js +190 -0
- package/dist/discord.prompt-context.test.js +1431 -0
- package/dist/discord.render.test.js +621 -0
- package/dist/discord.status-wiring.test.js +187 -0
- package/dist/engine/claudeCli.js +137 -0
- package/dist/engine/types.js +1 -0
- package/dist/group-queue.js +25 -0
- package/dist/health/credential-check.js +175 -0
- package/dist/health/credential-check.test.js +401 -0
- package/dist/health/startup-healing.js +139 -0
- package/dist/health/startup-healing.test.js +298 -0
- package/dist/identity.js +36 -0
- package/dist/index.js +1378 -0
- package/dist/logging/logger-like.js +1 -0
- package/dist/observability/memory-sampler.js +51 -0
- package/dist/observability/memory-sampler.test.js +93 -0
- package/dist/observability/metrics.js +88 -0
- package/dist/observability/metrics.test.js +42 -0
- package/dist/onboarding/onboarding-flow.js +246 -0
- package/dist/onboarding/onboarding-flow.test.js +238 -0
- package/dist/onboarding/onboarding-writer.js +102 -0
- package/dist/onboarding/onboarding-writer.test.js +143 -0
- package/dist/pidlock.js +187 -0
- package/dist/pidlock.test.js +128 -0
- package/dist/pipeline/engine.js +206 -0
- package/dist/pipeline/engine.test.js +771 -0
- package/dist/root-policy.js +21 -0
- package/dist/root-policy.test.js +55 -0
- package/dist/runtime/claude-code-cli.js +35 -0
- package/dist/runtime/claude-code-cli.test.js +1199 -0
- package/dist/runtime/cli-adapter.js +584 -0
- package/dist/runtime/cli-output-parsers.js +108 -0
- package/dist/runtime/cli-shared.js +96 -0
- package/dist/runtime/cli-shared.test.js +104 -0
- package/dist/runtime/cli-strategy.js +6 -0
- package/dist/runtime/codex-cli.js +16 -0
- package/dist/runtime/codex-cli.test.js +862 -0
- package/dist/runtime/concurrency-limit.js +80 -0
- package/dist/runtime/concurrency-limit.test.js +137 -0
- package/dist/runtime/gemini-cli.js +16 -0
- package/dist/runtime/gemini-cli.test.js +413 -0
- package/dist/runtime/long-running-process.js +415 -0
- package/dist/runtime/long-running-process.test.js +318 -0
- package/dist/runtime/model-smoke-helpers.js +160 -0
- package/dist/runtime/model-smoke.test.js +194 -0
- package/dist/runtime/model-tiers.js +33 -0
- package/dist/runtime/model-tiers.test.js +65 -0
- package/dist/runtime/openai-auth.js +151 -0
- package/dist/runtime/openai-auth.test.js +361 -0
- package/dist/runtime/openai-compat.js +178 -0
- package/dist/runtime/openai-compat.test.js +449 -0
- package/dist/runtime/process-pool.js +93 -0
- package/dist/runtime/process-pool.test.js +148 -0
- package/dist/runtime/registry.js +15 -0
- package/dist/runtime/registry.test.js +47 -0
- package/dist/runtime/session-scanner.js +186 -0
- package/dist/runtime/session-scanner.test.js +257 -0
- package/dist/runtime/strategies/claude-strategy.js +193 -0
- package/dist/runtime/strategies/codex-strategy.js +161 -0
- package/dist/runtime/strategies/gemini-strategy.js +64 -0
- package/dist/runtime/strategies/template-strategy.js +85 -0
- package/dist/runtime/tool-capabilities.js +27 -0
- package/dist/runtime/tool-capabilities.test.js +24 -0
- package/dist/runtime/tool-labels.js +48 -0
- package/dist/runtime/types.js +2 -0
- package/dist/sessionManager.js +47 -0
- package/dist/sessions.js +18 -0
- package/dist/tasks/architecture-contract.js +33 -0
- package/dist/tasks/architecture-contract.test.js +90 -0
- package/dist/tasks/auto-tag.js +50 -0
- package/dist/tasks/auto-tag.test.js +64 -0
- package/dist/tasks/bd-cli.js +164 -0
- package/dist/tasks/bd-cli.test.js +359 -0
- package/dist/tasks/bead-sync.js +1 -0
- package/dist/tasks/context-summary.js +27 -0
- package/dist/tasks/discord-sync.js +3 -0
- package/dist/tasks/discord-sync.test.js +685 -0
- package/dist/tasks/discord-types.js +4 -0
- package/dist/tasks/find-task-by-thread.test.js +36 -0
- package/dist/tasks/forum-guard.js +81 -0
- package/dist/tasks/forum-guard.test.js +192 -0
- package/dist/tasks/initialize.js +77 -0
- package/dist/tasks/initialize.test.js +263 -0
- package/dist/tasks/logger-types.js +1 -0
- package/dist/tasks/metrics-types.js +3 -0
- package/dist/tasks/migrate.js +33 -0
- package/dist/tasks/migrate.test.js +156 -0
- package/dist/tasks/path-defaults.js +67 -0
- package/dist/tasks/path-defaults.test.js +73 -0
- package/dist/tasks/runtime-types.js +1 -0
- package/dist/tasks/service.js +33 -0
- package/dist/tasks/service.test.js +51 -0
- package/dist/tasks/store.js +238 -0
- package/dist/tasks/store.test.js +417 -0
- package/dist/tasks/sync-context.js +1 -0
- package/dist/tasks/sync-contract.js +24 -0
- package/dist/tasks/sync-contract.test.js +25 -0
- package/dist/tasks/sync-coordinator-metrics.js +41 -0
- package/dist/tasks/sync-coordinator-retries.js +71 -0
- package/dist/tasks/sync-coordinator.js +96 -0
- package/dist/tasks/sync-coordinator.test.js +501 -0
- package/dist/tasks/sync-types.js +1 -0
- package/dist/tasks/sync-watcher.js +27 -0
- package/dist/tasks/sync-watcher.test.js +92 -0
- package/dist/tasks/tag-map.js +36 -0
- package/dist/tasks/tag-map.test.js +54 -0
- package/dist/tasks/task-action-contract.js +16 -0
- package/dist/tasks/task-action-contract.test.js +16 -0
- package/dist/tasks/task-action-executor.js +18 -0
- package/dist/tasks/task-action-executor.test.js +420 -0
- package/dist/tasks/task-action-mutation-helpers.js +17 -0
- package/dist/tasks/task-action-mutations.js +151 -0
- package/dist/tasks/task-action-prompt.js +62 -0
- package/dist/tasks/task-action-read-ops.js +73 -0
- package/dist/tasks/task-action-runner-types.js +1 -0
- package/dist/tasks/task-action-thread-sync.js +82 -0
- package/dist/tasks/task-actions.js +3 -0
- package/dist/tasks/task-cli.js +227 -0
- package/dist/tasks/task-context.js +1 -0
- package/dist/tasks/task-lifecycle.js +46 -0
- package/dist/tasks/task-lifecycle.test.js +35 -0
- package/dist/tasks/task-sync-apply-plan.js +95 -0
- package/dist/tasks/task-sync-apply-types.js +12 -0
- package/dist/tasks/task-sync-apply.js +319 -0
- package/dist/tasks/task-sync-cli.js +89 -0
- package/dist/tasks/task-sync-cli.test.js +70 -0
- package/dist/tasks/task-sync-engine.js +88 -0
- package/dist/tasks/task-sync-engine.test.js +934 -0
- package/dist/tasks/task-sync-phase-apply.js +171 -0
- package/dist/tasks/task-sync-pipeline.js +2 -0
- package/dist/tasks/task-sync-pipeline.test.js +265 -0
- package/dist/tasks/task-sync-reconcile-plan.js +182 -0
- package/dist/tasks/task-sync-reconcile.js +144 -0
- package/dist/tasks/task-sync.js +56 -0
- package/dist/tasks/task-sync.test.js +86 -0
- package/dist/tasks/thread-cache.js +42 -0
- package/dist/tasks/thread-cache.test.js +89 -0
- package/dist/tasks/thread-contracts.test.js +711 -0
- package/dist/tasks/thread-forum-ops.js +68 -0
- package/dist/tasks/thread-helpers.js +86 -0
- package/dist/tasks/thread-helpers.test.js +33 -0
- package/dist/tasks/thread-lifecycle-ops.js +144 -0
- package/dist/tasks/thread-ops-shared.js +21 -0
- package/dist/tasks/thread-ops.js +2 -0
- package/dist/tasks/types.js +20 -0
- package/dist/tasks/types.test.js +60 -0
- package/dist/test-setup.js +11 -0
- package/dist/test-setup.test.js +42 -0
- package/dist/transport/types.js +1 -0
- package/dist/validate.js +41 -0
- package/dist/validate.test.js +94 -0
- package/dist/version.js +15 -0
- package/dist/version.test.js +31 -0
- package/dist/webhook/server.js +199 -0
- package/dist/webhook/server.test.js +460 -0
- package/dist/workspace-bootstrap.js +135 -0
- package/dist/workspace-bootstrap.test.js +514 -0
- package/dist/workspace-permissions.js +134 -0
- package/dist/workspace-permissions.test.js +181 -0
- package/package.json +74 -0
- package/scripts/cron/cron-tag-map.json +9 -0
- package/scripts/tasks/tag-map.json +10 -0
- package/systemd/discoclaw.service +19 -0
- package/templates/recipes/integration.discoclaw-recipe.md +171 -0
- package/templates/workspace/AGENTS.md +217 -0
- package/templates/workspace/BOOTSTRAP.md +1 -0
- package/templates/workspace/HEARTBEAT.md +10 -0
- package/templates/workspace/IDENTITY.md +16 -0
- package/templates/workspace/MEMORY.md +24 -0
- package/templates/workspace/SOUL.md +52 -0
- package/templates/workspace/TOOLS.md +304 -0
- package/templates/workspace/USER.md +37 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { parseConfig, DEFAULT_DISCORD_ACTIONS_DEFER_MAX_CONCURRENT, DEFAULT_DISCORD_ACTIONS_DEFER_MAX_DELAY_SECONDS, } from './config.js';
|
|
3
|
+
function env(overrides = {}) {
|
|
4
|
+
return {
|
|
5
|
+
DISCORD_TOKEN: 'token',
|
|
6
|
+
DISCORD_ALLOW_USER_IDS: '123',
|
|
7
|
+
// Provide valid snowflakes for forums that are enabled by default.
|
|
8
|
+
DISCOCLAW_CRON_FORUM: '1000000000000000001',
|
|
9
|
+
DISCOCLAW_TASKS_FORUM: '1000000000000000002',
|
|
10
|
+
...overrides,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
describe('parseConfig', () => {
|
|
14
|
+
it('parses required fields and defaults', () => {
|
|
15
|
+
const { config, warnings, infos } = parseConfig(env());
|
|
16
|
+
expect(config.token).toBe('token');
|
|
17
|
+
expect(config.allowUserIds.has('123')).toBe(true);
|
|
18
|
+
expect(config.primaryRuntime).toBe('claude');
|
|
19
|
+
expect(config.runtimeModel).toBe('capable');
|
|
20
|
+
expect(config.summaryModel).toBe('fast');
|
|
21
|
+
expect(config.cronModel).toBe('fast');
|
|
22
|
+
expect(config.cronAutoTagModel).toBe('fast');
|
|
23
|
+
expect(config.tasksAutoTagModel).toBe('fast');
|
|
24
|
+
expect(config.tasksSyncFailureRetryEnabled).toBe(true);
|
|
25
|
+
expect(config.tasksSyncFailureRetryDelayMs).toBe(30_000);
|
|
26
|
+
expect(config.tasksSyncDeferredRetryDelayMs).toBe(30_000);
|
|
27
|
+
expect(config.outputFormat).toBe('text');
|
|
28
|
+
expect(warnings.some((w) => w.includes('category flags are ignored'))).toBe(false);
|
|
29
|
+
expect(infos.some((i) => i.includes('category flags are ignored'))).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
it('throws on invalid boolean values', () => {
|
|
32
|
+
expect(() => parseConfig(env({ DISCOCLAW_SUMMARY_ENABLED: 'yes' })))
|
|
33
|
+
.toThrow(/DISCOCLAW_SUMMARY_ENABLED must be "0"\/"1" or "true"\/"false"/);
|
|
34
|
+
});
|
|
35
|
+
it('parses true/false booleans', () => {
|
|
36
|
+
const { config } = parseConfig(env({ DISCOCLAW_SUMMARY_ENABLED: 'false', DISCOCLAW_CRON_ENABLED: 'true' }));
|
|
37
|
+
expect(config.summaryEnabled).toBe(false);
|
|
38
|
+
expect(config.cronEnabled).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
it('throws on invalid numeric values', () => {
|
|
41
|
+
expect(() => parseConfig(env({ RUNTIME_TIMEOUT_MS: '-1' })))
|
|
42
|
+
.toThrow(/RUNTIME_TIMEOUT_MS must be a positive number/);
|
|
43
|
+
});
|
|
44
|
+
it('warns (does not throw) on unknown runtime tools', () => {
|
|
45
|
+
const { config, warnings } = parseConfig(env({ RUNTIME_TOOLS: 'Read,InvalidTool' }));
|
|
46
|
+
expect(config.runtimeTools).toEqual(['Read', 'InvalidTool']);
|
|
47
|
+
expect(warnings.some((w) => w.includes('RUNTIME_TOOLS includes unknown tools'))).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
it('warns when DISCORD_CHANNEL_IDS has no valid IDs', () => {
|
|
50
|
+
const { warnings } = parseConfig(env({ DISCORD_CHANNEL_IDS: 'abc def' }));
|
|
51
|
+
expect(warnings.some((w) => w.includes('DISCORD_CHANNEL_IDS was set but no valid IDs'))).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
it('parses PRIMARY_RUNTIME and normalizes claude_code alias', () => {
|
|
54
|
+
const { config } = parseConfig(env({ PRIMARY_RUNTIME: 'claude_code' }));
|
|
55
|
+
expect(config.primaryRuntime).toBe('claude');
|
|
56
|
+
});
|
|
57
|
+
it('warns when PRIMARY_RUNTIME=openai without OPENAI_API_KEY', () => {
|
|
58
|
+
const { warnings } = parseConfig(env({ PRIMARY_RUNTIME: 'openai', OPENAI_API_KEY: undefined }));
|
|
59
|
+
expect(warnings.some((w) => w.includes('PRIMARY_RUNTIME=openai'))).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
it('does not warn about action category flags when master actions are enabled', () => {
|
|
62
|
+
const { warnings, infos } = parseConfig(env({ DISCOCLAW_DISCORD_ACTIONS: '1' }));
|
|
63
|
+
expect(warnings.some((w) => w.includes('category flags are ignored'))).toBe(false);
|
|
64
|
+
expect(infos.some((i) => i.includes('category flags are ignored'))).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
it('defaults discordActionsDefer settings', () => {
|
|
67
|
+
const { config } = parseConfig(env());
|
|
68
|
+
expect(config.discordActionsDefer).toBe(true);
|
|
69
|
+
expect(config.deferMaxDelaySeconds).toBe(DEFAULT_DISCORD_ACTIONS_DEFER_MAX_DELAY_SECONDS);
|
|
70
|
+
expect(config.deferMaxConcurrent).toBe(DEFAULT_DISCORD_ACTIONS_DEFER_MAX_CONCURRENT);
|
|
71
|
+
});
|
|
72
|
+
it('parses defer config overrides', () => {
|
|
73
|
+
const { config } = parseConfig(env({
|
|
74
|
+
DISCOCLAW_DISCORD_ACTIONS_DEFER: '1',
|
|
75
|
+
DISCOCLAW_DISCORD_ACTIONS_DEFER_MAX_DELAY_SECONDS: '900',
|
|
76
|
+
DISCOCLAW_DISCORD_ACTIONS_DEFER_MAX_CONCURRENT: '2',
|
|
77
|
+
}));
|
|
78
|
+
expect(config.discordActionsDefer).toBe(true);
|
|
79
|
+
expect(config.deferMaxDelaySeconds).toBe(900);
|
|
80
|
+
expect(config.deferMaxConcurrent).toBe(2);
|
|
81
|
+
});
|
|
82
|
+
it('reports ignored action category flags as info-level advisories', () => {
|
|
83
|
+
const { warnings, infos } = parseConfig(env({
|
|
84
|
+
DISCOCLAW_DISCORD_ACTIONS: '0',
|
|
85
|
+
DISCOCLAW_DISCORD_ACTIONS_MESSAGING: '1',
|
|
86
|
+
}));
|
|
87
|
+
expect(warnings.some((w) => w.includes('category flags are ignored'))).toBe(false);
|
|
88
|
+
expect(infos.some((i) => i.includes('category flags are ignored'))).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
it('reports ignored defer category flag when master actions off', () => {
|
|
91
|
+
const { infos } = parseConfig(env({
|
|
92
|
+
DISCOCLAW_DISCORD_ACTIONS: '0',
|
|
93
|
+
DISCOCLAW_DISCORD_ACTIONS_DEFER: '1',
|
|
94
|
+
}));
|
|
95
|
+
expect(infos.some((i) => i.includes('DISCOCLAW_DISCORD_ACTIONS_DEFER'))).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
it('parses DISCOCLAW_BOT_NAME when set', () => {
|
|
98
|
+
const { config } = parseConfig(env({ DISCOCLAW_BOT_NAME: 'Weston' }));
|
|
99
|
+
expect(config.botDisplayName).toBe('Weston');
|
|
100
|
+
});
|
|
101
|
+
it('returns undefined for botDisplayName when DISCOCLAW_BOT_NAME is unset', () => {
|
|
102
|
+
const { config } = parseConfig(env());
|
|
103
|
+
expect(config.botDisplayName).toBeUndefined();
|
|
104
|
+
});
|
|
105
|
+
it('returns undefined for botDisplayName when DISCOCLAW_BOT_NAME is whitespace-only', () => {
|
|
106
|
+
const { config } = parseConfig(env({ DISCOCLAW_BOT_NAME: ' ' }));
|
|
107
|
+
expect(config.botDisplayName).toBeUndefined();
|
|
108
|
+
});
|
|
109
|
+
// --- Bot profile: status ---
|
|
110
|
+
it('parses valid bot status values', () => {
|
|
111
|
+
for (const status of ['online', 'idle', 'dnd', 'invisible']) {
|
|
112
|
+
const { config } = parseConfig(env({ DISCOCLAW_BOT_STATUS: status }));
|
|
113
|
+
expect(config.botStatus).toBe(status);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
it('parses bot status case-insensitively', () => {
|
|
117
|
+
const { config } = parseConfig(env({ DISCOCLAW_BOT_STATUS: 'DND' }));
|
|
118
|
+
expect(config.botStatus).toBe('dnd');
|
|
119
|
+
});
|
|
120
|
+
it('throws on invalid bot status', () => {
|
|
121
|
+
expect(() => parseConfig(env({ DISCOCLAW_BOT_STATUS: 'away' })))
|
|
122
|
+
.toThrow(/DISCOCLAW_BOT_STATUS must be one of online\|idle\|dnd\|invisible/);
|
|
123
|
+
});
|
|
124
|
+
it('returns undefined for botStatus when unset', () => {
|
|
125
|
+
const { config } = parseConfig(env());
|
|
126
|
+
expect(config.botStatus).toBeUndefined();
|
|
127
|
+
});
|
|
128
|
+
// --- Bot profile: activity type ---
|
|
129
|
+
it('defaults botActivityType to Playing', () => {
|
|
130
|
+
const { config } = parseConfig(env());
|
|
131
|
+
expect(config.botActivityType).toBe('Playing');
|
|
132
|
+
});
|
|
133
|
+
it('parses activity type case-insensitively', () => {
|
|
134
|
+
const { config } = parseConfig(env({ DISCOCLAW_BOT_ACTIVITY_TYPE: 'listening' }));
|
|
135
|
+
expect(config.botActivityType).toBe('Listening');
|
|
136
|
+
});
|
|
137
|
+
it('throws on invalid activity type', () => {
|
|
138
|
+
expect(() => parseConfig(env({ DISCOCLAW_BOT_ACTIVITY_TYPE: 'Streaming' })))
|
|
139
|
+
.toThrow(/DISCOCLAW_BOT_ACTIVITY_TYPE must be one of Playing\|Listening\|Watching\|Competing\|Custom/);
|
|
140
|
+
});
|
|
141
|
+
// --- Bot profile: avatar ---
|
|
142
|
+
it('accepts absolute file path for botAvatar', () => {
|
|
143
|
+
const { config } = parseConfig(env({ DISCOCLAW_BOT_AVATAR: '/home/user/avatar.png' }));
|
|
144
|
+
expect(config.botAvatar).toBe('/home/user/avatar.png');
|
|
145
|
+
});
|
|
146
|
+
it('accepts https URL for botAvatar', () => {
|
|
147
|
+
const { config } = parseConfig(env({ DISCOCLAW_BOT_AVATAR: 'https://example.com/avatar.png' }));
|
|
148
|
+
expect(config.botAvatar).toBe('https://example.com/avatar.png');
|
|
149
|
+
});
|
|
150
|
+
it('accepts http URL for botAvatar', () => {
|
|
151
|
+
const { config } = parseConfig(env({ DISCOCLAW_BOT_AVATAR: 'http://example.com/avatar.png' }));
|
|
152
|
+
expect(config.botAvatar).toBe('http://example.com/avatar.png');
|
|
153
|
+
});
|
|
154
|
+
it('rejects relative path for botAvatar', () => {
|
|
155
|
+
expect(() => parseConfig(env({ DISCOCLAW_BOT_AVATAR: 'images/avatar.png' })))
|
|
156
|
+
.toThrow('DISCOCLAW_BOT_AVATAR must be an absolute file path or URL');
|
|
157
|
+
});
|
|
158
|
+
it('returns undefined for botAvatar when unset', () => {
|
|
159
|
+
const { config } = parseConfig(env());
|
|
160
|
+
expect(config.botAvatar).toBeUndefined();
|
|
161
|
+
});
|
|
162
|
+
// --- Bot profile: action flag ---
|
|
163
|
+
it('defaults discordActionsBotProfile to true', () => {
|
|
164
|
+
const { config } = parseConfig(env());
|
|
165
|
+
expect(config.discordActionsBotProfile).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
it('defaults discordActionsPlan to true', () => {
|
|
168
|
+
const { config } = parseConfig(env());
|
|
169
|
+
expect(config.discordActionsPlan).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
it('defaults discordActionsEnabled to true', () => {
|
|
172
|
+
const { config } = parseConfig(env());
|
|
173
|
+
expect(config.discordActionsEnabled).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
it('defaults discordActionsMessaging to true', () => {
|
|
176
|
+
const { config } = parseConfig(env());
|
|
177
|
+
expect(config.discordActionsMessaging).toBe(true);
|
|
178
|
+
});
|
|
179
|
+
it('defaults discordActionsGuild to true', () => {
|
|
180
|
+
const { config } = parseConfig(env());
|
|
181
|
+
expect(config.discordActionsGuild).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
it('defaults discordActionsPolls to true', () => {
|
|
184
|
+
const { config } = parseConfig(env());
|
|
185
|
+
expect(config.discordActionsPolls).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
it('defaults discordActionsMemory to true', () => {
|
|
188
|
+
const { config } = parseConfig(env());
|
|
189
|
+
expect(config.discordActionsMemory).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
it('defaults sessionScanning to true', () => {
|
|
192
|
+
const { config } = parseConfig(env());
|
|
193
|
+
expect(config.sessionScanning).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
it('defaults toolAwareStreaming to true', () => {
|
|
196
|
+
const { config } = parseConfig(env());
|
|
197
|
+
expect(config.toolAwareStreaming).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
it('defaults cronAutoTag to true', () => {
|
|
200
|
+
const { config } = parseConfig(env());
|
|
201
|
+
expect(config.cronAutoTag).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
it('defaults autoJoinThreads to true', () => {
|
|
204
|
+
const { config } = parseConfig(env());
|
|
205
|
+
expect(config.autoJoinThreads).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
it('reports ignored bot profile action flag when master actions off', () => {
|
|
208
|
+
const { infos } = parseConfig(env({
|
|
209
|
+
DISCOCLAW_DISCORD_ACTIONS: '0',
|
|
210
|
+
DISCOCLAW_DISCORD_ACTIONS_BOT_PROFILE: '1',
|
|
211
|
+
}));
|
|
212
|
+
expect(infos.some((i) => i.includes('DISCOCLAW_DISCORD_ACTIONS_BOT_PROFILE'))).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
// --- FORGE_DRAFTER_RUNTIME ---
|
|
215
|
+
it('returns undefined for forgeDrafterRuntime when unset', () => {
|
|
216
|
+
const { config } = parseConfig(env());
|
|
217
|
+
expect(config.forgeDrafterRuntime).toBeUndefined();
|
|
218
|
+
});
|
|
219
|
+
it('parses FORGE_DRAFTER_RUNTIME=openai', () => {
|
|
220
|
+
const { config } = parseConfig(env({ FORGE_DRAFTER_RUNTIME: 'openai', OPENAI_API_KEY: 'sk-test' }));
|
|
221
|
+
expect(config.forgeDrafterRuntime).toBe('openai');
|
|
222
|
+
});
|
|
223
|
+
it('normalizes FORGE_DRAFTER_RUNTIME claude_code to claude', () => {
|
|
224
|
+
const { config } = parseConfig(env({ FORGE_DRAFTER_RUNTIME: 'claude_code' }));
|
|
225
|
+
expect(config.forgeDrafterRuntime).toBe('claude');
|
|
226
|
+
});
|
|
227
|
+
it('warns when FORGE_DRAFTER_RUNTIME=openai without OPENAI_API_KEY', () => {
|
|
228
|
+
const { warnings } = parseConfig(env({ FORGE_DRAFTER_RUNTIME: 'openai', OPENAI_API_KEY: undefined }));
|
|
229
|
+
expect(warnings.some((w) => w.includes('FORGE_DRAFTER_RUNTIME=openai'))).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
// --- OpenRouter adapter ---
|
|
232
|
+
it('parses OPENROUTER_API_KEY when set', () => {
|
|
233
|
+
const { config } = parseConfig(env({ OPENROUTER_API_KEY: 'sk-or-test' }));
|
|
234
|
+
expect(config.openrouterApiKey).toBe('sk-or-test');
|
|
235
|
+
});
|
|
236
|
+
it('defaults openrouterModel to "anthropic/claude-sonnet-4"', () => {
|
|
237
|
+
const { config } = parseConfig(env());
|
|
238
|
+
expect(config.openrouterModel).toBe('anthropic/claude-sonnet-4');
|
|
239
|
+
});
|
|
240
|
+
it('warns when PRIMARY_RUNTIME=openrouter without OPENROUTER_API_KEY', () => {
|
|
241
|
+
const { warnings } = parseConfig(env({ PRIMARY_RUNTIME: 'openrouter', OPENROUTER_API_KEY: undefined }));
|
|
242
|
+
expect(warnings.some((w) => w.includes('PRIMARY_RUNTIME=openrouter'))).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
it('warns when FORGE_DRAFTER_RUNTIME=openrouter without OPENROUTER_API_KEY', () => {
|
|
245
|
+
const { warnings } = parseConfig(env({ FORGE_DRAFTER_RUNTIME: 'openrouter', OPENROUTER_API_KEY: undefined }));
|
|
246
|
+
expect(warnings.some((w) => w.includes('FORGE_DRAFTER_RUNTIME=openrouter'))).toBe(true);
|
|
247
|
+
});
|
|
248
|
+
it('warns when FORGE_AUDITOR_RUNTIME=openrouter without OPENROUTER_API_KEY', () => {
|
|
249
|
+
const { warnings } = parseConfig(env({ FORGE_AUDITOR_RUNTIME: 'openrouter', OPENROUTER_API_KEY: undefined }));
|
|
250
|
+
expect(warnings.some((w) => w.includes('FORGE_AUDITOR_RUNTIME=openrouter'))).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
// --- Forge auto-implement ---
|
|
253
|
+
it('defaults forgeAutoImplement to true', () => {
|
|
254
|
+
const { config } = parseConfig(env());
|
|
255
|
+
expect(config.forgeAutoImplement).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
it('parses FORGE_AUTO_IMPLEMENT=0 as false', () => {
|
|
258
|
+
const { config } = parseConfig(env({ FORGE_AUTO_IMPLEMENT: '0' }));
|
|
259
|
+
expect(config.forgeAutoImplement).toBe(false);
|
|
260
|
+
});
|
|
261
|
+
it('parses FORGE_AUTO_IMPLEMENT=true as true', () => {
|
|
262
|
+
const { config } = parseConfig(env({ FORGE_AUTO_IMPLEMENT: 'true' }));
|
|
263
|
+
expect(config.forgeAutoImplement).toBe(true);
|
|
264
|
+
});
|
|
265
|
+
// --- Summary-to-durable ---
|
|
266
|
+
it('defaults summaryToDurableEnabled to true', () => {
|
|
267
|
+
const { config } = parseConfig(env());
|
|
268
|
+
expect(config.summaryToDurableEnabled).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
it('parses DISCOCLAW_SUMMARY_TO_DURABLE_ENABLED=1 as true', () => {
|
|
271
|
+
const { config } = parseConfig(env({ DISCOCLAW_SUMMARY_TO_DURABLE_ENABLED: '1' }));
|
|
272
|
+
expect(config.summaryToDurableEnabled).toBe(true);
|
|
273
|
+
});
|
|
274
|
+
// --- Short-term memory ---
|
|
275
|
+
it('defaults shortTermMemoryEnabled to true', () => {
|
|
276
|
+
const { config } = parseConfig(env());
|
|
277
|
+
expect(config.shortTermMemoryEnabled).toBe(true);
|
|
278
|
+
});
|
|
279
|
+
it('parses short-term memory config fields', () => {
|
|
280
|
+
const { config } = parseConfig(env({
|
|
281
|
+
DISCOCLAW_SHORTTERM_MEMORY_ENABLED: '1',
|
|
282
|
+
DISCOCLAW_SHORTTERM_MAX_ENTRIES: '10',
|
|
283
|
+
DISCOCLAW_SHORTTERM_MAX_AGE_HOURS: '12',
|
|
284
|
+
DISCOCLAW_SHORTTERM_INJECT_MAX_CHARS: '500',
|
|
285
|
+
}));
|
|
286
|
+
expect(config.shortTermMemoryEnabled).toBe(true);
|
|
287
|
+
expect(config.shortTermMaxEntries).toBe(10);
|
|
288
|
+
expect(config.shortTermMaxAgeHours).toBe(12);
|
|
289
|
+
expect(config.shortTermInjectMaxChars).toBe(500);
|
|
290
|
+
});
|
|
291
|
+
it('parses DISCOCLAW_SHORTTERM_MEMORY_ENABLED=0 as false', () => {
|
|
292
|
+
const { config } = parseConfig(env({ DISCOCLAW_SHORTTERM_MEMORY_ENABLED: '0' }));
|
|
293
|
+
expect(config.shortTermMemoryEnabled).toBe(false);
|
|
294
|
+
});
|
|
295
|
+
it('uses default values for short-term memory fields', () => {
|
|
296
|
+
const { config } = parseConfig(env());
|
|
297
|
+
expect(config.shortTermMaxEntries).toBe(20);
|
|
298
|
+
expect(config.shortTermMaxAgeHours).toBe(6);
|
|
299
|
+
expect(config.shortTermInjectMaxChars).toBe(1000);
|
|
300
|
+
});
|
|
301
|
+
// --- Tasks enabled ---
|
|
302
|
+
it('defaults tasksEnabled to true', () => {
|
|
303
|
+
const { config } = parseConfig(env());
|
|
304
|
+
expect(config.tasksEnabled).toBe(true);
|
|
305
|
+
});
|
|
306
|
+
// --- Tasks sidebar ---
|
|
307
|
+
it('defaults tasksSidebar to true', () => {
|
|
308
|
+
const { config } = parseConfig(env());
|
|
309
|
+
expect(config.tasksSidebar).toBe(true);
|
|
310
|
+
});
|
|
311
|
+
it('parses DISCOCLAW_TASKS_SIDEBAR=1 as true', () => {
|
|
312
|
+
const { config } = parseConfig(env({ DISCOCLAW_TASKS_SIDEBAR: '1' }));
|
|
313
|
+
expect(config.tasksSidebar).toBe(true);
|
|
314
|
+
});
|
|
315
|
+
// --- Fallback model ---
|
|
316
|
+
it('parses RUNTIME_FALLBACK_MODEL when set', () => {
|
|
317
|
+
const { config } = parseConfig(env({ RUNTIME_FALLBACK_MODEL: 'sonnet' }));
|
|
318
|
+
expect(config.runtimeFallbackModel).toBe('sonnet');
|
|
319
|
+
});
|
|
320
|
+
it('returns undefined for runtimeFallbackModel when unset', () => {
|
|
321
|
+
const { config } = parseConfig(env());
|
|
322
|
+
expect(config.runtimeFallbackModel).toBeUndefined();
|
|
323
|
+
});
|
|
324
|
+
// --- Max budget USD ---
|
|
325
|
+
it('parses RUNTIME_MAX_BUDGET_USD positive number', () => {
|
|
326
|
+
const { config } = parseConfig(env({ RUNTIME_MAX_BUDGET_USD: '5.00' }));
|
|
327
|
+
expect(config.runtimeMaxBudgetUsd).toBe(5);
|
|
328
|
+
});
|
|
329
|
+
it('returns undefined for runtimeMaxBudgetUsd when unset', () => {
|
|
330
|
+
const { config } = parseConfig(env());
|
|
331
|
+
expect(config.runtimeMaxBudgetUsd).toBeUndefined();
|
|
332
|
+
});
|
|
333
|
+
it('throws on RUNTIME_MAX_BUDGET_USD=0', () => {
|
|
334
|
+
expect(() => parseConfig(env({ RUNTIME_MAX_BUDGET_USD: '0' })))
|
|
335
|
+
.toThrow(/RUNTIME_MAX_BUDGET_USD must be a positive number/);
|
|
336
|
+
});
|
|
337
|
+
it('throws on RUNTIME_MAX_BUDGET_USD negative', () => {
|
|
338
|
+
expect(() => parseConfig(env({ RUNTIME_MAX_BUDGET_USD: '-1' })))
|
|
339
|
+
.toThrow(/RUNTIME_MAX_BUDGET_USD must be a positive number/);
|
|
340
|
+
});
|
|
341
|
+
it('throws on RUNTIME_MAX_BUDGET_USD non-numeric', () => {
|
|
342
|
+
expect(() => parseConfig(env({ RUNTIME_MAX_BUDGET_USD: 'abc' })))
|
|
343
|
+
.toThrow(/RUNTIME_MAX_BUDGET_USD must be a positive number/);
|
|
344
|
+
});
|
|
345
|
+
// --- Append system prompt ---
|
|
346
|
+
it('parses CLAUDE_APPEND_SYSTEM_PROMPT when set', () => {
|
|
347
|
+
const { config } = parseConfig(env({ CLAUDE_APPEND_SYSTEM_PROMPT: 'You are Weston.' }));
|
|
348
|
+
expect(config.appendSystemPrompt).toBe('You are Weston.');
|
|
349
|
+
});
|
|
350
|
+
it('returns undefined for appendSystemPrompt when unset', () => {
|
|
351
|
+
const { config } = parseConfig(env());
|
|
352
|
+
expect(config.appendSystemPrompt).toBeUndefined();
|
|
353
|
+
});
|
|
354
|
+
it('throws when CLAUDE_APPEND_SYSTEM_PROMPT exceeds 4000 chars', () => {
|
|
355
|
+
expect(() => parseConfig(env({ CLAUDE_APPEND_SYSTEM_PROMPT: 'x'.repeat(4001) })))
|
|
356
|
+
.toThrow(/CLAUDE_APPEND_SYSTEM_PROMPT exceeds 4000 char limit/);
|
|
357
|
+
});
|
|
358
|
+
it('accepts CLAUDE_APPEND_SYSTEM_PROMPT at exactly 4000 chars', () => {
|
|
359
|
+
const { config } = parseConfig(env({ CLAUDE_APPEND_SYSTEM_PROMPT: 'x'.repeat(4000) }));
|
|
360
|
+
expect(config.appendSystemPrompt).toHaveLength(4000);
|
|
361
|
+
});
|
|
362
|
+
// --- Default tools include Glob, Grep, Write ---
|
|
363
|
+
it('default RUNTIME_TOOLS includes Glob, Grep, Write', () => {
|
|
364
|
+
const { config } = parseConfig(env());
|
|
365
|
+
expect(config.runtimeTools).toEqual(['Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'WebSearch', 'WebFetch']);
|
|
366
|
+
});
|
|
367
|
+
// --- Reaction remove handler ---
|
|
368
|
+
it('defaults reactionRemoveHandlerEnabled to false', () => {
|
|
369
|
+
const { config } = parseConfig(env());
|
|
370
|
+
expect(config.reactionRemoveHandlerEnabled).toBe(false);
|
|
371
|
+
});
|
|
372
|
+
it('parses DISCOCLAW_REACTION_REMOVE_HANDLER=1 as true', () => {
|
|
373
|
+
const { config } = parseConfig(env({ DISCOCLAW_REACTION_REMOVE_HANDLER: '1' }));
|
|
374
|
+
expect(config.reactionRemoveHandlerEnabled).toBe(true);
|
|
375
|
+
});
|
|
376
|
+
// --- Gemini CLI adapter ---
|
|
377
|
+
it('defaults geminiBin to "gemini"', () => {
|
|
378
|
+
const { config } = parseConfig(env());
|
|
379
|
+
expect(config.geminiBin).toBe('gemini');
|
|
380
|
+
});
|
|
381
|
+
it('parses GEMINI_BIN when set', () => {
|
|
382
|
+
const { config } = parseConfig(env({ GEMINI_BIN: '/usr/local/bin/gemini' }));
|
|
383
|
+
expect(config.geminiBin).toBe('/usr/local/bin/gemini');
|
|
384
|
+
});
|
|
385
|
+
it('defaults geminiModel to "gemini-2.5-pro"', () => {
|
|
386
|
+
const { config } = parseConfig(env());
|
|
387
|
+
expect(config.geminiModel).toBe('gemini-2.5-pro');
|
|
388
|
+
});
|
|
389
|
+
it('parses GEMINI_MODEL when set', () => {
|
|
390
|
+
const { config } = parseConfig(env({ GEMINI_MODEL: 'gemini-2.0-flash' }));
|
|
391
|
+
expect(config.geminiModel).toBe('gemini-2.0-flash');
|
|
392
|
+
});
|
|
393
|
+
it('does not warn when PRIMARY_RUNTIME=gemini (no preflight-checkable auth)', () => {
|
|
394
|
+
const { warnings } = parseConfig(env({ PRIMARY_RUNTIME: 'gemini' }));
|
|
395
|
+
expect(warnings.some((w) => w.includes('gemini'))).toBe(false);
|
|
396
|
+
});
|
|
397
|
+
// --- Codex dangerous bypass ---
|
|
398
|
+
it('defaults codexDangerouslyBypassApprovalsAndSandbox to false', () => {
|
|
399
|
+
const { config } = parseConfig(env());
|
|
400
|
+
expect(config.codexDangerouslyBypassApprovalsAndSandbox).toBe(false);
|
|
401
|
+
});
|
|
402
|
+
it('parses CODEX_DANGEROUSLY_BYPASS_APPROVALS_AND_SANDBOX=1 as true', () => {
|
|
403
|
+
const { config } = parseConfig(env({ CODEX_DANGEROUSLY_BYPASS_APPROVALS_AND_SANDBOX: '1' }));
|
|
404
|
+
expect(config.codexDangerouslyBypassApprovalsAndSandbox).toBe(true);
|
|
405
|
+
});
|
|
406
|
+
it('defaults codexDisableSessions to false', () => {
|
|
407
|
+
const { config } = parseConfig(env());
|
|
408
|
+
expect(config.codexDisableSessions).toBe(false);
|
|
409
|
+
});
|
|
410
|
+
it('parses CODEX_DISABLE_SESSIONS=1 as true', () => {
|
|
411
|
+
const { config } = parseConfig(env({ CODEX_DISABLE_SESSIONS: '1' }));
|
|
412
|
+
expect(config.codexDisableSessions).toBe(true);
|
|
413
|
+
});
|
|
414
|
+
// --- Forum ID validation (auto-create when missing, warn on invalid) ---
|
|
415
|
+
it('allows missing cronForum when cronEnabled (bootstrap will auto-create)', () => {
|
|
416
|
+
const { config } = parseConfig(env({ DISCOCLAW_CRON_ENABLED: '1', DISCOCLAW_CRON_FORUM: undefined }));
|
|
417
|
+
expect(config.cronEnabled).toBe(true);
|
|
418
|
+
expect(config.cronForum).toBeUndefined();
|
|
419
|
+
});
|
|
420
|
+
it('warns and clears cronForum when not a snowflake', () => {
|
|
421
|
+
const { config, warnings } = parseConfig(env({ DISCOCLAW_CRON_ENABLED: '1', DISCOCLAW_CRON_FORUM: 'crons' }));
|
|
422
|
+
expect(config.cronForum).toBeUndefined();
|
|
423
|
+
expect(warnings.some(w => w.includes('DISCOCLAW_CRON_FORUM is not a valid snowflake'))).toBe(true);
|
|
424
|
+
});
|
|
425
|
+
it('accepts valid snowflake for cronForum when cronEnabled=true', () => {
|
|
426
|
+
const { config } = parseConfig(env({ DISCOCLAW_CRON_ENABLED: '1', DISCOCLAW_CRON_FORUM: '1000000000000000002' }));
|
|
427
|
+
expect(config.cronForum).toBe('1000000000000000002');
|
|
428
|
+
expect(config.cronEnabled).toBe(true);
|
|
429
|
+
});
|
|
430
|
+
it('does not validate cronForum when cronEnabled=false', () => {
|
|
431
|
+
const { config } = parseConfig(env({ DISCOCLAW_CRON_ENABLED: '0' }));
|
|
432
|
+
expect(config.cronEnabled).toBe(false);
|
|
433
|
+
});
|
|
434
|
+
it('allows missing tasksForum when tasksEnabled (bootstrap will auto-create)', () => {
|
|
435
|
+
const { config } = parseConfig(env({ DISCOCLAW_TASKS_ENABLED: '1', DISCOCLAW_TASKS_FORUM: undefined }));
|
|
436
|
+
expect(config.tasksEnabled).toBe(true);
|
|
437
|
+
expect(config.tasksForum).toBeUndefined();
|
|
438
|
+
});
|
|
439
|
+
it('warns and clears tasksForum when not a snowflake', () => {
|
|
440
|
+
const { config, warnings } = parseConfig(env({ DISCOCLAW_TASKS_ENABLED: '1', DISCOCLAW_TASKS_FORUM: 'tasks' }));
|
|
441
|
+
expect(config.tasksForum).toBeUndefined();
|
|
442
|
+
expect(warnings.some(w => w.includes('DISCOCLAW_TASKS_FORUM is not a valid snowflake'))).toBe(true);
|
|
443
|
+
});
|
|
444
|
+
it('accepts valid snowflake for tasksForum when tasksEnabled=true', () => {
|
|
445
|
+
const { config } = parseConfig(env({ DISCOCLAW_TASKS_ENABLED: '1', DISCOCLAW_TASKS_FORUM: '1000000000000000002' }));
|
|
446
|
+
expect(config.tasksForum).toBe('1000000000000000002');
|
|
447
|
+
expect(config.tasksEnabled).toBe(true);
|
|
448
|
+
});
|
|
449
|
+
it('does not validate tasksForum when tasksEnabled=false', () => {
|
|
450
|
+
const { config } = parseConfig(env({ DISCOCLAW_TASKS_ENABLED: '0' }));
|
|
451
|
+
expect(config.tasksEnabled).toBe(false);
|
|
452
|
+
});
|
|
453
|
+
it('parses DISCOCLAW_TASKS_* vars', () => {
|
|
454
|
+
const { config } = parseConfig(env({
|
|
455
|
+
DISCOCLAW_TASKS_ENABLED: '0',
|
|
456
|
+
DISCOCLAW_TASKS_FORUM: '1000000000000000009',
|
|
457
|
+
DISCOCLAW_TASKS_CWD: '/tmp/tasks',
|
|
458
|
+
DISCOCLAW_TASKS_TAG_MAP: '/tmp/tasks/tag-map.json',
|
|
459
|
+
DISCOCLAW_TASKS_MENTION_USER: '123456789012345678',
|
|
460
|
+
DISCOCLAW_TASKS_SIDEBAR: '1',
|
|
461
|
+
DISCOCLAW_TASKS_AUTO_TAG: '0',
|
|
462
|
+
DISCOCLAW_TASKS_AUTO_TAG_MODEL: 'fast',
|
|
463
|
+
DISCOCLAW_TASKS_PREFIX: 'dev',
|
|
464
|
+
DISCOCLAW_TASKS_SYNC_SKIP_PHASE5: '1',
|
|
465
|
+
DISCOCLAW_TASKS_SYNC_FAILURE_RETRY_ENABLED: '0',
|
|
466
|
+
DISCOCLAW_TASKS_SYNC_FAILURE_RETRY_DELAY_MS: '12000',
|
|
467
|
+
DISCOCLAW_TASKS_SYNC_DEFERRED_RETRY_DELAY_MS: '18000',
|
|
468
|
+
}));
|
|
469
|
+
expect(config.tasksEnabled).toBe(false);
|
|
470
|
+
expect(config.tasksForum).toBe('1000000000000000009');
|
|
471
|
+
expect(config.tasksCwdOverride).toBe('/tmp/tasks');
|
|
472
|
+
expect(config.tasksTagMapPathOverride).toBe('/tmp/tasks/tag-map.json');
|
|
473
|
+
expect(config.tasksMentionUser).toBe('123456789012345678');
|
|
474
|
+
expect(config.tasksSidebar).toBe(true);
|
|
475
|
+
expect(config.tasksAutoTag).toBe(false);
|
|
476
|
+
expect(config.tasksAutoTagModel).toBe('fast');
|
|
477
|
+
expect(config.tasksPrefix).toBe('dev');
|
|
478
|
+
expect(config.tasksSyncSkipPhase5).toBe(true);
|
|
479
|
+
expect(config.tasksSyncFailureRetryEnabled).toBe(false);
|
|
480
|
+
expect(config.tasksSyncFailureRetryDelayMs).toBe(12000);
|
|
481
|
+
expect(config.tasksSyncDeferredRetryDelayMs).toBe(18000);
|
|
482
|
+
});
|
|
483
|
+
it('rejects non-positive task sync retry delay values', () => {
|
|
484
|
+
expect(() => parseConfig(env({ DISCOCLAW_TASKS_SYNC_FAILURE_RETRY_DELAY_MS: '0' })))
|
|
485
|
+
.toThrow(/DISCOCLAW_TASKS_SYNC_FAILURE_RETRY_DELAY_MS must be a positive number/);
|
|
486
|
+
expect(() => parseConfig(env({ DISCOCLAW_TASKS_SYNC_DEFERRED_RETRY_DELAY_MS: '-1' })))
|
|
487
|
+
.toThrow(/DISCOCLAW_TASKS_SYNC_DEFERRED_RETRY_DELAY_MS must be a positive number/);
|
|
488
|
+
});
|
|
489
|
+
// --- Verbose CLI flag ---
|
|
490
|
+
it('CLAUDE_VERBOSE defaults to false', () => {
|
|
491
|
+
const { config } = parseConfig(env());
|
|
492
|
+
expect(config.verbose).toBe(false);
|
|
493
|
+
});
|
|
494
|
+
it('CLAUDE_VERBOSE=1 sets verbose to true with stream-json', () => {
|
|
495
|
+
const { config } = parseConfig(env({ CLAUDE_VERBOSE: '1', CLAUDE_OUTPUT_FORMAT: 'stream-json' }));
|
|
496
|
+
expect(config.verbose).toBe(true);
|
|
497
|
+
});
|
|
498
|
+
it('CLAUDE_VERBOSE=1 is auto-disabled when outputFormat=text', () => {
|
|
499
|
+
const { config, warnings } = parseConfig(env({ CLAUDE_VERBOSE: '1', CLAUDE_OUTPUT_FORMAT: 'text' }));
|
|
500
|
+
expect(config.verbose).toBe(false);
|
|
501
|
+
expect(warnings).toContainEqual(expect.stringContaining('CLAUDE_VERBOSE=1 ignored'));
|
|
502
|
+
});
|
|
503
|
+
it('CLAUDE_VERBOSE=1 is allowed when outputFormat=stream-json', () => {
|
|
504
|
+
const { config, warnings } = parseConfig(env({ CLAUDE_VERBOSE: '1', CLAUDE_OUTPUT_FORMAT: 'stream-json' }));
|
|
505
|
+
expect(config.verbose).toBe(true);
|
|
506
|
+
expect(warnings).not.toContainEqual(expect.stringContaining('CLAUDE_VERBOSE=1 ignored'));
|
|
507
|
+
});
|
|
508
|
+
it('CLAUDE_VERBOSE=1 is auto-disabled when outputFormat defaults to text', () => {
|
|
509
|
+
const { config, warnings } = parseConfig(env({ CLAUDE_VERBOSE: '1' }));
|
|
510
|
+
// outputFormat defaults to 'text', so verbose should be auto-disabled
|
|
511
|
+
expect(config.verbose).toBe(false);
|
|
512
|
+
expect(warnings).toContainEqual(expect.stringContaining('CLAUDE_VERBOSE=1 ignored'));
|
|
513
|
+
});
|
|
514
|
+
// --- Stream stall detection ---
|
|
515
|
+
it('defaults streamStallTimeoutMs to 300000', () => {
|
|
516
|
+
const { config } = parseConfig(env());
|
|
517
|
+
expect(config.streamStallTimeoutMs).toBe(300000);
|
|
518
|
+
});
|
|
519
|
+
it('defaults streamStallWarningMs to 150000', () => {
|
|
520
|
+
const { config } = parseConfig(env());
|
|
521
|
+
expect(config.streamStallWarningMs).toBe(150000);
|
|
522
|
+
});
|
|
523
|
+
it('parses custom streamStallTimeoutMs', () => {
|
|
524
|
+
const { config } = parseConfig(env({ DISCOCLAW_STREAM_STALL_TIMEOUT_MS: '30000' }));
|
|
525
|
+
expect(config.streamStallTimeoutMs).toBe(30000);
|
|
526
|
+
});
|
|
527
|
+
it('parses custom streamStallWarningMs', () => {
|
|
528
|
+
const { config } = parseConfig(env({ DISCOCLAW_STREAM_STALL_WARNING_MS: '15000' }));
|
|
529
|
+
expect(config.streamStallWarningMs).toBe(15000);
|
|
530
|
+
});
|
|
531
|
+
it('accepts 0 for streamStallTimeoutMs (disables feature)', () => {
|
|
532
|
+
const { config } = parseConfig(env({ DISCOCLAW_STREAM_STALL_TIMEOUT_MS: '0' }));
|
|
533
|
+
expect(config.streamStallTimeoutMs).toBe(0);
|
|
534
|
+
});
|
|
535
|
+
it('accepts 0 for streamStallWarningMs (disables feature)', () => {
|
|
536
|
+
const { config } = parseConfig(env({ DISCOCLAW_STREAM_STALL_WARNING_MS: '0' }));
|
|
537
|
+
expect(config.streamStallWarningMs).toBe(0);
|
|
538
|
+
});
|
|
539
|
+
// --- Progress stall detection ---
|
|
540
|
+
it('defaults progressStallTimeoutMs to 300000', () => {
|
|
541
|
+
const { config } = parseConfig(env());
|
|
542
|
+
expect(config.progressStallTimeoutMs).toBe(300000);
|
|
543
|
+
});
|
|
544
|
+
it('parses custom progressStallTimeoutMs', () => {
|
|
545
|
+
const { config } = parseConfig(env({ DISCOCLAW_PROGRESS_STALL_TIMEOUT_MS: '60000' }));
|
|
546
|
+
expect(config.progressStallTimeoutMs).toBe(60000);
|
|
547
|
+
});
|
|
548
|
+
it('accepts 0 for progressStallTimeoutMs (disables feature)', () => {
|
|
549
|
+
const { config } = parseConfig(env({ DISCOCLAW_PROGRESS_STALL_TIMEOUT_MS: '0' }));
|
|
550
|
+
expect(config.progressStallTimeoutMs).toBe(0);
|
|
551
|
+
});
|
|
552
|
+
// --- Webhook ---
|
|
553
|
+
it('defaults webhookEnabled to false', () => {
|
|
554
|
+
const { config } = parseConfig(env());
|
|
555
|
+
expect(config.webhookEnabled).toBe(false);
|
|
556
|
+
});
|
|
557
|
+
it('parses DISCOCLAW_WEBHOOK_ENABLED=1 as true', () => {
|
|
558
|
+
const { config } = parseConfig(env({ DISCOCLAW_WEBHOOK_ENABLED: '1' }));
|
|
559
|
+
expect(config.webhookEnabled).toBe(true);
|
|
560
|
+
});
|
|
561
|
+
it('defaults webhookPort to 9400', () => {
|
|
562
|
+
const { config } = parseConfig(env());
|
|
563
|
+
expect(config.webhookPort).toBe(9400);
|
|
564
|
+
});
|
|
565
|
+
it('parses DISCOCLAW_WEBHOOK_PORT when set', () => {
|
|
566
|
+
const { config } = parseConfig(env({ DISCOCLAW_WEBHOOK_PORT: '8765' }));
|
|
567
|
+
expect(config.webhookPort).toBe(8765);
|
|
568
|
+
});
|
|
569
|
+
it('throws on DISCOCLAW_WEBHOOK_PORT=0 (non-positive)', () => {
|
|
570
|
+
expect(() => parseConfig(env({ DISCOCLAW_WEBHOOK_PORT: '0' })))
|
|
571
|
+
.toThrow(/DISCOCLAW_WEBHOOK_PORT must be a positive number/);
|
|
572
|
+
});
|
|
573
|
+
it('throws on DISCOCLAW_WEBHOOK_PORT=-1 (negative)', () => {
|
|
574
|
+
expect(() => parseConfig(env({ DISCOCLAW_WEBHOOK_PORT: '-1' })))
|
|
575
|
+
.toThrow(/DISCOCLAW_WEBHOOK_PORT must be a positive number/);
|
|
576
|
+
});
|
|
577
|
+
it('throws on DISCOCLAW_WEBHOOK_PORT=3000.5 (non-integer)', () => {
|
|
578
|
+
expect(() => parseConfig(env({ DISCOCLAW_WEBHOOK_PORT: '3000.5' })))
|
|
579
|
+
.toThrow(/DISCOCLAW_WEBHOOK_PORT must be an integer/);
|
|
580
|
+
});
|
|
581
|
+
it('returns undefined for webhookConfigPath when DISCOCLAW_WEBHOOK_CONFIG is unset', () => {
|
|
582
|
+
const { config } = parseConfig(env());
|
|
583
|
+
expect(config.webhookConfigPath).toBeUndefined();
|
|
584
|
+
});
|
|
585
|
+
it('parses DISCOCLAW_WEBHOOK_CONFIG when set', () => {
|
|
586
|
+
const { config } = parseConfig(env({ DISCOCLAW_WEBHOOK_CONFIG: '/etc/discoclaw/webhooks.json' }));
|
|
587
|
+
expect(config.webhookConfigPath).toBe('/etc/discoclaw/webhooks.json');
|
|
588
|
+
});
|
|
589
|
+
});
|