claude-code-session-manager 0.87.0 → 0.88.1
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 +26 -53
- package/dist/assets/{AgentLibrary-DyLWzZDf.js → AgentLibrary-kQF8_wAw.js} +1 -1
- package/dist/assets/{DataModel--mISIJ6h.js → DataModel-CY_mMnsr.js} +1 -1
- package/dist/assets/{History-C2ahUXTg.js → History-Sji2brb0.js} +1 -1
- package/dist/assets/{Hooks-BiC6oyR2.js → Hooks-CtqpO-p5.js} +1 -1
- package/dist/assets/{HostBilko-BPleEOld.js → HostBilko-DN0ydOCr.js} +1 -1
- package/dist/assets/{Library-Dc8Qst1R.js → Library-CoiLZTKh.js} +1 -1
- package/dist/assets/{ListDetail-DIXh-OLX.js → ListDetail-CmqiQ_io.js} +1 -1
- package/dist/assets/{MarkdownEditor-C90bkLXK.js → MarkdownEditor-HPoNeKhT.js} +1 -1
- package/dist/assets/{McpServers-DqcbLOLZ.js → McpServers-Bj1X3_NT.js} +1 -1
- package/dist/assets/{Memory-CW62MXlh.js → Memory-1PKjsM3b.js} +1 -1
- package/dist/assets/{Panel-Bw1FhRuF.js → Panel-I1s6ghPo.js} +1 -1
- package/dist/assets/{Permissions-BcUC-5y8.js → Permissions-BhiiZjHQ.js} +1 -1
- package/dist/assets/{Plugins-BnKx9flD.js → Plugins-gv3_fkPX.js} +2 -2
- package/dist/assets/{ProvenanceBadge-Bw5vNVPT.js → ProvenanceBadge-BGcgBd8V.js} +1 -1
- package/dist/assets/{SaveBar-CWr0O_w-.js → SaveBar-BGr9eZ4g.js} +1 -1
- package/dist/assets/{Scheduler-DYdLuUqq.js → Scheduler-Bdo_awPq.js} +7 -7
- package/dist/assets/{ScopeSwitcher-CrBLbg8s.js → ScopeSwitcher-ukl0qWaA.js} +1 -1
- package/dist/assets/{Settings-DluB-vN1.js → Settings-DJkFj22G.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-CHLSseay.js → SkillReferenceGraph-0t1Qblp6.js} +1 -1
- package/dist/assets/{Skills-gNdo_HNK.js → Skills-CeVdBGE-.js} +1 -1
- package/dist/assets/{SystemPrompt-Cru05-Ia.js → SystemPrompt-CfKvNHi3.js} +1 -1
- package/dist/assets/{TagLibrary-DNHY0xou.js → TagLibrary-DAzNaUPd.js} +1 -1
- package/dist/assets/{TiptapBody-I4lmbCgP.js → TiptapBody--S_rGXoA.js} +1 -1
- package/dist/assets/{Toggle-bWMHjmRh.js → Toggle-Dszm0xnp.js} +1 -1
- package/dist/assets/{index-DV3PorRY.css → index-BHTX4OTc.css} +1 -1
- package/dist/assets/{index-fc_JjdxL.js → index-Dp4Rc--q.js} +356 -356
- package/dist/assets/{settingsSchema-BfhtZnGD.js → settingsSchema-BLbssMYo.js} +1 -1
- package/dist/assets/{whisperWorker-Dbia1OpC.js → whisperWorker-C7ZGQwKg.js} +7 -7
- package/dist/index.html +2 -2
- package/dist/vad/ort-wasm-simd-threaded.asyncify.mjs +106 -110
- package/dist/vad/ort-wasm-simd-threaded.asyncify.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.jsep.mjs +98 -98
- package/dist/vad/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.jspi.mjs +99 -102
- package/dist/vad/ort-wasm-simd-threaded.jspi.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.mjs +46 -46
- package/dist/vad/ort-wasm-simd-threaded.wasm +0 -0
- package/package.json +10 -13
- package/scripts/README.md +59 -0
- package/scripts/audit-ops-hygiene.cjs +350 -0
- package/scripts/hooks/guard-destructive-git.cjs +10 -34
- package/scripts/hooks/guard-inline-implementation.cjs +14 -8
- package/scripts/hooks/guard-prd-writes.cjs +7 -36
- package/scripts/hooks/guard-self-schedule.cjs +175 -0
- package/scripts/ops-sweep.cjs +355 -0
- package/scripts/scheduler-mcp-server.cjs +28 -71
- package/src/main/__tests__/bilkoHost-integration.test.cjs +3 -3
- package/src/main/__tests__/chat-cancel-terminal.test.cjs +6 -9
- package/src/main/__tests__/chat-exit-close-race.test.cjs +3 -3
- package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +4 -5
- package/src/main/__tests__/chat-queue.test.cjs +2 -2
- package/src/main/__tests__/chat-stop-signal.test.cjs +2 -2
- package/src/main/__tests__/dep-orphan-archive-health.test.cjs +77 -0
- package/src/main/__tests__/dod-batchkey.test.cjs +2 -2
- package/src/main/__tests__/dod-drain-hook.test.cjs +2 -2
- package/src/main/__tests__/dod-report.test.cjs +2 -2
- package/src/main/__tests__/dod-reverify.test.cjs +2 -2
- package/src/main/__tests__/epicMint.test.cjs +2 -2
- package/src/main/__tests__/exchanges.test.cjs +2 -2
- package/src/main/__tests__/extractJson.test.cjs +2 -2
- package/src/main/__tests__/files-reject-credentials.test.cjs +1 -1
- package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +556 -0
- package/src/main/__tests__/health-build-freshness.test.cjs +39 -0
- package/src/main/__tests__/health-delegation-chain.test.cjs +15 -1
- package/src/main/__tests__/health-queue-dispatch.test.cjs +58 -7
- package/src/main/__tests__/health-tick-liveness.test.cjs +27 -3
- package/src/main/__tests__/health-usage-poller.test.cjs +70 -23
- package/src/main/__tests__/historyRollup.test.cjs +2 -2
- package/src/main/__tests__/kg-augment.test.cjs +2 -2
- package/src/main/__tests__/mcpStatus.test.cjs +2 -2
- package/src/main/__tests__/memoryAggregate.test.cjs +1 -1
- package/src/main/__tests__/memoryStale.test.cjs +1 -1
- package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +25 -1
- package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +33 -3
- package/src/main/__tests__/prd-group-allocator.test.cjs +2 -2
- package/src/main/__tests__/prdAdminRouteParity.test.cjs +2 -0
- package/src/main/__tests__/prdAdminRoutes.test.cjs +14 -2
- package/src/main/__tests__/prdAuthoringSeed.test.cjs +39 -0
- package/src/main/__tests__/prdLocationsArchived.test.cjs +20 -13
- package/src/main/__tests__/proc-role-env.test.cjs +125 -0
- package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +304 -0
- package/src/main/__tests__/procname-sm-processes.test.cjs +127 -0
- package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +81 -401
- package/src/main/__tests__/projectPages.test.cjs +63 -149
- package/src/main/__tests__/queue-health-verdict.test.cjs +9 -9
- package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +117 -20
- package/src/main/__tests__/queueHistory.test.cjs +2 -2
- package/src/main/__tests__/rateLimitPollerStreak.test.cjs +38 -3
- package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +181 -0
- package/src/main/__tests__/runVerify.test.cjs +5 -5
- package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +3 -3
- package/src/main/__tests__/scheduleJobTransitions.test.cjs +2 -2
- package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +143 -0
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +2 -2
- package/src/main/__tests__/scheduler-autopromote.test.cjs +2 -2
- package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +3 -5
- package/src/main/__tests__/scheduler-boot-orphans.test.cjs +78 -97
- package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +161 -0
- package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +58 -0
- package/src/main/__tests__/scheduler-epic-digest.test.cjs +3 -5
- package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +2 -2
- package/src/main/__tests__/scheduler-gate-shadow.test.cjs +119 -0
- package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +46 -0
- package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +80 -0
- package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +25 -18
- package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +4 -4
- package/src/main/__tests__/scheduler-launch-failure.test.cjs +3 -5
- package/src/main/__tests__/scheduler-looks-done.test.cjs +26 -7
- package/src/main/__tests__/scheduler-manual-pause.test.cjs +118 -0
- package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +26 -3
- package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +17 -2
- package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +3 -5
- package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +41 -6
- package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +62 -6
- package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +3 -5
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +23 -4
- package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +100 -0
- package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +3 -3
- package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +4 -4
- package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +110 -0
- package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +81 -1
- package/src/main/__tests__/scheduler-stall-per-project.test.cjs +14 -0
- package/src/main/__tests__/scheduler-starve-escalation.test.cjs +4 -4
- package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +245 -0
- package/src/main/__tests__/scheduler-supervisor-record.test.cjs +81 -0
- package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +2 -2
- package/src/main/__tests__/scheduler-tick-wedge.test.cjs +172 -0
- package/src/main/__tests__/scheduler-transient-failure.test.cjs +2 -2
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +37 -11
- package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +3 -5
- package/src/main/__tests__/usageSingleFlight.test.cjs +157 -0
- package/src/main/__tests__/workTypeLibrary.test.cjs +1 -1
- package/src/main/bilkoHost.cjs +16 -7
- package/src/main/build-info.json +8 -0
- package/src/main/chatRunner.cjs +4 -2
- package/src/main/config.cjs +2 -3
- package/src/main/docEdit.cjs +4 -2
- package/src/main/health.cjs +276 -52
- package/src/main/heapSnapshot.cjs +2 -2
- package/src/main/historyAggregator.cjs +1 -1
- package/src/main/index.cjs +61 -10
- package/src/main/ipcSchemas.cjs +6 -17
- package/src/main/lib/__tests__/auditLog.test.cjs +38 -0
- package/src/main/lib/__tests__/buildIdentity.test.cjs +121 -0
- package/src/main/lib/__tests__/cwdClassify.test.cjs +111 -0
- package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +95 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +1 -1
- package/src/main/lib/__tests__/dispatchLoop.test.cjs +63 -0
- package/src/main/lib/__tests__/gateFixtures.json +20 -0
- package/src/main/lib/__tests__/gitWorktree.test.cjs +8 -1
- package/src/main/lib/__tests__/instanceLock.test.cjs +93 -7
- package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +78 -0
- package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +11 -0
- package/src/main/lib/__tests__/localAdminHttp.test.cjs +1 -1
- package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +6 -1
- package/src/main/lib/__tests__/procIdentity.test.cjs +119 -0
- package/src/main/lib/__tests__/procName.test.cjs +92 -0
- package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +67 -0
- package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +6 -7
- package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +37 -204
- package/src/main/lib/__tests__/schedulerPaths.test.cjs +226 -0
- package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +94 -0
- package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +56 -0
- package/src/main/lib/__tests__/sessionSlots.test.cjs +45 -0
- package/src/main/lib/__tests__/telemetryBacklog.test.cjs +1 -1
- package/src/main/lib/__tests__/upgradeDrain.test.cjs +130 -0
- package/src/main/lib/__tests__/usageCircuit.test.cjs +61 -0
- package/src/main/lib/__tests__/watchdog-helpers.test.cjs +63 -0
- package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +73 -0
- package/src/main/lib/activeIndexRebuild.cjs +1 -3
- package/src/main/lib/activeSessions.cjs +20 -88
- package/src/main/lib/adoptedRunSupervisor.cjs +136 -0
- package/src/main/lib/agentModelResolve.cjs +33 -1
- package/src/main/lib/agentPersonaSchema.cjs +2 -2
- package/src/main/lib/auditLog.cjs +30 -5
- package/src/main/lib/buildIdentity.cjs +113 -0
- package/src/main/lib/classifyPromptTicket.cjs +3 -2
- package/src/main/lib/claudeBin.cjs +37 -2
- package/src/main/lib/cleanEnv.cjs +27 -1
- package/src/main/lib/credentials.cjs +4 -2
- package/src/main/lib/cwdClassify.cjs +185 -0
- package/src/main/lib/definitionOfDone.cjs +296 -52
- package/src/main/lib/dispatchLoop.cjs +40 -0
- package/src/main/lib/effectiveModelInfo.cjs +9 -10
- package/src/main/lib/ephemeralCwd.cjs +7 -28
- package/src/main/lib/gitWorktree.cjs +95 -8
- package/src/main/lib/guardShims.cjs +3 -3
- package/src/main/lib/historyRollup.cjs +6 -7
- package/src/main/lib/instanceLock.cjs +32 -5
- package/src/main/lib/jobSupervisorRecord.cjs +147 -0
- package/src/main/lib/jobWorktreeBootLive.cjs +9 -5
- package/src/main/lib/localAdminHttp.cjs +9 -27
- package/src/main/lib/mcpToolCatalog.cjs +29 -77
- package/src/main/lib/opsOwnership.cjs +27 -19
- package/src/main/lib/prdAuthoringSeed.cjs +36 -0
- package/src/main/lib/prdLocations.cjs +48 -1
- package/src/main/lib/procIdentity.cjs +126 -0
- package/src/main/lib/procName.cjs +98 -0
- package/src/main/lib/projectHomeAdminRoutes.cjs +56 -327
- package/src/main/lib/queueHistory.cjs +8 -7
- package/src/main/lib/queueStore.cjs +57 -37
- package/src/main/lib/quietMachineLease.cjs +19 -2
- package/src/main/lib/reservationExpiry.cjs +30 -0
- package/src/main/lib/runClaudeP.cjs +4 -2
- package/src/main/lib/runLogRetention.cjs +3 -2
- package/src/main/lib/scheduleJobSchema.cjs +2 -2
- package/src/main/lib/scheduleJobTransitions.cjs +29 -2
- package/src/main/lib/schedulerBatch.cjs +27 -2
- package/src/main/lib/schedulerPaths.cjs +175 -0
- package/src/main/lib/schedulerRuntimeState.cjs +59 -0
- package/src/main/lib/sessionSlots.cjs +47 -9
- package/src/main/lib/smProcNames.cjs +51 -0
- package/src/main/lib/upgradeDrain.cjs +188 -0
- package/src/main/lib/usageCircuit.cjs +53 -6
- package/src/main/lib/watchdogHelpers.cjs +70 -37
- package/src/main/lib/withTimeout.cjs +33 -0
- package/src/main/mcpStatus.cjs +4 -2
- package/src/main/pluginInstall.cjs +6 -2
- package/src/main/projectPages.cjs +41 -145
- package/src/main/pty.cjs +8 -1
- package/src/main/queueOps.cjs +10 -10
- package/src/main/runVerify.cjs +69 -3
- package/src/main/scheduler.cjs +1607 -383
- package/src/main/seedAgentPersonas.cjs +1 -1
- package/src/main/seedDevPlugin.cjs +1 -1
- package/src/main/seedSchedulerMcp.cjs +7 -6
- package/src/main/seedStatus.cjs +1 -1
- package/src/main/supervisor.cjs +5 -3
- package/src/main/usage.cjs +126 -62
- package/src/preload/api.d.ts +18 -64
- package/src/preload/index.cjs +2 -4
- package/src/seed/agents/project-home-builder.md +31 -48
- package/screenshots/.gitkeep +0 -0
- package/screenshots/README-screenshots.md +0 -13
- package/src/main/lib/projectPageSummarySchema.cjs +0 -181
- package/src/main/teams.cjs +0 -95
- package/src/main/templates/project-pages-catalog.json +0 -741
- package/src/main/templates/project-pages-default-home.html +0 -123
- package/src/main/templates/project-pages-pipeline.md +0 -417
- package/src/seed/prompts/code-review/ac-coverage-check.md +0 -8
- package/src/seed/prompts/code-review/correctness-only.md +0 -8
- package/src/seed/prompts/code-review/full-spectrum-high.md +0 -8
- package/src/seed/prompts/code-review/hallucination-check.md +0 -8
- package/src/seed/prompts/code-review/public-api-compat.md +0 -8
- package/src/seed/prompts/code-review/readability-naming.md +0 -8
- package/src/seed/prompts/debugging/bug-as-failing-test.md +0 -8
- package/src/seed/prompts/debugging/git-bisect-regression.md +0 -8
- package/src/seed/prompts/debugging/instrument-intermittent-bug.md +0 -8
- package/src/seed/prompts/debugging/localize-pipeline-failure.md +0 -8
- package/src/seed/prompts/debugging/reproduce-then-diagnose.md +0 -8
- package/src/seed/prompts/documentation/adr-from-change.md +0 -8
- package/src/seed/prompts/documentation/module-readme.md +0 -8
- package/src/seed/prompts/documentation/onboarding-plan.md +0 -8
- package/src/seed/prompts/documentation/refresh-claude-md.md +0 -8
- package/src/seed/prompts/documentation/tsdoc-public-exports.md +0 -8
- package/src/seed/prompts/git-pr/conventional-commit.md +0 -8
- package/src/seed/prompts/git-pr/draft-pr-title-body.md +0 -8
- package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +0 -8
- package/src/seed/prompts/git-pr/release-notes-block.md +0 -8
- package/src/seed/prompts/git-pr/split-large-pr.md +0 -8
- package/src/seed/prompts/performance/bundle-startup-audit.md +0 -8
- package/src/seed/prompts/performance/complexity-audit.md +0 -8
- package/src/seed/prompts/performance/cpu-profile-hot-path.md +0 -8
- package/src/seed/prompts/performance/db-query-plan-review.md +0 -8
- package/src/seed/prompts/performance/memory-leak-hunt.md +0 -8
- package/src/seed/prompts/qa/api-contract-tests.md +0 -8
- package/src/seed/prompts/qa/e2e-critical-path.md +0 -8
- package/src/seed/prompts/qa/failing-test-for-bug.md +0 -8
- package/src/seed/prompts/qa/find-missing-test-coverage.md +0 -8
- package/src/seed/prompts/qa/stabilize-flaky-test.md +0 -8
- package/src/seed/prompts/qa/tdd-red-first.md +0 -8
- package/src/seed/prompts/qa/visual-regression-review.md +0 -8
- package/src/seed/prompts/qa/wcag-axe-scan.md +0 -8
- package/src/seed/prompts/refactoring/dead-code-sweep.md +0 -8
- package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +0 -8
- package/src/seed/prompts/refactoring/modernize-legacy-file.md +0 -8
- package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +0 -8
- package/src/seed/prompts/refactoring/tighten-module-boundaries.md +0 -8
- package/src/seed/prompts/security/authz-audit.md +0 -8
- package/src/seed/prompts/security/crypto-correctness.md +0 -8
- package/src/seed/prompts/security/cwe-top-25-hunt.md +0 -8
- package/src/seed/prompts/security/dependency-audit.md +0 -8
- package/src/seed/prompts/security/ipc-boundary-hardening.md +0 -8
- package/src/seed/prompts/security/owasp-top-10-staged-diff.md +0 -8
- package/src/seed/prompts/security/secret-credential-scan.md +0 -8
- package/web/README.md +0 -41
- package/web/project-pages/logic/dist/logic.cjs +0 -4709
- package/web/project-pages/render.cjs +0 -70
- package/web/project-pages/renderer/dist/renderer.cjs +0 -18900
- package/web/project-pages/validate-summary.cjs +0 -62
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* projectHomeAdminRoutes.test.cjs —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* project-home-admin-routes).
|
|
2
|
+
* projectHomeAdminRoutes.test.cjs — lib/projectHomeAdminRoutes.cjs's single
|
|
3
|
+
* route, POST /admin/project-home/write {cwd, html}, and its self-contained-
|
|
4
|
+
* HTML validator.
|
|
6
5
|
*
|
|
7
6
|
* Run: timeout 120 npx vitest run src/main/__tests__/projectHomeAdminRoutes.test.cjs
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
'use strict';
|
|
11
10
|
|
|
12
|
-
import { test, expect, afterEach
|
|
11
|
+
import { test, expect, afterEach } from 'vitest';
|
|
13
12
|
const fs = require('node:fs');
|
|
14
13
|
const fsp = require('node:fs/promises');
|
|
15
14
|
const os = require('node:os');
|
|
16
15
|
const path = require('node:path');
|
|
17
16
|
const config = require('../config.cjs');
|
|
18
|
-
const { registerAdminRoute } = require('../lib/projectHomeAdminRoutes.cjs');
|
|
17
|
+
const { registerAdminRoute, validateHomeHtml, MAX_HTML_BYTES } = require('../lib/projectHomeAdminRoutes.cjs');
|
|
19
18
|
|
|
20
19
|
const tmpDirs = [];
|
|
21
20
|
afterEach(async () => {
|
|
@@ -33,19 +32,18 @@ async function mkProjectCwd() {
|
|
|
33
32
|
return dir;
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
/** Fake adminHttp stub
|
|
37
|
-
* captures registered routes and lets a test invoke one directly with a
|
|
38
|
-
* fake req/res/query, no real HTTP server or Electron boot needed. */
|
|
35
|
+
/** Fake adminHttp stub: captures registered routes and invokes one directly. */
|
|
39
36
|
function makeFakeAdminHttp() {
|
|
40
37
|
const routes = new Map();
|
|
41
38
|
return {
|
|
42
39
|
registerRoute(method, url, handler) {
|
|
43
40
|
routes.set(`${method} ${url}`, handler);
|
|
44
41
|
},
|
|
45
|
-
async call(method, url, {
|
|
42
|
+
async call(method, url, { rawBody, body } = {}) {
|
|
46
43
|
const handler = routes.get(`${method} ${url}`);
|
|
47
44
|
if (!handler) throw new Error(`no route registered for ${method} ${url}`);
|
|
48
|
-
const
|
|
45
|
+
const text = rawBody !== undefined ? rawBody : body !== undefined ? JSON.stringify(body) : '';
|
|
46
|
+
const chunks = text ? [Buffer.from(text)] : [];
|
|
49
47
|
const req = {
|
|
50
48
|
on(event, cb) {
|
|
51
49
|
if (event === 'data') chunks.forEach((c) => cb(c));
|
|
@@ -59,427 +57,109 @@ function makeFakeAdminHttp() {
|
|
|
59
57
|
writeHead(s) { status = s; },
|
|
60
58
|
end(b) { payload = b ? JSON.parse(b) : null; },
|
|
61
59
|
};
|
|
62
|
-
await handler(req, res,
|
|
60
|
+
await handler(req, res, new URLSearchParams());
|
|
63
61
|
return { status, body: payload };
|
|
64
62
|
},
|
|
65
63
|
routes,
|
|
66
64
|
};
|
|
67
65
|
}
|
|
68
66
|
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
identity: {
|
|
72
|
-
name: 'Demo', tag: 'demo', version: '1.0.0', oneLine: 'A demo project.',
|
|
73
|
-
claim: 'Ship faster.', sub: 'Subtitle text.', audience: 'Developers', install: 'npm i demo',
|
|
74
|
-
},
|
|
75
|
-
stats: [{ v: '5', k: 'years', n: 'since launch' }],
|
|
76
|
-
pillars: [{ t: 'Fast', d: 'It is fast.', k: 'speed' }],
|
|
77
|
-
quotes: [],
|
|
78
|
-
feature: {
|
|
79
|
-
name: 'Feature', kicker: 'New', status: 'shipped', owner: 'team', oneLine: 'One line.',
|
|
80
|
-
problem: 'Problem text.', solution: 'Solution text.',
|
|
81
|
-
steps: [{ t: 'Step', d: 'desc' }], rules: [{ t: 'Rule', d: 'desc' }],
|
|
82
|
-
specs: [['label', 'value', 'note']], faq: [{ q: 'Q', a: 'A' }],
|
|
83
|
-
timeline: [{ w: 'W1', t: 'Thing', s: 'done' }],
|
|
84
|
-
},
|
|
85
|
-
architecture: {
|
|
86
|
-
summary: 'Arch summary.',
|
|
87
|
-
principles: [{ t: 'P', d: 'd' }],
|
|
88
|
-
layers: [{ n: 'Layer', d: 'd', f: '5', tone: 'accent' }],
|
|
89
|
-
modules: [{ n: 'Mod', d: 'd', f: 3, dep: [], heat: 1 }],
|
|
90
|
-
flow: [{ a: 'A', b: 'B', t: 't', n: 'n' }],
|
|
91
|
-
decisions: [{ id: 'd1', t: 't', w: 'w', s: 'accepted' }],
|
|
92
|
-
risks: [],
|
|
93
|
-
},
|
|
94
|
-
...overrides,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
67
|
+
const GOOD_HTML = '<!DOCTYPE html><html><head><style>body{background:url(data:image/png;base64,AAAA)}</style></head><body><script>1+1</script></body></html>';
|
|
97
68
|
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
for (const slot of lens.slots) {
|
|
103
|
-
picks[lens.id][slot.id] = slot.variants[0].id;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return picks;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ─── Route registration ────────────────────────────────────────────────
|
|
110
|
-
test('registers all four project-home admin routes', () => {
|
|
111
|
-
const fake = makeFakeAdminHttp();
|
|
112
|
-
registerAdminRoute(fake);
|
|
113
|
-
expect(fake.routes.has('GET /admin/project-home/contract')).toBe(true);
|
|
114
|
-
expect(fake.routes.has('POST /admin/project-home/validate-summary')).toBe(true);
|
|
115
|
-
expect(fake.routes.has('POST /admin/project-home/render')).toBe(true);
|
|
116
|
-
expect(fake.routes.has('GET /admin/project-home/status')).toBe(true);
|
|
69
|
+
test('registers exactly one route: POST /admin/project-home/write', () => {
|
|
70
|
+
const adminHttp = makeFakeAdminHttp();
|
|
71
|
+
registerAdminRoute(adminHttp);
|
|
72
|
+
expect([...adminHttp.routes.keys()]).toEqual(['POST /admin/project-home/write']);
|
|
117
73
|
});
|
|
118
74
|
|
|
119
|
-
|
|
120
|
-
test('contract: self-sufficient — 5 lenses, non-empty slots, absolute paths, no repo-relative path strings', async () => {
|
|
75
|
+
test('valid html is written atomically to project-pages/home.html', async () => {
|
|
121
76
|
const cwd = await mkProjectCwd();
|
|
122
|
-
const
|
|
123
|
-
registerAdminRoute(
|
|
124
|
-
|
|
125
|
-
const { status, body } = await fake.call('GET', '/admin/project-home/contract', {
|
|
126
|
-
query: new URLSearchParams({ cwd }),
|
|
127
|
-
});
|
|
77
|
+
const adminHttp = makeFakeAdminHttp();
|
|
78
|
+
registerAdminRoute(adminHttp);
|
|
79
|
+
const { status, body } = await adminHttp.call('POST', '/admin/project-home/write', { body: { cwd, html: GOOD_HTML } });
|
|
128
80
|
expect(status).toBe(200);
|
|
129
81
|
expect(body.ok).toBe(true);
|
|
130
|
-
|
|
131
|
-
expect(body.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
);
|
|
136
|
-
for (const lens of body.catalog.lenses) {
|
|
137
|
-
expect(Array.isArray(lens.slots)).toBe(true);
|
|
138
|
-
expect(lens.slots.length).toBeGreaterThan(0);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
expect(path.isAbsolute(body.paths.summaryPath)).toBe(true);
|
|
142
|
-
expect(path.isAbsolute(body.paths.picksPath)).toBe(true);
|
|
143
|
-
expect(path.isAbsolute(body.paths.outputDir)).toBe(true);
|
|
144
|
-
expect(body.paths.summaryPath).toContain(cwd);
|
|
145
|
-
|
|
146
|
-
expect(body.summarySchema).toBeTruthy();
|
|
147
|
-
expect(body.summarySchema.properties.identity).toBeTruthy();
|
|
148
|
-
|
|
149
|
-
// The operational fields (catalog/paths/protocol) must never point the
|
|
150
|
-
// caller at a repo-relative path — the whole point of this route. The
|
|
151
|
-
// free-form "spec.text" field is the architecture doc's own prose, which
|
|
152
|
-
// legitimately documents source file locations for background reading;
|
|
153
|
-
// it is explicitly excluded from this check (see route file's loadCatalog
|
|
154
|
-
// comment for why the catalog's own $comment is stripped for the same
|
|
155
|
-
// reason).
|
|
156
|
-
const operationalFields = JSON.stringify({ protocol: body.protocol, catalog: body.catalog, paths: body.paths });
|
|
157
|
-
expect(operationalFields).not.toMatch(/src\/renderer|src\/main|scripts\//);
|
|
158
|
-
// PRD 1093's exact banned-substring list (the class of path that broke
|
|
159
|
-
// generation on a foreign machine before this route existed).
|
|
160
|
-
for (const banned of [
|
|
161
|
-
'session-manager-operations/architecture/',
|
|
162
|
-
'session-manager-operations/design-mocks/',
|
|
163
|
-
'.claude/agents/',
|
|
164
|
-
'src/renderer/',
|
|
165
|
-
'npm run build:project-pages',
|
|
166
|
-
]) {
|
|
167
|
-
expect(operationalFields, `operational fields must not mention "${banned}"`).not.toContain(banned);
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test('contract: rejects a missing/non-absolute cwd with a structured error, not a throw', async () => {
|
|
172
|
-
const fake = makeFakeAdminHttp();
|
|
173
|
-
registerAdminRoute(fake);
|
|
174
|
-
|
|
175
|
-
const missing = await fake.call('GET', '/admin/project-home/contract', { query: new URLSearchParams() });
|
|
176
|
-
expect(missing.status).toBe(400);
|
|
177
|
-
expect(missing.body.ok).toBe(false);
|
|
178
|
-
|
|
179
|
-
const relative = await fake.call('GET', '/admin/project-home/contract', {
|
|
180
|
-
query: new URLSearchParams({ cwd: 'relative/path' }),
|
|
181
|
-
});
|
|
182
|
-
expect(relative.status).toBe(400);
|
|
183
|
-
expect(relative.body.ok).toBe(false);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
test('contract: rejects a cwd with no session-manager-operations/ directory', async () => {
|
|
187
|
-
const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-not-a-project-'));
|
|
188
|
-
config.addAllowedRoot(dir);
|
|
189
|
-
tmpDirs.push(dir);
|
|
190
|
-
const fake = makeFakeAdminHttp();
|
|
191
|
-
registerAdminRoute(fake);
|
|
192
|
-
|
|
193
|
-
const { status, body } = await fake.call('GET', '/admin/project-home/contract', {
|
|
194
|
-
query: new URLSearchParams({ cwd: dir }),
|
|
195
|
-
});
|
|
196
|
-
expect(status).toBe(400);
|
|
197
|
-
expect(body.ok).toBe(false);
|
|
198
|
-
expect(body.error).toMatch(/not a Session Manager project/);
|
|
82
|
+
const target = path.join(fs.realpathSync(cwd), 'session-manager-operations', 'project-pages', 'home.html');
|
|
83
|
+
expect(body.path).toBe(target);
|
|
84
|
+
expect(fs.readFileSync(target, 'utf8')).toBe(GOOD_HTML);
|
|
85
|
+
// no per-lens / summary / manifest side files
|
|
86
|
+
expect(fs.readdirSync(path.dirname(target))).toEqual(['home.html']);
|
|
199
87
|
});
|
|
200
88
|
|
|
201
|
-
|
|
202
|
-
test('validate-summary: accepts a well-formed summary', async () => {
|
|
89
|
+
test('a second write replaces the first', async () => {
|
|
203
90
|
const cwd = await mkProjectCwd();
|
|
204
|
-
const
|
|
205
|
-
registerAdminRoute(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
expect(status).toBe(200);
|
|
211
|
-
expect(body.valid).toBe(true);
|
|
212
|
-
expect(body.errors).toEqual([]);
|
|
91
|
+
const adminHttp = makeFakeAdminHttp();
|
|
92
|
+
registerAdminRoute(adminHttp);
|
|
93
|
+
await adminHttp.call('POST', '/admin/project-home/write', { body: { cwd, html: '<p>one</p>' } });
|
|
94
|
+
await adminHttp.call('POST', '/admin/project-home/write', { body: { cwd, html: '<p>two</p>' } });
|
|
95
|
+
const target = path.join(fs.realpathSync(cwd), 'session-manager-operations', 'project-pages', 'home.html');
|
|
96
|
+
expect(fs.readFileSync(target, 'utf8')).toBe('<p>two</p>');
|
|
213
97
|
});
|
|
214
98
|
|
|
215
|
-
test(
|
|
99
|
+
test.each([
|
|
100
|
+
['empty', ''],
|
|
101
|
+
['whitespace only', ' \n '],
|
|
102
|
+
['script src', '<html><script src="app.js"></script></html>'],
|
|
103
|
+
['script src (remote, single quotes, attrs first)', "<script defer type='module' src='https://cdn.example.com/x.js'></script>"],
|
|
104
|
+
['link href http', '<link rel="stylesheet" href="https://fonts.googleapis.com/css">'],
|
|
105
|
+
['link href protocol-relative', '<link rel="stylesheet" href="//cdn.example.com/a.css">'],
|
|
106
|
+
['@import', '<style>@import url("https://x.com/a.css");</style>'],
|
|
107
|
+
['@import bare string', '<style>@import "a.css";</style>'],
|
|
108
|
+
['css url(http)', '<style>a{background:url(https://x.com/a.png)}</style>'],
|
|
109
|
+
['css url(quoted http)', "<style>a{background:url( 'http://x.com/a.png')}</style>"],
|
|
110
|
+
])('rejects %s with 400 and writes nothing', async (_label, html) => {
|
|
216
111
|
const cwd = await mkProjectCwd();
|
|
217
|
-
const
|
|
218
|
-
registerAdminRoute(
|
|
219
|
-
|
|
220
|
-
const bad = validSummary({ identity: { ...validSummary().identity, name: '' } });
|
|
221
|
-
const { status, body } = await fake.call('POST', '/admin/project-home/validate-summary', {
|
|
222
|
-
body: { cwd, summary: bad },
|
|
223
|
-
});
|
|
224
|
-
expect(status).toBe(200);
|
|
225
|
-
expect(body.valid).toBe(false);
|
|
226
|
-
expect(body.errors.length).toBeGreaterThan(0);
|
|
227
|
-
expect(body.errors[0]).toHaveProperty('field');
|
|
228
|
-
expect(body.errors[0]).toHaveProperty('message');
|
|
229
|
-
expect(body.errors.some((e) => e.field === 'identity.name')).toBe(true);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// ─── render ────────────────────────────────────────────────────────────
|
|
233
|
-
test('render: happy path writes summary.json, picks.json, all 5 lens htmls, and manifest.json', async () => {
|
|
234
|
-
const cwd = await mkProjectCwd();
|
|
235
|
-
const fake = makeFakeAdminHttp();
|
|
236
|
-
registerAdminRoute(fake);
|
|
237
|
-
|
|
238
|
-
const contract = await fake.call('GET', '/admin/project-home/contract', { query: new URLSearchParams({ cwd }) });
|
|
239
|
-
const picks = picksFromCatalog(contract.body.catalog);
|
|
240
|
-
const summary = validSummary();
|
|
241
|
-
|
|
242
|
-
const { status, body } = await fake.call('POST', '/admin/project-home/render', {
|
|
243
|
-
body: { cwd, summary, picks },
|
|
244
|
-
});
|
|
245
|
-
expect(status).toBe(200);
|
|
246
|
-
expect(body.ok).toBe(true);
|
|
247
|
-
expect(typeof body.generatedAt).toBe('string');
|
|
248
|
-
expect(Number.isNaN(Date.parse(body.generatedAt))).toBe(false);
|
|
249
|
-
expect(Array.isArray(body.filesWritten)).toBe(true);
|
|
250
|
-
|
|
251
|
-
const dir = path.join(cwd, 'session-manager-operations', 'project-pages');
|
|
252
|
-
expect(fs.existsSync(path.join(dir, 'summary.json'))).toBe(true);
|
|
253
|
-
expect(fs.existsSync(path.join(dir, 'picks.json'))).toBe(true);
|
|
254
|
-
for (const lens of ['home', 'marketing', 'feature', 'architecture', 'brief']) {
|
|
255
|
-
expect(fs.existsSync(path.join(dir, 'output', `${lens}.html`))).toBe(true);
|
|
256
|
-
}
|
|
257
|
-
const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'output', 'manifest.json'), 'utf8'));
|
|
258
|
-
expect(manifest.generatedAt).toBe(body.generatedAt);
|
|
259
|
-
|
|
260
|
-
expect(JSON.parse(fs.readFileSync(path.join(dir, 'summary.json'), 'utf8'))).toEqual(summary);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
test('render: a malformed summary is rejected and writes NOTHING', async () => {
|
|
264
|
-
const cwd = await mkProjectCwd();
|
|
265
|
-
const fake = makeFakeAdminHttp();
|
|
266
|
-
registerAdminRoute(fake);
|
|
267
|
-
|
|
268
|
-
const bad = validSummary({ architecture: { ...validSummary().architecture, summary: '' } });
|
|
269
|
-
const { status, body } = await fake.call('POST', '/admin/project-home/render', {
|
|
270
|
-
body: { cwd, summary: bad, picks: {} },
|
|
271
|
-
});
|
|
112
|
+
const adminHttp = makeFakeAdminHttp();
|
|
113
|
+
registerAdminRoute(adminHttp);
|
|
114
|
+
const { status, body } = await adminHttp.call('POST', '/admin/project-home/write', { body: { cwd, html } });
|
|
272
115
|
expect(status).toBe(400);
|
|
273
116
|
expect(body.ok).toBe(false);
|
|
274
|
-
expect(
|
|
275
|
-
expect(body.errors.length).toBeGreaterThan(0);
|
|
276
|
-
|
|
277
|
-
const dir = path.join(cwd, 'session-manager-operations', 'project-pages');
|
|
278
|
-
expect(fs.existsSync(dir)).toBe(false);
|
|
117
|
+
expect(fs.existsSync(path.join(cwd, 'session-manager-operations', 'project-pages'))).toBe(false);
|
|
279
118
|
});
|
|
280
119
|
|
|
281
|
-
test('
|
|
120
|
+
test('rejects html over 1MB', async () => {
|
|
282
121
|
const cwd = await mkProjectCwd();
|
|
283
|
-
const
|
|
284
|
-
registerAdminRoute(
|
|
285
|
-
|
|
286
|
-
const { status, body } = await
|
|
287
|
-
body: { cwd, summary: validSummary(), picks: { home: { hero: 42 } } },
|
|
288
|
-
});
|
|
122
|
+
const adminHttp = makeFakeAdminHttp();
|
|
123
|
+
registerAdminRoute(adminHttp);
|
|
124
|
+
const html = 'a'.repeat(MAX_HTML_BYTES + 1);
|
|
125
|
+
const { status, body } = await adminHttp.call('POST', '/admin/project-home/write', { body: { cwd, html } });
|
|
289
126
|
expect(status).toBe(400);
|
|
290
|
-
expect(body.
|
|
291
|
-
expect(body.errors.some((e) => e.field === 'picks.home.hero')).toBe(true);
|
|
292
|
-
|
|
293
|
-
const dir = path.join(cwd, 'session-manager-operations', 'project-pages');
|
|
294
|
-
expect(fs.existsSync(dir)).toBe(false);
|
|
127
|
+
expect(body.error).toContain('limit');
|
|
295
128
|
});
|
|
296
129
|
|
|
297
|
-
test('
|
|
298
|
-
// mkProjectCwd() (used by every other test here) calls config.addAllowedRoot
|
|
299
|
-
// itself so validatePath succeeds for an arbitrary os.tmpdir() location —
|
|
300
|
-
// which would defeat this specific assertion. Use a cwd that's readable
|
|
301
|
-
// via validatePath's own default allowed root (os.homedir()) WITHOUT any
|
|
302
|
-
// addAllowedRoot call, so a write only succeeds if a route handler itself
|
|
303
|
-
// called it — matching prdCreate.test.cjs's / prdMigration.test.cjs's own
|
|
304
|
-
// "homedir-rooted tmp cwd" pattern for exactly this kind of check.
|
|
305
|
-
const cwd = fs.realpathSync(await fsp.mkdtemp(path.join(os.homedir(), '.sm-project-home-admin-')));
|
|
306
|
-
tmpDirs.push(cwd);
|
|
307
|
-
fs.mkdirSync(path.join(cwd, 'session-manager-operations'), { recursive: true });
|
|
308
|
-
|
|
309
|
-
const fake = makeFakeAdminHttp();
|
|
310
|
-
registerAdminRoute(fake);
|
|
311
|
-
|
|
312
|
-
await fake.call('GET', '/admin/project-home/contract', { query: new URLSearchParams({ cwd }) });
|
|
313
|
-
await fake.call('GET', '/admin/project-home/status', { query: new URLSearchParams({ cwd }) });
|
|
314
|
-
|
|
315
|
-
// A write through config.cjs for this cwd must still be refused — the
|
|
316
|
-
// GET routes above must not have called config.addAllowedRoot for it.
|
|
317
|
-
const probePath = path.join(cwd, 'session-manager-operations', 'project-pages', 'probe.json');
|
|
318
|
-
await expect(config.writeJson(probePath, { a: 1 }, { writer: 'project-home' })).rejects.toThrow(
|
|
319
|
-
/Write outside allowed write boundaries/,
|
|
320
|
-
);
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
test('render: two concurrent renders for the same cwd are serialized, not interleaved', async () => {
|
|
130
|
+
test('a request body far beyond the cap is a clean 400, not a throw', async () => {
|
|
324
131
|
const cwd = await mkProjectCwd();
|
|
325
|
-
const
|
|
326
|
-
registerAdminRoute(
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
// Distinct summaries so writes for render A vs render B can be told apart
|
|
332
|
-
// by content — identity.oneLine is echoed into every lens's html output
|
|
333
|
-
// (see renderer.cjs's htmlDocument/proj.oneLine usage).
|
|
334
|
-
const summaryA = validSummary({ identity: { ...validSummary().identity, oneLine: 'MARKER_RENDER_A' } });
|
|
335
|
-
const summaryB = validSummary({ identity: { ...validSummary().identity, oneLine: 'MARKER_RENDER_B' } });
|
|
336
|
-
|
|
337
|
-
const events = [];
|
|
338
|
-
let releaseGate;
|
|
339
|
-
const gate = new Promise((resolve) => { releaseGate = resolve; });
|
|
340
|
-
let gateArmed = false;
|
|
341
|
-
|
|
342
|
-
function labelForArgs(args) {
|
|
343
|
-
const text = typeof args[1] === 'string' ? args[1] : JSON.stringify(args[1]);
|
|
344
|
-
if (text.includes('MARKER_RENDER_A')) return 'A';
|
|
345
|
-
if (text.includes('MARKER_RENDER_B')) return 'B';
|
|
346
|
-
return 'unknown';
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const realWriteJson = config.writeJson.bind(config);
|
|
350
|
-
const realWriteTextAtomic = config.writeTextAtomic.bind(config);
|
|
351
|
-
|
|
352
|
-
async function instrumented(real, args) {
|
|
353
|
-
const label = labelForArgs(args);
|
|
354
|
-
events.push({ label, phase: 'start' });
|
|
355
|
-
// Only the very first write of the whole test is gated — this is render
|
|
356
|
-
// A's first write, deep inside its own Promise.all of writes. Holding it
|
|
357
|
-
// open gives render B a real window to start writing if serialization
|
|
358
|
-
// were absent; every other write (A's remaining writes, all of B's
|
|
359
|
-
// writes) proceeds unblocked.
|
|
360
|
-
if (!gateArmed) {
|
|
361
|
-
gateArmed = true;
|
|
362
|
-
await gate;
|
|
363
|
-
}
|
|
364
|
-
const result = await real(...args);
|
|
365
|
-
events.push({ label, phase: 'end' });
|
|
366
|
-
return result;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const writeJsonSpy = vi.spyOn(config, 'writeJson').mockImplementation((...args) => instrumented(realWriteJson, args));
|
|
370
|
-
const writeTextAtomicSpy = vi
|
|
371
|
-
.spyOn(config, 'writeTextAtomic')
|
|
372
|
-
.mockImplementation((...args) => instrumented(realWriteTextAtomic, args));
|
|
373
|
-
|
|
374
|
-
try {
|
|
375
|
-
const renderA = fake.call('POST', '/admin/project-home/render', { body: { cwd, summary: summaryA, picks } });
|
|
376
|
-
const renderB = fake.call('POST', '/admin/project-home/render', { body: { cwd, summary: summaryB, picks } });
|
|
377
|
-
|
|
378
|
-
// Yield microtasks (no wall-clock wait) until render A's first write has
|
|
379
|
-
// registered, then release it.
|
|
380
|
-
for (let i = 0; i < 1000 && events.length === 0; i += 1) {
|
|
381
|
-
await Promise.resolve();
|
|
382
|
-
}
|
|
383
|
-
expect(events.length).toBeGreaterThan(0);
|
|
384
|
-
releaseGate();
|
|
385
|
-
|
|
386
|
-
const [resA, resB] = await Promise.all([renderA, renderB]);
|
|
387
|
-
expect(resA.status).toBe(200);
|
|
388
|
-
expect(resB.status).toBe(200);
|
|
389
|
-
|
|
390
|
-
const labels = events.map((e) => e.label);
|
|
391
|
-
const firstA = labels.indexOf('A');
|
|
392
|
-
const lastA = labels.lastIndexOf('A');
|
|
393
|
-
const firstB = labels.indexOf('B');
|
|
394
|
-
const lastB = labels.lastIndexOf('B');
|
|
395
|
-
expect(firstA).toBeGreaterThanOrEqual(0);
|
|
396
|
-
expect(firstB).toBeGreaterThanOrEqual(0);
|
|
397
|
-
// Serialized means one render's entire write span is fully outside the
|
|
398
|
-
// other's — never interleaved.
|
|
399
|
-
expect(lastA < firstB || lastB < firstA).toBe(true);
|
|
400
|
-
|
|
401
|
-
const dir = path.join(cwd, 'session-manager-operations', 'project-pages');
|
|
402
|
-
const outputFiles = fs.readdirSync(path.join(dir, 'output')).sort();
|
|
403
|
-
expect(outputFiles).toEqual([
|
|
404
|
-
'architecture.html',
|
|
405
|
-
'brief.html',
|
|
406
|
-
'feature.html',
|
|
407
|
-
'home.html',
|
|
408
|
-
'manifest.json',
|
|
409
|
-
'marketing.html',
|
|
410
|
-
]);
|
|
411
|
-
} finally {
|
|
412
|
-
writeJsonSpy.mockRestore();
|
|
413
|
-
writeTextAtomicSpy.mockRestore();
|
|
414
|
-
}
|
|
132
|
+
const adminHttp = makeFakeAdminHttp();
|
|
133
|
+
registerAdminRoute(adminHttp);
|
|
134
|
+
const html = 'a'.repeat(MAX_HTML_BYTES * 3);
|
|
135
|
+
const { status, body } = await adminHttp.call('POST', '/admin/project-home/write', { body: { cwd, html } });
|
|
136
|
+
expect(status).toBe(400);
|
|
137
|
+
expect(body.error).toContain('limit');
|
|
415
138
|
});
|
|
416
139
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const fake = makeFakeAdminHttp();
|
|
421
|
-
registerAdminRoute(fake);
|
|
422
|
-
|
|
423
|
-
const { status, body } = await fake.call('GET', '/admin/project-home/status', {
|
|
424
|
-
query: new URLSearchParams({ cwd }),
|
|
425
|
-
});
|
|
426
|
-
expect(status).toBe(200);
|
|
427
|
-
expect(body.ok).toBe(true);
|
|
428
|
-
expect(body.summary.exists).toBe(false);
|
|
429
|
-
expect(body.picks.exists).toBe(false);
|
|
430
|
-
expect(body.manifest.exists).toBe(false);
|
|
431
|
-
expect(body.manifest.generatedAt).toBe(null);
|
|
432
|
-
for (const lens of ['home', 'marketing', 'feature', 'architecture', 'brief']) {
|
|
433
|
-
expect(body.output[lens].exists).toBe(false);
|
|
434
|
-
}
|
|
140
|
+
test('accepts an inline <link> to a local/data href and plain http text content', () => {
|
|
141
|
+
expect(validateHomeHtml('<p>see https://example.com in prose</p>')).toBeNull();
|
|
142
|
+
expect(validateHomeHtml('<link rel="icon" href="data:image/png;base64,AAAA">')).toBeNull();
|
|
435
143
|
});
|
|
436
144
|
|
|
437
|
-
test('
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
registerAdminRoute(fake);
|
|
441
|
-
|
|
442
|
-
const contract = await fake.call('GET', '/admin/project-home/contract', { query: new URLSearchParams({ cwd }) });
|
|
443
|
-
const picks = picksFromCatalog(contract.body.catalog);
|
|
444
|
-
await fake.call('POST', '/admin/project-home/render', { body: { cwd, summary: validSummary(), picks } });
|
|
445
|
-
|
|
446
|
-
const { body } = await fake.call('GET', '/admin/project-home/status', { query: new URLSearchParams({ cwd }) });
|
|
447
|
-
expect(body.summary.exists).toBe(true);
|
|
448
|
-
expect(body.picks.exists).toBe(true);
|
|
449
|
-
expect(body.manifest.exists).toBe(true);
|
|
450
|
-
expect(typeof body.manifest.generatedAt).toBe('string');
|
|
451
|
-
for (const lens of ['home', 'marketing', 'feature', 'architecture', 'brief']) {
|
|
452
|
-
expect(body.output[lens].exists).toBe(true);
|
|
453
|
-
expect(typeof body.output[lens].mtimeMs).toBe('number');
|
|
454
|
-
}
|
|
145
|
+
test('validateHomeHtml exact 1MB is accepted, non-string rejected', () => {
|
|
146
|
+
expect(validateHomeHtml('a'.repeat(MAX_HTML_BYTES))).toBeNull();
|
|
147
|
+
expect(validateHomeHtml(undefined)).toMatch(/non-empty/);
|
|
455
148
|
});
|
|
456
149
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const bilkoHost = require('../bilkoHost.cjs');
|
|
474
|
-
const hostState = await bilkoHost.get({ cwd });
|
|
475
|
-
expect(hostState.hasMarketingPage).toBe(true);
|
|
476
|
-
|
|
477
|
-
await bilkoHost.addDocument({
|
|
478
|
-
cwd,
|
|
479
|
-
subpath: 'feature',
|
|
480
|
-
title: 'Feature',
|
|
481
|
-
source: { kind: 'project-page-lens', lens: 'feature' },
|
|
482
|
-
});
|
|
483
|
-
const result = await bilkoHost.prepareBundle({ cwd, slug: 'demo-app' });
|
|
484
|
-
expect(fs.existsSync(path.join(result.distPath, 'feature', 'index.html'))).toBe(true);
|
|
150
|
+
test('bad JSON, missing/relative cwd, non-project cwd, and unknown keys are 400s', async () => {
|
|
151
|
+
const adminHttp = makeFakeAdminHttp();
|
|
152
|
+
registerAdminRoute(adminHttp);
|
|
153
|
+
const route = '/admin/project-home/write';
|
|
154
|
+
expect((await adminHttp.call('POST', route, { rawBody: '{nope' })).status).toBe(400);
|
|
155
|
+
expect((await adminHttp.call('POST', route, { body: { html: 'x' } })).status).toBe(400);
|
|
156
|
+
expect((await adminHttp.call('POST', route, { body: { cwd: 'relative/dir', html: 'x' } })).status).toBe(400);
|
|
157
|
+
expect((await adminHttp.call('POST', route, { body: { cwd: '/x', html: 'x', extra: 1 } })).status).toBe(400);
|
|
158
|
+
|
|
159
|
+
const bare = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-project-home-bare-'));
|
|
160
|
+
tmpDirs.push(bare);
|
|
161
|
+
config.addAllowedRoot(bare);
|
|
162
|
+
const res = await adminHttp.call('POST', route, { body: { cwd: bare, html: '<p>x</p>' } });
|
|
163
|
+
expect(res.status).toBe(400);
|
|
164
|
+
expect(res.body.error).toContain('not a Session Manager project');
|
|
485
165
|
});
|