pi-harness-runtime 1.1.439 → 1.1.441
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/harness/e2e/auto-test-loop.ts +8 -2
- package/harness/e2e/browser-scrapers/base-browser-scraper.ts +2 -0
- package/harness/e2e/browser-scrapers/chrome-profile-manager.ts +2 -0
- package/harness/e2e/browser-scrapers/codex-browser-scraper.ts +2 -0
- package/harness/e2e/tools-detector.ts +2 -0
- package/harness/index.ts +2 -1
- package/harness/key-loader.ts +2 -0
- package/harness/langchain/checkpointer.ts +2 -0
- package/harness/langchain/cron-cli.ts +2 -0
- package/harness/langchain/graph.ts +2 -0
- package/harness/master-planner.ts +1 -1
- package/harness/notification-center.ts +2 -0
- package/harness/output-limit-handler.ts +1 -1
- package/harness/project-detector/detector.ts +2 -4
- package/harness/secret-provider.ts +2 -0
- package/harness/task-graph.ts +460 -113
- package/index.ts +16 -7
- package/package.json +7 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/dist/extension.d.ts.map +1 -1
- package/packages/agent-policy/dist/extension.js +2 -0
- package/packages/agent-policy/dist/extension.js.map +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/agent-policy/src/extension.ts +2 -0
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/dist/minimax-browser-auth.d.ts.map +1 -1
- package/packages/auth/dist/minimax-browser-auth.js +23 -1
- package/packages/auth/dist/minimax-browser-auth.js.map +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/auth/src/minimax-browser-auth.ts +30 -5
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/dist/inbox.d.ts.map +1 -1
- package/packages/autonomous-runtime/dist/inbox.js +2 -0
- package/packages/autonomous-runtime/dist/inbox.js.map +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/autonomous-runtime/src/inbox.ts +2 -0
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/dist/src/clipboard.d.ts.map +1 -1
- package/packages/clipboard/dist/src/clipboard.js +2 -0
- package/packages/clipboard/dist/src/clipboard.js.map +1 -1
- package/packages/clipboard/dist/src/copy-sync.d.ts.map +1 -1
- package/packages/clipboard/dist/src/copy-sync.js +2 -0
- package/packages/clipboard/dist/src/copy-sync.js.map +1 -1
- package/packages/clipboard/dist/src/gist-relay.d.ts.map +1 -1
- package/packages/clipboard/dist/src/gist-relay.js +2 -0
- package/packages/clipboard/dist/src/gist-relay.js.map +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard/src/clipboard.ts +2 -0
- package/packages/clipboard/src/copy-sync.ts +2 -0
- package/packages/clipboard/src/gist-relay.ts +2 -0
- package/packages/clipboard-plugin/dist/types.d.ts +9 -2
- package/packages/clipboard-plugin/dist/types.d.ts.map +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/clipboard-plugin/src/types.ts +9 -2
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/dist/delegation-store.js +1 -4
- package/packages/contracts/dist/workspace-env.js +28 -1
- package/packages/contracts/package.json +1 -1
- package/packages/contracts/src/delegation-store.ts +9 -4
- package/packages/contracts/src/workspace-env.ts +29 -4
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/dist/src/store.d.ts.map +1 -1
- package/packages/event-store/dist/src/store.js +2 -0
- package/packages/event-store/dist/src/store.js.map +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/event-store/src/store.ts +3 -0
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/dist/analyzer.d.ts.map +1 -1
- package/packages/generic-web-plugin/dist/analyzer.js +2 -0
- package/packages/generic-web-plugin/dist/analyzer.js.map +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/generic-web-plugin/src/analyzer.ts +3 -0
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/dist/mcp-client.d.ts.map +1 -1
- package/packages/knowledge-retrieval/dist/mcp-client.js +0 -1
- package/packages/knowledge-retrieval/dist/mcp-client.js.map +1 -1
- package/packages/knowledge-retrieval/package.json +1 -4
- package/packages/knowledge-retrieval/src/mcp-client.ts +0 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/logging/package.json +1 -1
- package/packages/mcp-adapter/dist/server.d.ts.map +1 -1
- package/packages/mcp-adapter/dist/server.js +2 -0
- package/packages/mcp-adapter/dist/server.js.map +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/mcp-adapter/src/server.ts +3 -0
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/dist/index.d.ts.map +1 -1
- package/packages/paperclip-integration/dist/index.js +2 -0
- package/packages/paperclip-integration/dist/index.js.map +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/paperclip-integration/src/index.ts +2 -0
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/dist/audit.d.ts.map +1 -1
- package/packages/privilege-broker/dist/audit.js +2 -0
- package/packages/privilege-broker/dist/audit.js.map +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/privilege-broker/src/audit.ts +2 -0
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/dist/compiler.d.ts.map +1 -1
- package/packages/prompt-compiler/dist/compiler.js +2 -0
- package/packages/prompt-compiler/dist/compiler.js.map +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-compiler/src/compiler.ts +2 -0
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/dist/client.d.ts.map +1 -1
- package/packages/skill-mcp-client/dist/client.js +0 -1
- package/packages/skill-mcp-client/dist/client.js.map +1 -1
- package/packages/skill-mcp-client/package.json +1 -4
- package/packages/skill-mcp-client/src/client.ts +0 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/dist/parser.d.ts.map +1 -1
- package/packages/skills/dist/parser.js +2 -0
- package/packages/skills/dist/parser.js.map +1 -1
- package/packages/skills/dist/skill-versioning.d.ts.map +1 -1
- package/packages/skills/dist/skill-versioning.js +2 -0
- package/packages/skills/dist/skill-versioning.js.map +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/skills/src/parser.ts +3 -0
- package/packages/skills/src/skill-versioning.ts +2 -0
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tooling-preference/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/two-phase-review/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/dist/index.js +0 -2
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/webmcp-tester/src/index.ts +0 -3
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/dist/trigger.d.ts.map +1 -1
- package/packages/write-review/dist/trigger.js +1 -2
- package/packages/write-review/dist/trigger.js.map +1 -1
- package/packages/write-review/package.json +1 -1
- package/packages/write-review/src/trigger.ts +1 -2
- package/scripts/lib/runtime-imports.ts +189 -3
- package/scripts/release-all.ts +5 -3
- package/scripts/telegram-webhook-server.ts +2 -0
- package/scripts/typecheck-packages.mjs +7 -4
- package/scripts/verify-package-contents.ts +161 -2
- package/harness/e2e/tools-detector.test.ts +0 -156
- package/harness/glm-quota-countdown.test.ts +0 -323
- package/harness/langchain/agents.test.ts +0 -39
- package/harness/langchain/checkpointer.test.ts +0 -243
- package/harness/langchain/daemon.test.ts +0 -459
- package/harness/langchain/model-router.test.ts +0 -191
- package/harness/langchain/ping-pong-middleware.test.ts +0 -69
- package/harness/langchain/scoreboard.test.ts +0 -245
- package/harness/langchain/status-line.test.ts +0 -106
- package/harness/langchain/surge.test.ts +0 -467
- package/harness/langchain/widget.test.ts +0 -251
- package/harness/skill-sync.test.ts +0 -57
- package/harness/ssh-detach-interceptor.test.ts +0 -263
- package/harness/ssh-hang-recovery.test.ts +0 -86
- package/packages/a2a-adapter/test/a2a.test.ts +0 -151
- package/packages/agent-policy/test/interlock.test.ts +0 -101
- package/packages/agent-policy/test/policy-retention.test.ts +0 -380
- package/packages/agent-policy/test/policy.test.ts +0 -387
- package/packages/auth/test/auth.test.ts +0 -238
- package/packages/autonomous-refactor/test/refactor.test.ts +0 -202
- package/packages/autonomous-runtime/test/types.test.ts +0 -95
- package/packages/cache-strategy/test/cache.test.ts +0 -137
- package/packages/capability-registry/test/registry.test.ts +0 -204
- package/packages/checkpoint/test/engine.test.ts +0 -265
- package/packages/cli-plugin-sdk/test/cli.test.ts +0 -45
- package/packages/code-generation/test/generator.test.ts +0 -238
- package/packages/code-review/test/engine.test.ts +0 -302
- package/packages/codex-adapter/test/provider.test.ts +0 -142
- package/packages/codex-watcher/test/watch.test.ts +0 -453
- package/packages/context-compiler/test/context-compiler.test.ts +0 -746
- package/packages/context-manager/test/context.test.ts +0 -198
- package/packages/contracts/test/delegation.test.ts +0 -350
- package/packages/contracts/test/workspace-env.test.ts +0 -802
- package/packages/cookie-sanitizer/test/infer.test.ts +0 -110
- package/packages/cookie-sanitizer/test/normalize.test.ts +0 -113
- package/packages/cookie-sanitizer/test/parse.test.ts +0 -204
- package/packages/cookie-sanitizer/test/sync.test.ts +0 -237
- package/packages/cookie-sanitizer/test/watcher.test.ts +0 -27
- package/packages/cost-optimizer/test/cost-optimizer.test.ts +0 -438
- package/packages/dependency-analyzer/test/analyzer.test.ts +0 -49
- package/packages/django-plugin/test/analyzer.test.ts +0 -195
- package/packages/doc-generator/test/docs.test.ts +0 -168
- package/packages/durable-review/test/acceptance.test.ts +0 -357
- package/packages/durable-review/test/smoke.test.ts +0 -107
- package/packages/evaluation-engine/test/engine.test.ts +0 -277
- package/packages/evaluation-runner/test/runner.test.ts +0 -168
- package/packages/event-bus/test/bus.test.ts +0 -100
- package/packages/event-store/test/store.test.ts +0 -115
- package/packages/experience-replay/test/replay.test.ts +0 -267
- package/packages/feedback-collector/test/collector.test.ts +0 -142
- package/packages/framework-detector/test/detector.test.ts +0 -75
- package/packages/framework-plugin-sdk/test/manager.test.ts +0 -320
- package/packages/frappe-plugin/test/analyzer.test.ts +0 -250
- package/packages/generic-web-plugin/test/analyzer.test.ts +0 -134
- package/packages/health-monitor/test/health.test.ts +0 -231
- package/packages/intent-analyzer/test/analyzer.test.ts +0 -72
- package/packages/intent-analyzer/test/ping-pong.test.ts +0 -96
- package/packages/jev-judge/tests/auto-continue-context.test.ts +0 -280
- package/packages/jev-judge/tests/continuation-controller.test.ts +0 -1396
- package/packages/jev-judge/tests/jev-provider-chain.test.ts +0 -536
- package/packages/jev-judge/tests/outcome-application.test.ts +0 -132
- package/packages/jev-judge/tests/question-risk.test.ts +0 -129
- package/packages/jev-judge/tests/typesafe-provider.test.ts +0 -640
- package/packages/jev-judge/tests/validation-ledger.test.ts +0 -181
- package/packages/jev-judge/tests/validation-plan.test.ts +0 -170
- package/packages/jev-judge/tests/validation-tie-breaker.test.ts +0 -88
- package/packages/laravel-plugin/test/analyzer.test.ts +0 -220
- package/packages/learning-engine/test/engine.test.ts +0 -331
- package/packages/logging/test/buffer.test.ts +0 -44
- package/packages/logging/test/classification.test.ts +0 -137
- package/packages/logging/test/config.test.ts +0 -97
- package/packages/logging/test/fanout.test.ts +0 -186
- package/packages/logging/test/identity-transport.test.ts +0 -197
- package/packages/logging/test/incidents.test.ts +0 -404
- package/packages/logging/test/ingest-schema.test.ts +0 -184
- package/packages/logging/test/integration.test.ts +0 -140
- package/packages/logging/test/log-http.test.ts +0 -246
- package/packages/logging/test/redact.test.ts +0 -109
- package/packages/logging/test/sampler.test.ts +0 -57
- package/packages/logging/test/server.test.ts +0 -444
- package/packages/logging/test/shipper.test.ts +0 -152
- package/packages/logging/test/spool.test.ts +0 -96
- package/packages/mcp-adapter/test/server.test.ts +0 -91
- package/packages/memory-engine/test/engine.test.ts +0 -456
- package/packages/model-registry/test/registry.test.ts +0 -342
- package/packages/nextjs-plugin/test/analyzer.test.ts +0 -149
- package/packages/notification/test/approval-payload.test.ts +0 -269
- package/packages/notification/test/detail-format.test.ts +0 -106
- package/packages/notification/test/notification.test.ts +0 -375
- package/packages/notification/test/pending-question-lifecycle.test.ts +0 -195
- package/packages/notification/test/pending-question.test.ts +0 -331
- package/packages/notification/test/tooling-proposal-payload.test.ts +0 -212
- package/packages/observability/test/observability.test.ts +0 -1350
- package/packages/performance-optimizer/test/optimizer.test.ts +0 -267
- package/packages/project-analyzer/test/index.test.ts +0 -1719
- package/packages/prompt-compiler/test/prompt-compiler.test.ts +0 -813
- package/packages/prompt-versioning/test/versioning.test.ts +0 -112
- package/packages/provider-adapter-sdk/test/sdk.test.ts +0 -220
- package/packages/provider-router/test/provider-router.test.ts +0 -405
- package/packages/provider-selector/test/selector.test.ts +0 -192
- package/packages/providers/test/providers.test.ts +0 -225
- package/packages/qdrant-skills/test/collection-info.test.ts +0 -165
- package/packages/qdrant-skills/test/embedder-batch.test.ts +0 -184
- package/packages/qdrant-skills/test/embedder.test.ts +0 -97
- package/packages/quota-manager/test/quota.test.ts +0 -432
- package/packages/rate-limiter/test/limiter.test.ts +0 -128
- package/packages/react-vite-plugin/test/analyzer.test.ts +0 -139
- package/packages/relay/test/client.test.ts +0 -446
- package/packages/relay/test/relay-e2e.test.ts +0 -299
- package/packages/relay/test/relay-endpoint.test.ts +0 -278
- package/packages/release-manager/test/release.test.ts +0 -215
- package/packages/requirement-compiler/test/requirement-compiler.test.ts +0 -743
- package/packages/runtime/test/runtime-api.test.ts +0 -169
- package/packages/runtime/test/runtime.test.ts +0 -297
- package/packages/scheduler/test/scheduler.test.ts +0 -125
- package/packages/session/test/manager.test.ts +0 -233
- package/packages/shared-context/test/shared-context.test.ts +0 -114
- package/packages/skill-registry/test/registry.test.ts +0 -508
- package/packages/skills/test/init-skills.test.ts +0 -114
- package/packages/skills/test/remote-skills.test.ts +0 -360
- package/packages/skills/test/remote-source.test.ts +0 -248
- package/packages/skills/test/sanitize.test.ts +0 -250
- package/packages/skills/test/vector-indexing.test.ts +0 -205
- package/packages/skills/test/vector-schema.test.ts +0 -180
- package/packages/skills/test/vector-threshold.test.ts +0 -108
- package/packages/sprint-planner/test/planner.test.ts +0 -187
- package/packages/task-compiler/test/task-compiler.test.ts +0 -628
- package/packages/test-data-generator/test/generator.test.ts +0 -265
- package/packages/test-generator/test/generator.test.ts +0 -156
- package/packages/token-estimation/test/token-estimation.test.ts +0 -207
- package/packages/token-optimizer/test/optimizer.test.ts +0 -141
- package/packages/tooling-preference/tests/changed-files.test.ts +0 -124
- package/packages/tooling-preference/tests/detect.test.ts +0 -188
- package/packages/tooling-preference/tests/proposal.test.ts +0 -107
- package/packages/tooling-preference/tests/quality-check.test.ts +0 -272
- package/packages/tooling-preference/tests/registry.test.ts +0 -51
- package/packages/tooling-preference/tests/store.test.ts +0 -99
- package/packages/trajectory/test/store.test.ts +0 -250
- package/packages/tui/test/harness-footer.test.ts +0 -559
- package/packages/tui/test/harness-header.test.ts +0 -590
- package/packages/two-phase-review/test/classify.test.ts +0 -102
- package/packages/two-phase-review/test/helpers.ts +0 -72
- package/packages/two-phase-review/test/rules.test.ts +0 -150
- package/packages/two-phase-review/test/runtime.test.ts +0 -362
- package/packages/two-phase-review/test/scope.test.ts +0 -186
- package/packages/two-phase-review/test/signals.test.ts +0 -183
- package/packages/two-phase-review/test/state.test.ts +0 -201
- package/packages/wiki/test/basic.test.ts +0 -242
- package/packages/workspace-scanner/test/scanner.test.ts +0 -43
- package/packages/worktree/test/worktree.test.ts +0 -163
|
@@ -1,1719 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Project Analyzer Tests (comprehensive)
|
|
3
|
-
*
|
|
4
|
-
* Tests for ProjectAnalyzer, detectLanguages, detectPackageManager,
|
|
5
|
-
* command discovery, caching, and filesystem walker.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, test, expect, afterEach, beforeEach } from "bun:test";
|
|
9
|
-
import { mkdtemp, writeFile, mkdir, rm } from "node:fs/promises";
|
|
10
|
-
import { join } from "node:path";
|
|
11
|
-
import { tmpdir } from "node:os";
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
ProjectAnalyzer,
|
|
15
|
-
createProjectAnalyzer,
|
|
16
|
-
detectLanguages,
|
|
17
|
-
} from "../src/index.js";
|
|
18
|
-
import {
|
|
19
|
-
AnalysisCache,
|
|
20
|
-
hashRuleFiles,
|
|
21
|
-
hashManifestFiles,
|
|
22
|
-
} from "../src/cache.js";
|
|
23
|
-
import {
|
|
24
|
-
parsePackageJsonScripts,
|
|
25
|
-
categorizeCommands,
|
|
26
|
-
detectPackageManager,
|
|
27
|
-
parsePythonCommands,
|
|
28
|
-
parseComposerScripts,
|
|
29
|
-
} from "../src/command-discovery.js";
|
|
30
|
-
import { FileSystemWalker } from "../src/walker.js";
|
|
31
|
-
import { GenericFrameworkDetector, scanSignals } from "../src/signals.js";
|
|
32
|
-
import {
|
|
33
|
-
parseRuleFile,
|
|
34
|
-
discoverRuleFiles,
|
|
35
|
-
mergeRules,
|
|
36
|
-
} from "../src/rule-discovery.js";
|
|
37
|
-
|
|
38
|
-
// --- Temp Fixture Helpers ---------------------------------------------
|
|
39
|
-
|
|
40
|
-
const tempDirs: string[] = [];
|
|
41
|
-
|
|
42
|
-
async function tmpDir(): Promise<string> {
|
|
43
|
-
const d = await mkdtemp(join(tmpdir(), "proj-analyzer-"));
|
|
44
|
-
tempDirs.push(d);
|
|
45
|
-
return d;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
afterEach(async () => {
|
|
49
|
-
for (const d of tempDirs) {
|
|
50
|
-
try {
|
|
51
|
-
await rm(d, { recursive: true, force: true });
|
|
52
|
-
} catch {
|
|
53
|
-
// ignore
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
tempDirs.length = 0;
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// --- detectLanguages Tests --------------------------------------------
|
|
60
|
-
|
|
61
|
-
describe("detectLanguages", () => {
|
|
62
|
-
test("detects TypeScript files", () => {
|
|
63
|
-
const files = [
|
|
64
|
-
{ relativePath: "src/index.ts", size: 1000 },
|
|
65
|
-
{ relativePath: "src/app.ts", size: 500 },
|
|
66
|
-
{ relativePath: "src/types.ts", size: 300 },
|
|
67
|
-
];
|
|
68
|
-
const langs = detectLanguages(files);
|
|
69
|
-
expect(langs.find((l) => l.name === "TypeScript")).toBeDefined();
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("detects Python files", () => {
|
|
73
|
-
const files = [
|
|
74
|
-
{ relativePath: "app/main.py", size: 5000 },
|
|
75
|
-
{ relativePath: "app/models.py", size: 2000 },
|
|
76
|
-
];
|
|
77
|
-
const langs = detectLanguages(files);
|
|
78
|
-
expect(langs.find((l) => l.name === "Python")).toBeDefined();
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("detects Go files", () => {
|
|
82
|
-
const files = [{ relativePath: "cmd/main.go", size: 3000 }];
|
|
83
|
-
const langs = detectLanguages(files);
|
|
84
|
-
expect(langs.find((l) => l.name === "Go")).toBeDefined();
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test("detects Java and Kotlin files", () => {
|
|
88
|
-
const files = [
|
|
89
|
-
{ relativePath: "src/Main.java", size: 2000 },
|
|
90
|
-
{ relativePath: "src/Utils.kt", size: 1000 },
|
|
91
|
-
];
|
|
92
|
-
const langs = detectLanguages(files);
|
|
93
|
-
expect(langs.find((l) => l.name === "Java")).toBeDefined();
|
|
94
|
-
expect(langs.find((l) => l.name === "JavaScript")).toBeUndefined();
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("detects Rust files", () => {
|
|
98
|
-
const files = [{ relativePath: "src/lib.rs", size: 4000 }];
|
|
99
|
-
const langs = detectLanguages(files);
|
|
100
|
-
expect(langs.find((l) => l.name === "Rust")).toBeDefined();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("detects C# files", () => {
|
|
104
|
-
const files = [{ relativePath: "Program.cs", size: 1500 }];
|
|
105
|
-
const langs = detectLanguages(files);
|
|
106
|
-
expect(langs.find((l) => l.name === "C#")).toBeDefined();
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("detects HTML and CSS", () => {
|
|
110
|
-
const files = [
|
|
111
|
-
{ relativePath: "index.html", size: 2000 },
|
|
112
|
-
{ relativePath: "styles.css", size: 500 },
|
|
113
|
-
{ relativePath: "theme.scss", size: 800 },
|
|
114
|
-
];
|
|
115
|
-
const langs = detectLanguages(files);
|
|
116
|
-
expect(langs.find((l) => l.name === "HTML")).toBeDefined();
|
|
117
|
-
expect(langs.find((l) => l.name === "CSS")).toBeDefined();
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("detects multiple languages with correct coverage", () => {
|
|
121
|
-
const files = [
|
|
122
|
-
{ relativePath: "main.ts", size: 500 },
|
|
123
|
-
{ relativePath: "app.py", size: 1000 },
|
|
124
|
-
];
|
|
125
|
-
const langs = detectLanguages(files);
|
|
126
|
-
const py = langs.find((l) => l.name === "Python");
|
|
127
|
-
const ts = langs.find((l) => l.name === "TypeScript");
|
|
128
|
-
expect(py).toBeDefined();
|
|
129
|
-
expect(ts).toBeDefined();
|
|
130
|
-
// Python should have higher coverage (1000 vs 500 bytes)
|
|
131
|
-
expect(py!.coverage).toBeGreaterThan(ts!.coverage);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test("sorts languages by coverage descending", () => {
|
|
135
|
-
const files = [
|
|
136
|
-
{ relativePath: "main.js", size: 9000 },
|
|
137
|
-
{ relativePath: "main.ts", size: 1000 },
|
|
138
|
-
];
|
|
139
|
-
const langs = detectLanguages(files);
|
|
140
|
-
expect(langs[0].name).toBe("JavaScript");
|
|
141
|
-
expect(langs[1].name).toBe("TypeScript");
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test("handles empty file list", () => {
|
|
145
|
-
expect(detectLanguages([])).toEqual([]);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test("handles files with no recognized extensions", () => {
|
|
149
|
-
const files = [
|
|
150
|
-
{ relativePath: "README", size: 500 },
|
|
151
|
-
{ relativePath: "Makefile", size: 200 },
|
|
152
|
-
];
|
|
153
|
-
const langs = detectLanguages(files);
|
|
154
|
-
expect(langs).toEqual([]);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("handles mixed TSX and JSX files", () => {
|
|
158
|
-
const files = [
|
|
159
|
-
{ relativePath: "Component.tsx", size: 500 },
|
|
160
|
-
{ relativePath: "App.jsx", size: 300 },
|
|
161
|
-
];
|
|
162
|
-
const langs = detectLanguages(files);
|
|
163
|
-
const ts = langs.find((l) => l.name === "TypeScript");
|
|
164
|
-
expect(ts).toBeDefined();
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test("handles Swift files", () => {
|
|
168
|
-
const files = [{ relativePath: "iOS/App.swift", size: 2000 }];
|
|
169
|
-
const langs = detectLanguages(files);
|
|
170
|
-
expect(langs.find((l) => l.name === "Swift")).toBeDefined();
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test("handles SQL files", () => {
|
|
174
|
-
const files = [{ relativePath: "schema.sql", size: 3000 }];
|
|
175
|
-
const langs = detectLanguages(files);
|
|
176
|
-
expect(langs.find((l) => l.name === "SQL")).toBeDefined();
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
test("handles Markdown files", () => {
|
|
180
|
-
const files = [
|
|
181
|
-
{ relativePath: "README.md", size: 1000 },
|
|
182
|
-
{ relativePath: "CHANGELOG.md", size: 500 },
|
|
183
|
-
];
|
|
184
|
-
const langs = detectLanguages(files);
|
|
185
|
-
expect(langs.find((l) => l.name === "Markdown")).toBeDefined();
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
test("coverage sums to 1.0 for single language", () => {
|
|
189
|
-
const files = [
|
|
190
|
-
{ relativePath: "a.py", size: 300 },
|
|
191
|
-
{ relativePath: "b.py", size: 700 },
|
|
192
|
-
];
|
|
193
|
-
const langs = detectLanguages(files);
|
|
194
|
-
const py = langs.find((l) => l.name === "Python");
|
|
195
|
-
expect(py!.coverage).toBeCloseTo(1.0, 2);
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
// --- detectPackageManager Tests ---------------------------------------
|
|
200
|
-
|
|
201
|
-
describe("detectPackageManager", () => {
|
|
202
|
-
test("detects npm from package.json", () => {
|
|
203
|
-
const result = detectPackageManager(["package.json"]);
|
|
204
|
-
expect(result).toBe("npm");
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
test("detects npm from package-lock.json", () => {
|
|
208
|
-
const result = detectPackageManager(["package-lock.json"]);
|
|
209
|
-
expect(result).toBe("npm");
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
test("detects yarn from yarn.lock", () => {
|
|
213
|
-
const result = detectPackageManager(["yarn.lock"]);
|
|
214
|
-
expect(result).toBe("yarn");
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
test("detects pnpm from pnpm-lock.yaml", () => {
|
|
218
|
-
const result = detectPackageManager(["pnpm-lock.yaml"]);
|
|
219
|
-
expect(result).toBe("pnpm");
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
test("detects bun from bun.lockb", () => {
|
|
223
|
-
const result = detectPackageManager(["bun.lockb"]);
|
|
224
|
-
expect(result).toBe("bun");
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
test("detects pip from requirements.txt", () => {
|
|
228
|
-
const result = detectPackageManager(["requirements.txt"]);
|
|
229
|
-
expect(result).toBe("pip");
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
test("detects poetry from pyproject.toml", () => {
|
|
233
|
-
const result = detectPackageManager(["pyproject.toml"]);
|
|
234
|
-
expect(result).toBe("poetry");
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
test("detects poetry from poetry.lock", () => {
|
|
238
|
-
const result = detectPackageManager(["poetry.lock"]);
|
|
239
|
-
expect(result).toBe("poetry");
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
test("returns null for empty list", () => {
|
|
243
|
-
expect(detectPackageManager([])).toBeNull();
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
test("prefers npm over yarn when both present", () => {
|
|
247
|
-
// npm gets score 1 (package.json), yarn gets score 1 (yarn.lock)
|
|
248
|
-
// npm comes first in tiebreak since it's the first non-zero score
|
|
249
|
-
const result = detectPackageManager(["package.json", "yarn.lock"]);
|
|
250
|
-
// yarn.lock beats package.json signal for yarn
|
|
251
|
-
expect(result === "yarn" || result === "npm").toBe(true);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
test("handles nested paths", () => {
|
|
255
|
-
const result = detectPackageManager(["apps/frontend/package.json"]);
|
|
256
|
-
expect(result).toBe("npm");
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
test("detects bench from apps.txt", () => {
|
|
260
|
-
const result = detectPackageManager(["apps.txt"]);
|
|
261
|
-
expect(result).toBe("bench");
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
test("detects bench from sites directory", () => {
|
|
265
|
-
const result = detectPackageManager(["sites/", "apps.txt"]);
|
|
266
|
-
expect(result).toBe("bench");
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
test("score beats previous with multiple signals", () => {
|
|
270
|
-
// yarn.lock gives yarn score 1; package-lock.json gives npm score 1; yarn wins on order
|
|
271
|
-
// Use only yarn.lock so only yarn has a signal
|
|
272
|
-
const result = detectPackageManager(["yarn.lock"]);
|
|
273
|
-
expect(result).toBe("yarn");
|
|
274
|
-
});
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
// --- parsePackageJsonScripts Tests ------------------------------------
|
|
278
|
-
|
|
279
|
-
describe("parsePackageJsonScripts", () => {
|
|
280
|
-
test("extracts all scripts", () => {
|
|
281
|
-
const pkg = {
|
|
282
|
-
scripts: {
|
|
283
|
-
test: "vitest",
|
|
284
|
-
build: "tsc && vite build",
|
|
285
|
-
dev: "vite",
|
|
286
|
-
},
|
|
287
|
-
};
|
|
288
|
-
const cmds = parsePackageJsonScripts(pkg);
|
|
289
|
-
expect(cmds.length).toBe(3);
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
test("marks test as primary", () => {
|
|
293
|
-
const pkg = { scripts: { test: "jest" } };
|
|
294
|
-
const cmds = parsePackageJsonScripts(pkg);
|
|
295
|
-
expect(cmds.find((c) => c.command === "npm run test")?.primary).toBe(true);
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
test("marks build as primary", () => {
|
|
299
|
-
const pkg = { scripts: { build: "webpack" } };
|
|
300
|
-
const cmds = parsePackageJsonScripts(pkg);
|
|
301
|
-
expect(cmds.find((c) => c.command === "npm run build")?.primary).toBe(true);
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
test("marks dev as primary", () => {
|
|
305
|
-
const pkg = { scripts: { dev: "vite" } };
|
|
306
|
-
const cmds = parsePackageJsonScripts(pkg);
|
|
307
|
-
expect(cmds.find((c) => c.command === "npm run dev")?.primary).toBe(true);
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
test("marks start as primary", () => {
|
|
311
|
-
const pkg = { scripts: { start: "node server.js" } };
|
|
312
|
-
const cmds = parsePackageJsonScripts(pkg);
|
|
313
|
-
expect(cmds.find((c) => c.command === "npm run start")?.primary).toBe(true);
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
test("marks lint as primary", () => {
|
|
317
|
-
const pkg = { scripts: { lint: "eslint ." } };
|
|
318
|
-
const cmds = parsePackageJsonScripts(pkg);
|
|
319
|
-
expect(cmds.find((c) => c.command === "npm run lint")?.primary).toBe(true);
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
test("marks non-standard scripts as non-primary", () => {
|
|
323
|
-
const pkg = { scripts: { myscript: "echo hello" } };
|
|
324
|
-
const cmds = parsePackageJsonScripts(pkg);
|
|
325
|
-
expect(cmds.find((c) => c.command === "npm run myscript")?.primary).toBe(
|
|
326
|
-
false,
|
|
327
|
-
);
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
test("handles empty scripts object", () => {
|
|
331
|
-
const pkg = { scripts: {} };
|
|
332
|
-
expect(parsePackageJsonScripts(pkg)).toEqual([]);
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
test("handles undefined scripts", () => {
|
|
336
|
-
const pkg = {};
|
|
337
|
-
expect(parsePackageJsonScripts(pkg)).toEqual([]);
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
test("source is always package.json", () => {
|
|
341
|
-
const pkg = { scripts: { test: "jest" } };
|
|
342
|
-
const cmds = parsePackageJsonScripts(pkg);
|
|
343
|
-
expect(cmds.every((c) => c.source === "package.json")).toBe(true);
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
test("command format is npm run <name>", () => {
|
|
347
|
-
const pkg = { scripts: { foo: "bar" } };
|
|
348
|
-
const cmds = parsePackageJsonScripts(pkg);
|
|
349
|
-
expect(cmds[0].command).toBe("npm run foo");
|
|
350
|
-
});
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
// --- categorizeCommands Tests -----------------------------------------
|
|
354
|
-
|
|
355
|
-
describe("categorizeCommands", () => {
|
|
356
|
-
test("categorizes jest test commands", () => {
|
|
357
|
-
const cmds = [
|
|
358
|
-
{ command: "npm run test", source: "package.json", primary: true },
|
|
359
|
-
];
|
|
360
|
-
const categorized = categorizeCommands(cmds);
|
|
361
|
-
expect(categorized.unitTest).toContain("npm run test");
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
test("categorizes vitest commands", () => {
|
|
365
|
-
const cmds = [
|
|
366
|
-
{ command: "vitest run", source: "package.json", primary: true },
|
|
367
|
-
];
|
|
368
|
-
const categorized = categorizeCommands(cmds);
|
|
369
|
-
expect(categorized.unitTest.some((c) => c.includes("vitest"))).toBe(true);
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
test("categorizes pytest commands", () => {
|
|
373
|
-
const cmds = [{ command: "pytest", source: "package.json", primary: true }];
|
|
374
|
-
const categorized = categorizeCommands(cmds);
|
|
375
|
-
expect(categorized.unitTest).toContain("pytest");
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
test("categorizes playwright e2e commands", () => {
|
|
379
|
-
const cmds = [
|
|
380
|
-
{ command: "playwright e2e", source: "package.json", primary: true },
|
|
381
|
-
];
|
|
382
|
-
const categorized = categorizeCommands(cmds);
|
|
383
|
-
expect(categorized.e2eTest).toContain("playwright e2e");
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
test("categorizes cypress e2e commands", () => {
|
|
387
|
-
const cmds = [
|
|
388
|
-
{ command: "cypress run", source: "package.json", primary: true },
|
|
389
|
-
];
|
|
390
|
-
const categorized = categorizeCommands(cmds);
|
|
391
|
-
expect(categorized.e2eTest).toContain("cypress run");
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
test("categorizes eslint lint commands", () => {
|
|
395
|
-
const cmds = [
|
|
396
|
-
{ command: "eslint .", source: "package.json", primary: false },
|
|
397
|
-
];
|
|
398
|
-
const categorized = categorizeCommands(cmds);
|
|
399
|
-
expect(categorized.lint.some((c) => c.includes("eslint"))).toBe(true);
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
test("categorizes prettier lint commands", () => {
|
|
403
|
-
const cmds = [
|
|
404
|
-
{ command: "prettier --check .", source: "package.json", primary: false },
|
|
405
|
-
];
|
|
406
|
-
const categorized = categorizeCommands(cmds);
|
|
407
|
-
expect(categorized.lint.some((c) => c.includes("prettier"))).toBe(true);
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
test("categorizes build commands", () => {
|
|
411
|
-
const cmds = [
|
|
412
|
-
{ command: "npm run build", source: "package.json", primary: true },
|
|
413
|
-
];
|
|
414
|
-
const categorized = categorizeCommands(cmds);
|
|
415
|
-
expect(categorized.build).toContain("npm run build");
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
test("categorizes vite build commands", () => {
|
|
419
|
-
const cmds = [
|
|
420
|
-
{ command: "vite build", source: "package.json", primary: true },
|
|
421
|
-
];
|
|
422
|
-
const categorized = categorizeCommands(cmds);
|
|
423
|
-
expect(categorized.build.some((c) => c.includes("vite"))).toBe(true);
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
test("categorizes next build commands", () => {
|
|
427
|
-
const cmds = [
|
|
428
|
-
{ command: "next build", source: "package.json", primary: true },
|
|
429
|
-
];
|
|
430
|
-
const categorized = categorizeCommands(cmds);
|
|
431
|
-
expect(categorized.build.some((c) => c.includes("next"))).toBe(true);
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
test("categorizes migrate commands", () => {
|
|
435
|
-
const cmds = [
|
|
436
|
-
{ command: "npm run migrate", source: "package.json", primary: false },
|
|
437
|
-
];
|
|
438
|
-
const categorized = categorizeCommands(cmds);
|
|
439
|
-
expect(categorized.migrate).toContain("npm run migrate");
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
test("categorizes bench migrate commands", () => {
|
|
443
|
-
const cmds = [
|
|
444
|
-
{ command: "bench migrate", source: "package.json", primary: false },
|
|
445
|
-
];
|
|
446
|
-
const categorized = categorizeCommands(cmds);
|
|
447
|
-
expect(categorized.migrate.some((c) => c.includes("bench"))).toBe(true);
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
test("avoids duplicate categorization", () => {
|
|
451
|
-
const cmds = [
|
|
452
|
-
{ command: "npm run test", source: "package.json", primary: true },
|
|
453
|
-
{ command: "npm run test", source: "package.json", primary: true },
|
|
454
|
-
];
|
|
455
|
-
const categorized = categorizeCommands(cmds);
|
|
456
|
-
expect(
|
|
457
|
-
categorized.unitTest.filter((c) => c === "npm run test").length,
|
|
458
|
-
).toBe(1);
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
test("handles empty command list", () => {
|
|
462
|
-
const categorized = categorizeCommands([]);
|
|
463
|
-
expect(categorized.unitTest).toEqual([]);
|
|
464
|
-
expect(categorized.build).toEqual([]);
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
test("categorizes integration test commands", () => {
|
|
468
|
-
const cmds = [
|
|
469
|
-
{
|
|
470
|
-
command: "npm run integration-tests",
|
|
471
|
-
source: "package.json",
|
|
472
|
-
primary: false,
|
|
473
|
-
},
|
|
474
|
-
];
|
|
475
|
-
const categorized = categorizeCommands(cmds);
|
|
476
|
-
expect(categorized.integrationTest).toContain("npm run integration-tests");
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
test("categorizes typecheck commands", () => {
|
|
480
|
-
const cmds = [
|
|
481
|
-
{ command: "tsc --noEmit", source: "package.json", primary: false },
|
|
482
|
-
];
|
|
483
|
-
const categorized = categorizeCommands(cmds);
|
|
484
|
-
expect(categorized.typecheck.some((c) => c.includes("tsc"))).toBe(true);
|
|
485
|
-
});
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
// --- parsePythonCommands Tests ----------------------------------------
|
|
489
|
-
|
|
490
|
-
describe("parsePythonCommands", () => {
|
|
491
|
-
test("detects pytest from pyproject.toml", () => {
|
|
492
|
-
const content = '[tool.pytest]\ntestpaths = ["tests"]';
|
|
493
|
-
const cmds = parsePythonCommands(content);
|
|
494
|
-
expect(cmds.find((c) => c.command === "pytest")).toBeDefined();
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
test("detects pytest from [pytest] section", () => {
|
|
498
|
-
const cmds = parsePythonCommands("[pytest]\ntestpaths = tests");
|
|
499
|
-
expect(cmds.find((c) => c.command === "pytest")).toBeDefined();
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
test("detects coverage commands", () => {
|
|
503
|
-
const cmds = parsePythonCommands("coverage = run: pytest");
|
|
504
|
-
expect(cmds.some((c) => c.command.includes("coverage run"))).toBe(true);
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
test("handles empty content", () => {
|
|
508
|
-
const cmds = parsePythonCommands("");
|
|
509
|
-
expect(cmds).toEqual([]);
|
|
510
|
-
});
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
// --- parseComposerScripts Tests ---------------------------------------
|
|
514
|
-
|
|
515
|
-
describe("parseComposerScripts", () => {
|
|
516
|
-
test("extracts composer scripts", () => {
|
|
517
|
-
const composer = { scripts: { test: "phpunit", lint: "phpcs" } };
|
|
518
|
-
const cmds = parseComposerScripts(composer);
|
|
519
|
-
expect(cmds.length).toBe(2);
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
test("command format is composer <name>", () => {
|
|
523
|
-
const composer = { scripts: { test: "phpunit" } };
|
|
524
|
-
const cmds = parseComposerScripts(composer);
|
|
525
|
-
expect(cmds[0].command).toBe("composer test");
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
test("marks test as primary", () => {
|
|
529
|
-
const composer = { scripts: { test: "phpunit" } };
|
|
530
|
-
const cmds = parseComposerScripts(composer);
|
|
531
|
-
expect(cmds.find((c) => c.command === "composer test")?.primary).toBe(true);
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
test("marks lint as primary", () => {
|
|
535
|
-
const composer = { scripts: { lint: "phpcs" } };
|
|
536
|
-
const cmds = parseComposerScripts(composer);
|
|
537
|
-
expect(cmds.find((c) => c.command === "composer lint")?.primary).toBe(true);
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
test("handles empty scripts", () => {
|
|
541
|
-
const composer = { scripts: {} };
|
|
542
|
-
expect(parseComposerScripts(composer)).toEqual([]);
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
test("handles undefined scripts", () => {
|
|
546
|
-
const composer = {};
|
|
547
|
-
expect(parseComposerScripts(composer)).toEqual([]);
|
|
548
|
-
});
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
// --- AnalysisCache Tests ----------------------------------------------
|
|
552
|
-
|
|
553
|
-
describe("AnalysisCache", () => {
|
|
554
|
-
test("generates consistent cache keys", () => {
|
|
555
|
-
const cache = new AnalysisCache({ enabled: false });
|
|
556
|
-
const key1 = cache.generateKey("/repo", "abc123", "def456");
|
|
557
|
-
const key2 = cache.generateKey("/repo", "abc123", "def456");
|
|
558
|
-
expect(key1.repositoryRoot).toBe(key2.repositoryRoot);
|
|
559
|
-
expect(key1.revision).toBe(key2.revision);
|
|
560
|
-
expect(key1.configHash).toBe(key2.configHash);
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
test("generates different keys for different roots", () => {
|
|
564
|
-
const cache = new AnalysisCache({ enabled: false });
|
|
565
|
-
const key1 = cache.generateKey("/repo1", "abc123", "def456");
|
|
566
|
-
const key2 = cache.generateKey("/repo2", "abc123", "def456");
|
|
567
|
-
expect(key1.repositoryRoot).not.toBe(key2.repositoryRoot);
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
test("generates different keys for different revisions", () => {
|
|
571
|
-
const cache = new AnalysisCache({ enabled: false });
|
|
572
|
-
const key1 = cache.generateKey("/repo", "abc123", "def456");
|
|
573
|
-
const key2 = cache.generateKey("/repo", "xyz789", "def456");
|
|
574
|
-
expect(key1.revision).not.toBe(key2.revision);
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
test("calculates hash consistently", () => {
|
|
578
|
-
const cache = new AnalysisCache({ enabled: false });
|
|
579
|
-
const h1 = cache.calculateHash("hello");
|
|
580
|
-
const h2 = cache.calculateHash("hello");
|
|
581
|
-
expect(h1).toBe(h2);
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
test("calculates different hashes for different content", () => {
|
|
585
|
-
const cache = new AnalysisCache({ enabled: false });
|
|
586
|
-
const h1 = cache.calculateHash("hello");
|
|
587
|
-
const h2 = cache.calculateHash("world");
|
|
588
|
-
expect(h1).not.toBe(h2);
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
test("calculates config hash", async () => {
|
|
592
|
-
const cache = new AnalysisCache({ enabled: false });
|
|
593
|
-
const hash = await cache.calculateConfigHash([
|
|
594
|
-
{ path: "a.json", content: '{"test":1}' },
|
|
595
|
-
]);
|
|
596
|
-
expect(typeof hash).toBe("string");
|
|
597
|
-
expect(hash.length).toBe(64); // sha256 hex
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
test("get returns null when disabled", async () => {
|
|
601
|
-
const cache = new AnalysisCache({ enabled: false });
|
|
602
|
-
const key = cache.generateKey("/repo", "abc", "def");
|
|
603
|
-
const result = await cache.get(key);
|
|
604
|
-
expect(result).toBeNull();
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
test("set does not throw when disabled", async () => {
|
|
608
|
-
const cache = new AnalysisCache({ enabled: false });
|
|
609
|
-
const key = cache.generateKey("/repo", "abc", "def");
|
|
610
|
-
await expect(
|
|
611
|
-
cache.set(
|
|
612
|
-
key,
|
|
613
|
-
{
|
|
614
|
-
repositoryRoot: "/repo",
|
|
615
|
-
repositoryName: "repo",
|
|
616
|
-
revision: "abc",
|
|
617
|
-
frameworks: [],
|
|
618
|
-
languages: [],
|
|
619
|
-
packageManagers: [],
|
|
620
|
-
applications: [],
|
|
621
|
-
commands: {
|
|
622
|
-
unitTest: [],
|
|
623
|
-
integrationTest: [],
|
|
624
|
-
e2eTest: [],
|
|
625
|
-
lint: [],
|
|
626
|
-
typecheck: [],
|
|
627
|
-
build: [],
|
|
628
|
-
migrate: [],
|
|
629
|
-
},
|
|
630
|
-
rules: [],
|
|
631
|
-
sensitivePaths: [],
|
|
632
|
-
generatedPaths: [],
|
|
633
|
-
testCapabilities: [],
|
|
634
|
-
confidence: 0,
|
|
635
|
-
warnings: [],
|
|
636
|
-
analyzedAt: new Date().toISOString(),
|
|
637
|
-
},
|
|
638
|
-
{ ruleHash: "r", manifestHash: "m", configHash: "c" },
|
|
639
|
-
),
|
|
640
|
-
).resolves.toBeUndefined();
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
test("clear does not throw when disabled", async () => {
|
|
644
|
-
const cache = new AnalysisCache({ enabled: false });
|
|
645
|
-
await expect(cache.clear()).resolves.toBeUndefined();
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
test("invalidate does not throw when disabled", async () => {
|
|
649
|
-
const cache = new AnalysisCache({ enabled: false });
|
|
650
|
-
await expect(cache.invalidate("/repo")).resolves.toBeUndefined();
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
test("getStats returns zeros when disabled", async () => {
|
|
654
|
-
const cache = new AnalysisCache({ enabled: false });
|
|
655
|
-
const stats = await cache.getStats();
|
|
656
|
-
expect(stats.entryCount).toBe(0);
|
|
657
|
-
expect(stats.totalSize).toBe(0);
|
|
658
|
-
});
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
// --- hashRuleFiles Tests ----------------------------------------------
|
|
662
|
-
|
|
663
|
-
describe("hashRuleFiles", () => {
|
|
664
|
-
test("generates consistent hash for same files", async () => {
|
|
665
|
-
const files = [{ path: "AGENTS.md", content: "# Rules\n\nTest content" }];
|
|
666
|
-
const h1 = await hashRuleFiles(files);
|
|
667
|
-
const h2 = await hashRuleFiles(files);
|
|
668
|
-
expect(h1).toBe(h2);
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
test("different content produces different hash", async () => {
|
|
672
|
-
const h1 = await hashRuleFiles([{ path: "a.md", content: "v1" }]);
|
|
673
|
-
const h2 = await hashRuleFiles([{ path: "a.md", content: "v2" }]);
|
|
674
|
-
expect(h1).not.toBe(h2);
|
|
675
|
-
});
|
|
676
|
-
|
|
677
|
-
test("different paths produce different hash", async () => {
|
|
678
|
-
const h1 = await hashRuleFiles([{ path: "a.md", content: "same" }]);
|
|
679
|
-
const h2 = await hashRuleFiles([{ path: "b.md", content: "same" }]);
|
|
680
|
-
expect(h1).not.toBe(h2);
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
test("empty list produces valid hash", async () => {
|
|
684
|
-
const hash = await hashRuleFiles([]);
|
|
685
|
-
expect(typeof hash).toBe("string");
|
|
686
|
-
expect(hash.length).toBe(64);
|
|
687
|
-
});
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
// --- hashManifestFiles Tests -------------------------------------------
|
|
691
|
-
|
|
692
|
-
describe("hashManifestFiles", () => {
|
|
693
|
-
test("generates consistent hash", async () => {
|
|
694
|
-
const files = [
|
|
695
|
-
{ path: "package.json", content: '{"scripts":{"test":"jest"}}' },
|
|
696
|
-
];
|
|
697
|
-
const h1 = await hashManifestFiles(files);
|
|
698
|
-
const h2 = await hashManifestFiles(files);
|
|
699
|
-
expect(h1).toBe(h2);
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
test("only scripts and deps are hashed", async () => {
|
|
703
|
-
const files = [
|
|
704
|
-
{
|
|
705
|
-
path: "package.json",
|
|
706
|
-
content: '{"scripts":{"test":"jest"},"extra":"ignored"}',
|
|
707
|
-
},
|
|
708
|
-
];
|
|
709
|
-
const hash = await hashManifestFiles(files);
|
|
710
|
-
expect(typeof hash).toBe("string");
|
|
711
|
-
expect(hash.length).toBe(64);
|
|
712
|
-
});
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
// --- FileSystemWalker Tests --------------------------------------------
|
|
716
|
-
|
|
717
|
-
describe("FileSystemWalker", () => {
|
|
718
|
-
test("rejects non-absolute root path", () => {
|
|
719
|
-
expect(() => new FileSystemWalker("relative/path")).toThrow();
|
|
720
|
-
});
|
|
721
|
-
|
|
722
|
-
test("scans empty directory", async () => {
|
|
723
|
-
const dir = await tmpDir();
|
|
724
|
-
const walker = new FileSystemWalker(dir);
|
|
725
|
-
const result = await walker.scan();
|
|
726
|
-
expect(result.files).toEqual([]);
|
|
727
|
-
expect(result.truncated).toBe(false);
|
|
728
|
-
expect(result.errors).toEqual([]);
|
|
729
|
-
});
|
|
730
|
-
|
|
731
|
-
test("scans files in directory", async () => {
|
|
732
|
-
const dir = await tmpDir();
|
|
733
|
-
await writeFile(join(dir, "index.ts"), "const x = 1;", "utf-8");
|
|
734
|
-
await writeFile(join(dir, "main.py"), "print('hello')", "utf-8");
|
|
735
|
-
|
|
736
|
-
const walker = new FileSystemWalker(dir);
|
|
737
|
-
const result = await walker.scan();
|
|
738
|
-
|
|
739
|
-
expect(result.files.length).toBeGreaterThanOrEqual(2);
|
|
740
|
-
const paths = result.files.map((f) => f.relativePath);
|
|
741
|
-
expect(paths).toContain("index.ts");
|
|
742
|
-
expect(paths).toContain("main.py");
|
|
743
|
-
});
|
|
744
|
-
|
|
745
|
-
test("detects sensitive files by pattern", async () => {
|
|
746
|
-
const dir = await tmpDir();
|
|
747
|
-
await writeFile(join(dir, ".env"), "SECRET=value", "utf-8");
|
|
748
|
-
await writeFile(join(dir, ".env.local"), "SECRET=local", "utf-8");
|
|
749
|
-
|
|
750
|
-
const walker = new FileSystemWalker(dir);
|
|
751
|
-
const result = await walker.scan();
|
|
752
|
-
|
|
753
|
-
const envFiles = result.files.filter((f) => f.sensitive);
|
|
754
|
-
expect(envFiles.length).toBeGreaterThan(0);
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
test("detects generated files by pattern", async () => {
|
|
758
|
-
const dir = await tmpDir();
|
|
759
|
-
await mkdir(join(dir, "dist"), { recursive: true });
|
|
760
|
-
await writeFile(join(dir, "dist/bundle.js"), "module.exports={};", "utf-8");
|
|
761
|
-
|
|
762
|
-
const walker = new FileSystemWalker(dir);
|
|
763
|
-
const result = await walker.scan();
|
|
764
|
-
|
|
765
|
-
const generatedFiles = result.files.filter((f) => f.generated);
|
|
766
|
-
expect(generatedFiles.length).toBeGreaterThan(0);
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
test("respects maxScanFiles limit", async () => {
|
|
770
|
-
const dir = await tmpDir();
|
|
771
|
-
// Create many files
|
|
772
|
-
for (let i = 0; i < 50; i++) {
|
|
773
|
-
await writeFile(join(dir, `file${i}.txt`), `content ${i}`, "utf-8");
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
const walker = new FileSystemWalker(dir, { maxScanFiles: 10 });
|
|
777
|
-
const result = await walker.scan();
|
|
778
|
-
|
|
779
|
-
expect(result.files.length).toBeLessThanOrEqual(10);
|
|
780
|
-
expect(result.truncated).toBe(true);
|
|
781
|
-
});
|
|
782
|
-
|
|
783
|
-
test("skips hidden directories except .git", async () => {
|
|
784
|
-
const dir = await tmpDir();
|
|
785
|
-
await mkdir(join(dir, ".hidden"), { recursive: true });
|
|
786
|
-
await writeFile(join(dir, ".hidden/file.txt"), "secret", "utf-8");
|
|
787
|
-
await mkdir(join(dir, ".git"), { recursive: true });
|
|
788
|
-
await writeFile(join(dir, ".git/config"), "[core]", "utf-8");
|
|
789
|
-
|
|
790
|
-
const walker = new FileSystemWalker(dir);
|
|
791
|
-
const result = await walker.scan();
|
|
792
|
-
|
|
793
|
-
const hiddenFiles = result.files.filter((f) =>
|
|
794
|
-
f.relativePath.includes(".hidden"),
|
|
795
|
-
);
|
|
796
|
-
expect(hiddenFiles).toEqual([]);
|
|
797
|
-
// .git should be included (not skipped)
|
|
798
|
-
});
|
|
799
|
-
|
|
800
|
-
test("resolves relative path correctly", async () => {
|
|
801
|
-
const dir = await tmpDir();
|
|
802
|
-
await mkdir(join(dir, "src"), { recursive: true });
|
|
803
|
-
await writeFile(join(dir, "src/index.ts"), "", "utf-8");
|
|
804
|
-
|
|
805
|
-
const walker = new FileSystemWalker(dir);
|
|
806
|
-
const result = await walker.scan();
|
|
807
|
-
|
|
808
|
-
const tsFile = result.files.find((f) => f.relativePath.endsWith(".ts"));
|
|
809
|
-
expect(tsFile?.relativePath).toBe("src/index.ts");
|
|
810
|
-
});
|
|
811
|
-
|
|
812
|
-
test("getStats returns scan statistics", async () => {
|
|
813
|
-
const dir = await tmpDir();
|
|
814
|
-
await writeFile(join(dir, "a.txt"), "a", "utf-8");
|
|
815
|
-
await writeFile(join(dir, "b.txt"), "b", "utf-8");
|
|
816
|
-
|
|
817
|
-
const walker = new FileSystemWalker(dir);
|
|
818
|
-
await walker.scan();
|
|
819
|
-
const stats = walker.getStats();
|
|
820
|
-
|
|
821
|
-
expect(stats.filesScanned).toBeGreaterThanOrEqual(2);
|
|
822
|
-
expect(stats.directoriesScanned).toBeGreaterThanOrEqual(1);
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
test("readFileSafe returns null for oversized files", async () => {
|
|
826
|
-
const dir = await tmpDir();
|
|
827
|
-
await writeFile(join(dir, "big.txt"), "x".repeat(1000), "utf-8");
|
|
828
|
-
|
|
829
|
-
const walker = new FileSystemWalker(dir, { maxFileSize: 100 });
|
|
830
|
-
const content = await walker.readFileSafe(join(dir, "big.txt"));
|
|
831
|
-
expect(content).toBeNull();
|
|
832
|
-
});
|
|
833
|
-
|
|
834
|
-
test("readFileSafe returns content for small files", async () => {
|
|
835
|
-
const dir = await tmpDir();
|
|
836
|
-
const content = "hello world";
|
|
837
|
-
await writeFile(join(dir, "small.txt"), content, "utf-8");
|
|
838
|
-
|
|
839
|
-
const walker = new FileSystemWalker(dir);
|
|
840
|
-
const result = await walker.readFileSafe(join(dir, "small.txt"));
|
|
841
|
-
expect(result).toBe(content);
|
|
842
|
-
});
|
|
843
|
-
|
|
844
|
-
test("exists returns true for existing path", async () => {
|
|
845
|
-
const dir = await tmpDir();
|
|
846
|
-
await writeFile(join(dir, "exists.txt"), "x", "utf-8");
|
|
847
|
-
|
|
848
|
-
const walker = new FileSystemWalker(dir);
|
|
849
|
-
expect(await walker.exists(join(dir, "exists.txt"))).toBe(true);
|
|
850
|
-
});
|
|
851
|
-
|
|
852
|
-
test("exists returns false for non-existent path", async () => {
|
|
853
|
-
const dir = await tmpDir();
|
|
854
|
-
const walker = new FileSystemWalker(dir);
|
|
855
|
-
expect(await walker.exists(join(dir, "nonexistent.txt"))).toBe(false);
|
|
856
|
-
});
|
|
857
|
-
|
|
858
|
-
test("findFiles matches regex patterns", async () => {
|
|
859
|
-
const dir = await tmpDir();
|
|
860
|
-
await writeFile(join(dir, "test.spec.ts"), "", "utf-8");
|
|
861
|
-
await writeFile(join(dir, "test.test.ts"), "", "utf-8");
|
|
862
|
-
await writeFile(join(dir, "app.ts"), "", "utf-8");
|
|
863
|
-
|
|
864
|
-
const walker = new FileSystemWalker(dir);
|
|
865
|
-
const results = await walker.findFiles(/\.test\.ts$/);
|
|
866
|
-
expect(results.length).toBe(1);
|
|
867
|
-
});
|
|
868
|
-
});
|
|
869
|
-
|
|
870
|
-
// --- Rule Discovery Tests ---------------------------------------------
|
|
871
|
-
|
|
872
|
-
describe("rule-discovery", () => {
|
|
873
|
-
test("parseRuleFile extracts sections from markdown", async () => {
|
|
874
|
-
const ruleFile = {
|
|
875
|
-
path: "/repo/AGENTS.md",
|
|
876
|
-
name: "AGENTS.md",
|
|
877
|
-
content: "# Section 1\n\nContent here.\n\n## Section 2\n\nMore content.",
|
|
878
|
-
userDefined: true,
|
|
879
|
-
};
|
|
880
|
-
const rule = await parseRuleFile(ruleFile);
|
|
881
|
-
expect(rule.sections.length).toBeGreaterThan(0);
|
|
882
|
-
expect(rule.priority).toBe("mandatory");
|
|
883
|
-
expect(rule.userDefined).toBe(true);
|
|
884
|
-
});
|
|
885
|
-
|
|
886
|
-
test("AGENTS.md has mandatory priority", async () => {
|
|
887
|
-
const rule = await parseRuleFile({
|
|
888
|
-
path: "/repo/AGENTS.md",
|
|
889
|
-
name: "AGENTS.md",
|
|
890
|
-
content: "# Rules",
|
|
891
|
-
userDefined: true,
|
|
892
|
-
});
|
|
893
|
-
expect(rule.priority).toBe("mandatory");
|
|
894
|
-
});
|
|
895
|
-
|
|
896
|
-
test("CONTRIBUTING.md has advisory priority", async () => {
|
|
897
|
-
const rule = await parseRuleFile({
|
|
898
|
-
path: "/repo/CONTRIBUTING.md",
|
|
899
|
-
name: "CONTRIBUTING.md",
|
|
900
|
-
content: "# Contributing",
|
|
901
|
-
userDefined: false,
|
|
902
|
-
});
|
|
903
|
-
expect(rule.priority).toBe("advisory");
|
|
904
|
-
});
|
|
905
|
-
|
|
906
|
-
test("discoverRuleFiles finds files in temp repo", async () => {
|
|
907
|
-
const dir = await tmpDir();
|
|
908
|
-
await writeFile(
|
|
909
|
-
join(dir, "AGENTS.md"),
|
|
910
|
-
"# Agent Rules\n\nUse TypeScript.",
|
|
911
|
-
"utf-8",
|
|
912
|
-
);
|
|
913
|
-
|
|
914
|
-
const fs = {
|
|
915
|
-
exists: async (p: string) => {
|
|
916
|
-
try {
|
|
917
|
-
const { access } = await import("node:fs/promises");
|
|
918
|
-
await access(p);
|
|
919
|
-
return true;
|
|
920
|
-
} catch {
|
|
921
|
-
return false;
|
|
922
|
-
}
|
|
923
|
-
},
|
|
924
|
-
readFile: async (p: string) => {
|
|
925
|
-
const { readFile } = await import("node:fs/promises");
|
|
926
|
-
return readFile(p, "utf-8");
|
|
927
|
-
},
|
|
928
|
-
readDir: async (p: string) => {
|
|
929
|
-
const { readdir } = await import("node:fs/promises");
|
|
930
|
-
return readdir(p);
|
|
931
|
-
},
|
|
932
|
-
isDirectory: async (p: string) => {
|
|
933
|
-
const { stat } = await import("node:fs/promises");
|
|
934
|
-
const s = await stat(p);
|
|
935
|
-
return s.isDirectory();
|
|
936
|
-
},
|
|
937
|
-
};
|
|
938
|
-
|
|
939
|
-
const rules = await discoverRuleFiles(dir, fs);
|
|
940
|
-
expect(rules.length).toBeGreaterThan(0);
|
|
941
|
-
expect(rules.find((r) => r.name === "AGENTS.md")).toBeDefined();
|
|
942
|
-
});
|
|
943
|
-
|
|
944
|
-
test("mergeRules sorts by priority", async () => {
|
|
945
|
-
const files = [
|
|
946
|
-
{
|
|
947
|
-
path: "/repo/CONTRIBUTING.md",
|
|
948
|
-
name: "CONTRIBUTING.md",
|
|
949
|
-
content: "# Contributing",
|
|
950
|
-
userDefined: false,
|
|
951
|
-
},
|
|
952
|
-
{
|
|
953
|
-
path: "/repo/AGENTS.md",
|
|
954
|
-
name: "AGENTS.md",
|
|
955
|
-
content: "# Agents",
|
|
956
|
-
userDefined: true,
|
|
957
|
-
},
|
|
958
|
-
];
|
|
959
|
-
const merged = mergeRules(files);
|
|
960
|
-
expect(merged[0].id).toBe("rule-AGENTS");
|
|
961
|
-
});
|
|
962
|
-
});
|
|
963
|
-
|
|
964
|
-
// --- Signal Detection Tests -------------------------------------------
|
|
965
|
-
|
|
966
|
-
describe("scanSignals", () => {
|
|
967
|
-
test("returns empty array when no signals match", async () => {
|
|
968
|
-
const fs = {
|
|
969
|
-
exists: async () => false,
|
|
970
|
-
glob: async () => [],
|
|
971
|
-
readFile: async () => "",
|
|
972
|
-
readDir: async () => [],
|
|
973
|
-
isDirectory: async () => false,
|
|
974
|
-
};
|
|
975
|
-
const signals = await scanSignals(fs);
|
|
976
|
-
expect(signals).toEqual([]);
|
|
977
|
-
});
|
|
978
|
-
|
|
979
|
-
test("detects file existence signals", async () => {
|
|
980
|
-
// Use a non-glob signal: frappe uses "apps.txt"
|
|
981
|
-
const fs = {
|
|
982
|
-
exists: async (path: string) => path === "apps.txt",
|
|
983
|
-
glob: async () => [],
|
|
984
|
-
readFile: async () => "",
|
|
985
|
-
readDir: async () => [],
|
|
986
|
-
isDirectory: async () => false,
|
|
987
|
-
};
|
|
988
|
-
const signals = await scanSignals(fs);
|
|
989
|
-
const frappeSignal = signals.find((s) => s.type === "frappe_hooks");
|
|
990
|
-
expect(frappeSignal).toBeDefined();
|
|
991
|
-
expect(frappeSignal?.path).toBe("apps.txt");
|
|
992
|
-
});
|
|
993
|
-
});
|
|
994
|
-
|
|
995
|
-
describe("GenericFrameworkDetector", () => {
|
|
996
|
-
test("detects Next.js from next.config.js", async () => {
|
|
997
|
-
const dir = await tmpDir();
|
|
998
|
-
await writeFile(
|
|
999
|
-
join(dir, "next.config.js"),
|
|
1000
|
-
"module.exports = {}",
|
|
1001
|
-
"utf-8",
|
|
1002
|
-
);
|
|
1003
|
-
await writeFile(
|
|
1004
|
-
join(dir, "package.json"),
|
|
1005
|
-
'{"dependencies":{"next":"14"}}',
|
|
1006
|
-
"utf-8",
|
|
1007
|
-
);
|
|
1008
|
-
|
|
1009
|
-
const walker = new FileSystemWalker(dir);
|
|
1010
|
-
const roFs = {
|
|
1011
|
-
exists: async (path: string) => walker.exists(path),
|
|
1012
|
-
readFile: async (path: string) => {
|
|
1013
|
-
const c = await walker.readFileSafe(path);
|
|
1014
|
-
if (c === null) throw new Error("not found");
|
|
1015
|
-
return c;
|
|
1016
|
-
},
|
|
1017
|
-
readDir: async (path: string) => {
|
|
1018
|
-
try {
|
|
1019
|
-
const { readdir } = await import("node:fs/promises");
|
|
1020
|
-
return readdir(walker.resolvePath(path));
|
|
1021
|
-
} catch {
|
|
1022
|
-
return [];
|
|
1023
|
-
}
|
|
1024
|
-
},
|
|
1025
|
-
isDirectory: async (path: string) => {
|
|
1026
|
-
try {
|
|
1027
|
-
const { stat } = await import("node:fs/promises");
|
|
1028
|
-
const s = await stat(walker.resolvePath(path));
|
|
1029
|
-
return s.isDirectory();
|
|
1030
|
-
} catch {
|
|
1031
|
-
return false;
|
|
1032
|
-
}
|
|
1033
|
-
},
|
|
1034
|
-
// glob is used for wildcard patterns like "next.config.*"
|
|
1035
|
-
glob: async (pattern: string) => {
|
|
1036
|
-
if (pattern === "next.config.*") {
|
|
1037
|
-
return ["next.config.js"];
|
|
1038
|
-
}
|
|
1039
|
-
return [];
|
|
1040
|
-
},
|
|
1041
|
-
};
|
|
1042
|
-
|
|
1043
|
-
const detector = new GenericFrameworkDetector();
|
|
1044
|
-
const results = await detector.detect(roFs);
|
|
1045
|
-
|
|
1046
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1047
|
-
const nextjs = results.find((r) => r.category === "nextjs");
|
|
1048
|
-
expect(nextjs).toBeDefined();
|
|
1049
|
-
expect(nextjs!.confidence).toBeGreaterThan(0);
|
|
1050
|
-
});
|
|
1051
|
-
|
|
1052
|
-
test("detects Django from manage.py", async () => {
|
|
1053
|
-
const dir = await tmpDir();
|
|
1054
|
-
await writeFile(join(dir, "manage.py"), "#!/usr/bin/env python", "utf-8");
|
|
1055
|
-
await writeFile(join(dir, "settings.py"), "SECRET_KEY = 'x'", "utf-8");
|
|
1056
|
-
|
|
1057
|
-
const walker = new FileSystemWalker(dir);
|
|
1058
|
-
const roFs = {
|
|
1059
|
-
exists: async (path: string) => walker.exists(path),
|
|
1060
|
-
readFile: async () => "",
|
|
1061
|
-
readDir: async () => [],
|
|
1062
|
-
isDirectory: async () => false,
|
|
1063
|
-
glob: async () => [],
|
|
1064
|
-
};
|
|
1065
|
-
|
|
1066
|
-
const detector = new GenericFrameworkDetector();
|
|
1067
|
-
const results = await detector.detect(roFs);
|
|
1068
|
-
|
|
1069
|
-
const django = results.find((r) => r.category === "django");
|
|
1070
|
-
expect(django).toBeDefined();
|
|
1071
|
-
expect(django!.confidence).toBeGreaterThan(0);
|
|
1072
|
-
});
|
|
1073
|
-
|
|
1074
|
-
test("returns empty results for unknown directory", async () => {
|
|
1075
|
-
await tmpDir(); // Creates empty temp dir
|
|
1076
|
-
const roFs = {
|
|
1077
|
-
exists: async () => false,
|
|
1078
|
-
readFile: async () => {
|
|
1079
|
-
throw new Error("not found");
|
|
1080
|
-
},
|
|
1081
|
-
readDir: async () => [],
|
|
1082
|
-
isDirectory: async () => false,
|
|
1083
|
-
glob: async () => [],
|
|
1084
|
-
};
|
|
1085
|
-
|
|
1086
|
-
const detector = new GenericFrameworkDetector();
|
|
1087
|
-
const results = await detector.detect(roFs);
|
|
1088
|
-
// Unknown framework is possible if signal matching doesn't find anything
|
|
1089
|
-
expect(Array.isArray(results)).toBe(true);
|
|
1090
|
-
});
|
|
1091
|
-
});
|
|
1092
|
-
|
|
1093
|
-
// --- ProjectAnalyzer Integration Tests --------------------------------
|
|
1094
|
-
|
|
1095
|
-
describe("ProjectAnalyzer", () => {
|
|
1096
|
-
let analyzer: ProjectAnalyzer;
|
|
1097
|
-
|
|
1098
|
-
beforeEach(() => {
|
|
1099
|
-
analyzer = createProjectAnalyzer({
|
|
1100
|
-
cache: new AnalysisCache({ enabled: false }),
|
|
1101
|
-
});
|
|
1102
|
-
});
|
|
1103
|
-
|
|
1104
|
-
test("analyzer is instance of ProjectAnalyzer", () => {
|
|
1105
|
-
expect(analyzer).toBeInstanceOf(ProjectAnalyzer);
|
|
1106
|
-
});
|
|
1107
|
-
|
|
1108
|
-
test("analyzes Next.js workspace", async () => {
|
|
1109
|
-
const dir = await tmpDir();
|
|
1110
|
-
await writeFile(
|
|
1111
|
-
join(dir, "package.json"),
|
|
1112
|
-
JSON.stringify({ dependencies: { next: "14.0.0" } }),
|
|
1113
|
-
"utf-8",
|
|
1114
|
-
);
|
|
1115
|
-
await writeFile(
|
|
1116
|
-
join(dir, "next.config.js"),
|
|
1117
|
-
"module.exports = {}",
|
|
1118
|
-
"utf-8",
|
|
1119
|
-
);
|
|
1120
|
-
await mkdir(join(dir, "app"), { recursive: true });
|
|
1121
|
-
await writeFile(
|
|
1122
|
-
join(dir, "app/page.tsx"),
|
|
1123
|
-
"export default function(){}",
|
|
1124
|
-
"utf-8",
|
|
1125
|
-
);
|
|
1126
|
-
|
|
1127
|
-
const result = await analyzer.analyze({ repositoryRoot: dir });
|
|
1128
|
-
|
|
1129
|
-
expect(result.success).toBe(true);
|
|
1130
|
-
expect(result.profile).toBeDefined();
|
|
1131
|
-
expect(result.profile!.repositoryRoot).toBe(dir);
|
|
1132
|
-
expect(result.profile!.repositoryName).toBeDefined();
|
|
1133
|
-
expect(result.profile!.analyzedAt).toBeDefined();
|
|
1134
|
-
});
|
|
1135
|
-
|
|
1136
|
-
test("analyzes React/Vite workspace", async () => {
|
|
1137
|
-
const dir = await tmpDir();
|
|
1138
|
-
await writeFile(
|
|
1139
|
-
join(dir, "package.json"),
|
|
1140
|
-
JSON.stringify({
|
|
1141
|
-
dependencies: { react: "^18" },
|
|
1142
|
-
devDependencies: { vite: "^5" },
|
|
1143
|
-
}),
|
|
1144
|
-
"utf-8",
|
|
1145
|
-
);
|
|
1146
|
-
await writeFile(
|
|
1147
|
-
join(dir, "vite.config.ts"),
|
|
1148
|
-
"import { defineConfig } from 'vite'",
|
|
1149
|
-
"utf-8",
|
|
1150
|
-
);
|
|
1151
|
-
await mkdir(join(dir, "src"), { recursive: true });
|
|
1152
|
-
await writeFile(
|
|
1153
|
-
join(dir, "src/main.tsx"),
|
|
1154
|
-
"import React from 'react'",
|
|
1155
|
-
"utf-8",
|
|
1156
|
-
);
|
|
1157
|
-
|
|
1158
|
-
const result = await analyzer.analyze({ repositoryRoot: dir });
|
|
1159
|
-
|
|
1160
|
-
expect(result.success).toBe(true);
|
|
1161
|
-
expect(result.profile).toBeDefined();
|
|
1162
|
-
});
|
|
1163
|
-
|
|
1164
|
-
test("analyzes Django workspace", async () => {
|
|
1165
|
-
const dir = await tmpDir();
|
|
1166
|
-
await writeFile(join(dir, "manage.py"), "#!/usr/bin/env python", "utf-8");
|
|
1167
|
-
await writeFile(join(dir, "settings.py"), "SECRET_KEY = 'dev'", "utf-8");
|
|
1168
|
-
await writeFile(join(dir, "requirements.txt"), "django==4.0", "utf-8");
|
|
1169
|
-
|
|
1170
|
-
const result = await analyzer.analyze({ repositoryRoot: dir });
|
|
1171
|
-
|
|
1172
|
-
expect(result.success).toBe(true);
|
|
1173
|
-
expect(result.profile).toBeDefined();
|
|
1174
|
-
});
|
|
1175
|
-
|
|
1176
|
-
test("analyzes Laravel workspace", async () => {
|
|
1177
|
-
const dir = await tmpDir();
|
|
1178
|
-
await writeFile(join(dir, "artisan"), "#!/usr/bin/env php", "utf-8");
|
|
1179
|
-
await writeFile(
|
|
1180
|
-
join(dir, "composer.json"),
|
|
1181
|
-
JSON.stringify({ require: { laravel: "^10" } }),
|
|
1182
|
-
"utf-8",
|
|
1183
|
-
);
|
|
1184
|
-
await mkdir(join(dir, "app", "Http", "Controllers"), { recursive: true });
|
|
1185
|
-
|
|
1186
|
-
const result = await analyzer.analyze({ repositoryRoot: dir });
|
|
1187
|
-
|
|
1188
|
-
expect(result.success).toBe(true);
|
|
1189
|
-
expect(result.profile).toBeDefined();
|
|
1190
|
-
});
|
|
1191
|
-
|
|
1192
|
-
test("detects language from scanned files", async () => {
|
|
1193
|
-
const dir = await tmpDir();
|
|
1194
|
-
await writeFile(join(dir, "package.json"), "{}", "utf-8");
|
|
1195
|
-
await writeFile(join(dir, "main.ts"), "const x: number = 1;", "utf-8");
|
|
1196
|
-
await writeFile(join(dir, "utils.py"), "def foo(): pass", "utf-8");
|
|
1197
|
-
|
|
1198
|
-
const result = await analyzer.analyze({ repositoryRoot: dir });
|
|
1199
|
-
|
|
1200
|
-
expect(result.success).toBe(true);
|
|
1201
|
-
expect(result.profile!.languages.length).toBeGreaterThan(0);
|
|
1202
|
-
});
|
|
1203
|
-
|
|
1204
|
-
test("detects package manager from lock files", async () => {
|
|
1205
|
-
const dir = await tmpDir();
|
|
1206
|
-
await writeFile(join(dir, "package.json"), "{}", "utf-8");
|
|
1207
|
-
await writeFile(join(dir, "bun.lockb"), "", "utf-8");
|
|
1208
|
-
|
|
1209
|
-
const result = await analyzer.analyze({ repositoryRoot: dir });
|
|
1210
|
-
|
|
1211
|
-
expect(result.success).toBe(true);
|
|
1212
|
-
expect(result.profile!.packageManagers.length).toBeGreaterThan(0);
|
|
1213
|
-
});
|
|
1214
|
-
|
|
1215
|
-
test("discovers commands from package.json", async () => {
|
|
1216
|
-
const dir = await tmpDir();
|
|
1217
|
-
await writeFile(
|
|
1218
|
-
join(dir, "package.json"),
|
|
1219
|
-
JSON.stringify({
|
|
1220
|
-
scripts: {
|
|
1221
|
-
test: "vitest",
|
|
1222
|
-
build: "tsc",
|
|
1223
|
-
lint: "eslint .",
|
|
1224
|
-
},
|
|
1225
|
-
}),
|
|
1226
|
-
"utf-8",
|
|
1227
|
-
);
|
|
1228
|
-
|
|
1229
|
-
const result = await analyzer.analyze({ repositoryRoot: dir });
|
|
1230
|
-
|
|
1231
|
-
expect(result.success).toBe(true);
|
|
1232
|
-
expect(result.profile!.commands.unitTest.length).toBeGreaterThan(0);
|
|
1233
|
-
});
|
|
1234
|
-
|
|
1235
|
-
test("discovers AGENTS.md rules", async () => {
|
|
1236
|
-
const dir = await tmpDir();
|
|
1237
|
-
await writeFile(join(dir, "package.json"), "{}", "utf-8");
|
|
1238
|
-
await writeFile(
|
|
1239
|
-
join(dir, "AGENTS.md"),
|
|
1240
|
-
"# Agent Rules\n\n## Code Style\n\nUse TypeScript.",
|
|
1241
|
-
"utf-8",
|
|
1242
|
-
);
|
|
1243
|
-
|
|
1244
|
-
const result = await analyzer.analyze({ repositoryRoot: dir });
|
|
1245
|
-
|
|
1246
|
-
expect(result.success).toBe(true);
|
|
1247
|
-
expect(result.profile!.rules.length).toBeGreaterThan(0);
|
|
1248
|
-
});
|
|
1249
|
-
|
|
1250
|
-
test("detects test capabilities", async () => {
|
|
1251
|
-
const dir = await tmpDir();
|
|
1252
|
-
await writeFile(join(dir, "package.json"), "{}", "utf-8");
|
|
1253
|
-
await writeFile(
|
|
1254
|
-
join(dir, "vitest.config.ts"),
|
|
1255
|
-
"import { defineConfig } from 'vitest/config'",
|
|
1256
|
-
"utf-8",
|
|
1257
|
-
);
|
|
1258
|
-
|
|
1259
|
-
const result = await analyzer.analyze({ repositoryRoot: dir });
|
|
1260
|
-
|
|
1261
|
-
expect(result.success).toBe(true);
|
|
1262
|
-
expect(result.profile!.testCapabilities.length).toBeGreaterThan(0);
|
|
1263
|
-
const vitest = result.profile!.testCapabilities.find(
|
|
1264
|
-
(t) => t.runner === "vitest",
|
|
1265
|
-
);
|
|
1266
|
-
expect(vitest).toBeDefined();
|
|
1267
|
-
expect(vitest!.available).toBe(true);
|
|
1268
|
-
});
|
|
1269
|
-
|
|
1270
|
-
test("detects pytest from pyproject.toml", async () => {
|
|
1271
|
-
const dir = await tmpDir();
|
|
1272
|
-
await writeFile(
|
|
1273
|
-
join(dir, "pyproject.toml"),
|
|
1274
|
-
"[tool.pytest]\ntestpaths = ['tests']",
|
|
1275
|
-
"utf-8",
|
|
1276
|
-
);
|
|
1277
|
-
|
|
1278
|
-
const result = await analyzer.analyze({ repositoryRoot: dir });
|
|
1279
|
-
|
|
1280
|
-
expect(result.success).toBe(true);
|
|
1281
|
-
const pytest = result.profile!.testCapabilities.find(
|
|
1282
|
-
(t) => t.runner === "pytest",
|
|
1283
|
-
);
|
|
1284
|
-
expect(pytest).toBeDefined();
|
|
1285
|
-
});
|
|
1286
|
-
|
|
1287
|
-
test("returns error for non-existent directory", async () => {
|
|
1288
|
-
const result = await analyzer.analyze({
|
|
1289
|
-
repositoryRoot: "/nonexistent/path/to/repo",
|
|
1290
|
-
});
|
|
1291
|
-
// The analyzer catches errors and returns success:false for non-existent paths
|
|
1292
|
-
if (!result.success) {
|
|
1293
|
-
expect(result.error).toBeDefined();
|
|
1294
|
-
expect(result.errorCode).toBeDefined();
|
|
1295
|
-
}
|
|
1296
|
-
});
|
|
1297
|
-
|
|
1298
|
-
test("reports duration", async () => {
|
|
1299
|
-
const dir = await tmpDir();
|
|
1300
|
-
await writeFile(join(dir, "package.json"), "{}", "utf-8");
|
|
1301
|
-
|
|
1302
|
-
const result = await analyzer.analyze({ repositoryRoot: dir });
|
|
1303
|
-
expect(result.durationMs).toBeGreaterThanOrEqual(0);
|
|
1304
|
-
});
|
|
1305
|
-
|
|
1306
|
-
test("analyzer with custom cache works", async () => {
|
|
1307
|
-
const cache = new AnalysisCache({ enabled: false });
|
|
1308
|
-
const customAnalyzer = createProjectAnalyzer({ cache });
|
|
1309
|
-
|
|
1310
|
-
const dir = await tmpDir();
|
|
1311
|
-
await writeFile(join(dir, "package.json"), "{}", "utf-8");
|
|
1312
|
-
|
|
1313
|
-
const result = await customAnalyzer.analyze({ repositoryRoot: dir });
|
|
1314
|
-
expect(result.success).toBe(true);
|
|
1315
|
-
});
|
|
1316
|
-
|
|
1317
|
-
test("analyzer config respects maxScanFiles", async () => {
|
|
1318
|
-
const dir = await tmpDir();
|
|
1319
|
-
await writeFile(join(dir, "package.json"), "{}", "utf-8");
|
|
1320
|
-
|
|
1321
|
-
// Create many files
|
|
1322
|
-
for (let i = 0; i < 30; i++) {
|
|
1323
|
-
await writeFile(join(dir, `file${i}.txt`), `content ${i}`, "utf-8");
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
const limitedAnalyzer = createProjectAnalyzer({
|
|
1327
|
-
cache: new AnalysisCache({ enabled: false }),
|
|
1328
|
-
});
|
|
1329
|
-
|
|
1330
|
-
const result = await limitedAnalyzer.analyze({
|
|
1331
|
-
repositoryRoot: dir,
|
|
1332
|
-
config: { maxScanFiles: 10 },
|
|
1333
|
-
});
|
|
1334
|
-
|
|
1335
|
-
expect(result.success).toBe(true);
|
|
1336
|
-
expect(result.profile).toBeDefined();
|
|
1337
|
-
});
|
|
1338
|
-
|
|
1339
|
-
test("analyzer with no monorepo detection", async () => {
|
|
1340
|
-
const dir = await tmpDir();
|
|
1341
|
-
await writeFile(
|
|
1342
|
-
join(dir, "package.json"),
|
|
1343
|
-
JSON.stringify({ name: "root" }),
|
|
1344
|
-
"utf-8",
|
|
1345
|
-
);
|
|
1346
|
-
await mkdir(join(dir, "packages", "app"), { recursive: true });
|
|
1347
|
-
await writeFile(
|
|
1348
|
-
join(dir, "packages", "app", "package.json"),
|
|
1349
|
-
JSON.stringify({ name: "app" }),
|
|
1350
|
-
"utf-8",
|
|
1351
|
-
);
|
|
1352
|
-
|
|
1353
|
-
const result = await analyzer.analyze({
|
|
1354
|
-
repositoryRoot: dir,
|
|
1355
|
-
config: { detectMonorepo: false },
|
|
1356
|
-
});
|
|
1357
|
-
|
|
1358
|
-
expect(result.success).toBe(true);
|
|
1359
|
-
});
|
|
1360
|
-
|
|
1361
|
-
test("generates warnings for conflicting frameworks", async () => {
|
|
1362
|
-
const dir = await tmpDir();
|
|
1363
|
-
await writeFile(join(dir, "package.json"), "{}", "utf-8");
|
|
1364
|
-
await writeFile(
|
|
1365
|
-
join(dir, "next.config.js"),
|
|
1366
|
-
"module.exports = {}",
|
|
1367
|
-
"utf-8",
|
|
1368
|
-
);
|
|
1369
|
-
await writeFile(
|
|
1370
|
-
join(dir, "vite.config.ts"),
|
|
1371
|
-
"import { defineConfig } from 'vite'",
|
|
1372
|
-
"utf-8",
|
|
1373
|
-
);
|
|
1374
|
-
|
|
1375
|
-
const result = await analyzer.analyze({ repositoryRoot: dir });
|
|
1376
|
-
|
|
1377
|
-
expect(result.success).toBe(true);
|
|
1378
|
-
// Both Next.js and Vite should be detected, may produce framework warnings
|
|
1379
|
-
expect(result.profile).toBeDefined();
|
|
1380
|
-
});
|
|
1381
|
-
});
|
|
1382
|
-
|
|
1383
|
-
// --- Complexity Estimation --------------------------------------------
|
|
1384
|
-
|
|
1385
|
-
describe("complexity signals from FileSystemWalker", () => {
|
|
1386
|
-
test("file count reflects all scanned files", async () => {
|
|
1387
|
-
const dir = await tmpDir();
|
|
1388
|
-
await writeFile(join(dir, "package.json"), "{}", "utf-8");
|
|
1389
|
-
await writeFile(join(dir, "a.ts"), "", "utf-8");
|
|
1390
|
-
await writeFile(join(dir, "b.ts"), "", "utf-8");
|
|
1391
|
-
await writeFile(join(dir, "c.ts"), "", "utf-8");
|
|
1392
|
-
|
|
1393
|
-
const walker = new FileSystemWalker(dir);
|
|
1394
|
-
const result = await walker.scan();
|
|
1395
|
-
|
|
1396
|
-
expect(result.files.length).toBeGreaterThanOrEqual(4);
|
|
1397
|
-
});
|
|
1398
|
-
|
|
1399
|
-
test("total size accumulates file sizes", async () => {
|
|
1400
|
-
const dir = await tmpDir();
|
|
1401
|
-
await writeFile(join(dir, "package.json"), '{"name":"test"}', "utf-8");
|
|
1402
|
-
await writeFile(join(dir, "main.ts"), "const x = 1;".repeat(100), "utf-8");
|
|
1403
|
-
|
|
1404
|
-
const walker = new FileSystemWalker(dir);
|
|
1405
|
-
const result = await walker.scan();
|
|
1406
|
-
|
|
1407
|
-
const tsFile = result.files.find((f) => f.relativePath === "main.ts");
|
|
1408
|
-
expect(tsFile!.size).toBeGreaterThan(500);
|
|
1409
|
-
});
|
|
1410
|
-
|
|
1411
|
-
test("scans nested directories", async () => {
|
|
1412
|
-
const dir = await tmpDir();
|
|
1413
|
-
await mkdir(join(dir, "src", "components", "ui"), { recursive: true });
|
|
1414
|
-
await writeFile(join(dir, "src/index.ts"), "", "utf-8");
|
|
1415
|
-
await writeFile(join(dir, "src/components/Button.tsx"), "", "utf-8");
|
|
1416
|
-
await writeFile(join(dir, "src/components/ui/Card.tsx"), "", "utf-8");
|
|
1417
|
-
|
|
1418
|
-
const walker = new FileSystemWalker(dir);
|
|
1419
|
-
const result = await walker.scan();
|
|
1420
|
-
|
|
1421
|
-
expect(result.files.length).toBeGreaterThanOrEqual(3);
|
|
1422
|
-
expect(result.directoriesScanned).toBeGreaterThan(3);
|
|
1423
|
-
});
|
|
1424
|
-
|
|
1425
|
-
test("node_modules files are marked as generated", async () => {
|
|
1426
|
-
const dir = await tmpDir();
|
|
1427
|
-
await mkdir(join(dir, "node_modules"), { recursive: true });
|
|
1428
|
-
await mkdir(join(dir, "node_modules/lodash"), { recursive: true });
|
|
1429
|
-
await writeFile(
|
|
1430
|
-
join(dir, "node_modules/lodash/index.js"),
|
|
1431
|
-
"module.exports={};",
|
|
1432
|
-
"utf-8",
|
|
1433
|
-
);
|
|
1434
|
-
await mkdir(join(dir, "src"), { recursive: true });
|
|
1435
|
-
await writeFile(join(dir, "src/main.ts"), "", "utf-8");
|
|
1436
|
-
|
|
1437
|
-
const walker = new FileSystemWalker(dir);
|
|
1438
|
-
const result = await walker.scan();
|
|
1439
|
-
|
|
1440
|
-
// node_modules files are scanned but marked as generated
|
|
1441
|
-
const generatedFiles = result.files.filter((f) => f.generated);
|
|
1442
|
-
expect(generatedFiles.length).toBeGreaterThan(0);
|
|
1443
|
-
});
|
|
1444
|
-
|
|
1445
|
-
test("depth limit prevents deep scanning", async () => {
|
|
1446
|
-
const dir = await tmpDir();
|
|
1447
|
-
// Create deeply nested structure
|
|
1448
|
-
let current = dir;
|
|
1449
|
-
for (let i = 0; i < 30; i++) {
|
|
1450
|
-
current = join(current, `level${i}`);
|
|
1451
|
-
}
|
|
1452
|
-
await mkdir(current, { recursive: true });
|
|
1453
|
-
await writeFile(join(current, "deep.ts"), "", "utf-8");
|
|
1454
|
-
|
|
1455
|
-
const walker = new FileSystemWalker(dir, { maxDepth: 5 });
|
|
1456
|
-
const result = await walker.scan();
|
|
1457
|
-
|
|
1458
|
-
const deepFile = result.files.find((f) =>
|
|
1459
|
-
f.relativePath.includes("deep.ts"),
|
|
1460
|
-
);
|
|
1461
|
-
expect(deepFile).toBeUndefined();
|
|
1462
|
-
});
|
|
1463
|
-
});
|
|
1464
|
-
|
|
1465
|
-
// --- Error Cases ------------------------------------------------------
|
|
1466
|
-
|
|
1467
|
-
describe("error handling", () => {
|
|
1468
|
-
test("analyzer handles corrupted JSON gracefully", async () => {
|
|
1469
|
-
const dir = await tmpDir();
|
|
1470
|
-
await writeFile(join(dir, "package.json"), "{ invalid json }", "utf-8");
|
|
1471
|
-
|
|
1472
|
-
const analyzer = createProjectAnalyzer({
|
|
1473
|
-
cache: new AnalysisCache({ enabled: false }),
|
|
1474
|
-
});
|
|
1475
|
-
const result = await analyzer.analyze({ repositoryRoot: dir });
|
|
1476
|
-
|
|
1477
|
-
// Should still succeed with partial results
|
|
1478
|
-
expect(result.success).toBe(true);
|
|
1479
|
-
});
|
|
1480
|
-
|
|
1481
|
-
test("walker handles unreadable directories", async () => {
|
|
1482
|
-
const dir = await tmpDir();
|
|
1483
|
-
// The walker should not crash on edge cases
|
|
1484
|
-
const walker = new FileSystemWalker(dir);
|
|
1485
|
-
const result = await walker.scan();
|
|
1486
|
-
expect(result.errors).toEqual([]);
|
|
1487
|
-
});
|
|
1488
|
-
|
|
1489
|
-
test("analyzer handles missing package.json scripts", async () => {
|
|
1490
|
-
const dir = await tmpDir();
|
|
1491
|
-
await writeFile(
|
|
1492
|
-
join(dir, "package.json"),
|
|
1493
|
-
JSON.stringify({ name: "no-scripts" }),
|
|
1494
|
-
"utf-8",
|
|
1495
|
-
);
|
|
1496
|
-
|
|
1497
|
-
const analyzer = createProjectAnalyzer({
|
|
1498
|
-
cache: new AnalysisCache({ enabled: false }),
|
|
1499
|
-
});
|
|
1500
|
-
const result = await analyzer.analyze({ repositoryRoot: dir });
|
|
1501
|
-
|
|
1502
|
-
expect(result.success).toBe(true);
|
|
1503
|
-
expect(result.profile!.commands).toBeDefined();
|
|
1504
|
-
});
|
|
1505
|
-
});
|
|
1506
|
-
|
|
1507
|
-
// --- Additional Cache TTL Tests ----------------------------------------
|
|
1508
|
-
|
|
1509
|
-
describe("AnalysisCache TTL behavior", () => {
|
|
1510
|
-
test("cache returns null for expired entries", async () => {
|
|
1511
|
-
const tmp = await mkdtemp(join(tmpdir(), "cache-ttl-"));
|
|
1512
|
-
tempDirs.push(tmp);
|
|
1513
|
-
|
|
1514
|
-
const cache = new AnalysisCache({
|
|
1515
|
-
enabled: true,
|
|
1516
|
-
cacheDir: tmp,
|
|
1517
|
-
maxAgeMs: 1, // 1 millisecond TTL
|
|
1518
|
-
});
|
|
1519
|
-
|
|
1520
|
-
const key = cache.generateKey("/repo", "abc", "def");
|
|
1521
|
-
await cache.set(
|
|
1522
|
-
key,
|
|
1523
|
-
{
|
|
1524
|
-
repositoryRoot: "/repo",
|
|
1525
|
-
repositoryName: "repo",
|
|
1526
|
-
revision: "abc",
|
|
1527
|
-
frameworks: [],
|
|
1528
|
-
languages: [],
|
|
1529
|
-
packageManagers: [],
|
|
1530
|
-
applications: [],
|
|
1531
|
-
commands: {
|
|
1532
|
-
unitTest: [],
|
|
1533
|
-
integrationTest: [],
|
|
1534
|
-
e2eTest: [],
|
|
1535
|
-
lint: [],
|
|
1536
|
-
typecheck: [],
|
|
1537
|
-
build: [],
|
|
1538
|
-
migrate: [],
|
|
1539
|
-
},
|
|
1540
|
-
rules: [],
|
|
1541
|
-
sensitivePaths: [],
|
|
1542
|
-
generatedPaths: [],
|
|
1543
|
-
testCapabilities: [],
|
|
1544
|
-
confidence: 0,
|
|
1545
|
-
warnings: [],
|
|
1546
|
-
analyzedAt: new Date().toISOString(),
|
|
1547
|
-
},
|
|
1548
|
-
{ ruleHash: "r", manifestHash: "m", configHash: "c" },
|
|
1549
|
-
);
|
|
1550
|
-
|
|
1551
|
-
// Wait for entry to expire
|
|
1552
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
1553
|
-
|
|
1554
|
-
const result = await cache.get(key);
|
|
1555
|
-
expect(result).toBeNull();
|
|
1556
|
-
|
|
1557
|
-
await rm(tmp, { recursive: true, force: true });
|
|
1558
|
-
tempDirs.pop();
|
|
1559
|
-
});
|
|
1560
|
-
|
|
1561
|
-
test("cache stores and retrieves profiles", async () => {
|
|
1562
|
-
const tmp = await mkdtemp(join(tmpdir(), "cache-store-"));
|
|
1563
|
-
tempDirs.push(tmp);
|
|
1564
|
-
|
|
1565
|
-
const cache = new AnalysisCache({
|
|
1566
|
-
enabled: true,
|
|
1567
|
-
cacheDir: tmp,
|
|
1568
|
-
maxAgeMs: 60000,
|
|
1569
|
-
});
|
|
1570
|
-
|
|
1571
|
-
const profile = {
|
|
1572
|
-
repositoryRoot: "/test-repo",
|
|
1573
|
-
repositoryName: "test-repo",
|
|
1574
|
-
revision: "abc123",
|
|
1575
|
-
frameworks: [
|
|
1576
|
-
{
|
|
1577
|
-
category: "react" as const,
|
|
1578
|
-
name: "React",
|
|
1579
|
-
confidence: 0.9,
|
|
1580
|
-
signals: [],
|
|
1581
|
-
primary: true,
|
|
1582
|
-
},
|
|
1583
|
-
],
|
|
1584
|
-
languages: [{ name: "TypeScript", coverage: 1.0 }],
|
|
1585
|
-
packageManagers: [
|
|
1586
|
-
{ type: "npm" as const, configPath: "package.json", primary: true },
|
|
1587
|
-
],
|
|
1588
|
-
applications: [],
|
|
1589
|
-
commands: {
|
|
1590
|
-
unitTest: ["npm run test"],
|
|
1591
|
-
integrationTest: [],
|
|
1592
|
-
e2eTest: [],
|
|
1593
|
-
lint: [],
|
|
1594
|
-
typecheck: [],
|
|
1595
|
-
build: [],
|
|
1596
|
-
migrate: [],
|
|
1597
|
-
},
|
|
1598
|
-
rules: [],
|
|
1599
|
-
sensitivePaths: [],
|
|
1600
|
-
generatedPaths: [],
|
|
1601
|
-
testCapabilities: [],
|
|
1602
|
-
confidence: 0.9,
|
|
1603
|
-
warnings: [],
|
|
1604
|
-
analyzedAt: new Date().toISOString(),
|
|
1605
|
-
};
|
|
1606
|
-
|
|
1607
|
-
const key = cache.generateKey("/test-repo", "abc123", "xyz");
|
|
1608
|
-
await cache.set(key, profile, {
|
|
1609
|
-
ruleHash: "r",
|
|
1610
|
-
manifestHash: "m",
|
|
1611
|
-
configHash: "xyz",
|
|
1612
|
-
});
|
|
1613
|
-
|
|
1614
|
-
const retrieved = await cache.get(key);
|
|
1615
|
-
expect(retrieved).not.toBeNull();
|
|
1616
|
-
expect(retrieved!.repositoryName).toBe("test-repo");
|
|
1617
|
-
expect(retrieved!.languages[0].name).toBe("TypeScript");
|
|
1618
|
-
|
|
1619
|
-
await rm(tmp, { recursive: true, force: true });
|
|
1620
|
-
tempDirs.pop();
|
|
1621
|
-
});
|
|
1622
|
-
|
|
1623
|
-
test("invalidate removes all entries for a repository", async () => {
|
|
1624
|
-
const tmp = await mkdtemp(join(tmpdir(), "cache-inval-"));
|
|
1625
|
-
tempDirs.push(tmp);
|
|
1626
|
-
|
|
1627
|
-
const cache = new AnalysisCache({
|
|
1628
|
-
enabled: true,
|
|
1629
|
-
cacheDir: tmp,
|
|
1630
|
-
});
|
|
1631
|
-
|
|
1632
|
-
const key1 = cache.generateKey("/repo", "v1", "h1");
|
|
1633
|
-
await cache.set(
|
|
1634
|
-
key1,
|
|
1635
|
-
{
|
|
1636
|
-
repositoryRoot: "/repo",
|
|
1637
|
-
repositoryName: "repo",
|
|
1638
|
-
revision: "v1",
|
|
1639
|
-
frameworks: [],
|
|
1640
|
-
languages: [],
|
|
1641
|
-
packageManagers: [],
|
|
1642
|
-
applications: [],
|
|
1643
|
-
commands: {
|
|
1644
|
-
unitTest: [],
|
|
1645
|
-
integrationTest: [],
|
|
1646
|
-
e2eTest: [],
|
|
1647
|
-
lint: [],
|
|
1648
|
-
typecheck: [],
|
|
1649
|
-
build: [],
|
|
1650
|
-
migrate: [],
|
|
1651
|
-
},
|
|
1652
|
-
rules: [],
|
|
1653
|
-
sensitivePaths: [],
|
|
1654
|
-
generatedPaths: [],
|
|
1655
|
-
testCapabilities: [],
|
|
1656
|
-
confidence: 0,
|
|
1657
|
-
warnings: [],
|
|
1658
|
-
analyzedAt: new Date().toISOString(),
|
|
1659
|
-
},
|
|
1660
|
-
{ ruleHash: "r", manifestHash: "m", configHash: "h1" },
|
|
1661
|
-
);
|
|
1662
|
-
|
|
1663
|
-
await cache.invalidate("/repo");
|
|
1664
|
-
|
|
1665
|
-
const stats = await cache.getStats();
|
|
1666
|
-
expect(stats.entryCount).toBe(0);
|
|
1667
|
-
|
|
1668
|
-
await rm(tmp, { recursive: true, force: true });
|
|
1669
|
-
tempDirs.pop();
|
|
1670
|
-
});
|
|
1671
|
-
|
|
1672
|
-
test("getStats counts entries correctly", async () => {
|
|
1673
|
-
const tmp = await mkdtemp(join(tmpdir(), "cache-stats-"));
|
|
1674
|
-
tempDirs.push(tmp);
|
|
1675
|
-
|
|
1676
|
-
const cache = new AnalysisCache({ enabled: true, cacheDir: tmp });
|
|
1677
|
-
|
|
1678
|
-
// Store two entries
|
|
1679
|
-
for (const rev of ["v1", "v2"]) {
|
|
1680
|
-
const key = cache.generateKey("/repo", rev, "h");
|
|
1681
|
-
await cache.set(
|
|
1682
|
-
key,
|
|
1683
|
-
{
|
|
1684
|
-
repositoryRoot: "/repo",
|
|
1685
|
-
repositoryName: "repo",
|
|
1686
|
-
revision: rev,
|
|
1687
|
-
frameworks: [],
|
|
1688
|
-
languages: [],
|
|
1689
|
-
packageManagers: [],
|
|
1690
|
-
applications: [],
|
|
1691
|
-
commands: {
|
|
1692
|
-
unitTest: [],
|
|
1693
|
-
integrationTest: [],
|
|
1694
|
-
e2eTest: [],
|
|
1695
|
-
lint: [],
|
|
1696
|
-
typecheck: [],
|
|
1697
|
-
build: [],
|
|
1698
|
-
migrate: [],
|
|
1699
|
-
},
|
|
1700
|
-
rules: [],
|
|
1701
|
-
sensitivePaths: [],
|
|
1702
|
-
generatedPaths: [],
|
|
1703
|
-
testCapabilities: [],
|
|
1704
|
-
confidence: 0,
|
|
1705
|
-
warnings: [],
|
|
1706
|
-
analyzedAt: new Date().toISOString(),
|
|
1707
|
-
},
|
|
1708
|
-
{ ruleHash: "r", manifestHash: "m", configHash: "h" },
|
|
1709
|
-
);
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
const stats = await cache.getStats();
|
|
1713
|
-
expect(stats.entryCount).toBe(2);
|
|
1714
|
-
expect(stats.totalSize).toBeGreaterThan(0);
|
|
1715
|
-
|
|
1716
|
-
await rm(tmp, { recursive: true, force: true });
|
|
1717
|
-
tempDirs.pop();
|
|
1718
|
-
});
|
|
1719
|
-
});
|