octocode-cli 1.2.8 → 1.2.9
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 +42 -35
- package/out/octocode-cli.js +36 -11767
- package/package.json +36 -36
- package/skills/README.md +42 -114
- package/skills/{octocode-code-engineer → octocode-engineer}/.claude/settings.local.json +2 -1
- package/skills/octocode-engineer/README.md +99 -0
- package/skills/octocode-engineer/SKILL.md +499 -0
- package/skills/octocode-engineer/build.mjs +29 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/eslint.config.mjs +3 -13
- package/skills/{octocode-code-engineer → octocode-engineer}/package.json +28 -27
- package/skills/octocode-engineer/references/ast-reference.md +166 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/references/cli-reference.md +80 -6
- package/skills/octocode-engineer/references/externals.md +86 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/references/output-files.md +46 -6
- package/skills/octocode-engineer/references/quality-indicators.md +202 -0
- package/skills/octocode-engineer/references/tool-workflows.md +298 -0
- package/skills/octocode-engineer/references/validation-playbooks.md +99 -0
- package/skills/octocode-engineer/scripts/ast/search.js +45 -0
- package/skills/octocode-engineer/scripts/ast/tree-search.js +27 -0
- package/skills/octocode-engineer/scripts/index.js +173 -0
- package/skills/octocode-engineer/scripts/run.js +179 -0
- package/skills/octocode-engineer/src/analysis/dependencies.ts +378 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/discovery.test.ts +57 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/discovery.ts +43 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/search.test.ts +113 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/search.ts +64 -1
- package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/tree-sitter.test.ts +118 -2
- package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/tree-sitter.ts +65 -3
- package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/ts-analyzer.test.ts +281 -1
- package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/ts-analyzer.ts +173 -3
- package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/security.test.ts +73 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/security.ts +62 -4
- package/skills/octocode-engineer/src/detector-gating.test.ts +59 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/code-quality.ts +342 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/index.ts +8 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/index.test.ts +565 -11
- package/skills/octocode-engineer/src/index.ts +468 -0
- package/skills/octocode-engineer/src/pipeline/affected.test.ts +147 -0
- package/skills/octocode-engineer/src/pipeline/affected.ts +68 -0
- package/skills/octocode-engineer/src/pipeline/baseline.test.ts +276 -0
- package/skills/octocode-engineer/src/pipeline/baseline.ts +76 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/cli.test.ts +300 -53
- package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/cli.ts +180 -36
- package/skills/octocode-engineer/src/pipeline/config-loader.test.ts +264 -0
- package/skills/octocode-engineer/src/pipeline/config-loader.ts +109 -0
- package/skills/octocode-engineer/src/pipeline/create-options.ts +55 -0
- package/skills/octocode-engineer/src/pipeline/health-score.test.ts +65 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/main.ts +130 -17
- package/skills/octocode-engineer/src/pipeline/progress.ts +51 -0
- package/skills/octocode-engineer/src/pipeline/reporters.test.ts +155 -0
- package/skills/octocode-engineer/src/pipeline/reporters.ts +64 -0
- package/skills/octocode-engineer/src/reporting/graph-features.test.ts +279 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/reporting/output-contract.test.ts +6 -0
- package/skills/octocode-engineer/src/reporting/summary-md.test.ts +1066 -0
- package/skills/octocode-engineer/src/reporting/summary-md.ts +1604 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/reporting/writer.ts +136 -13
- package/skills/octocode-engineer/src/run.ts +78 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/sanity.test.ts +1 -1
- package/skills/octocode-engineer/src/types/analysis.ts +25 -0
- package/skills/octocode-engineer/src/types/collectors.ts +134 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/types/constants.ts +75 -41
- package/skills/octocode-engineer/src/types/core.ts +203 -0
- package/skills/octocode-engineer/src/types/dependency.ts +215 -0
- package/skills/octocode-engineer/src/types/file-entry.ts +108 -0
- package/skills/octocode-engineer/src/types/findings.ts +105 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/types/index.ts +60 -30
- package/skills/octocode-engineer/src/types/tree-sitter.ts +38 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/tsconfig.json +1 -0
- package/skills/octocode-research/.octocode/scan/.cache/analysis-cache.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/architecture.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/ast-trees.txt +5566 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/code-quality.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/dead-code.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/file-inventory.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/findings.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/graph.md +189 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/security.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/summary.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/summary.md +265 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/architecture.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/ast-trees.txt +5555 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/code-quality.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/dead-code.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/file-inventory.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/findings.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/graph.md +190 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/security.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/summary.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/summary.md +265 -0
- package/skills/octocode-research/CHANGELOG.md +60 -0
- package/skills/octocode-research/README.md +102 -388
- package/skills/octocode-research/SKILL.md +169 -498
- package/skills/octocode-research/package.json +19 -31
- package/skills/octocode-research/references/PARALLEL_AGENT_PROTOCOL.md +19 -0
- package/skills/octocode-research/references/SESSION_MANAGEMENT.md +38 -0
- package/skills/octocode-research/scripts/server-init.js +1 -1
- package/skills/octocode-research/scripts/server.d.ts +2 -1
- package/skills/octocode-research/scripts/server.js +329 -233
- package/skills/octocode-research/src/__tests__/integration/promptsRoutes.test.ts +180 -0
- package/skills/octocode-research/src/__tests__/integration/serverHttp.test.ts +221 -0
- package/skills/octocode-research/src/__tests__/integration/serverLifecycle.test.ts +194 -0
- package/skills/octocode-research/src/__tests__/integration/toolsRoutes.test.ts +501 -0
- package/skills/octocode-research/src/__tests__/unit/readiness.test.ts +61 -0
- package/skills/octocode-research/src/__tests__/unit/resilience.test.ts +192 -0
- package/skills/octocode-research/src/__tests__/unit/responseFactory.test.ts +172 -0
- package/skills/octocode-research/src/__tests__/unit/responseParser.test.ts +288 -0
- package/skills/octocode-research/src/__tests__/unit/schemas.test.ts +509 -0
- package/skills/octocode-research/src/index.ts +4 -124
- package/skills/octocode-research/src/middleware/queryParser.ts +0 -26
- package/skills/octocode-research/src/routes/lsp.ts +58 -59
- package/skills/octocode-research/src/routes/package.ts +35 -65
- package/skills/octocode-research/src/routes/prompts.ts +3 -3
- package/skills/octocode-research/src/routes/tools.ts +8 -20
- package/skills/octocode-research/src/server-init.ts +30 -237
- package/skills/octocode-research/src/server.ts +50 -23
- package/skills/octocode-research/src/types/errorGuards.ts +9 -80
- package/skills/octocode-research/src/types/guards.ts +0 -28
- package/skills/octocode-research/src/types/mcp.ts +11 -66
- package/skills/octocode-research/src/types/responses.ts +11 -129
- package/skills/octocode-research/src/utils/circuitBreaker.ts +0 -21
- package/skills/octocode-research/src/utils/logger.ts +1 -97
- package/skills/octocode-research/src/utils/resilience.ts +2 -12
- package/skills/octocode-research/src/utils/responseFactory.ts +0 -42
- package/skills/octocode-research/src/utils/responseParser.ts +3 -25
- package/skills/octocode-research/src/utils/retry.ts +0 -63
- package/skills/octocode-research/src/utils/routeFactory.ts +1 -1
- package/skills/octocode-research/src/validation/httpPreprocess.ts +0 -3
- package/skills/octocode-research/src/validation/index.ts +0 -1
- package/skills/octocode-research/src/validation/schemas.ts +0 -63
- package/skills/octocode-research/src/validation/toolCallSchema.ts +3 -3
- package/skills/octocode-research/tsdown.config.ts +4 -0
- package/skills/octocode-research/vitest.config.ts +3 -0
- package/skills/octocode-code-engineer/.plan/VALIDATED_PLAN.md +0 -223
- package/skills/octocode-code-engineer/README.md +0 -178
- package/skills/octocode-code-engineer/SKILL.md +0 -418
- package/skills/octocode-code-engineer/minify-scripts.mjs +0 -32
- package/skills/octocode-code-engineer/references/agent-ast-reading-rfc.md +0 -95
- package/skills/octocode-code-engineer/references/architecture-techniques.md +0 -121
- package/skills/octocode-code-engineer/references/ast-search.md +0 -210
- package/skills/octocode-code-engineer/references/ast-tree-search.md +0 -151
- package/skills/octocode-code-engineer/references/concepts.md +0 -107
- package/skills/octocode-code-engineer/references/finding-categories.md +0 -128
- package/skills/octocode-code-engineer/references/improvement-roadmap.md +0 -304
- package/skills/octocode-code-engineer/references/playbooks.md +0 -204
- package/skills/octocode-code-engineer/references/present-results.md +0 -136
- package/skills/octocode-code-engineer/references/tool-workflows.md +0 -566
- package/skills/octocode-code-engineer/references/validate-investigate.md +0 -225
- package/skills/octocode-code-engineer/scripts/analysis/dependencies.js +0 -1
- package/skills/octocode-code-engineer/scripts/analysis/dependency-summary.js +0 -1
- package/skills/octocode-code-engineer/scripts/analysis/discovery.js +0 -1
- package/skills/octocode-code-engineer/scripts/analysis/graph-analytics.js +0 -1
- package/skills/octocode-code-engineer/scripts/analysis/semantic.js +0 -1
- package/skills/octocode-code-engineer/scripts/ast/helpers.js +0 -1
- package/skills/octocode-code-engineer/scripts/ast/metrics.js +0 -1
- package/skills/octocode-code-engineer/scripts/ast/search.js +0 -2
- package/skills/octocode-code-engineer/scripts/ast/tree-search.js +0 -2
- package/skills/octocode-code-engineer/scripts/ast/tree-sitter.js +0 -1
- package/skills/octocode-code-engineer/scripts/ast/ts-analyzer.js +0 -1
- package/skills/octocode-code-engineer/scripts/collectors/chains.js +0 -1
- package/skills/octocode-code-engineer/scripts/collectors/effects.js +0 -1
- package/skills/octocode-code-engineer/scripts/collectors/input-sources.js +0 -1
- package/skills/octocode-code-engineer/scripts/collectors/performance.js +0 -1
- package/skills/octocode-code-engineer/scripts/collectors/prototype-pollution.js +0 -1
- package/skills/octocode-code-engineer/scripts/collectors/security.js +0 -1
- package/skills/octocode-code-engineer/scripts/collectors/test-profile.js +0 -1
- package/skills/octocode-code-engineer/scripts/common/is-direct-run.js +0 -1
- package/skills/octocode-code-engineer/scripts/common/utils.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/code-quality.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/cohesion.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/coupling.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/cycle.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/dead-code.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/import-style.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/index.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/security.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/semantic.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/shared.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/test-quality.js +0 -1
- package/skills/octocode-code-engineer/scripts/index.js +0 -1
- package/skills/octocode-code-engineer/scripts/pipeline/cache.js +0 -1
- package/skills/octocode-code-engineer/scripts/pipeline/cli.js +0 -1
- package/skills/octocode-code-engineer/scripts/pipeline/main.js +0 -2
- package/skills/octocode-code-engineer/scripts/reporting/analysis.js +0 -1
- package/skills/octocode-code-engineer/scripts/reporting/summary-md.js +0 -1
- package/skills/octocode-code-engineer/scripts/reporting/writer.js +0 -1
- package/skills/octocode-code-engineer/scripts/types/constants.js +0 -1
- package/skills/octocode-code-engineer/scripts/types/index.js +0 -1
- package/skills/octocode-code-engineer/scripts/types/interfaces.js +0 -1
- package/skills/octocode-code-engineer/src/analysis/dependencies.ts +0 -406
- package/skills/octocode-code-engineer/src/index.ts +0 -403
- package/skills/octocode-code-engineer/src/reporting/summary-md.test.ts +0 -421
- package/skills/octocode-code-engineer/src/reporting/summary-md.ts +0 -714
- package/skills/octocode-code-engineer/src/types/interfaces.ts +0 -682
- package/skills/octocode-research/src/types/toolTypes.ts +0 -33
- package/skills/octocode-research/src/utils/logEmoji.ts +0 -103
- /package/skills/{octocode-code-engineer → octocode-engineer}/.octocode/rfc/RFC-code-engineer-weakness-fixes.md +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/architecture.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/ast-helpers.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/ast-search.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/base.css +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/block-navigation.js +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/cache.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/cli.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/clover.xml +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-effects.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-input-sources.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-performance.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-prototype-pollution.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-security.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-test-profile.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/coverage-final.json +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/dependencies.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/dependency-summary.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/discovery.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/favicon.png +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/graph-analytics.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/index.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/index.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/metrics.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/pipeline.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/prettify.css +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/prettify.js +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/report-analysis.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/report-writer.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/security-detectors.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/semantic-detectors.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/semantic.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/sort-arrow-sprite.png +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/sorter.js +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/summary-md.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/test-quality-detectors.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/tree-sitter-analyzer.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/ts-analyzer.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/types.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/utils.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/dependencies.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/dependency-summary.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/dependency-summary.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/graph-analytics.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/graph-analytics.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/semantic.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/semantic.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/helpers.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/helpers.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/metrics.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/metrics.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/tree-search.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/tree-search.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/chains.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/effects.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/effects.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/input-sources.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/input-sources.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/performance.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/performance.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/prototype-pollution.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/prototype-pollution.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/test-profile.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/test-profile.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/common/is-direct-run.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/common/is-direct-run.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/common/utils.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/common/utils.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/cohesion.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/coupling.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/cycle.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/dead-code.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/import-style.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/index.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/security.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/security.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/semantic.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/shared.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/test-quality.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/test-quality.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/cache.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/cache.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/main.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/reporting/analysis.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/reporting/analysis.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/vitest.config.ts +0 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
4
|
+
import express from 'express';
|
|
5
|
+
import request from 'supertest';
|
|
6
|
+
import { errorHandler } from '../../middleware/errorHandler.js';
|
|
7
|
+
|
|
8
|
+
vi.mock('../../mcpCache.js', () => ({
|
|
9
|
+
getMcpContent: vi.fn().mockReturnValue({
|
|
10
|
+
tools: {},
|
|
11
|
+
prompts: {
|
|
12
|
+
research: {
|
|
13
|
+
name: 'Research',
|
|
14
|
+
description: 'Start a code research session',
|
|
15
|
+
args: [
|
|
16
|
+
{ name: 'goal', description: 'The research goal', required: true },
|
|
17
|
+
{ name: 'context', description: 'Additional context', required: false },
|
|
18
|
+
],
|
|
19
|
+
content: 'You are a code research agent...',
|
|
20
|
+
},
|
|
21
|
+
research_local: {
|
|
22
|
+
name: 'Research Local',
|
|
23
|
+
description: 'Research local codebase',
|
|
24
|
+
args: [{ name: 'goal', description: 'What to investigate', required: true }],
|
|
25
|
+
content: 'Investigate the local codebase...',
|
|
26
|
+
},
|
|
27
|
+
plan: {
|
|
28
|
+
name: 'Plan',
|
|
29
|
+
description: 'Plan an implementation',
|
|
30
|
+
args: [],
|
|
31
|
+
content: 'Plan the implementation steps...',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
instructions: 'Test',
|
|
35
|
+
baseHints: [],
|
|
36
|
+
genericErrorHints: [],
|
|
37
|
+
}),
|
|
38
|
+
initializeMcpContent: vi.fn().mockResolvedValue({}),
|
|
39
|
+
isMcpInitialized: vi.fn().mockReturnValue(true),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
vi.mock('../../index.js', () => ({
|
|
43
|
+
logPromptCall: vi.fn().mockResolvedValue(undefined),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
vi.mock('../../utils/asyncTimeout.js', () => ({
|
|
47
|
+
fireAndForgetWithTimeout: vi.fn(),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
import { promptsRoutes } from '../../routes/prompts.js';
|
|
51
|
+
|
|
52
|
+
function createApp(): any {
|
|
53
|
+
const app = express();
|
|
54
|
+
app.use(express.json());
|
|
55
|
+
app.use('/prompts', promptsRoutes);
|
|
56
|
+
app.use(errorHandler);
|
|
57
|
+
return app;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('Prompts Routes', () => {
|
|
61
|
+
let app: any;
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
app = createApp();
|
|
65
|
+
vi.clearAllMocks();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('GET /prompts/list', () => {
|
|
69
|
+
it('returns 200 with all prompts', async () => {
|
|
70
|
+
const res = await request(app).get('/prompts/list');
|
|
71
|
+
expect(res.status).toBe(200);
|
|
72
|
+
expect(res.body.success).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('returns correct number of prompts', async () => {
|
|
76
|
+
const res = await request(app).get('/prompts/list');
|
|
77
|
+
expect(res.body.data.prompts).toHaveLength(3);
|
|
78
|
+
expect(res.body.data.totalCount).toBe(3);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('includes version', async () => {
|
|
82
|
+
const res = await request(app).get('/prompts/list');
|
|
83
|
+
expect(res.body.data).toHaveProperty('version');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('each prompt has name and description', async () => {
|
|
87
|
+
const res = await request(app).get('/prompts/list');
|
|
88
|
+
for (const prompt of res.body.data.prompts) {
|
|
89
|
+
expect(prompt).toHaveProperty('name');
|
|
90
|
+
expect(prompt).toHaveProperty('description');
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('uses key name not display name', async () => {
|
|
95
|
+
const res = await request(app).get('/prompts/list');
|
|
96
|
+
const names = res.body.data.prompts.map((p: any) => p.name);
|
|
97
|
+
expect(names).toContain('research');
|
|
98
|
+
expect(names).toContain('research_local');
|
|
99
|
+
expect(names).toContain('plan');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('includes arguments when present', async () => {
|
|
103
|
+
const res = await request(app).get('/prompts/list');
|
|
104
|
+
const research = res.body.data.prompts.find((p: any) => p.name === 'research');
|
|
105
|
+
expect(research.arguments).toHaveLength(2);
|
|
106
|
+
expect(research.arguments[0]).toEqual({
|
|
107
|
+
name: 'goal',
|
|
108
|
+
description: 'The research goal',
|
|
109
|
+
required: true,
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('includes hints', async () => {
|
|
114
|
+
const res = await request(app).get('/prompts/list');
|
|
115
|
+
expect(res.body.hints).toBeDefined();
|
|
116
|
+
expect(res.body.hints[0]).toContain('/prompts/info/');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('GET /prompts/info/:promptName', () => {
|
|
121
|
+
it('returns prompt details for valid prompt', async () => {
|
|
122
|
+
const res = await request(app).get('/prompts/info/research');
|
|
123
|
+
expect(res.status).toBe(200);
|
|
124
|
+
expect(res.body.success).toBe(true);
|
|
125
|
+
expect(res.body.data.name).toBe('Research');
|
|
126
|
+
expect(res.body.data.description).toBe('Start a code research session');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('includes prompt content', async () => {
|
|
130
|
+
const res = await request(app).get('/prompts/info/research');
|
|
131
|
+
expect(res.body.data.content).toBe('You are a code research agent...');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('includes arguments with required field', async () => {
|
|
135
|
+
const res = await request(app).get('/prompts/info/research');
|
|
136
|
+
expect(res.body.data.arguments).toHaveLength(2);
|
|
137
|
+
expect(res.body.data.arguments[0].required).toBe(true);
|
|
138
|
+
expect(res.body.data.arguments[1].required).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('returns prompt with no arguments', async () => {
|
|
142
|
+
const res = await request(app).get('/prompts/info/plan');
|
|
143
|
+
expect(res.status).toBe(200);
|
|
144
|
+
expect(res.body.data.name).toBe('Plan');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('returns 404 for unknown prompt', async () => {
|
|
148
|
+
const res = await request(app).get('/prompts/info/nonExistent');
|
|
149
|
+
expect(res.status).toBe(404);
|
|
150
|
+
expect(res.body.success).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('provides helpful hints for unknown prompt', async () => {
|
|
154
|
+
const res = await request(app).get('/prompts/info/badPrompt');
|
|
155
|
+
expect(res.body.hints).toBeDefined();
|
|
156
|
+
expect(res.body.hints.some((h: string) => h.includes('badPrompt'))).toBe(true);
|
|
157
|
+
expect(res.body.hints.some((h: string) => h.includes('Available prompts'))).toBe(true);
|
|
158
|
+
expect(res.body.hints.some((h: string) => h.includes('/prompts/list'))).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('logs prompt call for telemetry', async () => {
|
|
162
|
+
await request(app).get('/prompts/info/research');
|
|
163
|
+
const { fireAndForgetWithTimeout } = await import('../../utils/asyncTimeout.js');
|
|
164
|
+
expect(fireAndForgetWithTimeout).toHaveBeenCalled();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('Readiness gate', () => {
|
|
169
|
+
it('returns 503 when not initialized', async () => {
|
|
170
|
+
const { isMcpInitialized } = await import('../../mcpCache.js');
|
|
171
|
+
vi.mocked(isMcpInitialized).mockReturnValue(false);
|
|
172
|
+
|
|
173
|
+
const res = await request(app).get('/prompts/list');
|
|
174
|
+
expect(res.status).toBe(503);
|
|
175
|
+
expect(res.body.error.code).toBe('SERVER_INITIALIZING');
|
|
176
|
+
|
|
177
|
+
vi.mocked(isMcpInitialized).mockReturnValue(true);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
4
|
+
import request from 'supertest';
|
|
5
|
+
|
|
6
|
+
vi.mock('../../mcpCache.js', () => ({
|
|
7
|
+
getMcpContent: vi.fn().mockReturnValue({
|
|
8
|
+
tools: { localSearchCode: { name: 'localSearchCode', description: 'Search', schema: {}, hints: { hasResults: [], empty: [] } } },
|
|
9
|
+
prompts: { research: { name: 'research', description: 'Research', args: [], content: 'test' } },
|
|
10
|
+
instructions: 'Test instructions',
|
|
11
|
+
baseHints: [],
|
|
12
|
+
genericErrorHints: [],
|
|
13
|
+
baseSchema: {},
|
|
14
|
+
}),
|
|
15
|
+
initializeMcpContent: vi.fn().mockResolvedValue({}),
|
|
16
|
+
isMcpInitialized: vi.fn().mockReturnValue(true),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock('../../index.js', () => {
|
|
20
|
+
const r = { content: [{ type: 'text', text: 'results:\n - status: hasResults\n data:\n files: []\n totalMatches: 0' }] };
|
|
21
|
+
return {
|
|
22
|
+
initializeProviders: vi.fn().mockResolvedValue(undefined),
|
|
23
|
+
initializeSession: vi.fn(),
|
|
24
|
+
logSessionInit: vi.fn().mockResolvedValue(undefined),
|
|
25
|
+
logToolCall: vi.fn().mockResolvedValue(undefined),
|
|
26
|
+
logPromptCall: vi.fn().mockResolvedValue(undefined),
|
|
27
|
+
localSearchCode: vi.fn().mockResolvedValue(r),
|
|
28
|
+
localGetFileContent: vi.fn().mockResolvedValue(r),
|
|
29
|
+
localFindFiles: vi.fn().mockResolvedValue(r),
|
|
30
|
+
localViewStructure: vi.fn().mockResolvedValue(r),
|
|
31
|
+
githubSearchCode: vi.fn().mockResolvedValue(r),
|
|
32
|
+
githubGetFileContent: vi.fn().mockResolvedValue(r),
|
|
33
|
+
githubViewRepoStructure: vi.fn().mockResolvedValue(r),
|
|
34
|
+
githubSearchRepositories: vi.fn().mockResolvedValue(r),
|
|
35
|
+
githubSearchPullRequests: vi.fn().mockResolvedValue(r),
|
|
36
|
+
lspGotoDefinition: vi.fn().mockResolvedValue(r),
|
|
37
|
+
lspFindReferences: vi.fn().mockResolvedValue(r),
|
|
38
|
+
lspCallHierarchy: vi.fn().mockResolvedValue(r),
|
|
39
|
+
packageSearch: vi.fn().mockResolvedValue(r),
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
vi.mock('../../utils/logger.js', () => ({
|
|
44
|
+
initializeLogger: vi.fn(),
|
|
45
|
+
getLogsPath: vi.fn().mockReturnValue('/tmp/logs'),
|
|
46
|
+
logToolCall: vi.fn(),
|
|
47
|
+
logError: vi.fn(),
|
|
48
|
+
logWarn: vi.fn(),
|
|
49
|
+
sanitizeQueryParams: vi.fn().mockReturnValue({}),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
vi.mock('../../utils/circuitBreaker.js', () => ({
|
|
53
|
+
getAllCircuitStates: vi.fn().mockReturnValue({}),
|
|
54
|
+
clearAllCircuits: vi.fn(),
|
|
55
|
+
stopCircuitCleanup: vi.fn(),
|
|
56
|
+
configureCircuit: vi.fn(),
|
|
57
|
+
withCircuitBreaker: vi.fn((_name: string, fn: () => any) => fn()),
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
vi.mock('../../utils/resilience.js', () => ({
|
|
61
|
+
withGitHubResilience: vi.fn(async (fn: () => any) => fn()),
|
|
62
|
+
withLocalResilience: vi.fn(async (fn: () => any) => fn()),
|
|
63
|
+
withLspResilience: vi.fn(async (fn: () => any) => fn()),
|
|
64
|
+
withPackageResilience: vi.fn(async (fn: () => any) => fn()),
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
vi.mock('../../utils/asyncTimeout.js', () => ({
|
|
68
|
+
fireAndForgetWithTimeout: vi.fn(),
|
|
69
|
+
withTimeout: vi.fn(async (fn: () => any) => fn()),
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
vi.mock('../../utils/errorQueue.js', () => ({
|
|
73
|
+
errorQueue: {
|
|
74
|
+
getRecent: vi.fn().mockReturnValue([]),
|
|
75
|
+
push: vi.fn(),
|
|
76
|
+
size: 0,
|
|
77
|
+
},
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
import { createServer } from '../../server.js';
|
|
81
|
+
|
|
82
|
+
describe('Server HTTP Flows', () => {
|
|
83
|
+
let app: any;
|
|
84
|
+
|
|
85
|
+
beforeEach(async () => {
|
|
86
|
+
app = await createServer();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('GET /health', () => {
|
|
90
|
+
it('returns 200 with health data when initialized', async () => {
|
|
91
|
+
const res = await request(app).get('/health');
|
|
92
|
+
expect(res.status).toBe(200);
|
|
93
|
+
expect(res.body.status).toBe('ok');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('includes all required health fields', async () => {
|
|
97
|
+
const res = await request(app).get('/health');
|
|
98
|
+
const body = res.body;
|
|
99
|
+
expect(body).toHaveProperty('host');
|
|
100
|
+
expect(body).toHaveProperty('port');
|
|
101
|
+
expect(body).toHaveProperty('uptime');
|
|
102
|
+
expect(body).toHaveProperty('processManager');
|
|
103
|
+
expect(body).toHaveProperty('pid');
|
|
104
|
+
expect(body).toHaveProperty('idle');
|
|
105
|
+
expect(body).toHaveProperty('memory');
|
|
106
|
+
expect(body).toHaveProperty('circuits');
|
|
107
|
+
expect(body).toHaveProperty('errors');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('reports detached daemon as process manager', async () => {
|
|
111
|
+
const res = await request(app).get('/health');
|
|
112
|
+
expect(res.body.processManager).toContain('detached');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('returns numeric pid', async () => {
|
|
116
|
+
const res = await request(app).get('/health');
|
|
117
|
+
expect(typeof res.body.pid).toBe('number');
|
|
118
|
+
expect(res.body.pid).toBeGreaterThan(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('includes idle tracking info', async () => {
|
|
122
|
+
const res = await request(app).get('/health');
|
|
123
|
+
const { idle } = res.body;
|
|
124
|
+
expect(idle).toHaveProperty('currentMs');
|
|
125
|
+
expect(idle).toHaveProperty('thresholdMs');
|
|
126
|
+
expect(idle).toHaveProperty('checkIntervalMs');
|
|
127
|
+
expect(idle).toHaveProperty('percentToRestart');
|
|
128
|
+
expect(typeof idle.currentMs).toBe('number');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('includes memory stats in MB', async () => {
|
|
132
|
+
const res = await request(app).get('/health');
|
|
133
|
+
const { memory } = res.body;
|
|
134
|
+
expect(memory).toHaveProperty('heapUsed');
|
|
135
|
+
expect(memory).toHaveProperty('heapTotal');
|
|
136
|
+
expect(memory).toHaveProperty('rss');
|
|
137
|
+
expect(memory.heapUsed).toBeGreaterThan(0);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('returns initializing when MCP not ready', async () => {
|
|
141
|
+
const { isMcpInitialized } = await import('../../mcpCache.js');
|
|
142
|
+
vi.mocked(isMcpInitialized).mockReturnValueOnce(false);
|
|
143
|
+
const res = await request(app).get('/health');
|
|
144
|
+
expect(res.body.status).toBe('initializing');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('includes version string', async () => {
|
|
148
|
+
const res = await request(app).get('/health');
|
|
149
|
+
expect(res.body).toHaveProperty('version');
|
|
150
|
+
expect(typeof res.body.version).toBe('string');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('404 handler', () => {
|
|
155
|
+
it('returns 404 for unknown routes', async () => {
|
|
156
|
+
const res = await request(app).get('/nonexistent');
|
|
157
|
+
expect(res.status).toBe(404);
|
|
158
|
+
expect(res.body.success).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('includes error code NOT_FOUND', async () => {
|
|
162
|
+
const res = await request(app).get('/does-not-exist');
|
|
163
|
+
expect(res.body.error.code).toBe('NOT_FOUND');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('lists available routes in response', async () => {
|
|
167
|
+
const res = await request(app).get('/unknown');
|
|
168
|
+
expect(res.body.error.availableRoutes).toBeDefined();
|
|
169
|
+
expect(Array.isArray(res.body.error.availableRoutes)).toBe(true);
|
|
170
|
+
expect(res.body.error.availableRoutes.length).toBeGreaterThan(5);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('includes hint about POST tool calls', async () => {
|
|
174
|
+
const res = await request(app).get('/wrong-path');
|
|
175
|
+
expect(res.body.error.hint).toContain('POST');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('returns 404 for POST to unknown routes', async () => {
|
|
179
|
+
const res = await request(app).post('/unknown').send({});
|
|
180
|
+
expect(res.status).toBe(404);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('Middleware', () => {
|
|
185
|
+
it('parses JSON request bodies', async () => {
|
|
186
|
+
const res = await request(app)
|
|
187
|
+
.post('/tools/call/localSearchCode')
|
|
188
|
+
.send({
|
|
189
|
+
queries: [{
|
|
190
|
+
id: 'test-1',
|
|
191
|
+
researchGoal: 'test',
|
|
192
|
+
reasoning: 'test',
|
|
193
|
+
pattern: 'foo',
|
|
194
|
+
path: '/test',
|
|
195
|
+
}],
|
|
196
|
+
});
|
|
197
|
+
expect(res.status).not.toBe(415);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('updates idle timer on requests', async () => {
|
|
201
|
+
const res1 = await request(app).get('/health');
|
|
202
|
+
const idle1 = res1.body.idle.currentMs;
|
|
203
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
204
|
+
const res2 = await request(app).get('/health');
|
|
205
|
+
const idle2 = res2.body.idle.currentMs;
|
|
206
|
+
expect(idle2).toBeLessThanOrEqual(idle1 + 200);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('Route mounting', () => {
|
|
211
|
+
it('tools routes are mounted at /tools', async () => {
|
|
212
|
+
const res = await request(app).get('/tools/list');
|
|
213
|
+
expect(res.status).toBe(200);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('prompts routes are mounted at /prompts', async () => {
|
|
217
|
+
const res = await request(app).get('/prompts/list');
|
|
218
|
+
expect(res.status).toBe(200);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test: detached daemon lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Spawns server-init.js on a random test port, verifies that:
|
|
5
|
+
* 1. init exits after server is healthy (detached daemon behavior)
|
|
6
|
+
* 2. server survives init exit
|
|
7
|
+
* 3. second init detects running server and exits immediately
|
|
8
|
+
* 4. PID file is created on startup and removed on shutdown
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
12
|
+
import { spawn, type ChildProcess } from 'child_process';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
import { existsSync, readFileSync } from 'fs';
|
|
15
|
+
import { homedir } from 'os';
|
|
16
|
+
|
|
17
|
+
const SCRIPTS_DIR = join(import.meta.dirname, '..', '..', '..', 'scripts');
|
|
18
|
+
const SERVER_INIT_SCRIPT = join(SCRIPTS_DIR, 'server-init.js');
|
|
19
|
+
|
|
20
|
+
const TEST_PORT = 19871;
|
|
21
|
+
const HEALTH_URL = `http://localhost:${TEST_PORT}/health`;
|
|
22
|
+
const OCTOCODE_DIR = process.env.OCTOCODE_HOME || join(homedir(), '.octocode');
|
|
23
|
+
const PID_FILE = join(OCTOCODE_DIR, `research-server-${TEST_PORT}.pid`);
|
|
24
|
+
|
|
25
|
+
async function pollHealth(timeoutMs: number): Promise<boolean> {
|
|
26
|
+
const deadline = Date.now() + timeoutMs;
|
|
27
|
+
while (Date.now() < deadline) {
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch(HEALTH_URL, { signal: AbortSignal.timeout(2000) });
|
|
30
|
+
if (res.ok) {
|
|
31
|
+
const body = (await res.json()) as { status: string };
|
|
32
|
+
if (body.status === 'ok') return true;
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// not ready yet
|
|
36
|
+
}
|
|
37
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function isServerDown(timeoutMs: number): Promise<boolean> {
|
|
43
|
+
const deadline = Date.now() + timeoutMs;
|
|
44
|
+
while (Date.now() < deadline) {
|
|
45
|
+
try {
|
|
46
|
+
await fetch(HEALTH_URL, { signal: AbortSignal.timeout(1000) });
|
|
47
|
+
} catch {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function spawnInitScript(): ChildProcess {
|
|
56
|
+
return spawn('node', [SERVER_INIT_SCRIPT], {
|
|
57
|
+
env: {
|
|
58
|
+
...process.env,
|
|
59
|
+
OCTOCODE_PORT: String(TEST_PORT),
|
|
60
|
+
OCTOCODE_RESEARCH_PORT: String(TEST_PORT),
|
|
61
|
+
},
|
|
62
|
+
stdio: 'pipe',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function killListenersOnPort(port: number): Promise<void> {
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
const proc = spawn('sh', ['-c', `lsof -sTCP:LISTEN -ti :${port} | xargs kill -9 2>/dev/null`], {
|
|
69
|
+
stdio: 'ignore',
|
|
70
|
+
});
|
|
71
|
+
proc.on('close', () => resolve());
|
|
72
|
+
setTimeout(() => resolve(), 2000);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function waitForExit(proc: ChildProcess, timeoutMs: number): Promise<number | null> {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const timeout = setTimeout(() => {
|
|
79
|
+
proc.kill('SIGKILL');
|
|
80
|
+
resolve(null);
|
|
81
|
+
}, timeoutMs);
|
|
82
|
+
proc.on('exit', (code) => {
|
|
83
|
+
clearTimeout(timeout);
|
|
84
|
+
resolve(code);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe('Server Lifecycle (Detached Daemon)', () => {
|
|
90
|
+
let initProc: ChildProcess | null = null;
|
|
91
|
+
|
|
92
|
+
afterEach(async () => {
|
|
93
|
+
if (initProc && !initProc.killed) {
|
|
94
|
+
initProc.kill('SIGTERM');
|
|
95
|
+
await new Promise<void>((resolve) => {
|
|
96
|
+
const timeout = setTimeout(() => {
|
|
97
|
+
try { initProc?.kill('SIGKILL'); } catch { /* already dead */ }
|
|
98
|
+
resolve();
|
|
99
|
+
}, 5000);
|
|
100
|
+
initProc!.on('exit', () => { clearTimeout(timeout); resolve(); });
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
initProc = null;
|
|
104
|
+
await killListenersOnPort(TEST_PORT);
|
|
105
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('init exits after server is healthy and server survives', async () => {
|
|
109
|
+
initProc = spawnInitScript();
|
|
110
|
+
|
|
111
|
+
let initOutput = '';
|
|
112
|
+
initProc.stdout?.on('data', (chunk: Buffer) => { initOutput += chunk.toString(); });
|
|
113
|
+
initProc.stderr?.on('data', (chunk: Buffer) => { initOutput += chunk.toString(); });
|
|
114
|
+
|
|
115
|
+
// Init should exit on its own after server is ready
|
|
116
|
+
const exitCode = await waitForExit(initProc, 45_000);
|
|
117
|
+
expect(exitCode, `Init should exit 0. Output:\n${initOutput}`).toBe(0);
|
|
118
|
+
expect(initOutput).toContain('ok');
|
|
119
|
+
|
|
120
|
+
// Server should still be running after init exits
|
|
121
|
+
const serverAlive = await pollHealth(5_000);
|
|
122
|
+
expect(serverAlive, 'Server should survive init exit (detached daemon)').toBe(true);
|
|
123
|
+
|
|
124
|
+
// Verify health response reflects detached mode
|
|
125
|
+
const healthRes = await fetch(HEALTH_URL, { signal: AbortSignal.timeout(3000) });
|
|
126
|
+
const healthBody = (await healthRes.json()) as { status: string; port: number; processManager: string; pid: number };
|
|
127
|
+
expect(healthBody.status).toBe('ok');
|
|
128
|
+
expect(healthBody.port).toBe(TEST_PORT);
|
|
129
|
+
expect(healthBody.processManager).toContain('detached');
|
|
130
|
+
expect(healthBody.pid).toBeGreaterThan(0);
|
|
131
|
+
}, 60_000);
|
|
132
|
+
|
|
133
|
+
it('init exits immediately when server is already running', async () => {
|
|
134
|
+
// Start first init → starts server
|
|
135
|
+
initProc = spawnInitScript();
|
|
136
|
+
|
|
137
|
+
let output1 = '';
|
|
138
|
+
initProc.stdout?.on('data', (chunk: Buffer) => { output1 += chunk.toString(); });
|
|
139
|
+
|
|
140
|
+
const exitCode1 = await waitForExit(initProc, 45_000);
|
|
141
|
+
expect(exitCode1, `First init should exit 0. Output:\n${output1}`).toBe(0);
|
|
142
|
+
|
|
143
|
+
// Server should be alive
|
|
144
|
+
const ready = await pollHealth(5_000);
|
|
145
|
+
expect(ready, 'Server should be running').toBe(true);
|
|
146
|
+
|
|
147
|
+
// Start a SECOND init — should detect running server and exit fast
|
|
148
|
+
const secondInit = spawnInitScript();
|
|
149
|
+
|
|
150
|
+
let output2 = '';
|
|
151
|
+
secondInit.stdout?.on('data', (chunk: Buffer) => { output2 += chunk.toString(); });
|
|
152
|
+
|
|
153
|
+
const exitCode2 = await waitForExit(secondInit, 15_000);
|
|
154
|
+
expect(exitCode2, `Second init should exit 0 (fast path). Output:\n${output2}`).toBe(0);
|
|
155
|
+
expect(output2).toContain('ok');
|
|
156
|
+
|
|
157
|
+
// Server should still be alive
|
|
158
|
+
const stillAlive = await pollHealth(3_000);
|
|
159
|
+
expect(stillAlive).toBe(true);
|
|
160
|
+
}, 60_000);
|
|
161
|
+
|
|
162
|
+
it('PID file is created on startup and cleaned up on shutdown', async () => {
|
|
163
|
+
initProc = spawnInitScript();
|
|
164
|
+
|
|
165
|
+
let initOutput = '';
|
|
166
|
+
initProc.stdout?.on('data', (chunk: Buffer) => { initOutput += chunk.toString(); });
|
|
167
|
+
|
|
168
|
+
const exitCode = await waitForExit(initProc, 45_000);
|
|
169
|
+
expect(exitCode, `Init should exit 0. Output:\n${initOutput}`).toBe(0);
|
|
170
|
+
|
|
171
|
+
// PID file should exist
|
|
172
|
+
expect(existsSync(PID_FILE), 'PID file should exist after server starts').toBe(true);
|
|
173
|
+
|
|
174
|
+
const pidContent = readFileSync(PID_FILE, 'utf-8').trim();
|
|
175
|
+
const pid = parseInt(pidContent, 10);
|
|
176
|
+
expect(pid).toBeGreaterThan(0);
|
|
177
|
+
|
|
178
|
+
// Verify the PID matches the running server
|
|
179
|
+
const healthRes = await fetch(HEALTH_URL, { signal: AbortSignal.timeout(3000) });
|
|
180
|
+
const healthBody = (await healthRes.json()) as { pid: number };
|
|
181
|
+
expect(pid).toBe(healthBody.pid);
|
|
182
|
+
|
|
183
|
+
// Kill the server via its PID
|
|
184
|
+
process.kill(pid, 'SIGTERM');
|
|
185
|
+
|
|
186
|
+
// Server should stop
|
|
187
|
+
const serverDead = await isServerDown(10_000);
|
|
188
|
+
expect(serverDead, 'Server should stop after SIGTERM').toBe(true);
|
|
189
|
+
|
|
190
|
+
// PID file should be cleaned up
|
|
191
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
192
|
+
expect(existsSync(PID_FILE), 'PID file should be removed on shutdown').toBe(false);
|
|
193
|
+
}, 60_000);
|
|
194
|
+
});
|