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,134 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { DeferScheduler } from './defer-scheduler.js';
|
|
3
|
+
import { executeDeferAction } from './actions-defer.js';
|
|
4
|
+
const baseContext = {
|
|
5
|
+
guild: { id: 'guild-1' },
|
|
6
|
+
client: { token: 'dummy' },
|
|
7
|
+
channelId: 'channel-1',
|
|
8
|
+
messageId: 'message-1',
|
|
9
|
+
};
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.useRealTimers();
|
|
12
|
+
});
|
|
13
|
+
function createContext(overrides) {
|
|
14
|
+
return {
|
|
15
|
+
...baseContext,
|
|
16
|
+
...overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function makeScheduler(opts) {
|
|
20
|
+
const handler = opts?.jobHandler ?? vi.fn(async () => { });
|
|
21
|
+
const scheduler = new DeferScheduler({
|
|
22
|
+
maxDelaySeconds: opts?.maxDelaySeconds ?? 60,
|
|
23
|
+
maxConcurrent: opts?.maxConcurrent ?? 2,
|
|
24
|
+
jobHandler: handler,
|
|
25
|
+
});
|
|
26
|
+
return { scheduler, handler };
|
|
27
|
+
}
|
|
28
|
+
describe('executeDeferAction', () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
vi.useFakeTimers();
|
|
31
|
+
vi.setSystemTime(new Date('2025-01-01T00:00:00Z'));
|
|
32
|
+
});
|
|
33
|
+
it('requires scheduler configuration', async () => {
|
|
34
|
+
const result = await executeDeferAction({ type: 'defer', channel: 'general', prompt: 'check', delaySeconds: 10 }, baseContext);
|
|
35
|
+
expect(result.ok).toBe(false);
|
|
36
|
+
if (result.ok)
|
|
37
|
+
throw new Error('defer action unexpectedly succeeded without a scheduler');
|
|
38
|
+
expect(result.error).toContain('not configured');
|
|
39
|
+
});
|
|
40
|
+
it('validates input fields', async () => {
|
|
41
|
+
const { scheduler } = makeScheduler();
|
|
42
|
+
const ctx = {
|
|
43
|
+
...createContext(),
|
|
44
|
+
deferScheduler: scheduler,
|
|
45
|
+
};
|
|
46
|
+
await expect(executeDeferAction({ type: 'defer', channel: '', prompt: 'check', delaySeconds: 1 }, ctx)).resolves.toEqual(expect.objectContaining({ ok: false, error: expect.stringContaining('target channel') }));
|
|
47
|
+
await expect(executeDeferAction({ type: 'defer', channel: 'a', prompt: '', delaySeconds: 1 }, ctx)).resolves.toEqual(expect.objectContaining({ ok: false, error: expect.stringContaining('prompt') }));
|
|
48
|
+
await expect(executeDeferAction({ type: 'defer', channel: 'a', prompt: 'x', delaySeconds: 0 }, ctx)).resolves.toEqual(expect.objectContaining({ ok: false, error: expect.stringContaining('greater than zero') }));
|
|
49
|
+
});
|
|
50
|
+
it('schedules deferred run when valid', async () => {
|
|
51
|
+
const { scheduler, handler } = makeScheduler();
|
|
52
|
+
const ctx = {
|
|
53
|
+
...createContext(),
|
|
54
|
+
deferScheduler: scheduler,
|
|
55
|
+
};
|
|
56
|
+
const action = { type: 'defer', channel: 'general', prompt: 'report', delaySeconds: 5 };
|
|
57
|
+
const result = await executeDeferAction(action, ctx);
|
|
58
|
+
expect(result.ok).toBe(true);
|
|
59
|
+
if (!result.ok)
|
|
60
|
+
throw new Error('defer action failed when it should have succeeded');
|
|
61
|
+
expect(result.summary).toContain('general');
|
|
62
|
+
expect(result.summary).toContain('in 5s');
|
|
63
|
+
expect(result.summary).toContain('runs at 2025-01-01');
|
|
64
|
+
vi.advanceTimersByTime(5000);
|
|
65
|
+
await Promise.resolve();
|
|
66
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
67
|
+
expect(handler).toHaveBeenCalledWith(expect.objectContaining({
|
|
68
|
+
action: expect.objectContaining({
|
|
69
|
+
channel: 'general',
|
|
70
|
+
prompt: 'report',
|
|
71
|
+
delaySeconds: 5,
|
|
72
|
+
}),
|
|
73
|
+
context: ctx,
|
|
74
|
+
}));
|
|
75
|
+
});
|
|
76
|
+
it('returns rejection summaries when scheduler denies a job', async () => {
|
|
77
|
+
const { scheduler } = makeScheduler({ maxDelaySeconds: 5 });
|
|
78
|
+
const ctx = {
|
|
79
|
+
...createContext(),
|
|
80
|
+
deferScheduler: scheduler,
|
|
81
|
+
};
|
|
82
|
+
const action = { type: 'defer', channel: 'general', prompt: 'check back', delaySeconds: 10 };
|
|
83
|
+
const result = await executeDeferAction(action, ctx);
|
|
84
|
+
expect(result.ok).toBe(false);
|
|
85
|
+
if (result.ok)
|
|
86
|
+
throw new Error('defer action unexpectedly succeeded despite exceeding max delay');
|
|
87
|
+
expect(result.error).toBe('Deferred follow-up for general rejected: delaySeconds cannot exceed 5 seconds');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('DeferScheduler', () => {
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
vi.useFakeTimers();
|
|
93
|
+
});
|
|
94
|
+
it('enforces max delay', () => {
|
|
95
|
+
const { scheduler } = makeScheduler({ maxDelaySeconds: 5 });
|
|
96
|
+
const ctx = createContext();
|
|
97
|
+
const action = { type: 'defer', channel: 'a', prompt: 'x', delaySeconds: 10 };
|
|
98
|
+
const result = scheduler.schedule({ action, context: ctx });
|
|
99
|
+
expect(result.ok).toBe(false);
|
|
100
|
+
if (result.ok)
|
|
101
|
+
throw new Error('schedule unexpectedly succeeded despite exceeding max delay');
|
|
102
|
+
expect(result.error).toMatch(/delaySeconds cannot exceed 5/);
|
|
103
|
+
});
|
|
104
|
+
it('enforces concurrency cap', () => {
|
|
105
|
+
const { scheduler } = makeScheduler({ maxConcurrent: 1 });
|
|
106
|
+
const ctx = {
|
|
107
|
+
...createContext(),
|
|
108
|
+
deferScheduler: scheduler,
|
|
109
|
+
};
|
|
110
|
+
const action = { type: 'defer', channel: 'a', prompt: 'x', delaySeconds: 2 };
|
|
111
|
+
const first = scheduler.schedule({ action, context: ctx });
|
|
112
|
+
expect(first.ok).toBe(true);
|
|
113
|
+
const second = scheduler.schedule({ action: { ...action, channel: 'b' }, context: ctx });
|
|
114
|
+
expect(second.ok).toBe(false);
|
|
115
|
+
if (second.ok)
|
|
116
|
+
throw new Error('schedule unexpectedly succeeded despite concurrency cap');
|
|
117
|
+
expect(second.error).toMatch(/Maximum of 1 deferred actions/);
|
|
118
|
+
vi.advanceTimersByTime(2000);
|
|
119
|
+
});
|
|
120
|
+
it('releases slot after run completes', async () => {
|
|
121
|
+
const { scheduler } = makeScheduler({ maxConcurrent: 1 });
|
|
122
|
+
const ctx = {
|
|
123
|
+
...createContext(),
|
|
124
|
+
deferScheduler: scheduler,
|
|
125
|
+
};
|
|
126
|
+
const action = { type: 'defer', channel: 'a', prompt: 'x', delaySeconds: 1 };
|
|
127
|
+
const first = scheduler.schedule({ action, context: ctx });
|
|
128
|
+
expect(first.ok).toBe(true);
|
|
129
|
+
vi.advanceTimersByTime(1000);
|
|
130
|
+
await Promise.resolve();
|
|
131
|
+
const second = scheduler.schedule({ action: { ...action, channel: 'b' }, context: ctx });
|
|
132
|
+
expect(second.ok).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { findPlanFile } from './plan-commands.js';
|
|
2
|
+
import { getActiveOrchestrator, getActiveForgeId, acquireWriterLock, setActiveOrchestrator, } from './forge-plan-registry.js';
|
|
3
|
+
const FORGE_TYPE_MAP = {
|
|
4
|
+
forgeCreate: true,
|
|
5
|
+
forgeResume: true,
|
|
6
|
+
forgeStatus: true,
|
|
7
|
+
forgeCancel: true,
|
|
8
|
+
};
|
|
9
|
+
export const FORGE_ACTION_TYPES = new Set(Object.keys(FORGE_TYPE_MAP));
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Executor
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
export async function executeForgeAction(action, ctx, forgeCtx) {
|
|
14
|
+
switch (action.type) {
|
|
15
|
+
case 'forgeCreate': {
|
|
16
|
+
if ((forgeCtx.depth ?? 0) >= 1) {
|
|
17
|
+
return { ok: false, error: 'forgeCreate blocked: recursion depth >= 1 (forge cannot spawn another forge)' };
|
|
18
|
+
}
|
|
19
|
+
if (!action.description) {
|
|
20
|
+
return { ok: false, error: 'forgeCreate requires a description' };
|
|
21
|
+
}
|
|
22
|
+
const existing = getActiveOrchestrator();
|
|
23
|
+
if (existing?.isRunning) {
|
|
24
|
+
const activeId = getActiveForgeId();
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
error: `A forge is already running${activeId ? ` (${activeId})` : ''}. Cancel it first with forgeCancel.`,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const orchestrator = forgeCtx.orchestratorFactory();
|
|
31
|
+
setActiveOrchestrator(orchestrator);
|
|
32
|
+
// Fire and forget — forge runs asynchronously with progress callbacks.
|
|
33
|
+
const release = await acquireWriterLock();
|
|
34
|
+
void orchestrator
|
|
35
|
+
.run(action.description, forgeCtx.onProgress, action.context)
|
|
36
|
+
.catch((err) => {
|
|
37
|
+
forgeCtx.log?.error({ err }, 'forge:action:create failed');
|
|
38
|
+
})
|
|
39
|
+
.finally(() => {
|
|
40
|
+
setActiveOrchestrator(null);
|
|
41
|
+
release();
|
|
42
|
+
});
|
|
43
|
+
return { ok: true, summary: `Forge started for: "${action.description}"` };
|
|
44
|
+
}
|
|
45
|
+
case 'forgeResume': {
|
|
46
|
+
if ((forgeCtx.depth ?? 0) >= 1) {
|
|
47
|
+
return { ok: false, error: 'forgeResume blocked: recursion depth >= 1 (forge cannot spawn another forge)' };
|
|
48
|
+
}
|
|
49
|
+
if (!action.planId) {
|
|
50
|
+
return { ok: false, error: 'forgeResume requires a planId' };
|
|
51
|
+
}
|
|
52
|
+
const existing = getActiveOrchestrator();
|
|
53
|
+
if (existing?.isRunning) {
|
|
54
|
+
const activeId = getActiveForgeId();
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
error: `A forge is already running${activeId ? ` (${activeId})` : ''}. Cancel it first with forgeCancel.`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const planOpts = {
|
|
61
|
+
workspaceCwd: forgeCtx.workspaceCwd,
|
|
62
|
+
taskStore: forgeCtx.taskStore,
|
|
63
|
+
};
|
|
64
|
+
const found = await findPlanFile(forgeCtx.plansDir, action.planId);
|
|
65
|
+
if (!found) {
|
|
66
|
+
return { ok: false, error: `Plan not found: ${action.planId}` };
|
|
67
|
+
}
|
|
68
|
+
const orchestrator = forgeCtx.orchestratorFactory();
|
|
69
|
+
setActiveOrchestrator(orchestrator);
|
|
70
|
+
const release = await acquireWriterLock();
|
|
71
|
+
void orchestrator
|
|
72
|
+
.resume(found.header.planId, found.filePath, found.header.title, forgeCtx.onProgress)
|
|
73
|
+
.catch((err) => {
|
|
74
|
+
forgeCtx.log?.error({ err, planId: action.planId }, 'forge:action:resume failed');
|
|
75
|
+
})
|
|
76
|
+
.finally(() => {
|
|
77
|
+
setActiveOrchestrator(null);
|
|
78
|
+
release();
|
|
79
|
+
});
|
|
80
|
+
return { ok: true, summary: `Forge resumed for ${found.header.planId}: "${found.header.title}"` };
|
|
81
|
+
}
|
|
82
|
+
case 'forgeStatus': {
|
|
83
|
+
const orch = getActiveOrchestrator();
|
|
84
|
+
if (orch?.isRunning) {
|
|
85
|
+
const activeId = getActiveForgeId();
|
|
86
|
+
return { ok: true, summary: `Forge is running${activeId ? `: ${activeId}` : ''}` };
|
|
87
|
+
}
|
|
88
|
+
return { ok: true, summary: 'No forge is currently running.' };
|
|
89
|
+
}
|
|
90
|
+
case 'forgeCancel': {
|
|
91
|
+
const orch = getActiveOrchestrator();
|
|
92
|
+
if (!orch?.isRunning) {
|
|
93
|
+
return { ok: false, error: 'No forge is currently running.' };
|
|
94
|
+
}
|
|
95
|
+
orch.requestCancel();
|
|
96
|
+
const activeId = getActiveForgeId();
|
|
97
|
+
return { ok: true, summary: `Cancel requested${activeId ? ` for ${activeId}` : ''}` };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Prompt section
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
export function forgeActionsPromptSection() {
|
|
105
|
+
return `### Forge (Plan Drafting + Audit)
|
|
106
|
+
|
|
107
|
+
**forgeCreate** — Start a new forge run (drafts a plan, then audits/revises iteratively):
|
|
108
|
+
\`\`\`
|
|
109
|
+
<discord-action>{"type":"forgeCreate","description":"Add retry logic to webhook handler","context":"Optional extra context or requirements"}</discord-action>
|
|
110
|
+
\`\`\`
|
|
111
|
+
- \`description\` (required): What to plan for.
|
|
112
|
+
- \`context\` (optional): Additional context appended to the plan.
|
|
113
|
+
|
|
114
|
+
**forgeResume** — Resume auditing an existing plan (re-enters the audit/revise loop):
|
|
115
|
+
\`\`\`
|
|
116
|
+
<discord-action>{"type":"forgeResume","planId":"plan-042"}</discord-action>
|
|
117
|
+
\`\`\`
|
|
118
|
+
- \`planId\` (required): The plan ID to resume.
|
|
119
|
+
|
|
120
|
+
**forgeStatus** — Check if a forge is currently running:
|
|
121
|
+
\`\`\`
|
|
122
|
+
<discord-action>{"type":"forgeStatus"}</discord-action>
|
|
123
|
+
\`\`\`
|
|
124
|
+
|
|
125
|
+
**forgeCancel** — Cancel a running forge:
|
|
126
|
+
\`\`\`
|
|
127
|
+
<discord-action>{"type":"forgeCancel"}</discord-action>
|
|
128
|
+
\`\`\`
|
|
129
|
+
|
|
130
|
+
#### Forge Guidelines
|
|
131
|
+
- Only one forge can run at a time. Check status before starting a new one.
|
|
132
|
+
- Forge runs are asynchronous — progress updates are posted to the channel.
|
|
133
|
+
- Use forgeResume to re-audit a plan that needs another pass (e.g., after manual edits).`;
|
|
134
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { FORGE_ACTION_TYPES, executeForgeAction, forgeActionsPromptSection } from './actions-forge.js';
|
|
3
|
+
import { _resetForTest, setActiveOrchestrator } from './forge-plan-registry.js';
|
|
4
|
+
import { TaskStore } from '../tasks/store.js';
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Mocks
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
vi.mock('./plan-commands.js', () => ({
|
|
9
|
+
looksLikePlanId: vi.fn((id) => /^\d+$/.test(id) || /^plan-\d+$/.test(id)),
|
|
10
|
+
findPlanFile: vi.fn(async (_dir, id) => {
|
|
11
|
+
if (id === 'plan-notfound')
|
|
12
|
+
return null;
|
|
13
|
+
return {
|
|
14
|
+
filePath: `/tmp/plans/${id}-test.md`,
|
|
15
|
+
header: { planId: id, taskId: 'ws-001', status: 'REVIEW', title: 'Test Plan', project: 'discoclaw', created: '2026-01-01' },
|
|
16
|
+
};
|
|
17
|
+
}),
|
|
18
|
+
listPlanFiles: vi.fn(async () => []),
|
|
19
|
+
}));
|
|
20
|
+
vi.mock('./forge-commands.js', () => ({
|
|
21
|
+
buildPlanSummary: vi.fn(() => '**plan-042** — Test Plan\nStatus: REVIEW'),
|
|
22
|
+
}));
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Helpers
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
function makeCtx() {
|
|
27
|
+
return {
|
|
28
|
+
guild: {},
|
|
29
|
+
client: {},
|
|
30
|
+
channelId: 'test-channel',
|
|
31
|
+
messageId: 'test-message',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function makeMockOrchestrator(overrides) {
|
|
35
|
+
return {
|
|
36
|
+
isRunning: overrides?.isRunning ?? false,
|
|
37
|
+
activePlanId: overrides?.activePlanId,
|
|
38
|
+
requestCancel: vi.fn(),
|
|
39
|
+
run: vi.fn(async () => ({
|
|
40
|
+
planId: 'plan-042',
|
|
41
|
+
filePath: '/tmp/plans/plan-042-test.md',
|
|
42
|
+
finalVerdict: 'minor',
|
|
43
|
+
rounds: 2,
|
|
44
|
+
reachedMaxRounds: false,
|
|
45
|
+
})),
|
|
46
|
+
resume: vi.fn(async () => ({
|
|
47
|
+
planId: 'plan-042',
|
|
48
|
+
filePath: '/tmp/plans/plan-042-test.md',
|
|
49
|
+
finalVerdict: 'minor',
|
|
50
|
+
rounds: 1,
|
|
51
|
+
reachedMaxRounds: false,
|
|
52
|
+
})),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function makeForgeCtx(overrides) {
|
|
56
|
+
const mockOrch = makeMockOrchestrator();
|
|
57
|
+
return {
|
|
58
|
+
orchestratorFactory: vi.fn(() => mockOrch),
|
|
59
|
+
plansDir: '/tmp/plans',
|
|
60
|
+
workspaceCwd: '/tmp/workspace',
|
|
61
|
+
taskStore: new TaskStore(),
|
|
62
|
+
onProgress: vi.fn(async () => { }),
|
|
63
|
+
log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
64
|
+
...overrides,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Tests
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
_resetForTest();
|
|
72
|
+
});
|
|
73
|
+
describe('FORGE_ACTION_TYPES', () => {
|
|
74
|
+
it('contains all forge action types', () => {
|
|
75
|
+
expect(FORGE_ACTION_TYPES.has('forgeCreate')).toBe(true);
|
|
76
|
+
expect(FORGE_ACTION_TYPES.has('forgeResume')).toBe(true);
|
|
77
|
+
expect(FORGE_ACTION_TYPES.has('forgeStatus')).toBe(true);
|
|
78
|
+
expect(FORGE_ACTION_TYPES.has('forgeCancel')).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
it('does not contain non-forge types', () => {
|
|
81
|
+
expect(FORGE_ACTION_TYPES.has('beadCreate')).toBe(false);
|
|
82
|
+
expect(FORGE_ACTION_TYPES.has('cronCreate')).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe('executeForgeAction', () => {
|
|
86
|
+
describe('forgeCreate', () => {
|
|
87
|
+
it('starts a forge run and returns summary', async () => {
|
|
88
|
+
const forgeCtx = makeForgeCtx();
|
|
89
|
+
const result = await executeForgeAction({ type: 'forgeCreate', description: 'Add retry logic' }, makeCtx(), forgeCtx);
|
|
90
|
+
expect(result.ok).toBe(true);
|
|
91
|
+
if (result.ok) {
|
|
92
|
+
expect(result.summary).toContain('Forge started');
|
|
93
|
+
expect(result.summary).toContain('Add retry logic');
|
|
94
|
+
}
|
|
95
|
+
expect(forgeCtx.orchestratorFactory).toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
it('fails without description', async () => {
|
|
98
|
+
const result = await executeForgeAction({ type: 'forgeCreate', description: '' }, makeCtx(), makeForgeCtx());
|
|
99
|
+
expect(result.ok).toBe(false);
|
|
100
|
+
if (!result.ok)
|
|
101
|
+
expect(result.error).toContain('requires a description');
|
|
102
|
+
});
|
|
103
|
+
it('blocks at recursion depth >= 1', async () => {
|
|
104
|
+
const result = await executeForgeAction({ type: 'forgeCreate', description: 'New thing' }, makeCtx(), makeForgeCtx({ depth: 1 }));
|
|
105
|
+
expect(result.ok).toBe(false);
|
|
106
|
+
if (!result.ok)
|
|
107
|
+
expect(result.error).toContain('recursion depth');
|
|
108
|
+
});
|
|
109
|
+
it('rejects when a forge is already running', async () => {
|
|
110
|
+
const runningOrch = makeMockOrchestrator({ isRunning: true, activePlanId: 'plan-001' });
|
|
111
|
+
setActiveOrchestrator(runningOrch);
|
|
112
|
+
const result = await executeForgeAction({ type: 'forgeCreate', description: 'New thing' }, makeCtx(), makeForgeCtx());
|
|
113
|
+
expect(result.ok).toBe(false);
|
|
114
|
+
if (!result.ok) {
|
|
115
|
+
expect(result.error).toContain('already running');
|
|
116
|
+
expect(result.error).toContain('plan-001');
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
describe('forgeResume', () => {
|
|
121
|
+
it('resumes forge on existing plan', async () => {
|
|
122
|
+
const forgeCtx = makeForgeCtx();
|
|
123
|
+
const result = await executeForgeAction({ type: 'forgeResume', planId: 'plan-042' }, makeCtx(), forgeCtx);
|
|
124
|
+
expect(result.ok).toBe(true);
|
|
125
|
+
if (result.ok) {
|
|
126
|
+
expect(result.summary).toContain('Forge resumed');
|
|
127
|
+
expect(result.summary).toContain('plan-042');
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
it('blocks at recursion depth >= 1', async () => {
|
|
131
|
+
const result = await executeForgeAction({ type: 'forgeResume', planId: 'plan-042' }, makeCtx(), makeForgeCtx({ depth: 1 }));
|
|
132
|
+
expect(result.ok).toBe(false);
|
|
133
|
+
if (!result.ok)
|
|
134
|
+
expect(result.error).toContain('recursion depth');
|
|
135
|
+
});
|
|
136
|
+
it('fails without planId', async () => {
|
|
137
|
+
const result = await executeForgeAction({ type: 'forgeResume', planId: '' }, makeCtx(), makeForgeCtx());
|
|
138
|
+
expect(result.ok).toBe(false);
|
|
139
|
+
if (!result.ok)
|
|
140
|
+
expect(result.error).toContain('requires a planId');
|
|
141
|
+
});
|
|
142
|
+
it('fails when plan not found', async () => {
|
|
143
|
+
const result = await executeForgeAction({ type: 'forgeResume', planId: 'plan-notfound' }, makeCtx(), makeForgeCtx());
|
|
144
|
+
expect(result.ok).toBe(false);
|
|
145
|
+
if (!result.ok)
|
|
146
|
+
expect(result.error).toContain('Plan not found');
|
|
147
|
+
});
|
|
148
|
+
it('rejects when a forge is already running', async () => {
|
|
149
|
+
const runningOrch = makeMockOrchestrator({ isRunning: true });
|
|
150
|
+
setActiveOrchestrator(runningOrch);
|
|
151
|
+
const result = await executeForgeAction({ type: 'forgeResume', planId: 'plan-042' }, makeCtx(), makeForgeCtx());
|
|
152
|
+
expect(result.ok).toBe(false);
|
|
153
|
+
if (!result.ok)
|
|
154
|
+
expect(result.error).toContain('already running');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
describe('forgeStatus', () => {
|
|
158
|
+
it('reports when no forge is running', async () => {
|
|
159
|
+
const result = await executeForgeAction({ type: 'forgeStatus' }, makeCtx(), makeForgeCtx());
|
|
160
|
+
expect(result.ok).toBe(true);
|
|
161
|
+
if (result.ok)
|
|
162
|
+
expect(result.summary).toContain('No forge');
|
|
163
|
+
});
|
|
164
|
+
it('reports active forge with plan ID', async () => {
|
|
165
|
+
const runningOrch = makeMockOrchestrator({ isRunning: true, activePlanId: 'plan-007' });
|
|
166
|
+
setActiveOrchestrator(runningOrch);
|
|
167
|
+
const result = await executeForgeAction({ type: 'forgeStatus' }, makeCtx(), makeForgeCtx());
|
|
168
|
+
expect(result.ok).toBe(true);
|
|
169
|
+
if (result.ok) {
|
|
170
|
+
expect(result.summary).toContain('running');
|
|
171
|
+
expect(result.summary).toContain('plan-007');
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
describe('forgeCancel', () => {
|
|
176
|
+
it('cancels a running forge', async () => {
|
|
177
|
+
const runningOrch = makeMockOrchestrator({ isRunning: true, activePlanId: 'plan-010' });
|
|
178
|
+
setActiveOrchestrator(runningOrch);
|
|
179
|
+
const result = await executeForgeAction({ type: 'forgeCancel' }, makeCtx(), makeForgeCtx());
|
|
180
|
+
expect(result.ok).toBe(true);
|
|
181
|
+
if (result.ok)
|
|
182
|
+
expect(result.summary).toContain('Cancel requested');
|
|
183
|
+
expect(runningOrch.requestCancel).toHaveBeenCalled();
|
|
184
|
+
});
|
|
185
|
+
it('fails when no forge is running', async () => {
|
|
186
|
+
const result = await executeForgeAction({ type: 'forgeCancel' }, makeCtx(), makeForgeCtx());
|
|
187
|
+
expect(result.ok).toBe(false);
|
|
188
|
+
if (!result.ok)
|
|
189
|
+
expect(result.error).toContain('No forge');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
describe('forgeActionsPromptSection', () => {
|
|
194
|
+
it('returns non-empty prompt section', () => {
|
|
195
|
+
const section = forgeActionsPromptSection();
|
|
196
|
+
expect(section).toContain('forgeCreate');
|
|
197
|
+
expect(section).toContain('forgeResume');
|
|
198
|
+
expect(section).toContain('forgeStatus');
|
|
199
|
+
expect(section).toContain('forgeCancel');
|
|
200
|
+
});
|
|
201
|
+
it('includes forge guidelines', () => {
|
|
202
|
+
const section = forgeActionsPromptSection();
|
|
203
|
+
expect(section).toContain('one forge');
|
|
204
|
+
expect(section).toContain('asynchronous');
|
|
205
|
+
});
|
|
206
|
+
});
|