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,298 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { healStaleCronRecords, healStaleTaskThreadRefs, healCorruptedJsonStores, } from './startup-healing.js';
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Helpers
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
function makeMockLog() {
|
|
10
|
+
return { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
11
|
+
}
|
|
12
|
+
function makeMockStatsStore(jobs) {
|
|
13
|
+
const store = {
|
|
14
|
+
version: 3,
|
|
15
|
+
updatedAt: Date.now(),
|
|
16
|
+
jobs: jobs,
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
getStore: () => store,
|
|
20
|
+
removeByThreadId: vi.fn().mockResolvedValue(true),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function makeMockTaskStore(tasks) {
|
|
24
|
+
return {
|
|
25
|
+
list: vi.fn(() => tasks.filter((t) => t.status !== 'closed')),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function makeMockClient(fetchImpl) {
|
|
29
|
+
return {
|
|
30
|
+
channels: {
|
|
31
|
+
fetch: vi.fn(fetchImpl ?? (() => Promise.resolve({ id: 'channel-1' }))),
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// healStaleCronRecords — Scenario 2
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
describe('healStaleCronRecords', () => {
|
|
39
|
+
it('removes stale record when channels.fetch returns null, and logs warning', async () => {
|
|
40
|
+
const log = makeMockLog();
|
|
41
|
+
const statsStore = makeMockStatsStore({
|
|
42
|
+
'cron-abc': { cronId: 'cron-abc', threadId: 'thread-dead' },
|
|
43
|
+
});
|
|
44
|
+
const client = makeMockClient(() => Promise.resolve(null));
|
|
45
|
+
await healStaleCronRecords(statsStore, client, log);
|
|
46
|
+
expect(statsStore.removeByThreadId).toHaveBeenCalledWith('thread-dead');
|
|
47
|
+
expect(log.warn).toHaveBeenCalledWith(expect.objectContaining({ cronId: 'cron-abc', threadId: 'thread-dead' }), expect.stringContaining('stale'));
|
|
48
|
+
});
|
|
49
|
+
it('removes stale record when channels.fetch throws Discord error code 10003', async () => {
|
|
50
|
+
const log = makeMockLog();
|
|
51
|
+
const statsStore = makeMockStatsStore({
|
|
52
|
+
'cron-abc': { cronId: 'cron-abc', threadId: 'thread-gone' },
|
|
53
|
+
});
|
|
54
|
+
const discordError = Object.assign(new Error('Unknown Channel'), { code: 10003 });
|
|
55
|
+
const client = makeMockClient(() => Promise.reject(discordError));
|
|
56
|
+
await healStaleCronRecords(statsStore, client, log);
|
|
57
|
+
expect(statsStore.removeByThreadId).toHaveBeenCalledWith('thread-gone');
|
|
58
|
+
expect(log.warn).toHaveBeenCalledWith(expect.objectContaining({ cronId: 'cron-abc', threadId: 'thread-gone' }), expect.stringContaining('stale'));
|
|
59
|
+
});
|
|
60
|
+
it('removes stale record when channels.fetch throws with HTTP status 404', async () => {
|
|
61
|
+
const log = makeMockLog();
|
|
62
|
+
const statsStore = makeMockStatsStore({
|
|
63
|
+
'cron-abc': { cronId: 'cron-abc', threadId: 'thread-404' },
|
|
64
|
+
});
|
|
65
|
+
const httpError = Object.assign(new Error('Not Found'), { status: 404 });
|
|
66
|
+
const client = makeMockClient(() => Promise.reject(httpError));
|
|
67
|
+
await healStaleCronRecords(statsStore, client, log);
|
|
68
|
+
expect(statsStore.removeByThreadId).toHaveBeenCalledWith('thread-404');
|
|
69
|
+
});
|
|
70
|
+
it('skips the record and logs a fetch-error warning for non-404 network errors', async () => {
|
|
71
|
+
const log = makeMockLog();
|
|
72
|
+
const statsStore = makeMockStatsStore({
|
|
73
|
+
'cron-abc': { cronId: 'cron-abc', threadId: 'thread-1' },
|
|
74
|
+
});
|
|
75
|
+
const networkError = new Error('ECONNRESET');
|
|
76
|
+
const client = makeMockClient(() => Promise.reject(networkError));
|
|
77
|
+
await healStaleCronRecords(statsStore, client, log);
|
|
78
|
+
expect(statsStore.removeByThreadId).not.toHaveBeenCalled();
|
|
79
|
+
expect(log.warn).toHaveBeenCalledWith(expect.objectContaining({ cronId: 'cron-abc', threadId: 'thread-1' }), expect.stringContaining('fetch error'));
|
|
80
|
+
});
|
|
81
|
+
it('preserves the live record when one record is stale and one is live', async () => {
|
|
82
|
+
const log = makeMockLog();
|
|
83
|
+
const statsStore = makeMockStatsStore({
|
|
84
|
+
'cron-stale': { cronId: 'cron-stale', threadId: 'thread-dead' },
|
|
85
|
+
'cron-live': { cronId: 'cron-live', threadId: 'thread-alive' },
|
|
86
|
+
});
|
|
87
|
+
const client = makeMockClient((id) => id === 'thread-dead' ? Promise.resolve(null) : Promise.resolve({ id }));
|
|
88
|
+
await healStaleCronRecords(statsStore, client, log);
|
|
89
|
+
expect(statsStore.removeByThreadId).toHaveBeenCalledTimes(1);
|
|
90
|
+
expect(statsStore.removeByThreadId).toHaveBeenCalledWith('thread-dead');
|
|
91
|
+
expect(statsStore.removeByThreadId).not.toHaveBeenCalledWith('thread-alive');
|
|
92
|
+
});
|
|
93
|
+
it('logs and continues (fail-open) when removeByThreadId throws', async () => {
|
|
94
|
+
const log = makeMockLog();
|
|
95
|
+
const statsStore = makeMockStatsStore({
|
|
96
|
+
'cron-abc': { cronId: 'cron-abc', threadId: 'thread-dead' },
|
|
97
|
+
});
|
|
98
|
+
statsStore.removeByThreadId.mockRejectedValueOnce(new Error('write error'));
|
|
99
|
+
const client = makeMockClient(() => Promise.resolve(null));
|
|
100
|
+
await expect(healStaleCronRecords(statsStore, client, log)).resolves.not.toThrow();
|
|
101
|
+
expect(log.warn).toHaveBeenCalledWith(expect.objectContaining({ cronId: 'cron-abc', threadId: 'thread-dead' }), expect.stringContaining('failed to remove'));
|
|
102
|
+
});
|
|
103
|
+
it('is a no-op and does not throw when the stats store is empty', async () => {
|
|
104
|
+
const log = makeMockLog();
|
|
105
|
+
const statsStore = makeMockStatsStore({});
|
|
106
|
+
const client = makeMockClient();
|
|
107
|
+
await expect(healStaleCronRecords(statsStore, client, log)).resolves.not.toThrow();
|
|
108
|
+
expect(statsStore.removeByThreadId).not.toHaveBeenCalled();
|
|
109
|
+
expect(log.warn).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
it('works without a log argument', async () => {
|
|
112
|
+
const statsStore = makeMockStatsStore({
|
|
113
|
+
'cron-abc': { cronId: 'cron-abc', threadId: 'thread-dead' },
|
|
114
|
+
});
|
|
115
|
+
const client = makeMockClient(() => Promise.resolve(null));
|
|
116
|
+
await expect(healStaleCronRecords(statsStore, client)).resolves.not.toThrow();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// healStaleTaskThreadRefs — Scenario 3
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
describe('healStaleTaskThreadRefs', () => {
|
|
123
|
+
it('logs a warning when the referenced thread no longer exists (Discord code 10003)', async () => {
|
|
124
|
+
const log = makeMockLog();
|
|
125
|
+
const store = makeMockTaskStore([
|
|
126
|
+
{ id: 'ws-001', status: 'open', title: 'Task 1', external_ref: 'discord:thread-gone' },
|
|
127
|
+
]);
|
|
128
|
+
const discordError = Object.assign(new Error('Unknown Channel'), { code: 10003 });
|
|
129
|
+
const client = makeMockClient(() => Promise.reject(discordError));
|
|
130
|
+
await healStaleTaskThreadRefs(store, client, log);
|
|
131
|
+
expect(log.warn).toHaveBeenCalledWith(expect.objectContaining({ taskId: 'ws-001', threadId: 'thread-gone' }), expect.stringContaining('no longer exists'));
|
|
132
|
+
});
|
|
133
|
+
it('logs a warning when channels.fetch returns null', async () => {
|
|
134
|
+
const log = makeMockLog();
|
|
135
|
+
const store = makeMockTaskStore([
|
|
136
|
+
{ id: 'ws-001', status: 'open', title: 'Task 1', external_ref: 'discord:thread-null' },
|
|
137
|
+
]);
|
|
138
|
+
const client = makeMockClient(() => Promise.resolve(null));
|
|
139
|
+
await healStaleTaskThreadRefs(store, client, log);
|
|
140
|
+
expect(log.warn).toHaveBeenCalledWith(expect.objectContaining({ taskId: 'ws-001', threadId: 'thread-null' }), expect.stringContaining('no longer exists'));
|
|
141
|
+
});
|
|
142
|
+
it('does not warn when the thread exists', async () => {
|
|
143
|
+
const log = makeMockLog();
|
|
144
|
+
const store = makeMockTaskStore([
|
|
145
|
+
{ id: 'ws-001', status: 'open', title: 'Task 1', external_ref: 'discord:thread-alive' },
|
|
146
|
+
]);
|
|
147
|
+
const client = makeMockClient(() => Promise.resolve({ id: 'thread-alive' }));
|
|
148
|
+
await healStaleTaskThreadRefs(store, client, log);
|
|
149
|
+
expect(log.warn).not.toHaveBeenCalled();
|
|
150
|
+
});
|
|
151
|
+
it('does not modify external_ref even when the thread is gone', async () => {
|
|
152
|
+
const log = makeMockLog();
|
|
153
|
+
const tasks = [
|
|
154
|
+
{ id: 'ws-001', status: 'open', title: 'Task 1', external_ref: 'discord:thread-gone' },
|
|
155
|
+
];
|
|
156
|
+
const store = makeMockTaskStore(tasks);
|
|
157
|
+
const client = makeMockClient(() => Promise.resolve(null));
|
|
158
|
+
await healStaleTaskThreadRefs(store, client, log);
|
|
159
|
+
expect(tasks[0].external_ref).toBe('discord:thread-gone');
|
|
160
|
+
});
|
|
161
|
+
it('logs a fetch-error warning (not "no longer exists") for non-404 errors', async () => {
|
|
162
|
+
const log = makeMockLog();
|
|
163
|
+
const store = makeMockTaskStore([
|
|
164
|
+
{ id: 'ws-001', status: 'open', title: 'Task 1', external_ref: 'discord:thread-1' },
|
|
165
|
+
]);
|
|
166
|
+
const networkError = new Error('ECONNRESET');
|
|
167
|
+
const client = makeMockClient(() => Promise.reject(networkError));
|
|
168
|
+
await healStaleTaskThreadRefs(store, client, log);
|
|
169
|
+
expect(log.warn).toHaveBeenCalledWith(expect.objectContaining({ taskId: 'ws-001', threadId: 'thread-1' }), expect.stringContaining('fetch error'));
|
|
170
|
+
expect(log.warn).not.toHaveBeenCalledWith(expect.anything(), expect.stringContaining('no longer exists'));
|
|
171
|
+
});
|
|
172
|
+
it('skips tasks without external_ref', async () => {
|
|
173
|
+
const log = makeMockLog();
|
|
174
|
+
const store = makeMockTaskStore([
|
|
175
|
+
{ id: 'ws-001', status: 'open', title: 'Task 1' },
|
|
176
|
+
]);
|
|
177
|
+
const client = makeMockClient();
|
|
178
|
+
await healStaleTaskThreadRefs(store, client, log);
|
|
179
|
+
expect(client.channels.fetch).not.toHaveBeenCalled();
|
|
180
|
+
expect(log.warn).not.toHaveBeenCalled();
|
|
181
|
+
});
|
|
182
|
+
it('skips tasks with non-discord external_ref', async () => {
|
|
183
|
+
const log = makeMockLog();
|
|
184
|
+
const store = makeMockTaskStore([
|
|
185
|
+
{ id: 'ws-001', status: 'open', title: 'Task 1', external_ref: 'github:123' },
|
|
186
|
+
]);
|
|
187
|
+
const client = makeMockClient();
|
|
188
|
+
await healStaleTaskThreadRefs(store, client, log);
|
|
189
|
+
expect(client.channels.fetch).not.toHaveBeenCalled();
|
|
190
|
+
expect(log.warn).not.toHaveBeenCalled();
|
|
191
|
+
});
|
|
192
|
+
it('does not throw and logs for each missing thread when multiple tasks have gone threads', async () => {
|
|
193
|
+
const log = makeMockLog();
|
|
194
|
+
const store = makeMockTaskStore([
|
|
195
|
+
{ id: 'ws-001', status: 'open', title: 'Task 1', external_ref: 'discord:thread-a' },
|
|
196
|
+
{ id: 'ws-002', status: 'in_progress', title: 'Task 2', external_ref: 'discord:thread-b' },
|
|
197
|
+
]);
|
|
198
|
+
const client = makeMockClient(() => Promise.resolve(null));
|
|
199
|
+
await expect(healStaleTaskThreadRefs(store, client, log)).resolves.not.toThrow();
|
|
200
|
+
expect(log.warn).toHaveBeenCalledTimes(2);
|
|
201
|
+
});
|
|
202
|
+
it('works without a log argument', async () => {
|
|
203
|
+
const store = makeMockTaskStore([
|
|
204
|
+
{ id: 'ws-001', status: 'open', title: 'Task 1', external_ref: 'discord:thread-gone' },
|
|
205
|
+
]);
|
|
206
|
+
const client = makeMockClient(() => Promise.resolve(null));
|
|
207
|
+
await expect(healStaleTaskThreadRefs(store, client)).resolves.not.toThrow();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// healCorruptedJsonStores — Scenario 4
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
describe('healCorruptedJsonStores', () => {
|
|
214
|
+
let tmpDir;
|
|
215
|
+
beforeEach(async () => {
|
|
216
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'startup-healing-test-'));
|
|
217
|
+
});
|
|
218
|
+
afterEach(async () => {
|
|
219
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
220
|
+
});
|
|
221
|
+
it('backs up and removes a corrupted JSON file, logging label and parse error', async () => {
|
|
222
|
+
const log = makeMockLog();
|
|
223
|
+
const filePath = path.join(tmpDir, 'test.json');
|
|
224
|
+
const badContent = '{invalid json';
|
|
225
|
+
await fs.writeFile(filePath, badContent, 'utf-8');
|
|
226
|
+
await healCorruptedJsonStores([{ path: filePath, label: 'test-store' }], log);
|
|
227
|
+
// Original file should be removed.
|
|
228
|
+
await expect(fs.access(filePath)).rejects.toThrow();
|
|
229
|
+
// A backup file should exist with the original content.
|
|
230
|
+
const files = await fs.readdir(tmpDir);
|
|
231
|
+
const backupFiles = files.filter((f) => f.includes('.corrupt.'));
|
|
232
|
+
expect(backupFiles).toHaveLength(1);
|
|
233
|
+
const backupContent = await fs.readFile(path.join(tmpDir, backupFiles[0]), 'utf-8');
|
|
234
|
+
expect(backupContent).toBe(badContent);
|
|
235
|
+
// Warning logged with label, path, backupPath, and parseError.
|
|
236
|
+
expect(log.warn).toHaveBeenCalledWith(expect.objectContaining({
|
|
237
|
+
label: 'test-store',
|
|
238
|
+
path: filePath,
|
|
239
|
+
backupPath: expect.stringContaining('.corrupt.'),
|
|
240
|
+
parseError: expect.any(String),
|
|
241
|
+
}), expect.stringContaining('corrupted'));
|
|
242
|
+
});
|
|
243
|
+
it('does nothing for a valid JSON file', async () => {
|
|
244
|
+
const log = makeMockLog();
|
|
245
|
+
const filePath = path.join(tmpDir, 'valid.json');
|
|
246
|
+
await fs.writeFile(filePath, '{"version":3,"jobs":{}}', 'utf-8');
|
|
247
|
+
await healCorruptedJsonStores([{ path: filePath, label: 'valid-store' }], log);
|
|
248
|
+
// File should still exist.
|
|
249
|
+
await expect(fs.access(filePath)).resolves.not.toThrow();
|
|
250
|
+
// No warning emitted.
|
|
251
|
+
expect(log.warn).not.toHaveBeenCalled();
|
|
252
|
+
});
|
|
253
|
+
it('silently skips a missing file (ENOENT is not corruption)', async () => {
|
|
254
|
+
const log = makeMockLog();
|
|
255
|
+
const filePath = path.join(tmpDir, 'nonexistent.json');
|
|
256
|
+
await expect(healCorruptedJsonStores([{ path: filePath, label: 'missing-store' }], log)).resolves.not.toThrow();
|
|
257
|
+
expect(log.warn).not.toHaveBeenCalled();
|
|
258
|
+
});
|
|
259
|
+
it('processes multiple paths, healing only the corrupt ones', async () => {
|
|
260
|
+
const log = makeMockLog();
|
|
261
|
+
const corruptPath = path.join(tmpDir, 'corrupt.json');
|
|
262
|
+
const validPath = path.join(tmpDir, 'valid.json');
|
|
263
|
+
await fs.writeFile(corruptPath, '{bad', 'utf-8');
|
|
264
|
+
await fs.writeFile(validPath, '{"ok":true}', 'utf-8');
|
|
265
|
+
await healCorruptedJsonStores([
|
|
266
|
+
{ path: corruptPath, label: 'corrupt-store' },
|
|
267
|
+
{ path: validPath, label: 'valid-store' },
|
|
268
|
+
], log);
|
|
269
|
+
// Only the corrupt file is removed.
|
|
270
|
+
await expect(fs.access(corruptPath)).rejects.toThrow();
|
|
271
|
+
await expect(fs.access(validPath)).resolves.not.toThrow();
|
|
272
|
+
// Only one warning.
|
|
273
|
+
expect(log.warn).toHaveBeenCalledTimes(1);
|
|
274
|
+
expect(log.warn).toHaveBeenCalledWith(expect.objectContaining({ label: 'corrupt-store' }), expect.anything());
|
|
275
|
+
});
|
|
276
|
+
it('backup filename contains a timestamp segment', async () => {
|
|
277
|
+
const log = makeMockLog();
|
|
278
|
+
const filePath = path.join(tmpDir, 'ts-test.json');
|
|
279
|
+
await fs.writeFile(filePath, 'not json', 'utf-8');
|
|
280
|
+
await healCorruptedJsonStores([{ path: filePath, label: 'ts-store' }], log);
|
|
281
|
+
const files = await fs.readdir(tmpDir);
|
|
282
|
+
const backupFiles = files.filter((f) => f.startsWith('ts-test.json.corrupt.'));
|
|
283
|
+
expect(backupFiles).toHaveLength(1);
|
|
284
|
+
// The suffix after .corrupt. should be a non-empty timestamp string.
|
|
285
|
+
const suffix = backupFiles[0].split('.corrupt.')[1];
|
|
286
|
+
expect(suffix.length).toBeGreaterThan(0);
|
|
287
|
+
});
|
|
288
|
+
it('works without a log argument', async () => {
|
|
289
|
+
const filePath = path.join(tmpDir, 'corrupt.json');
|
|
290
|
+
await fs.writeFile(filePath, '{bad', 'utf-8');
|
|
291
|
+
await expect(healCorruptedJsonStores([{ path: filePath, label: 'test' }])).resolves.not.toThrow();
|
|
292
|
+
});
|
|
293
|
+
it('handles an empty paths array without throwing', async () => {
|
|
294
|
+
const log = makeMockLog();
|
|
295
|
+
await expect(healCorruptedJsonStores([], log)).resolves.not.toThrow();
|
|
296
|
+
expect(log.warn).not.toHaveBeenCalled();
|
|
297
|
+
});
|
|
298
|
+
});
|
package/dist/identity.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const DISCORD_NICKNAME_LIMIT = 32;
|
|
4
|
+
const DEFAULT_NAME = 'Discoclaw';
|
|
5
|
+
export async function resolveDisplayName(opts) {
|
|
6
|
+
let name = opts.configName ?? await parseIdentityName(opts.workspaceCwd) ?? DEFAULT_NAME;
|
|
7
|
+
if (name.length > DISCORD_NICKNAME_LIMIT) {
|
|
8
|
+
opts.log?.warn({ original: name, truncated: name.slice(0, DISCORD_NICKNAME_LIMIT) }, 'botDisplayName exceeds Discord 32-char nickname limit; truncating');
|
|
9
|
+
name = name.slice(0, DISCORD_NICKNAME_LIMIT);
|
|
10
|
+
}
|
|
11
|
+
if (!name.trim()) {
|
|
12
|
+
name = DEFAULT_NAME;
|
|
13
|
+
}
|
|
14
|
+
return name;
|
|
15
|
+
}
|
|
16
|
+
export async function parseIdentityName(workspaceCwd) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = await fs.readFile(path.join(workspaceCwd, 'IDENTITY.md'), 'utf8');
|
|
19
|
+
const patterns = [
|
|
20
|
+
/^-\s*\*\*Name:\*\*\s*(.+)$/m, // - **Name:** Weston
|
|
21
|
+
/^\*\*Name\*\*:\s*(.+)$/m, // **Name**: Weston
|
|
22
|
+
/^Name:\s*(.+)$/mi, // Name: Weston
|
|
23
|
+
];
|
|
24
|
+
for (const re of patterns) {
|
|
25
|
+
const match = raw.match(re);
|
|
26
|
+
if (match?.[1]?.trim())
|
|
27
|
+
return match[1].trim();
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
if (err.code === 'ENOENT')
|
|
33
|
+
return undefined;
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
}
|