pi-usereq 0.7.0 → 0.10.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/.gitignore +2 -0
- package/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/docs/pi.dev/coding-agent-docs/custom-provider.md +42 -1
- package/docs/pi.dev/coding-agent-docs/extensions.md +302 -28
- package/docs/pi.dev/coding-agent-docs/keybindings.md +23 -1
- package/docs/pi.dev/coding-agent-docs/models.md +3 -0
- package/docs/pi.dev/coding-agent-docs/packages.md +2 -2
- package/docs/pi.dev/coding-agent-docs/prompt-templates.md +21 -0
- package/docs/pi.dev/coding-agent-docs/providers.md +1 -0
- package/docs/pi.dev/coding-agent-docs/rpc.md +36 -6
- package/docs/pi.dev/coding-agent-docs/sdk.md +30 -5
- package/docs/pi.dev/coding-agent-docs/session.md +1 -1
- package/docs/pi.dev/coding-agent-docs/settings.md +2 -1
- package/docs/pi.dev/coding-agent-docs/skills.md +12 -12
- package/docs/pi.dev/coding-agent-docs/tree.md +2 -0
- package/docs/pi.dev/coding-agent-docs/tui.md +31 -0
- package/docs/pi.dev/mom-docs/new.md +4 -4
- package/package.json +1 -1
- package/pi-usereq/docs/REFERENCES.md +5184 -24470
- package/pi-usereq/docs/REQUIREMENTS.md +300 -166
- package/pi-usereq/docs/WORKFLOW.md +303 -314
- package/pi.dev-src/pi-mono/agent/src/agent-loop.ts +683 -0
- package/pi.dev-src/pi-mono/agent/src/agent.ts +543 -0
- package/pi.dev-src/pi-mono/agent/src/index.ts +8 -0
- package/pi.dev-src/pi-mono/agent/src/proxy.ts +367 -0
- package/pi.dev-src/pi-mono/agent/src/types.ts +364 -0
- package/pi.dev-src/pi-mono/agent/test/agent-loop.test.ts +1181 -0
- package/pi.dev-src/pi-mono/agent/test/agent.test.ts +463 -0
- package/pi.dev-src/pi-mono/agent/test/e2e.test.ts +404 -0
- package/pi.dev-src/pi-mono/agent/test/utils/calculate.ts +32 -0
- package/pi.dev-src/pi-mono/agent/test/utils/get-current-time.ts +46 -0
- package/pi.dev-src/pi-mono/agent/vitest.config.ts +9 -0
- package/pi.dev-src/pi-mono/ai/bedrock-provider.d.ts +1 -0
- package/pi.dev-src/pi-mono/ai/bedrock-provider.js +1 -0
- package/pi.dev-src/pi-mono/ai/scripts/generate-models.ts +1635 -0
- package/pi.dev-src/pi-mono/ai/scripts/generate-test-image.ts +34 -0
- package/pi.dev-src/pi-mono/ai/src/api-registry.ts +98 -0
- package/pi.dev-src/pi-mono/ai/src/bedrock-provider.ts +6 -0
- package/pi.dev-src/pi-mono/ai/src/cli.ts +133 -0
- package/pi.dev-src/pi-mono/ai/src/env-api-keys.ts +134 -0
- package/pi.dev-src/pi-mono/ai/src/index.ts +35 -0
- package/pi.dev-src/pi-mono/ai/src/models.generated.ts +14928 -0
- package/pi.dev-src/pi-mono/ai/src/models.ts +82 -0
- package/pi.dev-src/pi-mono/ai/src/oauth.ts +1 -0
- package/pi.dev-src/pi-mono/ai/src/providers/amazon-bedrock.ts +929 -0
- package/pi.dev-src/pi-mono/ai/src/providers/anthropic.ts +1125 -0
- package/pi.dev-src/pi-mono/ai/src/providers/azure-openai-responses.ts +253 -0
- package/pi.dev-src/pi-mono/ai/src/providers/faux.ts +499 -0
- package/pi.dev-src/pi-mono/ai/src/providers/github-copilot-headers.ts +37 -0
- package/pi.dev-src/pi-mono/ai/src/providers/google-gemini-cli.ts +996 -0
- package/pi.dev-src/pi-mono/ai/src/providers/google-shared.ts +354 -0
- package/pi.dev-src/pi-mono/ai/src/providers/google-vertex.ts +543 -0
- package/pi.dev-src/pi-mono/ai/src/providers/google.ts +500 -0
- package/pi.dev-src/pi-mono/ai/src/providers/mistral.ts +629 -0
- package/pi.dev-src/pi-mono/ai/src/providers/openai-codex-responses.ts +984 -0
- package/pi.dev-src/pi-mono/ai/src/providers/openai-completions.ts +1063 -0
- package/pi.dev-src/pi-mono/ai/src/providers/openai-responses-shared.ts +539 -0
- package/pi.dev-src/pi-mono/ai/src/providers/openai-responses.ts +264 -0
- package/pi.dev-src/pi-mono/ai/src/providers/register-builtins.ts +433 -0
- package/pi.dev-src/pi-mono/ai/src/providers/simple-options.ts +47 -0
- package/pi.dev-src/pi-mono/ai/src/providers/transform-messages.ts +220 -0
- package/pi.dev-src/pi-mono/ai/src/stream.ts +59 -0
- package/pi.dev-src/pi-mono/ai/src/types.ts +417 -0
- package/pi.dev-src/pi-mono/ai/src/utils/event-stream.ts +87 -0
- package/pi.dev-src/pi-mono/ai/src/utils/hash.ts +13 -0
- package/pi.dev-src/pi-mono/ai/src/utils/headers.ts +7 -0
- package/pi.dev-src/pi-mono/ai/src/utils/json-parse.ts +124 -0
- package/pi.dev-src/pi-mono/ai/src/utils/overflow.ts +138 -0
- package/pi.dev-src/pi-mono/ai/src/utils/sanitize-unicode.ts +25 -0
- package/pi.dev-src/pi-mono/ai/src/utils/typebox-helpers.ts +24 -0
- package/pi.dev-src/pi-mono/ai/src/utils/validation.ts +324 -0
- package/pi.dev-src/pi-mono/ai/test/abort.test.ts +260 -0
- package/pi.dev-src/pi-mono/ai/test/anthropic-oauth.test.ts +99 -0
- package/pi.dev-src/pi-mono/ai/test/anthropic-opus-4-7-smoke.test.ts +72 -0
- package/pi.dev-src/pi-mono/ai/test/anthropic-sse-parsing.test.ts +113 -0
- package/pi.dev-src/pi-mono/ai/test/anthropic-thinking-disable.test.ts +152 -0
- package/pi.dev-src/pi-mono/ai/test/anthropic-tool-name-normalization.test.ts +205 -0
- package/pi.dev-src/pi-mono/ai/test/azure-utils.ts +28 -0
- package/pi.dev-src/pi-mono/ai/test/bedrock-endpoint-resolution.test.ts +131 -0
- package/pi.dev-src/pi-mono/ai/test/bedrock-models.test.ts +66 -0
- package/pi.dev-src/pi-mono/ai/test/bedrock-thinking-payload.test.ts +107 -0
- package/pi.dev-src/pi-mono/ai/test/bedrock-utils.ts +18 -0
- package/pi.dev-src/pi-mono/ai/test/cache-retention.test.ts +335 -0
- package/pi.dev-src/pi-mono/ai/test/context-overflow.test.ts +714 -0
- package/pi.dev-src/pi-mono/ai/test/cross-provider-handoff.test.ts +458 -0
- package/pi.dev-src/pi-mono/ai/test/data/red-circle.png +0 -0
- package/pi.dev-src/pi-mono/ai/test/empty.test.ts +742 -0
- package/pi.dev-src/pi-mono/ai/test/faux-provider.test.ts +597 -0
- package/pi.dev-src/pi-mono/ai/test/fireworks-models.test.ts +49 -0
- package/pi.dev-src/pi-mono/ai/test/github-copilot-anthropic.test.ts +108 -0
- package/pi.dev-src/pi-mono/ai/test/github-copilot-oauth.test.ts +196 -0
- package/pi.dev-src/pi-mono/ai/test/google-gemini-cli-claude-thinking-header.test.ts +103 -0
- package/pi.dev-src/pi-mono/ai/test/google-gemini-cli-empty-stream.test.ts +108 -0
- package/pi.dev-src/pi-mono/ai/test/google-gemini-cli-retry-delay.test.ts +53 -0
- package/pi.dev-src/pi-mono/ai/test/google-shared-convert-tools.test.ts +187 -0
- package/pi.dev-src/pi-mono/ai/test/google-shared-gemini3-unsigned-tool-call.test.ts +172 -0
- package/pi.dev-src/pi-mono/ai/test/google-shared-image-tool-result-routing.test.ts +124 -0
- package/pi.dev-src/pi-mono/ai/test/google-thinking-disable.test.ts +183 -0
- package/pi.dev-src/pi-mono/ai/test/google-thinking-signature.test.ts +38 -0
- package/pi.dev-src/pi-mono/ai/test/google-tool-call-missing-args.test.ts +105 -0
- package/pi.dev-src/pi-mono/ai/test/google-vertex-api-key-resolution.test.ts +144 -0
- package/pi.dev-src/pi-mono/ai/test/image-tool-result.test.ts +481 -0
- package/pi.dev-src/pi-mono/ai/test/interleaved-thinking.test.ts +145 -0
- package/pi.dev-src/pi-mono/ai/test/lazy-module-load.test.ts +102 -0
- package/pi.dev-src/pi-mono/ai/test/mistral-reasoning-mode.test.ts +66 -0
- package/pi.dev-src/pi-mono/ai/test/mistral-tool-schema.test.ts +61 -0
- package/pi.dev-src/pi-mono/ai/test/oauth.ts +89 -0
- package/pi.dev-src/pi-mono/ai/test/openai-codex-cache-affinity-e2e.test.ts +36 -0
- package/pi.dev-src/pi-mono/ai/test/openai-codex-stream.test.ts +686 -0
- package/pi.dev-src/pi-mono/ai/test/openai-completions-cache-control-format.test.ts +189 -0
- package/pi.dev-src/pi-mono/ai/test/openai-completions-prompt-cache.test.ts +198 -0
- package/pi.dev-src/pi-mono/ai/test/openai-completions-thinking-as-text.test.ts +214 -0
- package/pi.dev-src/pi-mono/ai/test/openai-completions-tool-choice.test.ts +560 -0
- package/pi.dev-src/pi-mono/ai/test/openai-completions-tool-result-images.test.ts +101 -0
- package/pi.dev-src/pi-mono/ai/test/openai-responses-cache-affinity-e2e.test.ts +32 -0
- package/pi.dev-src/pi-mono/ai/test/openai-responses-copilot-provider.test.ts +128 -0
- package/pi.dev-src/pi-mono/ai/test/openai-responses-foreign-toolcall-id.test.ts +66 -0
- package/pi.dev-src/pi-mono/ai/test/openai-responses-partial-json-cleanup.test.ts +101 -0
- package/pi.dev-src/pi-mono/ai/test/openai-responses-reasoning-replay-e2e.test.ts +292 -0
- package/pi.dev-src/pi-mono/ai/test/openai-responses-tool-result-images.test.ts +200 -0
- package/pi.dev-src/pi-mono/ai/test/openrouter-cache-write-repro.test.ts +78 -0
- package/pi.dev-src/pi-mono/ai/test/overflow.test.ts +64 -0
- package/pi.dev-src/pi-mono/ai/test/responseid.test.ts +145 -0
- package/pi.dev-src/pi-mono/ai/test/stream.test.ts +1405 -0
- package/pi.dev-src/pi-mono/ai/test/supports-xhigh.test.ts +34 -0
- package/pi.dev-src/pi-mono/ai/test/tokens.test.ts +304 -0
- package/pi.dev-src/pi-mono/ai/test/tool-call-id-normalization.test.ts +291 -0
- package/pi.dev-src/pi-mono/ai/test/tool-call-without-result.test.ts +313 -0
- package/pi.dev-src/pi-mono/ai/test/total-tokens.test.ts +695 -0
- package/pi.dev-src/pi-mono/ai/test/transform-messages-copilot-openai-to-anthropic.test.ts +191 -0
- package/pi.dev-src/pi-mono/ai/test/unicode-surrogate.test.ts +754 -0
- package/pi.dev-src/pi-mono/ai/test/validation.test.ts +116 -0
- package/pi.dev-src/pi-mono/ai/test/xhigh.test.ts +71 -0
- package/pi.dev-src/pi-mono/ai/test/zen.test.ts +25 -0
- package/pi.dev-src/pi-mono/ai/vitest.config.ts +9 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/antigravity-image-gen.ts +418 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/auto-commit-on-exit.ts +49 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/bash-spawn-hook.ts +30 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/bookmark.ts +50 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/built-in-tool-renderer.ts +249 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/claude-rules.ts +86 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/commands.ts +72 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/confirm-destructive.ts +59 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/custom-compaction.ts +127 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/custom-footer.ts +64 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/custom-header.ts +73 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/custom-provider-anthropic/index.ts +604 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +24 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/custom-provider-gitlab-duo/index.ts +349 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/custom-provider-gitlab-duo/test.ts +82 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/custom-provider-qwen-cli/index.ts +345 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/dirty-repo-guard.ts +56 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/doom-overlay/doom/build.sh +152 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +72 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/doom-overlay/doom-component.ts +132 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/doom-overlay/doom-engine.ts +173 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/doom-overlay/doom-keys.ts +104 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/doom-overlay/index.ts +74 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/doom-overlay/wad-finder.ts +51 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/dynamic-resources/SKILL.md +8 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/dynamic-resources/dynamic.json +79 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/dynamic-resources/dynamic.md +5 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/dynamic-resources/index.ts +15 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/dynamic-tools.ts +74 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/event-bus.ts +43 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/file-trigger.ts +41 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/git-checkpoint.ts +53 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/github-issue-autocomplete.ts +185 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/handoff.ts +153 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/hello.ts +26 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/hidden-thinking-label.ts +53 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/inline-bash.ts +94 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/input-transform.ts +43 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/interactive-shell.ts +196 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/mac-system-theme.ts +47 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/message-renderer.ts +59 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/minimal-mode.ts +426 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/modal-editor.ts +85 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/model-status.ts +31 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/notify.ts +55 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/overlay-qa-tests.ts +1348 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/overlay-test.ts +150 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/permission-gate.ts +34 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/pirate.ts +47 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/plan-mode/index.ts +340 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/plan-mode/utils.ts +168 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/preset.ts +430 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/prompt-customizer.ts +97 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/protected-paths.ts +30 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/provider-payload.ts +18 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/qna.ts +122 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/question.ts +264 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/questionnaire.ts +427 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/rainbow-editor.ts +88 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/reload-runtime.ts +37 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/rpc-demo.ts +118 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/sandbox/index.ts +321 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/sandbox/package-lock.json +92 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/send-user-message.ts +97 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/session-name.ts +27 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/shutdown-command.ts +63 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/snake.ts +343 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/space-invaders.ts +560 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/ssh.ts +220 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/status-line.ts +32 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/structured-output.ts +65 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/subagent/agents/planner.md +37 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/subagent/agents/reviewer.md +35 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/subagent/agents/scout.md +50 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/subagent/agents/worker.md +24 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/subagent/agents.ts +126 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/subagent/index.ts +987 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/subagent/prompts/implement-and-review.md +10 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/subagent/prompts/implement.md +10 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/subagent/prompts/scout-and-plan.md +9 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/summarize.ts +206 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/system-prompt-header.ts +17 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/tic-tac-toe.ts +1008 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/timed-confirm.ts +70 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/titlebar-spinner.ts +58 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/todo.ts +297 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/tool-override.ts +144 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/tools.ts +141 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/trigger-compact.ts +50 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/truncated-tool.ts +195 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/widget-placement.ts +9 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/with-deps/index.ts +32 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/with-deps/package-lock.json +31 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/working-indicator.ts +123 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/extensions/working-message-test.ts +25 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/rpc-extension-ui.ts +632 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/sdk/01-minimal.ts +22 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/sdk/02-custom-model.ts +49 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/sdk/03-custom-prompt.ts +62 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/sdk/04-skills.ts +55 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/sdk/05-tools.ts +44 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/sdk/06-extensions.ts +90 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/sdk/07-context-files.ts +42 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/sdk/08-prompt-templates.ts +51 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/sdk/09-api-keys-and-oauth.ts +48 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/sdk/10-settings.ts +53 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/sdk/11-sessions.ts +48 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/sdk/12-full-control.ts +73 -0
- package/pi.dev-src/pi-mono/coding-agent/examples/sdk/13-session-runtime.ts +67 -0
- package/pi.dev-src/pi-mono/coding-agent/scripts/migrate-sessions.sh +93 -0
- package/pi.dev-src/pi-mono/coding-agent/src/bun/cli.ts +6 -0
- package/pi.dev-src/pi-mono/coding-agent/src/bun/register-bedrock.ts +4 -0
- package/pi.dev-src/pi-mono/coding-agent/src/cli/args.ts +335 -0
- package/pi.dev-src/pi-mono/coding-agent/src/cli/config-selector.ts +52 -0
- package/pi.dev-src/pi-mono/coding-agent/src/cli/file-processor.ts +100 -0
- package/pi.dev-src/pi-mono/coding-agent/src/cli/initial-message.ts +43 -0
- package/pi.dev-src/pi-mono/coding-agent/src/cli/list-models.ts +110 -0
- package/pi.dev-src/pi-mono/coding-agent/src/cli/session-picker.ts +52 -0
- package/pi.dev-src/pi-mono/coding-agent/src/cli.ts +17 -0
- package/pi.dev-src/pi-mono/coding-agent/src/config.ts +261 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/agent-session-runtime.ts +394 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/agent-session-services.ts +196 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/agent-session.ts +3096 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/auth-storage.ts +493 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/bash-executor.ts +154 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/compaction/branch-summarization.ts +355 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/compaction/compaction.ts +839 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/compaction/index.ts +7 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/compaction/utils.ts +170 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/defaults.ts +3 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/diagnostics.ts +15 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/event-bus.ts +33 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/exec.ts +107 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/export-html/ansi-to-html.ts +258 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/export-html/index.ts +314 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/export-html/template.css +1017 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/export-html/template.html +55 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/export-html/template.js +1770 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/export-html/tool-renderer.ts +158 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/export-html/vendor/marked.min.js +6 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/extensions/index.ts +171 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/extensions/loader.ts +606 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/extensions/runner.ts +1019 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/extensions/types.ts +1542 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/extensions/wrapper.ts +30 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/footer-data-provider.ts +354 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/index.ts +77 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/keybindings.ts +370 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/messages.ts +195 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/model-registry.ts +858 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/model-resolver.ts +629 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/output-guard.ts +74 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/package-manager.ts +2408 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/prompt-templates.ts +296 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/resolve-config-value.ts +142 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/resource-loader.ts +917 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/sdk.ts +380 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/session-cwd.ts +59 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/session-manager.ts +1425 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/settings-manager.ts +999 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/skills.ts +508 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/slash-commands.ts +39 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/source-info.ts +40 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/system-prompt.ts +172 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/telemetry.ts +13 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/timings.ts +31 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/tools/bash.ts +448 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/tools/edit-diff.ts +445 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/tools/edit.ts +487 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/tools/file-mutation-queue.ts +39 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/tools/find.ts +370 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/tools/grep.ts +384 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/tools/index.ts +196 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/tools/ls.ts +229 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/tools/path-utils.ts +94 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/tools/read.ts +273 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/tools/render-utils.ts +64 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/tools/tool-definition-wrapper.ts +45 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/tools/truncate.ts +265 -0
- package/pi.dev-src/pi-mono/coding-agent/src/core/tools/write.ts +281 -0
- package/pi.dev-src/pi-mono/coding-agent/src/index.ts +352 -0
- package/pi.dev-src/pi-mono/coding-agent/src/main.ts +731 -0
- package/pi.dev-src/pi-mono/coding-agent/src/migrations.ts +314 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/index.ts +9 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/assets/clankolas.png +0 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/armin.ts +382 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/assistant-message.ts +147 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/bash-execution.ts +218 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/bordered-loader.ts +68 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/branch-summary-message.ts +58 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/compaction-summary-message.ts +59 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/config-selector.ts +592 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/countdown-timer.ts +38 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/custom-editor.ts +80 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/custom-message.ts +99 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/daxnuts.ts +164 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/diff.ts +147 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/dynamic-border.ts +25 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/earendil-announcement.ts +53 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/extension-editor.ts +147 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/extension-input.ts +87 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/extension-selector.ts +107 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/footer.ts +220 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/index.ts +32 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/keybinding-hints.ts +24 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/login-dialog.ts +178 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/model-selector.ts +338 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/oauth-selector.ts +121 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/scoped-models-selector.ts +355 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/session-selector-search.ts +194 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/session-selector.ts +1026 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/settings-selector.ts +456 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/show-images-selector.ts +50 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/skill-invocation-message.ts +55 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/theme-selector.ts +67 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/thinking-selector.ts +74 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/tool-execution.ts +352 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/tree-selector.ts +1246 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/user-message-selector.ts +155 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/user-message.ts +35 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/visual-truncate.ts +50 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/interactive-mode.ts +5155 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/theme/dark.json +85 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/theme/light.json +84 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/theme/theme-schema.json +335 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/theme/theme.ts +1141 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/print-mode.ts +158 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/rpc/jsonl.ts +58 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/rpc/rpc-client.ts +515 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/rpc/rpc-mode.ts +745 -0
- package/pi.dev-src/pi-mono/coding-agent/src/modes/rpc/rpc-types.ts +264 -0
- package/pi.dev-src/pi-mono/coding-agent/src/package-manager-cli.ts +270 -0
- package/pi.dev-src/pi-mono/coding-agent/src/utils/changelog.ts +99 -0
- package/pi.dev-src/pi-mono/coding-agent/src/utils/child-process.ts +86 -0
- package/pi.dev-src/pi-mono/coding-agent/src/utils/clipboard-image.ts +300 -0
- package/pi.dev-src/pi-mono/coding-agent/src/utils/clipboard-native.ts +22 -0
- package/pi.dev-src/pi-mono/coding-agent/src/utils/clipboard.ts +81 -0
- package/pi.dev-src/pi-mono/coding-agent/src/utils/exif-orientation.ts +183 -0
- package/pi.dev-src/pi-mono/coding-agent/src/utils/frontmatter.ts +39 -0
- package/pi.dev-src/pi-mono/coding-agent/src/utils/fs-watch.ts +30 -0
- package/pi.dev-src/pi-mono/coding-agent/src/utils/git.ts +192 -0
- package/pi.dev-src/pi-mono/coding-agent/src/utils/image-convert.ts +41 -0
- package/pi.dev-src/pi-mono/coding-agent/src/utils/image-resize.ts +176 -0
- package/pi.dev-src/pi-mono/coding-agent/src/utils/mime.ts +30 -0
- package/pi.dev-src/pi-mono/coding-agent/src/utils/paths.ts +20 -0
- package/pi.dev-src/pi-mono/coding-agent/src/utils/photon.ts +139 -0
- package/pi.dev-src/pi-mono/coding-agent/src/utils/shell.ts +210 -0
- package/pi.dev-src/pi-mono/coding-agent/src/utils/sleep.ts +18 -0
- package/pi.dev-src/pi-mono/coding-agent/src/utils/tools-manager.ts +287 -0
- package/pi.dev-src/pi-mono/coding-agent/test/agent-session-auto-compaction-queue.test.ts +437 -0
- package/pi.dev-src/pi-mono/coding-agent/test/agent-session-branching.test.ts +156 -0
- package/pi.dev-src/pi-mono/coding-agent/test/agent-session-compaction.test.ts +208 -0
- package/pi.dev-src/pi-mono/coding-agent/test/agent-session-concurrent.test.ts +618 -0
- package/pi.dev-src/pi-mono/coding-agent/test/agent-session-dynamic-provider.test.ts +117 -0
- package/pi.dev-src/pi-mono/coding-agent/test/agent-session-dynamic-tools.test.ts +182 -0
- package/pi.dev-src/pi-mono/coding-agent/test/agent-session-retry.test.ts +318 -0
- package/pi.dev-src/pi-mono/coding-agent/test/agent-session-runtime-events.test.ts +208 -0
- package/pi.dev-src/pi-mono/coding-agent/test/agent-session-stats.test.ts +143 -0
- package/pi.dev-src/pi-mono/coding-agent/test/agent-session-tree-navigation.test.ts +323 -0
- package/pi.dev-src/pi-mono/coding-agent/test/args.test.ts +328 -0
- package/pi.dev-src/pi-mono/coding-agent/test/assistant-message.test.ts +57 -0
- package/pi.dev-src/pi-mono/coding-agent/test/auth-storage.test.ts +462 -0
- package/pi.dev-src/pi-mono/coding-agent/test/bash-close-hang-windows.test.ts +126 -0
- package/pi.dev-src/pi-mono/coding-agent/test/bash-execution-width.test.ts +80 -0
- package/pi.dev-src/pi-mono/coding-agent/test/block-images.test.ts +120 -0
- package/pi.dev-src/pi-mono/coding-agent/test/clipboard-image-bmp-conversion.test.ts +88 -0
- package/pi.dev-src/pi-mono/coding-agent/test/clipboard-image.test.ts +136 -0
- package/pi.dev-src/pi-mono/coding-agent/test/compaction-extensions-example.test.ts +66 -0
- package/pi.dev-src/pi-mono/coding-agent/test/compaction-extensions.test.ts +415 -0
- package/pi.dev-src/pi-mono/coding-agent/test/compaction-serialization.test.ts +79 -0
- package/pi.dev-src/pi-mono/coding-agent/test/compaction-summary-reasoning.test.ts +118 -0
- package/pi.dev-src/pi-mono/coding-agent/test/compaction-thinking-model.test.ts +223 -0
- package/pi.dev-src/pi-mono/coding-agent/test/compaction.test.ts +550 -0
- package/pi.dev-src/pi-mono/coding-agent/test/config.test.ts +30 -0
- package/pi.dev-src/pi-mono/coding-agent/test/edit-tool-legacy-input.test.ts +116 -0
- package/pi.dev-src/pi-mono/coding-agent/test/edit-tool-no-full-redraw.test.ts +235 -0
- package/pi.dev-src/pi-mono/coding-agent/test/export-html-whitespace.test.ts +12 -0
- package/pi.dev-src/pi-mono/coding-agent/test/export-html-xss.test.ts +28 -0
- package/pi.dev-src/pi-mono/coding-agent/test/extensions-discovery.test.ts +463 -0
- package/pi.dev-src/pi-mono/coding-agent/test/extensions-input-event.test.ts +112 -0
- package/pi.dev-src/pi-mono/coding-agent/test/extensions-runner.test.ts +805 -0
- package/pi.dev-src/pi-mono/coding-agent/test/file-mutation-queue.test.ts +163 -0
- package/pi.dev-src/pi-mono/coding-agent/test/fixtures/assistant-message-with-thinking-code.json +33 -0
- package/pi.dev-src/pi-mono/coding-agent/test/fixtures/before-compaction.jsonl +1003 -0
- package/pi.dev-src/pi-mono/coding-agent/test/fixtures/empty-agent/.gitkeep +0 -0
- package/pi.dev-src/pi-mono/coding-agent/test/fixtures/empty-cwd/.gitkeep +0 -0
- package/pi.dev-src/pi-mono/coding-agent/test/fixtures/large-session.jsonl +1019 -0
- package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/consecutive-hyphens/SKILL.md +8 -0
- package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/disable-model-invocation/SKILL.md +9 -0
- package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/invalid-name-chars/SKILL.md +8 -0
- package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/invalid-yaml/SKILL.md +8 -0
- package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/long-name/SKILL.md +8 -0
- package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/missing-description/SKILL.md +7 -0
- package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/multiline-description/SKILL.md +11 -0
- package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/name-mismatch/SKILL.md +8 -0
- package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/nested/child-skill/SKILL.md +8 -0
- package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/no-frontmatter/SKILL.md +3 -0
- package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/root-skill-preferred/SKILL.md +3 -0
- package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/root-skill-preferred/nested-child/SKILL.md +3 -0
- package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/unknown-field/SKILL.md +10 -0
- package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/valid-skill/SKILL.md +8 -0
- package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills-collision/first/calendar/SKILL.md +8 -0
- package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills-collision/second/calendar/SKILL.md +8 -0
- package/pi.dev-src/pi-mono/coding-agent/test/footer-data-provider.test.ts +265 -0
- package/pi.dev-src/pi-mono/coding-agent/test/footer-width.test.ts +115 -0
- package/pi.dev-src/pi-mono/coding-agent/test/frontmatter.test.ts +60 -0
- package/pi.dev-src/pi-mono/coding-agent/test/git-ssh-url.test.ts +78 -0
- package/pi.dev-src/pi-mono/coding-agent/test/git-update.test.ts +411 -0
- package/pi.dev-src/pi-mono/coding-agent/test/image-processing.test.ts +145 -0
- package/pi.dev-src/pi-mono/coding-agent/test/image-resize-callers.test.ts +53 -0
- package/pi.dev-src/pi-mono/coding-agent/test/initial-message.test.ts +48 -0
- package/pi.dev-src/pi-mono/coding-agent/test/interactive-mode-anthropic-warning.test.ts +73 -0
- package/pi.dev-src/pi-mono/coding-agent/test/interactive-mode-clone-command.test.ts +77 -0
- package/pi.dev-src/pi-mono/coding-agent/test/interactive-mode-compaction.test.ts +57 -0
- package/pi.dev-src/pi-mono/coding-agent/test/interactive-mode-import-command.test.ts +144 -0
- package/pi.dev-src/pi-mono/coding-agent/test/interactive-mode-status.test.ts +635 -0
- package/pi.dev-src/pi-mono/coding-agent/test/interactive-mode-suspend.test.ts +149 -0
- package/pi.dev-src/pi-mono/coding-agent/test/keybindings-migration.test.ts +88 -0
- package/pi.dev-src/pi-mono/coding-agent/test/model-registry.test.ts +1209 -0
- package/pi.dev-src/pi-mono/coding-agent/test/model-resolver.test.ts +436 -0
- package/pi.dev-src/pi-mono/coding-agent/test/package-command-paths.test.ts +144 -0
- package/pi.dev-src/pi-mono/coding-agent/test/package-manager-ssh.test.ts +97 -0
- package/pi.dev-src/pi-mono/coding-agent/test/package-manager.test.ts +1795 -0
- package/pi.dev-src/pi-mono/coding-agent/test/path-utils.test.ts +162 -0
- package/pi.dev-src/pi-mono/coding-agent/test/plan-mode-utils.test.ts +261 -0
- package/pi.dev-src/pi-mono/coding-agent/test/print-mode.test.ts +140 -0
- package/pi.dev-src/pi-mono/coding-agent/test/prompt-templates.test.ts +515 -0
- package/pi.dev-src/pi-mono/coding-agent/test/resource-loader.test.ts +576 -0
- package/pi.dev-src/pi-mono/coding-agent/test/rpc-client-clone.test.ts +29 -0
- package/pi.dev-src/pi-mono/coding-agent/test/rpc-example.ts +86 -0
- package/pi.dev-src/pi-mono/coding-agent/test/rpc-jsonl.test.ts +65 -0
- package/pi.dev-src/pi-mono/coding-agent/test/rpc-prompt-response-semantics.test.ts +285 -0
- package/pi.dev-src/pi-mono/coding-agent/test/rpc.test.ts +321 -0
- package/pi.dev-src/pi-mono/coding-agent/test/sdk-codex-cache-probe-tool-loop.ts +364 -0
- package/pi.dev-src/pi-mono/coding-agent/test/sdk-openrouter-attribution.test.ts +181 -0
- package/pi.dev-src/pi-mono/coding-agent/test/sdk-session-manager.test.ts +95 -0
- package/pi.dev-src/pi-mono/coding-agent/test/sdk-skills.test.ts +109 -0
- package/pi.dev-src/pi-mono/coding-agent/test/session-cwd.test.ts +91 -0
- package/pi.dev-src/pi-mono/coding-agent/test/session-info-modified-timestamp.test.ts +83 -0
- package/pi.dev-src/pi-mono/coding-agent/test/session-manager/build-context.test.ts +268 -0
- package/pi.dev-src/pi-mono/coding-agent/test/session-manager/custom-session-id.test.ts +109 -0
- package/pi.dev-src/pi-mono/coding-agent/test/session-manager/file-operations.test.ts +208 -0
- package/pi.dev-src/pi-mono/coding-agent/test/session-manager/labels.test.ts +198 -0
- package/pi.dev-src/pi-mono/coding-agent/test/session-manager/migration.test.ts +78 -0
- package/pi.dev-src/pi-mono/coding-agent/test/session-manager/save-entry.test.ts +55 -0
- package/pi.dev-src/pi-mono/coding-agent/test/session-manager/tree-traversal.test.ts +533 -0
- package/pi.dev-src/pi-mono/coding-agent/test/session-selector-path-delete.test.ts +315 -0
- package/pi.dev-src/pi-mono/coding-agent/test/session-selector-rename.test.ts +111 -0
- package/pi.dev-src/pi-mono/coding-agent/test/session-selector-search.test.ts +195 -0
- package/pi.dev-src/pi-mono/coding-agent/test/settings-manager-bug.test.ts +147 -0
- package/pi.dev-src/pi-mono/coding-agent/test/settings-manager.test.ts +319 -0
- package/pi.dev-src/pi-mono/coding-agent/test/skills.test.ts +432 -0
- package/pi.dev-src/pi-mono/coding-agent/test/stdout-cleanliness.test.ts +103 -0
- package/pi.dev-src/pi-mono/coding-agent/test/streaming-render-debug.ts +97 -0
- package/pi.dev-src/pi-mono/coding-agent/test/suite/agent-session-bash-persistence.test.ts +242 -0
- package/pi.dev-src/pi-mono/coding-agent/test/suite/agent-session-compaction.test.ts +343 -0
- package/pi.dev-src/pi-mono/coding-agent/test/suite/agent-session-model-extension.test.ts +325 -0
- package/pi.dev-src/pi-mono/coding-agent/test/suite/agent-session-prompt.test.ts +323 -0
- package/pi.dev-src/pi-mono/coding-agent/test/suite/agent-session-queue.test.ts +422 -0
- package/pi.dev-src/pi-mono/coding-agent/test/suite/agent-session-retry-events.test.ts +358 -0
- package/pi.dev-src/pi-mono/coding-agent/test/suite/agent-session-runtime.test.ts +553 -0
- package/pi.dev-src/pi-mono/coding-agent/test/suite/harness.ts +208 -0
- package/pi.dev-src/pi-mono/coding-agent/test/suite/regressions/2023-queued-slash-command-followup.test.ts +80 -0
- package/pi.dev-src/pi-mono/coding-agent/test/suite/regressions/2753-reload-stale-resource-settings.test.ts +100 -0
- package/pi.dev-src/pi-mono/coding-agent/test/suite/regressions/2781-skill-collision-precedence.test.ts +120 -0
- package/pi.dev-src/pi-mono/coding-agent/test/suite/regressions/2791-fswatch-error-crash.test.ts +106 -0
- package/pi.dev-src/pi-mono/coding-agent/test/suite/regressions/2835-tools-allowlist-filters-extension-tools.test.ts +94 -0
- package/pi.dev-src/pi-mono/coding-agent/test/suite/regressions/2860-replaced-session-context.test.ts +274 -0
- package/pi.dev-src/pi-mono/coding-agent/test/suite/regressions/3217-scoped-model-order.test.ts +104 -0
- package/pi.dev-src/pi-mono/coding-agent/test/suite/regressions/3302-find-path-glob.test.ts +72 -0
- package/pi.dev-src/pi-mono/coding-agent/test/suite/regressions/3303-find-nested-gitignore.test.ts +83 -0
- package/pi.dev-src/pi-mono/coding-agent/test/suite/regressions/3317-network-connection-lost-retry.test.ts +33 -0
- package/pi.dev-src/pi-mono/coding-agent/test/system-prompt.test.ts +102 -0
- package/pi.dev-src/pi-mono/coding-agent/test/test-harness.test.ts +321 -0
- package/pi.dev-src/pi-mono/coding-agent/test/test-harness.ts +446 -0
- package/pi.dev-src/pi-mono/coding-agent/test/test-theme-colors.ts +246 -0
- package/pi.dev-src/pi-mono/coding-agent/test/theme-export.test.ts +104 -0
- package/pi.dev-src/pi-mono/coding-agent/test/tool-execution-component.test.ts +331 -0
- package/pi.dev-src/pi-mono/coding-agent/test/tools.test.ts +889 -0
- package/pi.dev-src/pi-mono/coding-agent/test/tree-selector.test.ts +655 -0
- package/pi.dev-src/pi-mono/coding-agent/test/trigger-compact-extension.test.ts +57 -0
- package/pi.dev-src/pi-mono/coding-agent/test/truncate-to-width.test.ts +81 -0
- package/pi.dev-src/pi-mono/coding-agent/test/user-message.test.ts +25 -0
- package/pi.dev-src/pi-mono/coding-agent/test/utilities.ts +316 -0
- package/pi.dev-src/pi-mono/coding-agent/tsconfig.examples.json +16 -0
- package/pi.dev-src/pi-mono/coding-agent/vitest.config.ts +26 -0
- package/pi.dev-src/pi-mono/mom/dev.sh +30 -0
- package/pi.dev-src/pi-mono/mom/docker.sh +95 -0
- package/pi.dev-src/pi-mono/mom/scripts/migrate-timestamps.ts +121 -0
- package/pi.dev-src/pi-mono/mom/src/agent.ts +882 -0
- package/pi.dev-src/pi-mono/mom/src/context.ts +180 -0
- package/pi.dev-src/pi-mono/mom/src/download.ts +117 -0
- package/pi.dev-src/pi-mono/mom/src/events.ts +442 -0
- package/pi.dev-src/pi-mono/mom/src/fs-watch.ts +30 -0
- package/pi.dev-src/pi-mono/mom/src/log.ts +271 -0
- package/pi.dev-src/pi-mono/mom/src/main.ts +367 -0
- package/pi.dev-src/pi-mono/mom/src/sandbox.ts +221 -0
- package/pi.dev-src/pi-mono/mom/src/slack.ts +623 -0
- package/pi.dev-src/pi-mono/mom/src/store.ts +234 -0
- package/pi.dev-src/pi-mono/mom/src/tools/attach.ts +47 -0
- package/pi.dev-src/pi-mono/mom/src/tools/bash.ts +97 -0
- package/pi.dev-src/pi-mono/mom/src/tools/edit.ts +165 -0
- package/pi.dev-src/pi-mono/mom/src/tools/index.ts +19 -0
- package/pi.dev-src/pi-mono/mom/src/tools/read.ts +159 -0
- package/pi.dev-src/pi-mono/mom/src/tools/truncate.ts +236 -0
- package/pi.dev-src/pi-mono/mom/src/tools/write.ts +45 -0
- package/pi.dev-src/pi-mono/mom/test/events.test.ts +50 -0
- package/pi.dev-src/pi-mono/pods/scripts/model_run.sh +83 -0
- package/pi.dev-src/pi-mono/pods/scripts/pod_setup.sh +336 -0
- package/pi.dev-src/pi-mono/pods/src/cli.ts +360 -0
- package/pi.dev-src/pi-mono/pods/src/commands/models.ts +753 -0
- package/pi.dev-src/pi-mono/pods/src/commands/pods.ts +205 -0
- package/pi.dev-src/pi-mono/pods/src/commands/prompt.ts +84 -0
- package/pi.dev-src/pi-mono/pods/src/config.ts +80 -0
- package/pi.dev-src/pi-mono/pods/src/index.ts +2 -0
- package/pi.dev-src/pi-mono/pods/src/model-configs.ts +111 -0
- package/pi.dev-src/pi-mono/pods/src/models.json +295 -0
- package/pi.dev-src/pi-mono/pods/src/ssh.ts +151 -0
- package/pi.dev-src/pi-mono/pods/src/types.ts +27 -0
- package/pi.dev-src/pi-mono/tui/src/autocomplete.ts +783 -0
- package/pi.dev-src/pi-mono/tui/src/components/box.ts +137 -0
- package/pi.dev-src/pi-mono/tui/src/components/cancellable-loader.ts +40 -0
- package/pi.dev-src/pi-mono/tui/src/components/editor.ts +2280 -0
- package/pi.dev-src/pi-mono/tui/src/components/image.ts +104 -0
- package/pi.dev-src/pi-mono/tui/src/components/input.ts +503 -0
- package/pi.dev-src/pi-mono/tui/src/components/loader.ts +86 -0
- package/pi.dev-src/pi-mono/tui/src/components/markdown.ts +852 -0
- package/pi.dev-src/pi-mono/tui/src/components/select-list.ts +229 -0
- package/pi.dev-src/pi-mono/tui/src/components/settings-list.ts +250 -0
- package/pi.dev-src/pi-mono/tui/src/components/spacer.ts +28 -0
- package/pi.dev-src/pi-mono/tui/src/components/text.ts +106 -0
- package/pi.dev-src/pi-mono/tui/src/components/truncated-text.ts +65 -0
- package/pi.dev-src/pi-mono/tui/src/editor-component.ts +74 -0
- package/pi.dev-src/pi-mono/tui/src/fuzzy.ts +133 -0
- package/pi.dev-src/pi-mono/tui/src/index.ts +106 -0
- package/pi.dev-src/pi-mono/tui/src/keybindings.ts +244 -0
- package/pi.dev-src/pi-mono/tui/src/keys.ts +1400 -0
- package/pi.dev-src/pi-mono/tui/src/kill-ring.ts +46 -0
- package/pi.dev-src/pi-mono/tui/src/stdin-buffer.ts +386 -0
- package/pi.dev-src/pi-mono/tui/src/terminal-image.ts +414 -0
- package/pi.dev-src/pi-mono/tui/src/terminal.ts +373 -0
- package/pi.dev-src/pi-mono/tui/src/tui.ts +1243 -0
- package/pi.dev-src/pi-mono/tui/src/undo-stack.ts +28 -0
- package/pi.dev-src/pi-mono/tui/src/utils.ts +1085 -0
- package/pi.dev-src/pi-mono/tui/test/autocomplete.test.ts +542 -0
- package/pi.dev-src/pi-mono/tui/test/bug-regression-isimageline-startswith-bug.test.ts +237 -0
- package/pi.dev-src/pi-mono/tui/test/chat-simple.ts +129 -0
- package/pi.dev-src/pi-mono/tui/test/editor.test.ts +3812 -0
- package/pi.dev-src/pi-mono/tui/test/fuzzy.test.ts +98 -0
- package/pi.dev-src/pi-mono/tui/test/image-test.ts +56 -0
- package/pi.dev-src/pi-mono/tui/test/input.test.ts +580 -0
- package/pi.dev-src/pi-mono/tui/test/key-tester.ts +105 -0
- package/pi.dev-src/pi-mono/tui/test/keybindings.test.ts +38 -0
- package/pi.dev-src/pi-mono/tui/test/keys.test.ts +614 -0
- package/pi.dev-src/pi-mono/tui/test/markdown.test.ts +1236 -0
- package/pi.dev-src/pi-mono/tui/test/overlay-non-capturing.test.ts +609 -0
- package/pi.dev-src/pi-mono/tui/test/overlay-options.test.ts +538 -0
- package/pi.dev-src/pi-mono/tui/test/overlay-short-content.test.ts +56 -0
- package/pi.dev-src/pi-mono/tui/test/regression-regional-indicator-width.test.ts +52 -0
- package/pi.dev-src/pi-mono/tui/test/select-list.test.ts +116 -0
- package/pi.dev-src/pi-mono/tui/test/stdin-buffer.test.ts +422 -0
- package/pi.dev-src/pi-mono/tui/test/terminal-image.test.ts +279 -0
- package/pi.dev-src/pi-mono/tui/test/test-themes.ts +38 -0
- package/pi.dev-src/pi-mono/tui/test/truncate-to-width.test.ts +62 -0
- package/pi.dev-src/pi-mono/tui/test/truncated-text.test.ts +129 -0
- package/pi.dev-src/pi-mono/tui/test/tui-cell-size-input.test.ts +81 -0
- package/pi.dev-src/pi-mono/tui/test/tui-overlay-style-leak.test.ts +72 -0
- package/pi.dev-src/pi-mono/tui/test/tui-render.test.ts +509 -0
- package/pi.dev-src/pi-mono/tui/test/viewport-overwrite-repro.ts +106 -0
- package/pi.dev-src/pi-mono/tui/test/virtual-terminal.ts +218 -0
- package/pi.dev-src/pi-mono/tui/test/wrap-ansi.test.ts +207 -0
- package/pi.dev-src/pi-mono/tui/vitest.config.ts +7 -0
- package/pi.dev-src/pi-mono/web-ui/example/index.html +13 -0
- package/pi.dev-src/pi-mono/web-ui/example/src/app.css +1 -0
- package/pi.dev-src/pi-mono/web-ui/example/src/custom-messages.ts +99 -0
- package/pi.dev-src/pi-mono/web-ui/example/src/main.ts +421 -0
- package/pi.dev-src/pi-mono/web-ui/example/tsconfig.json +23 -0
- package/pi.dev-src/pi-mono/web-ui/example/vite.config.ts +6 -0
- package/pi.dev-src/pi-mono/web-ui/scripts/count-prompt-tokens.ts +88 -0
- package/pi.dev-src/pi-mono/web-ui/src/ChatPanel.ts +209 -0
- package/pi.dev-src/pi-mono/web-ui/src/app.css +68 -0
- package/pi.dev-src/pi-mono/web-ui/src/components/AgentInterface.ts +403 -0
- package/pi.dev-src/pi-mono/web-ui/src/components/AttachmentTile.ts +107 -0
- package/pi.dev-src/pi-mono/web-ui/src/components/ConsoleBlock.ts +72 -0
- package/pi.dev-src/pi-mono/web-ui/src/components/CustomProviderCard.ts +100 -0
- package/pi.dev-src/pi-mono/web-ui/src/components/ExpandableSection.ts +46 -0
- package/pi.dev-src/pi-mono/web-ui/src/components/Input.ts +113 -0
- package/pi.dev-src/pi-mono/web-ui/src/components/MessageEditor.ts +405 -0
- package/pi.dev-src/pi-mono/web-ui/src/components/MessageList.ts +98 -0
- package/pi.dev-src/pi-mono/web-ui/src/components/Messages.ts +383 -0
- package/pi.dev-src/pi-mono/web-ui/src/components/ProviderKeyInput.ts +153 -0
- package/pi.dev-src/pi-mono/web-ui/src/components/SandboxedIframe.ts +626 -0
- package/pi.dev-src/pi-mono/web-ui/src/components/StreamingMessageContainer.ts +103 -0
- package/pi.dev-src/pi-mono/web-ui/src/components/ThinkingBlock.ts +43 -0
- package/pi.dev-src/pi-mono/web-ui/src/components/message-renderer-registry.ts +28 -0
- package/pi.dev-src/pi-mono/web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts +220 -0
- package/pi.dev-src/pi-mono/web-ui/src/components/sandbox/AttachmentsRuntimeProvider.ts +66 -0
- package/pi.dev-src/pi-mono/web-ui/src/components/sandbox/ConsoleRuntimeProvider.ts +186 -0
- package/pi.dev-src/pi-mono/web-ui/src/components/sandbox/FileDownloadRuntimeProvider.ts +110 -0
- package/pi.dev-src/pi-mono/web-ui/src/components/sandbox/RuntimeMessageBridge.ts +82 -0
- package/pi.dev-src/pi-mono/web-ui/src/components/sandbox/RuntimeMessageRouter.ts +216 -0
- package/pi.dev-src/pi-mono/web-ui/src/components/sandbox/SandboxRuntimeProvider.ts +52 -0
- package/pi.dev-src/pi-mono/web-ui/src/dialogs/ApiKeyPromptDialog.ts +75 -0
- package/pi.dev-src/pi-mono/web-ui/src/dialogs/AttachmentOverlay.ts +636 -0
- package/pi.dev-src/pi-mono/web-ui/src/dialogs/CustomProviderDialog.ts +274 -0
- package/pi.dev-src/pi-mono/web-ui/src/dialogs/ModelSelector.ts +370 -0
- package/pi.dev-src/pi-mono/web-ui/src/dialogs/PersistentStorageDialog.ts +144 -0
- package/pi.dev-src/pi-mono/web-ui/src/dialogs/ProvidersModelsTab.ts +212 -0
- package/pi.dev-src/pi-mono/web-ui/src/dialogs/SessionListDialog.ts +150 -0
- package/pi.dev-src/pi-mono/web-ui/src/dialogs/SettingsDialog.ts +218 -0
- package/pi.dev-src/pi-mono/web-ui/src/index.ts +120 -0
- package/pi.dev-src/pi-mono/web-ui/src/prompts/prompts.ts +282 -0
- package/pi.dev-src/pi-mono/web-ui/src/storage/app-storage.ts +60 -0
- package/pi.dev-src/pi-mono/web-ui/src/storage/backends/indexeddb-storage-backend.ts +193 -0
- package/pi.dev-src/pi-mono/web-ui/src/storage/store.ts +33 -0
- package/pi.dev-src/pi-mono/web-ui/src/storage/stores/custom-providers-store.ts +62 -0
- package/pi.dev-src/pi-mono/web-ui/src/storage/stores/provider-keys-store.ts +33 -0
- package/pi.dev-src/pi-mono/web-ui/src/storage/stores/sessions-store.ts +136 -0
- package/pi.dev-src/pi-mono/web-ui/src/storage/stores/settings-store.ts +34 -0
- package/pi.dev-src/pi-mono/web-ui/src/storage/types.ts +206 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/ArtifactElement.ts +14 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/ArtifactPill.ts +26 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/Console.ts +93 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/DocxArtifact.ts +213 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/ExcelArtifact.ts +231 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/GenericArtifact.ts +117 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/HtmlArtifact.ts +195 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/ImageArtifact.ts +116 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/MarkdownArtifact.ts +82 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/PdfArtifact.ts +201 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/SvgArtifact.ts +119 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/TextArtifact.ts +148 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/artifacts-tool-renderer.ts +310 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/artifacts.ts +713 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/index.ts +7 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/extract-document.ts +275 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/index.ts +46 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/javascript-repl.ts +293 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/renderer-registry.ts +130 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/renderers/BashRenderer.ts +52 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/renderers/CalculateRenderer.ts +58 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/renderers/DefaultRenderer.ts +103 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/renderers/GetCurrentTimeRenderer.ts +92 -0
- package/pi.dev-src/pi-mono/web-ui/src/tools/types.ts +15 -0
- package/pi.dev-src/pi-mono/web-ui/src/utils/attachment-utils.ts +472 -0
- package/pi.dev-src/pi-mono/web-ui/src/utils/auth-token.ts +22 -0
- package/pi.dev-src/pi-mono/web-ui/src/utils/format.ts +42 -0
- package/pi.dev-src/pi-mono/web-ui/src/utils/i18n.ts +653 -0
- package/pi.dev-src/pi-mono/web-ui/src/utils/model-discovery.ts +277 -0
- package/pi.dev-src/pi-mono/web-ui/src/utils/proxy-utils.ts +139 -0
- package/pi.dev-src/pi-mono/web-ui/src/utils/test-sessions.ts +2357 -0
- package/pi.dev-src/pi-mono/web-ui/tsconfig.json +7 -0
- package/scripts/lib/extension-debug-harness.ts +3 -10
- package/scripts/lib/recording-extension-api.ts +71 -2
- package/scripts/pi-usereq-debug.sh +1 -1
- package/scripts/tool-args-to-params.ts +4 -15
- package/src/cli.ts +24 -68
- package/src/core/agent-tool-json.ts +53 -239
- package/src/core/compress-payload.ts +4 -74
- package/src/core/compress.ts +26 -15
- package/src/core/config.ts +468 -42
- package/src/core/debug-runtime.ts +542 -0
- package/src/core/extension-status.ts +296 -46
- package/src/core/find-constructs.ts +3 -3
- package/src/core/find-payload.ts +26 -132
- package/src/core/path-context.ts +346 -81
- package/src/core/pi-notify.ts +115 -5
- package/src/core/pi-usereq-tools.ts +57 -19
- package/src/core/prompt-command-catalog.ts +46 -0
- package/src/core/prompt-command-runtime.ts +1650 -0
- package/src/core/prompt-command-state.ts +211 -0
- package/src/core/prompts.ts +173 -41
- package/src/core/reference-payload.ts +5 -63
- package/src/core/resources.ts +61 -5
- package/src/core/settings-menu.ts +12 -6
- package/src/core/source-analyzer.ts +27 -7
- package/src/core/token-counter.ts +6 -76
- package/src/core/tool-runner.ts +38 -317
- package/src/index.ts +2062 -950
- package/src/resources/instructions/git_commit.md +16 -0
- package/src/resources/instructions/git_read-only.md +2 -0
- package/src/resources/prompts/analyze.md +12 -13
- package/src/resources/prompts/change.md +21 -59
- package/src/resources/prompts/check.md +14 -16
- package/src/resources/prompts/cover.md +21 -59
- package/src/resources/prompts/create.md +5 -4
- package/src/resources/prompts/fix.md +20 -58
- package/src/resources/prompts/flowchart.md +14 -49
- package/src/resources/prompts/implement.md +14 -50
- package/src/resources/prompts/new.md +20 -58
- package/src/resources/prompts/readme.md +14 -50
- package/src/resources/prompts/recreate.md +14 -45
- package/src/resources/prompts/refactor.md +20 -58
- package/src/resources/prompts/references.md +7 -43
- package/src/resources/prompts/renumber.md +8 -39
- package/src/resources/prompts/workflow.md +14 -50
- package/src/resources/prompts/write.md +5 -4
- package/tests/attended-results-scenarios.ts +30 -136
- package/tests/cli-command-option-parity.test.ts +148 -192
- package/tests/debug-extension-harness.test.ts +69 -68
- package/tests/extension-registration.test.ts +3118 -657
- package/tests/fixtures_attended_results/project/compress-line-numbers.json +1 -1
- package/tests/fixtures_attended_results/project/compress.json +1 -1
- package/tests/fixtures_attended_results/project/find-line-numbers.json +1 -1
- package/tests/fixtures_attended_results/project/find.json +1 -1
- package/tests/fixtures_attended_results/project/references.json +2 -2
- package/tests/fixtures_attended_results/standalone/files-compress/fixture_go.go.json +1 -1
- package/tests/fixtures_attended_results/standalone/files-compress-line-numbers/fixture_go.go.json +1 -1
- package/tests/fixtures_attended_results/standalone/files-find/fixture_go.go.json +1 -1
- package/tests/fixtures_attended_results/standalone/files-find-line-numbers/fixture_go.go.json +1 -1
- package/tests/fixtures_attended_results/standalone/files-references/fixture_c.c.json +1 -1
- package/tests/fixtures_attended_results/standalone/files-references/fixture_cpp.cpp.json +1 -1
- package/tests/fixtures_attended_results/standalone/files-references/fixture_csharp.cs.json +1 -1
- package/tests/fixtures_attended_results/standalone/files-references/fixture_elixir.ex.json +1 -1
- package/tests/fixtures_attended_results/standalone/files-references/fixture_go.go.json +1 -1
- package/tests/fixtures_attended_results/standalone/files-references/fixture_haskell.hs.json +1 -1
- package/tests/fixtures_attended_results/standalone/files-references/fixture_java.java.json +1 -1
- package/tests/fixtures_attended_results/standalone/files-references/fixture_javascript.js.json +1 -1
- package/tests/fixtures_attended_results/standalone/files-references/fixture_kotlin.kt.json +1 -1
- package/tests/fixtures_attended_results/standalone/files-references/fixture_lua.lua.json +1 -1
- package/tests/fixtures_attended_results/standalone/files-references/fixture_perl.pl.json +1 -1
- package/tests/fixtures_attended_results/standalone/files-references/fixture_php.php.json +1 -1
- package/tests/fixtures_attended_results/standalone/files-references/fixture_python.py.json +1 -1
- package/tests/fixtures_attended_results/standalone/files-references/fixture_ruby.rb.json +1 -1
- package/tests/fixtures_attended_results/standalone/files-references/fixture_rust.rs.json +1 -1
- package/tests/fixtures_attended_results/standalone/files-references/fixture_scala.scala.json +1 -1
- package/tests/fixtures_attended_results/standalone/files-references/fixture_shell.sh.json +1 -1
- package/tests/fixtures_attended_results/standalone/files-references/fixture_swift.swift.json +1 -1
- package/tests/fixtures_attended_results/standalone/files-references/fixture_typescript.ts.json +1 -1
- package/tests/fixtures_attended_results/standalone/files-references/fixture_zig.zig.json +1 -1
- package/tests/helpers.ts +24 -15
- package/tests/oracle-project.test.ts +29 -30
- package/tests/oracle-standalone.test.ts +27 -1
- package/tests/prompt-rendering.test.ts +40 -9
- package/tests/python_oracle_stubs/tiktoken.py +76 -0
|
@@ -6,10 +6,19 @@ import os from "node:os";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import piUsereqExtension from "../src/index.ts";
|
|
8
8
|
import {
|
|
9
|
+
createStaticCheckLanguageConfig,
|
|
9
10
|
DEFAULT_DOCS_DIR,
|
|
10
11
|
getDefaultConfig,
|
|
11
12
|
getProjectConfigPath,
|
|
13
|
+
loadConfig,
|
|
12
14
|
} from "../src/core/config.js";
|
|
15
|
+
import {
|
|
16
|
+
DEBUG_PROMPT_NAMES,
|
|
17
|
+
DEFAULT_DEBUG_LOG_FILE,
|
|
18
|
+
DEFAULT_DEBUG_LOG_ON_STATUS,
|
|
19
|
+
DEFAULT_DEBUG_WORKFLOW_EVENTS,
|
|
20
|
+
} from "../src/core/debug-runtime.js";
|
|
21
|
+
import { PROMPT_COMMAND_NAMES } from "../src/core/prompt-command-catalog.js";
|
|
13
22
|
import {
|
|
14
23
|
setJsTiktokenModuleLoaderForTests,
|
|
15
24
|
} from "../src/core/token-counter.js";
|
|
@@ -18,10 +27,27 @@ import {
|
|
|
18
27
|
setPiNotifySpawnForTests,
|
|
19
28
|
} from "../src/core/pi-notify.js";
|
|
20
29
|
import {
|
|
30
|
+
abortPromptCommandExecution,
|
|
31
|
+
activatePromptCommandExecution,
|
|
32
|
+
preparePromptCommandExecution,
|
|
33
|
+
restorePromptCommandExecution,
|
|
34
|
+
setPromptCommandPostCreateHookForTests,
|
|
35
|
+
} from "../src/core/prompt-command-runtime.js";
|
|
36
|
+
import {
|
|
37
|
+
readPersistedPromptCommandSessionContext,
|
|
38
|
+
} from "../src/core/prompt-command-state.js";
|
|
39
|
+
import {
|
|
40
|
+
comparePiUsereqStartupToolNames,
|
|
41
|
+
PI_USEREQ_CUSTOM_TOOL_NAMES,
|
|
21
42
|
PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES,
|
|
22
43
|
PI_USEREQ_EMBEDDED_TOOL_NAMES,
|
|
23
44
|
PI_USEREQ_STARTUP_TOOL_NAMES,
|
|
24
45
|
} from "../src/core/pi-usereq-tools.js";
|
|
46
|
+
import {
|
|
47
|
+
createPiUsereqStatusController,
|
|
48
|
+
setPiUsereqStatusConfig,
|
|
49
|
+
setPiUsereqWorkflowState,
|
|
50
|
+
} from "../src/core/extension-status.js";
|
|
25
51
|
import { getSupportedStaticCheckLanguageSupport } from "../src/core/static-check.js";
|
|
26
52
|
import { createTempDir, initFixtureRepo } from "./helpers.js";
|
|
27
53
|
|
|
@@ -44,6 +70,7 @@ type RegisteredTool = {
|
|
|
44
70
|
parameters?: unknown;
|
|
45
71
|
sourceInfo?: Record<string, unknown>;
|
|
46
72
|
execute?: (toolCallId: string, params: any, signal?: AbortSignal, onUpdate?: unknown, ctx?: any) => Promise<any> | any;
|
|
73
|
+
renderResult?: (result: any, options: { expanded?: boolean; isPartial?: boolean }, theme: FakeThemeAdapter, context: { args?: Record<string, unknown>; lastComponent?: unknown }) => { render: (width: number) => string[] };
|
|
47
74
|
};
|
|
48
75
|
|
|
49
76
|
/**
|
|
@@ -54,6 +81,39 @@ type RegisteredTool = {
|
|
|
54
81
|
*/
|
|
55
82
|
type RegisteredShortcut = { description?: string; handler: (ctx: any) => Promise<void> | void };
|
|
56
83
|
|
|
84
|
+
/**
|
|
85
|
+
* @brief Applies targeted `.pi-usereq.json` overrides inside one test project.
|
|
86
|
+
* @details Loads the persisted config JSON, merges the supplied top-level overrides, and writes the updated payload back with a trailing newline so prompt-command tests can script worktree and notification behavior deterministically. Runtime is O(n) in config size. Side effects include filesystem reads and file overwrite.
|
|
87
|
+
* @param[in] projectBase {string} Fixture project root.
|
|
88
|
+
* @param[in] overrides {Record<string, unknown>} Top-level config overrides.
|
|
89
|
+
* @return {void} No return value.
|
|
90
|
+
*/
|
|
91
|
+
function writeProjectConfigOverrides(projectBase: string, overrides: Record<string, unknown>): void {
|
|
92
|
+
const configPath = getProjectConfigPath(projectBase);
|
|
93
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8")) as Record<string, unknown>;
|
|
94
|
+
Object.assign(config, overrides);
|
|
95
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
96
|
+
if (fs.existsSync(path.join(projectBase, ".git"))) {
|
|
97
|
+
const gitEnv = {
|
|
98
|
+
...process.env,
|
|
99
|
+
GIT_AUTHOR_NAME: "pi-usereq",
|
|
100
|
+
GIT_AUTHOR_EMAIL: "pi-usereq@example.com",
|
|
101
|
+
GIT_COMMITTER_NAME: "pi-usereq",
|
|
102
|
+
GIT_COMMITTER_EMAIL: "pi-usereq@example.com",
|
|
103
|
+
};
|
|
104
|
+
assert.equal(spawnSync("git", ["add", ".pi-usereq.json", ".req/config.json"], {
|
|
105
|
+
cwd: projectBase,
|
|
106
|
+
encoding: "utf8",
|
|
107
|
+
}).status, 0);
|
|
108
|
+
const commit = spawnSync("git", ["commit", "-m", "config override"], {
|
|
109
|
+
cwd: projectBase,
|
|
110
|
+
encoding: "utf8",
|
|
111
|
+
env: gitEnv,
|
|
112
|
+
});
|
|
113
|
+
assert.equal(commit.status, 0, commit.stderr);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
57
117
|
/**
|
|
58
118
|
* @brief Represents the fake pi runtime returned by `createFakePi`.
|
|
59
119
|
* @details Captures registered commands, tools, shortcuts, event handlers, active-tool state, and prompt-delivery payloads for deterministic extension-registration assertions. The alias is compile-time only and introduces no runtime side effects.
|
|
@@ -83,6 +143,9 @@ interface FakeSessionManager {
|
|
|
83
143
|
appendMessage: (message: unknown) => string;
|
|
84
144
|
getBranch: () => FakeSessionEntry[];
|
|
85
145
|
getEntries: () => FakeSessionEntry[];
|
|
146
|
+
getCwd: () => string;
|
|
147
|
+
getSessionDir: () => string | undefined;
|
|
148
|
+
getSessionFile: () => string | undefined;
|
|
86
149
|
}
|
|
87
150
|
|
|
88
151
|
/**
|
|
@@ -154,9 +217,48 @@ function extractSessionMessageText(message: unknown): string {
|
|
|
154
217
|
* @details Stores appended custom entries and setup messages in deterministic insertion order, returns stable synthetic entry identifiers, and exposes branch reads used by `session_start` handlers. Runtime is O(1) per mutation plus O(n) per snapshot copy. Side effects are limited to in-memory state mutation.
|
|
155
218
|
* @return {FakeSessionManager} Fake session manager.
|
|
156
219
|
*/
|
|
157
|
-
function
|
|
220
|
+
function createFakeSessionFile(cwd: string): string {
|
|
221
|
+
const sessionDir = path.join(os.tmpdir(), "pi-usereq-test-sessions");
|
|
222
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
223
|
+
const sessionFile = path.join(sessionDir, `session-${Date.now()}-${Math.random().toString(16).slice(2)}.jsonl`);
|
|
224
|
+
fs.writeFileSync(sessionFile, `${JSON.stringify({
|
|
225
|
+
type: "session",
|
|
226
|
+
version: 3,
|
|
227
|
+
id: path.basename(sessionFile, ".jsonl"),
|
|
228
|
+
timestamp: new Date(0).toISOString(),
|
|
229
|
+
cwd,
|
|
230
|
+
})}\n`, "utf8");
|
|
231
|
+
return sessionFile;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @brief Reads the stored cwd header from one fake or persisted session file.
|
|
236
|
+
* @details Parses the first JSONL line and returns its `cwd` field when available so fake `ctx.switchSession(...)` calls can mirror pi session-path semantics without a live runtime. Runtime is O(n) in header size. Side effects are limited to filesystem reads.
|
|
237
|
+
* @param[in] sessionFile {string} Session file path.
|
|
238
|
+
* @param[in] fallbackCwd {string} Fallback cwd when the file cannot be parsed.
|
|
239
|
+
* @return {string} Parsed session cwd or the fallback value.
|
|
240
|
+
*/
|
|
241
|
+
function readFakeSessionFileCwd(sessionFile: string, fallbackCwd: string): string {
|
|
242
|
+
try {
|
|
243
|
+
const headerLine = fs.readFileSync(sessionFile, "utf8").split(/\r?\n/)[0] ?? "";
|
|
244
|
+
const header = JSON.parse(headerLine) as { cwd?: unknown };
|
|
245
|
+
return typeof header.cwd === "string" && header.cwd !== "" ? header.cwd : fallbackCwd;
|
|
246
|
+
} catch {
|
|
247
|
+
return fallbackCwd;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* @brief Creates a fake session manager for session-aware tests.
|
|
253
|
+
* @details Stores appended custom entries and setup messages in deterministic insertion order, returns stable synthetic entry identifiers, and exposes persisted session-file metadata used by prompt-command session switching. Runtime is O(1) per mutation plus O(n) per snapshot copy. Side effects include in-memory state mutation and one fake session-file write.
|
|
254
|
+
* @param[in] cwd {string} Initial session cwd.
|
|
255
|
+
* @return {FakeSessionManager} Fake session manager.
|
|
256
|
+
*/
|
|
257
|
+
function createFakeSessionManager(cwd: string): FakeSessionManager {
|
|
158
258
|
const entries: FakeSessionEntry[] = [];
|
|
159
259
|
let nextEntryId = 0;
|
|
260
|
+
let currentCwd = cwd;
|
|
261
|
+
const sessionFile = createFakeSessionFile(cwd);
|
|
160
262
|
const buildEntryId = (): string => `entry-${nextEntryId++}`;
|
|
161
263
|
return {
|
|
162
264
|
appendCustomEntry(customType: string, data?: unknown): string {
|
|
@@ -175,6 +277,16 @@ function createFakeSessionManager(): FakeSessionManager {
|
|
|
175
277
|
getEntries(): FakeSessionEntry[] {
|
|
176
278
|
return entries.map((entry) => ({ ...entry }));
|
|
177
279
|
},
|
|
280
|
+
getCwd(): string {
|
|
281
|
+
return currentCwd;
|
|
282
|
+
},
|
|
283
|
+
getSessionDir(): string {
|
|
284
|
+
return path.dirname(sessionFile);
|
|
285
|
+
},
|
|
286
|
+
getSessionFile(): string {
|
|
287
|
+
currentCwd = readFakeSessionFileCwd(sessionFile, currentCwd);
|
|
288
|
+
return sessionFile;
|
|
289
|
+
},
|
|
178
290
|
};
|
|
179
291
|
}
|
|
180
292
|
|
|
@@ -296,9 +408,7 @@ function formatFakeThemeBackgroundFromForeground(color: string, text: string): s
|
|
|
296
408
|
|
|
297
409
|
/**
|
|
298
410
|
* @brief Builds the expected fake context-gauge payload for assertions.
|
|
299
|
-
* @details Resolves the documented icon thresholds for `0-100
|
|
300
|
-
* blinking red overflow icon for percent values above `100`. Runtime is O(1).
|
|
301
|
-
* No external state is mutated.
|
|
411
|
+
* @details Resolves the documented icon thresholds for `0-90%`, emits the blinking warning full icon for percent values above `90` through `100`, and emits the blinking red overflow icon for percent values above `100`. Runtime is O(1). No external state is mutated.
|
|
302
412
|
* @param[in] options {{ filledCells: number; percent?: number | null }} Expected context-gauge facts.
|
|
303
413
|
* @return {string} Encoded context-gauge string.
|
|
304
414
|
*/
|
|
@@ -322,19 +432,17 @@ function buildExpectedFakeContextBar(options: {
|
|
|
322
432
|
if (percent <= 90) {
|
|
323
433
|
return formatFakeThemeForeground("warning", "▕▆▏");
|
|
324
434
|
}
|
|
325
|
-
return formatFakeThemeForeground("warning", "
|
|
435
|
+
return formatFakeThemeForeground("warning", "\u001b[5m▕█▏\u001b[25m");
|
|
326
436
|
}
|
|
327
437
|
|
|
328
438
|
/**
|
|
329
439
|
* @brief Builds the expected fake pi-usereq status-bar string for assertions.
|
|
330
|
-
* @details Reconstructs the field order, separators, icon-based context gauge,
|
|
331
|
-
*
|
|
332
|
-
* deterministic fake theme markers. Runtime is O(1). No external state is
|
|
333
|
-
* mutated.
|
|
334
|
-
* @param[in] options {{ basePath: string; contextFilledCells: number; contextPercent?: number | null; et: string; sound?: string }} Expected status facts.
|
|
440
|
+
* @details Reconstructs the field order, workflow-state highlighting, separators, icon-based context gauge, consolidated elapsed field, and sound field emitted by the extension using deterministic fake theme markers. Runtime is O(1). No external state is mutated.
|
|
441
|
+
* @param[in] options {{ workflowState?: string; basePath: string; contextFilledCells: number; contextPercent?: number | null; et: string; sound?: string }} Expected status facts where `basePath` carries the rendered `current-path` value.
|
|
335
442
|
* @return {string} Encoded status-bar string.
|
|
336
443
|
*/
|
|
337
444
|
function buildExpectedFakeStatusText(options: {
|
|
445
|
+
workflowState?: string;
|
|
338
446
|
basePath: string;
|
|
339
447
|
docsDir?: string;
|
|
340
448
|
testsDir?: string;
|
|
@@ -346,12 +454,17 @@ function buildExpectedFakeStatusText(options: {
|
|
|
346
454
|
}): string {
|
|
347
455
|
const buildField = (fieldName: string, value: string): string =>
|
|
348
456
|
`${formatFakeThemeForeground("accent", `${fieldName}:`)}${formatFakeThemeForeground("warning", value)}`;
|
|
457
|
+
const workflowStateText = options.workflowState ?? "idle";
|
|
458
|
+
const workflowStateValue = workflowStateText === "error"
|
|
459
|
+
? formatFakeThemeForeground("error", "\u001b[5merror\u001b[25m")
|
|
460
|
+
: formatFakeThemeForeground("warning", workflowStateText);
|
|
349
461
|
const contextBar = buildExpectedFakeContextBar({
|
|
350
462
|
filledCells: options.contextFilledCells,
|
|
351
463
|
percent: options.contextPercent,
|
|
352
464
|
});
|
|
353
465
|
return [
|
|
354
|
-
|
|
466
|
+
`${formatFakeThemeForeground("accent", "status:")}${workflowStateValue}`,
|
|
467
|
+
buildField("current-path", options.basePath),
|
|
355
468
|
`${formatFakeThemeForeground("accent", "context:")}${contextBar}`,
|
|
356
469
|
buildField("elapsed", options.et),
|
|
357
470
|
buildField("sound", options.sound ?? "none"),
|
|
@@ -396,8 +509,8 @@ function buildExpectedNotifyBasePath(cwd: string): string {
|
|
|
396
509
|
* @return {string} `~`-relative or absolute config-path display string.
|
|
397
510
|
*/
|
|
398
511
|
function buildExpectedShowConfigPath(cwd: string): string {
|
|
399
|
-
return buildExpectedNotifyBasePath(path.dirname(
|
|
400
|
-
? `${buildExpectedNotifyBasePath(cwd)}/.pi-usereq
|
|
512
|
+
return buildExpectedNotifyBasePath(path.dirname(getProjectConfigPath(cwd))) === buildExpectedNotifyBasePath(cwd)
|
|
513
|
+
? `${buildExpectedNotifyBasePath(cwd)}/.pi-usereq.json`
|
|
401
514
|
: getProjectConfigPath(cwd).split(path.sep).join("/");
|
|
402
515
|
}
|
|
403
516
|
|
|
@@ -430,7 +543,13 @@ function createFakeCtx(cwd: string, plan: FakeCtxPlan = { selects: [] }, options
|
|
|
430
543
|
const selects = [...plan.selects];
|
|
431
544
|
const inputs = [...(plan.inputs ?? [])];
|
|
432
545
|
const getContextUsage = options.getContextUsage ?? (() => undefined);
|
|
433
|
-
const
|
|
546
|
+
const baseSessionManager = options.sessionManager ?? createFakeSessionManager(cwd);
|
|
547
|
+
let currentSessionCwd = typeof baseSessionManager.getCwd === "function"
|
|
548
|
+
? baseSessionManager.getCwd()
|
|
549
|
+
: cwd;
|
|
550
|
+
let currentSessionFile = typeof baseSessionManager.getSessionFile === "function"
|
|
551
|
+
? baseSessionManager.getSessionFile()
|
|
552
|
+
: createFakeSessionFile(currentSessionCwd);
|
|
434
553
|
const theme = options.theme ?? {
|
|
435
554
|
fg: (color: string, text: string) => formatFakeThemeForeground(color, text),
|
|
436
555
|
bgFromFg: (color: string, text: string) => formatFakeThemeBackgroundFromForeground(color, text),
|
|
@@ -445,13 +564,43 @@ function createFakeCtx(cwd: string, plan: FakeCtxPlan = { selects: [] }, options
|
|
|
445
564
|
waitForIdleCalls: 0,
|
|
446
565
|
newSessions: [] as Array<{ messages: string[] }>,
|
|
447
566
|
};
|
|
448
|
-
|
|
567
|
+
const sessionManager: FakeSessionManager = {
|
|
568
|
+
appendCustomEntry(customType: string, data?: unknown): string {
|
|
569
|
+
return baseSessionManager.appendCustomEntry(customType, data);
|
|
570
|
+
},
|
|
571
|
+
appendMessage(message: unknown): string {
|
|
572
|
+
return baseSessionManager.appendMessage(message);
|
|
573
|
+
},
|
|
574
|
+
getBranch(): FakeSessionEntry[] {
|
|
575
|
+
return baseSessionManager.getBranch();
|
|
576
|
+
},
|
|
577
|
+
getEntries(): FakeSessionEntry[] {
|
|
578
|
+
return baseSessionManager.getEntries();
|
|
579
|
+
},
|
|
580
|
+
getCwd(): string {
|
|
581
|
+
return currentSessionCwd;
|
|
582
|
+
},
|
|
583
|
+
getSessionDir(): string | undefined {
|
|
584
|
+
return typeof currentSessionFile === "string" ? path.dirname(currentSessionFile) : undefined;
|
|
585
|
+
},
|
|
586
|
+
getSessionFile(): string | undefined {
|
|
587
|
+
return currentSessionFile;
|
|
588
|
+
},
|
|
589
|
+
};
|
|
590
|
+
const fakeCtx: any = {
|
|
449
591
|
cwd,
|
|
450
592
|
__state: state,
|
|
451
593
|
sessionManager,
|
|
452
594
|
async waitForIdle() {
|
|
453
595
|
state.waitForIdleCalls += 1;
|
|
454
596
|
},
|
|
597
|
+
async switchSession(sessionPath: string) {
|
|
598
|
+
currentSessionFile = sessionPath;
|
|
599
|
+
currentSessionCwd = readFakeSessionFileCwd(sessionPath, currentSessionCwd);
|
|
600
|
+
fakeCtx.cwd = currentSessionCwd;
|
|
601
|
+
process.chdir(currentSessionCwd);
|
|
602
|
+
return { cancelled: false };
|
|
603
|
+
},
|
|
455
604
|
async newSession(newSessionOptions?: {
|
|
456
605
|
parentSession?: string;
|
|
457
606
|
setup?: (sessionManager: FakeSessionManager) => Promise<void>;
|
|
@@ -473,6 +622,15 @@ function createFakeCtx(cwd: string, plan: FakeCtxPlan = { selects: [] }, options
|
|
|
473
622
|
getEntries(): FakeSessionEntry[] {
|
|
474
623
|
return sessionManager.getEntries();
|
|
475
624
|
},
|
|
625
|
+
getCwd(): string {
|
|
626
|
+
return sessionManager.getCwd();
|
|
627
|
+
},
|
|
628
|
+
getSessionDir(): string | undefined {
|
|
629
|
+
return sessionManager.getSessionDir();
|
|
630
|
+
},
|
|
631
|
+
getSessionFile(): string | undefined {
|
|
632
|
+
return sessionManager.getSessionFile();
|
|
633
|
+
},
|
|
476
634
|
};
|
|
477
635
|
if (newSessionOptions?.setup) {
|
|
478
636
|
await newSessionOptions.setup(setupSessionManager);
|
|
@@ -529,6 +687,8 @@ function createFakeCtx(cwd: string, plan: FakeCtxPlan = { selects: [] }, options
|
|
|
529
687
|
});
|
|
530
688
|
if (response === undefined) {
|
|
531
689
|
bridge.cancel();
|
|
690
|
+
} else if (response === "Save and close" && !bridge.choices.some((choice) => choice.label === response)) {
|
|
691
|
+
bridge.cancel();
|
|
532
692
|
} else {
|
|
533
693
|
assert.ok(bridge.selectByLabel(response), `unsupported custom selection ${response}`);
|
|
534
694
|
}
|
|
@@ -549,6 +709,7 @@ function createFakeCtx(cwd: string, plan: FakeCtxPlan = { selects: [] }, options
|
|
|
549
709
|
},
|
|
550
710
|
},
|
|
551
711
|
};
|
|
712
|
+
return fakeCtx;
|
|
552
713
|
}
|
|
553
714
|
|
|
554
715
|
/**
|
|
@@ -604,25 +765,19 @@ test("extension registers prompt and config commands while exposing tool capabil
|
|
|
604
765
|
]) {
|
|
605
766
|
assert.ok(commandNames.includes(name), `missing command ${name}`);
|
|
606
767
|
}
|
|
768
|
+
assert.equal(pi.commands.get("req-analyze")?.description, "Produce an analysis report");
|
|
607
769
|
|
|
608
770
|
for (const name of [
|
|
609
|
-
"git-path",
|
|
610
|
-
"get-base-path",
|
|
611
771
|
"files-tokens",
|
|
612
772
|
"files-references",
|
|
613
773
|
"files-compress",
|
|
614
|
-
"files-
|
|
774
|
+
"files-search",
|
|
615
775
|
"references",
|
|
616
776
|
"compress",
|
|
617
|
-
"
|
|
777
|
+
"search",
|
|
618
778
|
"tokens",
|
|
619
779
|
"files-static-check",
|
|
620
780
|
"static-check",
|
|
621
|
-
"git-check",
|
|
622
|
-
"docs-check",
|
|
623
|
-
"git-wt-name",
|
|
624
|
-
"git-wt-create",
|
|
625
|
-
"git-wt-delete",
|
|
626
781
|
"pi-usereq-show-config",
|
|
627
782
|
"test-static-check",
|
|
628
783
|
]) {
|
|
@@ -631,23 +786,16 @@ test("extension registers prompt and config commands while exposing tool capabil
|
|
|
631
786
|
|
|
632
787
|
const toolNames = pi.tools.map((tool: RegisteredTool) => tool.name).sort();
|
|
633
788
|
for (const name of [
|
|
634
|
-
"git-path",
|
|
635
|
-
"get-base-path",
|
|
636
789
|
"files-tokens",
|
|
637
790
|
"files-references",
|
|
638
791
|
"files-compress",
|
|
639
|
-
"files-
|
|
792
|
+
"files-search",
|
|
640
793
|
"references",
|
|
641
794
|
"compress",
|
|
642
|
-
"
|
|
795
|
+
"search",
|
|
643
796
|
"tokens",
|
|
644
797
|
"files-static-check",
|
|
645
798
|
"static-check",
|
|
646
|
-
"git-check",
|
|
647
|
-
"docs-check",
|
|
648
|
-
"git-wt-name",
|
|
649
|
-
"git-wt-create",
|
|
650
|
-
"git-wt-delete",
|
|
651
799
|
]) {
|
|
652
800
|
assert.ok(toolNames.includes(name), `missing tool ${name}`);
|
|
653
801
|
}
|
|
@@ -662,13 +810,13 @@ test("token tools register agent-oriented descriptions and schema details", () =
|
|
|
662
810
|
assert.ok(filesTokens, "missing files-tokens tool");
|
|
663
811
|
assert.ok(tokens, "missing tokens tool");
|
|
664
812
|
|
|
665
|
-
assert.match(filesTokens.description ?? "", /token
|
|
666
|
-
assert.ok(filesTokens.promptGuidelines?.some((line) => line.includes("
|
|
667
|
-
assert.match(String((filesTokens.parameters as { description?: string } | undefined)?.description ?? ""), /
|
|
813
|
+
assert.match(filesTokens.description ?? "", /monolithic token pack summary/);
|
|
814
|
+
assert.ok(filesTokens.promptGuidelines?.some((line) => line.includes("monolithic pack-summary text")));
|
|
815
|
+
assert.match(String((filesTokens.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic pack-summary text/);
|
|
668
816
|
|
|
669
817
|
assert.match(tokens.description ?? "", /canonical docs/);
|
|
670
|
-
assert.ok(tokens.promptGuidelines?.some((line) => line.includes("
|
|
671
|
-
assert.match(String((tokens.parameters as { description?: string } | undefined)?.description ?? ""), /
|
|
818
|
+
assert.ok(tokens.promptGuidelines?.some((line) => line.includes("monolithic pack-summary text")));
|
|
819
|
+
assert.match(String((tokens.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic pack-summary text/);
|
|
672
820
|
});
|
|
673
821
|
|
|
674
822
|
test("reference tools register agent-oriented descriptions and schema details", () => {
|
|
@@ -680,36 +828,36 @@ test("reference tools register agent-oriented descriptions and schema details",
|
|
|
680
828
|
assert.ok(filesReferences, "missing files-references tool");
|
|
681
829
|
assert.ok(references, "missing references tool");
|
|
682
830
|
|
|
683
|
-
assert.match(filesReferences.description ?? "", /
|
|
684
|
-
assert.ok(filesReferences.promptGuidelines?.some((line) => line.includes("
|
|
685
|
-
assert.ok(filesReferences.promptGuidelines?.some((line) => line.includes("
|
|
686
|
-
assert.match(String((filesReferences.parameters as { description?: string } | undefined)?.description ?? ""), /
|
|
831
|
+
assert.match(filesReferences.description ?? "", /monolithic references markdown report/);
|
|
832
|
+
assert.ok(filesReferences.promptGuidelines?.some((line) => line.includes("monolithic markdown")));
|
|
833
|
+
assert.ok(filesReferences.promptGuidelines?.some((line) => line.includes("Formatting contract:")));
|
|
834
|
+
assert.match(String((filesReferences.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic markdown/);
|
|
687
835
|
|
|
688
836
|
assert.match(references.description ?? "", /configured project source directories/);
|
|
689
|
-
assert.ok(references.promptGuidelines?.some((line) => line.includes("
|
|
837
|
+
assert.ok(references.promptGuidelines?.some((line) => line.includes("file-structure markdown block")));
|
|
690
838
|
assert.ok(references.promptGuidelines?.some((line) => line.includes("Configuration contract:")));
|
|
691
|
-
assert.match(String((references.parameters as { description?: string } | undefined)?.description ?? ""), /
|
|
839
|
+
assert.match(String((references.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic markdown/);
|
|
692
840
|
});
|
|
693
841
|
|
|
694
|
-
test("
|
|
842
|
+
test("search tools register agent-oriented descriptions and schema details", () => {
|
|
695
843
|
const pi = createFakePi();
|
|
696
844
|
piUsereqExtension(pi);
|
|
697
845
|
|
|
698
|
-
const
|
|
699
|
-
const
|
|
700
|
-
assert.ok(
|
|
701
|
-
assert.ok(
|
|
702
|
-
|
|
703
|
-
assert.match(
|
|
704
|
-
assert.ok(
|
|
705
|
-
assert.ok(
|
|
706
|
-
assert.ok(
|
|
707
|
-
assert.match(String((
|
|
708
|
-
|
|
709
|
-
assert.match(
|
|
710
|
-
assert.ok(
|
|
711
|
-
assert.ok(
|
|
712
|
-
assert.match(String((
|
|
846
|
+
const filesSearch = pi.tools.find((tool: RegisteredTool) => tool.name === "files-search");
|
|
847
|
+
const search = pi.tools.find((tool: RegisteredTool) => tool.name === "search");
|
|
848
|
+
assert.ok(filesSearch, "missing files-search tool");
|
|
849
|
+
assert.ok(search, "missing search tool");
|
|
850
|
+
|
|
851
|
+
assert.match(filesSearch.description ?? "", /monolithic construct-search markdown report/);
|
|
852
|
+
assert.ok(filesSearch.promptGuidelines?.some((line) => line.includes("Regex rule:")));
|
|
853
|
+
assert.ok(filesSearch.promptGuidelines?.some((line) => line.includes("Supported tags [Typescript]:")));
|
|
854
|
+
assert.ok(filesSearch.promptGuidelines?.some((line) => line.includes("Supported tags [Python]:")));
|
|
855
|
+
assert.match(String((filesSearch.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic markdown/);
|
|
856
|
+
|
|
857
|
+
assert.match(search.description ?? "", /configured project source directories/);
|
|
858
|
+
assert.ok(search.promptGuidelines?.some((line) => line.includes("monolithic markdown")));
|
|
859
|
+
assert.ok(search.promptGuidelines?.some((line) => line.includes("Tag rule:")));
|
|
860
|
+
assert.match(String((search.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic markdown/);
|
|
713
861
|
});
|
|
714
862
|
|
|
715
863
|
test("compression tools register agent-oriented descriptions and schema details", () => {
|
|
@@ -721,18 +869,69 @@ test("compression tools register agent-oriented descriptions and schema details"
|
|
|
721
869
|
assert.ok(filesCompress, "missing files-compress tool");
|
|
722
870
|
assert.ok(compress, "missing compress tool");
|
|
723
871
|
|
|
724
|
-
assert.match(filesCompress.description ?? "", /
|
|
872
|
+
assert.match(filesCompress.description ?? "", /monolithic compression markdown report/);
|
|
725
873
|
assert.ok(filesCompress.promptGuidelines?.some((line) => line.includes("Line-number behavior:")));
|
|
726
|
-
assert.ok(filesCompress.promptGuidelines?.some((line) => line.includes("
|
|
727
|
-
assert.match(String((filesCompress.parameters as { description?: string } | undefined)?.description ?? ""), /
|
|
874
|
+
assert.ok(filesCompress.promptGuidelines?.some((line) => line.includes("Formatting contract:")));
|
|
875
|
+
assert.match(String((filesCompress.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic markdown/);
|
|
728
876
|
|
|
729
877
|
assert.match(compress.description ?? "", /configured project source directories/);
|
|
730
|
-
assert.ok(compress.promptGuidelines?.some((line) => line.includes("
|
|
731
|
-
assert.ok(compress.promptGuidelines?.some((line) => line.includes("
|
|
732
|
-
assert.match(String((compress.parameters as { description?: string } | undefined)?.description ?? ""), /
|
|
878
|
+
assert.ok(compress.promptGuidelines?.some((line) => line.includes("monolithic markdown")));
|
|
879
|
+
assert.ok(compress.promptGuidelines?.some((line) => line.includes("Formatting contract:")));
|
|
880
|
+
assert.match(String((compress.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic markdown/);
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
test("agent tools define custom renderResult with compact and expanded structured views", async () => {
|
|
884
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
885
|
+
try {
|
|
886
|
+
const pi = createFakePi();
|
|
887
|
+
piUsereqExtension(pi);
|
|
888
|
+
const toolNames = [
|
|
889
|
+
"files-tokens",
|
|
890
|
+
"files-references",
|
|
891
|
+
"files-compress",
|
|
892
|
+
"files-search",
|
|
893
|
+
"references",
|
|
894
|
+
"compress",
|
|
895
|
+
"search",
|
|
896
|
+
"tokens",
|
|
897
|
+
"files-static-check",
|
|
898
|
+
"static-check",
|
|
899
|
+
];
|
|
900
|
+
for (const toolName of toolNames) {
|
|
901
|
+
const tool = pi.tools.find((candidate: RegisteredTool) => candidate.name === toolName);
|
|
902
|
+
assert.ok(tool, `missing tool ${toolName}`);
|
|
903
|
+
assert.equal(typeof tool?.renderResult, "function", `missing renderResult for ${toolName}`);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
fs.writeFileSync(path.join(projectBase, "src", "render_result_target.ts"), "export function alpha(): number {\n return 1;\n}\n", "utf8");
|
|
907
|
+
const tool = pi.tools.find((candidate: RegisteredTool) => candidate.name === "files-search");
|
|
908
|
+
assert.ok(tool?.renderResult, "missing renderResult for files-search");
|
|
909
|
+
const params = {
|
|
910
|
+
tag: "FUNCTION",
|
|
911
|
+
pattern: "^alpha$",
|
|
912
|
+
files: ["src/render_result_target.ts"],
|
|
913
|
+
enableLineNumbers: true,
|
|
914
|
+
};
|
|
915
|
+
const result = await executeRegisteredTool(pi, "files-search", projectBase, params);
|
|
916
|
+
const theme: FakeThemeAdapter = {
|
|
917
|
+
fg: (color: string, text: string) => formatFakeThemeForeground(color, text),
|
|
918
|
+
bgFromFg: (color: string, text: string) => formatFakeThemeBackgroundFromForeground(color, text),
|
|
919
|
+
bold: (text: string) => `<bold>${text}</bold>`,
|
|
920
|
+
};
|
|
921
|
+
const compact = tool.renderResult(result, { expanded: false, isPartial: false }, theme, { args: params }).render(120).join("\n");
|
|
922
|
+
const expanded = tool.renderResult(result, { expanded: true, isPartial: false }, theme, { args: params }).render(120).join("\n");
|
|
923
|
+
assert.match(compact, /files-search/);
|
|
924
|
+
assert.match(compact, /tag="FUNCTION"/);
|
|
925
|
+
assert.match(compact, /pattern="\^alpha\$"/);
|
|
926
|
+
assert.match(compact, /files=\["src\/render_result_target\.ts"\]/);
|
|
927
|
+
assert.doesNotMatch(compact, /### FUNCTION/);
|
|
928
|
+
assert.match(expanded, /### FUNCTION: `alpha`/);
|
|
929
|
+
} finally {
|
|
930
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
931
|
+
}
|
|
733
932
|
});
|
|
734
933
|
|
|
735
|
-
test("files-tokens returns token
|
|
934
|
+
test("files-tokens returns monolithic token summary text with execution diagnostics", async () => {
|
|
736
935
|
const { projectBase } = initFixtureRepo({
|
|
737
936
|
fixtures: [],
|
|
738
937
|
docs: {
|
|
@@ -748,45 +947,20 @@ test("files-tokens returns token-optimized structured source facts", async () =>
|
|
|
748
947
|
files: [`${DEFAULT_DOCS_DIR}/REQUIREMENTS.md`, `${DEFAULT_DOCS_DIR}/MISSING.md`],
|
|
749
948
|
}) as {
|
|
750
949
|
content?: Array<{ type: string; text?: string }>;
|
|
751
|
-
details?: {
|
|
752
|
-
summary: {
|
|
753
|
-
counted_file_count: number;
|
|
754
|
-
skipped_file_count: number;
|
|
755
|
-
};
|
|
756
|
-
files: Array<{
|
|
757
|
-
canonical_path: string;
|
|
758
|
-
status: string;
|
|
759
|
-
start_line_number: number;
|
|
760
|
-
end_line_number: number;
|
|
761
|
-
line_count: number;
|
|
762
|
-
byte_count: number;
|
|
763
|
-
primary_heading_text?: string;
|
|
764
|
-
error_message?: string;
|
|
765
|
-
}>;
|
|
766
|
-
execution: {
|
|
767
|
-
stderr: string;
|
|
768
|
-
};
|
|
769
|
-
};
|
|
950
|
+
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
770
951
|
};
|
|
771
952
|
|
|
772
|
-
const
|
|
773
|
-
assert.
|
|
774
|
-
assert.
|
|
775
|
-
assert.equal(
|
|
776
|
-
assert.
|
|
777
|
-
assert.equal(payload.files[0]?.start_line_number, 1);
|
|
778
|
-
assert.equal(payload.files[0]?.end_line_number, payload.files[0]?.line_count);
|
|
779
|
-
assert.ok((payload.files[0]?.byte_count ?? 0) > 0);
|
|
780
|
-
assert.equal(payload.files[0]?.primary_heading_text, "Requirements");
|
|
781
|
-
assert.equal(payload.files[1]?.canonical_path, `${DEFAULT_DOCS_DIR}/MISSING.md`);
|
|
782
|
-
assert.equal(payload.files[1]?.error_message, "not found");
|
|
783
|
-
assert.match(payload.execution.stderr, new RegExp(`skipped: ${DEFAULT_DOCS_DIR.replace("/", "\\/")}/MISSING\\.md: not found`));
|
|
953
|
+
const content = result.content?.[0]?.text ?? "";
|
|
954
|
+
assert.match(content, /Pack Summary/);
|
|
955
|
+
assert.match(content, /REQUIREMENTS\.md/);
|
|
956
|
+
assert.equal(result.details?.execution.code, 0);
|
|
957
|
+
assert.ok(result.details?.execution.stderr_lines?.some((line) => /MISSING\.md/.test(line)));
|
|
784
958
|
} finally {
|
|
785
959
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
786
960
|
}
|
|
787
961
|
});
|
|
788
962
|
|
|
789
|
-
test("files-tokens defers js-tiktoken loading until execution and returns
|
|
963
|
+
test("files-tokens defers js-tiktoken loading until execution and returns monolithic dependency failures", async () => {
|
|
790
964
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
791
965
|
const missingModuleError = new Error("Cannot find module 'js-tiktoken'");
|
|
792
966
|
setJsTiktokenModuleLoaderForTests(() => {
|
|
@@ -798,35 +972,20 @@ test("files-tokens defers js-tiktoken loading until execution and returns struct
|
|
|
798
972
|
const result = await executeRegisteredTool(pi, "files-tokens", projectBase, {
|
|
799
973
|
files: [`${DEFAULT_DOCS_DIR}/REQUIREMENTS.md`],
|
|
800
974
|
}) as {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
counted_file_count: number;
|
|
804
|
-
skipped_file_count: number;
|
|
805
|
-
processable_file_count: number;
|
|
806
|
-
};
|
|
807
|
-
files: unknown[];
|
|
808
|
-
execution: {
|
|
809
|
-
code: number;
|
|
810
|
-
stderr: string;
|
|
811
|
-
};
|
|
812
|
-
};
|
|
975
|
+
content?: Array<{ type: string; text?: string }>;
|
|
976
|
+
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
813
977
|
};
|
|
814
978
|
|
|
815
|
-
|
|
816
|
-
assert.
|
|
817
|
-
assert.
|
|
818
|
-
assert.equal(payload.summary.skipped_file_count, 0);
|
|
819
|
-
assert.deepEqual(payload.files, []);
|
|
820
|
-
assert.equal(payload.execution.code, 1);
|
|
821
|
-
assert.match(payload.execution.stderr, /js-tiktoken/);
|
|
822
|
-
assert.match(payload.execution.stderr, /npm ci/);
|
|
979
|
+
assert.equal(result.details?.execution.code, 1);
|
|
980
|
+
assert.match(result.content?.[0]?.text ?? "", /js-tiktoken/);
|
|
981
|
+
assert.ok(result.details?.execution.stderr_lines?.some((line) => /npm ci/.test(line)));
|
|
823
982
|
} finally {
|
|
824
983
|
setJsTiktokenModuleLoaderForTests(undefined);
|
|
825
984
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
826
985
|
}
|
|
827
986
|
});
|
|
828
987
|
|
|
829
|
-
test("files-references returns
|
|
988
|
+
test("files-references returns monolithic references markdown", async () => {
|
|
830
989
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
831
990
|
const samplePath = path.join(projectBase, "src", "sample.ts");
|
|
832
991
|
fs.writeFileSync(samplePath, `/**
|
|
@@ -851,66 +1010,22 @@ export function buildSample(input: string): string {
|
|
|
851
1010
|
files: ["src/sample.ts", "src/missing.ts"],
|
|
852
1011
|
}) as {
|
|
853
1012
|
content?: Array<{ type: string; text?: string }>;
|
|
854
|
-
details?: {
|
|
855
|
-
summary: {
|
|
856
|
-
analyzed_file_count: number;
|
|
857
|
-
skipped_file_count: number;
|
|
858
|
-
};
|
|
859
|
-
repository: {
|
|
860
|
-
file_canonical_paths: string[];
|
|
861
|
-
};
|
|
862
|
-
files: Array<{
|
|
863
|
-
canonical_path: string;
|
|
864
|
-
status: string;
|
|
865
|
-
symbol_count: number;
|
|
866
|
-
doxygen_field_count: number;
|
|
867
|
-
file_doxygen?: { brief?: string[] };
|
|
868
|
-
symbols: Array<{
|
|
869
|
-
symbol_name: string;
|
|
870
|
-
qualified_name: string;
|
|
871
|
-
symbol_kind: string;
|
|
872
|
-
line_range: [number, number];
|
|
873
|
-
doxygen?: {
|
|
874
|
-
brief?: string[];
|
|
875
|
-
params?: Array<{ direction: string; parameter_name?: string; value_type?: string; description?: string }>;
|
|
876
|
-
returns?: string[];
|
|
877
|
-
satisfies_requirement_ids?: string[];
|
|
878
|
-
};
|
|
879
|
-
}>;
|
|
880
|
-
}>;
|
|
881
|
-
execution: {
|
|
882
|
-
stderr: string;
|
|
883
|
-
};
|
|
884
|
-
};
|
|
1013
|
+
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
885
1014
|
};
|
|
886
1015
|
|
|
887
|
-
const
|
|
888
|
-
assert.
|
|
889
|
-
assert.
|
|
890
|
-
assert.
|
|
891
|
-
assert.
|
|
892
|
-
assert.
|
|
893
|
-
assert.
|
|
894
|
-
assert.equal(payload.files[0]?.symbol_count, 1);
|
|
895
|
-
assert.ok((payload.files[0]?.doxygen_field_count ?? 0) >= 2);
|
|
896
|
-
assert.deepEqual(payload.files[0]?.file_doxygen?.brief, ["Sample source file."]);
|
|
897
|
-
assert.equal(payload.files[0]?.symbols[0]?.symbol_name, "buildSample");
|
|
898
|
-
assert.equal(payload.files[0]?.symbols[0]?.qualified_name, "buildSample");
|
|
899
|
-
assert.equal(payload.files[0]?.symbols[0]?.symbol_kind, "FUNCTION");
|
|
900
|
-
assert.deepEqual(payload.files[0]?.symbols[0]?.line_range, [12, 14]);
|
|
901
|
-
assert.deepEqual(payload.files[0]?.symbols[0]?.doxygen?.brief, ["Builds the sample value."]);
|
|
902
|
-
assert.deepEqual(payload.files[0]?.symbols[0]?.doxygen?.params, [
|
|
903
|
-
{ direction: "in", parameter_name: "input", value_type: "string", description: "Caller-supplied value." },
|
|
904
|
-
]);
|
|
905
|
-
assert.deepEqual(payload.files[0]?.symbols[0]?.doxygen?.returns, ["{string} Upper-cased result."]);
|
|
906
|
-
assert.deepEqual(payload.files[0]?.symbols[0]?.doxygen?.satisfies_requirement_ids, ["REQ-011"]);
|
|
907
|
-
assert.match(payload.execution.stderr, /skipped: src\/missing\.ts: not found/);
|
|
1016
|
+
const content = result.content?.[0]?.text ?? "";
|
|
1017
|
+
assert.match(content, /# sample\.ts \| TypeScript/);
|
|
1018
|
+
assert.match(content, /## Definitions/);
|
|
1019
|
+
assert.match(content, /buildSample/);
|
|
1020
|
+
assert.match(content, /Caller-supplied value\./);
|
|
1021
|
+
assert.equal(result.details?.execution.code, 0);
|
|
1022
|
+
assert.deepEqual(result.details?.execution.stderr_lines, undefined);
|
|
908
1023
|
} finally {
|
|
909
1024
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
910
1025
|
}
|
|
911
1026
|
});
|
|
912
1027
|
|
|
913
|
-
test("references returns
|
|
1028
|
+
test("references returns monolithic project references markdown", async () => {
|
|
914
1029
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
915
1030
|
fs.writeFileSync(path.join(projectBase, "src", "alpha.ts"), "export const ALPHA = 1;\n", "utf8");
|
|
916
1031
|
fs.mkdirSync(path.join(projectBase, "src", "nested"), { recursive: true });
|
|
@@ -920,49 +1035,23 @@ test("references returns a structured repository tree for configured source dire
|
|
|
920
1035
|
piUsereqExtension(pi);
|
|
921
1036
|
const result = await executeRegisteredTool(pi, "references", projectBase, {}) as {
|
|
922
1037
|
content?: Array<{ type: string; text?: string }>;
|
|
923
|
-
details?: {
|
|
924
|
-
summary: {
|
|
925
|
-
analyzed_file_count: number;
|
|
926
|
-
total_symbol_count: number;
|
|
927
|
-
};
|
|
928
|
-
repository: {
|
|
929
|
-
source_directory_paths: string[];
|
|
930
|
-
file_canonical_paths: string[];
|
|
931
|
-
directory_tree: {
|
|
932
|
-
node_name: string;
|
|
933
|
-
children: Array<{ node_name: string; node_kind: string; children: Array<{ relative_path: string; node_kind: string }> }>;
|
|
934
|
-
};
|
|
935
|
-
};
|
|
936
|
-
files: Array<{
|
|
937
|
-
canonical_path: string;
|
|
938
|
-
line_range: [number, number];
|
|
939
|
-
symbols: Array<{ symbol_name: string; line_range: [number, number] }>;
|
|
940
|
-
}>;
|
|
941
|
-
};
|
|
1038
|
+
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
942
1039
|
};
|
|
943
1040
|
|
|
944
|
-
const
|
|
945
|
-
assert.
|
|
946
|
-
assert.
|
|
947
|
-
assert.
|
|
948
|
-
assert.
|
|
949
|
-
assert.
|
|
950
|
-
assert.equal(
|
|
951
|
-
assert.
|
|
952
|
-
assert.equal(payload.repository.directory_tree.children[0]?.node_kind, "directory");
|
|
953
|
-
assert.ok(payload.repository.directory_tree.children[0]?.children.some((child) => child.relative_path === "src/alpha.ts"));
|
|
954
|
-
assert.ok(payload.repository.directory_tree.children[0]?.children.some((child) => child.relative_path === "src/nested"));
|
|
955
|
-
assert.deepEqual(payload.files.map((file) => file.canonical_path), ["src/alpha.ts", "src/nested/beta.ts"]);
|
|
956
|
-
assert.deepEqual(payload.files[0]?.line_range, [1, 1]);
|
|
957
|
-
assert.equal(payload.files[0]?.symbols.length, 0);
|
|
958
|
-
assert.equal(payload.files[1]?.symbols[0]?.symbol_name, "beta");
|
|
959
|
-
assert.deepEqual(payload.files[1]?.symbols[0]?.line_range, [1, 3]);
|
|
1041
|
+
const content = result.content?.[0]?.text ?? "";
|
|
1042
|
+
assert.match(content, /# Files Structure/);
|
|
1043
|
+
assert.match(content, /src\/alpha\.ts/);
|
|
1044
|
+
assert.match(content, /src\/nested\/beta\.ts/);
|
|
1045
|
+
assert.match(content, /# alpha\.ts \| TypeScript/);
|
|
1046
|
+
assert.match(content, /# beta\.ts \| TypeScript/);
|
|
1047
|
+
assert.equal(result.details?.execution.code, 0);
|
|
1048
|
+
assert.deepEqual(result.details?.execution.stderr_lines, undefined);
|
|
960
1049
|
} finally {
|
|
961
1050
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
962
1051
|
}
|
|
963
1052
|
});
|
|
964
1053
|
|
|
965
|
-
test("files-compress returns
|
|
1054
|
+
test("files-compress returns monolithic compression markdown", async () => {
|
|
966
1055
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
967
1056
|
const samplePath = path.join(projectBase, "src", "sample.ts");
|
|
968
1057
|
fs.writeFileSync(samplePath, `/**
|
|
@@ -988,68 +1077,21 @@ export function buildSample(input: string): string {
|
|
|
988
1077
|
enableLineNumbers: true,
|
|
989
1078
|
}) as {
|
|
990
1079
|
content?: Array<{ type: string; text?: string }>;
|
|
991
|
-
details?: {
|
|
992
|
-
summary: {
|
|
993
|
-
compressed_file_count: number;
|
|
994
|
-
skipped_file_count: number;
|
|
995
|
-
total_symbol_count: number;
|
|
996
|
-
};
|
|
997
|
-
repository: {
|
|
998
|
-
file_canonical_paths: string[];
|
|
999
|
-
};
|
|
1000
|
-
files: Array<{
|
|
1001
|
-
canonical_path: string;
|
|
1002
|
-
status: string;
|
|
1003
|
-
line_number_mode: string;
|
|
1004
|
-
source_line_count: number;
|
|
1005
|
-
compressed_line_count: number;
|
|
1006
|
-
file_doxygen?: { brief?: string[] };
|
|
1007
|
-
symbols: Array<{
|
|
1008
|
-
symbol_name: string;
|
|
1009
|
-
symbol_kind: string;
|
|
1010
|
-
line_range: [number, number];
|
|
1011
|
-
doxygen?: { brief?: string[] };
|
|
1012
|
-
}>;
|
|
1013
|
-
compressed_lines: Array<{ source_line_number: number; display_text: string; text: string }>;
|
|
1014
|
-
compressed_source_text?: string;
|
|
1015
|
-
error_reason?: string;
|
|
1016
|
-
}>;
|
|
1017
|
-
execution: {
|
|
1018
|
-
code: number;
|
|
1019
|
-
stderr: string;
|
|
1020
|
-
};
|
|
1021
|
-
};
|
|
1080
|
+
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
1022
1081
|
};
|
|
1023
1082
|
|
|
1024
|
-
const
|
|
1025
|
-
assert.
|
|
1026
|
-
assert.
|
|
1027
|
-
assert.
|
|
1028
|
-
assert.equal(
|
|
1029
|
-
assert.
|
|
1030
|
-
assert.equal(payload.files[0]?.canonical_path, "src/sample.ts");
|
|
1031
|
-
assert.equal(payload.files[0]?.status, "compressed");
|
|
1032
|
-
assert.equal(payload.files[0]?.line_number_mode, "enabled");
|
|
1033
|
-
assert.ok((payload.files[0]?.source_line_count ?? 0) > 0);
|
|
1034
|
-
assert.ok((payload.files[0]?.compressed_line_count ?? 0) > 0);
|
|
1035
|
-
assert.deepEqual(payload.files[0]?.file_doxygen?.brief, ["Sample source file."]);
|
|
1036
|
-
assert.equal(payload.files[0]?.symbols[0]?.symbol_name, "buildSample");
|
|
1037
|
-
assert.equal(payload.files[0]?.symbols[0]?.symbol_kind, "FUNCTION");
|
|
1038
|
-
assert.deepEqual(payload.files[0]?.symbols[0]?.line_range, [11, 14]);
|
|
1039
|
-
assert.deepEqual(payload.files[0]?.symbols[0]?.doxygen?.brief, ["Builds the sample value."]);
|
|
1040
|
-
assert.equal(payload.files[0]?.compressed_lines[0]?.source_line_number, 11);
|
|
1041
|
-
assert.match(payload.files[0]?.compressed_lines[0]?.display_text ?? "", /^11: export function buildSample/);
|
|
1042
|
-
assert.match(payload.files[0]?.compressed_source_text ?? "", /^11: export function buildSample/m);
|
|
1043
|
-
assert.equal(payload.files[1]?.status, "skipped");
|
|
1044
|
-
assert.equal(payload.files[1]?.error_reason, "not_found");
|
|
1045
|
-
assert.equal(payload.execution.code, 0);
|
|
1046
|
-
assert.match(payload.execution.stderr, /skipped: src\/missing\.ts: not_found/);
|
|
1083
|
+
const content = result.content?.[0]?.text ?? "";
|
|
1084
|
+
assert.match(content, /@@@ src\/sample\.ts \| typescript/);
|
|
1085
|
+
assert.match(content, /> Lines: 11-14/);
|
|
1086
|
+
assert.match(content, /^11: export function buildSample/m);
|
|
1087
|
+
assert.equal(result.details?.execution.code, 0);
|
|
1088
|
+
assert.deepEqual(result.details?.execution.stderr_lines, undefined);
|
|
1047
1089
|
} finally {
|
|
1048
1090
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
1049
1091
|
}
|
|
1050
1092
|
});
|
|
1051
1093
|
|
|
1052
|
-
test("compress returns
|
|
1094
|
+
test("compress returns monolithic repository-scoped compression markdown", async () => {
|
|
1053
1095
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
1054
1096
|
fs.writeFileSync(
|
|
1055
1097
|
path.join(projectBase, "src", "alpha.ts"),
|
|
@@ -1069,45 +1111,21 @@ test("compress returns a structured repository-scoped compression payload", asyn
|
|
|
1069
1111
|
enableLineNumbers: false,
|
|
1070
1112
|
}) as {
|
|
1071
1113
|
content?: Array<{ type: string; text?: string }>;
|
|
1072
|
-
details?: {
|
|
1073
|
-
summary: {
|
|
1074
|
-
compressed_file_count: number;
|
|
1075
|
-
total_compressed_line_count: number;
|
|
1076
|
-
};
|
|
1077
|
-
repository: {
|
|
1078
|
-
source_directory_paths: string[];
|
|
1079
|
-
file_canonical_paths: string[];
|
|
1080
|
-
};
|
|
1081
|
-
files: Array<{
|
|
1082
|
-
canonical_path: string;
|
|
1083
|
-
line_number_mode: string;
|
|
1084
|
-
compressed_source_text?: string;
|
|
1085
|
-
compressed_lines: Array<{ display_text: string }>;
|
|
1086
|
-
}>;
|
|
1087
|
-
execution: {
|
|
1088
|
-
code: number;
|
|
1089
|
-
stderr: string;
|
|
1090
|
-
};
|
|
1091
|
-
};
|
|
1114
|
+
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
1092
1115
|
};
|
|
1093
1116
|
|
|
1094
|
-
const
|
|
1095
|
-
assert.
|
|
1096
|
-
assert.
|
|
1097
|
-
assert.
|
|
1098
|
-
assert.
|
|
1099
|
-
assert.deepEqual(
|
|
1100
|
-
assert.equal(payload.files[0]?.line_number_mode, "disabled");
|
|
1101
|
-
assert.doesNotMatch(payload.files[1]?.compressed_source_text ?? "", /^\d+:/m);
|
|
1102
|
-
assert.match(payload.files[1]?.compressed_lines[0]?.display_text ?? "", /^export function buildBeta/);
|
|
1103
|
-
assert.equal(payload.execution.code, 0);
|
|
1104
|
-
assert.equal(payload.execution.stderr, "");
|
|
1117
|
+
const content = result.content?.[0]?.text ?? "";
|
|
1118
|
+
assert.match(content, /@@@ src\/alpha\.ts \| typescript/);
|
|
1119
|
+
assert.match(content, /@@@ src\/nested\/beta\.ts \| typescript/);
|
|
1120
|
+
assert.match(content, /^export function buildBeta/m);
|
|
1121
|
+
assert.equal(result.details?.execution.code, 0);
|
|
1122
|
+
assert.deepEqual(result.details?.execution.stderr_lines, undefined);
|
|
1105
1123
|
} finally {
|
|
1106
1124
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
1107
1125
|
}
|
|
1108
1126
|
});
|
|
1109
1127
|
|
|
1110
|
-
test("files-
|
|
1128
|
+
test("files-search returns monolithic search markdown", async () => {
|
|
1111
1129
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
1112
1130
|
const samplePath = path.join(projectBase, "src", "sample.ts");
|
|
1113
1131
|
fs.writeFileSync(samplePath, `/**
|
|
@@ -1128,86 +1146,29 @@ export function buildSample(input: string): string {
|
|
|
1128
1146
|
try {
|
|
1129
1147
|
const pi = createFakePi();
|
|
1130
1148
|
piUsereqExtension(pi);
|
|
1131
|
-
const result = await executeRegisteredTool(pi, "files-
|
|
1149
|
+
const result = await executeRegisteredTool(pi, "files-search", projectBase, {
|
|
1132
1150
|
tag: "FUNCTION",
|
|
1133
1151
|
pattern: "^buildSample$",
|
|
1134
1152
|
files: ["src/sample.ts", "src/missing.ts"],
|
|
1135
1153
|
enableLineNumbers: true,
|
|
1136
1154
|
}) as {
|
|
1137
1155
|
content?: Array<{ type: string; text?: string }>;
|
|
1138
|
-
details?: {
|
|
1139
|
-
summary: {
|
|
1140
|
-
search_status: string;
|
|
1141
|
-
matched_file_count: number;
|
|
1142
|
-
skipped_file_count: number;
|
|
1143
|
-
total_match_count: number;
|
|
1144
|
-
};
|
|
1145
|
-
repository: {
|
|
1146
|
-
file_canonical_paths: string[];
|
|
1147
|
-
};
|
|
1148
|
-
files: Array<{
|
|
1149
|
-
canonical_path: string;
|
|
1150
|
-
status: string;
|
|
1151
|
-
match_count: number;
|
|
1152
|
-
supported_tags: string[];
|
|
1153
|
-
file_doxygen?: { brief?: string[] };
|
|
1154
|
-
matches: Array<{
|
|
1155
|
-
symbol_name: string;
|
|
1156
|
-
qualified_name: string;
|
|
1157
|
-
symbol_kind: string;
|
|
1158
|
-
line_range: [number, number];
|
|
1159
|
-
code_line_count: number;
|
|
1160
|
-
code_lines: Array<{ source_line_number: number; display_text: string; text: string }>;
|
|
1161
|
-
stripped_source_text?: string;
|
|
1162
|
-
doxygen?: {
|
|
1163
|
-
brief?: string[];
|
|
1164
|
-
params?: Array<{ direction: string; parameter_name?: string; value_type?: string; description?: string }>;
|
|
1165
|
-
returns?: string[];
|
|
1166
|
-
satisfies_requirement_ids?: string[];
|
|
1167
|
-
};
|
|
1168
|
-
}>;
|
|
1169
|
-
}>;
|
|
1170
|
-
execution: {
|
|
1171
|
-
code: number;
|
|
1172
|
-
stderr: string;
|
|
1173
|
-
};
|
|
1174
|
-
};
|
|
1156
|
+
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
1175
1157
|
};
|
|
1176
1158
|
|
|
1177
|
-
const
|
|
1178
|
-
assert.
|
|
1179
|
-
assert.
|
|
1180
|
-
assert.
|
|
1181
|
-
assert.
|
|
1182
|
-
assert.equal(
|
|
1183
|
-
assert.deepEqual(
|
|
1184
|
-
assert.deepEqual(payload.files.map((file) => file.status), ["matched", "skipped"]);
|
|
1185
|
-
assert.equal(payload.files[0]?.canonical_path, "src/sample.ts");
|
|
1186
|
-
assert.equal(payload.files[0]?.match_count, 1);
|
|
1187
|
-
assert.ok(payload.files[0]?.supported_tags.includes("FUNCTION"));
|
|
1188
|
-
assert.deepEqual(payload.files[0]?.file_doxygen?.brief, ["Sample source file."]);
|
|
1189
|
-
assert.equal(payload.files[0]?.matches[0]?.symbol_name, "buildSample");
|
|
1190
|
-
assert.equal(payload.files[0]?.matches[0]?.qualified_name, "buildSample");
|
|
1191
|
-
assert.equal(payload.files[0]?.matches[0]?.symbol_kind, "FUNCTION");
|
|
1192
|
-
assert.deepEqual(payload.files[0]?.matches[0]?.line_range, [12, 14]);
|
|
1193
|
-
assert.ok((payload.files[0]?.matches[0]?.code_line_count ?? 0) > 0);
|
|
1194
|
-
assert.equal(payload.files[0]?.matches[0]?.code_lines[0]?.source_line_number, 12);
|
|
1195
|
-
assert.match(payload.files[0]?.matches[0]?.code_lines[0]?.display_text ?? "", /^12: export function buildSample/);
|
|
1196
|
-
assert.match(payload.files[0]?.matches[0]?.stripped_source_text ?? "", /^12: export function buildSample/m);
|
|
1197
|
-
assert.deepEqual(payload.files[0]?.matches[0]?.doxygen?.brief, ["Builds the sample value."]);
|
|
1198
|
-
assert.deepEqual(payload.files[0]?.matches[0]?.doxygen?.params, [
|
|
1199
|
-
{ direction: "in", parameter_name: "input", value_type: "string", description: "Caller-supplied value." },
|
|
1200
|
-
]);
|
|
1201
|
-
assert.deepEqual(payload.files[0]?.matches[0]?.doxygen?.returns, ["{string} Upper-cased result."]);
|
|
1202
|
-
assert.deepEqual(payload.files[0]?.matches[0]?.doxygen?.satisfies_requirement_ids, ["REQ-089"]);
|
|
1203
|
-
assert.equal(payload.execution.code, 0);
|
|
1204
|
-
assert.match(payload.execution.stderr, /skipped: src\/missing\.ts: not_found/);
|
|
1159
|
+
const content = result.content?.[0]?.text ?? "";
|
|
1160
|
+
assert.match(content, /### FUNCTION: `buildSample`/);
|
|
1161
|
+
assert.match(content, /> Signature: `export function buildSample\(input: string\): string`/);
|
|
1162
|
+
assert.match(content, /> Lines: 12-14/);
|
|
1163
|
+
assert.match(content, /^12: export function buildSample/m);
|
|
1164
|
+
assert.equal(result.details?.execution.code, 0);
|
|
1165
|
+
assert.deepEqual(result.details?.execution.stderr_lines, undefined);
|
|
1205
1166
|
} finally {
|
|
1206
1167
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
1207
1168
|
}
|
|
1208
1169
|
});
|
|
1209
1170
|
|
|
1210
|
-
test("
|
|
1171
|
+
test("search returns monolithic repository-scoped construct-search markdown", async () => {
|
|
1211
1172
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
1212
1173
|
fs.writeFileSync(
|
|
1213
1174
|
path.join(projectBase, "src", "alpha.ts"),
|
|
@@ -1230,63 +1191,120 @@ export function buildAlpha(): number {
|
|
|
1230
1191
|
try {
|
|
1231
1192
|
const pi = createFakePi();
|
|
1232
1193
|
piUsereqExtension(pi);
|
|
1233
|
-
const result = await executeRegisteredTool(pi, "
|
|
1194
|
+
const result = await executeRegisteredTool(pi, "search", projectBase, {
|
|
1234
1195
|
tag: "FUNCTION",
|
|
1235
1196
|
pattern: "^build",
|
|
1236
1197
|
enableLineNumbers: false,
|
|
1237
1198
|
}) as {
|
|
1238
1199
|
content?: Array<{ type: string; text?: string }>;
|
|
1239
|
-
details?: {
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1200
|
+
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
const content = result.content?.[0]?.text ?? "";
|
|
1204
|
+
assert.match(content, /### FUNCTION: `buildAlpha`/);
|
|
1205
|
+
assert.match(content, /export function buildAlpha/);
|
|
1206
|
+
assert.doesNotMatch(content, /helperBeta/);
|
|
1207
|
+
assert.equal(result.details?.execution.code, 0);
|
|
1208
|
+
assert.deepEqual(result.details?.execution.stderr_lines, undefined);
|
|
1209
|
+
} finally {
|
|
1210
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
test("source-extraction agent tools preserve leading tabs in emitted content", async () => {
|
|
1215
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
1216
|
+
const samplePath = path.join(projectBase, "src", "sample.go");
|
|
1217
|
+
fs.writeFileSync(samplePath, `package main
|
|
1218
|
+
|
|
1219
|
+
import (
|
|
1220
|
+
"bytes"
|
|
1221
|
+
"context"
|
|
1222
|
+
"fmt"
|
|
1223
|
+
)
|
|
1224
|
+
|
|
1225
|
+
type Runner interface {
|
|
1226
|
+
Run(ctx context.Context) error
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
type Server struct{}
|
|
1230
|
+
|
|
1231
|
+
func (s *Server) Start(ctx context.Context) error {
|
|
1232
|
+
var buffer bytes.Buffer
|
|
1233
|
+
fmt.Println("start", buffer.Len())
|
|
1234
|
+
return nil
|
|
1235
|
+
}
|
|
1236
|
+
`, "utf8");
|
|
1237
|
+
try {
|
|
1238
|
+
const pi = createFakePi();
|
|
1239
|
+
piUsereqExtension(pi);
|
|
1240
|
+
|
|
1241
|
+
const filesReferencesResult = await executeRegisteredTool(pi, "files-references", projectBase, {
|
|
1242
|
+
files: ["src/sample.go"],
|
|
1243
|
+
}) as {
|
|
1244
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1245
|
+
details?: { execution: { code: number } };
|
|
1246
|
+
};
|
|
1247
|
+
assert.match(filesReferencesResult.content?.[0]?.text ?? "", /\tvar buffer bytes\.Buffer/);
|
|
1248
|
+
assert.equal(filesReferencesResult.details?.execution.code, 0);
|
|
1249
|
+
|
|
1250
|
+
const referencesResult = await executeRegisteredTool(pi, "references", projectBase, {}) as {
|
|
1251
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1252
|
+
details?: { execution: { code: number } };
|
|
1253
|
+
};
|
|
1254
|
+
assert.match(referencesResult.content?.[0]?.text ?? "", /\tvar buffer bytes\.Buffer/);
|
|
1255
|
+
assert.equal(referencesResult.details?.execution.code, 0);
|
|
1256
|
+
|
|
1257
|
+
const filesCompressResult = await executeRegisteredTool(pi, "files-compress", projectBase, {
|
|
1258
|
+
files: ["src/sample.go"],
|
|
1259
|
+
enableLineNumbers: true,
|
|
1260
|
+
}) as {
|
|
1261
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1262
|
+
details?: { execution: { code: number } };
|
|
1263
|
+
};
|
|
1264
|
+
assert.match(filesCompressResult.content?.[0]?.text ?? "", /\d+: \t"context"/);
|
|
1265
|
+
assert.equal(filesCompressResult.details?.execution.code, 0);
|
|
1266
|
+
|
|
1267
|
+
const compressResult = await executeRegisteredTool(pi, "compress", projectBase, {
|
|
1268
|
+
enableLineNumbers: true,
|
|
1269
|
+
}) as {
|
|
1270
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1271
|
+
details?: { execution: { code: number } };
|
|
1272
|
+
};
|
|
1273
|
+
assert.match(compressResult.content?.[0]?.text ?? "", /\d+: \t"context"/);
|
|
1274
|
+
assert.equal(compressResult.details?.execution.code, 0);
|
|
1275
|
+
|
|
1276
|
+
const filesSearchResult = await executeRegisteredTool(pi, "files-search", projectBase, {
|
|
1277
|
+
tag: "METHOD|FUNCTION",
|
|
1278
|
+
pattern: "^Start$",
|
|
1279
|
+
files: ["src/sample.go"],
|
|
1280
|
+
enableLineNumbers: true,
|
|
1281
|
+
}) as {
|
|
1282
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1283
|
+
details?: { execution: { code: number } };
|
|
1264
1284
|
};
|
|
1285
|
+
assert.match(filesSearchResult.content?.[0]?.text ?? "", /\d+: \tfmt\.Println\("start", buffer\.Len\(\)\)/);
|
|
1286
|
+
assert.equal(filesSearchResult.details?.execution.code, 0);
|
|
1265
1287
|
|
|
1266
|
-
const
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
assert.
|
|
1275
|
-
assert.equal(
|
|
1276
|
-
assert.match(payload.files[0]?.matches[0]?.code_lines[0]?.display_text ?? "", /^export function buildAlpha/);
|
|
1277
|
-
assert.doesNotMatch(payload.files[0]?.matches[0]?.stripped_source_text ?? "", /^\d+:/m);
|
|
1278
|
-
assert.equal(payload.execution.code, 0);
|
|
1279
|
-
assert.match(payload.execution.stderr, /info: no_match: src\/nested\/beta\.ts/);
|
|
1288
|
+
const searchResult = await executeRegisteredTool(pi, "search", projectBase, {
|
|
1289
|
+
tag: "METHOD|FUNCTION",
|
|
1290
|
+
pattern: "^Start$",
|
|
1291
|
+
enableLineNumbers: true,
|
|
1292
|
+
}) as {
|
|
1293
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1294
|
+
details?: { execution: { code: number } };
|
|
1295
|
+
};
|
|
1296
|
+
assert.match(searchResult.content?.[0]?.text ?? "", /\d+: \tfmt\.Println\("start", buffer\.Len\(\)\)/);
|
|
1297
|
+
assert.equal(searchResult.details?.execution.code, 0);
|
|
1280
1298
|
} finally {
|
|
1281
1299
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
1282
1300
|
}
|
|
1283
1301
|
});
|
|
1284
1302
|
|
|
1285
|
-
test("
|
|
1303
|
+
test("static-check tools return monolithic text payloads with minimal execution details", async () => {
|
|
1286
1304
|
const { projectBase } = initFixtureRepo({
|
|
1287
1305
|
fixtures: [],
|
|
1288
1306
|
staticCheck: {
|
|
1289
|
-
TypeScript: [{ module: "Dummy" }],
|
|
1307
|
+
TypeScript: createStaticCheckLanguageConfig([{ module: "Dummy" }], "enable"),
|
|
1290
1308
|
},
|
|
1291
1309
|
});
|
|
1292
1310
|
fs.writeFileSync(path.join(projectBase, "src", "check.ts"), "export const VALUE = 1;\n", "utf8");
|
|
@@ -1298,97 +1316,46 @@ test("path, static-check, git, docs, and worktree tools return structured JSON p
|
|
|
1298
1316
|
const pi = createFakePi();
|
|
1299
1317
|
piUsereqExtension(pi);
|
|
1300
1318
|
|
|
1301
|
-
const gitPathResult = await executeRegisteredTool(pi, "git-path", projectBase) as {
|
|
1302
|
-
content?: Array<{ text?: string }>;
|
|
1303
|
-
details?: {
|
|
1304
|
-
result: { path_value: string; path_present: boolean };
|
|
1305
|
-
execution: { code: number };
|
|
1306
|
-
};
|
|
1307
|
-
};
|
|
1308
|
-
assert.deepEqual(JSON.parse(gitPathResult.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(gitPathResult.details)));
|
|
1309
|
-
assert.equal(gitPathResult.details?.result.path_value, projectBase);
|
|
1310
|
-
assert.equal(gitPathResult.details?.result.path_present, true);
|
|
1311
|
-
assert.equal(gitPathResult.details?.execution.code, 0);
|
|
1312
|
-
|
|
1313
|
-
const basePathResult = await executeRegisteredTool(pi, "get-base-path", projectBase) as {
|
|
1314
|
-
details?: {
|
|
1315
|
-
result: { path_value: string };
|
|
1316
|
-
execution: { code: number };
|
|
1317
|
-
};
|
|
1318
|
-
};
|
|
1319
|
-
assert.equal(basePathResult.details?.result.path_value, projectBase);
|
|
1320
|
-
assert.equal(basePathResult.details?.execution.code, 0);
|
|
1321
|
-
|
|
1322
|
-
const gitCheckResult = await executeRegisteredTool(pi, "git-check", projectBase) as {
|
|
1323
|
-
details?: { result: { git_path_present: boolean; status: string }; execution: { code: number } };
|
|
1324
|
-
};
|
|
1325
|
-
assert.equal(gitCheckResult.details?.result.git_path_present, true);
|
|
1326
|
-
assert.equal(gitCheckResult.details?.result.status, "clean");
|
|
1327
|
-
assert.equal(gitCheckResult.details?.execution.code, 0);
|
|
1328
|
-
|
|
1329
|
-
const docsCheckResult = await executeRegisteredTool(pi, "docs-check", projectBase) as {
|
|
1330
|
-
details?: {
|
|
1331
|
-
summary: { missing_file_count: number; present_file_count: number };
|
|
1332
|
-
files: Array<{ file_name: string; status: string; prompt_command: string }>;
|
|
1333
|
-
execution: { code: number };
|
|
1334
|
-
};
|
|
1335
|
-
};
|
|
1336
|
-
assert.equal(docsCheckResult.details?.summary.missing_file_count, 0);
|
|
1337
|
-
assert.equal(docsCheckResult.details?.summary.present_file_count, 3);
|
|
1338
|
-
assert.deepEqual(docsCheckResult.details?.files.map((file) => file.file_name), ["REQUIREMENTS.md", "WORKFLOW.md", "REFERENCES.md"]);
|
|
1339
|
-
assert.ok(docsCheckResult.details?.files.every((file) => file.status === "present"));
|
|
1340
|
-
assert.equal(docsCheckResult.details?.execution.code, 0);
|
|
1341
|
-
|
|
1342
1319
|
const filesStaticCheckResult = await executeRegisteredTool(pi, "files-static-check", projectBase, {
|
|
1343
1320
|
files: ["src/check.ts", "src/missing.ts"],
|
|
1344
1321
|
}) as {
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
files: Array<{ canonical_path: string; language_name?: string; status: string; configured_checker_modules: string[] }>;
|
|
1348
|
-
execution: { code: number };
|
|
1349
|
-
};
|
|
1322
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1323
|
+
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
1350
1324
|
};
|
|
1351
|
-
assert.
|
|
1352
|
-
assert.equal(filesStaticCheckResult.details?.summary.skipped_file_count, 1);
|
|
1353
|
-
assert.equal(filesStaticCheckResult.details?.summary.total_configured_checker_count, 1);
|
|
1354
|
-
assert.equal(filesStaticCheckResult.details?.files[0]?.canonical_path, "src/check.ts");
|
|
1355
|
-
assert.equal(filesStaticCheckResult.details?.files[0]?.language_name, "TypeScript");
|
|
1356
|
-
assert.deepEqual(filesStaticCheckResult.details?.files[0]?.configured_checker_modules, ["Dummy"]);
|
|
1357
|
-
assert.equal(filesStaticCheckResult.details?.files[1]?.status, "skipped");
|
|
1325
|
+
assert.match(filesStaticCheckResult.content?.[0]?.text ?? "", /src\/missing\.ts/);
|
|
1358
1326
|
assert.equal(filesStaticCheckResult.details?.execution.code, 0);
|
|
1327
|
+
assert.ok(filesStaticCheckResult.details?.execution.stderr_lines?.some((line) => /src\/missing\.ts/.test(line)));
|
|
1359
1328
|
|
|
1360
1329
|
const staticCheckResult = await executeRegisteredTool(pi, "static-check", projectBase) as {
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
files: Array<{ canonical_path: string; status: string }>;
|
|
1364
|
-
execution: { code: number };
|
|
1365
|
-
};
|
|
1330
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1331
|
+
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
1366
1332
|
};
|
|
1367
|
-
assert.equal(staticCheckResult.
|
|
1368
|
-
assert.equal(staticCheckResult.details?.files[0]?.canonical_path, "src/check.ts");
|
|
1369
|
-
assert.equal(staticCheckResult.details?.files[0]?.status, "selected");
|
|
1333
|
+
assert.equal(staticCheckResult.content?.[0]?.text ?? "", "");
|
|
1370
1334
|
assert.equal(staticCheckResult.details?.execution.code, 0);
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
const
|
|
1387
|
-
|
|
1335
|
+
assert.deepEqual(staticCheckResult.details?.execution.stderr_lines, undefined);
|
|
1336
|
+
|
|
1337
|
+
fs.writeFileSync(
|
|
1338
|
+
getProjectConfigPath(projectBase),
|
|
1339
|
+
`${JSON.stringify({
|
|
1340
|
+
...getDefaultConfig(projectBase),
|
|
1341
|
+
"docs-dir": DEFAULT_DOCS_DIR,
|
|
1342
|
+
"tests-dir": "tests",
|
|
1343
|
+
"src-dir": ["src"],
|
|
1344
|
+
"static-check": {
|
|
1345
|
+
TypeScript: createStaticCheckLanguageConfig([{ module: "Dummy" }], "disable"),
|
|
1346
|
+
},
|
|
1347
|
+
}, null, 2)}\n`,
|
|
1348
|
+
"utf8",
|
|
1349
|
+
);
|
|
1350
|
+
const disabledFilesStaticCheckResult = await executeRegisteredTool(pi, "files-static-check", projectBase, {
|
|
1351
|
+
files: ["src/check.ts"],
|
|
1352
|
+
}) as {
|
|
1353
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1354
|
+
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
1388
1355
|
};
|
|
1389
|
-
assert.equal(
|
|
1390
|
-
assert.
|
|
1391
|
-
assert.
|
|
1356
|
+
assert.equal(disabledFilesStaticCheckResult.content?.[0]?.text ?? "", "");
|
|
1357
|
+
assert.equal(disabledFilesStaticCheckResult.details?.execution.code, 0);
|
|
1358
|
+
assert.deepEqual(disabledFilesStaticCheckResult.details?.execution.stderr_lines, undefined);
|
|
1392
1359
|
} finally {
|
|
1393
1360
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
1394
1361
|
}
|
|
@@ -1407,6 +1374,57 @@ test("configuration menu saves updated docs-dir in project config", async () =>
|
|
|
1407
1374
|
assert.equal(config["docs-dir"], "docs/custom");
|
|
1408
1375
|
});
|
|
1409
1376
|
|
|
1377
|
+
test("configuration menu persists trailing-slash-free relative directory values", async () => {
|
|
1378
|
+
const cwd = createTempDir("pi-usereq-menu-path-contracts-");
|
|
1379
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1380
|
+
const pi = createFakePi();
|
|
1381
|
+
piUsereqExtension(pi);
|
|
1382
|
+
const command = pi.commands.get("pi-usereq");
|
|
1383
|
+
assert.ok(command);
|
|
1384
|
+
const ctx = createFakeCtx(cwd, {
|
|
1385
|
+
selects: [
|
|
1386
|
+
"docs-dir",
|
|
1387
|
+
"tests-dir",
|
|
1388
|
+
"src-dir",
|
|
1389
|
+
"add-src-dir-entry",
|
|
1390
|
+
"Save and close",
|
|
1391
|
+
"Save and close",
|
|
1392
|
+
],
|
|
1393
|
+
inputs: ["docs/custom/", "tests/custom/", "src/custom/"],
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
await command!.handler("", ctx);
|
|
1397
|
+
|
|
1398
|
+
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
1399
|
+
assert.equal(config["docs-dir"], "docs/custom");
|
|
1400
|
+
assert.equal(config["tests-dir"], "tests/custom");
|
|
1401
|
+
assert.deepEqual(config["src-dir"], ["src", "src/custom"]);
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
test("configuration menu forces worktree rows off and dim when auto git commit is disabled", async () => {
|
|
1405
|
+
const cwd = createTempDir("pi-usereq-menu-worktree-");
|
|
1406
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1407
|
+
const pi = createFakePi();
|
|
1408
|
+
piUsereqExtension(pi);
|
|
1409
|
+
const command = pi.commands.get("pi-usereq");
|
|
1410
|
+
assert.ok(command);
|
|
1411
|
+
const ctx = createFakeCtx(cwd, {
|
|
1412
|
+
selects: ["Auto git commit", "Git worktree", "Worktree prefix", "Save and close"],
|
|
1413
|
+
inputs: ["custom-prefix-"],
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
await command!.handler("", ctx);
|
|
1417
|
+
|
|
1418
|
+
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
1419
|
+
const defaultConfig = getDefaultConfig(cwd);
|
|
1420
|
+
const renderedLockedMenu = (ctx.__state.customRenderLines[1] ?? []).join("\n");
|
|
1421
|
+
assert.equal(config.AUTO_GIT_COMMIT, "disable");
|
|
1422
|
+
assert.equal(config.GIT_WORKTREE_ENABLED, "disable");
|
|
1423
|
+
assert.equal(config.GIT_WORKTREE_PREFIX, defaultConfig.GIT_WORKTREE_PREFIX);
|
|
1424
|
+
assert.match(renderedLockedMenu, /<dim>Git worktree/);
|
|
1425
|
+
assert.match(renderedLockedMenu, /<dim>Worktree prefix/);
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1410
1428
|
test("configuration menu omits prompt-delivery controls and persisted config omits removed runtime fields", async () => {
|
|
1411
1429
|
const cwd = createTempDir("pi-usereq-menu-current-session-");
|
|
1412
1430
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
@@ -1419,11 +1437,8 @@ test("configuration menu omits prompt-delivery controls and persisted config omi
|
|
|
1419
1437
|
await command!.handler("", ctx);
|
|
1420
1438
|
|
|
1421
1439
|
const menuItems = ctx.__state.selectCalls[0]?.items ?? [];
|
|
1422
|
-
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
1423
1440
|
assert.equal(menuItems.some((item) => item.includes("reset-context")), false);
|
|
1424
|
-
assert.equal(
|
|
1425
|
-
assert.equal(Object.prototype.hasOwnProperty.call(config, "base-path"), false);
|
|
1426
|
-
assert.equal(Object.prototype.hasOwnProperty.call(config, "git-path"), false);
|
|
1441
|
+
assert.equal(fs.existsSync(getProjectConfigPath(cwd)), false);
|
|
1427
1442
|
});
|
|
1428
1443
|
|
|
1429
1444
|
test("configuration menus expose show-config ordering and omit overview or notification-reference rows", async () => {
|
|
@@ -1435,26 +1450,33 @@ test("configuration menus expose show-config ordering and omit overview or notif
|
|
|
1435
1450
|
piUsereqExtension(pi);
|
|
1436
1451
|
const command = pi.commands.get("pi-usereq");
|
|
1437
1452
|
assert.ok(command);
|
|
1438
|
-
const ctx = createFakeCtx(cwd, { selects: ["
|
|
1453
|
+
const ctx = createFakeCtx(cwd, { selects: ["notifications", "Save and close", "Save and close"] });
|
|
1439
1454
|
|
|
1440
1455
|
await command!.handler("", ctx);
|
|
1441
1456
|
|
|
1442
1457
|
const renderedMenu = (ctx.__state.customRenderLines[0] ?? []).join("\n");
|
|
1458
|
+
const renderedNotificationsMenu = (ctx.__state.customRenderLines[1] ?? []).join("\n");
|
|
1443
1459
|
assert.deepEqual(ctx.__state.selectCalls[0]?.items ?? [], [
|
|
1444
1460
|
"Document directory",
|
|
1445
1461
|
"Source-code directories",
|
|
1446
1462
|
"Unit tests directory",
|
|
1463
|
+
"Auto git commit",
|
|
1464
|
+
"Git worktree",
|
|
1465
|
+
"Worktree prefix",
|
|
1447
1466
|
"Language static code checkers",
|
|
1448
1467
|
"Enable tools",
|
|
1449
1468
|
"Notifications",
|
|
1469
|
+
"Debug",
|
|
1450
1470
|
"Show configuration",
|
|
1451
1471
|
"Reset defaults",
|
|
1452
|
-
"Save and close",
|
|
1453
1472
|
]);
|
|
1454
1473
|
assert.ok(renderedMenu.includes(buildExpectedShowConfigPath(cwd)));
|
|
1455
1474
|
assert.ok(renderedMenu.includes("notification:off • sound:none • pushover:off"));
|
|
1475
|
+
assert.ok(renderedMenu.includes("disable"));
|
|
1456
1476
|
assert.doesNotMatch(renderedMenu, /beep:/);
|
|
1457
|
-
assert.
|
|
1477
|
+
assert.match(renderedNotificationsMenu, /<dim>Enable pushover/);
|
|
1478
|
+
assert.match(renderedNotificationsMenu, /<dim>off<\/dim>/);
|
|
1479
|
+
assert.deepEqual(ctx.__state.selectCalls[1]?.items ?? [], [
|
|
1458
1480
|
"Enable notification",
|
|
1459
1481
|
"Notification events",
|
|
1460
1482
|
"Notify command",
|
|
@@ -1472,12 +1494,34 @@ test("configuration menus expose show-config ordering and omit overview or notif
|
|
|
1472
1494
|
"Pushover User Key/Delivery Group Key",
|
|
1473
1495
|
"Pushover Token/API Token Key",
|
|
1474
1496
|
"Reset defaults",
|
|
1475
|
-
"Save and close",
|
|
1476
1497
|
]);
|
|
1477
1498
|
});
|
|
1478
1499
|
|
|
1479
|
-
test("
|
|
1480
|
-
const cwd = createTempDir("pi-usereq-menu-
|
|
1500
|
+
test("show configuration saves pending config, closes the menu, and writes the persisted project config file text into the editor", async () => {
|
|
1501
|
+
const cwd = createTempDir("pi-usereq-menu-show-config-");
|
|
1502
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1503
|
+
const pi = createFakePi();
|
|
1504
|
+
piUsereqExtension(pi);
|
|
1505
|
+
const command = pi.commands.get("pi-usereq");
|
|
1506
|
+
assert.ok(command);
|
|
1507
|
+
const ctx = createFakeCtx(cwd, {
|
|
1508
|
+
selects: ["docs-dir", "show-config"],
|
|
1509
|
+
inputs: ["docs/custom"],
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
await command!.handler("", ctx);
|
|
1513
|
+
|
|
1514
|
+
const persistedText = fs.readFileSync(getProjectConfigPath(cwd), "utf8");
|
|
1515
|
+
assert.equal(ctx.__state.editorText, persistedText);
|
|
1516
|
+
assert.match(ctx.__state.editorText, /"docs-dir": "docs\/custom"/);
|
|
1517
|
+
assert.equal(
|
|
1518
|
+
ctx.__state.selectCalls.filter((call) => call.title === "pi-usereq").length,
|
|
1519
|
+
2,
|
|
1520
|
+
);
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
test("descendant configuration menus end with Reset defaults only", async () => {
|
|
1524
|
+
const cwd = createTempDir("pi-usereq-menu-terminal-rows-");
|
|
1481
1525
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1482
1526
|
const pi = createFakePi();
|
|
1483
1527
|
piUsereqExtension(pi);
|
|
@@ -1486,37 +1530,50 @@ test("notifications menu preserves focus on toggled and edited rows", async () =
|
|
|
1486
1530
|
const ctx = createFakeCtx(cwd, {
|
|
1487
1531
|
selects: [
|
|
1488
1532
|
"notifications",
|
|
1489
|
-
"
|
|
1490
|
-
"
|
|
1533
|
+
"Enable sound",
|
|
1534
|
+
"Save and close",
|
|
1535
|
+
"Pushover priority",
|
|
1536
|
+
"Save and close",
|
|
1537
|
+
"Save and close",
|
|
1538
|
+
"Enable tools",
|
|
1539
|
+
"Enable tools",
|
|
1540
|
+
"Save and close",
|
|
1541
|
+
"Save and close",
|
|
1542
|
+
"Debug",
|
|
1543
|
+
"Save and close",
|
|
1544
|
+
"Language static code checkers",
|
|
1545
|
+
"Add static code checker",
|
|
1546
|
+
"Save and close",
|
|
1547
|
+
"Remove static code checker",
|
|
1548
|
+
"Save and close",
|
|
1549
|
+
"Save and close",
|
|
1550
|
+
"Source-code directories",
|
|
1551
|
+
"Remove source-code directory",
|
|
1491
1552
|
"Save and close",
|
|
1492
|
-
"Notify command",
|
|
1493
1553
|
"Save and close",
|
|
1494
1554
|
"Save and close",
|
|
1495
1555
|
],
|
|
1496
|
-
inputs: ["echo notify"],
|
|
1497
1556
|
});
|
|
1498
1557
|
|
|
1499
1558
|
await command!.handler("", ctx);
|
|
1500
1559
|
|
|
1501
|
-
const
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1560
|
+
const getTerminalRows = (title: string, marker?: string): string[] => {
|
|
1561
|
+
const call = ctx.__state.selectCalls.find((entry) => entry.title === title && (marker === undefined || entry.items.includes(marker)));
|
|
1562
|
+
assert.ok(call, `missing menu ${title}`);
|
|
1563
|
+
return call.items.slice(-1);
|
|
1564
|
+
};
|
|
1505
1565
|
|
|
1506
|
-
|
|
1507
|
-
assert.deepEqual(
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
]);
|
|
1514
|
-
assert.equal(notificationEventCalls[0]?.selectedChoiceId, "notify-on-completed");
|
|
1515
|
-
assert.equal(notificationEventCalls[1]?.selectedChoiceId, "notify-on-failed");
|
|
1566
|
+
assert.deepEqual(getTerminalRows("Enable sound", "high"), ["Reset defaults"]);
|
|
1567
|
+
assert.deepEqual(getTerminalRows("Pushover priority", "High"), ["Reset defaults"]);
|
|
1568
|
+
assert.deepEqual(getTerminalRows("Enable tools", "static-check"), ["Reset defaults"]);
|
|
1569
|
+
assert.deepEqual(getTerminalRows("Debug", "Log on status"), ["Reset defaults"]);
|
|
1570
|
+
assert.deepEqual(getTerminalRows("static-check language", "Python"), ["Reset defaults"]);
|
|
1571
|
+
assert.deepEqual(getTerminalRows("Remove static code checker"), ["Reset defaults"]);
|
|
1572
|
+
assert.deepEqual(getTerminalRows("Remove source-code directory", "src"), ["Reset defaults"]);
|
|
1516
1573
|
});
|
|
1517
1574
|
|
|
1518
|
-
test("
|
|
1519
|
-
const cwd = createTempDir("pi-usereq-menu-
|
|
1575
|
+
test("debug menu dims locked rows and persists debug settings with focus-preserving re-renders", async () => {
|
|
1576
|
+
const cwd = createTempDir("pi-usereq-menu-debug-");
|
|
1520
1577
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1521
1578
|
const pi = createFakePi();
|
|
1522
1579
|
piUsereqExtension(pi);
|
|
@@ -1524,104 +1581,2313 @@ test("notifications reset defaults preserves non-notification settings", async (
|
|
|
1524
1581
|
assert.ok(command);
|
|
1525
1582
|
const ctx = createFakeCtx(cwd, {
|
|
1526
1583
|
selects: [
|
|
1527
|
-
"
|
|
1528
|
-
"
|
|
1529
|
-
"
|
|
1530
|
-
"
|
|
1531
|
-
"
|
|
1532
|
-
"
|
|
1584
|
+
"Debug",
|
|
1585
|
+
"Debug",
|
|
1586
|
+
"Log file",
|
|
1587
|
+
"Log on status",
|
|
1588
|
+
"any",
|
|
1589
|
+
"Status changes",
|
|
1590
|
+
"Workflow events",
|
|
1591
|
+
"files-tokens",
|
|
1592
|
+
"req-analyze",
|
|
1533
1593
|
"Save and close",
|
|
1534
1594
|
"Save and close",
|
|
1535
1595
|
],
|
|
1536
|
-
inputs: ["
|
|
1596
|
+
inputs: ["logs/debug-output.json"],
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
await command!.handler("", ctx);
|
|
1600
|
+
|
|
1601
|
+
const renderedInitialDebugMenu = (ctx.__state.customRenderLines[1] ?? []).join("\n");
|
|
1602
|
+
const persistedConfig = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
1603
|
+
const debugCalls = ctx.__state.selectCalls.filter((call) => call.title === "Debug");
|
|
1604
|
+
assert.match(renderedInitialDebugMenu, /<dim>Log file/);
|
|
1605
|
+
assert.match(renderedInitialDebugMenu, /<dim>Log on status/);
|
|
1606
|
+
assert.match(renderedInitialDebugMenu, /<dim>Status changes/);
|
|
1607
|
+
assert.match(renderedInitialDebugMenu, /<dim>Workflow events/);
|
|
1608
|
+
assert.match(renderedInitialDebugMenu, /<dim>compress/);
|
|
1609
|
+
assert.equal(persistedConfig.DEBUG_ENABLED, "enable");
|
|
1610
|
+
assert.equal(persistedConfig.DEBUG_LOG_FILE, "logs/debug-output.json");
|
|
1611
|
+
assert.equal(persistedConfig.DEBUG_STATUS_CHANGES, "enable");
|
|
1612
|
+
assert.equal(persistedConfig.DEBUG_WORKFLOW_EVENTS, "enable");
|
|
1613
|
+
assert.equal(persistedConfig.DEBUG_LOG_ON_STATUS, "any");
|
|
1614
|
+
assert.deepEqual(persistedConfig.DEBUG_ENABLED_TOOLS, ["files-tokens"]);
|
|
1615
|
+
assert.deepEqual(persistedConfig.DEBUG_ENABLED_PROMPTS, ["req-analyze"]);
|
|
1616
|
+
assert.equal(debugCalls[1]?.selectedChoiceId, "debug-enabled");
|
|
1617
|
+
assert.equal(debugCalls[2]?.selectedChoiceId, "debug-log-file");
|
|
1618
|
+
assert.equal(debugCalls[3]?.selectedChoiceId, "debug-log-on-status");
|
|
1619
|
+
assert.equal(debugCalls[4]?.selectedChoiceId, "debug-status-changes");
|
|
1620
|
+
assert.equal(debugCalls[5]?.selectedChoiceId, "debug-workflow-events");
|
|
1621
|
+
assert.equal(debugCalls[6]?.selectedChoiceId, "debug-tool:files-tokens");
|
|
1622
|
+
assert.equal(debugCalls[7]?.selectedChoiceId, "debug-prompt:req-analyze");
|
|
1623
|
+
|
|
1624
|
+
const resetCtx = createFakeCtx(cwd, {
|
|
1625
|
+
selects: ["Debug", "Reset defaults", "Approve reset", "Save and close"],
|
|
1537
1626
|
});
|
|
1627
|
+
await command!.handler("", resetCtx);
|
|
1628
|
+
const resetConfig = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
1629
|
+
assert.equal(resetConfig.DEBUG_ENABLED, "disable");
|
|
1630
|
+
assert.equal(resetConfig.DEBUG_LOG_FILE, DEFAULT_DEBUG_LOG_FILE);
|
|
1631
|
+
assert.equal(resetConfig.DEBUG_STATUS_CHANGES, "disable");
|
|
1632
|
+
assert.equal(resetConfig.DEBUG_WORKFLOW_EVENTS, DEFAULT_DEBUG_WORKFLOW_EVENTS);
|
|
1633
|
+
assert.equal(resetConfig.DEBUG_LOG_ON_STATUS, DEFAULT_DEBUG_LOG_ON_STATUS);
|
|
1634
|
+
assert.deepEqual(resetConfig.DEBUG_ENABLED_TOOLS, []);
|
|
1635
|
+
assert.deepEqual(resetConfig.DEBUG_ENABLED_PROMPTS, []);
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
test("debug menu rows derive from canonical tool and prompt inventories", async () => {
|
|
1639
|
+
const cwd = createTempDir("pi-usereq-menu-debug-inventory-");
|
|
1640
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1641
|
+
const pi = createFakePi();
|
|
1642
|
+
piUsereqExtension(pi);
|
|
1643
|
+
const command = pi.commands.get("pi-usereq");
|
|
1644
|
+
assert.ok(command);
|
|
1645
|
+
const ctx = createFakeCtx(cwd, {
|
|
1646
|
+
selects: ["Debug", "Save and close"]
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1649
|
+
await command!.handler("", ctx);
|
|
1650
|
+
|
|
1651
|
+
assert.deepEqual(ctx.__state.selectCalls[1]?.items ?? [], [
|
|
1652
|
+
"Debug",
|
|
1653
|
+
"Log file",
|
|
1654
|
+
"Log on status",
|
|
1655
|
+
"Status changes",
|
|
1656
|
+
"Workflow events",
|
|
1657
|
+
...[
|
|
1658
|
+
...PI_USEREQ_CUSTOM_TOOL_NAMES,
|
|
1659
|
+
...PI_USEREQ_EMBEDDED_TOOL_NAMES,
|
|
1660
|
+
].sort(comparePiUsereqStartupToolNames),
|
|
1661
|
+
...DEBUG_PROMPT_NAMES,
|
|
1662
|
+
"Reset defaults",
|
|
1663
|
+
]);
|
|
1664
|
+
assert.deepEqual(
|
|
1665
|
+
DEBUG_PROMPT_NAMES,
|
|
1666
|
+
PROMPT_COMMAND_NAMES.map((promptName) => `req-${promptName}`),
|
|
1667
|
+
);
|
|
1668
|
+
});
|
|
1669
|
+
|
|
1670
|
+
test("tool debug logging honors global enablement and workflow-status filters", async () => {
|
|
1671
|
+
const idleBase = createTempDir("pi-usereq-tool-debug-idle-");
|
|
1672
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(idleBase)), { recursive: true });
|
|
1673
|
+
fs.writeFileSync(
|
|
1674
|
+
getProjectConfigPath(idleBase),
|
|
1675
|
+
`${JSON.stringify({
|
|
1676
|
+
...getDefaultConfig(idleBase),
|
|
1677
|
+
DEBUG_ENABLED: "enable",
|
|
1678
|
+
DEBUG_LOG_FILE: "tool-debug.json",
|
|
1679
|
+
DEBUG_LOG_ON_STATUS: "any",
|
|
1680
|
+
DEBUG_ENABLED_TOOLS: ["bash"],
|
|
1681
|
+
}, null, 2)}\n`,
|
|
1682
|
+
"utf8",
|
|
1683
|
+
);
|
|
1684
|
+
const idlePi = createFakePi();
|
|
1685
|
+
piUsereqExtension(idlePi);
|
|
1686
|
+
const idleCtx = createFakeCtx(idleBase);
|
|
1687
|
+
await idlePi.emit("session_start", { reason: "startup" }, idleCtx);
|
|
1688
|
+
await idlePi.emit("tool_result", {
|
|
1689
|
+
toolName: "bash",
|
|
1690
|
+
input: { command: "echo idle" },
|
|
1691
|
+
content: [{ type: "text", text: "idle" }],
|
|
1692
|
+
details: { stdout: "idle" },
|
|
1693
|
+
isError: false,
|
|
1694
|
+
}, idleCtx);
|
|
1695
|
+
const idleLog = JSON.parse(fs.readFileSync(path.join(idleBase, "tool-debug.json"), "utf8"));
|
|
1696
|
+
assert.equal(idleLog.length, 1);
|
|
1697
|
+
assert.equal(idleLog[0].category, "tool");
|
|
1698
|
+
assert.equal(idleLog[0].name, "bash");
|
|
1699
|
+
assert.equal(idleLog[0].workflow_state, "idle");
|
|
1700
|
+
assert.deepEqual(idleLog[0].input, { command: "echo idle" });
|
|
1701
|
+
assert.deepEqual(idleLog[0].result, {
|
|
1702
|
+
content: [{ type: "text", text: "idle" }],
|
|
1703
|
+
details: { stdout: "idle" },
|
|
1704
|
+
});
|
|
1705
|
+
assert.equal(idleLog[0].is_error, false);
|
|
1706
|
+
|
|
1707
|
+
const runningFixture = initFixtureRepo();
|
|
1708
|
+
writeProjectConfigOverrides(runningFixture.projectBase, {
|
|
1709
|
+
GIT_WORKTREE_ENABLED: "disable",
|
|
1710
|
+
DEBUG_ENABLED: "enable",
|
|
1711
|
+
DEBUG_LOG_FILE: "tool-debug.json",
|
|
1712
|
+
DEBUG_LOG_ON_STATUS: "running",
|
|
1713
|
+
DEBUG_ENABLED_TOOLS: ["files-tokens"],
|
|
1714
|
+
});
|
|
1715
|
+
const runningPi = createFakePi();
|
|
1716
|
+
piUsereqExtension(runningPi);
|
|
1717
|
+
const runningCtx = createFakeCtx(runningFixture.projectBase);
|
|
1718
|
+
await runningPi.emit("session_start", { reason: "startup" }, runningCtx);
|
|
1719
|
+
await runningPi.emit("tool_result", {
|
|
1720
|
+
toolName: "files-tokens",
|
|
1721
|
+
input: { files: ["src/fixture_python.py"] },
|
|
1722
|
+
content: [{ type: "text", text: "idle" }],
|
|
1723
|
+
details: { summary: { counted_file_count: 1 } },
|
|
1724
|
+
isError: false,
|
|
1725
|
+
}, runningCtx);
|
|
1726
|
+
assert.equal(fs.existsSync(path.join(runningFixture.projectBase, "tool-debug.json")), false);
|
|
1727
|
+
const runningCommand = runningPi.commands.get("req-implement");
|
|
1728
|
+
assert.ok(runningCommand);
|
|
1729
|
+
await runningCommand!.handler("debug tools", runningCtx);
|
|
1730
|
+
await runningPi.emit("tool_result", {
|
|
1731
|
+
toolName: "files-tokens",
|
|
1732
|
+
input: { files: ["src/fixture_python.py"] },
|
|
1733
|
+
content: [{ type: "text", text: "running" }],
|
|
1734
|
+
details: { summary: { counted_file_count: 1 } },
|
|
1735
|
+
isError: false,
|
|
1736
|
+
}, runningCtx);
|
|
1737
|
+
const runningLog = JSON.parse(fs.readFileSync(path.join(runningFixture.projectBase, "tool-debug.json"), "utf8"));
|
|
1738
|
+
assert.equal(runningLog.length, 1);
|
|
1739
|
+
assert.equal(runningLog[0].workflow_state, "running");
|
|
1740
|
+
assert.equal(runningLog[0].name, "files-tokens");
|
|
1741
|
+
|
|
1742
|
+
const disabledBase = createTempDir("pi-usereq-tool-debug-disabled-");
|
|
1743
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(disabledBase)), { recursive: true });
|
|
1744
|
+
fs.writeFileSync(
|
|
1745
|
+
getProjectConfigPath(disabledBase),
|
|
1746
|
+
`${JSON.stringify({
|
|
1747
|
+
...getDefaultConfig(disabledBase),
|
|
1748
|
+
DEBUG_ENABLED: "disable",
|
|
1749
|
+
DEBUG_LOG_FILE: "tool-debug.json",
|
|
1750
|
+
DEBUG_LOG_ON_STATUS: "any",
|
|
1751
|
+
DEBUG_ENABLED_TOOLS: ["bash"],
|
|
1752
|
+
}, null, 2)}\n`,
|
|
1753
|
+
"utf8",
|
|
1754
|
+
);
|
|
1755
|
+
const disabledPi = createFakePi();
|
|
1756
|
+
piUsereqExtension(disabledPi);
|
|
1757
|
+
const disabledCtx = createFakeCtx(disabledBase);
|
|
1758
|
+
await disabledPi.emit("session_start", { reason: "startup" }, disabledCtx);
|
|
1759
|
+
await disabledPi.emit("tool_result", {
|
|
1760
|
+
toolName: "bash",
|
|
1761
|
+
input: { command: "echo disabled" },
|
|
1762
|
+
content: [{ type: "text", text: "disabled" }],
|
|
1763
|
+
details: { stdout: "disabled" },
|
|
1764
|
+
isError: false,
|
|
1765
|
+
}, disabledCtx);
|
|
1766
|
+
assert.equal(fs.existsSync(path.join(disabledBase, "tool-debug.json")), false);
|
|
1767
|
+
});
|
|
1768
|
+
|
|
1769
|
+
test("prompt debug logging captures failing and successful prompt orchestration entries", async () => {
|
|
1770
|
+
const failingFixture = initFixtureRepo();
|
|
1771
|
+
fs.rmSync(path.join(failingFixture.projectBase, DEFAULT_DOCS_DIR, "WORKFLOW.md"), { force: true });
|
|
1772
|
+
assert.equal(spawnSync("git", ["add", "-A"], { cwd: failingFixture.projectBase, encoding: "utf8" }).status, 0);
|
|
1773
|
+
assert.equal(
|
|
1774
|
+
spawnSync("git", ["commit", "-m", "remove workflow"], { cwd: failingFixture.projectBase, encoding: "utf8" }).status,
|
|
1775
|
+
0,
|
|
1776
|
+
);
|
|
1777
|
+
writeProjectConfigOverrides(failingFixture.projectBase, {
|
|
1778
|
+
DEBUG_ENABLED: "enable",
|
|
1779
|
+
DEBUG_LOG_FILE: "prompt-debug.json",
|
|
1780
|
+
DEBUG_STATUS_CHANGES: "enable",
|
|
1781
|
+
DEBUG_WORKFLOW_EVENTS: "enable",
|
|
1782
|
+
DEBUG_LOG_ON_STATUS: "any",
|
|
1783
|
+
DEBUG_ENABLED_PROMPTS: ["req-change"],
|
|
1784
|
+
});
|
|
1785
|
+
const failingPi = createFakePi();
|
|
1786
|
+
piUsereqExtension(failingPi);
|
|
1787
|
+
const failingCtx = createFakeCtx(failingFixture.projectBase);
|
|
1788
|
+
await failingPi.emit("session_start", { reason: "startup" }, failingCtx);
|
|
1789
|
+
const failingCommand = failingPi.commands.get("req-change");
|
|
1790
|
+
assert.ok(failingCommand);
|
|
1791
|
+
await assert.rejects(async () => {
|
|
1792
|
+
await failingCommand!.handler("debug prompt", failingCtx);
|
|
1793
|
+
}, /Git status unclear|WORKFLOW\.md/);
|
|
1794
|
+
const failingLog = JSON.parse(fs.readFileSync(path.join(failingFixture.projectBase, "prompt-debug.json"), "utf8"));
|
|
1795
|
+
assert.ok(failingLog.some((entry: any) => entry.action === "workflow_state" && entry.result?.workflow_state === "error"));
|
|
1796
|
+
|
|
1797
|
+
const successFixture = initFixtureRepo();
|
|
1798
|
+
writeProjectConfigOverrides(successFixture.projectBase, {
|
|
1799
|
+
DEBUG_ENABLED: "enable",
|
|
1800
|
+
DEBUG_LOG_FILE: "prompt-debug.json",
|
|
1801
|
+
DEBUG_STATUS_CHANGES: "enable",
|
|
1802
|
+
DEBUG_WORKFLOW_EVENTS: "enable",
|
|
1803
|
+
DEBUG_LOG_ON_STATUS: "any",
|
|
1804
|
+
DEBUG_ENABLED_PROMPTS: ["req-implement"],
|
|
1805
|
+
});
|
|
1806
|
+
const successPi = createFakePi();
|
|
1807
|
+
piUsereqExtension(successPi);
|
|
1808
|
+
const successCtx = createFakeCtx(successFixture.projectBase);
|
|
1809
|
+
await successPi.emit("session_start", { reason: "startup" }, successCtx);
|
|
1810
|
+
const successCommand = successPi.commands.get("req-implement");
|
|
1811
|
+
assert.ok(successCommand);
|
|
1812
|
+
await successCommand!.handler("debug prompt", successCtx);
|
|
1813
|
+
const promptText = String(successPi.sentUserMessages.at(-1)?.content ?? "");
|
|
1814
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
1815
|
+
assert.ok(worktreeMatch, promptText);
|
|
1816
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
1817
|
+
await successPi.emit("before_agent_start", {}, successCtx);
|
|
1818
|
+
await successPi.emit("agent_start", {}, successCtx);
|
|
1819
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "debug.ts"), "export const debug = true;\n", "utf8");
|
|
1820
|
+
assert.equal(spawnSync("git", ["add", "src/debug.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
1821
|
+
const worktreeCommit = spawnSync("git", ["commit", "-m", "debug prompt"], {
|
|
1822
|
+
cwd: executionBasePath,
|
|
1823
|
+
encoding: "utf8",
|
|
1824
|
+
});
|
|
1825
|
+
assert.equal(worktreeCommit.status, 0, worktreeCommit.stderr);
|
|
1826
|
+
await successPi.emit("agent_end", {
|
|
1827
|
+
messages: [{ role: "assistant", stopReason: "end_turn" }],
|
|
1828
|
+
}, successCtx);
|
|
1829
|
+
const successLog = JSON.parse(fs.readFileSync(path.join(successFixture.projectBase, "prompt-debug.json"), "utf8"));
|
|
1830
|
+
assert.ok(successLog.some((entry: any) => entry.action === "required_docs_check" && entry.result?.success === true));
|
|
1831
|
+
assert.ok(successLog.some((entry: any) => entry.action === "worktree_create" && entry.result?.success === true));
|
|
1832
|
+
assert.ok(successLog.some((entry: any) => entry.action === "workflow_activation" && entry.result?.success === true));
|
|
1833
|
+
assert.ok(successLog.some((entry: any) => entry.action === "workflow_closure" && entry.result?.should_finalize_matched_success === true));
|
|
1834
|
+
assert.ok(successLog.some((entry: any) => entry.action === "workflow_restore" && entry.result?.success === true));
|
|
1835
|
+
assert.ok(successLog.some((entry: any) => entry.action === "merge" && entry.result?.success === true));
|
|
1836
|
+
assert.ok(successLog.some((entry: any) => entry.action === "worktree_delete" && entry.result?.success === true));
|
|
1837
|
+
assert.ok(successLog.some((entry: any) => entry.action === "workflow_state" && entry.result?.workflow_state === "merging"));
|
|
1838
|
+
assert.ok(successLog.some((entry: any) => entry.action === "workflow_state" && entry.result?.workflow_state === "idle"));
|
|
1839
|
+
});
|
|
1840
|
+
|
|
1841
|
+
test("notifications menu preserves focus on toggled and edited rows", async () => {
|
|
1842
|
+
const cwd = createTempDir("pi-usereq-menu-focus-");
|
|
1843
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1844
|
+
const pi = createFakePi();
|
|
1845
|
+
piUsereqExtension(pi);
|
|
1846
|
+
const command = pi.commands.get("pi-usereq");
|
|
1847
|
+
assert.ok(command);
|
|
1848
|
+
const ctx = createFakeCtx(cwd, {
|
|
1849
|
+
selects: [
|
|
1850
|
+
"notifications",
|
|
1851
|
+
"Notification events",
|
|
1852
|
+
"Prompt failed",
|
|
1853
|
+
"Save and close",
|
|
1854
|
+
"Notify command",
|
|
1855
|
+
"Save and close",
|
|
1856
|
+
"Save and close",
|
|
1857
|
+
],
|
|
1858
|
+
inputs: ["echo notify"],
|
|
1859
|
+
});
|
|
1860
|
+
|
|
1861
|
+
await command!.handler("", ctx);
|
|
1862
|
+
|
|
1863
|
+
const notificationCalls = ctx.__state.selectCalls.filter((call) => call.title === "Notifications");
|
|
1864
|
+
assert.equal(notificationCalls[0]?.selectedChoiceId, "notify-enabled");
|
|
1865
|
+
assert.equal(notificationCalls[1]?.selectedChoiceId, "notification-events");
|
|
1866
|
+
assert.equal(notificationCalls[2]?.selectedChoiceId, "notify-command");
|
|
1867
|
+
|
|
1868
|
+
const notificationEventCalls = ctx.__state.selectCalls.filter((call) => call.title === "Notification events");
|
|
1869
|
+
assert.deepEqual(notificationEventCalls[0]?.items ?? [], [
|
|
1870
|
+
"Prompt completed",
|
|
1871
|
+
"Prompt interrupted",
|
|
1872
|
+
"Prompt failed",
|
|
1873
|
+
"Reset defaults",
|
|
1874
|
+
]);
|
|
1875
|
+
assert.equal(notificationEventCalls[0]?.selectedChoiceId, "notify-on-completed");
|
|
1876
|
+
assert.equal(notificationEventCalls[1]?.selectedChoiceId, "notify-on-failed");
|
|
1877
|
+
});
|
|
1878
|
+
|
|
1879
|
+
test("notifications reset defaults preserves non-notification settings", async () => {
|
|
1880
|
+
const cwd = createTempDir("pi-usereq-menu-reset-notifications-");
|
|
1881
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1882
|
+
const pi = createFakePi();
|
|
1883
|
+
piUsereqExtension(pi);
|
|
1884
|
+
const command = pi.commands.get("pi-usereq");
|
|
1885
|
+
assert.ok(command);
|
|
1886
|
+
const ctx = createFakeCtx(cwd, {
|
|
1887
|
+
selects: [
|
|
1888
|
+
"docs-dir",
|
|
1889
|
+
"notifications",
|
|
1890
|
+
"Enable notification",
|
|
1891
|
+
"Enable sound",
|
|
1892
|
+
"high",
|
|
1893
|
+
"Reset defaults",
|
|
1894
|
+
"Approve reset",
|
|
1895
|
+
"Save and close",
|
|
1896
|
+
"Save and close",
|
|
1897
|
+
],
|
|
1898
|
+
inputs: ["docs/custom"],
|
|
1899
|
+
});
|
|
1900
|
+
|
|
1901
|
+
await command!.handler("", ctx);
|
|
1902
|
+
|
|
1903
|
+
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
1904
|
+
assert.equal(config["docs-dir"], "docs/custom");
|
|
1905
|
+
assert.equal(config["notify-enabled"], false);
|
|
1906
|
+
assert.equal(config["notify-sound"], "none");
|
|
1907
|
+
assert.equal(config["notify-pushover-enabled"], false);
|
|
1908
|
+
});
|
|
1909
|
+
|
|
1910
|
+
test("top-level reset defaults restores all configuration values", async () => {
|
|
1911
|
+
const cwd = createTempDir("pi-usereq-menu-reset-all-");
|
|
1912
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1913
|
+
const pi = createFakePi();
|
|
1914
|
+
piUsereqExtension(pi);
|
|
1915
|
+
const command = pi.commands.get("pi-usereq");
|
|
1916
|
+
assert.ok(command);
|
|
1917
|
+
const ctx = createFakeCtx(cwd, {
|
|
1918
|
+
selects: [
|
|
1919
|
+
"docs-dir",
|
|
1920
|
+
"notifications",
|
|
1921
|
+
"Enable notification",
|
|
1922
|
+
"Save and close",
|
|
1923
|
+
"Reset defaults",
|
|
1924
|
+
"Approve reset",
|
|
1925
|
+
"Save and close",
|
|
1926
|
+
],
|
|
1927
|
+
inputs: ["docs/custom"],
|
|
1928
|
+
});
|
|
1929
|
+
|
|
1930
|
+
await command!.handler("", ctx);
|
|
1931
|
+
|
|
1932
|
+
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
1933
|
+
assert.equal(config["docs-dir"], DEFAULT_DOCS_DIR);
|
|
1934
|
+
assert.deepEqual(config["src-dir"], ["src"]);
|
|
1935
|
+
assert.equal(config["notify-enabled"], false);
|
|
1936
|
+
assert.equal(config["notify-sound"], "none");
|
|
1937
|
+
});
|
|
1938
|
+
|
|
1939
|
+
test("configuration menus reuse CLI settings theme semantics", async () => {
|
|
1940
|
+
const cwd = createTempDir("pi-usereq-menu-theme-");
|
|
1941
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1942
|
+
const usedColors = new Set<string>();
|
|
1943
|
+
const pi = createFakePi();
|
|
1944
|
+
piUsereqExtension(pi);
|
|
1945
|
+
const command = pi.commands.get("pi-usereq");
|
|
1946
|
+
assert.ok(command);
|
|
1947
|
+
const ctx = createFakeCtx(cwd, { selects: [] }, {
|
|
1948
|
+
theme: {
|
|
1949
|
+
fg(color: string, text: string) {
|
|
1950
|
+
usedColors.add(color);
|
|
1951
|
+
return formatFakeThemeForeground(color, text);
|
|
1952
|
+
},
|
|
1953
|
+
bgFromFg(color: string, text: string) {
|
|
1954
|
+
assert.ok(["accent", "muted", "dim"].includes(color));
|
|
1955
|
+
return formatFakeThemeBackgroundFromForeground(color, text);
|
|
1956
|
+
},
|
|
1957
|
+
bold(text: string) {
|
|
1958
|
+
return `<bold>${text}</bold>`;
|
|
1959
|
+
},
|
|
1960
|
+
},
|
|
1961
|
+
});
|
|
1962
|
+
|
|
1963
|
+
await command!.handler("", ctx);
|
|
1964
|
+
|
|
1965
|
+
const renderedMenu = (ctx.__state.customRenderLines[0] ?? []).join("\n");
|
|
1966
|
+
assert.match(renderedMenu, /<accent><bold>pi-usereq<\/bold><\/accent>/);
|
|
1967
|
+
assert.match(renderedMenu, /<accent>→ <\/accent>/);
|
|
1968
|
+
assert.match(renderedMenu, /<accent>pi-usereq\/docs<\/accent>/);
|
|
1969
|
+
assert.match(renderedMenu, /<muted>tests<\/muted>/);
|
|
1970
|
+
assert.match(renderedMenu, /<dim>/);
|
|
1971
|
+
assert.deepEqual([...usedColors].sort(), ["accent", "dim", "muted"]);
|
|
1972
|
+
});
|
|
1973
|
+
|
|
1974
|
+
test("prompt commands use the current session by default", async () => {
|
|
1975
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
1976
|
+
try {
|
|
1977
|
+
writeProjectConfigOverrides(projectBase, { GIT_WORKTREE_ENABLED: "disable" });
|
|
1978
|
+
const pi = createFakePi();
|
|
1979
|
+
piUsereqExtension(pi);
|
|
1980
|
+
const command = pi.commands.get("req-analyze");
|
|
1981
|
+
assert.ok(command);
|
|
1982
|
+
const ctx = createFakeCtx(projectBase);
|
|
1983
|
+
|
|
1984
|
+
await command!.handler("Inspect src/index.ts for prompt coverage", ctx);
|
|
1985
|
+
|
|
1986
|
+
assert.equal(ctx.__state.waitForIdleCalls, 0);
|
|
1987
|
+
assert.equal(ctx.__state.newSessions.length, 0);
|
|
1988
|
+
assert.equal(pi.sentUserMessages.length, 1);
|
|
1989
|
+
assert.match(String(pi.sentUserMessages[0]?.content), /Inspect src\/index\.ts for prompt coverage/);
|
|
1990
|
+
} finally {
|
|
1991
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
1992
|
+
}
|
|
1993
|
+
});
|
|
1994
|
+
|
|
1995
|
+
test("prompt commands skip worktree creation when auto git commit is disabled", async () => {
|
|
1996
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
1997
|
+
try {
|
|
1998
|
+
writeProjectConfigOverrides(projectBase, {
|
|
1999
|
+
AUTO_GIT_COMMIT: "disable",
|
|
2000
|
+
GIT_WORKTREE_ENABLED: "enable",
|
|
2001
|
+
});
|
|
2002
|
+
const pi = createFakePi();
|
|
2003
|
+
piUsereqExtension(pi);
|
|
2004
|
+
const command = pi.commands.get("req-change");
|
|
2005
|
+
assert.ok(command);
|
|
2006
|
+
const ctx = createFakeCtx(projectBase);
|
|
2007
|
+
|
|
2008
|
+
await command!.handler("Adjust docs", ctx);
|
|
2009
|
+
|
|
2010
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2011
|
+
const expectedBasePath = buildExpectedFakeBasePath(projectBase).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2012
|
+
assert.doesNotMatch(promptText, /created worktree/);
|
|
2013
|
+
assert.match(
|
|
2014
|
+
promptText,
|
|
2015
|
+
new RegExp(`worktree orchestration is disabled and context-path remains \`${expectedBasePath}\``),
|
|
2016
|
+
);
|
|
2017
|
+
} finally {
|
|
2018
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2019
|
+
}
|
|
2020
|
+
});
|
|
2021
|
+
|
|
2022
|
+
test("prompt commands capture checking before handoff and running at prompt injection", async () => {
|
|
2023
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2024
|
+
const previousCwd = process.cwd();
|
|
2025
|
+
let checkingStatus = "";
|
|
2026
|
+
let handoffStatus = "";
|
|
2027
|
+
try {
|
|
2028
|
+
const pi = createFakePi();
|
|
2029
|
+
piUsereqExtension(pi);
|
|
2030
|
+
const ctx = createFakeCtx(projectBase);
|
|
2031
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2032
|
+
const originalSendUserMessage = pi.sendUserMessage.bind(pi);
|
|
2033
|
+
pi.sendUserMessage = (content: unknown, options?: unknown) => {
|
|
2034
|
+
handoffStatus = ctx.__state.statuses.get("pi-usereq") ?? "";
|
|
2035
|
+
originalSendUserMessage(content, options);
|
|
2036
|
+
};
|
|
2037
|
+
setPromptCommandPostCreateHookForTests(() => {
|
|
2038
|
+
checkingStatus = ctx.__state.statuses.get("pi-usereq") ?? "";
|
|
2039
|
+
});
|
|
2040
|
+
|
|
2041
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2042
|
+
|
|
2043
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2044
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2045
|
+
assert.ok(worktreeMatch, promptText);
|
|
2046
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2047
|
+
assert.equal(
|
|
2048
|
+
checkingStatus,
|
|
2049
|
+
buildExpectedFakeStatusText({
|
|
2050
|
+
workflowState: "checking",
|
|
2051
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2052
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2053
|
+
testsDir: "tests",
|
|
2054
|
+
srcDir: ["src"],
|
|
2055
|
+
contextFilledCells: 0,
|
|
2056
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2057
|
+
}),
|
|
2058
|
+
);
|
|
2059
|
+
assert.equal(
|
|
2060
|
+
handoffStatus,
|
|
2061
|
+
buildExpectedFakeStatusText({
|
|
2062
|
+
workflowState: "checking",
|
|
2063
|
+
basePath: buildExpectedFakeBasePath(executionBasePath),
|
|
2064
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2065
|
+
testsDir: "tests",
|
|
2066
|
+
srcDir: ["src"],
|
|
2067
|
+
contextFilledCells: 0,
|
|
2068
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2069
|
+
}),
|
|
2070
|
+
);
|
|
2071
|
+
assert.equal(
|
|
2072
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2073
|
+
buildExpectedFakeStatusText({
|
|
2074
|
+
workflowState: "running",
|
|
2075
|
+
basePath: buildExpectedFakeBasePath(executionBasePath),
|
|
2076
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2077
|
+
testsDir: "tests",
|
|
2078
|
+
srcDir: ["src"],
|
|
2079
|
+
contextFilledCells: 0,
|
|
2080
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2081
|
+
}),
|
|
2082
|
+
);
|
|
2083
|
+
|
|
2084
|
+
await pi.emit("before_agent_start", {}, ctx);
|
|
2085
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2086
|
+
await pi.emit("agent_end", {
|
|
2087
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2088
|
+
}, ctx);
|
|
2089
|
+
} finally {
|
|
2090
|
+
setPromptCommandPostCreateHookForTests(undefined);
|
|
2091
|
+
process.chdir(previousCwd);
|
|
2092
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2093
|
+
}
|
|
2094
|
+
});
|
|
2095
|
+
|
|
2096
|
+
test("prompt commands reject non-idle workflow state before starting a new orchestration", async () => {
|
|
2097
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2098
|
+
try {
|
|
2099
|
+
writeProjectConfigOverrides(projectBase, { GIT_WORKTREE_ENABLED: "disable" });
|
|
2100
|
+
const pi = createFakePi();
|
|
2101
|
+
piUsereqExtension(pi);
|
|
2102
|
+
const ctx = createFakeCtx(projectBase);
|
|
2103
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2104
|
+
|
|
2105
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2106
|
+
assert.equal(
|
|
2107
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2108
|
+
buildExpectedFakeStatusText({
|
|
2109
|
+
workflowState: "running",
|
|
2110
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2111
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2112
|
+
testsDir: "tests",
|
|
2113
|
+
srcDir: ["src"],
|
|
2114
|
+
contextFilledCells: 0,
|
|
2115
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2116
|
+
}),
|
|
2117
|
+
);
|
|
2118
|
+
|
|
2119
|
+
await assert.rejects(
|
|
2120
|
+
pi.commands.get("req-analyze")!.handler("Inspect src/index.ts", ctx),
|
|
2121
|
+
/expected idle/,
|
|
2122
|
+
);
|
|
2123
|
+
assert.equal(pi.sentUserMessages.length, 1);
|
|
2124
|
+
assert.ok(ctx.__state.notifications.some((entry) => /expected idle/.test(entry.message)));
|
|
2125
|
+
assert.equal(
|
|
2126
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2127
|
+
buildExpectedFakeStatusText({
|
|
2128
|
+
workflowState: "running",
|
|
2129
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2130
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2131
|
+
testsDir: "tests",
|
|
2132
|
+
srcDir: ["src"],
|
|
2133
|
+
contextFilledCells: 0,
|
|
2134
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2135
|
+
}),
|
|
2136
|
+
);
|
|
2137
|
+
|
|
2138
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2139
|
+
await pi.emit("agent_end", {
|
|
2140
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2141
|
+
}, ctx);
|
|
2142
|
+
assert.equal(
|
|
2143
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2144
|
+
buildExpectedFakeStatusText({
|
|
2145
|
+
workflowState: "idle",
|
|
2146
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2147
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2148
|
+
testsDir: "tests",
|
|
2149
|
+
srcDir: ["src"],
|
|
2150
|
+
contextFilledCells: 0,
|
|
2151
|
+
et: "⏱︎ --:-- ⚑ 0:00 ⌛︎0:00",
|
|
2152
|
+
}),
|
|
2153
|
+
);
|
|
2154
|
+
} finally {
|
|
2155
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2156
|
+
}
|
|
2157
|
+
});
|
|
2158
|
+
|
|
2159
|
+
test("prompt commands abort when git validation fails", async () => {
|
|
2160
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2161
|
+
const previousCwd = process.cwd();
|
|
2162
|
+
try {
|
|
2163
|
+
fs.writeFileSync(path.join(projectBase, "DIRTY.txt"), "dirty\n", "utf8");
|
|
2164
|
+
const pi = createFakePi();
|
|
2165
|
+
piUsereqExtension(pi);
|
|
2166
|
+
const ctx = createFakeCtx(projectBase);
|
|
2167
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2168
|
+
|
|
2169
|
+
await assert.rejects(
|
|
2170
|
+
pi.commands.get("req-analyze")!.handler("Inspect src/index.ts", ctx),
|
|
2171
|
+
/ERROR: Git status unclear!/,
|
|
2172
|
+
);
|
|
2173
|
+
assert.equal(pi.sentUserMessages.length, 0);
|
|
2174
|
+
assert.ok(ctx.__state.notifications.some((entry) => /ERROR: Git status unclear!/.test(entry.message)));
|
|
2175
|
+
assert.equal(
|
|
2176
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2177
|
+
buildExpectedFakeStatusText({
|
|
2178
|
+
workflowState: "error",
|
|
2179
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2180
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2181
|
+
testsDir: "tests",
|
|
2182
|
+
srcDir: ["src"],
|
|
2183
|
+
contextFilledCells: 0,
|
|
2184
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2185
|
+
}),
|
|
2186
|
+
);
|
|
2187
|
+
} finally {
|
|
2188
|
+
process.chdir(previousCwd);
|
|
2189
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2190
|
+
}
|
|
2191
|
+
});
|
|
2192
|
+
|
|
2193
|
+
test("prompt commands enforce the documented required-doc matrix", async () => {
|
|
2194
|
+
const { projectBase } = initFixtureRepo({
|
|
2195
|
+
fixtures: [],
|
|
2196
|
+
docs: {
|
|
2197
|
+
"REQUIREMENTS.md": "# Requirements\n",
|
|
2198
|
+
},
|
|
2199
|
+
});
|
|
2200
|
+
const previousCwd = process.cwd();
|
|
2201
|
+
try {
|
|
2202
|
+
const worktreeParentPath = path.dirname(projectBase);
|
|
2203
|
+
const initialEntries = new Set(fs.readdirSync(worktreeParentPath));
|
|
2204
|
+
const pi = createFakePi();
|
|
2205
|
+
piUsereqExtension(pi);
|
|
2206
|
+
const ctx = createFakeCtx(projectBase);
|
|
2207
|
+
|
|
2208
|
+
await assert.rejects(
|
|
2209
|
+
pi.commands.get("req-change")!.handler("Adjust docs", ctx),
|
|
2210
|
+
/WORKFLOW\.md/,
|
|
2211
|
+
);
|
|
2212
|
+
const createdWorktreeEntries = fs.readdirSync(worktreeParentPath)
|
|
2213
|
+
.filter((entry) => !initialEntries.has(entry))
|
|
2214
|
+
.filter((entry) => /^PI-useReq-/.test(entry));
|
|
2215
|
+
assert.equal(pi.sentUserMessages.length, 0);
|
|
2216
|
+
assert.deepEqual(createdWorktreeEntries, []);
|
|
2217
|
+
assert.ok(ctx.__state.notifications.some((entry) => /WORKFLOW\.md/.test(entry.message)));
|
|
2218
|
+
} finally {
|
|
2219
|
+
process.chdir(previousCwd);
|
|
2220
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2221
|
+
}
|
|
2222
|
+
});
|
|
2223
|
+
|
|
2224
|
+
test("worktree-enabled prompt commands merge successful runs, restore base-path, and delete the worktree", async () => {
|
|
2225
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2226
|
+
const previousCwd = process.cwd();
|
|
2227
|
+
try {
|
|
2228
|
+
const pi = createFakePi();
|
|
2229
|
+
piUsereqExtension(pi);
|
|
2230
|
+
const ctx = createFakeCtx(projectBase);
|
|
2231
|
+
const recordedStatuses: string[] = [];
|
|
2232
|
+
const originalSetStatus = ctx.ui.setStatus.bind(ctx.ui);
|
|
2233
|
+
ctx.ui.setStatus = (key: string, value?: string) => {
|
|
2234
|
+
if (key === "pi-usereq" && value !== undefined) {
|
|
2235
|
+
recordedStatuses.push(value);
|
|
2236
|
+
}
|
|
2237
|
+
originalSetStatus(key, value);
|
|
2238
|
+
};
|
|
2239
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2240
|
+
|
|
2241
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2242
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2243
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2244
|
+
assert.ok(worktreeMatch, promptText);
|
|
2245
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
2246
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2247
|
+
assert.match(worktreeName, /^PI-useReq-/);
|
|
2248
|
+
assert.ok(fs.existsSync(executionBasePath));
|
|
2249
|
+
assert.equal(
|
|
2250
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
2251
|
+
cwd: projectBase,
|
|
2252
|
+
encoding: "utf8",
|
|
2253
|
+
}).status,
|
|
2254
|
+
0,
|
|
2255
|
+
);
|
|
2256
|
+
assert.equal(process.cwd(), executionBasePath);
|
|
2257
|
+
assert.equal(ctx.cwd, executionBasePath);
|
|
2258
|
+
assert.ok(recordedStatuses.some((status) => /<accent>status:<\/accent><warning>running<\/warning>/.test(status)));
|
|
2259
|
+
|
|
2260
|
+
await pi.emit("before_agent_start", {}, ctx);
|
|
2261
|
+
assert.equal(process.cwd(), executionBasePath);
|
|
2262
|
+
const filesTokensTool = pi.tools.find((candidate: RegisteredTool) => candidate.name === "files-tokens");
|
|
2263
|
+
assert.ok(filesTokensTool?.execute, "missing files-tokens execute handler");
|
|
2264
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
2265
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "tool-relative.ts"), "export const TOOL_RELATIVE = 1;\n", "utf8");
|
|
2266
|
+
const toolResult = await filesTokensTool.execute!("tool-call-id", { files: ["src/tool-relative.ts"] }, undefined, undefined, ctx) as {
|
|
2267
|
+
content?: Array<{ type?: string; text?: string }>;
|
|
2268
|
+
};
|
|
2269
|
+
assert.match(toolResult.content?.[0]?.text ?? "", /tool-relative\.ts/);
|
|
2270
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2271
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "orchestrated.ts"), "export const ORCHESTRATED = 1;\n", "utf8");
|
|
2272
|
+
assert.equal(spawnSync("git", ["add", "src/orchestrated.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
2273
|
+
const commit = spawnSync("git", ["commit", "-m", "worktree change"], { cwd: executionBasePath, encoding: "utf8" });
|
|
2274
|
+
assert.equal(commit.status, 0, commit.stderr);
|
|
2275
|
+
|
|
2276
|
+
await pi.emit("agent_end", {
|
|
2277
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2278
|
+
}, ctx);
|
|
2279
|
+
|
|
2280
|
+
assert.equal(process.cwd(), projectBase);
|
|
2281
|
+
assert.equal(ctx.cwd, projectBase);
|
|
2282
|
+
assert.ok(fs.existsSync(path.join(projectBase, "src", "orchestrated.ts")));
|
|
2283
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
2284
|
+
assert.notEqual(
|
|
2285
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
2286
|
+
cwd: projectBase,
|
|
2287
|
+
encoding: "utf8",
|
|
2288
|
+
}).status,
|
|
2289
|
+
0,
|
|
2290
|
+
);
|
|
2291
|
+
assert.equal(ctx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
|
|
2292
|
+
assert.ok(recordedStatuses.some((status) => /<accent>status:<\/accent><warning>merging<\/warning>/.test(status)));
|
|
2293
|
+
assert.match(ctx.__state.statuses.get("pi-usereq") ?? "", /<accent>status:<\/accent><warning>idle<\/warning>/);
|
|
2294
|
+
} finally {
|
|
2295
|
+
process.chdir(previousCwd);
|
|
2296
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2297
|
+
}
|
|
2298
|
+
});
|
|
2299
|
+
|
|
2300
|
+
/**
|
|
2301
|
+
* @brief Verifies prompt-command bootstrap ignores stale deleted-worktree session metadata on later commands.
|
|
2302
|
+
* @details Simulates the `/tmp/debug.json` follow-up failure mode: one successful worktree-backed `req-change` run restores `base-path` and deletes its worktree, then a later `req-change` invocation receives a stale command context whose `cwd` and session getters still point at the removed execution worktree. The test proves command bootstrap MUST discard deleted-worktree cwd/session residue, prepare a fresh worktree with a new identifier, and complete the second successful run without reusing the removed execution path.
|
|
2303
|
+
* @return {Promise<void>} Promise resolved after two successful prompt-command runs complete.
|
|
2304
|
+
* @throws {AssertionError} Throws when the second command aborts with the canonical git-status preflight error or reuses the deleted worktree context.
|
|
2305
|
+
* @satisfies REQ-200, REQ-206, REQ-208, REQ-219, REQ-257
|
|
2306
|
+
*/
|
|
2307
|
+
test("prompt-command bootstrap ignores stale deleted-worktree session metadata on later commands", async () => {
|
|
2308
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2309
|
+
const previousCwd = process.cwd();
|
|
2310
|
+
try {
|
|
2311
|
+
const pi = createFakePi();
|
|
2312
|
+
piUsereqExtension(pi);
|
|
2313
|
+
const ctx = createFakeCtx(projectBase);
|
|
2314
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2315
|
+
|
|
2316
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2317
|
+
const firstPromptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2318
|
+
const firstWorktreeMatch = firstPromptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2319
|
+
assert.ok(firstWorktreeMatch, firstPromptText);
|
|
2320
|
+
const firstWorktreeName = firstWorktreeMatch?.[1] ?? "";
|
|
2321
|
+
const firstExecutionBasePath = firstWorktreeMatch?.[2] ?? "";
|
|
2322
|
+
const firstExecutionSessionFile = ctx.sessionManager.getSessionFile() ?? "";
|
|
2323
|
+
assert.notEqual(firstExecutionSessionFile, "");
|
|
2324
|
+
|
|
2325
|
+
await pi.emit("before_agent_start", {}, ctx);
|
|
2326
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2327
|
+
fs.mkdirSync(path.join(firstExecutionBasePath, "src"), { recursive: true });
|
|
2328
|
+
fs.writeFileSync(path.join(firstExecutionBasePath, "src", "bootstrap-first.ts"), "export const BOOTSTRAP_FIRST = 1;\n", "utf8");
|
|
2329
|
+
assert.equal(
|
|
2330
|
+
spawnSync("git", ["add", "src/bootstrap-first.ts"], {
|
|
2331
|
+
cwd: firstExecutionBasePath,
|
|
2332
|
+
encoding: "utf8",
|
|
2333
|
+
}).status,
|
|
2334
|
+
0,
|
|
2335
|
+
);
|
|
2336
|
+
const firstCommit = spawnSync("git", ["commit", "-m", "bootstrap first"], {
|
|
2337
|
+
cwd: firstExecutionBasePath,
|
|
2338
|
+
encoding: "utf8",
|
|
2339
|
+
});
|
|
2340
|
+
assert.equal(firstCommit.status, 0, firstCommit.stderr);
|
|
2341
|
+
|
|
2342
|
+
await pi.emit("agent_end", {
|
|
2343
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2344
|
+
}, ctx);
|
|
2345
|
+
|
|
2346
|
+
assert.equal(process.cwd(), projectBase);
|
|
2347
|
+
assert.equal(ctx.cwd, projectBase);
|
|
2348
|
+
assert.equal(fs.existsSync(firstExecutionBasePath), false);
|
|
2349
|
+
assert.notEqual(
|
|
2350
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${firstWorktreeName}`], {
|
|
2351
|
+
cwd: projectBase,
|
|
2352
|
+
encoding: "utf8",
|
|
2353
|
+
}).status,
|
|
2354
|
+
0,
|
|
2355
|
+
);
|
|
2356
|
+
|
|
2357
|
+
ctx.cwd = firstExecutionBasePath;
|
|
2358
|
+
ctx.sessionManager.getCwd = () => firstExecutionBasePath;
|
|
2359
|
+
ctx.sessionManager.getSessionFile = () => firstExecutionSessionFile;
|
|
2360
|
+
ctx.sessionManager.getSessionDir = () => path.dirname(firstExecutionSessionFile);
|
|
2361
|
+
|
|
2362
|
+
await pi.commands.get("req-change")!.handler("Adjust docs again", ctx);
|
|
2363
|
+
const secondPromptText = String(pi.sentUserMessages[1]?.content ?? "");
|
|
2364
|
+
const secondWorktreeMatch = secondPromptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2365
|
+
assert.ok(secondWorktreeMatch, secondPromptText);
|
|
2366
|
+
const secondWorktreeName = secondWorktreeMatch?.[1] ?? "";
|
|
2367
|
+
const secondExecutionBasePath = secondWorktreeMatch?.[2] ?? "";
|
|
2368
|
+
|
|
2369
|
+
assert.notEqual(secondWorktreeName, firstWorktreeName);
|
|
2370
|
+
assert.notEqual(secondExecutionBasePath, firstExecutionBasePath);
|
|
2371
|
+
assert.equal(process.cwd(), secondExecutionBasePath);
|
|
2372
|
+
assert.equal(ctx.cwd, secondExecutionBasePath);
|
|
2373
|
+
|
|
2374
|
+
await pi.emit("before_agent_start", {}, ctx);
|
|
2375
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2376
|
+
fs.mkdirSync(path.join(secondExecutionBasePath, "src"), { recursive: true });
|
|
2377
|
+
fs.writeFileSync(path.join(secondExecutionBasePath, "src", "bootstrap-second.ts"), "export const BOOTSTRAP_SECOND = 1;\n", "utf8");
|
|
2378
|
+
assert.equal(
|
|
2379
|
+
spawnSync("git", ["add", "src/bootstrap-second.ts"], {
|
|
2380
|
+
cwd: secondExecutionBasePath,
|
|
2381
|
+
encoding: "utf8",
|
|
2382
|
+
}).status,
|
|
2383
|
+
0,
|
|
2384
|
+
);
|
|
2385
|
+
const secondCommit = spawnSync("git", ["commit", "-m", "bootstrap second"], {
|
|
2386
|
+
cwd: secondExecutionBasePath,
|
|
2387
|
+
encoding: "utf8",
|
|
2388
|
+
});
|
|
2389
|
+
assert.equal(secondCommit.status, 0, secondCommit.stderr);
|
|
2390
|
+
|
|
2391
|
+
await pi.emit("agent_end", {
|
|
2392
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2393
|
+
}, ctx);
|
|
2394
|
+
|
|
2395
|
+
assert.equal(process.cwd(), projectBase);
|
|
2396
|
+
assert.equal(ctx.cwd, projectBase);
|
|
2397
|
+
assert.equal(fs.existsSync(secondExecutionBasePath), false);
|
|
2398
|
+
assert.ok(fs.existsSync(path.join(projectBase, "src", "bootstrap-first.ts")));
|
|
2399
|
+
assert.ok(fs.existsSync(path.join(projectBase, "src", "bootstrap-second.ts")));
|
|
2400
|
+
assert.notEqual(
|
|
2401
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${secondWorktreeName}`], {
|
|
2402
|
+
cwd: projectBase,
|
|
2403
|
+
encoding: "utf8",
|
|
2404
|
+
}).status,
|
|
2405
|
+
0,
|
|
2406
|
+
);
|
|
2407
|
+
assert.equal(ctx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
|
|
2408
|
+
} finally {
|
|
2409
|
+
process.chdir(previousCwd);
|
|
2410
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2411
|
+
}
|
|
2412
|
+
});
|
|
2413
|
+
|
|
2414
|
+
test("worktree-backed finalization transitions through merging, error, and idle when merge fails", async () => {
|
|
2415
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2416
|
+
const previousCwd = process.cwd();
|
|
2417
|
+
try {
|
|
2418
|
+
const pi = createFakePi();
|
|
2419
|
+
piUsereqExtension(pi);
|
|
2420
|
+
const ctx = createFakeCtx(projectBase);
|
|
2421
|
+
const recordedStatuses: string[] = [];
|
|
2422
|
+
const originalSetStatus = ctx.ui.setStatus.bind(ctx.ui);
|
|
2423
|
+
ctx.ui.setStatus = (key: string, value?: string) => {
|
|
2424
|
+
if (key === "pi-usereq" && value !== undefined) {
|
|
2425
|
+
recordedStatuses.push(value);
|
|
2426
|
+
}
|
|
2427
|
+
originalSetStatus(key, value);
|
|
2428
|
+
};
|
|
2429
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2430
|
+
|
|
2431
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2432
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2433
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2434
|
+
assert.ok(worktreeMatch, promptText);
|
|
2435
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
2436
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2437
|
+
|
|
2438
|
+
await pi.emit("before_agent_start", {}, ctx);
|
|
2439
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2440
|
+
fs.writeFileSync(path.join(projectBase, "divergent.txt"), "root\n", "utf8");
|
|
2441
|
+
assert.equal(spawnSync("git", ["add", "divergent.txt"], { cwd: projectBase, encoding: "utf8" }).status, 0);
|
|
2442
|
+
const rootCommit = spawnSync("git", ["commit", "-m", "root change"], { cwd: projectBase, encoding: "utf8" });
|
|
2443
|
+
assert.equal(rootCommit.status, 0, rootCommit.stderr);
|
|
2444
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
2445
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "merge-failed.ts"), "export const MERGE_FAILED = 1;\n", "utf8");
|
|
2446
|
+
assert.equal(spawnSync("git", ["add", "src/merge-failed.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
2447
|
+
const worktreeCommit = spawnSync("git", ["commit", "-m", "worktree change"], { cwd: executionBasePath, encoding: "utf8" });
|
|
2448
|
+
assert.equal(worktreeCommit.status, 0, worktreeCommit.stderr);
|
|
2449
|
+
|
|
2450
|
+
await pi.emit("agent_end", {
|
|
2451
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2452
|
+
}, ctx);
|
|
2453
|
+
|
|
2454
|
+
assert.equal(process.cwd(), projectBase);
|
|
2455
|
+
assert.equal(ctx.cwd, projectBase);
|
|
2456
|
+
assert.equal(fs.existsSync(executionBasePath), true);
|
|
2457
|
+
assert.equal(fs.existsSync(path.join(projectBase, "src", "merge-failed.ts")), false);
|
|
2458
|
+
assert.ok(fs.existsSync(path.join(projectBase, "divergent.txt")));
|
|
2459
|
+
assert.equal(
|
|
2460
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
2461
|
+
cwd: projectBase,
|
|
2462
|
+
encoding: "utf8",
|
|
2463
|
+
}).status,
|
|
2464
|
+
0,
|
|
2465
|
+
);
|
|
2466
|
+
assert.ok(recordedStatuses.some((status) => /<accent>status:<\/accent><warning>merging<\/warning>/.test(status)));
|
|
2467
|
+
assert.ok(recordedStatuses.some((status) => /<accent>status:<\/accent><error>\u001b\[5merror\u001b\[25m<\/error>/.test(status)));
|
|
2468
|
+
assert.match(ctx.__state.statuses.get("pi-usereq") ?? "", /<accent>status:<\/accent><warning>idle<\/warning>/);
|
|
2469
|
+
assert.ok(ctx.__state.notifications.some((entry) => /Fast-forward merge failed/.test(entry.message)));
|
|
2470
|
+
} finally {
|
|
2471
|
+
process.chdir(previousCwd);
|
|
2472
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2473
|
+
}
|
|
2474
|
+
});
|
|
2475
|
+
|
|
2476
|
+
test("worktree-enabled prompt commands honor overridden worktree prefixes", async () => {
|
|
2477
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2478
|
+
const previousCwd = process.cwd();
|
|
2479
|
+
try {
|
|
2480
|
+
writeProjectConfigOverrides(projectBase, { GIT_WORKTREE_PREFIX: "custom-prefix-" });
|
|
2481
|
+
const pi = createFakePi();
|
|
2482
|
+
piUsereqExtension(pi);
|
|
2483
|
+
const ctx = createFakeCtx(projectBase);
|
|
2484
|
+
|
|
2485
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2486
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2487
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2488
|
+
assert.ok(worktreeMatch, promptText);
|
|
2489
|
+
assert.match(worktreeMatch?.[1] ?? "", /^custom-prefix-/);
|
|
2490
|
+
} finally {
|
|
2491
|
+
process.chdir(previousCwd);
|
|
2492
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2493
|
+
}
|
|
2494
|
+
});
|
|
2495
|
+
|
|
2496
|
+
test("worktree-backed prompt failures skip merge, restore base-path, and retain worktree", async () => {
|
|
2497
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2498
|
+
const previousCwd = process.cwd();
|
|
2499
|
+
try {
|
|
2500
|
+
const pi = createFakePi();
|
|
2501
|
+
piUsereqExtension(pi);
|
|
2502
|
+
const ctx = createFakeCtx(projectBase);
|
|
2503
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2504
|
+
|
|
2505
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2506
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2507
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2508
|
+
assert.ok(worktreeMatch, promptText);
|
|
2509
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
2510
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2511
|
+
|
|
2512
|
+
await pi.emit("before_agent_start", {}, ctx);
|
|
2513
|
+
assert.equal(process.cwd(), executionBasePath);
|
|
2514
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2515
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
2516
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "failed.ts"), "export const FAILED = 1;\n", "utf8");
|
|
2517
|
+
await pi.emit("agent_end", {
|
|
2518
|
+
messages: [{ role: "assistant", stopReason: "error", content: [] }],
|
|
2519
|
+
}, ctx);
|
|
2520
|
+
|
|
2521
|
+
assert.equal(process.cwd(), projectBase);
|
|
2522
|
+
assert.equal(ctx.cwd, projectBase);
|
|
2523
|
+
assert.equal(fs.existsSync(executionBasePath), true);
|
|
2524
|
+
assert.equal(fs.existsSync(path.join(projectBase, "src", "failed.ts")), false);
|
|
2525
|
+
assert.equal(
|
|
2526
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
2527
|
+
cwd: projectBase,
|
|
2528
|
+
encoding: "utf8",
|
|
2529
|
+
}).status,
|
|
2530
|
+
0,
|
|
2531
|
+
);
|
|
2532
|
+
assert.ok(ctx.__state.notifications.some((entry) => entry.level === "error"
|
|
2533
|
+
&& /Prompt closure retained worktree .* after failed outcome\./.test(entry.message)));
|
|
2534
|
+
assert.equal(
|
|
2535
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2536
|
+
buildExpectedFakeStatusText({
|
|
2537
|
+
workflowState: "idle",
|
|
2538
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2539
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2540
|
+
testsDir: "tests",
|
|
2541
|
+
srcDir: ["src"],
|
|
2542
|
+
contextFilledCells: 0,
|
|
2543
|
+
et: "⏱︎ --:-- ⚑ 0:00 ⌛︎0:00",
|
|
2544
|
+
}),
|
|
2545
|
+
);
|
|
2546
|
+
} finally {
|
|
2547
|
+
process.chdir(previousCwd);
|
|
2548
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2549
|
+
}
|
|
2550
|
+
});
|
|
2551
|
+
|
|
2552
|
+
test("worktree-backed prompt commands use replacement-session callbacks for prompt dispatch and restore base-path after merge", async () => {
|
|
2553
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2554
|
+
const previousCwd = process.cwd();
|
|
2555
|
+
try {
|
|
2556
|
+
const pi = createFakePi();
|
|
2557
|
+
piUsereqExtension(pi);
|
|
2558
|
+
const ctx = createFakeCtx(projectBase);
|
|
2559
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2560
|
+
|
|
2561
|
+
const originalSessionFile = ctx.sessionManager.getSessionFile();
|
|
2562
|
+
const originalSwitchSession = ctx.switchSession.bind(ctx);
|
|
2563
|
+
let activeSessionCtx: any = ctx;
|
|
2564
|
+
ctx.switchSession = async (
|
|
2565
|
+
sessionPath: string,
|
|
2566
|
+
options?: { withSession?: (replacementCtx: any) => Promise<void> },
|
|
2567
|
+
) => {
|
|
2568
|
+
const result = await originalSwitchSession(sessionPath);
|
|
2569
|
+
const replacementCwd = readFakeSessionFileCwd(sessionPath, projectBase);
|
|
2570
|
+
const replacementCtx = {
|
|
2571
|
+
...ctx,
|
|
2572
|
+
cwd: replacementCwd,
|
|
2573
|
+
sessionManager: {
|
|
2574
|
+
...ctx.sessionManager,
|
|
2575
|
+
getCwd: () => replacementCwd,
|
|
2576
|
+
getSessionFile: () => sessionPath,
|
|
2577
|
+
},
|
|
2578
|
+
async sendUserMessage(content: unknown, sendOptions?: unknown) {
|
|
2579
|
+
pi.sentUserMessages.push({ content, options: sendOptions });
|
|
2580
|
+
},
|
|
2581
|
+
};
|
|
2582
|
+
activeSessionCtx = replacementCtx;
|
|
2583
|
+
if (options?.withSession) {
|
|
2584
|
+
process.chdir(replacementCwd);
|
|
2585
|
+
await options.withSession(replacementCtx);
|
|
2586
|
+
ctx.cwd = projectBase;
|
|
2587
|
+
ctx.sessionManager.getSessionFile = () => originalSessionFile;
|
|
2588
|
+
ctx.sessionManager.getCwd = () => projectBase;
|
|
2589
|
+
return result;
|
|
2590
|
+
}
|
|
2591
|
+
ctx.cwd = projectBase;
|
|
2592
|
+
ctx.sessionManager.getSessionFile = () => originalSessionFile;
|
|
2593
|
+
ctx.sessionManager.getCwd = () => projectBase;
|
|
2594
|
+
process.chdir(projectBase);
|
|
2595
|
+
return result;
|
|
2596
|
+
};
|
|
2597
|
+
|
|
2598
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2599
|
+
|
|
2600
|
+
assert.equal(pi.sentUserMessages.length, 1);
|
|
2601
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2602
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2603
|
+
assert.ok(worktreeMatch, promptText);
|
|
2604
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
2605
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2606
|
+
assert.equal(activeSessionCtx.cwd, executionBasePath);
|
|
2607
|
+
assert.equal(process.cwd(), executionBasePath);
|
|
2608
|
+
assert.equal(fs.existsSync(executionBasePath), true);
|
|
2609
|
+
|
|
2610
|
+
await pi.emit("before_agent_start", {}, activeSessionCtx);
|
|
2611
|
+
await pi.emit("agent_start", {}, activeSessionCtx);
|
|
2612
|
+
|
|
2613
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
2614
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "with-session.ts"), "export const WITH_SESSION = 1;\n", "utf8");
|
|
2615
|
+
assert.equal(spawnSync("git", ["add", "src/with-session.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
2616
|
+
const worktreeCommit = spawnSync("git", ["commit", "-m", "with session"], {
|
|
2617
|
+
cwd: executionBasePath,
|
|
2618
|
+
encoding: "utf8",
|
|
2619
|
+
});
|
|
2620
|
+
assert.equal(worktreeCommit.status, 0, worktreeCommit.stderr);
|
|
2621
|
+
|
|
2622
|
+
await pi.emit("agent_end", {
|
|
2623
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2624
|
+
}, activeSessionCtx);
|
|
2625
|
+
|
|
2626
|
+
assert.equal(process.cwd(), projectBase);
|
|
2627
|
+
assert.equal(activeSessionCtx.cwd, projectBase);
|
|
2628
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
2629
|
+
assert.equal(fs.existsSync(path.join(projectBase, "src", "with-session.ts")), true);
|
|
2630
|
+
assert.notEqual(
|
|
2631
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
2632
|
+
cwd: projectBase,
|
|
2633
|
+
encoding: "utf8",
|
|
2634
|
+
}).status,
|
|
2635
|
+
0,
|
|
2636
|
+
);
|
|
2637
|
+
} finally {
|
|
2638
|
+
process.chdir(previousCwd);
|
|
2639
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2640
|
+
}
|
|
2641
|
+
});
|
|
2642
|
+
|
|
2643
|
+
/**
|
|
2644
|
+
* @brief Verifies worktree-backed prompt orchestration against the pi runtime session-switch contract.
|
|
2645
|
+
* @details Simulates the current pi runtime behavior discovered in `/tmp/pi-mono`: `ctx.switchSession(...)` exposes the replacement session through `withSession(...)` and replacement-session contexts while the host process cwd stays anchored to the original project directory until the extension updates it explicitly. The test proves prompt delivery and prompt-end closure must synchronize `process.cwd()` with the active worktree session during execution and restore `base-path` plus remove the worktree after successful merge even when the pi host leaves cwd unchanged. Runtime is dominated by temporary git worktree setup and teardown. Side effects are limited to temporary repository mutation and temporary session-file writes.
|
|
2646
|
+
* @return {Promise<void>} Promise resolved after prompt delivery, merge handling, and restored-base assertions complete.
|
|
2647
|
+
* @throws {AssertionError} Throws when prompt activation or prompt-end closure fails to synchronize `process.cwd()` with the expected session path.
|
|
2648
|
+
* @satisfies REQ-068, REQ-208, REQ-257, REQ-271, REQ-272
|
|
2649
|
+
*/
|
|
2650
|
+
test("worktree-backed prompt commands restore base-path when switchSession does not change process cwd", async () => {
|
|
2651
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2652
|
+
const previousCwd = process.cwd();
|
|
2653
|
+
try {
|
|
2654
|
+
process.chdir(projectBase);
|
|
2655
|
+
const pi = createFakePi();
|
|
2656
|
+
piUsereqExtension(pi);
|
|
2657
|
+
const ctx = createFakeCtx(projectBase);
|
|
2658
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2659
|
+
|
|
2660
|
+
let activeSessionCtx: any = ctx;
|
|
2661
|
+
ctx.switchSession = async (
|
|
2662
|
+
sessionPath: string,
|
|
2663
|
+
options?: { withSession?: (replacementCtx: any) => Promise<void> },
|
|
2664
|
+
) => {
|
|
2665
|
+
const replacementCwd = readFakeSessionFileCwd(sessionPath, projectBase);
|
|
2666
|
+
const replacementCtx = {
|
|
2667
|
+
...ctx,
|
|
2668
|
+
cwd: replacementCwd,
|
|
2669
|
+
sessionManager: {
|
|
2670
|
+
...ctx.sessionManager,
|
|
2671
|
+
getCwd: () => replacementCwd,
|
|
2672
|
+
getSessionFile: () => sessionPath,
|
|
2673
|
+
},
|
|
2674
|
+
switchSession: ctx.switchSession,
|
|
2675
|
+
async sendUserMessage(content: unknown, sendOptions?: unknown) {
|
|
2676
|
+
pi.sentUserMessages.push({ content, options: sendOptions });
|
|
2677
|
+
},
|
|
2678
|
+
};
|
|
2679
|
+
activeSessionCtx = replacementCtx;
|
|
2680
|
+
await options?.withSession?.(replacementCtx);
|
|
2681
|
+
return { cancelled: false };
|
|
2682
|
+
};
|
|
2683
|
+
|
|
2684
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2685
|
+
|
|
2686
|
+
assert.equal(pi.sentUserMessages.length, 1);
|
|
2687
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2688
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2689
|
+
assert.ok(worktreeMatch, promptText);
|
|
2690
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
2691
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2692
|
+
assert.equal(activeSessionCtx.cwd, executionBasePath);
|
|
2693
|
+
assert.equal(activeSessionCtx.sessionManager.getCwd(), executionBasePath);
|
|
2694
|
+
assert.equal(ctx.cwd, projectBase);
|
|
2695
|
+
assert.equal(process.cwd(), executionBasePath);
|
|
2696
|
+
|
|
2697
|
+
await pi.emit("before_agent_start", {}, activeSessionCtx);
|
|
2698
|
+
await pi.emit("agent_start", {}, activeSessionCtx);
|
|
2699
|
+
|
|
2700
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
2701
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "runtime-anchor.ts"), "export const RUNTIME_ANCHOR = 1;\n", "utf8");
|
|
2702
|
+
assert.equal(
|
|
2703
|
+
spawnSync("git", ["add", "src/runtime-anchor.ts"], {
|
|
2704
|
+
cwd: executionBasePath,
|
|
2705
|
+
encoding: "utf8",
|
|
2706
|
+
}).status,
|
|
2707
|
+
0,
|
|
2708
|
+
);
|
|
2709
|
+
const worktreeCommit = spawnSync("git", ["commit", "-m", "runtime anchor"], {
|
|
2710
|
+
cwd: executionBasePath,
|
|
2711
|
+
encoding: "utf8",
|
|
2712
|
+
});
|
|
2713
|
+
assert.equal(worktreeCommit.status, 0, worktreeCommit.stderr);
|
|
2714
|
+
|
|
2715
|
+
await pi.emit("agent_end", {
|
|
2716
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2717
|
+
}, activeSessionCtx);
|
|
2718
|
+
|
|
2719
|
+
assert.equal(process.cwd(), projectBase);
|
|
2720
|
+
assert.equal(activeSessionCtx.cwd, projectBase);
|
|
2721
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
2722
|
+
assert.equal(fs.existsSync(path.join(projectBase, "src", "runtime-anchor.ts")), true);
|
|
2723
|
+
assert.notEqual(
|
|
2724
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
2725
|
+
cwd: projectBase,
|
|
2726
|
+
encoding: "utf8",
|
|
2727
|
+
}).status,
|
|
2728
|
+
0,
|
|
2729
|
+
);
|
|
2730
|
+
} finally {
|
|
2731
|
+
process.chdir(previousCwd);
|
|
2732
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2733
|
+
}
|
|
2734
|
+
});
|
|
2735
|
+
|
|
2736
|
+
/**
|
|
2737
|
+
* @brief Verifies restored base-path closure tolerates stale `ctx.cwd` mutations after a successful session switch.
|
|
2738
|
+
* @details Simulates the real pi runtime behavior observed in `/tmp/debug.json`, where restoring the original session succeeds but the old replacement-session context becomes stale immediately afterward, so mutating its `cwd` mirror throws the documented stale-extension-context error. The test proves `restorePromptCommandExecution(...)` MUST ignore that late stale mutation failure once session-target verification already succeeded, leaving prompt finalization free to continue merge and cleanup.
|
|
2739
|
+
* @return {Promise<void>} Promise resolved after restored-base assertions and cleanup complete.
|
|
2740
|
+
* @throws {AssertionError} Throws when the stale post-restore `cwd` mutation still aborts restoration.
|
|
2741
|
+
* @satisfies REQ-208, REQ-257, REQ-276, REQ-280, REQ-282
|
|
2742
|
+
*/
|
|
2743
|
+
test("restorePromptCommandExecution ignores stale ctx.cwd mutations after a verified restore switch", async () => {
|
|
2744
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2745
|
+
const previousCwd = process.cwd();
|
|
2746
|
+
let executionPlan: ReturnType<typeof preparePromptCommandExecution> | undefined;
|
|
2747
|
+
try {
|
|
2748
|
+
const config = loadConfig(projectBase);
|
|
2749
|
+
const ctx = createFakeCtx(projectBase);
|
|
2750
|
+
executionPlan = preparePromptCommandExecution(
|
|
2751
|
+
"change",
|
|
2752
|
+
"Adjust docs",
|
|
2753
|
+
projectBase,
|
|
2754
|
+
config,
|
|
2755
|
+
ctx.sessionManager.getSessionFile(),
|
|
2756
|
+
ctx.sessionManager.getSessionDir?.(),
|
|
2757
|
+
ctx.sessionManager.getBranch?.(),
|
|
2758
|
+
);
|
|
2759
|
+
const activeContext = await activatePromptCommandExecution(executionPlan, ctx);
|
|
2760
|
+
assert.ok(activeContext);
|
|
2761
|
+
let activeContextCwd = activeContext?.cwd;
|
|
2762
|
+
let staleAfterSwitch = false;
|
|
2763
|
+
Object.defineProperty(activeContext!, "cwd", {
|
|
2764
|
+
configurable: true,
|
|
2765
|
+
get() {
|
|
2766
|
+
return activeContextCwd;
|
|
2767
|
+
},
|
|
2768
|
+
set(value: string) {
|
|
2769
|
+
if (staleAfterSwitch) {
|
|
2770
|
+
throw new Error("This extension ctx is stale after session replacement or reload.");
|
|
2771
|
+
}
|
|
2772
|
+
activeContextCwd = value;
|
|
2773
|
+
},
|
|
2774
|
+
});
|
|
2775
|
+
activeContext!.switchSession = async (sessionPath: string) => {
|
|
2776
|
+
activeContextCwd = projectBase;
|
|
2777
|
+
activeContext!.sessionManager.getSessionFile = () => sessionPath;
|
|
2778
|
+
activeContext!.sessionManager.getSessionDir = () => path.dirname(sessionPath);
|
|
2779
|
+
activeContext!.sessionManager.getCwd = () => projectBase;
|
|
2780
|
+
process.chdir(projectBase);
|
|
2781
|
+
staleAfterSwitch = true;
|
|
2782
|
+
return { cancelled: false };
|
|
2783
|
+
};
|
|
2784
|
+
|
|
2785
|
+
const restoredContext = await restorePromptCommandExecution(executionPlan, activeContext, {
|
|
2786
|
+
config,
|
|
2787
|
+
workflowState: "merging",
|
|
2788
|
+
});
|
|
2789
|
+
|
|
2790
|
+
assert.equal(process.cwd(), projectBase);
|
|
2791
|
+
assert.equal(restoredContext, activeContext);
|
|
2792
|
+
assert.equal(fs.existsSync(executionPlan.worktreePath ?? ""), true);
|
|
2793
|
+
} finally {
|
|
2794
|
+
if (executionPlan) {
|
|
2795
|
+
await abortPromptCommandExecution(executionPlan);
|
|
2796
|
+
}
|
|
2797
|
+
process.chdir(previousCwd);
|
|
2798
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2799
|
+
}
|
|
2800
|
+
});
|
|
2801
|
+
|
|
2802
|
+
/**
|
|
2803
|
+
* @brief Verifies restored base-path closure tolerates getter-only `ctx.cwd` mirrors after a successful session switch.
|
|
2804
|
+
* @details Simulates the real pi replacement-session context shape observed during `/tmp/debug.json` triage, where `cwd` is exposed through a getter-only property. The test proves `restorePromptCommandExecution(...)` MUST treat a non-writable `cwd` mirror as advisory-only after session-target verification succeeds, instead of aborting prompt finalization with a TypeError before merge.
|
|
2805
|
+
* @return {Promise<void>} Promise resolved after restored-base assertions and cleanup complete.
|
|
2806
|
+
* @throws {AssertionError} Throws when a getter-only `cwd` property still aborts restoration.
|
|
2807
|
+
* @satisfies REQ-208, REQ-257, REQ-276, REQ-282
|
|
2808
|
+
*/
|
|
2809
|
+
test("restorePromptCommandExecution ignores getter-only ctx.cwd mirrors after a verified restore switch", async () => {
|
|
2810
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2811
|
+
const previousCwd = process.cwd();
|
|
2812
|
+
let executionPlan: ReturnType<typeof preparePromptCommandExecution> | undefined;
|
|
2813
|
+
try {
|
|
2814
|
+
const config = loadConfig(projectBase);
|
|
2815
|
+
const ctx = createFakeCtx(projectBase);
|
|
2816
|
+
executionPlan = preparePromptCommandExecution(
|
|
2817
|
+
"change",
|
|
2818
|
+
"Adjust docs",
|
|
2819
|
+
projectBase,
|
|
2820
|
+
config,
|
|
2821
|
+
ctx.sessionManager.getSessionFile(),
|
|
2822
|
+
ctx.sessionManager.getSessionDir?.(),
|
|
2823
|
+
ctx.sessionManager.getBranch?.(),
|
|
2824
|
+
);
|
|
2825
|
+
const activeContext = await activatePromptCommandExecution(executionPlan, ctx);
|
|
2826
|
+
assert.ok(activeContext);
|
|
2827
|
+
let activeContextCwd = activeContext?.cwd;
|
|
2828
|
+
Object.defineProperty(activeContext!, "cwd", {
|
|
2829
|
+
configurable: true,
|
|
2830
|
+
get() {
|
|
2831
|
+
return activeContextCwd;
|
|
2832
|
+
},
|
|
2833
|
+
});
|
|
2834
|
+
activeContext!.switchSession = async (sessionPath: string) => {
|
|
2835
|
+
activeContextCwd = projectBase;
|
|
2836
|
+
activeContext!.sessionManager.getSessionFile = () => sessionPath;
|
|
2837
|
+
activeContext!.sessionManager.getSessionDir = () => path.dirname(sessionPath);
|
|
2838
|
+
activeContext!.sessionManager.getCwd = () => projectBase;
|
|
2839
|
+
process.chdir(projectBase);
|
|
2840
|
+
return { cancelled: false };
|
|
2841
|
+
};
|
|
2842
|
+
|
|
2843
|
+
const restoredContext = await restorePromptCommandExecution(executionPlan, activeContext, {
|
|
2844
|
+
config,
|
|
2845
|
+
workflowState: "merging",
|
|
2846
|
+
});
|
|
2847
|
+
|
|
2848
|
+
assert.equal(process.cwd(), projectBase);
|
|
2849
|
+
assert.equal(restoredContext, activeContext);
|
|
2850
|
+
assert.equal(fs.existsSync(executionPlan.worktreePath ?? ""), true);
|
|
2851
|
+
} finally {
|
|
2852
|
+
if (executionPlan) {
|
|
2853
|
+
await abortPromptCommandExecution(executionPlan);
|
|
2854
|
+
}
|
|
2855
|
+
process.chdir(previousCwd);
|
|
2856
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2857
|
+
}
|
|
2858
|
+
});
|
|
2859
|
+
|
|
2860
|
+
/**
|
|
2861
|
+
* @brief Verifies worktree-backed closure succeeds when lifecycle event contexts omit `switchSession()`.
|
|
2862
|
+
* @details Simulates the documented pi extension contract where lifecycle hooks receive `ExtensionContext` rather than `ExtensionCommandContext`, so `agent_end` contexts expose the active execution-session file and cwd but omit session-replacement methods. The test proves closure must reuse the persisted command-capable replacement-session context captured during activation to restore `base-path`, merge the successful worktree branch, and delete the worktree plus branch. Runtime is dominated by temporary git worktree setup, commit creation, and prompt finalization. Side effects are limited to temporary repository mutation and temporary session-file writes.
|
|
2863
|
+
* @return {Promise<void>} Promise resolved after closure assertions complete.
|
|
2864
|
+
* @throws {AssertionError} Throws when closure fails to restore `base-path`, merge the worktree, or remove worktree resources.
|
|
2865
|
+
* @satisfies REQ-208, REQ-209, REQ-257, REQ-272, REQ-276, TST-089
|
|
2866
|
+
*/
|
|
2867
|
+
test("worktree-backed closure succeeds when agent_end lifecycle contexts omit switchSession", async () => {
|
|
2868
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2869
|
+
const previousCwd = process.cwd();
|
|
2870
|
+
try {
|
|
2871
|
+
const pi = createFakePi();
|
|
2872
|
+
piUsereqExtension(pi);
|
|
2873
|
+
const commandCtx = createFakeCtx(projectBase);
|
|
2874
|
+
await pi.emit("session_start", { reason: "startup" }, commandCtx);
|
|
2875
|
+
|
|
2876
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", commandCtx);
|
|
2877
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2878
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2879
|
+
assert.ok(worktreeMatch, promptText);
|
|
2880
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
2881
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2882
|
+
const executionSessionFile = commandCtx.sessionManager.getSessionFile() ?? "";
|
|
2883
|
+
assert.notEqual(executionSessionFile, "");
|
|
2884
|
+
|
|
2885
|
+
const eventCtx = createFakeCtx(executionBasePath);
|
|
2886
|
+
eventCtx.sessionManager.getSessionFile = () => executionSessionFile;
|
|
2887
|
+
eventCtx.sessionManager.getSessionDir = () => path.dirname(executionSessionFile);
|
|
2888
|
+
eventCtx.sessionManager.getCwd = () => executionBasePath;
|
|
2889
|
+
eventCtx.cwd = executionBasePath;
|
|
2890
|
+
delete eventCtx.switchSession;
|
|
2891
|
+
|
|
2892
|
+
await pi.emit("before_agent_start", {}, eventCtx);
|
|
2893
|
+
await pi.emit("agent_start", {}, eventCtx);
|
|
2894
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
2895
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "closure-context.ts"), "export const CLOSURE_CONTEXT = 1;\n", "utf8");
|
|
2896
|
+
assert.equal(spawnSync("git", ["add", "src/closure-context.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
2897
|
+
const worktreeCommit = spawnSync("git", ["commit", "-m", "closure context"], {
|
|
2898
|
+
cwd: executionBasePath,
|
|
2899
|
+
encoding: "utf8",
|
|
2900
|
+
});
|
|
2901
|
+
assert.equal(worktreeCommit.status, 0, worktreeCommit.stderr);
|
|
2902
|
+
|
|
2903
|
+
await pi.emit("agent_end", {
|
|
2904
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2905
|
+
}, eventCtx);
|
|
2906
|
+
|
|
2907
|
+
assert.equal(process.cwd(), projectBase);
|
|
2908
|
+
assert.equal(commandCtx.cwd, projectBase);
|
|
2909
|
+
assert.ok(fs.existsSync(path.join(projectBase, "src", "closure-context.ts")));
|
|
2910
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
2911
|
+
assert.notEqual(
|
|
2912
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
2913
|
+
cwd: projectBase,
|
|
2914
|
+
encoding: "utf8",
|
|
2915
|
+
}).status,
|
|
2916
|
+
0,
|
|
2917
|
+
);
|
|
2918
|
+
assert.equal(commandCtx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
|
|
2919
|
+
} finally {
|
|
2920
|
+
process.chdir(previousCwd);
|
|
2921
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2922
|
+
}
|
|
2923
|
+
});
|
|
2924
|
+
|
|
2925
|
+
/**
|
|
2926
|
+
* @brief Verifies switch-triggered `session_shutdown` preserves prompt-command state for same-runtime workflow closure.
|
|
2927
|
+
* @details Simulates the real pi runtime ordering captured in `/tmp/debug.json`, where `ctx.switchSession(...)` can trigger `session_shutdown` before the initiating command handler resumes. The test proves the initiating runtime must preserve its in-memory workflow state plus pending prompt request across that shutdown so activation remains in `checking`, the handler transitions to `running`, and later `agent_end` restores `base-path`, merges the successful worktree branch, and deletes the worktree plus branch.
|
|
2928
|
+
* @return {Promise<void>} Promise resolved after session-shutdown preservation and closure assertions complete.
|
|
2929
|
+
* @throws {AssertionError} Throws when switch-triggered shutdown clears prompt state, logs the wrong workflow state, or prevents merge and cleanup.
|
|
2930
|
+
* @satisfies REQ-221, REQ-227, REQ-278, TST-090
|
|
2931
|
+
*/
|
|
2932
|
+
test("switch-triggered session_shutdown preserves prompt state for same-runtime workflow closure", async () => {
|
|
2933
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2934
|
+
const previousCwd = process.cwd();
|
|
2935
|
+
try {
|
|
2936
|
+
writeProjectConfigOverrides(projectBase, {
|
|
2937
|
+
DEBUG_ENABLED: "enable",
|
|
2938
|
+
DEBUG_LOG_FILE: "prompt-debug.json",
|
|
2939
|
+
DEBUG_STATUS_CHANGES: "enable",
|
|
2940
|
+
DEBUG_WORKFLOW_EVENTS: "enable",
|
|
2941
|
+
DEBUG_LOG_ON_STATUS: "any",
|
|
2942
|
+
DEBUG_ENABLED_PROMPTS: ["req-change"],
|
|
2943
|
+
});
|
|
2944
|
+
const pi = createFakePi();
|
|
2945
|
+
piUsereqExtension(pi);
|
|
2946
|
+
const ctx = createFakeCtx(projectBase);
|
|
2947
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2948
|
+
|
|
2949
|
+
const originalSwitchSession = ctx.switchSession.bind(ctx);
|
|
2950
|
+
ctx.switchSession = async (sessionPath: string) => {
|
|
2951
|
+
await pi.emit("session_shutdown", {
|
|
2952
|
+
reason: "resume",
|
|
2953
|
+
targetSessionFile: sessionPath,
|
|
2954
|
+
}, ctx);
|
|
2955
|
+
return originalSwitchSession(sessionPath);
|
|
2956
|
+
};
|
|
2957
|
+
|
|
2958
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2959
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2960
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2961
|
+
assert.ok(worktreeMatch, promptText);
|
|
2962
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2963
|
+
|
|
2964
|
+
const activationLog = JSON.parse(fs.readFileSync(path.join(projectBase, "prompt-debug.json"), "utf8"));
|
|
2965
|
+
const shutdownEntry = activationLog.find((entry: any) => entry.action === "workflow_session_shutdown");
|
|
2966
|
+
assert.equal(shutdownEntry?.workflow_state, "checking");
|
|
2967
|
+
assert.equal(shutdownEntry?.result?.preserve_prompt_command_state, true);
|
|
2968
|
+
assert.equal(shutdownEntry?.result?.pending_prompt_request, true);
|
|
2969
|
+
assert.equal(shutdownEntry?.result?.active_prompt_request, false);
|
|
2970
|
+
assert.ok(activationLog.some((entry: any) => entry.action === "workflow_activation" && entry.workflow_state === "checking"));
|
|
2971
|
+
assert.ok(
|
|
2972
|
+
activationLog.some(
|
|
2973
|
+
(entry: any) => entry.action === "workflow_state"
|
|
2974
|
+
&& entry.input?.from_state === "checking"
|
|
2975
|
+
&& entry.result?.workflow_state === "running",
|
|
2976
|
+
),
|
|
2977
|
+
);
|
|
2978
|
+
|
|
2979
|
+
await pi.emit("before_agent_start", {}, ctx);
|
|
2980
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2981
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
2982
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "shutdown-preserve.ts"), "export const SHUTDOWN_PRESERVE = 1;\n", "utf8");
|
|
2983
|
+
assert.equal(spawnSync("git", ["add", "src/shutdown-preserve.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
2984
|
+
const worktreeCommit = spawnSync("git", ["commit", "-m", "shutdown preserve"], {
|
|
2985
|
+
cwd: executionBasePath,
|
|
2986
|
+
encoding: "utf8",
|
|
2987
|
+
});
|
|
2988
|
+
assert.equal(worktreeCommit.status, 0, worktreeCommit.stderr);
|
|
2989
|
+
|
|
2990
|
+
await pi.emit("agent_end", {
|
|
2991
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2992
|
+
}, ctx);
|
|
2993
|
+
|
|
2994
|
+
assert.equal(process.cwd(), projectBase);
|
|
2995
|
+
assert.equal(ctx.cwd, projectBase);
|
|
2996
|
+
assert.ok(fs.existsSync(path.join(projectBase, "src", "shutdown-preserve.ts")));
|
|
2997
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
2998
|
+
assert.equal(ctx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
|
|
2999
|
+
} finally {
|
|
3000
|
+
process.chdir(previousCwd);
|
|
3001
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3002
|
+
}
|
|
3003
|
+
});
|
|
3004
|
+
|
|
3005
|
+
/**
|
|
3006
|
+
* @brief Verifies replacement-session runtimes resynchronize persisted prompt state after switch-triggered rebinding.
|
|
3007
|
+
* @details Simulates the real pi ordering where the new execution-session extension instance receives `session_start` before the initiating command handler finishes the post-switch `running` transition. The test proves later lifecycle hooks on the replacement-session runtime must re-read persisted prompt-command state so `agent_end` still recognizes a matched successful run, restores `base-path`, merges the worktree branch, and deletes the worktree plus branch.
|
|
3008
|
+
* @return {Promise<void>} Promise resolved after rebound-session closure assertions complete.
|
|
3009
|
+
* @throws {AssertionError} Throws when the replacement-session runtime misses the persisted `running` state and skips merge plus cleanup.
|
|
3010
|
+
* @satisfies REQ-227, REQ-279, TST-091
|
|
3011
|
+
*/
|
|
3012
|
+
test("replacement-session runtime resynchronizes persisted running state before prompt closure", async () => {
|
|
3013
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3014
|
+
const previousCwd = process.cwd();
|
|
3015
|
+
try {
|
|
3016
|
+
writeProjectConfigOverrides(projectBase, {
|
|
3017
|
+
DEBUG_ENABLED: "enable",
|
|
3018
|
+
DEBUG_LOG_FILE: "prompt-debug.json",
|
|
3019
|
+
DEBUG_STATUS_CHANGES: "enable",
|
|
3020
|
+
DEBUG_WORKFLOW_EVENTS: "enable",
|
|
3021
|
+
DEBUG_LOG_ON_STATUS: "any",
|
|
3022
|
+
DEBUG_ENABLED_PROMPTS: ["req-change"],
|
|
3023
|
+
});
|
|
3024
|
+
const firstPi = createFakePi();
|
|
3025
|
+
piUsereqExtension(firstPi);
|
|
3026
|
+
const firstCtx = createFakeCtx(projectBase);
|
|
3027
|
+
await firstPi.emit("session_start", { reason: "startup" }, firstCtx);
|
|
3028
|
+
|
|
3029
|
+
const originalSessionFile = firstCtx.sessionManager.getSessionFile() ?? "";
|
|
3030
|
+
const originalSwitchSession = firstCtx.switchSession.bind(firstCtx);
|
|
3031
|
+
let reboundPi: FakePi | undefined;
|
|
3032
|
+
let reboundCtx: any;
|
|
3033
|
+
firstCtx.switchSession = async (sessionPath: string) => {
|
|
3034
|
+
const executionSessionCwd = readFakeSessionFileCwd(sessionPath, firstCtx.cwd);
|
|
3035
|
+
await firstPi.emit("session_shutdown", {
|
|
3036
|
+
reason: "resume",
|
|
3037
|
+
targetSessionFile: sessionPath,
|
|
3038
|
+
}, firstCtx);
|
|
3039
|
+
if (!reboundPi) {
|
|
3040
|
+
reboundPi = createFakePi();
|
|
3041
|
+
piUsereqExtension(reboundPi);
|
|
3042
|
+
reboundCtx = createFakeCtx(executionSessionCwd);
|
|
3043
|
+
reboundCtx.sessionManager.getSessionFile = () => sessionPath;
|
|
3044
|
+
reboundCtx.sessionManager.getSessionDir = () => path.dirname(sessionPath);
|
|
3045
|
+
reboundCtx.sessionManager.getCwd = () => executionSessionCwd;
|
|
3046
|
+
reboundCtx.cwd = executionSessionCwd;
|
|
3047
|
+
await reboundPi.emit("session_start", {
|
|
3048
|
+
reason: "resume",
|
|
3049
|
+
previousSessionFile: originalSessionFile,
|
|
3050
|
+
}, reboundCtx);
|
|
3051
|
+
}
|
|
3052
|
+
return originalSwitchSession(sessionPath);
|
|
3053
|
+
};
|
|
3054
|
+
|
|
3055
|
+
await firstPi.commands.get("req-change")!.handler("Adjust docs", firstCtx);
|
|
3056
|
+
assert.ok(reboundPi);
|
|
3057
|
+
assert.ok(reboundCtx);
|
|
3058
|
+
const promptText = String(firstPi.sentUserMessages[0]?.content ?? "");
|
|
3059
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3060
|
+
assert.ok(worktreeMatch, promptText);
|
|
3061
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3062
|
+
|
|
3063
|
+
await reboundPi!.emit("before_agent_start", {}, reboundCtx);
|
|
3064
|
+
await reboundPi!.emit("agent_start", {}, reboundCtx);
|
|
3065
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
3066
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "rebound-running.ts"), "export const REBOUND_RUNNING = 1;\n", "utf8");
|
|
3067
|
+
assert.equal(spawnSync("git", ["add", "src/rebound-running.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
3068
|
+
const reboundCommit = spawnSync("git", ["commit", "-m", "rebound running"], { cwd: executionBasePath, encoding: "utf8" });
|
|
3069
|
+
assert.equal(reboundCommit.status, 0, reboundCommit.stderr);
|
|
3070
|
+
|
|
3071
|
+
await reboundPi!.emit("agent_end", {
|
|
3072
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
3073
|
+
}, reboundCtx);
|
|
3074
|
+
|
|
3075
|
+
const debugLog = JSON.parse(fs.readFileSync(path.join(projectBase, "prompt-debug.json"), "utf8"));
|
|
3076
|
+
assert.ok(
|
|
3077
|
+
debugLog.some(
|
|
3078
|
+
(entry: any) => entry.action === "workflow_closure"
|
|
3079
|
+
&& entry.result?.should_finalize_matched_success === true,
|
|
3080
|
+
),
|
|
3081
|
+
);
|
|
3082
|
+
assert.equal(process.cwd(), projectBase);
|
|
3083
|
+
assert.equal(reboundCtx.cwd, projectBase);
|
|
3084
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
3085
|
+
assert.equal(fs.existsSync(path.join(projectBase, "src", "rebound-running.ts")), true);
|
|
3086
|
+
assert.equal(reboundCtx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
|
|
3087
|
+
} finally {
|
|
3088
|
+
process.chdir(previousCwd);
|
|
3089
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3090
|
+
}
|
|
3091
|
+
});
|
|
3092
|
+
|
|
3093
|
+
/**
|
|
3094
|
+
* @brief Verifies `running` is persisted before async replacement-session prompt delivery completes.
|
|
3095
|
+
* @details Simulates the real pi runtime behavior visible in `/tmp/debug.json`, where replacement-session `sendUserMessage(...)` does not resolve until the prompt run has already reached `agent_end`. The test proves `req-<prompt>` must transition and persist `running` immediately after prompt handoff begins, so a rebound execution-session runtime can still finalize the matched successful closure while the initiating command handler remains blocked on prompt-delivery completion.
|
|
3096
|
+
* @return {Promise<void>} Promise resolved after delayed prompt delivery, rebound closure, and cleanup assertions complete.
|
|
3097
|
+
* @throws {AssertionError} Throws when `running` is delayed until after `agent_end`, causing closure to skip merge or cleanup.
|
|
3098
|
+
* @satisfies REQ-227, REQ-279, REQ-281, TST-093
|
|
3099
|
+
*/
|
|
3100
|
+
test("replacement-session prompt delivery persists running before async sendUserMessage resolves", async () => {
|
|
3101
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3102
|
+
const previousCwd = process.cwd();
|
|
3103
|
+
try {
|
|
3104
|
+
writeProjectConfigOverrides(projectBase, {
|
|
3105
|
+
DEBUG_ENABLED: "enable",
|
|
3106
|
+
DEBUG_LOG_FILE: "prompt-debug.json",
|
|
3107
|
+
DEBUG_STATUS_CHANGES: "enable",
|
|
3108
|
+
DEBUG_WORKFLOW_EVENTS: "enable",
|
|
3109
|
+
DEBUG_LOG_ON_STATUS: "any",
|
|
3110
|
+
DEBUG_ENABLED_PROMPTS: ["req-change"],
|
|
3111
|
+
});
|
|
3112
|
+
const firstPi = createFakePi();
|
|
3113
|
+
piUsereqExtension(firstPi);
|
|
3114
|
+
const firstCtx = createFakeCtx(projectBase);
|
|
3115
|
+
await firstPi.emit("session_start", { reason: "startup" }, firstCtx);
|
|
3116
|
+
|
|
3117
|
+
const originalSessionFile = firstCtx.sessionManager.getSessionFile() ?? "";
|
|
3118
|
+
const originalSwitchSession = firstCtx.switchSession.bind(firstCtx);
|
|
3119
|
+
let reboundPi: FakePi | undefined;
|
|
3120
|
+
let reboundCtx: any;
|
|
3121
|
+
let resolvePromptDelivery: (() => void) | undefined;
|
|
3122
|
+
let handoffStatus = "";
|
|
3123
|
+
firstCtx.switchSession = async (
|
|
3124
|
+
sessionPath: string,
|
|
3125
|
+
options?: { withSession?: (replacementCtx: any) => Promise<void> },
|
|
3126
|
+
) => {
|
|
3127
|
+
const executionSessionCwd = readFakeSessionFileCwd(sessionPath, firstCtx.cwd);
|
|
3128
|
+
await firstPi.emit("session_shutdown", {
|
|
3129
|
+
reason: "resume",
|
|
3130
|
+
targetSessionFile: sessionPath,
|
|
3131
|
+
}, firstCtx);
|
|
3132
|
+
if (!reboundPi) {
|
|
3133
|
+
reboundPi = createFakePi();
|
|
3134
|
+
piUsereqExtension(reboundPi);
|
|
3135
|
+
reboundCtx = createFakeCtx(executionSessionCwd);
|
|
3136
|
+
reboundCtx.sessionManager.getSessionFile = () => sessionPath;
|
|
3137
|
+
reboundCtx.sessionManager.getSessionDir = () => path.dirname(sessionPath);
|
|
3138
|
+
reboundCtx.sessionManager.getCwd = () => executionSessionCwd;
|
|
3139
|
+
reboundCtx.cwd = executionSessionCwd;
|
|
3140
|
+
await reboundPi.emit("session_start", {
|
|
3141
|
+
reason: "resume",
|
|
3142
|
+
previousSessionFile: originalSessionFile,
|
|
3143
|
+
}, reboundCtx);
|
|
3144
|
+
}
|
|
3145
|
+
const switchResult = await originalSwitchSession(sessionPath);
|
|
3146
|
+
const replacementCtx = {
|
|
3147
|
+
...firstCtx,
|
|
3148
|
+
cwd: executionSessionCwd,
|
|
3149
|
+
sessionManager: {
|
|
3150
|
+
...firstCtx.sessionManager,
|
|
3151
|
+
getCwd: () => executionSessionCwd,
|
|
3152
|
+
getSessionFile: () => sessionPath,
|
|
3153
|
+
getSessionDir: () => path.dirname(sessionPath),
|
|
3154
|
+
},
|
|
3155
|
+
switchSession: firstCtx.switchSession,
|
|
3156
|
+
async sendUserMessage(content: unknown, sendOptions?: unknown) {
|
|
3157
|
+
handoffStatus = firstCtx.__state.statuses.get("pi-usereq") ?? "";
|
|
3158
|
+
firstPi.sentUserMessages.push({ content, options: sendOptions });
|
|
3159
|
+
await new Promise<void>((resolve) => {
|
|
3160
|
+
resolvePromptDelivery = resolve;
|
|
3161
|
+
});
|
|
3162
|
+
},
|
|
3163
|
+
};
|
|
3164
|
+
await options?.withSession?.(replacementCtx);
|
|
3165
|
+
return switchResult;
|
|
3166
|
+
};
|
|
3167
|
+
|
|
3168
|
+
const handlerPromise = firstPi.commands.get("req-change")!.handler("Adjust docs", firstCtx);
|
|
3169
|
+
for (let attempt = 0; attempt < 20 && resolvePromptDelivery === undefined; attempt += 1) {
|
|
3170
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
3171
|
+
}
|
|
3172
|
+
assert.ok(resolvePromptDelivery);
|
|
3173
|
+
assert.ok(reboundPi);
|
|
3174
|
+
assert.ok(reboundCtx);
|
|
3175
|
+
const promptText = String(firstPi.sentUserMessages[0]?.content ?? "");
|
|
3176
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3177
|
+
assert.ok(worktreeMatch, promptText);
|
|
3178
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3179
|
+
|
|
3180
|
+
assert.equal(
|
|
3181
|
+
handoffStatus,
|
|
3182
|
+
buildExpectedFakeStatusText({
|
|
3183
|
+
workflowState: "checking",
|
|
3184
|
+
basePath: buildExpectedFakeBasePath(executionBasePath),
|
|
3185
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
3186
|
+
testsDir: "tests",
|
|
3187
|
+
srcDir: ["src"],
|
|
3188
|
+
contextFilledCells: 0,
|
|
3189
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
3190
|
+
}),
|
|
3191
|
+
);
|
|
3192
|
+
assert.equal(
|
|
3193
|
+
firstCtx.__state.statuses.get("pi-usereq"),
|
|
3194
|
+
buildExpectedFakeStatusText({
|
|
3195
|
+
workflowState: "running",
|
|
3196
|
+
basePath: buildExpectedFakeBasePath(executionBasePath),
|
|
3197
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
3198
|
+
testsDir: "tests",
|
|
3199
|
+
srcDir: ["src"],
|
|
3200
|
+
contextFilledCells: 0,
|
|
3201
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
3202
|
+
}),
|
|
3203
|
+
);
|
|
3204
|
+
|
|
3205
|
+
await reboundPi!.emit("before_agent_start", {}, reboundCtx);
|
|
3206
|
+
await reboundPi!.emit("agent_start", {}, reboundCtx);
|
|
3207
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
3208
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "delayed-running.ts"), "export const DELAYED_RUNNING = 1;\n", "utf8");
|
|
3209
|
+
assert.equal(spawnSync("git", ["add", "src/delayed-running.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
3210
|
+
const reboundCommit = spawnSync("git", ["commit", "-m", "delayed running"], { cwd: executionBasePath, encoding: "utf8" });
|
|
3211
|
+
assert.equal(reboundCommit.status, 0, reboundCommit.stderr);
|
|
3212
|
+
|
|
3213
|
+
await reboundPi!.emit("agent_end", {
|
|
3214
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
3215
|
+
}, reboundCtx);
|
|
3216
|
+
|
|
3217
|
+
const debugLog = JSON.parse(fs.readFileSync(path.join(projectBase, "prompt-debug.json"), "utf8"));
|
|
3218
|
+
assert.ok(
|
|
3219
|
+
debugLog.some(
|
|
3220
|
+
(entry: any) => entry.action === "workflow_closure"
|
|
3221
|
+
&& entry.result?.should_finalize_matched_success === true,
|
|
3222
|
+
),
|
|
3223
|
+
);
|
|
3224
|
+
|
|
3225
|
+
resolvePromptDelivery!();
|
|
3226
|
+
await handlerPromise;
|
|
3227
|
+
|
|
3228
|
+
assert.equal(process.cwd(), projectBase);
|
|
3229
|
+
assert.equal(reboundCtx.cwd, projectBase);
|
|
3230
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
3231
|
+
assert.equal(fs.existsSync(path.join(projectBase, "src", "delayed-running.ts")), true);
|
|
3232
|
+
assert.equal(reboundCtx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
|
|
3233
|
+
} finally {
|
|
3234
|
+
process.chdir(previousCwd);
|
|
3235
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3236
|
+
}
|
|
3237
|
+
});
|
|
3238
|
+
|
|
3239
|
+
/**
|
|
3240
|
+
* @brief Verifies late stale replacement-session prompt-delivery rejections do not abort successful worktree closure.
|
|
3241
|
+
* @details Simulates the real pi runtime behavior observed in `/tmp/debug.json`, where the replacement-session `sendUserMessage(...)` promise rejects with the stale-extension-context error only after prompt-end restoration switches back to the original session. The test proves the initiating `req-<prompt>` handler MUST ignore that late stale rejection once workflow ownership has already advanced beyond `running`, allowing the rebound execution-session runtime to restore `base-path`, merge from `base-path`, and delete the worktree plus branch.
|
|
3242
|
+
* @return {Promise<void>} Promise resolved after stale delivery rejection, matched-success closure, and cleanup assertions complete.
|
|
3243
|
+
* @throws {AssertionError} Throws when the late stale rejection bubbles out of the command handler or prevents merge plus cleanup.
|
|
3244
|
+
* @satisfies REQ-208, REQ-276, REQ-278, REQ-279, REQ-280, REQ-281, REQ-282
|
|
3245
|
+
*/
|
|
3246
|
+
test("late stale replacement-session prompt-delivery rejections do not interrupt worktree closure", async () => {
|
|
3247
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3248
|
+
const previousCwd = process.cwd();
|
|
3249
|
+
try {
|
|
3250
|
+
writeProjectConfigOverrides(projectBase, {
|
|
3251
|
+
DEBUG_ENABLED: "enable",
|
|
3252
|
+
DEBUG_LOG_FILE: "prompt-debug.json",
|
|
3253
|
+
DEBUG_STATUS_CHANGES: "enable",
|
|
3254
|
+
DEBUG_WORKFLOW_EVENTS: "enable",
|
|
3255
|
+
DEBUG_LOG_ON_STATUS: "any",
|
|
3256
|
+
DEBUG_ENABLED_PROMPTS: ["req-change"],
|
|
3257
|
+
});
|
|
3258
|
+
const firstPi = createFakePi();
|
|
3259
|
+
piUsereqExtension(firstPi);
|
|
3260
|
+
const firstCtx = createFakeCtx(projectBase);
|
|
3261
|
+
await firstPi.emit("session_start", { reason: "startup" }, firstCtx);
|
|
3262
|
+
|
|
3263
|
+
const originalSessionFile = firstCtx.sessionManager.getSessionFile() ?? "";
|
|
3264
|
+
const originalSwitchSession = firstCtx.switchSession.bind(firstCtx);
|
|
3265
|
+
let reboundPi: FakePi | undefined;
|
|
3266
|
+
let reboundCtx: any;
|
|
3267
|
+
let rejectPromptDelivery: ((error: unknown) => void) | undefined;
|
|
3268
|
+
firstCtx.switchSession = async (
|
|
3269
|
+
sessionPath: string,
|
|
3270
|
+
options?: { withSession?: (replacementCtx: any) => Promise<void> },
|
|
3271
|
+
) => {
|
|
3272
|
+
const replacementCwd = readFakeSessionFileCwd(sessionPath, firstCtx.cwd);
|
|
3273
|
+
await firstPi.emit("session_shutdown", {
|
|
3274
|
+
reason: "resume",
|
|
3275
|
+
targetSessionFile: sessionPath,
|
|
3276
|
+
}, firstCtx);
|
|
3277
|
+
if (!reboundPi) {
|
|
3278
|
+
reboundPi = createFakePi();
|
|
3279
|
+
piUsereqExtension(reboundPi);
|
|
3280
|
+
reboundCtx = createFakeCtx(replacementCwd);
|
|
3281
|
+
reboundCtx.sessionManager.getSessionFile = () => sessionPath;
|
|
3282
|
+
reboundCtx.sessionManager.getSessionDir = () => path.dirname(sessionPath);
|
|
3283
|
+
reboundCtx.sessionManager.getCwd = () => replacementCwd;
|
|
3284
|
+
reboundCtx.cwd = replacementCwd;
|
|
3285
|
+
await reboundPi.emit("session_start", {
|
|
3286
|
+
reason: "resume",
|
|
3287
|
+
previousSessionFile: originalSessionFile,
|
|
3288
|
+
}, reboundCtx);
|
|
3289
|
+
}
|
|
3290
|
+
const switchResult = await originalSwitchSession(sessionPath);
|
|
3291
|
+
const replacementCtx = {
|
|
3292
|
+
...firstCtx,
|
|
3293
|
+
cwd: replacementCwd,
|
|
3294
|
+
sessionManager: {
|
|
3295
|
+
...firstCtx.sessionManager,
|
|
3296
|
+
getCwd: () => replacementCwd,
|
|
3297
|
+
getSessionFile: () => sessionPath,
|
|
3298
|
+
getSessionDir: () => path.dirname(sessionPath),
|
|
3299
|
+
},
|
|
3300
|
+
switchSession: firstCtx.switchSession,
|
|
3301
|
+
async sendUserMessage(content: unknown, sendOptions?: unknown) {
|
|
3302
|
+
firstPi.sentUserMessages.push({ content, options: sendOptions });
|
|
3303
|
+
await new Promise<void>((_resolve, reject) => {
|
|
3304
|
+
rejectPromptDelivery = reject;
|
|
3305
|
+
});
|
|
3306
|
+
},
|
|
3307
|
+
};
|
|
3308
|
+
if (path.resolve(sessionPath) === path.resolve(originalSessionFile)) {
|
|
3309
|
+
rejectPromptDelivery?.(
|
|
3310
|
+
new Error("This extension ctx is stale after session replacement or reload."),
|
|
3311
|
+
);
|
|
3312
|
+
rejectPromptDelivery = undefined;
|
|
3313
|
+
}
|
|
3314
|
+
await options?.withSession?.(replacementCtx);
|
|
3315
|
+
return switchResult;
|
|
3316
|
+
};
|
|
3317
|
+
|
|
3318
|
+
const handlerPromise = firstPi.commands.get("req-change")!.handler("Adjust docs", firstCtx);
|
|
3319
|
+
const handlerOutcomePromise = handlerPromise.then(
|
|
3320
|
+
() => ({ error: undefined as unknown }),
|
|
3321
|
+
(error) => ({ error }),
|
|
3322
|
+
);
|
|
3323
|
+
for (let attempt = 0; attempt < 20 && rejectPromptDelivery === undefined; attempt += 1) {
|
|
3324
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
3325
|
+
}
|
|
3326
|
+
assert.ok(rejectPromptDelivery);
|
|
3327
|
+
assert.ok(reboundPi);
|
|
3328
|
+
assert.ok(reboundCtx);
|
|
3329
|
+
const promptText = String(firstPi.sentUserMessages[0]?.content ?? "");
|
|
3330
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3331
|
+
assert.ok(worktreeMatch, promptText);
|
|
3332
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
3333
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3334
|
+
const executionSessionFile = firstCtx.sessionManager.getSessionFile() ?? "";
|
|
3335
|
+
assert.notEqual(executionSessionFile, "");
|
|
3336
|
+
|
|
3337
|
+
await reboundPi!.emit("before_agent_start", {}, reboundCtx);
|
|
3338
|
+
await reboundPi!.emit("agent_start", {}, reboundCtx);
|
|
3339
|
+
|
|
3340
|
+
const persistedContext = readPersistedPromptCommandSessionContext(executionSessionFile);
|
|
3341
|
+
assert.ok(persistedContext);
|
|
3342
|
+
reboundCtx.switchSession = async (sessionPath: string) => {
|
|
3343
|
+
assert.equal(path.resolve(sessionPath), path.resolve(originalSessionFile));
|
|
3344
|
+
const pendingPromptDeliveryRejection = rejectPromptDelivery;
|
|
3345
|
+
rejectPromptDelivery = undefined;
|
|
3346
|
+
queueMicrotask(() => {
|
|
3347
|
+
pendingPromptDeliveryRejection?.(
|
|
3348
|
+
new Error("This extension ctx is stale after session replacement or reload."),
|
|
3349
|
+
);
|
|
3350
|
+
});
|
|
3351
|
+
reboundCtx.cwd = projectBase;
|
|
3352
|
+
reboundCtx.sessionManager.getSessionFile = () => originalSessionFile;
|
|
3353
|
+
reboundCtx.sessionManager.getSessionDir = () => path.dirname(originalSessionFile);
|
|
3354
|
+
reboundCtx.sessionManager.getCwd = () => projectBase;
|
|
3355
|
+
process.chdir(projectBase);
|
|
3356
|
+
return { cancelled: false };
|
|
3357
|
+
};
|
|
3358
|
+
|
|
3359
|
+
await Promise.race([
|
|
3360
|
+
reboundPi!.emit("agent_end", {
|
|
3361
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
3362
|
+
}, reboundCtx),
|
|
3363
|
+
new Promise<never>((_resolve, reject) => {
|
|
3364
|
+
setTimeout(() => reject(new Error("agent_end timed out")), 1_000);
|
|
3365
|
+
}),
|
|
3366
|
+
]);
|
|
3367
|
+
assert.equal(rejectPromptDelivery, undefined);
|
|
3368
|
+
const handlerOutcome = await Promise.race([
|
|
3369
|
+
handlerOutcomePromise,
|
|
3370
|
+
new Promise<never>((_resolve, reject) => {
|
|
3371
|
+
setTimeout(() => reject(new Error("handler timed out")), 1_000);
|
|
3372
|
+
}),
|
|
3373
|
+
]);
|
|
3374
|
+
assert.equal(handlerOutcome.error, undefined);
|
|
3375
|
+
|
|
3376
|
+
const debugLog = JSON.parse(fs.readFileSync(path.join(projectBase, "prompt-debug.json"), "utf8"));
|
|
3377
|
+
assert.ok(debugLog.some((entry: any) => entry.action === "workflow_restore" && entry.result?.success === true));
|
|
3378
|
+
assert.ok(debugLog.some((entry: any) => entry.action === "merge" && entry.result?.success === true));
|
|
3379
|
+
assert.equal(process.cwd(), projectBase);
|
|
3380
|
+
assert.equal(reboundCtx.cwd, projectBase);
|
|
3381
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
3382
|
+
assert.notEqual(
|
|
3383
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
3384
|
+
cwd: projectBase,
|
|
3385
|
+
encoding: "utf8",
|
|
3386
|
+
}).status,
|
|
3387
|
+
0,
|
|
3388
|
+
);
|
|
3389
|
+
assert.equal(reboundCtx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
|
|
3390
|
+
} finally {
|
|
3391
|
+
process.chdir(previousCwd);
|
|
3392
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3393
|
+
}
|
|
3394
|
+
});
|
|
3395
|
+
|
|
3396
|
+
test("worktree-backed prompt execution survives switch-triggered extension rebinding", async () => {
|
|
3397
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3398
|
+
const previousCwd = process.cwd();
|
|
3399
|
+
try {
|
|
3400
|
+
const firstPi = createFakePi();
|
|
3401
|
+
piUsereqExtension(firstPi);
|
|
3402
|
+
const firstCtx = createFakeCtx(projectBase);
|
|
3403
|
+
await firstPi.emit("session_start", { reason: "startup" }, firstCtx);
|
|
3404
|
+
|
|
3405
|
+
await firstPi.commands.get("req-change")!.handler("Adjust docs", firstCtx);
|
|
3406
|
+
const promptText = String(firstPi.sentUserMessages[0]?.content ?? "");
|
|
3407
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3408
|
+
assert.ok(worktreeMatch, promptText);
|
|
3409
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3410
|
+
const executionSessionFile = firstCtx.sessionManager.getSessionFile() ?? "";
|
|
3411
|
+
assert.notEqual(executionSessionFile, "");
|
|
3412
|
+
|
|
3413
|
+
const reboundPi = createFakePi();
|
|
3414
|
+
piUsereqExtension(reboundPi);
|
|
3415
|
+
const reboundCtx = createFakeCtx(executionBasePath);
|
|
3416
|
+
reboundCtx.sessionManager.getSessionFile = () => executionSessionFile;
|
|
3417
|
+
reboundCtx.sessionManager.getSessionDir = () => path.dirname(executionSessionFile);
|
|
3418
|
+
reboundCtx.sessionManager.getCwd = () => executionBasePath;
|
|
3419
|
+
reboundCtx.cwd = executionBasePath;
|
|
3420
|
+
await reboundPi.emit("session_start", {
|
|
3421
|
+
reason: "resume",
|
|
3422
|
+
previousSessionFile: firstCtx.sessionManager.getSessionFile(),
|
|
3423
|
+
}, reboundCtx);
|
|
3424
|
+
|
|
3425
|
+
await reboundPi.emit("before_agent_start", {}, reboundCtx);
|
|
3426
|
+
await reboundPi.emit("agent_start", {}, reboundCtx);
|
|
3427
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
3428
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "rebound.ts"), "export const REBOUND = 1;\n", "utf8");
|
|
3429
|
+
assert.equal(spawnSync("git", ["add", "src/rebound.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
3430
|
+
const reboundCommit = spawnSync("git", ["commit", "-m", "rebound"], { cwd: executionBasePath, encoding: "utf8" });
|
|
3431
|
+
assert.equal(reboundCommit.status, 0, reboundCommit.stderr);
|
|
3432
|
+
|
|
3433
|
+
await reboundPi.emit("agent_end", {
|
|
3434
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
3435
|
+
}, reboundCtx);
|
|
3436
|
+
|
|
3437
|
+
assert.equal(process.cwd(), projectBase);
|
|
3438
|
+
assert.equal(reboundCtx.cwd, projectBase);
|
|
3439
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
3440
|
+
assert.equal(fs.existsSync(path.join(projectBase, "src", "rebound.ts")), true);
|
|
3441
|
+
assert.equal(reboundCtx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
|
|
3442
|
+
} finally {
|
|
3443
|
+
process.chdir(previousCwd);
|
|
3444
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3445
|
+
}
|
|
3446
|
+
});
|
|
3447
|
+
|
|
3448
|
+
/**
|
|
3449
|
+
* @brief Reproduces the defect where `ctx.switchSession(sessionPath)` ignores a `withSession` option and leaves the handler-scoped ctx frozen on the pre-switch session.
|
|
3450
|
+
* @details Simulates the real pi SDK contract observed in `@mariozechner/pi-coding-agent`, where `ctx.switchSession(sessionPath)` accepts a single argument, never invokes caller-supplied callbacks, and never mutates `ctx.cwd` or `ctx.sessionManager`. The test proves that worktree-backed `req-<prompt>` activation must succeed against that contract by relying on the persisted session-file header and `process.cwd()` instead of the stale handler-scoped `ctx` probes. Runtime is dominated by temporary git worktree setup and teardown. Side effects are limited to temporary repository mutation and temporary session-file writes.
|
|
3451
|
+
* @return {Promise<void>} Promise resolved after prompt delivery, activation, and assertions complete.
|
|
3452
|
+
* @throws {AssertionError} Throws when prompt activation aborts even though the persisted execution-session file and the host `process.cwd()` both match the worktree path.
|
|
3453
|
+
* @satisfies REQ-068, REQ-208, REQ-257, REQ-271, REQ-272
|
|
3454
|
+
*/
|
|
3455
|
+
test("worktree-backed prompt commands activate when ctx.switchSession only accepts a single argument and leaves handler ctx stale", async () => {
|
|
3456
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3457
|
+
const previousCwd = process.cwd();
|
|
3458
|
+
try {
|
|
3459
|
+
process.chdir(projectBase);
|
|
3460
|
+
const pi = createFakePi();
|
|
3461
|
+
piUsereqExtension(pi);
|
|
3462
|
+
const ctx = createFakeCtx(projectBase);
|
|
3463
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
3464
|
+
|
|
3465
|
+
const frozenSessionFile = ctx.sessionManager.getSessionFile() ?? "";
|
|
3466
|
+
const frozenSessionCwd = ctx.sessionManager.getCwd();
|
|
3467
|
+
ctx.switchSession = async (sessionPath: string, ..._ignored: unknown[]) => {
|
|
3468
|
+
// Mirror the real pi SDK: accept only sessionPath, never mutate ctx.cwd or ctx.sessionManager.
|
|
3469
|
+
void sessionPath;
|
|
3470
|
+
return { cancelled: false };
|
|
3471
|
+
};
|
|
3472
|
+
|
|
3473
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
3474
|
+
|
|
3475
|
+
assert.equal(pi.sentUserMessages.length, 1);
|
|
3476
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
3477
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3478
|
+
assert.ok(worktreeMatch, promptText);
|
|
3479
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3480
|
+
assert.equal(process.cwd(), executionBasePath);
|
|
3481
|
+
assert.equal(ctx.cwd, projectBase);
|
|
3482
|
+
assert.equal(ctx.sessionManager.getSessionFile(), frozenSessionFile);
|
|
3483
|
+
assert.equal(ctx.sessionManager.getCwd(), frozenSessionCwd);
|
|
3484
|
+
assert.equal(
|
|
3485
|
+
ctx.__state.notifications.some((entry) => /Prompt execution activation expected/.test(entry.message)),
|
|
3486
|
+
false,
|
|
3487
|
+
);
|
|
3488
|
+
} finally {
|
|
3489
|
+
process.chdir(previousCwd);
|
|
3490
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3491
|
+
}
|
|
3492
|
+
});
|
|
3493
|
+
|
|
3494
|
+
test("worktree-backed prompt commands tolerate stale ctx.sessionManager probes after switchSession returns non-cancelled", async () => {
|
|
3495
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3496
|
+
const previousCwd = process.cwd();
|
|
3497
|
+
try {
|
|
3498
|
+
const pi = createFakePi();
|
|
3499
|
+
piUsereqExtension(pi);
|
|
3500
|
+
const ctx = createFakeCtx(projectBase);
|
|
3501
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
3502
|
+
|
|
3503
|
+
const originalSessionFile = ctx.sessionManager.getSessionFile();
|
|
3504
|
+
const originalSessionCwd = ctx.sessionManager.getCwd();
|
|
3505
|
+
const originalSwitchSession = ctx.switchSession.bind(ctx);
|
|
3506
|
+
ctx.switchSession = async (sessionPath: string) => {
|
|
3507
|
+
// Mirror the real pi SDK: session switching runs internally but ctx.sessionManager stays frozen.
|
|
3508
|
+
const result = await originalSwitchSession(sessionPath);
|
|
3509
|
+
ctx.sessionManager.getSessionFile = () => originalSessionFile;
|
|
3510
|
+
ctx.sessionManager.getCwd = () => originalSessionCwd;
|
|
3511
|
+
return result;
|
|
3512
|
+
};
|
|
3513
|
+
|
|
3514
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
3515
|
+
|
|
3516
|
+
assert.equal(pi.sentUserMessages.length, 1);
|
|
3517
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
3518
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3519
|
+
assert.ok(worktreeMatch, promptText);
|
|
3520
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3521
|
+
assert.equal(process.cwd(), executionBasePath);
|
|
3522
|
+
assert.equal(ctx.sessionManager.getSessionFile(), originalSessionFile);
|
|
3523
|
+
assert.equal(ctx.sessionManager.getCwd(), originalSessionCwd);
|
|
3524
|
+
assert.equal(
|
|
3525
|
+
ctx.__state.notifications.some((entry) => /Prompt execution activation expected/.test(entry.message)),
|
|
3526
|
+
false,
|
|
3527
|
+
);
|
|
3528
|
+
} finally {
|
|
3529
|
+
process.chdir(previousCwd);
|
|
3530
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3531
|
+
}
|
|
3532
|
+
});
|
|
3533
|
+
|
|
3534
|
+
test("worktree-backed prompt commands tolerate a pending-persistence execution-session file when process.cwd() is aligned with worktree-path", async () => {
|
|
3535
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3536
|
+
const previousCwd = process.cwd();
|
|
3537
|
+
try {
|
|
3538
|
+
const pi = createFakePi();
|
|
3539
|
+
piUsereqExtension(pi);
|
|
3540
|
+
const ctx = createFakeCtx(projectBase);
|
|
3541
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
3542
|
+
|
|
3543
|
+
const originalSwitchSession = ctx.switchSession.bind(ctx);
|
|
3544
|
+
ctx.switchSession = async (sessionPath: string) => {
|
|
3545
|
+
const result = await originalSwitchSession(sessionPath);
|
|
3546
|
+
// Simulate pi's lazy session-file persistence: ctx.switchSession returns non-cancelled and realigns process.cwd(), but pi defers writing the session file until the first assistant flush.
|
|
3547
|
+
if (typeof sessionPath === "string" && fs.existsSync(sessionPath)) {
|
|
3548
|
+
fs.rmSync(sessionPath, { force: true });
|
|
3549
|
+
}
|
|
3550
|
+
return result;
|
|
3551
|
+
};
|
|
3552
|
+
|
|
3553
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
3554
|
+
|
|
3555
|
+
assert.equal(pi.sentUserMessages.length, 1);
|
|
3556
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
3557
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3558
|
+
assert.ok(worktreeMatch, promptText);
|
|
3559
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3560
|
+
assert.equal(process.cwd(), executionBasePath);
|
|
3561
|
+
assert.equal(
|
|
3562
|
+
ctx.__state.notifications.some((entry) => /Prompt execution activation expected session/.test(entry.message)),
|
|
3563
|
+
false,
|
|
3564
|
+
);
|
|
3565
|
+
} finally {
|
|
3566
|
+
process.chdir(previousCwd);
|
|
3567
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3568
|
+
}
|
|
3569
|
+
});
|
|
3570
|
+
|
|
3571
|
+
test("worktree-backed prompt commands abort before prompt dispatch when the persisted execution-session header cwd diverges from worktree-path", async () => {
|
|
3572
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3573
|
+
const previousCwd = process.cwd();
|
|
3574
|
+
try {
|
|
3575
|
+
const pi = createFakePi();
|
|
3576
|
+
piUsereqExtension(pi);
|
|
3577
|
+
const ctx = createFakeCtx(projectBase);
|
|
3578
|
+
const originalSwitchSession = ctx.switchSession.bind(ctx);
|
|
3579
|
+
ctx.switchSession = async (sessionPath: string) => {
|
|
3580
|
+
const result = await originalSwitchSession(sessionPath);
|
|
3581
|
+
// Simulate pi runtimes that persist a tampered header cwd, forcing the on-disk cwd verification to fail.
|
|
3582
|
+
if (typeof sessionPath === "string" && fs.existsSync(sessionPath)) {
|
|
3583
|
+
const tamperedHeader = `${JSON.stringify({ type: "session", cwd: projectBase })}\n`;
|
|
3584
|
+
fs.writeFileSync(sessionPath, tamperedHeader, "utf8");
|
|
3585
|
+
}
|
|
3586
|
+
return result;
|
|
3587
|
+
};
|
|
3588
|
+
|
|
3589
|
+
await assert.rejects(
|
|
3590
|
+
pi.commands.get("req-change")!.handler("Adjust docs", ctx),
|
|
3591
|
+
/Prompt execution activation expected session/,
|
|
3592
|
+
);
|
|
3593
|
+
|
|
3594
|
+
assert.equal(pi.sentUserMessages.length, 0);
|
|
3595
|
+
assert.ok(ctx.__state.notifications.some((entry) => /Prompt execution activation expected session/.test(entry.message)));
|
|
3596
|
+
} finally {
|
|
3597
|
+
process.chdir(previousCwd);
|
|
3598
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3599
|
+
}
|
|
3600
|
+
});
|
|
3601
|
+
|
|
3602
|
+
test("worktree-backed successful merge stops before merge when fork-session verification cannot reattach", async () => {
|
|
3603
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3604
|
+
const previousCwd = process.cwd();
|
|
3605
|
+
try {
|
|
3606
|
+
const pi = createFakePi();
|
|
3607
|
+
piUsereqExtension(pi);
|
|
3608
|
+
const ctx = createFakeCtx(projectBase);
|
|
3609
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
3610
|
+
|
|
3611
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
3612
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
3613
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3614
|
+
assert.ok(worktreeMatch, promptText);
|
|
3615
|
+
const executionSessionFile = ctx.sessionManager.getSessionFile();
|
|
3616
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3617
|
+
|
|
3618
|
+
await pi.emit("before_agent_start", {}, ctx);
|
|
3619
|
+
await pi.emit("agent_start", {}, ctx);
|
|
3620
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
3621
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "restore-failed.ts"), "export const RESTORE_FAILED = 1;\n", "utf8");
|
|
3622
|
+
assert.equal(spawnSync("git", ["add", "src/restore-failed.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
3623
|
+
const worktreeCommit = spawnSync("git", ["commit", "-m", "worktree change"], { cwd: executionBasePath, encoding: "utf8" });
|
|
3624
|
+
assert.equal(worktreeCommit.status, 0, worktreeCommit.stderr);
|
|
3625
|
+
|
|
3626
|
+
// Simulate a pi runtime that rewrites the execution-session header with a divergent cwd at prompt-end so closure verification must abort before merge.
|
|
3627
|
+
assert.equal(typeof executionSessionFile, "string");
|
|
3628
|
+
if (typeof executionSessionFile === "string" && fs.existsSync(executionSessionFile)) {
|
|
3629
|
+
const tamperedHeader = `${JSON.stringify({ type: "session", cwd: projectBase })}\n`;
|
|
3630
|
+
fs.writeFileSync(executionSessionFile, tamperedHeader, "utf8");
|
|
3631
|
+
}
|
|
3632
|
+
// Make ctx.sessionManager report a stale session file path so base-path restoration performs an explicit switch attempt after finalization verification fails.
|
|
3633
|
+
ctx.sessionManager.getSessionFile = () => path.join(projectBase, "stale-execution-session.jsonl");
|
|
3634
|
+
ctx.switchSession = async (_sessionPath: string) => ({ cancelled: false });
|
|
3635
|
+
|
|
3636
|
+
await pi.emit("agent_end", {
|
|
3637
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
3638
|
+
}, ctx);
|
|
3639
|
+
|
|
3640
|
+
assert.equal(process.cwd(), projectBase);
|
|
3641
|
+
assert.equal(ctx.cwd, projectBase);
|
|
3642
|
+
assert.equal(fs.existsSync(executionBasePath), true);
|
|
3643
|
+
assert.equal(fs.existsSync(path.join(projectBase, "src", "restore-failed.ts")), false);
|
|
3644
|
+
assert.ok(ctx.__state.notifications.some((entry) => /Prompt execution finalization expected session/.test(entry.message)));
|
|
3645
|
+
} finally {
|
|
3646
|
+
process.chdir(previousCwd);
|
|
3647
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3648
|
+
}
|
|
3649
|
+
});
|
|
3650
|
+
|
|
3651
|
+
/**
|
|
3652
|
+
* @brief Verifies successful closure merges from `base-path` even when end-of-session timing already moved the persisted replacement context off the worktree session.
|
|
3653
|
+
* @details Simulates real pi session-end timing such as automatic compaction or other CLI housekeeping that can advance the live replacement-session context back to the original base session before the `agent_end` lifecycle hook executes. The persisted command-capable context is forced to report the original base session and to reject any attempt to reattach the execution session. The test proves prompt-end closure must restore `base-path` and merge directly from `base-path` using persisted execution artifacts instead of failing on a best-effort reactivation of the worktree session.
|
|
3654
|
+
* @return {Promise<void>} Promise resolved after closure assertions complete.
|
|
3655
|
+
* @throws {AssertionError} Throws when closure attempts an execution-session reattach or skips the merge plus cleanup.
|
|
3656
|
+
* @satisfies REQ-208, REQ-209, REQ-282, TST-094
|
|
3657
|
+
*/
|
|
3658
|
+
test("worktree-backed closure merges from base-path when end-of-session timing already moved the persisted context", async () => {
|
|
3659
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3660
|
+
const previousCwd = process.cwd();
|
|
3661
|
+
try {
|
|
3662
|
+
writeProjectConfigOverrides(projectBase, {
|
|
3663
|
+
DEBUG_ENABLED: "enable",
|
|
3664
|
+
DEBUG_LOG_FILE: "prompt-debug.json",
|
|
3665
|
+
DEBUG_STATUS_CHANGES: "enable",
|
|
3666
|
+
DEBUG_WORKFLOW_EVENTS: "enable",
|
|
3667
|
+
DEBUG_LOG_ON_STATUS: "any",
|
|
3668
|
+
DEBUG_ENABLED_PROMPTS: ["req-change"],
|
|
3669
|
+
});
|
|
3670
|
+
const pi = createFakePi();
|
|
3671
|
+
piUsereqExtension(pi);
|
|
3672
|
+
const commandCtx = createFakeCtx(projectBase);
|
|
3673
|
+
await pi.emit("session_start", { reason: "startup" }, commandCtx);
|
|
3674
|
+
|
|
3675
|
+
const originalSessionFile = commandCtx.sessionManager.getSessionFile() ?? "";
|
|
3676
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", commandCtx);
|
|
3677
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
3678
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3679
|
+
assert.ok(worktreeMatch, promptText);
|
|
3680
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
3681
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3682
|
+
const executionSessionFile = commandCtx.sessionManager.getSessionFile() ?? "";
|
|
3683
|
+
assert.notEqual(executionSessionFile, "");
|
|
3684
|
+
|
|
3685
|
+
await pi.emit("before_agent_start", {}, commandCtx);
|
|
3686
|
+
await pi.emit("agent_start", {}, commandCtx);
|
|
3687
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
3688
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "base-merge.ts"), "export const BASE_MERGE = 1;\n", "utf8");
|
|
3689
|
+
assert.equal(spawnSync("git", ["add", "src/base-merge.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
3690
|
+
const worktreeCommit = spawnSync("git", ["commit", "-m", "base merge"], { cwd: executionBasePath, encoding: "utf8" });
|
|
3691
|
+
assert.equal(worktreeCommit.status, 0, worktreeCommit.stderr);
|
|
3692
|
+
|
|
3693
|
+
const persistedContext = readPersistedPromptCommandSessionContext(executionSessionFile) as any;
|
|
3694
|
+
assert.ok(persistedContext);
|
|
3695
|
+
persistedContext.cwd = projectBase;
|
|
3696
|
+
persistedContext.sessionManager.getSessionFile = () => originalSessionFile;
|
|
3697
|
+
persistedContext.sessionManager.getSessionDir = () => path.dirname(originalSessionFile);
|
|
3698
|
+
persistedContext.sessionManager.getCwd = () => projectBase;
|
|
3699
|
+
persistedContext.switchSession = async (sessionPath: string) => {
|
|
3700
|
+
if (path.resolve(sessionPath) !== path.resolve(originalSessionFile)) {
|
|
3701
|
+
throw new Error(`Unexpected execution-session reattach: ${sessionPath}`);
|
|
3702
|
+
}
|
|
3703
|
+
process.chdir(projectBase);
|
|
3704
|
+
return { cancelled: false };
|
|
3705
|
+
};
|
|
1538
3706
|
|
|
1539
|
-
|
|
3707
|
+
const eventCtx = createFakeCtx(projectBase);
|
|
3708
|
+
eventCtx.sessionManager.getSessionFile = () => originalSessionFile;
|
|
3709
|
+
eventCtx.sessionManager.getSessionDir = () => path.dirname(originalSessionFile);
|
|
3710
|
+
eventCtx.sessionManager.getCwd = () => projectBase;
|
|
3711
|
+
eventCtx.cwd = projectBase;
|
|
3712
|
+
delete eventCtx.switchSession;
|
|
3713
|
+
process.chdir(projectBase);
|
|
1540
3714
|
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
3715
|
+
await pi.emit("agent_end", {
|
|
3716
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
3717
|
+
}, eventCtx);
|
|
3718
|
+
|
|
3719
|
+
const debugLog = JSON.parse(fs.readFileSync(path.join(projectBase, "prompt-debug.json"), "utf8"));
|
|
3720
|
+
assert.ok(debugLog.some((entry: any) => entry.action === "workflow_restore" && entry.result?.success === true));
|
|
3721
|
+
assert.ok(debugLog.some((entry: any) => entry.action === "merge" && entry.result?.success === true));
|
|
3722
|
+
assert.equal(process.cwd(), projectBase);
|
|
3723
|
+
assert.equal(eventCtx.cwd, projectBase);
|
|
3724
|
+
assert.ok(fs.existsSync(path.join(projectBase, "src", "base-merge.ts")));
|
|
3725
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
3726
|
+
assert.notEqual(
|
|
3727
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
3728
|
+
cwd: projectBase,
|
|
3729
|
+
encoding: "utf8",
|
|
3730
|
+
}).status,
|
|
3731
|
+
0,
|
|
3732
|
+
);
|
|
3733
|
+
assert.equal(eventCtx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
|
|
3734
|
+
} finally {
|
|
3735
|
+
process.chdir(previousCwd);
|
|
3736
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3737
|
+
}
|
|
1546
3738
|
});
|
|
1547
3739
|
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
3740
|
+
/**
|
|
3741
|
+
* @brief Verifies matched successful closure ignores stale session-switch errors after restored base-path verification.
|
|
3742
|
+
* @details Simulates the real pi runtime behavior observed in `/tmp/debug.json`, where switching the persisted command-capable replacement-session context back to the original session completes the base-path reattachment but still surfaces the stale-extension-context error from the invalidated execution-session closure. The test proves prompt-end closure MUST treat that stale switch error as ignorable after the restored base session verifies successfully, then continue the fast-forward merge plus worktree deletion.
|
|
3743
|
+
* @return {Promise<void>} Promise resolved after restored-base verification, merge, and cleanup assertions complete.
|
|
3744
|
+
* @throws {AssertionError} Throws when the stale restore-switch error prevents merge plus cleanup.
|
|
3745
|
+
* @satisfies REQ-208, REQ-276, REQ-280, REQ-282
|
|
3746
|
+
*/
|
|
3747
|
+
test("worktree-backed closure ignores stale restore-switch errors after base-path verification", async () => {
|
|
3748
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3749
|
+
const previousCwd = process.cwd();
|
|
3750
|
+
try {
|
|
3751
|
+
writeProjectConfigOverrides(projectBase, {
|
|
3752
|
+
DEBUG_ENABLED: "enable",
|
|
3753
|
+
DEBUG_LOG_FILE: "prompt-debug.json",
|
|
3754
|
+
DEBUG_STATUS_CHANGES: "enable",
|
|
3755
|
+
DEBUG_WORKFLOW_EVENTS: "enable",
|
|
3756
|
+
DEBUG_LOG_ON_STATUS: "any",
|
|
3757
|
+
DEBUG_ENABLED_PROMPTS: ["req-change"],
|
|
3758
|
+
});
|
|
3759
|
+
const pi = createFakePi();
|
|
3760
|
+
piUsereqExtension(pi);
|
|
3761
|
+
const commandCtx = createFakeCtx(projectBase);
|
|
3762
|
+
await pi.emit("session_start", { reason: "startup" }, commandCtx);
|
|
3763
|
+
|
|
3764
|
+
const originalSessionFile = commandCtx.sessionManager.getSessionFile() ?? "";
|
|
3765
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", commandCtx);
|
|
3766
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
3767
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3768
|
+
assert.ok(worktreeMatch, promptText);
|
|
3769
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
3770
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3771
|
+
const executionSessionFile = commandCtx.sessionManager.getSessionFile() ?? "";
|
|
3772
|
+
assert.notEqual(executionSessionFile, "");
|
|
3773
|
+
|
|
3774
|
+
await pi.emit("before_agent_start", {}, commandCtx);
|
|
3775
|
+
await pi.emit("agent_start", {}, commandCtx);
|
|
3776
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
3777
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "stale-restore.ts"), "export const STALE_RESTORE = 1;\n", "utf8");
|
|
3778
|
+
assert.equal(spawnSync("git", ["add", "src/stale-restore.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
3779
|
+
const worktreeCommit = spawnSync("git", ["commit", "-m", "stale restore"], { cwd: executionBasePath, encoding: "utf8" });
|
|
3780
|
+
assert.equal(worktreeCommit.status, 0, worktreeCommit.stderr);
|
|
3781
|
+
|
|
3782
|
+
const persistedContext = readPersistedPromptCommandSessionContext(executionSessionFile) as any;
|
|
3783
|
+
assert.ok(persistedContext);
|
|
3784
|
+
persistedContext.cwd = projectBase;
|
|
3785
|
+
persistedContext.sessionManager.getSessionFile = () => originalSessionFile;
|
|
3786
|
+
persistedContext.sessionManager.getSessionDir = () => path.dirname(originalSessionFile);
|
|
3787
|
+
persistedContext.sessionManager.getCwd = () => projectBase;
|
|
3788
|
+
persistedContext.switchSession = async (sessionPath: string) => {
|
|
3789
|
+
assert.equal(path.resolve(sessionPath), path.resolve(originalSessionFile));
|
|
3790
|
+
process.chdir(projectBase);
|
|
3791
|
+
persistedContext.cwd = projectBase;
|
|
3792
|
+
throw new Error("This extension ctx is stale after session replacement or reload.");
|
|
3793
|
+
};
|
|
1566
3794
|
|
|
1567
|
-
|
|
3795
|
+
const eventCtx = createFakeCtx(projectBase);
|
|
3796
|
+
eventCtx.sessionManager.getSessionFile = () => originalSessionFile;
|
|
3797
|
+
eventCtx.sessionManager.getSessionDir = () => path.dirname(originalSessionFile);
|
|
3798
|
+
eventCtx.sessionManager.getCwd = () => projectBase;
|
|
3799
|
+
eventCtx.cwd = projectBase;
|
|
3800
|
+
delete eventCtx.switchSession;
|
|
3801
|
+
process.chdir(projectBase);
|
|
1568
3802
|
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
3803
|
+
await pi.emit("agent_end", {
|
|
3804
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
3805
|
+
}, eventCtx);
|
|
3806
|
+
|
|
3807
|
+
const debugLog = JSON.parse(fs.readFileSync(path.join(projectBase, "prompt-debug.json"), "utf8"));
|
|
3808
|
+
assert.ok(debugLog.some((entry: any) => entry.action === "workflow_restore" && entry.result?.success === true));
|
|
3809
|
+
assert.ok(debugLog.some((entry: any) => entry.action === "merge" && entry.result?.success === true));
|
|
3810
|
+
assert.equal(process.cwd(), projectBase);
|
|
3811
|
+
assert.equal(eventCtx.cwd, projectBase);
|
|
3812
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
3813
|
+
assert.equal(fs.existsSync(path.join(projectBase, "src", "stale-restore.ts")), true);
|
|
3814
|
+
assert.notEqual(
|
|
3815
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
3816
|
+
cwd: projectBase,
|
|
3817
|
+
encoding: "utf8",
|
|
3818
|
+
}).status,
|
|
3819
|
+
0,
|
|
3820
|
+
);
|
|
3821
|
+
assert.equal(eventCtx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
|
|
3822
|
+
} finally {
|
|
3823
|
+
process.chdir(previousCwd);
|
|
3824
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3825
|
+
}
|
|
1574
3826
|
});
|
|
1575
3827
|
|
|
1576
|
-
test("
|
|
1577
|
-
const
|
|
1578
|
-
|
|
1579
|
-
const
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
},
|
|
1598
|
-
});
|
|
3828
|
+
test("worktree-backed prompt commands abort before prompt dispatch when post-create verification fails", async () => {
|
|
3829
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3830
|
+
const previousCwd = process.cwd();
|
|
3831
|
+
const originalExistsSync = fs.existsSync;
|
|
3832
|
+
let failedWorktreeName = "";
|
|
3833
|
+
let failedWorktreePath = "";
|
|
3834
|
+
try {
|
|
3835
|
+
const pi = createFakePi();
|
|
3836
|
+
piUsereqExtension(pi);
|
|
3837
|
+
const ctx = createFakeCtx(projectBase);
|
|
3838
|
+
setPromptCommandPostCreateHookForTests(({ worktreeDir, worktreeRootPath, worktreePath }) => {
|
|
3839
|
+
failedWorktreeName = worktreeDir;
|
|
3840
|
+
failedWorktreePath = worktreePath;
|
|
3841
|
+
fs.existsSync = ((candidatePath: Parameters<typeof fs.existsSync>[0]) => {
|
|
3842
|
+
const normalizedPath = typeof candidatePath === "string" ? candidatePath : String(candidatePath);
|
|
3843
|
+
if (normalizedPath === worktreePath || normalizedPath === worktreeRootPath) {
|
|
3844
|
+
return false;
|
|
3845
|
+
}
|
|
3846
|
+
return originalExistsSync(candidatePath);
|
|
3847
|
+
}) as typeof fs.existsSync;
|
|
3848
|
+
});
|
|
1599
3849
|
|
|
1600
|
-
|
|
3850
|
+
await assert.rejects(
|
|
3851
|
+
pi.commands.get("req-change")!.handler("Adjust docs", ctx),
|
|
3852
|
+
/ERROR: Worktree verification failed/,
|
|
3853
|
+
);
|
|
1601
3854
|
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
3855
|
+
assert.equal(pi.sentUserMessages.length, 0);
|
|
3856
|
+
fs.existsSync = originalExistsSync;
|
|
3857
|
+
assert.equal(fs.existsSync(failedWorktreePath), false);
|
|
3858
|
+
assert.notEqual(failedWorktreeName, "");
|
|
3859
|
+
assert.notEqual(
|
|
3860
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${failedWorktreeName}`], {
|
|
3861
|
+
cwd: projectBase,
|
|
3862
|
+
encoding: "utf8",
|
|
3863
|
+
}).status,
|
|
3864
|
+
0,
|
|
3865
|
+
);
|
|
3866
|
+
assert.ok(ctx.__state.notifications.some((entry) => /Worktree verification failed/.test(entry.message)));
|
|
3867
|
+
} finally {
|
|
3868
|
+
fs.existsSync = originalExistsSync;
|
|
3869
|
+
setPromptCommandPostCreateHookForTests(undefined);
|
|
3870
|
+
process.chdir(previousCwd);
|
|
3871
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3872
|
+
}
|
|
1609
3873
|
});
|
|
1610
3874
|
|
|
1611
|
-
test("prompt commands
|
|
1612
|
-
const
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
3875
|
+
test("prompt commands remain functional when custom tool registrations are removed from the runtime inventory", async () => {
|
|
3876
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3877
|
+
try {
|
|
3878
|
+
writeProjectConfigOverrides(projectBase, { GIT_WORKTREE_ENABLED: "disable" });
|
|
3879
|
+
const pi = createFakePi();
|
|
3880
|
+
piUsereqExtension(pi);
|
|
3881
|
+
pi.tools.splice(0, pi.tools.length);
|
|
3882
|
+
const ctx = createFakeCtx(projectBase);
|
|
1618
3883
|
|
|
1619
|
-
|
|
3884
|
+
await pi.commands.get("req-analyze")!.handler("Inspect src/index.ts", ctx);
|
|
1620
3885
|
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
3886
|
+
assert.equal(pi.sentUserMessages.length, 1);
|
|
3887
|
+
assert.match(String(pi.sentUserMessages[0]?.content ?? ""), /Inspect src\/index\.ts/);
|
|
3888
|
+
} finally {
|
|
3889
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3890
|
+
}
|
|
1625
3891
|
});
|
|
1626
3892
|
|
|
1627
3893
|
test("session_start applies configured pi-usereq startup tools", async () => {
|
|
@@ -1634,7 +3900,7 @@ test("session_start applies configured pi-usereq startup tools", async () => {
|
|
|
1634
3900
|
"tests-dir": "tests",
|
|
1635
3901
|
"src-dir": ["src", "foobar"],
|
|
1636
3902
|
"static-check": {},
|
|
1637
|
-
"enabled-tools": ["
|
|
3903
|
+
"enabled-tools": ["files-tokens", "static-check"],
|
|
1638
3904
|
}, null, 2)}\n`,
|
|
1639
3905
|
"utf8",
|
|
1640
3906
|
);
|
|
@@ -1645,7 +3911,7 @@ test("session_start applies configured pi-usereq startup tools", async () => {
|
|
|
1645
3911
|
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
1646
3912
|
|
|
1647
3913
|
const activeTools = new Set(pi.getActiveTools());
|
|
1648
|
-
assert.deepEqual([...activeTools].sort(), ["
|
|
3914
|
+
assert.deepEqual([...activeTools].sort(), ["files-tokens", "static-check"]);
|
|
1649
3915
|
assert.equal(
|
|
1650
3916
|
ctx.__state.statuses.get("pi-usereq"),
|
|
1651
3917
|
buildExpectedFakeStatusText({
|
|
@@ -1684,6 +3950,33 @@ test("session_start renders the 0-percent context icon when context usage is emp
|
|
|
1684
3950
|
);
|
|
1685
3951
|
});
|
|
1686
3952
|
|
|
3953
|
+
/**
|
|
3954
|
+
* @brief Verifies stale replacement contexts do not break workflow-state rendering.
|
|
3955
|
+
* @details Simulates the guarded pi runtime error thrown when an extension context is accessed after session replacement or reload. The test proves workflow-state updates must preserve internal state while suppressing stale-context render failures instead of surfacing extension errors.
|
|
3956
|
+
* @return {void} No return value.
|
|
3957
|
+
* @throws {AssertionError} Throws when stale-context rendering escapes or leaves the stale context cached for later renders.
|
|
3958
|
+
* @satisfies REQ-280, TST-092
|
|
3959
|
+
*/
|
|
3960
|
+
test("stale replacement contexts do not break workflow-state rendering", () => {
|
|
3961
|
+
const cwd = createTempDir("pi-usereq-stale-render-");
|
|
3962
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
3963
|
+
const controller = createPiUsereqStatusController();
|
|
3964
|
+
setPiUsereqStatusConfig(controller, getDefaultConfig());
|
|
3965
|
+
const staleCtx = createFakeCtx(cwd);
|
|
3966
|
+
Object.defineProperty(staleCtx, "ui", {
|
|
3967
|
+
configurable: true,
|
|
3968
|
+
get() {
|
|
3969
|
+
throw new Error("This extension instance is stale after session replacement or reload. Use the provided replacement-session context instead.");
|
|
3970
|
+
},
|
|
3971
|
+
});
|
|
3972
|
+
|
|
3973
|
+
assert.doesNotThrow(() => {
|
|
3974
|
+
setPiUsereqWorkflowState(controller, "running", staleCtx);
|
|
3975
|
+
});
|
|
3976
|
+
assert.equal(controller.state.workflowState, "running");
|
|
3977
|
+
assert.equal(controller.latestContext, undefined);
|
|
3978
|
+
});
|
|
3979
|
+
|
|
1687
3980
|
test("extension registers wrappers for all pi-usereq status hooks", () => {
|
|
1688
3981
|
const pi = createFakePi();
|
|
1689
3982
|
piUsereqExtension(pi);
|
|
@@ -1924,6 +4217,71 @@ test("agent timing status updates et while preserving accumulated completed runt
|
|
|
1924
4217
|
}
|
|
1925
4218
|
});
|
|
1926
4219
|
|
|
4220
|
+
test("elapsed counters persist across new-session rebinding and reset on reload", async () => {
|
|
4221
|
+
const cwd = createTempDir("pi-usereq-timing-rebind-");
|
|
4222
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
4223
|
+
const originalDateNow = Date.now;
|
|
4224
|
+
let nowMs = 0;
|
|
4225
|
+
Date.now = () => nowMs;
|
|
4226
|
+
|
|
4227
|
+
try {
|
|
4228
|
+
const firstPi = createFakePi();
|
|
4229
|
+
piUsereqExtension(firstPi);
|
|
4230
|
+
const firstCtx = createFakeCtx(cwd);
|
|
4231
|
+
await firstPi.emit("session_start", { reason: "startup" }, firstCtx);
|
|
4232
|
+
await firstPi.emit("agent_start", {}, firstCtx);
|
|
4233
|
+
nowMs = 61_000;
|
|
4234
|
+
await firstPi.emit("agent_end", {
|
|
4235
|
+
messages: [
|
|
4236
|
+
{
|
|
4237
|
+
role: "assistant",
|
|
4238
|
+
stopReason: "stop",
|
|
4239
|
+
content: [],
|
|
4240
|
+
},
|
|
4241
|
+
],
|
|
4242
|
+
}, firstCtx);
|
|
4243
|
+
await firstPi.emit("session_shutdown", {}, firstCtx);
|
|
4244
|
+
|
|
4245
|
+
const newPi = createFakePi();
|
|
4246
|
+
piUsereqExtension(newPi);
|
|
4247
|
+
const newCtx = createFakeCtx(cwd);
|
|
4248
|
+
await newPi.emit("session_start", {
|
|
4249
|
+
reason: "new",
|
|
4250
|
+
previousSessionFile: "/tmp/previous-session.jsonl",
|
|
4251
|
+
}, newCtx);
|
|
4252
|
+
assert.equal(
|
|
4253
|
+
newCtx.__state.statuses.get("pi-usereq"),
|
|
4254
|
+
buildExpectedFakeStatusText({
|
|
4255
|
+
basePath: buildExpectedFakeBasePath(cwd),
|
|
4256
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
4257
|
+
testsDir: "tests",
|
|
4258
|
+
srcDir: ["src"],
|
|
4259
|
+
contextFilledCells: 0,
|
|
4260
|
+
et: "⏱︎ --:-- ⚑ 1:01 ⌛︎1:01",
|
|
4261
|
+
}),
|
|
4262
|
+
);
|
|
4263
|
+
await newPi.emit("session_shutdown", {}, newCtx);
|
|
4264
|
+
|
|
4265
|
+
const reloadPi = createFakePi();
|
|
4266
|
+
piUsereqExtension(reloadPi);
|
|
4267
|
+
const reloadCtx = createFakeCtx(cwd);
|
|
4268
|
+
await reloadPi.emit("session_start", { reason: "reload" }, reloadCtx);
|
|
4269
|
+
assert.equal(
|
|
4270
|
+
reloadCtx.__state.statuses.get("pi-usereq"),
|
|
4271
|
+
buildExpectedFakeStatusText({
|
|
4272
|
+
basePath: buildExpectedFakeBasePath(cwd),
|
|
4273
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
4274
|
+
testsDir: "tests",
|
|
4275
|
+
srcDir: ["src"],
|
|
4276
|
+
contextFilledCells: 0,
|
|
4277
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
4278
|
+
}),
|
|
4279
|
+
);
|
|
4280
|
+
} finally {
|
|
4281
|
+
Date.now = originalDateNow;
|
|
4282
|
+
}
|
|
4283
|
+
});
|
|
4284
|
+
|
|
1927
4285
|
test("session_start enables default custom tools and default embedded tools only", async () => {
|
|
1928
4286
|
const cwd = createTempDir("pi-usereq-default-tools-");
|
|
1929
4287
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
@@ -1936,14 +4294,67 @@ test("session_start enables default custom tools and default embedded tools only
|
|
|
1936
4294
|
for (const toolName of PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES) {
|
|
1937
4295
|
assert.ok(activeTools.has(toolName), `missing default active tool ${toolName}`);
|
|
1938
4296
|
}
|
|
4297
|
+
assert.equal(activeTools.has("search"), true);
|
|
4298
|
+
assert.equal(activeTools.has("files-search"), true);
|
|
1939
4299
|
assert.equal(activeTools.has("find"), false);
|
|
1940
4300
|
assert.equal(activeTools.has("grep"), false);
|
|
1941
4301
|
assert.equal(activeTools.has("ls"), false);
|
|
1942
4302
|
});
|
|
1943
4303
|
|
|
1944
|
-
test("default configuration applies the documented notify, sound, and pushover defaults", () => {
|
|
4304
|
+
test("default configuration applies the documented static-check, debug, notify, sound, and pushover defaults", () => {
|
|
1945
4305
|
const config = getDefaultConfig(createTempDir("pi-usereq-default-notify-"));
|
|
1946
4306
|
|
|
4307
|
+
assert.equal(config.AUTO_GIT_COMMIT, "enable");
|
|
4308
|
+
assert.deepEqual(Object.keys(config["static-check"]).length, 20);
|
|
4309
|
+
assert.deepEqual(config["static-check"].C, createStaticCheckLanguageConfig([
|
|
4310
|
+
{
|
|
4311
|
+
module: "Command",
|
|
4312
|
+
cmd: "cppcheck",
|
|
4313
|
+
params: [
|
|
4314
|
+
"--error-exitcode=1",
|
|
4315
|
+
"--enable=warning,style,performance,portability",
|
|
4316
|
+
"--std=c11",
|
|
4317
|
+
],
|
|
4318
|
+
},
|
|
4319
|
+
{
|
|
4320
|
+
module: "Command",
|
|
4321
|
+
cmd: "clang-format",
|
|
4322
|
+
params: ["--dry-run", "--Werror"],
|
|
4323
|
+
},
|
|
4324
|
+
], "enable"));
|
|
4325
|
+
assert.deepEqual(config["static-check"]["C++"], createStaticCheckLanguageConfig([
|
|
4326
|
+
{
|
|
4327
|
+
module: "Command",
|
|
4328
|
+
cmd: "cppcheck",
|
|
4329
|
+
params: [
|
|
4330
|
+
"--error-exitcode=1",
|
|
4331
|
+
"--enable=warning,style,performance,portability",
|
|
4332
|
+
"--std=c++20",
|
|
4333
|
+
],
|
|
4334
|
+
},
|
|
4335
|
+
{
|
|
4336
|
+
module: "Command",
|
|
4337
|
+
cmd: "clang-format",
|
|
4338
|
+
params: ["--dry-run", "--Werror"],
|
|
4339
|
+
},
|
|
4340
|
+
], "enable"));
|
|
4341
|
+
assert.deepEqual(config["static-check"].Python, createStaticCheckLanguageConfig([
|
|
4342
|
+
{ module: "Command", cmd: "pyright", params: ["--outputjson"] },
|
|
4343
|
+
{ module: "Command", cmd: "ruff", params: ["check"] },
|
|
4344
|
+
], "enable"));
|
|
4345
|
+
assert.deepEqual(config["static-check"].JavaScript, createStaticCheckLanguageConfig([
|
|
4346
|
+
{ module: "Command", cmd: "node", params: ["--check"] },
|
|
4347
|
+
], "enable"));
|
|
4348
|
+
assert.deepEqual(config["static-check"].TypeScript, createStaticCheckLanguageConfig([
|
|
4349
|
+
{ module: "Command", cmd: "npx", params: ["eslint"] },
|
|
4350
|
+
], "enable"));
|
|
4351
|
+
assert.deepEqual(config["static-check"].Ruby, createStaticCheckLanguageConfig([], "disable"));
|
|
4352
|
+
assert.equal(config.DEBUG_ENABLED, "disable");
|
|
4353
|
+
assert.equal(config.DEBUG_LOG_FILE, DEFAULT_DEBUG_LOG_FILE);
|
|
4354
|
+
assert.equal(config.DEBUG_WORKFLOW_EVENTS, DEFAULT_DEBUG_WORKFLOW_EVENTS);
|
|
4355
|
+
assert.equal(config.DEBUG_LOG_ON_STATUS, DEFAULT_DEBUG_LOG_ON_STATUS);
|
|
4356
|
+
assert.deepEqual(config.DEBUG_ENABLED_TOOLS, []);
|
|
4357
|
+
assert.deepEqual(config.DEBUG_ENABLED_PROMPTS, []);
|
|
1947
4358
|
assert.equal(config["notify-enabled"], false);
|
|
1948
4359
|
assert.equal(config["notify-on-completed"], true);
|
|
1949
4360
|
assert.equal(config["notify-on-interrupted"], false);
|
|
@@ -1960,22 +4371,40 @@ test("default configuration applies the documented notify, sound, and pushover d
|
|
|
1960
4371
|
assert.equal(config["notify-pushover-text"], "%%RESULT%%\n%%ARGS%%");
|
|
1961
4372
|
});
|
|
1962
4373
|
|
|
1963
|
-
test("configuration menu can enable embedded builtin tools", async () => {
|
|
4374
|
+
test("configuration menu orders tool toggles and can enable embedded builtin tools", async () => {
|
|
1964
4375
|
const cwd = createTempDir("pi-usereq-menu-embedded-");
|
|
1965
4376
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1966
4377
|
const pi = createFakePi();
|
|
1967
4378
|
piUsereqExtension(pi);
|
|
1968
4379
|
const command = pi.commands.get("pi-usereq");
|
|
1969
4380
|
assert.ok(command);
|
|
4381
|
+
const ctx = createFakeCtx(cwd, {
|
|
4382
|
+
selects: ["Enable tools", "Enable tools", "grep", "Save and close", "Save and close"],
|
|
4383
|
+
});
|
|
1970
4384
|
|
|
1971
|
-
await command!.handler(
|
|
1972
|
-
"",
|
|
1973
|
-
createFakeCtx(cwd, {
|
|
1974
|
-
selects: ["Enable tools", "Enable tools", "grep", "Save and close", "Save and close"],
|
|
1975
|
-
}),
|
|
1976
|
-
);
|
|
4385
|
+
await command!.handler("", ctx);
|
|
1977
4386
|
|
|
1978
4387
|
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
4388
|
+
assert.deepEqual(ctx.__state.selectCalls[2]?.items ?? [], [
|
|
4389
|
+
"compress",
|
|
4390
|
+
"references",
|
|
4391
|
+
"search",
|
|
4392
|
+
"static-check",
|
|
4393
|
+
"tokens",
|
|
4394
|
+
"files-compress",
|
|
4395
|
+
"files-references",
|
|
4396
|
+
"files-search",
|
|
4397
|
+
"files-static-check",
|
|
4398
|
+
"files-tokens",
|
|
4399
|
+
"bash",
|
|
4400
|
+
"edit",
|
|
4401
|
+
"read",
|
|
4402
|
+
"write",
|
|
4403
|
+
"find",
|
|
4404
|
+
"grep",
|
|
4405
|
+
"ls",
|
|
4406
|
+
"Reset defaults",
|
|
4407
|
+
]);
|
|
1979
4408
|
assert.ok(config["enabled-tools"].includes("grep"));
|
|
1980
4409
|
assert.ok(pi.getActiveTools().includes("grep"));
|
|
1981
4410
|
});
|
|
@@ -2076,6 +4505,8 @@ test("configuration menu can persist pushover settings", async () => {
|
|
|
2076
4505
|
const ctx = createFakeCtx(cwd, {
|
|
2077
4506
|
selects: [
|
|
2078
4507
|
"notifications",
|
|
4508
|
+
"Pushover User Key/Delivery Group Key",
|
|
4509
|
+
"Pushover Token/API Token Key",
|
|
2079
4510
|
"Enable pushover",
|
|
2080
4511
|
"Pushover events",
|
|
2081
4512
|
"Prompt failed",
|
|
@@ -2084,16 +4515,14 @@ test("configuration menu can persist pushover settings", async () => {
|
|
|
2084
4515
|
"High",
|
|
2085
4516
|
"Pushover title",
|
|
2086
4517
|
"Pushover text",
|
|
2087
|
-
"Pushover User Key/Delivery Group Key",
|
|
2088
|
-
"Pushover Token/API Token Key",
|
|
2089
4518
|
"Save and close",
|
|
2090
4519
|
"Save and close",
|
|
2091
4520
|
],
|
|
2092
4521
|
inputs: [
|
|
2093
|
-
"%%PROMT%% @ %%BASE%% [%%TIME%%]",
|
|
2094
|
-
"%%RESULT%%\n%%ARGS%%",
|
|
2095
4522
|
"gzfjjvp1xxmhibqwzh9m7i1zwvf83j",
|
|
2096
4523
|
"ah6bf5u2sj63mcvou6qamiabeoubbe",
|
|
4524
|
+
"%%PROMT%% @ %%BASE%% [%%TIME%%]",
|
|
4525
|
+
"%%RESULT%%\\n%%ARGS%%",
|
|
2097
4526
|
],
|
|
2098
4527
|
});
|
|
2099
4528
|
|
|
@@ -2123,16 +4552,40 @@ test("configuration menu can persist pushover settings", async () => {
|
|
|
2123
4552
|
);
|
|
2124
4553
|
});
|
|
2125
4554
|
|
|
2126
|
-
test("
|
|
2127
|
-
const cwd = createTempDir("pi-usereq-pushover-
|
|
4555
|
+
test("notifications menu escapes pushover text and locks pushover enablement until credentials exist", async () => {
|
|
4556
|
+
const cwd = createTempDir("pi-usereq-menu-pushover-display-");
|
|
2128
4557
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
4558
|
+
fs.writeFileSync(
|
|
4559
|
+
getProjectConfigPath(cwd),
|
|
4560
|
+
`${JSON.stringify({
|
|
2132
4561
|
...getDefaultConfig(cwd),
|
|
4562
|
+
"notify-pushover-text": "first line\nsecond line",
|
|
4563
|
+
}, null, 2)}\n`,
|
|
4564
|
+
"utf8",
|
|
4565
|
+
);
|
|
4566
|
+
const pi = createFakePi();
|
|
4567
|
+
piUsereqExtension(pi);
|
|
4568
|
+
const command = pi.commands.get("pi-usereq");
|
|
4569
|
+
assert.ok(command);
|
|
4570
|
+
const ctx = createFakeCtx(cwd, {
|
|
4571
|
+
selects: ["notifications", "Pushover text", "Save and close", "Save and close"],
|
|
4572
|
+
});
|
|
4573
|
+
|
|
4574
|
+
await command!.handler("", ctx);
|
|
4575
|
+
|
|
4576
|
+
const renderedNotificationsMenu = (ctx.__state.customRenderLines[2] ?? []).join("\n");
|
|
4577
|
+
assert.match(renderedNotificationsMenu, /first line\\nsecond line/);
|
|
4578
|
+
assert.match(renderedNotificationsMenu, /<dim>Enable pushover/);
|
|
4579
|
+
assert.match(renderedNotificationsMenu, /<dim>off<\/dim>/);
|
|
4580
|
+
});
|
|
4581
|
+
|
|
4582
|
+
test("prompt-end pushover requests honor global enable, event toggles, credentials, priority, and templates", async () => {
|
|
4583
|
+
const { projectBase: cwd } = initFixtureRepo({ fixtures: [] });
|
|
4584
|
+
const writeConfig = (overrides: Record<string, unknown>) => {
|
|
4585
|
+
writeProjectConfigOverrides(cwd, {
|
|
4586
|
+
GIT_WORKTREE_ENABLED: "disable",
|
|
2133
4587
|
...overrides,
|
|
2134
|
-
};
|
|
2135
|
-
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
4588
|
+
});
|
|
2136
4589
|
};
|
|
2137
4590
|
const pi = createFakePi();
|
|
2138
4591
|
piUsereqExtension(pi);
|
|
@@ -2308,12 +4761,12 @@ test("prompt-end pushover requests honor global enable, event toggles, credentia
|
|
|
2308
4761
|
} finally {
|
|
2309
4762
|
setPiNotifyHttpsRequestForTests(undefined);
|
|
2310
4763
|
Date.now = originalDateNow;
|
|
4764
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
2311
4765
|
}
|
|
2312
4766
|
});
|
|
2313
4767
|
|
|
2314
4768
|
test("pushover global enable controls status and suppresses delivery when disabled", async () => {
|
|
2315
|
-
const cwd =
|
|
2316
|
-
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
4769
|
+
const { projectBase: cwd } = initFixtureRepo({ fixtures: [] });
|
|
2317
4770
|
const pi = createFakePi();
|
|
2318
4771
|
piUsereqExtension(pi);
|
|
2319
4772
|
const command = pi.commands.get("pi-usereq");
|
|
@@ -2333,6 +4786,7 @@ test("pushover global enable controls status and suppresses delivery when disabl
|
|
|
2333
4786
|
});
|
|
2334
4787
|
|
|
2335
4788
|
await command!.handler("", ctx);
|
|
4789
|
+
writeProjectConfigOverrides(cwd, { GIT_WORKTREE_ENABLED: "disable" });
|
|
2336
4790
|
|
|
2337
4791
|
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
2338
4792
|
assert.equal(config["notify-pushover-enabled"], false);
|
|
@@ -2390,19 +4844,17 @@ test("pushover global enable controls status and suppresses delivery when disabl
|
|
|
2390
4844
|
assert.deepEqual(recordedRequests, []);
|
|
2391
4845
|
} finally {
|
|
2392
4846
|
setPiNotifyHttpsRequestForTests(undefined);
|
|
4847
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
2393
4848
|
}
|
|
2394
4849
|
});
|
|
2395
4850
|
|
|
2396
4851
|
test("command notify routes through PI_NOTIFY_CMD placeholders and per-event toggles", async () => {
|
|
2397
|
-
const cwd =
|
|
2398
|
-
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
2399
|
-
const configPath = getProjectConfigPath(cwd);
|
|
4852
|
+
const { projectBase: cwd } = initFixtureRepo({ fixtures: [] });
|
|
2400
4853
|
const writeConfig = (overrides: Record<string, unknown>) => {
|
|
2401
|
-
|
|
2402
|
-
|
|
4854
|
+
writeProjectConfigOverrides(cwd, {
|
|
4855
|
+
GIT_WORKTREE_ENABLED: "disable",
|
|
2403
4856
|
...overrides,
|
|
2404
|
-
};
|
|
2405
|
-
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
4857
|
+
});
|
|
2406
4858
|
};
|
|
2407
4859
|
const pi = createFakePi();
|
|
2408
4860
|
piUsereqExtension(pi);
|
|
@@ -2502,19 +4954,17 @@ test("command notify routes through PI_NOTIFY_CMD placeholders and per-event tog
|
|
|
2502
4954
|
} finally {
|
|
2503
4955
|
setPiNotifySpawnForTests(undefined);
|
|
2504
4956
|
Date.now = originalDateNow;
|
|
4957
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
2505
4958
|
}
|
|
2506
4959
|
});
|
|
2507
4960
|
|
|
2508
4961
|
test("sound routing honors the selected sound state and per-event toggles", async () => {
|
|
2509
|
-
const cwd =
|
|
2510
|
-
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
2511
|
-
const configPath = getProjectConfigPath(cwd);
|
|
4962
|
+
const { projectBase: cwd } = initFixtureRepo({ fixtures: [] });
|
|
2512
4963
|
const writeConfig = (overrides: Record<string, unknown>) => {
|
|
2513
|
-
|
|
2514
|
-
|
|
4964
|
+
writeProjectConfigOverrides(cwd, {
|
|
4965
|
+
GIT_WORKTREE_ENABLED: "disable",
|
|
2515
4966
|
...overrides,
|
|
2516
|
-
};
|
|
2517
|
-
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
4967
|
+
});
|
|
2518
4968
|
};
|
|
2519
4969
|
const pi = createFakePi();
|
|
2520
4970
|
piUsereqExtension(pi);
|
|
@@ -2585,6 +5035,7 @@ test("sound routing honors the selected sound state and per-event toggles", asyn
|
|
|
2585
5035
|
assert.equal(recordedCommands.length, 1);
|
|
2586
5036
|
} finally {
|
|
2587
5037
|
setPiNotifySpawnForTests(undefined);
|
|
5038
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
2588
5039
|
}
|
|
2589
5040
|
});
|
|
2590
5041
|
|
|
@@ -2624,28 +5075,6 @@ test("sound toggle shortcut cycles persisted pi-notify sound levels", async () =
|
|
|
2624
5075
|
}
|
|
2625
5076
|
});
|
|
2626
5077
|
|
|
2627
|
-
test("git-path tool derives the repository root at runtime", async () => {
|
|
2628
|
-
const { projectBase } = initFixtureRepo();
|
|
2629
|
-
try {
|
|
2630
|
-
const configPath = getProjectConfigPath(projectBase);
|
|
2631
|
-
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
2632
|
-
config["git-path"] = "/tmp/wrong-git-root";
|
|
2633
|
-
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
2634
|
-
|
|
2635
|
-
const pi = createFakePi();
|
|
2636
|
-
piUsereqExtension(pi);
|
|
2637
|
-
const result = await executeRegisteredTool(pi, "git-path", projectBase) as {
|
|
2638
|
-
content?: Array<{ text?: string }>;
|
|
2639
|
-
details?: { result: { path_value: string } };
|
|
2640
|
-
};
|
|
2641
|
-
|
|
2642
|
-
assert.deepEqual(JSON.parse(result.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(result.details)));
|
|
2643
|
-
assert.equal(result.details?.result.path_value, projectBase);
|
|
2644
|
-
} finally {
|
|
2645
|
-
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2646
|
-
}
|
|
2647
|
-
});
|
|
2648
|
-
|
|
2649
5078
|
test("static-check exposes the supported programming languages", () => {
|
|
2650
5079
|
const supported = getSupportedStaticCheckLanguageSupport();
|
|
2651
5080
|
assert.equal(supported.length, 20);
|
|
@@ -2670,8 +5099,15 @@ test("configuration menu omits removed static-check raw-spec and reference actio
|
|
|
2670
5099
|
await command!.handler("", ctx);
|
|
2671
5100
|
|
|
2672
5101
|
const renderedStaticCheckMenu = (ctx.__state.customRenderLines[1] ?? []).join("\n");
|
|
5102
|
+
const staticCheckItems = ctx.__state.selectCalls[1]?.items ?? [];
|
|
2673
5103
|
assert.match(renderedStaticCheckMenu, /Add static code checker/);
|
|
2674
5104
|
assert.match(renderedStaticCheckMenu, /Remove static code checker/);
|
|
5105
|
+
assert.deepEqual(staticCheckItems.slice(0, 2), ["Add static code checker", "Remove static code checker"]);
|
|
5106
|
+
assert.deepEqual(
|
|
5107
|
+
staticCheckItems.slice(2, 22),
|
|
5108
|
+
getSupportedStaticCheckLanguageSupport().map((entry) => entry.language),
|
|
5109
|
+
);
|
|
5110
|
+
assert.deepEqual(staticCheckItems.slice(-1), ["Reset defaults"]);
|
|
2675
5111
|
assert.doesNotMatch(renderedStaticCheckMenu, /Add entry from LANG=MODULE\[,CMD\[,PARAM\.\.\.\]\]/);
|
|
2676
5112
|
assert.doesNotMatch(renderedStaticCheckMenu, /Show supported languages/);
|
|
2677
5113
|
});
|
|
@@ -2696,6 +5132,29 @@ test("configuration menu hides removed static-check modules from user-facing act
|
|
|
2696
5132
|
assert.doesNotMatch(renderedStaticCheckMenu, /Dummy/);
|
|
2697
5133
|
});
|
|
2698
5134
|
|
|
5135
|
+
test("configuration menu can toggle per-language static-check enablement", async () => {
|
|
5136
|
+
const cwd = createTempDir("pi-usereq-menu-sc-toggle-");
|
|
5137
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
5138
|
+
const pi = createFakePi();
|
|
5139
|
+
piUsereqExtension(pi);
|
|
5140
|
+
const command = pi.commands.get("pi-usereq");
|
|
5141
|
+
assert.ok(command);
|
|
5142
|
+
|
|
5143
|
+
await command!.handler(
|
|
5144
|
+
"",
|
|
5145
|
+
createFakeCtx(cwd, {
|
|
5146
|
+
selects: ["static-check", "Python", "Save and close", "Save and close"],
|
|
5147
|
+
}),
|
|
5148
|
+
);
|
|
5149
|
+
|
|
5150
|
+
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
5151
|
+
assert.equal(config["static-check"].Python.enabled, "disable");
|
|
5152
|
+
assert.deepEqual(config["static-check"].Python.checkers, [
|
|
5153
|
+
{ module: "Command", cmd: "pyright", params: ["--outputjson"] },
|
|
5154
|
+
{ module: "Command", cmd: "ruff", params: ["check"] },
|
|
5155
|
+
]);
|
|
5156
|
+
});
|
|
5157
|
+
|
|
2699
5158
|
test("configuration menu can add guided static-check entries for explicit supported languages", async () => {
|
|
2700
5159
|
const cwd = createTempDir("pi-usereq-menu-sc-guided-");
|
|
2701
5160
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
@@ -2710,7 +5169,7 @@ test("configuration menu can add guided static-check entries for explicit suppor
|
|
|
2710
5169
|
selects: [
|
|
2711
5170
|
"static-check",
|
|
2712
5171
|
"Add static code checker",
|
|
2713
|
-
"
|
|
5172
|
+
"Ruby",
|
|
2714
5173
|
"Save and close",
|
|
2715
5174
|
"Save and close",
|
|
2716
5175
|
],
|
|
@@ -2719,5 +5178,7 @@ test("configuration menu can add guided static-check entries for explicit suppor
|
|
|
2719
5178
|
);
|
|
2720
5179
|
|
|
2721
5180
|
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
2722
|
-
assert.deepEqual(config["static-check"].
|
|
5181
|
+
assert.deepEqual(config["static-check"].Ruby, createStaticCheckLanguageConfig([
|
|
5182
|
+
{ module: "Command", cmd: "true" },
|
|
5183
|
+
], "enable"));
|
|
2723
5184
|
});
|