fabro/lib/crates/fabro-cli/tests/it/support/mod.rs
fabro-sh-0530[bot] 59a4afa188
Make run actors and provenance total; remove Principal::Anonymous (#463)
## Summary

Removes `Principal::Anonymous` as an actor variant and makes run
creator/provenance non-optional across the full stack — Rust types,
OpenAPI schema, generated TypeScript client, and frontend components.
Every persisted run now has a mandatory creator; there are no nullable
`created_by`, `provenance`, or `subject` fields anywhere in the system.

As a separate cleanup, all `cargo` invocations in CI and dev tooling
gain `--locked` to prevent unintended dependency resolution drift.

### Plan Summary

- **Commit 1 – Remove `Principal::Anonymous`**:
`RequestAuthContext.principal` becomes `Option<Principal>`; `initial()`
and `rejected()` set `None`; all auth gate helpers match on
`Option<Principal>`; HTTP log computes `principal_kind` as
`principal.as_ref().map_or("none", Principal::kind)`;
`PrincipalAnonymous` removed from the OpenAPI schema and generated
client; frontend `principalDisplay` drops the `"anonymous"` case.

- **Commit 2 – Total provenance**: `RunProvenance.subject`,
`RunSpec.provenance`, `Run.created_by`, and `RunCreatedProps.provenance`
all become non-optional. `Default` derive dropped from `RunProvenance`.
Demo mode gets a synthetic `DEMO_PRINCIPAL` via `AuthMethod::DevToken`.
A new `fabro-types` `test-support` feature exports `test_principal()`
and `test_run_provenance()` used across all affected crates and TS
tests. All `provenance: None` / `created_by: null` literals in tests are
replaced with the test fixture.

- **`--locked` CI hardening**: Every `cargo` invocation in GitHub
Actions workflows and `fabro-dev` tooling commands now passes
`--locked`.

### Key design decisions

- `None` principal (unauthenticated / rejected requests) is now
structurally distinct from any actor variant — no sentinel value that
could accidentally pass actor checks.
- `RunProvenance` no longer implements `Default` because a provenance
with no subject is incoherent; callers must supply a real actor at
construction time.
- The `cancel_run` handler was refactored as part of this change to
correctly handle in-process blocked runs: a new test
(`cancel_durably_blocked_in_process_run_cancels_pending_interview_without_abort_signal`)
covers the cancellation path that should let the workflow unwind rather
than aborting it.
- Test fixtures live under the existing `test-support` feature flag
pattern; no fake-auth helpers were added to the `fixtures` module.


### Fabro Details

<details>
<summary>Ran 9 stages in 129m 57s for $53.54</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 10s | – | 0 |
| preflight_lint | 2m 22s | – | 0 |
| implement | 76m 55s | $47.53 | 0 |
| simplify_opus | 14m 46s | $3.21 | 0 |
| simplify_gpt | 2m 26s | $1.04 | 0 |
| verify | 14m 44s | – | 0 |
| fixup | 15m 43s | $1.76 | 0 |
| **Total** | **129m 57s** | **$53.54** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>

```dot
digraph ImplementPlan {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { model: claude-opus-4-7; }
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=succeeded"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=succeeded"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=succeeded"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_opus -> simplify_gpt -> verify
    verify -> exit  [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: fabro-sh-0530[bot] <281434857+fabro-sh-0530[bot]@users.noreply.github.com>
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <19+brynary@users.noreply.github.com>
Co-authored-by: Release Repro <release-repro@example.com>
2026-05-31 11:18:01 -04:00

121 lines
3.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use fabro_types::test_support;
mod auth_harness;
mod auth_tokens;
use assert_cmd::Command;
pub(crate) use auth_harness::{
RealAuthHarness, TEST_DEV_TOKEN, complete_login_via_browser, expire_saved_access_token,
no_redirect_browser_client, run_detached, saved_auth_entry, seed_dev_token_auth,
};
pub(crate) use auth_tokens::{TEST_SESSION_SECRET, issue_test_github_jwt, issue_test_worker_jwt};
use fabro_store::EventEnvelope;
use fabro_test::{EnvVars, TestContext, preserve_coverage_env};
use fabro_types::{Graph, RunId, RunSpec, WorkflowSettings};
pub(crate) fn run_output_filters(context: &TestContext) -> Vec<(String, String)> {
let mut filters = context.filters();
filters.push((r"\b\d+ms\b".to_string(), "[TIME]".to_string()));
filters.push((
r"(?m)^(Graph: ).+$".to_string(),
"${1}[GRAPH_PATH]".to_string(),
));
filters
}
pub(crate) fn fatal_error_line(stderr: &[u8]) -> String {
let stderr = String::from_utf8_lossy(stderr);
console::strip_ansi_codes(&stderr)
.lines()
.rev()
.find_map(|line| {
line.strip_prefix("error: ")
.or_else(|| line.trim_start().strip_prefix("× "))
.map(ToOwned::to_owned)
})
.expect("stderr should contain a fatal error line")
}
pub(crate) fn unique_run_id() -> String {
RunId::new().to_string()
}
pub(crate) fn run_projection_json(run_id: &str, status: &serde_json::Value) -> serde_json::Value {
let run_id = run_id.parse::<RunId>().expect("test run id should parse");
let spec = RunSpec {
run_id,
settings: WorkflowSettings::default(),
graph: Graph::new("Remote Workflow"),
graph_source: None,
workflow_slug: Some("remote-workflow".to_string()),
automation: None,
source_directory: Some("/srv/repo".to_string()),
labels: std::collections::HashMap::default(),
provenance: test_support::test_run_provenance(),
manifest_blob: None,
definition_blob: None,
git: None,
fork_source_ref: None,
};
serde_json::json!({
"spec": serde_json::to_value(spec).expect("run spec should serialize"),
"start": null,
"status": status,
"status_updated_at": "2026-04-05T12:00:01Z",
"last_event_at": "2026-04-05T12:00:01Z",
"pending_control": null,
"checkpoints": [],
"conclusion": null,
"sandbox": null,
"pull_request": null,
"superseded_by": null,
"pending_interviews": {},
"stages": {}
})
}
pub(crate) fn parse_event_envelopes(response: &serde_json::Value) -> Vec<EventEnvelope> {
response["data"]
.as_array()
.expect("event list response should contain a data array")
.iter()
.cloned()
.map(serde_json::from_value)
.collect::<Result<Vec<_>, _>>()
.expect("wire event envelope list should parse")
}
pub(crate) struct LightweightCli {
home_dir: tempfile::TempDir,
}
impl LightweightCli {
pub(crate) fn new() -> Self {
Self {
home_dir: tempfile::tempdir().expect("temp home dir should exist"),
}
}
pub(crate) fn home(&self) -> &std::path::Path {
self.home_dir.path()
}
#[expect(
clippy::disallowed_methods,
reason = "Lightweight CLI test harness reconstructs a minimal process env for subprocesses."
)]
pub(crate) fn command(&self) -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_fabro"));
cmd.env_clear();
preserve_coverage_env!(cmd);
if let Some(path) = std::env::var_os(EnvVars::PATH) {
cmd.env(EnvVars::PATH, path);
}
cmd.env(EnvVars::HOME, self.home_dir.path());
cmd.env(EnvVars::NO_COLOR, "1");
cmd.env(EnvVars::FABRO_NO_UPGRADE_CHECK, "true")
.env(EnvVars::FABRO_HTTP_PROXY_POLICY, "disabled");
cmd.current_dir(self.home_dir.path());
cmd
}
}