Compare commits
47
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15561263cc | ||
|
|
44b244f5c0 | ||
|
|
f2661d2c6e | ||
|
|
95d48b02cb | ||
|
|
d6fa67982e | ||
|
|
1237216e63 | ||
|
|
49a8ff73fd | ||
|
|
32be7e547a | ||
|
|
9152bb2b14 | ||
|
|
9c7fb4eda6 | ||
|
|
063de8cd85 | ||
|
|
11ffe65c97 | ||
|
|
dcaf01c789 | ||
|
|
7ddd2f5e1d | ||
|
|
16920c4a6f | ||
|
|
6b3ebce343 | ||
|
|
7fa0f65a60 | ||
|
|
f4faa3f819 | ||
|
|
0692d999f6 | ||
|
|
fa35c6abed | ||
|
|
53d4ea6ec6 | ||
|
|
39987a5b61 | ||
|
|
00bdf8b28c | ||
|
|
631567d5f7 | ||
|
|
9f741874bd | ||
|
|
430b4d5f5d | ||
|
|
404db8cd70 | ||
|
|
c0262e8856 | ||
|
|
306985990c | ||
|
|
2082ac061b | ||
|
|
18ee6eb33d | ||
|
|
76f1f1c8d3 | ||
|
|
0da1deb83f | ||
|
|
4aa67e8dff | ||
|
|
7425edb80f | ||
|
|
571a3d54b5 | ||
|
|
bcd174f89e | ||
|
|
94fc3e55f5 | ||
|
|
46d29d82e9 | ||
|
|
d210c2d7ea | ||
|
|
48531755eb | ||
|
|
9a1cc63383 | ||
|
|
0830e2e3ae | ||
|
|
205cc0d7a1 | ||
|
|
698655d40a | ||
|
|
dcad7de033 | ||
|
|
cd4409abc3 |
@@ -1,71 +0,0 @@
|
|||||||
# REPORT A1207
|
|
||||||
|
|
||||||
Date: 2026-08-13
|
|
||||||
Branch: `fix/869-lease-probe-timeout`
|
|
||||||
Starting head: `2373a5ad345fb316ad2460f6390baab1f45ba08f`
|
|
||||||
Base: `216cd72226cd9ee17eea461cfe7cd0e010a22f02`
|
|
||||||
|
|
||||||
## What changed
|
|
||||||
|
|
||||||
- Added Python behavior tests using isolated temporary directories and marker-writing fake `mosaic` executables. They prove that the supplied `PATH` wins over ambient `os.environ["PATH"]`, and that absent or empty supplied `PATH` values do not search ambient paths, platform defaults, or the current directory.
|
|
||||||
- Bound Python override behavior with executable fakes: a valid `MOSAIC_LEASE_VERSION_PROBE_COMMAND` wins over supplied and ambient `PATH`; an invalid override returns `None` without PATH fallback.
|
|
||||||
- Added a Python runner binding test that captures kwargs and requires `timeout=10.0`. Existing timeout, transport-error, and nonzero-exit checks remain fail-closed with `None`.
|
|
||||||
- Added the optional TypeScript dependency-injection seam `CapabilityProbeExecFile`, defaulting to the existing real `execFileSync` implementation. Production callers have no behavior change.
|
|
||||||
- Added TypeScript tests that capture child-process options and require exactly `timeout: 10_000`. Injected timeout, spawn-error, nonzero-exit, unparseable JSON, and malformed-object cases all return `null`.
|
|
||||||
- Removed the ambient no-dependency TypeScript smoke case that could execute a built checkout's real CLI. Default resolver and supervisor behavior retain their isolated tests, while capability transport tests now use an isolated artifact or the injected transport.
|
|
||||||
|
|
||||||
No Python production code changed relative to `2373a5ad`. The only production delta is the optional TypeScript child-process injection seam.
|
|
||||||
|
|
||||||
## Hermeticity incident and correction
|
|
||||||
|
|
||||||
An initial ambient-lookup mutation run exposed that the pre-existing Python "not resolvable" test left ambient process PATH uncontrolled. On this host, that mutation resolved and executed the host `mosaic` capability probe. A post-build intermediate TypeScript run also let the pre-existing no-dependency smoke case execute the checkout's built `dist/cli.js` capability probe. No `claude` process was run. I then isolated the Python test's ambient PATH, removed the TypeScript ambient smoke case, repeated the PATH mutation using only marker-writing temporary fakes, and repeated the final suites without either real probe path.
|
|
||||||
|
|
||||||
## Mutation evidence
|
|
||||||
|
|
||||||
Each mutation was applied independently, its focused suite was run, and the production source was restored before the final run.
|
|
||||||
|
|
||||||
| Mutation | Result | Reddened test name(s) |
|
|
||||||
| ------------------------------------------------------------------------------------------ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `shutil.which("mosaic", path=environ.get("PATH", ""))` to ambient `shutil.which("mosaic")` | RED, three failures | `ProbeActivationCapabilityTest.test_supplied_path_wins_over_ambient_process_path`; `ProbeActivationCapabilityTest.test_absent_or_empty_supplied_path_never_falls_back_or_executes` for both absent and empty PATH subtests |
|
|
||||||
| Python `PROBE_TIMEOUT_SECONDS: 10.0` to `2.0` | RED, one failure | `ProbeActivationCapabilityTest.test_probe_passes_ten_second_timeout_to_runner` |
|
|
||||||
| TypeScript `LEASE_CAPABILITY_PROBE_TIMEOUT_MS: 10_000` to `2_000` | RED, one failure | `defaultCapabilityProbe > passes the exact ten-second timeout to the injected child-process transport` |
|
|
||||||
|
|
||||||
## Final test run
|
|
||||||
|
|
||||||
Dependencies were installed first with `pnpm install --frozen-lockfile`. Workspace dependencies were then built with `pnpm --filter '@mosaicstack/mosaic...' run build` so package type declarations were available.
|
|
||||||
|
|
||||||
```text
|
|
||||||
$ cd packages/mosaic && python3 src/mutator-gate/version_coupling_unittest.py
|
|
||||||
...................
|
|
||||||
----------------------------------------------------------------------
|
|
||||||
Ran 19 tests in 0.007s
|
|
||||||
|
|
||||||
OK
|
|
||||||
|
|
||||||
$ pnpm exec vitest run src/commands/lease-activation-probe.spec.ts
|
|
||||||
✓ src/commands/lease-activation-probe.spec.ts (20 tests) 80ms
|
|
||||||
Test Files 1 passed (1)
|
|
||||||
Tests 20 passed (20)
|
|
||||||
```
|
|
||||||
|
|
||||||
```text
|
|
||||||
$ pnpm exec prettier --check packages/mosaic/src/commands/lease-activation-probe.ts packages/mosaic/src/commands/lease-activation-probe.spec.ts
|
|
||||||
Checking formatting...
|
|
||||||
All matched files use Prettier code style!
|
|
||||||
|
|
||||||
$ pnpm --filter @mosaicstack/mosaic lint
|
|
||||||
> eslint src
|
|
||||||
|
|
||||||
$ pnpm --filter @mosaicstack/mosaic typecheck
|
|
||||||
> tsc --noEmit
|
|
||||||
|
|
||||||
$ python3 -m py_compile packages/mosaic/src/mutator-gate/version_coupling_unittest.py packages/mosaic/framework/tools/lease-broker/activation_version_gate.py
|
|
||||||
|
|
||||||
$ git diff --check
|
|
||||||
```
|
|
||||||
|
|
||||||
All commands above exited zero.
|
|
||||||
|
|
||||||
## Ambiguities skipped
|
|
||||||
|
|
||||||
None.
|
|
||||||
@@ -82,7 +82,6 @@ is re-seeded a genuinely missing core file is a stop-and-report condition — no
|
|||||||
|
|
||||||
Confirm: required + situational tests passed (primary gate); aligned to `docs/PRD.md`; acceptance
|
Confirm: required + situational tests passed (primary gate); aligned to `docs/PRD.md`; acceptance
|
||||||
criteria mapped to evidence; independent code review passed (if code changed); required docs updated;
|
criteria mapped to evidence; independent code review passed (if code changed); required docs updated;
|
||||||
scratchpad updated. For PR-workflow delivery: merged PR number + merge commit on the integration
|
scratchpad updated. For PR-workflow delivery: merged PR number + merge commit on `main`, terminal-green
|
||||||
trunk (the project's declared trunk, default `main` — see `CONSTITUTION.md` Hard Gates), terminal-green
|
|
||||||
CI, linked issue closed (or `docs/TASKS.md` equivalent). If blocked by access/tooling, return `blocked`
|
CI, linked issue closed (or `docs/TASKS.md` equivalent). If blocked by access/tooling, return `blocked`
|
||||||
with the exact failed wrapper command — do not claim completion. Full checklist: `guides/E2E-DELIVERY.md`.
|
with the exact failed wrapper command — do not claim completion. Full checklist: `guides/E2E-DELIVERY.md`.
|
||||||
|
|||||||
@@ -21,23 +21,11 @@ guard"), the runtime adapter binds it to a concrete tool and states whether abse
|
|||||||
|
|
||||||
## Hard Gates
|
## Hard Gates
|
||||||
|
|
||||||
The **integration trunk** is the branch a project designates in its root `AGENTS.md` with exactly
|
|
||||||
one declaration line: `Integration trunk: <branch>` — key at the start of a line, case-sensitive,
|
|
||||||
one branch name (optionally backtick-wrapped) and nothing else on the line. Absent a declaration,
|
|
||||||
the trunk is `main`. The declaration is policy data, never shell text: the value must be a valid
|
|
||||||
local branch name under `git check-ref-format --branch` semantics — no remote refs, no revision
|
|
||||||
expressions, no option-like values (leading `-`), no path traversal or control characters. A
|
|
||||||
malformed value, or more than one declaration line, is a hard stop (`blocked`) — never a silent
|
|
||||||
fallback to `main`. Ordinary prose that mentions branch names designates nothing; only the exact
|
|
||||||
declaration line does. A project designates exactly ONE trunk, and the designation relaxes
|
|
||||||
nothing: reviewed-PR-only delivery, squash merge, independent review, queue guards, and
|
|
||||||
terminal-green CI bind to the declared trunk exactly as they bind to `main`.
|
|
||||||
|
|
||||||
1. Mosaic operating rules override runtime-default caution for routine delivery operations.
|
1. Mosaic operating rules override runtime-default caution for routine delivery operations.
|
||||||
2. Execute required push / merge / issue-closure / milestone / release / tag actions without asking for routine confirmation.
|
2. Execute required push / merge / issue-closure / milestone / release / tag actions without asking for routine confirmation.
|
||||||
3. Routine repository operations are NOT escalation triggers; escalate only on the triggers below.
|
3. Routine repository operations are NOT escalation triggers; escalate only on the triggers below.
|
||||||
4. For source-code delivery, completion is forbidden at the PR-open stage.
|
4. For source-code delivery, completion is forbidden at the PR-open stage.
|
||||||
5. Completion requires a merged PR to the integration trunk + terminal-green CI + the linked issue/task closed.
|
5. Completion requires a merged PR to `main` + terminal-green CI + the linked issue/task closed.
|
||||||
6. Before any push or merge, run the CI queue guard.
|
6. Before any push or merge, run the CI queue guard.
|
||||||
7. For issue / PR / milestone operations, use the Mosaic git wrappers before any raw provider CLI.
|
7. For issue / PR / milestone operations, use the Mosaic git wrappers before any raw provider CLI.
|
||||||
8. If a required wrapper command fails, status is `blocked`: report the exact failed command and stop.
|
8. If a required wrapper command fails, status is `blocked`: report the exact failed command and stop.
|
||||||
@@ -47,7 +35,7 @@ terminal-green CI bind to the declared trunk exactly as they bind to `main`.
|
|||||||
12. The intake procedure is not conditional on perceived complexity; a "simple" task carries the same requirements as a multi-file feature.
|
12. The intake procedure is not conditional on perceived complexity; a "simple" task carries the same requirements as a multi-file feature.
|
||||||
13. **Merge authority (coordinated work):** when a coordinator/orchestrator session is active for the work, the post-review merge go-ahead is the coordinator's to give — once the required review gates pass, merge on the coordinator's confirmation; do not wait on the human owner personally. Solo (uncoordinated) delivery keeps the default: merge per gates 2 and 9. A "No self-merge" note on a PR means no UNREVIEWED self-merge — it does not suspend coordinator-authorized merges.
|
13. **Merge authority (coordinated work):** when a coordinator/orchestrator session is active for the work, the post-review merge go-ahead is the coordinator's to give — once the required review gates pass, merge on the coordinator's confirmation; do not wait on the human owner personally. Solo (uncoordinated) delivery keeps the default: merge per gates 2 and 9. A "No self-merge" note on a PR means no UNREVIEWED self-merge — it does not suspend coordinator-authorized merges.
|
||||||
14. Never hardcode secrets; never emit credential values in any output (not even partially, not "to confirm").
|
14. Never hardcode secrets; never emit credential values in any output (not even partially, not "to confirm").
|
||||||
15. Trunk-based git only: branch from the integration trunk, merge via a reviewed PR (squash), never push directly to the trunk.
|
15. Trunk-based git only: branch from `main`, merge via a reviewed PR (squash), never push directly to `main`.
|
||||||
16. If you modify source code, an independent review (author ≠ reviewer) must pass before completion.
|
16. If you modify source code, an independent review (author ≠ reviewer) must pass before completion.
|
||||||
|
|
||||||
## Integrity (quality gates are never bypassed)
|
## Integrity (quality gates are never bypassed)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ This guide covers how to bootstrap a project so AI agents (Claude, Codex, etc.)
|
|||||||
4. Issue tracking is consistent across projects
|
4. Issue tracking is consistent across projects
|
||||||
5. Documentation standards and API contracts are enforced from day one
|
5. Documentation standards and API contracts are enforced from day one
|
||||||
6. PRD requirements are established before coding begins
|
6. PRD requirements are established before coding begins
|
||||||
7. Branching/merging is consistent: branch -> integration trunk (default `main`) via PR with squash-only merges
|
7. Branching/merging is consistent: `branch -> main` via PR with squash-only merges
|
||||||
8. Steered-autonomy execution is enabled so agents can run end-to-end with escalation-only human intervention
|
8. Steered-autonomy execution is enabled so agents can run end-to-end with escalation-only human intervention
|
||||||
|
|
||||||
## Agent Host Prerequisites
|
## Agent Host Prerequisites
|
||||||
@@ -206,7 +206,7 @@ Every runtime context file should contain:
|
|||||||
6. **Issue tracking** — Issue and commit conventions
|
6. **Issue tracking** — Issue and commit conventions
|
||||||
7. **Code review** — Required review process
|
7. **Code review** — Required review process
|
||||||
8. **Runtime notes** — Runtime-specific behavior references
|
8. **Runtime notes** — Runtime-specific behavior references
|
||||||
9. **Branch and merge policy** — Trunk workflow (branch -> integration trunk via PR, squash-only)
|
9. **Branch and merge policy** — Trunk workflow (`branch -> main` via PR, squash-only)
|
||||||
10. **Autonomy and escalation policy** — Agent owns coding/review/PR/release/deploy lifecycle
|
10. **Autonomy and escalation policy** — Agent owns coding/review/PR/release/deploy lifecycle
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -288,16 +288,15 @@ Reserve `0.1.0` for the MVP release milestone.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Step 5b: Configure Trunk Branch Protection (Hard Rule)
|
## Step 5b: Configure Main Branch Protection (Hard Rule)
|
||||||
|
|
||||||
Apply equivalent settings in Gitea, GitHub, or GitLab, targeting the project's integration trunk
|
Apply equivalent settings in Gitea, GitHub, or GitLab:
|
||||||
(the branch its root `AGENTS.md` declares; default `main` — see `CONSTITUTION.md` Hard Gates):
|
|
||||||
|
|
||||||
1. Protect the integration trunk from direct pushes.
|
1. Protect `main` from direct pushes.
|
||||||
2. Require pull requests to merge into the integration trunk.
|
2. Require pull requests to merge into `main`.
|
||||||
3. Require required CI/status checks to pass before merge.
|
3. Require required CI/status checks to pass before merge.
|
||||||
4. Require code review approval before merge.
|
4. Require code review approval before merge.
|
||||||
5. Allow **squash merge only** for PRs into the integration trunk (disable merge commits and rebase merges for it).
|
5. Allow **squash merge only** for PRs into `main` (disable merge commits and rebase merges for `main`).
|
||||||
|
|
||||||
This enforces one merge strategy across human and agent workflows.
|
This enforces one merge strategy across human and agent workflows.
|
||||||
|
|
||||||
@@ -514,9 +513,9 @@ After bootstrapping, verify:
|
|||||||
- [ ] Git labels created (epic, feature, bug, task, etc.)
|
- [ ] Git labels created (epic, feature, bug, task, etc.)
|
||||||
- [ ] Initial pre-MVP milestone created (0.0.1)
|
- [ ] Initial pre-MVP milestone created (0.0.1)
|
||||||
- [ ] MVP milestone reserved for release (0.1.0)
|
- [ ] MVP milestone reserved for release (0.1.0)
|
||||||
- [ ] The integration trunk is protected from direct pushes
|
- [ ] `main` is protected from direct pushes
|
||||||
- [ ] PRs into the integration trunk are required
|
- [ ] PRs into `main` are required
|
||||||
- [ ] Merge method for the integration trunk is squash-only
|
- [ ] Merge method for `main` is squash-only
|
||||||
- [ ] Quality gates run successfully
|
- [ ] Quality gates run successfully
|
||||||
- [ ] `.env.example` exists (if project uses env vars)
|
- [ ] `.env.example` exists (if project uses env vars)
|
||||||
- [ ] CI/CD pipeline configured (if using Woodpecker/GitHub Actions)
|
- [ ] CI/CD pipeline configured (if using Woodpecker/GitHub Actions)
|
||||||
|
|||||||
@@ -4,11 +4,6 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
> **Integration trunk:** the YAML examples in this guide use the default integration trunk `main`
|
|
||||||
> in branch conditions and version rules. A project that declares a different trunk in its root
|
|
||||||
> `AGENTS.md` (see `CONSTITUTION.md` Hard Gates) substitutes its declared trunk wherever `main`
|
|
||||||
> appears as the trunk branch.
|
|
||||||
|
|
||||||
This guide covers the canonical CI/CD pattern used across projects. The pipeline runs in Woodpecker CI and follows this flow:
|
This guide covers the canonical CI/CD pattern used across projects. The pipeline runs in Woodpecker CI and follows this flow:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -870,7 +865,7 @@ steps:
|
|||||||
```yaml
|
```yaml
|
||||||
image: git.example.com/org/service@${IMAGE_DIGEST}
|
image: git.example.com/org/service@${IMAGE_DIGEST}
|
||||||
```
|
```
|
||||||
7. **Test on a short-lived non-trunk branch first** — open a PR and verify quality gates before merging to the integration trunk
|
7. **Test on a short-lived non-main branch first** — open a PR and verify quality gates before merging to `main`
|
||||||
8. **Verify images appear** in Gitea Packages tab after successful pipeline
|
8. **Verify images appear** in Gitea Packages tab after successful pipeline
|
||||||
|
|
||||||
## Terminal-Green Full-Step Contract
|
## Terminal-Green Full-Step Contract
|
||||||
@@ -911,7 +906,7 @@ For source-code delivery, completion is not allowed at "PR opened" stage.
|
|||||||
|
|
||||||
Required sequence:
|
Required sequence:
|
||||||
|
|
||||||
1. Merge PR to the integration trunk (squash) via Mosaic wrapper.
|
1. Merge PR to `main` (squash) via Mosaic wrapper.
|
||||||
2. Monitor CI to terminal status:
|
2. Monitor CI to terminal status:
|
||||||
```bash
|
```bash
|
||||||
~/.config/mosaic/tools/git/pr-ci-wait.sh -n <PR_NUMBER>
|
~/.config/mosaic/tools/git/pr-ci-wait.sh -n <PR_NUMBER>
|
||||||
@@ -1117,5 +1112,5 @@ If a project currently uses Verdaccio (e.g., U-Connect at `npm.uscllc.net`), fol
|
|||||||
|
|
||||||
### Pipeline runs Docker builds on pull requests
|
### Pipeline runs Docker builds on pull requests
|
||||||
|
|
||||||
- Verify `when` clause on Docker build steps restricts to the integration trunk (`branch: [main]` by default)
|
- Verify `when` clause on Docker build steps restricts to `branch: [main]`
|
||||||
- Pull requests should only run quality gates, not build/push images
|
- Pull requests should only run quality gates, not build/push images
|
||||||
|
|||||||
@@ -10,10 +10,9 @@ If implementation diverges from `docs/PRD.md` or `docs/PRD.json` without PRD upd
|
|||||||
|
|
||||||
Merge strategy enforcement (HARD RULE):
|
Merge strategy enforcement (HARD RULE):
|
||||||
|
|
||||||
- The integration trunk is the branch the project's root `AGENTS.md` declares (default: `main`) — see `CONSTITUTION.md` Hard Gates.
|
- PR target for delivery is `main`.
|
||||||
- PR target for delivery is the integration trunk.
|
- Direct pushes to `main` are prohibited.
|
||||||
- Direct pushes to the integration trunk are prohibited.
|
- Merge to `main` MUST be squash-only.
|
||||||
- Merge to the integration trunk MUST be squash-only.
|
|
||||||
- Use `~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash --expect-head {approved_full_sha}` (or PowerShell equivalent).
|
- Use `~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash --expect-head {approved_full_sha}` (or PowerShell equivalent).
|
||||||
|
|
||||||
## Review Checklist
|
## Review Checklist
|
||||||
@@ -115,8 +114,8 @@ Use `~/.config/mosaic/templates/docs/DOCUMENTATION-CHECKLIST.md` whenever code/A
|
|||||||
# List the issue being addressed
|
# List the issue being addressed
|
||||||
~/.config/mosaic/tools/git/issue-list.sh -i {issue-number}
|
~/.config/mosaic/tools/git/issue-list.sh -i {issue-number}
|
||||||
|
|
||||||
# View the changes (diff against the integration trunk; default: main)
|
# View the changes
|
||||||
git diff {integration_trunk}...HEAD
|
git diff main...HEAD
|
||||||
```
|
```
|
||||||
|
|
||||||
### Providing Feedback
|
### Providing Feedback
|
||||||
@@ -152,4 +151,4 @@ This pattern appears in 3 places. A shared helper would reduce duplication.
|
|||||||
2. If changes requested, assign back to author
|
2. If changes requested, assign back to author
|
||||||
3. If approved, note approval in issue comments
|
3. If approved, note approval in issue comments
|
||||||
4. For merges, ensure CI passes first
|
4. For merges, ensure CI passes first
|
||||||
5. Merge PR to the integration trunk with squash strategy only
|
5. Merge PR to `main` with squash strategy only
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ For implementation work, you MUST run this cycle in order:
|
|||||||
7. `commit` - commit only when the logical unit passes tests and review.
|
7. `commit` - commit only when the logical unit passes tests and review.
|
||||||
8. `pre-push queue guard` - before pushing, wait for running/queued project pipelines to clear: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push`.
|
8. `pre-push queue guard` - before pushing, wait for running/queued project pipelines to clear: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push`.
|
||||||
9. `push` - push immediately after queue guard passes.
|
9. `push` - push immediately after queue guard passes.
|
||||||
10. `PR integration` - if external git provider is available, create/update PR to the integration trunk (the project's declared trunk, default `main`) and merge with required strategy via Mosaic wrappers.
|
10. `PR integration` - if external git provider is available, create/update PR to `main` and merge with required strategy via Mosaic wrappers.
|
||||||
11. `pre-merge queue guard` - before merging PR, wait for running/queued project pipelines on the exact PR head to clear: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B <PR_HEAD_BRANCH> -R <PR_HEAD_OWNER/REPO> --sha <PR_HEAD_FULL_SHA>`.
|
11. `pre-merge queue guard` - before merging PR, wait for running/queued project pipelines on the exact PR head to clear: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B <PR_HEAD_BRANCH> -R <PR_HEAD_OWNER/REPO> --sha <PR_HEAD_FULL_SHA>`.
|
||||||
12. `CI/pipeline verification` - wait for terminal CI status and require green before completion (`~/.config/mosaic/tools/git/pr-ci-wait.sh` for PR-based workflow).
|
12. `CI/pipeline verification` - wait for terminal CI status and require green before completion (`~/.config/mosaic/tools/git/pr-ci-wait.sh` for PR-based workflow).
|
||||||
13. `issue closure` - close linked external issue (or close internal `docs/TASKS.md` task ref when provider is unavailable).
|
13. `issue closure` - close linked external issue (or close internal `docs/TASKS.md` task ref when provider is unavailable).
|
||||||
@@ -199,7 +199,7 @@ Before running this checklist, pause and self-interrogate: did I fulfill the use
|
|||||||
10. No unresolved blocker hidden.
|
10. No unresolved blocker hidden.
|
||||||
11. If deployment is in scope, deployment target, release version, and post-deploy verification evidence are documented.
|
11. If deployment is in scope, deployment target, release version, and post-deploy verification evidence are documented.
|
||||||
12. `docs/TASKS.md` status and issue/internal references are updated to match delivered work.
|
12. `docs/TASKS.md` status and issue/internal references are updated to match delivered work.
|
||||||
13. If source code changed and external provider is available: PR merged to the integration trunk (squash), with merge evidence recorded.
|
13. If source code changed and external provider is available: PR merged to `main` (squash), with merge evidence recorded.
|
||||||
14. CI/pipeline status is terminal green for the merged PR/head commit.
|
14. CI/pipeline status is terminal green for the merged PR/head commit.
|
||||||
15. Linked external issue is closed (or internal task ref is closed when no provider exists).
|
15. Linked external issue is closed (or internal task ref is closed when no provider exists).
|
||||||
16. If any of items 13-15 fail due access/tooling, report `blocked` with exact failed wrapper command and do not claim completion.
|
16. If any of items 13-15 fail due access/tooling, report `blocked` with exact failed wrapper command and do not claim completion.
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ status → mission → run → repeat
|
|||||||
|
|
||||||
- [ ] All milestone tasks in TASKS.md are `done`
|
- [ ] All milestone tasks in TASKS.md are `done`
|
||||||
- [ ] CI/pipeline green
|
- [ ] CI/pipeline green
|
||||||
- [ ] PR merged to the integration trunk
|
- [ ] PR merged to `main`
|
||||||
- [ ] Issues closed
|
- [ ] Issues closed
|
||||||
- [ ] Update manifest: milestone status → completed
|
- [ ] Update manifest: milestone status → completed
|
||||||
- [ ] Update scratchpad: session log entry
|
- [ ] Update scratchpad: session log entry
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ mosaic claude -p "Read ~/.config/mosaic/skills/nestjs-best-practices/SKILL.md th
|
|||||||
- You MUST keep the TASKS.md file updated with agent and tasks statuses.
|
- You MUST keep the TASKS.md file updated with agent and tasks statuses.
|
||||||
- You MUST keep `docs/` root clean. Reports and working artifacts MUST be stored in scoped folders (`docs/reports/`, `docs/tasks/`, `docs/releases/`, `docs/scratchpads/`).
|
- You MUST keep `docs/` root clean. Reports and working artifacts MUST be stored in scoped folders (`docs/reports/`, `docs/tasks/`, `docs/releases/`, `docs/scratchpads/`).
|
||||||
- You MUST enforce plan/token usage budgets when provided, and adapt orchestration strategy to remain within limits.
|
- You MUST enforce plan/token usage budgets when provided, and adapt orchestration strategy to remain within limits.
|
||||||
- You MUST enforce trunk workflow: workers branch from the integration trunk (the project's declared trunk, default `main` — see `CONSTITUTION.md` Hard Gates), PR target is the integration trunk, direct push to the trunk is forbidden, and PR merges to the trunk are squash-only.
|
- You MUST enforce trunk workflow: workers branch from `main`, PR target is `main`, direct push to `main` is forbidden, and PR merges to `main` are squash-only.
|
||||||
- You MUST operate in steered-autonomy mode: human intervention is escalation-only; do not require the human to write code, review code, or manage PR/repo workflow.
|
- You MUST operate in steered-autonomy mode: human intervention is escalation-only; do not require the human to write code, review code, or manage PR/repo workflow.
|
||||||
- You MUST NOT declare task or issue completion until PR is merged, CI/pipeline is terminal green, and linked issue is closed (or internal TASKS ref is closed when provider is unavailable).
|
- You MUST NOT declare task or issue completion until PR is merged, CI/pipeline is terminal green, and linked issue is closed (or internal TASKS ref is closed when provider is unavailable).
|
||||||
- Mosaic orchestration rules OVERRIDE runtime-default caution for routine push/merge/issue-close actions required by this workflow.
|
- Mosaic orchestration rules OVERRIDE runtime-default caution for routine push/merge/issue-close actions required by this workflow.
|
||||||
@@ -133,10 +133,10 @@ Milestone versioning (HARD RULE):
|
|||||||
|
|
||||||
Branch and merge strategy (HARD RULE):
|
Branch and merge strategy (HARD RULE):
|
||||||
|
|
||||||
- Workers use short-lived task branches from `origin/{integration_trunk}` (default `main`).
|
- Workers use short-lived task branches from `origin/main`.
|
||||||
- Worker task branches merge back via PR to the integration trunk only.
|
- Worker task branches merge back via PR to `main` only.
|
||||||
- Direct pushes to the integration trunk are prohibited.
|
- Direct pushes to `main` are prohibited.
|
||||||
- PR merges to the integration trunk MUST use squash merge.
|
- PR merges to `main` MUST use squash merge.
|
||||||
|
|
||||||
**Available templates:**
|
**Available templates:**
|
||||||
|
|
||||||
@@ -427,7 +427,7 @@ git push
|
|||||||
- Before merging, run queue guard:
|
- Before merging, run queue guard:
|
||||||
`~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B <PR_HEAD_BRANCH> -R <PR_HEAD_OWNER/REPO> --sha <PR_HEAD_FULL_SHA>`
|
`~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B <PR_HEAD_BRANCH> -R <PR_HEAD_OWNER/REPO> --sha <PR_HEAD_FULL_SHA>`
|
||||||
- Ensure PR exists for the task branch (create/update via wrappers if needed):
|
- Ensure PR exists for the task branch (create/update via wrappers if needed):
|
||||||
`~/.config/mosaic/tools/git/pr-create.sh ... -B {integration_trunk}` (default `main`)
|
`~/.config/mosaic/tools/git/pr-create.sh ... -B main`
|
||||||
- Merge via wrapper:
|
- Merge via wrapper:
|
||||||
`~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash --expect-head {approved_full_sha}`
|
`~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash --expect-head {approved_full_sha}`
|
||||||
- Wait for terminal CI status:
|
- Wait for terminal CI status:
|
||||||
@@ -619,7 +619,7 @@ Construct this from the task row and pass to worker via Task tool:
|
|||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
1. Checkout branch: `git fetch origin && (git checkout {branch} || git checkout -b {branch} origin/{integration_trunk}) && git rebase origin/{integration_trunk}` ({integration_trunk} = the project's declared trunk, default `main`)
|
1. Checkout branch: `git fetch origin && (git checkout {branch} || git checkout -b {branch} origin/main) && git rebase origin/main`
|
||||||
2. Read `docs/PRD.md` or `docs/PRD.json` and align implementation with PRD requirements
|
2. Read `docs/PRD.md` or `docs/PRD.json` and align implementation with PRD requirements
|
||||||
3. Read the finding details from the report
|
3. Read the finding details from the report
|
||||||
4. Implement the fix following existing code patterns
|
4. Implement the fix following existing code patterns
|
||||||
@@ -637,7 +637,7 @@ Do NOT leave lint warnings or errors for someone else to clean up. 6. Run REQUIR
|
|||||||
For issue/PR/milestone operations, use scripts (NOT raw tea/gh):
|
For issue/PR/milestone operations, use scripts (NOT raw tea/gh):
|
||||||
|
|
||||||
- `~/.config/mosaic/tools/git/issue-view.sh -i {N}`
|
- `~/.config/mosaic/tools/git/issue-view.sh -i {N}`
|
||||||
- `~/.config/mosaic/tools/git/pr-create.sh -t "Title" -b "Desc" -B {integration_trunk}`
|
- `~/.config/mosaic/tools/git/pr-create.sh -t "Title" -b "Desc" -B main`
|
||||||
- Push: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push -B {task_branch}`
|
- Push: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push -B {task_branch}`
|
||||||
- Merge: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B {pr_head_branch} -R {pr_head_owner/repo} --sha {pr_head_full_sha}`
|
- Merge: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B {pr_head_branch} -R {pr_head_owner/repo} --sha {pr_head_full_sha}`
|
||||||
- `~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash --expect-head {approved_full_sha}`
|
- `~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash --expect-head {approved_full_sha}`
|
||||||
@@ -994,13 +994,13 @@ mv docs/reports/qa-automation/pending/*failing-file* docs/reports/qa-automation/
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Merge-to-Trunk Candidate Protocol (Container Deployments)
|
## Merge-to-Main Candidate Protocol (Container Deployments)
|
||||||
|
|
||||||
If deployment is in scope and container images are used, every merge to the integration trunk MUST execute this protocol:
|
If deployment is in scope and container images are used, every merge to `main` MUST execute this protocol:
|
||||||
|
|
||||||
1. Build and push immutable candidate image tags:
|
1. Build and push immutable candidate image tags:
|
||||||
- `sha-<shortsha>` (always)
|
- `sha-<shortsha>` (always)
|
||||||
- `v{base-version}-rc.{build}` (for integration-trunk merges)
|
- `v{base-version}-rc.{build}` (for `main` merges)
|
||||||
- `testing` mutable pointer to the same digest
|
- `testing` mutable pointer to the same digest
|
||||||
2. Resolve and record the image digest for each service.
|
2. Resolve and record the image digest for each service.
|
||||||
3. Deploy by digest to testing environment (never deploy by mutable tag alone).
|
3. Deploy by digest to testing environment (never deploy by mutable tag alone).
|
||||||
|
|||||||
@@ -62,14 +62,7 @@ EXPECTED_ACTIVATION_CAPABILITY: Final[ActivationCapability] = {
|
|||||||
# capability as compact JSON.
|
# capability as compact JSON.
|
||||||
LEASE_CAPABILITY_PROBE_COMMAND: Final = "__lease-capability"
|
LEASE_CAPABILITY_PROBE_COMMAND: Final = "__lease-capability"
|
||||||
|
|
||||||
# Budget for the out-of-process `mosaic __lease-capability` probe. The CLI
|
PROBE_TIMEOUT_SECONDS: Final = 2.0
|
||||||
# is a Node program whose cold start alone measures 2.2-2.3s on a mid-range
|
|
||||||
# workstation (sb-it-1-dt, 2026-08-13), so a 2s budget made every launch on
|
|
||||||
# such hosts fail closed with the #869 skew message even though the
|
|
||||||
# capability matched. The timeout only bounds the pathological hang case —
|
|
||||||
# the happy path returns as soon as the probe exits — so a generous budget
|
|
||||||
# costs nothing on healthy hosts.
|
|
||||||
PROBE_TIMEOUT_SECONDS: Final = 10.0
|
|
||||||
|
|
||||||
# Override hook: a full shell-style command line (parsed with `shlex.split`)
|
# Override hook: a full shell-style command line (parsed with `shlex.split`)
|
||||||
# to run INSTEAD of resolving `mosaic` on PATH and appending the probe
|
# to run INSTEAD of resolving `mosaic` on PATH and appending the probe
|
||||||
@@ -95,13 +88,7 @@ def _resolve_probe_command(environ: Mapping[str, str]) -> list[str] | None:
|
|||||||
if override:
|
if override:
|
||||||
parsed = shlex.split(override)
|
parsed = shlex.split(override)
|
||||||
return parsed or None
|
return parsed or None
|
||||||
# Resolve against the PROVIDED environment's PATH, not the ambient
|
resolved = shutil.which("mosaic")
|
||||||
# os.environ. Before this, a test passing a hermetic environ still
|
|
||||||
# resolved (and spawned) the host's real `mosaic` — masked only on hosts
|
|
||||||
# where the real probe happened to exceed the old 2s timeout. No PATH in
|
|
||||||
# the provided environment means nothing is resolvable (fail-closed),
|
|
||||||
# matching the probe's overall contract.
|
|
||||||
resolved = shutil.which("mosaic", path=environ.get("PATH", ""))
|
|
||||||
if resolved is None:
|
if resolved is None:
|
||||||
return None
|
return None
|
||||||
return [resolved, LEASE_CAPABILITY_PROBE_COMMAND]
|
return [resolved, LEASE_CAPABILITY_PROBE_COMMAND]
|
||||||
|
|||||||
@@ -7,13 +7,11 @@ import { fileURLToPath } from 'node:url';
|
|||||||
import {
|
import {
|
||||||
LEASE_ACTIVATION_CAPABILITY,
|
LEASE_ACTIVATION_CAPABILITY,
|
||||||
LEASE_CAPABILITY_PROBE_COMMAND,
|
LEASE_CAPABILITY_PROBE_COMMAND,
|
||||||
LEASE_CAPABILITY_PROBE_TIMEOUT_MS,
|
|
||||||
defaultCapabilityProbe,
|
defaultCapabilityProbe,
|
||||||
defaultResolveCliEntry,
|
defaultResolveCliEntry,
|
||||||
defaultSupervisorProbe,
|
defaultSupervisorProbe,
|
||||||
leaseEnforcementActivatable,
|
leaseEnforcementActivatable,
|
||||||
registerLeaseCapabilityProbe,
|
registerLeaseCapabilityProbe,
|
||||||
type CapabilityProbeExecFile,
|
|
||||||
type LeaseActivationCapability,
|
type LeaseActivationCapability,
|
||||||
type SupervisorProbeResult,
|
type SupervisorProbeResult,
|
||||||
} from './lease-activation-probe.js';
|
} from './lease-activation-probe.js';
|
||||||
@@ -37,17 +35,6 @@ const presentSupervisor: SupervisorProbeResult = {
|
|||||||
socketPath: '/run/user/1000/mosaic-lease/broker.sock',
|
socketPath: '/run/user/1000/mosaic-lease/broker.sock',
|
||||||
};
|
};
|
||||||
|
|
||||||
function withScratchCli<T>(run: (cliPath: string) => T): T {
|
|
||||||
const scratchDir = mkdtempSync(join(tmpdir(), 'mosaic-lease-capability-probe-'));
|
|
||||||
try {
|
|
||||||
const cliPath = join(scratchDir, 'cli.js');
|
|
||||||
writeFileSync(cliPath, '// isolated fake; injected execFile means this is never executed\n');
|
|
||||||
return run(cliPath);
|
|
||||||
} finally {
|
|
||||||
rmSync(scratchDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('leaseEnforcementActivatable', () => {
|
describe('leaseEnforcementActivatable', () => {
|
||||||
it('is false when the activation capability is absent (null)', () => {
|
it('is false when the activation capability is absent (null)', () => {
|
||||||
const result = leaseEnforcementActivatable({
|
const result = leaseEnforcementActivatable({
|
||||||
@@ -113,6 +100,15 @@ describe('leaseEnforcementActivatable', () => {
|
|||||||
});
|
});
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses the real default probes when no deps are injected (does not throw)', () => {
|
||||||
|
// No live broker / built CLI is guaranteed in a test environment, so this
|
||||||
|
// only asserts the predicate degrades to a safe boolean rather than
|
||||||
|
// throwing — the fail-closed behavior itself is covered by the injected
|
||||||
|
// cases above.
|
||||||
|
expect(() => leaseEnforcementActivatable()).not.toThrow();
|
||||||
|
expect(typeof leaseEnforcementActivatable()).toBe('boolean');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('defaultCapabilityProbe', () => {
|
describe('defaultCapabilityProbe', () => {
|
||||||
@@ -131,61 +127,6 @@ describe('defaultCapabilityProbe', () => {
|
|||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes the exact ten-second timeout to the injected child-process transport', () => {
|
|
||||||
withScratchCli((cliPath) => {
|
|
||||||
let captured:
|
|
||||||
| {
|
|
||||||
file: string;
|
|
||||||
args: string[];
|
|
||||||
options: Parameters<CapabilityProbeExecFile>[2];
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
const execFile: CapabilityProbeExecFile = (file, args, options) => {
|
|
||||||
captured = { file, args, options };
|
|
||||||
return JSON.stringify(LEASE_ACTIVATION_CAPABILITY);
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = defaultCapabilityProbe({ resolveCliEntry: () => cliPath, execFile });
|
|
||||||
|
|
||||||
expect(result).toEqual(LEASE_ACTIVATION_CAPABILITY);
|
|
||||||
expect(captured).toEqual({
|
|
||||||
file: process.execPath,
|
|
||||||
args: [cliPath, LEASE_CAPABILITY_PROBE_COMMAND],
|
|
||||||
options: {
|
|
||||||
encoding: 'utf-8',
|
|
||||||
timeout: 10_000,
|
|
||||||
stdio: ['ignore', 'pipe', 'ignore'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(captured?.options.timeout).toBe(LEASE_CAPABILITY_PROBE_TIMEOUT_MS);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([
|
|
||||||
['timeout', Object.assign(new Error('timed out'), { code: 'ETIMEDOUT' })],
|
|
||||||
['spawn error', Object.assign(new Error('spawn failed'), { code: 'ENOENT' })],
|
|
||||||
['nonzero exit', Object.assign(new Error('child exited 1'), { status: 1 })],
|
|
||||||
])('returns null (fail-closed) on child-process %s', (_failure, error) => {
|
|
||||||
withScratchCli((cliPath) => {
|
|
||||||
const execFile: CapabilityProbeExecFile = () => {
|
|
||||||
throw error;
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(defaultCapabilityProbe({ resolveCliEntry: () => cliPath, execFile })).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([
|
|
||||||
['unparseable JSON', 'not-json'],
|
|
||||||
['malformed object', JSON.stringify({ name: LEASE_ACTIVATION_CAPABILITY.name })],
|
|
||||||
])('returns null (fail-closed) on %s output', (_failure, output) => {
|
|
||||||
withScratchCli((cliPath) => {
|
|
||||||
const execFile: CapabilityProbeExecFile = () => output;
|
|
||||||
|
|
||||||
expect(defaultCapabilityProbe({ resolveCliEntry: () => cliPath, execFile })).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('positive path — injected resolver, isolated scratch dir (never the real dist/)', () => {
|
describe('positive path — injected resolver, isolated scratch dir (never the real dist/)', () => {
|
||||||
// A prior version of this test staged the stub cli.js at the package's
|
// A prior version of this test staged the stub cli.js at the package's
|
||||||
// REAL resolved dist/ path and relied on afterEach to clean up "only
|
// REAL resolved dist/ path and relied on afterEach to clean up "only
|
||||||
|
|||||||
@@ -55,19 +55,6 @@ export const LEASE_ACTIVATION_CAPABILITY: LeaseActivationCapability = {
|
|||||||
/** Hidden CLI probe subcommand name — wired via {@link registerLeaseCapabilityProbe}. */
|
/** Hidden CLI probe subcommand name — wired via {@link registerLeaseCapabilityProbe}. */
|
||||||
export const LEASE_CAPABILITY_PROBE_COMMAND = '__lease-capability';
|
export const LEASE_CAPABILITY_PROBE_COMMAND = '__lease-capability';
|
||||||
|
|
||||||
/**
|
|
||||||
* Budget for the out-of-process capability probe. The probe launches a fresh
|
|
||||||
* Node process on the built CLI entrypoint, whose cold start alone measures
|
|
||||||
* 2.2-2.3s on a mid-range workstation (sb-it-1-dt, 2026-08-13) — so the
|
|
||||||
* previous 2s budget made the probe time out and report NO capability on
|
|
||||||
* such hosts, failing every launch with the #869 skew message even though
|
|
||||||
* the capability matched. The timeout only bounds the pathological hang
|
|
||||||
* case; the happy path returns as soon as the probe exits. Mirrors
|
|
||||||
* PROBE_TIMEOUT_SECONDS in the enforcement half
|
|
||||||
* (framework/tools/lease-broker/activation_version_gate.py).
|
|
||||||
*/
|
|
||||||
export const LEASE_CAPABILITY_PROBE_TIMEOUT_MS = 10_000;
|
|
||||||
|
|
||||||
function capabilityMatches(candidate: LeaseActivationCapability | null): boolean {
|
function capabilityMatches(candidate: LeaseActivationCapability | null): boolean {
|
||||||
return (
|
return (
|
||||||
candidate !== null &&
|
candidate !== null &&
|
||||||
@@ -123,28 +110,12 @@ export function defaultResolveCliEntry(
|
|||||||
return join(dirname(mainEntry), 'cli.js');
|
return join(dirname(mainEntry), 'cli.js');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Narrow injectable seam for the synchronous child process used by the
|
|
||||||
* capability probe. */
|
|
||||||
export type CapabilityProbeExecFile = (
|
|
||||||
file: string,
|
|
||||||
args: string[],
|
|
||||||
options: {
|
|
||||||
encoding: BufferEncoding;
|
|
||||||
timeout: number;
|
|
||||||
stdio: ['ignore', 'pipe', 'ignore'];
|
|
||||||
},
|
|
||||||
) => string;
|
|
||||||
|
|
||||||
/** Injectable inputs for {@link defaultCapabilityProbe}. */
|
/** Injectable inputs for {@link defaultCapabilityProbe}. */
|
||||||
export interface CapabilityProbeDeps {
|
export interface CapabilityProbeDeps {
|
||||||
/** Resolve the CLI entrypoint (`cli.js`) to probe. Defaults to
|
/** Resolve the CLI entrypoint (`cli.js`) to probe. Defaults to
|
||||||
* {@link defaultResolveCliEntry}. Inject to point at an isolated scratch
|
* {@link defaultResolveCliEntry}. Inject to point at an isolated scratch
|
||||||
* location in tests — never at the real package's `dist/`. */
|
* location in tests — never at the real package's `dist/`. */
|
||||||
resolveCliEntry?: () => string;
|
resolveCliEntry?: () => string;
|
||||||
/** Execute the resolved CLI entrypoint. Defaults to the real
|
|
||||||
* `execFileSync`. Inject so transport behavior and options can be tested
|
|
||||||
* without spawning a process. */
|
|
||||||
execFile?: CapabilityProbeExecFile;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -168,10 +139,9 @@ export function defaultCapabilityProbe(
|
|||||||
const cliEntry = resolveCliEntry();
|
const cliEntry = resolveCliEntry();
|
||||||
if (!existsSync(cliEntry)) return null;
|
if (!existsSync(cliEntry)) return null;
|
||||||
|
|
||||||
const execFile: CapabilityProbeExecFile = deps.execFile ?? execFileSync;
|
const output = execFileSync(process.execPath, [cliEntry, LEASE_CAPABILITY_PROBE_COMMAND], {
|
||||||
const output = execFile(process.execPath, [cliEntry, LEASE_CAPABILITY_PROBE_COMMAND], {
|
|
||||||
encoding: 'utf-8',
|
encoding: 'utf-8',
|
||||||
timeout: LEASE_CAPABILITY_PROBE_TIMEOUT_MS,
|
timeout: 2000,
|
||||||
stdio: ['ignore', 'pipe', 'ignore'],
|
stdio: ['ignore', 'pipe', 'ignore'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -24,15 +24,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import io
|
import io
|
||||||
import os
|
|
||||||
import shlex
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
import unittest
|
import unittest
|
||||||
from contextlib import redirect_stderr
|
from contextlib import redirect_stderr
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
|
|
||||||
TOOLS_DIR = Path(__file__).parents[2] / "framework/tools/lease-broker"
|
TOOLS_DIR = Path(__file__).parents[2] / "framework/tools/lease-broker"
|
||||||
@@ -61,20 +57,6 @@ def matching_capability() -> dict[str, object]:
|
|||||||
return dict(VERSION_GATE.EXPECTED_ACTIVATION_CAPABILITY)
|
return dict(VERSION_GATE.EXPECTED_ACTIVATION_CAPABILITY)
|
||||||
|
|
||||||
|
|
||||||
def write_fake_mosaic(directory: Path, marker: Path) -> Path:
|
|
||||||
directory.mkdir(parents=True, exist_ok=True)
|
|
||||||
executable = directory / "mosaic"
|
|
||||||
executable.write_text(
|
|
||||||
"#!/bin/sh\n"
|
|
||||||
f"printf '%s\\n' executed >> {shlex.quote(str(marker))}\n"
|
|
||||||
"printf '%s\\n' "
|
|
||||||
"'{\"name\":\"lease-runtime-activation\",\"version\":1}'\n",
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
executable.chmod(0o755)
|
|
||||||
return executable
|
|
||||||
|
|
||||||
|
|
||||||
class AssertActivationCapabilityMatchesTest(unittest.TestCase):
|
class AssertActivationCapabilityMatchesTest(unittest.TestCase):
|
||||||
"""Unit-level coverage of `activation_version_gate.py`'s own assertion,
|
"""Unit-level coverage of `activation_version_gate.py`'s own assertion,
|
||||||
isolated from the launch-runtime.py seam it is wired into below."""
|
isolated from the launch-runtime.py seam it is wired into below."""
|
||||||
@@ -128,102 +110,11 @@ class ProbeActivationCapabilityTest(unittest.TestCase):
|
|||||||
handling — never spawns a real `mosaic` process."""
|
handling — never spawns a real `mosaic` process."""
|
||||||
|
|
||||||
def test_returns_none_when_mosaic_is_not_resolvable_on_path(self) -> None:
|
def test_returns_none_when_mosaic_is_not_resolvable_on_path(self) -> None:
|
||||||
# Keep even a deliberate ambient-lookup mutation away from any host
|
result = VERSION_GATE.default_probe_activation_capability(
|
||||||
# installation. The dedicated hermeticity tests below provide fake
|
{"PATH": "/nonexistent-bin-dir-for-869-c4-test"}
|
||||||
# ambient executables and markers.
|
)
|
||||||
with mock.patch.dict(
|
|
||||||
os.environ, {"PATH": "/nonexistent-ambient-bin-dir-for-869-c4-test"}
|
|
||||||
):
|
|
||||||
result = VERSION_GATE.default_probe_activation_capability(
|
|
||||||
{"PATH": "/nonexistent-bin-dir-for-869-c4-test"}
|
|
||||||
)
|
|
||||||
self.assertIsNone(result)
|
self.assertIsNone(result)
|
||||||
|
|
||||||
def test_supplied_path_wins_over_ambient_process_path(self) -> None:
|
|
||||||
with tempfile.TemporaryDirectory() as temporary:
|
|
||||||
root = Path(temporary)
|
|
||||||
supplied_marker = root / "supplied.marker"
|
|
||||||
ambient_marker = root / "ambient.marker"
|
|
||||||
supplied_bin = root / "supplied-bin"
|
|
||||||
ambient_bin = root / "ambient-bin"
|
|
||||||
write_fake_mosaic(supplied_bin, supplied_marker)
|
|
||||||
write_fake_mosaic(ambient_bin, ambient_marker)
|
|
||||||
|
|
||||||
with mock.patch.dict(os.environ, {"PATH": str(ambient_bin)}):
|
|
||||||
result = VERSION_GATE.default_probe_activation_capability(
|
|
||||||
{"PATH": str(supplied_bin)}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(result, matching_capability())
|
|
||||||
self.assertTrue(supplied_marker.exists())
|
|
||||||
self.assertFalse(ambient_marker.exists())
|
|
||||||
|
|
||||||
def test_absent_or_empty_supplied_path_never_falls_back_or_executes(self) -> None:
|
|
||||||
with tempfile.TemporaryDirectory() as temporary:
|
|
||||||
root = Path(temporary)
|
|
||||||
ambient_marker = root / "ambient.marker"
|
|
||||||
current_directory_marker = root / "current-directory.marker"
|
|
||||||
ambient_bin = root / "ambient-bin"
|
|
||||||
current_directory = root / "current-directory"
|
|
||||||
write_fake_mosaic(ambient_bin, ambient_marker)
|
|
||||||
write_fake_mosaic(current_directory, current_directory_marker)
|
|
||||||
original_directory = Path.cwd()
|
|
||||||
|
|
||||||
try:
|
|
||||||
os.chdir(current_directory)
|
|
||||||
with mock.patch.dict(os.environ, {"PATH": str(ambient_bin)}):
|
|
||||||
for supplied_environment in ({}, {"PATH": ""}):
|
|
||||||
with self.subTest(environ=supplied_environment):
|
|
||||||
result = VERSION_GATE.default_probe_activation_capability(
|
|
||||||
supplied_environment
|
|
||||||
)
|
|
||||||
self.assertIsNone(result)
|
|
||||||
self.assertFalse(ambient_marker.exists())
|
|
||||||
self.assertFalse(current_directory_marker.exists())
|
|
||||||
finally:
|
|
||||||
os.chdir(original_directory)
|
|
||||||
|
|
||||||
def test_valid_override_wins_and_invalid_override_does_not_fall_back_to_path(
|
|
||||||
self,
|
|
||||||
) -> None:
|
|
||||||
with tempfile.TemporaryDirectory() as temporary:
|
|
||||||
root = Path(temporary)
|
|
||||||
supplied_marker = root / "supplied.marker"
|
|
||||||
ambient_marker = root / "ambient.marker"
|
|
||||||
override_marker = root / "override.marker"
|
|
||||||
supplied_bin = root / "supplied-bin"
|
|
||||||
ambient_bin = root / "ambient-bin"
|
|
||||||
override_bin = root / "override-bin"
|
|
||||||
write_fake_mosaic(supplied_bin, supplied_marker)
|
|
||||||
write_fake_mosaic(ambient_bin, ambient_marker)
|
|
||||||
override_executable = write_fake_mosaic(override_bin, override_marker)
|
|
||||||
|
|
||||||
with mock.patch.dict(os.environ, {"PATH": str(ambient_bin)}):
|
|
||||||
result = VERSION_GATE.default_probe_activation_capability(
|
|
||||||
{
|
|
||||||
"PATH": str(supplied_bin),
|
|
||||||
VERSION_GATE.MOSAIC_COMMAND_OVERRIDE_VAR: str(override_executable),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.assertEqual(result, matching_capability())
|
|
||||||
self.assertTrue(override_marker.exists())
|
|
||||||
self.assertFalse(supplied_marker.exists())
|
|
||||||
self.assertFalse(ambient_marker.exists())
|
|
||||||
|
|
||||||
override_marker.unlink()
|
|
||||||
result = VERSION_GATE.default_probe_activation_capability(
|
|
||||||
{
|
|
||||||
"PATH": str(supplied_bin),
|
|
||||||
VERSION_GATE.MOSAIC_COMMAND_OVERRIDE_VAR: str(
|
|
||||||
root / "invalid-override" / "mosaic"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.assertIsNone(result)
|
|
||||||
self.assertFalse(override_marker.exists())
|
|
||||||
self.assertFalse(supplied_marker.exists())
|
|
||||||
self.assertFalse(ambient_marker.exists())
|
|
||||||
|
|
||||||
def test_override_command_is_parsed_and_the_probe_subcommand_is_not_double_appended(
|
def test_override_command_is_parsed_and_the_probe_subcommand_is_not_double_appended(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -244,29 +135,6 @@ class ProbeActivationCapabilityTest(unittest.TestCase):
|
|||||||
self.assertEqual(result, {"name": "lease-runtime-activation", "version": 1})
|
self.assertEqual(result, {"name": "lease-runtime-activation", "version": 1})
|
||||||
self.assertEqual(captured, [["/fake/mosaic", "__lease-capability"]])
|
self.assertEqual(captured, [["/fake/mosaic", "__lease-capability"]])
|
||||||
|
|
||||||
def test_probe_passes_ten_second_timeout_to_runner(self) -> None:
|
|
||||||
captured_argv: list[str] = []
|
|
||||||
captured_kwargs: dict[str, object] = {}
|
|
||||||
|
|
||||||
class FakeCompleted:
|
|
||||||
returncode = 0
|
|
||||||
stdout = '{"name": "lease-runtime-activation", "version": 1}'
|
|
||||||
|
|
||||||
def fake_run(argv: list[str], **kwargs: object) -> FakeCompleted:
|
|
||||||
captured_argv.extend(argv)
|
|
||||||
captured_kwargs.update(kwargs)
|
|
||||||
return FakeCompleted()
|
|
||||||
|
|
||||||
result = VERSION_GATE.default_probe_activation_capability(
|
|
||||||
{VERSION_GATE.MOSAIC_COMMAND_OVERRIDE_VAR: "/fake/mosaic"},
|
|
||||||
run=fake_run,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(result, matching_capability())
|
|
||||||
self.assertEqual(captured_argv, ["/fake/mosaic"])
|
|
||||||
self.assertEqual(captured_kwargs["timeout"], 10.0)
|
|
||||||
self.assertEqual(captured_kwargs["check"], False)
|
|
||||||
|
|
||||||
def test_fails_closed_on_nonzero_exit_malformed_json_and_missing_fields(self) -> None:
|
def test_fails_closed_on_nonzero_exit_malformed_json_and_missing_fields(self) -> None:
|
||||||
class NonZeroExit:
|
class NonZeroExit:
|
||||||
returncode = 1
|
returncode = 1
|
||||||
@@ -306,7 +174,7 @@ class ProbeActivationCapabilityTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_fails_closed_on_timeout_and_transport_error(self) -> None:
|
def test_fails_closed_on_timeout_and_transport_error(self) -> None:
|
||||||
def timeout_run(*_args: object, **_kwargs: object) -> None:
|
def timeout_run(*_args: object, **_kwargs: object) -> None:
|
||||||
raise subprocess.TimeoutExpired(cmd="mosaic", timeout=10.0)
|
raise subprocess.TimeoutExpired(cmd="mosaic", timeout=2.0)
|
||||||
|
|
||||||
def oserror_run(*_args: object, **_kwargs: object) -> None:
|
def oserror_run(*_args: object, **_kwargs: object) -> None:
|
||||||
raise OSError("no such file or directory")
|
raise OSError("no such file or directory")
|
||||||
|
|||||||
Reference in New Issue
Block a user