pi-usereq 0.7.0 → 0.11.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 +24 -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 +5214 -24470
- package/pi-usereq/docs/REQUIREMENTS.md +307 -170
- package/pi-usereq/docs/WORKFLOW.md +334 -316
- 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 +314 -59
- 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 +1816 -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/runtime-project-paths.ts +21 -1
- 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 +2047 -947
- 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 +77 -70
- package/tests/extension-registration.test.ts +3205 -658
- 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
|
|
|
@@ -294,11 +406,30 @@ function formatFakeThemeBackgroundFromForeground(color: string, text: string): s
|
|
|
294
406
|
return `<bg-from-fg-${color}>${text}</bg-from-fg-${color}>`;
|
|
295
407
|
}
|
|
296
408
|
|
|
409
|
+
/**
|
|
410
|
+
* @brief Resolves the expected fake branch payload for status assertions.
|
|
411
|
+
* @details Reads `git branch --show-current` from the supplied base path and falls back to `unknown` when the path is outside a repository or HEAD has no branch name. Runtime is dominated by git execution. Side effects include subprocess creation.
|
|
412
|
+
* @param[in] basePath {string | undefined} Base path represented by the fake context.
|
|
413
|
+
* @return {string} Expected branch name or `unknown` when unavailable.
|
|
414
|
+
*/
|
|
415
|
+
function buildExpectedFakeBranchValue(basePath: string | undefined): string {
|
|
416
|
+
if (!basePath || !fs.existsSync(basePath)) {
|
|
417
|
+
return "unknown";
|
|
418
|
+
}
|
|
419
|
+
const result = spawnSync("git", ["branch", "--show-current"], {
|
|
420
|
+
cwd: basePath,
|
|
421
|
+
encoding: "utf8",
|
|
422
|
+
});
|
|
423
|
+
if (result.error || result.status !== 0) {
|
|
424
|
+
return "unknown";
|
|
425
|
+
}
|
|
426
|
+
const branchName = result.stdout.trim();
|
|
427
|
+
return branchName === "" ? "unknown" : branchName;
|
|
428
|
+
}
|
|
429
|
+
|
|
297
430
|
/**
|
|
298
431
|
* @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.
|
|
432
|
+
* @details Resolves the documented icon thresholds for `0`, `>0-<25`, `>=25-<50`, `>=50-<75`, and `>=75-<90`, emits the non-blinking error full icon for `>=90-<100`, and emits the blinking error full icon for `>=100`. Runtime is O(1). No external state is mutated.
|
|
302
433
|
* @param[in] options {{ filledCells: number; percent?: number | null }} Expected context-gauge facts.
|
|
303
434
|
* @return {string} Encoded context-gauge string.
|
|
304
435
|
*/
|
|
@@ -308,34 +439,35 @@ function buildExpectedFakeContextBar(options: {
|
|
|
308
439
|
}): string {
|
|
309
440
|
const percent = options.percent;
|
|
310
441
|
if (percent === undefined || percent === null || percent <= 0) {
|
|
311
|
-
return
|
|
442
|
+
return "▕_▏";
|
|
312
443
|
}
|
|
313
|
-
if (percent
|
|
444
|
+
if (percent >= 100) {
|
|
314
445
|
return formatFakeThemeForeground("error", "\u001b[5m▕█▏\u001b[25m");
|
|
315
446
|
}
|
|
316
|
-
if (percent
|
|
317
|
-
return formatFakeThemeForeground("
|
|
447
|
+
if (percent >= 90) {
|
|
448
|
+
return formatFakeThemeForeground("error", "▕█▏");
|
|
318
449
|
}
|
|
319
|
-
if (percent
|
|
320
|
-
return
|
|
450
|
+
if (percent < 25) {
|
|
451
|
+
return "▕▂▏";
|
|
321
452
|
}
|
|
322
|
-
if (percent
|
|
323
|
-
return
|
|
453
|
+
if (percent < 50) {
|
|
454
|
+
return "▕▄▏";
|
|
324
455
|
}
|
|
325
|
-
|
|
456
|
+
if (percent < 75) {
|
|
457
|
+
return "▕▆▏";
|
|
458
|
+
}
|
|
459
|
+
return "▕█▏";
|
|
326
460
|
}
|
|
327
461
|
|
|
328
462
|
/**
|
|
329
463
|
* @brief Builds the expected fake pi-usereq status-bar string for assertions.
|
|
330
|
-
* @details Reconstructs the field order,
|
|
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.
|
|
464
|
+
* @details Reconstructs the field order, workflow-state highlighting, branch field, icon-based context gauge, consolidated elapsed field, and sound field emitted by the extension using deterministic fake theme markers. Runtime is O(1) plus optional git execution for branch discovery. No external state is mutated.
|
|
465
|
+
* @param[in] options {{ workflowState?: string; basePath?: string; contextFilledCells: number; contextPercent?: number | null; et: string; sound?: string }} Expected status facts for rendered `status`, `branch`, `context`, `elapsed`, and `sound` fields.
|
|
335
466
|
* @return {string} Encoded status-bar string.
|
|
336
467
|
*/
|
|
337
468
|
function buildExpectedFakeStatusText(options: {
|
|
338
|
-
|
|
469
|
+
workflowState?: string;
|
|
470
|
+
basePath?: string;
|
|
339
471
|
docsDir?: string;
|
|
340
472
|
testsDir?: string;
|
|
341
473
|
srcDir?: string[];
|
|
@@ -346,12 +478,18 @@ function buildExpectedFakeStatusText(options: {
|
|
|
346
478
|
}): string {
|
|
347
479
|
const buildField = (fieldName: string, value: string): string =>
|
|
348
480
|
`${formatFakeThemeForeground("accent", `${fieldName}:`)}${formatFakeThemeForeground("warning", value)}`;
|
|
481
|
+
const workflowStateText = options.workflowState ?? "idle";
|
|
482
|
+
const workflowStateValue = workflowStateText === "error"
|
|
483
|
+
? formatFakeThemeForeground("error", "\u001b[5merror\u001b[25m")
|
|
484
|
+
: formatFakeThemeForeground("warning", workflowStateText);
|
|
485
|
+
const branchValue = buildExpectedFakeBranchValue(options.basePath);
|
|
349
486
|
const contextBar = buildExpectedFakeContextBar({
|
|
350
487
|
filledCells: options.contextFilledCells,
|
|
351
488
|
percent: options.contextPercent,
|
|
352
489
|
});
|
|
353
490
|
return [
|
|
354
|
-
|
|
491
|
+
`${formatFakeThemeForeground("accent", "status:")}${workflowStateValue}`,
|
|
492
|
+
buildField("branch", branchValue),
|
|
355
493
|
`${formatFakeThemeForeground("accent", "context:")}${contextBar}`,
|
|
356
494
|
buildField("elapsed", options.et),
|
|
357
495
|
buildField("sound", options.sound ?? "none"),
|
|
@@ -359,10 +497,11 @@ function buildExpectedFakeStatusText(options: {
|
|
|
359
497
|
}
|
|
360
498
|
|
|
361
499
|
/**
|
|
362
|
-
* @brief Formats one expected absolute base path for
|
|
500
|
+
* @brief Formats one expected absolute base path for prompt and status assertions.
|
|
363
501
|
* @details Resolves the supplied cwd and normalizes path separators to `/` so
|
|
364
|
-
*
|
|
365
|
-
* in path length. No external state is
|
|
502
|
+
* prompt-text comparisons and legacy status-call-site inputs stay stable across
|
|
503
|
+
* operating systems. Runtime is O(p) in path length. No external state is
|
|
504
|
+
* mutated.
|
|
366
505
|
* @param[in] cwd {string} Runtime working directory.
|
|
367
506
|
* @return {string} Slash-normalized absolute base path.
|
|
368
507
|
*/
|
|
@@ -396,8 +535,8 @@ function buildExpectedNotifyBasePath(cwd: string): string {
|
|
|
396
535
|
* @return {string} `~`-relative or absolute config-path display string.
|
|
397
536
|
*/
|
|
398
537
|
function buildExpectedShowConfigPath(cwd: string): string {
|
|
399
|
-
return buildExpectedNotifyBasePath(path.dirname(
|
|
400
|
-
? `${buildExpectedNotifyBasePath(cwd)}/.pi-usereq
|
|
538
|
+
return buildExpectedNotifyBasePath(path.dirname(getProjectConfigPath(cwd))) === buildExpectedNotifyBasePath(cwd)
|
|
539
|
+
? `${buildExpectedNotifyBasePath(cwd)}/.pi-usereq.json`
|
|
401
540
|
: getProjectConfigPath(cwd).split(path.sep).join("/");
|
|
402
541
|
}
|
|
403
542
|
|
|
@@ -430,7 +569,13 @@ function createFakeCtx(cwd: string, plan: FakeCtxPlan = { selects: [] }, options
|
|
|
430
569
|
const selects = [...plan.selects];
|
|
431
570
|
const inputs = [...(plan.inputs ?? [])];
|
|
432
571
|
const getContextUsage = options.getContextUsage ?? (() => undefined);
|
|
433
|
-
const
|
|
572
|
+
const baseSessionManager = options.sessionManager ?? createFakeSessionManager(cwd);
|
|
573
|
+
let currentSessionCwd = typeof baseSessionManager.getCwd === "function"
|
|
574
|
+
? baseSessionManager.getCwd()
|
|
575
|
+
: cwd;
|
|
576
|
+
let currentSessionFile = typeof baseSessionManager.getSessionFile === "function"
|
|
577
|
+
? baseSessionManager.getSessionFile()
|
|
578
|
+
: createFakeSessionFile(currentSessionCwd);
|
|
434
579
|
const theme = options.theme ?? {
|
|
435
580
|
fg: (color: string, text: string) => formatFakeThemeForeground(color, text),
|
|
436
581
|
bgFromFg: (color: string, text: string) => formatFakeThemeBackgroundFromForeground(color, text),
|
|
@@ -445,13 +590,43 @@ function createFakeCtx(cwd: string, plan: FakeCtxPlan = { selects: [] }, options
|
|
|
445
590
|
waitForIdleCalls: 0,
|
|
446
591
|
newSessions: [] as Array<{ messages: string[] }>,
|
|
447
592
|
};
|
|
448
|
-
|
|
593
|
+
const sessionManager: FakeSessionManager = {
|
|
594
|
+
appendCustomEntry(customType: string, data?: unknown): string {
|
|
595
|
+
return baseSessionManager.appendCustomEntry(customType, data);
|
|
596
|
+
},
|
|
597
|
+
appendMessage(message: unknown): string {
|
|
598
|
+
return baseSessionManager.appendMessage(message);
|
|
599
|
+
},
|
|
600
|
+
getBranch(): FakeSessionEntry[] {
|
|
601
|
+
return baseSessionManager.getBranch();
|
|
602
|
+
},
|
|
603
|
+
getEntries(): FakeSessionEntry[] {
|
|
604
|
+
return baseSessionManager.getEntries();
|
|
605
|
+
},
|
|
606
|
+
getCwd(): string {
|
|
607
|
+
return currentSessionCwd;
|
|
608
|
+
},
|
|
609
|
+
getSessionDir(): string | undefined {
|
|
610
|
+
return typeof currentSessionFile === "string" ? path.dirname(currentSessionFile) : undefined;
|
|
611
|
+
},
|
|
612
|
+
getSessionFile(): string | undefined {
|
|
613
|
+
return currentSessionFile;
|
|
614
|
+
},
|
|
615
|
+
};
|
|
616
|
+
const fakeCtx: any = {
|
|
449
617
|
cwd,
|
|
450
618
|
__state: state,
|
|
451
619
|
sessionManager,
|
|
452
620
|
async waitForIdle() {
|
|
453
621
|
state.waitForIdleCalls += 1;
|
|
454
622
|
},
|
|
623
|
+
async switchSession(sessionPath: string) {
|
|
624
|
+
currentSessionFile = sessionPath;
|
|
625
|
+
currentSessionCwd = readFakeSessionFileCwd(sessionPath, currentSessionCwd);
|
|
626
|
+
fakeCtx.cwd = currentSessionCwd;
|
|
627
|
+
process.chdir(currentSessionCwd);
|
|
628
|
+
return { cancelled: false };
|
|
629
|
+
},
|
|
455
630
|
async newSession(newSessionOptions?: {
|
|
456
631
|
parentSession?: string;
|
|
457
632
|
setup?: (sessionManager: FakeSessionManager) => Promise<void>;
|
|
@@ -473,6 +648,15 @@ function createFakeCtx(cwd: string, plan: FakeCtxPlan = { selects: [] }, options
|
|
|
473
648
|
getEntries(): FakeSessionEntry[] {
|
|
474
649
|
return sessionManager.getEntries();
|
|
475
650
|
},
|
|
651
|
+
getCwd(): string {
|
|
652
|
+
return sessionManager.getCwd();
|
|
653
|
+
},
|
|
654
|
+
getSessionDir(): string | undefined {
|
|
655
|
+
return sessionManager.getSessionDir();
|
|
656
|
+
},
|
|
657
|
+
getSessionFile(): string | undefined {
|
|
658
|
+
return sessionManager.getSessionFile();
|
|
659
|
+
},
|
|
476
660
|
};
|
|
477
661
|
if (newSessionOptions?.setup) {
|
|
478
662
|
await newSessionOptions.setup(setupSessionManager);
|
|
@@ -529,6 +713,8 @@ function createFakeCtx(cwd: string, plan: FakeCtxPlan = { selects: [] }, options
|
|
|
529
713
|
});
|
|
530
714
|
if (response === undefined) {
|
|
531
715
|
bridge.cancel();
|
|
716
|
+
} else if (response === "Save and close" && !bridge.choices.some((choice) => choice.label === response)) {
|
|
717
|
+
bridge.cancel();
|
|
532
718
|
} else {
|
|
533
719
|
assert.ok(bridge.selectByLabel(response), `unsupported custom selection ${response}`);
|
|
534
720
|
}
|
|
@@ -549,6 +735,7 @@ function createFakeCtx(cwd: string, plan: FakeCtxPlan = { selects: [] }, options
|
|
|
549
735
|
},
|
|
550
736
|
},
|
|
551
737
|
};
|
|
738
|
+
return fakeCtx;
|
|
552
739
|
}
|
|
553
740
|
|
|
554
741
|
/**
|
|
@@ -604,25 +791,19 @@ test("extension registers prompt and config commands while exposing tool capabil
|
|
|
604
791
|
]) {
|
|
605
792
|
assert.ok(commandNames.includes(name), `missing command ${name}`);
|
|
606
793
|
}
|
|
794
|
+
assert.equal(pi.commands.get("req-analyze")?.description, "Produce an analysis report");
|
|
607
795
|
|
|
608
796
|
for (const name of [
|
|
609
|
-
"git-path",
|
|
610
|
-
"get-base-path",
|
|
611
797
|
"files-tokens",
|
|
612
798
|
"files-references",
|
|
613
799
|
"files-compress",
|
|
614
|
-
"files-
|
|
800
|
+
"files-search",
|
|
615
801
|
"references",
|
|
616
802
|
"compress",
|
|
617
|
-
"
|
|
803
|
+
"search",
|
|
618
804
|
"tokens",
|
|
619
805
|
"files-static-check",
|
|
620
806
|
"static-check",
|
|
621
|
-
"git-check",
|
|
622
|
-
"docs-check",
|
|
623
|
-
"git-wt-name",
|
|
624
|
-
"git-wt-create",
|
|
625
|
-
"git-wt-delete",
|
|
626
807
|
"pi-usereq-show-config",
|
|
627
808
|
"test-static-check",
|
|
628
809
|
]) {
|
|
@@ -631,23 +812,16 @@ test("extension registers prompt and config commands while exposing tool capabil
|
|
|
631
812
|
|
|
632
813
|
const toolNames = pi.tools.map((tool: RegisteredTool) => tool.name).sort();
|
|
633
814
|
for (const name of [
|
|
634
|
-
"git-path",
|
|
635
|
-
"get-base-path",
|
|
636
815
|
"files-tokens",
|
|
637
816
|
"files-references",
|
|
638
817
|
"files-compress",
|
|
639
|
-
"files-
|
|
818
|
+
"files-search",
|
|
640
819
|
"references",
|
|
641
820
|
"compress",
|
|
642
|
-
"
|
|
821
|
+
"search",
|
|
643
822
|
"tokens",
|
|
644
823
|
"files-static-check",
|
|
645
824
|
"static-check",
|
|
646
|
-
"git-check",
|
|
647
|
-
"docs-check",
|
|
648
|
-
"git-wt-name",
|
|
649
|
-
"git-wt-create",
|
|
650
|
-
"git-wt-delete",
|
|
651
825
|
]) {
|
|
652
826
|
assert.ok(toolNames.includes(name), `missing tool ${name}`);
|
|
653
827
|
}
|
|
@@ -662,13 +836,13 @@ test("token tools register agent-oriented descriptions and schema details", () =
|
|
|
662
836
|
assert.ok(filesTokens, "missing files-tokens tool");
|
|
663
837
|
assert.ok(tokens, "missing tokens tool");
|
|
664
838
|
|
|
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 ?? ""), /
|
|
839
|
+
assert.match(filesTokens.description ?? "", /monolithic token pack summary/);
|
|
840
|
+
assert.ok(filesTokens.promptGuidelines?.some((line) => line.includes("monolithic pack-summary text")));
|
|
841
|
+
assert.match(String((filesTokens.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic pack-summary text/);
|
|
668
842
|
|
|
669
843
|
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 ?? ""), /
|
|
844
|
+
assert.ok(tokens.promptGuidelines?.some((line) => line.includes("monolithic pack-summary text")));
|
|
845
|
+
assert.match(String((tokens.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic pack-summary text/);
|
|
672
846
|
});
|
|
673
847
|
|
|
674
848
|
test("reference tools register agent-oriented descriptions and schema details", () => {
|
|
@@ -680,36 +854,36 @@ test("reference tools register agent-oriented descriptions and schema details",
|
|
|
680
854
|
assert.ok(filesReferences, "missing files-references tool");
|
|
681
855
|
assert.ok(references, "missing references tool");
|
|
682
856
|
|
|
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 ?? ""), /
|
|
857
|
+
assert.match(filesReferences.description ?? "", /monolithic references markdown report/);
|
|
858
|
+
assert.ok(filesReferences.promptGuidelines?.some((line) => line.includes("monolithic markdown")));
|
|
859
|
+
assert.ok(filesReferences.promptGuidelines?.some((line) => line.includes("Formatting contract:")));
|
|
860
|
+
assert.match(String((filesReferences.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic markdown/);
|
|
687
861
|
|
|
688
862
|
assert.match(references.description ?? "", /configured project source directories/);
|
|
689
|
-
assert.ok(references.promptGuidelines?.some((line) => line.includes("
|
|
863
|
+
assert.ok(references.promptGuidelines?.some((line) => line.includes("file-structure markdown block")));
|
|
690
864
|
assert.ok(references.promptGuidelines?.some((line) => line.includes("Configuration contract:")));
|
|
691
|
-
assert.match(String((references.parameters as { description?: string } | undefined)?.description ?? ""), /
|
|
865
|
+
assert.match(String((references.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic markdown/);
|
|
692
866
|
});
|
|
693
867
|
|
|
694
|
-
test("
|
|
868
|
+
test("search tools register agent-oriented descriptions and schema details", () => {
|
|
695
869
|
const pi = createFakePi();
|
|
696
870
|
piUsereqExtension(pi);
|
|
697
871
|
|
|
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((
|
|
872
|
+
const filesSearch = pi.tools.find((tool: RegisteredTool) => tool.name === "files-search");
|
|
873
|
+
const search = pi.tools.find((tool: RegisteredTool) => tool.name === "search");
|
|
874
|
+
assert.ok(filesSearch, "missing files-search tool");
|
|
875
|
+
assert.ok(search, "missing search tool");
|
|
876
|
+
|
|
877
|
+
assert.match(filesSearch.description ?? "", /monolithic construct-search markdown report/);
|
|
878
|
+
assert.ok(filesSearch.promptGuidelines?.some((line) => line.includes("Regex rule:")));
|
|
879
|
+
assert.ok(filesSearch.promptGuidelines?.some((line) => line.includes("Supported tags [Typescript]:")));
|
|
880
|
+
assert.ok(filesSearch.promptGuidelines?.some((line) => line.includes("Supported tags [Python]:")));
|
|
881
|
+
assert.match(String((filesSearch.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic markdown/);
|
|
882
|
+
|
|
883
|
+
assert.match(search.description ?? "", /configured project source directories/);
|
|
884
|
+
assert.ok(search.promptGuidelines?.some((line) => line.includes("monolithic markdown")));
|
|
885
|
+
assert.ok(search.promptGuidelines?.some((line) => line.includes("Tag rule:")));
|
|
886
|
+
assert.match(String((search.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic markdown/);
|
|
713
887
|
});
|
|
714
888
|
|
|
715
889
|
test("compression tools register agent-oriented descriptions and schema details", () => {
|
|
@@ -721,18 +895,69 @@ test("compression tools register agent-oriented descriptions and schema details"
|
|
|
721
895
|
assert.ok(filesCompress, "missing files-compress tool");
|
|
722
896
|
assert.ok(compress, "missing compress tool");
|
|
723
897
|
|
|
724
|
-
assert.match(filesCompress.description ?? "", /
|
|
898
|
+
assert.match(filesCompress.description ?? "", /monolithic compression markdown report/);
|
|
725
899
|
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 ?? ""), /
|
|
900
|
+
assert.ok(filesCompress.promptGuidelines?.some((line) => line.includes("Formatting contract:")));
|
|
901
|
+
assert.match(String((filesCompress.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic markdown/);
|
|
728
902
|
|
|
729
903
|
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 ?? ""), /
|
|
904
|
+
assert.ok(compress.promptGuidelines?.some((line) => line.includes("monolithic markdown")));
|
|
905
|
+
assert.ok(compress.promptGuidelines?.some((line) => line.includes("Formatting contract:")));
|
|
906
|
+
assert.match(String((compress.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic markdown/);
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
test("agent tools define custom renderResult with compact and expanded structured views", async () => {
|
|
910
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
911
|
+
try {
|
|
912
|
+
const pi = createFakePi();
|
|
913
|
+
piUsereqExtension(pi);
|
|
914
|
+
const toolNames = [
|
|
915
|
+
"files-tokens",
|
|
916
|
+
"files-references",
|
|
917
|
+
"files-compress",
|
|
918
|
+
"files-search",
|
|
919
|
+
"references",
|
|
920
|
+
"compress",
|
|
921
|
+
"search",
|
|
922
|
+
"tokens",
|
|
923
|
+
"files-static-check",
|
|
924
|
+
"static-check",
|
|
925
|
+
];
|
|
926
|
+
for (const toolName of toolNames) {
|
|
927
|
+
const tool = pi.tools.find((candidate: RegisteredTool) => candidate.name === toolName);
|
|
928
|
+
assert.ok(tool, `missing tool ${toolName}`);
|
|
929
|
+
assert.equal(typeof tool?.renderResult, "function", `missing renderResult for ${toolName}`);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
fs.writeFileSync(path.join(projectBase, "src", "render_result_target.ts"), "export function alpha(): number {\n return 1;\n}\n", "utf8");
|
|
933
|
+
const tool = pi.tools.find((candidate: RegisteredTool) => candidate.name === "files-search");
|
|
934
|
+
assert.ok(tool?.renderResult, "missing renderResult for files-search");
|
|
935
|
+
const params = {
|
|
936
|
+
tag: "FUNCTION",
|
|
937
|
+
pattern: "^alpha$",
|
|
938
|
+
files: ["src/render_result_target.ts"],
|
|
939
|
+
enableLineNumbers: true,
|
|
940
|
+
};
|
|
941
|
+
const result = await executeRegisteredTool(pi, "files-search", projectBase, params);
|
|
942
|
+
const theme: FakeThemeAdapter = {
|
|
943
|
+
fg: (color: string, text: string) => formatFakeThemeForeground(color, text),
|
|
944
|
+
bgFromFg: (color: string, text: string) => formatFakeThemeBackgroundFromForeground(color, text),
|
|
945
|
+
bold: (text: string) => `<bold>${text}</bold>`,
|
|
946
|
+
};
|
|
947
|
+
const compact = tool.renderResult(result, { expanded: false, isPartial: false }, theme, { args: params }).render(120).join("\n");
|
|
948
|
+
const expanded = tool.renderResult(result, { expanded: true, isPartial: false }, theme, { args: params }).render(120).join("\n");
|
|
949
|
+
assert.match(compact, /files-search/);
|
|
950
|
+
assert.match(compact, /tag="FUNCTION"/);
|
|
951
|
+
assert.match(compact, /pattern="\^alpha\$"/);
|
|
952
|
+
assert.match(compact, /files=\["src\/render_result_target\.ts"\]/);
|
|
953
|
+
assert.doesNotMatch(compact, /### FUNCTION/);
|
|
954
|
+
assert.match(expanded, /### FUNCTION: `alpha`/);
|
|
955
|
+
} finally {
|
|
956
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
957
|
+
}
|
|
733
958
|
});
|
|
734
959
|
|
|
735
|
-
test("files-tokens returns token
|
|
960
|
+
test("files-tokens returns monolithic token summary text with execution diagnostics", async () => {
|
|
736
961
|
const { projectBase } = initFixtureRepo({
|
|
737
962
|
fixtures: [],
|
|
738
963
|
docs: {
|
|
@@ -748,45 +973,20 @@ test("files-tokens returns token-optimized structured source facts", async () =>
|
|
|
748
973
|
files: [`${DEFAULT_DOCS_DIR}/REQUIREMENTS.md`, `${DEFAULT_DOCS_DIR}/MISSING.md`],
|
|
749
974
|
}) as {
|
|
750
975
|
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
|
-
};
|
|
976
|
+
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
770
977
|
};
|
|
771
978
|
|
|
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`));
|
|
979
|
+
const content = result.content?.[0]?.text ?? "";
|
|
980
|
+
assert.match(content, /Pack Summary/);
|
|
981
|
+
assert.match(content, /REQUIREMENTS\.md/);
|
|
982
|
+
assert.equal(result.details?.execution.code, 0);
|
|
983
|
+
assert.ok(result.details?.execution.stderr_lines?.some((line) => /MISSING\.md/.test(line)));
|
|
784
984
|
} finally {
|
|
785
985
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
786
986
|
}
|
|
787
987
|
});
|
|
788
988
|
|
|
789
|
-
test("files-tokens defers js-tiktoken loading until execution and returns
|
|
989
|
+
test("files-tokens defers js-tiktoken loading until execution and returns monolithic dependency failures", async () => {
|
|
790
990
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
791
991
|
const missingModuleError = new Error("Cannot find module 'js-tiktoken'");
|
|
792
992
|
setJsTiktokenModuleLoaderForTests(() => {
|
|
@@ -798,35 +998,20 @@ test("files-tokens defers js-tiktoken loading until execution and returns struct
|
|
|
798
998
|
const result = await executeRegisteredTool(pi, "files-tokens", projectBase, {
|
|
799
999
|
files: [`${DEFAULT_DOCS_DIR}/REQUIREMENTS.md`],
|
|
800
1000
|
}) 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
|
-
};
|
|
1001
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1002
|
+
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
813
1003
|
};
|
|
814
1004
|
|
|
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/);
|
|
1005
|
+
assert.equal(result.details?.execution.code, 1);
|
|
1006
|
+
assert.match(result.content?.[0]?.text ?? "", /js-tiktoken/);
|
|
1007
|
+
assert.ok(result.details?.execution.stderr_lines?.some((line) => /npm ci/.test(line)));
|
|
823
1008
|
} finally {
|
|
824
1009
|
setJsTiktokenModuleLoaderForTests(undefined);
|
|
825
1010
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
826
1011
|
}
|
|
827
1012
|
});
|
|
828
1013
|
|
|
829
|
-
test("files-references returns
|
|
1014
|
+
test("files-references returns monolithic references markdown", async () => {
|
|
830
1015
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
831
1016
|
const samplePath = path.join(projectBase, "src", "sample.ts");
|
|
832
1017
|
fs.writeFileSync(samplePath, `/**
|
|
@@ -851,66 +1036,22 @@ export function buildSample(input: string): string {
|
|
|
851
1036
|
files: ["src/sample.ts", "src/missing.ts"],
|
|
852
1037
|
}) as {
|
|
853
1038
|
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
|
-
};
|
|
1039
|
+
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
885
1040
|
};
|
|
886
1041
|
|
|
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/);
|
|
1042
|
+
const content = result.content?.[0]?.text ?? "";
|
|
1043
|
+
assert.match(content, /# sample\.ts \| TypeScript/);
|
|
1044
|
+
assert.match(content, /## Definitions/);
|
|
1045
|
+
assert.match(content, /buildSample/);
|
|
1046
|
+
assert.match(content, /Caller-supplied value\./);
|
|
1047
|
+
assert.equal(result.details?.execution.code, 0);
|
|
1048
|
+
assert.deepEqual(result.details?.execution.stderr_lines, undefined);
|
|
908
1049
|
} finally {
|
|
909
1050
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
910
1051
|
}
|
|
911
1052
|
});
|
|
912
1053
|
|
|
913
|
-
test("references returns
|
|
1054
|
+
test("references returns monolithic project references markdown", async () => {
|
|
914
1055
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
915
1056
|
fs.writeFileSync(path.join(projectBase, "src", "alpha.ts"), "export const ALPHA = 1;\n", "utf8");
|
|
916
1057
|
fs.mkdirSync(path.join(projectBase, "src", "nested"), { recursive: true });
|
|
@@ -920,49 +1061,23 @@ test("references returns a structured repository tree for configured source dire
|
|
|
920
1061
|
piUsereqExtension(pi);
|
|
921
1062
|
const result = await executeRegisteredTool(pi, "references", projectBase, {}) as {
|
|
922
1063
|
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
|
-
};
|
|
1064
|
+
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
942
1065
|
};
|
|
943
1066
|
|
|
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]);
|
|
1067
|
+
const content = result.content?.[0]?.text ?? "";
|
|
1068
|
+
assert.match(content, /# Files Structure/);
|
|
1069
|
+
assert.match(content, /src\/alpha\.ts/);
|
|
1070
|
+
assert.match(content, /src\/nested\/beta\.ts/);
|
|
1071
|
+
assert.match(content, /# alpha\.ts \| TypeScript/);
|
|
1072
|
+
assert.match(content, /# beta\.ts \| TypeScript/);
|
|
1073
|
+
assert.equal(result.details?.execution.code, 0);
|
|
1074
|
+
assert.deepEqual(result.details?.execution.stderr_lines, undefined);
|
|
960
1075
|
} finally {
|
|
961
1076
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
962
1077
|
}
|
|
963
1078
|
});
|
|
964
1079
|
|
|
965
|
-
test("files-compress returns
|
|
1080
|
+
test("files-compress returns monolithic compression markdown", async () => {
|
|
966
1081
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
967
1082
|
const samplePath = path.join(projectBase, "src", "sample.ts");
|
|
968
1083
|
fs.writeFileSync(samplePath, `/**
|
|
@@ -988,68 +1103,21 @@ export function buildSample(input: string): string {
|
|
|
988
1103
|
enableLineNumbers: true,
|
|
989
1104
|
}) as {
|
|
990
1105
|
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
|
-
};
|
|
1106
|
+
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
1022
1107
|
};
|
|
1023
1108
|
|
|
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/);
|
|
1109
|
+
const content = result.content?.[0]?.text ?? "";
|
|
1110
|
+
assert.match(content, /@@@ src\/sample\.ts \| typescript/);
|
|
1111
|
+
assert.match(content, /> Lines: 11-14/);
|
|
1112
|
+
assert.match(content, /^11: export function buildSample/m);
|
|
1113
|
+
assert.equal(result.details?.execution.code, 0);
|
|
1114
|
+
assert.deepEqual(result.details?.execution.stderr_lines, undefined);
|
|
1047
1115
|
} finally {
|
|
1048
1116
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
1049
1117
|
}
|
|
1050
1118
|
});
|
|
1051
1119
|
|
|
1052
|
-
test("compress returns
|
|
1120
|
+
test("compress returns monolithic repository-scoped compression markdown", async () => {
|
|
1053
1121
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
1054
1122
|
fs.writeFileSync(
|
|
1055
1123
|
path.join(projectBase, "src", "alpha.ts"),
|
|
@@ -1069,45 +1137,21 @@ test("compress returns a structured repository-scoped compression payload", asyn
|
|
|
1069
1137
|
enableLineNumbers: false,
|
|
1070
1138
|
}) as {
|
|
1071
1139
|
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
|
-
};
|
|
1140
|
+
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
1092
1141
|
};
|
|
1093
1142
|
|
|
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, "");
|
|
1143
|
+
const content = result.content?.[0]?.text ?? "";
|
|
1144
|
+
assert.match(content, /@@@ src\/alpha\.ts \| typescript/);
|
|
1145
|
+
assert.match(content, /@@@ src\/nested\/beta\.ts \| typescript/);
|
|
1146
|
+
assert.match(content, /^export function buildBeta/m);
|
|
1147
|
+
assert.equal(result.details?.execution.code, 0);
|
|
1148
|
+
assert.deepEqual(result.details?.execution.stderr_lines, undefined);
|
|
1105
1149
|
} finally {
|
|
1106
1150
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
1107
1151
|
}
|
|
1108
1152
|
});
|
|
1109
1153
|
|
|
1110
|
-
test("files-
|
|
1154
|
+
test("files-search returns monolithic search markdown", async () => {
|
|
1111
1155
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
1112
1156
|
const samplePath = path.join(projectBase, "src", "sample.ts");
|
|
1113
1157
|
fs.writeFileSync(samplePath, `/**
|
|
@@ -1128,86 +1172,29 @@ export function buildSample(input: string): string {
|
|
|
1128
1172
|
try {
|
|
1129
1173
|
const pi = createFakePi();
|
|
1130
1174
|
piUsereqExtension(pi);
|
|
1131
|
-
const result = await executeRegisteredTool(pi, "files-
|
|
1175
|
+
const result = await executeRegisteredTool(pi, "files-search", projectBase, {
|
|
1132
1176
|
tag: "FUNCTION",
|
|
1133
1177
|
pattern: "^buildSample$",
|
|
1134
1178
|
files: ["src/sample.ts", "src/missing.ts"],
|
|
1135
1179
|
enableLineNumbers: true,
|
|
1136
1180
|
}) as {
|
|
1137
1181
|
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
|
-
};
|
|
1182
|
+
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
1175
1183
|
};
|
|
1176
1184
|
|
|
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/);
|
|
1185
|
+
const content = result.content?.[0]?.text ?? "";
|
|
1186
|
+
assert.match(content, /### FUNCTION: `buildSample`/);
|
|
1187
|
+
assert.match(content, /> Signature: `export function buildSample\(input: string\): string`/);
|
|
1188
|
+
assert.match(content, /> Lines: 12-14/);
|
|
1189
|
+
assert.match(content, /^12: export function buildSample/m);
|
|
1190
|
+
assert.equal(result.details?.execution.code, 0);
|
|
1191
|
+
assert.deepEqual(result.details?.execution.stderr_lines, undefined);
|
|
1205
1192
|
} finally {
|
|
1206
1193
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
1207
1194
|
}
|
|
1208
1195
|
});
|
|
1209
1196
|
|
|
1210
|
-
test("
|
|
1197
|
+
test("search returns monolithic repository-scoped construct-search markdown", async () => {
|
|
1211
1198
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
1212
1199
|
fs.writeFileSync(
|
|
1213
1200
|
path.join(projectBase, "src", "alpha.ts"),
|
|
@@ -1230,63 +1217,120 @@ export function buildAlpha(): number {
|
|
|
1230
1217
|
try {
|
|
1231
1218
|
const pi = createFakePi();
|
|
1232
1219
|
piUsereqExtension(pi);
|
|
1233
|
-
const result = await executeRegisteredTool(pi, "
|
|
1220
|
+
const result = await executeRegisteredTool(pi, "search", projectBase, {
|
|
1234
1221
|
tag: "FUNCTION",
|
|
1235
1222
|
pattern: "^build",
|
|
1236
1223
|
enableLineNumbers: false,
|
|
1237
1224
|
}) as {
|
|
1238
1225
|
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
|
-
|
|
1226
|
+
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
1227
|
+
};
|
|
1228
|
+
|
|
1229
|
+
const content = result.content?.[0]?.text ?? "";
|
|
1230
|
+
assert.match(content, /### FUNCTION: `buildAlpha`/);
|
|
1231
|
+
assert.match(content, /export function buildAlpha/);
|
|
1232
|
+
assert.doesNotMatch(content, /helperBeta/);
|
|
1233
|
+
assert.equal(result.details?.execution.code, 0);
|
|
1234
|
+
assert.deepEqual(result.details?.execution.stderr_lines, undefined);
|
|
1235
|
+
} finally {
|
|
1236
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
test("source-extraction agent tools preserve leading tabs in emitted content", async () => {
|
|
1241
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
1242
|
+
const samplePath = path.join(projectBase, "src", "sample.go");
|
|
1243
|
+
fs.writeFileSync(samplePath, `package main
|
|
1244
|
+
|
|
1245
|
+
import (
|
|
1246
|
+
"bytes"
|
|
1247
|
+
"context"
|
|
1248
|
+
"fmt"
|
|
1249
|
+
)
|
|
1250
|
+
|
|
1251
|
+
type Runner interface {
|
|
1252
|
+
Run(ctx context.Context) error
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
type Server struct{}
|
|
1256
|
+
|
|
1257
|
+
func (s *Server) Start(ctx context.Context) error {
|
|
1258
|
+
var buffer bytes.Buffer
|
|
1259
|
+
fmt.Println("start", buffer.Len())
|
|
1260
|
+
return nil
|
|
1261
|
+
}
|
|
1262
|
+
`, "utf8");
|
|
1263
|
+
try {
|
|
1264
|
+
const pi = createFakePi();
|
|
1265
|
+
piUsereqExtension(pi);
|
|
1266
|
+
|
|
1267
|
+
const filesReferencesResult = await executeRegisteredTool(pi, "files-references", projectBase, {
|
|
1268
|
+
files: ["src/sample.go"],
|
|
1269
|
+
}) as {
|
|
1270
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1271
|
+
details?: { execution: { code: number } };
|
|
1272
|
+
};
|
|
1273
|
+
assert.match(filesReferencesResult.content?.[0]?.text ?? "", /\tvar buffer bytes\.Buffer/);
|
|
1274
|
+
assert.equal(filesReferencesResult.details?.execution.code, 0);
|
|
1275
|
+
|
|
1276
|
+
const referencesResult = await executeRegisteredTool(pi, "references", projectBase, {}) as {
|
|
1277
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1278
|
+
details?: { execution: { code: number } };
|
|
1279
|
+
};
|
|
1280
|
+
assert.match(referencesResult.content?.[0]?.text ?? "", /\tvar buffer bytes\.Buffer/);
|
|
1281
|
+
assert.equal(referencesResult.details?.execution.code, 0);
|
|
1282
|
+
|
|
1283
|
+
const filesCompressResult = await executeRegisteredTool(pi, "files-compress", projectBase, {
|
|
1284
|
+
files: ["src/sample.go"],
|
|
1285
|
+
enableLineNumbers: true,
|
|
1286
|
+
}) as {
|
|
1287
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1288
|
+
details?: { execution: { code: number } };
|
|
1289
|
+
};
|
|
1290
|
+
assert.match(filesCompressResult.content?.[0]?.text ?? "", /\d+: \t"context"/);
|
|
1291
|
+
assert.equal(filesCompressResult.details?.execution.code, 0);
|
|
1292
|
+
|
|
1293
|
+
const compressResult = await executeRegisteredTool(pi, "compress", projectBase, {
|
|
1294
|
+
enableLineNumbers: true,
|
|
1295
|
+
}) as {
|
|
1296
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1297
|
+
details?: { execution: { code: number } };
|
|
1298
|
+
};
|
|
1299
|
+
assert.match(compressResult.content?.[0]?.text ?? "", /\d+: \t"context"/);
|
|
1300
|
+
assert.equal(compressResult.details?.execution.code, 0);
|
|
1301
|
+
|
|
1302
|
+
const filesSearchResult = await executeRegisteredTool(pi, "files-search", projectBase, {
|
|
1303
|
+
tag: "METHOD|FUNCTION",
|
|
1304
|
+
pattern: "^Start$",
|
|
1305
|
+
files: ["src/sample.go"],
|
|
1306
|
+
enableLineNumbers: true,
|
|
1307
|
+
}) as {
|
|
1308
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1309
|
+
details?: { execution: { code: number } };
|
|
1264
1310
|
};
|
|
1311
|
+
assert.match(filesSearchResult.content?.[0]?.text ?? "", /\d+: \tfmt\.Println\("start", buffer\.Len\(\)\)/);
|
|
1312
|
+
assert.equal(filesSearchResult.details?.execution.code, 0);
|
|
1265
1313
|
|
|
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/);
|
|
1314
|
+
const searchResult = await executeRegisteredTool(pi, "search", projectBase, {
|
|
1315
|
+
tag: "METHOD|FUNCTION",
|
|
1316
|
+
pattern: "^Start$",
|
|
1317
|
+
enableLineNumbers: true,
|
|
1318
|
+
}) as {
|
|
1319
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1320
|
+
details?: { execution: { code: number } };
|
|
1321
|
+
};
|
|
1322
|
+
assert.match(searchResult.content?.[0]?.text ?? "", /\d+: \tfmt\.Println\("start", buffer\.Len\(\)\)/);
|
|
1323
|
+
assert.equal(searchResult.details?.execution.code, 0);
|
|
1280
1324
|
} finally {
|
|
1281
1325
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
1282
1326
|
}
|
|
1283
1327
|
});
|
|
1284
1328
|
|
|
1285
|
-
test("
|
|
1329
|
+
test("static-check tools return monolithic text payloads with minimal execution details", async () => {
|
|
1286
1330
|
const { projectBase } = initFixtureRepo({
|
|
1287
1331
|
fixtures: [],
|
|
1288
1332
|
staticCheck: {
|
|
1289
|
-
TypeScript: [{ module: "Dummy" }],
|
|
1333
|
+
TypeScript: createStaticCheckLanguageConfig([{ module: "Dummy" }], "enable"),
|
|
1290
1334
|
},
|
|
1291
1335
|
});
|
|
1292
1336
|
fs.writeFileSync(path.join(projectBase, "src", "check.ts"), "export const VALUE = 1;\n", "utf8");
|
|
@@ -1298,97 +1342,46 @@ test("path, static-check, git, docs, and worktree tools return structured JSON p
|
|
|
1298
1342
|
const pi = createFakePi();
|
|
1299
1343
|
piUsereqExtension(pi);
|
|
1300
1344
|
|
|
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
1345
|
const filesStaticCheckResult = await executeRegisteredTool(pi, "files-static-check", projectBase, {
|
|
1343
1346
|
files: ["src/check.ts", "src/missing.ts"],
|
|
1344
1347
|
}) as {
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
files: Array<{ canonical_path: string; language_name?: string; status: string; configured_checker_modules: string[] }>;
|
|
1348
|
-
execution: { code: number };
|
|
1349
|
-
};
|
|
1348
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1349
|
+
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
1350
1350
|
};
|
|
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");
|
|
1351
|
+
assert.match(filesStaticCheckResult.content?.[0]?.text ?? "", /src\/missing\.ts/);
|
|
1358
1352
|
assert.equal(filesStaticCheckResult.details?.execution.code, 0);
|
|
1353
|
+
assert.ok(filesStaticCheckResult.details?.execution.stderr_lines?.some((line) => /src\/missing\.ts/.test(line)));
|
|
1359
1354
|
|
|
1360
1355
|
const staticCheckResult = await executeRegisteredTool(pi, "static-check", projectBase) as {
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
files: Array<{ canonical_path: string; status: string }>;
|
|
1364
|
-
execution: { code: number };
|
|
1365
|
-
};
|
|
1356
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1357
|
+
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
1366
1358
|
};
|
|
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");
|
|
1359
|
+
assert.equal(staticCheckResult.content?.[0]?.text ?? "", "");
|
|
1370
1360
|
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
|
-
|
|
1361
|
+
assert.deepEqual(staticCheckResult.details?.execution.stderr_lines, undefined);
|
|
1362
|
+
|
|
1363
|
+
fs.writeFileSync(
|
|
1364
|
+
getProjectConfigPath(projectBase),
|
|
1365
|
+
`${JSON.stringify({
|
|
1366
|
+
...getDefaultConfig(projectBase),
|
|
1367
|
+
"docs-dir": DEFAULT_DOCS_DIR,
|
|
1368
|
+
"tests-dir": "tests",
|
|
1369
|
+
"src-dir": ["src"],
|
|
1370
|
+
"static-check": {
|
|
1371
|
+
TypeScript: createStaticCheckLanguageConfig([{ module: "Dummy" }], "disable"),
|
|
1372
|
+
},
|
|
1373
|
+
}, null, 2)}\n`,
|
|
1374
|
+
"utf8",
|
|
1375
|
+
);
|
|
1376
|
+
const disabledFilesStaticCheckResult = await executeRegisteredTool(pi, "files-static-check", projectBase, {
|
|
1377
|
+
files: ["src/check.ts"],
|
|
1378
|
+
}) as {
|
|
1379
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1380
|
+
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
1388
1381
|
};
|
|
1389
|
-
assert.equal(
|
|
1390
|
-
assert.
|
|
1391
|
-
assert.
|
|
1382
|
+
assert.equal(disabledFilesStaticCheckResult.content?.[0]?.text ?? "", "");
|
|
1383
|
+
assert.equal(disabledFilesStaticCheckResult.details?.execution.code, 0);
|
|
1384
|
+
assert.deepEqual(disabledFilesStaticCheckResult.details?.execution.stderr_lines, undefined);
|
|
1392
1385
|
} finally {
|
|
1393
1386
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
1394
1387
|
}
|
|
@@ -1407,6 +1400,57 @@ test("configuration menu saves updated docs-dir in project config", async () =>
|
|
|
1407
1400
|
assert.equal(config["docs-dir"], "docs/custom");
|
|
1408
1401
|
});
|
|
1409
1402
|
|
|
1403
|
+
test("configuration menu persists trailing-slash-free relative directory values", async () => {
|
|
1404
|
+
const cwd = createTempDir("pi-usereq-menu-path-contracts-");
|
|
1405
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1406
|
+
const pi = createFakePi();
|
|
1407
|
+
piUsereqExtension(pi);
|
|
1408
|
+
const command = pi.commands.get("pi-usereq");
|
|
1409
|
+
assert.ok(command);
|
|
1410
|
+
const ctx = createFakeCtx(cwd, {
|
|
1411
|
+
selects: [
|
|
1412
|
+
"docs-dir",
|
|
1413
|
+
"tests-dir",
|
|
1414
|
+
"src-dir",
|
|
1415
|
+
"add-src-dir-entry",
|
|
1416
|
+
"Save and close",
|
|
1417
|
+
"Save and close",
|
|
1418
|
+
],
|
|
1419
|
+
inputs: ["docs/custom/", "tests/custom/", "src/custom/"],
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
await command!.handler("", ctx);
|
|
1423
|
+
|
|
1424
|
+
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
1425
|
+
assert.equal(config["docs-dir"], "docs/custom");
|
|
1426
|
+
assert.equal(config["tests-dir"], "tests/custom");
|
|
1427
|
+
assert.deepEqual(config["src-dir"], ["src", "src/custom"]);
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
test("configuration menu forces worktree rows off and dim when auto git commit is disabled", async () => {
|
|
1431
|
+
const cwd = createTempDir("pi-usereq-menu-worktree-");
|
|
1432
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1433
|
+
const pi = createFakePi();
|
|
1434
|
+
piUsereqExtension(pi);
|
|
1435
|
+
const command = pi.commands.get("pi-usereq");
|
|
1436
|
+
assert.ok(command);
|
|
1437
|
+
const ctx = createFakeCtx(cwd, {
|
|
1438
|
+
selects: ["Auto git commit", "Git worktree", "Worktree prefix", "Save and close"],
|
|
1439
|
+
inputs: ["custom-prefix-"],
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
await command!.handler("", ctx);
|
|
1443
|
+
|
|
1444
|
+
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
1445
|
+
const defaultConfig = getDefaultConfig(cwd);
|
|
1446
|
+
const renderedLockedMenu = (ctx.__state.customRenderLines[1] ?? []).join("\n");
|
|
1447
|
+
assert.equal(config.AUTO_GIT_COMMIT, "disable");
|
|
1448
|
+
assert.equal(config.GIT_WORKTREE_ENABLED, "disable");
|
|
1449
|
+
assert.equal(config.GIT_WORKTREE_PREFIX, defaultConfig.GIT_WORKTREE_PREFIX);
|
|
1450
|
+
assert.match(renderedLockedMenu, /<dim>Git worktree/);
|
|
1451
|
+
assert.match(renderedLockedMenu, /<dim>Worktree prefix/);
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1410
1454
|
test("configuration menu omits prompt-delivery controls and persisted config omits removed runtime fields", async () => {
|
|
1411
1455
|
const cwd = createTempDir("pi-usereq-menu-current-session-");
|
|
1412
1456
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
@@ -1419,11 +1463,8 @@ test("configuration menu omits prompt-delivery controls and persisted config omi
|
|
|
1419
1463
|
await command!.handler("", ctx);
|
|
1420
1464
|
|
|
1421
1465
|
const menuItems = ctx.__state.selectCalls[0]?.items ?? [];
|
|
1422
|
-
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
1423
1466
|
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);
|
|
1467
|
+
assert.equal(fs.existsSync(getProjectConfigPath(cwd)), false);
|
|
1427
1468
|
});
|
|
1428
1469
|
|
|
1429
1470
|
test("configuration menus expose show-config ordering and omit overview or notification-reference rows", async () => {
|
|
@@ -1435,26 +1476,33 @@ test("configuration menus expose show-config ordering and omit overview or notif
|
|
|
1435
1476
|
piUsereqExtension(pi);
|
|
1436
1477
|
const command = pi.commands.get("pi-usereq");
|
|
1437
1478
|
assert.ok(command);
|
|
1438
|
-
const ctx = createFakeCtx(cwd, { selects: ["
|
|
1479
|
+
const ctx = createFakeCtx(cwd, { selects: ["notifications", "Save and close", "Save and close"] });
|
|
1439
1480
|
|
|
1440
1481
|
await command!.handler("", ctx);
|
|
1441
1482
|
|
|
1442
1483
|
const renderedMenu = (ctx.__state.customRenderLines[0] ?? []).join("\n");
|
|
1484
|
+
const renderedNotificationsMenu = (ctx.__state.customRenderLines[1] ?? []).join("\n");
|
|
1443
1485
|
assert.deepEqual(ctx.__state.selectCalls[0]?.items ?? [], [
|
|
1444
1486
|
"Document directory",
|
|
1445
1487
|
"Source-code directories",
|
|
1446
1488
|
"Unit tests directory",
|
|
1489
|
+
"Auto git commit",
|
|
1490
|
+
"Git worktree",
|
|
1491
|
+
"Worktree prefix",
|
|
1447
1492
|
"Language static code checkers",
|
|
1448
1493
|
"Enable tools",
|
|
1449
1494
|
"Notifications",
|
|
1495
|
+
"Debug",
|
|
1450
1496
|
"Show configuration",
|
|
1451
1497
|
"Reset defaults",
|
|
1452
|
-
"Save and close",
|
|
1453
1498
|
]);
|
|
1454
1499
|
assert.ok(renderedMenu.includes(buildExpectedShowConfigPath(cwd)));
|
|
1455
1500
|
assert.ok(renderedMenu.includes("notification:off • sound:none • pushover:off"));
|
|
1501
|
+
assert.ok(renderedMenu.includes("disable"));
|
|
1456
1502
|
assert.doesNotMatch(renderedMenu, /beep:/);
|
|
1457
|
-
assert.
|
|
1503
|
+
assert.match(renderedNotificationsMenu, /<dim>Enable pushover/);
|
|
1504
|
+
assert.match(renderedNotificationsMenu, /<dim>off<\/dim>/);
|
|
1505
|
+
assert.deepEqual(ctx.__state.selectCalls[1]?.items ?? [], [
|
|
1458
1506
|
"Enable notification",
|
|
1459
1507
|
"Notification events",
|
|
1460
1508
|
"Notify command",
|
|
@@ -1472,51 +1520,34 @@ test("configuration menus expose show-config ordering and omit overview or notif
|
|
|
1472
1520
|
"Pushover User Key/Delivery Group Key",
|
|
1473
1521
|
"Pushover Token/API Token Key",
|
|
1474
1522
|
"Reset defaults",
|
|
1475
|
-
"Save and close",
|
|
1476
1523
|
]);
|
|
1477
1524
|
});
|
|
1478
1525
|
|
|
1479
|
-
test("
|
|
1480
|
-
const cwd = createTempDir("pi-usereq-menu-
|
|
1526
|
+
test("show configuration saves pending config, closes the menu, and writes the persisted project config file text into the editor", async () => {
|
|
1527
|
+
const cwd = createTempDir("pi-usereq-menu-show-config-");
|
|
1481
1528
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1482
1529
|
const pi = createFakePi();
|
|
1483
1530
|
piUsereqExtension(pi);
|
|
1484
1531
|
const command = pi.commands.get("pi-usereq");
|
|
1485
1532
|
assert.ok(command);
|
|
1486
1533
|
const ctx = createFakeCtx(cwd, {
|
|
1487
|
-
selects: [
|
|
1488
|
-
|
|
1489
|
-
"Notification events",
|
|
1490
|
-
"Prompt failed",
|
|
1491
|
-
"Save and close",
|
|
1492
|
-
"Notify command",
|
|
1493
|
-
"Save and close",
|
|
1494
|
-
"Save and close",
|
|
1495
|
-
],
|
|
1496
|
-
inputs: ["echo notify"],
|
|
1534
|
+
selects: ["docs-dir", "show-config"],
|
|
1535
|
+
inputs: ["docs/custom"],
|
|
1497
1536
|
});
|
|
1498
1537
|
|
|
1499
1538
|
await command!.handler("", ctx);
|
|
1500
1539
|
|
|
1501
|
-
const
|
|
1502
|
-
assert.equal(
|
|
1503
|
-
assert.
|
|
1504
|
-
assert.equal(
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
"Prompt completed",
|
|
1509
|
-
"Prompt interrupted",
|
|
1510
|
-
"Prompt failed",
|
|
1511
|
-
"Reset defaults",
|
|
1512
|
-
"Save and close",
|
|
1513
|
-
]);
|
|
1514
|
-
assert.equal(notificationEventCalls[0]?.selectedChoiceId, "notify-on-completed");
|
|
1515
|
-
assert.equal(notificationEventCalls[1]?.selectedChoiceId, "notify-on-failed");
|
|
1540
|
+
const persistedText = fs.readFileSync(getProjectConfigPath(cwd), "utf8");
|
|
1541
|
+
assert.equal(ctx.__state.editorText, persistedText);
|
|
1542
|
+
assert.match(ctx.__state.editorText, /"docs-dir": "docs\/custom"/);
|
|
1543
|
+
assert.equal(
|
|
1544
|
+
ctx.__state.selectCalls.filter((call) => call.title === "pi-usereq").length,
|
|
1545
|
+
2,
|
|
1546
|
+
);
|
|
1516
1547
|
});
|
|
1517
1548
|
|
|
1518
|
-
test("
|
|
1519
|
-
const cwd = createTempDir("pi-usereq-menu-
|
|
1549
|
+
test("descendant configuration menus end with Reset defaults only", async () => {
|
|
1550
|
+
const cwd = createTempDir("pi-usereq-menu-terminal-rows-");
|
|
1520
1551
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1521
1552
|
const pi = createFakePi();
|
|
1522
1553
|
piUsereqExtension(pi);
|
|
@@ -1524,24 +1555,396 @@ test("notifications reset defaults preserves non-notification settings", async (
|
|
|
1524
1555
|
assert.ok(command);
|
|
1525
1556
|
const ctx = createFakeCtx(cwd, {
|
|
1526
1557
|
selects: [
|
|
1527
|
-
"docs-dir",
|
|
1528
1558
|
"notifications",
|
|
1529
|
-
"Enable notification",
|
|
1530
1559
|
"Enable sound",
|
|
1531
|
-
"
|
|
1532
|
-
"
|
|
1560
|
+
"Save and close",
|
|
1561
|
+
"Pushover priority",
|
|
1562
|
+
"Save and close",
|
|
1563
|
+
"Save and close",
|
|
1564
|
+
"Enable tools",
|
|
1565
|
+
"Enable tools",
|
|
1566
|
+
"Save and close",
|
|
1567
|
+
"Save and close",
|
|
1568
|
+
"Debug",
|
|
1569
|
+
"Save and close",
|
|
1570
|
+
"Language static code checkers",
|
|
1571
|
+
"Add static code checker",
|
|
1572
|
+
"Save and close",
|
|
1573
|
+
"Remove static code checker",
|
|
1574
|
+
"Save and close",
|
|
1575
|
+
"Save and close",
|
|
1576
|
+
"Source-code directories",
|
|
1577
|
+
"Remove source-code directory",
|
|
1578
|
+
"Save and close",
|
|
1533
1579
|
"Save and close",
|
|
1534
1580
|
"Save and close",
|
|
1535
1581
|
],
|
|
1536
|
-
inputs: ["docs/custom"],
|
|
1537
1582
|
});
|
|
1538
1583
|
|
|
1539
1584
|
await command!.handler("", ctx);
|
|
1540
1585
|
|
|
1541
|
-
const
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1586
|
+
const getMenuIndex = (title: string, marker?: string): number => {
|
|
1587
|
+
const index = ctx.__state.selectCalls.findIndex(
|
|
1588
|
+
(entry) => entry.title === title && (marker === undefined || entry.items.includes(marker)),
|
|
1589
|
+
);
|
|
1590
|
+
assert.notEqual(index, -1, `missing menu ${title}`);
|
|
1591
|
+
return index;
|
|
1592
|
+
};
|
|
1593
|
+
const getTerminalRows = (title: string, marker?: string): string[] => {
|
|
1594
|
+
const index = getMenuIndex(title, marker);
|
|
1595
|
+
return ctx.__state.selectCalls[index]!.items.slice(-1);
|
|
1596
|
+
};
|
|
1597
|
+
const getRenderedMenu = (title: string, marker?: string): string => {
|
|
1598
|
+
const index = getMenuIndex(title, marker);
|
|
1599
|
+
return (ctx.__state.customRenderLines[index] ?? []).join("\n");
|
|
1600
|
+
};
|
|
1601
|
+
|
|
1602
|
+
assert.deepEqual(getTerminalRows("Enable sound", "high"), ["Reset defaults"]);
|
|
1603
|
+
assert.deepEqual(getTerminalRows("Pushover priority", "High"), ["Reset defaults"]);
|
|
1604
|
+
assert.deepEqual(getTerminalRows("Enable tools", "static-check"), ["Reset defaults"]);
|
|
1605
|
+
assert.deepEqual(getTerminalRows("Debug", "Log on status"), ["Reset defaults"]);
|
|
1606
|
+
assert.deepEqual(getTerminalRows("static-check language", "Python"), ["Reset defaults"]);
|
|
1607
|
+
assert.deepEqual(getTerminalRows("Remove static code checker"), ["Reset defaults"]);
|
|
1608
|
+
assert.deepEqual(getTerminalRows("Remove source-code directory", "src"), ["Reset defaults"]);
|
|
1609
|
+
assert.doesNotMatch(getRenderedMenu("Enable sound", "high"), /Reset defaults.*none/);
|
|
1610
|
+
assert.doesNotMatch(getRenderedMenu("Pushover priority", "High"), /Reset defaults.*Normal/);
|
|
1611
|
+
assert.doesNotMatch(getRenderedMenu("Enable tools", "static-check"), /Reset defaults.*defaults/);
|
|
1612
|
+
assert.doesNotMatch(getRenderedMenu("Debug", "Log on status"), /Reset defaults.*running/);
|
|
1613
|
+
assert.doesNotMatch(getRenderedMenu("Remove source-code directory", "src"), /Reset defaults.*src/);
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1616
|
+
test("debug menu dims locked rows and persists debug settings with focus-preserving re-renders", async () => {
|
|
1617
|
+
const cwd = createTempDir("pi-usereq-menu-debug-");
|
|
1618
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1619
|
+
const pi = createFakePi();
|
|
1620
|
+
piUsereqExtension(pi);
|
|
1621
|
+
const command = pi.commands.get("pi-usereq");
|
|
1622
|
+
assert.ok(command);
|
|
1623
|
+
const ctx = createFakeCtx(cwd, {
|
|
1624
|
+
selects: [
|
|
1625
|
+
"Debug",
|
|
1626
|
+
"Debug",
|
|
1627
|
+
"Log file",
|
|
1628
|
+
"Log on status",
|
|
1629
|
+
"any",
|
|
1630
|
+
"Status changes",
|
|
1631
|
+
"Workflow events",
|
|
1632
|
+
"files-tokens",
|
|
1633
|
+
"req-analyze",
|
|
1634
|
+
"Save and close",
|
|
1635
|
+
"Save and close",
|
|
1636
|
+
],
|
|
1637
|
+
inputs: ["logs/debug-output.json"],
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
await command!.handler("", ctx);
|
|
1641
|
+
|
|
1642
|
+
const renderedInitialDebugMenu = (ctx.__state.customRenderLines[1] ?? []).join("\n");
|
|
1643
|
+
const persistedConfig = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
1644
|
+
const debugCalls = ctx.__state.selectCalls.filter((call) => call.title === "Debug");
|
|
1645
|
+
assert.match(renderedInitialDebugMenu, /<dim>Log file/);
|
|
1646
|
+
assert.match(renderedInitialDebugMenu, /<dim>Log on status/);
|
|
1647
|
+
assert.match(renderedInitialDebugMenu, /<dim>Status changes/);
|
|
1648
|
+
assert.match(renderedInitialDebugMenu, /<dim>Workflow events/);
|
|
1649
|
+
assert.match(renderedInitialDebugMenu, /<dim>compress/);
|
|
1650
|
+
assert.equal(persistedConfig.DEBUG_ENABLED, "enable");
|
|
1651
|
+
assert.equal(persistedConfig.DEBUG_LOG_FILE, "logs/debug-output.json");
|
|
1652
|
+
assert.equal(persistedConfig.DEBUG_STATUS_CHANGES, "enable");
|
|
1653
|
+
assert.equal(persistedConfig.DEBUG_WORKFLOW_EVENTS, "enable");
|
|
1654
|
+
assert.equal(persistedConfig.DEBUG_LOG_ON_STATUS, "any");
|
|
1655
|
+
assert.deepEqual(persistedConfig.DEBUG_ENABLED_TOOLS, ["files-tokens"]);
|
|
1656
|
+
assert.deepEqual(persistedConfig.DEBUG_ENABLED_PROMPTS, ["req-analyze"]);
|
|
1657
|
+
assert.equal(debugCalls[1]?.selectedChoiceId, "debug-enabled");
|
|
1658
|
+
assert.equal(debugCalls[2]?.selectedChoiceId, "debug-log-file");
|
|
1659
|
+
assert.equal(debugCalls[3]?.selectedChoiceId, "debug-log-on-status");
|
|
1660
|
+
assert.equal(debugCalls[4]?.selectedChoiceId, "debug-status-changes");
|
|
1661
|
+
assert.equal(debugCalls[5]?.selectedChoiceId, "debug-workflow-events");
|
|
1662
|
+
assert.equal(debugCalls[6]?.selectedChoiceId, "debug-tool:files-tokens");
|
|
1663
|
+
assert.equal(debugCalls[7]?.selectedChoiceId, "debug-prompt:req-analyze");
|
|
1664
|
+
|
|
1665
|
+
const resetCtx = createFakeCtx(cwd, {
|
|
1666
|
+
selects: ["Debug", "Reset defaults", "Approve reset", "Save and close"],
|
|
1667
|
+
});
|
|
1668
|
+
await command!.handler("", resetCtx);
|
|
1669
|
+
const resetConfig = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
1670
|
+
assert.equal(resetConfig.DEBUG_ENABLED, "disable");
|
|
1671
|
+
assert.equal(resetConfig.DEBUG_LOG_FILE, DEFAULT_DEBUG_LOG_FILE);
|
|
1672
|
+
assert.equal(resetConfig.DEBUG_STATUS_CHANGES, "disable");
|
|
1673
|
+
assert.equal(resetConfig.DEBUG_WORKFLOW_EVENTS, DEFAULT_DEBUG_WORKFLOW_EVENTS);
|
|
1674
|
+
assert.equal(resetConfig.DEBUG_LOG_ON_STATUS, DEFAULT_DEBUG_LOG_ON_STATUS);
|
|
1675
|
+
assert.deepEqual(resetConfig.DEBUG_ENABLED_TOOLS, []);
|
|
1676
|
+
assert.deepEqual(resetConfig.DEBUG_ENABLED_PROMPTS, []);
|
|
1677
|
+
});
|
|
1678
|
+
|
|
1679
|
+
test("debug menu rows derive from canonical tool and prompt inventories", async () => {
|
|
1680
|
+
const cwd = createTempDir("pi-usereq-menu-debug-inventory-");
|
|
1681
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1682
|
+
const pi = createFakePi();
|
|
1683
|
+
piUsereqExtension(pi);
|
|
1684
|
+
const command = pi.commands.get("pi-usereq");
|
|
1685
|
+
assert.ok(command);
|
|
1686
|
+
const ctx = createFakeCtx(cwd, {
|
|
1687
|
+
selects: ["Debug", "Save and close"]
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
await command!.handler("", ctx);
|
|
1691
|
+
|
|
1692
|
+
assert.deepEqual(ctx.__state.selectCalls[1]?.items ?? [], [
|
|
1693
|
+
"Debug",
|
|
1694
|
+
"Log file",
|
|
1695
|
+
"Log on status",
|
|
1696
|
+
"Status changes",
|
|
1697
|
+
"Workflow events",
|
|
1698
|
+
...[
|
|
1699
|
+
...PI_USEREQ_CUSTOM_TOOL_NAMES,
|
|
1700
|
+
...PI_USEREQ_EMBEDDED_TOOL_NAMES,
|
|
1701
|
+
].sort(comparePiUsereqStartupToolNames),
|
|
1702
|
+
...DEBUG_PROMPT_NAMES,
|
|
1703
|
+
"Reset defaults",
|
|
1704
|
+
]);
|
|
1705
|
+
assert.deepEqual(
|
|
1706
|
+
DEBUG_PROMPT_NAMES,
|
|
1707
|
+
PROMPT_COMMAND_NAMES.map((promptName) => `req-${promptName}`),
|
|
1708
|
+
);
|
|
1709
|
+
});
|
|
1710
|
+
|
|
1711
|
+
test("tool debug logging honors global enablement and workflow-status filters", async () => {
|
|
1712
|
+
const idleBase = createTempDir("pi-usereq-tool-debug-idle-");
|
|
1713
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(idleBase)), { recursive: true });
|
|
1714
|
+
fs.writeFileSync(
|
|
1715
|
+
getProjectConfigPath(idleBase),
|
|
1716
|
+
`${JSON.stringify({
|
|
1717
|
+
...getDefaultConfig(idleBase),
|
|
1718
|
+
DEBUG_ENABLED: "enable",
|
|
1719
|
+
DEBUG_LOG_FILE: "tool-debug.json",
|
|
1720
|
+
DEBUG_LOG_ON_STATUS: "any",
|
|
1721
|
+
DEBUG_ENABLED_TOOLS: ["bash"],
|
|
1722
|
+
}, null, 2)}\n`,
|
|
1723
|
+
"utf8",
|
|
1724
|
+
);
|
|
1725
|
+
const idlePi = createFakePi();
|
|
1726
|
+
piUsereqExtension(idlePi);
|
|
1727
|
+
const idleCtx = createFakeCtx(idleBase);
|
|
1728
|
+
await idlePi.emit("session_start", { reason: "startup" }, idleCtx);
|
|
1729
|
+
await idlePi.emit("tool_result", {
|
|
1730
|
+
toolName: "bash",
|
|
1731
|
+
input: { command: "echo idle" },
|
|
1732
|
+
content: [{ type: "text", text: "idle" }],
|
|
1733
|
+
details: { stdout: "idle" },
|
|
1734
|
+
isError: false,
|
|
1735
|
+
}, idleCtx);
|
|
1736
|
+
const idleLog = JSON.parse(fs.readFileSync(path.join(idleBase, "tool-debug.json"), "utf8"));
|
|
1737
|
+
assert.equal(idleLog.length, 1);
|
|
1738
|
+
assert.equal(idleLog[0].category, "tool");
|
|
1739
|
+
assert.equal(idleLog[0].name, "bash");
|
|
1740
|
+
assert.equal(idleLog[0].workflow_state, "idle");
|
|
1741
|
+
assert.deepEqual(idleLog[0].input, { command: "echo idle" });
|
|
1742
|
+
assert.deepEqual(idleLog[0].result, {
|
|
1743
|
+
content: [{ type: "text", text: "idle" }],
|
|
1744
|
+
details: { stdout: "idle" },
|
|
1745
|
+
});
|
|
1746
|
+
assert.equal(idleLog[0].is_error, false);
|
|
1747
|
+
|
|
1748
|
+
const runningFixture = initFixtureRepo();
|
|
1749
|
+
writeProjectConfigOverrides(runningFixture.projectBase, {
|
|
1750
|
+
GIT_WORKTREE_ENABLED: "disable",
|
|
1751
|
+
DEBUG_ENABLED: "enable",
|
|
1752
|
+
DEBUG_LOG_FILE: "tool-debug.json",
|
|
1753
|
+
DEBUG_LOG_ON_STATUS: "running",
|
|
1754
|
+
DEBUG_ENABLED_TOOLS: ["files-tokens"],
|
|
1755
|
+
});
|
|
1756
|
+
const runningPi = createFakePi();
|
|
1757
|
+
piUsereqExtension(runningPi);
|
|
1758
|
+
const runningCtx = createFakeCtx(runningFixture.projectBase);
|
|
1759
|
+
await runningPi.emit("session_start", { reason: "startup" }, runningCtx);
|
|
1760
|
+
await runningPi.emit("tool_result", {
|
|
1761
|
+
toolName: "files-tokens",
|
|
1762
|
+
input: { files: ["src/fixture_python.py"] },
|
|
1763
|
+
content: [{ type: "text", text: "idle" }],
|
|
1764
|
+
details: { summary: { counted_file_count: 1 } },
|
|
1765
|
+
isError: false,
|
|
1766
|
+
}, runningCtx);
|
|
1767
|
+
assert.equal(fs.existsSync(path.join(runningFixture.projectBase, "tool-debug.json")), false);
|
|
1768
|
+
const runningCommand = runningPi.commands.get("req-implement");
|
|
1769
|
+
assert.ok(runningCommand);
|
|
1770
|
+
await runningCommand!.handler("debug tools", runningCtx);
|
|
1771
|
+
await runningPi.emit("tool_result", {
|
|
1772
|
+
toolName: "files-tokens",
|
|
1773
|
+
input: { files: ["src/fixture_python.py"] },
|
|
1774
|
+
content: [{ type: "text", text: "running" }],
|
|
1775
|
+
details: { summary: { counted_file_count: 1 } },
|
|
1776
|
+
isError: false,
|
|
1777
|
+
}, runningCtx);
|
|
1778
|
+
const runningLog = JSON.parse(fs.readFileSync(path.join(runningFixture.projectBase, "tool-debug.json"), "utf8"));
|
|
1779
|
+
assert.equal(runningLog.length, 1);
|
|
1780
|
+
assert.equal(runningLog[0].workflow_state, "running");
|
|
1781
|
+
assert.equal(runningLog[0].name, "files-tokens");
|
|
1782
|
+
|
|
1783
|
+
const disabledBase = createTempDir("pi-usereq-tool-debug-disabled-");
|
|
1784
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(disabledBase)), { recursive: true });
|
|
1785
|
+
fs.writeFileSync(
|
|
1786
|
+
getProjectConfigPath(disabledBase),
|
|
1787
|
+
`${JSON.stringify({
|
|
1788
|
+
...getDefaultConfig(disabledBase),
|
|
1789
|
+
DEBUG_ENABLED: "disable",
|
|
1790
|
+
DEBUG_LOG_FILE: "tool-debug.json",
|
|
1791
|
+
DEBUG_LOG_ON_STATUS: "any",
|
|
1792
|
+
DEBUG_ENABLED_TOOLS: ["bash"],
|
|
1793
|
+
}, null, 2)}\n`,
|
|
1794
|
+
"utf8",
|
|
1795
|
+
);
|
|
1796
|
+
const disabledPi = createFakePi();
|
|
1797
|
+
piUsereqExtension(disabledPi);
|
|
1798
|
+
const disabledCtx = createFakeCtx(disabledBase);
|
|
1799
|
+
await disabledPi.emit("session_start", { reason: "startup" }, disabledCtx);
|
|
1800
|
+
await disabledPi.emit("tool_result", {
|
|
1801
|
+
toolName: "bash",
|
|
1802
|
+
input: { command: "echo disabled" },
|
|
1803
|
+
content: [{ type: "text", text: "disabled" }],
|
|
1804
|
+
details: { stdout: "disabled" },
|
|
1805
|
+
isError: false,
|
|
1806
|
+
}, disabledCtx);
|
|
1807
|
+
assert.equal(fs.existsSync(path.join(disabledBase, "tool-debug.json")), false);
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
test("prompt debug logging captures failing and successful prompt orchestration entries", async () => {
|
|
1811
|
+
const failingFixture = initFixtureRepo();
|
|
1812
|
+
fs.rmSync(path.join(failingFixture.projectBase, DEFAULT_DOCS_DIR, "WORKFLOW.md"), { force: true });
|
|
1813
|
+
assert.equal(spawnSync("git", ["add", "-A"], { cwd: failingFixture.projectBase, encoding: "utf8" }).status, 0);
|
|
1814
|
+
assert.equal(
|
|
1815
|
+
spawnSync("git", ["commit", "-m", "remove workflow"], { cwd: failingFixture.projectBase, encoding: "utf8" }).status,
|
|
1816
|
+
0,
|
|
1817
|
+
);
|
|
1818
|
+
writeProjectConfigOverrides(failingFixture.projectBase, {
|
|
1819
|
+
DEBUG_ENABLED: "enable",
|
|
1820
|
+
DEBUG_LOG_FILE: "prompt-debug.json",
|
|
1821
|
+
DEBUG_STATUS_CHANGES: "enable",
|
|
1822
|
+
DEBUG_WORKFLOW_EVENTS: "enable",
|
|
1823
|
+
DEBUG_LOG_ON_STATUS: "any",
|
|
1824
|
+
DEBUG_ENABLED_PROMPTS: ["req-change"],
|
|
1825
|
+
});
|
|
1826
|
+
const failingPi = createFakePi();
|
|
1827
|
+
piUsereqExtension(failingPi);
|
|
1828
|
+
const failingCtx = createFakeCtx(failingFixture.projectBase);
|
|
1829
|
+
await failingPi.emit("session_start", { reason: "startup" }, failingCtx);
|
|
1830
|
+
const failingCommand = failingPi.commands.get("req-change");
|
|
1831
|
+
assert.ok(failingCommand);
|
|
1832
|
+
await assert.rejects(async () => {
|
|
1833
|
+
await failingCommand!.handler("debug prompt", failingCtx);
|
|
1834
|
+
}, /Git status unclear|WORKFLOW\.md/);
|
|
1835
|
+
const failingLog = JSON.parse(fs.readFileSync(path.join(failingFixture.projectBase, "prompt-debug.json"), "utf8"));
|
|
1836
|
+
assert.ok(failingLog.some((entry: any) => entry.action === "workflow_state" && entry.result?.workflow_state === "error"));
|
|
1837
|
+
|
|
1838
|
+
const successFixture = initFixtureRepo();
|
|
1839
|
+
writeProjectConfigOverrides(successFixture.projectBase, {
|
|
1840
|
+
DEBUG_ENABLED: "enable",
|
|
1841
|
+
DEBUG_LOG_FILE: "prompt-debug.json",
|
|
1842
|
+
DEBUG_STATUS_CHANGES: "enable",
|
|
1843
|
+
DEBUG_WORKFLOW_EVENTS: "enable",
|
|
1844
|
+
DEBUG_LOG_ON_STATUS: "any",
|
|
1845
|
+
DEBUG_ENABLED_PROMPTS: ["req-implement"],
|
|
1846
|
+
});
|
|
1847
|
+
const successPi = createFakePi();
|
|
1848
|
+
piUsereqExtension(successPi);
|
|
1849
|
+
const successCtx = createFakeCtx(successFixture.projectBase);
|
|
1850
|
+
await successPi.emit("session_start", { reason: "startup" }, successCtx);
|
|
1851
|
+
const successCommand = successPi.commands.get("req-implement");
|
|
1852
|
+
assert.ok(successCommand);
|
|
1853
|
+
await successCommand!.handler("debug prompt", successCtx);
|
|
1854
|
+
const promptText = String(successPi.sentUserMessages.at(-1)?.content ?? "");
|
|
1855
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
1856
|
+
assert.ok(worktreeMatch, promptText);
|
|
1857
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
1858
|
+
await successPi.emit("before_agent_start", {}, successCtx);
|
|
1859
|
+
await successPi.emit("agent_start", {}, successCtx);
|
|
1860
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "debug.ts"), "export const debug = true;\n", "utf8");
|
|
1861
|
+
assert.equal(spawnSync("git", ["add", "src/debug.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
1862
|
+
const worktreeCommit = spawnSync("git", ["commit", "-m", "debug prompt"], {
|
|
1863
|
+
cwd: executionBasePath,
|
|
1864
|
+
encoding: "utf8",
|
|
1865
|
+
});
|
|
1866
|
+
assert.equal(worktreeCommit.status, 0, worktreeCommit.stderr);
|
|
1867
|
+
await successPi.emit("agent_end", {
|
|
1868
|
+
messages: [{ role: "assistant", stopReason: "end_turn" }],
|
|
1869
|
+
}, successCtx);
|
|
1870
|
+
const successLog = JSON.parse(fs.readFileSync(path.join(successFixture.projectBase, "prompt-debug.json"), "utf8"));
|
|
1871
|
+
assert.ok(successLog.some((entry: any) => entry.action === "required_docs_check" && entry.result?.success === true));
|
|
1872
|
+
assert.ok(successLog.some((entry: any) => entry.action === "worktree_create" && entry.result?.success === true));
|
|
1873
|
+
assert.ok(successLog.some((entry: any) => entry.action === "workflow_activation" && entry.result?.success === true));
|
|
1874
|
+
assert.ok(successLog.some((entry: any) => entry.action === "workflow_closure" && entry.result?.should_finalize_matched_success === true));
|
|
1875
|
+
assert.ok(successLog.some((entry: any) => entry.action === "workflow_restore" && entry.result?.success === true));
|
|
1876
|
+
assert.ok(successLog.some((entry: any) => entry.action === "merge" && entry.result?.success === true));
|
|
1877
|
+
assert.ok(successLog.some((entry: any) => entry.action === "worktree_delete" && entry.result?.success === true));
|
|
1878
|
+
assert.ok(successLog.some((entry: any) => entry.action === "workflow_state" && entry.result?.workflow_state === "merging"));
|
|
1879
|
+
assert.ok(successLog.some((entry: any) => entry.action === "workflow_state" && entry.result?.workflow_state === "idle"));
|
|
1880
|
+
});
|
|
1881
|
+
|
|
1882
|
+
test("notifications menu preserves focus on toggled and edited rows", async () => {
|
|
1883
|
+
const cwd = createTempDir("pi-usereq-menu-focus-");
|
|
1884
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1885
|
+
const pi = createFakePi();
|
|
1886
|
+
piUsereqExtension(pi);
|
|
1887
|
+
const command = pi.commands.get("pi-usereq");
|
|
1888
|
+
assert.ok(command);
|
|
1889
|
+
const ctx = createFakeCtx(cwd, {
|
|
1890
|
+
selects: [
|
|
1891
|
+
"notifications",
|
|
1892
|
+
"Notification events",
|
|
1893
|
+
"Prompt failed",
|
|
1894
|
+
"Save and close",
|
|
1895
|
+
"Notify command",
|
|
1896
|
+
"Save and close",
|
|
1897
|
+
"Save and close",
|
|
1898
|
+
],
|
|
1899
|
+
inputs: ["echo notify"],
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
await command!.handler("", ctx);
|
|
1903
|
+
|
|
1904
|
+
const notificationCalls = ctx.__state.selectCalls.filter((call) => call.title === "Notifications");
|
|
1905
|
+
assert.equal(notificationCalls[0]?.selectedChoiceId, "notify-enabled");
|
|
1906
|
+
assert.equal(notificationCalls[1]?.selectedChoiceId, "notification-events");
|
|
1907
|
+
assert.equal(notificationCalls[2]?.selectedChoiceId, "notify-command");
|
|
1908
|
+
|
|
1909
|
+
const notificationEventCalls = ctx.__state.selectCalls.filter((call) => call.title === "Notification events");
|
|
1910
|
+
assert.deepEqual(notificationEventCalls[0]?.items ?? [], [
|
|
1911
|
+
"Prompt completed",
|
|
1912
|
+
"Prompt interrupted",
|
|
1913
|
+
"Prompt failed",
|
|
1914
|
+
"Reset defaults",
|
|
1915
|
+
]);
|
|
1916
|
+
assert.equal(notificationEventCalls[0]?.selectedChoiceId, "notify-on-completed");
|
|
1917
|
+
assert.equal(notificationEventCalls[1]?.selectedChoiceId, "notify-on-failed");
|
|
1918
|
+
});
|
|
1919
|
+
|
|
1920
|
+
test("notifications reset defaults preserves non-notification settings", async () => {
|
|
1921
|
+
const cwd = createTempDir("pi-usereq-menu-reset-notifications-");
|
|
1922
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1923
|
+
const pi = createFakePi();
|
|
1924
|
+
piUsereqExtension(pi);
|
|
1925
|
+
const command = pi.commands.get("pi-usereq");
|
|
1926
|
+
assert.ok(command);
|
|
1927
|
+
const ctx = createFakeCtx(cwd, {
|
|
1928
|
+
selects: [
|
|
1929
|
+
"docs-dir",
|
|
1930
|
+
"notifications",
|
|
1931
|
+
"Enable notification",
|
|
1932
|
+
"Enable sound",
|
|
1933
|
+
"high",
|
|
1934
|
+
"Reset defaults",
|
|
1935
|
+
"Approve reset",
|
|
1936
|
+
"Save and close",
|
|
1937
|
+
"Save and close",
|
|
1938
|
+
],
|
|
1939
|
+
inputs: ["docs/custom"],
|
|
1940
|
+
});
|
|
1941
|
+
|
|
1942
|
+
await command!.handler("", ctx);
|
|
1943
|
+
|
|
1944
|
+
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
1945
|
+
assert.equal(config["docs-dir"], "docs/custom");
|
|
1946
|
+
assert.equal(config["notify-enabled"], false);
|
|
1947
|
+
assert.equal(config["notify-sound"], "none");
|
|
1545
1948
|
assert.equal(config["notify-pushover-enabled"], false);
|
|
1546
1949
|
});
|
|
1547
1950
|
|
|
@@ -1559,69 +1962,1978 @@ test("top-level reset defaults restores all configuration values", async () => {
|
|
|
1559
1962
|
"Enable notification",
|
|
1560
1963
|
"Save and close",
|
|
1561
1964
|
"Reset defaults",
|
|
1965
|
+
"Approve reset",
|
|
1562
1966
|
"Save and close",
|
|
1563
1967
|
],
|
|
1564
1968
|
inputs: ["docs/custom"],
|
|
1565
1969
|
});
|
|
1566
1970
|
|
|
1567
|
-
await command!.handler("", ctx);
|
|
1971
|
+
await command!.handler("", ctx);
|
|
1972
|
+
|
|
1973
|
+
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
1974
|
+
assert.equal(config["docs-dir"], DEFAULT_DOCS_DIR);
|
|
1975
|
+
assert.deepEqual(config["src-dir"], ["src"]);
|
|
1976
|
+
assert.equal(config["notify-enabled"], false);
|
|
1977
|
+
assert.equal(config["notify-sound"], "none");
|
|
1978
|
+
});
|
|
1979
|
+
|
|
1980
|
+
test("configuration menus reuse CLI settings theme semantics", async () => {
|
|
1981
|
+
const cwd = createTempDir("pi-usereq-menu-theme-");
|
|
1982
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1983
|
+
const usedColors = new Set<string>();
|
|
1984
|
+
const pi = createFakePi();
|
|
1985
|
+
piUsereqExtension(pi);
|
|
1986
|
+
const command = pi.commands.get("pi-usereq");
|
|
1987
|
+
assert.ok(command);
|
|
1988
|
+
const ctx = createFakeCtx(cwd, { selects: [] }, {
|
|
1989
|
+
theme: {
|
|
1990
|
+
fg(color: string, text: string) {
|
|
1991
|
+
usedColors.add(color);
|
|
1992
|
+
return formatFakeThemeForeground(color, text);
|
|
1993
|
+
},
|
|
1994
|
+
bgFromFg(color: string, text: string) {
|
|
1995
|
+
assert.ok(["accent", "muted", "dim"].includes(color));
|
|
1996
|
+
return formatFakeThemeBackgroundFromForeground(color, text);
|
|
1997
|
+
},
|
|
1998
|
+
bold(text: string) {
|
|
1999
|
+
return `<bold>${text}</bold>`;
|
|
2000
|
+
},
|
|
2001
|
+
},
|
|
2002
|
+
});
|
|
2003
|
+
|
|
2004
|
+
await command!.handler("", ctx);
|
|
2005
|
+
|
|
2006
|
+
const renderedMenu = (ctx.__state.customRenderLines[0] ?? []).join("\n");
|
|
2007
|
+
assert.match(renderedMenu, /<accent><bold>pi-usereq<\/bold><\/accent>/);
|
|
2008
|
+
assert.match(renderedMenu, /<accent>→ <\/accent>/);
|
|
2009
|
+
assert.match(renderedMenu, /<accent>pi-usereq\/docs<\/accent>/);
|
|
2010
|
+
assert.match(renderedMenu, /<muted>tests<\/muted>/);
|
|
2011
|
+
assert.match(renderedMenu, /<dim>/);
|
|
2012
|
+
assert.deepEqual([...usedColors].sort(), ["accent", "dim", "muted"]);
|
|
2013
|
+
});
|
|
2014
|
+
|
|
2015
|
+
test("prompt commands use the current session by default", async () => {
|
|
2016
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2017
|
+
try {
|
|
2018
|
+
writeProjectConfigOverrides(projectBase, { GIT_WORKTREE_ENABLED: "disable" });
|
|
2019
|
+
const pi = createFakePi();
|
|
2020
|
+
piUsereqExtension(pi);
|
|
2021
|
+
const command = pi.commands.get("req-analyze");
|
|
2022
|
+
assert.ok(command);
|
|
2023
|
+
const ctx = createFakeCtx(projectBase);
|
|
2024
|
+
|
|
2025
|
+
await command!.handler("Inspect src/index.ts for prompt coverage", ctx);
|
|
2026
|
+
|
|
2027
|
+
assert.equal(ctx.__state.waitForIdleCalls, 0);
|
|
2028
|
+
assert.equal(ctx.__state.newSessions.length, 0);
|
|
2029
|
+
assert.equal(pi.sentUserMessages.length, 1);
|
|
2030
|
+
assert.match(String(pi.sentUserMessages[0]?.content), /Inspect src\/index\.ts for prompt coverage/);
|
|
2031
|
+
} finally {
|
|
2032
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2033
|
+
}
|
|
2034
|
+
});
|
|
2035
|
+
|
|
2036
|
+
test("prompt commands skip worktree creation when auto git commit is disabled", async () => {
|
|
2037
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2038
|
+
try {
|
|
2039
|
+
writeProjectConfigOverrides(projectBase, {
|
|
2040
|
+
AUTO_GIT_COMMIT: "disable",
|
|
2041
|
+
GIT_WORKTREE_ENABLED: "enable",
|
|
2042
|
+
});
|
|
2043
|
+
const pi = createFakePi();
|
|
2044
|
+
piUsereqExtension(pi);
|
|
2045
|
+
const command = pi.commands.get("req-change");
|
|
2046
|
+
assert.ok(command);
|
|
2047
|
+
const ctx = createFakeCtx(projectBase);
|
|
2048
|
+
|
|
2049
|
+
await command!.handler("Adjust docs", ctx);
|
|
2050
|
+
|
|
2051
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2052
|
+
const expectedBasePath = buildExpectedFakeBasePath(projectBase).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2053
|
+
assert.doesNotMatch(promptText, /created worktree/);
|
|
2054
|
+
assert.match(
|
|
2055
|
+
promptText,
|
|
2056
|
+
new RegExp(`worktree orchestration is disabled and context-path remains \`${expectedBasePath}\``),
|
|
2057
|
+
);
|
|
2058
|
+
} finally {
|
|
2059
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2060
|
+
}
|
|
2061
|
+
});
|
|
2062
|
+
|
|
2063
|
+
test("prompt commands capture checking before handoff and running at prompt injection", async () => {
|
|
2064
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2065
|
+
const previousCwd = process.cwd();
|
|
2066
|
+
let checkingStatus = "";
|
|
2067
|
+
let handoffStatus = "";
|
|
2068
|
+
try {
|
|
2069
|
+
const pi = createFakePi();
|
|
2070
|
+
piUsereqExtension(pi);
|
|
2071
|
+
const ctx = createFakeCtx(projectBase);
|
|
2072
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2073
|
+
const originalSendUserMessage = pi.sendUserMessage.bind(pi);
|
|
2074
|
+
pi.sendUserMessage = (content: unknown, options?: unknown) => {
|
|
2075
|
+
handoffStatus = ctx.__state.statuses.get("pi-usereq") ?? "";
|
|
2076
|
+
originalSendUserMessage(content, options);
|
|
2077
|
+
};
|
|
2078
|
+
setPromptCommandPostCreateHookForTests(() => {
|
|
2079
|
+
checkingStatus = ctx.__state.statuses.get("pi-usereq") ?? "";
|
|
2080
|
+
});
|
|
2081
|
+
|
|
2082
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2083
|
+
|
|
2084
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2085
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2086
|
+
assert.ok(worktreeMatch, promptText);
|
|
2087
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2088
|
+
assert.equal(
|
|
2089
|
+
checkingStatus,
|
|
2090
|
+
buildExpectedFakeStatusText({
|
|
2091
|
+
workflowState: "checking",
|
|
2092
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2093
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2094
|
+
testsDir: "tests",
|
|
2095
|
+
srcDir: ["src"],
|
|
2096
|
+
contextFilledCells: 0,
|
|
2097
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2098
|
+
}),
|
|
2099
|
+
);
|
|
2100
|
+
assert.equal(
|
|
2101
|
+
handoffStatus,
|
|
2102
|
+
buildExpectedFakeStatusText({
|
|
2103
|
+
workflowState: "checking",
|
|
2104
|
+
basePath: buildExpectedFakeBasePath(executionBasePath),
|
|
2105
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2106
|
+
testsDir: "tests",
|
|
2107
|
+
srcDir: ["src"],
|
|
2108
|
+
contextFilledCells: 0,
|
|
2109
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2110
|
+
}),
|
|
2111
|
+
);
|
|
2112
|
+
assert.equal(
|
|
2113
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2114
|
+
buildExpectedFakeStatusText({
|
|
2115
|
+
workflowState: "running",
|
|
2116
|
+
basePath: buildExpectedFakeBasePath(executionBasePath),
|
|
2117
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2118
|
+
testsDir: "tests",
|
|
2119
|
+
srcDir: ["src"],
|
|
2120
|
+
contextFilledCells: 0,
|
|
2121
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2122
|
+
}),
|
|
2123
|
+
);
|
|
2124
|
+
|
|
2125
|
+
await pi.emit("before_agent_start", {}, ctx);
|
|
2126
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2127
|
+
await pi.emit("agent_end", {
|
|
2128
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2129
|
+
}, ctx);
|
|
2130
|
+
} finally {
|
|
2131
|
+
setPromptCommandPostCreateHookForTests(undefined);
|
|
2132
|
+
process.chdir(previousCwd);
|
|
2133
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2134
|
+
}
|
|
2135
|
+
});
|
|
2136
|
+
|
|
2137
|
+
test("prompt commands reject non-idle workflow state before starting a new orchestration", async () => {
|
|
2138
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2139
|
+
try {
|
|
2140
|
+
writeProjectConfigOverrides(projectBase, { GIT_WORKTREE_ENABLED: "disable" });
|
|
2141
|
+
const pi = createFakePi();
|
|
2142
|
+
piUsereqExtension(pi);
|
|
2143
|
+
const ctx = createFakeCtx(projectBase);
|
|
2144
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2145
|
+
|
|
2146
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2147
|
+
assert.equal(
|
|
2148
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2149
|
+
buildExpectedFakeStatusText({
|
|
2150
|
+
workflowState: "running",
|
|
2151
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2152
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2153
|
+
testsDir: "tests",
|
|
2154
|
+
srcDir: ["src"],
|
|
2155
|
+
contextFilledCells: 0,
|
|
2156
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2157
|
+
}),
|
|
2158
|
+
);
|
|
2159
|
+
|
|
2160
|
+
await assert.rejects(
|
|
2161
|
+
pi.commands.get("req-analyze")!.handler("Inspect src/index.ts", ctx),
|
|
2162
|
+
/expected idle/,
|
|
2163
|
+
);
|
|
2164
|
+
assert.equal(pi.sentUserMessages.length, 1);
|
|
2165
|
+
assert.ok(ctx.__state.notifications.some((entry) => /expected idle/.test(entry.message)));
|
|
2166
|
+
assert.equal(
|
|
2167
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2168
|
+
buildExpectedFakeStatusText({
|
|
2169
|
+
workflowState: "running",
|
|
2170
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2171
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2172
|
+
testsDir: "tests",
|
|
2173
|
+
srcDir: ["src"],
|
|
2174
|
+
contextFilledCells: 0,
|
|
2175
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2176
|
+
}),
|
|
2177
|
+
);
|
|
2178
|
+
|
|
2179
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2180
|
+
await pi.emit("agent_end", {
|
|
2181
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2182
|
+
}, ctx);
|
|
2183
|
+
assert.equal(
|
|
2184
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2185
|
+
buildExpectedFakeStatusText({
|
|
2186
|
+
workflowState: "idle",
|
|
2187
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2188
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2189
|
+
testsDir: "tests",
|
|
2190
|
+
srcDir: ["src"],
|
|
2191
|
+
contextFilledCells: 0,
|
|
2192
|
+
et: "⏱︎ --:-- ⚑ 0:00 ⌛︎0:00",
|
|
2193
|
+
}),
|
|
2194
|
+
);
|
|
2195
|
+
} finally {
|
|
2196
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2197
|
+
}
|
|
2198
|
+
});
|
|
2199
|
+
|
|
2200
|
+
test("prompt commands abort when git validation fails", async () => {
|
|
2201
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2202
|
+
const previousCwd = process.cwd();
|
|
2203
|
+
try {
|
|
2204
|
+
fs.writeFileSync(path.join(projectBase, "DIRTY.txt"), "dirty\n", "utf8");
|
|
2205
|
+
const pi = createFakePi();
|
|
2206
|
+
piUsereqExtension(pi);
|
|
2207
|
+
const ctx = createFakeCtx(projectBase);
|
|
2208
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2209
|
+
|
|
2210
|
+
await assert.rejects(
|
|
2211
|
+
pi.commands.get("req-analyze")!.handler("Inspect src/index.ts", ctx),
|
|
2212
|
+
/ERROR: Git status unclear!/,
|
|
2213
|
+
);
|
|
2214
|
+
assert.equal(pi.sentUserMessages.length, 0);
|
|
2215
|
+
assert.ok(ctx.__state.notifications.some((entry) => /ERROR: Git status unclear!/.test(entry.message)));
|
|
2216
|
+
assert.equal(
|
|
2217
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2218
|
+
buildExpectedFakeStatusText({
|
|
2219
|
+
workflowState: "error",
|
|
2220
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2221
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2222
|
+
testsDir: "tests",
|
|
2223
|
+
srcDir: ["src"],
|
|
2224
|
+
contextFilledCells: 0,
|
|
2225
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2226
|
+
}),
|
|
2227
|
+
);
|
|
2228
|
+
} finally {
|
|
2229
|
+
process.chdir(previousCwd);
|
|
2230
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2231
|
+
}
|
|
2232
|
+
});
|
|
2233
|
+
|
|
2234
|
+
test("prompt commands enforce the documented required-doc matrix", async () => {
|
|
2235
|
+
const { projectBase } = initFixtureRepo({
|
|
2236
|
+
fixtures: [],
|
|
2237
|
+
docs: {
|
|
2238
|
+
"REQUIREMENTS.md": "# Requirements\n",
|
|
2239
|
+
},
|
|
2240
|
+
});
|
|
2241
|
+
const previousCwd = process.cwd();
|
|
2242
|
+
try {
|
|
2243
|
+
const worktreeParentPath = path.dirname(projectBase);
|
|
2244
|
+
const initialEntries = new Set(fs.readdirSync(worktreeParentPath));
|
|
2245
|
+
const pi = createFakePi();
|
|
2246
|
+
piUsereqExtension(pi);
|
|
2247
|
+
const ctx = createFakeCtx(projectBase);
|
|
2248
|
+
|
|
2249
|
+
await assert.rejects(
|
|
2250
|
+
pi.commands.get("req-change")!.handler("Adjust docs", ctx),
|
|
2251
|
+
/WORKFLOW\.md/,
|
|
2252
|
+
);
|
|
2253
|
+
const createdWorktreeEntries = fs.readdirSync(worktreeParentPath)
|
|
2254
|
+
.filter((entry) => !initialEntries.has(entry))
|
|
2255
|
+
.filter((entry) => /^PI-useReq-/.test(entry));
|
|
2256
|
+
assert.equal(pi.sentUserMessages.length, 0);
|
|
2257
|
+
assert.deepEqual(createdWorktreeEntries, []);
|
|
2258
|
+
assert.ok(ctx.__state.notifications.some((entry) => /WORKFLOW\.md/.test(entry.message)));
|
|
2259
|
+
} finally {
|
|
2260
|
+
process.chdir(previousCwd);
|
|
2261
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2262
|
+
}
|
|
2263
|
+
});
|
|
2264
|
+
|
|
2265
|
+
test("worktree-enabled prompt commands merge successful runs, restore base-path, and delete the worktree", async () => {
|
|
2266
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2267
|
+
const previousCwd = process.cwd();
|
|
2268
|
+
try {
|
|
2269
|
+
const pi = createFakePi();
|
|
2270
|
+
piUsereqExtension(pi);
|
|
2271
|
+
const ctx = createFakeCtx(projectBase);
|
|
2272
|
+
const recordedStatuses: string[] = [];
|
|
2273
|
+
const originalSetStatus = ctx.ui.setStatus.bind(ctx.ui);
|
|
2274
|
+
ctx.ui.setStatus = (key: string, value?: string) => {
|
|
2275
|
+
if (key === "pi-usereq" && value !== undefined) {
|
|
2276
|
+
recordedStatuses.push(value);
|
|
2277
|
+
}
|
|
2278
|
+
originalSetStatus(key, value);
|
|
2279
|
+
};
|
|
2280
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2281
|
+
|
|
2282
|
+
const originalBranchName = buildExpectedFakeBranchValue(projectBase);
|
|
2283
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2284
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2285
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2286
|
+
assert.ok(worktreeMatch, promptText);
|
|
2287
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
2288
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2289
|
+
assert.match(worktreeName, /^PI-useReq-/);
|
|
2290
|
+
assert.ok(fs.existsSync(executionBasePath));
|
|
2291
|
+
assert.equal(
|
|
2292
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
2293
|
+
cwd: projectBase,
|
|
2294
|
+
encoding: "utf8",
|
|
2295
|
+
}).status,
|
|
2296
|
+
0,
|
|
2297
|
+
);
|
|
2298
|
+
assert.equal(process.cwd(), executionBasePath);
|
|
2299
|
+
assert.equal(ctx.cwd, executionBasePath);
|
|
2300
|
+
assert.ok(recordedStatuses.some((status) => /<accent>status:<\/accent><warning>running<\/warning>/.test(status)));
|
|
2301
|
+
assert.ok(recordedStatuses.some((status) => status.includes(`${formatFakeThemeForeground("accent", "branch:")}${formatFakeThemeForeground("warning", worktreeName)}`)));
|
|
2302
|
+
|
|
2303
|
+
await pi.emit("before_agent_start", {}, ctx);
|
|
2304
|
+
assert.equal(process.cwd(), executionBasePath);
|
|
2305
|
+
const filesTokensTool = pi.tools.find((candidate: RegisteredTool) => candidate.name === "files-tokens");
|
|
2306
|
+
assert.ok(filesTokensTool?.execute, "missing files-tokens execute handler");
|
|
2307
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
2308
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "tool-relative.ts"), "export const TOOL_RELATIVE = 1;\n", "utf8");
|
|
2309
|
+
const toolResult = await filesTokensTool.execute!("tool-call-id", { files: ["src/tool-relative.ts"] }, undefined, undefined, ctx) as {
|
|
2310
|
+
content?: Array<{ type?: string; text?: string }>;
|
|
2311
|
+
};
|
|
2312
|
+
assert.match(toolResult.content?.[0]?.text ?? "", /tool-relative\.ts/);
|
|
2313
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2314
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "orchestrated.ts"), "export const ORCHESTRATED = 1;\n", "utf8");
|
|
2315
|
+
assert.equal(spawnSync("git", ["add", "src/orchestrated.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
2316
|
+
const commit = spawnSync("git", ["commit", "-m", "worktree change"], { cwd: executionBasePath, encoding: "utf8" });
|
|
2317
|
+
assert.equal(commit.status, 0, commit.stderr);
|
|
2318
|
+
|
|
2319
|
+
await pi.emit("agent_end", {
|
|
2320
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2321
|
+
}, ctx);
|
|
2322
|
+
|
|
2323
|
+
assert.equal(process.cwd(), projectBase);
|
|
2324
|
+
assert.equal(ctx.cwd, projectBase);
|
|
2325
|
+
assert.ok(fs.existsSync(path.join(projectBase, "src", "orchestrated.ts")));
|
|
2326
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
2327
|
+
assert.notEqual(
|
|
2328
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
2329
|
+
cwd: projectBase,
|
|
2330
|
+
encoding: "utf8",
|
|
2331
|
+
}).status,
|
|
2332
|
+
0,
|
|
2333
|
+
);
|
|
2334
|
+
assert.equal(ctx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
|
|
2335
|
+
assert.ok(recordedStatuses.some((status) => /<accent>status:<\/accent><warning>merging<\/warning>/.test(status)));
|
|
2336
|
+
assert.ok(recordedStatuses.some((status) => status.includes(`${formatFakeThemeForeground("accent", "branch:")}${formatFakeThemeForeground("warning", originalBranchName)}`)));
|
|
2337
|
+
const finalStatus = ctx.__state.statuses.get("pi-usereq") ?? "";
|
|
2338
|
+
assert.match(finalStatus, /<accent>status:<\/accent><warning>idle<\/warning>/);
|
|
2339
|
+
assert.ok(finalStatus.includes(`${formatFakeThemeForeground("accent", "branch:")}${formatFakeThemeForeground("warning", originalBranchName)}`));
|
|
2340
|
+
} finally {
|
|
2341
|
+
process.chdir(previousCwd);
|
|
2342
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2343
|
+
}
|
|
2344
|
+
});
|
|
2345
|
+
|
|
2346
|
+
/**
|
|
2347
|
+
* @brief Verifies prompt-command bootstrap ignores stale deleted-worktree session metadata on later commands.
|
|
2348
|
+
* @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.
|
|
2349
|
+
* @return {Promise<void>} Promise resolved after two successful prompt-command runs complete.
|
|
2350
|
+
* @throws {AssertionError} Throws when the second command aborts with the canonical git-status preflight error or reuses the deleted worktree context.
|
|
2351
|
+
* @satisfies REQ-200, REQ-206, REQ-208, REQ-219, REQ-257
|
|
2352
|
+
*/
|
|
2353
|
+
test("prompt-command bootstrap ignores stale deleted-worktree session metadata on later commands", async () => {
|
|
2354
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2355
|
+
const previousCwd = process.cwd();
|
|
2356
|
+
try {
|
|
2357
|
+
const pi = createFakePi();
|
|
2358
|
+
piUsereqExtension(pi);
|
|
2359
|
+
const ctx = createFakeCtx(projectBase);
|
|
2360
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2361
|
+
|
|
2362
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2363
|
+
const firstPromptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2364
|
+
const firstWorktreeMatch = firstPromptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2365
|
+
assert.ok(firstWorktreeMatch, firstPromptText);
|
|
2366
|
+
const firstWorktreeName = firstWorktreeMatch?.[1] ?? "";
|
|
2367
|
+
const firstExecutionBasePath = firstWorktreeMatch?.[2] ?? "";
|
|
2368
|
+
const firstExecutionSessionFile = ctx.sessionManager.getSessionFile() ?? "";
|
|
2369
|
+
assert.notEqual(firstExecutionSessionFile, "");
|
|
2370
|
+
|
|
2371
|
+
await pi.emit("before_agent_start", {}, ctx);
|
|
2372
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2373
|
+
fs.mkdirSync(path.join(firstExecutionBasePath, "src"), { recursive: true });
|
|
2374
|
+
fs.writeFileSync(path.join(firstExecutionBasePath, "src", "bootstrap-first.ts"), "export const BOOTSTRAP_FIRST = 1;\n", "utf8");
|
|
2375
|
+
assert.equal(
|
|
2376
|
+
spawnSync("git", ["add", "src/bootstrap-first.ts"], {
|
|
2377
|
+
cwd: firstExecutionBasePath,
|
|
2378
|
+
encoding: "utf8",
|
|
2379
|
+
}).status,
|
|
2380
|
+
0,
|
|
2381
|
+
);
|
|
2382
|
+
const firstCommit = spawnSync("git", ["commit", "-m", "bootstrap first"], {
|
|
2383
|
+
cwd: firstExecutionBasePath,
|
|
2384
|
+
encoding: "utf8",
|
|
2385
|
+
});
|
|
2386
|
+
assert.equal(firstCommit.status, 0, firstCommit.stderr);
|
|
2387
|
+
|
|
2388
|
+
await pi.emit("agent_end", {
|
|
2389
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2390
|
+
}, ctx);
|
|
2391
|
+
|
|
2392
|
+
assert.equal(process.cwd(), projectBase);
|
|
2393
|
+
assert.equal(ctx.cwd, projectBase);
|
|
2394
|
+
assert.equal(fs.existsSync(firstExecutionBasePath), false);
|
|
2395
|
+
assert.notEqual(
|
|
2396
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${firstWorktreeName}`], {
|
|
2397
|
+
cwd: projectBase,
|
|
2398
|
+
encoding: "utf8",
|
|
2399
|
+
}).status,
|
|
2400
|
+
0,
|
|
2401
|
+
);
|
|
2402
|
+
|
|
2403
|
+
ctx.cwd = firstExecutionBasePath;
|
|
2404
|
+
ctx.sessionManager.getCwd = () => firstExecutionBasePath;
|
|
2405
|
+
ctx.sessionManager.getSessionFile = () => firstExecutionSessionFile;
|
|
2406
|
+
ctx.sessionManager.getSessionDir = () => path.dirname(firstExecutionSessionFile);
|
|
2407
|
+
|
|
2408
|
+
await pi.commands.get("req-change")!.handler("Adjust docs again", ctx);
|
|
2409
|
+
const secondPromptText = String(pi.sentUserMessages[1]?.content ?? "");
|
|
2410
|
+
const secondWorktreeMatch = secondPromptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2411
|
+
assert.ok(secondWorktreeMatch, secondPromptText);
|
|
2412
|
+
const secondWorktreeName = secondWorktreeMatch?.[1] ?? "";
|
|
2413
|
+
const secondExecutionBasePath = secondWorktreeMatch?.[2] ?? "";
|
|
2414
|
+
|
|
2415
|
+
assert.notEqual(secondWorktreeName, firstWorktreeName);
|
|
2416
|
+
assert.notEqual(secondExecutionBasePath, firstExecutionBasePath);
|
|
2417
|
+
assert.equal(process.cwd(), secondExecutionBasePath);
|
|
2418
|
+
assert.equal(ctx.cwd, secondExecutionBasePath);
|
|
2419
|
+
|
|
2420
|
+
await pi.emit("before_agent_start", {}, ctx);
|
|
2421
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2422
|
+
fs.mkdirSync(path.join(secondExecutionBasePath, "src"), { recursive: true });
|
|
2423
|
+
fs.writeFileSync(path.join(secondExecutionBasePath, "src", "bootstrap-second.ts"), "export const BOOTSTRAP_SECOND = 1;\n", "utf8");
|
|
2424
|
+
assert.equal(
|
|
2425
|
+
spawnSync("git", ["add", "src/bootstrap-second.ts"], {
|
|
2426
|
+
cwd: secondExecutionBasePath,
|
|
2427
|
+
encoding: "utf8",
|
|
2428
|
+
}).status,
|
|
2429
|
+
0,
|
|
2430
|
+
);
|
|
2431
|
+
const secondCommit = spawnSync("git", ["commit", "-m", "bootstrap second"], {
|
|
2432
|
+
cwd: secondExecutionBasePath,
|
|
2433
|
+
encoding: "utf8",
|
|
2434
|
+
});
|
|
2435
|
+
assert.equal(secondCommit.status, 0, secondCommit.stderr);
|
|
2436
|
+
|
|
2437
|
+
await pi.emit("agent_end", {
|
|
2438
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2439
|
+
}, ctx);
|
|
2440
|
+
|
|
2441
|
+
assert.equal(process.cwd(), projectBase);
|
|
2442
|
+
assert.equal(ctx.cwd, projectBase);
|
|
2443
|
+
assert.equal(fs.existsSync(secondExecutionBasePath), false);
|
|
2444
|
+
assert.ok(fs.existsSync(path.join(projectBase, "src", "bootstrap-first.ts")));
|
|
2445
|
+
assert.ok(fs.existsSync(path.join(projectBase, "src", "bootstrap-second.ts")));
|
|
2446
|
+
assert.notEqual(
|
|
2447
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${secondWorktreeName}`], {
|
|
2448
|
+
cwd: projectBase,
|
|
2449
|
+
encoding: "utf8",
|
|
2450
|
+
}).status,
|
|
2451
|
+
0,
|
|
2452
|
+
);
|
|
2453
|
+
assert.equal(ctx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
|
|
2454
|
+
} finally {
|
|
2455
|
+
process.chdir(previousCwd);
|
|
2456
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2457
|
+
}
|
|
2458
|
+
});
|
|
2459
|
+
|
|
2460
|
+
test("worktree-backed finalization transitions through merging, error, and idle when merge fails", async () => {
|
|
2461
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2462
|
+
const previousCwd = process.cwd();
|
|
2463
|
+
try {
|
|
2464
|
+
const pi = createFakePi();
|
|
2465
|
+
piUsereqExtension(pi);
|
|
2466
|
+
const ctx = createFakeCtx(projectBase);
|
|
2467
|
+
const recordedStatuses: string[] = [];
|
|
2468
|
+
const originalSetStatus = ctx.ui.setStatus.bind(ctx.ui);
|
|
2469
|
+
ctx.ui.setStatus = (key: string, value?: string) => {
|
|
2470
|
+
if (key === "pi-usereq" && value !== undefined) {
|
|
2471
|
+
recordedStatuses.push(value);
|
|
2472
|
+
}
|
|
2473
|
+
originalSetStatus(key, value);
|
|
2474
|
+
};
|
|
2475
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2476
|
+
|
|
2477
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2478
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2479
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2480
|
+
assert.ok(worktreeMatch, promptText);
|
|
2481
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
2482
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2483
|
+
|
|
2484
|
+
await pi.emit("before_agent_start", {}, ctx);
|
|
2485
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2486
|
+
fs.writeFileSync(path.join(projectBase, "divergent.txt"), "root\n", "utf8");
|
|
2487
|
+
assert.equal(spawnSync("git", ["add", "divergent.txt"], { cwd: projectBase, encoding: "utf8" }).status, 0);
|
|
2488
|
+
const rootCommit = spawnSync("git", ["commit", "-m", "root change"], { cwd: projectBase, encoding: "utf8" });
|
|
2489
|
+
assert.equal(rootCommit.status, 0, rootCommit.stderr);
|
|
2490
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
2491
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "merge-failed.ts"), "export const MERGE_FAILED = 1;\n", "utf8");
|
|
2492
|
+
assert.equal(spawnSync("git", ["add", "src/merge-failed.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
2493
|
+
const worktreeCommit = spawnSync("git", ["commit", "-m", "worktree change"], { cwd: executionBasePath, encoding: "utf8" });
|
|
2494
|
+
assert.equal(worktreeCommit.status, 0, worktreeCommit.stderr);
|
|
2495
|
+
|
|
2496
|
+
await pi.emit("agent_end", {
|
|
2497
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2498
|
+
}, ctx);
|
|
2499
|
+
|
|
2500
|
+
assert.equal(process.cwd(), projectBase);
|
|
2501
|
+
assert.equal(ctx.cwd, projectBase);
|
|
2502
|
+
assert.equal(fs.existsSync(executionBasePath), true);
|
|
2503
|
+
assert.equal(fs.existsSync(path.join(projectBase, "src", "merge-failed.ts")), false);
|
|
2504
|
+
assert.ok(fs.existsSync(path.join(projectBase, "divergent.txt")));
|
|
2505
|
+
assert.equal(
|
|
2506
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
2507
|
+
cwd: projectBase,
|
|
2508
|
+
encoding: "utf8",
|
|
2509
|
+
}).status,
|
|
2510
|
+
0,
|
|
2511
|
+
);
|
|
2512
|
+
assert.ok(recordedStatuses.some((status) => /<accent>status:<\/accent><warning>merging<\/warning>/.test(status)));
|
|
2513
|
+
assert.ok(recordedStatuses.some((status) => /<accent>status:<\/accent><error>\u001b\[5merror\u001b\[25m<\/error>/.test(status)));
|
|
2514
|
+
assert.match(ctx.__state.statuses.get("pi-usereq") ?? "", /<accent>status:<\/accent><warning>idle<\/warning>/);
|
|
2515
|
+
assert.ok(ctx.__state.notifications.some((entry) => /Fast-forward merge failed/.test(entry.message)));
|
|
2516
|
+
} finally {
|
|
2517
|
+
process.chdir(previousCwd);
|
|
2518
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2519
|
+
}
|
|
2520
|
+
});
|
|
2521
|
+
|
|
2522
|
+
test("worktree-enabled prompt commands honor overridden worktree prefixes", async () => {
|
|
2523
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2524
|
+
const previousCwd = process.cwd();
|
|
2525
|
+
try {
|
|
2526
|
+
writeProjectConfigOverrides(projectBase, { GIT_WORKTREE_PREFIX: "custom-prefix-" });
|
|
2527
|
+
const pi = createFakePi();
|
|
2528
|
+
piUsereqExtension(pi);
|
|
2529
|
+
const ctx = createFakeCtx(projectBase);
|
|
2530
|
+
|
|
2531
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2532
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2533
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2534
|
+
assert.ok(worktreeMatch, promptText);
|
|
2535
|
+
assert.match(worktreeMatch?.[1] ?? "", /^custom-prefix-/);
|
|
2536
|
+
} finally {
|
|
2537
|
+
process.chdir(previousCwd);
|
|
2538
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2539
|
+
}
|
|
2540
|
+
});
|
|
2541
|
+
|
|
2542
|
+
test("worktree-backed prompt failures skip merge, restore base-path, and retain worktree", async () => {
|
|
2543
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2544
|
+
const previousCwd = process.cwd();
|
|
2545
|
+
try {
|
|
2546
|
+
const pi = createFakePi();
|
|
2547
|
+
piUsereqExtension(pi);
|
|
2548
|
+
const ctx = createFakeCtx(projectBase);
|
|
2549
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2550
|
+
|
|
2551
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2552
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2553
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2554
|
+
assert.ok(worktreeMatch, promptText);
|
|
2555
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
2556
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2557
|
+
|
|
2558
|
+
await pi.emit("before_agent_start", {}, ctx);
|
|
2559
|
+
assert.equal(process.cwd(), executionBasePath);
|
|
2560
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2561
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
2562
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "failed.ts"), "export const FAILED = 1;\n", "utf8");
|
|
2563
|
+
await pi.emit("agent_end", {
|
|
2564
|
+
messages: [{ role: "assistant", stopReason: "error", content: [] }],
|
|
2565
|
+
}, ctx);
|
|
2566
|
+
|
|
2567
|
+
assert.equal(process.cwd(), projectBase);
|
|
2568
|
+
assert.equal(ctx.cwd, projectBase);
|
|
2569
|
+
assert.equal(fs.existsSync(executionBasePath), true);
|
|
2570
|
+
assert.equal(fs.existsSync(path.join(projectBase, "src", "failed.ts")), false);
|
|
2571
|
+
assert.equal(
|
|
2572
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
2573
|
+
cwd: projectBase,
|
|
2574
|
+
encoding: "utf8",
|
|
2575
|
+
}).status,
|
|
2576
|
+
0,
|
|
2577
|
+
);
|
|
2578
|
+
assert.ok(ctx.__state.notifications.some((entry) => entry.level === "error"
|
|
2579
|
+
&& /Prompt closure retained worktree .* after failed outcome\./.test(entry.message)));
|
|
2580
|
+
assert.equal(
|
|
2581
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2582
|
+
buildExpectedFakeStatusText({
|
|
2583
|
+
workflowState: "idle",
|
|
2584
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2585
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2586
|
+
testsDir: "tests",
|
|
2587
|
+
srcDir: ["src"],
|
|
2588
|
+
contextFilledCells: 0,
|
|
2589
|
+
et: "⏱︎ --:-- ⚑ 0:00 ⌛︎0:00",
|
|
2590
|
+
}),
|
|
2591
|
+
);
|
|
2592
|
+
} finally {
|
|
2593
|
+
process.chdir(previousCwd);
|
|
2594
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2595
|
+
}
|
|
2596
|
+
});
|
|
2597
|
+
|
|
2598
|
+
test("worktree-backed prompt commands use replacement-session callbacks for prompt dispatch and restore base-path after merge", async () => {
|
|
2599
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2600
|
+
const previousCwd = process.cwd();
|
|
2601
|
+
try {
|
|
2602
|
+
const pi = createFakePi();
|
|
2603
|
+
piUsereqExtension(pi);
|
|
2604
|
+
const ctx = createFakeCtx(projectBase);
|
|
2605
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2606
|
+
|
|
2607
|
+
const originalSessionFile = ctx.sessionManager.getSessionFile();
|
|
2608
|
+
const originalSwitchSession = ctx.switchSession.bind(ctx);
|
|
2609
|
+
let activeSessionCtx: any = ctx;
|
|
2610
|
+
ctx.switchSession = async (
|
|
2611
|
+
sessionPath: string,
|
|
2612
|
+
options?: { withSession?: (replacementCtx: any) => Promise<void> },
|
|
2613
|
+
) => {
|
|
2614
|
+
const result = await originalSwitchSession(sessionPath);
|
|
2615
|
+
const replacementCwd = readFakeSessionFileCwd(sessionPath, projectBase);
|
|
2616
|
+
const replacementCtx = {
|
|
2617
|
+
...ctx,
|
|
2618
|
+
cwd: replacementCwd,
|
|
2619
|
+
sessionManager: {
|
|
2620
|
+
...ctx.sessionManager,
|
|
2621
|
+
getCwd: () => replacementCwd,
|
|
2622
|
+
getSessionFile: () => sessionPath,
|
|
2623
|
+
},
|
|
2624
|
+
async sendUserMessage(content: unknown, sendOptions?: unknown) {
|
|
2625
|
+
pi.sentUserMessages.push({ content, options: sendOptions });
|
|
2626
|
+
},
|
|
2627
|
+
};
|
|
2628
|
+
activeSessionCtx = replacementCtx;
|
|
2629
|
+
if (options?.withSession) {
|
|
2630
|
+
process.chdir(replacementCwd);
|
|
2631
|
+
await options.withSession(replacementCtx);
|
|
2632
|
+
ctx.cwd = projectBase;
|
|
2633
|
+
ctx.sessionManager.getSessionFile = () => originalSessionFile;
|
|
2634
|
+
ctx.sessionManager.getCwd = () => projectBase;
|
|
2635
|
+
return result;
|
|
2636
|
+
}
|
|
2637
|
+
ctx.cwd = projectBase;
|
|
2638
|
+
ctx.sessionManager.getSessionFile = () => originalSessionFile;
|
|
2639
|
+
ctx.sessionManager.getCwd = () => projectBase;
|
|
2640
|
+
process.chdir(projectBase);
|
|
2641
|
+
return result;
|
|
2642
|
+
};
|
|
2643
|
+
|
|
2644
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2645
|
+
|
|
2646
|
+
assert.equal(pi.sentUserMessages.length, 1);
|
|
2647
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2648
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2649
|
+
assert.ok(worktreeMatch, promptText);
|
|
2650
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
2651
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2652
|
+
assert.equal(activeSessionCtx.cwd, executionBasePath);
|
|
2653
|
+
assert.equal(process.cwd(), executionBasePath);
|
|
2654
|
+
assert.equal(fs.existsSync(executionBasePath), true);
|
|
2655
|
+
|
|
2656
|
+
await pi.emit("before_agent_start", {}, activeSessionCtx);
|
|
2657
|
+
await pi.emit("agent_start", {}, activeSessionCtx);
|
|
2658
|
+
|
|
2659
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
2660
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "with-session.ts"), "export const WITH_SESSION = 1;\n", "utf8");
|
|
2661
|
+
assert.equal(spawnSync("git", ["add", "src/with-session.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
2662
|
+
const worktreeCommit = spawnSync("git", ["commit", "-m", "with session"], {
|
|
2663
|
+
cwd: executionBasePath,
|
|
2664
|
+
encoding: "utf8",
|
|
2665
|
+
});
|
|
2666
|
+
assert.equal(worktreeCommit.status, 0, worktreeCommit.stderr);
|
|
2667
|
+
|
|
2668
|
+
await pi.emit("agent_end", {
|
|
2669
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2670
|
+
}, activeSessionCtx);
|
|
2671
|
+
|
|
2672
|
+
assert.equal(process.cwd(), projectBase);
|
|
2673
|
+
assert.equal(activeSessionCtx.cwd, projectBase);
|
|
2674
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
2675
|
+
assert.equal(fs.existsSync(path.join(projectBase, "src", "with-session.ts")), true);
|
|
2676
|
+
assert.notEqual(
|
|
2677
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
2678
|
+
cwd: projectBase,
|
|
2679
|
+
encoding: "utf8",
|
|
2680
|
+
}).status,
|
|
2681
|
+
0,
|
|
2682
|
+
);
|
|
2683
|
+
} finally {
|
|
2684
|
+
process.chdir(previousCwd);
|
|
2685
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2686
|
+
}
|
|
2687
|
+
});
|
|
2688
|
+
|
|
2689
|
+
/**
|
|
2690
|
+
* @brief Verifies worktree-backed prompt orchestration against the pi runtime session-switch contract.
|
|
2691
|
+
* @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.
|
|
2692
|
+
* @return {Promise<void>} Promise resolved after prompt delivery, merge handling, and restored-base assertions complete.
|
|
2693
|
+
* @throws {AssertionError} Throws when prompt activation or prompt-end closure fails to synchronize `process.cwd()` with the expected session path.
|
|
2694
|
+
* @satisfies REQ-068, REQ-208, REQ-257, REQ-271, REQ-272
|
|
2695
|
+
*/
|
|
2696
|
+
test("worktree-backed prompt commands restore base-path when switchSession does not change process cwd", async () => {
|
|
2697
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2698
|
+
const previousCwd = process.cwd();
|
|
2699
|
+
try {
|
|
2700
|
+
process.chdir(projectBase);
|
|
2701
|
+
const pi = createFakePi();
|
|
2702
|
+
piUsereqExtension(pi);
|
|
2703
|
+
const ctx = createFakeCtx(projectBase);
|
|
2704
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2705
|
+
|
|
2706
|
+
let activeSessionCtx: any = ctx;
|
|
2707
|
+
ctx.switchSession = async (
|
|
2708
|
+
sessionPath: string,
|
|
2709
|
+
options?: { withSession?: (replacementCtx: any) => Promise<void> },
|
|
2710
|
+
) => {
|
|
2711
|
+
const replacementCwd = readFakeSessionFileCwd(sessionPath, projectBase);
|
|
2712
|
+
const replacementCtx = {
|
|
2713
|
+
...ctx,
|
|
2714
|
+
cwd: replacementCwd,
|
|
2715
|
+
sessionManager: {
|
|
2716
|
+
...ctx.sessionManager,
|
|
2717
|
+
getCwd: () => replacementCwd,
|
|
2718
|
+
getSessionFile: () => sessionPath,
|
|
2719
|
+
},
|
|
2720
|
+
switchSession: ctx.switchSession,
|
|
2721
|
+
async sendUserMessage(content: unknown, sendOptions?: unknown) {
|
|
2722
|
+
pi.sentUserMessages.push({ content, options: sendOptions });
|
|
2723
|
+
},
|
|
2724
|
+
};
|
|
2725
|
+
activeSessionCtx = replacementCtx;
|
|
2726
|
+
await options?.withSession?.(replacementCtx);
|
|
2727
|
+
return { cancelled: false };
|
|
2728
|
+
};
|
|
2729
|
+
|
|
2730
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2731
|
+
|
|
2732
|
+
assert.equal(pi.sentUserMessages.length, 1);
|
|
2733
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2734
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2735
|
+
assert.ok(worktreeMatch, promptText);
|
|
2736
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
2737
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2738
|
+
assert.equal(activeSessionCtx.cwd, executionBasePath);
|
|
2739
|
+
assert.equal(activeSessionCtx.sessionManager.getCwd(), executionBasePath);
|
|
2740
|
+
assert.equal(ctx.cwd, projectBase);
|
|
2741
|
+
assert.equal(process.cwd(), executionBasePath);
|
|
2742
|
+
|
|
2743
|
+
await pi.emit("before_agent_start", {}, activeSessionCtx);
|
|
2744
|
+
await pi.emit("agent_start", {}, activeSessionCtx);
|
|
2745
|
+
|
|
2746
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
2747
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "runtime-anchor.ts"), "export const RUNTIME_ANCHOR = 1;\n", "utf8");
|
|
2748
|
+
assert.equal(
|
|
2749
|
+
spawnSync("git", ["add", "src/runtime-anchor.ts"], {
|
|
2750
|
+
cwd: executionBasePath,
|
|
2751
|
+
encoding: "utf8",
|
|
2752
|
+
}).status,
|
|
2753
|
+
0,
|
|
2754
|
+
);
|
|
2755
|
+
const worktreeCommit = spawnSync("git", ["commit", "-m", "runtime anchor"], {
|
|
2756
|
+
cwd: executionBasePath,
|
|
2757
|
+
encoding: "utf8",
|
|
2758
|
+
});
|
|
2759
|
+
assert.equal(worktreeCommit.status, 0, worktreeCommit.stderr);
|
|
2760
|
+
|
|
2761
|
+
await pi.emit("agent_end", {
|
|
2762
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2763
|
+
}, activeSessionCtx);
|
|
2764
|
+
|
|
2765
|
+
assert.equal(process.cwd(), projectBase);
|
|
2766
|
+
assert.equal(activeSessionCtx.cwd, projectBase);
|
|
2767
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
2768
|
+
assert.equal(fs.existsSync(path.join(projectBase, "src", "runtime-anchor.ts")), true);
|
|
2769
|
+
assert.notEqual(
|
|
2770
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
2771
|
+
cwd: projectBase,
|
|
2772
|
+
encoding: "utf8",
|
|
2773
|
+
}).status,
|
|
2774
|
+
0,
|
|
2775
|
+
);
|
|
2776
|
+
} finally {
|
|
2777
|
+
process.chdir(previousCwd);
|
|
2778
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2779
|
+
}
|
|
2780
|
+
});
|
|
2781
|
+
|
|
2782
|
+
/**
|
|
2783
|
+
* @brief Verifies restored base-path closure tolerates stale `ctx.cwd` mutations after a successful session switch.
|
|
2784
|
+
* @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.
|
|
2785
|
+
* @return {Promise<void>} Promise resolved after restored-base assertions and cleanup complete.
|
|
2786
|
+
* @throws {AssertionError} Throws when the stale post-restore `cwd` mutation still aborts restoration.
|
|
2787
|
+
* @satisfies REQ-208, REQ-257, REQ-276, REQ-280, REQ-282
|
|
2788
|
+
*/
|
|
2789
|
+
test("restorePromptCommandExecution ignores stale ctx.cwd mutations after a verified restore switch", async () => {
|
|
2790
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2791
|
+
const previousCwd = process.cwd();
|
|
2792
|
+
let executionPlan: ReturnType<typeof preparePromptCommandExecution> | undefined;
|
|
2793
|
+
try {
|
|
2794
|
+
const config = loadConfig(projectBase);
|
|
2795
|
+
const ctx = createFakeCtx(projectBase);
|
|
2796
|
+
executionPlan = preparePromptCommandExecution(
|
|
2797
|
+
"change",
|
|
2798
|
+
"Adjust docs",
|
|
2799
|
+
projectBase,
|
|
2800
|
+
config,
|
|
2801
|
+
ctx.sessionManager.getSessionFile(),
|
|
2802
|
+
ctx.sessionManager.getSessionDir?.(),
|
|
2803
|
+
ctx.sessionManager.getBranch?.(),
|
|
2804
|
+
);
|
|
2805
|
+
const activeContext = await activatePromptCommandExecution(executionPlan, ctx);
|
|
2806
|
+
assert.ok(activeContext);
|
|
2807
|
+
let activeContextCwd = activeContext?.cwd;
|
|
2808
|
+
let staleAfterSwitch = false;
|
|
2809
|
+
Object.defineProperty(activeContext!, "cwd", {
|
|
2810
|
+
configurable: true,
|
|
2811
|
+
get() {
|
|
2812
|
+
return activeContextCwd;
|
|
2813
|
+
},
|
|
2814
|
+
set(value: string) {
|
|
2815
|
+
if (staleAfterSwitch) {
|
|
2816
|
+
throw new Error("This extension ctx is stale after session replacement or reload.");
|
|
2817
|
+
}
|
|
2818
|
+
activeContextCwd = value;
|
|
2819
|
+
},
|
|
2820
|
+
});
|
|
2821
|
+
activeContext!.switchSession = async (sessionPath: string) => {
|
|
2822
|
+
activeContextCwd = projectBase;
|
|
2823
|
+
activeContext!.sessionManager.getSessionFile = () => sessionPath;
|
|
2824
|
+
activeContext!.sessionManager.getSessionDir = () => path.dirname(sessionPath);
|
|
2825
|
+
activeContext!.sessionManager.getCwd = () => projectBase;
|
|
2826
|
+
process.chdir(projectBase);
|
|
2827
|
+
staleAfterSwitch = true;
|
|
2828
|
+
return { cancelled: false };
|
|
2829
|
+
};
|
|
2830
|
+
|
|
2831
|
+
const restoredContext = await restorePromptCommandExecution(executionPlan, activeContext, {
|
|
2832
|
+
config,
|
|
2833
|
+
workflowState: "merging",
|
|
2834
|
+
});
|
|
2835
|
+
|
|
2836
|
+
assert.equal(process.cwd(), projectBase);
|
|
2837
|
+
assert.equal(restoredContext, activeContext);
|
|
2838
|
+
assert.equal(fs.existsSync(executionPlan.worktreePath ?? ""), true);
|
|
2839
|
+
} finally {
|
|
2840
|
+
if (executionPlan) {
|
|
2841
|
+
await abortPromptCommandExecution(executionPlan);
|
|
2842
|
+
}
|
|
2843
|
+
process.chdir(previousCwd);
|
|
2844
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2845
|
+
}
|
|
2846
|
+
});
|
|
2847
|
+
|
|
2848
|
+
/**
|
|
2849
|
+
* @brief Verifies restored base-path closure tolerates getter-only `ctx.cwd` mirrors after a successful session switch.
|
|
2850
|
+
* @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.
|
|
2851
|
+
* @return {Promise<void>} Promise resolved after restored-base assertions and cleanup complete.
|
|
2852
|
+
* @throws {AssertionError} Throws when a getter-only `cwd` property still aborts restoration.
|
|
2853
|
+
* @satisfies REQ-208, REQ-257, REQ-276, REQ-282
|
|
2854
|
+
*/
|
|
2855
|
+
test("restorePromptCommandExecution ignores getter-only ctx.cwd mirrors after a verified restore switch", async () => {
|
|
2856
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2857
|
+
const previousCwd = process.cwd();
|
|
2858
|
+
let executionPlan: ReturnType<typeof preparePromptCommandExecution> | undefined;
|
|
2859
|
+
try {
|
|
2860
|
+
const config = loadConfig(projectBase);
|
|
2861
|
+
const ctx = createFakeCtx(projectBase);
|
|
2862
|
+
executionPlan = preparePromptCommandExecution(
|
|
2863
|
+
"change",
|
|
2864
|
+
"Adjust docs",
|
|
2865
|
+
projectBase,
|
|
2866
|
+
config,
|
|
2867
|
+
ctx.sessionManager.getSessionFile(),
|
|
2868
|
+
ctx.sessionManager.getSessionDir?.(),
|
|
2869
|
+
ctx.sessionManager.getBranch?.(),
|
|
2870
|
+
);
|
|
2871
|
+
const activeContext = await activatePromptCommandExecution(executionPlan, ctx);
|
|
2872
|
+
assert.ok(activeContext);
|
|
2873
|
+
let activeContextCwd = activeContext?.cwd;
|
|
2874
|
+
Object.defineProperty(activeContext!, "cwd", {
|
|
2875
|
+
configurable: true,
|
|
2876
|
+
get() {
|
|
2877
|
+
return activeContextCwd;
|
|
2878
|
+
},
|
|
2879
|
+
});
|
|
2880
|
+
activeContext!.switchSession = async (sessionPath: string) => {
|
|
2881
|
+
activeContextCwd = projectBase;
|
|
2882
|
+
activeContext!.sessionManager.getSessionFile = () => sessionPath;
|
|
2883
|
+
activeContext!.sessionManager.getSessionDir = () => path.dirname(sessionPath);
|
|
2884
|
+
activeContext!.sessionManager.getCwd = () => projectBase;
|
|
2885
|
+
process.chdir(projectBase);
|
|
2886
|
+
return { cancelled: false };
|
|
2887
|
+
};
|
|
2888
|
+
|
|
2889
|
+
const restoredContext = await restorePromptCommandExecution(executionPlan, activeContext, {
|
|
2890
|
+
config,
|
|
2891
|
+
workflowState: "merging",
|
|
2892
|
+
});
|
|
2893
|
+
|
|
2894
|
+
assert.equal(process.cwd(), projectBase);
|
|
2895
|
+
assert.equal(restoredContext, activeContext);
|
|
2896
|
+
assert.equal(fs.existsSync(executionPlan.worktreePath ?? ""), true);
|
|
2897
|
+
} finally {
|
|
2898
|
+
if (executionPlan) {
|
|
2899
|
+
await abortPromptCommandExecution(executionPlan);
|
|
2900
|
+
}
|
|
2901
|
+
process.chdir(previousCwd);
|
|
2902
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2903
|
+
}
|
|
2904
|
+
});
|
|
2905
|
+
|
|
2906
|
+
/**
|
|
2907
|
+
* @brief Verifies worktree-backed closure succeeds when lifecycle event contexts omit `switchSession()`.
|
|
2908
|
+
* @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.
|
|
2909
|
+
* @return {Promise<void>} Promise resolved after closure assertions complete.
|
|
2910
|
+
* @throws {AssertionError} Throws when closure fails to restore `base-path`, merge the worktree, or remove worktree resources.
|
|
2911
|
+
* @satisfies REQ-208, REQ-209, REQ-257, REQ-272, REQ-276, TST-089
|
|
2912
|
+
*/
|
|
2913
|
+
test("worktree-backed closure succeeds when agent_end lifecycle contexts omit switchSession", async () => {
|
|
2914
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2915
|
+
const previousCwd = process.cwd();
|
|
2916
|
+
try {
|
|
2917
|
+
const pi = createFakePi();
|
|
2918
|
+
piUsereqExtension(pi);
|
|
2919
|
+
const commandCtx = createFakeCtx(projectBase);
|
|
2920
|
+
await pi.emit("session_start", { reason: "startup" }, commandCtx);
|
|
2921
|
+
|
|
2922
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", commandCtx);
|
|
2923
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2924
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2925
|
+
assert.ok(worktreeMatch, promptText);
|
|
2926
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
2927
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2928
|
+
const executionSessionFile = commandCtx.sessionManager.getSessionFile() ?? "";
|
|
2929
|
+
assert.notEqual(executionSessionFile, "");
|
|
2930
|
+
|
|
2931
|
+
const eventCtx = createFakeCtx(executionBasePath);
|
|
2932
|
+
eventCtx.sessionManager.getSessionFile = () => executionSessionFile;
|
|
2933
|
+
eventCtx.sessionManager.getSessionDir = () => path.dirname(executionSessionFile);
|
|
2934
|
+
eventCtx.sessionManager.getCwd = () => executionBasePath;
|
|
2935
|
+
eventCtx.cwd = executionBasePath;
|
|
2936
|
+
delete eventCtx.switchSession;
|
|
2937
|
+
|
|
2938
|
+
await pi.emit("before_agent_start", {}, eventCtx);
|
|
2939
|
+
await pi.emit("agent_start", {}, eventCtx);
|
|
2940
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
2941
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "closure-context.ts"), "export const CLOSURE_CONTEXT = 1;\n", "utf8");
|
|
2942
|
+
assert.equal(spawnSync("git", ["add", "src/closure-context.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
2943
|
+
const worktreeCommit = spawnSync("git", ["commit", "-m", "closure context"], {
|
|
2944
|
+
cwd: executionBasePath,
|
|
2945
|
+
encoding: "utf8",
|
|
2946
|
+
});
|
|
2947
|
+
assert.equal(worktreeCommit.status, 0, worktreeCommit.stderr);
|
|
2948
|
+
|
|
2949
|
+
await pi.emit("agent_end", {
|
|
2950
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2951
|
+
}, eventCtx);
|
|
2952
|
+
|
|
2953
|
+
assert.equal(process.cwd(), projectBase);
|
|
2954
|
+
assert.equal(commandCtx.cwd, projectBase);
|
|
2955
|
+
assert.ok(fs.existsSync(path.join(projectBase, "src", "closure-context.ts")));
|
|
2956
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
2957
|
+
assert.notEqual(
|
|
2958
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
2959
|
+
cwd: projectBase,
|
|
2960
|
+
encoding: "utf8",
|
|
2961
|
+
}).status,
|
|
2962
|
+
0,
|
|
2963
|
+
);
|
|
2964
|
+
assert.equal(commandCtx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
|
|
2965
|
+
} finally {
|
|
2966
|
+
process.chdir(previousCwd);
|
|
2967
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2968
|
+
}
|
|
2969
|
+
});
|
|
2970
|
+
|
|
2971
|
+
/**
|
|
2972
|
+
* @brief Verifies switch-triggered `session_shutdown` preserves prompt-command state for same-runtime workflow closure.
|
|
2973
|
+
* @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.
|
|
2974
|
+
* @return {Promise<void>} Promise resolved after session-shutdown preservation and closure assertions complete.
|
|
2975
|
+
* @throws {AssertionError} Throws when switch-triggered shutdown clears prompt state, logs the wrong workflow state, or prevents merge and cleanup.
|
|
2976
|
+
* @satisfies REQ-221, REQ-227, REQ-278, TST-090
|
|
2977
|
+
*/
|
|
2978
|
+
test("switch-triggered session_shutdown preserves prompt state for same-runtime workflow closure", async () => {
|
|
2979
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2980
|
+
const previousCwd = process.cwd();
|
|
2981
|
+
try {
|
|
2982
|
+
writeProjectConfigOverrides(projectBase, {
|
|
2983
|
+
DEBUG_ENABLED: "enable",
|
|
2984
|
+
DEBUG_LOG_FILE: "prompt-debug.json",
|
|
2985
|
+
DEBUG_STATUS_CHANGES: "enable",
|
|
2986
|
+
DEBUG_WORKFLOW_EVENTS: "enable",
|
|
2987
|
+
DEBUG_LOG_ON_STATUS: "any",
|
|
2988
|
+
DEBUG_ENABLED_PROMPTS: ["req-change"],
|
|
2989
|
+
});
|
|
2990
|
+
const pi = createFakePi();
|
|
2991
|
+
piUsereqExtension(pi);
|
|
2992
|
+
const ctx = createFakeCtx(projectBase);
|
|
2993
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2994
|
+
|
|
2995
|
+
const originalSwitchSession = ctx.switchSession.bind(ctx);
|
|
2996
|
+
ctx.switchSession = async (sessionPath: string) => {
|
|
2997
|
+
await pi.emit("session_shutdown", {
|
|
2998
|
+
reason: "resume",
|
|
2999
|
+
targetSessionFile: sessionPath,
|
|
3000
|
+
}, ctx);
|
|
3001
|
+
return originalSwitchSession(sessionPath);
|
|
3002
|
+
};
|
|
3003
|
+
|
|
3004
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
3005
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
3006
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3007
|
+
assert.ok(worktreeMatch, promptText);
|
|
3008
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3009
|
+
|
|
3010
|
+
const activationLog = JSON.parse(fs.readFileSync(path.join(projectBase, "prompt-debug.json"), "utf8"));
|
|
3011
|
+
const shutdownEntry = activationLog.find((entry: any) => entry.action === "workflow_session_shutdown");
|
|
3012
|
+
assert.equal(shutdownEntry?.workflow_state, "checking");
|
|
3013
|
+
assert.equal(shutdownEntry?.result?.preserve_prompt_command_state, true);
|
|
3014
|
+
assert.equal(shutdownEntry?.result?.pending_prompt_request, true);
|
|
3015
|
+
assert.equal(shutdownEntry?.result?.active_prompt_request, false);
|
|
3016
|
+
assert.ok(activationLog.some((entry: any) => entry.action === "workflow_activation" && entry.workflow_state === "checking"));
|
|
3017
|
+
assert.ok(
|
|
3018
|
+
activationLog.some(
|
|
3019
|
+
(entry: any) => entry.action === "workflow_state"
|
|
3020
|
+
&& entry.input?.from_state === "checking"
|
|
3021
|
+
&& entry.result?.workflow_state === "running",
|
|
3022
|
+
),
|
|
3023
|
+
);
|
|
3024
|
+
|
|
3025
|
+
await pi.emit("before_agent_start", {}, ctx);
|
|
3026
|
+
await pi.emit("agent_start", {}, ctx);
|
|
3027
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
3028
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "shutdown-preserve.ts"), "export const SHUTDOWN_PRESERVE = 1;\n", "utf8");
|
|
3029
|
+
assert.equal(spawnSync("git", ["add", "src/shutdown-preserve.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
3030
|
+
const worktreeCommit = spawnSync("git", ["commit", "-m", "shutdown preserve"], {
|
|
3031
|
+
cwd: executionBasePath,
|
|
3032
|
+
encoding: "utf8",
|
|
3033
|
+
});
|
|
3034
|
+
assert.equal(worktreeCommit.status, 0, worktreeCommit.stderr);
|
|
3035
|
+
|
|
3036
|
+
await pi.emit("agent_end", {
|
|
3037
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
3038
|
+
}, ctx);
|
|
3039
|
+
|
|
3040
|
+
assert.equal(process.cwd(), projectBase);
|
|
3041
|
+
assert.equal(ctx.cwd, projectBase);
|
|
3042
|
+
assert.ok(fs.existsSync(path.join(projectBase, "src", "shutdown-preserve.ts")));
|
|
3043
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
3044
|
+
assert.equal(ctx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
|
|
3045
|
+
} finally {
|
|
3046
|
+
process.chdir(previousCwd);
|
|
3047
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3048
|
+
}
|
|
3049
|
+
});
|
|
3050
|
+
|
|
3051
|
+
/**
|
|
3052
|
+
* @brief Verifies replacement-session runtimes resynchronize persisted prompt state after switch-triggered rebinding.
|
|
3053
|
+
* @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.
|
|
3054
|
+
* @return {Promise<void>} Promise resolved after rebound-session closure assertions complete.
|
|
3055
|
+
* @throws {AssertionError} Throws when the replacement-session runtime misses the persisted `running` state and skips merge plus cleanup.
|
|
3056
|
+
* @satisfies REQ-227, REQ-279, TST-091
|
|
3057
|
+
*/
|
|
3058
|
+
test("replacement-session runtime resynchronizes persisted running state before prompt closure", async () => {
|
|
3059
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3060
|
+
const previousCwd = process.cwd();
|
|
3061
|
+
try {
|
|
3062
|
+
writeProjectConfigOverrides(projectBase, {
|
|
3063
|
+
DEBUG_ENABLED: "enable",
|
|
3064
|
+
DEBUG_LOG_FILE: "prompt-debug.json",
|
|
3065
|
+
DEBUG_STATUS_CHANGES: "enable",
|
|
3066
|
+
DEBUG_WORKFLOW_EVENTS: "enable",
|
|
3067
|
+
DEBUG_LOG_ON_STATUS: "any",
|
|
3068
|
+
DEBUG_ENABLED_PROMPTS: ["req-change"],
|
|
3069
|
+
});
|
|
3070
|
+
const firstPi = createFakePi();
|
|
3071
|
+
piUsereqExtension(firstPi);
|
|
3072
|
+
const firstCtx = createFakeCtx(projectBase);
|
|
3073
|
+
await firstPi.emit("session_start", { reason: "startup" }, firstCtx);
|
|
3074
|
+
|
|
3075
|
+
const originalSessionFile = firstCtx.sessionManager.getSessionFile() ?? "";
|
|
3076
|
+
const originalSwitchSession = firstCtx.switchSession.bind(firstCtx);
|
|
3077
|
+
let reboundPi: FakePi | undefined;
|
|
3078
|
+
let reboundCtx: any;
|
|
3079
|
+
firstCtx.switchSession = async (sessionPath: string) => {
|
|
3080
|
+
const executionSessionCwd = readFakeSessionFileCwd(sessionPath, firstCtx.cwd);
|
|
3081
|
+
await firstPi.emit("session_shutdown", {
|
|
3082
|
+
reason: "resume",
|
|
3083
|
+
targetSessionFile: sessionPath,
|
|
3084
|
+
}, firstCtx);
|
|
3085
|
+
if (!reboundPi) {
|
|
3086
|
+
reboundPi = createFakePi();
|
|
3087
|
+
piUsereqExtension(reboundPi);
|
|
3088
|
+
reboundCtx = createFakeCtx(executionSessionCwd);
|
|
3089
|
+
reboundCtx.sessionManager.getSessionFile = () => sessionPath;
|
|
3090
|
+
reboundCtx.sessionManager.getSessionDir = () => path.dirname(sessionPath);
|
|
3091
|
+
reboundCtx.sessionManager.getCwd = () => executionSessionCwd;
|
|
3092
|
+
reboundCtx.cwd = executionSessionCwd;
|
|
3093
|
+
await reboundPi.emit("session_start", {
|
|
3094
|
+
reason: "resume",
|
|
3095
|
+
previousSessionFile: originalSessionFile,
|
|
3096
|
+
}, reboundCtx);
|
|
3097
|
+
}
|
|
3098
|
+
return originalSwitchSession(sessionPath);
|
|
3099
|
+
};
|
|
3100
|
+
|
|
3101
|
+
await firstPi.commands.get("req-change")!.handler("Adjust docs", firstCtx);
|
|
3102
|
+
assert.ok(reboundPi);
|
|
3103
|
+
assert.ok(reboundCtx);
|
|
3104
|
+
const promptText = String(firstPi.sentUserMessages[0]?.content ?? "");
|
|
3105
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3106
|
+
assert.ok(worktreeMatch, promptText);
|
|
3107
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3108
|
+
|
|
3109
|
+
await reboundPi!.emit("before_agent_start", {}, reboundCtx);
|
|
3110
|
+
await reboundPi!.emit("agent_start", {}, reboundCtx);
|
|
3111
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
3112
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "rebound-running.ts"), "export const REBOUND_RUNNING = 1;\n", "utf8");
|
|
3113
|
+
assert.equal(spawnSync("git", ["add", "src/rebound-running.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
3114
|
+
const reboundCommit = spawnSync("git", ["commit", "-m", "rebound running"], { cwd: executionBasePath, encoding: "utf8" });
|
|
3115
|
+
assert.equal(reboundCommit.status, 0, reboundCommit.stderr);
|
|
3116
|
+
|
|
3117
|
+
await reboundPi!.emit("agent_end", {
|
|
3118
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
3119
|
+
}, reboundCtx);
|
|
3120
|
+
|
|
3121
|
+
const debugLog = JSON.parse(fs.readFileSync(path.join(projectBase, "prompt-debug.json"), "utf8"));
|
|
3122
|
+
assert.ok(
|
|
3123
|
+
debugLog.some(
|
|
3124
|
+
(entry: any) => entry.action === "workflow_closure"
|
|
3125
|
+
&& entry.result?.should_finalize_matched_success === true,
|
|
3126
|
+
),
|
|
3127
|
+
);
|
|
3128
|
+
assert.equal(process.cwd(), projectBase);
|
|
3129
|
+
assert.equal(reboundCtx.cwd, projectBase);
|
|
3130
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
3131
|
+
assert.equal(fs.existsSync(path.join(projectBase, "src", "rebound-running.ts")), true);
|
|
3132
|
+
assert.equal(reboundCtx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
|
|
3133
|
+
} finally {
|
|
3134
|
+
process.chdir(previousCwd);
|
|
3135
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3136
|
+
}
|
|
3137
|
+
});
|
|
3138
|
+
|
|
3139
|
+
/**
|
|
3140
|
+
* @brief Verifies `running` is persisted before async replacement-session prompt delivery completes.
|
|
3141
|
+
* @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.
|
|
3142
|
+
* @return {Promise<void>} Promise resolved after delayed prompt delivery, rebound closure, and cleanup assertions complete.
|
|
3143
|
+
* @throws {AssertionError} Throws when `running` is delayed until after `agent_end`, causing closure to skip merge or cleanup.
|
|
3144
|
+
* @satisfies REQ-227, REQ-279, REQ-281, TST-093
|
|
3145
|
+
*/
|
|
3146
|
+
test("replacement-session prompt delivery persists running before async sendUserMessage resolves", async () => {
|
|
3147
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3148
|
+
const previousCwd = process.cwd();
|
|
3149
|
+
try {
|
|
3150
|
+
writeProjectConfigOverrides(projectBase, {
|
|
3151
|
+
DEBUG_ENABLED: "enable",
|
|
3152
|
+
DEBUG_LOG_FILE: "prompt-debug.json",
|
|
3153
|
+
DEBUG_STATUS_CHANGES: "enable",
|
|
3154
|
+
DEBUG_WORKFLOW_EVENTS: "enable",
|
|
3155
|
+
DEBUG_LOG_ON_STATUS: "any",
|
|
3156
|
+
DEBUG_ENABLED_PROMPTS: ["req-change"],
|
|
3157
|
+
});
|
|
3158
|
+
const firstPi = createFakePi();
|
|
3159
|
+
piUsereqExtension(firstPi);
|
|
3160
|
+
const firstCtx = createFakeCtx(projectBase);
|
|
3161
|
+
await firstPi.emit("session_start", { reason: "startup" }, firstCtx);
|
|
3162
|
+
|
|
3163
|
+
const originalSessionFile = firstCtx.sessionManager.getSessionFile() ?? "";
|
|
3164
|
+
const originalSwitchSession = firstCtx.switchSession.bind(firstCtx);
|
|
3165
|
+
let reboundPi: FakePi | undefined;
|
|
3166
|
+
let reboundCtx: any;
|
|
3167
|
+
let resolvePromptDelivery: (() => void) | undefined;
|
|
3168
|
+
let handoffStatus = "";
|
|
3169
|
+
firstCtx.switchSession = async (
|
|
3170
|
+
sessionPath: string,
|
|
3171
|
+
options?: { withSession?: (replacementCtx: any) => Promise<void> },
|
|
3172
|
+
) => {
|
|
3173
|
+
const executionSessionCwd = readFakeSessionFileCwd(sessionPath, firstCtx.cwd);
|
|
3174
|
+
await firstPi.emit("session_shutdown", {
|
|
3175
|
+
reason: "resume",
|
|
3176
|
+
targetSessionFile: sessionPath,
|
|
3177
|
+
}, firstCtx);
|
|
3178
|
+
if (!reboundPi) {
|
|
3179
|
+
reboundPi = createFakePi();
|
|
3180
|
+
piUsereqExtension(reboundPi);
|
|
3181
|
+
reboundCtx = createFakeCtx(executionSessionCwd);
|
|
3182
|
+
reboundCtx.sessionManager.getSessionFile = () => sessionPath;
|
|
3183
|
+
reboundCtx.sessionManager.getSessionDir = () => path.dirname(sessionPath);
|
|
3184
|
+
reboundCtx.sessionManager.getCwd = () => executionSessionCwd;
|
|
3185
|
+
reboundCtx.cwd = executionSessionCwd;
|
|
3186
|
+
await reboundPi.emit("session_start", {
|
|
3187
|
+
reason: "resume",
|
|
3188
|
+
previousSessionFile: originalSessionFile,
|
|
3189
|
+
}, reboundCtx);
|
|
3190
|
+
}
|
|
3191
|
+
const switchResult = await originalSwitchSession(sessionPath);
|
|
3192
|
+
const replacementCtx = {
|
|
3193
|
+
...firstCtx,
|
|
3194
|
+
cwd: executionSessionCwd,
|
|
3195
|
+
sessionManager: {
|
|
3196
|
+
...firstCtx.sessionManager,
|
|
3197
|
+
getCwd: () => executionSessionCwd,
|
|
3198
|
+
getSessionFile: () => sessionPath,
|
|
3199
|
+
getSessionDir: () => path.dirname(sessionPath),
|
|
3200
|
+
},
|
|
3201
|
+
switchSession: firstCtx.switchSession,
|
|
3202
|
+
async sendUserMessage(content: unknown, sendOptions?: unknown) {
|
|
3203
|
+
handoffStatus = firstCtx.__state.statuses.get("pi-usereq") ?? "";
|
|
3204
|
+
firstPi.sentUserMessages.push({ content, options: sendOptions });
|
|
3205
|
+
await new Promise<void>((resolve) => {
|
|
3206
|
+
resolvePromptDelivery = resolve;
|
|
3207
|
+
});
|
|
3208
|
+
},
|
|
3209
|
+
};
|
|
3210
|
+
await options?.withSession?.(replacementCtx);
|
|
3211
|
+
return switchResult;
|
|
3212
|
+
};
|
|
3213
|
+
|
|
3214
|
+
const handlerPromise = firstPi.commands.get("req-change")!.handler("Adjust docs", firstCtx);
|
|
3215
|
+
for (let attempt = 0; attempt < 20 && resolvePromptDelivery === undefined; attempt += 1) {
|
|
3216
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
3217
|
+
}
|
|
3218
|
+
assert.ok(resolvePromptDelivery);
|
|
3219
|
+
assert.ok(reboundPi);
|
|
3220
|
+
assert.ok(reboundCtx);
|
|
3221
|
+
const promptText = String(firstPi.sentUserMessages[0]?.content ?? "");
|
|
3222
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3223
|
+
assert.ok(worktreeMatch, promptText);
|
|
3224
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3225
|
+
|
|
3226
|
+
assert.equal(
|
|
3227
|
+
handoffStatus,
|
|
3228
|
+
buildExpectedFakeStatusText({
|
|
3229
|
+
workflowState: "checking",
|
|
3230
|
+
basePath: buildExpectedFakeBasePath(executionBasePath),
|
|
3231
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
3232
|
+
testsDir: "tests",
|
|
3233
|
+
srcDir: ["src"],
|
|
3234
|
+
contextFilledCells: 0,
|
|
3235
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
3236
|
+
}),
|
|
3237
|
+
);
|
|
3238
|
+
assert.equal(
|
|
3239
|
+
firstCtx.__state.statuses.get("pi-usereq"),
|
|
3240
|
+
buildExpectedFakeStatusText({
|
|
3241
|
+
workflowState: "running",
|
|
3242
|
+
basePath: buildExpectedFakeBasePath(executionBasePath),
|
|
3243
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
3244
|
+
testsDir: "tests",
|
|
3245
|
+
srcDir: ["src"],
|
|
3246
|
+
contextFilledCells: 0,
|
|
3247
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
3248
|
+
}),
|
|
3249
|
+
);
|
|
3250
|
+
|
|
3251
|
+
await reboundPi!.emit("before_agent_start", {}, reboundCtx);
|
|
3252
|
+
await reboundPi!.emit("agent_start", {}, reboundCtx);
|
|
3253
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
3254
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "delayed-running.ts"), "export const DELAYED_RUNNING = 1;\n", "utf8");
|
|
3255
|
+
assert.equal(spawnSync("git", ["add", "src/delayed-running.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
3256
|
+
const reboundCommit = spawnSync("git", ["commit", "-m", "delayed running"], { cwd: executionBasePath, encoding: "utf8" });
|
|
3257
|
+
assert.equal(reboundCommit.status, 0, reboundCommit.stderr);
|
|
3258
|
+
|
|
3259
|
+
await reboundPi!.emit("agent_end", {
|
|
3260
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
3261
|
+
}, reboundCtx);
|
|
3262
|
+
|
|
3263
|
+
const debugLog = JSON.parse(fs.readFileSync(path.join(projectBase, "prompt-debug.json"), "utf8"));
|
|
3264
|
+
assert.ok(
|
|
3265
|
+
debugLog.some(
|
|
3266
|
+
(entry: any) => entry.action === "workflow_closure"
|
|
3267
|
+
&& entry.result?.should_finalize_matched_success === true,
|
|
3268
|
+
),
|
|
3269
|
+
);
|
|
3270
|
+
|
|
3271
|
+
resolvePromptDelivery!();
|
|
3272
|
+
await handlerPromise;
|
|
3273
|
+
|
|
3274
|
+
assert.equal(process.cwd(), projectBase);
|
|
3275
|
+
assert.equal(reboundCtx.cwd, projectBase);
|
|
3276
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
3277
|
+
assert.equal(fs.existsSync(path.join(projectBase, "src", "delayed-running.ts")), true);
|
|
3278
|
+
assert.equal(reboundCtx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
|
|
3279
|
+
} finally {
|
|
3280
|
+
process.chdir(previousCwd);
|
|
3281
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3282
|
+
}
|
|
3283
|
+
});
|
|
3284
|
+
|
|
3285
|
+
/**
|
|
3286
|
+
* @brief Verifies late stale replacement-session prompt-delivery rejections do not abort successful worktree closure.
|
|
3287
|
+
* @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.
|
|
3288
|
+
* @return {Promise<void>} Promise resolved after stale delivery rejection, matched-success closure, and cleanup assertions complete.
|
|
3289
|
+
* @throws {AssertionError} Throws when the late stale rejection bubbles out of the command handler or prevents merge plus cleanup.
|
|
3290
|
+
* @satisfies REQ-208, REQ-276, REQ-278, REQ-279, REQ-280, REQ-281, REQ-282
|
|
3291
|
+
*/
|
|
3292
|
+
test("late stale replacement-session prompt-delivery rejections do not interrupt worktree closure", async () => {
|
|
3293
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3294
|
+
const previousCwd = process.cwd();
|
|
3295
|
+
try {
|
|
3296
|
+
writeProjectConfigOverrides(projectBase, {
|
|
3297
|
+
DEBUG_ENABLED: "enable",
|
|
3298
|
+
DEBUG_LOG_FILE: "prompt-debug.json",
|
|
3299
|
+
DEBUG_STATUS_CHANGES: "enable",
|
|
3300
|
+
DEBUG_WORKFLOW_EVENTS: "enable",
|
|
3301
|
+
DEBUG_LOG_ON_STATUS: "any",
|
|
3302
|
+
DEBUG_ENABLED_PROMPTS: ["req-change"],
|
|
3303
|
+
});
|
|
3304
|
+
const firstPi = createFakePi();
|
|
3305
|
+
piUsereqExtension(firstPi);
|
|
3306
|
+
const firstCtx = createFakeCtx(projectBase);
|
|
3307
|
+
await firstPi.emit("session_start", { reason: "startup" }, firstCtx);
|
|
3308
|
+
|
|
3309
|
+
const originalSessionFile = firstCtx.sessionManager.getSessionFile() ?? "";
|
|
3310
|
+
const originalSwitchSession = firstCtx.switchSession.bind(firstCtx);
|
|
3311
|
+
let reboundPi: FakePi | undefined;
|
|
3312
|
+
let reboundCtx: any;
|
|
3313
|
+
let rejectPromptDelivery: ((error: unknown) => void) | undefined;
|
|
3314
|
+
firstCtx.switchSession = async (
|
|
3315
|
+
sessionPath: string,
|
|
3316
|
+
options?: { withSession?: (replacementCtx: any) => Promise<void> },
|
|
3317
|
+
) => {
|
|
3318
|
+
const replacementCwd = readFakeSessionFileCwd(sessionPath, firstCtx.cwd);
|
|
3319
|
+
await firstPi.emit("session_shutdown", {
|
|
3320
|
+
reason: "resume",
|
|
3321
|
+
targetSessionFile: sessionPath,
|
|
3322
|
+
}, firstCtx);
|
|
3323
|
+
if (!reboundPi) {
|
|
3324
|
+
reboundPi = createFakePi();
|
|
3325
|
+
piUsereqExtension(reboundPi);
|
|
3326
|
+
reboundCtx = createFakeCtx(replacementCwd);
|
|
3327
|
+
reboundCtx.sessionManager.getSessionFile = () => sessionPath;
|
|
3328
|
+
reboundCtx.sessionManager.getSessionDir = () => path.dirname(sessionPath);
|
|
3329
|
+
reboundCtx.sessionManager.getCwd = () => replacementCwd;
|
|
3330
|
+
reboundCtx.cwd = replacementCwd;
|
|
3331
|
+
await reboundPi.emit("session_start", {
|
|
3332
|
+
reason: "resume",
|
|
3333
|
+
previousSessionFile: originalSessionFile,
|
|
3334
|
+
}, reboundCtx);
|
|
3335
|
+
}
|
|
3336
|
+
const switchResult = await originalSwitchSession(sessionPath);
|
|
3337
|
+
const replacementCtx = {
|
|
3338
|
+
...firstCtx,
|
|
3339
|
+
cwd: replacementCwd,
|
|
3340
|
+
sessionManager: {
|
|
3341
|
+
...firstCtx.sessionManager,
|
|
3342
|
+
getCwd: () => replacementCwd,
|
|
3343
|
+
getSessionFile: () => sessionPath,
|
|
3344
|
+
getSessionDir: () => path.dirname(sessionPath),
|
|
3345
|
+
},
|
|
3346
|
+
switchSession: firstCtx.switchSession,
|
|
3347
|
+
async sendUserMessage(content: unknown, sendOptions?: unknown) {
|
|
3348
|
+
firstPi.sentUserMessages.push({ content, options: sendOptions });
|
|
3349
|
+
await new Promise<void>((_resolve, reject) => {
|
|
3350
|
+
rejectPromptDelivery = reject;
|
|
3351
|
+
});
|
|
3352
|
+
},
|
|
3353
|
+
};
|
|
3354
|
+
if (path.resolve(sessionPath) === path.resolve(originalSessionFile)) {
|
|
3355
|
+
rejectPromptDelivery?.(
|
|
3356
|
+
new Error("This extension ctx is stale after session replacement or reload."),
|
|
3357
|
+
);
|
|
3358
|
+
rejectPromptDelivery = undefined;
|
|
3359
|
+
}
|
|
3360
|
+
await options?.withSession?.(replacementCtx);
|
|
3361
|
+
return switchResult;
|
|
3362
|
+
};
|
|
3363
|
+
|
|
3364
|
+
const handlerPromise = firstPi.commands.get("req-change")!.handler("Adjust docs", firstCtx);
|
|
3365
|
+
const handlerOutcomePromise = handlerPromise.then(
|
|
3366
|
+
() => ({ error: undefined as unknown }),
|
|
3367
|
+
(error) => ({ error }),
|
|
3368
|
+
);
|
|
3369
|
+
for (let attempt = 0; attempt < 20 && rejectPromptDelivery === undefined; attempt += 1) {
|
|
3370
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
3371
|
+
}
|
|
3372
|
+
assert.ok(rejectPromptDelivery);
|
|
3373
|
+
assert.ok(reboundPi);
|
|
3374
|
+
assert.ok(reboundCtx);
|
|
3375
|
+
const promptText = String(firstPi.sentUserMessages[0]?.content ?? "");
|
|
3376
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3377
|
+
assert.ok(worktreeMatch, promptText);
|
|
3378
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
3379
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3380
|
+
const executionSessionFile = firstCtx.sessionManager.getSessionFile() ?? "";
|
|
3381
|
+
assert.notEqual(executionSessionFile, "");
|
|
3382
|
+
|
|
3383
|
+
await reboundPi!.emit("before_agent_start", {}, reboundCtx);
|
|
3384
|
+
await reboundPi!.emit("agent_start", {}, reboundCtx);
|
|
3385
|
+
|
|
3386
|
+
const persistedContext = readPersistedPromptCommandSessionContext(executionSessionFile);
|
|
3387
|
+
assert.ok(persistedContext);
|
|
3388
|
+
reboundCtx.switchSession = async (sessionPath: string) => {
|
|
3389
|
+
assert.equal(path.resolve(sessionPath), path.resolve(originalSessionFile));
|
|
3390
|
+
const pendingPromptDeliveryRejection = rejectPromptDelivery;
|
|
3391
|
+
rejectPromptDelivery = undefined;
|
|
3392
|
+
queueMicrotask(() => {
|
|
3393
|
+
pendingPromptDeliveryRejection?.(
|
|
3394
|
+
new Error("This extension ctx is stale after session replacement or reload."),
|
|
3395
|
+
);
|
|
3396
|
+
});
|
|
3397
|
+
reboundCtx.cwd = projectBase;
|
|
3398
|
+
reboundCtx.sessionManager.getSessionFile = () => originalSessionFile;
|
|
3399
|
+
reboundCtx.sessionManager.getSessionDir = () => path.dirname(originalSessionFile);
|
|
3400
|
+
reboundCtx.sessionManager.getCwd = () => projectBase;
|
|
3401
|
+
process.chdir(projectBase);
|
|
3402
|
+
return { cancelled: false };
|
|
3403
|
+
};
|
|
3404
|
+
|
|
3405
|
+
await Promise.race([
|
|
3406
|
+
reboundPi!.emit("agent_end", {
|
|
3407
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
3408
|
+
}, reboundCtx),
|
|
3409
|
+
new Promise<never>((_resolve, reject) => {
|
|
3410
|
+
setTimeout(() => reject(new Error("agent_end timed out")), 1_000);
|
|
3411
|
+
}),
|
|
3412
|
+
]);
|
|
3413
|
+
assert.equal(rejectPromptDelivery, undefined);
|
|
3414
|
+
const handlerOutcome = await Promise.race([
|
|
3415
|
+
handlerOutcomePromise,
|
|
3416
|
+
new Promise<never>((_resolve, reject) => {
|
|
3417
|
+
setTimeout(() => reject(new Error("handler timed out")), 1_000);
|
|
3418
|
+
}),
|
|
3419
|
+
]);
|
|
3420
|
+
assert.equal(handlerOutcome.error, undefined);
|
|
3421
|
+
|
|
3422
|
+
const debugLog = JSON.parse(fs.readFileSync(path.join(projectBase, "prompt-debug.json"), "utf8"));
|
|
3423
|
+
assert.ok(debugLog.some((entry: any) => entry.action === "workflow_restore" && entry.result?.success === true));
|
|
3424
|
+
assert.ok(debugLog.some((entry: any) => entry.action === "merge" && entry.result?.success === true));
|
|
3425
|
+
assert.equal(process.cwd(), projectBase);
|
|
3426
|
+
assert.equal(reboundCtx.cwd, projectBase);
|
|
3427
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
3428
|
+
assert.notEqual(
|
|
3429
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
3430
|
+
cwd: projectBase,
|
|
3431
|
+
encoding: "utf8",
|
|
3432
|
+
}).status,
|
|
3433
|
+
0,
|
|
3434
|
+
);
|
|
3435
|
+
assert.equal(reboundCtx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
|
|
3436
|
+
} finally {
|
|
3437
|
+
process.chdir(previousCwd);
|
|
3438
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3439
|
+
}
|
|
3440
|
+
});
|
|
3441
|
+
|
|
3442
|
+
test("worktree-backed prompt execution survives switch-triggered extension rebinding", async () => {
|
|
3443
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3444
|
+
const previousCwd = process.cwd();
|
|
3445
|
+
try {
|
|
3446
|
+
const firstPi = createFakePi();
|
|
3447
|
+
piUsereqExtension(firstPi);
|
|
3448
|
+
const firstCtx = createFakeCtx(projectBase);
|
|
3449
|
+
await firstPi.emit("session_start", { reason: "startup" }, firstCtx);
|
|
3450
|
+
|
|
3451
|
+
await firstPi.commands.get("req-change")!.handler("Adjust docs", firstCtx);
|
|
3452
|
+
const promptText = String(firstPi.sentUserMessages[0]?.content ?? "");
|
|
3453
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3454
|
+
assert.ok(worktreeMatch, promptText);
|
|
3455
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3456
|
+
const executionSessionFile = firstCtx.sessionManager.getSessionFile() ?? "";
|
|
3457
|
+
assert.notEqual(executionSessionFile, "");
|
|
3458
|
+
|
|
3459
|
+
const reboundPi = createFakePi();
|
|
3460
|
+
piUsereqExtension(reboundPi);
|
|
3461
|
+
const reboundCtx = createFakeCtx(executionBasePath);
|
|
3462
|
+
reboundCtx.sessionManager.getSessionFile = () => executionSessionFile;
|
|
3463
|
+
reboundCtx.sessionManager.getSessionDir = () => path.dirname(executionSessionFile);
|
|
3464
|
+
reboundCtx.sessionManager.getCwd = () => executionBasePath;
|
|
3465
|
+
reboundCtx.cwd = executionBasePath;
|
|
3466
|
+
await reboundPi.emit("session_start", {
|
|
3467
|
+
reason: "resume",
|
|
3468
|
+
previousSessionFile: firstCtx.sessionManager.getSessionFile(),
|
|
3469
|
+
}, reboundCtx);
|
|
3470
|
+
|
|
3471
|
+
await reboundPi.emit("before_agent_start", {}, reboundCtx);
|
|
3472
|
+
await reboundPi.emit("agent_start", {}, reboundCtx);
|
|
3473
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
3474
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "rebound.ts"), "export const REBOUND = 1;\n", "utf8");
|
|
3475
|
+
assert.equal(spawnSync("git", ["add", "src/rebound.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
3476
|
+
const reboundCommit = spawnSync("git", ["commit", "-m", "rebound"], { cwd: executionBasePath, encoding: "utf8" });
|
|
3477
|
+
assert.equal(reboundCommit.status, 0, reboundCommit.stderr);
|
|
3478
|
+
|
|
3479
|
+
await reboundPi.emit("agent_end", {
|
|
3480
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
3481
|
+
}, reboundCtx);
|
|
3482
|
+
|
|
3483
|
+
assert.equal(process.cwd(), projectBase);
|
|
3484
|
+
assert.equal(reboundCtx.cwd, projectBase);
|
|
3485
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
3486
|
+
assert.equal(fs.existsSync(path.join(projectBase, "src", "rebound.ts")), true);
|
|
3487
|
+
assert.equal(reboundCtx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
|
|
3488
|
+
} finally {
|
|
3489
|
+
process.chdir(previousCwd);
|
|
3490
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3491
|
+
}
|
|
3492
|
+
});
|
|
3493
|
+
|
|
3494
|
+
/**
|
|
3495
|
+
* @brief Reproduces the defect where `ctx.switchSession(sessionPath)` ignores a `withSession` option and leaves the handler-scoped ctx frozen on the pre-switch session.
|
|
3496
|
+
* @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.
|
|
3497
|
+
* @return {Promise<void>} Promise resolved after prompt delivery, activation, and assertions complete.
|
|
3498
|
+
* @throws {AssertionError} Throws when prompt activation aborts even though the persisted execution-session file and the host `process.cwd()` both match the worktree path.
|
|
3499
|
+
* @satisfies REQ-068, REQ-208, REQ-257, REQ-271, REQ-272
|
|
3500
|
+
*/
|
|
3501
|
+
test("worktree-backed prompt commands activate when ctx.switchSession only accepts a single argument and leaves handler ctx stale", async () => {
|
|
3502
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3503
|
+
const previousCwd = process.cwd();
|
|
3504
|
+
try {
|
|
3505
|
+
process.chdir(projectBase);
|
|
3506
|
+
const pi = createFakePi();
|
|
3507
|
+
piUsereqExtension(pi);
|
|
3508
|
+
const ctx = createFakeCtx(projectBase);
|
|
3509
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
3510
|
+
|
|
3511
|
+
const frozenSessionFile = ctx.sessionManager.getSessionFile() ?? "";
|
|
3512
|
+
const frozenSessionCwd = ctx.sessionManager.getCwd();
|
|
3513
|
+
ctx.switchSession = async (sessionPath: string, ..._ignored: unknown[]) => {
|
|
3514
|
+
// Mirror the real pi SDK: accept only sessionPath, never mutate ctx.cwd or ctx.sessionManager.
|
|
3515
|
+
void sessionPath;
|
|
3516
|
+
return { cancelled: false };
|
|
3517
|
+
};
|
|
3518
|
+
|
|
3519
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
3520
|
+
|
|
3521
|
+
assert.equal(pi.sentUserMessages.length, 1);
|
|
3522
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
3523
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3524
|
+
assert.ok(worktreeMatch, promptText);
|
|
3525
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3526
|
+
assert.equal(process.cwd(), executionBasePath);
|
|
3527
|
+
assert.equal(ctx.cwd, projectBase);
|
|
3528
|
+
assert.equal(ctx.sessionManager.getSessionFile(), frozenSessionFile);
|
|
3529
|
+
assert.equal(ctx.sessionManager.getCwd(), frozenSessionCwd);
|
|
3530
|
+
assert.equal(
|
|
3531
|
+
ctx.__state.notifications.some((entry) => /Prompt execution activation expected/.test(entry.message)),
|
|
3532
|
+
false,
|
|
3533
|
+
);
|
|
3534
|
+
} finally {
|
|
3535
|
+
process.chdir(previousCwd);
|
|
3536
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3537
|
+
}
|
|
3538
|
+
});
|
|
3539
|
+
|
|
3540
|
+
test("worktree-backed prompt commands tolerate stale ctx.sessionManager probes after switchSession returns non-cancelled", async () => {
|
|
3541
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3542
|
+
const previousCwd = process.cwd();
|
|
3543
|
+
try {
|
|
3544
|
+
const pi = createFakePi();
|
|
3545
|
+
piUsereqExtension(pi);
|
|
3546
|
+
const ctx = createFakeCtx(projectBase);
|
|
3547
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
3548
|
+
|
|
3549
|
+
const originalSessionFile = ctx.sessionManager.getSessionFile();
|
|
3550
|
+
const originalSessionCwd = ctx.sessionManager.getCwd();
|
|
3551
|
+
const originalSwitchSession = ctx.switchSession.bind(ctx);
|
|
3552
|
+
ctx.switchSession = async (sessionPath: string) => {
|
|
3553
|
+
// Mirror the real pi SDK: session switching runs internally but ctx.sessionManager stays frozen.
|
|
3554
|
+
const result = await originalSwitchSession(sessionPath);
|
|
3555
|
+
ctx.sessionManager.getSessionFile = () => originalSessionFile;
|
|
3556
|
+
ctx.sessionManager.getCwd = () => originalSessionCwd;
|
|
3557
|
+
return result;
|
|
3558
|
+
};
|
|
3559
|
+
|
|
3560
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
3561
|
+
|
|
3562
|
+
assert.equal(pi.sentUserMessages.length, 1);
|
|
3563
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
3564
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3565
|
+
assert.ok(worktreeMatch, promptText);
|
|
3566
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3567
|
+
assert.equal(process.cwd(), executionBasePath);
|
|
3568
|
+
assert.equal(ctx.sessionManager.getSessionFile(), originalSessionFile);
|
|
3569
|
+
assert.equal(ctx.sessionManager.getCwd(), originalSessionCwd);
|
|
3570
|
+
assert.equal(
|
|
3571
|
+
ctx.__state.notifications.some((entry) => /Prompt execution activation expected/.test(entry.message)),
|
|
3572
|
+
false,
|
|
3573
|
+
);
|
|
3574
|
+
} finally {
|
|
3575
|
+
process.chdir(previousCwd);
|
|
3576
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3577
|
+
}
|
|
3578
|
+
});
|
|
3579
|
+
|
|
3580
|
+
test("worktree-backed prompt commands tolerate a pending-persistence execution-session file when process.cwd() is aligned with worktree-path", async () => {
|
|
3581
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3582
|
+
const previousCwd = process.cwd();
|
|
3583
|
+
try {
|
|
3584
|
+
const pi = createFakePi();
|
|
3585
|
+
piUsereqExtension(pi);
|
|
3586
|
+
const ctx = createFakeCtx(projectBase);
|
|
3587
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
3588
|
+
|
|
3589
|
+
const originalSwitchSession = ctx.switchSession.bind(ctx);
|
|
3590
|
+
ctx.switchSession = async (sessionPath: string) => {
|
|
3591
|
+
const result = await originalSwitchSession(sessionPath);
|
|
3592
|
+
// 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.
|
|
3593
|
+
if (typeof sessionPath === "string" && fs.existsSync(sessionPath)) {
|
|
3594
|
+
fs.rmSync(sessionPath, { force: true });
|
|
3595
|
+
}
|
|
3596
|
+
return result;
|
|
3597
|
+
};
|
|
3598
|
+
|
|
3599
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
3600
|
+
|
|
3601
|
+
assert.equal(pi.sentUserMessages.length, 1);
|
|
3602
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
3603
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3604
|
+
assert.ok(worktreeMatch, promptText);
|
|
3605
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3606
|
+
assert.equal(process.cwd(), executionBasePath);
|
|
3607
|
+
assert.equal(
|
|
3608
|
+
ctx.__state.notifications.some((entry) => /Prompt execution activation expected session/.test(entry.message)),
|
|
3609
|
+
false,
|
|
3610
|
+
);
|
|
3611
|
+
} finally {
|
|
3612
|
+
process.chdir(previousCwd);
|
|
3613
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3614
|
+
}
|
|
3615
|
+
});
|
|
3616
|
+
|
|
3617
|
+
test("worktree-backed prompt commands abort before prompt dispatch when the persisted execution-session header cwd diverges from worktree-path", async () => {
|
|
3618
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3619
|
+
const previousCwd = process.cwd();
|
|
3620
|
+
try {
|
|
3621
|
+
const pi = createFakePi();
|
|
3622
|
+
piUsereqExtension(pi);
|
|
3623
|
+
const ctx = createFakeCtx(projectBase);
|
|
3624
|
+
const originalSwitchSession = ctx.switchSession.bind(ctx);
|
|
3625
|
+
ctx.switchSession = async (sessionPath: string) => {
|
|
3626
|
+
const result = await originalSwitchSession(sessionPath);
|
|
3627
|
+
// Simulate pi runtimes that persist a tampered header cwd, forcing the on-disk cwd verification to fail.
|
|
3628
|
+
if (typeof sessionPath === "string" && fs.existsSync(sessionPath)) {
|
|
3629
|
+
const tamperedHeader = `${JSON.stringify({ type: "session", cwd: projectBase })}\n`;
|
|
3630
|
+
fs.writeFileSync(sessionPath, tamperedHeader, "utf8");
|
|
3631
|
+
}
|
|
3632
|
+
return result;
|
|
3633
|
+
};
|
|
3634
|
+
|
|
3635
|
+
await assert.rejects(
|
|
3636
|
+
pi.commands.get("req-change")!.handler("Adjust docs", ctx),
|
|
3637
|
+
/Prompt execution activation expected session/,
|
|
3638
|
+
);
|
|
3639
|
+
|
|
3640
|
+
assert.equal(pi.sentUserMessages.length, 0);
|
|
3641
|
+
assert.ok(ctx.__state.notifications.some((entry) => /Prompt execution activation expected session/.test(entry.message)));
|
|
3642
|
+
} finally {
|
|
3643
|
+
process.chdir(previousCwd);
|
|
3644
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3645
|
+
}
|
|
3646
|
+
});
|
|
3647
|
+
|
|
3648
|
+
test("worktree-backed successful merge stops before merge when fork-session verification cannot reattach", async () => {
|
|
3649
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3650
|
+
const previousCwd = process.cwd();
|
|
3651
|
+
try {
|
|
3652
|
+
const pi = createFakePi();
|
|
3653
|
+
piUsereqExtension(pi);
|
|
3654
|
+
const ctx = createFakeCtx(projectBase);
|
|
3655
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
3656
|
+
|
|
3657
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
3658
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
3659
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3660
|
+
assert.ok(worktreeMatch, promptText);
|
|
3661
|
+
const executionSessionFile = ctx.sessionManager.getSessionFile();
|
|
3662
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3663
|
+
|
|
3664
|
+
await pi.emit("before_agent_start", {}, ctx);
|
|
3665
|
+
await pi.emit("agent_start", {}, ctx);
|
|
3666
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
3667
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "restore-failed.ts"), "export const RESTORE_FAILED = 1;\n", "utf8");
|
|
3668
|
+
assert.equal(spawnSync("git", ["add", "src/restore-failed.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
3669
|
+
const worktreeCommit = spawnSync("git", ["commit", "-m", "worktree change"], { cwd: executionBasePath, encoding: "utf8" });
|
|
3670
|
+
assert.equal(worktreeCommit.status, 0, worktreeCommit.stderr);
|
|
3671
|
+
|
|
3672
|
+
// Simulate a pi runtime that rewrites the execution-session header with a divergent cwd at prompt-end so closure verification must abort before merge.
|
|
3673
|
+
assert.equal(typeof executionSessionFile, "string");
|
|
3674
|
+
if (typeof executionSessionFile === "string" && fs.existsSync(executionSessionFile)) {
|
|
3675
|
+
const tamperedHeader = `${JSON.stringify({ type: "session", cwd: projectBase })}\n`;
|
|
3676
|
+
fs.writeFileSync(executionSessionFile, tamperedHeader, "utf8");
|
|
3677
|
+
}
|
|
3678
|
+
// Make ctx.sessionManager report a stale session file path so base-path restoration performs an explicit switch attempt after finalization verification fails.
|
|
3679
|
+
ctx.sessionManager.getSessionFile = () => path.join(projectBase, "stale-execution-session.jsonl");
|
|
3680
|
+
ctx.switchSession = async (_sessionPath: string) => ({ cancelled: false });
|
|
3681
|
+
|
|
3682
|
+
await pi.emit("agent_end", {
|
|
3683
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
3684
|
+
}, ctx);
|
|
3685
|
+
|
|
3686
|
+
assert.equal(process.cwd(), projectBase);
|
|
3687
|
+
assert.equal(ctx.cwd, projectBase);
|
|
3688
|
+
assert.equal(fs.existsSync(executionBasePath), true);
|
|
3689
|
+
assert.equal(fs.existsSync(path.join(projectBase, "src", "restore-failed.ts")), false);
|
|
3690
|
+
assert.ok(ctx.__state.notifications.some((entry) => /Prompt execution finalization expected session/.test(entry.message)));
|
|
3691
|
+
} finally {
|
|
3692
|
+
process.chdir(previousCwd);
|
|
3693
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3694
|
+
}
|
|
3695
|
+
});
|
|
3696
|
+
|
|
3697
|
+
/**
|
|
3698
|
+
* @brief Verifies successful closure merges from `base-path` even when end-of-session timing already moved the persisted replacement context off the worktree session.
|
|
3699
|
+
* @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.
|
|
3700
|
+
* @return {Promise<void>} Promise resolved after closure assertions complete.
|
|
3701
|
+
* @throws {AssertionError} Throws when closure attempts an execution-session reattach or skips the merge plus cleanup.
|
|
3702
|
+
* @satisfies REQ-208, REQ-209, REQ-282, TST-094
|
|
3703
|
+
*/
|
|
3704
|
+
test("worktree-backed closure merges from base-path when end-of-session timing already moved the persisted context", async () => {
|
|
3705
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3706
|
+
const previousCwd = process.cwd();
|
|
3707
|
+
try {
|
|
3708
|
+
writeProjectConfigOverrides(projectBase, {
|
|
3709
|
+
DEBUG_ENABLED: "enable",
|
|
3710
|
+
DEBUG_LOG_FILE: "prompt-debug.json",
|
|
3711
|
+
DEBUG_STATUS_CHANGES: "enable",
|
|
3712
|
+
DEBUG_WORKFLOW_EVENTS: "enable",
|
|
3713
|
+
DEBUG_LOG_ON_STATUS: "any",
|
|
3714
|
+
DEBUG_ENABLED_PROMPTS: ["req-change"],
|
|
3715
|
+
});
|
|
3716
|
+
const pi = createFakePi();
|
|
3717
|
+
piUsereqExtension(pi);
|
|
3718
|
+
const commandCtx = createFakeCtx(projectBase);
|
|
3719
|
+
await pi.emit("session_start", { reason: "startup" }, commandCtx);
|
|
3720
|
+
|
|
3721
|
+
const originalSessionFile = commandCtx.sessionManager.getSessionFile() ?? "";
|
|
3722
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", commandCtx);
|
|
3723
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
3724
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3725
|
+
assert.ok(worktreeMatch, promptText);
|
|
3726
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
3727
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3728
|
+
const executionSessionFile = commandCtx.sessionManager.getSessionFile() ?? "";
|
|
3729
|
+
assert.notEqual(executionSessionFile, "");
|
|
3730
|
+
|
|
3731
|
+
await pi.emit("before_agent_start", {}, commandCtx);
|
|
3732
|
+
await pi.emit("agent_start", {}, commandCtx);
|
|
3733
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
3734
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "base-merge.ts"), "export const BASE_MERGE = 1;\n", "utf8");
|
|
3735
|
+
assert.equal(spawnSync("git", ["add", "src/base-merge.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
3736
|
+
const worktreeCommit = spawnSync("git", ["commit", "-m", "base merge"], { cwd: executionBasePath, encoding: "utf8" });
|
|
3737
|
+
assert.equal(worktreeCommit.status, 0, worktreeCommit.stderr);
|
|
3738
|
+
|
|
3739
|
+
const persistedContext = readPersistedPromptCommandSessionContext(executionSessionFile) as any;
|
|
3740
|
+
assert.ok(persistedContext);
|
|
3741
|
+
persistedContext.cwd = projectBase;
|
|
3742
|
+
persistedContext.sessionManager.getSessionFile = () => originalSessionFile;
|
|
3743
|
+
persistedContext.sessionManager.getSessionDir = () => path.dirname(originalSessionFile);
|
|
3744
|
+
persistedContext.sessionManager.getCwd = () => projectBase;
|
|
3745
|
+
persistedContext.switchSession = async (sessionPath: string) => {
|
|
3746
|
+
if (path.resolve(sessionPath) !== path.resolve(originalSessionFile)) {
|
|
3747
|
+
throw new Error(`Unexpected execution-session reattach: ${sessionPath}`);
|
|
3748
|
+
}
|
|
3749
|
+
process.chdir(projectBase);
|
|
3750
|
+
return { cancelled: false };
|
|
3751
|
+
};
|
|
3752
|
+
|
|
3753
|
+
const eventCtx = createFakeCtx(projectBase);
|
|
3754
|
+
eventCtx.sessionManager.getSessionFile = () => originalSessionFile;
|
|
3755
|
+
eventCtx.sessionManager.getSessionDir = () => path.dirname(originalSessionFile);
|
|
3756
|
+
eventCtx.sessionManager.getCwd = () => projectBase;
|
|
3757
|
+
eventCtx.cwd = projectBase;
|
|
3758
|
+
delete eventCtx.switchSession;
|
|
3759
|
+
process.chdir(projectBase);
|
|
3760
|
+
|
|
3761
|
+
await pi.emit("agent_end", {
|
|
3762
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
3763
|
+
}, eventCtx);
|
|
3764
|
+
|
|
3765
|
+
const debugLog = JSON.parse(fs.readFileSync(path.join(projectBase, "prompt-debug.json"), "utf8"));
|
|
3766
|
+
assert.ok(debugLog.some((entry: any) => entry.action === "workflow_restore" && entry.result?.success === true));
|
|
3767
|
+
assert.ok(debugLog.some((entry: any) => entry.action === "merge" && entry.result?.success === true));
|
|
3768
|
+
assert.equal(process.cwd(), projectBase);
|
|
3769
|
+
assert.equal(eventCtx.cwd, projectBase);
|
|
3770
|
+
assert.ok(fs.existsSync(path.join(projectBase, "src", "base-merge.ts")));
|
|
3771
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
3772
|
+
assert.notEqual(
|
|
3773
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
3774
|
+
cwd: projectBase,
|
|
3775
|
+
encoding: "utf8",
|
|
3776
|
+
}).status,
|
|
3777
|
+
0,
|
|
3778
|
+
);
|
|
3779
|
+
assert.equal(eventCtx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
|
|
3780
|
+
} finally {
|
|
3781
|
+
process.chdir(previousCwd);
|
|
3782
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3783
|
+
}
|
|
3784
|
+
});
|
|
1568
3785
|
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
3786
|
+
/**
|
|
3787
|
+
* @brief Verifies matched successful closure ignores stale session-switch errors after restored base-path verification.
|
|
3788
|
+
* @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.
|
|
3789
|
+
* @return {Promise<void>} Promise resolved after restored-base verification, merge, and cleanup assertions complete.
|
|
3790
|
+
* @throws {AssertionError} Throws when the stale restore-switch error prevents merge plus cleanup.
|
|
3791
|
+
* @satisfies REQ-208, REQ-276, REQ-280, REQ-282
|
|
3792
|
+
*/
|
|
3793
|
+
test("worktree-backed closure ignores stale restore-switch errors after base-path verification", async () => {
|
|
3794
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3795
|
+
const previousCwd = process.cwd();
|
|
3796
|
+
try {
|
|
3797
|
+
writeProjectConfigOverrides(projectBase, {
|
|
3798
|
+
DEBUG_ENABLED: "enable",
|
|
3799
|
+
DEBUG_LOG_FILE: "prompt-debug.json",
|
|
3800
|
+
DEBUG_STATUS_CHANGES: "enable",
|
|
3801
|
+
DEBUG_WORKFLOW_EVENTS: "enable",
|
|
3802
|
+
DEBUG_LOG_ON_STATUS: "any",
|
|
3803
|
+
DEBUG_ENABLED_PROMPTS: ["req-change"],
|
|
3804
|
+
});
|
|
3805
|
+
const pi = createFakePi();
|
|
3806
|
+
piUsereqExtension(pi);
|
|
3807
|
+
const commandCtx = createFakeCtx(projectBase);
|
|
3808
|
+
await pi.emit("session_start", { reason: "startup" }, commandCtx);
|
|
3809
|
+
|
|
3810
|
+
const originalSessionFile = commandCtx.sessionManager.getSessionFile() ?? "";
|
|
3811
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", commandCtx);
|
|
3812
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
3813
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3814
|
+
assert.ok(worktreeMatch, promptText);
|
|
3815
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
3816
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3817
|
+
const executionSessionFile = commandCtx.sessionManager.getSessionFile() ?? "";
|
|
3818
|
+
assert.notEqual(executionSessionFile, "");
|
|
3819
|
+
|
|
3820
|
+
await pi.emit("before_agent_start", {}, commandCtx);
|
|
3821
|
+
await pi.emit("agent_start", {}, commandCtx);
|
|
3822
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
3823
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "stale-restore.ts"), "export const STALE_RESTORE = 1;\n", "utf8");
|
|
3824
|
+
assert.equal(spawnSync("git", ["add", "src/stale-restore.ts"], { cwd: executionBasePath, encoding: "utf8" }).status, 0);
|
|
3825
|
+
const worktreeCommit = spawnSync("git", ["commit", "-m", "stale restore"], { cwd: executionBasePath, encoding: "utf8" });
|
|
3826
|
+
assert.equal(worktreeCommit.status, 0, worktreeCommit.stderr);
|
|
3827
|
+
|
|
3828
|
+
const persistedContext = readPersistedPromptCommandSessionContext(executionSessionFile) as any;
|
|
3829
|
+
assert.ok(persistedContext);
|
|
3830
|
+
persistedContext.cwd = projectBase;
|
|
3831
|
+
persistedContext.sessionManager.getSessionFile = () => originalSessionFile;
|
|
3832
|
+
persistedContext.sessionManager.getSessionDir = () => path.dirname(originalSessionFile);
|
|
3833
|
+
persistedContext.sessionManager.getCwd = () => projectBase;
|
|
3834
|
+
persistedContext.switchSession = async (sessionPath: string) => {
|
|
3835
|
+
assert.equal(path.resolve(sessionPath), path.resolve(originalSessionFile));
|
|
3836
|
+
process.chdir(projectBase);
|
|
3837
|
+
persistedContext.cwd = projectBase;
|
|
3838
|
+
throw new Error("This extension ctx is stale after session replacement or reload.");
|
|
3839
|
+
};
|
|
3840
|
+
|
|
3841
|
+
const eventCtx = createFakeCtx(projectBase);
|
|
3842
|
+
eventCtx.sessionManager.getSessionFile = () => originalSessionFile;
|
|
3843
|
+
eventCtx.sessionManager.getSessionDir = () => path.dirname(originalSessionFile);
|
|
3844
|
+
eventCtx.sessionManager.getCwd = () => projectBase;
|
|
3845
|
+
eventCtx.cwd = projectBase;
|
|
3846
|
+
delete eventCtx.switchSession;
|
|
3847
|
+
process.chdir(projectBase);
|
|
3848
|
+
|
|
3849
|
+
await pi.emit("agent_end", {
|
|
3850
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
3851
|
+
}, eventCtx);
|
|
3852
|
+
|
|
3853
|
+
const debugLog = JSON.parse(fs.readFileSync(path.join(projectBase, "prompt-debug.json"), "utf8"));
|
|
3854
|
+
assert.ok(debugLog.some((entry: any) => entry.action === "workflow_restore" && entry.result?.success === true));
|
|
3855
|
+
assert.ok(debugLog.some((entry: any) => entry.action === "merge" && entry.result?.success === true));
|
|
3856
|
+
assert.equal(process.cwd(), projectBase);
|
|
3857
|
+
assert.equal(eventCtx.cwd, projectBase);
|
|
3858
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
3859
|
+
assert.equal(fs.existsSync(path.join(projectBase, "src", "stale-restore.ts")), true);
|
|
3860
|
+
assert.notEqual(
|
|
3861
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
3862
|
+
cwd: projectBase,
|
|
3863
|
+
encoding: "utf8",
|
|
3864
|
+
}).status,
|
|
3865
|
+
0,
|
|
3866
|
+
);
|
|
3867
|
+
assert.equal(eventCtx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
|
|
3868
|
+
} finally {
|
|
3869
|
+
process.chdir(previousCwd);
|
|
3870
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3871
|
+
}
|
|
1574
3872
|
});
|
|
1575
3873
|
|
|
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
|
-
});
|
|
3874
|
+
test("worktree-backed prompt commands abort before prompt dispatch when post-create verification fails", async () => {
|
|
3875
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3876
|
+
const previousCwd = process.cwd();
|
|
3877
|
+
const originalExistsSync = fs.existsSync;
|
|
3878
|
+
let failedWorktreeName = "";
|
|
3879
|
+
let failedWorktreePath = "";
|
|
3880
|
+
try {
|
|
3881
|
+
const pi = createFakePi();
|
|
3882
|
+
piUsereqExtension(pi);
|
|
3883
|
+
const ctx = createFakeCtx(projectBase);
|
|
3884
|
+
setPromptCommandPostCreateHookForTests(({ worktreeDir, worktreeRootPath, worktreePath }) => {
|
|
3885
|
+
failedWorktreeName = worktreeDir;
|
|
3886
|
+
failedWorktreePath = worktreePath;
|
|
3887
|
+
fs.existsSync = ((candidatePath: Parameters<typeof fs.existsSync>[0]) => {
|
|
3888
|
+
const normalizedPath = typeof candidatePath === "string" ? candidatePath : String(candidatePath);
|
|
3889
|
+
if (normalizedPath === worktreePath || normalizedPath === worktreeRootPath) {
|
|
3890
|
+
return false;
|
|
3891
|
+
}
|
|
3892
|
+
return originalExistsSync(candidatePath);
|
|
3893
|
+
}) as typeof fs.existsSync;
|
|
3894
|
+
});
|
|
1599
3895
|
|
|
1600
|
-
|
|
3896
|
+
await assert.rejects(
|
|
3897
|
+
pi.commands.get("req-change")!.handler("Adjust docs", ctx),
|
|
3898
|
+
/ERROR: Worktree verification failed/,
|
|
3899
|
+
);
|
|
1601
3900
|
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
3901
|
+
assert.equal(pi.sentUserMessages.length, 0);
|
|
3902
|
+
fs.existsSync = originalExistsSync;
|
|
3903
|
+
assert.equal(fs.existsSync(failedWorktreePath), false);
|
|
3904
|
+
assert.notEqual(failedWorktreeName, "");
|
|
3905
|
+
assert.notEqual(
|
|
3906
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${failedWorktreeName}`], {
|
|
3907
|
+
cwd: projectBase,
|
|
3908
|
+
encoding: "utf8",
|
|
3909
|
+
}).status,
|
|
3910
|
+
0,
|
|
3911
|
+
);
|
|
3912
|
+
assert.ok(ctx.__state.notifications.some((entry) => /Worktree verification failed/.test(entry.message)));
|
|
3913
|
+
} finally {
|
|
3914
|
+
fs.existsSync = originalExistsSync;
|
|
3915
|
+
setPromptCommandPostCreateHookForTests(undefined);
|
|
3916
|
+
process.chdir(previousCwd);
|
|
3917
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3918
|
+
}
|
|
1609
3919
|
});
|
|
1610
3920
|
|
|
1611
|
-
test("prompt commands
|
|
1612
|
-
const
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
3921
|
+
test("prompt commands remain functional when custom tool registrations are removed from the runtime inventory", async () => {
|
|
3922
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3923
|
+
try {
|
|
3924
|
+
writeProjectConfigOverrides(projectBase, { GIT_WORKTREE_ENABLED: "disable" });
|
|
3925
|
+
const pi = createFakePi();
|
|
3926
|
+
piUsereqExtension(pi);
|
|
3927
|
+
pi.tools.splice(0, pi.tools.length);
|
|
3928
|
+
const ctx = createFakeCtx(projectBase);
|
|
1618
3929
|
|
|
1619
|
-
|
|
3930
|
+
await pi.commands.get("req-analyze")!.handler("Inspect src/index.ts", ctx);
|
|
1620
3931
|
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
3932
|
+
assert.equal(pi.sentUserMessages.length, 1);
|
|
3933
|
+
assert.match(String(pi.sentUserMessages[0]?.content ?? ""), /Inspect src\/index\.ts/);
|
|
3934
|
+
} finally {
|
|
3935
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3936
|
+
}
|
|
1625
3937
|
});
|
|
1626
3938
|
|
|
1627
3939
|
test("session_start applies configured pi-usereq startup tools", async () => {
|
|
@@ -1634,7 +3946,7 @@ test("session_start applies configured pi-usereq startup tools", async () => {
|
|
|
1634
3946
|
"tests-dir": "tests",
|
|
1635
3947
|
"src-dir": ["src", "foobar"],
|
|
1636
3948
|
"static-check": {},
|
|
1637
|
-
"enabled-tools": ["
|
|
3949
|
+
"enabled-tools": ["files-tokens", "static-check"],
|
|
1638
3950
|
}, null, 2)}\n`,
|
|
1639
3951
|
"utf8",
|
|
1640
3952
|
);
|
|
@@ -1645,7 +3957,7 @@ test("session_start applies configured pi-usereq startup tools", async () => {
|
|
|
1645
3957
|
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
1646
3958
|
|
|
1647
3959
|
const activeTools = new Set(pi.getActiveTools());
|
|
1648
|
-
assert.deepEqual([...activeTools].sort(), ["
|
|
3960
|
+
assert.deepEqual([...activeTools].sort(), ["files-tokens", "static-check"]);
|
|
1649
3961
|
assert.equal(
|
|
1650
3962
|
ctx.__state.statuses.get("pi-usereq"),
|
|
1651
3963
|
buildExpectedFakeStatusText({
|
|
@@ -1684,6 +3996,33 @@ test("session_start renders the 0-percent context icon when context usage is emp
|
|
|
1684
3996
|
);
|
|
1685
3997
|
});
|
|
1686
3998
|
|
|
3999
|
+
/**
|
|
4000
|
+
* @brief Verifies stale replacement contexts do not break workflow-state rendering.
|
|
4001
|
+
* @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.
|
|
4002
|
+
* @return {void} No return value.
|
|
4003
|
+
* @throws {AssertionError} Throws when stale-context rendering escapes or leaves the stale context cached for later renders.
|
|
4004
|
+
* @satisfies REQ-280, TST-092
|
|
4005
|
+
*/
|
|
4006
|
+
test("stale replacement contexts do not break workflow-state rendering", () => {
|
|
4007
|
+
const cwd = createTempDir("pi-usereq-stale-render-");
|
|
4008
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
4009
|
+
const controller = createPiUsereqStatusController();
|
|
4010
|
+
setPiUsereqStatusConfig(controller, getDefaultConfig());
|
|
4011
|
+
const staleCtx = createFakeCtx(cwd);
|
|
4012
|
+
Object.defineProperty(staleCtx, "ui", {
|
|
4013
|
+
configurable: true,
|
|
4014
|
+
get() {
|
|
4015
|
+
throw new Error("This extension instance is stale after session replacement or reload. Use the provided replacement-session context instead.");
|
|
4016
|
+
},
|
|
4017
|
+
});
|
|
4018
|
+
|
|
4019
|
+
assert.doesNotThrow(() => {
|
|
4020
|
+
setPiUsereqWorkflowState(controller, "running", staleCtx);
|
|
4021
|
+
});
|
|
4022
|
+
assert.equal(controller.state.workflowState, "running");
|
|
4023
|
+
assert.equal(controller.latestContext, undefined);
|
|
4024
|
+
});
|
|
4025
|
+
|
|
1687
4026
|
test("extension registers wrappers for all pi-usereq status hooks", () => {
|
|
1688
4027
|
const pi = createFakePi();
|
|
1689
4028
|
piUsereqExtension(pi);
|
|
@@ -1751,7 +4090,47 @@ test("context hook refreshes context usage and rounds progress cells upward", as
|
|
|
1751
4090
|
);
|
|
1752
4091
|
});
|
|
1753
4092
|
|
|
1754
|
-
|
|
4093
|
+
/**
|
|
4094
|
+
* @brief Verifies context-gauge icon cutovers below the error range.
|
|
4095
|
+
* @details Replays deterministic context percentages at documented threshold boundaries below `90%` so the status bar proves the `▕▂▏`, `▕▄▏`, `▕▆▏`, and `▕█▏` transitions while keeping the default-color contract intact. Runtime is O(n) in case count. Side effects are limited to in-memory status updates.
|
|
4096
|
+
* @return {Promise<void>} Promise resolved after all threshold assertions complete.
|
|
4097
|
+
* @throws {AssertionError} Throws when any threshold renders the wrong icon.
|
|
4098
|
+
* @satisfies TST-096
|
|
4099
|
+
*/
|
|
4100
|
+
test("context hook applies quarter-threshold gauge icons below the error range", async () => {
|
|
4101
|
+
const cwd = createTempDir("pi-usereq-context-threshold-status-");
|
|
4102
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
4103
|
+
const contextUsage = { tokens: 0, contextWindow: 1000, percent: 0 };
|
|
4104
|
+
const pi = createFakePi();
|
|
4105
|
+
piUsereqExtension(pi);
|
|
4106
|
+
const ctx = createFakeCtx(cwd, { selects: [] }, {
|
|
4107
|
+
getContextUsage: () => ({ ...contextUsage }),
|
|
4108
|
+
});
|
|
4109
|
+
|
|
4110
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
4111
|
+
|
|
4112
|
+
for (const percent of [24.9, 25, 49.9, 50, 74.9, 75, 89.9]) {
|
|
4113
|
+
contextUsage.tokens = Math.round(percent * 10);
|
|
4114
|
+
contextUsage.percent = percent;
|
|
4115
|
+
await pi.emit("context", { messages: [] }, ctx);
|
|
4116
|
+
|
|
4117
|
+
assert.equal(
|
|
4118
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
4119
|
+
buildExpectedFakeStatusText({
|
|
4120
|
+
basePath: buildExpectedFakeBasePath(cwd),
|
|
4121
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
4122
|
+
testsDir: "tests",
|
|
4123
|
+
srcDir: ["src"],
|
|
4124
|
+
contextFilledCells: Math.ceil(percent / 10),
|
|
4125
|
+
contextPercent: percent,
|
|
4126
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
4127
|
+
}),
|
|
4128
|
+
`unexpected context icon at ${percent}%`,
|
|
4129
|
+
);
|
|
4130
|
+
}
|
|
4131
|
+
});
|
|
4132
|
+
|
|
4133
|
+
test("context hook renders the error full context icon when usage exceeds ninety percent", async () => {
|
|
1755
4134
|
const cwd = createTempDir("pi-usereq-context-full-status-");
|
|
1756
4135
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1757
4136
|
const contextUsage = { tokens: 910, contextWindow: 1000, percent: 91 };
|
|
@@ -1778,14 +4157,14 @@ test("context hook renders the full context icon when usage exceeds ninety perce
|
|
|
1778
4157
|
);
|
|
1779
4158
|
});
|
|
1780
4159
|
|
|
1781
|
-
test("status overflow context icon uses only CLI-supported theme tokens", async () => {
|
|
4160
|
+
test("status overflow-threshold context icon uses only CLI-supported theme tokens", async () => {
|
|
1782
4161
|
const cwd = createTempDir("pi-usereq-context-theme-contract-");
|
|
1783
4162
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1784
4163
|
const usedColors = new Set<string>();
|
|
1785
4164
|
const pi = createFakePi();
|
|
1786
4165
|
piUsereqExtension(pi);
|
|
1787
4166
|
const ctx = createFakeCtx(cwd, { selects: [] }, {
|
|
1788
|
-
getContextUsage: () => ({ tokens:
|
|
4167
|
+
getContextUsage: () => ({ tokens: 1000, contextWindow: 1000, percent: 100 }),
|
|
1789
4168
|
theme: {
|
|
1790
4169
|
fg(color: string, text: string) {
|
|
1791
4170
|
assert.ok(["accent", "warning", "dim", "error"].includes(color));
|
|
@@ -1814,7 +4193,7 @@ test("status overflow context icon uses only CLI-supported theme tokens", async
|
|
|
1814
4193
|
testsDir: "tests",
|
|
1815
4194
|
srcDir: ["src"],
|
|
1816
4195
|
contextFilledCells: 10,
|
|
1817
|
-
contextPercent:
|
|
4196
|
+
contextPercent: 100,
|
|
1818
4197
|
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
1819
4198
|
}),
|
|
1820
4199
|
);
|
|
@@ -1924,6 +4303,71 @@ test("agent timing status updates et while preserving accumulated completed runt
|
|
|
1924
4303
|
}
|
|
1925
4304
|
});
|
|
1926
4305
|
|
|
4306
|
+
test("elapsed counters persist across new-session rebinding and reset on reload", async () => {
|
|
4307
|
+
const cwd = createTempDir("pi-usereq-timing-rebind-");
|
|
4308
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
4309
|
+
const originalDateNow = Date.now;
|
|
4310
|
+
let nowMs = 0;
|
|
4311
|
+
Date.now = () => nowMs;
|
|
4312
|
+
|
|
4313
|
+
try {
|
|
4314
|
+
const firstPi = createFakePi();
|
|
4315
|
+
piUsereqExtension(firstPi);
|
|
4316
|
+
const firstCtx = createFakeCtx(cwd);
|
|
4317
|
+
await firstPi.emit("session_start", { reason: "startup" }, firstCtx);
|
|
4318
|
+
await firstPi.emit("agent_start", {}, firstCtx);
|
|
4319
|
+
nowMs = 61_000;
|
|
4320
|
+
await firstPi.emit("agent_end", {
|
|
4321
|
+
messages: [
|
|
4322
|
+
{
|
|
4323
|
+
role: "assistant",
|
|
4324
|
+
stopReason: "stop",
|
|
4325
|
+
content: [],
|
|
4326
|
+
},
|
|
4327
|
+
],
|
|
4328
|
+
}, firstCtx);
|
|
4329
|
+
await firstPi.emit("session_shutdown", {}, firstCtx);
|
|
4330
|
+
|
|
4331
|
+
const newPi = createFakePi();
|
|
4332
|
+
piUsereqExtension(newPi);
|
|
4333
|
+
const newCtx = createFakeCtx(cwd);
|
|
4334
|
+
await newPi.emit("session_start", {
|
|
4335
|
+
reason: "new",
|
|
4336
|
+
previousSessionFile: "/tmp/previous-session.jsonl",
|
|
4337
|
+
}, newCtx);
|
|
4338
|
+
assert.equal(
|
|
4339
|
+
newCtx.__state.statuses.get("pi-usereq"),
|
|
4340
|
+
buildExpectedFakeStatusText({
|
|
4341
|
+
basePath: buildExpectedFakeBasePath(cwd),
|
|
4342
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
4343
|
+
testsDir: "tests",
|
|
4344
|
+
srcDir: ["src"],
|
|
4345
|
+
contextFilledCells: 0,
|
|
4346
|
+
et: "⏱︎ --:-- ⚑ 1:01 ⌛︎1:01",
|
|
4347
|
+
}),
|
|
4348
|
+
);
|
|
4349
|
+
await newPi.emit("session_shutdown", {}, newCtx);
|
|
4350
|
+
|
|
4351
|
+
const reloadPi = createFakePi();
|
|
4352
|
+
piUsereqExtension(reloadPi);
|
|
4353
|
+
const reloadCtx = createFakeCtx(cwd);
|
|
4354
|
+
await reloadPi.emit("session_start", { reason: "reload" }, reloadCtx);
|
|
4355
|
+
assert.equal(
|
|
4356
|
+
reloadCtx.__state.statuses.get("pi-usereq"),
|
|
4357
|
+
buildExpectedFakeStatusText({
|
|
4358
|
+
basePath: buildExpectedFakeBasePath(cwd),
|
|
4359
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
4360
|
+
testsDir: "tests",
|
|
4361
|
+
srcDir: ["src"],
|
|
4362
|
+
contextFilledCells: 0,
|
|
4363
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
4364
|
+
}),
|
|
4365
|
+
);
|
|
4366
|
+
} finally {
|
|
4367
|
+
Date.now = originalDateNow;
|
|
4368
|
+
}
|
|
4369
|
+
});
|
|
4370
|
+
|
|
1927
4371
|
test("session_start enables default custom tools and default embedded tools only", async () => {
|
|
1928
4372
|
const cwd = createTempDir("pi-usereq-default-tools-");
|
|
1929
4373
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
@@ -1936,14 +4380,67 @@ test("session_start enables default custom tools and default embedded tools only
|
|
|
1936
4380
|
for (const toolName of PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES) {
|
|
1937
4381
|
assert.ok(activeTools.has(toolName), `missing default active tool ${toolName}`);
|
|
1938
4382
|
}
|
|
4383
|
+
assert.equal(activeTools.has("search"), true);
|
|
4384
|
+
assert.equal(activeTools.has("files-search"), true);
|
|
1939
4385
|
assert.equal(activeTools.has("find"), false);
|
|
1940
4386
|
assert.equal(activeTools.has("grep"), false);
|
|
1941
4387
|
assert.equal(activeTools.has("ls"), false);
|
|
1942
4388
|
});
|
|
1943
4389
|
|
|
1944
|
-
test("default configuration applies the documented notify, sound, and pushover defaults", () => {
|
|
4390
|
+
test("default configuration applies the documented static-check, debug, notify, sound, and pushover defaults", () => {
|
|
1945
4391
|
const config = getDefaultConfig(createTempDir("pi-usereq-default-notify-"));
|
|
1946
4392
|
|
|
4393
|
+
assert.equal(config.AUTO_GIT_COMMIT, "enable");
|
|
4394
|
+
assert.deepEqual(Object.keys(config["static-check"]).length, 20);
|
|
4395
|
+
assert.deepEqual(config["static-check"].C, createStaticCheckLanguageConfig([
|
|
4396
|
+
{
|
|
4397
|
+
module: "Command",
|
|
4398
|
+
cmd: "cppcheck",
|
|
4399
|
+
params: [
|
|
4400
|
+
"--error-exitcode=1",
|
|
4401
|
+
"--enable=warning,style,performance,portability",
|
|
4402
|
+
"--std=c11",
|
|
4403
|
+
],
|
|
4404
|
+
},
|
|
4405
|
+
{
|
|
4406
|
+
module: "Command",
|
|
4407
|
+
cmd: "clang-format",
|
|
4408
|
+
params: ["--dry-run", "--Werror"],
|
|
4409
|
+
},
|
|
4410
|
+
], "enable"));
|
|
4411
|
+
assert.deepEqual(config["static-check"]["C++"], createStaticCheckLanguageConfig([
|
|
4412
|
+
{
|
|
4413
|
+
module: "Command",
|
|
4414
|
+
cmd: "cppcheck",
|
|
4415
|
+
params: [
|
|
4416
|
+
"--error-exitcode=1",
|
|
4417
|
+
"--enable=warning,style,performance,portability",
|
|
4418
|
+
"--std=c++20",
|
|
4419
|
+
],
|
|
4420
|
+
},
|
|
4421
|
+
{
|
|
4422
|
+
module: "Command",
|
|
4423
|
+
cmd: "clang-format",
|
|
4424
|
+
params: ["--dry-run", "--Werror"],
|
|
4425
|
+
},
|
|
4426
|
+
], "enable"));
|
|
4427
|
+
assert.deepEqual(config["static-check"].Python, createStaticCheckLanguageConfig([
|
|
4428
|
+
{ module: "Command", cmd: "pyright", params: ["--outputjson"] },
|
|
4429
|
+
{ module: "Command", cmd: "ruff", params: ["check"] },
|
|
4430
|
+
], "enable"));
|
|
4431
|
+
assert.deepEqual(config["static-check"].JavaScript, createStaticCheckLanguageConfig([
|
|
4432
|
+
{ module: "Command", cmd: "node", params: ["--check"] },
|
|
4433
|
+
], "enable"));
|
|
4434
|
+
assert.deepEqual(config["static-check"].TypeScript, createStaticCheckLanguageConfig([
|
|
4435
|
+
{ module: "Command", cmd: "npx", params: ["eslint"] },
|
|
4436
|
+
], "enable"));
|
|
4437
|
+
assert.deepEqual(config["static-check"].Ruby, createStaticCheckLanguageConfig([], "disable"));
|
|
4438
|
+
assert.equal(config.DEBUG_ENABLED, "disable");
|
|
4439
|
+
assert.equal(config.DEBUG_LOG_FILE, DEFAULT_DEBUG_LOG_FILE);
|
|
4440
|
+
assert.equal(config.DEBUG_WORKFLOW_EVENTS, DEFAULT_DEBUG_WORKFLOW_EVENTS);
|
|
4441
|
+
assert.equal(config.DEBUG_LOG_ON_STATUS, DEFAULT_DEBUG_LOG_ON_STATUS);
|
|
4442
|
+
assert.deepEqual(config.DEBUG_ENABLED_TOOLS, []);
|
|
4443
|
+
assert.deepEqual(config.DEBUG_ENABLED_PROMPTS, []);
|
|
1947
4444
|
assert.equal(config["notify-enabled"], false);
|
|
1948
4445
|
assert.equal(config["notify-on-completed"], true);
|
|
1949
4446
|
assert.equal(config["notify-on-interrupted"], false);
|
|
@@ -1960,22 +4457,40 @@ test("default configuration applies the documented notify, sound, and pushover d
|
|
|
1960
4457
|
assert.equal(config["notify-pushover-text"], "%%RESULT%%\n%%ARGS%%");
|
|
1961
4458
|
});
|
|
1962
4459
|
|
|
1963
|
-
test("configuration menu can enable embedded builtin tools", async () => {
|
|
4460
|
+
test("configuration menu orders tool toggles and can enable embedded builtin tools", async () => {
|
|
1964
4461
|
const cwd = createTempDir("pi-usereq-menu-embedded-");
|
|
1965
4462
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1966
4463
|
const pi = createFakePi();
|
|
1967
4464
|
piUsereqExtension(pi);
|
|
1968
4465
|
const command = pi.commands.get("pi-usereq");
|
|
1969
4466
|
assert.ok(command);
|
|
4467
|
+
const ctx = createFakeCtx(cwd, {
|
|
4468
|
+
selects: ["Enable tools", "Enable tools", "grep", "Save and close", "Save and close"],
|
|
4469
|
+
});
|
|
1970
4470
|
|
|
1971
|
-
await command!.handler(
|
|
1972
|
-
"",
|
|
1973
|
-
createFakeCtx(cwd, {
|
|
1974
|
-
selects: ["Enable tools", "Enable tools", "grep", "Save and close", "Save and close"],
|
|
1975
|
-
}),
|
|
1976
|
-
);
|
|
4471
|
+
await command!.handler("", ctx);
|
|
1977
4472
|
|
|
1978
4473
|
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
4474
|
+
assert.deepEqual(ctx.__state.selectCalls[2]?.items ?? [], [
|
|
4475
|
+
"compress",
|
|
4476
|
+
"references",
|
|
4477
|
+
"search",
|
|
4478
|
+
"static-check",
|
|
4479
|
+
"tokens",
|
|
4480
|
+
"files-compress",
|
|
4481
|
+
"files-references",
|
|
4482
|
+
"files-search",
|
|
4483
|
+
"files-static-check",
|
|
4484
|
+
"files-tokens",
|
|
4485
|
+
"bash",
|
|
4486
|
+
"edit",
|
|
4487
|
+
"read",
|
|
4488
|
+
"write",
|
|
4489
|
+
"find",
|
|
4490
|
+
"grep",
|
|
4491
|
+
"ls",
|
|
4492
|
+
"Reset defaults",
|
|
4493
|
+
]);
|
|
1979
4494
|
assert.ok(config["enabled-tools"].includes("grep"));
|
|
1980
4495
|
assert.ok(pi.getActiveTools().includes("grep"));
|
|
1981
4496
|
});
|
|
@@ -2076,6 +4591,8 @@ test("configuration menu can persist pushover settings", async () => {
|
|
|
2076
4591
|
const ctx = createFakeCtx(cwd, {
|
|
2077
4592
|
selects: [
|
|
2078
4593
|
"notifications",
|
|
4594
|
+
"Pushover User Key/Delivery Group Key",
|
|
4595
|
+
"Pushover Token/API Token Key",
|
|
2079
4596
|
"Enable pushover",
|
|
2080
4597
|
"Pushover events",
|
|
2081
4598
|
"Prompt failed",
|
|
@@ -2084,16 +4601,14 @@ test("configuration menu can persist pushover settings", async () => {
|
|
|
2084
4601
|
"High",
|
|
2085
4602
|
"Pushover title",
|
|
2086
4603
|
"Pushover text",
|
|
2087
|
-
"Pushover User Key/Delivery Group Key",
|
|
2088
|
-
"Pushover Token/API Token Key",
|
|
2089
4604
|
"Save and close",
|
|
2090
4605
|
"Save and close",
|
|
2091
4606
|
],
|
|
2092
4607
|
inputs: [
|
|
2093
|
-
"%%PROMT%% @ %%BASE%% [%%TIME%%]",
|
|
2094
|
-
"%%RESULT%%\n%%ARGS%%",
|
|
2095
4608
|
"gzfjjvp1xxmhibqwzh9m7i1zwvf83j",
|
|
2096
4609
|
"ah6bf5u2sj63mcvou6qamiabeoubbe",
|
|
4610
|
+
"%%PROMT%% @ %%BASE%% [%%TIME%%]",
|
|
4611
|
+
"%%RESULT%%\\n%%ARGS%%",
|
|
2097
4612
|
],
|
|
2098
4613
|
});
|
|
2099
4614
|
|
|
@@ -2123,16 +4638,40 @@ test("configuration menu can persist pushover settings", async () => {
|
|
|
2123
4638
|
);
|
|
2124
4639
|
});
|
|
2125
4640
|
|
|
2126
|
-
test("
|
|
2127
|
-
const cwd = createTempDir("pi-usereq-pushover-
|
|
4641
|
+
test("notifications menu escapes pushover text and locks pushover enablement until credentials exist", async () => {
|
|
4642
|
+
const cwd = createTempDir("pi-usereq-menu-pushover-display-");
|
|
2128
4643
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
4644
|
+
fs.writeFileSync(
|
|
4645
|
+
getProjectConfigPath(cwd),
|
|
4646
|
+
`${JSON.stringify({
|
|
2132
4647
|
...getDefaultConfig(cwd),
|
|
4648
|
+
"notify-pushover-text": "first line\nsecond line",
|
|
4649
|
+
}, null, 2)}\n`,
|
|
4650
|
+
"utf8",
|
|
4651
|
+
);
|
|
4652
|
+
const pi = createFakePi();
|
|
4653
|
+
piUsereqExtension(pi);
|
|
4654
|
+
const command = pi.commands.get("pi-usereq");
|
|
4655
|
+
assert.ok(command);
|
|
4656
|
+
const ctx = createFakeCtx(cwd, {
|
|
4657
|
+
selects: ["notifications", "Pushover text", "Save and close", "Save and close"],
|
|
4658
|
+
});
|
|
4659
|
+
|
|
4660
|
+
await command!.handler("", ctx);
|
|
4661
|
+
|
|
4662
|
+
const renderedNotificationsMenu = (ctx.__state.customRenderLines[2] ?? []).join("\n");
|
|
4663
|
+
assert.match(renderedNotificationsMenu, /first line\\nsecond line/);
|
|
4664
|
+
assert.match(renderedNotificationsMenu, /<dim>Enable pushover/);
|
|
4665
|
+
assert.match(renderedNotificationsMenu, /<dim>off<\/dim>/);
|
|
4666
|
+
});
|
|
4667
|
+
|
|
4668
|
+
test("prompt-end pushover requests honor global enable, event toggles, credentials, priority, and templates", async () => {
|
|
4669
|
+
const { projectBase: cwd } = initFixtureRepo({ fixtures: [] });
|
|
4670
|
+
const writeConfig = (overrides: Record<string, unknown>) => {
|
|
4671
|
+
writeProjectConfigOverrides(cwd, {
|
|
4672
|
+
GIT_WORKTREE_ENABLED: "disable",
|
|
2133
4673
|
...overrides,
|
|
2134
|
-
};
|
|
2135
|
-
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
4674
|
+
});
|
|
2136
4675
|
};
|
|
2137
4676
|
const pi = createFakePi();
|
|
2138
4677
|
piUsereqExtension(pi);
|
|
@@ -2308,12 +4847,12 @@ test("prompt-end pushover requests honor global enable, event toggles, credentia
|
|
|
2308
4847
|
} finally {
|
|
2309
4848
|
setPiNotifyHttpsRequestForTests(undefined);
|
|
2310
4849
|
Date.now = originalDateNow;
|
|
4850
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
2311
4851
|
}
|
|
2312
4852
|
});
|
|
2313
4853
|
|
|
2314
4854
|
test("pushover global enable controls status and suppresses delivery when disabled", async () => {
|
|
2315
|
-
const cwd =
|
|
2316
|
-
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
4855
|
+
const { projectBase: cwd } = initFixtureRepo({ fixtures: [] });
|
|
2317
4856
|
const pi = createFakePi();
|
|
2318
4857
|
piUsereqExtension(pi);
|
|
2319
4858
|
const command = pi.commands.get("pi-usereq");
|
|
@@ -2333,6 +4872,7 @@ test("pushover global enable controls status and suppresses delivery when disabl
|
|
|
2333
4872
|
});
|
|
2334
4873
|
|
|
2335
4874
|
await command!.handler("", ctx);
|
|
4875
|
+
writeProjectConfigOverrides(cwd, { GIT_WORKTREE_ENABLED: "disable" });
|
|
2336
4876
|
|
|
2337
4877
|
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
2338
4878
|
assert.equal(config["notify-pushover-enabled"], false);
|
|
@@ -2390,19 +4930,17 @@ test("pushover global enable controls status and suppresses delivery when disabl
|
|
|
2390
4930
|
assert.deepEqual(recordedRequests, []);
|
|
2391
4931
|
} finally {
|
|
2392
4932
|
setPiNotifyHttpsRequestForTests(undefined);
|
|
4933
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
2393
4934
|
}
|
|
2394
4935
|
});
|
|
2395
4936
|
|
|
2396
4937
|
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);
|
|
4938
|
+
const { projectBase: cwd } = initFixtureRepo({ fixtures: [] });
|
|
2400
4939
|
const writeConfig = (overrides: Record<string, unknown>) => {
|
|
2401
|
-
|
|
2402
|
-
|
|
4940
|
+
writeProjectConfigOverrides(cwd, {
|
|
4941
|
+
GIT_WORKTREE_ENABLED: "disable",
|
|
2403
4942
|
...overrides,
|
|
2404
|
-
};
|
|
2405
|
-
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
4943
|
+
});
|
|
2406
4944
|
};
|
|
2407
4945
|
const pi = createFakePi();
|
|
2408
4946
|
piUsereqExtension(pi);
|
|
@@ -2502,19 +5040,17 @@ test("command notify routes through PI_NOTIFY_CMD placeholders and per-event tog
|
|
|
2502
5040
|
} finally {
|
|
2503
5041
|
setPiNotifySpawnForTests(undefined);
|
|
2504
5042
|
Date.now = originalDateNow;
|
|
5043
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
2505
5044
|
}
|
|
2506
5045
|
});
|
|
2507
5046
|
|
|
2508
5047
|
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);
|
|
5048
|
+
const { projectBase: cwd } = initFixtureRepo({ fixtures: [] });
|
|
2512
5049
|
const writeConfig = (overrides: Record<string, unknown>) => {
|
|
2513
|
-
|
|
2514
|
-
|
|
5050
|
+
writeProjectConfigOverrides(cwd, {
|
|
5051
|
+
GIT_WORKTREE_ENABLED: "disable",
|
|
2515
5052
|
...overrides,
|
|
2516
|
-
};
|
|
2517
|
-
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
5053
|
+
});
|
|
2518
5054
|
};
|
|
2519
5055
|
const pi = createFakePi();
|
|
2520
5056
|
piUsereqExtension(pi);
|
|
@@ -2585,6 +5121,7 @@ test("sound routing honors the selected sound state and per-event toggles", asyn
|
|
|
2585
5121
|
assert.equal(recordedCommands.length, 1);
|
|
2586
5122
|
} finally {
|
|
2587
5123
|
setPiNotifySpawnForTests(undefined);
|
|
5124
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
2588
5125
|
}
|
|
2589
5126
|
});
|
|
2590
5127
|
|
|
@@ -2624,28 +5161,6 @@ test("sound toggle shortcut cycles persisted pi-notify sound levels", async () =
|
|
|
2624
5161
|
}
|
|
2625
5162
|
});
|
|
2626
5163
|
|
|
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
5164
|
test("static-check exposes the supported programming languages", () => {
|
|
2650
5165
|
const supported = getSupportedStaticCheckLanguageSupport();
|
|
2651
5166
|
assert.equal(supported.length, 20);
|
|
@@ -2670,8 +5185,15 @@ test("configuration menu omits removed static-check raw-spec and reference actio
|
|
|
2670
5185
|
await command!.handler("", ctx);
|
|
2671
5186
|
|
|
2672
5187
|
const renderedStaticCheckMenu = (ctx.__state.customRenderLines[1] ?? []).join("\n");
|
|
5188
|
+
const staticCheckItems = ctx.__state.selectCalls[1]?.items ?? [];
|
|
2673
5189
|
assert.match(renderedStaticCheckMenu, /Add static code checker/);
|
|
2674
5190
|
assert.match(renderedStaticCheckMenu, /Remove static code checker/);
|
|
5191
|
+
assert.deepEqual(staticCheckItems.slice(0, 2), ["Add static code checker", "Remove static code checker"]);
|
|
5192
|
+
assert.deepEqual(
|
|
5193
|
+
staticCheckItems.slice(2, 22),
|
|
5194
|
+
getSupportedStaticCheckLanguageSupport().map((entry) => entry.language),
|
|
5195
|
+
);
|
|
5196
|
+
assert.deepEqual(staticCheckItems.slice(-1), ["Reset defaults"]);
|
|
2675
5197
|
assert.doesNotMatch(renderedStaticCheckMenu, /Add entry from LANG=MODULE\[,CMD\[,PARAM\.\.\.\]\]/);
|
|
2676
5198
|
assert.doesNotMatch(renderedStaticCheckMenu, /Show supported languages/);
|
|
2677
5199
|
});
|
|
@@ -2696,6 +5218,29 @@ test("configuration menu hides removed static-check modules from user-facing act
|
|
|
2696
5218
|
assert.doesNotMatch(renderedStaticCheckMenu, /Dummy/);
|
|
2697
5219
|
});
|
|
2698
5220
|
|
|
5221
|
+
test("configuration menu can toggle per-language static-check enablement", async () => {
|
|
5222
|
+
const cwd = createTempDir("pi-usereq-menu-sc-toggle-");
|
|
5223
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
5224
|
+
const pi = createFakePi();
|
|
5225
|
+
piUsereqExtension(pi);
|
|
5226
|
+
const command = pi.commands.get("pi-usereq");
|
|
5227
|
+
assert.ok(command);
|
|
5228
|
+
|
|
5229
|
+
await command!.handler(
|
|
5230
|
+
"",
|
|
5231
|
+
createFakeCtx(cwd, {
|
|
5232
|
+
selects: ["static-check", "Python", "Save and close", "Save and close"],
|
|
5233
|
+
}),
|
|
5234
|
+
);
|
|
5235
|
+
|
|
5236
|
+
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
5237
|
+
assert.equal(config["static-check"].Python.enabled, "disable");
|
|
5238
|
+
assert.deepEqual(config["static-check"].Python.checkers, [
|
|
5239
|
+
{ module: "Command", cmd: "pyright", params: ["--outputjson"] },
|
|
5240
|
+
{ module: "Command", cmd: "ruff", params: ["check"] },
|
|
5241
|
+
]);
|
|
5242
|
+
});
|
|
5243
|
+
|
|
2699
5244
|
test("configuration menu can add guided static-check entries for explicit supported languages", async () => {
|
|
2700
5245
|
const cwd = createTempDir("pi-usereq-menu-sc-guided-");
|
|
2701
5246
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
@@ -2710,7 +5255,7 @@ test("configuration menu can add guided static-check entries for explicit suppor
|
|
|
2710
5255
|
selects: [
|
|
2711
5256
|
"static-check",
|
|
2712
5257
|
"Add static code checker",
|
|
2713
|
-
"
|
|
5258
|
+
"Ruby",
|
|
2714
5259
|
"Save and close",
|
|
2715
5260
|
"Save and close",
|
|
2716
5261
|
],
|
|
@@ -2719,5 +5264,7 @@ test("configuration menu can add guided static-check entries for explicit suppor
|
|
|
2719
5264
|
);
|
|
2720
5265
|
|
|
2721
5266
|
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
2722
|
-
assert.deepEqual(config["static-check"].
|
|
5267
|
+
assert.deepEqual(config["static-check"].Ruby, createStaticCheckLanguageConfig([
|
|
5268
|
+
{ module: "Command", cmd: "true" },
|
|
5269
|
+
], "enable"));
|
|
2723
5270
|
});
|