foundry-testing-actor 0.1.0__tar.gz

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.
Files changed (44) hide show
  1. foundry_testing_actor-0.1.0/.github/workflows/ci.yml +140 -0
  2. foundry_testing_actor-0.1.0/.github/workflows/release.yml +216 -0
  3. foundry_testing_actor-0.1.0/.gitignore +6 -0
  4. foundry_testing_actor-0.1.0/CLAUDE.md +98 -0
  5. foundry_testing_actor-0.1.0/PKG-INFO +258 -0
  6. foundry_testing_actor-0.1.0/README.md +235 -0
  7. foundry_testing_actor-0.1.0/adr/ADR-FTA-0001-the-machinery-leaves-the-capability.md +95 -0
  8. foundry_testing_actor-0.1.0/adr/ADR-FTA-0002-this-actors-half-of-the-three-amigos-round.md +82 -0
  9. foundry_testing_actor-0.1.0/adr/README.md +24 -0
  10. foundry_testing_actor-0.1.0/adr/template.md +28 -0
  11. foundry_testing_actor-0.1.0/docker/Dockerfile +111 -0
  12. foundry_testing_actor-0.1.0/examples/ACME.PARTS.CAP.SUP.007.WID-testing/Dockerfile +42 -0
  13. foundry_testing_actor-0.1.0/examples/ACME.PARTS.CAP.SUP.007.WID-testing/actor-agentic-context.yaml +48 -0
  14. foundry_testing_actor-0.1.0/examples/README.md +123 -0
  15. foundry_testing_actor-0.1.0/pyproject.toml +52 -0
  16. foundry_testing_actor-0.1.0/scripts/probe_grounding.py +67 -0
  17. foundry_testing_actor-0.1.0/src/foundry_testing_actor/__init__.py +56 -0
  18. foundry_testing_actor-0.1.0/src/foundry_testing_actor/cards/actor-data.yaml +68 -0
  19. foundry_testing_actor-0.1.0/src/foundry_testing_actor/cards/actor-message.yaml +32 -0
  20. foundry_testing_actor-0.1.0/src/foundry_testing_actor/cards/actor-synchronous-messaging.yaml +59 -0
  21. foundry_testing_actor-0.1.0/src/foundry_testing_actor/cards/actor.yaml +19 -0
  22. foundry_testing_actor-0.1.0/src/foundry_testing_actor/cli.py +181 -0
  23. foundry_testing_actor-0.1.0/src/foundry_testing_actor/config.py +525 -0
  24. foundry_testing_actor-0.1.0/src/foundry_testing_actor/conformance.py +133 -0
  25. foundry_testing_actor-0.1.0/src/foundry_testing_actor/correlation.py +193 -0
  26. foundry_testing_actor-0.1.0/src/foundry_testing_actor/engine.py +889 -0
  27. foundry_testing_actor-0.1.0/src/foundry_testing_actor/grounding.py +206 -0
  28. foundry_testing_actor-0.1.0/src/foundry_testing_actor/handler.py +248 -0
  29. foundry_testing_actor-0.1.0/src/foundry_testing_actor/instance.py +129 -0
  30. foundry_testing_actor-0.1.0/src/foundry_testing_actor/runner/Dockerfile +32 -0
  31. foundry_testing_actor-0.1.0/src/foundry_testing_actor/schemas/agentic-context.schema.yaml +168 -0
  32. foundry_testing_actor-0.1.0/src/foundry_testing_actor/serve.py +129 -0
  33. foundry_testing_actor-0.1.0/tests/conftest.py +126 -0
  34. foundry_testing_actor-0.1.0/tests/fixtures/broken/actor-agentic-context.yaml +17 -0
  35. foundry_testing_actor-0.1.0/tests/test_cards.py +138 -0
  36. foundry_testing_actor-0.1.0/tests/test_cli.py +68 -0
  37. foundry_testing_actor-0.1.0/tests/test_config.py +260 -0
  38. foundry_testing_actor-0.1.0/tests/test_conformance.py +165 -0
  39. foundry_testing_actor-0.1.0/tests/test_engine.py +527 -0
  40. foundry_testing_actor-0.1.0/tests/test_grounding.py +155 -0
  41. foundry_testing_actor-0.1.0/tests/test_handler.py +205 -0
  42. foundry_testing_actor-0.1.0/tests/test_instance.py +136 -0
  43. foundry_testing_actor-0.1.0/tests/test_portability.py +79 -0
  44. foundry_testing_actor-0.1.0/uv.lock +396 -0
@@ -0,0 +1,140 @@
1
+ name: ci
2
+ on: [push, pull_request]
3
+
4
+ # NO CREDENTIAL. Nothing here authenticates to anything: no lab repo, no knowledge registry, no
5
+ # PyPI upload, no image push. Everything this pipeline asserts about THIS package, it asserts from
6
+ # source and from artifacts it built itself — the wheel it just built, and an image built from
7
+ # that wheel rather than from a published one.
8
+ #
9
+ # It does reach the network for inputs: public base images and the npm package the engine drives.
10
+ # That is fetching, not authenticating, and the distinction is the one that matters here — no
11
+ # secret is available to this workflow, so nothing it runs can reach a real capability's repo.
12
+
13
+ jobs:
14
+ build:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: astral-sh/setup-uv@v5
19
+
20
+ - name: the test suite
21
+ run: uv run --extra dev pytest -q
22
+
23
+ - name: build
24
+ run: uv build
25
+
26
+ # ── the two gates a contract-shipping package runs against its own artifact ────────────
27
+ #
28
+ # The suite above proves the code works in a source checkout. Neither of these does — they
29
+ # prove the WHEEL works, which is a different claim and the one a consumer actually depends
30
+ # on. A schema left out of the build passes every test in tests/ and fails here.
31
+
32
+ - name: the wheel must carry its contract
33
+ run: |
34
+ uv venv /tmp/probe
35
+ uv pip install --python /tmp/probe/bin/python -q dist/*.whl
36
+ /tmp/probe/bin/foundry-testing-actor lint examples/ACME.PARTS.CAP.SUP.007.WID-testing
37
+ /tmp/probe/bin/foundry-testing-actor show examples/ACME.PARTS.CAP.SUP.007.WID-testing \
38
+ --registry reg.example.com | tee /tmp/show.txt
39
+ grep -q 'reg.example.com/acme.parts/sup.007.wid/backend/tests:<version>' /tmp/show.txt
40
+ grep -q 'reg.example.com/acme.parts/sup.007.wid/backend:<version>' /tmp/show.txt
41
+
42
+ - name: the wheel must carry the actor's own cards
43
+ # The `-actor` suffix asserts a papeete-actor underneath, which lint-card can check
44
+ # (ADR-ECO-0022). The suite proves the cards are conformant in a source checkout; this
45
+ # proves they SHIPPED. A card folder left out of the build passes every test in tests/ and
46
+ # makes the name a claim the artifact cannot honour. `papeete-actor-synchronous-messaging`
47
+ # is already a runtime dependency, so its gate is in the probe venv with nothing to add.
48
+ run: |
49
+ /tmp/probe/bin/papeete-actor-synchronous-messaging lint-card \
50
+ "$(/tmp/probe/bin/python -c 'from foundry_testing_actor import cards_path; print(cards_path())')"
51
+
52
+ - name: the wheel must carry the default runner
53
+ # A component that declares no `runner:` is built with the Dockerfile the wheel ships.
54
+ # A wheel without it lints every sidecar green and fails at the first publish — after a
55
+ # commit and a push have already landed.
56
+ run: |
57
+ /tmp/probe/bin/python -c 'from foundry_testing_actor import runner_path; p = runner_path() / "Dockerfile"; assert p.is_file(), p; print(p)'
58
+
59
+ - name: the gate must run
60
+ # A gate that cannot fail is not a gate. Lint a deliberately non-conformant sidecar with
61
+ # the INSTALLED wheel and require a non-zero exit.
62
+ run: |
63
+ if /tmp/probe/bin/foundry-testing-actor lint tests/fixtures/broken; then
64
+ echo "::error::lint accepted a non-conformant sidecar — the gate is not running"
65
+ exit 1
66
+ fi
67
+ echo "the gate refused a non-conformant sidecar, as it should"
68
+
69
+ - name: grounding renders a CLAUDE.md whose imports resolve
70
+ # The property the whole design rests on: an @-import naming a file that is not there is a
71
+ # session grounded in nothing, and it looks identical to one grounded correctly.
72
+ run: /tmp/probe/bin/python scripts/probe_grounding.py examples/ACME.PARTS.CAP.SUP.007.WID-testing
73
+
74
+ # ── the other half of what this package ships ─────────────────────────────────────────────────
75
+ #
76
+ # ADR-FTA-0001 (after ADR-FIA-0005): a use is a sidecar, and the image is what turns one into a
77
+ # running actor. The job above proves the WHEEL works. This one proves the claim a consumer actually depends on —
78
+ # that a folder containing one YAML file builds into an actor that boots and answers its doors.
79
+ # Nothing here reaches a registry: the image is built, run and thrown away in the runner.
80
+ image:
81
+ runs-on: ubuntu-latest
82
+ steps:
83
+ - uses: actions/checkout@v4
84
+ - uses: astral-sh/setup-uv@v5
85
+
86
+ - name: build the wheel this image will carry
87
+ # The image installs the wheel from dist/ rather than from PyPI, so that an image tagged
88
+ # <version> cannot contain some other build of <version> — see docker/Dockerfile.
89
+ run: uv build
90
+
91
+ - name: the base image
92
+ run: docker build -f docker/Dockerfile -t foundry-testing-actor:ci .
93
+
94
+ - name: the image and the wheel are one release
95
+ # Two artifacts, one version. An image whose wheel says something else is the failure this
96
+ # arrangement creates, so it is the one CI asserts.
97
+ run: |
98
+ wheel="$(ls dist/*.whl | sed -E 's/.*-([0-9][^-]*)-py3.*/\1/')"
99
+ in_image="$(docker run --rm --entrypoint python foundry-testing-actor:ci \
100
+ -c 'from foundry_testing_actor import version; print(version())')"
101
+ echo "wheel=$wheel image=$in_image"
102
+ [ "$wheel" = "$in_image" ] || { echo "::error::image carries $in_image, wheel is $wheel"; exit 1; }
103
+
104
+ - name: a use is a sidecar
105
+ # The example folder holds exactly one file besides its Dockerfile. If this builds, the
106
+ # claim in the README is true of a real folder rather than of prose.
107
+ run: |
108
+ test "$(ls examples/ACME.PARTS.CAP.SUP.007.WID-testing | grep -cv '^Dockerfile$')" = "1"
109
+ docker build --build-arg ACTOR_IMAGE=foundry-testing-actor:ci \
110
+ -t use:ci examples/ACME.PARTS.CAP.SUP.007.WID-testing
111
+
112
+ - name: the rendered cards are what a caller is validated against
113
+ # Rendering is the construction that replaces the hand copy conformance.check was built to
114
+ # police. Assert it inside the image, against the cards the image actually runs.
115
+ run: |
116
+ docker run --rm use:ci foundry-testing-actor lint /actor
117
+
118
+ - name: it boots, and answers the doors its cards declare
119
+ # The credentials are deliberately junk. The engine refuses to construct without a
120
+ # GITHUB_TOKEN — which is itself worth having, and is why they are set rather than omitted
121
+ # — and no door is actually exercised here: a 400 means routed and refused the payload, a
122
+ # 404 would mean the route was never built.
123
+ run: |
124
+ docker run -d --name smoke -p 18080:8080 \
125
+ -e GITHUB_TOKEN=ci -e CLAUDE_CODE_OAUTH_TOKEN=ci use:ci
126
+ for _ in $(seq 1 30); do
127
+ curl -fsS -m 2 http://localhost:18080/health >/dev/null 2>&1 && break
128
+ sleep 1
129
+ done
130
+ curl -fsS -m 5 http://localhost:18080/health | grep -q ok
131
+ for door in test-task propose-acceptance; do
132
+ code="$(curl -sS -o /dev/null -w '%{http_code}' -m 5 -X POST \
133
+ -H 'content-type: application/json' -d '{}' "http://localhost:18080/$door")"
134
+ [ "$code" = "400" ] || { echo "::error::POST /$door answered $code, expected 400 (routed, bad payload)"; exit 1; }
135
+ done
136
+ code="$(curl -sS -o /dev/null -w '%{http_code}' -m 5 -X POST \
137
+ -H 'content-type: application/json' -d '{}' http://localhost:18080/no-such-door)"
138
+ [ "$code" = "404" ] || { echo "::error::an undeclared door answered $code, expected 404"; exit 1; }
139
+ docker logs smoke | grep -q actor-started
140
+ docker rm -f smoke
@@ -0,0 +1,216 @@
1
+ name: release
2
+ on:
3
+ push:
4
+ tags: ["v*"]
5
+
6
+ # A REGISTRY ADDED AFTER A TAG WAS ALREADY CUT. The image job below publishes to two registries,
7
+ # and the second one was not always there. Running this workflow by hand backfills the image for
8
+ # a version whose wheel is already on PyPI — without inventing a version number whose only
9
+ # content is "the last release missed a registry".
10
+ #
11
+ # THE TAG IS AN INPUT, NOT THE REF THIS RUNS FROM. GitHub takes the workflow FILE from the ref it
12
+ # is dispatched on, and the tag needing a backfill is by definition older than the workflow that
13
+ # can do it — dispatching on that tag would run the very file that lacks this trigger. So run it
14
+ # from the default branch and name the tag here; the checkout below takes the CONTENT from the
15
+ # tag, and the version gate still refuses a tag that disagrees with the `pyproject.toml` beside
16
+ # it.
17
+ workflow_dispatch:
18
+ inputs:
19
+ tag:
20
+ description: "Existing tag to publish an image for, e.g. v0.1.0"
21
+ required: true
22
+
23
+ # PyPI Trusted Publishing (OIDC) — no token is stored anywhere. The one-time setup is a pending
24
+ # publisher on pypi.org naming this repo and this workflow; after the first release it becomes a
25
+ # normal publisher. See README.
26
+ permissions:
27
+ id-token: write # PyPI Trusted Publishing
28
+ contents: read
29
+ packages: write # the image, to this repo's own container registry
30
+
31
+ jobs:
32
+ publish:
33
+ # ONLY ON A TAG PUSH. A manual run exists to backfill an IMAGE (see `on:` above), and PyPI
34
+ # refuses a version it already holds — so re-uploading is not merely redundant, it fails, and
35
+ # would take the image job down with it.
36
+ if: github.event_name == 'push'
37
+ runs-on: ubuntu-latest
38
+ environment: pypi
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+ - uses: astral-sh/setup-uv@v5
42
+
43
+ - name: build
44
+ # No fetch step and no external token: the contract is committed here. A release depends
45
+ # on nothing but this checkout and PyPI.
46
+ run: uv build
47
+
48
+ - name: the wheel must carry its contract
49
+ run: |
50
+ uv venv /tmp/probe
51
+ uv pip install --python /tmp/probe/bin/python -q dist/*.whl
52
+ /tmp/probe/bin/foundry-testing-actor lint examples/ACME.PARTS.CAP.SUP.007.WID-testing
53
+
54
+ - name: the wheel must carry the actor's own cards
55
+ # Same reason as the line above, for the other half of what this package ships. The
56
+ # `-actor` suffix asserts a papeete-actor underneath (ADR-ECO-0022); a wheel published
57
+ # without its cards makes the name a claim the artifact cannot honour, and a published
58
+ # version is not something to discover that from.
59
+ run: |
60
+ /tmp/probe/bin/papeete-actor-synchronous-messaging lint-card \
61
+ "$(/tmp/probe/bin/python -c 'from foundry_testing_actor import cards_path; print(cards_path())')"
62
+
63
+ - name: the wheel must carry the default runner
64
+ # A component with no `runner:` is built from the Dockerfile the wheel ships; a published
65
+ # wheel without it fails at a use's first publish, after its commit and push have landed.
66
+ run: |
67
+ /tmp/probe/bin/python -c 'from foundry_testing_actor import runner_path; p = runner_path() / "Dockerfile"; assert p.is_file(), p; print(p)'
68
+
69
+ - name: grounding renders a CLAUDE.md whose imports resolve
70
+ # BEFORE the upload, not after. A wheel that ships without its schema, or whose grounding
71
+ # emits an @-import to a file it never wrote, produces sessions grounded in nothing that
72
+ # look identical to correct ones. That is not something to discover from a published
73
+ # version.
74
+ run: /tmp/probe/bin/python scripts/probe_grounding.py examples/ACME.PARTS.CAP.SUP.007.WID-testing
75
+
76
+ - name: publish to PyPI
77
+ run: uv publish --trusted-publishing always
78
+
79
+ # ── the image, from the same tag and the same checkout ────────────────────────────────────────
80
+ #
81
+ # TWO ARTIFACTS, ONE RELEASE (ADR-FTA-0001, after ADR-FIA-0005). A use pins an image; an
82
+ # embedder pins a wheel; both are this package at this version. A tag that produced one and not the other leaves a consumer
83
+ # unable to take the release at all — so this job builds the wheel again from the same checkout
84
+ # rather than installing the one just uploaded: the image must not depend on PyPI having already
85
+ # indexed it, and must not be able to pick up a different build of the same version.
86
+ #
87
+ # It runs AFTER the wheel is published, not beside it. A failed upload should not leave an image
88
+ # published for a version that does not exist on PyPI; the reverse — a wheel with the image still
89
+ # to come — is recoverable by re-running this job alone.
90
+ image:
91
+ needs: publish
92
+ # `needs` still orders this after the wheel on a tag push. On a manual backfill `publish` is
93
+ # skipped, and a skipped dependency skips its dependents too unless `always()` says otherwise —
94
+ # so the condition has to name both cases rather than relying on `needs` alone.
95
+ if: always() && (needs.publish.result == 'success' || github.event_name == 'workflow_dispatch')
96
+ runs-on: ubuntu-latest
97
+ steps:
98
+ - uses: actions/checkout@v4
99
+ with:
100
+ # Empty on a tag push, which is `checkout`'s own default — the ref that triggered the
101
+ # run. On a manual backfill it is the tag being republished, so every step below reads
102
+ # that tag's own tree rather than whatever the default branch has moved on to.
103
+ ref: ${{ inputs.tag }}
104
+
105
+ - uses: astral-sh/setup-uv@v5
106
+
107
+ - name: the version is the tag
108
+ # The tag is what a consumer writes in a FROM line. A tag that disagrees with
109
+ # pyproject.toml would publish an image whose own `version()` contradicts its name, which
110
+ # is exactly the confusion the CI gate above exists to prevent for the wheel. On a backfill
111
+ # this compares the named tag against the `pyproject.toml` checked out FROM that tag, so a
112
+ # typo in the input is refused rather than published under the wrong name.
113
+ id: version
114
+ env:
115
+ TAG: ${{ inputs.tag || github.ref_name }}
116
+ run: |
117
+ tag="${TAG#v}"
118
+ declared="$(uv run --quiet python -c 'import tomllib,pathlib; print(tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"])')"
119
+ [ "$tag" = "$declared" ] || { echo "::error::tag $TAG does not match pyproject version $declared"; exit 1; }
120
+ echo "version=$tag" >> "$GITHUB_OUTPUT"
121
+
122
+ - name: build
123
+ run: uv build
124
+
125
+ - uses: docker/login-action@v3
126
+ with:
127
+ registry: ghcr.io
128
+ username: ${{ github.actor }}
129
+ password: ${{ secrets.GITHUB_TOKEN }}
130
+
131
+ - name: the image, tagged and latest
132
+ env:
133
+ VERSION: ${{ steps.version.outputs.version }}
134
+ # `latest` FOLLOWS THE RELEASE STREAM, NOT A BACKFILL. A manual run exists to give an
135
+ # already-released version an image in a registry it missed; run against an older tag it
136
+ # would otherwise drag `latest` backwards onto it, silently, in whichever registry a
137
+ # consumer happens to pull from. The version tag is the whole point of a backfill —
138
+ # `latest` already names something newer, and stays there.
139
+ MOVE_LATEST: ${{ github.event_name == 'push' }}
140
+ run: |
141
+ image="ghcr.io/${{ github.repository }}"
142
+ docker build -f docker/Dockerfile -t "$image:$VERSION" -t "$image:latest" .
143
+ docker push "$image:$VERSION"
144
+ if [ "$MOVE_LATEST" = "true" ]; then docker push "$image:latest"; fi
145
+
146
+ # ── the product's own registry ────────────────────────────────────────────────────────────
147
+ #
148
+ # TWO REGISTRIES, ONE IMAGE. GHCR is where this package publishes; a product that RUNS uses
149
+ # of it pulls from its own registry, which its cluster already authenticates to and which a
150
+ # `FROM` line inside that cluster's builder can reach without a second credential. The base
151
+ # image has to exist in both, because the two are reached by different things: an embedder
152
+ # writing a Dockerfile reads the README and pins GHCR, while a use built by an in-cluster
153
+ # builder resolves its `FROM` against the registry that builder has a push token for.
154
+ #
155
+ # THE SAME IMAGE, NOT ANOTHER BUILD. `docker tag` re-points the build the step above already
156
+ # pushed, so both registries hold one digest. Building a second time would let them diverge —
157
+ # a base-image refresh between two steps of one job is enough — and nothing downstream would
158
+ # ever notice that `:0.1.0` meant two different things depending on where you pulled it.
159
+ #
160
+ # ONE VARIABLE NAMES THE WHOLE TARGET. `vars.PRODUCT_IMAGE` is the full repository path, e.g.
161
+ # `<name>.azurecr.io/<product>/foundry-testing-actor`; the login server is its first
162
+ # segment. This package is generic across capabilities and names no product of its own — the
163
+ # same rule `tests/test_portability.py` enforces over `src/` — so the product that hosts a
164
+ # registry names itself in a repository variable rather than in this file.
165
+ #
166
+ # UNSET IS A VALID STATE, AND IT SAYS SO. A fork, or this repo before its credentials were
167
+ # added, publishes to GHCR alone; that is a `::notice`, not a failure. Configured-but-broken
168
+ # IS a failure — the push below is not best-effort, because an image the product's cluster
169
+ # cannot pull is the whole reason this block exists.
170
+ - name: is a product registry configured?
171
+ id: product
172
+ env:
173
+ PRODUCT_IMAGE: ${{ vars.PRODUCT_IMAGE }}
174
+ run: |
175
+ if [ -z "$PRODUCT_IMAGE" ]; then
176
+ echo "::notice::PRODUCT_IMAGE is unset — publishing to GHCR only"
177
+ echo "configured=false" >> "$GITHUB_OUTPUT"
178
+ exit 0
179
+ fi
180
+ case "$PRODUCT_IMAGE" in
181
+ */*/*) : ;;
182
+ *) echo "::error::PRODUCT_IMAGE must be <registry>/<path>/<repository>, got $PRODUCT_IMAGE"; exit 1 ;;
183
+ esac
184
+ echo "server=${PRODUCT_IMAGE%%/*}" >> "$GITHUB_OUTPUT"
185
+ echo "configured=true" >> "$GITHUB_OUTPUT"
186
+
187
+ - uses: docker/login-action@v3
188
+ if: steps.product.outputs.configured == 'true'
189
+ with:
190
+ registry: ${{ steps.product.outputs.server }}
191
+ username: ${{ secrets.ACR_PUSH_USERNAME }}
192
+ password: ${{ secrets.ACR_PUSH_PASSWORD }}
193
+
194
+ - name: the same image, in the product's registry
195
+ if: steps.product.outputs.configured == 'true'
196
+ env:
197
+ PRODUCT_IMAGE: ${{ vars.PRODUCT_IMAGE }}
198
+ VERSION: ${{ steps.version.outputs.version }}
199
+ MOVE_LATEST: ${{ github.event_name == 'push' }}
200
+ run: |
201
+ ghcr="ghcr.io/${{ github.repository }}"
202
+ docker tag "$ghcr:$VERSION" "$PRODUCT_IMAGE:$VERSION"
203
+ docker push "$PRODUCT_IMAGE:$VERSION"
204
+ if [ "$MOVE_LATEST" = "true" ]; then
205
+ docker tag "$ghcr:latest" "$PRODUCT_IMAGE:latest"
206
+ docker push "$PRODUCT_IMAGE:latest"
207
+ fi
208
+
209
+ - name: a use built on it is still an actor
210
+ # BEFORE the image is something anyone can pin — the same reason the grounding probe runs
211
+ # before the upload above. An image that builds but produces a use that does not conform is
212
+ # not something to discover from a published tag.
213
+ run: |
214
+ docker build --build-arg ACTOR_IMAGE=ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }} \
215
+ -t use:release examples/ACME.PARTS.CAP.SUP.007.WID-testing
216
+ docker run --rm use:release foundry-testing-actor lint /actor
@@ -0,0 +1,6 @@
1
+ dist/
2
+ *.egg-info/
3
+ __pycache__/
4
+ .venv/
5
+ .pytest_cache/
6
+ .serena/
@@ -0,0 +1,98 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this
4
+ repository.
5
+
6
+ ## What this is
7
+
8
+ `foundry-testing-actor` runs a headless Claude Code session that authors black-box tests for one
9
+ capability's increment in that capability's own testing repo, then commits, pushes and publishes a
10
+ runnable test image. Before anything is built it answers `propose-acceptance`, its half of the
11
+ three amigos round. It is the **machinery** — it carries no capability of its own. The capability
12
+ arrives in a sidecar (`actor-agentic-context.yaml`, contract
13
+ `foundry-testing-actor/agentic-context/v1`) that the consuming repo writes.
14
+
15
+ Ported from one capability's hand-written testing actor, with `foundry-implementation-actor`
16
+ (FIA) as the template. See `adr/ADR-FTA-0001-*.md`.
17
+
18
+ ## Commands
19
+
20
+ ```bash
21
+ uv run --extra dev pytest -q # full suite (what CI runs)
22
+ uv run --extra dev pytest -q tests/test_config.py # one file
23
+ uv run --extra dev pytest -q tests/test_config.py::test_name # one test
24
+ uv build # sdist/wheel (hatchling)
25
+ uv run foundry-testing-actor lint <folder> # validate a sidecar
26
+ uv run foundry-testing-actor show <folder> --registry r # every derived rendering
27
+ ```
28
+
29
+ There is no separate lint/format command configured in this repo.
30
+
31
+ ## Architecture
32
+
33
+ Modules under `src/foundry_testing_actor/`:
34
+
35
+ - **`config.py`** — `CapabilityConfig`. Loads the sidecar and derives **every** rendering of the
36
+ capability id from `capability` and `source_repo` (plus `implementation_repo`, derived unless
37
+ declared). Carries `lint()`, `Report`, `cards_path()` and `runner_path()`.
38
+ - **`grounding.py`** — copied from FIA: runs each `ground_in` `fetch:`, writes envelopes inside
39
+ the clone, renders the `CLAUDE.md` that `@`-imports the eager ones.
40
+ - **`engine.py`** — `ClaudeCodeTesterEngine`. One engine, two doors, dispatched on the door id.
41
+ `propose-acceptance`: read-only clones (testing repo + implementation DEFAULT branch), `Read,
42
+ Glob,Grep`, small budget, projected JSON reply. `test-task`: rw testing clone on `test/<id>`,
43
+ implementation clone on `impl/<id>` for image recomputation only, full tool session. Never
44
+ commits.
45
+ - **`handler.py`** — `make_test_task(config)`. Containment, commit, push, publish. Never opens a PR.
46
+ - **`correlation.py`** — the two ids and the step vocabulary; byte-identical to FIA's.
47
+ - **`conformance.py`**, **`instance.py`**, **`serve.py`**, **`cli.py`** — copied from FIA, renamed.
48
+
49
+ Beside them, three folders shipped in the wheel: `schemas/` (the sidecar contract), `cards/` (the
50
+ actor's four cards), and `runner/` (the default test-runner Dockerfile a component without
51
+ `runner:` is built with).
52
+
53
+ ## Core invariants that any change must preserve
54
+
55
+ - **The proposal is never derived from the build.** `propose-acceptance` clones the implementation
56
+ repo's default branch and never checks out `impl/<task_id>`; `test-task` checks it out but never
57
+ shows it to the session. `tests/test_engine.py` pins both. This is ADR-FIA-0004's "before or
58
+ after", and the whole value of the round.
59
+ - **A door that cannot write beats a door asked not to.** `PROPOSE_TOOLS` carries no Write, Edit or
60
+ Bash. The propose door registers no handler.
61
+ - **The wire contract is shared with two other packages.** Door ids, message names and fields are
62
+ fixed by the three-amigos contract FIA and the orchestration actor also implement. Do not rename
63
+ them; `tests/test_cards.py` pins the shapes.
64
+ - **A use's cards are rendered, and hand copies drift.** `conformance.check` compares the DERIVED
65
+ wire contract, never prose. CI runs `lint-card` against the built wheel.
66
+ - **No capability literal, no knowledge tool name, ever.** `tests/test_portability.py` greps `src/`
67
+ for both. A knowledge source is a `ground_in` entry, never a change here.
68
+ - **The image refs are three-way contracts.** `<registry>/<capability_path>/<component>/tests:<v>`
69
+ (published) and `<registry>/<capability_path>/<component>:<v>` (recomputed, must match what the
70
+ implementation actor published). `config.test_image_ref` / `config.image_ref` output or nothing.
71
+ - **The write boundary is stated once**, as `components[].tests`. Resolution is longest-prefix.
72
+ - **The propose reply is projected** onto the completion message's fields. The framework leaves a
73
+ single-outcome completion schema open, so a session's stray keys would otherwise reach the
74
+ orchestrating actor dressed as part of a shared contract.
75
+ - **The observability record schema is the contract; step names are not.** Every emitted log line
76
+ is budgeted to 64 KB.
77
+ - **The clone is full, never `--depth 1`** — `papeete_version.compute()` needs tags reachable.
78
+ - **The generated `CLAUDE.md` appends** to one the repo already commits. Never overwrite.
79
+ - **The image is one build in two registries** (FIA's ADR-FIA-0006, copied). Never a second
80
+ `docker build`, never a hardcoded product registry.
81
+ - **Never set `ANTHROPIC_API_KEY` in a container running this.**
82
+
83
+ ## Scope discipline
84
+
85
+ Generic across **capabilities**, not across actor *kinds*. The machinery shared with FIA is a
86
+ **copy by decision**, not a shared kit (ADR-FTA-0001): do not extract a common package, and do not
87
+ change FIA from here. No hooks, no base classes, no strategy objects for a hypothetical consumer.
88
+
89
+ Design rationale lives in `adr/`. Add a new ADR (copy `adr/template.md`) for any decision of
90
+ similar weight rather than only writing it into code comments.
91
+
92
+ ## Releasing
93
+
94
+ Tag-triggered (`v*`) via `.github/workflows/release.yml`: PyPI through Trusted Publishing, the image
95
+ to GHCR and, when `vars.PRODUCT_IMAGE` names one, to a product registry too. A manual run takes a
96
+ tag as input to backfill an image; it skips PyPI and does not move `latest`. `ci.yml` runs the
97
+ suite, the wheel gates (contract, cards, the gate must fail, grounding probe), and builds + boots
98
+ the example use.