smol-symphony 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +41 -22
- package/DESIGN.md +494 -273
- package/README.md +109 -57
- package/SPEC.md +33 -24
- package/WORKFLOW.minimal.yaml +34 -0
- package/{WORKFLOW.template.md → WORKFLOW.template.yaml} +409 -256
- package/WORKFLOW.yaml +487 -0
- package/assets/skills/symphony-issues/SKILL.md +136 -0
- package/assets/symphony-mise.system.toml +68 -0
- package/dist/src/bin/symphony.js +30 -0
- package/dist/src/bin/symphony.js.map +1 -0
- package/dist/src/core/actions/context.js +109 -0
- package/dist/src/core/actions/context.js.map +1 -0
- package/dist/{actions/parsing.js → src/core/actions/parse.js} +33 -114
- package/dist/src/core/actions/parse.js.map +1 -0
- package/dist/src/core/actions/plan.js +197 -0
- package/dist/src/core/actions/plan.js.map +1 -0
- package/dist/src/core/actions/predicates.js +111 -0
- package/dist/src/core/actions/predicates.js.map +1 -0
- package/dist/src/core/actions/run-fold.js +248 -0
- package/dist/src/core/actions/run-fold.js.map +1 -0
- package/dist/src/core/actions/template.js +118 -0
- package/dist/src/core/actions/template.js.map +1 -0
- package/dist/src/core/cli/args.js +116 -0
- package/dist/src/core/cli/args.js.map +1 -0
- package/dist/src/core/coerce.js +75 -0
- package/dist/src/core/coerce.js.map +1 -0
- package/dist/src/core/credential/account-id.js +20 -0
- package/dist/src/core/credential/account-id.js.map +1 -0
- package/dist/src/core/credential/adapter-config.js +136 -0
- package/dist/src/core/credential/adapter-config.js.map +1 -0
- package/dist/src/core/credential/availability.js +98 -0
- package/dist/src/core/credential/availability.js.map +1 -0
- package/dist/src/core/credential/extract.js +228 -0
- package/dist/src/core/credential/extract.js.map +1 -0
- package/dist/src/core/credential/fake-creds.js +171 -0
- package/dist/src/core/credential/fake-creds.js.map +1 -0
- package/dist/src/core/credential/identity.js +125 -0
- package/dist/src/core/credential/identity.js.map +1 -0
- package/dist/src/core/credential/shape.js +230 -0
- package/dist/src/core/credential/shape.js.map +1 -0
- package/dist/src/core/credential/strings.js +15 -0
- package/dist/src/core/credential/strings.js.map +1 -0
- package/dist/src/core/doctor/checks.js +303 -0
- package/dist/src/core/doctor/checks.js.map +1 -0
- package/dist/src/core/git/result.js +107 -0
- package/dist/src/core/git/result.js.map +1 -0
- package/dist/src/core/http/decisions.js +225 -0
- package/dist/src/core/http/decisions.js.map +1 -0
- package/dist/{http.js → src/core/http/render.js} +472 -738
- package/dist/src/core/http/render.js.map +1 -0
- package/dist/{http-handlers.js → src/core/http/routes.js} +52 -87
- package/dist/src/core/http/routes.js.map +1 -0
- package/dist/src/core/http/views.js +181 -0
- package/dist/src/core/http/views.js.map +1 -0
- package/dist/src/core/image/managed-image.js +95 -0
- package/dist/src/core/image/managed-image.js.map +1 -0
- package/dist/src/core/issue/file.js +149 -0
- package/dist/src/core/issue/file.js.map +1 -0
- package/dist/src/core/issue/parse.js +210 -0
- package/dist/src/core/issue/parse.js.map +1 -0
- package/dist/src/core/mcp/dispatch.js +239 -0
- package/dist/src/core/mcp/dispatch.js.map +1 -0
- package/dist/src/core/mcp/post-move.js +92 -0
- package/dist/src/core/mcp/post-move.js.map +1 -0
- package/dist/src/core/mcp/protocol.js +293 -0
- package/dist/src/core/mcp/protocol.js.map +1 -0
- package/dist/src/core/mcp/url.js +162 -0
- package/dist/src/core/mcp/url.js.map +1 -0
- package/dist/src/core/path.js +63 -0
- package/dist/src/core/path.js.map +1 -0
- package/dist/src/core/reconcile/image-decide.js +48 -0
- package/dist/src/core/reconcile/image-decide.js.map +1 -0
- package/dist/src/core/reconcile/ledger.js +142 -0
- package/dist/src/core/reconcile/ledger.js.map +1 -0
- package/dist/src/core/reconcile/pr-classify.js +62 -0
- package/dist/src/core/reconcile/pr-classify.js.map +1 -0
- package/dist/{reconciler → src/core/reconcile}/pr-decide.js +25 -12
- package/dist/src/core/reconcile/pr-decide.js.map +1 -0
- package/dist/src/core/reconcile/pr-loop.js +161 -0
- package/dist/src/core/reconcile/pr-loop.js.map +1 -0
- package/dist/src/core/reconcile/pr-notes.js +35 -0
- package/dist/src/core/reconcile/pr-notes.js.map +1 -0
- package/dist/src/core/reconcile/vm-decide.js +70 -0
- package/dist/src/core/reconcile/vm-decide.js.map +1 -0
- package/dist/src/core/reconcile/vm-reap.js +207 -0
- package/dist/src/core/reconcile/vm-reap.js.map +1 -0
- package/dist/src/core/reconcile/workspace-decide.js +162 -0
- package/dist/src/core/reconcile/workspace-decide.js.map +1 -0
- package/dist/src/core/runlog/summary.js +231 -0
- package/dist/src/core/runlog/summary.js.map +1 -0
- package/dist/src/core/runner/dispatch-config.js +95 -0
- package/dist/src/core/runner/dispatch-config.js.map +1 -0
- package/dist/src/core/runner/injection.js +61 -0
- package/dist/src/core/runner/injection.js.map +1 -0
- package/dist/src/core/runner/mise.js +210 -0
- package/dist/src/core/runner/mise.js.map +1 -0
- package/dist/src/core/runner/prompt.js +720 -0
- package/dist/src/core/runner/prompt.js.map +1 -0
- package/dist/src/core/runner/turn.js +242 -0
- package/dist/src/core/runner/turn.js.map +1 -0
- package/dist/src/core/runner/vm-plan.js +390 -0
- package/dist/src/core/runner/vm-plan.js.map +1 -0
- package/dist/src/core/schedule/admission.js +123 -0
- package/dist/src/core/schedule/admission.js.map +1 -0
- package/dist/src/core/schedule/circuit-breaker.js +111 -0
- package/dist/src/core/schedule/circuit-breaker.js.map +1 -0
- package/dist/src/core/schedule/eligibility.js +83 -0
- package/dist/src/core/schedule/eligibility.js.map +1 -0
- package/dist/src/core/schedule/reconcile-issue.js +82 -0
- package/dist/src/core/schedule/reconcile-issue.js.map +1 -0
- package/dist/src/core/schedule/retry.js +96 -0
- package/dist/src/core/schedule/retry.js.map +1 -0
- package/dist/src/core/schedule/sleep-cycle.js +133 -0
- package/dist/src/core/schedule/sleep-cycle.js.map +1 -0
- package/dist/src/core/schedule/slots.js +124 -0
- package/dist/src/core/schedule/slots.js.map +1 -0
- package/dist/src/core/schedule/tick.js +553 -0
- package/dist/src/core/schedule/tick.js.map +1 -0
- package/dist/src/core/schedule/token-fold.js +181 -0
- package/dist/src/core/schedule/token-fold.js.map +1 -0
- package/dist/src/core/state-resolve.js +86 -0
- package/dist/src/core/state-resolve.js.map +1 -0
- package/dist/src/core/vm-guards.js +278 -0
- package/dist/src/core/vm-guards.js.map +1 -0
- package/dist/src/core/workflow/derive.js +107 -0
- package/dist/src/core/workflow/derive.js.map +1 -0
- package/dist/src/core/workflow/parse.js +687 -0
- package/dist/src/core/workflow/parse.js.map +1 -0
- package/dist/src/core/workflow/prompt-probe.js +78 -0
- package/dist/src/core/workflow/prompt-probe.js.map +1 -0
- package/dist/src/core/workflow/validate.js +189 -0
- package/dist/src/core/workflow/validate.js.map +1 -0
- package/dist/src/core/workspace-key.js +19 -0
- package/dist/src/core/workspace-key.js.map +1 -0
- package/dist/src/shell/actions-runner.js +356 -0
- package/dist/src/shell/actions-runner.js.map +1 -0
- package/dist/src/shell/adapter/adapter-registry.js +45 -0
- package/dist/src/shell/adapter/adapter-registry.js.map +1 -0
- package/dist/src/shell/adapter/clock-random.js +96 -0
- package/dist/src/shell/adapter/clock-random.js.map +1 -0
- package/dist/src/shell/adapter/gondolin-dispatch-helpers.js +158 -0
- package/dist/src/shell/adapter/gondolin-dispatch-helpers.js.map +1 -0
- package/dist/src/shell/adapter/gondolin-dispatch.js +385 -0
- package/dist/src/shell/adapter/gondolin-dispatch.js.map +1 -0
- package/dist/src/shell/adapter/gondolin-image-converter.js +233 -0
- package/dist/src/shell/adapter/gondolin-image-converter.js.map +1 -0
- package/dist/src/shell/adapter/gondolin-image-fetch.js +180 -0
- package/dist/src/shell/adapter/gondolin-image-fetch.js.map +1 -0
- package/dist/src/shell/adapter/launcher-asset.js +57 -0
- package/dist/src/shell/adapter/launcher-asset.js.map +1 -0
- package/dist/src/shell/adapter/mise-config-asset.js +65 -0
- package/dist/src/shell/adapter/mise-config-asset.js.map +1 -0
- package/dist/src/shell/adapter/workflow-loader.js +304 -0
- package/dist/src/shell/adapter/workflow-loader.js.map +1 -0
- package/dist/src/shell/cli/doctor.js +268 -0
- package/dist/src/shell/cli/doctor.js.map +1 -0
- package/dist/src/shell/effect-interpreter-families.js +314 -0
- package/dist/src/shell/effect-interpreter-families.js.map +1 -0
- package/dist/src/shell/effect-interpreter.js +29 -0
- package/dist/src/shell/effect-interpreter.js.map +1 -0
- package/dist/src/shell/interp/acp-frame.js +137 -0
- package/dist/src/shell/interp/acp-frame.js.map +1 -0
- package/dist/src/shell/interp/acp-ws-conn.js +320 -0
- package/dist/src/shell/interp/acp-ws-conn.js.map +1 -0
- package/dist/src/shell/interp/acp-ws-frames.js +159 -0
- package/dist/src/shell/interp/acp-ws-frames.js.map +1 -0
- package/dist/src/shell/interp/acp-ws.js +197 -0
- package/dist/src/shell/interp/acp-ws.js.map +1 -0
- package/dist/src/shell/interp/acp.js +319 -0
- package/dist/src/shell/interp/acp.js.map +1 -0
- package/dist/src/shell/interp/credential-defaults.js +128 -0
- package/dist/src/shell/interp/credential-defaults.js.map +1 -0
- package/dist/src/shell/interp/credential-hooks.js +149 -0
- package/dist/src/shell/interp/credential-hooks.js.map +1 -0
- package/dist/src/shell/interp/credential-registry.js +226 -0
- package/dist/src/shell/interp/credential-registry.js.map +1 -0
- package/dist/src/shell/interp/credential.js +103 -0
- package/dist/src/shell/interp/credential.js.map +1 -0
- package/dist/src/shell/interp/gh.js +163 -0
- package/dist/src/shell/interp/gh.js.map +1 -0
- package/dist/src/shell/interp/git.js +28 -0
- package/dist/src/shell/interp/git.js.map +1 -0
- package/dist/src/shell/interp/log.js +213 -0
- package/dist/src/shell/interp/log.js.map +1 -0
- package/dist/src/shell/interp/process.js +178 -0
- package/dist/src/shell/interp/process.js.map +1 -0
- package/dist/src/shell/interp/runlog.js +193 -0
- package/dist/src/shell/interp/runlog.js.map +1 -0
- package/dist/src/shell/interp/timer.js +64 -0
- package/dist/src/shell/interp/timer.js.map +1 -0
- package/dist/src/shell/interp/tracker-disk.js +99 -0
- package/dist/src/shell/interp/tracker-disk.js.map +1 -0
- package/dist/src/shell/interp/tracker-parse.js +71 -0
- package/dist/src/shell/interp/tracker-parse.js.map +1 -0
- package/dist/src/shell/interp/tracker-scan.js +238 -0
- package/dist/src/shell/interp/tracker-scan.js.map +1 -0
- package/dist/src/shell/interp/tracker-write.js +91 -0
- package/dist/src/shell/interp/tracker-write.js.map +1 -0
- package/dist/src/shell/interp/tracker.js +41 -0
- package/dist/src/shell/interp/tracker.js.map +1 -0
- package/dist/src/shell/interp/tty.js +48 -0
- package/dist/src/shell/interp/tty.js.map +1 -0
- package/dist/src/shell/interp/vm.js +199 -0
- package/dist/src/shell/interp/vm.js.map +1 -0
- package/dist/src/shell/interp/workspace.js +310 -0
- package/dist/src/shell/interp/workspace.js.map +1 -0
- package/dist/src/shell/main-acp.js +78 -0
- package/dist/src/shell/main-acp.js.map +1 -0
- package/dist/src/shell/main-adapters.js +222 -0
- package/dist/src/shell/main-adapters.js.map +1 -0
- package/dist/src/shell/main-credential.js +122 -0
- package/dist/src/shell/main-credential.js.map +1 -0
- package/dist/src/shell/main-doctor.js +22 -0
- package/dist/src/shell/main-doctor.js.map +1 -0
- package/dist/src/shell/main-entry.js +46 -0
- package/dist/src/shell/main-entry.js.map +1 -0
- package/dist/src/shell/main-http-csrf.js +45 -0
- package/dist/src/shell/main-http-csrf.js.map +1 -0
- package/dist/src/shell/main-http-handler.js +389 -0
- package/dist/src/shell/main-http-handler.js.map +1 -0
- package/dist/src/shell/main-http-mcp.js +122 -0
- package/dist/src/shell/main-http-mcp.js.map +1 -0
- package/dist/src/shell/main-http-views.js +253 -0
- package/dist/src/shell/main-http-views.js.map +1 -0
- package/dist/src/shell/main-http.js +76 -0
- package/dist/src/shell/main-http.js.map +1 -0
- package/dist/src/shell/main-loops.js +130 -0
- package/dist/src/shell/main-loops.js.map +1 -0
- package/dist/src/shell/main-mcp.js +129 -0
- package/dist/src/shell/main-mcp.js.map +1 -0
- package/dist/src/shell/main-orchestrator.js +120 -0
- package/dist/src/shell/main-orchestrator.js.map +1 -0
- package/dist/src/shell/main-preflight.js +43 -0
- package/dist/src/shell/main-preflight.js.map +1 -0
- package/dist/src/shell/main-reconcilers-helpers.js +244 -0
- package/dist/src/shell/main-reconcilers-helpers.js.map +1 -0
- package/dist/src/shell/main-reconcilers-pr.js +148 -0
- package/dist/src/shell/main-reconcilers-pr.js.map +1 -0
- package/dist/src/shell/main-reconcilers.js +225 -0
- package/dist/src/shell/main-reconcilers.js.map +1 -0
- package/dist/src/shell/main-runner.js +355 -0
- package/dist/src/shell/main-runner.js.map +1 -0
- package/dist/src/shell/main-scaffold.js +116 -0
- package/dist/src/shell/main-scaffold.js.map +1 -0
- package/dist/src/shell/main-shutdown.js +115 -0
- package/dist/src/shell/main-shutdown.js.map +1 -0
- package/dist/src/shell/main-startup.js +48 -0
- package/dist/src/shell/main-startup.js.map +1 -0
- package/dist/src/shell/main-substrates.js +43 -0
- package/dist/src/shell/main-substrates.js.map +1 -0
- package/dist/src/shell/main.js +385 -0
- package/dist/src/shell/main.js.map +1 -0
- package/dist/src/shell/orchestrator-feedback.js +69 -0
- package/dist/src/shell/orchestrator-feedback.js.map +1 -0
- package/dist/src/shell/orchestrator-image.js +167 -0
- package/dist/src/shell/orchestrator-image.js.map +1 -0
- package/dist/src/shell/orchestrator-loop.js +468 -0
- package/dist/src/shell/orchestrator-loop.js.map +1 -0
- package/dist/src/shell/orchestrator-reconcile.js +36 -0
- package/dist/src/shell/orchestrator-reconcile.js.map +1 -0
- package/dist/src/shell/reconciler-loop.js +228 -0
- package/dist/src/shell/reconciler-loop.js.map +1 -0
- package/dist/src/shell/runner-loop-turn.js +301 -0
- package/dist/src/shell/runner-loop-turn.js.map +1 -0
- package/dist/src/shell/runner-loop.js +338 -0
- package/dist/src/shell/runner-loop.js.map +1 -0
- package/dist/src/shell/server/http.js +208 -0
- package/dist/src/shell/server/http.js.map +1 -0
- package/dist/src/shell/server/mcp-runtime-effects.js +237 -0
- package/dist/src/shell/server/mcp-runtime-effects.js.map +1 -0
- package/dist/src/shell/server/mcp-runtime.js +99 -0
- package/dist/src/shell/server/mcp-runtime.js.map +1 -0
- package/dist/src/shell/workspace-key.js +14 -0
- package/dist/src/shell/workspace-key.js.map +1 -0
- package/dist/src/types/acp.js +8 -0
- package/dist/src/types/acp.js.map +1 -0
- package/dist/src/types/actions/plan.js +6 -0
- package/dist/src/types/actions/plan.js.map +1 -0
- package/dist/src/types/actions/predicates.js +6 -0
- package/dist/src/types/actions/predicates.js.map +1 -0
- package/dist/src/types/actions/run-fold.js +8 -0
- package/dist/src/types/actions/run-fold.js.map +1 -0
- package/dist/src/types/actions.js +7 -0
- package/dist/src/types/actions.js.map +1 -0
- package/dist/src/types/adapter/clock-random.js +4 -0
- package/dist/src/types/adapter/clock-random.js.map +1 -0
- package/dist/src/types/adapter/gondolin-image-converter.js +5 -0
- package/dist/src/types/adapter/gondolin-image-converter.js.map +1 -0
- package/dist/src/types/adapter/gondolin-image-fetch.js +5 -0
- package/dist/src/types/adapter/gondolin-image-fetch.js.map +1 -0
- package/dist/src/types/adapter/workflow-loader.js +4 -0
- package/dist/src/types/adapter/workflow-loader.js.map +1 -0
- package/dist/src/types/cli/args.js +8 -0
- package/dist/src/types/cli/args.js.map +1 -0
- package/dist/src/types/config.js +8 -0
- package/dist/src/types/config.js.map +1 -0
- package/dist/src/types/credential-interp.js +6 -0
- package/dist/src/types/credential-interp.js.map +1 -0
- package/dist/src/types/credentials.js +10 -0
- package/dist/src/types/credentials.js.map +1 -0
- package/dist/src/types/doctor.js +7 -0
- package/dist/src/types/doctor.js.map +1 -0
- package/dist/src/types/domain.js +7 -0
- package/dist/src/types/domain.js.map +1 -0
- package/dist/src/types/effect.js +15 -0
- package/dist/src/types/effect.js.map +1 -0
- package/dist/src/types/errors.js +39 -0
- package/dist/src/types/errors.js.map +1 -0
- package/dist/src/types/http/decisions.js +6 -0
- package/dist/src/types/http/decisions.js.map +1 -0
- package/dist/src/types/http/render.js +10 -0
- package/dist/src/types/http/render.js.map +1 -0
- package/dist/src/types/http/views.js +6 -0
- package/dist/src/types/http/views.js.map +1 -0
- package/dist/src/types/http.js +9 -0
- package/dist/src/types/http.js.map +1 -0
- package/dist/src/types/image/managed-image.js +7 -0
- package/dist/src/types/image/managed-image.js.map +1 -0
- package/dist/src/types/interp/effect-interpreter.js +8 -0
- package/dist/src/types/interp/effect-interpreter.js.map +1 -0
- package/dist/src/types/interp/tracker.js +7 -0
- package/dist/src/types/interp/tracker.js.map +1 -0
- package/dist/src/types/issue/file.js +6 -0
- package/dist/src/types/issue/file.js.map +1 -0
- package/dist/src/types/issue/parse.js +8 -0
- package/dist/src/types/issue/parse.js.map +1 -0
- package/dist/src/types/main-acp.js +13 -0
- package/dist/src/types/main-acp.js.map +1 -0
- package/dist/src/types/main-adapters.js +5 -0
- package/dist/src/types/main-adapters.js.map +1 -0
- package/dist/src/types/main-credential.js +21 -0
- package/dist/src/types/main-credential.js.map +1 -0
- package/dist/src/types/main-doctor.js +6 -0
- package/dist/src/types/main-doctor.js.map +1 -0
- package/dist/src/types/main-http-handler.js +12 -0
- package/dist/src/types/main-http-handler.js.map +1 -0
- package/dist/src/types/main-http.js +5 -0
- package/dist/src/types/main-http.js.map +1 -0
- package/dist/src/types/main-loops.js +5 -0
- package/dist/src/types/main-loops.js.map +1 -0
- package/dist/src/types/main-mcp.js +12 -0
- package/dist/src/types/main-mcp.js.map +1 -0
- package/dist/src/types/main-orchestrator.js +5 -0
- package/dist/src/types/main-orchestrator.js.map +1 -0
- package/dist/src/types/main-reconcilers.js +11 -0
- package/dist/src/types/main-reconcilers.js.map +1 -0
- package/dist/src/types/main-runner.js +13 -0
- package/dist/src/types/main-runner.js.map +1 -0
- package/dist/src/types/main-startup.js +5 -0
- package/dist/src/types/main-startup.js.map +1 -0
- package/dist/src/types/main-substrates.js +5 -0
- package/dist/src/types/main-substrates.js.map +1 -0
- package/dist/src/types/mcp/dispatch.js +4 -0
- package/dist/src/types/mcp/dispatch.js.map +1 -0
- package/dist/src/types/mcp/post-move.js +7 -0
- package/dist/src/types/mcp/post-move.js.map +1 -0
- package/dist/src/types/mcp.js +9 -0
- package/dist/src/types/mcp.js.map +1 -0
- package/dist/src/types/ports.js +12 -0
- package/dist/src/types/ports.js.map +1 -0
- package/dist/src/types/reconcile/image-decide.js +5 -0
- package/dist/src/types/reconcile/image-decide.js.map +1 -0
- package/dist/src/types/reconcile/ledger.js +7 -0
- package/dist/src/types/reconcile/ledger.js.map +1 -0
- package/dist/src/types/reconcile/pr-loop.js +8 -0
- package/dist/src/types/reconcile/pr-loop.js.map +1 -0
- package/dist/src/types/reconcile/vm-reap.js +8 -0
- package/dist/src/types/reconcile/vm-reap.js.map +1 -0
- package/dist/src/types/reconcile/workspace-decide.js +7 -0
- package/dist/src/types/reconcile/workspace-decide.js.map +1 -0
- package/dist/src/types/reconcile.js +9 -0
- package/dist/src/types/reconcile.js.map +1 -0
- package/dist/src/types/runlog.js +7 -0
- package/dist/src/types/runlog.js.map +1 -0
- package/dist/src/types/runner/actions-runner.js +12 -0
- package/dist/src/types/runner/actions-runner.js.map +1 -0
- package/dist/src/types/runner/gondolin-dispatch.js +5 -0
- package/dist/src/types/runner/gondolin-dispatch.js.map +1 -0
- package/dist/src/types/runner/injection.js +6 -0
- package/dist/src/types/runner/injection.js.map +1 -0
- package/dist/src/types/runner/runner-loop.js +5 -0
- package/dist/src/types/runner/runner-loop.js.map +1 -0
- package/dist/src/types/runner/turn.js +4 -0
- package/dist/src/types/runner/turn.js.map +1 -0
- package/dist/src/types/runner/vm-plan.js +4 -0
- package/dist/src/types/runner/vm-plan.js.map +1 -0
- package/dist/src/types/runtime.js +9 -0
- package/dist/src/types/runtime.js.map +1 -0
- package/dist/src/types/schedule/admission.js +7 -0
- package/dist/src/types/schedule/admission.js.map +1 -0
- package/dist/src/types/schedule/circuit-breaker.js +2 -0
- package/dist/src/types/schedule/circuit-breaker.js.map +1 -0
- package/dist/src/types/schedule/eligibility.js +9 -0
- package/dist/src/types/schedule/eligibility.js.map +1 -0
- package/dist/src/types/schedule/orchestrator-loop.js +10 -0
- package/dist/src/types/schedule/orchestrator-loop.js.map +1 -0
- package/dist/src/types/schedule/sleep-cycle.js +4 -0
- package/dist/src/types/schedule/sleep-cycle.js.map +1 -0
- package/dist/src/types/schedule/slots.js +8 -0
- package/dist/src/types/schedule/slots.js.map +1 -0
- package/dist/src/types/schedule/tick.js +9 -0
- package/dist/src/types/schedule/tick.js.map +1 -0
- package/dist/src/types/server/mcp-runtime.js +8 -0
- package/dist/src/types/server/mcp-runtime.js.map +1 -0
- package/dist/src/types/workflow/parse.js +4 -0
- package/dist/src/types/workflow/parse.js.map +1 -0
- package/dist/tests/core/account-id.test.js +35 -0
- package/dist/tests/core/account-id.test.js.map +1 -0
- package/dist/tests/core/actions-parse.test.js +176 -0
- package/dist/tests/core/actions-parse.test.js.map +1 -0
- package/dist/tests/core/adapter-config.test.js +133 -0
- package/dist/tests/core/adapter-config.test.js.map +1 -0
- package/dist/tests/core/admission.test.js +215 -0
- package/dist/tests/core/admission.test.js.map +1 -0
- package/dist/tests/core/args.test.js +132 -0
- package/dist/tests/core/args.test.js.map +1 -0
- package/dist/tests/core/availability.test.js +62 -0
- package/dist/tests/core/availability.test.js.map +1 -0
- package/dist/tests/core/checks.test.js +395 -0
- package/dist/tests/core/checks.test.js.map +1 -0
- package/dist/tests/core/circuit-breaker.test.js +172 -0
- package/dist/tests/core/circuit-breaker.test.js.map +1 -0
- package/dist/tests/core/coerce.test.js +87 -0
- package/dist/tests/core/coerce.test.js.map +1 -0
- package/dist/tests/core/context.test.js +228 -0
- package/dist/tests/core/context.test.js.map +1 -0
- package/dist/tests/core/decisions.test.js +310 -0
- package/dist/tests/core/decisions.test.js.map +1 -0
- package/dist/tests/core/derive.test.js +205 -0
- package/dist/tests/core/derive.test.js.map +1 -0
- package/dist/tests/core/dispatch-config.test.js +164 -0
- package/dist/tests/core/dispatch-config.test.js.map +1 -0
- package/dist/tests/core/dispatch.test.js +302 -0
- package/dist/tests/core/dispatch.test.js.map +1 -0
- package/dist/tests/core/eligibility.test.js +163 -0
- package/dist/tests/core/eligibility.test.js.map +1 -0
- package/dist/tests/core/extract.test.js +139 -0
- package/dist/tests/core/extract.test.js.map +1 -0
- package/dist/tests/core/fake-creds.test.js +134 -0
- package/dist/tests/core/fake-creds.test.js.map +1 -0
- package/dist/tests/core/file.test.js +197 -0
- package/dist/tests/core/file.test.js.map +1 -0
- package/dist/tests/core/git-result.test.js +113 -0
- package/dist/tests/core/git-result.test.js.map +1 -0
- package/dist/tests/core/identity.test.js +180 -0
- package/dist/tests/core/identity.test.js.map +1 -0
- package/dist/tests/core/image-decide.test.js +59 -0
- package/dist/tests/core/image-decide.test.js.map +1 -0
- package/dist/tests/core/injection.test.js +163 -0
- package/dist/tests/core/injection.test.js.map +1 -0
- package/dist/tests/core/ledger.test.js +218 -0
- package/dist/tests/core/ledger.test.js.map +1 -0
- package/dist/tests/core/managed-image.test.js +68 -0
- package/dist/tests/core/managed-image.test.js.map +1 -0
- package/dist/tests/core/mise.test.js +138 -0
- package/dist/tests/core/mise.test.js.map +1 -0
- package/dist/tests/core/parse.test.js +174 -0
- package/dist/tests/core/parse.test.js.map +1 -0
- package/dist/tests/core/path.test.js +50 -0
- package/dist/tests/core/path.test.js.map +1 -0
- package/dist/tests/core/plan.test.js +218 -0
- package/dist/tests/core/plan.test.js.map +1 -0
- package/dist/tests/core/post-move.test.js +162 -0
- package/dist/tests/core/post-move.test.js.map +1 -0
- package/dist/tests/core/pr-classify.test.js +117 -0
- package/dist/tests/core/pr-classify.test.js.map +1 -0
- package/dist/tests/core/pr-decide.test.js +298 -0
- package/dist/tests/core/pr-decide.test.js.map +1 -0
- package/dist/tests/core/pr-loop.test.js +301 -0
- package/dist/tests/core/pr-loop.test.js.map +1 -0
- package/dist/tests/core/pr-notes.test.js +165 -0
- package/dist/tests/core/pr-notes.test.js.map +1 -0
- package/dist/tests/core/predicates.test.js +154 -0
- package/dist/tests/core/predicates.test.js.map +1 -0
- package/dist/tests/core/prompt.test.js +189 -0
- package/dist/tests/core/prompt.test.js.map +1 -0
- package/dist/tests/core/protocol.test.js +195 -0
- package/dist/tests/core/protocol.test.js.map +1 -0
- package/dist/tests/core/reconcile-issue.test.js +116 -0
- package/dist/tests/core/reconcile-issue.test.js.map +1 -0
- package/dist/tests/core/render.test.js +549 -0
- package/dist/tests/core/render.test.js.map +1 -0
- package/dist/tests/core/retry.test.js +186 -0
- package/dist/tests/core/retry.test.js.map +1 -0
- package/dist/tests/core/routes.test.js +247 -0
- package/dist/tests/core/routes.test.js.map +1 -0
- package/dist/tests/core/run-fold.test.js +299 -0
- package/dist/tests/core/run-fold.test.js.map +1 -0
- package/dist/tests/core/shape.test.js +185 -0
- package/dist/tests/core/shape.test.js.map +1 -0
- package/dist/tests/core/sleep-cycle.test.js +150 -0
- package/dist/tests/core/sleep-cycle.test.js.map +1 -0
- package/dist/tests/core/slots.test.js +201 -0
- package/dist/tests/core/slots.test.js.map +1 -0
- package/dist/tests/core/state-resolve.test.js +80 -0
- package/dist/tests/core/state-resolve.test.js.map +1 -0
- package/dist/tests/core/summary.test.js +200 -0
- package/dist/tests/core/summary.test.js.map +1 -0
- package/dist/tests/core/template.test.js +116 -0
- package/dist/tests/core/template.test.js.map +1 -0
- package/dist/tests/core/tick.test.js +558 -0
- package/dist/tests/core/tick.test.js.map +1 -0
- package/dist/tests/core/token-fold.test.js +176 -0
- package/dist/tests/core/token-fold.test.js.map +1 -0
- package/dist/tests/core/turn.test.js +388 -0
- package/dist/tests/core/turn.test.js.map +1 -0
- package/dist/tests/core/url.test.js +118 -0
- package/dist/tests/core/url.test.js.map +1 -0
- package/dist/tests/core/validate.test.js +247 -0
- package/dist/tests/core/validate.test.js.map +1 -0
- package/dist/tests/core/views.test.js +252 -0
- package/dist/tests/core/views.test.js.map +1 -0
- package/dist/tests/core/vm-decide.test.js +110 -0
- package/dist/tests/core/vm-decide.test.js.map +1 -0
- package/dist/tests/core/vm-guards.test.js +153 -0
- package/dist/tests/core/vm-guards.test.js.map +1 -0
- package/dist/tests/core/vm-plan.test.js +332 -0
- package/dist/tests/core/vm-plan.test.js.map +1 -0
- package/dist/tests/core/vm-reap.test.js +196 -0
- package/dist/tests/core/vm-reap.test.js.map +1 -0
- package/dist/tests/core/workflow-parse.test.js +493 -0
- package/dist/tests/core/workflow-parse.test.js.map +1 -0
- package/dist/tests/core/workspace-decide.test.js +236 -0
- package/dist/tests/core/workspace-decide.test.js.map +1 -0
- package/dist/tests/helpers/fixtures.js +167 -0
- package/dist/tests/helpers/fixtures.js.map +1 -0
- package/dist/tests/shell/acp-substrate.test.js +101 -0
- package/dist/tests/shell/acp-substrate.test.js.map +1 -0
- package/dist/tests/shell/actions-runner-push.test.js +203 -0
- package/dist/tests/shell/actions-runner-push.test.js.map +1 -0
- package/dist/tests/shell/credential-hooks.test.js +36 -0
- package/dist/tests/shell/credential-hooks.test.js.map +1 -0
- package/dist/tests/shell/credential-registry.test.js +165 -0
- package/dist/tests/shell/credential-registry.test.js.map +1 -0
- package/dist/tests/shell/credential-substrate.test.js +179 -0
- package/dist/tests/shell/credential-substrate.test.js.map +1 -0
- package/dist/tests/shell/dockerfile-mise-pin.test.js +51 -0
- package/dist/tests/shell/dockerfile-mise-pin.test.js.map +1 -0
- package/dist/tests/shell/doctor.test.js +101 -0
- package/dist/tests/shell/doctor.test.js.map +1 -0
- package/dist/tests/shell/effect-vm-create.test.js +52 -0
- package/dist/tests/shell/effect-vm-create.test.js.map +1 -0
- package/dist/tests/shell/gh-port.test.js +63 -0
- package/dist/tests/shell/gh-port.test.js.map +1 -0
- package/dist/tests/shell/gondolin-dispatch-guard.test.js +144 -0
- package/dist/tests/shell/gondolin-dispatch-guard.test.js.map +1 -0
- package/dist/tests/shell/gondolin-dispatch-shquote.test.js +168 -0
- package/dist/tests/shell/gondolin-dispatch-shquote.test.js.map +1 -0
- package/dist/tests/shell/gondolin-image-converter.test.js +208 -0
- package/dist/tests/shell/gondolin-image-converter.test.js.map +1 -0
- package/dist/tests/shell/gondolin-image-fetch.test.js +93 -0
- package/dist/tests/shell/gondolin-image-fetch.test.js.map +1 -0
- package/dist/tests/shell/http-handler.test.js +608 -0
- package/dist/tests/shell/http-handler.test.js.map +1 -0
- package/dist/tests/shell/http-server.test.js +53 -0
- package/dist/tests/shell/http-server.test.js.map +1 -0
- package/dist/tests/shell/mcp-runtime.test.js +366 -0
- package/dist/tests/shell/mcp-runtime.test.js.map +1 -0
- package/dist/tests/shell/mise-config-asset.test.js +87 -0
- package/dist/tests/shell/mise-config-asset.test.js.map +1 -0
- package/dist/tests/shell/orchestrator-loop.test.js +583 -0
- package/dist/tests/shell/orchestrator-loop.test.js.map +1 -0
- package/dist/tests/shell/reconciler-passes.test.js +314 -0
- package/dist/tests/shell/reconciler-passes.test.js.map +1 -0
- package/dist/tests/shell/runner-loop-turn.test.js +97 -0
- package/dist/tests/shell/runner-loop-turn.test.js.map +1 -0
- package/dist/tests/shell/runner-slice.test.js +536 -0
- package/dist/tests/shell/runner-slice.test.js.map +1 -0
- package/dist/tests/shell/scaffold.test.js +65 -0
- package/dist/tests/shell/scaffold.test.js.map +1 -0
- package/dist/tests/shell/tick-config.test.js +83 -0
- package/dist/tests/shell/tick-config.test.js.map +1 -0
- package/dist/tests/shell/tracker-parse-dates.test.js +44 -0
- package/dist/tests/shell/tracker-parse-dates.test.js.map +1 -0
- package/dist/tests/shell/tracker-write-issue.test.js +154 -0
- package/dist/tests/shell/tracker-write-issue.test.js.map +1 -0
- package/dist/tests/shell/workflow-prompt-split.test.js +208 -0
- package/dist/tests/shell/workflow-prompt-split.test.js.map +1 -0
- package/dist/tests/shell/workspace-live-config.test.js +140 -0
- package/dist/tests/shell/workspace-live-config.test.js.map +1 -0
- package/package.json +21 -9
- package/patches/@earendil-works+gondolin+0.12.0.patch +173 -0
- package/prompts/Reflect.md +91 -0
- package/prompts/Review.md +97 -0
- package/prompts/Todo.md +96 -0
- package/prompts/_footer.md +41 -0
- package/prompts/_preamble.md +42 -0
- package/prompts-minimal/Todo.md +26 -0
- package/scripts/postinstall.mjs +63 -0
- package/scripts/vm-agent.mjs +312 -90
- package/WORKFLOW.md +0 -744
- package/dist/acp-bridge.js +0 -324
- package/dist/acp-bridge.js.map +0 -1
- package/dist/actions/cache.js +0 -191
- package/dist/actions/cache.js.map +0 -1
- package/dist/actions/effects.js +0 -41
- package/dist/actions/effects.js.map +0 -1
- package/dist/actions/executor.js +0 -570
- package/dist/actions/executor.js.map +0 -1
- package/dist/actions/index.js +0 -13
- package/dist/actions/index.js.map +0 -1
- package/dist/actions/parsing.js.map +0 -1
- package/dist/actions/predicate-env.js +0 -27
- package/dist/actions/predicate-env.js.map +0 -1
- package/dist/actions/predicates.js +0 -49
- package/dist/actions/predicates.js.map +0 -1
- package/dist/actions/templating.js +0 -66
- package/dist/actions/templating.js.map +0 -1
- package/dist/actions/types.js +0 -15
- package/dist/actions/types.js.map +0 -1
- package/dist/agent/acp.js +0 -473
- package/dist/agent/acp.js.map +0 -1
- package/dist/agent/adapter-names.js +0 -159
- package/dist/agent/adapter-names.js.map +0 -1
- package/dist/agent/adapters.js +0 -511
- package/dist/agent/adapters.js.map +0 -1
- package/dist/agent/credential-extractors.js +0 -342
- package/dist/agent/credential-extractors.js.map +0 -1
- package/dist/agent/credential-secrets.js +0 -628
- package/dist/agent/credential-secrets.js.map +0 -1
- package/dist/agent/credential-ticker.js +0 -57
- package/dist/agent/credential-ticker.js.map +0 -1
- package/dist/agent/gondolin-creds-staging.js +0 -356
- package/dist/agent/gondolin-creds-staging.js.map +0 -1
- package/dist/agent/gondolin-dispatch.js +0 -375
- package/dist/agent/gondolin-dispatch.js.map +0 -1
- package/dist/agent/gondolin.js +0 -124
- package/dist/agent/gondolin.js.map +0 -1
- package/dist/agent/runner-decisions.js +0 -134
- package/dist/agent/runner-decisions.js.map +0 -1
- package/dist/agent/runner.js +0 -1456
- package/dist/agent/runner.js.map +0 -1
- package/dist/agent/tool-call-summary.js +0 -102
- package/dist/agent/tool-call-summary.js.map +0 -1
- package/dist/agent/vm-acp-mapping.js +0 -73
- package/dist/agent/vm-acp-mapping.js.map +0 -1
- package/dist/agent/vm-guards.js +0 -262
- package/dist/agent/vm-guards.js.map +0 -1
- package/dist/agent/vm-port.js +0 -22
- package/dist/agent/vm-port.js.map +0 -1
- package/dist/agent/vm-process-registry.js +0 -79
- package/dist/agent/vm-process-registry.js.map +0 -1
- package/dist/bin/cli-args.js +0 -105
- package/dist/bin/cli-args.js.map +0 -1
- package/dist/bin/symphony.js +0 -794
- package/dist/bin/symphony.js.map +0 -1
- package/dist/errors.js +0 -15
- package/dist/errors.js.map +0 -1
- package/dist/http-disk.js +0 -135
- package/dist/http-disk.js.map +0 -1
- package/dist/http-handlers.js.map +0 -1
- package/dist/http.js.map +0 -1
- package/dist/issues.js +0 -178
- package/dist/issues.js.map +0 -1
- package/dist/logging.js +0 -203
- package/dist/logging.js.map +0 -1
- package/dist/mcp.js +0 -706
- package/dist/mcp.js.map +0 -1
- package/dist/memory.js +0 -85
- package/dist/memory.js.map +0 -1
- package/dist/orchestrator-decisions.js +0 -331
- package/dist/orchestrator-decisions.js.map +0 -1
- package/dist/orchestrator.js +0 -1569
- package/dist/orchestrator.js.map +0 -1
- package/dist/prompt.js +0 -65
- package/dist/prompt.js.map +0 -1
- package/dist/reconciler/cache.js +0 -65
- package/dist/reconciler/cache.js.map +0 -1
- package/dist/reconciler/index.js +0 -448
- package/dist/reconciler/index.js.map +0 -1
- package/dist/reconciler/ledger.js +0 -131
- package/dist/reconciler/ledger.js.map +0 -1
- package/dist/reconciler/pr-adapters.js +0 -174
- package/dist/reconciler/pr-adapters.js.map +0 -1
- package/dist/reconciler/pr-decide.js.map +0 -1
- package/dist/reconciler/pr.js +0 -422
- package/dist/reconciler/pr.js.map +0 -1
- package/dist/reconciler/types.js +0 -12
- package/dist/reconciler/types.js.map +0 -1
- package/dist/reconciler/vm.js +0 -243
- package/dist/reconciler/vm.js.map +0 -1
- package/dist/reconciler/workspace-defaults.js +0 -83
- package/dist/reconciler/workspace-defaults.js.map +0 -1
- package/dist/reconciler/workspace.js +0 -272
- package/dist/reconciler/workspace.js.map +0 -1
- package/dist/runlog.js +0 -403
- package/dist/runlog.js.map +0 -1
- package/dist/scaffold.js +0 -165
- package/dist/scaffold.js.map +0 -1
- package/dist/trackers/local.js +0 -445
- package/dist/trackers/local.js.map +0 -1
- package/dist/trackers/types.js +0 -10
- package/dist/trackers/types.js.map +0 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +0 -1
- package/dist/util/clock.js +0 -12
- package/dist/util/clock.js.map +0 -1
- package/dist/util/crypto.js +0 -25
- package/dist/util/crypto.js.map +0 -1
- package/dist/util/frontmatter.js +0 -70
- package/dist/util/frontmatter.js.map +0 -1
- package/dist/util/fs-issues.js +0 -22
- package/dist/util/fs-issues.js.map +0 -1
- package/dist/util/process.js +0 -152
- package/dist/util/process.js.map +0 -1
- package/dist/util/workspace-key.js +0 -10
- package/dist/util/workspace-key.js.map +0 -1
- package/dist/workflow-loader.js +0 -147
- package/dist/workflow-loader.js.map +0 -1
- package/dist/workflow.js +0 -822
- package/dist/workflow.js.map +0 -1
- package/dist/workspace-types.js +0 -8
- package/dist/workspace-types.js.map +0 -1
- package/dist/workspace.js +0 -443
- package/dist/workspace.js.map +0 -1
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// Behavioral tests for the push_branch action in the terminal-state actions
|
|
2
|
+
// runner (shell/actions-runner.ts), pinning the 227 fixes:
|
|
3
|
+
//
|
|
4
|
+
// • A server-side `! [remote rejected] … (push declined …)` push fails FAST
|
|
5
|
+
// with its own stderr and NEVER enters the NFF fetch→force recovery — the
|
|
6
|
+
// old bare-`rejected` regex mis-routed it there, where the fetch then masked
|
|
7
|
+
// the real reason with "couldn't find remote ref".
|
|
8
|
+
// • A genuine non-fast-forward still triggers force-with-lease recovery
|
|
9
|
+
// (existing behavior preserved).
|
|
10
|
+
// • When the NFF recovery's own fetch fails, the ORIGINAL push reason is
|
|
11
|
+
// surfaced rather than the masking fetch error.
|
|
12
|
+
//
|
|
13
|
+
// The REAL pure core deciders (planActions / run-fold) are wired so the
|
|
14
|
+
// assertions exercise the genuine plan → apply → fold loop. The interpreter is a
|
|
15
|
+
// fake that runs the push's classification through the REAL classifyPushResult.
|
|
16
|
+
import { test } from 'node:test';
|
|
17
|
+
import assert from 'node:assert/strict';
|
|
18
|
+
import { createActionsRunner } from '../../src/shell/actions-runner.js';
|
|
19
|
+
import { classifyPushResult, describeHandoffFailure } from '../../src/core/git/result.js';
|
|
20
|
+
// REAL pure core (the same fns main.ts injects).
|
|
21
|
+
import { planActions } from '../../src/core/actions/plan.js';
|
|
22
|
+
import { foldPredicateProbe } from '../../src/core/actions/predicates.js';
|
|
23
|
+
import { beginAction, foldActionOutcome, foldRenderFailed, foldSkipped, foldPredicateError, summarizeRun, } from '../../src/core/actions/run-fold.js';
|
|
24
|
+
function result(over) {
|
|
25
|
+
return { ran: true, exit_code: 0, signal: null, timed_out: false, stdout: '', stderr: '', ...over };
|
|
26
|
+
}
|
|
27
|
+
// A push_branch action with retries disabled so the first failure aborts (keeps
|
|
28
|
+
// the interpreter call counts deterministic for the no-fetch assertion).
|
|
29
|
+
const PUSH_ACTION = {
|
|
30
|
+
kind: 'push_branch',
|
|
31
|
+
name: 'push-branch',
|
|
32
|
+
remote: 'origin',
|
|
33
|
+
ref: 'agent/227',
|
|
34
|
+
on_error: { retry: { count: 0, backoff_ms: 0 } },
|
|
35
|
+
};
|
|
36
|
+
/** A fake interpreter scripting the git effects; records every effect it ran. A
|
|
37
|
+
* `tracker.move_state` (the issue-235 reroute) resolves as a successful move unless
|
|
38
|
+
* `moveState` overrides it (e.g. a failed move, to exercise the doubly-failed path). */
|
|
39
|
+
function makeInterpreter(script) {
|
|
40
|
+
const calls = [];
|
|
41
|
+
const interpreter = {
|
|
42
|
+
execute: async (effect) => {
|
|
43
|
+
calls.push(effect);
|
|
44
|
+
if (effect.family === 'git') {
|
|
45
|
+
if (effect.kind === 'push') {
|
|
46
|
+
return { kind: 'git_status', ...classifyPushResult(script.push), result: script.push };
|
|
47
|
+
}
|
|
48
|
+
if (effect.kind === 'fetch')
|
|
49
|
+
return script.fetch ?? { kind: 'run_result', result: result({ exit_code: 0 }) };
|
|
50
|
+
if (effect.kind === 'rev_parse')
|
|
51
|
+
return { kind: 'rev_parse', sha: 'deadbeefcafe' };
|
|
52
|
+
if (effect.kind === 'push_force_with_lease') {
|
|
53
|
+
const fp = script.forcePush ?? result({ exit_code: 0 });
|
|
54
|
+
return { kind: 'git_status', ...classifyPushResult(fp), result: fp };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (effect.family === 'tracker' && effect.kind === 'move_state') {
|
|
58
|
+
return script.moveState ?? { kind: 'move_state', fromState: effect.fromState ?? 'Done', toState: effect.toState, newPath: `/trk/${effect.toState}/235.md` };
|
|
59
|
+
}
|
|
60
|
+
return { ok: true };
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
return { interpreter, calls };
|
|
64
|
+
}
|
|
65
|
+
/** Wire the runner with the real core fns + a stubbed action resolve/context. */
|
|
66
|
+
function makeRunner(interpreter, actions = [PUSH_ACTION]) {
|
|
67
|
+
const core = {
|
|
68
|
+
resolveActionsForState: () => actions,
|
|
69
|
+
deriveActionContext: () => ({}),
|
|
70
|
+
planActions,
|
|
71
|
+
foldPredicateProbe,
|
|
72
|
+
beginAction,
|
|
73
|
+
foldActionOutcome,
|
|
74
|
+
foldRenderFailed,
|
|
75
|
+
foldSkipped,
|
|
76
|
+
foldPredicateError,
|
|
77
|
+
summarizeRun,
|
|
78
|
+
describeHandoffFailure,
|
|
79
|
+
};
|
|
80
|
+
const deps = {
|
|
81
|
+
core,
|
|
82
|
+
interpreter,
|
|
83
|
+
clock: { now: () => 0, iso: () => '1970-01-01T00:00:00.000Z' },
|
|
84
|
+
log: { emit: () => { }, withIssue: () => ({ emit: () => { } }) },
|
|
85
|
+
runlog: { system: () => { }, record: () => { } },
|
|
86
|
+
cfg: (() => ({ workspace: { github_repo: 'o/r', base_branch: 'main' } })),
|
|
87
|
+
readEnv: () => undefined,
|
|
88
|
+
};
|
|
89
|
+
const entry = {
|
|
90
|
+
issue_id: 'i-235',
|
|
91
|
+
resolved_actor: 'claude/opus',
|
|
92
|
+
tracker_root_at_dispatch: '/trk',
|
|
93
|
+
active_tool_calls: new Set(),
|
|
94
|
+
last_transition: null,
|
|
95
|
+
};
|
|
96
|
+
const runner = createActionsRunner(deps);
|
|
97
|
+
return { runner, entry };
|
|
98
|
+
}
|
|
99
|
+
const RAN = (calls, kind) => calls.some((c) => c.family === 'git' && c.kind === kind);
|
|
100
|
+
test('push_branch: a server-side [remote rejected] decline fails fast with its real stderr, no fetch (227)', async () => {
|
|
101
|
+
const declineStderr = ' ! [remote rejected] agent/227 -> agent/227 (refusing to allow a Personal Access Token ' +
|
|
102
|
+
'to create or update workflow `.github/workflows/release.yml` without `workflow` scope)\n' +
|
|
103
|
+
'error: failed to push some refs';
|
|
104
|
+
const { interpreter, calls } = makeInterpreter({ push: result({ exit_code: 1, stderr: declineStderr }) });
|
|
105
|
+
const { runner, entry } = makeRunner(interpreter);
|
|
106
|
+
const { nonRouted: reason } = await runner.runStateActions({ state: 'Done', entry, workspacePath: '/ws', vm: null });
|
|
107
|
+
assert.ok(reason && reason.includes('remote rejected'), `expected the real decline reason, got: ${reason}`);
|
|
108
|
+
assert.ok(reason.includes('workflow'), 'reason carries the actionable workflow-scope text');
|
|
109
|
+
assert.ok(!reason.includes("couldn't find remote ref"), 'NOT masked by the NFF fetch error');
|
|
110
|
+
// The whole point: a decline must NOT enter the force-with-lease recovery.
|
|
111
|
+
assert.equal(RAN(calls, 'fetch'), false, 'no NFF fetch was attempted');
|
|
112
|
+
assert.equal(RAN(calls, 'push_force_with_lease'), false, 'no force-with-lease was attempted');
|
|
113
|
+
});
|
|
114
|
+
test('push_branch: a genuine non-fast-forward still triggers force-with-lease recovery', async () => {
|
|
115
|
+
const nffStderr = ' ! [rejected] agent/227 -> agent/227 (non-fast-forward)\nerror: failed to push some refs';
|
|
116
|
+
const { interpreter, calls } = makeInterpreter({
|
|
117
|
+
push: result({ exit_code: 1, stderr: nffStderr }),
|
|
118
|
+
forcePush: result({ exit_code: 0 }),
|
|
119
|
+
});
|
|
120
|
+
const { runner, entry } = makeRunner(interpreter);
|
|
121
|
+
const { nonRouted: reason } = await runner.runStateActions({ state: 'Done', entry, workspacePath: '/ws', vm: null });
|
|
122
|
+
assert.equal(reason, null, 'recovery succeeded → action ok');
|
|
123
|
+
assert.equal(RAN(calls, 'fetch'), true, 'NFF fetch ran');
|
|
124
|
+
assert.equal(RAN(calls, 'rev_parse'), true, 'remote-tracking rev-parse ran');
|
|
125
|
+
assert.equal(RAN(calls, 'push_force_with_lease'), true, 'force-with-lease ran');
|
|
126
|
+
});
|
|
127
|
+
test('push_branch: when the NFF recovery fetch fails, the ORIGINAL push reason is surfaced not the fetch mask (227)', async () => {
|
|
128
|
+
const nffStderr = ' ! [rejected] agent/227 -> agent/227 (non-fast-forward)\nerror: failed to push some refs';
|
|
129
|
+
const { interpreter } = makeInterpreter({
|
|
130
|
+
push: result({ exit_code: 1, stderr: nffStderr }),
|
|
131
|
+
fetch: { kind: 'run_result', result: result({ exit_code: 128, stderr: "fatal: couldn't find remote ref agent/227" }) },
|
|
132
|
+
});
|
|
133
|
+
const { runner, entry } = makeRunner(interpreter);
|
|
134
|
+
const { nonRouted: reason } = await runner.runStateActions({ state: 'Done', entry, workspacePath: '/ws', vm: null });
|
|
135
|
+
assert.ok(reason && reason.includes('non-fast-forward'), `expected the original push reason, got: ${reason}`);
|
|
136
|
+
assert.ok(!reason.includes("couldn't find remote ref"), 'the masking fetch error is NOT surfaced');
|
|
137
|
+
});
|
|
138
|
+
// ─── issue 235: a failed handoff reroutes to a holding state ──────────────────
|
|
139
|
+
const RAN_TRACKER_MOVE = (calls) => calls.find((c) => c.family === 'tracker' && c.kind === 'move_state');
|
|
140
|
+
// push_branch with on_error.then.route_to — the Done handoff shape (issue 235).
|
|
141
|
+
const PUSH_WITH_ROUTE = {
|
|
142
|
+
kind: 'push_branch',
|
|
143
|
+
name: 'push-branch',
|
|
144
|
+
remote: 'origin',
|
|
145
|
+
ref: 'agent/235',
|
|
146
|
+
on_error: { retry: { count: 0, backoff_ms: 0 }, then: { route_to: 'HandoffFailed' } },
|
|
147
|
+
};
|
|
148
|
+
test('push_branch: a workflow-scope decline reroutes the issue to HandoffFailed and retains the workspace (235)', async () => {
|
|
149
|
+
const declineStderr = ' ! [remote rejected] agent/235 -> agent/235 (refusing to allow a Personal Access Token ' +
|
|
150
|
+
'to create or update workflow `.github/workflows/ci.yml` without `workflow` scope)\n' +
|
|
151
|
+
'error: failed to push some refs';
|
|
152
|
+
const { interpreter, calls } = makeInterpreter({ push: result({ exit_code: 1, stderr: declineStderr }) });
|
|
153
|
+
const { runner, entry } = makeRunner(interpreter, [PUSH_WITH_ROUTE]);
|
|
154
|
+
const run = await runner.runStateActions({ state: 'Done', entry, workspacePath: '/ws', vm: null });
|
|
155
|
+
// A route is a clean attempt (NOT a non-routed failure)...
|
|
156
|
+
assert.equal(run.nonRouted, null, 'a routed handoff is not surfaced as an attempt failure');
|
|
157
|
+
// ...but the workspace must be retained, carrying the actionable reason.
|
|
158
|
+
assert.ok(run.markHandoffReason && run.markHandoffReason.startsWith('manual SSH push required'), `expected the manual-SSH-push reason, got: ${run.markHandoffReason}`);
|
|
159
|
+
// The issue is physically moved OUT of terminal Done into the holding state.
|
|
160
|
+
const move = RAN_TRACKER_MOVE(calls);
|
|
161
|
+
assert.ok(move, 'a tracker.move_state effect ran');
|
|
162
|
+
assert.equal(move.toState, 'HandoffFailed');
|
|
163
|
+
assert.equal(move.fromState, 'Done');
|
|
164
|
+
assert.equal(move.issueId, 'i-235');
|
|
165
|
+
assert.ok(move.notes.includes('manual SSH push required'), 'the move appends the actionable reason as notes');
|
|
166
|
+
// The rerouted transition is recorded on the live entry (non-terminal, rerouted).
|
|
167
|
+
assert.equal(entry.last_transition?.to_state, 'HandoffFailed');
|
|
168
|
+
assert.equal(entry.last_transition?.rerouted, true);
|
|
169
|
+
assert.equal(entry.last_transition?.terminal, false);
|
|
170
|
+
});
|
|
171
|
+
test('push_branch: when the reroute move ALSO fails the attempt exits abnormal and the workspace is still retained (235)', async () => {
|
|
172
|
+
const declineStderr = ' ! [remote rejected] agent/235 -> agent/235 (refusing to allow a Personal Access Token ' +
|
|
173
|
+
'to create or update workflow `.github/workflows/ci.yml` without `workflow` scope)\n' +
|
|
174
|
+
'error: failed to push some refs';
|
|
175
|
+
// The handoff push declines AND the tracker reroute move then fails — the issue is
|
|
176
|
+
// stranded in terminal Done with a broken tracker.
|
|
177
|
+
const { interpreter, calls } = makeInterpreter({
|
|
178
|
+
push: result({ exit_code: 1, stderr: declineStderr }),
|
|
179
|
+
moveState: { ok: false, error: 'tracker offline' },
|
|
180
|
+
});
|
|
181
|
+
const { runner, entry } = makeRunner(interpreter, [PUSH_WITH_ROUTE]);
|
|
182
|
+
const run = await runner.runStateActions({ state: 'Done', entry, workspacePath: '/ws', vm: null });
|
|
183
|
+
// A failed reroute is surfaced as a non-routed attempt failure (worker exits abnormal,
|
|
184
|
+
// operator-visible) rather than a clean route into a silent terminal-Done.
|
|
185
|
+
assert.ok(run.nonRouted && run.nonRouted.includes('reroute to HandoffFailed failed'), `expected the failed-reroute reason, got: ${run.nonRouted}`);
|
|
186
|
+
assert.ok(run.nonRouted.includes('tracker offline'), 'carries the underlying move error');
|
|
187
|
+
// ...but the workspace is STILL retained so the unpushed commit survives.
|
|
188
|
+
assert.ok(run.markHandoffReason && run.markHandoffReason.startsWith('manual SSH push required'), `expected the manual-SSH-push reason, got: ${run.markHandoffReason}`);
|
|
189
|
+
// The move was attempted, but since it failed NO rerouted transition is recorded —
|
|
190
|
+
// the entry must not claim the issue moved when it is still in Done.
|
|
191
|
+
assert.ok(RAN_TRACKER_MOVE(calls), 'a tracker.move_state effect was attempted');
|
|
192
|
+
assert.equal(entry.last_transition, null, 'no rerouted transition recorded when the move failed');
|
|
193
|
+
});
|
|
194
|
+
test('push_branch: a successful push neither reroutes nor retains the workspace (235)', async () => {
|
|
195
|
+
const { interpreter, calls } = makeInterpreter({ push: result({ exit_code: 0 }) });
|
|
196
|
+
const { runner, entry } = makeRunner(interpreter, [PUSH_WITH_ROUTE]);
|
|
197
|
+
const run = await runner.runStateActions({ state: 'Done', entry, workspacePath: '/ws', vm: null });
|
|
198
|
+
assert.equal(run.nonRouted, null);
|
|
199
|
+
assert.equal(run.markHandoffReason, null, 'a clean handoff retains nothing');
|
|
200
|
+
assert.equal(RAN_TRACKER_MOVE(calls), undefined, 'no reroute move on success');
|
|
201
|
+
assert.equal(entry.last_transition, null, 'no rerouted transition recorded');
|
|
202
|
+
});
|
|
203
|
+
//# sourceMappingURL=actions-runner-push.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions-runner-push.test.js","sourceRoot":"","sources":["../../../tests/shell/actions-runner-push.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Unit tests for the egress request-side lazy-refresh hook (issue 214,
|
|
2
|
+
// src/shell/interp/credential-hooks.ts:makeLazyRefreshHook). The hook is a pure
|
|
3
|
+
// pass-through `onRequest`: it awaits the registry's expiry-gated refresh ONLY for
|
|
4
|
+
// requests bound to the credential's substitution hosts, NEVER returns a Response, and
|
|
5
|
+
// NEVER throws (logging must not break egress). It is REQUEST-side only — it can never
|
|
6
|
+
// re-introduce the issue-135 streaming regression an onResponse hook would.
|
|
7
|
+
import { test } from 'node:test';
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import { makeLazyRefreshHook } from '../../src/shell/interp/credential-hooks.js';
|
|
10
|
+
const noopLog = { emit: () => undefined };
|
|
11
|
+
test('lazy refresh awaits a credential refresh for requests on a substitution host', async () => {
|
|
12
|
+
let calls = 0;
|
|
13
|
+
const hook = makeLazyRefreshHook(['api.anthropic.com'], async () => { calls++; }, noopLog);
|
|
14
|
+
const out = await hook(new Request('https://api.anthropic.com/v1/messages', { method: 'POST' }));
|
|
15
|
+
assert.equal(calls, 1, 'the refresh is awaited before the request proceeds');
|
|
16
|
+
assert.equal(out, undefined, 'pass-through: never returns a Response (Gondolin substitutes after)');
|
|
17
|
+
});
|
|
18
|
+
test('lazy refresh also fires on a SUBDOMAIN of a substitution host', async () => {
|
|
19
|
+
let calls = 0;
|
|
20
|
+
const hook = makeLazyRefreshHook(['anthropic.com'], async () => { calls++; }, noopLog);
|
|
21
|
+
await hook(new Request('https://api.anthropic.com/v1/messages'));
|
|
22
|
+
assert.equal(calls, 1);
|
|
23
|
+
});
|
|
24
|
+
test('lazy refresh does NOT refresh for requests to other (non-credential) hosts', async () => {
|
|
25
|
+
let calls = 0;
|
|
26
|
+
const hook = makeLazyRefreshHook(['api.anthropic.com'], async () => { calls++; }, noopLog);
|
|
27
|
+
await hook(new Request('https://registry.npmjs.org/some-pkg'));
|
|
28
|
+
assert.equal(calls, 0, 'plain egress hosts never trigger a credential refresh');
|
|
29
|
+
});
|
|
30
|
+
test('lazy refresh swallows a refresh error — egress is never broken by it', async () => {
|
|
31
|
+
const hook = makeLazyRefreshHook(['api.anthropic.com'], async () => { throw new Error('refresh boom'); }, noopLog);
|
|
32
|
+
// Must resolve (not reject) and not return a Response.
|
|
33
|
+
const out = await hook(new Request('https://api.anthropic.com/v1/messages'));
|
|
34
|
+
assert.equal(out, undefined);
|
|
35
|
+
});
|
|
36
|
+
//# sourceMappingURL=credential-hooks.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-hooks.test.js","sourceRoot":"","sources":["../../../tests/shell/credential-hooks.test.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,gFAAgF;AAChF,mFAAmF;AACnF,uFAAuF;AACvF,uFAAuF;AACvF,4EAA4E;AAE5E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AAExC,OAAO,EAAE,mBAAmB,EAAE,MAAM,4CAA4C,CAAC;AAGjF,MAAM,OAAO,GAAgB,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC;AAEvD,IAAI,CAAC,8EAA8E,EAAE,KAAK,IAAI,EAAE;IAC9F,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,MAAM,IAAI,GAAG,mBAAmB,CAAC,CAAC,mBAAmB,CAAC,EAAE,KAAK,IAAI,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAC3F,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,IAAI,OAAO,CAAC,uCAAuC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;IACjG,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,oDAAoD,CAAC,CAAC;IAC7E,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,SAAS,EAAE,qEAAqE,CAAC,CAAC;AACtG,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,+DAA+D,EAAE,KAAK,IAAI,EAAE;IAC/E,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,MAAM,IAAI,GAAG,mBAAmB,CAAC,CAAC,eAAe,CAAC,EAAE,KAAK,IAAI,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IACvF,MAAM,IAAI,CAAC,IAAI,OAAO,CAAC,uCAAuC,CAAC,CAAC,CAAC;IACjE,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;AACzB,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,4EAA4E,EAAE,KAAK,IAAI,EAAE;IAC5F,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,MAAM,IAAI,GAAG,mBAAmB,CAAC,CAAC,mBAAmB,CAAC,EAAE,KAAK,IAAI,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAC3F,MAAM,IAAI,CAAC,IAAI,OAAO,CAAC,qCAAqC,CAAC,CAAC,CAAC;IAC/D,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,uDAAuD,CAAC,CAAC;AAClF,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,sEAAsE,EAAE,KAAK,IAAI,EAAE;IACtF,MAAM,IAAI,GAAG,mBAAmB,CAAC,CAAC,mBAAmB,CAAC,EAAE,KAAK,IAAI,EAAE,GAAG,MAAM,IAAI,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IACnH,uDAAuD;IACvD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,IAAI,OAAO,CAAC,uCAAuC,CAAC,CAAC,CAAC;IAC7E,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;AAC/B,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Unit tests for the live per-VM SecretManager registry (issue 214):
|
|
2
|
+
// • the proactive pre-expiry tick is scheduled at `expiresAt - margin`, using the
|
|
3
|
+
// CONFIGURABLE margin, with the default RAISED from the original 60s to 5 min.
|
|
4
|
+
// • refreshAdapter reports success/failure as a boolean (the in-session 401 recovery
|
|
5
|
+
// keys its re-prompt-vs-end decision on it) and dedupes concurrent refreshes.
|
|
6
|
+
// • ensureFreshForRequest (the egress request-side lazy refresh) only refreshes when the
|
|
7
|
+
// cached expiry is KNOWN and within margin.
|
|
8
|
+
import { test } from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { CredentialSecretRegistry } from '../../src/shell/interp/credential-registry.js';
|
|
11
|
+
const noopLog = { emit: () => undefined };
|
|
12
|
+
/** A no-op SecretManager that just records the latest pushed value. */
|
|
13
|
+
function fakeManager() {
|
|
14
|
+
const m = {
|
|
15
|
+
value: null,
|
|
16
|
+
updateSecret(_name, opts) { m.value = opts.value; },
|
|
17
|
+
listSecrets() { return []; },
|
|
18
|
+
};
|
|
19
|
+
return m;
|
|
20
|
+
}
|
|
21
|
+
/** Build a registry with injected clock + a capturing setTimer (no real timers fire). */
|
|
22
|
+
function buildRegistry(opts) {
|
|
23
|
+
const timers = [];
|
|
24
|
+
let readCount = 0;
|
|
25
|
+
let refreshCount = 0;
|
|
26
|
+
const registry = new CredentialSecretRegistry({
|
|
27
|
+
readToken: async () => { readCount++; opts.onReadToken?.(); return opts.token; },
|
|
28
|
+
refresh: opts.refresh ?? (async () => { refreshCount++; }),
|
|
29
|
+
log: noopLog,
|
|
30
|
+
now: () => opts.now,
|
|
31
|
+
setTimer: (cb, delay) => { timers.push({ cb, delay }); return { unref() { } }; },
|
|
32
|
+
clearTimer: () => undefined,
|
|
33
|
+
refreshMarginMs: opts.refreshMarginMs,
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
registry,
|
|
37
|
+
timers,
|
|
38
|
+
counts: () => ({ readCount, refreshCount: opts.refresh ? -1 : refreshCount }),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// ─── proactive schedule: expiresAt - margin, configurable, default raised from 60s ──
|
|
42
|
+
test('proactive refresh is scheduled at expiresAt - margin (configurable margin)', async () => {
|
|
43
|
+
const now = 1_000_000;
|
|
44
|
+
const expiresAtMs = now + 600_000; // 10 min out
|
|
45
|
+
const { registry, timers } = buildRegistry({
|
|
46
|
+
now, token: { accessToken: 'tok', expiresAtMs }, refreshMarginMs: 120_000,
|
|
47
|
+
});
|
|
48
|
+
await registry.register({ manager: fakeManager(), secretName: 'S', adapterId: 'claude' });
|
|
49
|
+
assert.equal(timers.length, 1, 'one proactive timer is armed');
|
|
50
|
+
// delay = expiresAt - now - margin = 600_000 - 120_000 = 480_000
|
|
51
|
+
assert.equal(timers[0].delay, 480_000);
|
|
52
|
+
});
|
|
53
|
+
test('the default refresh margin is RAISED from 60s to 5 min', async () => {
|
|
54
|
+
const now = 1_000_000;
|
|
55
|
+
const expiresAtMs = now + 600_000; // 10 min out
|
|
56
|
+
// No refreshMarginMs → registry default. Under the OLD 60s margin the delay would be
|
|
57
|
+
// 540_000; under the new 5-min default it is 300_000.
|
|
58
|
+
const { registry, timers } = buildRegistry({ now, token: { accessToken: 'tok', expiresAtMs } });
|
|
59
|
+
await registry.register({ manager: fakeManager(), secretName: 'S', adapterId: 'claude' });
|
|
60
|
+
assert.equal(timers[0].delay, 300_000, 'default margin is 5 min (300_000), not the old 60s');
|
|
61
|
+
assert.notEqual(timers[0].delay, 540_000, 'the old 60s-margin delay is gone');
|
|
62
|
+
});
|
|
63
|
+
test('proactive delay floors at 0 when the token is already inside the margin', async () => {
|
|
64
|
+
const now = 1_000_000;
|
|
65
|
+
const expiresAtMs = now + 10_000; // 10s out — already well inside a 5-min margin
|
|
66
|
+
const { registry, timers } = buildRegistry({ now, token: { accessToken: 'tok', expiresAtMs } });
|
|
67
|
+
await registry.register({ manager: fakeManager(), secretName: 'S', adapterId: 'claude' });
|
|
68
|
+
assert.equal(timers[0].delay, 0, 'a past-margin token schedules an immediate refresh');
|
|
69
|
+
});
|
|
70
|
+
// ─── refreshAdapter: success/failure boolean + concurrency dedup ────────────────────
|
|
71
|
+
test('refreshAdapter returns true when a fresh token is obtained', async () => {
|
|
72
|
+
const { registry } = buildRegistry({ now: 0, token: { accessToken: 'fresh', expiresAtMs: 9_999_999 } });
|
|
73
|
+
assert.equal(await registry.refreshAdapter('claude'), true);
|
|
74
|
+
});
|
|
75
|
+
test('refreshAdapter returns false on a genuine auth failure (refresh threw, no token)', async () => {
|
|
76
|
+
const { registry } = buildRegistry({
|
|
77
|
+
now: 0,
|
|
78
|
+
token: null, // re-read yields nothing (revoked refresh token)
|
|
79
|
+
refresh: async () => { throw new Error('claude exited with code 1'); },
|
|
80
|
+
});
|
|
81
|
+
assert.equal(await registry.refreshAdapter('claude'), false);
|
|
82
|
+
});
|
|
83
|
+
test('refreshAdapter returns false when the refresh ITSELF failed, even if a stale token is re-read', async () => {
|
|
84
|
+
// The refresh spawn threw (revoked token) but a (stale) token is still on disk: the
|
|
85
|
+
// recovery decision keys on the refresh failing, so this still reports false → end attempt.
|
|
86
|
+
const { registry } = buildRegistry({
|
|
87
|
+
now: 0,
|
|
88
|
+
token: { accessToken: 'stale', expiresAtMs: 1 },
|
|
89
|
+
refresh: async () => { throw new Error('refresh failed'); },
|
|
90
|
+
});
|
|
91
|
+
assert.equal(await registry.refreshAdapter('claude'), false);
|
|
92
|
+
});
|
|
93
|
+
test('refreshAdapter dedupes concurrent calls into ONE host refresh round-trip', async () => {
|
|
94
|
+
let refreshCount = 0;
|
|
95
|
+
let release;
|
|
96
|
+
const gate = new Promise((r) => { release = r; });
|
|
97
|
+
const registry = new CredentialSecretRegistry({
|
|
98
|
+
readToken: async () => ({ accessToken: 'tok', expiresAtMs: 9_999_999 }),
|
|
99
|
+
refresh: async () => { refreshCount++; await gate; },
|
|
100
|
+
log: noopLog,
|
|
101
|
+
now: () => 0,
|
|
102
|
+
setTimer: () => ({ unref() { } }),
|
|
103
|
+
clearTimer: () => undefined,
|
|
104
|
+
});
|
|
105
|
+
const a = registry.refreshAdapter('claude');
|
|
106
|
+
const b = registry.refreshAdapter('claude');
|
|
107
|
+
release();
|
|
108
|
+
const [ra, rb] = await Promise.all([a, b]);
|
|
109
|
+
assert.equal(refreshCount, 1, 'two concurrent refreshes collapse to one spawn');
|
|
110
|
+
assert.equal(ra, true);
|
|
111
|
+
assert.equal(rb, true);
|
|
112
|
+
// After settling a fresh refresh runs again (the in-flight slot was released).
|
|
113
|
+
await registry.refreshAdapter('claude');
|
|
114
|
+
assert.equal(refreshCount, 2);
|
|
115
|
+
});
|
|
116
|
+
// ─── ensureFreshForRequest: expiry-gated egress request-side lazy refresh ────────────
|
|
117
|
+
test('ensureFreshForRequest refreshes when the cached token is within margin', async () => {
|
|
118
|
+
const now = 1_000_000;
|
|
119
|
+
const expiresAtMs = now + 60_000; // 60s out — inside the 5-min default margin
|
|
120
|
+
let refreshCount = 0;
|
|
121
|
+
const registry = new CredentialSecretRegistry({
|
|
122
|
+
readToken: async () => ({ accessToken: 'tok', expiresAtMs }),
|
|
123
|
+
refresh: async () => { refreshCount++; },
|
|
124
|
+
log: noopLog,
|
|
125
|
+
now: () => now,
|
|
126
|
+
setTimer: () => ({ unref() { } }),
|
|
127
|
+
clearTimer: () => undefined,
|
|
128
|
+
});
|
|
129
|
+
// register seeds the cached expiry (within margin) without firing the captured timer.
|
|
130
|
+
await registry.register({ manager: fakeManager(), secretName: 'S', adapterId: 'claude' });
|
|
131
|
+
await registry.ensureFreshForRequest('claude');
|
|
132
|
+
assert.equal(refreshCount, 1, 'a within-margin egress request awaits a refresh');
|
|
133
|
+
});
|
|
134
|
+
test('ensureFreshForRequest is a no-op when the token is comfortably fresh', async () => {
|
|
135
|
+
const now = 1_000_000;
|
|
136
|
+
const expiresAtMs = now + 3_600_000; // 1h out — well outside the margin
|
|
137
|
+
let refreshCount = 0;
|
|
138
|
+
const registry = new CredentialSecretRegistry({
|
|
139
|
+
readToken: async () => ({ accessToken: 'tok', expiresAtMs }),
|
|
140
|
+
refresh: async () => { refreshCount++; },
|
|
141
|
+
log: noopLog,
|
|
142
|
+
now: () => now,
|
|
143
|
+
setTimer: () => ({ unref() { } }),
|
|
144
|
+
clearTimer: () => undefined,
|
|
145
|
+
});
|
|
146
|
+
await registry.register({ manager: fakeManager(), secretName: 'S', adapterId: 'claude' });
|
|
147
|
+
await registry.ensureFreshForRequest('claude');
|
|
148
|
+
assert.equal(refreshCount, 0, 'a fresh token does not trigger a refresh on the request path');
|
|
149
|
+
});
|
|
150
|
+
test('ensureFreshForRequest is a no-op when expiry is unknown (left to the ticker)', async () => {
|
|
151
|
+
const now = 1_000_000;
|
|
152
|
+
let refreshCount = 0;
|
|
153
|
+
const registry = new CredentialSecretRegistry({
|
|
154
|
+
readToken: async () => ({ accessToken: 'tok', expiresAtMs: null }),
|
|
155
|
+
refresh: async () => { refreshCount++; },
|
|
156
|
+
log: noopLog,
|
|
157
|
+
now: () => now,
|
|
158
|
+
setTimer: () => ({ unref() { } }),
|
|
159
|
+
clearTimer: () => undefined,
|
|
160
|
+
});
|
|
161
|
+
await registry.register({ manager: fakeManager(), secretName: 'S', adapterId: 'claude' });
|
|
162
|
+
await registry.ensureFreshForRequest('claude');
|
|
163
|
+
assert.equal(refreshCount, 0, 'unknown expiry never hammers the host per-request');
|
|
164
|
+
});
|
|
165
|
+
//# sourceMappingURL=credential-registry.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-registry.test.js","sourceRoot":"","sources":["../../../tests/shell/credential-registry.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// FCIS rewrite — integration tests for the credential-egress substrate wiring
|
|
2
|
+
// (src/shell/main-credential.ts), the way the composition root composes it.
|
|
3
|
+
//
|
|
4
|
+
// Wires the REAL pure core deciders into buildCredentialSubstrate (mirroring
|
|
5
|
+
// main.ts's CREDENTIAL_CORE_DEPS), then proves the load-bearing invariants:
|
|
6
|
+
// (a) the guest spec carries a token-SHAPED PLACEHOLDER (sk-ant-/JWT/gho_), NEVER
|
|
7
|
+
// the real token; the REAL token is registered for EGRESS SUBSTITUTION only
|
|
8
|
+
// (secrets[].hosts via createHttpHooks);
|
|
9
|
+
// (b) the per-VM request-hook chain INCLUDES the PR#119 codex chatgpt-backend
|
|
10
|
+
// route swap (host→chatgpt.com, /v1→/backend-api/codex, chatgpt-account-id);
|
|
11
|
+
// (c) NO onResponse hook is registered (issue-135 streaming regression).
|
|
12
|
+
import { test } from 'node:test';
|
|
13
|
+
import assert from 'node:assert/strict';
|
|
14
|
+
import { buildCredentialSubstrate } from '../../src/shell/main-credential.js';
|
|
15
|
+
// REAL pure core credential deciders (the only place core is wired — like main.ts).
|
|
16
|
+
import { extractClaudeToken, extractCodexToken, codexEnvFallback, } from '../../src/core/credential/extract.js';
|
|
17
|
+
import { extractClaudeIdentity, extractCodexMetadata } from '../../src/core/credential/identity.js';
|
|
18
|
+
import { buildAdapterCredentialSpecWithPlaceholder, buildAdapterHooksConfig, codexUpstreamRoute, } from '../../src/core/credential/shape.js';
|
|
19
|
+
import { buildGondolinFakeCreds } from '../../src/core/credential/fake-creds.js';
|
|
20
|
+
import { buildStagedConfigs } from '../../src/core/credential/adapter-config.js';
|
|
21
|
+
const noopLog = { emit: () => undefined, withIssue() { return noopLog; } };
|
|
22
|
+
// Deterministic RandomSource so the placeholder is exact-matchable.
|
|
23
|
+
let seq = 0;
|
|
24
|
+
const fixedRandom = { newToken: () => `RAND-${++seq}` };
|
|
25
|
+
const CORE = {
|
|
26
|
+
pure: {
|
|
27
|
+
extractClaudeToken, extractCodexToken, codexEnvFallback, extractClaudeIdentity,
|
|
28
|
+
extractCodexMetadata,
|
|
29
|
+
},
|
|
30
|
+
buildAdapterCredentialSpecWithPlaceholder,
|
|
31
|
+
buildAdapterHooksConfig,
|
|
32
|
+
buildGondolinFakeCreds,
|
|
33
|
+
buildStagedConfigs,
|
|
34
|
+
codexUpstreamRoute,
|
|
35
|
+
};
|
|
36
|
+
function build(env = {}) {
|
|
37
|
+
seq = 0;
|
|
38
|
+
return buildCredentialSubstrate({
|
|
39
|
+
core: CORE, random: fixedRandom, tickerIntervalMs: () => 0, refreshMarginMs: () => 300_000,
|
|
40
|
+
requiredAdapters: () => new Set(['claude', 'codex']), env, log: noopLog,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// A real ChatGPT-OAuth codex credential (the PR#119 case).
|
|
44
|
+
const CODEX_OAUTH_TOKEN = {
|
|
45
|
+
accessToken: 'REAL-SECRET-codex-oauth-access-token',
|
|
46
|
+
expiresAtMs: null,
|
|
47
|
+
chatgptOAuth: true,
|
|
48
|
+
chatgptAccountId: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
|
|
49
|
+
};
|
|
50
|
+
test('(a) claude: guest is staged a token-SHAPED placeholder, never the real token', () => {
|
|
51
|
+
const sub = build();
|
|
52
|
+
const spec = sub.buildAdapterSpec('claude');
|
|
53
|
+
// The placeholder is token-SHAPED (sk-ant-) — NOT a real token.
|
|
54
|
+
assert.match(spec.placeholder, /^sk-ant-/);
|
|
55
|
+
assert.equal(spec.secretName, 'ANTHROPIC_AUTH_TOKEN');
|
|
56
|
+
// The fake-creds the guest receives carry the placeholder VERBATIM as the bearer,
|
|
57
|
+
// and the env entry is keyed under the secret name with the placeholder value.
|
|
58
|
+
const fake = sub.buildFakeCreds({
|
|
59
|
+
adapter: 'claude', placeholder: spec.placeholder, secretName: spec.secretName,
|
|
60
|
+
claudeIdentity: { accountUuid: 'u', organizationUuid: 'o' }, codexIdentity: null,
|
|
61
|
+
});
|
|
62
|
+
assert.equal(fake.env[spec.secretName], spec.placeholder);
|
|
63
|
+
const creds = fake.files.find((f) => f.guestPath.endsWith('.credentials.json'));
|
|
64
|
+
assert.ok(creds, 'a claude creds file is staged');
|
|
65
|
+
const parsed = JSON.parse(creds.content);
|
|
66
|
+
assert.equal(parsed.claudeAiOauth.accessToken, spec.placeholder); // placeholder, not the real token
|
|
67
|
+
// The refresh token staged into the guest is junk (the real refresh token stays host-side).
|
|
68
|
+
assert.ok(!/^sk-ant-/.test(parsed.claudeAiOauth.refreshToken));
|
|
69
|
+
assert.match(parsed.claudeAiOauth.refreshToken, /placeholder/i);
|
|
70
|
+
});
|
|
71
|
+
test('(a) the REAL token is registered for egress substitution ONLY, never the guest', () => {
|
|
72
|
+
const sub = build();
|
|
73
|
+
const spec = sub.buildAdapterSpec('claude');
|
|
74
|
+
const hooksConfig = sub.buildHooksConfig([spec], ['registry.npmjs.org']);
|
|
75
|
+
// createHttpHooks result: the placeholder is the GUEST-visible env value; the real
|
|
76
|
+
// token is seeded into the SecretManager (the egress-substitution slot) only.
|
|
77
|
+
const realToken = { accessToken: 'REAL-SECRET-claude-token', expiresAtMs: null };
|
|
78
|
+
const result = sub.buildPerVmHooks({
|
|
79
|
+
adapterId: 'claude', secretName: spec.secretName, placeholder: spec.placeholder,
|
|
80
|
+
substitutionHosts: spec.substitutionHosts, allowedHosts: hooksConfig.allowedHosts, token: realToken,
|
|
81
|
+
});
|
|
82
|
+
// The guest-visible env carries the PLACEHOLDER, never the real token.
|
|
83
|
+
assert.equal(result.env[spec.secretName], spec.placeholder);
|
|
84
|
+
assert.ok(!Object.values(result.env).includes(realToken.accessToken));
|
|
85
|
+
// The secret is scoped to the credential's substitution hosts (api.anthropic.com)
|
|
86
|
+
// — NOT the general egress allowlist (registry.npmjs.org gets plain egress only).
|
|
87
|
+
const secret = result.secretManager.listSecrets().find((s) => s.name === spec.secretName);
|
|
88
|
+
assert.ok(secret, 'the secret is registered with the SecretManager');
|
|
89
|
+
assert.deepEqual(secret.hosts, ['api.anthropic.com']);
|
|
90
|
+
assert.ok(!secret.hosts.includes('registry.npmjs.org'));
|
|
91
|
+
// The firewall (allowedHosts) is the UNION; the substitution scope is not.
|
|
92
|
+
assert.ok(result.allowedHosts.includes('registry.npmjs.org'));
|
|
93
|
+
assert.ok(result.allowedHosts.includes('api.anthropic.com'));
|
|
94
|
+
});
|
|
95
|
+
test('(b) the request-hook chain swaps the codex chatgpt-backend upstream route (PR#119)', async () => {
|
|
96
|
+
const sub = build();
|
|
97
|
+
const spec = sub.buildAdapterSpec('codex', CODEX_OAUTH_TOKEN.chatgptAccountId);
|
|
98
|
+
const result = sub.buildPerVmHooks({
|
|
99
|
+
adapterId: 'codex', secretName: spec.secretName, placeholder: spec.placeholder,
|
|
100
|
+
substitutionHosts: spec.substitutionHosts, allowedHosts: ['chatgpt.com'], token: CODEX_OAUTH_TOKEN,
|
|
101
|
+
});
|
|
102
|
+
assert.ok(result.httpHooks.onRequest, 'a request hook is wired');
|
|
103
|
+
const req = new Request('https://api.openai.com/v1/responses', { method: 'POST', headers: { 'content-type': 'application/json' } });
|
|
104
|
+
const out = await result.httpHooks.onRequest(req);
|
|
105
|
+
// The chain rewrote the request to the ChatGPT backend (the OAuth subscription
|
|
106
|
+
// token is rejected by the metered platform API).
|
|
107
|
+
assert.ok(out instanceof Request, 'the codex route hook returns a rewritten Request');
|
|
108
|
+
const rewritten = out;
|
|
109
|
+
const url = new URL(rewritten.url);
|
|
110
|
+
assert.equal(url.hostname, 'chatgpt.com');
|
|
111
|
+
assert.equal(url.pathname, '/backend-api/codex/responses');
|
|
112
|
+
assert.equal(rewritten.headers.get('chatgpt-account-id'), CODEX_OAUTH_TOKEN.chatgptAccountId);
|
|
113
|
+
});
|
|
114
|
+
test('(b) a ChatGPT-OAuth codex token leaves NON-platform egress (npm/nodejs) untouched — regression for the codex-VM mise-install 403 (docs/research/codex-route-hook-host-rewrite-rca.md)', async () => {
|
|
115
|
+
const sub = build();
|
|
116
|
+
const spec = sub.buildAdapterSpec('codex', CODEX_OAUTH_TOKEN.chatgptAccountId);
|
|
117
|
+
const result = sub.buildPerVmHooks({
|
|
118
|
+
adapterId: 'codex', secretName: spec.secretName, placeholder: spec.placeholder,
|
|
119
|
+
substitutionHosts: spec.substitutionHosts,
|
|
120
|
+
allowedHosts: ['chatgpt.com', 'registry.npmjs.org', 'nodejs.org'], token: CODEX_OAUTH_TOKEN,
|
|
121
|
+
});
|
|
122
|
+
// The route swap must redirect ONLY the metered platform host. Under the SAME OAuth
|
|
123
|
+
// token that rewrites api.openai.com → chatgpt.com (test above), a `mise install`
|
|
124
|
+
// egress to the npm registry / nodejs dist index must NOT be rewritten — else it
|
|
125
|
+
// lands on chatgpt.com and 403s. The hook passes them through (void) or, if the
|
|
126
|
+
// gondolin wrapper returns a Request, keeps the ORIGIN host (never chatgpt.com).
|
|
127
|
+
for (const target of [
|
|
128
|
+
'https://registry.npmjs.org/@anthropic-ai%2fclaude-code',
|
|
129
|
+
'https://nodejs.org/dist/index.json',
|
|
130
|
+
]) {
|
|
131
|
+
const out = await result.httpHooks.onRequest(new Request(target));
|
|
132
|
+
if (out instanceof Request) {
|
|
133
|
+
assert.equal(new URL(out.url).hostname, new URL(target).hostname);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
test('(b) an API-key (non-OAuth) codex credential leaves the platform route unchanged', async () => {
|
|
138
|
+
const sub = build();
|
|
139
|
+
const spec = sub.buildAdapterSpec('codex');
|
|
140
|
+
const apiKeyToken = { accessToken: 'sk-real-api-key', expiresAtMs: null };
|
|
141
|
+
const result = sub.buildPerVmHooks({
|
|
142
|
+
adapterId: 'codex', secretName: spec.secretName, placeholder: spec.placeholder,
|
|
143
|
+
substitutionHosts: spec.substitutionHosts, allowedHosts: ['chatgpt.com'], token: apiKeyToken,
|
|
144
|
+
});
|
|
145
|
+
const req = new Request('https://api.openai.com/v1/responses', { method: 'POST' });
|
|
146
|
+
const out = await result.httpHooks.onRequest(req);
|
|
147
|
+
// No OAuth → null route → the codex route hook does NOT rewrite: the platform host
|
|
148
|
+
// + path are unchanged (gondolin's wrapper still returns a Request after its own
|
|
149
|
+
// secret-substitution pass, but our route swap left the URL alone).
|
|
150
|
+
if (out instanceof Request) {
|
|
151
|
+
const url = new URL(out.url);
|
|
152
|
+
assert.equal(url.hostname, 'api.openai.com');
|
|
153
|
+
assert.equal(url.pathname, '/v1/responses');
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
test('(c) NO onResponse hook is ever registered (issue-135 streaming regression guard)', () => {
|
|
157
|
+
const sub = build();
|
|
158
|
+
for (const adapter of ['claude', 'codex']) {
|
|
159
|
+
const spec = sub.buildAdapterSpec(adapter);
|
|
160
|
+
const token = adapter === 'codex' ? CODEX_OAUTH_TOKEN : { accessToken: 'real', expiresAtMs: null };
|
|
161
|
+
const result = sub.buildPerVmHooks({
|
|
162
|
+
adapterId: adapter, secretName: spec.secretName, placeholder: spec.placeholder,
|
|
163
|
+
substitutionHosts: spec.substitutionHosts, allowedHosts: [...spec.substitutionHosts], token,
|
|
164
|
+
});
|
|
165
|
+
assert.equal(result.httpHooks.onResponse, undefined, `${adapter}: onResponse must be unset`);
|
|
166
|
+
assert.ok(result.httpHooks.onRequest, `${adapter}: onRequest IS wired`);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
test('the credential port + registry expose the host-only refresh surface', () => {
|
|
170
|
+
const sub = build();
|
|
171
|
+
// The port is the host-only CredentialPort; the registry is the runner's
|
|
172
|
+
// SecretRegistry (register/deregister); the ticker has start/stop.
|
|
173
|
+
assert.equal(typeof sub.port.probe, 'function');
|
|
174
|
+
assert.equal(typeof sub.port.refresh, 'function');
|
|
175
|
+
assert.equal(typeof sub.registry.register, 'function');
|
|
176
|
+
assert.equal(typeof sub.ticker.start, 'function');
|
|
177
|
+
assert.equal(typeof sub.ticker.stop, 'function');
|
|
178
|
+
});
|
|
179
|
+
//# sourceMappingURL=credential-substrate.test.js.map
|