lastlight 0.1.15 → 0.2.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 +265 -118
- package/agent-context/rules.md +62 -9
- package/agent-context/security.md +69 -0
- package/config/default.yaml +89 -0
- package/deploy/.env.production.example +20 -2
- package/deploy/entrypoint.sh +16 -17
- package/deploy/native/README.md +160 -0
- package/deploy/native/install.sh +110 -0
- package/deploy/native/lastlight.env.example +95 -0
- package/deploy/native/lastlight.service +50 -0
- package/deploy/sandbox-entrypoint.sh +59 -42
- package/dist/admin/auth.d.ts +1 -1
- package/dist/admin/auth.js +3 -1
- package/dist/admin/auth.js.map +1 -1
- package/dist/admin/chat-session-reader.d.ts +11 -14
- package/dist/admin/chat-session-reader.js +27 -51
- package/dist/admin/chat-session-reader.js.map +1 -1
- package/dist/admin/docker.d.ts +37 -0
- package/dist/admin/docker.js +86 -1
- package/dist/admin/docker.js.map +1 -1
- package/dist/admin/index.js +38 -3
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/log-search.test.d.ts +1 -0
- package/dist/admin/log-search.test.js +78 -0
- package/dist/admin/log-search.test.js.map +1 -0
- package/dist/admin/routes.d.ts +52 -3
- package/dist/admin/routes.js +780 -63
- package/dist/admin/routes.js.map +1 -1
- package/dist/admin/routes.test.js +680 -14
- package/dist/admin/routes.test.js.map +1 -1
- package/dist/admin/server-logs.test.d.ts +1 -0
- package/dist/admin/server-logs.test.js +58 -0
- package/dist/admin/server-logs.test.js.map +1 -0
- package/dist/admin/sessions.d.ts +26 -39
- package/dist/admin/sessions.js +86 -310
- package/dist/admin/sessions.js.map +1 -1
- package/dist/admin/sessions.test.d.ts +1 -0
- package/dist/admin/sessions.test.js +78 -0
- package/dist/admin/sessions.test.js.map +1 -0
- package/dist/admin/version.d.ts +15 -0
- package/dist/admin/version.js +73 -0
- package/dist/admin/version.js.map +1 -0
- package/dist/admin/version.test.d.ts +1 -0
- package/dist/admin/version.test.js +71 -0
- package/dist/admin/version.test.js.map +1 -0
- package/dist/cli-config.d.ts +55 -0
- package/dist/cli-config.js +119 -0
- package/dist/cli-config.js.map +1 -0
- package/dist/cli-config.test.d.ts +1 -0
- package/dist/cli-config.test.js +92 -0
- package/dist/cli-config.test.js.map +1 -0
- package/dist/cli-format.d.ts +19 -0
- package/dist/cli-format.js +107 -0
- package/dist/cli-format.js.map +1 -0
- package/dist/cli-server.d.ts +73 -0
- package/dist/cli-server.js +347 -0
- package/dist/cli-server.js.map +1 -0
- package/dist/cli-server.test.d.ts +1 -0
- package/dist/cli-server.test.js +38 -0
- package/dist/cli-server.test.js.map +1 -0
- package/dist/cli-timeline.d.ts +35 -0
- package/dist/cli-timeline.js +373 -0
- package/dist/cli-timeline.js.map +1 -0
- package/dist/cli-timeline.test.d.ts +1 -0
- package/dist/cli-timeline.test.js +104 -0
- package/dist/cli-timeline.test.js.map +1 -0
- package/dist/cli.d.ts +0 -19
- package/dist/cli.js +910 -194
- package/dist/cli.js.map +1 -1
- package/dist/config-overlay.test.d.ts +1 -0
- package/dist/config-overlay.test.js +112 -0
- package/dist/config-overlay.test.js.map +1 -0
- package/dist/config-resolve.d.ts +28 -0
- package/dist/config-resolve.js +47 -0
- package/dist/config-resolve.js.map +1 -0
- package/dist/config-resolve.test.d.ts +1 -0
- package/dist/config-resolve.test.js +68 -0
- package/dist/config-resolve.test.js.map +1 -0
- package/dist/config.d.ts +62 -49
- package/dist/config.js +452 -76
- package/dist/config.js.map +1 -1
- package/dist/config.test.js +201 -32
- package/dist/config.test.js.map +1 -1
- package/dist/connectors/github-webhook.js +83 -5
- package/dist/connectors/github-webhook.js.map +1 -1
- package/dist/connectors/github-webhook.test.d.ts +1 -0
- package/dist/connectors/github-webhook.test.js +156 -0
- package/dist/connectors/github-webhook.test.js.map +1 -0
- package/dist/connectors/messaging/session-manager.d.ts +18 -0
- package/dist/connectors/messaging/session-manager.js +83 -2
- package/dist/connectors/messaging/session-manager.js.map +1 -1
- package/dist/connectors/messaging/session-manager.test.d.ts +1 -0
- package/dist/connectors/messaging/session-manager.test.js +144 -0
- package/dist/connectors/messaging/session-manager.test.js.map +1 -0
- package/dist/connectors/slack/connector.d.ts +9 -0
- package/dist/connectors/slack/connector.js +15 -0
- package/dist/connectors/slack/connector.js.map +1 -1
- package/dist/connectors/slack/mrkdwn.js +81 -0
- package/dist/connectors/slack/mrkdwn.js.map +1 -1
- package/dist/connectors/slack/mrkdwn.test.js +49 -0
- package/dist/connectors/slack/mrkdwn.test.js.map +1 -1
- package/dist/connectors/types.d.ts +1 -1
- package/dist/cron/fanout.d.ts +33 -0
- package/dist/cron/fanout.js +55 -0
- package/dist/cron/fanout.js.map +1 -0
- package/dist/cron/fanout.test.d.ts +1 -0
- package/dist/cron/fanout.test.js +73 -0
- package/dist/cron/fanout.test.js.map +1 -0
- package/dist/cron/jobs.d.ts +5 -0
- package/dist/cron/jobs.js +10 -3
- package/dist/cron/jobs.js.map +1 -1
- package/dist/cron/scheduler.d.ts +12 -0
- package/dist/cron/scheduler.js +27 -1
- package/dist/cron/scheduler.js.map +1 -1
- package/dist/engine/agent-executor.d.ts +158 -0
- package/dist/engine/agent-executor.js +1140 -0
- package/dist/engine/agent-executor.js.map +1 -0
- package/dist/engine/agent-executor.test.d.ts +1 -0
- package/dist/engine/agent-executor.test.js +395 -0
- package/dist/engine/agent-executor.test.js.map +1 -0
- package/dist/engine/chat-runner.d.ts +123 -0
- package/dist/engine/chat-runner.js +379 -0
- package/dist/engine/chat-runner.js.map +1 -0
- package/dist/engine/chat-runner.test.d.ts +1 -0
- package/dist/engine/chat-runner.test.js +104 -0
- package/dist/engine/chat-runner.test.js.map +1 -0
- package/dist/engine/chat-skills.d.ts +45 -0
- package/dist/engine/chat-skills.js +184 -0
- package/dist/engine/chat-skills.js.map +1 -0
- package/dist/engine/chat-skills.test.d.ts +1 -0
- package/dist/engine/chat-skills.test.js +20 -0
- package/dist/engine/chat-skills.test.js.map +1 -0
- package/dist/engine/chat.d.ts +20 -23
- package/dist/engine/chat.js +251 -155
- package/dist/engine/chat.js.map +1 -1
- package/dist/engine/chat.test.d.ts +1 -0
- package/dist/engine/chat.test.js +42 -0
- package/dist/engine/chat.test.js.map +1 -0
- package/dist/engine/classifier.d.ts +35 -2
- package/dist/engine/classifier.js +170 -31
- package/dist/engine/classifier.js.map +1 -1
- package/dist/engine/classifier.test.d.ts +1 -0
- package/dist/engine/classifier.test.js +115 -0
- package/dist/engine/classifier.test.js.map +1 -0
- package/dist/engine/dispatcher.d.ts +60 -0
- package/dist/engine/dispatcher.js +601 -0
- package/dist/engine/dispatcher.js.map +1 -0
- package/dist/engine/dispatcher.test.d.ts +1 -0
- package/dist/engine/dispatcher.test.js +511 -0
- package/dist/engine/dispatcher.test.js.map +1 -0
- package/dist/engine/event-shim.d.ts +117 -0
- package/dist/engine/event-shim.js +435 -0
- package/dist/engine/event-shim.js.map +1 -0
- package/dist/engine/event-shim.test.d.ts +1 -0
- package/dist/engine/event-shim.test.js +194 -0
- package/dist/engine/event-shim.test.js.map +1 -0
- package/dist/engine/git-auth.d.ts +13 -17
- package/dist/engine/git-auth.js +99 -42
- package/dist/engine/git-auth.js.map +1 -1
- package/dist/engine/git-auth.test.d.ts +1 -0
- package/dist/engine/git-auth.test.js +117 -0
- package/dist/engine/git-auth.test.js.map +1 -0
- package/dist/engine/github-app-client.d.ts +7 -0
- package/dist/engine/github-app-client.js +16 -0
- package/dist/engine/github-app-client.js.map +1 -0
- package/dist/engine/github-app-client.test.d.ts +1 -0
- package/dist/engine/github-app-client.test.js +44 -0
- package/dist/engine/github-app-client.test.js.map +1 -0
- package/dist/engine/github-tools.d.ts +12 -0
- package/dist/engine/github-tools.js +261 -0
- package/dist/engine/github-tools.js.map +1 -0
- package/dist/engine/github.d.ts +1027 -43
- package/dist/engine/github.js +153 -14
- package/dist/engine/github.js.map +1 -1
- package/dist/engine/llm.d.ts +47 -0
- package/dist/engine/llm.js +221 -0
- package/dist/engine/llm.js.map +1 -0
- package/dist/engine/llm.test.d.ts +1 -0
- package/dist/engine/llm.test.js +189 -0
- package/dist/engine/llm.test.js.map +1 -0
- package/dist/engine/profiles.d.ts +249 -0
- package/dist/engine/profiles.js +42 -0
- package/dist/engine/profiles.js.map +1 -0
- package/dist/engine/router.d.ts +12 -6
- package/dist/engine/router.js +339 -81
- package/dist/engine/router.js.map +1 -1
- package/dist/engine/router.test.js +428 -78
- package/dist/engine/router.test.js.map +1 -1
- package/dist/engine/screen.d.ts +41 -0
- package/dist/engine/screen.js +93 -0
- package/dist/engine/screen.js.map +1 -0
- package/dist/engine/screen.test.d.ts +1 -0
- package/dist/engine/screen.test.js +82 -0
- package/dist/engine/screen.test.js.map +1 -0
- package/dist/index.js +377 -432
- package/dist/index.js.map +1 -1
- package/dist/managed-repos.d.ts +7 -9
- package/dist/managed-repos.js +12 -15
- package/dist/managed-repos.js.map +1 -1
- package/dist/managed-repos.test.js +24 -19
- package/dist/managed-repos.test.js.map +1 -1
- package/dist/notify/index.d.ts +15 -0
- package/dist/notify/index.js +6 -0
- package/dist/notify/index.js.map +1 -0
- package/dist/notify/model.d.ts +57 -0
- package/dist/notify/model.js +89 -0
- package/dist/notify/model.js.map +1 -0
- package/dist/notify/model.test.d.ts +1 -0
- package/dist/notify/model.test.js +109 -0
- package/dist/notify/model.test.js.map +1 -0
- package/dist/notify/notifier.d.ts +17 -0
- package/dist/notify/notifier.js +87 -0
- package/dist/notify/notifier.js.map +1 -0
- package/dist/notify/notifier.test.d.ts +1 -0
- package/dist/notify/notifier.test.js +100 -0
- package/dist/notify/notifier.test.js.map +1 -0
- package/dist/notify/render.d.ts +21 -0
- package/dist/notify/render.js +59 -0
- package/dist/notify/render.js.map +1 -0
- package/dist/notify/render.test.d.ts +1 -0
- package/dist/notify/render.test.js +65 -0
- package/dist/notify/render.test.js.map +1 -0
- package/dist/notify/transports/github.d.ts +26 -0
- package/dist/notify/transports/github.js +26 -0
- package/dist/notify/transports/github.js.map +1 -0
- package/dist/notify/transports/slack.d.ts +26 -0
- package/dist/notify/transports/slack.js +28 -0
- package/dist/notify/transports/slack.js.map +1 -0
- package/dist/notify/transports.test.d.ts +1 -0
- package/dist/notify/transports.test.js +71 -0
- package/dist/notify/transports.test.js.map +1 -0
- package/dist/notify/types.d.ts +95 -0
- package/dist/notify/types.js +16 -0
- package/dist/notify/types.js.map +1 -0
- package/dist/sandbox/docker-compose.test.d.ts +1 -0
- package/dist/sandbox/docker-compose.test.js +128 -0
- package/dist/sandbox/docker-compose.test.js.map +1 -0
- package/dist/sandbox/docker.d.ts +97 -9
- package/dist/sandbox/docker.js +192 -41
- package/dist/sandbox/docker.js.map +1 -1
- package/dist/sandbox/docker.test.d.ts +1 -0
- package/dist/sandbox/docker.test.js +137 -0
- package/dist/sandbox/docker.test.js.map +1 -0
- package/dist/sandbox/egress-allowlist.d.ts +69 -0
- package/dist/sandbox/egress-allowlist.js +164 -0
- package/dist/sandbox/egress-allowlist.js.map +1 -0
- package/dist/sandbox/egress-allowlist.test.d.ts +1 -0
- package/dist/sandbox/egress-allowlist.test.js +65 -0
- package/dist/sandbox/egress-allowlist.test.js.map +1 -0
- package/dist/sandbox/egress-firewall-config.d.ts +127 -0
- package/dist/sandbox/egress-firewall-config.js +434 -0
- package/dist/sandbox/egress-firewall-config.js.map +1 -0
- package/dist/sandbox/egress-firewall-config.test.d.ts +1 -0
- package/dist/sandbox/egress-firewall-config.test.js +244 -0
- package/dist/sandbox/egress-firewall-config.test.js.map +1 -0
- package/dist/sandbox/images.d.ts +23 -0
- package/dist/sandbox/images.js +55 -0
- package/dist/sandbox/images.js.map +1 -0
- package/dist/sandbox/index.d.ts +82 -3
- package/dist/sandbox/index.js +223 -93
- package/dist/sandbox/index.js.map +1 -1
- package/dist/sandbox/index.test.d.ts +1 -0
- package/dist/sandbox/index.test.js +157 -0
- package/dist/sandbox/index.test.js.map +1 -0
- package/dist/session-log.d.ts +63 -0
- package/dist/session-log.js +290 -0
- package/dist/session-log.js.map +1 -0
- package/dist/session-log.test.d.ts +1 -0
- package/dist/session-log.test.js +85 -0
- package/dist/session-log.test.js.map +1 -0
- package/dist/setup.d.ts +30 -1
- package/dist/setup.js +241 -25
- package/dist/setup.js.map +1 -1
- package/dist/setup.test.js +138 -6
- package/dist/setup.test.js.map +1 -1
- package/dist/state/approval-store.d.ts +86 -0
- package/dist/state/approval-store.js +140 -0
- package/dist/state/approval-store.js.map +1 -0
- package/dist/state/build-assets.d.ts +50 -0
- package/dist/state/build-assets.js +154 -0
- package/dist/state/build-assets.js.map +1 -0
- package/dist/state/build-assets.test.d.ts +1 -0
- package/dist/state/build-assets.test.js +106 -0
- package/dist/state/build-assets.test.js.map +1 -0
- package/dist/state/db.d.ts +65 -333
- package/dist/state/db.js +112 -835
- package/dist/state/db.js.map +1 -1
- package/dist/state/db.test.js +127 -91
- package/dist/state/db.test.js.map +1 -1
- package/dist/state/execution-store.d.ts +257 -0
- package/dist/state/execution-store.js +527 -0
- package/dist/state/execution-store.js.map +1 -0
- package/dist/state/migrate.d.ts +15 -0
- package/dist/state/migrate.js +175 -0
- package/dist/state/migrate.js.map +1 -0
- package/dist/state/workflow-run-store.d.ts +195 -0
- package/dist/state/workflow-run-store.js +363 -0
- package/dist/state/workflow-run-store.js.map +1 -0
- package/dist/state/workflow-run-store.test.d.ts +1 -0
- package/dist/state/workflow-run-store.test.js +264 -0
- package/dist/state/workflow-run-store.test.js.map +1 -0
- package/dist/telemetry/index.d.ts +38 -0
- package/dist/telemetry/index.js +253 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/telemetry/index.test.d.ts +1 -0
- package/dist/telemetry/index.test.js +73 -0
- package/dist/telemetry/index.test.js.map +1 -0
- package/dist/telemetry/pi-events.d.ts +12 -0
- package/dist/telemetry/pi-events.js +134 -0
- package/dist/telemetry/pi-events.js.map +1 -0
- package/dist/telemetry/pi-events.test.d.ts +1 -0
- package/dist/telemetry/pi-events.test.js +69 -0
- package/dist/telemetry/pi-events.test.js.map +1 -0
- package/dist/workflows/dag.d.ts +13 -1
- package/dist/workflows/dag.js +7 -3
- package/dist/workflows/dag.js.map +1 -1
- package/dist/workflows/dag.test.js +22 -0
- package/dist/workflows/dag.test.js.map +1 -1
- package/dist/workflows/golden-build.test.d.ts +1 -0
- package/dist/workflows/golden-build.test.js +45 -0
- package/dist/workflows/golden-build.test.js.map +1 -0
- package/dist/workflows/loader-overlay.test.d.ts +1 -0
- package/dist/workflows/loader-overlay.test.js +101 -0
- package/dist/workflows/loader-overlay.test.js.map +1 -0
- package/dist/workflows/loader.d.ts +36 -11
- package/dist/workflows/loader.js +311 -87
- package/dist/workflows/loader.js.map +1 -1
- package/dist/workflows/loader.test.js +114 -10
- package/dist/workflows/loader.test.js.map +1 -1
- package/dist/workflows/loop-eval.js +2 -0
- package/dist/workflows/loop-eval.js.map +1 -1
- package/dist/workflows/loop-eval.test.js +14 -0
- package/dist/workflows/loop-eval.test.js.map +1 -1
- package/dist/workflows/phase-executor.d.ts +145 -0
- package/dist/workflows/phase-executor.js +691 -0
- package/dist/workflows/phase-executor.js.map +1 -0
- package/dist/workflows/phase-executor.test.d.ts +1 -0
- package/dist/workflows/phase-executor.test.js +434 -0
- package/dist/workflows/phase-executor.test.js.map +1 -0
- package/dist/workflows/phase-ref.d.ts +44 -0
- package/dist/workflows/phase-ref.js +78 -0
- package/dist/workflows/phase-ref.js.map +1 -0
- package/dist/workflows/phase-ref.test.d.ts +1 -0
- package/dist/workflows/phase-ref.test.js +24 -0
- package/dist/workflows/phase-ref.test.js.map +1 -0
- package/dist/workflows/resume.d.ts +5 -11
- package/dist/workflows/resume.js +88 -7
- package/dist/workflows/resume.js.map +1 -1
- package/dist/workflows/runner.d.ts +34 -27
- package/dist/workflows/runner.js +252 -930
- package/dist/workflows/runner.js.map +1 -1
- package/dist/workflows/runner.test.js +346 -35
- package/dist/workflows/runner.test.js.map +1 -1
- package/dist/workflows/schema.d.ts +52 -10
- package/dist/workflows/schema.js +147 -7
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/simple.d.ts +67 -3
- package/dist/workflows/simple.js +248 -87
- package/dist/workflows/simple.js.map +1 -1
- package/dist/workflows/simple.test.d.ts +1 -0
- package/dist/workflows/simple.test.js +107 -0
- package/dist/workflows/simple.test.js.map +1 -0
- package/dist/workflows/templates.d.ts +29 -0
- package/dist/workflows/templates.js +42 -4
- package/dist/workflows/templates.js.map +1 -1
- package/dist/workflows/templates.test.js +103 -0
- package/dist/workflows/templates.test.js.map +1 -1
- package/dist/workflows/triggers.d.ts +21 -0
- package/dist/workflows/triggers.js +44 -0
- package/dist/workflows/triggers.js.map +1 -0
- package/dist/workflows/verdict.d.ts +19 -0
- package/dist/workflows/verdict.js +30 -0
- package/dist/workflows/verdict.js.map +1 -0
- package/dist/workflows/verdict.test.d.ts +1 -0
- package/dist/workflows/verdict.test.js +44 -0
- package/dist/workflows/verdict.test.js.map +1 -0
- package/dist/worktree/manager.d.ts +1 -1
- package/dist/worktree/manager.js +14 -14
- package/dist/worktree/manager.js.map +1 -1
- package/dist/worktree/manager.test.d.ts +1 -0
- package/dist/worktree/manager.test.js +87 -0
- package/dist/worktree/manager.test.js.map +1 -0
- package/docker-compose.yml +206 -5
- package/package.json +19 -9
- package/sandbox.Dockerfile +97 -18
- package/skills/README.md +40 -0
- package/skills/browser-qa/SKILL.md +193 -0
- package/skills/browser-qa/scripts/agent-browser.mjs +380 -0
- package/skills/building/SKILL.md +95 -0
- package/skills/chat/SKILL.md +30 -11
- package/skills/code-review/SKILL.md +59 -0
- package/skills/debug-production/SKILL.md +108 -0
- package/skills/demo/SKILL.md +194 -0
- package/skills/demo/scripts/compose-demo.sh +205 -0
- package/skills/issue-answer/SKILL.md +90 -0
- package/skills/issue-comment/SKILL.md +38 -24
- package/skills/issue-triage/SKILL.md +103 -37
- package/skills/issue-triage/references/AGENT-BRIEF.md +61 -0
- package/skills/pr-comment/SKILL.md +64 -0
- package/skills/pr-review/SKILL.md +65 -49
- package/skills/qa-test/SKILL.md +97 -0
- package/skills/repo-health/SKILL.md +48 -45
- package/skills/security-feedback/SKILL.md +173 -0
- package/skills/security-feedback/references/templates.md +77 -0
- package/skills/security-review/SKILL.md +213 -0
- package/skills/security-review/references/issue-format.md +302 -0
- package/skills/verify/SKILL.md +118 -0
- package/workflows/answer.yaml +37 -0
- package/workflows/build.yaml +32 -22
- package/workflows/cron-security.yaml +6 -0
- package/workflows/demo.yaml +55 -0
- package/workflows/explore.yaml +9 -0
- package/workflows/pr-comment.yaml +16 -0
- package/workflows/pr-fix.yaml +3 -0
- package/workflows/pr-review.yaml +4 -1
- package/workflows/prompts/answer.md +67 -0
- package/workflows/prompts/architect.md +24 -7
- package/workflows/prompts/demo.md +100 -0
- package/workflows/prompts/executor.md +29 -20
- package/workflows/prompts/explore-ask.md +51 -22
- package/workflows/prompts/explore-publish.md +2 -2
- package/workflows/prompts/explore-read.md +24 -10
- package/workflows/prompts/explore-synthesize.md +15 -8
- package/workflows/prompts/fix.md +9 -10
- package/workflows/prompts/guardrails.md +26 -8
- package/workflows/prompts/pr-fix.md +6 -7
- package/workflows/prompts/pr.md +11 -10
- package/workflows/prompts/qa-browser.md +95 -0
- package/workflows/prompts/qa-synth.md +44 -0
- package/workflows/prompts/qa-test.md +60 -0
- package/workflows/prompts/re-reviewer.md +8 -9
- package/workflows/prompts/reviewer.md +10 -9
- package/workflows/prompts/verify-browser.md +93 -0
- package/workflows/prompts/verify-synth.md +44 -0
- package/workflows/prompts/verify.md +63 -0
- package/workflows/qa-test.yaml +87 -0
- package/workflows/security-feedback.yaml +25 -0
- package/workflows/security-review.yaml +29 -0
- package/workflows/verify.yaml +85 -0
- package/deploy/mcp-config.tmpl.json +0 -14
- package/dist/cron/rate-limits.d.ts +0 -26
- package/dist/cron/rate-limits.js +0 -193
- package/dist/cron/rate-limits.js.map +0 -1
- package/dist/engine/executor.d.ts +0 -75
- package/dist/engine/executor.js +0 -292
- package/dist/engine/executor.js.map +0 -1
- package/mcp-github-app/package.json +0 -18
- package/mcp-github-app/src/auth.js +0 -66
- package/mcp-github-app/src/github.js +0 -371
- package/mcp-github-app/src/index.js +0 -534
- package/skills/devops/webhook-subscriptions/SKILL.md +0 -180
- package/skills/github/DESCRIPTION.md +0 -3
- package/skills/github/codebase-inspection/SKILL.md +0 -115
- package/skills/github/github-auth/SKILL.md +0 -54
- package/skills/github/github-auth/scripts/gh-env.sh +0 -66
- package/skills/github/github-code-review/SKILL.md +0 -480
- package/skills/github/github-code-review/references/review-output-template.md +0 -74
- package/skills/github/github-issues/SKILL.md +0 -369
- package/skills/github/github-issues/templates/bug-report.md +0 -35
- package/skills/github/github-issues/templates/feature-request.md +0 -31
- package/skills/github/github-pr-workflow/SKILL.md +0 -395
- package/skills/github/github-pr-workflow/references/ci-troubleshooting.md +0 -183
- package/skills/github/github-pr-workflow/references/conventional-commits.md +0 -71
- package/skills/github/github-pr-workflow/templates/pr-body-bugfix.md +0 -35
- package/skills/github/github-pr-workflow/templates/pr-body-feature.md +0 -33
- package/skills/github/github-repo-management/SKILL.md +0 -515
- package/skills/github/github-repo-management/references/github-api-cheatsheet.md +0 -161
- package/skills/github-orchestrator/SKILL.md +0 -103
- package/skills/mcp/DESCRIPTION.md +0 -3
- package/skills/mcp/mcporter/SKILL.md +0 -122
- package/skills/mcp/native-mcp/SKILL.md +0 -356
- package/skills/software-development/architect/SKILL.md +0 -154
- package/skills/software-development/assure-guardrails/SKILL.md +0 -239
- package/skills/software-development/plan/SKILL.md +0 -64
- package/skills/software-development/requesting-code-review/SKILL.md +0 -291
- package/skills/software-development/subagent-driven-development/SKILL.md +0 -433
- package/skills/software-development/systematic-debugging/SKILL.md +0 -366
- package/skills/software-development/test-driven-development/SKILL.md +0 -342
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { Hono } from "hono";
|
|
2
6
|
import { createAdminRoutes } from "./routes.js";
|
|
7
|
+
import { mountAdmin } from "./index.js";
|
|
8
|
+
import { BuildAssetStore } from "../state/build-assets.js";
|
|
3
9
|
// Mock docker so tests don't need a running daemon
|
|
4
10
|
vi.mock("./docker.js", () => ({
|
|
5
11
|
listRunningContainers: vi.fn(async () => []),
|
|
6
12
|
killContainer: vi.fn(async () => { }),
|
|
7
13
|
getContainerStats: vi.fn(async () => []),
|
|
8
14
|
}));
|
|
9
|
-
// Mock arctic so we control OAuth flow without hitting Slack
|
|
15
|
+
// Mock arctic so we control OAuth flow without hitting Slack or GitHub
|
|
10
16
|
vi.mock("arctic", () => {
|
|
11
17
|
class Slack {
|
|
12
18
|
createAuthorizationURL(_state, _scopes) {
|
|
@@ -16,21 +22,33 @@ vi.mock("arctic", () => {
|
|
|
16
22
|
return { accessToken: () => "mock-slack-access-token" };
|
|
17
23
|
}
|
|
18
24
|
}
|
|
19
|
-
|
|
25
|
+
class GitHub {
|
|
26
|
+
createAuthorizationURL(_state, _scopes) {
|
|
27
|
+
return new URL("https://github.com/login/oauth/authorize?mocked=1");
|
|
28
|
+
}
|
|
29
|
+
async validateAuthorizationCode(_code) {
|
|
30
|
+
return { accessToken: () => "mock-github-access-token" };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { Slack, GitHub };
|
|
20
34
|
});
|
|
21
35
|
// Minimal mocks
|
|
22
36
|
const mockDb = {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
executions: {
|
|
38
|
+
executionStats: vi.fn(() => ({ total: 0, running: 0, success: 0, failed: 0 })),
|
|
39
|
+
dailyStats: vi.fn(() => []),
|
|
40
|
+
hourlyStats: vi.fn(() => []),
|
|
41
|
+
allExecutions: vi.fn(() => []),
|
|
42
|
+
runningExecutions: vi.fn(() => []),
|
|
43
|
+
},
|
|
44
|
+
runs: {
|
|
45
|
+
list: vi.fn(() => ({ runs: [], total: 0 })),
|
|
46
|
+
distinctNames: vi.fn(() => []),
|
|
47
|
+
getRun: vi.fn(() => null),
|
|
48
|
+
},
|
|
49
|
+
approvals: {
|
|
50
|
+
listPending: vi.fn(() => []),
|
|
51
|
+
},
|
|
34
52
|
};
|
|
35
53
|
const mockSessions = {
|
|
36
54
|
listSessionIds: vi.fn(() => []),
|
|
@@ -38,6 +56,7 @@ const mockSessions = {
|
|
|
38
56
|
exists: vi.fn(() => false),
|
|
39
57
|
read: vi.fn(async () => []),
|
|
40
58
|
getFilePath: vi.fn(() => null),
|
|
59
|
+
normalizeRawLine: vi.fn((raw) => [raw]),
|
|
41
60
|
};
|
|
42
61
|
function makeConfig(overrides = {}) {
|
|
43
62
|
return {
|
|
@@ -182,6 +201,226 @@ describe("GET /oauth/slack/callback", () => {
|
|
|
182
201
|
global.fetch = originalFetch;
|
|
183
202
|
});
|
|
184
203
|
});
|
|
204
|
+
describe("GET /auth-required (GitHub OAuth)", () => {
|
|
205
|
+
it("returns githubOAuth: false when not configured", async () => {
|
|
206
|
+
const app = createAdminRoutes(mockDb, mockSessions, mockSessions, makeConfig());
|
|
207
|
+
const res = await request(app, "/auth-required");
|
|
208
|
+
const body = await res.json();
|
|
209
|
+
expect(res.status).toBe(200);
|
|
210
|
+
expect(body.githubOAuth).toBe(false);
|
|
211
|
+
});
|
|
212
|
+
it("returns githubOAuth: true when client ID, secret, and allowed org are configured", async () => {
|
|
213
|
+
const app = createAdminRoutes(mockDb, mockSessions, mockSessions, makeConfig({
|
|
214
|
+
githubOAuthClientId: "GH_CLIENT",
|
|
215
|
+
githubOAuthClientSecret: "GH_SECRET",
|
|
216
|
+
githubAllowedOrg: "acme",
|
|
217
|
+
}));
|
|
218
|
+
const res = await request(app, "/auth-required");
|
|
219
|
+
const body = await res.json();
|
|
220
|
+
expect(res.status).toBe(200);
|
|
221
|
+
expect(body.githubOAuth).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
it("returns githubOAuth: true when allowed org is \"*\" (allow any user)", async () => {
|
|
224
|
+
const app = createAdminRoutes(mockDb, mockSessions, mockSessions, makeConfig({
|
|
225
|
+
githubOAuthClientId: "GH_CLIENT",
|
|
226
|
+
githubOAuthClientSecret: "GH_SECRET",
|
|
227
|
+
githubAllowedOrg: "*",
|
|
228
|
+
}));
|
|
229
|
+
const res = await request(app, "/auth-required");
|
|
230
|
+
const body = await res.json();
|
|
231
|
+
expect(body.githubOAuth).toBe(true);
|
|
232
|
+
});
|
|
233
|
+
it("returns githubOAuth: false when only clientId is set (secret missing)", async () => {
|
|
234
|
+
const app = createAdminRoutes(mockDb, mockSessions, mockSessions, makeConfig({
|
|
235
|
+
githubOAuthClientId: "GH_CLIENT",
|
|
236
|
+
}));
|
|
237
|
+
const res = await request(app, "/auth-required");
|
|
238
|
+
const body = await res.json();
|
|
239
|
+
expect(body.githubOAuth).toBe(false);
|
|
240
|
+
});
|
|
241
|
+
it("returns githubOAuth: false when id+secret set but allowed org is missing", async () => {
|
|
242
|
+
const errSpy = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
243
|
+
const app = createAdminRoutes(mockDb, mockSessions, mockSessions, makeConfig({
|
|
244
|
+
githubOAuthClientId: "GH_CLIENT",
|
|
245
|
+
githubOAuthClientSecret: "GH_SECRET",
|
|
246
|
+
}));
|
|
247
|
+
const res = await request(app, "/auth-required");
|
|
248
|
+
const body = await res.json();
|
|
249
|
+
expect(body.githubOAuth).toBe(false);
|
|
250
|
+
expect(errSpy).toHaveBeenCalled();
|
|
251
|
+
errSpy.mockRestore();
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
describe("GET /oauth/github/authorize", () => {
|
|
255
|
+
it("returns 404 when GitHub OAuth not configured", async () => {
|
|
256
|
+
const app = createAdminRoutes(mockDb, mockSessions, mockSessions, makeConfig());
|
|
257
|
+
const res = await request(app, "/oauth/github/authorize");
|
|
258
|
+
expect(res.status).toBe(404);
|
|
259
|
+
});
|
|
260
|
+
it("returns 404 when id+secret set but allowed org is missing", async () => {
|
|
261
|
+
const errSpy = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
262
|
+
const app = createAdminRoutes(mockDb, mockSessions, mockSessions, makeConfig({
|
|
263
|
+
githubOAuthClientId: "GH_CLIENT",
|
|
264
|
+
githubOAuthClientSecret: "GH_SECRET",
|
|
265
|
+
}));
|
|
266
|
+
const res = await request(app, "/oauth/github/authorize");
|
|
267
|
+
expect(res.status).toBe(404);
|
|
268
|
+
errSpy.mockRestore();
|
|
269
|
+
});
|
|
270
|
+
it("redirects to GitHub when configured with an allowed org", async () => {
|
|
271
|
+
const app = createAdminRoutes(mockDb, mockSessions, mockSessions, makeConfig({
|
|
272
|
+
githubOAuthClientId: "GH_CLIENT",
|
|
273
|
+
githubOAuthClientSecret: "GH_SECRET",
|
|
274
|
+
githubOAuthRedirectUri: "http://localhost/callback",
|
|
275
|
+
githubAllowedOrg: "acme",
|
|
276
|
+
}));
|
|
277
|
+
const res = await request(app, "/oauth/github/authorize");
|
|
278
|
+
expect(res.status).toBe(302);
|
|
279
|
+
const location = res.headers.get("location") ?? "";
|
|
280
|
+
expect(location).toContain("github.com");
|
|
281
|
+
});
|
|
282
|
+
it("sets github_oauth_state cookie when configured", async () => {
|
|
283
|
+
const app = createAdminRoutes(mockDb, mockSessions, mockSessions, makeConfig({
|
|
284
|
+
githubOAuthClientId: "GH_CLIENT",
|
|
285
|
+
githubOAuthClientSecret: "GH_SECRET",
|
|
286
|
+
githubOAuthRedirectUri: "http://localhost/callback",
|
|
287
|
+
githubAllowedOrg: "acme",
|
|
288
|
+
}));
|
|
289
|
+
const res = await request(app, "/oauth/github/authorize");
|
|
290
|
+
const setCookie = res.headers.get("set-cookie") ?? "";
|
|
291
|
+
expect(setCookie).toContain("github_oauth_state=");
|
|
292
|
+
expect(setCookie).toContain("HttpOnly");
|
|
293
|
+
expect(setCookie).toContain("SameSite=Lax");
|
|
294
|
+
expect(setCookie).toContain("Path=/");
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
/** Helper: mock global.fetch routing /user and /orgs/... to different responses */
|
|
298
|
+
function mockGithubFetch({ userLogin, orgStatus }) {
|
|
299
|
+
return vi.fn(async (url) => {
|
|
300
|
+
const urlStr = typeof url === "string" ? url : url instanceof URL ? url.href : url.url;
|
|
301
|
+
if (urlStr.includes("/orgs/")) {
|
|
302
|
+
return new Response(null, { status: orgStatus ?? 204 });
|
|
303
|
+
}
|
|
304
|
+
// Default: /user
|
|
305
|
+
return new Response(JSON.stringify({ login: userLogin }), { headers: { "Content-Type": "application/json" } });
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
describe("GET /oauth/github/callback", () => {
|
|
309
|
+
it("returns 404 when GitHub OAuth not configured", async () => {
|
|
310
|
+
const app = createAdminRoutes(mockDb, mockSessions, mockSessions, makeConfig());
|
|
311
|
+
const res = await request(app, "/oauth/github/callback?code=abc&state=xyz");
|
|
312
|
+
expect(res.status).toBe(404);
|
|
313
|
+
});
|
|
314
|
+
it("returns 400 when state is missing or mismatched", async () => {
|
|
315
|
+
const app = createAdminRoutes(mockDb, mockSessions, mockSessions, makeConfig({
|
|
316
|
+
githubOAuthClientId: "GH_CLIENT",
|
|
317
|
+
githubOAuthClientSecret: "GH_SECRET",
|
|
318
|
+
githubOAuthRedirectUri: "http://localhost/callback",
|
|
319
|
+
githubAllowedOrg: "acme",
|
|
320
|
+
}));
|
|
321
|
+
// No cookie set → state mismatch
|
|
322
|
+
const res = await request(app, "/oauth/github/callback?code=abc&state=bad-state");
|
|
323
|
+
expect(res.status).toBe(302);
|
|
324
|
+
expect(res.headers.get("location")).toBe("/admin/?error=oauth_state");
|
|
325
|
+
});
|
|
326
|
+
it("redirects to /admin/?error=oauth_code when code is missing", async () => {
|
|
327
|
+
const app = createAdminRoutes(mockDb, mockSessions, mockSessions, makeConfig({
|
|
328
|
+
githubOAuthClientId: "GH_CLIENT",
|
|
329
|
+
githubOAuthClientSecret: "GH_SECRET",
|
|
330
|
+
githubAllowedOrg: "acme",
|
|
331
|
+
}));
|
|
332
|
+
const state = "teststate000";
|
|
333
|
+
const req = new Request(`http://localhost/oauth/github/callback?state=${state}`, {
|
|
334
|
+
headers: { Cookie: `github_oauth_state=${state}` },
|
|
335
|
+
});
|
|
336
|
+
const res = await app.fetch(req);
|
|
337
|
+
expect(res.status).toBe(302);
|
|
338
|
+
expect(res.headers.get("location")).toBe("/admin/?error=oauth_code");
|
|
339
|
+
});
|
|
340
|
+
it("redirects with token and skips org fetch when allowlist is \"*\"", async () => {
|
|
341
|
+
const originalFetch = global.fetch;
|
|
342
|
+
const fetchMock = mockGithubFetch({ userLogin: "alice" });
|
|
343
|
+
global.fetch = fetchMock;
|
|
344
|
+
const app = createAdminRoutes(mockDb, mockSessions, mockSessions, makeConfig({
|
|
345
|
+
githubOAuthClientId: "GH_CLIENT",
|
|
346
|
+
githubOAuthClientSecret: "GH_SECRET",
|
|
347
|
+
githubOAuthRedirectUri: "http://localhost/callback",
|
|
348
|
+
githubAllowedOrg: "*",
|
|
349
|
+
}));
|
|
350
|
+
const state = "teststate111";
|
|
351
|
+
const req = new Request(`http://localhost/oauth/github/callback?code=abc&state=${state}`, {
|
|
352
|
+
headers: { Cookie: `github_oauth_state=${state}` },
|
|
353
|
+
});
|
|
354
|
+
const res = await app.fetch(req);
|
|
355
|
+
expect(res.status).toBe(302);
|
|
356
|
+
expect(res.headers.get("location")).toContain("/admin/?token=");
|
|
357
|
+
// Must not have called the /orgs/ membership endpoint under "*"
|
|
358
|
+
const calls = fetchMock.mock.calls.map((c) => {
|
|
359
|
+
const u = c[0];
|
|
360
|
+
return typeof u === "string" ? u : u instanceof URL ? u.href : u.url;
|
|
361
|
+
});
|
|
362
|
+
expect(calls.some((u) => u.includes("/orgs/"))).toBe(false);
|
|
363
|
+
global.fetch = originalFetch;
|
|
364
|
+
});
|
|
365
|
+
it("redirects with token when org membership returns 204", async () => {
|
|
366
|
+
const originalFetch = global.fetch;
|
|
367
|
+
global.fetch = mockGithubFetch({ userLogin: "alice", orgStatus: 204 });
|
|
368
|
+
const app = createAdminRoutes(mockDb, mockSessions, mockSessions, makeConfig({
|
|
369
|
+
githubOAuthClientId: "GH_CLIENT",
|
|
370
|
+
githubOAuthClientSecret: "GH_SECRET",
|
|
371
|
+
githubOAuthRedirectUri: "http://localhost/callback",
|
|
372
|
+
githubAllowedOrg: "acme",
|
|
373
|
+
}));
|
|
374
|
+
const state = "teststate222";
|
|
375
|
+
const req = new Request(`http://localhost/oauth/github/callback?code=abc&state=${state}`, {
|
|
376
|
+
headers: { Cookie: `github_oauth_state=${state}` },
|
|
377
|
+
});
|
|
378
|
+
const res = await app.fetch(req);
|
|
379
|
+
expect(res.status).toBe(302);
|
|
380
|
+
expect(res.headers.get("location")).toContain("/admin/?token=");
|
|
381
|
+
global.fetch = originalFetch;
|
|
382
|
+
});
|
|
383
|
+
it("redirects to /admin/?error=github_org when membership returns 404 (not a member)", async () => {
|
|
384
|
+
const originalFetch = global.fetch;
|
|
385
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
386
|
+
global.fetch = mockGithubFetch({ userLogin: "alice", orgStatus: 404 });
|
|
387
|
+
const app = createAdminRoutes(mockDb, mockSessions, mockSessions, makeConfig({
|
|
388
|
+
githubOAuthClientId: "GH_CLIENT",
|
|
389
|
+
githubOAuthClientSecret: "GH_SECRET",
|
|
390
|
+
githubOAuthRedirectUri: "http://localhost/callback",
|
|
391
|
+
githubAllowedOrg: "acme",
|
|
392
|
+
}));
|
|
393
|
+
const state = "teststate333";
|
|
394
|
+
const req = new Request(`http://localhost/oauth/github/callback?code=abc&state=${state}`, {
|
|
395
|
+
headers: { Cookie: `github_oauth_state=${state}` },
|
|
396
|
+
});
|
|
397
|
+
const res = await app.fetch(req);
|
|
398
|
+
expect(res.status).toBe(302);
|
|
399
|
+
expect(res.headers.get("location")).toBe("/admin/?error=github_org");
|
|
400
|
+
global.fetch = originalFetch;
|
|
401
|
+
warnSpy.mockRestore();
|
|
402
|
+
});
|
|
403
|
+
it("redirects to /admin/?error=github_org when membership returns 302 (insufficient visibility)", async () => {
|
|
404
|
+
const originalFetch = global.fetch;
|
|
405
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
406
|
+
global.fetch = mockGithubFetch({ userLogin: "alice", orgStatus: 302 });
|
|
407
|
+
const app = createAdminRoutes(mockDb, mockSessions, mockSessions, makeConfig({
|
|
408
|
+
githubOAuthClientId: "GH_CLIENT",
|
|
409
|
+
githubOAuthClientSecret: "GH_SECRET",
|
|
410
|
+
githubOAuthRedirectUri: "http://localhost/callback",
|
|
411
|
+
githubAllowedOrg: "acme",
|
|
412
|
+
}));
|
|
413
|
+
const state = "teststate444";
|
|
414
|
+
const req = new Request(`http://localhost/oauth/github/callback?code=abc&state=${state}`, {
|
|
415
|
+
headers: { Cookie: `github_oauth_state=${state}` },
|
|
416
|
+
});
|
|
417
|
+
const res = await app.fetch(req);
|
|
418
|
+
expect(res.status).toBe(302);
|
|
419
|
+
expect(res.headers.get("location")).toBe("/admin/?error=github_org");
|
|
420
|
+
global.fetch = originalFetch;
|
|
421
|
+
warnSpy.mockRestore();
|
|
422
|
+
});
|
|
423
|
+
});
|
|
185
424
|
describe("POST /login (password)", () => {
|
|
186
425
|
it("still works with correct password", async () => {
|
|
187
426
|
const app = createAdminRoutes(mockDb, mockSessions, mockSessions, makeConfig({
|
|
@@ -208,4 +447,431 @@ describe("POST /login (password)", () => {
|
|
|
208
447
|
expect(res.status).toBe(401);
|
|
209
448
|
});
|
|
210
449
|
});
|
|
450
|
+
describe("POST /workflow-runs/:id/cancel", () => {
|
|
451
|
+
// Helper to make a cancel-test db that returns a single run and
|
|
452
|
+
// records calls to the mutating methods we care about.
|
|
453
|
+
function makeCancelDb(opts) {
|
|
454
|
+
const finishes = [];
|
|
455
|
+
const cancels = [];
|
|
456
|
+
const db = {
|
|
457
|
+
...mockDb,
|
|
458
|
+
runs: {
|
|
459
|
+
...(mockDb.runs),
|
|
460
|
+
getRun: vi.fn(() => opts.run.status === "cancelled" ? null : {
|
|
461
|
+
id: opts.run.id,
|
|
462
|
+
workflowName: "pr-review",
|
|
463
|
+
triggerId: opts.run.triggerId,
|
|
464
|
+
currentPhase: "review",
|
|
465
|
+
phaseHistory: [],
|
|
466
|
+
status: opts.run.status,
|
|
467
|
+
context: opts.run.taskId ? { taskId: opts.run.taskId } : {},
|
|
468
|
+
startedAt: "",
|
|
469
|
+
updatedAt: "",
|
|
470
|
+
}),
|
|
471
|
+
cancelRun: vi.fn((id) => { cancels.push(id); }),
|
|
472
|
+
},
|
|
473
|
+
executions: {
|
|
474
|
+
...(mockDb.executions),
|
|
475
|
+
runningExecutions: vi.fn(() => opts.runningExecutions ?? []),
|
|
476
|
+
recordFinish: vi.fn((id, res) => { finishes.push({ id, error: res.error }); }),
|
|
477
|
+
},
|
|
478
|
+
};
|
|
479
|
+
return { db, finishes, cancels };
|
|
480
|
+
}
|
|
481
|
+
it("returns 404 when run not found", async () => {
|
|
482
|
+
const db = {
|
|
483
|
+
...mockDb,
|
|
484
|
+
runs: {
|
|
485
|
+
...(mockDb.runs),
|
|
486
|
+
getRun: vi.fn(() => null),
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
const app = createAdminRoutes(db, mockSessions, mockSessions, makeConfig({ adminPassword: "" }));
|
|
490
|
+
const res = await request(app, "/workflow-runs/run-abc/cancel", { method: "POST" });
|
|
491
|
+
expect(res.status).toBe(404);
|
|
492
|
+
});
|
|
493
|
+
it("returns 400 when run already in a terminal state", async () => {
|
|
494
|
+
const { db } = makeCancelDb({ run: { id: "r1", status: "succeeded", triggerId: "t1" } });
|
|
495
|
+
const app = createAdminRoutes(db, mockSessions, mockSessions, makeConfig({ adminPassword: "" }));
|
|
496
|
+
const res = await request(app, "/workflow-runs/r1/cancel", { method: "POST" });
|
|
497
|
+
expect(res.status).toBe(400);
|
|
498
|
+
});
|
|
499
|
+
it("cancels and kills containers matching the run's taskId prefix", async () => {
|
|
500
|
+
const dockerMod = await import("./docker.js");
|
|
501
|
+
const listMock = vi.mocked(dockerMod.listRunningContainers);
|
|
502
|
+
const killMock = vi.mocked(dockerMod.killContainer);
|
|
503
|
+
listMock.mockResolvedValueOnce([
|
|
504
|
+
{ id: "c1", name: "lastlight-sandbox-task-xyz-aaaaaaaa", taskId: "task-xyz", status: "running", created: "", image: "" },
|
|
505
|
+
{ id: "c2", name: "lastlight-sandbox-task-xyz-review-bbbbbbbb", taskId: "task-xyz-review", status: "running", created: "", image: "" },
|
|
506
|
+
{ id: "c3", name: "lastlight-sandbox-other-cccccccc", taskId: "other", status: "running", created: "", image: "" },
|
|
507
|
+
]);
|
|
508
|
+
const { db, finishes, cancels } = makeCancelDb({
|
|
509
|
+
run: { id: "r1", status: "running", triggerId: "t1", taskId: "task-xyz" },
|
|
510
|
+
runningExecutions: [
|
|
511
|
+
{ id: "e1", workflowRunId: "r1", triggerId: "t1" },
|
|
512
|
+
{ id: "e2", workflowRunId: "r1", triggerId: "t1" },
|
|
513
|
+
],
|
|
514
|
+
});
|
|
515
|
+
const app = createAdminRoutes(db, mockSessions, mockSessions, makeConfig({ adminPassword: "" }));
|
|
516
|
+
const res = await request(app, "/workflow-runs/r1/cancel", { method: "POST" });
|
|
517
|
+
expect(res.status).toBe(200);
|
|
518
|
+
expect(cancels).toEqual(["r1"]);
|
|
519
|
+
expect(killMock).toHaveBeenCalledTimes(2);
|
|
520
|
+
expect(killMock).toHaveBeenCalledWith("lastlight-sandbox-task-xyz-aaaaaaaa");
|
|
521
|
+
expect(killMock).toHaveBeenCalledWith("lastlight-sandbox-task-xyz-review-bbbbbbbb");
|
|
522
|
+
expect(killMock).not.toHaveBeenCalledWith("lastlight-sandbox-other-cccccccc");
|
|
523
|
+
expect(finishes.map((f) => f.id).sort()).toEqual(["e1", "e2"]);
|
|
524
|
+
expect(finishes.every((f) => f.error === "cancelled via admin dashboard")).toBe(true);
|
|
525
|
+
});
|
|
526
|
+
it("does NOT mark sibling-run executions as failed when cancelling a run with a shared triggerId", async () => {
|
|
527
|
+
// Regression for the PR #38 review: matching by triggerId clobbered
|
|
528
|
+
// execution rows belonging to a concurrent run on the same trigger.
|
|
529
|
+
// Matching by workflowRunId must only finish rows for THIS run.
|
|
530
|
+
const dockerMod = await import("./docker.js");
|
|
531
|
+
vi.mocked(dockerMod.listRunningContainers).mockResolvedValueOnce([]);
|
|
532
|
+
const { db, finishes } = makeCancelDb({
|
|
533
|
+
run: { id: "r1", status: "running", triggerId: "shared-trigger", taskId: "task-r1" },
|
|
534
|
+
runningExecutions: [
|
|
535
|
+
{ id: "e1", workflowRunId: "r1", triggerId: "shared-trigger" },
|
|
536
|
+
{ id: "e2", workflowRunId: "r2", triggerId: "shared-trigger" },
|
|
537
|
+
{ id: "e3", workflowRunId: undefined, triggerId: "shared-trigger" },
|
|
538
|
+
],
|
|
539
|
+
});
|
|
540
|
+
const app = createAdminRoutes(db, mockSessions, mockSessions, makeConfig({ adminPassword: "" }));
|
|
541
|
+
const res = await request(app, "/workflow-runs/r1/cancel", { method: "POST" });
|
|
542
|
+
expect(res.status).toBe(200);
|
|
543
|
+
expect(finishes.map((f) => f.id)).toEqual(["e1"]);
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
describe("GET /workflow-runs/:id/approvals", () => {
|
|
547
|
+
function makeApprovalsDb(run, approvals) {
|
|
548
|
+
const listForWorkflow = vi.fn(() => approvals);
|
|
549
|
+
const db = {
|
|
550
|
+
...mockDb,
|
|
551
|
+
runs: {
|
|
552
|
+
...(mockDb.runs),
|
|
553
|
+
getRun: vi.fn(() => run),
|
|
554
|
+
},
|
|
555
|
+
approvals: {
|
|
556
|
+
...(mockDb.approvals),
|
|
557
|
+
listForWorkflow,
|
|
558
|
+
},
|
|
559
|
+
};
|
|
560
|
+
return { db, listForWorkflow };
|
|
561
|
+
}
|
|
562
|
+
it("returns 404 when the run is not found", async () => {
|
|
563
|
+
const { db } = makeApprovalsDb(null, []);
|
|
564
|
+
const app = createAdminRoutes(db, mockSessions, mockSessions, makeConfig({ adminPassword: "" }));
|
|
565
|
+
const res = await request(app, "/workflow-runs/missing/approvals");
|
|
566
|
+
expect(res.status).toBe(404);
|
|
567
|
+
});
|
|
568
|
+
it("returns every approval (all statuses) for the run, looked up by run id", async () => {
|
|
569
|
+
const run = { id: "run-1", triggerId: "t1", workflowName: "build" };
|
|
570
|
+
const approvals = [
|
|
571
|
+
{ id: "a1", workflowRunId: "run-1", gate: "post_architect", status: "approved", respondedBy: "alice", createdAt: "2026-01-01T00:00:00Z" },
|
|
572
|
+
{ id: "a2", workflowRunId: "run-1", gate: "pre_fix", status: "pending", createdAt: "2026-01-01T01:00:00Z" },
|
|
573
|
+
];
|
|
574
|
+
const { db, listForWorkflow } = makeApprovalsDb(run, approvals);
|
|
575
|
+
const app = createAdminRoutes(db, mockSessions, mockSessions, makeConfig({ adminPassword: "" }));
|
|
576
|
+
const res = await request(app, "/workflow-runs/run-1/approvals");
|
|
577
|
+
expect(res.status).toBe(200);
|
|
578
|
+
const body = await res.json();
|
|
579
|
+
expect(body.approvals).toEqual(approvals);
|
|
580
|
+
expect(listForWorkflow).toHaveBeenCalledWith("run-1");
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
describe("GET /sessions — content filter + liveness", () => {
|
|
584
|
+
const now = Date.now() / 1000;
|
|
585
|
+
const meta = (over) => ({
|
|
586
|
+
id: "x", source: "agent", sessionType: "agent", model: "m",
|
|
587
|
+
started_at: now, last_message_at: now, message_count: 5,
|
|
588
|
+
tool_call_count: 0, conversation_message_count: 5,
|
|
589
|
+
last_assistant_content: null, agentIds: [], ...over,
|
|
590
|
+
});
|
|
591
|
+
function appWith(metas) {
|
|
592
|
+
const sessions = {
|
|
593
|
+
...mockSessions,
|
|
594
|
+
listSessionIds: vi.fn(() => metas.map((m) => m.id)),
|
|
595
|
+
getSessionMeta: vi.fn(async (id) => metas.find((m) => m.id === id) ?? null),
|
|
596
|
+
};
|
|
597
|
+
return createAdminRoutes(mockDb, sessions, mockSessions, makeConfig({ adminPassword: "" }));
|
|
598
|
+
}
|
|
599
|
+
it("omits zero-message sessions (empty/aborted runs left behind)", async () => {
|
|
600
|
+
const dockerMod = await import("./docker.js");
|
|
601
|
+
vi.mocked(dockerMod.listRunningContainers).mockResolvedValueOnce([]);
|
|
602
|
+
const app = appWith([
|
|
603
|
+
meta({ id: "real", message_count: 3 }),
|
|
604
|
+
meta({ id: "exec-drizzle-553-pr-review-e34282db", message_count: 0 }),
|
|
605
|
+
]);
|
|
606
|
+
const res = await request(app, "/sessions");
|
|
607
|
+
const body = await res.json();
|
|
608
|
+
expect(body.sessions.map((s) => s.id)).toEqual(["real"]);
|
|
609
|
+
});
|
|
610
|
+
it("marks an exec-<taskId> session live only when its container is running", async () => {
|
|
611
|
+
const dockerMod = await import("./docker.js");
|
|
612
|
+
vi.mocked(dockerMod.listRunningContainers).mockResolvedValueOnce([
|
|
613
|
+
{ id: "c1", name: "lastlight-sandbox-task-xyz-aaaaaaaa", taskId: "task-xyz", status: "running", created: "", image: "" },
|
|
614
|
+
]);
|
|
615
|
+
const app = appWith([
|
|
616
|
+
meta({ id: "exec-task-xyz", message_count: 2 }), // matches running container
|
|
617
|
+
meta({ id: "exec-task-other", message_count: 2 }), // no matching container
|
|
618
|
+
]);
|
|
619
|
+
const res = await request(app, "/sessions");
|
|
620
|
+
const body = await res.json();
|
|
621
|
+
const byId = Object.fromEntries(body.sessions.map((s) => [s.id, s.live]));
|
|
622
|
+
expect(byId["exec-task-xyz"]).toBe(true);
|
|
623
|
+
expect(byId["exec-task-other"]).toBe(false);
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
describe("POST /approvals/:id/respond", () => {
|
|
627
|
+
// Build a db whose approval/run sub-stores record the lifecycle mutations
|
|
628
|
+
// the approval route can issue, so a test can assert which ones fired.
|
|
629
|
+
function makeApprovalDb(over = {}) {
|
|
630
|
+
const calls = {
|
|
631
|
+
respond: [],
|
|
632
|
+
resolveGateAndResume: [],
|
|
633
|
+
resolveGateAndFail: [],
|
|
634
|
+
};
|
|
635
|
+
const approval = "approval" in over ? over.approval : { id: "appr-1", status: "pending", workflowRunId: "run-1" };
|
|
636
|
+
const run = "run" in over ? over.run : { id: "run-1", workflowName: "build", triggerId: "o/r#3", issueNumber: 3 };
|
|
637
|
+
// respond() returns the compare-and-set row count; default 1 (won the CAS).
|
|
638
|
+
const respondChanges = over.respondChanges ?? 1;
|
|
639
|
+
const db = {
|
|
640
|
+
...mockDb,
|
|
641
|
+
approvals: {
|
|
642
|
+
...(mockDb.approvals),
|
|
643
|
+
getById: vi.fn(() => approval),
|
|
644
|
+
respond: vi.fn((...args) => { calls.respond.push(args); return respondChanges; }),
|
|
645
|
+
},
|
|
646
|
+
runs: {
|
|
647
|
+
...(mockDb.runs),
|
|
648
|
+
getRun: vi.fn(() => run),
|
|
649
|
+
resolveGateAndResume: vi.fn((...args) => { calls.resolveGateAndResume.push(args); return run; }),
|
|
650
|
+
resolveGateAndFail: vi.fn((...args) => { calls.resolveGateAndFail.push(args); return run; }),
|
|
651
|
+
},
|
|
652
|
+
};
|
|
653
|
+
return { db, calls };
|
|
654
|
+
}
|
|
655
|
+
async function respond(db, id, payload, cfg = {}) {
|
|
656
|
+
const app = createAdminRoutes(db, mockSessions, mockSessions, makeConfig({ adminPassword: "", ...cfg }));
|
|
657
|
+
return request(app, `/approvals/${id}/respond`, {
|
|
658
|
+
method: "POST",
|
|
659
|
+
headers: { "Content-Type": "application/json" },
|
|
660
|
+
body: JSON.stringify(payload),
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
it("404s when the approval is missing", async () => {
|
|
664
|
+
const { db } = makeApprovalDb({ approval: null });
|
|
665
|
+
const res = await respond(db, "nope", { decision: "approved" });
|
|
666
|
+
expect(res.status).toBe(404);
|
|
667
|
+
});
|
|
668
|
+
it("400s when the approval is already resolved", async () => {
|
|
669
|
+
const { db } = makeApprovalDb({ approval: { id: "appr-1", status: "approved", workflowRunId: "run-1" } });
|
|
670
|
+
const res = await respond(db, "appr-1", { decision: "approved" });
|
|
671
|
+
expect(res.status).toBe(400);
|
|
672
|
+
});
|
|
673
|
+
it("approves and dispatches a resume when resumeWorkflow is wired", async () => {
|
|
674
|
+
const { db, calls } = makeApprovalDb();
|
|
675
|
+
const resumeWorkflow = vi.fn(async () => { });
|
|
676
|
+
const res = await respond(db, "appr-1", { decision: "approved" }, { resumeWorkflow });
|
|
677
|
+
expect(res.status).toBe(200);
|
|
678
|
+
// The approval is recorded and the resume helper is handed the run...
|
|
679
|
+
expect(calls.respond).toEqual([["appr-1", "approved", "admin", undefined]]);
|
|
680
|
+
expect(resumeWorkflow).toHaveBeenCalledWith(expect.objectContaining({ id: "run-1" }), "admin");
|
|
681
|
+
// ...and the route does NOT pre-flip the run to running via the atomic op —
|
|
682
|
+
// resumeWorkflow owns the status flip so it only happens with a dispatch.
|
|
683
|
+
expect(calls.resolveGateAndResume).toEqual([]);
|
|
684
|
+
});
|
|
685
|
+
it("records the approval WITHOUT flipping the run to running when no resume can be dispatched", async () => {
|
|
686
|
+
// Regression (PR #105 review): the atomic approve+resume flipped the run to
|
|
687
|
+
// `running` even when no worker would be dispatched (no resumeWorkflow
|
|
688
|
+
// wired, App down, or a non-issue trigger), orphaning the row. The approval
|
|
689
|
+
// must be recorded, but the run must NOT become `running`.
|
|
690
|
+
const { db, calls } = makeApprovalDb();
|
|
691
|
+
const res = await respond(db, "appr-1", { decision: "approved" }); // no resumeWorkflow
|
|
692
|
+
expect(res.status).toBe(200);
|
|
693
|
+
expect(calls.respond).toEqual([["appr-1", "approved", "admin", undefined]]);
|
|
694
|
+
expect(calls.resolveGateAndResume).toEqual([]);
|
|
695
|
+
});
|
|
696
|
+
it("409s and does NOT resume when the approval CAS loses a race", async () => {
|
|
697
|
+
// The status check above is a TOCTOU read; if a concurrent responder wins,
|
|
698
|
+
// respond() changes 0 rows. The loser must 409 and never hand the run to
|
|
699
|
+
// resumeWorkflow for a second dispatch.
|
|
700
|
+
const { db, calls } = makeApprovalDb({ respondChanges: 0 });
|
|
701
|
+
const resumeWorkflow = vi.fn(async () => { });
|
|
702
|
+
const res = await respond(db, "appr-1", { decision: "approved" }, { resumeWorkflow });
|
|
703
|
+
expect(res.status).toBe(409);
|
|
704
|
+
expect(resumeWorkflow).not.toHaveBeenCalled();
|
|
705
|
+
expect(calls.resolveGateAndResume).toEqual([]);
|
|
706
|
+
});
|
|
707
|
+
it("rejects by failing the run atomically", async () => {
|
|
708
|
+
const { db, calls } = makeApprovalDb();
|
|
709
|
+
const res = await respond(db, "appr-1", { decision: "rejected", reason: "too risky" });
|
|
710
|
+
expect(res.status).toBe(200);
|
|
711
|
+
expect(calls.resolveGateAndFail).toEqual([["appr-1", "admin", "too risky"]]);
|
|
712
|
+
expect(calls.respond).toEqual([]);
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
describe("GET /approvals/:id (focused approval enrichment)", () => {
|
|
716
|
+
function makeDb(approval, run) {
|
|
717
|
+
return {
|
|
718
|
+
...mockDb,
|
|
719
|
+
approvals: {
|
|
720
|
+
...(mockDb.approvals),
|
|
721
|
+
getById: vi.fn(() => approval),
|
|
722
|
+
},
|
|
723
|
+
runs: {
|
|
724
|
+
...(mockDb.runs),
|
|
725
|
+
getRun: vi.fn(() => run),
|
|
726
|
+
},
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
async function getApproval(db, id, cfg = {}) {
|
|
730
|
+
const app = createAdminRoutes(db, mockSessions, mockSessions, makeConfig({ adminPassword: "", ...cfg }));
|
|
731
|
+
return request(app, `/approvals/${id}`);
|
|
732
|
+
}
|
|
733
|
+
// Production shape: workflow_runs.repo is the BARE name; owner lives in
|
|
734
|
+
// context (matching src/workflows/simple.ts). The Slack-invoked build flow
|
|
735
|
+
// uses the same createRun path, so this also covers it.
|
|
736
|
+
const run = {
|
|
737
|
+
id: "run-1",
|
|
738
|
+
workflowName: "build",
|
|
739
|
+
triggerId: "acme/widget#3",
|
|
740
|
+
repo: "widget",
|
|
741
|
+
issueNumber: 3,
|
|
742
|
+
context: { owner: "acme", branch: "lastlight/issue-3", issueDir: ".lastlight/issue-3" },
|
|
743
|
+
};
|
|
744
|
+
it("404s when the approval is missing", async () => {
|
|
745
|
+
const res = await getApproval(makeDb(null, run), "nope");
|
|
746
|
+
expect(res.status).toBe(404);
|
|
747
|
+
});
|
|
748
|
+
it("returns a server-mode artifactRef (editable store doc)", async () => {
|
|
749
|
+
const approval = { id: "a1", status: "pending", workflowRunId: "run-1", artifact: "architect-plan.md" };
|
|
750
|
+
const res = await getApproval(makeDb(approval, run), "a1", { buildAssets: "server" });
|
|
751
|
+
const body = await res.json();
|
|
752
|
+
expect(res.status).toBe(200);
|
|
753
|
+
expect(body.artifactRef).toMatchObject({
|
|
754
|
+
mode: "server",
|
|
755
|
+
owner: "acme",
|
|
756
|
+
repo: "widget",
|
|
757
|
+
issueKey: "issue-3",
|
|
758
|
+
doc: "architect-plan.md",
|
|
759
|
+
});
|
|
760
|
+
expect(body.artifactRef.githubUrl).toBeUndefined();
|
|
761
|
+
});
|
|
762
|
+
it("returns a repo-mode artifactRef with a GitHub blob URL", async () => {
|
|
763
|
+
const approval = { id: "a1", status: "pending", workflowRunId: "run-1", artifact: "architect-plan.md" };
|
|
764
|
+
const res = await getApproval(makeDb(approval, run), "a1", { buildAssets: "repo" });
|
|
765
|
+
const body = await res.json();
|
|
766
|
+
expect(res.status).toBe(200);
|
|
767
|
+
expect(body.artifactRef.mode).toBe("repo");
|
|
768
|
+
expect(body.artifactRef.githubUrl).toBe("https://github.com/acme/widget/blob/lastlight%2Fissue-3/.lastlight/issue-3/architect-plan.md");
|
|
769
|
+
});
|
|
770
|
+
it("returns artifactRef: null when the gate carries no artifact", async () => {
|
|
771
|
+
const approval = { id: "a1", status: "pending", workflowRunId: "run-1" };
|
|
772
|
+
const res = await getApproval(makeDb(approval, run), "a1", { buildAssets: "server" });
|
|
773
|
+
const body = await res.json();
|
|
774
|
+
expect(res.status).toBe(200);
|
|
775
|
+
expect(body.artifactRef).toBeNull();
|
|
776
|
+
});
|
|
777
|
+
});
|
|
778
|
+
describe("artifacts endpoints (server-mode build assets)", () => {
|
|
779
|
+
let dir;
|
|
780
|
+
// Auth disabled (empty password) so the round-trip test doesn't need a token.
|
|
781
|
+
const app = () => createAdminRoutes(mockDb, mockSessions, mockSessions, makeConfig({ adminPassword: "", buildAssetsDir: dir }));
|
|
782
|
+
beforeEach(() => { dir = mkdtempSync(join(tmpdir(), "ll-routes-assets-")); });
|
|
783
|
+
afterEach(() => { rmSync(dir, { recursive: true, force: true }); });
|
|
784
|
+
it("PUT then GET round-trips a doc and lists keys/files", async () => {
|
|
785
|
+
const a = app();
|
|
786
|
+
const put = await request(a, "/artifacts/acme/widget/issue-42/architect-plan.md", {
|
|
787
|
+
method: "PUT",
|
|
788
|
+
headers: { "Content-Type": "text/plain" },
|
|
789
|
+
body: "# Plan\n",
|
|
790
|
+
});
|
|
791
|
+
expect(put.status).toBe(200);
|
|
792
|
+
expect(await put.json()).toEqual({ ok: true });
|
|
793
|
+
const keys = await request(a, "/artifacts?repo=acme/widget");
|
|
794
|
+
expect(await keys.json()).toEqual({ keys: ["issue-42"] });
|
|
795
|
+
const files = await request(a, "/artifacts/acme/widget/issue-42");
|
|
796
|
+
expect(await files.json()).toEqual({ files: ["architect-plan.md"] });
|
|
797
|
+
const doc = await request(a, "/artifacts/acme/widget/issue-42/architect-plan.md");
|
|
798
|
+
expect(doc.status).toBe(200);
|
|
799
|
+
expect(await doc.text()).toBe("# Plan\n");
|
|
800
|
+
});
|
|
801
|
+
it("400s on a traversal attempt in the doc name", async () => {
|
|
802
|
+
const res = await request(app(), "/artifacts/acme/widget/issue-42/..%2f..%2fsecret", {
|
|
803
|
+
method: "PUT",
|
|
804
|
+
body: "x",
|
|
805
|
+
});
|
|
806
|
+
expect(res.status).toBe(400);
|
|
807
|
+
});
|
|
808
|
+
it("404 on an unknown doc; empty lists for an unknown repo", async () => {
|
|
809
|
+
const a = app();
|
|
810
|
+
const missing = await request(a, "/artifacts/acme/widget/issue-1/nope.md");
|
|
811
|
+
expect(missing.status).toBe(404);
|
|
812
|
+
const keys = await request(a, "/artifacts?repo=nobody/nothing");
|
|
813
|
+
expect(await keys.json()).toEqual({ keys: [] });
|
|
814
|
+
});
|
|
815
|
+
it("reports empty when no store dir is configured", async () => {
|
|
816
|
+
const a = createAdminRoutes(mockDb, mockSessions, mockSessions, makeConfig({ adminPassword: "" }));
|
|
817
|
+
const keys = await request(a, "/artifacts?repo=acme/widget");
|
|
818
|
+
expect(await keys.json()).toEqual({ keys: [] });
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
describe("public artifact image route (mountAdmin carve-out)", () => {
|
|
822
|
+
let dir;
|
|
823
|
+
// Auth ENABLED — the public image route must still be reachable without a
|
|
824
|
+
// token (it's registered on the parent app, before the auth-guarded
|
|
825
|
+
// /admin/api sub-app), while the auth-gated route is not.
|
|
826
|
+
const mount = (buildAssetsDir) => {
|
|
827
|
+
const app = new Hono();
|
|
828
|
+
mountAdmin(app, mockDb, makeConfig({ adminPassword: "secret", buildAssetsDir }));
|
|
829
|
+
return app;
|
|
830
|
+
};
|
|
831
|
+
const get = (app, path) => app.fetch(new Request(`http://localhost${path}`));
|
|
832
|
+
beforeEach(() => {
|
|
833
|
+
dir = mkdtempSync(join(tmpdir(), "ll-public-assets-"));
|
|
834
|
+
new BuildAssetStore(dir).write({ owner: "acme", repo: "widget", issueKey: "issue-1" }, "home.png", "PNG-BYTES");
|
|
835
|
+
new BuildAssetStore(dir).write({ owner: "acme", repo: "widget", issueKey: "issue-1" }, "plan.md", "# secret plan\n");
|
|
836
|
+
});
|
|
837
|
+
afterEach(() => rmSync(dir, { recursive: true, force: true }));
|
|
838
|
+
it("serves a PNG as image/png with NO auth token", async () => {
|
|
839
|
+
const res = await get(mount(dir), "/admin/api/public/artifacts/acme/widget/issue-1/home.png");
|
|
840
|
+
expect(res.status).toBe(200);
|
|
841
|
+
expect(res.headers.get("Content-Type")).toBe("image/png");
|
|
842
|
+
expect(await res.text()).toBe("PNG-BYTES");
|
|
843
|
+
});
|
|
844
|
+
it("serves an mp4 as video/mp4 with Range support and NO auth token", async () => {
|
|
845
|
+
new BuildAssetStore(dir).write({ owner: "acme", repo: "widget", issueKey: "issue-1" }, "demo.mp4", "MP4-BYTES-0123456789");
|
|
846
|
+
const app = mount(dir);
|
|
847
|
+
const full = await get(app, "/admin/api/public/artifacts/acme/widget/issue-1/demo.mp4");
|
|
848
|
+
expect(full.status).toBe(200);
|
|
849
|
+
expect(full.headers.get("Content-Type")).toBe("video/mp4");
|
|
850
|
+
expect(full.headers.get("Accept-Ranges")).toBe("bytes");
|
|
851
|
+
// A Range request (what a <video> element / GitHub player sends) must
|
|
852
|
+
// return 206 Partial Content with a Content-Range so seeking works.
|
|
853
|
+
const ranged = await app.fetch(new Request("http://localhost/admin/api/public/artifacts/acme/widget/issue-1/demo.mp4", {
|
|
854
|
+
headers: { Range: "bytes=0-3" },
|
|
855
|
+
}));
|
|
856
|
+
expect(ranged.status).toBe(206);
|
|
857
|
+
expect(ranged.headers.get("Content-Range")).toBe("bytes 0-3/20");
|
|
858
|
+
expect(await ranged.text()).toBe("MP4-");
|
|
859
|
+
});
|
|
860
|
+
it("404s on a non-media doc (media-only gate keeps text docs private)", async () => {
|
|
861
|
+
const res = await get(mount(dir), "/admin/api/public/artifacts/acme/widget/issue-1/plan.md");
|
|
862
|
+
expect(res.status).toBe(404);
|
|
863
|
+
});
|
|
864
|
+
it("404s for a missing image", async () => {
|
|
865
|
+
const res = await get(mount(dir), "/admin/api/public/artifacts/acme/widget/issue-1/nope.png");
|
|
866
|
+
expect(res.status).toBe(404);
|
|
867
|
+
});
|
|
868
|
+
it("404s when the build-assets store is not configured", async () => {
|
|
869
|
+
const res = await get(mount(undefined), "/admin/api/public/artifacts/acme/widget/issue-1/home.png");
|
|
870
|
+
expect(res.status).toBe(404);
|
|
871
|
+
});
|
|
872
|
+
it("the auth-gated artifacts route still requires a token (contrast)", async () => {
|
|
873
|
+
const res = await get(mount(dir), "/admin/api/artifacts/acme/widget/issue-1/plan.md");
|
|
874
|
+
expect(res.status).toBe(401);
|
|
875
|
+
});
|
|
876
|
+
});
|
|
211
877
|
//# sourceMappingURL=routes.test.js.map
|