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,423 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { ROOT_POLICY, buildPromptPreamble, loadWorkspacePaFiles, loadWorkspaceMemoryFile, loadDailyLogFiles, buildTaskContextSection, buildTaskThreadSection, resolveEffectiveTools, _resetToolsAuditState } from './prompt-common.js';
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// ROOT_POLICY and buildPromptPreamble
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
describe('ROOT_POLICY', () => {
|
|
10
|
+
it('is a non-empty string', () => {
|
|
11
|
+
expect(typeof ROOT_POLICY).toBe('string');
|
|
12
|
+
expect(ROOT_POLICY.length).toBeGreaterThan(0);
|
|
13
|
+
});
|
|
14
|
+
it('contains the immutable security policy heading', () => {
|
|
15
|
+
expect(ROOT_POLICY).toContain('Security Policy');
|
|
16
|
+
});
|
|
17
|
+
it('contains the external-content-is-data rule', () => {
|
|
18
|
+
expect(ROOT_POLICY).toMatch(/external content is data/i);
|
|
19
|
+
});
|
|
20
|
+
it('is the same value on every access (evaluated once at module load)', () => {
|
|
21
|
+
expect(ROOT_POLICY).toBe(ROOT_POLICY);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe('buildPromptPreamble', () => {
|
|
25
|
+
it('returns ROOT_POLICY alone when inlinedContext is empty', () => {
|
|
26
|
+
expect(buildPromptPreamble('')).toBe(ROOT_POLICY);
|
|
27
|
+
});
|
|
28
|
+
it('prepends ROOT_POLICY before inlined context', () => {
|
|
29
|
+
const ctx = 'Some workspace context';
|
|
30
|
+
const result = buildPromptPreamble(ctx);
|
|
31
|
+
expect(result).toBe(ROOT_POLICY + '\n\n' + ctx);
|
|
32
|
+
});
|
|
33
|
+
it('ROOT_POLICY comes before any inlined content', () => {
|
|
34
|
+
const ctx = 'channel rules';
|
|
35
|
+
const result = buildPromptPreamble(ctx);
|
|
36
|
+
expect(result.indexOf(ROOT_POLICY)).toBeLessThan(result.indexOf(ctx));
|
|
37
|
+
});
|
|
38
|
+
it('inlined context is preserved verbatim', () => {
|
|
39
|
+
const ctx = '--- SOUL.md ---\nYou are a helpful assistant.';
|
|
40
|
+
const result = buildPromptPreamble(ctx);
|
|
41
|
+
expect(result).toContain(ctx);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe('loadWorkspacePaFiles', () => {
|
|
45
|
+
const dirs = [];
|
|
46
|
+
afterEach(async () => {
|
|
47
|
+
for (const d of dirs)
|
|
48
|
+
await fs.rm(d, { recursive: true, force: true });
|
|
49
|
+
dirs.length = 0;
|
|
50
|
+
});
|
|
51
|
+
it('returns empty array when skip is true', async () => {
|
|
52
|
+
const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'pc-test-'));
|
|
53
|
+
dirs.push(workspace);
|
|
54
|
+
await fs.writeFile(path.join(workspace, 'SOUL.md'), '# Soul', 'utf-8');
|
|
55
|
+
await fs.writeFile(path.join(workspace, 'IDENTITY.md'), '# ID', 'utf-8');
|
|
56
|
+
const files = await loadWorkspacePaFiles(workspace, { skip: true });
|
|
57
|
+
expect(files).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
it('returns PA files when skip is false', async () => {
|
|
60
|
+
const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'pc-test-'));
|
|
61
|
+
dirs.push(workspace);
|
|
62
|
+
await fs.writeFile(path.join(workspace, 'SOUL.md'), '# Soul', 'utf-8');
|
|
63
|
+
await fs.writeFile(path.join(workspace, 'IDENTITY.md'), '# ID', 'utf-8');
|
|
64
|
+
const files = await loadWorkspacePaFiles(workspace, { skip: false });
|
|
65
|
+
expect(files).toEqual([
|
|
66
|
+
path.join(workspace, 'SOUL.md'),
|
|
67
|
+
path.join(workspace, 'IDENTITY.md'),
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
it('returns PA files when opts is omitted', async () => {
|
|
71
|
+
const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'pc-test-'));
|
|
72
|
+
dirs.push(workspace);
|
|
73
|
+
await fs.writeFile(path.join(workspace, 'USER.md'), '# User', 'utf-8');
|
|
74
|
+
const files = await loadWorkspacePaFiles(workspace);
|
|
75
|
+
expect(files).toEqual([path.join(workspace, 'USER.md')]);
|
|
76
|
+
});
|
|
77
|
+
it('includes BOOTSTRAP.md before PA files when onboarding is incomplete', async () => {
|
|
78
|
+
const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'pc-test-'));
|
|
79
|
+
dirs.push(workspace);
|
|
80
|
+
await fs.writeFile(path.join(workspace, 'BOOTSTRAP.md'), '# Bootstrap', 'utf-8');
|
|
81
|
+
await fs.writeFile(path.join(workspace, 'SOUL.md'), '# Soul', 'utf-8');
|
|
82
|
+
// No IDENTITY.md with real content — onboarding incomplete.
|
|
83
|
+
const files = await loadWorkspacePaFiles(workspace);
|
|
84
|
+
expect(files[0]).toBe(path.join(workspace, 'BOOTSTRAP.md'));
|
|
85
|
+
expect(files[1]).toBe(path.join(workspace, 'SOUL.md'));
|
|
86
|
+
});
|
|
87
|
+
it('excludes BOOTSTRAP.md when onboarding is complete', async () => {
|
|
88
|
+
const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'pc-test-'));
|
|
89
|
+
dirs.push(workspace);
|
|
90
|
+
await fs.writeFile(path.join(workspace, 'BOOTSTRAP.md'), '# Bootstrap', 'utf-8');
|
|
91
|
+
await fs.writeFile(path.join(workspace, 'SOUL.md'), '# Soul', 'utf-8');
|
|
92
|
+
// IDENTITY.md with real content — onboarding complete.
|
|
93
|
+
await fs.writeFile(path.join(workspace, 'IDENTITY.md'), '# Identity\n\nName: Claw\nVibe: Snarky but helpful\nEmoji: 🦀\nCreature: A sentient crustacean AI', 'utf-8');
|
|
94
|
+
// USER.md with real content — required for onboarding complete.
|
|
95
|
+
await fs.writeFile(path.join(workspace, 'USER.md'), '# USER.md - About Your Human\n\n- **Name:** Test User\n- **What to call them:** Test\n', 'utf-8');
|
|
96
|
+
const files = await loadWorkspacePaFiles(workspace);
|
|
97
|
+
expect(files).not.toContainEqual(expect.stringContaining('BOOTSTRAP.md'));
|
|
98
|
+
expect(files).toContainEqual(expect.stringContaining('SOUL.md'));
|
|
99
|
+
expect(files).toContainEqual(expect.stringContaining('IDENTITY.md'));
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe('loadWorkspaceMemoryFile', () => {
|
|
103
|
+
const dirs = [];
|
|
104
|
+
afterEach(async () => {
|
|
105
|
+
for (const d of dirs)
|
|
106
|
+
await fs.rm(d, { recursive: true, force: true });
|
|
107
|
+
dirs.length = 0;
|
|
108
|
+
});
|
|
109
|
+
it('returns path when MEMORY.md exists', async () => {
|
|
110
|
+
const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'pc-test-'));
|
|
111
|
+
dirs.push(workspace);
|
|
112
|
+
await fs.writeFile(path.join(workspace, 'MEMORY.md'), '# Memory', 'utf-8');
|
|
113
|
+
const result = await loadWorkspaceMemoryFile(workspace);
|
|
114
|
+
expect(result).toBe(path.join(workspace, 'MEMORY.md'));
|
|
115
|
+
});
|
|
116
|
+
it('returns null when MEMORY.md does not exist', async () => {
|
|
117
|
+
const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'pc-test-'));
|
|
118
|
+
dirs.push(workspace);
|
|
119
|
+
const result = await loadWorkspaceMemoryFile(workspace);
|
|
120
|
+
expect(result).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe('loadDailyLogFiles', () => {
|
|
124
|
+
const dirs = [];
|
|
125
|
+
afterEach(async () => {
|
|
126
|
+
for (const d of dirs)
|
|
127
|
+
await fs.rm(d, { recursive: true, force: true });
|
|
128
|
+
dirs.length = 0;
|
|
129
|
+
});
|
|
130
|
+
function dateStr(d) {
|
|
131
|
+
return d.toISOString().slice(0, 10);
|
|
132
|
+
}
|
|
133
|
+
it('returns today and yesterday log paths when both exist', async () => {
|
|
134
|
+
const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'pc-test-'));
|
|
135
|
+
dirs.push(workspace);
|
|
136
|
+
const memDir = path.join(workspace, 'memory');
|
|
137
|
+
await fs.mkdir(memDir, { recursive: true });
|
|
138
|
+
const today = new Date();
|
|
139
|
+
const yesterday = new Date(today);
|
|
140
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
141
|
+
await fs.writeFile(path.join(memDir, dateStr(today) + '.md'), 'today', 'utf-8');
|
|
142
|
+
await fs.writeFile(path.join(memDir, dateStr(yesterday) + '.md'), 'yesterday', 'utf-8');
|
|
143
|
+
const result = await loadDailyLogFiles(workspace);
|
|
144
|
+
expect(result).toEqual([
|
|
145
|
+
path.join(memDir, dateStr(today) + '.md'),
|
|
146
|
+
path.join(memDir, dateStr(yesterday) + '.md'),
|
|
147
|
+
]);
|
|
148
|
+
});
|
|
149
|
+
it('returns only today when yesterday does not exist', async () => {
|
|
150
|
+
const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'pc-test-'));
|
|
151
|
+
dirs.push(workspace);
|
|
152
|
+
const memDir = path.join(workspace, 'memory');
|
|
153
|
+
await fs.mkdir(memDir, { recursive: true });
|
|
154
|
+
const today = new Date();
|
|
155
|
+
await fs.writeFile(path.join(memDir, dateStr(today) + '.md'), 'today', 'utf-8');
|
|
156
|
+
const result = await loadDailyLogFiles(workspace);
|
|
157
|
+
expect(result).toEqual([path.join(memDir, dateStr(today) + '.md')]);
|
|
158
|
+
});
|
|
159
|
+
it('returns empty array when no daily logs exist', async () => {
|
|
160
|
+
const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'pc-test-'));
|
|
161
|
+
dirs.push(workspace);
|
|
162
|
+
const result = await loadDailyLogFiles(workspace);
|
|
163
|
+
expect(result).toEqual([]);
|
|
164
|
+
});
|
|
165
|
+
it('returns empty array when memory dir does not exist', async () => {
|
|
166
|
+
const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'pc-test-'));
|
|
167
|
+
dirs.push(workspace);
|
|
168
|
+
const result = await loadDailyLogFiles(workspace);
|
|
169
|
+
expect(result).toEqual([]);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// buildTaskContextSection
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
function makeBead(overrides = {}) {
|
|
176
|
+
return { id: 'ws-042', title: 'Fix auth bug', status: 'in_progress', ...overrides };
|
|
177
|
+
}
|
|
178
|
+
describe('buildTaskContextSection', () => {
|
|
179
|
+
it('formats all fields as JSON', () => {
|
|
180
|
+
const bead = makeBead({
|
|
181
|
+
priority: 2,
|
|
182
|
+
owner: 'David',
|
|
183
|
+
labels: ['bug', 'auth'],
|
|
184
|
+
description: 'Users are getting 401 errors on login.',
|
|
185
|
+
});
|
|
186
|
+
const section = buildTaskContextSection(bead);
|
|
187
|
+
expect(section).toContain('```json');
|
|
188
|
+
const json = JSON.parse(section.split('```json\n')[1].split('\n```')[0]);
|
|
189
|
+
expect(json.id).toBe('ws-042');
|
|
190
|
+
expect(json.title).toBe('Fix auth bug');
|
|
191
|
+
expect(json.status).toBe('in_progress');
|
|
192
|
+
expect(json.priority).toBe(2);
|
|
193
|
+
expect(json.owner).toBe('David');
|
|
194
|
+
expect(json.labels).toEqual(['bug', 'auth']);
|
|
195
|
+
expect(json.description).toBe('Users are getting 401 errors on login.');
|
|
196
|
+
});
|
|
197
|
+
it('handles missing optional fields', () => {
|
|
198
|
+
const bead = makeBead(); // no priority, owner, labels, description
|
|
199
|
+
const section = buildTaskContextSection(bead);
|
|
200
|
+
const json = JSON.parse(section.split('```json\n')[1].split('\n```')[0]);
|
|
201
|
+
expect(json.id).toBe('ws-042');
|
|
202
|
+
expect(json.priority).toBeUndefined();
|
|
203
|
+
expect(json.owner).toBeUndefined();
|
|
204
|
+
expect(json.labels).toBeUndefined();
|
|
205
|
+
expect(json.description).toBeUndefined();
|
|
206
|
+
});
|
|
207
|
+
it('truncates long descriptions', () => {
|
|
208
|
+
const longDesc = 'A'.repeat(600);
|
|
209
|
+
const bead = makeBead({ description: longDesc });
|
|
210
|
+
const section = buildTaskContextSection(bead);
|
|
211
|
+
const json = JSON.parse(section.split('```json\n')[1].split('\n```')[0]);
|
|
212
|
+
expect(json.description.length).toBe(500);
|
|
213
|
+
expect(json.description).toMatch(/\u2026$/);
|
|
214
|
+
});
|
|
215
|
+
it('includes forum sendMessage guidance for active beads', () => {
|
|
216
|
+
const bead = makeBead();
|
|
217
|
+
const section = buildTaskContextSection(bead);
|
|
218
|
+
expect(section).toContain('Do not emit a sendMessage action targeting the parent forum channel');
|
|
219
|
+
});
|
|
220
|
+
it('omits forum sendMessage guidance for closed beads', () => {
|
|
221
|
+
const bead = makeBead({ status: 'closed' });
|
|
222
|
+
const section = buildTaskContextSection(bead);
|
|
223
|
+
expect(section).not.toContain('Do not emit a sendMessage action targeting the parent forum channel');
|
|
224
|
+
});
|
|
225
|
+
it('emits minimal context for closed beads', () => {
|
|
226
|
+
const bead = makeBead({
|
|
227
|
+
status: 'closed',
|
|
228
|
+
priority: 1,
|
|
229
|
+
owner: 'David',
|
|
230
|
+
labels: ['bug'],
|
|
231
|
+
description: 'Full description here',
|
|
232
|
+
});
|
|
233
|
+
const section = buildTaskContextSection(bead);
|
|
234
|
+
const json = JSON.parse(section.split('```json\n')[1].split('\n```')[0]);
|
|
235
|
+
expect(json.id).toBe('ws-042');
|
|
236
|
+
expect(json.title).toBe('Fix auth bug');
|
|
237
|
+
expect(json.status).toBe('closed');
|
|
238
|
+
// Closed beads should NOT include verbose fields.
|
|
239
|
+
expect(json.priority).toBeUndefined();
|
|
240
|
+
expect(json.owner).toBeUndefined();
|
|
241
|
+
expect(json.labels).toBeUndefined();
|
|
242
|
+
expect(json.description).toBeUndefined();
|
|
243
|
+
// Should include the behavioral hint.
|
|
244
|
+
expect(section).toContain('This task is resolved');
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
// buildTaskThreadSection
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Mock the cache so tests can control task lookups deterministically.
|
|
251
|
+
vi.mock('../tasks/thread-cache.js', () => ({
|
|
252
|
+
taskThreadCache: {
|
|
253
|
+
get: vi.fn(),
|
|
254
|
+
},
|
|
255
|
+
}));
|
|
256
|
+
import { taskThreadCache } from '../tasks/thread-cache.js';
|
|
257
|
+
const mockedCacheGet = vi.mocked(taskThreadCache.get);
|
|
258
|
+
const SNOWFLAKE_FORUM_ID = '12345678901234567890';
|
|
259
|
+
function makeBeadCtx(overrides = {}) {
|
|
260
|
+
return {
|
|
261
|
+
tasksCwd: '/tmp/beads',
|
|
262
|
+
forumId: SNOWFLAKE_FORUM_ID,
|
|
263
|
+
tagMap: {},
|
|
264
|
+
store: {},
|
|
265
|
+
runtime: {},
|
|
266
|
+
autoTag: false,
|
|
267
|
+
autoTagModel: 'haiku',
|
|
268
|
+
...overrides,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
describe('buildTaskThreadSection', () => {
|
|
272
|
+
beforeEach(() => {
|
|
273
|
+
vi.clearAllMocks();
|
|
274
|
+
});
|
|
275
|
+
it('returns empty string when not a thread', async () => {
|
|
276
|
+
const result = await buildTaskThreadSection({
|
|
277
|
+
isThread: false,
|
|
278
|
+
threadId: null,
|
|
279
|
+
threadParentId: null,
|
|
280
|
+
taskCtx: makeBeadCtx(),
|
|
281
|
+
});
|
|
282
|
+
expect(result).toBe('');
|
|
283
|
+
});
|
|
284
|
+
it('returns empty string when taskCtx is undefined', async () => {
|
|
285
|
+
const result = await buildTaskThreadSection({
|
|
286
|
+
isThread: true,
|
|
287
|
+
threadId: 'thread-1',
|
|
288
|
+
threadParentId: SNOWFLAKE_FORUM_ID,
|
|
289
|
+
taskCtx: undefined,
|
|
290
|
+
});
|
|
291
|
+
expect(result).toBe('');
|
|
292
|
+
});
|
|
293
|
+
it('returns empty string when threadParentId does not match forumId', async () => {
|
|
294
|
+
const result = await buildTaskThreadSection({
|
|
295
|
+
isThread: true,
|
|
296
|
+
threadId: 'thread-1',
|
|
297
|
+
threadParentId: '99999999999999999999',
|
|
298
|
+
taskCtx: makeBeadCtx(),
|
|
299
|
+
});
|
|
300
|
+
expect(result).toBe('');
|
|
301
|
+
});
|
|
302
|
+
it('returns empty string when forumId is not a snowflake (logs warning)', async () => {
|
|
303
|
+
const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
304
|
+
const result = await buildTaskThreadSection({
|
|
305
|
+
isThread: true,
|
|
306
|
+
threadId: 'thread-1',
|
|
307
|
+
threadParentId: 'beads',
|
|
308
|
+
taskCtx: makeBeadCtx({ forumId: 'beads' }),
|
|
309
|
+
log,
|
|
310
|
+
});
|
|
311
|
+
expect(result).toBe('');
|
|
312
|
+
expect(log.warn).toHaveBeenCalledWith(expect.objectContaining({ forumId: 'beads' }), expect.stringContaining('not a snowflake'));
|
|
313
|
+
});
|
|
314
|
+
it('returns formatted section when bead found', async () => {
|
|
315
|
+
mockedCacheGet.mockResolvedValue(makeBead({ priority: 1, owner: 'David' }));
|
|
316
|
+
const result = await buildTaskThreadSection({
|
|
317
|
+
isThread: true,
|
|
318
|
+
threadId: 'thread-1',
|
|
319
|
+
threadParentId: SNOWFLAKE_FORUM_ID,
|
|
320
|
+
taskCtx: makeBeadCtx(),
|
|
321
|
+
});
|
|
322
|
+
expect(result).toContain('Task context for this thread');
|
|
323
|
+
expect(result).toContain('```json');
|
|
324
|
+
expect(result).toContain('ws-042');
|
|
325
|
+
});
|
|
326
|
+
it('returns empty string when bead not found', async () => {
|
|
327
|
+
mockedCacheGet.mockResolvedValue(null);
|
|
328
|
+
const result = await buildTaskThreadSection({
|
|
329
|
+
isThread: true,
|
|
330
|
+
threadId: 'thread-1',
|
|
331
|
+
threadParentId: SNOWFLAKE_FORUM_ID,
|
|
332
|
+
taskCtx: makeBeadCtx(),
|
|
333
|
+
});
|
|
334
|
+
expect(result).toBe('');
|
|
335
|
+
});
|
|
336
|
+
it('returns empty string when cache throws (graceful degradation)', async () => {
|
|
337
|
+
mockedCacheGet.mockRejectedValue(new Error('bd CLI not available'));
|
|
338
|
+
const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
339
|
+
const result = await buildTaskThreadSection({
|
|
340
|
+
isThread: true,
|
|
341
|
+
threadId: 'thread-1',
|
|
342
|
+
threadParentId: SNOWFLAKE_FORUM_ID,
|
|
343
|
+
taskCtx: makeBeadCtx(),
|
|
344
|
+
log,
|
|
345
|
+
});
|
|
346
|
+
expect(result).toBe('');
|
|
347
|
+
expect(log.warn).toHaveBeenCalled();
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
// resolveEffectiveTools — fingerprint audit logging
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
describe('resolveEffectiveTools audit logging', () => {
|
|
354
|
+
const dirs = [];
|
|
355
|
+
beforeEach(() => {
|
|
356
|
+
_resetToolsAuditState();
|
|
357
|
+
});
|
|
358
|
+
afterEach(async () => {
|
|
359
|
+
for (const d of dirs)
|
|
360
|
+
await fs.rm(d, { recursive: true, force: true });
|
|
361
|
+
dirs.length = 0;
|
|
362
|
+
});
|
|
363
|
+
async function tmpDir() {
|
|
364
|
+
const d = await fs.mkdtemp(path.join(os.tmpdir(), 'tools-audit-'));
|
|
365
|
+
dirs.push(d);
|
|
366
|
+
return d;
|
|
367
|
+
}
|
|
368
|
+
it('stores fingerprint without warning on first call', async () => {
|
|
369
|
+
const workspace = await tmpDir();
|
|
370
|
+
await fs.writeFile(path.join(workspace, 'PERMISSIONS.json'), '{"tier":"readonly"}');
|
|
371
|
+
const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
372
|
+
await resolveEffectiveTools({ workspaceCwd: workspace, runtimeTools: ['Bash', 'Read'], log });
|
|
373
|
+
expect(log.warn).not.toHaveBeenCalled();
|
|
374
|
+
});
|
|
375
|
+
it('produces no warning when tools are unchanged', async () => {
|
|
376
|
+
const workspace = await tmpDir();
|
|
377
|
+
await fs.writeFile(path.join(workspace, 'PERMISSIONS.json'), '{"tier":"readonly"}');
|
|
378
|
+
const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
379
|
+
await resolveEffectiveTools({ workspaceCwd: workspace, runtimeTools: ['Bash', 'Read'], log });
|
|
380
|
+
await resolveEffectiveTools({ workspaceCwd: workspace, runtimeTools: ['Bash', 'Read'], log });
|
|
381
|
+
expect(log.warn).not.toHaveBeenCalled();
|
|
382
|
+
});
|
|
383
|
+
it('warns when effective tools change between invocations', async () => {
|
|
384
|
+
const workspace = await tmpDir();
|
|
385
|
+
await fs.writeFile(path.join(workspace, 'PERMISSIONS.json'), '{"tier":"readonly"}');
|
|
386
|
+
const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
387
|
+
await resolveEffectiveTools({ workspaceCwd: workspace, runtimeTools: ['Bash', 'Read'], log });
|
|
388
|
+
// Simulate tier change by rewriting PERMISSIONS.json.
|
|
389
|
+
await fs.writeFile(path.join(workspace, 'PERMISSIONS.json'), '{"tier":"full"}');
|
|
390
|
+
await resolveEffectiveTools({ workspaceCwd: workspace, runtimeTools: ['Bash', 'Read'], log });
|
|
391
|
+
expect(log.warn).toHaveBeenCalledWith(expect.objectContaining({ workspaceCwd: workspace }), expect.stringContaining('effective tools changed'));
|
|
392
|
+
});
|
|
393
|
+
it('tracks different workspaceCwd values independently', async () => {
|
|
394
|
+
const ws1 = await tmpDir();
|
|
395
|
+
const ws2 = await tmpDir();
|
|
396
|
+
await fs.writeFile(path.join(ws1, 'PERMISSIONS.json'), '{"tier":"readonly"}');
|
|
397
|
+
await fs.writeFile(path.join(ws2, 'PERMISSIONS.json'), '{"tier":"full"}');
|
|
398
|
+
const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
399
|
+
await resolveEffectiveTools({ workspaceCwd: ws1, runtimeTools: ['Bash', 'Read'], log });
|
|
400
|
+
await resolveEffectiveTools({ workspaceCwd: ws2, runtimeTools: ['Bash', 'Read'], log });
|
|
401
|
+
// Neither should warn — they're different workspaces.
|
|
402
|
+
expect(log.warn).not.toHaveBeenCalled();
|
|
403
|
+
});
|
|
404
|
+
it('drops tools unsupported by runtime capabilities', async () => {
|
|
405
|
+
const workspace = await tmpDir();
|
|
406
|
+
await fs.writeFile(path.join(workspace, 'PERMISSIONS.json'), '{"tier":"full"}');
|
|
407
|
+
const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
408
|
+
const result = await resolveEffectiveTools({
|
|
409
|
+
workspaceCwd: workspace,
|
|
410
|
+
runtimeTools: ['Bash', 'Read', 'WebSearch'],
|
|
411
|
+
runtimeCapabilities: new Set(['tools_fs']),
|
|
412
|
+
runtimeId: 'codex',
|
|
413
|
+
log,
|
|
414
|
+
});
|
|
415
|
+
expect(result.effectiveTools).toEqual(['Read', 'Write', 'Edit', 'Glob', 'Grep']);
|
|
416
|
+
expect(result.runtimeCapabilityNote).toContain('Bash');
|
|
417
|
+
expect(result.runtimeCapabilityNote).toContain('WebSearch');
|
|
418
|
+
expect(log.warn).toHaveBeenCalledWith(expect.objectContaining({
|
|
419
|
+
runtimeId: 'codex',
|
|
420
|
+
droppedTools: expect.arrayContaining(['Bash', 'WebSearch']),
|
|
421
|
+
}), expect.stringContaining('dropped unsupported tools'));
|
|
422
|
+
});
|
|
423
|
+
});
|