@pi-harness/pi-harness 0.1.23 → 0.1.25
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/README.md +14 -9
- package/apps/web/dist/assets/index-Deo3hxmM.js +25 -0
- package/apps/web/dist/assets/index-xF647pQJ.css +2 -0
- package/apps/web/dist/index.html +2 -2
- package/apps/web/package.json +6 -6
- package/apps/web/profile/cordis.yml +1 -0
- package/apps/web/server-dist/bin.js +27 -2
- package/apps/web/server-dist/bin.js.map +1 -1
- package/apps/web/src/server/bin.ts +27 -2
- package/apps/web/test/bin.test.ts +228 -5
- package/node_modules/@pi-harness/api-gateway/dist/index.d.ts.map +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/index.js +160 -69
- package/node_modules/@pi-harness/api-gateway/dist/index.js.map +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/agent-teams.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/anchored-standard.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/annotation.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/archify.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/at-file.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/auto-mode.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/better-sidebar.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/browser-fetch.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/browser-session.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/canvas-draw.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/change-verifier.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cleaner.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cli-notifier.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/code2skill.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/colleague-skill.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/context-doctor.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/context.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cost-meter.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/dependency-checker.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/docker-sandbox.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/fail-logger.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/genui.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/git-time-capsule.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/graph-memory.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/history-compressor.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/hol-guard.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/i18n-pair.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/image-compressor.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/llm-verifier.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mcp-client.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mcp-panel.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/memory.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mirage-bridge.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mock-server.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/modlens.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/module-search.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/obsidian-sync.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/openpets.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plan-execute.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-check.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-dev.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-finder.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-radar.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-stars.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/prompt-guard.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/prompt-library.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/readme-gen.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/recall-unread.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/reverse-skill.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/reviewer-bot.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/runtime-doctor.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/secure-audit.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-bookmarks.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-bridge.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-compare.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-export.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-insights.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-search.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/skill-catalog.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/skill-guard.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/sql-lens.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/synapse.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/tab-manager.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/taskboard.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/telemetry-blocker.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/test-harness.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/theme-studio.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/token-guard.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/turn-rewind.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/undo-savepoint.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/vision-toolkit.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/web-research.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/workspace-navigator.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/workspace-search.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/yaml-validator.json +1 -1
- package/node_modules/@pi-harness/api-gateway/package.json +1 -1
- package/node_modules/@pi-harness/cli/dist/main.d.ts +1 -0
- package/node_modules/@pi-harness/cli/dist/main.d.ts.map +1 -1
- package/node_modules/@pi-harness/cli/dist/main.js +11 -0
- package/node_modules/@pi-harness/cli/dist/main.js.map +1 -1
- package/node_modules/@pi-harness/cli/dist/relaunch.d.ts.map +1 -1
- package/node_modules/@pi-harness/cli/dist/relaunch.js +2 -4
- package/node_modules/@pi-harness/cli/dist/relaunch.js.map +1 -1
- package/node_modules/@pi-harness/cli/package.json +1 -1
- package/node_modules/@pi-harness/host-webserver/dist/index.d.ts +1 -0
- package/node_modules/@pi-harness/host-webserver/dist/index.d.ts.map +1 -1
- package/node_modules/@pi-harness/host-webserver/dist/index.js +43 -7
- package/node_modules/@pi-harness/host-webserver/dist/index.js.map +1 -1
- package/node_modules/@pi-harness/host-webserver/package.json +1 -1
- package/node_modules/@pi-harness/web-app/dist/index.d.ts.map +1 -1
- package/node_modules/@pi-harness/web-app/dist/index.js +12 -3
- package/node_modules/@pi-harness/web-app/dist/index.js.map +1 -1
- package/node_modules/@pi-harness/web-app/package.json +1 -1
- package/package.json +7 -7
- package/packages/api-gateway/dist/index.d.ts.map +1 -1
- package/packages/api-gateway/dist/index.js +160 -69
- package/packages/api-gateway/dist/index.js.map +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/agent-teams.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/anchored-standard.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/annotation.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/archify.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/at-file.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/auto-mode.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/better-sidebar.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/browser-fetch.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/browser-session.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/canvas-draw.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/change-verifier.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/cleaner.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/cli-notifier.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/code2skill.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/colleague-skill.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/context-doctor.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/context.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/cost-meter.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/dependency-checker.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/docker-sandbox.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/fail-logger.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/genui.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/git-time-capsule.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/graph-memory.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/history-compressor.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/hol-guard.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/i18n-pair.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/image-compressor.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/llm-verifier.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/mcp-client.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/mcp-panel.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/memory.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/mirage-bridge.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/mock-server.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/modlens.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/module-search.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/obsidian-sync.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/openpets.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/plan-execute.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/plugin-check.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/plugin-dev.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/plugin-finder.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/plugin-radar.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/plugin-stars.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/prompt-guard.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/prompt-library.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/readme-gen.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/recall-unread.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/reverse-skill.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/reviewer-bot.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/runtime-doctor.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/secure-audit.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/session-bookmarks.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/session-bridge.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/session-compare.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/session-export.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/session-insights.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/session-search.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/skill-catalog.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/skill-guard.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/sql-lens.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/synapse.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/tab-manager.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/taskboard.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/telemetry-blocker.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/test-harness.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/theme-studio.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/token-guard.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/turn-rewind.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/undo-savepoint.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/vision-toolkit.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/web-research.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/workspace-navigator.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/workspace-search.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/yaml-validator.json +1 -1
- package/packages/api-gateway/package.json +1 -1
- package/packages/api-gateway/src/index.ts +186 -88
- package/packages/api-gateway/src/marketplace-entries/official/agent-teams.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/anchored-standard.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/annotation.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/archify.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/at-file.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/auto-mode.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/better-sidebar.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/browser-fetch.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/browser-session.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/canvas-draw.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/change-verifier.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/cleaner.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/cli-notifier.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/code2skill.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/colleague-skill.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/context-doctor.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/context.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/cost-meter.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/dependency-checker.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/docker-sandbox.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/fail-logger.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/genui.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/git-time-capsule.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/graph-memory.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/history-compressor.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/hol-guard.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/i18n-pair.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/image-compressor.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/llm-verifier.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/mcp-client.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/mcp-panel.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/memory.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/mirage-bridge.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/mock-server.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/modlens.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/module-search.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/obsidian-sync.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/openpets.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/plan-execute.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/plugin-check.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/plugin-dev.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/plugin-finder.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/plugin-radar.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/plugin-stars.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/prompt-guard.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/prompt-library.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/readme-gen.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/recall-unread.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/reverse-skill.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/reviewer-bot.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/runtime-doctor.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/secure-audit.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/session-bookmarks.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/session-bridge.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/session-compare.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/session-export.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/session-insights.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/session-search.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/skill-catalog.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/skill-guard.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/sql-lens.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/synapse.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/tab-manager.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/taskboard.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/telemetry-blocker.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/test-harness.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/theme-studio.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/token-guard.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/turn-rewind.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/undo-savepoint.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/vision-toolkit.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/web-research.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/workspace-navigator.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/workspace-search.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/yaml-validator.json +1 -1
- package/packages/api-gateway/test/index.test.ts +656 -4
- package/packages/api-gateway/test/packed-package.test.ts +70 -0
- package/packages/api-gateway/test/reference-docs.test.ts +50 -0
- package/packages/api-gateway/test/release-package.test.ts +32 -1
- package/packages/api-gateway/test/release-version.test.ts +40 -0
- package/packages/bundle-web-app/dist/index.d.ts.map +1 -1
- package/packages/bundle-web-app/dist/index.js +12 -3
- package/packages/bundle-web-app/dist/index.js.map +1 -1
- package/packages/bundle-web-app/package.json +1 -1
- package/packages/bundle-web-app/src/index.ts +13 -3
- package/packages/bundle-web-app/test/index.test.ts +83 -2
- package/packages/cli/dist/main.d.ts +1 -0
- package/packages/cli/dist/main.d.ts.map +1 -1
- package/packages/cli/dist/main.js +11 -0
- package/packages/cli/dist/main.js.map +1 -1
- package/packages/cli/dist/relaunch.d.ts.map +1 -1
- package/packages/cli/dist/relaunch.js +2 -4
- package/packages/cli/dist/relaunch.js.map +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/main.ts +11 -0
- package/packages/cli/src/relaunch.ts +2 -3
- package/packages/cli/test/cli.test.ts +3 -1
- package/packages/cli/test/main.test.ts +134 -0
- package/packages/cli/test/relaunch.test.ts +86 -11
- package/packages/host-webserver/dist/index.d.ts +1 -0
- package/packages/host-webserver/dist/index.d.ts.map +1 -1
- package/packages/host-webserver/dist/index.js +43 -7
- package/packages/host-webserver/dist/index.js.map +1 -1
- package/packages/host-webserver/package.json +1 -1
- package/packages/host-webserver/src/index.ts +48 -8
- package/packages/host-webserver/test/index.test.ts +58 -5
- package/apps/web/dist/assets/index-BVI_IS-R.css +0 -2
- package/apps/web/dist/assets/index-Bo9CNI_U.js +0 -22
|
@@ -1,21 +1,62 @@
|
|
|
1
|
-
import { chmod, mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
1
|
+
import { chmod, lstat, mkdir, mkdtemp, readdir, readFile, realpath, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import { execFile as execFileCallback } from "node:child_process";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
|
-
import { join } from "node:path";
|
|
6
|
+
import { join, sep } from "node:path";
|
|
7
7
|
import { DatabaseSync } from "node:sqlite";
|
|
8
8
|
import { Context } from "@deepseek-ai/cordis";
|
|
9
9
|
import timerPlugin from "@deepseek-ai/cordis-plugin-timer";
|
|
10
10
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import { afterEach, describe, expect, test } from "vitest";
|
|
11
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
12
12
|
import webServerPlugin from "@pi-harness/host-webserver";
|
|
13
13
|
import { PiPluginUiRegistry, PiToolRegistry } from "@pi-harness/core";
|
|
14
|
+
import type { MarketplacePlugin } from "../src/marketplace.js";
|
|
14
15
|
import apiPlugin from "../src/index.js";
|
|
16
|
+
import type * as FsPromises from "node:fs/promises";
|
|
17
|
+
|
|
18
|
+
// The gateway reads the session metadata file through node:fs/promises; this pass-through mock lets one test hold a read open after its bytes arrived so lock ordering can be observed deterministically. Everything else goes straight to the real implementation.
|
|
19
|
+
const fsHooks = vi.hoisted(() => ({ afterReadFile: undefined as ((path: string) => Promise<void>) | undefined }));
|
|
20
|
+
vi.mock("node:fs/promises", async (importOriginal) => {
|
|
21
|
+
const actual = await importOriginal<typeof FsPromises>();
|
|
22
|
+
const readFile = async (path: unknown, options?: unknown) => {
|
|
23
|
+
const result: unknown = await (actual.readFile as (path: unknown, options?: unknown) => Promise<unknown>)(path, options);
|
|
24
|
+
if (fsHooks.afterReadFile) await fsHooks.afterReadFile(String(path));
|
|
25
|
+
return result;
|
|
26
|
+
};
|
|
27
|
+
return { ...actual, readFile };
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Every shipped catalog entry that npm has to install names a bare package, so `packageName` and its npm package name coincide and the install route cannot show whether it narrows the specifier. This entry is the case where the two differ: a deep import path whose npm package is only the first two segments. It is hoisted because the module mock below reads it while the test module body is still in its temporal dead zone.
|
|
31
|
+
const { subpathPlugin } = vi.hoisted(() => ({
|
|
32
|
+
subpathPlugin: {
|
|
33
|
+
id: "subpath-toolkit",
|
|
34
|
+
packageName: "@example-scope/toolkit/plugins/subpath",
|
|
35
|
+
version: "1.2.3",
|
|
36
|
+
name: "Subpath toolkit",
|
|
37
|
+
description: "Catalog entry whose profile name is a deep import of its npm package",
|
|
38
|
+
author: "example",
|
|
39
|
+
repository: "https://example.invalid/toolkit",
|
|
40
|
+
license: "MIT",
|
|
41
|
+
source: "community",
|
|
42
|
+
status: "experimental",
|
|
43
|
+
category: { id: "tools", label: "工具" },
|
|
44
|
+
capabilities: ["subpath-install"],
|
|
45
|
+
hooks: ["apply"],
|
|
46
|
+
profile: { name: "@example-scope/toolkit/plugins/subpath", config: {} },
|
|
47
|
+
} satisfies MarketplacePlugin,
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
// Only `MARKETPLACE_PLUGINS` is widened: the search and pagination helpers close over the module's own catalog, so the listing endpoints keep serving exactly the shipped entries.
|
|
51
|
+
vi.mock("../src/marketplace.js", async (importOriginal) => {
|
|
52
|
+
const actual = await importOriginal<{ MARKETPLACE_PLUGINS: readonly MarketplacePlugin[] }>();
|
|
53
|
+
return { ...actual, MARKETPLACE_PLUGINS: [...actual.MARKETPLACE_PLUGINS, subpathPlugin] };
|
|
54
|
+
});
|
|
15
55
|
|
|
16
56
|
const contexts: Context[] = [];
|
|
17
57
|
const temporaryDirectories: string[] = [];
|
|
18
58
|
const execFile = promisify(execFileCallback);
|
|
59
|
+
const sleep = (ms: number) => new Promise<void>((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
19
60
|
|
|
20
61
|
afterEach(async () => {
|
|
21
62
|
await Promise.all(contexts.splice(0).map(async (context) => context.fiber.dispose()));
|
|
@@ -2554,7 +2595,12 @@ describe("API gateway plugin", () => {
|
|
|
2554
2595
|
expect(failed.status).toBe(500);
|
|
2555
2596
|
await expect(failed.json()).resolves.toEqual({ error: "fatal: index file corrupt" });
|
|
2556
2597
|
|
|
2557
|
-
|
|
2598
|
+
// The gateway consumes NUL-separated `--porcelain -z` entries and then asks git for the cwd prefix, so the shim answers both calls.
|
|
2599
|
+
await writeFile(
|
|
2600
|
+
shim,
|
|
2601
|
+
"#!/bin/sh\ncase \"$1\" in rev-parse) exit 0;; esac\nprintf ' M tracked.txt\\0'\nseq 1 40000 | sed 's/^/?? untracked-/;s/$/.txt/' | tr '\\n' '\\0'\n",
|
|
2602
|
+
{ mode: 0o755 },
|
|
2603
|
+
);
|
|
2558
2604
|
const truncated = await fetch(context.webServer.url + "/api/files");
|
|
2559
2605
|
expect(truncated.status).toBe(200);
|
|
2560
2606
|
const payload = (await truncated.json()) as { items: { path: string }[]; truncated?: boolean };
|
|
@@ -2781,4 +2827,610 @@ describe("API gateway plugin", () => {
|
|
|
2781
2827
|
await expect(readFile(join(targetDir, targetFiles[0] ?? ""), "utf8")).resolves.toContain("x".repeat(100 * 1024));
|
|
2782
2828
|
await expect(stat(join(directory, "escape.jsonl"))).rejects.toMatchObject({ code: "ENOENT" });
|
|
2783
2829
|
});
|
|
2830
|
+
|
|
2831
|
+
test("decodes a request body whose multi-byte characters straddle chunk boundaries", async () => {
|
|
2832
|
+
const context = new Context();
|
|
2833
|
+
contexts.push(context);
|
|
2834
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2835
|
+
const session = { sessionId: "utf8-body-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
2836
|
+
const prompts: string[] = [];
|
|
2837
|
+
context.provide("piRuntime", {
|
|
2838
|
+
session,
|
|
2839
|
+
prompt: (prompt: string) => {
|
|
2840
|
+
prompts.push(prompt);
|
|
2841
|
+
return Promise.resolve();
|
|
2842
|
+
},
|
|
2843
|
+
} as never);
|
|
2844
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
2845
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
2846
|
+
await context.plugin(apiPlugin);
|
|
2847
|
+
|
|
2848
|
+
const prompt = "汉字".repeat(64);
|
|
2849
|
+
const encoded = new TextEncoder().encode(JSON.stringify({ prompt }));
|
|
2850
|
+
// Cuts the first 汉 in half, so a decoder that works chunk by chunk emits U+FFFD instead of the character.
|
|
2851
|
+
const split = 12;
|
|
2852
|
+
const body = new ReadableStream<Uint8Array>({
|
|
2853
|
+
async start(controller) {
|
|
2854
|
+
controller.enqueue(encoded.slice(0, split));
|
|
2855
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, 25));
|
|
2856
|
+
controller.enqueue(encoded.slice(split));
|
|
2857
|
+
controller.close();
|
|
2858
|
+
},
|
|
2859
|
+
});
|
|
2860
|
+
const response = await fetch(context.webServer.url + "/api/prompt", {
|
|
2861
|
+
method: "POST",
|
|
2862
|
+
headers: { "content-type": "application/json" },
|
|
2863
|
+
body,
|
|
2864
|
+
duplex: "half",
|
|
2865
|
+
} as RequestInit & { duplex: "half" });
|
|
2866
|
+
|
|
2867
|
+
expect(response.status).toBe(200);
|
|
2868
|
+
expect(prompts).toEqual([prompt]);
|
|
2869
|
+
});
|
|
2870
|
+
|
|
2871
|
+
test("keeps a message that a later trajectory event repeats instead of reporting it as circular", async () => {
|
|
2872
|
+
const context = new Context();
|
|
2873
|
+
contexts.push(context);
|
|
2874
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2875
|
+
type Listener = (event: { type: string; [key: string]: unknown }) => void;
|
|
2876
|
+
const listeners = new Set<Listener>();
|
|
2877
|
+
const message = { role: "assistant", content: [{ type: "text", text: "done" }], timestamp: 2 };
|
|
2878
|
+
const cyclic: Record<string, unknown> = { type: "turn_end" };
|
|
2879
|
+
cyclic.self = cyclic;
|
|
2880
|
+
const session = {
|
|
2881
|
+
sessionId: "shared-message-session",
|
|
2882
|
+
sessionFile: undefined,
|
|
2883
|
+
messages: [] as Array<Record<string, unknown>>,
|
|
2884
|
+
isStreaming: false,
|
|
2885
|
+
subscribe(listener: Listener) {
|
|
2886
|
+
listeners.add(listener);
|
|
2887
|
+
return () => listeners.delete(listener);
|
|
2888
|
+
},
|
|
2889
|
+
};
|
|
2890
|
+
context.provide("piRuntime", {
|
|
2891
|
+
session,
|
|
2892
|
+
prompt: () => {
|
|
2893
|
+
// Pi pushes the very object carried by the event into the message list, so both fields of the response reference one instance.
|
|
2894
|
+
session.messages.push(message);
|
|
2895
|
+
listeners.forEach((listener) => listener({ type: "message_end", message }));
|
|
2896
|
+
listeners.forEach((listener) => listener(cyclic as { type: string }));
|
|
2897
|
+
return Promise.resolve();
|
|
2898
|
+
},
|
|
2899
|
+
} as never);
|
|
2900
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
2901
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
2902
|
+
await context.plugin(apiPlugin);
|
|
2903
|
+
|
|
2904
|
+
await fetch(context.webServer.url + "/api/prompt", {
|
|
2905
|
+
method: "POST",
|
|
2906
|
+
headers: { "content-type": "application/json" },
|
|
2907
|
+
body: JSON.stringify({ prompt: "hello" }),
|
|
2908
|
+
});
|
|
2909
|
+
const response = await fetch(context.webServer.url + "/api/session");
|
|
2910
|
+
expect(response.status).toBe(200);
|
|
2911
|
+
const payload = (await response.json()) as { messages: unknown[]; events: Array<Record<string, unknown>> };
|
|
2912
|
+
expect(payload.messages).toEqual([message]);
|
|
2913
|
+
expect(payload.events[0]).toEqual({ type: "message_end", message });
|
|
2914
|
+
expect(payload.events[1]).toEqual({ type: "turn_end", self: "[Circular]" });
|
|
2915
|
+
});
|
|
2916
|
+
|
|
2917
|
+
test("returns git status paths verbatim when they hold spaces, non-ASCII characters or a rename", async () => {
|
|
2918
|
+
const context = new Context();
|
|
2919
|
+
contexts.push(context);
|
|
2920
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-quoted-paths-"));
|
|
2921
|
+
temporaryDirectories.push(directory);
|
|
2922
|
+
await execFile("git", ["init", "-q"], { cwd: directory });
|
|
2923
|
+
await execFile("git", ["config", "user.email", "pi-harness@test.invalid"], { cwd: directory });
|
|
2924
|
+
await execFile("git", ["config", "user.name", "Pi Harness Test"], { cwd: directory });
|
|
2925
|
+
await writeFile(join(directory, "notes draft.md"), "before\n", "utf8");
|
|
2926
|
+
await writeFile(join(directory, "old name.md"), "keep\n", "utf8");
|
|
2927
|
+
await execFile("git", ["add", "-A"], { cwd: directory });
|
|
2928
|
+
await execFile("git", ["commit", "-qm", "initial"], { cwd: directory });
|
|
2929
|
+
await writeFile(join(directory, "notes draft.md"), "after\n", "utf8");
|
|
2930
|
+
await execFile("git", ["mv", "old name.md", "renamed name.md"], { cwd: directory });
|
|
2931
|
+
await writeFile(join(directory, "汉字.txt"), "新建\n", "utf8");
|
|
2932
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2933
|
+
const session = { sessionId: "quoted-paths-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
2934
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve(), abort: () => Promise.resolve(), dispose: () => Promise.resolve() } as never);
|
|
2935
|
+
context.provide("piModels", { model: { provider: "test", id: "model" }, runtime: { getModels: () => [], getModel: () => undefined } } as never);
|
|
2936
|
+
context.provide("piHarnessLaunch", { cwd: directory, agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
2937
|
+
await context.plugin(apiPlugin);
|
|
2938
|
+
|
|
2939
|
+
const response = await fetch(context.webServer.url + "/api/files");
|
|
2940
|
+
expect(response.status).toBe(200);
|
|
2941
|
+
const payload = (await response.json()) as { items: { path: string; status: string; label: string }[] };
|
|
2942
|
+
expect(payload.items).toHaveLength(3);
|
|
2943
|
+
expect(payload.items).toEqual(
|
|
2944
|
+
expect.arrayContaining([
|
|
2945
|
+
{ path: "notes draft.md", status: "M", label: "modified" },
|
|
2946
|
+
{ path: "renamed name.md", status: "R", label: "modified" },
|
|
2947
|
+
{ path: "汉字.txt", status: "??", label: "untracked" },
|
|
2948
|
+
]),
|
|
2949
|
+
);
|
|
2950
|
+
|
|
2951
|
+
const modified = payload.items.find((item) => item.status === "M");
|
|
2952
|
+
const diff = await fetch(context.webServer.url + "/api/files/diff?path=" + encodeURIComponent(modified?.path ?? ""));
|
|
2953
|
+
expect(diff.status).toBe(200);
|
|
2954
|
+
const diffPayload = (await diff.json()) as { path: string; diff: string };
|
|
2955
|
+
expect(diffPayload.path).toBe("notes draft.md");
|
|
2956
|
+
expect(diffPayload.diff).toContain("after");
|
|
2957
|
+
|
|
2958
|
+
const commit = await fetch(context.webServer.url + "/api/files/commit", {
|
|
2959
|
+
method: "POST",
|
|
2960
|
+
headers: { "content-type": "application/json" },
|
|
2961
|
+
body: JSON.stringify({ paths: ["notes draft.md", "汉字.txt"], message: "Commit quoted paths" }),
|
|
2962
|
+
});
|
|
2963
|
+
expect(commit.status).toBe(200);
|
|
2964
|
+
const after = await execFile("git", ["status", "--short", "-z"], { cwd: directory });
|
|
2965
|
+
expect(after.stdout).not.toContain("notes draft.md");
|
|
2966
|
+
expect(after.stdout).not.toContain("汉字.txt");
|
|
2967
|
+
});
|
|
2968
|
+
|
|
2969
|
+
test("refuses to delete a session outside the session directory or without confirmation", async () => {
|
|
2970
|
+
const context = new Context();
|
|
2971
|
+
contexts.push(context);
|
|
2972
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-session-delete-"));
|
|
2973
|
+
temporaryDirectories.push(directory);
|
|
2974
|
+
const sessionDir = join(directory, "sessions");
|
|
2975
|
+
await mkdir(sessionDir);
|
|
2976
|
+
const victim = join(directory, "secrets.jsonl");
|
|
2977
|
+
await writeFile(victim, "keep me\n", "utf8");
|
|
2978
|
+
const note = join(sessionDir, "notes.txt");
|
|
2979
|
+
await writeFile(note, "keep me\n", "utf8");
|
|
2980
|
+
const target = join(sessionDir, "2026-08-30T00-00-00-000Z_target.jsonl");
|
|
2981
|
+
await writeFile(target, "{}\n", "utf8");
|
|
2982
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2983
|
+
const session = {
|
|
2984
|
+
sessionId: "session-delete-session",
|
|
2985
|
+
sessionFile: undefined,
|
|
2986
|
+
messages: [],
|
|
2987
|
+
isStreaming: false,
|
|
2988
|
+
sessionManager: { getSessionDir: () => sessionDir, isPersisted: () => true, getEntries: () => [] },
|
|
2989
|
+
subscribe: () => () => {},
|
|
2990
|
+
};
|
|
2991
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
2992
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
2993
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
2994
|
+
await context.plugin(apiPlugin);
|
|
2995
|
+
const post = (body: unknown) =>
|
|
2996
|
+
fetch(context.webServer.url + "/api/session/delete", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
2997
|
+
|
|
2998
|
+
const pinned = await fetch(context.webServer.url + "/api/session/metadata", {
|
|
2999
|
+
method: "POST",
|
|
3000
|
+
headers: { "content-type": "application/json" },
|
|
3001
|
+
body: JSON.stringify({ path: target, pinned: true }),
|
|
3002
|
+
});
|
|
3003
|
+
expect(pinned.status).toBe(200);
|
|
3004
|
+
|
|
3005
|
+
const outside = await post({ path: victim, confirm: true });
|
|
3006
|
+
expect(outside.status).toBe(400);
|
|
3007
|
+
await expect(outside.json()).resolves.toEqual({ error: "Invalid session path" });
|
|
3008
|
+
// The literal `..` segment has to survive onto the wire: path.join would collapse it into the plain `outside` path and the request would no longer escape anything.
|
|
3009
|
+
const traversalPath = `${sessionDir}${sep}..${sep}secrets.jsonl`;
|
|
3010
|
+
expect(traversalPath).not.toBe(victim);
|
|
3011
|
+
const traversal = await post({ path: traversalPath, confirm: true });
|
|
3012
|
+
expect(traversal.status).toBe(400);
|
|
3013
|
+
await expect(traversal.json()).resolves.toEqual({ error: "Invalid session path" });
|
|
3014
|
+
const suffix = await post({ path: note, confirm: true });
|
|
3015
|
+
expect(suffix.status).toBe(400);
|
|
3016
|
+
const unconfirmed = await post({ path: target });
|
|
3017
|
+
expect(unconfirmed.status).toBe(400);
|
|
3018
|
+
await expect(unconfirmed.json()).resolves.toEqual({ error: "confirm must be true to delete a session" });
|
|
3019
|
+
await expect(readFile(victim, "utf8")).resolves.toBe("keep me\n");
|
|
3020
|
+
await expect(readFile(note, "utf8")).resolves.toBe("keep me\n");
|
|
3021
|
+
await expect(readFile(target, "utf8")).resolves.toBe("{}\n");
|
|
3022
|
+
|
|
3023
|
+
const deleted = await post({ path: target, confirm: true });
|
|
3024
|
+
expect(deleted.status).toBe(200);
|
|
3025
|
+
await expect(deleted.json()).resolves.toMatchObject({ deleted: true, path: target });
|
|
3026
|
+
await expect(stat(target)).rejects.toMatchObject({ code: "ENOENT" });
|
|
3027
|
+
expect(JSON.parse(await readFile(join(sessionDir, ".pi-harness-session-meta.json"), "utf8"))).toEqual({});
|
|
3028
|
+
});
|
|
3029
|
+
|
|
3030
|
+
test("keeps batch deletion going past a failing session and persists the metadata it did remove", async () => {
|
|
3031
|
+
const context = new Context();
|
|
3032
|
+
contexts.push(context);
|
|
3033
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-batch-delete-"));
|
|
3034
|
+
temporaryDirectories.push(directory);
|
|
3035
|
+
const first = join(directory, "2026-08-30T00-00-00-000Z_first.jsonl");
|
|
3036
|
+
const missing = join(directory, "2026-08-30T00-00-01-000Z_missing.jsonl");
|
|
3037
|
+
const stuck = join(directory, "2026-08-30T00-00-02-000Z_stuck.jsonl");
|
|
3038
|
+
const last = join(directory, "2026-08-30T00-00-03-000Z_last.jsonl");
|
|
3039
|
+
await writeFile(first, "{}\n", "utf8");
|
|
3040
|
+
await writeFile(last, "{}\n", "utf8");
|
|
3041
|
+
// A directory cannot be unlinked, which is the closest reproducible stand-in for a session the harness is not allowed to remove.
|
|
3042
|
+
await mkdir(stuck);
|
|
3043
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
3044
|
+
const session = {
|
|
3045
|
+
sessionId: "batch-delete-session",
|
|
3046
|
+
sessionFile: undefined,
|
|
3047
|
+
messages: [],
|
|
3048
|
+
isStreaming: false,
|
|
3049
|
+
sessionManager: { getSessionDir: () => directory, isPersisted: () => true, getEntries: () => [] },
|
|
3050
|
+
subscribe: () => () => {},
|
|
3051
|
+
};
|
|
3052
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
3053
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
3054
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
3055
|
+
await context.plugin(apiPlugin);
|
|
3056
|
+
|
|
3057
|
+
for (const path of [first, missing, stuck, last]) {
|
|
3058
|
+
const pinned = await fetch(context.webServer.url + "/api/session/metadata", {
|
|
3059
|
+
method: "POST",
|
|
3060
|
+
headers: { "content-type": "application/json" },
|
|
3061
|
+
body: JSON.stringify({ path, pinned: true }),
|
|
3062
|
+
});
|
|
3063
|
+
expect(pinned.status).toBe(200);
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
const response = await fetch(context.webServer.url + "/api/sessions/batch", {
|
|
3067
|
+
method: "POST",
|
|
3068
|
+
headers: { "content-type": "application/json" },
|
|
3069
|
+
body: JSON.stringify({ action: "delete", paths: [first, missing, stuck, last], confirm: true }),
|
|
3070
|
+
});
|
|
3071
|
+
expect(response.status).toBe(409);
|
|
3072
|
+
await expect(response.json()).resolves.toMatchObject({ action: "delete", count: 3, failed: [{ path: stuck }] });
|
|
3073
|
+
await expect(stat(first)).rejects.toMatchObject({ code: "ENOENT" });
|
|
3074
|
+
await expect(stat(last)).rejects.toMatchObject({ code: "ENOENT" });
|
|
3075
|
+
expect((await stat(stuck)).isDirectory()).toBe(true);
|
|
3076
|
+
expect(JSON.parse(await readFile(join(directory, ".pi-harness-session-meta.json"), "utf8"))).toEqual({ [stuck]: { pinned: true } });
|
|
3077
|
+
});
|
|
3078
|
+
|
|
3079
|
+
test("installs a marketplace package into the package that owns the profile rather than the launch directory", async () => {
|
|
3080
|
+
const context = new Context();
|
|
3081
|
+
contexts.push(context);
|
|
3082
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-npm-install-"));
|
|
3083
|
+
temporaryDirectories.push(directory);
|
|
3084
|
+
const harnessDir = join(directory, "harness");
|
|
3085
|
+
const profileDir = join(harnessDir, "profile");
|
|
3086
|
+
const workspace = join(directory, "workspace");
|
|
3087
|
+
const shimDirectory = join(directory, "bin");
|
|
3088
|
+
await mkdir(profileDir, { recursive: true });
|
|
3089
|
+
await mkdir(workspace);
|
|
3090
|
+
await mkdir(shimDirectory);
|
|
3091
|
+
await writeFile(join(harnessDir, "package.json"), '{ "name": "harness" }\n', "utf8");
|
|
3092
|
+
const configPath = join(profileDir, "cordis.yml");
|
|
3093
|
+
await writeFile(configPath, '- id: runtime\n name: "@pi-harness/core/plugins/runtime"\n config: {}\n', "utf8");
|
|
3094
|
+
const npmLog = join(directory, "npm.log");
|
|
3095
|
+
await writeFile(join(shimDirectory, "npm"), `#!/bin/sh\nprintf '%s|%s\\n' "$(pwd)" "$*" >> ${JSON.stringify(npmLog)}\n`, { mode: 0o755 });
|
|
3096
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
3097
|
+
const session = { sessionId: "npm-install-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
3098
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
3099
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
3100
|
+
context.provide("piHarnessLaunch", { cwd: workspace, agentDir: workspace, configPath, args: [], requestExit() {} });
|
|
3101
|
+
const created: Array<{ name?: unknown }> = [];
|
|
3102
|
+
context.reflect.provide("loader", {
|
|
3103
|
+
entries: () => [],
|
|
3104
|
+
create: (options: { name?: unknown }) => {
|
|
3105
|
+
created.push(options);
|
|
3106
|
+
return Promise.resolve("marketplace-entry");
|
|
3107
|
+
},
|
|
3108
|
+
resolve: () => ({ fiber: { await: () => Promise.resolve() } }),
|
|
3109
|
+
remove: () => Promise.resolve(),
|
|
3110
|
+
});
|
|
3111
|
+
await context.plugin(apiPlugin);
|
|
3112
|
+
|
|
3113
|
+
const originalPath = process.env.PATH ?? "";
|
|
3114
|
+
process.env.PATH = `${shimDirectory}:${originalPath}`;
|
|
3115
|
+
let response: Response;
|
|
3116
|
+
try {
|
|
3117
|
+
response = await fetch(context.webServer.url + "/api/marketplace/install", {
|
|
3118
|
+
method: "POST",
|
|
3119
|
+
headers: { "content-type": "application/json" },
|
|
3120
|
+
body: JSON.stringify({ id: "cordis-timer" }),
|
|
3121
|
+
});
|
|
3122
|
+
} finally {
|
|
3123
|
+
process.env.PATH = originalPath;
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
expect(response.status).toBe(200);
|
|
3127
|
+
await expect(response.json()).resolves.toMatchObject({ installed: true, plugin: { id: "cordis-timer" } });
|
|
3128
|
+
const invocations = (await readFile(npmLog, "utf8")).trim().split("\n");
|
|
3129
|
+
expect(invocations).toHaveLength(1);
|
|
3130
|
+
const [invocationCwd, invocationArgs] = (invocations[0] ?? "").split("|");
|
|
3131
|
+
expect(await realpath(invocationCwd ?? "")).toBe(await realpath(harnessDir));
|
|
3132
|
+
expect(invocationArgs).toMatch(/^install --save-exact --package-lock=false @deepseek-ai\/cordis-plugin-timer@\d/);
|
|
3133
|
+
expect(created).toHaveLength(1);
|
|
3134
|
+
await expect(readFile(configPath, "utf8")).resolves.toContain('name: "@deepseek-ai/cordis-plugin-timer"');
|
|
3135
|
+
await expect(stat(join(workspace, "package.json"))).rejects.toMatchObject({ code: "ENOENT" });
|
|
3136
|
+
});
|
|
3137
|
+
|
|
3138
|
+
test("hands npm the package name rather than the deep import path the profile entry uses", async () => {
|
|
3139
|
+
const context = new Context();
|
|
3140
|
+
contexts.push(context);
|
|
3141
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-npm-subpath-"));
|
|
3142
|
+
temporaryDirectories.push(directory);
|
|
3143
|
+
const harnessDir = join(directory, "harness");
|
|
3144
|
+
const profileDir = join(harnessDir, "profile");
|
|
3145
|
+
const shimDirectory = join(directory, "bin");
|
|
3146
|
+
await mkdir(profileDir, { recursive: true });
|
|
3147
|
+
await mkdir(shimDirectory);
|
|
3148
|
+
await writeFile(join(harnessDir, "package.json"), '{ "name": "harness" }\n', "utf8");
|
|
3149
|
+
const configPath = join(profileDir, "cordis.yml");
|
|
3150
|
+
await writeFile(configPath, '- id: runtime\n name: "@pi-harness/core/plugins/runtime"\n config: {}\n', "utf8");
|
|
3151
|
+
const npmLog = join(directory, "npm.log");
|
|
3152
|
+
await writeFile(join(shimDirectory, "npm"), `#!/bin/sh\nprintf '%s\\n' "$*" >> ${JSON.stringify(npmLog)}\n`, { mode: 0o755 });
|
|
3153
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
3154
|
+
const session = { sessionId: "npm-subpath-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
3155
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
3156
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
3157
|
+
context.provide("piHarnessLaunch", { cwd: harnessDir, agentDir: harnessDir, configPath, args: [], requestExit() {} });
|
|
3158
|
+
const created: Array<{ name?: unknown }> = [];
|
|
3159
|
+
context.reflect.provide("loader", {
|
|
3160
|
+
entries: () => [],
|
|
3161
|
+
create: (options: { name?: unknown }) => {
|
|
3162
|
+
created.push(options);
|
|
3163
|
+
return Promise.resolve("marketplace-entry");
|
|
3164
|
+
},
|
|
3165
|
+
resolve: () => ({ fiber: { await: () => Promise.resolve() } }),
|
|
3166
|
+
remove: () => Promise.resolve(),
|
|
3167
|
+
});
|
|
3168
|
+
await context.plugin(apiPlugin);
|
|
3169
|
+
|
|
3170
|
+
const originalPath = process.env.PATH ?? "";
|
|
3171
|
+
process.env.PATH = `${shimDirectory}:${originalPath}`;
|
|
3172
|
+
let response: Response;
|
|
3173
|
+
try {
|
|
3174
|
+
response = await fetch(context.webServer.url + "/api/marketplace/install", {
|
|
3175
|
+
method: "POST",
|
|
3176
|
+
headers: { "content-type": "application/json" },
|
|
3177
|
+
body: JSON.stringify({ id: subpathPlugin.id }),
|
|
3178
|
+
});
|
|
3179
|
+
} finally {
|
|
3180
|
+
process.env.PATH = originalPath;
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
expect(response.status).toBe(200);
|
|
3184
|
+
await expect(response.json()).resolves.toMatchObject({ installed: true, plugin: { id: subpathPlugin.id } });
|
|
3185
|
+
// `npm install @example-scope/toolkit/plugins/subpath@1.2.3` is not a specifier npm can resolve, so the deep import path must be narrowed to its package before it reaches the process.
|
|
3186
|
+
expect((await readFile(npmLog, "utf8")).trim()).toBe("install --save-exact --package-lock=false @example-scope/toolkit@1.2.3");
|
|
3187
|
+
expect(created).toEqual([expect.objectContaining({ name: "@example-scope/toolkit/plugins/subpath" })]);
|
|
3188
|
+
await expect(readFile(configPath, "utf8")).resolves.toContain('name: "@example-scope/toolkit/plugins/subpath"');
|
|
3189
|
+
});
|
|
3190
|
+
|
|
3191
|
+
test("removes the files npm created when the installed marketplace plugin fails to load", async () => {
|
|
3192
|
+
const context = new Context();
|
|
3193
|
+
contexts.push(context);
|
|
3194
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-npm-rollback-"));
|
|
3195
|
+
temporaryDirectories.push(directory);
|
|
3196
|
+
const harnessDir = join(directory, "harness");
|
|
3197
|
+
const profileDir = join(harnessDir, "profile");
|
|
3198
|
+
const shimDirectory = join(directory, "bin");
|
|
3199
|
+
await mkdir(profileDir, { recursive: true });
|
|
3200
|
+
await mkdir(shimDirectory);
|
|
3201
|
+
await writeFile(join(harnessDir, "package.json"), '{ "name": "harness" }\n', "utf8");
|
|
3202
|
+
const configPath = join(profileDir, "cordis.yml");
|
|
3203
|
+
const profileBefore = '- id: runtime\n name: "@pi-harness/core/plugins/runtime"\n config: {}\n';
|
|
3204
|
+
await writeFile(configPath, profileBefore, "utf8");
|
|
3205
|
+
await writeFile(join(shimDirectory, "npm"), '#!/bin/sh\nprintf \'{ "lockfileVersion": 3 }\\n\' > "$(pwd)/package-lock.json"\n', { mode: 0o755 });
|
|
3206
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
3207
|
+
const session = { sessionId: "npm-rollback-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
3208
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
3209
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
3210
|
+
context.provide("piHarnessLaunch", { cwd: harnessDir, agentDir: harnessDir, configPath, args: [], requestExit() {} });
|
|
3211
|
+
context.reflect.provide("loader", {
|
|
3212
|
+
entries: () => [],
|
|
3213
|
+
create: () => Promise.reject(new Error("plugin entry failed to load")),
|
|
3214
|
+
remove: () => Promise.resolve(),
|
|
3215
|
+
});
|
|
3216
|
+
await context.plugin(apiPlugin);
|
|
3217
|
+
|
|
3218
|
+
const originalPath = process.env.PATH ?? "";
|
|
3219
|
+
process.env.PATH = `${shimDirectory}:${originalPath}`;
|
|
3220
|
+
let response: Response;
|
|
3221
|
+
try {
|
|
3222
|
+
response = await fetch(context.webServer.url + "/api/marketplace/install", {
|
|
3223
|
+
method: "POST",
|
|
3224
|
+
headers: { "content-type": "application/json" },
|
|
3225
|
+
body: JSON.stringify({ id: "cordis-timer" }),
|
|
3226
|
+
});
|
|
3227
|
+
} finally {
|
|
3228
|
+
process.env.PATH = originalPath;
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
expect(response.status).toBe(502);
|
|
3232
|
+
await expect(response.json()).resolves.toEqual({ error: "plugin entry failed to load" });
|
|
3233
|
+
await expect(readFile(configPath, "utf8")).resolves.toBe(profileBefore);
|
|
3234
|
+
await expect(readFile(join(harnessDir, "package.json"), "utf8")).resolves.toBe('{ "name": "harness" }\n');
|
|
3235
|
+
await expect(stat(join(harnessDir, "package-lock.json"))).rejects.toMatchObject({ code: "ENOENT" });
|
|
3236
|
+
});
|
|
3237
|
+
|
|
3238
|
+
test("propagates metadata read errors other than ENOENT instead of quarantining a file it could not read", async () => {
|
|
3239
|
+
const context = new Context();
|
|
3240
|
+
contexts.push(context);
|
|
3241
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-metadata-eisdir-"));
|
|
3242
|
+
temporaryDirectories.push(directory);
|
|
3243
|
+
const metadataFile = join(directory, ".pi-harness-session-meta.json");
|
|
3244
|
+
// A directory at the metadata path makes readFile fail with EISDIR while rename would still succeed, which is exactly the shape of a transient read failure.
|
|
3245
|
+
await mkdir(metadataFile);
|
|
3246
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
3247
|
+
const session = {
|
|
3248
|
+
sessionId: "metadata-session",
|
|
3249
|
+
sessionFile: undefined,
|
|
3250
|
+
messages: [],
|
|
3251
|
+
isStreaming: false,
|
|
3252
|
+
sessionManager: { getSessionDir: () => directory, isPersisted: () => true, getEntries: () => [] },
|
|
3253
|
+
subscribe: () => () => {},
|
|
3254
|
+
};
|
|
3255
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
3256
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
3257
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
3258
|
+
await context.plugin(apiPlugin);
|
|
3259
|
+
|
|
3260
|
+
const list = await fetch(context.webServer.url + "/api/sessions");
|
|
3261
|
+
expect(list.status).toBe(500);
|
|
3262
|
+
const listBody = (await list.json()) as { error: string };
|
|
3263
|
+
expect(listBody.error).toContain("EISDIR");
|
|
3264
|
+
const update = await fetch(context.webServer.url + "/api/session/metadata", {
|
|
3265
|
+
method: "POST",
|
|
3266
|
+
headers: { "content-type": "application/json" },
|
|
3267
|
+
body: JSON.stringify({ path: join(directory, "2026-08-30T00-00-00-000Z_pinned.jsonl"), pinned: true }),
|
|
3268
|
+
});
|
|
3269
|
+
expect(update.status).toBe(400);
|
|
3270
|
+
expect((await stat(metadataFile)).isDirectory()).toBe(true);
|
|
3271
|
+
expect((await readdir(directory)).filter((name) => name.startsWith(".pi-harness-session-meta.json.corrupt-"))).toEqual([]);
|
|
3272
|
+
});
|
|
3273
|
+
|
|
3274
|
+
test("serializes the session list read with metadata mutations so a stale quarantine cannot discard a fresh write", async () => {
|
|
3275
|
+
const context = new Context();
|
|
3276
|
+
contexts.push(context);
|
|
3277
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-metadata-race-"));
|
|
3278
|
+
temporaryDirectories.push(directory);
|
|
3279
|
+
const metadataFile = join(directory, ".pi-harness-session-meta.json");
|
|
3280
|
+
const corrupt = '{ "other.jsonl": { "arc';
|
|
3281
|
+
await writeFile(metadataFile, corrupt, "utf8");
|
|
3282
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
3283
|
+
const session = {
|
|
3284
|
+
sessionId: "metadata-session",
|
|
3285
|
+
sessionFile: undefined,
|
|
3286
|
+
messages: [],
|
|
3287
|
+
isStreaming: false,
|
|
3288
|
+
sessionManager: { getSessionDir: () => directory, isPersisted: () => true, getEntries: () => [] },
|
|
3289
|
+
subscribe: () => () => {},
|
|
3290
|
+
};
|
|
3291
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
3292
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
3293
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
3294
|
+
await context.plugin(apiPlugin);
|
|
3295
|
+
|
|
3296
|
+
// Hold the first metadata read after its (corrupt) bytes arrived: the reader now owns the lock but has not parsed yet.
|
|
3297
|
+
let release = () => {};
|
|
3298
|
+
const held = new Promise<void>((resolveHeld) => {
|
|
3299
|
+
let first = true;
|
|
3300
|
+
fsHooks.afterReadFile = async (path) => {
|
|
3301
|
+
if (path !== metadataFile || !first) return;
|
|
3302
|
+
first = false;
|
|
3303
|
+
resolveHeld();
|
|
3304
|
+
await new Promise<void>((resolveRelease) => {
|
|
3305
|
+
release = resolveRelease;
|
|
3306
|
+
});
|
|
3307
|
+
};
|
|
3308
|
+
});
|
|
3309
|
+
try {
|
|
3310
|
+
const list = fetch(context.webServer.url + "/api/sessions");
|
|
3311
|
+
await held;
|
|
3312
|
+
const path = join(directory, "2026-08-30T00-00-00-000Z_pinned.jsonl");
|
|
3313
|
+
const update = fetch(context.webServer.url + "/api/session/metadata", {
|
|
3314
|
+
method: "POST",
|
|
3315
|
+
headers: { "content-type": "application/json" },
|
|
3316
|
+
body: JSON.stringify({ path, pinned: true }),
|
|
3317
|
+
});
|
|
3318
|
+
await expect(Promise.race([update.then(() => "settled"), sleep(300).then(() => "pending")])).resolves.toBe("pending");
|
|
3319
|
+
release();
|
|
3320
|
+
expect((await list).status).toBe(200);
|
|
3321
|
+
const updated = await update;
|
|
3322
|
+
expect(updated.status).toBe(200);
|
|
3323
|
+
await expect(updated.json()).resolves.toEqual({ path, metadata: { pinned: true } });
|
|
3324
|
+
expect(JSON.parse(await readFile(metadataFile, "utf8"))).toEqual({ [path]: { pinned: true } });
|
|
3325
|
+
const quarantined = (await readdir(directory)).filter((name) => name.startsWith(".pi-harness-session-meta.json.corrupt-"));
|
|
3326
|
+
expect(quarantined).toHaveLength(1);
|
|
3327
|
+
await expect(readFile(join(directory, quarantined[0] ?? ""), "utf8")).resolves.toBe(corrupt);
|
|
3328
|
+
} finally {
|
|
3329
|
+
fsHooks.afterReadFile = undefined;
|
|
3330
|
+
release();
|
|
3331
|
+
}
|
|
3332
|
+
});
|
|
3333
|
+
|
|
3334
|
+
test("lets git commit outlive the read-only timeout so slow hooks are not killed mid-commit", { timeout: 40_000 }, async () => {
|
|
3335
|
+
const context = new Context();
|
|
3336
|
+
contexts.push(context);
|
|
3337
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-git-slow-commit-"));
|
|
3338
|
+
temporaryDirectories.push(directory);
|
|
3339
|
+
const shimDirectory = join(directory, "bin");
|
|
3340
|
+
await mkdir(shimDirectory);
|
|
3341
|
+
// A commit that takes longer than the 15 s read-only bound, as a pre-commit hook running a test suite would.
|
|
3342
|
+
await writeFile(join(shimDirectory, "git"), '#!/bin/sh\ncase "$1" in commit) sleep 16;; rev-parse) printf abc1234;; esac\nexit 0\n', { mode: 0o755 });
|
|
3343
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
3344
|
+
const session = { sessionId: "commit-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
3345
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve(), abort: () => Promise.resolve(), dispose: () => Promise.resolve() } as never);
|
|
3346
|
+
context.provide("piModels", { model: { provider: "test", id: "model" }, runtime: { getModels: () => [], getModel: () => undefined } } as never);
|
|
3347
|
+
context.provide("piHarnessLaunch", { cwd: directory, agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
3348
|
+
await context.plugin(apiPlugin);
|
|
3349
|
+
|
|
3350
|
+
const originalPath = process.env.PATH ?? "";
|
|
3351
|
+
process.env.PATH = `${shimDirectory}:${originalPath}`;
|
|
3352
|
+
try {
|
|
3353
|
+
const response = await fetch(context.webServer.url + "/api/files/commit", {
|
|
3354
|
+
method: "POST",
|
|
3355
|
+
headers: { "content-type": "application/json" },
|
|
3356
|
+
body: JSON.stringify({ paths: ["README.md"], message: "Slow hook" }),
|
|
3357
|
+
});
|
|
3358
|
+
expect(response.status).toBe(200);
|
|
3359
|
+
await expect(response.json()).resolves.toEqual({ committed: true, message: "Slow hook", commit: "abc1234" });
|
|
3360
|
+
} finally {
|
|
3361
|
+
process.env.PATH = originalPath;
|
|
3362
|
+
}
|
|
3363
|
+
});
|
|
3364
|
+
|
|
3365
|
+
test("reports workspace paths relative to a subdirectory cwd so diff and revert resolve them", async () => {
|
|
3366
|
+
const context = new Context();
|
|
3367
|
+
contexts.push(context);
|
|
3368
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-files-subdir-"));
|
|
3369
|
+
temporaryDirectories.push(directory);
|
|
3370
|
+
await execFile("git", ["init", "-q"], { cwd: directory });
|
|
3371
|
+
await execFile("git", ["config", "user.email", "pi-harness@test.invalid"], { cwd: directory });
|
|
3372
|
+
await execFile("git", ["config", "user.name", "Pi Harness Test"], { cwd: directory });
|
|
3373
|
+
await mkdir(join(directory, "sub", "nested"), { recursive: true });
|
|
3374
|
+
await writeFile(join(directory, "sub", "nested", "inner.txt"), "before\n", "utf8");
|
|
3375
|
+
await writeFile(join(directory, "root.txt"), "before\n", "utf8");
|
|
3376
|
+
await execFile("git", ["add", "-A"], { cwd: directory });
|
|
3377
|
+
await execFile("git", ["commit", "-qm", "initial"], { cwd: directory });
|
|
3378
|
+
await writeFile(join(directory, "sub", "nested", "inner.txt"), "after\n", "utf8");
|
|
3379
|
+
await writeFile(join(directory, "root.txt"), "after\n", "utf8");
|
|
3380
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
3381
|
+
const session = { sessionId: "files-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
3382
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve(), abort: () => Promise.resolve(), dispose: () => Promise.resolve() } as never);
|
|
3383
|
+
context.provide("piModels", { model: { provider: "test", id: "model" }, runtime: { getModels: () => [], getModel: () => undefined } } as never);
|
|
3384
|
+
context.provide("piHarnessLaunch", { cwd: join(directory, "sub"), agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
3385
|
+
await context.plugin(apiPlugin);
|
|
3386
|
+
|
|
3387
|
+
const list = await fetch(context.webServer.url + "/api/files");
|
|
3388
|
+
const payload = (await list.json()) as { items: { path: string }[] };
|
|
3389
|
+
expect(payload.items.map((item) => item.path).sort()).toEqual(["../root.txt", "nested/inner.txt"]);
|
|
3390
|
+
const diff = await fetch(context.webServer.url + "/api/files/diff?path=" + encodeURIComponent("nested/inner.txt"));
|
|
3391
|
+
const nestedDiffBody = (await diff.json()) as { path: string; diff: string };
|
|
3392
|
+
expect(nestedDiffBody.path).toBe("nested/inner.txt");
|
|
3393
|
+
expect(nestedDiffBody.diff).toContain("+after");
|
|
3394
|
+
const revert = await fetch(context.webServer.url + "/api/files/revert", {
|
|
3395
|
+
method: "POST",
|
|
3396
|
+
headers: { "content-type": "application/json" },
|
|
3397
|
+
body: JSON.stringify({ paths: ["nested/inner.txt"], confirm: true }),
|
|
3398
|
+
});
|
|
3399
|
+
expect(revert.status).toBe(200);
|
|
3400
|
+
await expect(readFile(join(directory, "sub", "nested", "inner.txt"), "utf8")).resolves.toBe("before\n");
|
|
3401
|
+
await expect(readFile(join(directory, "root.txt"), "utf8")).resolves.toBe("after\n");
|
|
3402
|
+
});
|
|
3403
|
+
|
|
3404
|
+
test("writes through a symlinked settings.json instead of replacing the link with a regular file", async () => {
|
|
3405
|
+
const context = new Context();
|
|
3406
|
+
contexts.push(context);
|
|
3407
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-agent-symlink-"));
|
|
3408
|
+
temporaryDirectories.push(directory);
|
|
3409
|
+
const agentDir = join(directory, "agent");
|
|
3410
|
+
const dotfiles = join(directory, "dotfiles");
|
|
3411
|
+
await mkdir(agentDir);
|
|
3412
|
+
await mkdir(dotfiles);
|
|
3413
|
+
const realSettings = join(dotfiles, "settings.json");
|
|
3414
|
+
const settingsPath = join(agentDir, "settings.json");
|
|
3415
|
+
await writeFile(realSettings, '{"stale":true}\n', "utf8");
|
|
3416
|
+
await symlink(realSettings, settingsPath);
|
|
3417
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
3418
|
+
const settingsManager = new Proxy({}, { get: () => () => ({}) });
|
|
3419
|
+
const session = { sessionId: "config-session", sessionFile: undefined, messages: [], isStreaming: false, settingsManager, subscribe: () => () => {} };
|
|
3420
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
3421
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
3422
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir, args: [], requestExit() {} });
|
|
3423
|
+
await context.plugin(apiPlugin);
|
|
3424
|
+
|
|
3425
|
+
const response = await fetch(context.webServer.url + "/api/config/source", {
|
|
3426
|
+
method: "POST",
|
|
3427
|
+
headers: { "content-type": "application/json" },
|
|
3428
|
+
body: JSON.stringify({ source: '{"defaultProvider":"test"}' }),
|
|
3429
|
+
});
|
|
3430
|
+
expect(response.status).toBe(200);
|
|
3431
|
+
expect((await lstat(settingsPath)).isSymbolicLink()).toBe(true);
|
|
3432
|
+
await expect(readFile(realSettings, "utf8")).resolves.toBe('{\n "defaultProvider": "test"\n}\n');
|
|
3433
|
+
expect((await readdir(dotfiles)).filter((name) => name.endsWith(".tmp"))).toEqual([]);
|
|
3434
|
+
expect((await readdir(agentDir)).filter((name) => name.endsWith(".tmp"))).toEqual([]);
|
|
3435
|
+
});
|
|
2784
3436
|
});
|