claude-code-session-manager 0.98.0 → 0.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/bin/cli.cjs +16 -4
- package/bin/node-floor.cjs +41 -0
- package/dist/assets/{DataModel-DCEgMTjH.js → DataModel-HdR0AxFq.js} +1 -1
- package/dist/assets/{History-Bfdoywlh.js → History-Dr4Jr_5z.js} +2 -2
- package/dist/assets/{Hooks-CKDJjTJV.js → Hooks-NhrvH3JP.js} +3 -3
- package/dist/assets/{Library-7akfgy67.js → Library-Dv_EtIMr.js} +1 -1
- package/dist/assets/{MarkdownEditor-DDesiqet.js → MarkdownEditor-DTKDWV_n.js} +1 -1
- package/dist/assets/{McpServers-D4lZtXof.js → McpServers-M034G9B3.js} +2 -2
- package/dist/assets/{Memory-CeI8G1w5.js → Memory-DzF1B4gx.js} +6 -6
- package/dist/assets/{Permissions-BGleLja4.js → Permissions-DrgbSGWl.js} +3 -3
- package/dist/assets/{Plugins-P4eCXy_S.js → Plugins-Bd3ef35w.js} +2 -2
- package/dist/assets/{ProvenanceBadge-BFczzI1B.js → ProvenanceBadge-DcBHT1Iy.js} +1 -1
- package/dist/assets/{SaveBar-l9jnEMES.js → SaveBar-rimJem-Y.js} +1 -1
- package/dist/assets/Scheduler-DjgFJl2w.js +16 -0
- package/dist/assets/{ScopeSwitcher-CjJi8ST5.js → ScopeSwitcher-CnAzTBrl.js} +1 -1
- package/dist/assets/Settings-Dc1F_sSg.js +3 -0
- package/dist/assets/{SkillReferenceGraph-CjbO01Sg.js → SkillReferenceGraph-B9aZDecM.js} +1 -1
- package/dist/assets/{Skills-BxZ9_fCR.js → Skills-DRXd18gu.js} +2 -2
- package/dist/assets/{SystemPrompt-D8vfFb8b.js → SystemPrompt-ahy5-Fwe.js} +1 -1
- package/dist/assets/{TagLibrary-2vzUo7YL.js → TagLibrary-C_tToDeL.js} +1 -1
- package/dist/assets/{TiptapBody-B8ZfbK90.js → TiptapBody-DvdefS6K.js} +1 -1
- package/dist/assets/{Toggle-BbW4nGGf.js → Toggle-6Jl5Njc6.js} +1 -1
- package/dist/assets/{index-CKH5Uxik.css → index-Bta-hwud.css} +1 -1
- package/dist/assets/{index-Cd98Q1lD.js → index-vx8O73l8.js} +497 -502
- package/dist/assets/{settingsSchema-IhUfucCF.js → settingsSchema-D6iRguNV.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +25 -29
- package/plugins/CLAUDE.md +6 -6
- package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +21 -19
- package/plugins/session-manager-dev/skills/builder/SKILL.md +3 -3
- package/plugins/session-manager-dev/skills/develop/SKILL.md +254 -510
- package/plugins/session-manager-dev/skills/develop/standards.md +17 -22
- package/scripts/README.md +3 -11
- package/scripts/audit-ops-hygiene.cjs +3 -3
- package/scripts/hooks/lib/guard-prd-writes-policy.cjs +1 -1
- package/scripts/hooks/lib/guard-self-schedule-policy.cjs +1 -1
- package/scripts/mint-epic.cjs +2 -3
- package/scripts/scheduler-mcp-server.cjs +36 -7
- package/src/main/agentLibrary.cjs +22 -2
- package/src/main/build-info.json +4 -4
- package/src/main/config.cjs +41 -38
- package/src/main/docEdit.cjs +4 -1
- package/src/main/files.cjs +2 -5
- package/src/main/health.cjs +1 -1
- package/src/main/index.cjs +23 -10
- package/src/main/ipcSchemas.cjs +28 -35
- package/src/main/lib/agentPersonaSchema.cjs +13 -2
- package/src/main/lib/atomicFs.cjs +116 -0
- package/src/main/lib/branchSweep.cjs +13 -12
- package/src/main/lib/buildTarget.cjs +3 -3
- package/src/main/lib/claudeCliCaps.cjs +129 -0
- package/src/main/lib/credentials.cjs +2 -4
- package/src/main/lib/crossProjectFeedback.cjs +3 -3
- package/src/main/lib/cwdClassify.cjs +15 -3
- package/src/main/lib/definitionOfDone.cjs +207 -125
- package/src/main/lib/dodDrainHook.cjs +11 -5
- package/src/main/lib/epicMint.cjs +2 -4
- package/src/main/lib/epicStatusMirror.cjs +8 -10
- package/src/main/lib/epicWorktreeProjectConfig.cjs +2 -4
- package/src/main/lib/gateAuthority.cjs +96 -0
- package/src/main/lib/gitExec.cjs +69 -0
- package/src/main/lib/gitWorktree.cjs +656 -92
- package/src/main/lib/instanceLock.cjs +5 -8
- package/src/main/lib/launchFailure.cjs +2 -3
- package/src/main/lib/macroLibrary.cjs +386 -0
- package/src/main/lib/mcpToolCatalog.cjs +26 -17
- package/src/main/lib/needsReviewLedger.cjs +1 -1
- package/src/main/lib/opsErrorLog.cjs +11 -1
- package/src/main/lib/opsOwnership.cjs +0 -5
- package/src/main/lib/pidAlive.cjs +32 -0
- package/src/main/lib/prdCreate.cjs +121 -12
- package/src/main/lib/prdDisposition.cjs +2 -2
- package/src/main/lib/prdGateFiles.cjs +284 -0
- package/src/main/lib/prdLocations.cjs +61 -0
- package/src/main/lib/prdMigration.cjs +2 -3
- package/src/main/lib/prdSizing.cjs +2 -1
- package/src/main/lib/promptSessionSchema.cjs +10 -2
- package/src/main/lib/rcaReport.cjs +4 -6
- package/src/main/lib/reaperHelpers.cjs +8 -1
- package/src/main/lib/reservationExpiry.cjs +6 -1
- package/src/main/lib/reviewNotice.cjs +263 -0
- package/src/main/lib/runClaudeP.cjs +4 -1
- package/src/main/lib/schedulerPaths.cjs +22 -2
- package/src/main/lib/sessionSlots.cjs +2 -4
- package/src/main/lib/shippedPersonaSeeds.cjs +179 -0
- package/src/main/lib/timeoutShim.cjs +123 -0
- package/src/main/lib/timeoutShimScript.cjs +275 -0
- package/src/main/lib/upgradeDrain.cjs +4 -10
- package/src/main/lib/watchdogHelpers.cjs +12 -25
- package/src/main/lib/workTypeLibrary.cjs +7 -1
- package/src/main/promptSessionEvents.cjs +37 -13
- package/src/main/scheduler.cjs +910 -198
- package/src/main/seedAgentPersonas.cjs +278 -23
- package/src/main/supervisor.cjs +4 -1
- package/src/main/templates/PRD_AUTHORING.md +126 -406
- package/src/preload/__tests__/preload-surface.test.cjs +68 -0
- package/src/preload/api.d.ts +41 -94
- package/src/preload/index.cjs +14 -9
- package/src/seed/agents/architect.md +1 -0
- package/src/seed/agents/dev-lead.md +17 -18
- package/src/seed/agents/project-home-builder.md +1 -0
- package/src/seed/agents/validator.md +10 -4
- package/dist/assets/HostBilko-CDVHE0hT.js +0 -1
- package/dist/assets/Scheduler-C_phpeUS.js +0 -16
- package/dist/assets/Settings-BIwZ3FGQ.js +0 -3
- package/src/main/__tests__/activeIndexMerge.test.cjs +0 -235
- package/src/main/__tests__/agentEffortResolve.test.cjs +0 -117
- package/src/main/__tests__/agentLibrary.test.cjs +0 -276
- package/src/main/__tests__/agentModelResolve.test.cjs +0 -311
- package/src/main/__tests__/agentOverlayWrite.test.cjs +0 -95
- package/src/main/__tests__/bilkoHost-deriveSlug.test.cjs +0 -26
- package/src/main/__tests__/bilkoHost-integration.test.cjs +0 -118
- package/src/main/__tests__/bilkoHostCore.test.cjs +0 -72
- package/src/main/__tests__/broadcastCoalescer.test.cjs +0 -122
- package/src/main/__tests__/chat-cancel-terminal.test.cjs +0 -120
- package/src/main/__tests__/chat-dead-channels.test.cjs +0 -63
- package/src/main/__tests__/chat-exit-close-race.test.cjs +0 -146
- package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +0 -139
- package/src/main/__tests__/chat-preamble-anchors.test.cjs +0 -100
- package/src/main/__tests__/chat-queue.test.cjs +0 -97
- package/src/main/__tests__/chat-stop-signal.test.cjs +0 -89
- package/src/main/__tests__/chatRunner-epic-worktree-execcwd.test.cjs +0 -125
- package/src/main/__tests__/chatRunner-session-flag-retry.test.cjs +0 -252
- package/src/main/__tests__/classifyPromptTicket.test.cjs +0 -101
- package/src/main/__tests__/classifyTranscriptLine.test.cjs +0 -201
- package/src/main/__tests__/computeDepHistorySatisfaction.test.cjs +0 -66
- package/src/main/__tests__/config-readText-bounded.test.cjs +0 -84
- package/src/main/__tests__/configWriteBoundaryOwners.test.cjs +0 -59
- package/src/main/__tests__/crossProjectFeedback.test.cjs +0 -334
- package/src/main/__tests__/crossProjectFeedbackRoutes.test.cjs +0 -161
- package/src/main/__tests__/dep-orphan-archive-health.test.cjs +0 -77
- package/src/main/__tests__/develop-skill-failure-modes.test.cjs +0 -70
- package/src/main/__tests__/docEdit.test.cjs +0 -244
- package/src/main/__tests__/dod-batchkey.test.cjs +0 -183
- package/src/main/__tests__/dod-drain-hook.test.cjs +0 -302
- package/src/main/__tests__/dod-report.test.cjs +0 -304
- package/src/main/__tests__/dod-reverify.test.cjs +0 -285
- package/src/main/__tests__/epicContextDigest.test.cjs +0 -174
- package/src/main/__tests__/epicMint.test.cjs +0 -332
- package/src/main/__tests__/epicMintTelemetryTap.test.cjs +0 -64
- package/src/main/__tests__/epicStatusMirror.test.cjs +0 -110
- package/src/main/__tests__/epicValidationHook.test.cjs +0 -291
- package/src/main/__tests__/exchanges.test.cjs +0 -122
- package/src/main/__tests__/exchangesPromptId.test.cjs +0 -61
- package/src/main/__tests__/extractJson.test.cjs +0 -51
- package/src/main/__tests__/files-reject-credentials.test.cjs +0 -40
- package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +0 -556
- package/src/main/__tests__/flatPrdTickSweep.test.cjs +0 -110
- package/src/main/__tests__/health-build-freshness.test.cjs +0 -39
- package/src/main/__tests__/health-claude-md-budget.test.cjs +0 -57
- package/src/main/__tests__/health-credentials.test.cjs +0 -81
- package/src/main/__tests__/health-delegation-chain.test.cjs +0 -124
- package/src/main/__tests__/health-per-project-stall.test.cjs +0 -84
- package/src/main/__tests__/health-prd-migration.test.cjs +0 -37
- package/src/main/__tests__/health-queue-dispatch.test.cjs +0 -135
- package/src/main/__tests__/health-starve-escalation.test.cjs +0 -94
- package/src/main/__tests__/health-tick-liveness.test.cjs +0 -179
- package/src/main/__tests__/health-usage-poller.test.cjs +0 -144
- package/src/main/__tests__/health-worktree-cap-blocked.test.cjs +0 -65
- package/src/main/__tests__/heapSnapshot.test.cjs +0 -121
- package/src/main/__tests__/historyAggregatorIntraday.test.cjs +0 -313
- package/src/main/__tests__/historyDashboard.test.cjs +0 -163
- package/src/main/__tests__/historyRollup.test.cjs +0 -333
- package/src/main/__tests__/intradayRefresh.test.cjs +0 -39
- package/src/main/__tests__/ipcSchemas-dependsOn.test.cjs +0 -39
- package/src/main/__tests__/kg-augment.test.cjs +0 -195
- package/src/main/__tests__/loadGateDetailTick.test.cjs +0 -31
- package/src/main/__tests__/machineProfile.test.cjs +0 -152
- package/src/main/__tests__/mcpStatus.test.cjs +0 -61
- package/src/main/__tests__/memoryAggregate.test.cjs +0 -109
- package/src/main/__tests__/memoryStale.test.cjs +0 -88
- package/src/main/__tests__/needsReviewLedger.test.cjs +0 -162
- package/src/main/__tests__/openExternalApp-spawn-error.test.cjs +0 -25
- package/src/main/__tests__/opsErrorLog.test.cjs +0 -109
- package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +0 -173
- package/src/main/__tests__/personaMerge.test.cjs +0 -169
- package/src/main/__tests__/planValidator.test.cjs +0 -125
- package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +0 -176
- package/src/main/__tests__/prd-group-allocator.test.cjs +0 -119
- package/src/main/__tests__/prdAdminRouteParity.test.cjs +0 -70
- package/src/main/__tests__/prdAdminRoutes.test.cjs +0 -718
- package/src/main/__tests__/prdAgentType.test.cjs +0 -103
- package/src/main/__tests__/prdAuthoringSeed.test.cjs +0 -39
- package/src/main/__tests__/prdCreate.test.cjs +0 -979
- package/src/main/__tests__/prdCreateAdoption.test.cjs +0 -205
- package/src/main/__tests__/prdCreateDisposition.test.cjs +0 -201
- package/src/main/__tests__/prdCreatePlanId.test.cjs +0 -132
- package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +0 -117
- package/src/main/__tests__/prdFrontmatterDependsOn.test.cjs +0 -136
- package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +0 -125
- package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +0 -108
- package/src/main/__tests__/prdLocations.test.cjs +0 -195
- package/src/main/__tests__/prdLocationsArchived.test.cjs +0 -201
- package/src/main/__tests__/prdMigration.test.cjs +0 -349
- package/src/main/__tests__/prdMigrationLegacyAdopt.test.cjs +0 -91
- package/src/main/__tests__/prdParserHighWater.test.cjs +0 -74
- package/src/main/__tests__/prdParserSourcePromptId.test.cjs +0 -65
- package/src/main/__tests__/prdSetDisposition.test.cjs +0 -222
- package/src/main/__tests__/prdSizing.test.cjs +0 -106
- package/src/main/__tests__/prdSourcePromptIdBackfill.test.cjs +0 -118
- package/src/main/__tests__/prdUpdateDependsOn.test.cjs +0 -160
- package/src/main/__tests__/proc-role-env.test.cjs +0 -125
- package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +0 -304
- package/src/main/__tests__/procname-sm-processes.test.cjs +0 -127
- package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +0 -177
- package/src/main/__tests__/projectPages.test.cjs +0 -137
- package/src/main/__tests__/promptSessionEvents.test.cjs +0 -234
- package/src/main/__tests__/promptSessionSchema.test.cjs +0 -101
- package/src/main/__tests__/promptSessionTranscript.test.cjs +0 -0
- package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +0 -159
- package/src/main/__tests__/pty-epic-worktree-spawn-cwd.test.cjs +0 -282
- package/src/main/__tests__/pty-session-open-telemetry.test.cjs +0 -96
- package/src/main/__tests__/pty-write-result.test.cjs +0 -46
- package/src/main/__tests__/queue-health-verdict.test.cjs +0 -170
- package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +0 -286
- package/src/main/__tests__/queue-starvation-per-project.test.cjs +0 -147
- package/src/main/__tests__/queueHistory.test.cjs +0 -355
- package/src/main/__tests__/queueOps-interactive-ac-lint.test.cjs +0 -65
- package/src/main/__tests__/queueOpsArchiveDestination.test.cjs +0 -65
- package/src/main/__tests__/queueOpsAutoArchive.test.cjs +0 -211
- package/src/main/__tests__/rateLimitPollerStreak.test.cjs +0 -128
- package/src/main/__tests__/rcaReport.test.cjs +0 -266
- package/src/main/__tests__/reconcileFlatPrdSweep.test.cjs +0 -119
- package/src/main/__tests__/reconcileTiming.test.cjs +0 -135
- package/src/main/__tests__/runLogRetention.test.cjs +0 -489
- package/src/main/__tests__/runVerify-atomic-verdicts.test.cjs +0 -26
- package/src/main/__tests__/runVerify-blocked-by-foreign-wip.test.cjs +0 -58
- package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +0 -191
- package/src/main/__tests__/runVerify-policy-denial.test.cjs +0 -89
- package/src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs +0 -225
- package/src/main/__tests__/runVerify.test.cjs +0 -1784
- package/src/main/__tests__/scheduleJobSchema.test.cjs +0 -127
- package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +0 -65
- package/src/main/__tests__/scheduleJobTransitions.test.cjs +0 -277
- package/src/main/__tests__/scheduleJobTransitionsGrep.test.cjs +0 -59
- package/src/main/__tests__/scheduleJobTransitionsTelemetryTap.test.cjs +0 -72
- package/src/main/__tests__/scheduler-admin-routes.test.cjs +0 -199
- package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +0 -143
- package/src/main/__tests__/scheduler-already-satisfied-on-main.test.cjs +0 -105
- package/src/main/__tests__/scheduler-archive-completed-prd.test.cjs +0 -102
- package/src/main/__tests__/scheduler-archived-twin-guard.test.cjs +0 -155
- package/src/main/__tests__/scheduler-autofix-outcome.test.cjs +0 -188
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +0 -439
- package/src/main/__tests__/scheduler-autopromote.test.cjs +0 -51
- package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +0 -101
- package/src/main/__tests__/scheduler-blocked-by-foreign-wip.test.cjs +0 -107
- package/src/main/__tests__/scheduler-boot-orphans.test.cjs +0 -153
- package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +0 -121
- package/src/main/__tests__/scheduler-clear-queue-history.test.cjs +0 -134
- package/src/main/__tests__/scheduler-commit-guard-noop.test.cjs +0 -244
- package/src/main/__tests__/scheduler-committed-in-window.test.cjs +0 -182
- package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +0 -43
- package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +0 -168
- package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +0 -58
- package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +0 -81
- package/src/main/__tests__/scheduler-epic-digest.test.cjs +0 -227
- package/src/main/__tests__/scheduler-failed-autoreset.test.cjs +0 -121
- package/src/main/__tests__/scheduler-finalize-dispatch-guards.test.cjs +0 -229
- package/src/main/__tests__/scheduler-find-prd-dir.test.cjs +0 -75
- package/src/main/__tests__/scheduler-fix-plan-path.test.cjs +0 -119
- package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +0 -46
- package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +0 -78
- package/src/main/__tests__/scheduler-gate-shadow.test.cjs +0 -119
- package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +0 -390
- package/src/main/__tests__/scheduler-heal-refusal.test.cjs +0 -61
- package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +0 -80
- package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +0 -323
- package/src/main/__tests__/scheduler-integration-failure-stamp.test.cjs +0 -41
- package/src/main/__tests__/scheduler-investigation-clean-skip.test.cjs +0 -63
- package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +0 -123
- package/src/main/__tests__/scheduler-job-budget.test.cjs +0 -172
- package/src/main/__tests__/scheduler-job-overrun.test.cjs +0 -175
- package/src/main/__tests__/scheduler-launch-failure.test.cjs +0 -199
- package/src/main/__tests__/scheduler-leftover-fields.test.cjs +0 -52
- package/src/main/__tests__/scheduler-leftover-quarantine.test.cjs +0 -199
- package/src/main/__tests__/scheduler-looks-done.test.cjs +0 -537
- package/src/main/__tests__/scheduler-manual-pause.test.cjs +0 -118
- package/src/main/__tests__/scheduler-mechanical-recovery.test.cjs +0 -245
- package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +0 -46
- package/src/main/__tests__/scheduler-mutate-reentrancy.test.cjs +0 -117
- package/src/main/__tests__/scheduler-needs-review-autoresolve.test.cjs +0 -197
- package/src/main/__tests__/scheduler-never-stop.test.cjs +0 -157
- package/src/main/__tests__/scheduler-no-dead-end-status.test.cjs +0 -152
- package/src/main/__tests__/scheduler-no-orphan-run-dir.test.cjs +0 -81
- package/src/main/__tests__/scheduler-notify-originating-tab-transcript.test.cjs +0 -87
- package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +0 -343
- package/src/main/__tests__/scheduler-periodic-reverify-guard.test.cjs +0 -217
- package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +0 -164
- package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +0 -161
- package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +0 -178
- package/src/main/__tests__/scheduler-quarantine-autoresolve.test.cjs +0 -165
- package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +0 -257
- package/src/main/__tests__/scheduler-rate-limit-cooldown-freshness.test.cjs +0 -123
- package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +0 -152
- package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +0 -156
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +0 -754
- package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +0 -87
- package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +0 -100
- package/src/main/__tests__/scheduler-reconcile-history-backfill.test.cjs +0 -105
- package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +0 -203
- package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +0 -247
- package/src/main/__tests__/scheduler-reset-job-fields-guard.test.cjs +0 -77
- package/src/main/__tests__/scheduler-resume-recovery.test.cjs +0 -254
- package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +0 -115
- package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +0 -380
- package/src/main/__tests__/scheduler-sigterm-commit.test.cjs +0 -43
- package/src/main/__tests__/scheduler-stall-per-project.test.cjs +0 -126
- package/src/main/__tests__/scheduler-starve-escalation.test.cjs +0 -154
- package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +0 -252
- package/src/main/__tests__/scheduler-stranded-investigation.test.cjs +0 -185
- package/src/main/__tests__/scheduler-stuck-failed-escalation.test.cjs +0 -136
- package/src/main/__tests__/scheduler-supervisor-record.test.cjs +0 -81
- package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +0 -54
- package/src/main/__tests__/scheduler-tick-wedge.test.cjs +0 -172
- package/src/main/__tests__/scheduler-transient-failure.test.cjs +0 -141
- package/src/main/__tests__/scheduler-unreadable-queue-guard.test.cjs +0 -62
- package/src/main/__tests__/scheduler-utilization-hold.test.cjs +0 -89
- package/src/main/__tests__/scheduler-verify-prd-path.test.cjs +0 -109
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +0 -234
- package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +0 -120
- package/src/main/__tests__/scheduler-writeprd-epic-rollback.test.cjs +0 -105
- package/src/main/__tests__/schedulerBatchRootBlocker.test.cjs +0 -117
- package/src/main/__tests__/schedulerStateSidecarRestore.test.cjs +0 -110
- package/src/main/__tests__/seedAgentPersonas.test.cjs +0 -184
- package/src/main/__tests__/seedSchedulerMcp.test.cjs +0 -211
- package/src/main/__tests__/seedStatus.test.cjs +0 -100
- package/src/main/__tests__/seedValidatorPersona.test.cjs +0 -116
- package/src/main/__tests__/stop-signal-anchor.test.cjs +0 -75
- package/src/main/__tests__/telemetryClient.test.cjs +0 -1055
- package/src/main/__tests__/telemetryContract.test.cjs +0 -930
- package/src/main/__tests__/telemetrySettings.test.cjs +0 -210
- package/src/main/__tests__/transcripts-batch-flush.test.cjs +0 -249
- package/src/main/__tests__/transcripts-doFlush-array.test.cjs +0 -124
- package/src/main/__tests__/transcripts-paged-reads.test.cjs +0 -241
- package/src/main/__tests__/transcripts-worktree-epic-path.test.cjs +0 -154
- package/src/main/__tests__/transcriptsUsageFor.test.cjs +0 -206
- package/src/main/__tests__/uniquePrdNumbers.test.cjs +0 -153
- package/src/main/__tests__/usageSingleFlight.test.cjs +0 -169
- package/src/main/__tests__/validationSentinels.test.cjs +0 -84
- package/src/main/__tests__/workTypeLibrary.test.cjs +0 -89
- package/src/main/bilkoHost.cjs +0 -314
- package/src/main/bilkoHostCore.cjs +0 -89
- package/src/main/lib/__tests__/active-sessions.test.cjs +0 -251
- package/src/main/lib/__tests__/activeIndexRebuild.test.cjs +0 -179
- package/src/main/lib/__tests__/agentPersonaSchema.test.cjs +0 -67
- package/src/main/lib/__tests__/auditLog.test.cjs +0 -38
- package/src/main/lib/__tests__/bootSelfHeal.test.cjs +0 -107
- package/src/main/lib/__tests__/branchSweep.test.cjs +0 -164
- package/src/main/lib/__tests__/buildIdentity.test.cjs +0 -121
- package/src/main/lib/__tests__/buildTarget.test.cjs +0 -52
- package/src/main/lib/__tests__/childWithLog.test.cjs +0 -321
- package/src/main/lib/__tests__/coldBootPromptSessionsWrite.test.cjs +0 -87
- package/src/main/lib/__tests__/crashTelemetry.test.cjs +0 -103
- package/src/main/lib/__tests__/credentials-futile-refresh.test.cjs +0 -115
- package/src/main/lib/__tests__/cwdClassify.test.cjs +0 -111
- package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +0 -95
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +0 -1175
- package/src/main/lib/__tests__/dispatchLoop.test.cjs +0 -63
- package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +0 -244
- package/src/main/lib/__tests__/ephemeralCwd.test.cjs +0 -91
- package/src/main/lib/__tests__/epicDelegationStats.test.cjs +0 -137
- package/src/main/lib/__tests__/epicSpawnCwd.test.cjs +0 -283
- package/src/main/lib/__tests__/epicSpawnPlan.test.cjs +0 -196
- package/src/main/lib/__tests__/epicTranscriptPath.test.cjs +0 -163
- package/src/main/lib/__tests__/epicWorktreeBoot.test.cjs +0 -136
- package/src/main/lib/__tests__/epicWorktreeMerge.test.cjs +0 -130
- package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +0 -117
- package/src/main/lib/__tests__/epicWorktreeProjectConfig.test.cjs +0 -137
- package/src/main/lib/__tests__/fixChainDepth.test.cjs +0 -40
- package/src/main/lib/__tests__/fixtures/204-mercury-steam-horse.log.txt +0 -13
- package/src/main/lib/__tests__/fixtures/scheduler-machine.json.corrupt-1789147548 +0 -34
- package/src/main/lib/__tests__/gateFixtures.json +0 -20
- package/src/main/lib/__tests__/gitCacheBound.test.cjs +0 -69
- package/src/main/lib/__tests__/gitWorktree.test.cjs +0 -1478
- package/src/main/lib/__tests__/gitWorktreeSalvage.test.cjs +0 -107
- package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +0 -153
- package/src/main/lib/__tests__/guardShims.test.cjs +0 -158
- package/src/main/lib/__tests__/importReferences.spec.cjs +0 -56
- package/src/main/lib/__tests__/instanceLock.test.cjs +0 -173
- package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +0 -78
- package/src/main/lib/__tests__/jobWorktree.test.cjs +0 -199
- package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +0 -82
- package/src/main/lib/__tests__/landedSinceRun.test.cjs +0 -133
- package/src/main/lib/__tests__/launchFailure.test.cjs +0 -220
- package/src/main/lib/__tests__/loadGate.test.cjs +0 -303
- package/src/main/lib/__tests__/localAdminHttp.test.cjs +0 -214
- package/src/main/lib/__tests__/loopDelay.test.cjs +0 -68
- package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +0 -107
- package/src/main/lib/__tests__/modelCatalog.test.cjs +0 -202
- package/src/main/lib/__tests__/opsOwnership.test.cjs +0 -113
- package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +0 -328
- package/src/main/lib/__tests__/opsRootNestedWrite.test.cjs +0 -51
- package/src/main/lib/__tests__/opsRootResolve.test.cjs +0 -149
- package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +0 -82
- package/src/main/lib/__tests__/prdDisposition.test.cjs +0 -224
- package/src/main/lib/__tests__/procIdentity.test.cjs +0 -119
- package/src/main/lib/__tests__/procName.test.cjs +0 -92
- package/src/main/lib/__tests__/projectBriefCore.test.cjs +0 -216
- package/src/main/lib/__tests__/projectRootResolve.test.cjs +0 -148
- package/src/main/lib/__tests__/queueHealth.test.cjs +0 -58
- package/src/main/lib/__tests__/queueStoreAtomicWrite.test.cjs +0 -88
- package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +0 -190
- package/src/main/lib/__tests__/quietMachineLease.test.cjs +0 -39
- package/src/main/lib/__tests__/rateLimitWindow.test.cjs +0 -88
- package/src/main/lib/__tests__/reaperHelpers.test.cjs +0 -577
- package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +0 -312
- package/src/main/lib/__tests__/schedulerBatchFairness.test.cjs +0 -213
- package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +0 -125
- package/src/main/lib/__tests__/schedulerBatchProjectCap.test.cjs +0 -127
- package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +0 -109
- package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +0 -71
- package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +0 -216
- package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +0 -183
- package/src/main/lib/__tests__/schedulerPaths.test.cjs +0 -226
- package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +0 -133
- package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +0 -56
- package/src/main/lib/__tests__/sessionSlots.test.cjs +0 -144
- package/src/main/lib/__tests__/telemetryBacklog.test.cjs +0 -626
- package/src/main/lib/__tests__/telemetryBoot.test.cjs +0 -134
- package/src/main/lib/__tests__/telemetryConsent.test.cjs +0 -136
- package/src/main/lib/__tests__/telemetryCounters.test.cjs +0 -57
- package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +0 -98
- package/src/main/lib/__tests__/terminalRunOutcome.test.cjs +0 -200
- package/src/main/lib/__tests__/toolUseClassify.test.cjs +0 -53
- package/src/main/lib/__tests__/updateCheck.test.cjs +0 -63
- package/src/main/lib/__tests__/upgradeDrain.test.cjs +0 -130
- package/src/main/lib/__tests__/usageCircuit.test.cjs +0 -354
- package/src/main/lib/__tests__/watchdog-helpers.test.cjs +0 -441
- package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +0 -266
- package/src/main/lib/kgExchangePairing.cjs +0 -75
|
@@ -1,979 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* prdCreate.test.cjs — unit tests for the create-PRD body builder (PRD 549,
|
|
3
|
-
* gh-issue-6) and, since PRD 689, its registerAdminRoute create-prd HTTP
|
|
4
|
-
* route (moved verbatim out of the former standalone admin HTTP server
|
|
5
|
-
* module's handleRequest).
|
|
6
|
-
*
|
|
7
|
-
* Run: timeout 120 npx vitest run src/main/__tests__/prdCreate.test.cjs
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
'use strict';
|
|
11
|
-
|
|
12
|
-
import { test, expect, vi } from 'vitest';
|
|
13
|
-
const fs = require('node:fs');
|
|
14
|
-
const fsp = require('node:fs/promises');
|
|
15
|
-
const os = require('node:os');
|
|
16
|
-
const path = require('node:path');
|
|
17
|
-
const http = require('node:http');
|
|
18
|
-
const { execFileSync } = require('node:child_process');
|
|
19
|
-
const {
|
|
20
|
-
deriveSlugFromTitle,
|
|
21
|
-
buildPrdBody,
|
|
22
|
-
readStandards,
|
|
23
|
-
STANDARDS_PATH,
|
|
24
|
-
PRD_CREATE_SLUG_RE,
|
|
25
|
-
registerAdminRoute,
|
|
26
|
-
createPrd,
|
|
27
|
-
resolveSourcePromptIdFromClaudeSession,
|
|
28
|
-
} = require('../lib/prdCreate.cjs');
|
|
29
|
-
const { createAdminHttp } = require('../lib/localAdminHttp.cjs');
|
|
30
|
-
const config = require('../config.cjs');
|
|
31
|
-
const prdParser = require('../scheduler/prdParser.cjs');
|
|
32
|
-
|
|
33
|
-
test('deriveSlugFromTitle lowercases, kebab-cases, and strips non-alnum runs', () => {
|
|
34
|
-
expect(deriveSlugFromTitle('Add Foo Bar!! Baz')).toBe('add-foo-bar-baz');
|
|
35
|
-
expect(deriveSlugFromTitle(' leading/trailing ')).toBe('leading-trailing');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test('deriveSlugFromTitle output always satisfies PRD_CREATE_SLUG_RE', () => {
|
|
39
|
-
const slug = deriveSlugFromTitle('Some Title 123');
|
|
40
|
-
expect(PRD_CREATE_SLUG_RE.test(slug)).toBeTruthy();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test('readStandards reads the real standards.md and returns non-empty text', async () => {
|
|
44
|
-
const text = await readStandards();
|
|
45
|
-
expect(text.includes('Execution discipline')).toBeTruthy();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test('buildPrdBody emits required frontmatter keys and body sections in order', async () => {
|
|
49
|
-
const body = buildPrdBody({
|
|
50
|
-
title: 'Do the thing',
|
|
51
|
-
cwd: '~/Projects/session-manager',
|
|
52
|
-
estimateMinutes: 15,
|
|
53
|
-
goal: 'Build the thing.',
|
|
54
|
-
acceptanceCriteria: ['thing exists', 'tests pass'],
|
|
55
|
-
implementationNotes: 'See file.cjs:10.',
|
|
56
|
-
outOfScope: ['not this'],
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
expect(body.startsWith('---\n')).toBeTruthy();
|
|
60
|
-
expect(body).toMatch(/title: Do the thing/);
|
|
61
|
-
expect(body).toMatch(/cwd: ~\/Projects\/session-manager/);
|
|
62
|
-
expect(body).toMatch(/estimateMinutes: 15/);
|
|
63
|
-
|
|
64
|
-
const goalIdx = body.indexOf('# Goal');
|
|
65
|
-
const acIdx = body.indexOf('# Acceptance criteria');
|
|
66
|
-
const implIdx = body.indexOf('# Implementation notes');
|
|
67
|
-
const oosIdx = body.indexOf('# Out of scope');
|
|
68
|
-
const standardsIdx = body.indexOf('## Engineering standards');
|
|
69
|
-
// sections must appear in Goal -> AC -> Implementation notes -> Out of scope -> Engineering standards order
|
|
70
|
-
expect(goalIdx > 0 && goalIdx < acIdx && acIdx < implIdx && implIdx < oosIdx && oosIdx < standardsIdx).toBeTruthy();
|
|
71
|
-
|
|
72
|
-
expect(body).toMatch(/- \[ \] thing exists/);
|
|
73
|
-
expect(body).toMatch(/- \[ \] tests pass/);
|
|
74
|
-
expect(body).toMatch(/- not this/);
|
|
75
|
-
// must point at STANDARDS_PATH rather than inline the standards.md content
|
|
76
|
-
expect(body.includes(STANDARDS_PATH)).toBeTruthy();
|
|
77
|
-
expect(body).toMatch(/Your system prompt carries the ordered run contract/);
|
|
78
|
-
// the pointer block mentions "Execution discipline" by name, but must not
|
|
79
|
-
// inline the full standards.md prose (e.g. its Performance-section rules)
|
|
80
|
-
expect(body.includes('Lay out hot data contiguously')).toBeFalsy();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test('buildPrdBody emits agentType: dev-lead by default when omitted (PRD 1114)', () => {
|
|
84
|
-
const body = buildPrdBody({
|
|
85
|
-
title: 't', cwd: '~/x', estimateMinutes: 5, goal: 'g',
|
|
86
|
-
acceptanceCriteria: ['a'], implementationNotes: 'n',
|
|
87
|
-
});
|
|
88
|
-
expect(body).toMatch(/^agentType: dev-lead$/m);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test('buildPrdBody emits the supplied agentType instead of the default', () => {
|
|
92
|
-
const body = buildPrdBody({
|
|
93
|
-
title: 't', cwd: '~/x', estimateMinutes: 5, goal: 'g',
|
|
94
|
-
acceptanceCriteria: ['a'], implementationNotes: 'n',
|
|
95
|
-
agentType: 'architect',
|
|
96
|
-
});
|
|
97
|
-
expect(body).toMatch(/^agentType: architect$/m);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test('buildPrdBody omits parallelGroup frontmatter key when not supplied', () => {
|
|
101
|
-
const body = buildPrdBody({
|
|
102
|
-
title: 't', cwd: '~/x', estimateMinutes: 5, goal: 'g',
|
|
103
|
-
acceptanceCriteria: ['a'], implementationNotes: 'n',
|
|
104
|
-
});
|
|
105
|
-
expect(!/parallelGroup:/.test(body)).toBeTruthy();
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test('buildPrdBody writes sourcePromptId into frontmatter when supplied (PRD 749 traceability)', () => {
|
|
109
|
-
const body = buildPrdBody({
|
|
110
|
-
title: 't', cwd: '~/x', estimateMinutes: 5, goal: 'g',
|
|
111
|
-
acceptanceCriteria: ['a'], implementationNotes: 'n',
|
|
112
|
-
sourcePromptId: 'ticket-abc-123',
|
|
113
|
-
});
|
|
114
|
-
expect(body).toMatch(/sourcePromptId: ticket-abc-123/);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test('buildPrdBody omits sourcePromptId frontmatter key when not supplied', () => {
|
|
118
|
-
const body = buildPrdBody({
|
|
119
|
-
title: 't', cwd: '~/x', estimateMinutes: 5, goal: 'g',
|
|
120
|
-
acceptanceCriteria: ['a'], implementationNotes: 'n',
|
|
121
|
-
});
|
|
122
|
-
expect(!/sourcePromptId:/.test(body)).toBeTruthy();
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test('readStandards result is byte-identical to the on-disk file (single source of truth)', async () => {
|
|
126
|
-
const onDisk = fs.readFileSync(
|
|
127
|
-
path.join(__dirname, '..', '..', '..', 'plugins', 'session-manager-dev', 'skills', 'develop', 'standards.md'),
|
|
128
|
-
'utf8',
|
|
129
|
-
);
|
|
130
|
-
expect(await readStandards()).toBe(onDisk);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// ──────────────────────────────────────────── create-prd admin route
|
|
134
|
-
|
|
135
|
-
function request(port, { method = 'GET', path: reqPath, token, body }) {
|
|
136
|
-
return new Promise((resolve, reject) => {
|
|
137
|
-
const headers = {};
|
|
138
|
-
if (token !== undefined) headers.Authorization = `Bearer ${token}`;
|
|
139
|
-
let payload;
|
|
140
|
-
if (body !== undefined) {
|
|
141
|
-
payload = JSON.stringify(body);
|
|
142
|
-
headers['Content-Type'] = 'application/json';
|
|
143
|
-
headers['Content-Length'] = Buffer.byteLength(payload);
|
|
144
|
-
}
|
|
145
|
-
const req = http.request({ hostname: '127.0.0.1', port, method, path: reqPath, headers }, (res) => {
|
|
146
|
-
const chunks = [];
|
|
147
|
-
res.on('data', (c) => chunks.push(c));
|
|
148
|
-
res.on('end', () => {
|
|
149
|
-
const text = Buffer.concat(chunks).toString('utf8');
|
|
150
|
-
let json = null;
|
|
151
|
-
try { json = JSON.parse(text); } catch { /* leave null for non-JSON bodies */ }
|
|
152
|
-
resolve({ status: res.statusCode, json, text });
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
req.on('error', reject);
|
|
156
|
-
if (payload) req.write(payload);
|
|
157
|
-
req.end();
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// config.cjs's validateWrite only allows writes under a registered non-home
|
|
162
|
-
// allowedRoot's `.claude/` subtree (mirrors the real ROOT layout, which is
|
|
163
|
-
// $HOME/.claude/session-manager/...) — so the temp prdsDir must live under
|
|
164
|
-
// `<root>/.claude/...`, not directly under the temp root itself.
|
|
165
|
-
async function mkTmpPrdsDir() {
|
|
166
|
-
const root = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-admin-create-prd-'));
|
|
167
|
-
config.addAllowedRoot(root);
|
|
168
|
-
const prdsDir = path.join(root, '.claude', 'prds');
|
|
169
|
-
await fsp.mkdir(prdsDir, { recursive: true });
|
|
170
|
-
return prdsDir;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Mirrors scheduler.cjs's real `remote.allocateParallelGroup` / `remote.writePrd`
|
|
174
|
-
// (same underlying prdParser.allocateParallelGroup + config.writeTextAtomic
|
|
175
|
-
// calls) but pointed at a throwaway temp dir instead of the user's real
|
|
176
|
-
// scheduled-plans/prds — so these tests never touch $HOME/.claude.
|
|
177
|
-
function makeFakeRemoteWithPrdsDir(prdsDir) {
|
|
178
|
-
return {
|
|
179
|
-
async allocateParallelGroup() {
|
|
180
|
-
return prdParser.allocateParallelGroup(prdsDir);
|
|
181
|
-
},
|
|
182
|
-
async readPrd(slug) {
|
|
183
|
-
try {
|
|
184
|
-
const text = await fsp.readFile(path.join(prdsDir, `${slug}.md`), 'utf8');
|
|
185
|
-
return { ok: true, text };
|
|
186
|
-
} catch (e) {
|
|
187
|
-
return { ok: false, error: e?.message };
|
|
188
|
-
}
|
|
189
|
-
},
|
|
190
|
-
async writePrd(slug, body) {
|
|
191
|
-
try {
|
|
192
|
-
const filePath = path.join(prdsDir, `${slug}.md`);
|
|
193
|
-
await config.writeTextAtomic(filePath, body, { writer: 'scheduler' });
|
|
194
|
-
return { ok: true };
|
|
195
|
-
} catch (e) {
|
|
196
|
-
return { ok: false, error: e?.message };
|
|
197
|
-
}
|
|
198
|
-
},
|
|
199
|
-
// Minimal stand-in for scheduler.cjs's real remote.listPrds: every
|
|
200
|
-
// `<slug>.md` under the fake project's prdsDir, as a `{ slug }` row —
|
|
201
|
-
// enough for prdCreate.cjs's dependsOn-existence validation, which only
|
|
202
|
-
// ever reads `.slug` off each entry.
|
|
203
|
-
async listPrds() {
|
|
204
|
-
const entries = await fsp.readdir(prdsDir);
|
|
205
|
-
const prds = entries
|
|
206
|
-
.filter((f) => f.endsWith('.md'))
|
|
207
|
-
.map((f) => ({ slug: f.slice(0, -3) }));
|
|
208
|
-
return { prds, total: prds.length, limit: prds.length, offset: 0, hasMore: false };
|
|
209
|
-
},
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/** Pre-seed the fake project's prdsDir with an existing (empty-body) PRD file at `slug`. */
|
|
214
|
-
async function seedExistingPrd(prdsDir, slug) {
|
|
215
|
-
await fsp.writeFile(path.join(prdsDir, `${slug}.md`), '---\ntitle: seed\n---\n# Goal\n');
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function validCreateBody(overrides = {}) {
|
|
219
|
-
return {
|
|
220
|
-
title: 'Add widget frobnication',
|
|
221
|
-
cwd: os.homedir(),
|
|
222
|
-
estimateMinutes: 15,
|
|
223
|
-
goal: 'Add frobnication to the widget subsystem.',
|
|
224
|
-
acceptanceCriteria: ['widget frobnicates on click', 'timeout 300 npm run typecheck passes'],
|
|
225
|
-
implementationNotes: 'See src/widget.cjs:10.',
|
|
226
|
-
outOfScope: ['not touching gadgets'],
|
|
227
|
-
...overrides,
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async function startWithRemote(remote) {
|
|
232
|
-
// Force the e2e-suffixed token path so this suite never races the real
|
|
233
|
-
// ~/.claude/session-manager/admin-api.json against other test files (or a
|
|
234
|
-
// live production instance) that also call createAdminHttp() concurrently.
|
|
235
|
-
const prevE2e = process.env.SM_E2E;
|
|
236
|
-
process.env.SM_E2E = '1';
|
|
237
|
-
const admin = createAdminHttp();
|
|
238
|
-
registerAdminRoute(admin, remote);
|
|
239
|
-
const { port, token } = await admin.start();
|
|
240
|
-
if (prevE2e === undefined) delete process.env.SM_E2E; else process.env.SM_E2E = prevE2e;
|
|
241
|
-
return { admin, port, token };
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
test('POST /admin/scheduler/create-prd without token returns 401', async () => {
|
|
245
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
246
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
247
|
-
const { admin, port } = await startWithRemote(remote);
|
|
248
|
-
try {
|
|
249
|
-
const res = await request(port, {
|
|
250
|
-
method: 'POST', path: '/admin/scheduler/create-prd', body: validCreateBody(),
|
|
251
|
-
});
|
|
252
|
-
expect(res.status).toBe(401);
|
|
253
|
-
} finally {
|
|
254
|
-
await admin.stop();
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
test('POST /admin/scheduler/create-prd with a valid payload writes a file with frontmatter + standards appended, returns {nn, filename, enqueued:false}', async () => {
|
|
259
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
260
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
261
|
-
const { admin, port, token } = await startWithRemote(remote);
|
|
262
|
-
try {
|
|
263
|
-
const res = await request(port, {
|
|
264
|
-
method: 'POST', path: '/admin/scheduler/create-prd', token, body: validCreateBody(),
|
|
265
|
-
});
|
|
266
|
-
expect(res.status).toBe(200);
|
|
267
|
-
// Never a 'status' field, and never the literal 'queued' — this route
|
|
268
|
-
// only writes the PRD file; the queue row is derived by the next
|
|
269
|
-
// reconcile() pass, not created here. See prdCreate.cjs's createPrd.
|
|
270
|
-
expect(res.json.status).toBeUndefined();
|
|
271
|
-
expect(res.json.enqueued).toBe(false);
|
|
272
|
-
expect(typeof res.json.note).toBe('string');
|
|
273
|
-
expect(typeof res.json.nn).toBe('number');
|
|
274
|
-
expect(res.json.filename).toMatch(/^\d+-add-widget-frobnication\.md$/);
|
|
275
|
-
|
|
276
|
-
const written = await fsp.readFile(path.join(prdsDir, res.json.filename), 'utf8');
|
|
277
|
-
expect(written).toMatch(/^---\n/);
|
|
278
|
-
expect(written).toMatch(/title: Add widget frobnication/);
|
|
279
|
-
expect(written).toMatch(/cwd: .+/);
|
|
280
|
-
expect(written).toMatch(/estimateMinutes: 15/);
|
|
281
|
-
expect(written).toMatch(/# Goal/);
|
|
282
|
-
expect(written).toMatch(/# Acceptance criteria/);
|
|
283
|
-
expect(written).toMatch(/- \[ \] widget frobnicates on click/);
|
|
284
|
-
expect(written).toMatch(/# Implementation notes/);
|
|
285
|
-
expect(written).toMatch(/# Out of scope/);
|
|
286
|
-
expect(written).toMatch(/## Engineering standards/);
|
|
287
|
-
// must point at STANDARDS_PATH rather than inline standards.md
|
|
288
|
-
expect(written.includes(STANDARDS_PATH)).toBeTruthy();
|
|
289
|
-
expect(written.includes('Lay out hot data contiguously')).toBeFalsy();
|
|
290
|
-
} finally {
|
|
291
|
-
await admin.stop();
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
test('POST /admin/scheduler/create-prd with malformed frontmatter (missing acceptanceCriteria) is rejected and writes no file', async () => {
|
|
296
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
297
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
298
|
-
const { admin, port, token } = await startWithRemote(remote);
|
|
299
|
-
try {
|
|
300
|
-
const body = validCreateBody();
|
|
301
|
-
delete body.acceptanceCriteria;
|
|
302
|
-
const res = await request(port, {
|
|
303
|
-
method: 'POST', path: '/admin/scheduler/create-prd', token, body,
|
|
304
|
-
});
|
|
305
|
-
expect(res.status).toBe(400);
|
|
306
|
-
expect(res.json.ok).toBe(false);
|
|
307
|
-
const entries = await fsp.readdir(prdsDir);
|
|
308
|
-
expect(entries.filter((f) => f.endsWith('.md')).length).toBe(0);
|
|
309
|
-
} finally {
|
|
310
|
-
await admin.stop();
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
test('POST /admin/scheduler/create-prd with cwd outside allowed roots is rejected and writes no file', async () => {
|
|
315
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
316
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
317
|
-
const { admin, port, token } = await startWithRemote(remote);
|
|
318
|
-
try {
|
|
319
|
-
const res = await request(port, {
|
|
320
|
-
method: 'POST', path: '/admin/scheduler/create-prd', token,
|
|
321
|
-
body: validCreateBody({ cwd: '/etc/passwd-adjacent-outside-home' }),
|
|
322
|
-
});
|
|
323
|
-
expect(res.status).toBe(400);
|
|
324
|
-
expect(res.json.ok).toBe(false);
|
|
325
|
-
const entries = await fsp.readdir(prdsDir);
|
|
326
|
-
expect(entries.filter((f) => f.endsWith('.md')).length).toBe(0);
|
|
327
|
-
} finally {
|
|
328
|
-
await admin.stop();
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
test('POST /admin/scheduler/create-prd rejects a title containing a newline (frontmatter-injection guard) and writes no file', async () => {
|
|
333
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
334
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
335
|
-
const { admin, port, token } = await startWithRemote(remote);
|
|
336
|
-
try {
|
|
337
|
-
const res = await request(port, {
|
|
338
|
-
method: 'POST', path: '/admin/scheduler/create-prd', token,
|
|
339
|
-
body: validCreateBody({ title: 'Legit title\n---\nEvil: injected' }),
|
|
340
|
-
});
|
|
341
|
-
expect(res.status).toBe(400);
|
|
342
|
-
expect(res.json.ok).toBe(false);
|
|
343
|
-
const entries = await fsp.readdir(prdsDir);
|
|
344
|
-
expect(entries.filter((f) => f.endsWith('.md')).length).toBe(0);
|
|
345
|
-
} finally {
|
|
346
|
-
await admin.stop();
|
|
347
|
-
}
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
test('PRD 832: an explicit parallelGroup colliding with an existing file is ignored — a fresh unique NN is allocated and nothing is clobbered', async () => {
|
|
351
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
352
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
353
|
-
const { admin, port, token } = await startWithRemote(remote);
|
|
354
|
-
try {
|
|
355
|
-
await fsp.writeFile(path.join(prdsDir, '777-my-explicit-slug.md'), 'ORIGINAL CONTENT\n');
|
|
356
|
-
const res = await request(port, {
|
|
357
|
-
method: 'POST', path: '/admin/scheduler/create-prd', token,
|
|
358
|
-
body: validCreateBody({ slug: 'my-explicit-slug', parallelGroup: 777 }),
|
|
359
|
-
});
|
|
360
|
-
expect(res.status).toBe(200);
|
|
361
|
-
// Allocator scans past the existing 777 — unique per project, never reused.
|
|
362
|
-
expect(res.json.nn).toBe(778);
|
|
363
|
-
expect(res.json.filename).toBe('778-my-explicit-slug.md');
|
|
364
|
-
const stillThere = await fsp.readFile(path.join(prdsDir, '777-my-explicit-slug.md'), 'utf8');
|
|
365
|
-
expect(stillThere).toBe('ORIGINAL CONTENT\n');
|
|
366
|
-
} finally {
|
|
367
|
-
await admin.stop();
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
test('PRD 832: explicit parallelGroup input is deprecated and ignored — NN always comes from the allocator', async () => {
|
|
372
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
373
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
374
|
-
const { admin, port, token } = await startWithRemote(remote);
|
|
375
|
-
try {
|
|
376
|
-
const res = await request(port, {
|
|
377
|
-
method: 'POST', path: '/admin/scheduler/create-prd', token,
|
|
378
|
-
body: validCreateBody({ slug: 'my-explicit-slug', parallelGroup: 777 }),
|
|
379
|
-
});
|
|
380
|
-
expect(res.status).toBe(200);
|
|
381
|
-
expect(res.json.nn).toBe(1);
|
|
382
|
-
expect(res.json.filename).toBe('1-my-explicit-slug.md');
|
|
383
|
-
expect(fs.existsSync(path.join(prdsDir, '1-my-explicit-slug.md'))).toBeTruthy();
|
|
384
|
-
} finally {
|
|
385
|
-
await admin.stop();
|
|
386
|
-
}
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
// ──────────────────────────────────────────── createPrd() (chat:create-prd IPC handler, PRD 749 follow-up)
|
|
390
|
-
//
|
|
391
|
-
// createPrd() is the function ipcMain.handle('chat:create-prd', ...) in
|
|
392
|
-
// index.cjs calls directly (in-process, no HTTP) — same function
|
|
393
|
-
// registerAdminRoute's HTTP handler calls above. These tests exercise it
|
|
394
|
-
// directly, mirroring the handler's own call shape: createPrd(input, remote).
|
|
395
|
-
|
|
396
|
-
test('createPrd rejects a cwd outside allowedRoots before any write, returning {ok:false, status:400}', async () => {
|
|
397
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
398
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
399
|
-
const writePrdSpy = vi.fn(remote.writePrd);
|
|
400
|
-
remote.writePrd = writePrdSpy;
|
|
401
|
-
|
|
402
|
-
const result = await createPrd(validCreateBody({ cwd: '/etc/passwd-adjacent-outside-home' }), remote);
|
|
403
|
-
|
|
404
|
-
expect(result.ok).toBe(false);
|
|
405
|
-
expect(result.status).toBe(400);
|
|
406
|
-
expect(writePrdSpy).not.toHaveBeenCalled();
|
|
407
|
-
const entries = await fsp.readdir(prdsDir);
|
|
408
|
-
expect(entries.filter((f) => f.endsWith('.md')).length).toBe(0);
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
test('createPrd with a valid payload allocates a group, writes through remote.writePrd, and returns {ok:true, nn, filename}', async () => {
|
|
412
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
413
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
414
|
-
|
|
415
|
-
const result = await createPrd(validCreateBody({ sourcePromptId: 'ticket-abc', sourceTabId: 'tab-1' }), remote);
|
|
416
|
-
|
|
417
|
-
expect(result.ok).toBe(true);
|
|
418
|
-
expect(typeof result.nn).toBe('number');
|
|
419
|
-
expect(result.filename).toMatch(/^\d+-add-widget-frobnication\.md$/);
|
|
420
|
-
// PRD 1403: sizing warnings ride along in the success response, advisory only.
|
|
421
|
-
expect(Array.isArray(result.warnings)).toBe(true);
|
|
422
|
-
|
|
423
|
-
const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
|
|
424
|
-
expect(written).toMatch(/^---\n/);
|
|
425
|
-
expect(written).toMatch(/title: Add widget frobnication/);
|
|
426
|
-
expect(written).toMatch(/sourcePromptId: ticket-abc/);
|
|
427
|
-
expect(written).toMatch(/sourceTabId: tab-1/);
|
|
428
|
-
expect(written).toMatch(/# Goal/);
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
test('createPrd returns {ok:false, status:409} without clobbering when the destination already exists', async () => {
|
|
432
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
433
|
-
// The existence guard survives PRD 832: whatever NN the allocator picks,
|
|
434
|
-
// a pre-existing file at that exact destination must 409, never clobber.
|
|
435
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
436
|
-
const realRead = remote.readPrd.bind(remote);
|
|
437
|
-
remote.readPrd = async (slug) => (slug.endsWith('my-explicit-slug') ? { ok: true, text: 'ORIGINAL' } : realRead(slug));
|
|
438
|
-
const writes = [];
|
|
439
|
-
const realWrite = remote.writePrd.bind(remote);
|
|
440
|
-
remote.writePrd = async (...a) => { writes.push(a[0]); return realWrite(...a); };
|
|
441
|
-
|
|
442
|
-
const result = await createPrd(validCreateBody({ slug: 'my-explicit-slug' }), remote);
|
|
443
|
-
|
|
444
|
-
expect(result.ok).toBe(false);
|
|
445
|
-
expect(result.status).toBe(409);
|
|
446
|
-
expect(writes).toEqual([]);
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
// ──────────────────────────────────────────── PRD 825: Epic PRD dir write grant + `~` cwd normalization
|
|
450
|
-
|
|
451
|
-
test('validateWrite allows writes under <root>/session-manager-operations/scheduler/epics/<id>/prds/', async () => {
|
|
452
|
-
const root = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-scheduler-epics-'));
|
|
453
|
-
config.addAllowedRoot(root);
|
|
454
|
-
const prdsDir = path.join(root, 'session-manager-operations', 'scheduler', 'epics', 'epic-abc123', 'prds');
|
|
455
|
-
await fsp.mkdir(prdsDir, { recursive: true });
|
|
456
|
-
const filePath = path.join(prdsDir, '1-do-thing.md');
|
|
457
|
-
|
|
458
|
-
// Writes into scheduler/ must declare the owning surface (single-writer law).
|
|
459
|
-
await config.writeTextAtomic(filePath, 'PRD body\n', { writer: 'scheduler' });
|
|
460
|
-
|
|
461
|
-
expect(fs.existsSync(filePath)).toBeTruthy();
|
|
462
|
-
expect(await fsp.readFile(filePath, 'utf8')).toBe('PRD body\n');
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
test('createPrd normalizes a `~`-prefixed cwd via expandHome before calling remote', async () => {
|
|
466
|
-
const receivedCwds = [];
|
|
467
|
-
const remote = {
|
|
468
|
-
async allocateParallelGroup(cwd) { receivedCwds.push(cwd); return 42; },
|
|
469
|
-
async readPrd(slug, cwd) { receivedCwds.push(cwd); return { ok: false }; },
|
|
470
|
-
async writePrd(slug, body, cwd) { receivedCwds.push(cwd); return { ok: true, bytesWritten: body.length }; },
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
const result = await createPrd(validCreateBody({ cwd: '~' }), remote);
|
|
474
|
-
|
|
475
|
-
expect(result.ok).toBe(true);
|
|
476
|
-
expect(receivedCwds.length).toBeGreaterThan(0);
|
|
477
|
-
for (const cwd of receivedCwds) {
|
|
478
|
-
expect(cwd.startsWith('~')).toBe(false);
|
|
479
|
-
expect(cwd).toBe(os.homedir());
|
|
480
|
-
}
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
// ──────────────────────────────────────────── PRD 862: register cwd as an
|
|
484
|
-
// allowed write root at createPrd() time, not only at pty.spawn() time
|
|
485
|
-
//
|
|
486
|
-
// Before this fix, config.cjs's allowedRoots (the write-boundary allowlist)
|
|
487
|
-
// only ever grew via pty.cjs's addAllowedRoot call inside pty.spawn(). A
|
|
488
|
-
// chat-only Epic (headless `claude -p --resume`, no Terminal PTY ever
|
|
489
|
-
// spawned for its project cwd) reached createPrd() -> remote.writePrd() ->
|
|
490
|
-
// config.writeTextAtomic() -> config.validateWrite() and failed with "Write
|
|
491
|
-
// outside allowed write boundaries", purely because no PTY had registered
|
|
492
|
-
// that cwd yet in this process's lifetime. This test reproduces that exact
|
|
493
|
-
// scenario: a brand-new project root that has NEVER had config.addAllowedRoot
|
|
494
|
-
// called for it (mirroring pty.spawn() never having run), writing through the
|
|
495
|
-
// real config.writeTextAtomic into the real session-manager-operations/
|
|
496
|
-
// scheduler/ subtree (not the mocked-remote temp dirs the other tests here
|
|
497
|
-
// use, which pre-register their root via addAllowedRoot in mkTmpPrdsDir).
|
|
498
|
-
test('createPrd registers cwd as an allowed write root itself, so a chat-only Epic (no Terminal PTY ever spawned for this cwd) can still write its PRD', async () => {
|
|
499
|
-
// Must live under $HOME: validatePath's read boundary is home-dir-wide
|
|
500
|
-
// regardless of allowedRoots registration (matching every real project
|
|
501
|
-
// cwd, which checkInsideHome forces inside $HOME) — the bug this test
|
|
502
|
-
// reproduces is specifically about the *write* boundary (validateWrite),
|
|
503
|
-
// not the read one.
|
|
504
|
-
const root = await fsp.mkdtemp(path.join(os.homedir(), '.sm-chat-only-epic-'));
|
|
505
|
-
// Deliberately do NOT call config.addAllowedRoot(root) here — pty.spawn()
|
|
506
|
-
// is the only other caller of addAllowedRoot, and it never runs for a
|
|
507
|
-
// chat-only Epic. createPrd() itself must perform this registration.
|
|
508
|
-
const prdsEpicDir = path.join(root, 'session-manager-operations', 'scheduler', 'epics', 'chat-only-epic', 'prds');
|
|
509
|
-
|
|
510
|
-
try {
|
|
511
|
-
const remote = {
|
|
512
|
-
async allocateParallelGroup() { return 1; },
|
|
513
|
-
async readPrd() { return { ok: false }; },
|
|
514
|
-
async writePrd(slug, body) {
|
|
515
|
-
await fsp.mkdir(prdsEpicDir, { recursive: true });
|
|
516
|
-
const filePath = path.join(prdsEpicDir, `${slug}.md`);
|
|
517
|
-
// Real config.cjs write path — this is what threw
|
|
518
|
-
// "Write outside allowed write boundaries" before the fix.
|
|
519
|
-
await config.writeTextAtomic(filePath, body, { writer: 'scheduler' });
|
|
520
|
-
return { ok: true };
|
|
521
|
-
},
|
|
522
|
-
};
|
|
523
|
-
|
|
524
|
-
const result = await createPrd(validCreateBody({ cwd: root }), remote);
|
|
525
|
-
|
|
526
|
-
expect(result.ok).toBe(true);
|
|
527
|
-
const written = await fsp.readFile(path.join(prdsEpicDir, result.filename), 'utf8');
|
|
528
|
-
expect(written).toMatch(/title: Add widget frobnication/);
|
|
529
|
-
} finally {
|
|
530
|
-
await fsp.rm(root, { recursive: true, force: true });
|
|
531
|
-
}
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
test('POST /admin/scheduler/create-prd with sourcePromptId writes it into the created PRD frontmatter', async () => {
|
|
535
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
536
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
537
|
-
const { admin, port, token } = await startWithRemote(remote);
|
|
538
|
-
try {
|
|
539
|
-
const res = await request(port, {
|
|
540
|
-
method: 'POST', path: '/admin/scheduler/create-prd', token,
|
|
541
|
-
body: validCreateBody({ slug: 'from-a-ticket', sourcePromptId: 'ticket-xyz-789' }),
|
|
542
|
-
});
|
|
543
|
-
expect(res.status).toBe(200);
|
|
544
|
-
const written = fs.readFileSync(path.join(prdsDir, `${res.json.nn}-from-a-ticket.md`), 'utf8');
|
|
545
|
-
expect(written).toMatch(/sourcePromptId: ticket-xyz-789/);
|
|
546
|
-
} finally {
|
|
547
|
-
await admin.stop();
|
|
548
|
-
}
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
test('POST /admin/scheduler/create-prd with tag writes it into the created PRD frontmatter (PRD 1041)', async () => {
|
|
552
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
553
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
554
|
-
const { admin, port, token } = await startWithRemote(remote);
|
|
555
|
-
try {
|
|
556
|
-
const res = await request(port, {
|
|
557
|
-
method: 'POST', path: '/admin/scheduler/create-prd', token,
|
|
558
|
-
body: validCreateBody({ slug: 'with-a-tag', tag: 'bug' }),
|
|
559
|
-
});
|
|
560
|
-
expect(res.status).toBe(200);
|
|
561
|
-
const written = fs.readFileSync(path.join(prdsDir, `${res.json.nn}-with-a-tag.md`), 'utf8');
|
|
562
|
-
expect(written).toMatch(/^tag: bug$/m);
|
|
563
|
-
} finally {
|
|
564
|
-
await admin.stop();
|
|
565
|
-
}
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
// ──────────────────────────────────────────── PRD 1114: agentType (WHO
|
|
569
|
-
// executes a PRD) — throw on write for an unresolvable persona, default to
|
|
570
|
-
// dev-lead when omitted.
|
|
571
|
-
|
|
572
|
-
test('createPrd rejects an unknown agentType at write time, naming available personas, and writes no file', async () => {
|
|
573
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
574
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
575
|
-
const writePrdSpy = vi.fn(remote.writePrd);
|
|
576
|
-
remote.writePrd = writePrdSpy;
|
|
577
|
-
|
|
578
|
-
const result = await createPrd(
|
|
579
|
-
validCreateBody({ agentType: 'zzz-definitely-not-a-real-persona-12345' }),
|
|
580
|
-
remote,
|
|
581
|
-
);
|
|
582
|
-
|
|
583
|
-
expect(result.ok).toBe(false);
|
|
584
|
-
expect(result.status).toBe(400);
|
|
585
|
-
expect(result.error).toMatch(/does not resolve to a readable persona file/);
|
|
586
|
-
expect(result.error).toMatch(/Available personas/);
|
|
587
|
-
expect(writePrdSpy).not.toHaveBeenCalled();
|
|
588
|
-
const entries = await fsp.readdir(prdsDir);
|
|
589
|
-
expect(entries.filter((f) => f.endsWith('.md')).length).toBe(0);
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
test('createPrd accepts a caller-supplied agentType that resolves to a project-overlay persona file', async () => {
|
|
593
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
594
|
-
const cwd = path.dirname(path.dirname(prdsDir)); // mkTmpPrdsDir's root (prdsDir = root/.claude/prds)
|
|
595
|
-
const agentsDir = path.join(cwd, '.claude', 'agents');
|
|
596
|
-
await fsp.mkdir(agentsDir, { recursive: true });
|
|
597
|
-
await fsp.writeFile(path.join(agentsDir, 'my-persona.md'), '---\nname: my-persona\n---\nbody\n', 'utf8');
|
|
598
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
599
|
-
|
|
600
|
-
const result = await createPrd(validCreateBody({ cwd, slug: 'with-agent-type', agentType: 'my-persona' }), remote);
|
|
601
|
-
|
|
602
|
-
expect(result.ok).toBe(true);
|
|
603
|
-
const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
|
|
604
|
-
expect(written).toMatch(/^agentType: my-persona$/m);
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
test('createPrd defaults agentType to dev-lead when omitted, without any persona-existence check', async () => {
|
|
608
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
609
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
610
|
-
|
|
611
|
-
const result = await createPrd(validCreateBody({ slug: 'no-agent-type-supplied' }), remote);
|
|
612
|
-
|
|
613
|
-
expect(result.ok).toBe(true);
|
|
614
|
-
const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
|
|
615
|
-
expect(written).toMatch(/^agentType: dev-lead$/m);
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
// ──────────────────────────────────────────── originClaudeSessionId fallback
|
|
619
|
-
// (2026-08-01 fix: a PRD queued via the scheduler_create_prd MCP tool from
|
|
620
|
-
// inside an Epic's own chat session, without an explicit sourcePromptId,
|
|
621
|
-
// must still join that Epic rather than mint an unrelated sibling one.)
|
|
622
|
-
|
|
623
|
-
async function writeActiveIndexFixture(cwd, sessions) {
|
|
624
|
-
const dir = path.join(cwd, 'session-manager-operations', 'prompt-sessions');
|
|
625
|
-
await fsp.mkdir(dir, { recursive: true });
|
|
626
|
-
await fsp.writeFile(path.join(dir, 'active-index.json'), JSON.stringify({ sessions, events: {} }, null, 2));
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
test('resolveSourcePromptIdFromClaudeSession finds the Epic whose claudeSessionId matches', async () => {
|
|
630
|
-
const root = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-resolve-epic-'));
|
|
631
|
-
try {
|
|
632
|
-
await writeActiveIndexFixture(root, {
|
|
633
|
-
'psess-abc123': { id: 'psess-abc123', claudeSessionId: 'claude-sess-1', status: 'active' },
|
|
634
|
-
});
|
|
635
|
-
expect(resolveSourcePromptIdFromClaudeSession(root, 'claude-sess-1')).toBe('psess-abc123');
|
|
636
|
-
expect(resolveSourcePromptIdFromClaudeSession(root, 'claude-sess-unknown')).toBeNull();
|
|
637
|
-
expect(resolveSourcePromptIdFromClaudeSession(root, null)).toBeNull();
|
|
638
|
-
} finally {
|
|
639
|
-
await fsp.rm(root, { recursive: true, force: true });
|
|
640
|
-
}
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
// Real-homedir-rooted like the chat-only-epic test above (see its comment at
|
|
644
|
-
// line ~449): validatePath's read boundary is home-dir-wide, and this
|
|
645
|
-
// exercises createPrd's real config.addAllowedRoot/writeTextAtomic path. Safe
|
|
646
|
-
// because the random suffix is never a registered project the live scheduler
|
|
647
|
-
// scans for PRDs, and it's removed in `finally`.
|
|
648
|
-
test('createPrd resolves sourcePromptId from originClaudeSessionId when the caller omitted sourcePromptId', async () => {
|
|
649
|
-
const root = await fsp.mkdtemp(path.join(os.homedir(), '.sm-origin-session-epic-'));
|
|
650
|
-
config.addAllowedRoot(root);
|
|
651
|
-
try {
|
|
652
|
-
await writeActiveIndexFixture(root, {
|
|
653
|
-
'psess-always-select-tab': { id: 'psess-always-select-tab', claudeSessionId: 'claude-sess-epic-1', status: 'active' },
|
|
654
|
-
});
|
|
655
|
-
const prdsDir = path.join(root, 'session-manager-operations', 'scheduler', 'epics', 'psess-always-select-tab', 'prds');
|
|
656
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
657
|
-
|
|
658
|
-
const result = await createPrd(
|
|
659
|
-
validCreateBody({ cwd: root, slug: 'joins-the-epic', originClaudeSessionId: 'claude-sess-epic-1' }),
|
|
660
|
-
remote,
|
|
661
|
-
);
|
|
662
|
-
|
|
663
|
-
expect(result.ok).toBe(true);
|
|
664
|
-
const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
|
|
665
|
-
expect(written).toMatch(/sourcePromptId: psess-always-select-tab/);
|
|
666
|
-
} finally {
|
|
667
|
-
await fsp.rm(root, { recursive: true, force: true });
|
|
668
|
-
}
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
// Same real-homedir rationale as the two tests above.
|
|
672
|
-
// ──────────────────────────────────────────── worktree-cwd Epic-lookup
|
|
673
|
-
// hazard (PRD: projectRootResolve.cjs) — reproduces the live starry-night-
|
|
674
|
-
// ships shape: main's active-index.json has the Epic, a linked worktree's
|
|
675
|
-
// own (git-tracked, frozen-at-branch-time) copy does not. Passing the
|
|
676
|
-
// worktree's pwd as `cwd` must still resolve to main and join the Epic there.
|
|
677
|
-
|
|
678
|
-
function git(args, cwd) {
|
|
679
|
-
return execFileSync('git', args, { cwd, encoding: 'utf8' });
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
function initRepo(dir) {
|
|
683
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
684
|
-
git(['init', '-q'], dir);
|
|
685
|
-
git(['config', 'user.email', 'test@example.com'], dir);
|
|
686
|
-
git(['config', 'user.name', 'Test'], dir);
|
|
687
|
-
fs.writeFileSync(path.join(dir, 'README.md'), 'hello\n', 'utf8');
|
|
688
|
-
git(['add', '-A'], dir);
|
|
689
|
-
git(['commit', '-q', '-m', 'initial'], dir);
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
test('createPrd resolves a worktree cwd to the main tree and joins the Epic whose row only exists in main\'s active-index (live starry-night-ships shape)', async () => {
|
|
693
|
-
const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-worktree-cwd-hazard-'));
|
|
694
|
-
const mainRepo = path.join(tmpRoot, 'main');
|
|
695
|
-
const worktreeDir = path.join(tmpRoot, 'worktree', 'epic-1');
|
|
696
|
-
config.addAllowedRoot(mainRepo);
|
|
697
|
-
try {
|
|
698
|
-
initRepo(mainRepo);
|
|
699
|
-
fs.mkdirSync(path.dirname(worktreeDir), { recursive: true });
|
|
700
|
-
git(['worktree', 'add', '-b', 'sm-epic/epic-1', worktreeDir, 'HEAD'], mainRepo);
|
|
701
|
-
|
|
702
|
-
// Main's index has the Epic; the worktree's own copy (as checked out at
|
|
703
|
-
// branch time) does not — the exact live-repro shape.
|
|
704
|
-
await writeActiveIndexFixture(mainRepo, {
|
|
705
|
-
'epic-1': { id: 'epic-1', claudeSessionId: 'claude-sess-epic-1', status: 'active' },
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
const prdsDir = path.join(mainRepo, 'session-manager-operations', 'scheduler', 'epics', 'epic-1', 'prds');
|
|
709
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
710
|
-
const receivedCwds = [];
|
|
711
|
-
const realAllocate = remote.allocateParallelGroup.bind(remote);
|
|
712
|
-
remote.allocateParallelGroup = async (cwd) => { receivedCwds.push(cwd); return realAllocate(cwd); };
|
|
713
|
-
|
|
714
|
-
const result = await createPrd(
|
|
715
|
-
validCreateBody({ cwd: worktreeDir, slug: 'joins-from-worktree', sourcePromptId: 'epic-1' }),
|
|
716
|
-
remote,
|
|
717
|
-
);
|
|
718
|
-
|
|
719
|
-
expect(result.ok).toBe(true);
|
|
720
|
-
// Resolved and passed to the remote as the MAIN tree, never the worktree.
|
|
721
|
-
expect(receivedCwds.every((c) => c === mainRepo)).toBe(true);
|
|
722
|
-
const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
|
|
723
|
-
expect(written).toMatch(/sourcePromptId: epic-1/);
|
|
724
|
-
expect(written).toMatch(new RegExp(`cwd: ${mainRepo.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
|
|
725
|
-
} finally {
|
|
726
|
-
await fsp.rm(tmpRoot, { recursive: true, force: true });
|
|
727
|
-
}
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
test('createPrd omits cwd entirely and resolves the project purely from sourcePromptId (cwd optional inside an Epic session)', async () => {
|
|
731
|
-
const root = await fsp.mkdtemp(path.join(os.homedir(), '.sm-cwd-optional-epic-'));
|
|
732
|
-
config.addAllowedRoot(root);
|
|
733
|
-
// resolveProjectContext's cross-project scan (used when cwd is omitted
|
|
734
|
-
// entirely) is built on activeSessions.allProjectCwds — a scan of REAL
|
|
735
|
-
// ~/.claude/projects transcripts. In production this is always populated
|
|
736
|
-
// (the calling session itself opened `root` as a project), so seed a
|
|
737
|
-
// matching transcript here rather than the fixture root being invisible to
|
|
738
|
-
// the scan. Cleaned up in `finally`.
|
|
739
|
-
const transcriptDir = path.join(os.homedir(), '.claude', 'projects', `sm-cwd-optional-test-${Date.now()}`);
|
|
740
|
-
try {
|
|
741
|
-
await writeActiveIndexFixture(root, {
|
|
742
|
-
'epic-cwd-optional': { id: 'epic-cwd-optional', claudeSessionId: 'claude-sess-cwd-optional', status: 'active' },
|
|
743
|
-
});
|
|
744
|
-
await fsp.mkdir(transcriptDir, { recursive: true });
|
|
745
|
-
await fsp.writeFile(path.join(transcriptDir, 'a.jsonl'), `${JSON.stringify({ cwd: root })}\n`, 'utf8');
|
|
746
|
-
|
|
747
|
-
const prdsDir = path.join(root, 'session-manager-operations', 'scheduler', 'epics', 'epic-cwd-optional', 'prds');
|
|
748
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
749
|
-
|
|
750
|
-
const body = validCreateBody({ slug: 'no-cwd-supplied', sourcePromptId: 'epic-cwd-optional' });
|
|
751
|
-
delete body.cwd;
|
|
752
|
-
|
|
753
|
-
const result = await createPrd(body, remote);
|
|
754
|
-
|
|
755
|
-
expect(result.ok).toBe(true);
|
|
756
|
-
const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
|
|
757
|
-
expect(written).toMatch(/sourcePromptId: epic-cwd-optional/);
|
|
758
|
-
} finally {
|
|
759
|
-
await fsp.rm(transcriptDir, { recursive: true, force: true });
|
|
760
|
-
await fsp.rm(root, { recursive: true, force: true });
|
|
761
|
-
}
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
test('createPrd with neither cwd nor a resolvable Epic fails with a clear cwd-required error', async () => {
|
|
765
|
-
const remote = {
|
|
766
|
-
async allocateParallelGroup() { throw new Error('must not be called'); },
|
|
767
|
-
async readPrd() { throw new Error('must not be called'); },
|
|
768
|
-
async writePrd() { throw new Error('must not be called'); },
|
|
769
|
-
};
|
|
770
|
-
const body = validCreateBody();
|
|
771
|
-
delete body.cwd;
|
|
772
|
-
|
|
773
|
-
const result = await createPrd(body, remote);
|
|
774
|
-
|
|
775
|
-
expect(result.ok).toBe(false);
|
|
776
|
-
expect(result.status).toBe(400);
|
|
777
|
-
expect(result.error).toMatch(/cwd is required/);
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
// ──────────────────────────────────────────── PRD 1113: fix-chain depth guard
|
|
781
|
-
// at the authoring boundary. lib/prdCreate.cjs's createPrd() is the shared
|
|
782
|
-
// write path behind both scheduler_create_prd (MCP) and chat:create-prd
|
|
783
|
-
// (IPC) — enforcing here covers both callers with one check.
|
|
784
|
-
|
|
785
|
-
test('createPrd refuses a depth-2 fix-chain slug (fix-of-a-fix) with a distinct, human-escalation error and writes no file', async () => {
|
|
786
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
787
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
788
|
-
const writePrdSpy = vi.fn(remote.writePrd);
|
|
789
|
-
remote.writePrd = writePrdSpy;
|
|
790
|
-
|
|
791
|
-
const result = await createPrd(validCreateBody({ slug: 'fix-fix-widget-frobnication' }), remote);
|
|
792
|
-
|
|
793
|
-
expect(result.ok).toBe(false);
|
|
794
|
-
expect(result.status).toBe(409);
|
|
795
|
-
// Distinct from the provenance/validation error text elsewhere in this
|
|
796
|
-
// file ('cwd rejected', 'invalid PRD payload', 'already exists') and
|
|
797
|
-
// explicitly states escalation, not another attempt, is correct.
|
|
798
|
-
expect(result.error).toMatch(/fix-chain depth cap/i);
|
|
799
|
-
expect(result.error).toMatch(/not the correct response/i);
|
|
800
|
-
expect(result.error).toMatch(/human/i);
|
|
801
|
-
expect(writePrdSpy).not.toHaveBeenCalled();
|
|
802
|
-
const entries = await fsp.readdir(prdsDir);
|
|
803
|
-
expect(entries.filter((f) => f.endsWith('.md')).length).toBe(0);
|
|
804
|
-
});
|
|
805
|
-
|
|
806
|
-
test('createPrd refuses a depth-3 fix-chain slug the same way as depth-2', async () => {
|
|
807
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
808
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
809
|
-
|
|
810
|
-
const result = await createPrd(validCreateBody({ slug: 'fix-fix-fix-widget-frobnication' }), remote);
|
|
811
|
-
|
|
812
|
-
expect(result.ok).toBe(false);
|
|
813
|
-
expect(result.status).toBe(409);
|
|
814
|
-
const entries = await fsp.readdir(prdsDir);
|
|
815
|
-
expect(entries.filter((f) => f.endsWith('.md')).length).toBe(0);
|
|
816
|
-
});
|
|
817
|
-
|
|
818
|
-
test('createPrd refuses a depth-1 fix slug too (PRD 1131) — that shape is reserved for spawnInvestigation-authored fix plans, not this API', async () => {
|
|
819
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
820
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
821
|
-
const writePrdSpy = vi.fn(remote.writePrd);
|
|
822
|
-
remote.writePrd = writePrdSpy;
|
|
823
|
-
|
|
824
|
-
const result = await createPrd(validCreateBody({ slug: 'fix-widget-frobnication' }), remote);
|
|
825
|
-
|
|
826
|
-
expect(result.ok).toBe(false);
|
|
827
|
-
expect(result.status).toBe(400);
|
|
828
|
-
expect(result.error).toMatch(/reserved NN-fix-\.\.\. fix-plan naming convention/i);
|
|
829
|
-
expect(writePrdSpy).not.toHaveBeenCalled();
|
|
830
|
-
const entries = await fsp.readdir(prdsDir);
|
|
831
|
-
expect(entries.filter((f) => f.endsWith('.md')).length).toBe(0);
|
|
832
|
-
});
|
|
833
|
-
|
|
834
|
-
test('createPrd is unaffected by the fix-chain guard for an ordinary slug with no fix chain at all', async () => {
|
|
835
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
836
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
837
|
-
|
|
838
|
-
const result = await createPrd(validCreateBody(), remote);
|
|
839
|
-
|
|
840
|
-
expect(result.ok).toBe(true);
|
|
841
|
-
const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
|
|
842
|
-
expect(written).toMatch(/title: Add widget frobnication/);
|
|
843
|
-
});
|
|
844
|
-
|
|
845
|
-
test('createPrd refuses a slug starting with "fix-" even when a later segment merely reads "fix" (isFixPlanSlug matches on the leading segment alone, unlike fixChainDepthOf)', async () => {
|
|
846
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
847
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
848
|
-
|
|
849
|
-
const result = await createPrd(validCreateBody({ slug: 'fix-loop-extraction-and-round-trip' }), remote);
|
|
850
|
-
|
|
851
|
-
expect(result.ok).toBe(false);
|
|
852
|
-
expect(result.status).toBe(400);
|
|
853
|
-
expect(result.error).toMatch(/reserved NN-fix-\.\.\. fix-plan naming convention/i);
|
|
854
|
-
});
|
|
855
|
-
|
|
856
|
-
test('createPrd leaves sourcePromptId unset when originClaudeSessionId matches no known Epic (plain SessionTab chat)', async () => {
|
|
857
|
-
const root = await fsp.mkdtemp(path.join(os.homedir(), '.sm-origin-session-no-match-'));
|
|
858
|
-
config.addAllowedRoot(root);
|
|
859
|
-
try {
|
|
860
|
-
await writeActiveIndexFixture(root, {});
|
|
861
|
-
const prdsDir = path.join(root, 'session-manager-operations', 'scheduler', 'prds');
|
|
862
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
863
|
-
|
|
864
|
-
const result = await createPrd(
|
|
865
|
-
validCreateBody({ cwd: root, slug: 'no-epic-match', originClaudeSessionId: 'claude-sess-not-an-epic' }),
|
|
866
|
-
remote,
|
|
867
|
-
);
|
|
868
|
-
|
|
869
|
-
expect(result.ok).toBe(true);
|
|
870
|
-
const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
|
|
871
|
-
expect(!/sourcePromptId:/.test(written)).toBeTruthy();
|
|
872
|
-
} finally {
|
|
873
|
-
await fsp.rm(root, { recursive: true, force: true });
|
|
874
|
-
}
|
|
875
|
-
});
|
|
876
|
-
|
|
877
|
-
// ──────────────────────────────────────────── PRD 1121: reject double-prefixed
|
|
878
|
-
// slugs, validate dependsOn against existing PRDs at write time
|
|
879
|
-
|
|
880
|
-
test('createPrd refuses a caller-supplied slug already starting with a numeric prefix', async () => {
|
|
881
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
882
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
883
|
-
|
|
884
|
-
const result = await createPrd(validCreateBody({ slug: '254-perf-x' }), remote);
|
|
885
|
-
|
|
886
|
-
expect(result.ok).toBe(false);
|
|
887
|
-
expect(result.status).toBe(400);
|
|
888
|
-
expect(result.error).toMatch(/254-/);
|
|
889
|
-
expect(result.error).toMatch(/bare name/);
|
|
890
|
-
const entries = await fsp.readdir(prdsDir);
|
|
891
|
-
expect(entries.filter((f) => f.endsWith('.md')).length).toBe(0);
|
|
892
|
-
});
|
|
893
|
-
|
|
894
|
-
test('createPrd still accepts a bare (non-prefixed) caller-supplied slug', async () => {
|
|
895
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
896
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
897
|
-
|
|
898
|
-
const result = await createPrd(validCreateBody({ slug: 'perf-x' }), remote);
|
|
899
|
-
|
|
900
|
-
expect(result.ok).toBe(true);
|
|
901
|
-
expect(result.filename).toMatch(/^\d+-perf-x\.md$/);
|
|
902
|
-
});
|
|
903
|
-
|
|
904
|
-
test('createPrd accepts dependsOn naming an existing bare slug', async () => {
|
|
905
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
906
|
-
await seedExistingPrd(prdsDir, '3-widget-base');
|
|
907
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
908
|
-
|
|
909
|
-
const result = await createPrd(validCreateBody({ slug: 'widget-follow-up', dependsOn: ['widget-base'] }), remote);
|
|
910
|
-
|
|
911
|
-
expect(result.ok).toBe(true);
|
|
912
|
-
const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
|
|
913
|
-
expect(written).toMatch(/^dependsOn: \[widget-base\]$/m);
|
|
914
|
-
});
|
|
915
|
-
|
|
916
|
-
test('createPrd accepts dependsOn naming an existing full NN-prefixed slug', async () => {
|
|
917
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
918
|
-
await seedExistingPrd(prdsDir, '3-widget-base');
|
|
919
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
920
|
-
|
|
921
|
-
const result = await createPrd(validCreateBody({ slug: 'widget-follow-up-2', dependsOn: ['3-widget-base'] }), remote);
|
|
922
|
-
|
|
923
|
-
expect(result.ok).toBe(true);
|
|
924
|
-
});
|
|
925
|
-
|
|
926
|
-
test('createPrd refuses dependsOn naming nothing, listing near-matches in the message, and writes no file', async () => {
|
|
927
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
928
|
-
await seedExistingPrd(prdsDir, '3-widget-base');
|
|
929
|
-
await seedExistingPrd(prdsDir, '4-widget-basics');
|
|
930
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
931
|
-
|
|
932
|
-
const result = await createPrd(validCreateBody({ slug: 'widget-follow-up-3', dependsOn: ['wigdet-bsae'] }), remote);
|
|
933
|
-
|
|
934
|
-
expect(result.ok).toBe(false);
|
|
935
|
-
expect(result.status).toBe(400);
|
|
936
|
-
expect(result.error).toMatch(/wigdet-bsae/);
|
|
937
|
-
expect(result.error).toMatch(/widget-base/);
|
|
938
|
-
const entries = await fsp.readdir(prdsDir);
|
|
939
|
-
expect(entries.filter((f) => f.endsWith('.md') && f.includes('widget-follow-up-3')).length).toBe(0);
|
|
940
|
-
});
|
|
941
|
-
|
|
942
|
-
test('createPrd treats an empty dependsOn array as a no-op (no listPrds call, no rejection)', async () => {
|
|
943
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
944
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
945
|
-
const listPrdsSpy = vi.fn(remote.listPrds);
|
|
946
|
-
remote.listPrds = listPrdsSpy;
|
|
947
|
-
|
|
948
|
-
const result = await createPrd(validCreateBody({ slug: 'no-deps-here', dependsOn: [] }), remote);
|
|
949
|
-
|
|
950
|
-
expect(result.ok).toBe(true);
|
|
951
|
-
expect(listPrdsSpy).not.toHaveBeenCalled();
|
|
952
|
-
});
|
|
953
|
-
|
|
954
|
-
test('createPrd treats an absent dependsOn as a no-op (no listPrds call, no rejection)', async () => {
|
|
955
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
956
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
957
|
-
const listPrdsSpy = vi.fn(remote.listPrds);
|
|
958
|
-
remote.listPrds = listPrdsSpy;
|
|
959
|
-
|
|
960
|
-
const result = await createPrd(validCreateBody({ slug: 'no-deps-field' }), remote);
|
|
961
|
-
|
|
962
|
-
expect(result.ok).toBe(true);
|
|
963
|
-
expect(listPrdsSpy).not.toHaveBeenCalled();
|
|
964
|
-
});
|
|
965
|
-
|
|
966
|
-
test('createPrd warns and skips dependsOn validation (never fails the write) when listPrds throws', async () => {
|
|
967
|
-
const prdsDir = await mkTmpPrdsDir();
|
|
968
|
-
const remote = makeFakeRemoteWithPrdsDir(prdsDir);
|
|
969
|
-
remote.listPrds = async () => { throw new Error('simulated I/O failure'); };
|
|
970
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
971
|
-
|
|
972
|
-
try {
|
|
973
|
-
const result = await createPrd(validCreateBody({ slug: 'deps-during-outage', dependsOn: ['anything-at-all'] }), remote);
|
|
974
|
-
expect(result.ok).toBe(true);
|
|
975
|
-
expect(warnSpy).toHaveBeenCalled();
|
|
976
|
-
} finally {
|
|
977
|
-
warnSpy.mockRestore();
|
|
978
|
-
}
|
|
979
|
-
});
|