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,56 @@
|
|
|
1
|
+
"""foundry-testing-actor — run a headless Claude Code black-box test-authoring session against one
|
|
2
|
+
capability's own testing repo.
|
|
3
|
+
|
|
4
|
+
An actor, for one use, with a `papeete-actor` underneath. The capability it serves is supplied by
|
|
5
|
+
a sidecar (`actor-agentic-context.yaml`, `foundry-testing-actor/agentic-context/v1`), never by this
|
|
6
|
+
package: a second capability instantiates the same actor by writing that file and nothing else.
|
|
7
|
+
|
|
8
|
+
Two doors. `propose-acceptance` is this actor's half of the three amigos round — a query, answered
|
|
9
|
+
read-only, before anything is built (ADR-FTA-0002). `test-task` authors and extends the black-box
|
|
10
|
+
suite, commits it, and publishes one runnable test image per component it touched.
|
|
11
|
+
|
|
12
|
+
Wiring one up is four lines:
|
|
13
|
+
|
|
14
|
+
from foundry_testing_actor import CapabilityConfig, ClaudeCodeTesterEngine, make_test_task
|
|
15
|
+
|
|
16
|
+
config = CapabilityConfig.load(".")
|
|
17
|
+
actor = Actor.from_card(".", mailbox=mailbox,
|
|
18
|
+
engines={config.engine: ClaudeCodeTesterEngine(config)},
|
|
19
|
+
actions={"test-task": make_test_task(config)})
|
|
20
|
+
|
|
21
|
+
`correlation` is exported too — an entrypoint installs its filter on the root logger's handlers
|
|
22
|
+
after configuring observability, so every record the process emits carries this request's ids.
|
|
23
|
+
|
|
24
|
+
Those four lines are for embedding. A use that just wants the actor writes no Python at all: the
|
|
25
|
+
image this package publishes renders the cards from the sidecar and runs `serve`, which is those
|
|
26
|
+
four lines plus the observability wiring that used to be copied into every repo (ADR-FTA-0001).
|
|
27
|
+
"""
|
|
28
|
+
from .config import (CapabilityConfig, Component, ConfigError, Grounding, Report,
|
|
29
|
+
cards_path, lint, runner_path, version)
|
|
30
|
+
from .engine import ClaudeCodeTesterEngine
|
|
31
|
+
from .handler import HandlerError, make_test_task
|
|
32
|
+
from .instance import render_cards
|
|
33
|
+
from .serve import ServeError, serve
|
|
34
|
+
from . import conformance, correlation, grounding, instance
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"CapabilityConfig",
|
|
38
|
+
"ClaudeCodeTesterEngine",
|
|
39
|
+
"Component",
|
|
40
|
+
"ConfigError",
|
|
41
|
+
"Grounding",
|
|
42
|
+
"HandlerError",
|
|
43
|
+
"Report",
|
|
44
|
+
"cards_path",
|
|
45
|
+
"conformance",
|
|
46
|
+
"correlation",
|
|
47
|
+
"grounding",
|
|
48
|
+
"instance",
|
|
49
|
+
"lint",
|
|
50
|
+
"make_test_task",
|
|
51
|
+
"render_cards",
|
|
52
|
+
"runner_path",
|
|
53
|
+
"serve",
|
|
54
|
+
"ServeError",
|
|
55
|
+
"version",
|
|
56
|
+
]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# WHAT DATA EXISTS. `papeete-actor-data/v0`, owned by `papeete-actor-message` — a named, minimally
|
|
2
|
+
# typed dictionary. Every field here belongs to the ACTOR, not to any capability: a task id, the
|
|
3
|
+
# caller's statement of the task, what was proposed, what was agreed, and what came back. Nothing
|
|
4
|
+
# names a capability or a knowledge tool, because none of those are known until a sidecar supplies
|
|
5
|
+
# them. `components` is a list of NAMES; which tests root each one maps to is the sidecar's.
|
|
6
|
+
data: papeete-actor-data/v0
|
|
7
|
+
items:
|
|
8
|
+
- name: task_id
|
|
9
|
+
type: string
|
|
10
|
+
description: the TASK-NNN card id this actor was asked about (e.g. "TASK-009")
|
|
11
|
+
- name: title
|
|
12
|
+
type: string
|
|
13
|
+
description: the task's short title/summary, supplied by the caller — no card lookup is performed here
|
|
14
|
+
- name: context
|
|
15
|
+
type: string
|
|
16
|
+
description: free-text elaboration beyond the title — background, constraints, links; optional
|
|
17
|
+
- name: definition_of_done
|
|
18
|
+
type: list
|
|
19
|
+
description: the acceptance criteria the tests must assert against, as the caller states them
|
|
20
|
+
- name: components
|
|
21
|
+
type: list
|
|
22
|
+
description: >-
|
|
23
|
+
the component name(s) the task concerns (e.g. ["backend"]), as the caller names them — each
|
|
24
|
+
one a component this use's sidecar declares a tests root for
|
|
25
|
+
- name: remediation_context
|
|
26
|
+
type: string
|
|
27
|
+
description: >-
|
|
28
|
+
on a retry, the prior attempt's failing test criteria — the session judges whether the fault
|
|
29
|
+
is in the test itself (fix it here) or in the implementation (leave the test as-is); optional,
|
|
30
|
+
absent on a first attempt
|
|
31
|
+
- name: acceptance_surface
|
|
32
|
+
type: list
|
|
33
|
+
description: >-
|
|
34
|
+
the agreed observable expectations this increment must satisfy — proposed at this actor's
|
|
35
|
+
own propose-acceptance door, then accepted, with commitments, by the implementation actor,
|
|
36
|
+
before anything was built. Each entry is an object with at least `id`, `statement` and
|
|
37
|
+
`handle`; extra keys (an attached `commitments` list, for one) are allowed. The dictionary
|
|
38
|
+
types it as a list and stops there: the entry shape is written in the doors' own means
|
|
39
|
+
- name: expectations
|
|
40
|
+
type: list
|
|
41
|
+
description: >-
|
|
42
|
+
what this actor proposes to assert black-box once the increment is built, each an object
|
|
43
|
+
with a stable `id` (short, `E1` or kebab-case, unique within the proposal — a later verdict
|
|
44
|
+
names it), a `statement` of what must hold, a `handle` saying exactly how a black-box test
|
|
45
|
+
reaches it, and optionally the `component` it belongs to. Proposed BEFORE anything is built,
|
|
46
|
+
so a concrete value in a handle is a proposal the implementer commits to or objects to, never
|
|
47
|
+
a report of what was found
|
|
48
|
+
- name: open_questions
|
|
49
|
+
type: list
|
|
50
|
+
description: >-
|
|
51
|
+
what the task and the capability's standing context do not determine, and no proposed value
|
|
52
|
+
can settle — each a string, or an object with `about` and `question`. Not a failure: a
|
|
53
|
+
non-empty list stops the round and is meant to reach a human; empty when nothing is open
|
|
54
|
+
- name: accepted
|
|
55
|
+
type: boolean
|
|
56
|
+
description: whether the test-task request was accepted, pushed, and its test images published
|
|
57
|
+
- name: because
|
|
58
|
+
type: string
|
|
59
|
+
description: why a request was refused (containment violation, nothing staged, an undeclared component, ...)
|
|
60
|
+
- name: branch
|
|
61
|
+
type: string
|
|
62
|
+
description: the branch the tests were pushed to (test/TASK-NNN)
|
|
63
|
+
- name: images
|
|
64
|
+
type: list
|
|
65
|
+
description: >-
|
|
66
|
+
the full ref of each touched component's published test image,
|
|
67
|
+
`<registry>/<capability path>/<component>/tests:<version>`, by convention — the pass/fail
|
|
68
|
+
verdict is rendered by whichever actor runs it, never reported here
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# WHAT MESSAGES EXIST. `papeete-actor-message/v1`, owned by `papeete-actor-message` — named
|
|
2
|
+
# messages, each a pure regrouping of references into the data dictionary, under an intent.
|
|
3
|
+
# Wire-agnostic by that package's own design: WHAT a message is, never HOW it is carried.
|
|
4
|
+
message: papeete-actor-message/v1
|
|
5
|
+
messages:
|
|
6
|
+
- name: propose-acceptance-cmd
|
|
7
|
+
intent: >-
|
|
8
|
+
ask this actor what it will assert, black-box, about a TASK-NNN card once it is built — and
|
|
9
|
+
what the task leaves undetermined — BEFORE any of it is built
|
|
10
|
+
references: [task_id, title, definition_of_done, components, context]
|
|
11
|
+
optional: [context]
|
|
12
|
+
|
|
13
|
+
- name: acceptance-proposed-result
|
|
14
|
+
intent: >-
|
|
15
|
+
answer with the proposed expectations, each with a stable id, a statement and a handle, and
|
|
16
|
+
the questions the task leaves open
|
|
17
|
+
references: [expectations, open_questions]
|
|
18
|
+
optional: [open_questions]
|
|
19
|
+
|
|
20
|
+
- name: test-task-cmd
|
|
21
|
+
intent: ask this actor to author black-box tests for the published image(s) of a TASK-NNN card
|
|
22
|
+
references: [task_id, title, definition_of_done, components, context, remediation_context,
|
|
23
|
+
acceptance_surface]
|
|
24
|
+
optional: [context, remediation_context, acceptance_surface]
|
|
25
|
+
|
|
26
|
+
- name: test-task-result
|
|
27
|
+
intent: report that the tests were written, pushed, and their touched components' test images published
|
|
28
|
+
references: [accepted, branch, images]
|
|
29
|
+
|
|
30
|
+
- name: test-task-refused-result
|
|
31
|
+
intent: report that the task was not tested, and why
|
|
32
|
+
references: [accepted, because]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# THE DOORS. `synchronous-messaging-doors/v1` — one action that authors, one query that only
|
|
2
|
+
# proposes.
|
|
3
|
+
#
|
|
4
|
+
# `engine: claude-code` is this actor kind's declared engine, on BOTH doors. A use's sidecar
|
|
5
|
+
# repeats it in its own `engine:` field, and the entrypoint registers the engine under exactly that
|
|
6
|
+
# key — so a use whose sidecar names a different engine is caught by its own card rather than at
|
|
7
|
+
# the first request. One engine instance serves both: `Actor.judge()` hands it the door id, and it
|
|
8
|
+
# dispatches on that.
|
|
9
|
+
#
|
|
10
|
+
# WHY `propose-acceptance` IS A QUERY. An action is a promise to try; a query is a promise to
|
|
11
|
+
# answer, from this actor's own state and nothing invented. Proposing what will be asserted changes
|
|
12
|
+
# nothing — no branch, no commit, no image — and placing it under `queries:` makes that structural
|
|
13
|
+
# rather than merely promised. It is also why it registers no handler: with an engine and no
|
|
14
|
+
# handler, `Actor.receive()` returns the judged dict as the reply. See ADR-FTA-0002, and
|
|
15
|
+
# ADR-FIA-0004 for the round both halves belong to.
|
|
16
|
+
manifest: synchronous-messaging-doors/v1
|
|
17
|
+
actions:
|
|
18
|
+
- id: test-task
|
|
19
|
+
means: >-
|
|
20
|
+
the door for "author black-box tests for TASK-NNN of the capability I serve, now that its
|
|
21
|
+
increment is built". Send it here with the task id, title, definition of done, components,
|
|
22
|
+
and (optionally) context, remediation_context and the agreed `acceptance_surface` — the
|
|
23
|
+
caller supplies everything, no task card is looked up here. I clone my capability's testing
|
|
24
|
+
repository into my own private copy, and read-only clone its implementation repository at
|
|
25
|
+
impl/TASK-NNN solely to recompute, by convention, the image refs published there (never
|
|
26
|
+
passed to me, and never shown to my session); ground myself in that capability's own
|
|
27
|
+
standing context; and extend the persistent suite under each named component's declared
|
|
28
|
+
tests root — asserting every agreed expectation by its id, through its handle, where a
|
|
29
|
+
surface was sent. I never bring up the images, hit a live endpoint, or render a verdict.
|
|
30
|
+
Then I commit and push to test/TASK-NNN, and build each touched component's test image in
|
|
31
|
+
the cluster's shared buildkit and push it to the registry, named and versioned by
|
|
32
|
+
convention. I never open a pull request — an orchestrating actor runs that image and does.
|
|
33
|
+
completion: whether I accepted, pushed, and published test images (with the branch and image refs), or refused, and why.
|
|
34
|
+
door_schema: test-task-cmd
|
|
35
|
+
completion_schema: [test-task-result, test-task-refused-result]
|
|
36
|
+
engine: claude-code
|
|
37
|
+
|
|
38
|
+
queries:
|
|
39
|
+
- id: propose-acceptance
|
|
40
|
+
means: >-
|
|
41
|
+
the door for "what will you assert about TASK-NNN once it is built?", asked BEFORE anything
|
|
42
|
+
is built. Send the task id, title, definition of done, the component names the task
|
|
43
|
+
concerns, and optional context. I clone my capability's testing repository and its
|
|
44
|
+
implementation repository's DEFAULT BRANCH read-only — never impl/TASK-NNN, because
|
|
45
|
+
assertions derived from what was built can only confirm the build — ground myself in the
|
|
46
|
+
capability's own standing context, and propose the acceptance surface. I write nothing, build
|
|
47
|
+
nothing, and publish nothing.
|
|
48
|
+
completion: >-
|
|
49
|
+
the expectations I will assert, each an object with a stable, short `id` unique within the
|
|
50
|
+
proposal, a `statement` of what must hold, a `handle` saying exactly how a black-box test
|
|
51
|
+
reaches it (the component, the endpoint, the event routing key, the environment variable its
|
|
52
|
+
base URL arrives in — with a concrete value proposed wherever a test needs one the task does
|
|
53
|
+
not name, for the implementer to commit to or object to), and optionally its `component`;
|
|
54
|
+
and the open questions the task and the standing context leave undetermined, each a string
|
|
55
|
+
or an `about`/`question` object, never an invented answer. Any open question is meant to
|
|
56
|
+
stop the round and reach a human rather than a session.
|
|
57
|
+
door_schema: propose-acceptance-cmd
|
|
58
|
+
completion_schema: acceptance-proposed-result
|
|
59
|
+
engine: claude-code
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# WHO. The actor this package defines — `papeete-actor-manifest/v0`, owned by `papeete-actor`.
|
|
2
|
+
#
|
|
3
|
+
# THIS FOLDER IS THE DEFINITION, NOT A USE. The four cards here say what a foundry testing actor
|
|
4
|
+
# IS: the data it knows, the messages it exchanges, and the two doors it answers. They name no
|
|
5
|
+
# capability, because which capability an instance serves is not part of what the actor is — that
|
|
6
|
+
# is `actor-agentic-context.yaml`, supplied per use.
|
|
7
|
+
#
|
|
8
|
+
# A use of this actor is a folder carrying that sidecar. Its four cards are rendered from these at
|
|
9
|
+
# `docker build` time (`foundry-testing-actor render-cards`), with `name:` set to the use's own.
|
|
10
|
+
manifest: papeete-actor-manifest/v0
|
|
11
|
+
name: foundry-testing-actor
|
|
12
|
+
description: >-
|
|
13
|
+
Authors black-box tests for one TASK-NNN card of the business capability its sidecar names.
|
|
14
|
+
Before anything is built, proposes what it will assert, read-only, with each expectation's id,
|
|
15
|
+
statement and handle, and names what the task leaves undetermined. Once the increment is built,
|
|
16
|
+
extends that capability's persistent test suite in a private clone of its own testing
|
|
17
|
+
repository, writing only under the tests roots the sidecar declares — then commits, pushes a
|
|
18
|
+
branch, and publishes one runnable test image per component the task touched. Never runs the
|
|
19
|
+
tests, renders a verdict, or opens a pull request: an orchestrating actor does.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""`foundry-testing-actor` — the gate, and the derivation table.
|
|
2
|
+
|
|
3
|
+
Four subcommands. `lint` is what CI runs against a sidecar; `show` prints every rendering
|
|
4
|
+
`config.py` derives from the two fields that are actually written down, so an operator can check
|
|
5
|
+
the test image ref an actor WILL publish, and the image ref it will recompute for the component
|
|
6
|
+
under test, before either exists — the hand-written actor this replaces could only be checked by
|
|
7
|
+
reading a running actor's logs after the fact.
|
|
8
|
+
|
|
9
|
+
`render-cards` and `serve` are what make a use's whole repository one sidecar (ADR-FTA-0001): the
|
|
10
|
+
first writes the actor's four cards beside it from the definition in this wheel, the second boots
|
|
11
|
+
the thing. Both run inside the image this package publishes, which is where a use meets them; both
|
|
12
|
+
work in a plain venv too, which is where this repo's own gates run them.
|
|
13
|
+
|
|
14
|
+
House rule: every published package in this ecosystem ships a CLI named exactly the package.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from . import conformance
|
|
23
|
+
from .config import CapabilityConfig, ConfigError, lint, runner_path, version
|
|
24
|
+
from .instance import render_cards
|
|
25
|
+
from .serve import DEFAULT_PORT, ServeError, serve
|
|
26
|
+
|
|
27
|
+
_REGISTRY_PLACEHOLDER = "<registry>"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _cmd_lint(args: argparse.Namespace) -> int:
|
|
31
|
+
# Two gates, one command. The sidecar says which capability this use serves; the cards say
|
|
32
|
+
# which actor it claims to be. A use can be wrong about either independently, and the second
|
|
33
|
+
# check is the only thing standing between a hand-copied card set and a caller being refused
|
|
34
|
+
# at a door — see `conformance.py`.
|
|
35
|
+
report = lint(Path(args.folder))
|
|
36
|
+
conformance_report = conformance.check(Path(args.folder))
|
|
37
|
+
report.oks.extend(conformance_report.oks)
|
|
38
|
+
report.warns.extend(conformance_report.warns)
|
|
39
|
+
report.errors.extend(conformance_report.errors)
|
|
40
|
+
for warning in report.warns:
|
|
41
|
+
print(f" ! {warning}")
|
|
42
|
+
for error in report.errors:
|
|
43
|
+
print(f" FAIL {error}")
|
|
44
|
+
if not report.ok:
|
|
45
|
+
print(f"✗ {len(report.errors)} error(s)")
|
|
46
|
+
return 1
|
|
47
|
+
for line in report.oks:
|
|
48
|
+
print(f" ok {line}")
|
|
49
|
+
checked_cards = any((Path(args.folder) / name).exists() for name in conformance.CARD_FILES)
|
|
50
|
+
print("✓ sidecar and cards conform" if checked_cards else "✓ sidecar conforms")
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _cmd_show(args: argparse.Namespace) -> int:
|
|
55
|
+
try:
|
|
56
|
+
config = CapabilityConfig.load(Path(args.folder))
|
|
57
|
+
except ConfigError as e:
|
|
58
|
+
print(f" FAIL {e}")
|
|
59
|
+
return 2
|
|
60
|
+
registry = args.registry or _REGISTRY_PLACEHOLDER
|
|
61
|
+
|
|
62
|
+
rows = [
|
|
63
|
+
("capability", config.capability),
|
|
64
|
+
("source_repo", config.source_repo),
|
|
65
|
+
("implementation_repo", config.implementation_repo
|
|
66
|
+
+ ("" if config.declared_implementation_repo else " (derived)")),
|
|
67
|
+
("registry_repo", config.registry_repo),
|
|
68
|
+
("engine", config.engine),
|
|
69
|
+
("actor name / git author", config.git_author_name),
|
|
70
|
+
("git author email", config.git_author_email),
|
|
71
|
+
("clone prefix", config.clone_prefix("TASK-NNN")),
|
|
72
|
+
("code clone prefix", config.code_clone_prefix("TASK-NNN")),
|
|
73
|
+
("registry path", config.capability_path),
|
|
74
|
+
("writes only under", ", ".join(config.writes_only_under)),
|
|
75
|
+
]
|
|
76
|
+
width = max(len(label) for label, _ in rows)
|
|
77
|
+
for label, value in rows:
|
|
78
|
+
print(f" {label:<{width}} {value}")
|
|
79
|
+
|
|
80
|
+
print("\n components")
|
|
81
|
+
for component in config.components:
|
|
82
|
+
print(f" {component.name}")
|
|
83
|
+
print(f" tests {component.tests}")
|
|
84
|
+
print(f" runner "
|
|
85
|
+
f"{component.runner or f'(default) {runner_path()}'}")
|
|
86
|
+
print(f" test image name {config.test_image_name(component.name)}")
|
|
87
|
+
print(f" test image ref "
|
|
88
|
+
f"{config.test_image_ref(registry, component.name, '<version>')}")
|
|
89
|
+
print(f" under test "
|
|
90
|
+
f"{config.image_ref(registry, component.name, '<version>')}")
|
|
91
|
+
|
|
92
|
+
print("\n ground_in")
|
|
93
|
+
for entry in config.ground_in:
|
|
94
|
+
print(f" {entry.name} [{entry.load}] → {entry.into}")
|
|
95
|
+
print(f" answers {entry.answers}")
|
|
96
|
+
print(f" fetch {' '.join(config.expand(entry.fetch))}")
|
|
97
|
+
return 0
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _cmd_render_cards(args: argparse.Namespace) -> int:
|
|
101
|
+
# The step that deletes the hand copy. Run at `docker build` time, right after the sidecar is
|
|
102
|
+
# COPY'd in: from then on the cards in that folder came from a version, not from someone's
|
|
103
|
+
# editor, and the next version's cards arrive with the next `FROM`.
|
|
104
|
+
try:
|
|
105
|
+
config = CapabilityConfig.load(Path(args.folder))
|
|
106
|
+
except ConfigError as e:
|
|
107
|
+
print(f" FAIL {e}")
|
|
108
|
+
return 2
|
|
109
|
+
try:
|
|
110
|
+
written = render_cards(config, Path(args.folder))
|
|
111
|
+
except FileNotFoundError as e:
|
|
112
|
+
print(f" FAIL {e}")
|
|
113
|
+
return 2
|
|
114
|
+
for path in written:
|
|
115
|
+
print(f" ok rendered {path}")
|
|
116
|
+
print(f"✓ {config.actor_name} — four cards from the definition in "
|
|
117
|
+
f"foundry-testing-actor=={version()}")
|
|
118
|
+
return 0
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _cmd_serve(args: argparse.Namespace) -> int:
|
|
122
|
+
# No try/except around the boot itself. A misconfigured actor that starts anyway and refuses
|
|
123
|
+
# every caller at the door is strictly worse than a pod that crash-loops with the reason on
|
|
124
|
+
# stdout, which is what an uncaught ConfigError produces here.
|
|
125
|
+
try:
|
|
126
|
+
serve(Path(args.folder), port=args.port)
|
|
127
|
+
except (ConfigError, ServeError) as e:
|
|
128
|
+
print(f" FAIL {e}", file=sys.stderr)
|
|
129
|
+
return 2
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
134
|
+
parser = argparse.ArgumentParser(
|
|
135
|
+
prog="foundry-testing-actor",
|
|
136
|
+
description="Inspect and validate one capability's agentic-context sidecar.",
|
|
137
|
+
)
|
|
138
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
139
|
+
|
|
140
|
+
lint_parser = sub.add_parser(
|
|
141
|
+
"lint", help="validate a sidecar against foundry-testing-actor/agentic-context/v1")
|
|
142
|
+
lint_parser.add_argument(
|
|
143
|
+
"folder", nargs="?", default=".",
|
|
144
|
+
help="the actor's folder, or the sidecar file itself (default: .)")
|
|
145
|
+
lint_parser.set_defaults(func=_cmd_lint)
|
|
146
|
+
|
|
147
|
+
show_parser = sub.add_parser(
|
|
148
|
+
"show", help="print every identifier derived from the sidecar's capability and repo")
|
|
149
|
+
show_parser.add_argument("folder", nargs="?", default=".")
|
|
150
|
+
show_parser.add_argument(
|
|
151
|
+
"--registry", help=f"render image refs against this registry (default: {_REGISTRY_PLACEHOLDER})")
|
|
152
|
+
show_parser.set_defaults(func=_cmd_show)
|
|
153
|
+
|
|
154
|
+
render_parser = sub.add_parser(
|
|
155
|
+
"render-cards",
|
|
156
|
+
help="write the actor's four cards into a use's folder, from the definition in this wheel")
|
|
157
|
+
render_parser.add_argument(
|
|
158
|
+
"folder", nargs="?", default=".",
|
|
159
|
+
help="the folder holding this use's actor-agentic-context.yaml (default: .)")
|
|
160
|
+
render_parser.set_defaults(func=_cmd_render_cards)
|
|
161
|
+
|
|
162
|
+
serve_parser = sub.add_parser(
|
|
163
|
+
"serve", help="boot this actor and answer its doors (needs the `serve` extra)")
|
|
164
|
+
serve_parser.add_argument(
|
|
165
|
+
"folder", nargs="?", default=".",
|
|
166
|
+
help="the folder holding this use's sidecar and cards (default: .)")
|
|
167
|
+
serve_parser.add_argument(
|
|
168
|
+
"--port", type=int, default=None,
|
|
169
|
+
help=f"override $PORT (default: $PORT, else {DEFAULT_PORT})")
|
|
170
|
+
serve_parser.set_defaults(func=_cmd_serve)
|
|
171
|
+
|
|
172
|
+
return parser
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def main(argv: list[str] | None = None) -> int:
|
|
176
|
+
args = build_parser().parse_args(argv)
|
|
177
|
+
return args.func(args)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
if __name__ == "__main__":
|
|
181
|
+
sys.exit(main())
|