forgedock 1.29.1 → 1.30.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/bin/engine/outcome-codes.mjs +4 -2
- package/bin/engine/phases.mjs +20 -3
- package/bin/engine-cli.mjs +94 -36
- package/bin/engine.mjs +3 -1
- package/bin/lean/batch.test.mjs +62 -1
- package/bin/lean/conformance.mjs +129 -8
- package/bin/lean/conformance.test.mjs +77 -3
- package/bin/lean/context.mjs +17 -12
- package/bin/lean/context.test.mjs +36 -5
- package/bin/lean/effects.mjs +20 -6
- package/bin/lean/enumerating.mjs +189 -0
- package/bin/lean/enumerating.test.mjs +97 -0
- package/bin/lean/lean.mjs +189 -43
- package/bin/lean/lean.test.mjs +305 -6
- package/bin/lean/orchestrate.mjs +104 -85
- package/bin/lean/precheck.mjs +55 -0
- package/bin/lean/precheck.test.mjs +93 -0
- package/bin/lean/prefilter.mjs +21 -4
- package/bin/lean/prefilter.test.mjs +24 -1
- package/bin/lean/preflight.mjs +133 -0
- package/bin/lean/preflight.test.mjs +61 -0
- package/bin/lean/prompts.mjs +37 -3
- package/bin/lean/recovery.test.mjs +75 -0
- package/bin/lean/sequence.mjs +102 -0
- package/bin/lean/sequence.test.mjs +112 -0
- package/bin/lean/testmap.mjs +43 -6
- package/bin/lean/testmap.test.mjs +38 -1
- package/bin/lean/verify.mjs +7 -2
- package/bin/lean/verify.test.mjs +8 -2
- package/commands/orchestrate.md +3 -1
- package/docs/CONFIG.md +8 -1
- package/docs/OUTCOME-CODES.md +6 -4
- package/docs/RELEASE-NOTES.md +10 -0
- package/package.json +1 -1
- package/scripts/acceptance-red-green.sh +13 -4
- package/scripts/acceptance-red-green.test.sh +14 -0
- package/scripts/test-map.sh +63 -17
- package/scripts/test-map.test.sh +55 -0
package/bin/lean/verify.test.mjs
CHANGED
|
@@ -22,9 +22,9 @@ function fake(o = {}) {
|
|
|
22
22
|
|
|
23
23
|
describe("verify-by-CI config (#4167)", () => {
|
|
24
24
|
it("parseVerify defaults to local and rejects invalid values", () => {
|
|
25
|
-
assert.deepEqual(parseVerify({}), { mode: "local", workflow: null, pathsInput: null, skipPaths: [...DEFAULT_SKIP_PATHS], maxRounds: 2, precheckTimeoutSeconds: 300, checkRun: false });
|
|
25
|
+
assert.deepEqual(parseVerify({}), { mode: "local", workflow: null, pathsInput: null, skipPaths: [...DEFAULT_SKIP_PATHS], maxRounds: 2, precheckTimeoutSeconds: 300, precheckTotalSeconds: 600, checkRun: false });
|
|
26
26
|
const v = parseVerify({ mode: "ci", workflow: ".github/workflows/verify.yml", paths_input: "changed_paths", skip_paths: "*.md, docs/** ,", max_rounds: "3" });
|
|
27
|
-
assert.deepEqual(v, { mode: "ci", workflow: ".github/workflows/verify.yml", pathsInput: "changed_paths", skipPaths: ["*.md", "docs/**"], maxRounds: 3, precheckTimeoutSeconds: 300, checkRun: false });
|
|
27
|
+
assert.deepEqual(v, { mode: "ci", workflow: ".github/workflows/verify.yml", pathsInput: "changed_paths", skipPaths: ["*.md", "docs/**"], maxRounds: 3, precheckTimeoutSeconds: 300, precheckTotalSeconds: 600, checkRun: false });
|
|
28
28
|
assert.equal(parseVerify({ mode: "remote" }).mode, "local");
|
|
29
29
|
assert.equal(parseVerify({ workflow: "x; rm -rf /" }).workflow, null);
|
|
30
30
|
assert.equal(parseVerify({ max_rounds: "-1" }).maxRounds, 2);
|
|
@@ -122,6 +122,12 @@ describe("forgedock/verify check run (#4270)", () => {
|
|
|
122
122
|
const result = { validation: [{ id: "V1", exit: 0 }, { id: "V2", ci: true }], requirements: [{ id: "R1", status: "implemented" }] };
|
|
123
123
|
const out = (r) => JSON.parse(/```json\n([\s\S]*)\n```/.exec(r.text)[1]);
|
|
124
124
|
|
|
125
|
+
it("#4318 parseVerify precheck_total_seconds: default 600, integer 1-7200 accepted, others fall back", () => {
|
|
126
|
+
assert.equal(parseVerify({}).precheckTotalSeconds, 600);
|
|
127
|
+
assert.equal(parseVerify({ precheck_total_seconds: "1800" }).precheckTotalSeconds, 1800);
|
|
128
|
+
assert.equal(parseVerify({ precheck_total_seconds: "7200" }).precheckTotalSeconds, 7200);
|
|
129
|
+
for (const bad of ["0", "-5", "1.5", "abc", "7201"]) assert.equal(parseVerify({ precheck_total_seconds: bad }).precheckTotalSeconds, 600, bad);
|
|
130
|
+
});
|
|
125
131
|
it("parseVerify: check_run is true only for the literal true; default false", () => {
|
|
126
132
|
assert.equal(parseVerify({}).checkRun, false);
|
|
127
133
|
assert.equal(parseVerify({ check_run: "true" }).checkRun, true);
|
package/commands/orchestrate.md
CHANGED
|
@@ -141,7 +141,9 @@ When a lane finishes (agent lanes; the batch runner applies the same rules itsel
|
|
|
141
141
|
wait on the sub-issues.
|
|
142
142
|
- `merged` or `invalid`: start the next ready issue.
|
|
143
143
|
- `needs-human` with a **recoverable code**: `ci.red`, `base.conflict` (merge conflict), `ci-pending` on a conflicting
|
|
144
|
-
PR, `contract.invalid` caused by an issue edited mid-run,
|
|
144
|
+
PR, `contract.invalid` caused by an issue edited mid-run, `builder.block_untyped` (an in-scope technical choice),
|
|
145
|
+
and the environment codes `env.tool_missing` (a validate command the host cannot run) and `env.repro_unrunnable`
|
|
146
|
+
(the repro harness could not run the repro test).
|
|
145
147
|
These are mechanical. **Do not ask for confirmation on them.** The batch runner recovers each one itself
|
|
146
148
|
(`orchestration.recovery.max_attempts`, default 1 per issue). It launches one recovery lane under the same lease, lane
|
|
147
149
|
and admission rules as any lane, with `run-issue --recover <code> [--recover-attempt n]` (core path only:
|
package/docs/CONFIG.md
CHANGED
|
@@ -590,11 +590,14 @@ Settings of the core work-on (`bin/lean/`) that change its contract.
|
|
|
590
590
|
```yaml
|
|
591
591
|
lean:
|
|
592
592
|
repro_test: prefer # off | prefer (default)
|
|
593
|
+
precheck:
|
|
594
|
+
max_minutes: 10 # optional pre-PR check round budget in minutes (1-120)
|
|
593
595
|
```
|
|
594
596
|
|
|
595
597
|
| Field | Type | Required | Description |
|
|
596
598
|
|-------|------|----------|-------------|
|
|
597
|
-
| `repro_test` | string | No | `prefer` (default): on an issue labelled `bug` or `type:bug`, the plan step must freeze a `repro_test` in the contract (`{"path","id","command"}`) when the bug is testable. Before review, the engine runs `scripts/acceptance-red-green.sh` for that command on the host in every verify mode: one targeted test, red on the base (head's changed test files overlaid) and green on head.
|
|
599
|
+
| `repro_test` | string | No | `prefer` (default): on an issue labelled `bug` or `type:bug`, the plan step must freeze a `repro_test` in the contract (`{"path","id","command"}`) when the bug is testable. Before review, the engine runs `scripts/acceptance-red-green.sh` for that command on the host in every verify mode: one targeted test, red on the base (head's changed test files overlaid) and green on head. A test that ran but is not red on the base and green on head fails the precheck as `conformance.unimplemented` (it blocks in shadow too) and goes back to the builder for one in-session fix round. A head the harness could not run at all (timeout, missing runner or dependencies) ends `needs-human` with the recoverable code `env.repro_unrunnable`, not a builder round: the batch runner's recovery lane reruns it through the repo's own toolchain (see docs/OUTCOME-CODES.md). The harness runs with the lane env (`TMPDIR`, toolchain) and the workspace dependency trees, and a repro test file that is new on head is run against the base sources. A bug that is not testable (specs, docs, workflows, environment) records `"repro_test":"none"` with a one-line `repro_test_reason` and the lane continues as before. `off` gives exactly the previous contract and precheck. Each proof emits a `REPRO_RED_GREEN` run-log event (`base`, `head`) and adds a trajectory decision. |
|
|
600
|
+
| `precheck.max_minutes` | number | No | Total budget in minutes (1–120) of one round of pre-PR check commands (#4331). The flat form `lean.precheck_max_minutes` also works and wins. When unset, `verify.precheck_total_seconds` applies (default 600 s). Check commands run change-scoped (a learned command with a test-map entry runs only when its globs match the changed files; unmapped commands always run), concurrently up to the host worker cap under one heavy slot, each timed (`ms` in the `PRECHECK_COMMANDS` run-log event). Commands not started when the budget runs out are recorded `deferred-to-ci` (an anomaly, not a failure) and left to CI. |
|
|
598
601
|
|
|
599
602
|
## `verify` (OPTIONAL)
|
|
600
603
|
|
|
@@ -608,6 +611,7 @@ verify:
|
|
|
608
611
|
skip_paths: "*.md,docs/**" # optional, comma-separated; default *.md,*.mdx,*.rst,*.txt,docs/**,devdocs/**
|
|
609
612
|
max_rounds: 2 # optional CI fix rounds before needs-human (default 2)
|
|
610
613
|
precheck_timeout_seconds: 300 # optional per-command timeout of the declared pre-PR checks (default 300)
|
|
614
|
+
precheck_total_seconds: 600 # optional total budget of one pre-PR check round (default 600)
|
|
611
615
|
check_run: true # optional; post the forgedock/verify check run (default false)
|
|
612
616
|
```
|
|
613
617
|
|
|
@@ -619,6 +623,7 @@ verify:
|
|
|
619
623
|
| `skip_paths` | string | No | Comma-separated globs. A glob without `/` matches the basename anywhere; `**` spans directories. When every changed file matches, the lane skips CI verification (a docs-only lane runs no CI matrix). |
|
|
620
624
|
| `max_rounds` | integer | No | Builder fix rounds for red CI (0–10, default 2). Also bounds the merge-time fix rounds and the declared-check fix rounds below, in every mode. |
|
|
621
625
|
| `precheck_timeout_seconds` | integer | No | Timeout of each declared check command (1–3600, default 300); a timed-out command counts as failed (exit 124). |
|
|
626
|
+
| `precheck_total_seconds` | integer | No | Total budget of one round of pre-PR check commands (1–7200, default 600; it was 1800 before 1.30.0). `lean.precheck.max_minutes` takes precedence. Commands not started within the budget are deferred to CI, not failed. |
|
|
622
627
|
| `check_run` | boolean | No | Default `false` (no Checks API call, behaviour unchanged). `true`: after each push of a reviewed head (PR open, base-sync, merge-time fix round) the engine posts one completed `forgedock/verify` check run on that head (forge#4270), `success` iff every deterministic gate the lane already ran passed: declared check commands, undeclared package edges, docs freshness, contract scope, validate ids, requirement status, hotspot growth, amendments. The output carries each gate's id, pass/fail and evidence as JSON; the run log records a `VERIFY` event. Report only: ForgeDock never makes it required and never waits on it (it is ignored by its own CI waits); requiring it is the repository's branch-protection choice. Creating check runs needs a token that may (a GitHub App with `checks: write`); a Checks API failure is logged as an anomaly and never fails the lane. |
|
|
623
628
|
|
|
624
629
|
**Flow** (`ci`/`hybrid`, `bin/lean/verify.mjs` `runCiVerify`):
|
|
@@ -821,6 +826,8 @@ decomposition:
|
|
|
821
826
|
|
|
822
827
|
**With `close_parent: false`** (default): unchanged. The parent stays open and is closed by C3 when the last child closes.
|
|
823
828
|
|
|
829
|
+
The lean (core) path reads the same key (#4337): with `close_parent: true` it closes the parent at decomposition as `not planned`, keeps `workflow:decomposed` and lists the sub-issues in the closing comment; dependents still wait on the sub-issues, and a trusted `FORGE:DECOMPOSED:REVOKED` reopens it. With the default `false` the lean parent stays open.
|
|
830
|
+
|
|
824
831
|
**Limitation**: `--retry` and engine resume refuse closed issues, so a closed decomposed parent cannot be re-run. It has nothing left to run; work on its open children instead.
|
|
825
832
|
|
|
826
833
|
**Commands that use this section**: `work-on` (`decompose` D6.5, `close` C3), `orchestrate`
|
package/docs/OUTCOME-CODES.md
CHANGED
|
@@ -47,6 +47,8 @@ An absent or unknown code is recorded as `unclassified`, with the raw reason kep
|
|
|
47
47
|
| `cli.timeout` | CLI child ran and was killed at the wall-clock limit; a normal attempt (consumes one of `maxAttempts`), usage recorded when the CLI emitted any | engine | `err.code` `CLI_TIMEOUT` |
|
|
48
48
|
| `cli.spawn_failed` | The CLI could not be started for a reason that repeats on every rerun (`E2BIG` argv too large, `ENOENT`/`EACCES`/`ENOEXEC` binary missing or not executable, `ENAMETOOLONG`). Core work-on ends `needs-human` with this code instead of `engine-error`, so `forgedock orchestrate` does not spend its relaunch on it | lean, engine | `err.code` `CLI_SPAWN_FAILED` (E2BIG) or `err.spawnDeterministic` (other errnos keep their `err.code`); `err.spawnErrno` holds the errno |
|
|
49
49
|
| `env.setup_failed` | forge.yaml `env.setup` (run once per fresh lane worktree, before the builder) exited non-zero, timed out or could not start. An environment error, not a build failure: the builder never ran. Core work-on ends `needs-human` with this code instead of `engine-error`, so `forgedock orchestrate` does not relaunch it; fix the setup command or the environment and rerun | lean | `io.runSetup` exit code / spawn error |
|
|
50
|
+
| `env.tool_missing` | Core work-on (forge#4323): a validate command the builder recorded as failed was rerun by the engine and could not run because the host lacks a tool or module (`command not found` / exit 127, Python `No module named x`, Node `Cannot find module|package 'x'` for a bare name the repo does not track). An environment error, not a conformance failure: no builder resume, no reviewer. Ends `needs-human` naming the tool; recoverable by `forgedock orchestrate` (the recovery lane runs the check through the repo's own toolchain). Skipped on a CI-verified head | lean | `gate()` rerun via `io.runCommand`, `{rc, output}` classified by `conformance.mjs` `missingTool` |
|
|
51
|
+
| `env.repro_unrunnable` | Core work-on (forge#4334): on a bug lane the frozen `repro_test` could not be run on head by `scripts/acceptance-red-green.sh` (`HEAD=UNRUNNABLE`: timeout, exit 126/127, unclassifiable output) or the harness returned no result. An environment error, not a conformance failure: no builder resume, no reviewer. Ends `needs-human`; recoverable by `forgedock orchestrate` (the recovery lane reruns the repro command through the repo's own toolchain and installed dependencies). Other non-proofs (passes on base, fails on head, base unrunnable) stay `conformance.unimplemented` | lean | `gate()` `io.redGreen` words, classified by `conformance.mjs` `reproFailures` |
|
|
50
52
|
| `sandbox.start_failed` | A `forgedock orchestrate` lane sandbox (forge.yaml `execution`, provider `container` or `remote`) could not start: bad image, missing endpoint or credential, refused launch. The lane never ran. Final `needs-human` row: not relaunched and not counted by the engine-error circuit breaker | orchestrate | `bin/lean/execution.mjs` `providerLanes`: `provider.start` threw without `retryable` |
|
|
51
53
|
| `sandbox.capacity` | The sandbox provider had no capacity for the lane (HTTP 429/503 from the remote transport). `deferred` with backoff (`Retry-After` when sent), bounded by `orchestration.max_deferrals` | orchestrate | `provider.start` threw with `err.retryable: true` |
|
|
52
54
|
| `lease.held` | Another live agent holds the issue lease | engine | Foreign live lease on acquire |
|
|
@@ -58,7 +60,7 @@ An absent or unknown code is recorded as `unclassified`, with the raw reason kep
|
|
|
58
60
|
| `lane.branch_missing` | Core work-on: the lane branch (e.g. `milestone/<slug>`) does not exist on origin; fail closed | lean | `git ls-remote --heads origin` |
|
|
59
61
|
| `lane.unresolved` | Core work-on: the lane could not be worked out (milestone or default branch unreadable, empty milestone slug, bad `feature_pattern`); fail closed | lean | Failed `gh` read or config |
|
|
60
62
|
| `invalid` | Issue is invalid or in a divergent closed state | investigate, engine | `INVESTIGATION:INVALID` marker, or live snapshot |
|
|
61
|
-
| `decomposed` | Issue was decomposed into sub-issues. In core work-on, the builder reported `block.kind: "split-required"`; the engine files the sub-issues (`**Parent**` and `**Depends on**` lines) and returns them as `subissues` for `forgedock orchestrate` to run | investigate, decompose, lean | `FORGE:DECOMPOSED` or the decomposition assessment; `result.json` `block.subissues` |
|
|
63
|
+
| `decomposed` | Issue was decomposed into sub-issues. In core work-on, the builder reported `block.kind: "split-required"`; the engine files the sub-issues (`**Parent**` and `**Depends on**` lines) and returns them as `subissues` for `forgedock orchestrate` to run. With `decomposition.close_parent: true` the parent is then closed `not planned` (label kept) and its dependents wait on the sub-issues | investigate, decompose, lean | `FORGE:DECOMPOSED` or the decomposition assessment; `result.json` `block.subissues` |
|
|
62
64
|
| `builder.blocked` | Core work-on: the builder reported an external action agents cannot take, or a product decision too critical for agents: `block.kind: "human"` with `why` one of `credentials`, `spend`, `promotion`, `customer`, `product-decision`. An upstream issue, a needed split, or prod data that monitoring access can read never count | lean | `result.json` status `blocked` + typed `block` |
|
|
63
65
|
| `builder.block_untyped` | Core work-on: the builder stayed `blocked` without a valid typed reason (missing `block`, a non-human `why` such as prod data, or a dependency that is not open) after one corrective resume | lean | `result.json` status `blocked`, twice |
|
|
64
66
|
| `builder.no_evidence` | Core work-on: the builder reported invalid/already-fixed without file:line evidence, so the issue was not closed | lean | `result.json` with empty `evidence` |
|
|
@@ -69,7 +71,7 @@ An absent or unknown code is recorded as `unclassified`, with the raw reason kep
|
|
|
69
71
|
| `review.incomplete` | Core work-on: the conformance review twice returned no complete rows (a generic PASS, a missing requirement/scope/preserve row, evidence in a file that does not exist); nothing merged | lean | `checkCompleteness` problems on both reviewer runs |
|
|
70
72
|
| `review.inconsistent` | Core work-on: the reviewer twice said NONCONFORMING without any failing row; the reviewer is re-asked, the builder's resume is not spent | lean | Verdict/row mismatch on both reviewer runs |
|
|
71
73
|
| `conformance.omission` | Core work-on: an issue requirement is missing from or narrowed by the frozen contract, after one builder resume | lean | Review `issue_coverage` row not `covered` |
|
|
72
|
-
| `conformance.unimplemented` | Core work-on: a contracted requirement is partial, missing or unvalidated, or a validate command failed, after one builder resume | lean | Precheck (result.json, validation) or review requirement row |
|
|
74
|
+
| `conformance.unimplemented` | Core work-on: a contracted requirement is partial, missing or unvalidated, or a validate command failed, after one builder resume (a validate the host cannot run for a missing tool or module is `env.tool_missing`, and a repro test the harness cannot run on head is `env.repro_unrunnable`, instead) | lean | Precheck (result.json, validation) or review requirement row |
|
|
73
75
|
| `conformance.scope_creep` | Core work-on: a changed file is not authorized by a deliverable or typed amendment, after one builder resume | lean | Precheck changed files or review `scope` row `none` |
|
|
74
76
|
| `conformance.untracked_deferral` | Core work-on: a requirement was deferred without a typed amendment or a tracking issue, after one builder resume | lean | Precheck or review requirement row `deferred` on a `deliver` requirement |
|
|
75
77
|
| `conformance.unjustified_deferral` | Core work-on: the reviewer judged a deferred or post-deploy requirement unjustified (it belongs in this change), after one builder resume; nothing is filed or merged | lean | Review requirement row `deferral_ok: false` |
|
|
@@ -93,11 +95,11 @@ An absent or unknown code is recorded as `unclassified`, with the raw reason kep
|
|
|
93
95
|
`forgedock orchestrate` recovers mechanical outcomes inside the batch:
|
|
94
96
|
|
|
95
97
|
- **recoverable**:
|
|
96
|
-
- needs-human with `ci.red`, `base.conflict` or `
|
|
98
|
+
- needs-human with `ci.red`, `base.conflict`, `builder.block_untyped`, `env.tool_missing` or `env.repro_unrunnable`;
|
|
97
99
|
- `contract.invalid` when the issue was edited mid-run (`issueEdited`);
|
|
98
100
|
- `ci-pending` on a conflicting PR, recovered as `base.conflict`.
|
|
99
101
|
|
|
100
|
-
The runner relaunches the issue once per `orchestration.recovery.max_attempts` (default 1) as a recovery lane (`run-issue --recover <code>`), under the normal admission rules. That lane removes the `needs-human` label, posts `FORGE:DECISION`, and resumes the open PR: base-sync, then CI fix rounds from the failed-check logs. Dependents stay queued. A merged recovery is reported as `recovered`. A failed one is final, keeps the recovery evidence on the row, and skips its dependents. A failed recovery stays `needs-human`: the lane re-adds the label (with a `FORGE:NEEDS_HUMAN` comment carrying the evidence) only for a final `needs-human` or `ci-pending`, and only when it is the last attempt (`attempt == max_attempts`); a non-final attempt that ends `needs-human` or `ci-pending` without another attempt following is re-labelled once by the batch runner; the batch runner re-adds it, once, for a final `engine-error` or `deferred` after the recovery pass (those may be requeued, so the lane cannot know). `invalid`, `decomposed`, `dependency-*` and `merged` never re-label.
|
|
102
|
+
The runner relaunches the issue once per `orchestration.recovery.max_attempts` (default 1) as a recovery lane (`run-issue --recover <code>`), under the normal admission rules. That lane removes the `needs-human` label, posts `FORGE:DECISION`, and resumes the open PR: base-sync, then CI fix rounds from the failed-check logs. Dependents stay queued. A merged recovery is reported as `recovered`. A failed one is final, keeps the recovery evidence on the row, and skips its dependents. A failed recovery stays `needs-human`: the lane re-adds the label (with a `FORGE:NEEDS_HUMAN` comment carrying the evidence) only for a final `needs-human` or `ci-pending`, and only when it is the last attempt (`attempt == max_attempts`); a non-final attempt that ends `needs-human` or `ci-pending` without another attempt following is re-labelled once by the batch runner; the batch runner re-adds it, once, for a final `engine-error` or `deferred` after the recovery pass (those may be requeued, so the lane cannot know). `invalid`, `decomposed`, `dependency-*` and `merged` never re-label. In-batch recovery is off when `engine.path: full`: the runner passes no recovery to the batch, and `run-issue --path full` rejects `--recover`.
|
|
101
103
|
- **human**: `builder.blocked`, which is only reached by a typed human block: credentials, spend, promotion, customer or product-decision. It is never recovered, it skips dependents, and it is the only class in the report's escalation list.
|
|
102
104
|
- **terminal**: every other code. No recovery runs, dependents are skipped, and the row is reported as `unrecovered`.
|
|
103
105
|
|
package/docs/RELEASE-NOTES.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
Operator-facing notes for behavior changes that need action or awareness. Newest first.
|
|
4
4
|
|
|
5
|
+
## v1.30.0: environment failures are recoverable, scoped pre-PR checks, decomposed parents can close
|
|
6
|
+
|
|
7
|
+
- **New recoverable codes.** A validate command the host cannot run (missing interpreter, module or executable) ends `env.tool_missing`, and a repro test the harness could not run on head ends `env.repro_unrunnable`. Both were terminal `conformance.unimplemented` before. The batch runner recovers them like `ci.red` (see docs/OUTCOME-CODES.md).
|
|
8
|
+
- **Contract freeze is stricter.** Validate and repro commands that read `$TMPDIR` files they do not create are rejected at freeze, and validate commands are preflighted for their interpreters and modules (a miss asks the builder for one contract correction). Engine-run commands now get the builder's lane env (`TMPDIR`, toolchain), and the repro harness uses the workspace dependency trees and runs a new head test file against the base.
|
|
9
|
+
- **Pre-PR checks.** Learned check commands run change-scoped (when a test map covers them), concurrently and timed, within a round budget: `lean.precheck.max_minutes`, else `verify.precheck_total_seconds`, whose default drops from 1800 s to **600 s**. Commands left when the budget runs out are `deferred-to-ci`, not failed. Set `FORGE_HOST_PARALLELISM=1` if your check commands share state (cache files, a test database, a fixed port).
|
|
10
|
+
- **Decomposed parents.** The lean path honours `decomposition.close_parent`: with `true` a decomposed parent closes `not planned` right after decomposition (label kept, sub-issues listed); its dependents still wait on the sub-issues, and a trusted revoke reopens it. Default `false` keeps it open.
|
|
11
|
+
- **Hand-off.** Enumerating tests (exhaustiveness guards) are discovered for the change, and sequence-numbered migrations that collide with the base are renumbered by the builder.
|
|
12
|
+
|
|
13
|
+
Action: set `decomposition.close_parent: true` to close decomposed parents. Raise `lean.precheck.max_minutes` if your pre-PR checks legitimately need more than 10 minutes.
|
|
14
|
+
|
|
5
15
|
## v1.29.0: in-batch recovery and context pack on by default
|
|
6
16
|
|
|
7
17
|
- **In-batch recovery** (`orchestration.recovery.max_attempts`, default 1): `forgedock orchestrate` now launches one recovery lane per issue for a mechanical `needs-human` (`ci.red`, `base.conflict`, `ci-pending` on a conflicting PR, `contract.invalid` after a mid-run issue edit, `builder.block_untyped`). The lane removes `needs-human`, resumes the PR and re-queues the issue. A recovery that ends anything but `merged` re-applies `needs-human` with a recovery-evidence `FORGE:NEEDS_HUMAN` comment.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forgedock",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.0",
|
|
4
4
|
"description": "Autonomous development pipeline for coding agents. GitHub as institutional memory, with native Claude Code and OpenCode entrypoints.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"forgedock": "bin/forgedock.mjs"
|
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
# Usage: acceptance-red-green.sh --repo-path <worktree> --base-ref <ref> --test-cmd "<cmd>" [--timeout <sec>]
|
|
7
7
|
# Runs <cmd> in <worktree> (head) first. Only when head RAN does it create a detached base worktree at
|
|
8
8
|
# `git merge-base HEAD <base-ref>` (under mktemp -d, never inside the repo), overlay head's changed test files
|
|
9
|
-
# onto it,
|
|
9
|
+
# onto it, link head's untracked dependency trees (every node_modules next to a tracked package.json, a root .venv),
|
|
10
|
+
# and run <cmd> there. Each run is classified with classify-test-run.sh.
|
|
10
11
|
# Prints exactly one line on stdout: BASE=<FAILED|RAN|RAN_NOTHING|UNRUNNABLE|SKIPPED> HEAD=<FAILED|RAN|RAN_NOTHING|UNRUNNABLE>
|
|
11
12
|
# FAILED the run executed and failed
|
|
12
13
|
# RAN the run executed and passed
|
|
13
14
|
# RAN_NOTHING the runner exercised nothing
|
|
14
15
|
# UNRUNNABLE could not execute (timeout 124, not executable 126/127, worktree/overlay/classifier failure)
|
|
15
|
-
# SKIPPED base was not run because head did not PASS
|
|
16
|
+
# SKIPPED base was not run because head did not PASS (never a missing file: head's new test files are overlaid)
|
|
16
17
|
# Never prints a verdict on success of cleanup: the base worktree is removed on every exit path (trap + explicit remove).
|
|
17
18
|
# Callers map: HEAD=RAN BASE=FAILED -> proof; BASE=RAN -> passes on base (gate FAIL); anything else that is not a proof -> DEGRADED/FAIL.
|
|
18
19
|
# Exit: 0 line printed | 2 usage error (stdout empty so callers fail closed)
|
|
@@ -81,8 +82,16 @@ while IFS= read -r f; do
|
|
|
81
82
|
[ -f "$REPO/$f" ] && [ ! -L "$REPO/$f" ] || continue # deleted at head, or a symlink: never copied
|
|
82
83
|
mkdir -p "$BASE_WT/$(dirname "$f")" && cp -p "$REPO/$f" "$BASE_WT/$f" 2>/dev/null && [ -f "$BASE_WT/$f" ] || OVERLAY_OK=false
|
|
83
84
|
done <<< "$CHANGED"
|
|
84
|
-
# Untracked dependency
|
|
85
|
-
|
|
85
|
+
# Untracked dependency trees (never in git): link head's so a missing install is not mistaken for a red run (#4334):
|
|
86
|
+
# every node_modules next to a tracked package.json (workspace packages too, e.g. apps/web/node_modules) and a root .venv.
|
|
87
|
+
DEPS=$( { git -C "$REPO" ls-files -- package.json '*/package.json' 2>/dev/null | while IFS= read -r p; do
|
|
88
|
+
d=$(dirname "$p"); [ "$d" = "." ] && echo node_modules || echo "$d/node_modules"; done; echo .venv; } | sort -u )
|
|
89
|
+
while IFS= read -r t; do
|
|
90
|
+
[ -n "$t" ] || continue
|
|
91
|
+
case "$t" in */node_modules/*) continue ;; esac # a package.json inside an installed tree
|
|
92
|
+
[ -d "$REPO/$t" ] && [ ! -e "$BASE_WT/$t" ] && [ ! -L "$BASE_WT/$t" ] || continue
|
|
93
|
+
mkdir -p "$BASE_WT/$(dirname "$t")" 2>/dev/null && ln -s "$REPO/$t" "$BASE_WT/$t" 2>/dev/null
|
|
94
|
+
done <<< "$DEPS"
|
|
86
95
|
|
|
87
96
|
if [ "$OVERLAY_OK" != "true" ]; then BASE_RES=UNRUNNABLE; else BASE_RES=$(run_in "$BASE_WT"); fi
|
|
88
97
|
cleanup # explicit; the EXIT trap is the backstop
|
|
@@ -70,5 +70,19 @@ fi
|
|
|
70
70
|
out=$(bash "$RG" --repo-path "$R1" 2>/dev/null); rc=$?
|
|
71
71
|
{ [ "$rc" -eq 2 ] && [ -z "$out" ]; } && ok "missing args exit 2, empty stdout" || bad "usage: rc=$rc out='$out'"
|
|
72
72
|
|
|
73
|
+
# 11. workspace monorepo (#4334): a new test file in a package needs that package's untracked node_modules and the root
|
|
74
|
+
# .venv; base gets head's trees linked, so it runs and fails on the defect (not 127 on a missing install)
|
|
75
|
+
R5="$TMP/r5"; make_repo "$R5"
|
|
76
|
+
mkdir -p "$R5/apps/web"; printf '{}\n' > "$R5/package.json"; printf '{"name":"web"}\n' > "$R5/apps/web/package.json"
|
|
77
|
+
printf 'node_modules/\n.venv/\n' > "$R5/.gitignore"; git -C "$R5" add -A; git -C "$R5" commit -qm pkgs; git -C "$R5" update-ref refs/remotes/origin/main HEAD
|
|
78
|
+
fix_head "$R5"
|
|
79
|
+
mkdir -p "$R5/apps/web/node_modules/dep" "$R5/.venv/bin"
|
|
80
|
+
printf 'echo dep\n' > "$R5/apps/web/node_modules/dep/run.sh"; printf 'echo venv\n' > "$R5/.venv/bin/tool"
|
|
81
|
+
mkdir -p "$R5/apps/web/__tests__"
|
|
82
|
+
printf 'bash node_modules/dep/run.sh >/dev/null 2>&1 || exit 127\nbash ../../.venv/bin/tool >/dev/null 2>&1 || exit 127\ntest "$(bash ../../app.sh)" = good && echo "1 passed"\n' > "$R5/apps/web/__tests__/t.test.sh"
|
|
83
|
+
git -C "$R5" add -A; git -C "$R5" commit -qm test
|
|
84
|
+
check "workspace node_modules and .venv linked into base; new nested test overlaid" "BASE=FAILED HEAD=RAN" "$R5" "cd apps/web && bash __tests__/t.test.sh"
|
|
85
|
+
[ -d "$R5/apps/web/node_modules/dep" ] && [ -d "$R5/.venv/bin" ] && ok "head dependency trees survive base cleanup" || bad "head dependency trees removed by cleanup"
|
|
86
|
+
|
|
73
87
|
echo "$PASS passed, $FAILN failed"
|
|
74
88
|
[ "$FAILN" -eq 0 ]
|
package/scripts/test-map.sh
CHANGED
|
@@ -27,6 +27,12 @@
|
|
|
27
27
|
# substitution, `${{ }}`, `$GITHUB_*`, redirect or `<(...)`/`>(...)`, a non-check make target, or a fixer form
|
|
28
28
|
# (`--fix`, `--write`, `-w`, `black`/`ruff format`/`gofmt`/`prettier` without their check flag) drops the step (forge#4316). Globs from the tool (python/go/shell/js, under the cwd), else the
|
|
29
29
|
# workflow's `paths:` filter, else `DIR/**` for a cwd; a workflow entry never gets a bare `**`.
|
|
30
|
+
# forge#4318: also dropped: any quote, fixer/snapshot flag families (`--fix-only`, `--add-noqa`, `--write=*`, `--w`,
|
|
31
|
+
# `--update*`, `--updateSnapshot`, `--snapshot-update`, `-u` on jest/vitest/package managers), write targets (`go -o`,
|
|
32
|
+
# `-exec`, `-toolexec`, `tsc --outDir/--outFile`, `pytest --junitxml/--basetemp/--html`, any absolute or `..` path) and
|
|
33
|
+
# tox/nox `dist`/`pypi`/`twine` envs in any spelling; `-w` is a workspace on npm/pnpm. Also mapped: `uv run python -m X`,
|
|
34
|
+
# `python3.N -m X`, `npx --yes X`, `pnpm --filter X`, `npm -w PKG`, `make -C DIR` (DIR becomes the cwd), `cargo +TC <sub>`,
|
|
35
|
+
# `cargo fmt --check`, gradle `--tests X` / maven `-pl X`, `./x.test.sh`, `bash -x x.test.sh`. `clean` goals stay unmapped.
|
|
30
36
|
# Exit: 0 ok | 2 usage or bad repo path. bash 3.2 portable.
|
|
31
37
|
set -uo pipefail
|
|
32
38
|
|
|
@@ -207,6 +213,8 @@ check_cmd() {
|
|
|
207
213
|
esac
|
|
208
214
|
c=${c%" && popd"}
|
|
209
215
|
case "$c" in *';'*|*'|'*|*'&'*) return 1 ;; esac
|
|
216
|
+
# forge#4318: a quoted word hides a flag from the word checks below ("--fix"); a real check never needs quotes
|
|
217
|
+
case "$c" in *\"*|*\'*) return 1 ;; esac
|
|
210
218
|
CK_CMD=$c
|
|
211
219
|
set -f
|
|
212
220
|
# shellcheck disable=SC2086 # split into words on purpose; set -f stops glob expansion
|
|
@@ -214,11 +222,24 @@ check_cmd() {
|
|
|
214
222
|
set +f
|
|
215
223
|
while [ $# -gt 0 ]; do case "$1" in [A-Za-z_]*=*) shift ;; *) break ;; esac; done
|
|
216
224
|
case "${1:-} ${2:-}" in
|
|
217
|
-
"uv run"|"poetry run"
|
|
218
|
-
"
|
|
225
|
+
"uv run"|"poetry run") shift 2; case "${1:-} ${2:-}" in "python -m"|"python3 -m") shift 2 ;; esac ;; # forge#4318: uv run python -m X
|
|
226
|
+
"pnpm exec"|"yarn exec"|"python -m"|"python3 -m"|"bundle exec") shift 2 ;;
|
|
227
|
+
"python3."[0-9]" -m"|"python3."[0-9][0-9]" -m") shift 2 ;; # forge#4318: a versioned interpreter
|
|
228
|
+
"npx "*|"bunx "*) shift; case "${1:-}" in --yes|-y) shift ;; esac ;;
|
|
219
229
|
esac
|
|
220
|
-
|
|
221
|
-
|
|
230
|
+
tool=${1:-}
|
|
231
|
+
# forge#4316/#4318: a fixer, snapshot updater or write-target flag rewrites the tree or writes outside it; matched as a
|
|
232
|
+
# flag family (`--fix-only`, `--write=true`, `--w`), and `-w` only off a package manager (there it names a workspace)
|
|
233
|
+
for w in "$@"; do
|
|
234
|
+
case "$w" in
|
|
235
|
+
--fix|--fix[=-]*|--add-noqa*|--unsafe-fixes*|--write|--write[=-]*|--update|--update[=-]*|--updateSnapshot*|--snapshot-update*) return 1 ;;
|
|
236
|
+
--outDir*|--outdir*|--out-dir*|--outFile*|--declarationDir*|--tsBuildInfoFile*|--junitxml*|--junit-xml*|--basetemp*|--html*|-exec|-exec=*|-toolexec*) return 1 ;;
|
|
237
|
+
/*|*=/*|..|../*|*/..|*/../*|*=..|*=../*) return 1 ;; # an absolute or parent path is outside the checkout
|
|
238
|
+
-w|-w=*|--w|--w=*) case "$tool" in npm|pnpm|yarn|bun) ;; *) return 1 ;; esac ;;
|
|
239
|
+
-u) case "$tool" in jest|vitest|npm|pnpm|yarn|bun) return 1 ;; esac ;;
|
|
240
|
+
-o|-o=*) [ "$tool" = go ] && return 1 ;;
|
|
241
|
+
esac
|
|
242
|
+
done
|
|
222
243
|
case "${1:-} ${2:-}" in
|
|
223
244
|
"black "*|"ruff format") has_word --check "$@" || return 1 ;;
|
|
224
245
|
"gofmt "*) has_word -l "$@" || has_word -d "$@" || return 1 ;;
|
|
@@ -227,21 +248,26 @@ check_cmd() {
|
|
|
227
248
|
case "${1:-}" in
|
|
228
249
|
npm|pnpm|yarn|bun)
|
|
229
250
|
pm=$1; shift
|
|
230
|
-
case "$pm ${1:-}" in # forge#4315: pnpm -r test, npm --prefix DIR test
|
|
231
|
-
"pnpm -r"|"pnpm --recursive") shift ;;
|
|
232
|
-
"npm --prefix") [ $# -ge 2 ] && safe_dir "$2" || return 1; shift 2 ;;
|
|
251
|
+
case "$pm ${1:-}" in # forge#4315: pnpm -r test, npm --prefix DIR test; forge#4318: npm -w PKG, pnpm -w, pnpm --filter X
|
|
252
|
+
"pnpm -r"|"pnpm --recursive"|"pnpm -w"|"pnpm --workspace-root") shift ;;
|
|
253
|
+
"npm --prefix"|"npm -w"|"npm --workspace") [ $# -ge 2 ] && safe_dir "$2" || return 1; shift 2 ;;
|
|
254
|
+
"pnpm --filter"|"pnpm -F") [ $# -ge 2 ] || return 1; case "$2" in *[!A-Za-z0-9@._/-]*) return 1 ;; esac; shift 2 ;;
|
|
233
255
|
esac
|
|
234
256
|
[ "${1:-}" = test ] && return 0
|
|
235
257
|
[ "${1:-}" = run ] && pkg_check_script "${2:-}" && return 0
|
|
236
258
|
case "$pm" in yarn|pnpm) pkg_check_script "${1:-}" && return 0 ;; esac
|
|
237
259
|
return 1 ;;
|
|
238
|
-
cargo)
|
|
260
|
+
cargo)
|
|
261
|
+
a=${2:-}; [ "${a#+}" != "$a" ] && a=${3:-} # forge#4318: cargo +TOOLCHAIN <sub>
|
|
262
|
+
case "$a" in test|clippy|check) return 0 ;; fmt) has_word --check "$@"; return ;; esac; return 1 ;;
|
|
239
263
|
./gradlew|gradlew|gradle|./mvnw|mvnw|mvn)
|
|
240
|
-
# every non-flag word is a check task/goal (`:mod:test` qualified for gradle); publish/deploy/install never pass
|
|
264
|
+
# every non-flag word is a check task/goal (`:mod:test` qualified for gradle); publish/deploy/install/clean never pass;
|
|
265
|
+
# forge#4318: the value of --tests / -pl / --projects / -P / -D is not a goal
|
|
241
266
|
case "$1" in *mvn*) goals="test|verify|validate" ;; *) goals="test|check|lint" ;; esac
|
|
242
|
-
shift; nt=0
|
|
267
|
+
shift; nt=0; skipv=""
|
|
243
268
|
for w in "$@"; do
|
|
244
|
-
|
|
269
|
+
if [ -n "$skipv" ]; then skipv=""; continue; fi
|
|
270
|
+
case "$w" in --tests|-pl|--projects|-P|-D) skipv=1; continue ;; -*) continue ;; esac
|
|
245
271
|
t=${w##*:}
|
|
246
272
|
case "|$goals|" in *"|$t|"*) [ -n "$t" ] && nt=$((nt+1)) ;; *) return 1 ;; esac
|
|
247
273
|
done
|
|
@@ -249,19 +275,39 @@ check_cmd() {
|
|
|
249
275
|
rspec|unittest) return 0 ;;
|
|
250
276
|
python|python3) [ "${2:-}" = manage.py ] && [ "${3:-}" = test ]; return ;;
|
|
251
277
|
tox|nox)
|
|
278
|
+
# forge#4318: `--env=X`, `-eX` and comma lists are split, case-folded, and packaging envs (dist, pypi) are publish too
|
|
252
279
|
shift; prev=""
|
|
253
280
|
for w in "$@"; do
|
|
254
|
-
|
|
255
|
-
case "$
|
|
281
|
+
v=""
|
|
282
|
+
case "$prev" in -e|-s|--env|--session|--sessions) v=$w ;; esac
|
|
283
|
+
case "$w" in --env=*|--session=*|--sessions=*) v=${w#*=} ;; --*) ;; -e?*|-s?*) v=${w#-?} ;; esac
|
|
284
|
+
if [ -n "$v" ]; then
|
|
285
|
+
v=$(printf '%s' "$v" | tr '[:upper:]' '[:lower:]')
|
|
286
|
+
case ",$v," in *publish*|*release*|*deploy*|*upload*|*pypi*|*twine*|*,dist,*) return 1 ;; esac
|
|
287
|
+
fi
|
|
256
288
|
prev=$w
|
|
257
289
|
done
|
|
258
290
|
return 0 ;;
|
|
259
291
|
deno) [ "${2:-}" = test ]; return ;;
|
|
260
|
-
bash|sh)
|
|
292
|
+
bash|sh)
|
|
293
|
+
# forge#4318: shell option words (-x, -eu) before the script are allowed
|
|
294
|
+
shift; while [ $# -gt 0 ]; do case "$1" in -[a-z]*) shift ;; *) break ;; esac; done
|
|
295
|
+
case "${1:-}" in *.test.sh) safe_dir "$1"; return ;; esac; return 1 ;;
|
|
296
|
+
./*.test.sh) safe_dir "${1#./}"; return ;;
|
|
261
297
|
make)
|
|
262
298
|
# forge#4316: every target must be a check (`make test deploy` is dropped); VAR=value and flags are not targets
|
|
263
|
-
|
|
264
|
-
|
|
299
|
+
# forge#4318: `make -C DIR` runs in DIR: it becomes the entry cwd, not a target
|
|
300
|
+
shift; nt=0; prev=""; mdir=""
|
|
301
|
+
for w in "$@"; do
|
|
302
|
+
if [ "$prev" = -C ]; then mdir=$w; prev=$w; continue; fi
|
|
303
|
+
prev=$w
|
|
304
|
+
case "$w" in -*|*=*) continue ;; esac; make_check_target "$w" || return 1; nt=$((nt+1))
|
|
305
|
+
done
|
|
306
|
+
if [ -n "$mdir" ]; then
|
|
307
|
+
safe_dir "$mdir" || return 1
|
|
308
|
+
CK_CMD=${CK_CMD/ -C $mdir/}; mdir=${mdir%/}
|
|
309
|
+
if [ "$mdir" != . ]; then if [ "$CK_CWD" = . ]; then CK_CWD=$mdir; else CK_CWD="$CK_CWD/$mdir"; fi; fi
|
|
310
|
+
fi
|
|
265
311
|
[ "$nt" -gt 0 ]; return ;;
|
|
266
312
|
go) case "${2:-}" in test|vet|build) return 0 ;; esac; return 1 ;;
|
|
267
313
|
node) [ "${2:-}" = --test ]; return ;;
|
|
@@ -280,7 +326,7 @@ tool_globs() {
|
|
|
280
326
|
*\ deno\ *|*\ bun\ *|*\ bunx\ *) ext_globs . "$JS_EXT"; return ;;
|
|
281
327
|
*[\ /]ruff\ *|*[\ /]flake8\ *|*[\ /]pylint\ *|*[\ /]mypy\ *|*[\ /]black\ *|*[\ /]pytest\ *) printf '**/*.py'; return ;;
|
|
282
328
|
*[\ /]golangci-lint\ *|*[\ /]gofmt\ *|*[\ /]staticcheck\ *|*\ go\ *) printf '**/*.go'; return ;;
|
|
283
|
-
*[\ /]shellcheck\ *|*\ bash\ *.sh\ *|*\ sh\ *.sh\ *) printf '**/*.sh'; return ;;
|
|
329
|
+
*[\ /]shellcheck\ *|*\ bash\ *.sh\ *|*\ sh\ *.sh\ *|*.test.sh\ *) printf '**/*.sh'; return ;;
|
|
284
330
|
*[\ /]eslint\ *|*[\ /]tsc\ *|*[\ /]prettier\ *|*[\ /]vitest\ *|*[\ /]jest\ *|*\ node\ --test*|*\ npm\ *|*\ pnpm\ *|*\ yarn\ *|*\ npx\ *) ext_globs . "$JS_EXT"; return ;;
|
|
285
331
|
esac
|
|
286
332
|
printf ''
|
package/scripts/test-map.test.sh
CHANGED
|
@@ -345,6 +345,61 @@ eslint .
|
|
|
345
345
|
WF
|
|
346
346
|
eq "unsafe forms dropped, check forms kept" "$(cmds)" "black --check .|eslint .|gofmt -l .|make test V=1|make test lint|prettier --check .|ruff format --check ."
|
|
347
347
|
|
|
348
|
+
# ---- 5g. forge#4318: flag-family fixers, snapshot updaters, write targets, quotes and tox publish envs are dropped ----
|
|
349
|
+
wf_case bypass <<'WF'
|
|
350
|
+
cargo clippy --fix
|
|
351
|
+
tox --env=publish
|
|
352
|
+
tox --session=release
|
|
353
|
+
tox -ePUBLISH
|
|
354
|
+
tox -e dist,pypi
|
|
355
|
+
nox -s Release
|
|
356
|
+
eslint "--fix" .
|
|
357
|
+
gofmt -l --w .
|
|
358
|
+
gofmt -w=true .
|
|
359
|
+
prettier --write=true .
|
|
360
|
+
prettier -cw .
|
|
361
|
+
ruff --fix-only .
|
|
362
|
+
ruff check --add-noqa .
|
|
363
|
+
jest -u
|
|
364
|
+
jest --updateSnapshot
|
|
365
|
+
vitest -u
|
|
366
|
+
vitest --update
|
|
367
|
+
pytest --snapshot-update
|
|
368
|
+
npm test -- -u
|
|
369
|
+
go build -o /tmp/x ./...
|
|
370
|
+
go test -o out.test ./...
|
|
371
|
+
go test -exec foo ./...
|
|
372
|
+
tsc --outDir /tmp/out
|
|
373
|
+
tsc --outFile out.js
|
|
374
|
+
pytest --junitxml=/abs/x.xml
|
|
375
|
+
pytest --basetemp=../t
|
|
376
|
+
mvn clean test
|
|
377
|
+
./gradlew clean test
|
|
378
|
+
tox -e py311,lint
|
|
379
|
+
pytest -q
|
|
380
|
+
WF
|
|
381
|
+
eq "bypass forms dropped" "$(cmds)" "pytest -q|tox -e py311,lint"
|
|
382
|
+
|
|
383
|
+
# ---- 5h. forge#4318: workspace flags, versioned/wrapped runners, -C, toolchains and test scripts are mapped ----
|
|
384
|
+
wf_case forms <<'WF'
|
|
385
|
+
npm -w pkg test
|
|
386
|
+
pnpm -w test
|
|
387
|
+
uv run python -m pytest
|
|
388
|
+
python3.12 -m pytest
|
|
389
|
+
npx --yes vitest
|
|
390
|
+
pnpm --filter web test
|
|
391
|
+
make -C sub test
|
|
392
|
+
mvn -pl mod test
|
|
393
|
+
./gradlew test --tests Foo
|
|
394
|
+
cargo +nightly test
|
|
395
|
+
cargo fmt --check
|
|
396
|
+
./x.test.sh
|
|
397
|
+
bash -x x.test.sh
|
|
398
|
+
WF
|
|
399
|
+
eq "real forms mapped" "$(cmds)" "./gradlew test --tests Foo|./x.test.sh|bash -x x.test.sh|cargo +nightly test|cargo fmt --check|make test|mvn -pl mod test|npm -w pkg test|npx --yes vitest|pnpm --filter web test|pnpm -w test|python3.12 -m pytest|uv run python -m pytest"
|
|
400
|
+
eq "make -C dir is the cwd" "$(printf '%s' "$OUTS" | jq -r '.entries[] | select(.command == "make test") | "\(.cwd) \(.globs | join(","))"')" "sub sub/**"
|
|
401
|
+
eq "test script globs" "$(globs_of "./x.test.sh")" "**/*.sh"
|
|
402
|
+
|
|
348
403
|
# ---- 6. usage ----
|
|
349
404
|
bash "$S" --repo-path "$TMP/missing" >/dev/null 2>&1; eq "bad repo path" "$?" 2
|
|
350
405
|
bash "$S" --nope >/dev/null 2>&1; eq "unknown option" "$?" 2
|