foundry-testing-actor 0.1.0__py3-none-any.whl
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.
- foundry_testing_actor/__init__.py +56 -0
- foundry_testing_actor/cards/actor-data.yaml +68 -0
- foundry_testing_actor/cards/actor-message.yaml +32 -0
- foundry_testing_actor/cards/actor-synchronous-messaging.yaml +59 -0
- foundry_testing_actor/cards/actor.yaml +19 -0
- foundry_testing_actor/cli.py +181 -0
- foundry_testing_actor/config.py +525 -0
- foundry_testing_actor/conformance.py +133 -0
- foundry_testing_actor/correlation.py +193 -0
- foundry_testing_actor/engine.py +889 -0
- foundry_testing_actor/grounding.py +206 -0
- foundry_testing_actor/handler.py +248 -0
- foundry_testing_actor/instance.py +129 -0
- foundry_testing_actor/runner/Dockerfile +32 -0
- foundry_testing_actor/schemas/agentic-context.schema.yaml +168 -0
- foundry_testing_actor/serve.py +129 -0
- foundry_testing_actor-0.1.0.dist-info/METADATA +258 -0
- foundry_testing_actor-0.1.0.dist-info/RECORD +20 -0
- foundry_testing_actor-0.1.0.dist-info/WHEEL +4 -0
- foundry_testing_actor-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# agentic-context.schema.yaml — the foundry-testing-actor/agentic-context contract.
|
|
2
|
+
#
|
|
3
|
+
# WHAT THIS FILE IS FOR. One capability's own declaration of what this actor's non-deterministic
|
|
4
|
+
# doors should ground themselves in, where each component's black-box tests live, which runner
|
|
5
|
+
# builds them, and which implementation they are written against. The machinery in this package
|
|
6
|
+
# carries no capability literal of its own — every identifier it needs is DERIVED from
|
|
7
|
+
# `capability` and `source_repo` below (see `config.py`), so a second capability instantiates the
|
|
8
|
+
# same actor by writing this file and nothing else.
|
|
9
|
+
#
|
|
10
|
+
# THE CONTRACT IS COMMITTED SOURCE, NOT A FETCHED GATE. It lives inside the package, the same path
|
|
11
|
+
# in a source checkout and in an installed wheel, so `foundry-testing-actor lint` needs no network
|
|
12
|
+
# and no credential. Same discipline as papeete-actor's own manifest schema, and as the sibling
|
|
13
|
+
# `foundry-implementation-actor/agentic-context/v1` this one is shaped after.
|
|
14
|
+
#
|
|
15
|
+
# ─── WHY A SIDECAR AND NOT THE CARD ───────────────────────────────────────────────────────────
|
|
16
|
+
# `papeete_actor_synchronous_messaging.card` opens exactly four hardcoded filenames and never
|
|
17
|
+
# globs, so `Actor.from_card()` will never see this file. That is deliberate: this is the actor's
|
|
18
|
+
# own operational configuration, not part of any published card contract, and it is loaded by this
|
|
19
|
+
# package's `config.py` directly.
|
|
20
|
+
#
|
|
21
|
+
# ─── WHY `writes_only_under` IS NOT DECLARED HERE ─────────────────────────────────────────────
|
|
22
|
+
# The hand-written actor this replaces declared it beside a hardcoded copy in the module that
|
|
23
|
+
# ENFORCED it. There is now exactly one source: `components[].tests`. A boundary that can be stated
|
|
24
|
+
# twice will eventually be stated differently.
|
|
25
|
+
#
|
|
26
|
+
# ─── WHY THE READS OF `required` ARE FROM HERE ────────────────────────────────────────────────
|
|
27
|
+
# `config.py` reads both `required` lists below rather than carrying its own copy. The sibling
|
|
28
|
+
# package carried a hardcoded key tuple beside its schema's `items.required`, and noticed only when
|
|
29
|
+
# a removal had to touch both (ADR-FIA-0002's follow-up). This package starts without the second
|
|
30
|
+
# copy.
|
|
31
|
+
|
|
32
|
+
contract: foundry-testing-actor/agentic-context/v1
|
|
33
|
+
|
|
34
|
+
kind: agentic-context
|
|
35
|
+
|
|
36
|
+
location: actor-agentic-context.yaml # at the actor's folder root, beside actor.yaml
|
|
37
|
+
|
|
38
|
+
required:
|
|
39
|
+
- context
|
|
40
|
+
- engine
|
|
41
|
+
- capability
|
|
42
|
+
- source_repo
|
|
43
|
+
- registry_repo
|
|
44
|
+
- components
|
|
45
|
+
- ground_in
|
|
46
|
+
|
|
47
|
+
fields:
|
|
48
|
+
|
|
49
|
+
context:
|
|
50
|
+
type: string
|
|
51
|
+
const: foundry-testing-actor/agentic-context/v1
|
|
52
|
+
doc: >-
|
|
53
|
+
The contract this file claims. A file declaring some other value is read, warned, and not
|
|
54
|
+
checked further — UNMIGRATED is not the same as non-conformant, and migration is each
|
|
55
|
+
pair's own act.
|
|
56
|
+
|
|
57
|
+
engine:
|
|
58
|
+
type: string
|
|
59
|
+
doc: >-
|
|
60
|
+
The engine name this actor's doors name on its card (both of them). Registered under
|
|
61
|
+
exactly this key in `Actor.from_card(engines={...})`.
|
|
62
|
+
|
|
63
|
+
capability:
|
|
64
|
+
type: string
|
|
65
|
+
doc: >-
|
|
66
|
+
The capability id, in its canonical dotted form with the `CAP` segment present — e.g.
|
|
67
|
+
`<ENT>.<DOMAIN>.CAP.<TYPE>.<NNN>.<CODE>`. Every other rendering of the id is derived from
|
|
68
|
+
this one; none is declared. See `config.py` for the full table.
|
|
69
|
+
|
|
70
|
+
source_repo:
|
|
71
|
+
type: string
|
|
72
|
+
doc: >-
|
|
73
|
+
`<owner>/<repo>` of the TESTING repository this actor clones, writes black-box tests into,
|
|
74
|
+
and pushes a `test/<task_id>` branch to. The actor's own name and its git commit identity
|
|
75
|
+
are derived from the repo half.
|
|
76
|
+
|
|
77
|
+
implementation_repo:
|
|
78
|
+
type: string
|
|
79
|
+
optional: true
|
|
80
|
+
default: "<owner of source_repo>/<capability>-implementation"
|
|
81
|
+
doc: >-
|
|
82
|
+
`<owner>/<repo>` of the repository the implementation actor pushes to. Read, never written.
|
|
83
|
+
`propose-acceptance` reads its DEFAULT BRANCH only — the state before the increment, never
|
|
84
|
+
`impl/<task_id>` (ADR-FTA-0002). `test-task` checks out `impl/<task_id>` solely to recompute
|
|
85
|
+
the image refs that actor published; the session is not shown that source. Declare it only
|
|
86
|
+
when the repo does not follow the `<capability>-implementation` convention.
|
|
87
|
+
|
|
88
|
+
registry_repo:
|
|
89
|
+
type: string
|
|
90
|
+
doc: >-
|
|
91
|
+
`<owner>/<repo>` of the knowledge registry the `ground_in` fetches resolve through. There is
|
|
92
|
+
NO default: a fetch that silently reaches a fallback registry is worse than one that fails.
|
|
93
|
+
|
|
94
|
+
components:
|
|
95
|
+
type: array
|
|
96
|
+
doc: >-
|
|
97
|
+
The components this actor writes black-box tests for and publishes one test image of. Each
|
|
98
|
+
entry declares where its tests live and, optionally, which runner builds them — two things
|
|
99
|
+
that were hardcoded, in two different files, before this contract existed. A component's
|
|
100
|
+
`name` is also the folder its implementation lives under in `implementation_repo`, which is
|
|
101
|
+
where the ref of the image under test is recomputed from.
|
|
102
|
+
items:
|
|
103
|
+
required: [name, tests]
|
|
104
|
+
fields:
|
|
105
|
+
name:
|
|
106
|
+
type: string
|
|
107
|
+
doc: >-
|
|
108
|
+
The component's own name — as a caller names it in a payload's `components`, and as it
|
|
109
|
+
appears in both the image under test and the published test image refs.
|
|
110
|
+
tests:
|
|
111
|
+
type: string
|
|
112
|
+
doc: >-
|
|
113
|
+
The component's PERSISTENT, accumulated test root inside the testing repo, trailing
|
|
114
|
+
slash included (e.g. `backend/tests/`). The union of these IS the write boundary —
|
|
115
|
+
nothing else declares it. It is also the build context of the test image, so the image
|
|
116
|
+
always carries the full regression suite rather than one task's slice. May be more than
|
|
117
|
+
one segment deep; the component a staged path belongs to is resolved by longest
|
|
118
|
+
prefix.
|
|
119
|
+
runner:
|
|
120
|
+
type: string
|
|
121
|
+
optional: true
|
|
122
|
+
default: "the runner shipped in foundry-testing-actor (runner/Dockerfile)"
|
|
123
|
+
doc: >-
|
|
124
|
+
The directory holding the Dockerfile that turns this component's tests root into a
|
|
125
|
+
runnable image, as `buildctl --local dockerfile=` takes it — relative to the testing
|
|
126
|
+
repo's root, and it must exist in the clone. OMIT IT to use the runner this package
|
|
127
|
+
ships: a lean pytest image whose CMD is the contract the orchestrating actor runs. A
|
|
128
|
+
capability declares its own only when its tests need something that runner does not
|
|
129
|
+
carry; the declared one then owns that contract.
|
|
130
|
+
|
|
131
|
+
ground_in:
|
|
132
|
+
type: array
|
|
133
|
+
doc: >-
|
|
134
|
+
What the session is grounded in, before its first turn, at both doors. Each entry is a
|
|
135
|
+
command to run and a place to put its output inside the clone — this package hardcodes no
|
|
136
|
+
knowledge tool. A fourth knowledge source is an entry here, not a change to this package.
|
|
137
|
+
items:
|
|
138
|
+
required: [name, answers, fetch, into, load]
|
|
139
|
+
fields:
|
|
140
|
+
name:
|
|
141
|
+
type: string
|
|
142
|
+
doc: Short identifier for the source. Titles its section in the rendered Markdown.
|
|
143
|
+
answers:
|
|
144
|
+
type: string
|
|
145
|
+
doc: >-
|
|
146
|
+
One line saying what question this source answers. It is not decoration: for an
|
|
147
|
+
on-demand entry it is the ONLY thing the session sees, and what it decides to read
|
|
148
|
+
the file on.
|
|
149
|
+
fetch:
|
|
150
|
+
type: array
|
|
151
|
+
doc: >-
|
|
152
|
+
argv to run, as a list. `{capability}`, `{registry_repo}`, `{source_repo}` and
|
|
153
|
+
`{implementation_repo}` are substituted from the fields above. Non-zero exit, a missing
|
|
154
|
+
binary, or a timeout all fail the request before any session time is spent.
|
|
155
|
+
into:
|
|
156
|
+
type: string
|
|
157
|
+
doc: >-
|
|
158
|
+
Repo-relative path to write the envelope to, INSIDE the clone. Inside, because a
|
|
159
|
+
CLAUDE.md `@`-import resolves relative to the file that contains it — an envelope in
|
|
160
|
+
a sibling tempdir cannot be imported, only described.
|
|
161
|
+
load:
|
|
162
|
+
type: string
|
|
163
|
+
enum: [eager, on-demand]
|
|
164
|
+
doc: >-
|
|
165
|
+
`eager` is `@`-imported by the generated CLAUDE.md and enters the window before turn
|
|
166
|
+
one, unconditionally, every session. `on-demand` is listed by its `answers` line and
|
|
167
|
+
its path, and costs nothing until the session opens it. Choose from a measurement of
|
|
168
|
+
the envelope, not from taste.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""The entrypoint — moved here so two uses cannot wire their observability differently.
|
|
2
|
+
|
|
3
|
+
WHAT THIS REPLACES. A hand-written `app.py` in every use's repo: some sixty lines that configure
|
|
4
|
+
observability, add a console handler beside the OTLP one, install the correlation filter, read
|
|
5
|
+
`PORT`, build an `HttpMailbox`, call `Actor.from_card`, and emit an `actor-started` event. Not one
|
|
6
|
+
line of it is about the capability. Worse, the reasoning behind the strangest line — the console
|
|
7
|
+
handler — was discovered the hard way by an operator and then existed only as a comment someone
|
|
8
|
+
would have to copy correctly (ADR-FIA-0005, copied here by ADR-FTA-0001).
|
|
9
|
+
|
|
10
|
+
WHY THE IMPORTS ARE LAZY. `papeete-actor-synchronous-messaging-http` and `papeete-observability`
|
|
11
|
+
are how this actor is CARRIED. ADR-PAM-0001 keeps wire out of the message contract, and the same
|
|
12
|
+
discipline keeps an HTTP server out of the dependency set of a consumer that only wants
|
|
13
|
+
`CapabilityConfig`. They arrive through the `serve` extra, and importing them inside the function
|
|
14
|
+
is what lets `foundry-testing-actor lint` run in a venv that has neither.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
import tempfile
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from . import correlation
|
|
25
|
+
from .config import CapabilityConfig
|
|
26
|
+
from .engine import ClaudeCodeTesterEngine
|
|
27
|
+
from .handler import make_test_task
|
|
28
|
+
from .instance import CARD_FILES, render_cards
|
|
29
|
+
|
|
30
|
+
DEFAULT_PORT = 8080
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ServeError(RuntimeError):
|
|
34
|
+
"""Raised when the wire half of this actor is not installed."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _imports():
|
|
38
|
+
try:
|
|
39
|
+
from papeete_actor_synchronous_messaging.actor import Actor
|
|
40
|
+
from papeete_actor_synchronous_messaging_http.mailbox import HttpMailbox
|
|
41
|
+
from papeete_observability import configure
|
|
42
|
+
except ImportError as e: # pragma: no cover - exercised in the image, not the suite
|
|
43
|
+
raise ServeError(
|
|
44
|
+
f"the `serve` extra is not installed: {e}. `foundry-testing-actor serve` needs "
|
|
45
|
+
f"a mailbox and an observability backend, which are wire concerns this package does "
|
|
46
|
+
f"not depend on by default — install `foundry-testing-actor[serve]`, or use "
|
|
47
|
+
f"the image this package publishes, which already has."
|
|
48
|
+
) from e
|
|
49
|
+
return Actor, HttpMailbox, configure
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _cards_for(config: CapabilityConfig, folder: Path) -> Path:
|
|
53
|
+
"""The folder `Actor.from_card` is pointed at.
|
|
54
|
+
|
|
55
|
+
Rendered at `docker build` time in the normal case, and this finds them already sitting beside
|
|
56
|
+
the sidecar. A use that skipped that step gets them rendered into a tempdir now — the cards are
|
|
57
|
+
derived from the definition either way, so there is no version of this that reads differently.
|
|
58
|
+
Build time is still the better place: it is inspectable (`cat /actor/actor.yaml`), it fails
|
|
59
|
+
while someone is watching a build rather than at a boot, and it needs no writable filesystem.
|
|
60
|
+
"""
|
|
61
|
+
if all((folder / name).exists() for name in CARD_FILES):
|
|
62
|
+
return folder
|
|
63
|
+
rendered = Path(tempfile.mkdtemp(prefix=f"{config.actor_slug}-cards-"))
|
|
64
|
+
render_cards(config, rendered)
|
|
65
|
+
correlation.event(
|
|
66
|
+
"cards-rendered", folder=str(rendered), actor=config.actor_name,
|
|
67
|
+
because="the actor's folder carried no cards; render them at build time to have them "
|
|
68
|
+
"beside the sidecar instead",
|
|
69
|
+
)
|
|
70
|
+
return rendered
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def serve(folder: str | Path = ".", port: int | None = None) -> None:
|
|
74
|
+
"""Boot this actor and answer its doors until the process is stopped."""
|
|
75
|
+
Actor, HttpMailbox, configure = _imports()
|
|
76
|
+
|
|
77
|
+
configure()
|
|
78
|
+
# papeete_observability.configure() attaches an OTLP log handler to the root logger but never
|
|
79
|
+
# raises its level off the stdlib default WARNING, so logging.info(...) calls (including
|
|
80
|
+
# HttpMailbox's own access log) would otherwise never reach it.
|
|
81
|
+
logging.getLogger().setLevel(logging.INFO)
|
|
82
|
+
# ...and a console handler beside it. The OTLP handler alone is write-only from an operator's
|
|
83
|
+
# seat: with nothing on stdout, `kubectl logs` shows this pod's startup line and nothing else —
|
|
84
|
+
# so a record that never reaches the log backend is indistinguishable from a record that was
|
|
85
|
+
# never emitted, exactly when the telemetry backend is the thing under suspicion. Found the
|
|
86
|
+
# hard way, against an HTTP binding that predated its own OTel wiring and emitted no records at
|
|
87
|
+
# all, where `kubectl logs` could not have shown it. `StreamHandler.emit()` flushes per record,
|
|
88
|
+
# so this needs no PYTHONUNBUFFERED to survive a container's block-buffered stdout.
|
|
89
|
+
#
|
|
90
|
+
# The formatter applies to THIS handler only — the OTLP `LoggingHandler` builds its body from
|
|
91
|
+
# `record.getMessage()`, so what the log backend stores stays the bare message (for the
|
|
92
|
+
# engine's own stream-json records, exactly the JSON object it logged).
|
|
93
|
+
console = logging.StreamHandler(sys.stdout)
|
|
94
|
+
console.setFormatter(
|
|
95
|
+
correlation.ConsoleFormatter("%(asctime)s %(levelname)s %(name)s %(message)s")
|
|
96
|
+
)
|
|
97
|
+
logging.getLogger().addHandler(console)
|
|
98
|
+
# ...and, on both handlers, the filter that stamps every record with this request's own
|
|
99
|
+
# correlation ids. On the HANDLERS, not the root logger: a record from a named logger (the HTTP
|
|
100
|
+
# binding's own access log, most of all) reaches the root's handlers without the root LOGGER's
|
|
101
|
+
# filters ever seeing it — see correlation.py's own docstring.
|
|
102
|
+
correlation.install()
|
|
103
|
+
|
|
104
|
+
folder = Path(folder)
|
|
105
|
+
# The sidecar is the only thing in that folder this package did not put there.
|
|
106
|
+
config = CapabilityConfig.load(folder)
|
|
107
|
+
cards = _cards_for(config, folder)
|
|
108
|
+
|
|
109
|
+
# PORT, not a hardcoded default: an env var is how an environment moves it without editing an
|
|
110
|
+
# image, and it is what a Service's targetPort is set against.
|
|
111
|
+
port = port if port is not None else int(os.environ.get("PORT", str(DEFAULT_PORT)))
|
|
112
|
+
mailbox = HttpMailbox(port=port) # 0.0.0.0:<port> — one POST per door, plus GET /health
|
|
113
|
+
actor = Actor.from_card(
|
|
114
|
+
cards,
|
|
115
|
+
mailbox=mailbox,
|
|
116
|
+
# One engine instance serves every door that names one: `Actor.judge()` hands it the door
|
|
117
|
+
# id and it dispatches on that. The key comes from the sidecar rather than a literal here,
|
|
118
|
+
# so a use whose sidecar names another engine is caught by its own card.
|
|
119
|
+
engines={config.engine: ClaudeCodeTesterEngine(config)},
|
|
120
|
+
# `propose-acceptance` needs no entry: it is a query with an engine and no handler, so
|
|
121
|
+
# the engine's own judgement is the reply and there is no deterministic half to contain.
|
|
122
|
+
actions={"test-task": make_test_task(config)},
|
|
123
|
+
)
|
|
124
|
+
# An `event` record rather than a `print`: a restart in the middle of a run is one of the most
|
|
125
|
+
# explanatory things a pipeline panel can show, and `print` reaches only the container's own
|
|
126
|
+
# stdout — never the OTLP handler, so never the log backend.
|
|
127
|
+
correlation.event("actor-started", actor=actor.name, port=port, capability=config.capability,
|
|
128
|
+
cards=str(cards))
|
|
129
|
+
mailbox.serve_forever()
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: foundry-testing-actor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Runs a headless Claude Code black-box test-authoring session against one capability's own testing repo — a papeete-actor for one use, with the capability supplied by a sidecar.
|
|
5
|
+
Project-URL: Homepage, https://github.com/papeete-hub/foundry-testing-actor
|
|
6
|
+
Author-email: Papeete Consulting <yoann.remy@outlook.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: actor-model,agentic,capability,claude-code,papeete,testing
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Topic :: Software Development :: Testing
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Requires-Dist: papeete-actor-synchronous-messaging>=0.2.1
|
|
15
|
+
Requires-Dist: papeete-version>=0.1.0
|
|
16
|
+
Requires-Dist: pyyaml>=6.0
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
19
|
+
Provides-Extra: serve
|
|
20
|
+
Requires-Dist: papeete-actor-synchronous-messaging-http>=0.4.0; extra == 'serve'
|
|
21
|
+
Requires-Dist: papeete-observability>=0.1.0; extra == 'serve'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# foundry-testing-actor
|
|
25
|
+
|
|
26
|
+
Runs a headless [Claude Code](https://claude.com/claude-code) session that authors **black-box
|
|
27
|
+
tests** for one capability's increment, inside that capability's own testing repository — then
|
|
28
|
+
commits, pushes and publishes a runnable test image, inside a write boundary the actor enforces
|
|
29
|
+
rather than requests. Before anything is built, it proposes what it will assert.
|
|
30
|
+
|
|
31
|
+
An **actor, for one use**, with a [`papeete-actor`](https://github.com/papeete-hub/papeete-actor)
|
|
32
|
+
underneath. The `-actor` suffix is that claim; `papeete-actor-*` names, by contrast, are transverse
|
|
33
|
+
features *of* the framework (`ADR-ECO-0022`). It is the sibling of
|
|
34
|
+
[`foundry-implementation-actor`](https://github.com/papeete-hub/foundry-implementation-actor), and
|
|
35
|
+
is built exactly the way that package is.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install foundry-testing-actor
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
> **New here?** [`examples/`](examples/) walks through a complete, working use of this actor. Every
|
|
42
|
+
> command in it that does not build an image runs with no credentials and no network.
|
|
43
|
+
|
|
44
|
+
**It authors a harness; it does not execute it.** No session this actor starts brings up docker,
|
|
45
|
+
hits a live endpoint, or renders a pass/fail verdict. The orchestrating actor deploys the
|
|
46
|
+
increment into an ephemeral namespace, runs the test image this one published, and judges.
|
|
47
|
+
|
|
48
|
+
## What it is
|
|
49
|
+
|
|
50
|
+
The actor's **definition** — its four cards, and the machinery behind them. It carries **no
|
|
51
|
+
capability of its own**: the capability it serves arrives in a sidecar the consuming repo writes:
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
# actor-agentic-context.yaml
|
|
55
|
+
context: foundry-testing-actor/agentic-context/v1
|
|
56
|
+
engine: claude-code
|
|
57
|
+
capability: ACME.PARTS.CAP.SUP.007.WID
|
|
58
|
+
source_repo: acme-lab/ACME.PARTS.CAP.SUP.007.WID-testing
|
|
59
|
+
registry_repo: acme-lab/acme-governance
|
|
60
|
+
# implementation_repo: acme-lab/ACME.PARTS.CAP.SUP.007.WID-implementation ← derived; declare to override
|
|
61
|
+
components:
|
|
62
|
+
- {name: backend, tests: backend/tests/} # the runner this package ships
|
|
63
|
+
- {name: stub, tests: stub/tests/, runner: deployment/stub-runner} # a runner of its own
|
|
64
|
+
ground_in:
|
|
65
|
+
- name: business
|
|
66
|
+
answers: the WHAT/WHY — domain vision, business events, ubiquitous language
|
|
67
|
+
fetch: [kpack, pack, "{capability}", --deep, --compact, --registry-repo, "{registry_repo}"]
|
|
68
|
+
into: .foundry/business.md
|
|
69
|
+
load: eager
|
|
70
|
+
- name: process
|
|
71
|
+
answers: the HOW — aggregates, commands, policies, read-models, bus, api, JSON schemas
|
|
72
|
+
fetch: [kontract, fetch, "{capability}", --compact, --registry-repo, "{registry_repo}"]
|
|
73
|
+
into: .foundry/process.md
|
|
74
|
+
load: eager
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
and, beside it, a four-line Dockerfile:
|
|
78
|
+
|
|
79
|
+
```dockerfile
|
|
80
|
+
FROM ghcr.io/papeete-hub/foundry-testing-actor:0.1.0
|
|
81
|
+
RUN pip install --no-cache-dir kpack==2.0.1 kontract==0.1.0 # what this sidecar's ground_in names
|
|
82
|
+
COPY actor-agentic-context.yaml /actor/
|
|
83
|
+
RUN foundry-testing-actor render-cards /actor && foundry-testing-actor lint /actor
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**That is the whole repository** — beside the tests themselves, which are what this actor writes.
|
|
87
|
+
No cards, no entrypoint, no Python, and no runner Dockerfile unless a component wants its own.
|
|
88
|
+
|
|
89
|
+
### The sidecar, field by field
|
|
90
|
+
|
|
91
|
+
| field | required | default | what it is |
|
|
92
|
+
|---|---|---|---|
|
|
93
|
+
| `context` | yes | — | `foundry-testing-actor/agentic-context/v1` |
|
|
94
|
+
| `engine` | yes | — | the engine key both doors name; `claude-code` |
|
|
95
|
+
| `capability` | yes | — | the dotted id, `CAP` segment present |
|
|
96
|
+
| `source_repo` | yes | — | `<owner>/<repo>` of the **testing** repo: cloned, written, pushed to |
|
|
97
|
+
| `implementation_repo` | no | `<owner of source_repo>/<capability>-implementation` | read, never written |
|
|
98
|
+
| `registry_repo` | yes | none, deliberately | the knowledge registry `ground_in` fetches resolve through |
|
|
99
|
+
| `components[].name` | yes | — | as callers name it; also the implementation's folder for that component |
|
|
100
|
+
| `components[].tests` | yes | — | the persistent tests root, trailing `/`. **The write boundary**, and the test image's build context |
|
|
101
|
+
| `components[].runner` | no | the runner shipped in this package | a directory in the testing repo holding the runner's Dockerfile |
|
|
102
|
+
| `ground_in[]` | yes | — | `name`, `answers`, `fetch` (argv), `into` (inside the clone), `load` (`eager`/`on-demand`) |
|
|
103
|
+
|
|
104
|
+
**Why the runner ships in the package.** The hand-written actor kept a lean pytest Dockerfile in
|
|
105
|
+
its own repo that named nothing about the capability — a Python base, pytest, `requests`,
|
|
106
|
+
`aio-pika`, and `CMD pytest`. Its CMD is the contract between this actor and the orchestrating
|
|
107
|
+
actor that runs what it publishes, so it belongs with the actor. `buildctl --local dockerfile=`
|
|
108
|
+
reads a directory client-side, so the copy inside the installed wheel is addressed exactly as a
|
|
109
|
+
directory in the clone would be. A capability whose tests need more declares `runner:` and owns
|
|
110
|
+
that file.
|
|
111
|
+
|
|
112
|
+
### Embedding it instead
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from foundry_testing_actor import CapabilityConfig, ClaudeCodeTesterEngine, make_test_task
|
|
116
|
+
|
|
117
|
+
config = CapabilityConfig.load(".")
|
|
118
|
+
actor = Actor.from_card(".", mailbox=mailbox,
|
|
119
|
+
engines={config.engine: ClaudeCodeTesterEngine(config)},
|
|
120
|
+
actions={"test-task": make_test_task(config)})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`propose-acceptance` needs no entry: it is a query with an engine and no handler, so the engine's
|
|
124
|
+
own judgement is the reply. The mailbox and observability backend `serve` needs live in the
|
|
125
|
+
`[serve]` extra.
|
|
126
|
+
|
|
127
|
+
## Two doors
|
|
128
|
+
|
|
129
|
+
| door | verb | what it does |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| `propose-acceptance` | query | proposes the acceptance surface, before anything is built. Writes nothing |
|
|
132
|
+
| `test-task` | request | extends the suite, commits, pushes `test/<task_id>`, publishes one test image per touched component |
|
|
133
|
+
|
|
134
|
+
Both name the same engine. `Actor.judge()` hands it the door id, and it dispatches on that.
|
|
135
|
+
|
|
136
|
+
### `propose-acceptance` — this actor's half of the three amigos round
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
orchestration ──▶ testing: propose-acceptance {task_id, title, definition_of_done, components, context?}
|
|
140
|
+
◀── {expectations: [{id, statement, handle, component?}], open_questions: [...]}
|
|
141
|
+
orchestration ──▶ implementation: assess-task {..., acceptance_surface: expectations}
|
|
142
|
+
◀── {feasible, objections, commitments}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
A **query**, read-only: a clone of the testing repo, a clone of the implementation repo's **default
|
|
146
|
+
branch** — never `impl/<task_id>` — grounded exactly as the test door is, and a session invoked with
|
|
147
|
+
`Read,Glob,Grep` and nothing that writes. Its budget is the implementation actor's assess budget
|
|
148
|
+
(15 turns, 600 s), as constructor keywords.
|
|
149
|
+
|
|
150
|
+
The session is told three things that matter: **nothing is built yet**; where a test needs a
|
|
151
|
+
concrete value the task does not name — a fixture id, a routing key — **propose one**, for the
|
|
152
|
+
implementer to commit to or object to; and anything the task does not determine goes in
|
|
153
|
+
`open_questions`, **never** into an invented statement. A non-empty `open_questions` stops the round
|
|
154
|
+
and reaches a human. See `adr/ADR-FTA-0002-*.md`.
|
|
155
|
+
|
|
156
|
+
The reply is **projected** onto `{expectations, open_questions}` — a session's stray keys never
|
|
157
|
+
reach the orchestrating actor dressed as contract — after checking that every expectation carries
|
|
158
|
+
an `id`, a `statement` and a `handle`, with ids unique. `open_questions` is always a list.
|
|
159
|
+
|
|
160
|
+
### `test-task`
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
clone the testing repo (full) → checkout -b test/TASK-NNN
|
|
164
|
+
clone the implementation repo → checkout impl/TASK-NNN, recompute each image under test (never shown to the session)
|
|
165
|
+
run every ground_in fetch, write it into the testing clone, render CLAUDE.md
|
|
166
|
+
claude --print --output-format stream-json --permission-mode acceptEdits
|
|
167
|
+
git add <each tests root>; refuse anything staged outside them
|
|
168
|
+
commit as the actor, force-push test/TASK-NNN
|
|
169
|
+
buildctl build + push one test image per touched component
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
With an `acceptance_surface`, the prompt says it was agreed with the implementer before anything
|
|
173
|
+
was built: each expectation is asserted by its id, addressed exactly via its handle, wins where it
|
|
174
|
+
is more specific than the DoD, and is named in each test. With a `remediation_context`, the session
|
|
175
|
+
decides per failing criterion whether the test or the implementation is at fault, and fixes only
|
|
176
|
+
the former.
|
|
177
|
+
|
|
178
|
+
It never opens a pull request, and never runs what it wrote beyond `pytest --collect-only`.
|
|
179
|
+
|
|
180
|
+
## Derived renderings, zero literals
|
|
181
|
+
|
|
182
|
+
`foundry-testing-actor show` prints them for a given sidecar:
|
|
183
|
+
|
|
184
|
+
| rendering | from |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `ACME.PARTS.CAP.SUP.007.WID-testing` | actor name, git `user.name` (repo half of `source_repo`) |
|
|
187
|
+
| `acme-lab/ACME.PARTS.CAP.SUP.007.WID-implementation` | `implementation_repo`, when not declared |
|
|
188
|
+
| `acme.parts/sup.007.wid` | the id, split at its `CAP` segment |
|
|
189
|
+
| `acme.parts.cap.sup.007.wid-backend-tests` | the name `papeete-version` versions the test image under |
|
|
190
|
+
| `<registry>/acme.parts/sup.007.wid/backend/tests:<version>` | **the test image this actor publishes** |
|
|
191
|
+
| `<registry>/acme.parts/sup.007.wid/backend:<version>` | **the image under test**, recomputed |
|
|
192
|
+
|
|
193
|
+
**Both refs are a three-way contract.** The implementation actor publishes the second; the
|
|
194
|
+
orchestrating actor runs the first and parses both apart. Derivation output or nothing.
|
|
195
|
+
|
|
196
|
+
## Grounding, containment, observability
|
|
197
|
+
|
|
198
|
+
Identical to `foundry-implementation-actor`'s, deliberately copied rather than shared
|
|
199
|
+
(ADR-FTA-0001): envelopes are written **inside the clone** and `@`-imported from a generated
|
|
200
|
+
`CLAUDE.md` (appended to a committed one, never substituting it); containment is `git add <root>`
|
|
201
|
+
then a prefix assertion with longest-prefix component resolution; every emitted log line is
|
|
202
|
+
budgeted to 64 KB; the record schema `{event: step|event|result, ...}` plus `correlation_id` and
|
|
203
|
+
`task_id` is the contract, step names are not.
|
|
204
|
+
|
|
205
|
+
Step names this actor emits: `clone-tests`, `clone-code`, `ground-<name>`, `claude-session` or
|
|
206
|
+
`propose-session`, `containment-commit`, `push-branch`, `publish-test-image`, plus the events
|
|
207
|
+
`images-under-test`, `acceptance-proposed`, `components-touched`, `test-image-published`,
|
|
208
|
+
`test-task-refused`.
|
|
209
|
+
|
|
210
|
+
## Credentials
|
|
211
|
+
|
|
212
|
+
| variable | what for |
|
|
213
|
+
|---|---|
|
|
214
|
+
| `GITHUB_TOKEN` | fine-grained PAT: `contents:write` on `source_repo`; read-only `contents` on `implementation_repo` and on whatever the `ground_in` fetches resolve through |
|
|
215
|
+
| `CLAUDE_CODE_OAUTH_TOKEN` | from `claude setup-token`, tied to a Pro/Max/Team/Enterprise subscription |
|
|
216
|
+
| `IMAGE_REGISTRY`, `BUILDKIT_HOST`, `DOCKER_CONFIG` | `test-task` only: where images are recomputed from and pushed to |
|
|
217
|
+
|
|
218
|
+
> **Do not also set `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN`.** In `claude -p`
|
|
219
|
+
> non-interactive mode an API key present in the environment is ALWAYS preferred over
|
|
220
|
+
> `CLAUDE_CODE_OAUTH_TOKEN`, silently routing every session through metered billing.
|
|
221
|
+
|
|
222
|
+
## CLI
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
foundry-testing-actor lint . # validate the sidecar and the cards
|
|
226
|
+
foundry-testing-actor show . --registry reg.example.com # every derived rendering
|
|
227
|
+
foundry-testing-actor render-cards . # write the four cards from the wheel
|
|
228
|
+
foundry-testing-actor serve . # boot it (needs the `serve` extra)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Where this came from
|
|
232
|
+
|
|
233
|
+
Ported from one capability's hand-written testing actor — an `app.py`, an engine that named two
|
|
234
|
+
knowledge tools and their registry as module constants, a handler whose write boundary and
|
|
235
|
+
capability path were module constants too, four cards, and a runner Dockerfile — using
|
|
236
|
+
`foundry-implementation-actor` as the template. `adr/` records the decisions.
|
|
237
|
+
|
|
238
|
+
## Releasing, and which registry to pin
|
|
239
|
+
|
|
240
|
+
A tag (`v*`) publishes the wheel to PyPI and the image to two registries holding one digest, as
|
|
241
|
+
`foundry-implementation-actor` does (ADR-FIA-0006):
|
|
242
|
+
|
|
243
|
+
| Registry | Pin it when |
|
|
244
|
+
|---|---|
|
|
245
|
+
| `ghcr.io/papeete-hub/foundry-testing-actor` | you write a use's Dockerfile yourself and build it with your own Docker |
|
|
246
|
+
| a product's own registry, `vars.PRODUCT_IMAGE` | the use is built in-cluster, by a builder whose one registry credential is that product's |
|
|
247
|
+
|
|
248
|
+
A manual run backfills an image for an existing tag: `gh workflow run release.yml --ref main -f
|
|
249
|
+
tag=v0.1.0`. It skips PyPI and does not move `latest`.
|
|
250
|
+
|
|
251
|
+
## Development
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
uv run --extra dev pytest -q # what CI runs
|
|
255
|
+
uv build
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
There is no separate lint/format command configured in this repo.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
foundry_testing_actor/__init__.py,sha256=S2AERZkWdEbqmoVoNzDixWM22NsepCHYFKnf_8eW6qc,2308
|
|
2
|
+
foundry_testing_actor/cli.py,sha256=dxOIrjONTD8gtEk36JXtVTIvNF68OmqD5-gGr5GWun8,7621
|
|
3
|
+
foundry_testing_actor/config.py,sha256=iBWT80hiB2yP4IYcbrO69lDfvK2Eg7XhAotGUoh7Hig,25324
|
|
4
|
+
foundry_testing_actor/conformance.py,sha256=8WT_T75hJXRnoRlitqLE1JcM_kJTxiwQTrftIOad75E,7333
|
|
5
|
+
foundry_testing_actor/correlation.py,sha256=PmnlGDKzu9ZBB8NEHaB4mNTimYZSHdWw31BNFbb0nNo,10658
|
|
6
|
+
foundry_testing_actor/engine.py,sha256=cg9Yff2eq2O-79TXpa1-SE6EwxYnyNuE3meDkaEH4A0,50309
|
|
7
|
+
foundry_testing_actor/grounding.py,sha256=JIj8K15TR1ctXNrjoeb1SLS8rrVe4YObrLuo011i8mM,9502
|
|
8
|
+
foundry_testing_actor/handler.py,sha256=Yor47nNr2Mge0GdoAAwbVPC5hwYWcr3wjSWmjo3UrIo,12466
|
|
9
|
+
foundry_testing_actor/instance.py,sha256=hSQ2bP0dj7fs_AuGiiZP063f_iI64gp7VU-aBeMxD98,6708
|
|
10
|
+
foundry_testing_actor/serve.py,sha256=llRur42JG5E8h34Vs4e9MpXB02JzYSFif-dpwN6ngAk,7073
|
|
11
|
+
foundry_testing_actor/cards/actor-data.yaml,sha256=isEMJwEAwqNCmCKGbC3dKgpq56JMm_Y9DUkLlxl0sDw,3737
|
|
12
|
+
foundry_testing_actor/cards/actor-message.yaml,sha256=KW_uNIUJQ05UfVnodIh-xoa4uM_KRpnVcHEz8cJcsUY,1535
|
|
13
|
+
foundry_testing_actor/cards/actor-synchronous-messaging.yaml,sha256=dlY6QCj_uioZxQ2YXfxyjE0MwWZTfoc7jVQnjP6rGwE,4149
|
|
14
|
+
foundry_testing_actor/cards/actor.yaml,sha256=iSpegjN7VT_8oocfgX_Ily4GmUqzu-33GF9fgvAGDYM,1373
|
|
15
|
+
foundry_testing_actor/runner/Dockerfile,sha256=lCGX5wMRmAi--AbiMcXi9A6IA75p45DjgvtaATYVKg8,1955
|
|
16
|
+
foundry_testing_actor/schemas/agentic-context.schema.yaml,sha256=0UeeYF8TtT4C4SO0w_dr6f3QezyqK-tXqPLWRqQAtcQ,8629
|
|
17
|
+
foundry_testing_actor-0.1.0.dist-info/METADATA,sha256=26pfkedlhhqOaT-udJS-RIWA0dXb7oLEPa9qIPwMzTU,13505
|
|
18
|
+
foundry_testing_actor-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
19
|
+
foundry_testing_actor-0.1.0.dist-info/entry_points.txt,sha256=mmZNR9DfIhAQu3KXB-ruHwWNrmBsRjxQawxr4xZ8JQg,73
|
|
20
|
+
foundry_testing_actor-0.1.0.dist-info/RECORD,,
|