mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-29 01:42:21 +00:00
Merge remote-tracking branch 'origin/feat/settings-environments-crud' into feat/settings-environments-crud
This commit is contained in:
commit
2595d55d8b
112 changed files with 577 additions and 431 deletions
2
.github/workflows/nightly.yml
vendored
2
.github/workflows/nightly.yml
vendored
|
|
@ -67,4 +67,4 @@ jobs:
|
|||
git remote set-url origin \
|
||||
"https://x-access-token:${release_token}@github.com/${GITHUB_REPOSITORY}.git"
|
||||
unset release_token
|
||||
cargo dev release --nightly
|
||||
cargo --locked dev release --nightly
|
||||
|
|
|
|||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
|
|
@ -84,7 +84,7 @@ jobs:
|
|||
- uses: taiki-e/install-action@773334c0e05d7e699e4d78234494308223f3a2cf # nextest
|
||||
|
||||
- name: Refresh embedded SPA
|
||||
run: cargo dev spa refresh
|
||||
run: cargo --locked dev spa refresh
|
||||
|
||||
- name: Test (x86_64-musl)
|
||||
# nextest still shells through cargo test for this target, so
|
||||
|
|
@ -93,22 +93,22 @@ jobs:
|
|||
env:
|
||||
CC_x86_64_unknown_linux_musl: musl-gcc
|
||||
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER: musl-gcc
|
||||
run: cargo nextest run --workspace --target ${{ matrix.target }} --release --status-level slow --profile ci
|
||||
run: cargo nextest run --locked --workspace --target ${{ matrix.target }} --release --status-level slow --profile ci
|
||||
|
||||
- name: Test
|
||||
# aarch64-musl test runs have not been validated on the compile
|
||||
# runner yet; shipping binary is exercised via Docker smoke tests.
|
||||
# Re-enable after verifying the workspace passes on this target.
|
||||
if: matrix.target != 'aarch64-unknown-linux-musl' && matrix.target != 'x86_64-unknown-linux-musl'
|
||||
run: cargo nextest run --workspace --target ${{ matrix.target }} --release --status-level slow --profile ci
|
||||
run: cargo nextest run --locked --workspace --target ${{ matrix.target }} --release --status-level slow --profile ci
|
||||
|
||||
- name: Build (musl via cargo-zigbuild)
|
||||
if: matrix.musl
|
||||
run: cargo zigbuild --target ${{ matrix.target }} --release -p fabro-cli
|
||||
run: cargo zigbuild --locked --target ${{ matrix.target }} --release -p fabro-cli
|
||||
|
||||
- name: Build
|
||||
if: ${{ !matrix.musl }}
|
||||
run: cargo build --target ${{ matrix.target }} --release -p fabro-cli
|
||||
run: cargo build --locked --target ${{ matrix.target }} --release -p fabro-cli
|
||||
|
||||
- name: Package
|
||||
run: |
|
||||
|
|
|
|||
8
.github/workflows/rust.yml
vendored
8
.github/workflows/rust.yml
vendored
|
|
@ -76,7 +76,7 @@ jobs:
|
|||
run: |
|
||||
! 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
|
||||
- run: cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings
|
||||
- run: cargo +nightly-2026-04-14 clippy --locked --workspace --all-targets -- -D warnings
|
||||
|
||||
generated-docs:
|
||||
name: Generated Docs
|
||||
|
|
@ -91,7 +91,7 @@ jobs:
|
|||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- run: cargo dev docs check
|
||||
- run: cargo --locked dev docs check
|
||||
|
||||
test:
|
||||
name: Test (Linux)
|
||||
|
|
@ -107,7 +107,7 @@ jobs:
|
|||
with:
|
||||
cache-on-failure: true
|
||||
- uses: taiki-e/install-action@773334c0e05d7e699e4d78234494308223f3a2cf # nextest
|
||||
- run: cargo nextest run --workspace --status-level slow --profile ci
|
||||
- run: cargo nextest run --locked --workspace --status-level slow --profile ci
|
||||
|
||||
test-macos:
|
||||
name: Test (macOS)
|
||||
|
|
@ -124,4 +124,4 @@ jobs:
|
|||
with:
|
||||
cache-on-failure: true
|
||||
- uses: taiki-e/install-action@773334c0e05d7e699e4d78234494308223f3a2cf # nextest
|
||||
- run: cargo nextest run --workspace --status-level slow --profile ci
|
||||
- run: cargo nextest run --locked --workspace --status-level slow --profile ci
|
||||
|
|
|
|||
2
.github/workflows/typescript.yml
vendored
2
.github/workflows/typescript.yml
vendored
|
|
@ -73,5 +73,5 @@ jobs:
|
|||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
- run: bun install --frozen-lockfile
|
||||
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
- run: cargo dev build -- -p fabro-cli --release
|
||||
- run: cargo --locked dev build -- --locked -p fabro-cli --release
|
||||
- run: wc -c < target/release/fabro
|
||||
|
|
|
|||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -2609,6 +2609,7 @@ dependencies = [
|
|||
"clap",
|
||||
"dirs",
|
||||
"fabro-model",
|
||||
"fabro-types",
|
||||
"fabro-util",
|
||||
"hex",
|
||||
"serde",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
RunSummaryPanelView,
|
||||
type RunSummaryPanelViewProps,
|
||||
} from "./run-summary-panel";
|
||||
import { TEST_PRINCIPAL } from "../lib/test-fixtures";
|
||||
|
||||
function instanceText(instance: TestRenderer.ReactTestInstance): string {
|
||||
const parts: string[] = [];
|
||||
|
|
@ -53,7 +54,7 @@ function cellAfterLabel(
|
|||
function makeRun(overrides: Record<string, any> = {}) {
|
||||
return {
|
||||
id: "run_1",
|
||||
created_by: null,
|
||||
created_by: TEST_PRINCIPAL,
|
||||
diff: null,
|
||||
billing: null,
|
||||
...overrides,
|
||||
|
|
@ -71,9 +72,9 @@ describe("RunSummaryPanelView", () => {
|
|||
}
|
||||
});
|
||||
|
||||
test("shows unavailable copy for missing run fields after load", () => {
|
||||
test("shows creator and unavailable copy for optional missing run fields after load", () => {
|
||||
const tree = render({ run: makeRun() });
|
||||
expect(instanceText(cellAfterLabel(tree, "Created by"))).toBe(EMPTY_VALUE);
|
||||
expect(instanceText(cellAfterLabel(tree, "Created by"))).toBe("Ttest");
|
||||
expect(instanceText(cellAfterLabel(tree, "Changes"))).toBe(EMPTY_VALUE);
|
||||
expect(instanceText(cellAfterLabel(tree, "Cost"))).toBe(EMPTY_VALUE);
|
||||
});
|
||||
|
|
@ -226,7 +227,7 @@ describe("RunSummaryPanelView", () => {
|
|||
kind: "user",
|
||||
identity: { issuer: "github", subject: "1" },
|
||||
login: "brynary",
|
||||
auth_method: "oauth",
|
||||
auth_method: "github",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
|
@ -240,7 +241,7 @@ describe("RunSummaryPanelView", () => {
|
|||
kind: "user",
|
||||
identity: { issuer: "github", subject: "1" },
|
||||
login: "brynary",
|
||||
auth_method: "oauth",
|
||||
auth_method: "github",
|
||||
avatar_url: "https://example.com/brynary.png",
|
||||
},
|
||||
}),
|
||||
|
|
@ -252,7 +253,7 @@ describe("RunSummaryPanelView", () => {
|
|||
});
|
||||
|
||||
test("renders non-user actor with kind label", () => {
|
||||
for (const kind of ["agent", "system", "slack", "webhook", "worker", "anonymous"]) {
|
||||
for (const kind of ["agent", "system", "slack", "webhook", "worker"]) {
|
||||
const tree = render({ run: makeRun({ created_by: { kind } as any }) });
|
||||
expect(instanceText(cellAfterLabel(tree, "Created by"))).toContain(kind);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { ReactNode } from "react";
|
||||
import type {
|
||||
Principal,
|
||||
Run,
|
||||
SandboxResources,
|
||||
SandboxState,
|
||||
|
|
@ -50,6 +51,16 @@ function Cell({ label, children }: { label: string; children: ReactNode }) {
|
|||
);
|
||||
}
|
||||
|
||||
function CreatedByValue({ actor }: { actor: Principal }) {
|
||||
const created = principalDisplay(actor);
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{created.glyph}
|
||||
<span className={VALUE_CLASS}>{created.label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export interface RunSummaryPanelViewProps {
|
||||
run: Run | null;
|
||||
runLoading: boolean;
|
||||
|
|
@ -116,7 +127,6 @@ export function RunSummaryPanelView({
|
|||
artifactsCount,
|
||||
artifactsLoading,
|
||||
}: RunSummaryPanelViewProps) {
|
||||
const created = run?.created_by ? principalDisplay(run.created_by) : null;
|
||||
const diff = run?.diff ?? null;
|
||||
const cost = formatUsdMicros(run?.billing?.total_usd_micros);
|
||||
const sandboxKind = sandboxLifecycleKind(run?.sandbox);
|
||||
|
|
@ -127,11 +137,8 @@ export function RunSummaryPanelView({
|
|||
<Cell label="Created by">
|
||||
{runLoading ? (
|
||||
<Skeleton widthClass="w-20" />
|
||||
) : created ? (
|
||||
<div className="flex items-center gap-2">
|
||||
{created.glyph}
|
||||
<span className={VALUE_CLASS}>{created.label}</span>
|
||||
</div>
|
||||
) : run ? (
|
||||
<CreatedByValue actor={run.created_by} />
|
||||
) : (
|
||||
<EmptyValue />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export function RunTableRow({
|
|||
}) {
|
||||
const lifecycleLabel = listLifecycleStatusLabel(run);
|
||||
const statusDisplay = columnStatusDisplay[run.status];
|
||||
const creator = principalDisplay(run.createdBy);
|
||||
const show = (col: ToggleableColumn) => !hiddenColumns.has(col);
|
||||
|
||||
return (
|
||||
|
|
@ -54,14 +55,9 @@ export function RunTableRow({
|
|||
</td>
|
||||
{show("created_by") && (
|
||||
<td className="relative z-10 w-8 whitespace-nowrap px-3 py-2.5">
|
||||
{run.createdBy && (() => {
|
||||
const display = principalDisplay(run.createdBy);
|
||||
return (
|
||||
<Tooltip label={display.label}>
|
||||
<span aria-label={`Created by ${display.label}`}>{display.glyph}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
})()}
|
||||
<Tooltip label={creator.label}>
|
||||
<span aria-label={`Created by ${creator.label}`}>{creator.glyph}</span>
|
||||
</Tooltip>
|
||||
</td>
|
||||
)}
|
||||
{show("repo") && (
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
mapRunToRunItem,
|
||||
runStatusDisplay,
|
||||
} from "./runs";
|
||||
import { TEST_PRINCIPAL } from "../lib/test-fixtures";
|
||||
|
||||
function makeRun(overrides: Partial<Run> = {}): Run {
|
||||
return {
|
||||
|
|
@ -17,7 +18,7 @@ function makeRun(overrides: Partial<Run> = {}): Run {
|
|||
workflow: { slug: "fix_build", name: "Fix Build", graph_name: "FixBuild", node_count: 0, edge_count: 0 },
|
||||
automation: null,
|
||||
repository: { name: "myrepo", origin_url: null, provider: "unknown" },
|
||||
created_by: null,
|
||||
created_by: TEST_PRINCIPAL,
|
||||
origin: { kind: "api" },
|
||||
labels: {},
|
||||
lifecycle: {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export interface RunItem {
|
|||
sandboxWorkingDirectory?: string;
|
||||
sourceDirectory?: string;
|
||||
createdAt?: string;
|
||||
createdBy?: Principal | null;
|
||||
createdBy: Principal;
|
||||
lastEventAt?: string;
|
||||
size?: RunSize;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import {
|
|||
ChatBubbleLeftEllipsisIcon,
|
||||
Cog6ToothIcon,
|
||||
CpuChipIcon,
|
||||
QuestionMarkCircleIcon,
|
||||
ServerIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import type { Principal } from "@qltysh/fabro-api-client";
|
||||
|
|
@ -57,10 +56,5 @@ export function principalDisplay(actor: Principal): PrincipalDisplay {
|
|||
return { glyph: principalIconGlyph(<BoltIcon className="size-3" />), label: "webhook" };
|
||||
case "worker":
|
||||
return { glyph: principalIconGlyph(<ServerIcon className="size-3" />), label: "worker" };
|
||||
case "anonymous":
|
||||
return {
|
||||
glyph: principalIconGlyph(<QuestionMarkCircleIcon className="size-3" />),
|
||||
label: "anonymous",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
unarchiveRuns,
|
||||
} from "./run-actions";
|
||||
import { generatedAxios } from "./api-client";
|
||||
import { TEST_PRINCIPAL } from "./test-fixtures";
|
||||
|
||||
type StubResponseInit = {
|
||||
status: number;
|
||||
|
|
@ -47,7 +48,7 @@ function makeRun(status: RunStatus, archived = false): Run {
|
|||
workflow: { slug: "fix_build", name: "Fix Build", graph_name: null, node_count: 0, edge_count: 0 },
|
||||
automation: null,
|
||||
repository: null,
|
||||
created_by: null,
|
||||
created_by: TEST_PRINCIPAL,
|
||||
origin: { kind: "api" },
|
||||
labels: {},
|
||||
lifecycle: {
|
||||
|
|
|
|||
8
apps/fabro-web/app/lib/test-fixtures.ts
Normal file
8
apps/fabro-web/app/lib/test-fixtures.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import type { Principal } from "@qltysh/fabro-api-client";
|
||||
|
||||
export const TEST_PRINCIPAL: Principal = {
|
||||
kind: "user",
|
||||
identity: { issuer: "fabro:test", subject: "test-user" },
|
||||
login: "test",
|
||||
auth_method: "dev_token",
|
||||
};
|
||||
|
|
@ -4,6 +4,7 @@ import TestRenderer, { act } from "react-test-renderer";
|
|||
import { createMemoryRouter, RouterProvider } from "react-router";
|
||||
|
||||
import { ToastProvider } from "../components/toast";
|
||||
import { TEST_PRINCIPAL } from "../lib/test-fixtures";
|
||||
import { setupReactTestEnv } from "../lib/test-utils";
|
||||
|
||||
let currentRun: any = null;
|
||||
|
|
@ -120,7 +121,7 @@ function makeRun(overrides: Record<string, unknown> = {}) {
|
|||
origin_url: "https://github.com/fallback/repo.git",
|
||||
provider: "github",
|
||||
},
|
||||
created_by: null,
|
||||
created_by: TEST_PRINCIPAL,
|
||||
origin: { kind: "api" },
|
||||
labels: {},
|
||||
lifecycle: {
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ function AutomationCard({
|
|||
onDelete: () => void;
|
||||
}) {
|
||||
const Icon = automation.icon;
|
||||
const runDisabled = busy || !automation.apiEnabled;
|
||||
const runDisabled = busy || running || !automation.apiEnabled;
|
||||
return (
|
||||
<div className="group flex items-center gap-4 rounded-md border border-line bg-panel/80 p-4 transition-all duration-200 hover:border-line-strong hover:bg-panel hover:shadow-lg hover:shadow-black/20">
|
||||
<Link to={`/automations/${automation.id}`} className="flex min-w-0 flex-1 items-center gap-4">
|
||||
|
|
@ -153,7 +153,7 @@ function AutomationCard({
|
|||
<button
|
||||
type="button"
|
||||
onClick={onRun}
|
||||
disabled={running || runDisabled}
|
||||
disabled={runDisabled}
|
||||
aria-label={running ? "Starting run…" : "Run automation"}
|
||||
title={
|
||||
running
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
|
||||
import { ToastProvider } from "../components/toast";
|
||||
import { DemoModeProvider } from "../lib/demo-mode";
|
||||
import { TEST_PRINCIPAL } from "../lib/test-fixtures";
|
||||
|
||||
let currentRunSummary: any = null;
|
||||
let currentRunState: any = null;
|
||||
|
|
@ -221,7 +222,7 @@ function makeRunSummary({
|
|||
workflow: { slug: "default", name: "Default", graph_name: null, node_count: 0, edge_count: 0 },
|
||||
automation,
|
||||
repository: { name: "fabro", origin_url: null, provider: "unknown" },
|
||||
created_by: null,
|
||||
created_by: TEST_PRINCIPAL,
|
||||
origin: { kind: "api" },
|
||||
labels: {},
|
||||
lifecycle: {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { MemoryRouter, Route, Routes } from "react-router";
|
|||
import { toast as sonnerToast } from "sonner";
|
||||
|
||||
import { ToastProvider } from "../components/toast";
|
||||
import { TEST_PRINCIPAL } from "../lib/test-fixtures";
|
||||
|
||||
let currentFilesPayload: any = null;
|
||||
let currentCommitsPayload: any = null;
|
||||
|
|
@ -51,7 +52,7 @@ mock.module("../lib/queries", () => ({
|
|||
workflow: { slug: "default", name: "Default", graph_name: null, node_count: 0, edge_count: 0 },
|
||||
automation: null,
|
||||
repository: { name: "fabro", origin_url: null, provider: "unknown" },
|
||||
created_by: null,
|
||||
created_by: TEST_PRINCIPAL,
|
||||
origin: { kind: "api" },
|
||||
labels: {},
|
||||
lifecycle: {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type { PaginatedRunList, Run } from "@qltysh/fabro-api-client";
|
|||
|
||||
import { ToastProvider } from "../components/toast";
|
||||
import { CHILD_RUNS_LIST_PREFERENCES_STORAGE_KEY } from "../components/runs-list/preferences";
|
||||
import { TEST_PRINCIPAL } from "../lib/test-fixtures";
|
||||
import { setupReactTestEnv } from "../lib/test-utils";
|
||||
|
||||
class MemoryStorage {
|
||||
|
|
@ -35,7 +36,7 @@ function run(id: string, repo = "qlty/fabro", workflow = "release"): Run {
|
|||
workflow: { slug: workflow, name: workflow, graph_name: null, node_count: 0, edge_count: 0 },
|
||||
automation: null,
|
||||
repository: { name: repo, origin_url: null, provider: "github" },
|
||||
created_by: null,
|
||||
created_by: TEST_PRINCIPAL,
|
||||
origin: { kind: "api" },
|
||||
labels: {},
|
||||
lifecycle: {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
shouldRefreshBoardForEvent,
|
||||
} from "./runs";
|
||||
import { summarizeBatchLifecycleAction } from "../components/runs-list/batch-lifecycle";
|
||||
import { TEST_PRINCIPAL } from "../lib/test-fixtures";
|
||||
|
||||
function boardRun(id: string, column: BoardColumn, questionText?: string): Run {
|
||||
const status =
|
||||
|
|
@ -34,7 +35,7 @@ function boardRun(id: string, column: BoardColumn, questionText?: string): Run {
|
|||
workflow: { slug: "test", name: "Test", graph_name: null, node_count: 0, edge_count: 0 },
|
||||
automation: null,
|
||||
repository: { name: "repo", origin_url: null, provider: "unknown" },
|
||||
created_by: null,
|
||||
created_by: TEST_PRINCIPAL,
|
||||
origin: { kind: "api" },
|
||||
labels: {},
|
||||
lifecycle: {
|
||||
|
|
|
|||
|
|
@ -80,7 +80,10 @@ Important rules:
|
|||
|
||||
- Optional envelope fields are omitted, not serialized as `null`.
|
||||
- Event-specific fields do not get flattened into the top level.
|
||||
- Actor identity lives only in top-level `actor: Principal`; never duplicate it in event-specific properties.
|
||||
- Actor identity normally lives only in top-level `actor: Principal`; do not duplicate it in
|
||||
event-specific properties. The exception is `run.created`, whose
|
||||
`properties.provenance.subject` is the durable run creator stored in `RunSpec`; its envelope
|
||||
`actor` is derived from the same principal.
|
||||
- User actors must carry canonical IdP identity through `Principal::User { identity, login, auth_method }`, not a login-only string.
|
||||
- `EventPayload` validation requires `id`, `ts`, `run_id`, and `event`.
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,18 @@ Emitted when the run record is created.
|
|||
}
|
||||
},
|
||||
"fork_source_ref": null,
|
||||
"in_place": false
|
||||
"in_place": false,
|
||||
"provenance": {
|
||||
"subject": {
|
||||
"kind": "user",
|
||||
"identity": {
|
||||
"issuer": "https://github.com",
|
||||
"subject": "12345"
|
||||
},
|
||||
"login": "octocat",
|
||||
"auth_method": "github"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -76,7 +87,7 @@ Emitted when the run record is created.
|
|||
| `base_branch` | string? | Submitter-side base branch |
|
||||
| `workflow_slug` | string? | Workflow slug |
|
||||
| `db_prefix` | string? | Store prefix used for the run |
|
||||
| `provenance` | object? | Actor and request provenance |
|
||||
| `provenance` | object | Actor and request provenance |
|
||||
| `manifest_blob` | string? | Blob id for the submitted manifest |
|
||||
| `pre_run_git` | object? | Submitter-side pre-run git context and push outcome |
|
||||
| `fork_source_ref` | object? | Source run/checkpoint reference when this run was forked |
|
||||
|
|
|
|||
|
|
@ -118,11 +118,11 @@ Fields are key-value pairs that make events queryable. Include enough context th
|
|||
| `error` | Error value on failure |
|
||||
| `path` | File system path |
|
||||
| `duration_ms` | Elapsed time in milliseconds |
|
||||
| `principal_kind` | HTTP caller category (`user`, `worker`, `webhook`, `anonymous`, etc.) |
|
||||
| `principal_kind` | HTTP caller category (`user`, `worker`, `webhook`, `none`, etc.) |
|
||||
| `auth_status` | HTTP authentication result (`missing`, `invalid`, `expired`, `authenticated`) |
|
||||
| `idp_issuer`, `idp_subject` | Canonical user identity for authenticated user requests |
|
||||
|
||||
For HTTP request logs, use the request `Principal` projection rather than hand-assembled auth strings. User identity fields are present only for `Principal::User`; worker and webhook requests use their variant-specific fields (`run_id`, `delivery_id`).
|
||||
For HTTP request logs, use the request `Principal` projection rather than hand-assembled auth strings. User identity fields are present only for `Principal::User`; worker and webhook requests use their variant-specific fields (`run_id`, `delivery_id`). Requests without a principal use `principal_kind="none"`; `auth_status` distinguishes missing, invalid, expired, and authenticated auth state.
|
||||
|
||||
Server auth intentionally exposes a mutable `RequestAuth` context slot for public auth routes and guard extractors such as `RequiredUser` / `RequireRunScoped` for protected routes. There is no loose `RequestPrincipal` extractor; route-facing extractors should enforce the route's auth contract while the slot supplies the final HTTP log fields.
|
||||
| `input_tokens` | Token count for LLM input |
|
||||
|
|
|
|||
|
|
@ -8973,6 +8973,8 @@ components:
|
|||
|
||||
RunProvenance:
|
||||
type: object
|
||||
required:
|
||||
- subject
|
||||
properties:
|
||||
server:
|
||||
oneOf:
|
||||
|
|
@ -8983,9 +8985,7 @@ components:
|
|||
- $ref: "#/components/schemas/RunClientProvenance"
|
||||
- type: "null"
|
||||
subject:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/Principal"
|
||||
- type: "null"
|
||||
$ref: "#/components/schemas/Principal"
|
||||
|
||||
Principal:
|
||||
oneOf:
|
||||
|
|
@ -8995,7 +8995,6 @@ components:
|
|||
- $ref: "#/components/schemas/PrincipalSlack"
|
||||
- $ref: "#/components/schemas/PrincipalAgent"
|
||||
- $ref: "#/components/schemas/PrincipalSystem"
|
||||
- $ref: "#/components/schemas/PrincipalAnonymous"
|
||||
discriminator:
|
||||
propertyName: kind
|
||||
mapping:
|
||||
|
|
@ -9005,7 +9004,6 @@ components:
|
|||
slack: "#/components/schemas/PrincipalSlack"
|
||||
agent: "#/components/schemas/PrincipalAgent"
|
||||
system: "#/components/schemas/PrincipalSystem"
|
||||
anonymous: "#/components/schemas/PrincipalAnonymous"
|
||||
|
||||
PrincipalUser:
|
||||
type: object
|
||||
|
|
@ -9095,15 +9093,6 @@ components:
|
|||
system_kind:
|
||||
$ref: "#/components/schemas/SystemActorKind"
|
||||
|
||||
PrincipalAnonymous:
|
||||
type: object
|
||||
required:
|
||||
- kind
|
||||
properties:
|
||||
kind:
|
||||
type: string
|
||||
enum: [anonymous]
|
||||
|
||||
RunEvent:
|
||||
description: >
|
||||
Internal RunEvent-compatible JSON payload. The server validates this
|
||||
|
|
@ -10231,6 +10220,7 @@ components:
|
|||
- run_id
|
||||
- settings
|
||||
- graph
|
||||
- provenance
|
||||
properties:
|
||||
run_id:
|
||||
type: string
|
||||
|
|
@ -10254,9 +10244,7 @@ components:
|
|||
additionalProperties:
|
||||
type: string
|
||||
provenance:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/RunProvenance"
|
||||
- type: "null"
|
||||
$ref: "#/components/schemas/RunProvenance"
|
||||
manifest_blob:
|
||||
type: ["string", "null"]
|
||||
definition_blob:
|
||||
|
|
@ -10568,9 +10556,7 @@ components:
|
|||
- $ref: "#/components/schemas/RepositoryRef"
|
||||
- type: "null"
|
||||
created_by:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/Principal"
|
||||
- type: "null"
|
||||
$ref: "#/components/schemas/Principal"
|
||||
origin:
|
||||
$ref: "#/components/schemas/RunOrigin"
|
||||
labels:
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ The dock listens to interview events and refreshes as questions arrive, so a par
|
|||
|
||||
## Principal attribution and auth routing
|
||||
|
||||
Run events and run creation now carry clearer principal information for users, workers, systems, Slack interactions, webhooks, agents, and anonymous actors. API clients get explicit provenance objects instead of older actor-shaped fields that could lose where a run came from.
|
||||
Run events and run creation now carry clearer principal information for users, workers, systems, Slack interactions, webhooks, and agents. API clients get explicit provenance objects instead of older actor-shaped fields that could lose where a run came from.
|
||||
|
||||
This also closes attribution gaps across web, CLI, worker-token, Slack, and human-interview paths. Runs created or advanced through different surfaces now preserve who or what took the action more consistently.
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ This also closes attribution gaps across web, CLI, worker-token, Slack, and huma
|
|||
|
||||
<Accordion title="API">
|
||||
- Run specs now include client and server provenance shapes
|
||||
- Run events use unified principal shapes for user, worker, system, Slack, webhook, agent, and anonymous subjects
|
||||
- Run events use unified principal shapes for user, worker, system, Slack, webhook, and agent subjects
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="CLI">
|
||||
|
|
|
|||
|
|
@ -34,3 +34,6 @@ serde_json = "1"
|
|||
serde_yaml = "0.9"
|
||||
prettyplease = "0.2"
|
||||
syn = "2"
|
||||
|
||||
[dev-dependencies]
|
||||
fabro-types = { path = "../fabro-types", features = ["test-support"] }
|
||||
|
|
|
|||
|
|
@ -118,7 +118,6 @@ fn principal_round_trips_every_variant_through_api_type() {
|
|||
Principal::System {
|
||||
system_kind: SystemActorKind::Watchdog,
|
||||
},
|
||||
Principal::Anonymous,
|
||||
];
|
||||
|
||||
for principal in variants {
|
||||
|
|
@ -140,9 +139,9 @@ fn run_provenance_subject_round_trips_as_principal() {
|
|||
name: Some("fabro-cli".to_string()),
|
||||
version: Some("0.1.0".to_string()),
|
||||
}),
|
||||
subject: Some(Principal::Worker {
|
||||
subject: Principal::Worker {
|
||||
run_id: fixtures::RUN_1,
|
||||
}),
|
||||
},
|
||||
};
|
||||
let json = serde_json::to_value(&provenance).unwrap();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::any::{TypeId, type_name};
|
||||
|
||||
use fabro_api::types::RunEvent as ApiRunEvent;
|
||||
use fabro_types::{Graph, RunEvent, WorkflowSettings, fixtures};
|
||||
use fabro_types::{Graph, RunEvent, WorkflowSettings, fixtures, test_support};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
#[test]
|
||||
|
|
@ -20,7 +20,8 @@ fn run_event_round_trips_run_created() {
|
|||
"settings": WorkflowSettings::default(),
|
||||
"graph": Graph::new("test"),
|
||||
"run_dir": "/tmp/fabro/run-1",
|
||||
"source_directory": "/tmp/fabro/run-1"
|
||||
"source_directory": "/tmp/fabro/run-1",
|
||||
"provenance": test_support::test_run_provenance()
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -39,7 +40,8 @@ fn run_event_round_trips_run_created_with_web_url() {
|
|||
"graph": Graph::new("test"),
|
||||
"run_dir": "/tmp/fabro/run-1",
|
||||
"source_directory": "/tmp/fabro/run-1",
|
||||
"web_url": format!("http://localhost:3000/runs/{}", fixtures::RUN_1)
|
||||
"web_url": format!("http://localhost:3000/runs/{}", fixtures::RUN_1),
|
||||
"provenance": test_support::test_run_provenance()
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
use std::any::{TypeId, type_name};
|
||||
|
||||
use fabro_api::types::RunProjection as ApiRunProjection;
|
||||
use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings};
|
||||
use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings, test_support};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn run_projection_reuses_canonical_type() {
|
||||
assert_same_type::<ApiRunProjection, RunProjection>();
|
||||
|
|
@ -137,7 +136,7 @@ fn run_spec_json() -> serde_json::Value {
|
|||
automation: None,
|
||||
source_directory: None,
|
||||
labels: std::collections::HashMap::new(),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
git: None,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use fabro_types::{
|
|||
AskFabro, AskFabroUnavailableReason, AutomationRef, DiffSummary, PullRequestLink,
|
||||
RepositoryProvider, RepositoryRef, Run, RunApproval, RunApprovalState, RunBillingSummary,
|
||||
RunId, RunLifecycle, RunLinks, RunOrigin, RunRunnableSource, RunSize, RunTimestamps, RunTiming,
|
||||
WorkflowRef, fixtures,
|
||||
WorkflowRef, fixtures, test_support,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
|
|
@ -88,7 +88,7 @@ fn run_summary_json_matches_openapi_shape() {
|
|||
origin_url: None,
|
||||
provider: RepositoryProvider::Unknown,
|
||||
}),
|
||||
created_by: None,
|
||||
created_by: test_support::test_principal(),
|
||||
origin: RunOrigin::default(),
|
||||
labels: HashMap::from([("team".to_string(), "core".to_string())]),
|
||||
lifecycle: RunLifecycle {
|
||||
|
|
@ -161,7 +161,15 @@ fn run_summary_json_matches_openapi_shape() {
|
|||
"origin_url": null,
|
||||
"provider": "unknown"
|
||||
},
|
||||
"created_by": null,
|
||||
"created_by": {
|
||||
"kind": "user",
|
||||
"identity": {
|
||||
"issuer": "fabro:test",
|
||||
"subject": "test-user"
|
||||
},
|
||||
"login": "test",
|
||||
"auth_method": "dev_token"
|
||||
},
|
||||
"origin": {
|
||||
"kind": "api"
|
||||
},
|
||||
|
|
@ -253,6 +261,7 @@ fn run_summary_deserializes_when_optional_fields_are_absent() {
|
|||
"origin_url": null,
|
||||
"provider": "unknown"
|
||||
},
|
||||
"created_by": test_support::test_principal(),
|
||||
"models": [],
|
||||
"timestamps": {
|
||||
"created_at": "2026-04-20T12:00:00Z",
|
||||
|
|
@ -275,6 +284,7 @@ fn run_summary_deserializes_when_optional_fields_are_absent() {
|
|||
assert_eq!(summary.workflow.edge_count, 0);
|
||||
assert_eq!(summary.goal, "ship it");
|
||||
assert_eq!(summary.title, "ship it");
|
||||
assert_eq!(summary.created_by, test_support::test_principal());
|
||||
assert_eq!(summary.labels, HashMap::new());
|
||||
assert_eq!(summary.source_directory, None);
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ assert_cmd = "2"
|
|||
fabro-acp = { path = "../fabro-acp", features = ["test-support"] }
|
||||
fabro-build-support = { path = "../build-support" }
|
||||
fabro-server = { path = "../fabro-server", features = ["test-support"] }
|
||||
fabro-types = { path = "../fabro-types", features = ["clap", "test-support"] }
|
||||
insta = { workspace = true, features = ["filters"] }
|
||||
paste = "1"
|
||||
predicates = "3"
|
||||
|
|
|
|||
|
|
@ -822,6 +822,7 @@ mod tests {
|
|||
)]
|
||||
|
||||
use fabro_interview::{Answer, AnswerValue};
|
||||
use fabro_types::test_support;
|
||||
use fabro_util::terminal::Styles;
|
||||
use httpmock::MockServer;
|
||||
|
||||
|
|
@ -841,7 +842,7 @@ mod tests {
|
|||
automation: None,
|
||||
source_directory: None,
|
||||
labels: std::collections::HashMap::default(),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
git: None,
|
||||
|
|
|
|||
|
|
@ -220,7 +220,18 @@ fn inspect_resolves_selector_via_server_endpoint() {
|
|||
"attrs": {}
|
||||
},
|
||||
"workflow_slug": "remote-workflow",
|
||||
"source_directory": "/srv/repo"
|
||||
"source_directory": "/srv/repo",
|
||||
"provenance": {
|
||||
"subject": {
|
||||
"kind": "user",
|
||||
"identity": {
|
||||
"issuer": "fabro:test",
|
||||
"subject": "test-user"
|
||||
},
|
||||
"login": "test",
|
||||
"auth_method": "dev_token"
|
||||
}
|
||||
}
|
||||
},
|
||||
"start_record": null,
|
||||
"conclusion": null,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ use fabro_config::daemon::ServerDaemon;
|
|||
use fabro_config::{Storage, envfile};
|
||||
use fabro_store::EventEnvelope;
|
||||
use fabro_test::{TestContext, expect_reqwest_status};
|
||||
use fabro_types::test_support::test_principal;
|
||||
use fabro_types::{RunId, StageId};
|
||||
use httpmock::{Mock, MockServer};
|
||||
use serde_json::Value;
|
||||
|
|
@ -177,6 +178,8 @@ pub(crate) fn remote_run_summary_json(
|
|||
"origin_url": null,
|
||||
"provider": "unknown"
|
||||
},
|
||||
"created_by": serde_json::to_value(test_principal())
|
||||
.expect("test principal should serialize"),
|
||||
"origin": {
|
||||
"kind": "api"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use fabro_types::test_support;
|
||||
mod auth_harness;
|
||||
mod auth_tokens;
|
||||
|
||||
|
|
@ -49,7 +50,7 @@ pub(crate) fn run_projection_json(run_id: &str, status: &serde_json::Value) -> s
|
|||
automation: None,
|
||||
source_directory: Some("/srv/repo".to_string()),
|
||||
labels: std::collections::HashMap::default(),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
git: None,
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ impl DockerBuildPlan {
|
|||
|
||||
fn spa_refresh_command() -> PlannedCommand {
|
||||
PlannedCommand::new("cargo")
|
||||
.arg("--locked")
|
||||
.arg("dev")
|
||||
.arg("spa")
|
||||
.arg("refresh")
|
||||
|
|
@ -235,6 +236,6 @@ fn build_script(target: &str, zig_arch: &str) -> String {
|
|||
cargo install --locked --root /opt/cargo-tools cargo-zigbuild; \
|
||||
fi; \
|
||||
rustup target add {target}; \
|
||||
cargo zigbuild --release -p fabro-cli --target {target}"
|
||||
cargo zigbuild --locked --release -p fabro-cli --target {target}"
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ pub(crate) fn docs_cli_reference_root(root: &Path, check: bool) -> Result<()> {
|
|||
fn render_cli_reference() -> Result<String> {
|
||||
let command = PlannedCommand::new("cargo")
|
||||
.arg("run")
|
||||
.arg("--locked")
|
||||
.arg("-p")
|
||||
.arg("fabro-cli")
|
||||
.arg("--")
|
||||
|
|
|
|||
|
|
@ -237,6 +237,7 @@ impl ReleasePlan {
|
|||
|
||||
fn spa_refresh_command() -> PlannedCommand {
|
||||
PlannedCommand::new("cargo")
|
||||
.arg("--locked")
|
||||
.arg("dev")
|
||||
.arg("spa")
|
||||
.arg("refresh")
|
||||
|
|
@ -249,6 +250,7 @@ impl ReleasePlan {
|
|||
.env("SEGMENT_WRITE_KEY", RELEASE_TEST_SEGMENT_WRITE_KEY)
|
||||
.arg("nextest")
|
||||
.arg("run")
|
||||
.arg("--locked")
|
||||
.arg("--workspace")
|
||||
.arg("--release")
|
||||
.arg("--profile")
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ fn dry_run_prints_equivalent_build_commands() {
|
|||
let stdout = output_text(&output.stdout);
|
||||
|
||||
assert!(
|
||||
stdout.contains("cargo dev spa refresh"),
|
||||
stdout.contains("cargo --locked dev spa refresh"),
|
||||
"dry-run should print SPA refresh command:\n{stdout}"
|
||||
);
|
||||
assert!(
|
||||
|
|
@ -61,7 +61,9 @@ fn dry_run_prints_equivalent_build_commands() {
|
|||
"dry-run should print builder docker run:\n{stdout}"
|
||||
);
|
||||
assert!(
|
||||
stdout.contains("cargo zigbuild --release -p fabro-cli --target x86_64-unknown-linux-musl"),
|
||||
stdout.contains(
|
||||
"cargo zigbuild --locked --release -p fabro-cli --target x86_64-unknown-linux-musl"
|
||||
),
|
||||
"dry-run should print cargo-zigbuild target:\n{stdout}"
|
||||
);
|
||||
assert!(
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ fn dry_run_computes_stable_version_from_date() {
|
|||
"dry-run should compute base version from date:\n{stdout}"
|
||||
);
|
||||
assert!(
|
||||
stdout.contains("cargo dev spa refresh"),
|
||||
stdout.contains("cargo --locked dev spa refresh"),
|
||||
"dry-run should print one SPA refresh command:\n{stdout}"
|
||||
);
|
||||
assert!(
|
||||
|
|
@ -109,7 +109,7 @@ fn dry_run_computes_stable_version_from_date() {
|
|||
);
|
||||
assert!(
|
||||
stdout.contains(
|
||||
"unset GH_TOKEN GITHUB_TOKEN && SEGMENT_WRITE_KEY=fake-for-local-smoke cargo nextest run"
|
||||
"unset GH_TOKEN GITHUB_TOKEN && SEGMENT_WRITE_KEY=fake-for-local-smoke cargo nextest run --locked"
|
||||
),
|
||||
"dry-run should show release tests without inherited GitHub tokens:\n{stdout}"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -476,6 +476,7 @@ mod tests {
|
|||
Checkpoint, CheckpointRecord, Conclusion, RunDiff, RunSandbox, RunSandboxInstance,
|
||||
RunSandboxPlan, RunStatus, SandboxProviderKind, StageCompletion, StageModelUsage,
|
||||
StageOutcome, StartRecord, SuccessReason, WorkflowSettings, first_event_seq, fixtures,
|
||||
test_support,
|
||||
};
|
||||
use futures::executor;
|
||||
|
||||
|
|
@ -498,7 +499,7 @@ mod tests {
|
|||
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
|
||||
}),
|
||||
labels: HashMap::from([("team".to_string(), "platform".to_string())]),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
|
|
|
|||
|
|
@ -1671,11 +1671,11 @@ client_id = "github-client-id"
|
|||
let [first, second, third] = <[RequestAuthContext; 3]>::try_from(contexts)
|
||||
.expect("expected three captured auth contexts");
|
||||
assert_eq!(first.auth_status, AuthStatus::Authenticated);
|
||||
assert_eq!(first.principal.display(), "octocat");
|
||||
assert_eq!(first.principal.expect("principal").display(), "octocat");
|
||||
assert_eq!(second.auth_status, AuthStatus::Authenticated);
|
||||
assert_eq!(second.principal.display(), "octocat");
|
||||
assert_eq!(second.principal.expect("principal").display(), "octocat");
|
||||
assert_eq!(third.auth_status, AuthStatus::Authenticated);
|
||||
assert_eq!(third.principal.display(), "octocat");
|
||||
assert_eq!(third.principal.expect("principal").display(), "octocat");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -2080,7 +2080,10 @@ client_id = "github-client-id"
|
|||
|
||||
let contexts = captured.lock().expect("captured auth contexts").clone();
|
||||
assert_eq!(contexts[0].auth_status, AuthStatus::Authenticated);
|
||||
assert_eq!(contexts[0].principal.display(), "octocat");
|
||||
assert_eq!(
|
||||
contexts[0].principal.as_ref().expect("principal").display(),
|
||||
"octocat"
|
||||
);
|
||||
assert_eq!(contexts[1].auth_status, AuthStatus::Invalid);
|
||||
assert_eq!(
|
||||
contexts[1].auth_error_code,
|
||||
|
|
@ -2273,8 +2276,11 @@ client_id = "github-client-id"
|
|||
|
||||
let contexts = captured.lock().expect("captured auth contexts").clone();
|
||||
assert_eq!(contexts[0].auth_status, AuthStatus::Authenticated);
|
||||
assert_eq!(contexts[0].principal.display(), "octocat");
|
||||
let Principal::User(user) = &contexts[0].principal else {
|
||||
assert_eq!(
|
||||
contexts[0].principal.as_ref().expect("principal").display(),
|
||||
"octocat"
|
||||
);
|
||||
let Some(Principal::User(user)) = &contexts[0].principal else {
|
||||
panic!("expected user principal");
|
||||
};
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -1081,7 +1081,7 @@ fn ts(s: &str) -> DateTime<Utc> {
|
|||
|
||||
mod runs {
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::{LazyLock, OnceLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use fabro_api::types::*;
|
||||
|
|
@ -1092,13 +1092,22 @@ mod runs {
|
|||
};
|
||||
use fabro_types::settings::{InterpString, ProjectNamespace, WorkflowNamespace};
|
||||
use fabro_types::{
|
||||
PendingReason, RepositoryRef, RunBillingSummary, RunId, RunLifecycle, RunLinks, RunOrigin,
|
||||
RunSize, RunTimestamps, StageId, WorkflowRef, WorkflowSettings,
|
||||
AuthMethod, IdpIdentity, PendingReason, Principal, RepositoryRef, RunBillingSummary, RunId,
|
||||
RunLifecycle, RunLinks, RunOrigin, RunSize, RunTimestamps, StageId, WorkflowRef,
|
||||
WorkflowSettings,
|
||||
};
|
||||
|
||||
use super::ts;
|
||||
use crate::server::run_stage_from_stage_id;
|
||||
|
||||
static DEMO_PRINCIPAL: LazyLock<Principal> = LazyLock::new(|| {
|
||||
Principal::user(
|
||||
IdpIdentity::new("fabro:demo", "demo").expect("demo identity should be valid"),
|
||||
"demo".to_string(),
|
||||
AuthMethod::DevToken,
|
||||
)
|
||||
});
|
||||
|
||||
fn labels(entries: &[(&str, &str)]) -> HashMap<String, String> {
|
||||
entries
|
||||
.iter()
|
||||
|
|
@ -1171,7 +1180,7 @@ mod runs {
|
|||
repo_origin_url,
|
||||
source_directory.as_deref(),
|
||||
)),
|
||||
created_by: None,
|
||||
created_by: DEMO_PRINCIPAL.clone(),
|
||||
origin: RunOrigin::default(),
|
||||
labels: labels(entries),
|
||||
lifecycle: RunLifecycle {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ use crate::worker_token::{self, WORKER_TOKEN_KID, WorkerScopeSet};
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct RequestAuthContext {
|
||||
pub principal: Principal,
|
||||
pub principal: Option<Principal>,
|
||||
pub auth_status: AuthStatus,
|
||||
pub auth_error_code: Option<AuthErrorCode>,
|
||||
pub user_profile: Option<UserProfile>,
|
||||
|
|
@ -76,7 +76,7 @@ impl RequestAuthContext {
|
|||
#[must_use]
|
||||
pub(crate) fn initial() -> Self {
|
||||
Self {
|
||||
principal: Principal::Anonymous,
|
||||
principal: None,
|
||||
auth_status: AuthStatus::Missing,
|
||||
auth_error_code: None,
|
||||
user_profile: None,
|
||||
|
|
@ -87,7 +87,7 @@ impl RequestAuthContext {
|
|||
#[must_use]
|
||||
pub(crate) fn authenticated(principal: Principal, user_profile: Option<UserProfile>) -> Self {
|
||||
Self {
|
||||
principal,
|
||||
principal: Some(principal),
|
||||
auth_status: AuthStatus::Authenticated,
|
||||
auth_error_code: None,
|
||||
user_profile,
|
||||
|
|
@ -98,7 +98,7 @@ impl RequestAuthContext {
|
|||
#[must_use]
|
||||
pub(crate) fn authenticated_worker(run_id: RunId, scopes: WorkerScopeSet) -> Self {
|
||||
Self {
|
||||
principal: Principal::Worker { run_id },
|
||||
principal: Some(Principal::Worker { run_id }),
|
||||
auth_status: AuthStatus::Authenticated,
|
||||
auth_error_code: None,
|
||||
user_profile: None,
|
||||
|
|
@ -125,7 +125,7 @@ impl RequestAuthContext {
|
|||
#[must_use]
|
||||
pub(crate) fn rejected(status: AuthStatus, code: Option<AuthErrorCode>) -> Self {
|
||||
Self {
|
||||
principal: Principal::Anonymous,
|
||||
principal: None,
|
||||
auth_status: status,
|
||||
auth_error_code: code,
|
||||
user_profile: None,
|
||||
|
|
@ -148,7 +148,7 @@ impl AuthStatus {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct RequestAuthLogContext {
|
||||
pub principal: Principal,
|
||||
pub principal: Option<Principal>,
|
||||
pub auth_status: AuthStatus,
|
||||
pub auth_error_code: Option<AuthErrorCode>,
|
||||
}
|
||||
|
|
@ -172,25 +172,13 @@ impl AuthContextSlot {
|
|||
pub(crate) fn log_snapshot(&self) -> RequestAuthLogContext {
|
||||
let context = self.0.lock().expect("auth context lock poisoned");
|
||||
RequestAuthLogContext {
|
||||
principal: principal_without_log_unused_fields(&context.principal),
|
||||
principal: context.principal.clone(),
|
||||
auth_status: context.auth_status,
|
||||
auth_error_code: context.auth_error_code,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn principal_without_log_unused_fields(principal: &Principal) -> Principal {
|
||||
match principal {
|
||||
Principal::User(user) => Principal::User(UserPrincipal {
|
||||
identity: user.identity.clone(),
|
||||
login: user.login.clone(),
|
||||
auth_method: user.auth_method,
|
||||
avatar_url: None,
|
||||
}),
|
||||
principal => principal.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Send + Sync> FromRequestParts<S> for RequestAuth {
|
||||
type Rejection = Infallible;
|
||||
|
||||
|
|
@ -402,8 +390,8 @@ fn auth_slot_from_parts(parts: &Parts) -> AuthContextSlot {
|
|||
pub(crate) fn require_user(slot: &AuthContextSlot) -> Result<UserPrincipal, ApiError> {
|
||||
let context = slot.0.lock().expect("auth context lock poisoned");
|
||||
match &context.principal {
|
||||
Principal::User(user) => Ok(user.clone()),
|
||||
_ => Err(auth_rejection(context.auth_status, context.auth_error_code)),
|
||||
Some(Principal::User(user)) => Ok(user.clone()),
|
||||
None | Some(_) => Err(auth_rejection(context.auth_status, context.auth_error_code)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -412,7 +400,7 @@ pub(crate) fn require_authenticated_user(
|
|||
) -> Result<AuthenticatedUser, ApiError> {
|
||||
let context = slot.snapshot();
|
||||
match context.principal {
|
||||
Principal::User(principal) => {
|
||||
Some(Principal::User(principal)) => {
|
||||
let Some(profile) = context.user_profile else {
|
||||
return Err(ApiError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
|
|
@ -421,19 +409,19 @@ pub(crate) fn require_authenticated_user(
|
|||
};
|
||||
Ok(AuthenticatedUser { principal, profile })
|
||||
}
|
||||
_ => Err(auth_rejection(context.auth_status, context.auth_error_code)),
|
||||
None | Some(_) => Err(auth_rejection(context.auth_status, context.auth_error_code)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn require_run_management_actor(slot: &AuthContextSlot) -> Result<Principal, ApiError> {
|
||||
let context = slot.0.lock().expect("auth context lock poisoned");
|
||||
match &context.principal {
|
||||
Principal::User(user) => Ok(Principal::User(user.clone())),
|
||||
Principal::Worker { run_id } if context.worker_scopes.has_agent_run_tools() => {
|
||||
Some(Principal::User(user)) => Ok(Principal::User(user.clone())),
|
||||
Some(Principal::Worker { run_id }) if context.worker_scopes.has_agent_run_tools() => {
|
||||
Ok(Principal::Worker { run_id: *run_id })
|
||||
}
|
||||
Principal::Worker { .. } => Err(ApiError::forbidden()),
|
||||
_ => Err(auth_rejection(context.auth_status, context.auth_error_code)),
|
||||
Some(Principal::Worker { .. }) => Err(ApiError::forbidden()),
|
||||
None | Some(_) => Err(auth_rejection(context.auth_status, context.auth_error_code)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -443,19 +431,19 @@ fn require_worker_or_user_for_run(
|
|||
) -> Result<(), ApiError> {
|
||||
let context = slot.0.lock().expect("auth context lock poisoned");
|
||||
match &context.principal {
|
||||
Principal::User(_) => Ok(()),
|
||||
Principal::Worker { run_id } if run_id == route_run_id => Ok(()),
|
||||
Principal::Worker { .. } => Err(ApiError::forbidden()),
|
||||
_ => Err(auth_rejection(context.auth_status, context.auth_error_code)),
|
||||
Some(Principal::User(_)) => Ok(()),
|
||||
Some(Principal::Worker { run_id }) if run_id == route_run_id => Ok(()),
|
||||
Some(Principal::Worker { .. }) => Err(ApiError::forbidden()),
|
||||
None | Some(_) => Err(auth_rejection(context.auth_status, context.auth_error_code)),
|
||||
}
|
||||
}
|
||||
|
||||
fn require_worker_for_run(slot: &AuthContextSlot, route_run_id: &RunId) -> Result<(), ApiError> {
|
||||
let context = slot.0.lock().expect("auth context lock poisoned");
|
||||
match &context.principal {
|
||||
Principal::Worker { run_id } if run_id == route_run_id => Ok(()),
|
||||
Principal::Worker { .. } | Principal::User(_) => Err(ApiError::forbidden()),
|
||||
_ => Err(auth_rejection(context.auth_status, context.auth_error_code)),
|
||||
Some(Principal::Worker { run_id }) if run_id == route_run_id => Ok(()),
|
||||
Some(Principal::Worker { .. } | Principal::User(_)) => Err(ApiError::forbidden()),
|
||||
None | Some(_) => Err(auth_rejection(context.auth_status, context.auth_error_code)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -465,14 +453,14 @@ fn require_run_management_target(
|
|||
) -> Result<Principal, ApiError> {
|
||||
let context = slot.0.lock().expect("auth context lock poisoned");
|
||||
match &context.principal {
|
||||
Principal::User(user) => Ok(Principal::User(user.clone())),
|
||||
Principal::Worker { run_id }
|
||||
Some(Principal::User(user)) => Ok(Principal::User(user.clone())),
|
||||
Some(Principal::Worker { run_id })
|
||||
if run_id == route_run_id || context.worker_scopes.has_agent_run_tools() =>
|
||||
{
|
||||
Ok(Principal::Worker { run_id: *run_id })
|
||||
}
|
||||
Principal::Worker { .. } => Err(ApiError::forbidden()),
|
||||
_ => Err(auth_rejection(context.auth_status, context.auth_error_code)),
|
||||
Some(Principal::Worker { .. }) => Err(ApiError::forbidden()),
|
||||
None | Some(_) => Err(auth_rejection(context.auth_status, context.auth_error_code)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -687,7 +675,7 @@ mod tests {
|
|||
let context = classify_request(&request, state.as_ref());
|
||||
|
||||
assert_eq!(context.auth_status, AuthStatus::Authenticated);
|
||||
assert!(matches!(context.principal, Principal::User(_)));
|
||||
assert!(matches!(context.principal, Some(Principal::User(_))));
|
||||
assert!(context.user_profile.is_some());
|
||||
}
|
||||
|
||||
|
|
@ -727,7 +715,7 @@ mod tests {
|
|||
let context = classify_request(&request, state.as_ref());
|
||||
|
||||
assert_eq!(context.auth_status, AuthStatus::Authenticated);
|
||||
assert_eq!(context.principal, Principal::Worker { run_id });
|
||||
assert_eq!(context.principal, Some(Principal::Worker { run_id }));
|
||||
assert!(!context.worker_scopes.has_agent_run_tools());
|
||||
}
|
||||
|
||||
|
|
@ -746,7 +734,7 @@ mod tests {
|
|||
let context = classify_request(&request, state.as_ref());
|
||||
|
||||
assert_eq!(context.auth_status, AuthStatus::Authenticated);
|
||||
assert_eq!(context.principal, Principal::Worker { run_id });
|
||||
assert_eq!(context.principal, Some(Principal::Worker { run_id }));
|
||||
assert!(context.worker_scopes.has_agent_run_tools());
|
||||
}
|
||||
|
||||
|
|
@ -825,7 +813,7 @@ mod tests {
|
|||
|
||||
assert_eq!(context.auth_status, AuthStatus::Missing);
|
||||
assert_eq!(context.auth_error_code, None);
|
||||
assert_eq!(context.principal, Principal::Anonymous);
|
||||
assert_eq!(context.principal, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1717,7 +1717,7 @@ fn count_flags(data: &[FileDiff]) -> (u64, u64, u64, u64) {
|
|||
mod tests {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use fabro_types::{CommandTermination, RunId};
|
||||
use fabro_types::{CommandTermination, RunId, test_support};
|
||||
use tokio::time::{Duration, sleep};
|
||||
|
||||
use super::*;
|
||||
|
|
@ -2389,7 +2389,7 @@ index 1111111..2222222 160000
|
|||
automation: None,
|
||||
source_directory: None,
|
||||
labels: HashMap::default(),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
git: None,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ use fabro_static::EnvVars;
|
|||
use fabro_types::settings::cli::OutputVerbosity;
|
||||
use fabro_types::settings::interp::InterpString;
|
||||
use fabro_types::settings::run::{EnvironmentProvider, RunGoal, RunNamespace};
|
||||
use fabro_types::{ManifestPath, RunId, SandboxProviderKind, ServerSettings, WorkflowSettings};
|
||||
use fabro_types::{
|
||||
ManifestPath, RunId, RunProvenance, SandboxProviderKind, ServerSettings, WorkflowSettings,
|
||||
};
|
||||
use fabro_util::check_report::{CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus};
|
||||
use fabro_validate::Severity;
|
||||
use fabro_workflow::Error as WorkflowError;
|
||||
|
|
@ -193,6 +195,7 @@ pub(crate) fn validate_prepared_manifest(
|
|||
pub(crate) fn create_run_input(
|
||||
prepared: PreparedManifest,
|
||||
configured_providers: Vec<ProviderId>,
|
||||
provenance: RunProvenance,
|
||||
web_url: Option<String>,
|
||||
) -> CreateRunInput {
|
||||
CreateRunInput {
|
||||
|
|
@ -209,7 +212,7 @@ pub(crate) fn create_run_input(
|
|||
git: prepared.git,
|
||||
fork_source_ref: None,
|
||||
parent_id: prepared.parent_id,
|
||||
provenance: None,
|
||||
provenance,
|
||||
configured_providers,
|
||||
web_url,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1876,7 +1876,10 @@ async fn http_log_middleware(mut req: axum_extract::Request, next: Next) -> Resp
|
|||
let status = response.status().as_u16();
|
||||
let latency_ms = start.elapsed().as_millis();
|
||||
let auth_context = auth_slot.log_snapshot();
|
||||
let principal_kind = auth_context.principal.kind();
|
||||
let principal_kind = auth_context
|
||||
.principal
|
||||
.as_ref()
|
||||
.map_or("none", Principal::kind);
|
||||
let auth_status = auth_context.auth_status.as_str();
|
||||
|
||||
macro_rules! emit_http_log {
|
||||
|
|
@ -1914,27 +1917,27 @@ async fn http_log_middleware(mut req: axum_extract::Request, next: Next) -> Resp
|
|||
macro_rules! emit_principal_http_log {
|
||||
($level:ident) => {{
|
||||
match &auth_context.principal {
|
||||
Principal::User(user) => emit_http_log!(
|
||||
Some(Principal::User(user)) => emit_http_log!(
|
||||
$level,
|
||||
user_auth_method = user.auth_method.as_str(),
|
||||
idp_issuer = user.identity.issuer(),
|
||||
idp_subject = user.identity.subject(),
|
||||
login = user.login.as_str(),
|
||||
),
|
||||
Principal::Worker { run_id } => {
|
||||
Some(Principal::Worker { run_id }) => {
|
||||
emit_http_log!($level, run_id = run_id.to_string().as_str(),)
|
||||
}
|
||||
Principal::Webhook { delivery_id } => {
|
||||
Some(Principal::Webhook { delivery_id }) => {
|
||||
emit_http_log!($level, delivery_id = delivery_id.as_str(),)
|
||||
}
|
||||
Principal::Slack {
|
||||
Some(Principal::Slack {
|
||||
team_id, user_id, ..
|
||||
} => emit_http_log!(
|
||||
}) => emit_http_log!(
|
||||
$level,
|
||||
team_id = team_id.as_str(),
|
||||
user_id = user_id.as_str(),
|
||||
),
|
||||
Principal::Agent { .. } | Principal::System { .. } | Principal::Anonymous => {
|
||||
None | Some(Principal::Agent { .. } | Principal::System { .. }) => {
|
||||
emit_http_log!($level)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -535,7 +535,7 @@ mod stage_events_tests {
|
|||
use axum::body::{Body, to_bytes};
|
||||
use axum::http::{Request, StatusCode, header};
|
||||
use fabro_store::EventPayload;
|
||||
use fabro_types::{Graph, RunId, WorkflowSettings};
|
||||
use fabro_types::{Graph, RunId, WorkflowSettings, test_support};
|
||||
use fabro_workflow::event as workflow_event;
|
||||
use http_body_util::BodyExt;
|
||||
use serde_json::json;
|
||||
|
|
@ -570,7 +570,7 @@ mod stage_events_tests {
|
|||
workflow_slug: None,
|
||||
automation: None,
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
|
|
|
|||
|
|
@ -371,46 +371,67 @@ async fn cancel_run(
|
|||
if let Some(response) = reject_if_archived(state.as_ref(), &id).await {
|
||||
return response;
|
||||
}
|
||||
let pending_control = match load_pending_control(state.as_ref(), id).await {
|
||||
Ok(pending_control) => pending_control,
|
||||
let durable_summary = match state.store.runs().find(&id).await {
|
||||
Ok(summary) => summary,
|
||||
Err(err) => {
|
||||
return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let pending_control = durable_summary
|
||||
.as_ref()
|
||||
.and_then(|summary| summary.lifecycle.pending_control);
|
||||
let durable_status = durable_summary
|
||||
.as_ref()
|
||||
.map(|summary| summary.lifecycle.status);
|
||||
let cancel_target = {
|
||||
let mut runs = state.runs.lock().expect("runs lock poisoned");
|
||||
match runs.get_mut(&id) {
|
||||
Some(managed_run) => match managed_run.status {
|
||||
RunStatus::Submitted
|
||||
| RunStatus::Pending { .. }
|
||||
| RunStatus::Runnable
|
||||
| RunStatus::Starting
|
||||
| RunStatus::Running
|
||||
| RunStatus::Blocked { .. }
|
||||
| RunStatus::Paused { .. } => {
|
||||
let persist_cancelled_status = matches!(
|
||||
managed_run.status,
|
||||
RunStatus::Submitted | RunStatus::Pending { .. } | RunStatus::Runnable
|
||||
);
|
||||
if persist_cancelled_status {
|
||||
managed_run.status = RunStatus::Failed {
|
||||
reason: FailureReason::Cancelled,
|
||||
Some(managed_run) => {
|
||||
let managed_status = managed_run.status;
|
||||
match managed_status {
|
||||
RunStatus::Submitted
|
||||
| RunStatus::Pending { .. }
|
||||
| RunStatus::Runnable
|
||||
| RunStatus::Starting
|
||||
| RunStatus::Running
|
||||
| RunStatus::Blocked { .. }
|
||||
| RunStatus::Paused { .. } => {
|
||||
let answer_transport = managed_run.answer_transport.clone();
|
||||
let should_cancel_pending_interview =
|
||||
matches!(
|
||||
&answer_transport,
|
||||
Some(RunAnswerTransport::InProcess { .. })
|
||||
) && (matches!(managed_status, RunStatus::Blocked { .. })
|
||||
|| matches!(durable_status, Some(RunStatus::Blocked { .. })));
|
||||
let persist_cancelled_status = matches!(
|
||||
managed_status,
|
||||
RunStatus::Submitted | RunStatus::Pending { .. } | RunStatus::Runnable
|
||||
) && !should_cancel_pending_interview;
|
||||
if persist_cancelled_status {
|
||||
managed_run.status = RunStatus::Failed {
|
||||
reason: FailureReason::Cancelled,
|
||||
};
|
||||
}
|
||||
let cancel_tx = if should_cancel_pending_interview {
|
||||
None
|
||||
} else {
|
||||
managed_run.cancel_tx.take()
|
||||
};
|
||||
Some((
|
||||
persist_cancelled_status,
|
||||
answer_transport,
|
||||
managed_run.cancel_token.clone(),
|
||||
cancel_tx,
|
||||
managed_run.worker_ref.clone(),
|
||||
))
|
||||
}
|
||||
_ => {
|
||||
return ApiError::new(StatusCode::CONFLICT, "Run is not cancellable.")
|
||||
.into_response();
|
||||
}
|
||||
Some((
|
||||
persist_cancelled_status,
|
||||
managed_run.answer_transport.clone(),
|
||||
managed_run.cancel_token.clone(),
|
||||
managed_run.cancel_tx.take(),
|
||||
managed_run.worker_ref.clone(),
|
||||
))
|
||||
}
|
||||
_ => {
|
||||
return ApiError::new(StatusCode::CONFLICT, "Run is not cancellable.")
|
||||
.into_response();
|
||||
}
|
||||
},
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
};
|
||||
|
|
@ -873,7 +894,7 @@ async fn retry_run(
|
|||
let input = operations::RetryRunInput {
|
||||
source_run_id: id,
|
||||
new_run_id,
|
||||
provenance: Some(run_provenance(&headers, &actor)),
|
||||
provenance: run_provenance(&headers, &actor),
|
||||
web_url: state.run_web_url(&new_run_id),
|
||||
};
|
||||
match Box::pin(operations::retry_run(&state.store, &input)).await {
|
||||
|
|
|
|||
|
|
@ -850,7 +850,7 @@ mod tests {
|
|||
use fabro_types::run_event::AgentMessageProps;
|
||||
use fabro_types::{
|
||||
BilledTokenCounts, EventEnvelope, Graph, PairMessageId, RunEvent, StageId,
|
||||
WorkflowSettings, fixtures,
|
||||
WorkflowSettings, fixtures, test_support,
|
||||
};
|
||||
use fabro_workflow::event as workflow_event;
|
||||
use tower::ServiceExt;
|
||||
|
|
@ -1024,7 +1024,7 @@ mod tests {
|
|||
workflow_slug: None,
|
||||
automation: None,
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
|
|
|
|||
|
|
@ -687,13 +687,14 @@ pub(crate) async fn create_run_from_manifest(
|
|||
.as_ref()
|
||||
.map(LlmClientResult::provider_ids)
|
||||
.unwrap_or_default();
|
||||
let provenance = run_provenance(&headers, &actor);
|
||||
let mut create_input = run_manifest::create_run_input(
|
||||
prepared.clone(),
|
||||
ready_provider_ids.clone(),
|
||||
provenance,
|
||||
web_url.clone(),
|
||||
);
|
||||
create_input.run_id = Some(run_id);
|
||||
create_input.provenance = Some(run_provenance(&headers, &actor));
|
||||
create_input.submitted_manifest_bytes = Some(submitted_manifest_bytes);
|
||||
create_input.automation = automation;
|
||||
|
||||
|
|
@ -864,7 +865,7 @@ pub(super) fn run_provenance(headers: &HeaderMap, subject: &Principal) -> RunPro
|
|||
version: FABRO_VERSION.to_string(),
|
||||
}),
|
||||
client: run_client_provenance(headers),
|
||||
subject: Some(subject.clone()),
|
||||
subject: subject.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1299,7 +1299,7 @@ FABRO_PROC_NET_TCP /proc/net/tcp6
|
|||
mod retrieve_sandbox_tests {
|
||||
use axum::body::{Body, to_bytes};
|
||||
use axum::http::{Request, StatusCode};
|
||||
use fabro_types::{Graph, RunId, WorkflowSettings};
|
||||
use fabro_types::{Graph, RunId, WorkflowSettings, test_support};
|
||||
use serde_json::{Value, json};
|
||||
use tower::ServiceExt;
|
||||
|
||||
|
|
@ -1339,6 +1339,7 @@ mod retrieve_sandbox_tests {
|
|||
"settings": WorkflowSettings::default(),
|
||||
"graph": Graph::new("test"),
|
||||
"run_dir": "/tmp/test",
|
||||
"provenance": test_support::test_run_provenance(),
|
||||
},
|
||||
}),
|
||||
run_id,
|
||||
|
|
|
|||
|
|
@ -1506,6 +1506,7 @@ mod tests {
|
|||
use fabro_agent::config::ToolAccess;
|
||||
use fabro_agent::tool_registry::{RegisteredTool, ToolContext, ToolRegistry, ToolSource};
|
||||
use fabro_llm::types::{ToolCall, ToolDefinition};
|
||||
use fabro_types::test_support;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
|
@ -1700,7 +1701,7 @@ mod tests {
|
|||
automation: None,
|
||||
source_directory: None,
|
||||
labels: HashMap::default(),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
git: None,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ use fabro_types::{
|
|||
SandboxProviderKind, StageContextWindowBreakdownItem, StageContextWindowCategory,
|
||||
StageContextWindowCountMethod, StageContextWindowProjection, StageContextWindowStaleness,
|
||||
StageContextWindowWarning, StageModelUsage, StageTiming, SuccessReason, SystemActorKind,
|
||||
WorkflowSettings, fixtures,
|
||||
WorkflowSettings, fixtures, test_support,
|
||||
};
|
||||
use fabro_util::check_report::CheckStatus;
|
||||
use fabro_workflow::records::CheckpointExt;
|
||||
|
|
@ -3980,7 +3980,7 @@ async fn append_default_run_created(run_store: &fabro_store::RunDatabase, run_id
|
|||
workflow_slug: None,
|
||||
automation: None,
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
|
|
@ -4034,7 +4034,7 @@ async fn create_slack_notification_run(
|
|||
workflow_slug: workflow_slug.map(str::to_string),
|
||||
automation: None,
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
|
|
@ -5041,7 +5041,7 @@ async fn list_run_stages_distinguishes_visits() {
|
|||
workflow_slug: Some("test".to_string()),
|
||||
automation: None,
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
|
|
@ -6098,7 +6098,7 @@ async fn create_completed_run_ready_for_pull_request(
|
|||
source_directory: Some("/tmp/project".to_string()),
|
||||
git: git.clone(),
|
||||
labels: HashMap::new(),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
|
|
@ -9901,15 +9901,10 @@ async fn run_tool_worker_token_can_use_client_backend_routes_across_runs() {
|
|||
.unwrap()
|
||||
.expect("created run should be cached");
|
||||
assert_eq!(
|
||||
cached
|
||||
.projection
|
||||
.spec
|
||||
.provenance
|
||||
.as_ref()
|
||||
.and_then(|provenance| provenance.subject.as_ref()),
|
||||
Some(&Principal::Worker {
|
||||
cached.projection.spec.provenance.subject,
|
||||
Principal::Worker {
|
||||
run_id: parent_run_id,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
let response = app
|
||||
|
|
@ -12268,7 +12263,7 @@ async fn create_preserved_local_sandbox_run(state: &Arc<AppState>, run_id: RunId
|
|||
workflow_slug: Some("test".to_string()),
|
||||
automation: None,
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
|
|
@ -13020,7 +13015,7 @@ async fn delete_run_retry_after_missing_provider_resource_removes_metadata() {
|
|||
workflow_slug: Some("test".to_string()),
|
||||
automation: None,
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
|
|
@ -13628,6 +13623,72 @@ async fn cancel_run_requests_worker_runtime_stop_when_control_unavailable() {
|
|||
assert_eq!(runtime.requested_refs(), vec![worker_ref]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cancel_durably_blocked_in_process_run_cancels_pending_interview_without_abort_signal() {
|
||||
let state = test_app_state();
|
||||
let app = crate::test_support::build_test_router(Arc::clone(&state));
|
||||
let run_id = fixtures::RUN_1;
|
||||
create_durable_run_with_events(&state, run_id, &[
|
||||
workflow_event::Event::RunRunning,
|
||||
workflow_event::Event::RunBlocked {
|
||||
blocked_reason: BlockedReason::HumanInputRequired,
|
||||
},
|
||||
])
|
||||
.await;
|
||||
|
||||
let interviewer = Arc::new(ControlInterviewer::new());
|
||||
let mut question = Question::new("approve?", QuestionType::YesNo);
|
||||
question.id = "q-1".to_string();
|
||||
let ask_interviewer = Arc::clone(&interviewer);
|
||||
let ask = tokio::spawn(async move { ask_interviewer.ask(question).await });
|
||||
tokio::task::yield_now().await;
|
||||
|
||||
let (cancel_tx, mut cancel_rx) = oneshot::channel();
|
||||
let cancel_token = CancellationToken::new();
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let mut run = managed_run(
|
||||
MINIMAL_DOT.to_string(),
|
||||
RunStatus::Running,
|
||||
Utc::now(),
|
||||
temp_dir.path().join(run_id.to_string()),
|
||||
RunExecutionMode::Start,
|
||||
);
|
||||
run.answer_transport = Some(RunAnswerTransport::InProcess {
|
||||
interviewer,
|
||||
steering_hub: Arc::new(fabro_workflow::SteeringHub::new(Arc::new(
|
||||
fabro_workflow::event::Emitter::new(run_id),
|
||||
))),
|
||||
});
|
||||
run.cancel_token = Some(cancel_token);
|
||||
run.cancel_tx = Some(cancel_tx);
|
||||
state
|
||||
.runs
|
||||
.lock()
|
||||
.expect("runs lock poisoned")
|
||||
.insert(run_id, run);
|
||||
|
||||
let req = Request::builder()
|
||||
.method("POST")
|
||||
.uri(api(&format!("/runs/{run_id}/cancel")))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let response = app.oneshot(req).await.unwrap();
|
||||
assert_status!(response, StatusCode::OK).await;
|
||||
|
||||
let submission = tokio::time::timeout(std::time::Duration::from_millis(100), ask)
|
||||
.await
|
||||
.expect("cancel should resolve the pending in-process interview")
|
||||
.expect("interview task should not panic");
|
||||
assert_eq!(submission.answer.value, AnswerValue::Cancelled);
|
||||
assert!(
|
||||
matches!(
|
||||
cancel_rx.try_recv(),
|
||||
Err(tokio::sync::oneshot::error::TryRecvError::Empty)
|
||||
),
|
||||
"blocked in-process cancellation should let the workflow unwind instead of aborting it"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pause_run_rejects_when_control_is_already_pending() {
|
||||
let state = test_app_state();
|
||||
|
|
|
|||
|
|
@ -1365,7 +1365,7 @@ client_id = "github-client-id"
|
|||
|
||||
let contexts = captured.lock().expect("captured auth contexts").clone();
|
||||
assert_eq!(contexts[0].auth_status, AuthStatus::Authenticated);
|
||||
assert!(matches!(contexts[0].principal, Principal::User(_)));
|
||||
assert!(matches!(contexts[0].principal, Some(Principal::User(_))));
|
||||
assert_eq!(contexts[1].auth_status, AuthStatus::Invalid);
|
||||
assert_eq!(
|
||||
contexts[1].auth_error_code,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use axum::body::Body;
|
|||
use axum::http::{Request, StatusCode};
|
||||
use fabro_server::test_support::test_app_state_with_store;
|
||||
use fabro_store::{ArtifactStore, Database};
|
||||
use fabro_types::{Graph, RunId, SandboxProviderKind, WorkflowSettings};
|
||||
use fabro_types::{Graph, RunId, SandboxProviderKind, WorkflowSettings, test_support};
|
||||
use fabro_workflow::event as workflow_event;
|
||||
use fabro_workflow::run_status::SuccessReason;
|
||||
use object_store::memory::InMemory as MemoryObjectStore;
|
||||
|
|
@ -69,7 +69,7 @@ async fn append_completed_run_with_final_patch(
|
|||
workflow_slug: None,
|
||||
automation: None,
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ futures.workspace = true
|
|||
uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
fabro-types = { path = "../fabro-types", features = ["test-support"] }
|
||||
tokio = { workspace = true, features = ["test-util", "macros"] }
|
||||
tempfile = "3"
|
||||
ulid.workspace = true
|
||||
|
|
|
|||
|
|
@ -922,11 +922,7 @@ pub(crate) fn build_summary(state: &RunProjection, run_id: &RunId) -> Run {
|
|||
})
|
||||
.map(|(_, record)| record.question.clone());
|
||||
let models = run_models(state);
|
||||
let created_by = state
|
||||
.spec
|
||||
.provenance
|
||||
.as_ref()
|
||||
.and_then(|provenance| provenance.subject.clone());
|
||||
let created_by = state.spec.provenance.subject.clone();
|
||||
let source_directory = state.spec.source_directory.clone();
|
||||
let repo_origin_url = state.spec.git.as_ref().map(|git| git.origin_url.clone());
|
||||
let start_time = state.start.as_ref().map(|start| start.start_time);
|
||||
|
|
@ -1276,7 +1272,7 @@ mod tests {
|
|||
StageContextWindowBreakdownItem, StageContextWindowCategory, StageContextWindowCountMethod,
|
||||
StageContextWindowProjection, StageContextWindowStaleness, StageContextWindowWarning,
|
||||
StageModelUsage, StageOutcome, StageState, SubAgentStatus, SuccessReason, WorkflowSettings,
|
||||
first_event_seq, fixtures,
|
||||
first_event_seq, fixtures, test_support,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
|
|
@ -1358,7 +1354,7 @@ mod tests {
|
|||
automation: None,
|
||||
source_directory: None,
|
||||
labels: HashMap::new(),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
git: None,
|
||||
|
|
@ -1439,7 +1435,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_run_created_projects_retried_from_none() {
|
||||
fn run_created_without_retried_from_projects_retried_from_none() {
|
||||
let event = test_raw_event(
|
||||
1,
|
||||
"run.created",
|
||||
|
|
@ -1447,7 +1443,8 @@ mod tests {
|
|||
"settings": WorkflowSettings::default(),
|
||||
"graph": Graph::new("test"),
|
||||
"labels": {},
|
||||
"run_dir": "/tmp/run"
|
||||
"run_dir": "/tmp/run",
|
||||
"provenance": test_support::test_run_provenance()
|
||||
}),
|
||||
None,
|
||||
);
|
||||
|
|
@ -1475,7 +1472,8 @@ mod tests {
|
|||
"graph": Graph::new("test"),
|
||||
"automation": automation,
|
||||
"labels": {},
|
||||
"run_dir": "/tmp/run"
|
||||
"run_dir": "/tmp/run",
|
||||
"provenance": test_support::test_run_provenance()
|
||||
}),
|
||||
None,
|
||||
);
|
||||
|
|
@ -1498,7 +1496,8 @@ mod tests {
|
|||
"settings": WorkflowSettings::default(),
|
||||
"graph": Graph::new("test"),
|
||||
"labels": {},
|
||||
"run_dir": "/tmp/run"
|
||||
"run_dir": "/tmp/run",
|
||||
"provenance": test_support::test_run_provenance()
|
||||
}),
|
||||
None,
|
||||
)])
|
||||
|
|
@ -1520,7 +1519,8 @@ mod tests {
|
|||
"settings": WorkflowSettings::default(),
|
||||
"graph": Graph::new("test"),
|
||||
"labels": {},
|
||||
"run_dir": "/tmp/run"
|
||||
"run_dir": "/tmp/run",
|
||||
"provenance": test_support::test_run_provenance()
|
||||
}),
|
||||
None,
|
||||
)])
|
||||
|
|
@ -1597,7 +1597,8 @@ mod tests {
|
|||
"settings": WorkflowSettings::default(),
|
||||
"graph": Graph::new("test"),
|
||||
"labels": {},
|
||||
"run_dir": "/tmp/run"
|
||||
"run_dir": "/tmp/run",
|
||||
"provenance": test_support::test_run_provenance()
|
||||
}),
|
||||
None,
|
||||
),
|
||||
|
|
@ -1822,7 +1823,7 @@ mod tests {
|
|||
"repo_origin_url": null,
|
||||
"base_branch": null,
|
||||
"labels": {},
|
||||
"provenance": null,
|
||||
"provenance": test_support::test_run_provenance(),
|
||||
"manifest_blob": null,
|
||||
"definition_blob": null,
|
||||
"git": null,
|
||||
|
|
@ -2851,7 +2852,7 @@ mod tests {
|
|||
source_directory: Some("/tmp/repo".to_string()),
|
||||
git: None,
|
||||
labels: HashMap::new(),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
|
|
@ -2877,7 +2878,7 @@ mod tests {
|
|||
source_directory: Some("/tmp/repo".to_string()),
|
||||
git: None,
|
||||
labels: HashMap::new(),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
|
|
@ -2916,7 +2917,8 @@ mod tests {
|
|||
"attrs": { "goal": { "String": "Goal title" } }
|
||||
},
|
||||
"labels": {},
|
||||
"run_dir": "/tmp/run"
|
||||
"run_dir": "/tmp/run",
|
||||
"provenance": test_support::test_run_provenance()
|
||||
}),
|
||||
None,
|
||||
);
|
||||
|
|
@ -2930,7 +2932,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_run_created_without_title_infers_projection_title() {
|
||||
fn run_created_without_title_infers_projection_title() {
|
||||
let event = test_raw_event(
|
||||
1,
|
||||
"run.created",
|
||||
|
|
@ -2943,7 +2945,8 @@ mod tests {
|
|||
"attrs": { "goal": { "String": "## Plan: Legacy title\n\nDetails" } }
|
||||
},
|
||||
"labels": {},
|
||||
"run_dir": "/tmp/run"
|
||||
"run_dir": "/tmp/run",
|
||||
"provenance": test_support::test_run_provenance()
|
||||
}),
|
||||
None,
|
||||
);
|
||||
|
|
@ -2972,7 +2975,8 @@ mod tests {
|
|||
"attrs": { "goal": { "String": "Goal title" } }
|
||||
},
|
||||
"labels": {},
|
||||
"run_dir": "/tmp/run"
|
||||
"run_dir": "/tmp/run",
|
||||
"provenance": test_support::test_run_provenance()
|
||||
}),
|
||||
None,
|
||||
),
|
||||
|
|
@ -3016,6 +3020,7 @@ mod tests {
|
|||
"labels": {},
|
||||
"run_dir": "/tmp/run",
|
||||
"source_directory": "/tmp/run",
|
||||
"provenance": test_support::test_run_provenance(),
|
||||
"manifest_blob": manifest_blob
|
||||
}
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -472,7 +472,7 @@ mod tests {
|
|||
use chrono::{DateTime, Utc};
|
||||
use fabro_types::{
|
||||
AttrValue, FailureReason, Graph, RunControlAction, RunSpec, RunStatus, StageId,
|
||||
SuccessReason, WorkflowSettings,
|
||||
SuccessReason, WorkflowSettings, test_support,
|
||||
};
|
||||
use futures::TryStreamExt;
|
||||
use object_store::memory::InMemory;
|
||||
|
|
@ -542,7 +542,7 @@ mod tests {
|
|||
automation: None,
|
||||
source_directory: Some(format!("/tmp/{label}")),
|
||||
labels: std::collections::HashMap::from([("team".to_string(), "infra".to_string())]),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
git: Some(fabro_types::GitContext {
|
||||
|
|
@ -601,6 +601,7 @@ mod tests {
|
|||
"run_dir": format!("/tmp/{label}"),
|
||||
"git": run_spec.git,
|
||||
"labels": run_spec.labels,
|
||||
"provenance": run_spec.provenance,
|
||||
}),
|
||||
))
|
||||
.await
|
||||
|
|
@ -627,6 +628,7 @@ mod tests {
|
|||
"git": run_spec.git,
|
||||
"labels": run_spec.labels,
|
||||
"parent_id": parent_id,
|
||||
"provenance": run_spec.provenance,
|
||||
}),
|
||||
))
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -667,7 +667,7 @@ mod tests {
|
|||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use fabro_types::{Graph, RunId, SessionId, StageId, WorkflowSettings};
|
||||
use fabro_types::{Graph, RunId, SessionId, StageId, WorkflowSettings, test_support};
|
||||
use object_store::memory::InMemory;
|
||||
use serde_json::json;
|
||||
|
||||
|
|
@ -723,6 +723,7 @@ mod tests {
|
|||
"settings": WorkflowSettings::default(),
|
||||
"graph": Graph::new("test"),
|
||||
"run_dir": "/tmp/test",
|
||||
"provenance": test_support::test_run_provenance(),
|
||||
},
|
||||
}),
|
||||
run_id,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use fabro_types::{
|
|||
BilledModelUsage, BilledTokenCounts, Checkpoint, CheckpointRecord, InterviewQuestionRecord,
|
||||
QuestionType, RunDiff, RunSandbox, RunSandboxInstance, RunSandboxPlan, RunSandboxRuntime,
|
||||
RunStatus, SandboxProviderKind, StageCompletion, StageModelUsage, StageOutcome, StartRecord,
|
||||
WorkflowSettings, first_event_seq, fixtures,
|
||||
WorkflowSettings, first_event_seq, fixtures, test_support,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ fn sample_run_spec() -> RunSpec {
|
|||
automation: None,
|
||||
source_directory: Some("/tmp/project".to_string()),
|
||||
labels: HashMap::from([("team".to_string(), "platform".to_string())]),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
git: Some(fabro_types::GitContext {
|
||||
|
|
|
|||
|
|
@ -29,4 +29,5 @@ tokio.workspace = true
|
|||
toml.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
fabro-types = { path = "../fabro-types", features = ["test-support"] }
|
||||
tempfile = "3"
|
||||
|
|
|
|||
|
|
@ -307,7 +307,9 @@ fn format_tool_error(err: &anyhow::Error) -> String {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::{TimeZone, Utc};
|
||||
use fabro_types::{RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef};
|
||||
use fabro_types::{
|
||||
RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef, test_support,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
|
@ -413,7 +415,7 @@ mod tests {
|
|||
},
|
||||
automation: None,
|
||||
repository: None,
|
||||
created_by: None,
|
||||
created_by: test_support::test_principal(),
|
||||
origin: RunOrigin::default(),
|
||||
labels: HashMap::new(),
|
||||
lifecycle: RunLifecycle {
|
||||
|
|
|
|||
|
|
@ -508,7 +508,7 @@ mod tests {
|
|||
use fabro_api::types;
|
||||
use fabro_types::{
|
||||
EventEnvelope, Run, RunLifecycle, RunLinks, RunOrigin, RunProjection, RunStatus,
|
||||
RunTimestamps, WorkflowRef,
|
||||
RunTimestamps, WorkflowRef, test_support,
|
||||
};
|
||||
use schemars::SchemaGenerator;
|
||||
use serde_json::json;
|
||||
|
|
@ -902,7 +902,7 @@ mod tests {
|
|||
},
|
||||
automation: None,
|
||||
repository: None,
|
||||
created_by: None,
|
||||
created_by: test_support::test_principal(),
|
||||
origin: RunOrigin::default(),
|
||||
labels: HashMap::new(),
|
||||
lifecycle: RunLifecycle {
|
||||
|
|
|
|||
|
|
@ -453,7 +453,7 @@ mod tests {
|
|||
use chrono::{TimeZone, Utc};
|
||||
use fabro_types::{
|
||||
EventEnvelope, FailureReason, Run, RunId, RunLifecycle, RunLinks, RunOrigin, RunProjection,
|
||||
RunStatus, RunTimestamps, WorkflowRef,
|
||||
RunStatus, RunTimestamps, WorkflowRef, test_support,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
|
|
@ -690,7 +690,7 @@ mod tests {
|
|||
},
|
||||
automation: None,
|
||||
repository: None,
|
||||
created_by: None,
|
||||
created_by: test_support::test_principal(),
|
||||
origin: RunOrigin::default(),
|
||||
labels: HashMap::new(),
|
||||
lifecycle: RunLifecycle {
|
||||
|
|
|
|||
|
|
@ -293,7 +293,9 @@ mod tests {
|
|||
use std::collections::HashMap;
|
||||
|
||||
use chrono::{TimeZone, Utc};
|
||||
use fabro_types::{RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef};
|
||||
use fabro_types::{
|
||||
RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef, test_support,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
|
@ -444,7 +446,7 @@ mod tests {
|
|||
},
|
||||
automation: None,
|
||||
repository: None,
|
||||
created_by: None,
|
||||
created_by: test_support::test_principal(),
|
||||
origin: RunOrigin::default(),
|
||||
labels: HashMap::from([("group".to_string(), group.to_string())]),
|
||||
lifecycle: RunLifecycle {
|
||||
|
|
|
|||
|
|
@ -33,4 +33,5 @@ ulid.workspace = true
|
|||
url.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
fabro-types = { path = ".", features = ["test-support"] }
|
||||
tempfile = "3"
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ pub mod start;
|
|||
pub mod status;
|
||||
pub mod steering;
|
||||
pub mod system_integrations;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test_support;
|
||||
pub mod timing;
|
||||
pub mod todo;
|
||||
pub mod transcript;
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@ pub struct UserPrincipal {
|
|||
pub avatar_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, IntoStaticStr)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum Principal {
|
||||
User(UserPrincipal),
|
||||
Worker {
|
||||
|
|
@ -39,7 +40,6 @@ pub enum Principal {
|
|||
System {
|
||||
system_kind: SystemActorKind,
|
||||
},
|
||||
Anonymous,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, IntoStaticStr)]
|
||||
|
|
@ -90,15 +90,7 @@ impl Principal {
|
|||
|
||||
#[must_use]
|
||||
pub fn kind(&self) -> &'static str {
|
||||
match self {
|
||||
Self::User(_) => "user",
|
||||
Self::Worker { .. } => "worker",
|
||||
Self::Webhook { .. } => "webhook",
|
||||
Self::Slack { .. } => "slack",
|
||||
Self::Agent { .. } => "agent",
|
||||
Self::System { .. } => "system",
|
||||
Self::Anonymous => "anonymous",
|
||||
}
|
||||
self.into()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
|
@ -123,7 +115,6 @@ impl Principal {
|
|||
} => session_id.clone(),
|
||||
Self::Agent { .. } => "agent".to_string(),
|
||||
Self::System { system_kind } => format!("system:{system_kind}"),
|
||||
Self::Anonymous => "anonymous".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -291,11 +282,6 @@ mod tests {
|
|||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trips_anonymous_variant() {
|
||||
assert_round_trip(&Principal::Anonymous);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_method_as_str_matches_serde() {
|
||||
assert_eq!(AuthMethod::Github.as_str(), "github");
|
||||
|
|
|
|||
|
|
@ -24,14 +24,13 @@ pub struct RunClientProvenance {
|
|||
pub version: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RunProvenance {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub server: Option<RunServerProvenance>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub client: Option<RunClientProvenance>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub subject: Option<Principal>,
|
||||
pub subject: Principal,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
|
@ -93,8 +92,7 @@ pub struct RunSpec {
|
|||
pub source_directory: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||
pub labels: HashMap<String, String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub provenance: Option<RunProvenance>,
|
||||
pub provenance: RunProvenance,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub manifest_blob: Option<RunBlobId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
|
|
|||
|
|
@ -933,7 +933,7 @@ mod tests {
|
|||
use super::*;
|
||||
use crate::{
|
||||
AuthMethod, Edge, Graph, IdpIdentity, Node, PendingReason, RunBlobId, WorkflowSettings,
|
||||
fixtures,
|
||||
fixtures, test_support,
|
||||
};
|
||||
|
||||
fn user_principal(login: &str) -> Principal {
|
||||
|
|
@ -1017,7 +1017,8 @@ mod tests {
|
|||
"graph": graph,
|
||||
"labels": {},
|
||||
"run_dir": "/tmp/run",
|
||||
"source_directory": "/tmp/run"
|
||||
"source_directory": "/tmp/run",
|
||||
"provenance": test_support::test_run_provenance()
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -1038,6 +1039,7 @@ mod tests {
|
|||
"labels": {},
|
||||
"run_dir": "/tmp/run",
|
||||
"source_directory": "/tmp/run",
|
||||
"provenance": test_support::test_run_provenance(),
|
||||
"manifest_blob": RunBlobId::new(br#"{"version":1}"#).to_string()
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,8 +30,7 @@ pub struct RunCreatedProps {
|
|||
pub automation: Option<AutomationRef>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub db_prefix: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub provenance: Option<RunProvenance>,
|
||||
pub provenance: RunProvenance,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub manifest_blob: Option<RunBlobId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
|
|
|||
|
|
@ -681,7 +681,7 @@ mod title_tests {
|
|||
|
||||
use chrono::Utc;
|
||||
|
||||
use crate::{AttrValue, Graph, RunId, RunProjection, RunSpec, WorkflowSettings};
|
||||
use crate::{AttrValue, Graph, RunId, RunProjection, RunSpec, WorkflowSettings, test_support};
|
||||
|
||||
fn projection_with_goal(goal: Option<&str>) -> RunProjection {
|
||||
let mut graph = Graph::new("test");
|
||||
|
|
@ -700,7 +700,7 @@ mod title_tests {
|
|||
automation: None,
|
||||
source_directory: None,
|
||||
labels: HashMap::new(),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
git: None,
|
||||
|
|
@ -752,7 +752,7 @@ mod iter_stages_tests {
|
|||
use serde_json::json;
|
||||
|
||||
use super::RunProjection;
|
||||
use crate::{Graph, RunId, RunSpec, StageProjection, WorkflowSettings};
|
||||
use crate::{Graph, RunId, RunSpec, StageProjection, WorkflowSettings, test_support};
|
||||
|
||||
fn seq(n: u32) -> NonZeroU32 {
|
||||
NonZeroU32::new(n).unwrap()
|
||||
|
|
@ -770,7 +770,7 @@ mod iter_stages_tests {
|
|||
automation: None,
|
||||
source_directory: None,
|
||||
labels: HashMap::default(),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
git: None,
|
||||
|
|
|
|||
|
|
@ -52,8 +52,7 @@ pub struct Run {
|
|||
pub automation: Option<AutomationRef>,
|
||||
#[serde(default)]
|
||||
pub repository: Option<RepositoryRef>,
|
||||
#[serde(default)]
|
||||
pub created_by: Option<Principal>,
|
||||
pub created_by: Principal,
|
||||
pub origin: RunOrigin,
|
||||
pub labels: HashMap<String, String>,
|
||||
pub lifecycle: RunLifecycle,
|
||||
|
|
|
|||
19
lib/crates/fabro-types/src/test_support.rs
Normal file
19
lib/crates/fabro-types/src/test_support.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
use crate::{AuthMethod, IdpIdentity, Principal, RunProvenance};
|
||||
|
||||
#[must_use]
|
||||
pub fn test_principal() -> Principal {
|
||||
Principal::user(
|
||||
IdpIdentity::new("fabro:test", "test-user").expect("test identity should be valid"),
|
||||
"test".to_string(),
|
||||
AuthMethod::DevToken,
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn test_run_provenance() -> RunProvenance {
|
||||
RunProvenance {
|
||||
server: None,
|
||||
client: None,
|
||||
subject: test_principal(),
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ use fabro_types::run_event::run::{RunCreatedProps, RunParentLinkedProps, RunPare
|
|||
use fabro_types::run_event::{RunSessionTurnFailedCode, RunSessionTurnFailedProps};
|
||||
use fabro_types::settings::InterpString;
|
||||
use fabro_types::settings::run::RunGoal;
|
||||
use fabro_types::test_support::test_run_provenance;
|
||||
use fabro_types::{AutomationRef, EventBody, TurnId, WorkflowSettings, fixtures};
|
||||
|
||||
fn templated_settings() -> WorkflowSettings {
|
||||
|
|
@ -32,7 +33,7 @@ fn run_created_props_round_trip_templated_settings() {
|
|||
trigger_id: Some("schedule_1".to_string()),
|
||||
}),
|
||||
db_prefix: Some("run_".to_string()),
|
||||
provenance: None,
|
||||
provenance: test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
git: Some(GitContext {
|
||||
origin_url: "https://github.com/fabro-sh/fabro.git".to_string(),
|
||||
|
|
@ -97,7 +98,7 @@ fn run_created_props_omits_web_url_when_absent() {
|
|||
workflow_slug: None,
|
||||
automation: None,
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
provenance: test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
|
|
@ -134,7 +135,8 @@ fn run_created_props_defaults_additive_fields_for_legacy_events() {
|
|||
"settings": WorkflowSettings::default(),
|
||||
"graph": Graph::new("ship"),
|
||||
"labels": {},
|
||||
"run_dir": "/tmp/run"
|
||||
"run_dir": "/tmp/run",
|
||||
"provenance": test_run_provenance()
|
||||
});
|
||||
|
||||
let props: RunCreatedProps =
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use std::collections::HashMap;
|
|||
use fabro_types::graph::Graph;
|
||||
use fabro_types::run::{DirtyStatus, GitContext, PreRunPushOutcome, RunSpec};
|
||||
use fabro_types::settings::{ProjectNamespace, WorkflowNamespace};
|
||||
use fabro_types::test_support::test_run_provenance;
|
||||
use fabro_types::{WorkflowSettings, fixtures};
|
||||
|
||||
fn sample_run_spec() -> RunSpec {
|
||||
|
|
@ -27,7 +28,7 @@ fn sample_run_spec() -> RunSpec {
|
|||
automation: None,
|
||||
source_directory: Some("/Users/client/project".to_string()),
|
||||
labels: HashMap::from([("team".to_string(), "platform".to_string())]),
|
||||
provenance: None,
|
||||
provenance: test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
git: Some(GitContext {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use fabro_types::graph::Graph;
|
|||
use fabro_types::run::{DirtyStatus, ForkSourceRef, GitContext, PreRunPushOutcome, RunSpec};
|
||||
use fabro_types::settings::InterpString;
|
||||
use fabro_types::settings::run::RunGoal;
|
||||
use fabro_types::test_support::test_run_provenance;
|
||||
use fabro_types::{AutomationRef, WorkflowSettings, fixtures};
|
||||
|
||||
fn templated_settings() -> WorkflowSettings {
|
||||
|
|
@ -27,7 +28,7 @@ fn run_spec_round_trips_templated_settings() {
|
|||
}),
|
||||
source_directory: Some("/Users/client/project".to_string()),
|
||||
labels: HashMap::from([("team".to_string(), "platform".to_string())]),
|
||||
provenance: None,
|
||||
provenance: test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
git: Some(GitContext {
|
||||
|
|
@ -80,7 +81,8 @@ fn run_spec_defaults_automation_for_legacy_specs() {
|
|||
"run_id": fixtures::RUN_1,
|
||||
"settings": WorkflowSettings::default(),
|
||||
"graph": Graph::new("ship"),
|
||||
"labels": {}
|
||||
"labels": {},
|
||||
"provenance": test_run_provenance()
|
||||
});
|
||||
|
||||
let record: RunSpec = serde_json::from_value(json).expect("legacy spec should deserialize");
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ mod tests {
|
|||
use fabro_model::{Catalog, ModelRef, ProviderId};
|
||||
use fabro_types::{
|
||||
AttrValue, BilledTokenCounts, Graph, Node, RunProjection, RunSpec, StageCompletion,
|
||||
StageOutcome, WorkflowSettings, first_event_seq, fixtures,
|
||||
StageOutcome, WorkflowSettings, first_event_seq, fixtures, test_support,
|
||||
};
|
||||
|
||||
use super::billing_rollup_from_projection;
|
||||
|
|
@ -353,7 +353,7 @@ mod tests {
|
|||
automation: None,
|
||||
source_directory: None,
|
||||
labels: HashMap::new(),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
git: None,
|
||||
|
|
|
|||
|
|
@ -2339,7 +2339,7 @@ mod tests {
|
|||
let provenance = RunProvenance {
|
||||
server: None,
|
||||
client: None,
|
||||
subject: Some(user_principal("alice")),
|
||||
subject: user_principal("alice"),
|
||||
};
|
||||
let automation = AutomationRef {
|
||||
id: "nightly".to_string(),
|
||||
|
|
@ -2348,25 +2348,25 @@ mod tests {
|
|||
};
|
||||
|
||||
let stored = to_run_event(&fixtures::RUN_1, &Event::RunCreated {
|
||||
run_id: fixtures::RUN_1,
|
||||
title: None,
|
||||
settings: serde_json::to_value(WorkflowSettings::default()).unwrap(),
|
||||
graph: serde_json::to_value(Graph::new("test")).unwrap(),
|
||||
workflow_source: None,
|
||||
workflow_config: None,
|
||||
labels: BTreeMap::default(),
|
||||
run_dir: "/tmp/run".to_string(),
|
||||
run_id: fixtures::RUN_1,
|
||||
title: None,
|
||||
settings: serde_json::to_value(WorkflowSettings::default()).unwrap(),
|
||||
graph: serde_json::to_value(Graph::new("test")).unwrap(),
|
||||
workflow_source: None,
|
||||
workflow_config: None,
|
||||
labels: BTreeMap::default(),
|
||||
run_dir: "/tmp/run".to_string(),
|
||||
source_directory: Some("/tmp/run".to_string()),
|
||||
workflow_slug: None,
|
||||
automation: Some(automation.clone()),
|
||||
db_prefix: None,
|
||||
provenance: Some(provenance),
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
workflow_slug: None,
|
||||
automation: Some(automation.clone()),
|
||||
db_prefix: None,
|
||||
provenance,
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
});
|
||||
let actor = stored.actor.as_ref().expect("actor set");
|
||||
assert_eq!(actor, &user_principal("alice"));
|
||||
|
|
|
|||
|
|
@ -41,8 +41,7 @@ pub enum Event {
|
|||
automation: Option<AutomationRef>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
db_prefix: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
provenance: Option<RunProvenance>,
|
||||
provenance: RunProvenance,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
manifest_blob: Option<RunBlobId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
|
|
|||
|
|
@ -213,6 +213,7 @@ mod tests {
|
|||
use std::sync::Arc;
|
||||
|
||||
use ::fabro_types::{Graph, RunNoticeLevel, WorkflowSettings, fixtures};
|
||||
use fabro_types::test_support;
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
|
||||
use super::*;
|
||||
|
|
@ -244,7 +245,7 @@ mod tests {
|
|||
workflow_slug: None,
|
||||
automation: None,
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ pub(super) fn stored_event_fields(event: &Event, scope: Option<&StageScope>) ->
|
|||
fn stored_event_fields_for_variant(event: &Event) -> StoredEventFields {
|
||||
match event {
|
||||
Event::RunCreated { provenance, .. } => StoredEventFields {
|
||||
actor: provenance.as_ref().and_then(|p| p.subject.clone()),
|
||||
actor: Some(provenance.subject.clone()),
|
||||
..StoredEventFields::default()
|
||||
},
|
||||
Event::RunCancelRequested { actor }
|
||||
|
|
|
|||
|
|
@ -343,7 +343,7 @@ mod tests {
|
|||
|
||||
use fabro_dump::RunDump;
|
||||
use fabro_store::Database;
|
||||
use fabro_types::{CommandTermination, StageModelUsage, fixtures};
|
||||
use fabro_types::{CommandTermination, StageModelUsage, fixtures, test_support};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
use super::*;
|
||||
|
|
@ -469,7 +469,7 @@ mod tests {
|
|||
workflow_slug: None,
|
||||
automation: None,
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
|
|
|
|||
|
|
@ -429,7 +429,7 @@ mod tests {
|
|||
use fabro_graphviz::graph::AttrValue;
|
||||
use fabro_model::{ReasoningEffort, Speed};
|
||||
use fabro_store::{Database, RunDatabase, StageId};
|
||||
use fabro_types::fixtures;
|
||||
use fabro_types::{fixtures, test_support};
|
||||
use object_store::memory::InMemory;
|
||||
use tempfile::TempDir;
|
||||
|
||||
|
|
@ -484,7 +484,7 @@ mod tests {
|
|||
workflow_slug: None,
|
||||
automation: None,
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ mod tests {
|
|||
use bytes::Bytes;
|
||||
use fabro_graphviz::graph::AttrValue;
|
||||
use fabro_store::{Database, RunDatabase, StageId};
|
||||
use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings, fixtures};
|
||||
use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings, fixtures, test_support};
|
||||
use object_store::memory::InMemory;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
|
|
@ -256,7 +256,7 @@ mod tests {
|
|||
automation: None,
|
||||
source_directory: None,
|
||||
labels: std::collections::HashMap::default(),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
git: None,
|
||||
|
|
@ -357,7 +357,7 @@ mod tests {
|
|||
workflow_slug: None,
|
||||
automation: None,
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
|
|
|
|||
|
|
@ -1600,6 +1600,7 @@ mod tests {
|
|||
use fabro_types::{
|
||||
EventEnvelope, FailureReason, Run, RunId, RunLifecycle, RunLinks, RunOrigin,
|
||||
RunPairStatusResponse, RunProjection, RunStatus, RunTimestamps, SuccessReason, WorkflowRef,
|
||||
test_support,
|
||||
};
|
||||
use fabro_vault::{SecretType, Vault};
|
||||
use futures::stream;
|
||||
|
|
@ -2133,7 +2134,7 @@ reasoning = false
|
|||
},
|
||||
automation: None,
|
||||
repository: None,
|
||||
created_by: None,
|
||||
created_by: test_support::test_principal(),
|
||||
origin: RunOrigin::default(),
|
||||
labels: HashMap::new(),
|
||||
lifecycle: RunLifecycle {
|
||||
|
|
|
|||
|
|
@ -692,7 +692,7 @@ mod tests {
|
|||
|
||||
use fabro_graphviz::graph::{AttrValue, Edge};
|
||||
use fabro_store::{Database, StageId};
|
||||
use fabro_types::fixtures;
|
||||
use fabro_types::{fixtures, test_support};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
use super::*;
|
||||
|
|
@ -728,7 +728,7 @@ mod tests {
|
|||
workflow_slug: None,
|
||||
automation: None,
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ mod tests {
|
|||
use fabro_graphviz::graph::AttrValue;
|
||||
use fabro_model::{ReasoningEffort, Speed};
|
||||
use fabro_store::{Database, RunDatabase, StageId};
|
||||
use fabro_types::fixtures;
|
||||
use fabro_types::{fixtures, test_support};
|
||||
use object_store::memory::InMemory;
|
||||
use tempfile::TempDir;
|
||||
|
||||
|
|
@ -283,7 +283,7 @@ mod tests {
|
|||
workflow_slug: None,
|
||||
automation: None,
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
|
|
|
|||
|
|
@ -598,7 +598,7 @@ mod tests {
|
|||
use fabro_model::Catalog;
|
||||
use fabro_store::{Database, EventEnvelope, RunDatabase, RunProjection};
|
||||
use fabro_types::run_event::{MetadataSnapshotFailureKind, MetadataSnapshotPhase};
|
||||
use fabro_types::{EventBody, RunBlobId, RunEvent, WorkflowSettings, fixtures};
|
||||
use fabro_types::{EventBody, RunBlobId, RunEvent, WorkflowSettings, fixtures, test_support};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
use super::*;
|
||||
|
|
@ -736,7 +736,7 @@ mod tests {
|
|||
workflow_slug: Some("metadata".to_string()),
|
||||
automation: None,
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
|
|
|
|||
|
|
@ -136,7 +136,9 @@ mod tests {
|
|||
use std::time::Duration;
|
||||
|
||||
use fabro_store::Database;
|
||||
use fabro_types::{FailureReason, RunId, SuccessReason, TerminalStatus, fixtures};
|
||||
use fabro_types::{
|
||||
FailureReason, RunId, SuccessReason, TerminalStatus, fixtures, test_support,
|
||||
};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
use super::*;
|
||||
|
|
@ -226,7 +228,7 @@ mod tests {
|
|||
workflow_slug: None,
|
||||
automation: None,
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ pub struct CreateRunInput {
|
|||
pub git: Option<GitContext>,
|
||||
pub fork_source_ref: Option<ForkSourceRef>,
|
||||
pub parent_id: Option<RunId>,
|
||||
pub provenance: Option<RunProvenance>,
|
||||
pub provenance: RunProvenance,
|
||||
pub configured_providers: Vec<ProviderId>,
|
||||
/// Public URL where this run can be viewed in the web UI, when the server
|
||||
/// has the web UI enabled. Recorded on the `run.created` event so attach
|
||||
|
|
@ -72,7 +72,7 @@ struct PersistCreateOptions {
|
|||
automation: Option<AutomationRef>,
|
||||
git: Option<GitContext>,
|
||||
fork_source_ref: Option<ForkSourceRef>,
|
||||
provenance: Option<RunProvenance>,
|
||||
provenance: RunProvenance,
|
||||
configured_providers: Vec<ProviderId>,
|
||||
catalog: Arc<Catalog>,
|
||||
}
|
||||
|
|
@ -422,7 +422,7 @@ mod tests {
|
|||
use fabro_store::Database;
|
||||
use fabro_types::settings::InterpString;
|
||||
use fabro_types::settings::run::RunMode;
|
||||
use fabro_types::{WorkflowSettings, fixtures};
|
||||
use fabro_types::{WorkflowSettings, fixtures, test_support};
|
||||
use fabro_util::error::collect_chain;
|
||||
use fabro_validate::Severity;
|
||||
use object_store::local::LocalFileSystem;
|
||||
|
|
@ -1115,7 +1115,7 @@ mod tests {
|
|||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
},
|
||||
|
|
@ -1183,7 +1183,7 @@ mod tests {
|
|||
}),
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
},
|
||||
|
|
@ -1295,7 +1295,7 @@ mod tests {
|
|||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
},
|
||||
|
|
@ -1341,7 +1341,7 @@ mod tests {
|
|||
}),
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
},
|
||||
|
|
@ -1414,7 +1414,7 @@ mod tests {
|
|||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
},
|
||||
|
|
@ -1467,7 +1467,7 @@ mod tests {
|
|||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
provenance: Some(fabro_types::RunProvenance {
|
||||
provenance: fabro_types::RunProvenance {
|
||||
server: Some(fabro_types::RunServerProvenance {
|
||||
version: "0.9.0".to_string(),
|
||||
}),
|
||||
|
|
@ -1476,12 +1476,12 @@ mod tests {
|
|||
name: Some("fabro-cli".to_string()),
|
||||
version: Some("0.9.0".to_string()),
|
||||
}),
|
||||
subject: Some(fabro_types::Principal::user(
|
||||
subject: fabro_types::Principal::user(
|
||||
fabro_types::IdpIdentity::new("https://github.com", "12345").unwrap(),
|
||||
"octocat".to_string(),
|
||||
fabro_types::AuthMethod::Github,
|
||||
)),
|
||||
}),
|
||||
),
|
||||
},
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
},
|
||||
|
|
@ -1494,7 +1494,7 @@ mod tests {
|
|||
let run_store = store.open_run_reader(&created.run_id).await.unwrap();
|
||||
let state = run_store.state().await.unwrap();
|
||||
let run = state.spec;
|
||||
let provenance = run.provenance.expect("provenance should be projected");
|
||||
let provenance = run.provenance;
|
||||
|
||||
assert_eq!(provenance.server.unwrap().version, "0.9.0");
|
||||
assert_eq!(
|
||||
|
|
@ -1502,7 +1502,7 @@ mod tests {
|
|||
Some("fabro-cli")
|
||||
);
|
||||
assert_eq!(
|
||||
provenance.subject.unwrap(),
|
||||
provenance.subject,
|
||||
fabro_types::Principal::user(
|
||||
fabro_types::IdpIdentity::new("https://github.com", "12345").unwrap(),
|
||||
"octocat".to_string(),
|
||||
|
|
|
|||
|
|
@ -284,7 +284,7 @@ mod tests {
|
|||
|
||||
use fabro_graphviz::graph::Graph;
|
||||
use fabro_store::{Database, RunProjectionReducer};
|
||||
use fabro_types::{StageId, WorkflowSettings, fixtures};
|
||||
use fabro_types::{StageId, WorkflowSettings, fixtures, test_support};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
use super::*;
|
||||
|
|
@ -383,7 +383,7 @@ mod tests {
|
|||
workflow_slug: Some("fork-source".to_string()),
|
||||
automation: None,
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
git: Some(fabro_types::GitContext {
|
||||
origin_url: "https://github.com/example/repo.git".to_string(),
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use crate::event::{self, Event};
|
|||
pub struct RetryRunInput {
|
||||
pub source_run_id: RunId,
|
||||
pub new_run_id: RunId,
|
||||
pub provenance: Option<RunProvenance>,
|
||||
pub provenance: RunProvenance,
|
||||
pub web_url: Option<String>,
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +122,7 @@ mod tests {
|
|||
use fabro_types::{
|
||||
AuthMethod, DirtyStatus, FailureReason, ForkSourceRef, GitContext, Graph, IdpIdentity,
|
||||
PreRunPushOutcome, Principal, PullRequestLink, RunBlobId, RunRunnableSource,
|
||||
RunServerProvenance, RunTiming, UserPrincipal, WorkflowSettings, fixtures,
|
||||
RunServerProvenance, RunTiming, WorkflowSettings, fixtures,
|
||||
};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
|
|
@ -138,12 +138,11 @@ mod tests {
|
|||
}
|
||||
|
||||
fn actor(login: &str) -> Principal {
|
||||
Principal::User(UserPrincipal {
|
||||
identity: IdpIdentity::new("github", format!("user:{login}")).unwrap(),
|
||||
login: login.to_string(),
|
||||
auth_method: AuthMethod::DevToken,
|
||||
avatar_url: None,
|
||||
})
|
||||
Principal::user(
|
||||
IdpIdentity::new("github", format!("user:{login}")).unwrap(),
|
||||
login.to_string(),
|
||||
AuthMethod::DevToken,
|
||||
)
|
||||
}
|
||||
|
||||
fn provenance(login: &str) -> RunProvenance {
|
||||
|
|
@ -152,7 +151,7 @@ mod tests {
|
|||
version: "test".to_string(),
|
||||
}),
|
||||
client: None,
|
||||
subject: Some(actor(login)),
|
||||
subject: actor(login),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -191,7 +190,7 @@ mod tests {
|
|||
workflow_slug: Some("retry-source".to_string()),
|
||||
automation: None,
|
||||
db_prefix: None,
|
||||
provenance: Some(provenance("source-user")),
|
||||
provenance: provenance("source-user"),
|
||||
manifest_blob,
|
||||
git: Some(git_context()),
|
||||
fork_source_ref,
|
||||
|
|
@ -368,7 +367,7 @@ mod tests {
|
|||
let outcome = retry_run(&store, &RetryRunInput {
|
||||
source_run_id,
|
||||
new_run_id: RunId::new(),
|
||||
provenance: Some(provenance("retry-user")),
|
||||
provenance: provenance("retry-user"),
|
||||
web_url: Some("http://localhost:3000/runs/retry".to_string()),
|
||||
})
|
||||
.await
|
||||
|
|
@ -402,14 +401,7 @@ mod tests {
|
|||
assert_eq!(retry_state.spec.manifest_blob, manifest_blob);
|
||||
assert_eq!(retry_state.spec.definition_blob, definition_blob);
|
||||
assert_eq!(retry_state.spec.fork_source_ref, Some(fork_source_ref));
|
||||
assert_eq!(
|
||||
retry_state
|
||||
.spec
|
||||
.provenance
|
||||
.as_ref()
|
||||
.and_then(|provenance| provenance.subject.as_ref()),
|
||||
Some(&actor("retry-user"))
|
||||
);
|
||||
assert_eq!(retry_state.spec.provenance.subject, actor("retry-user"));
|
||||
assert_eq!(
|
||||
retry_state.web_url.as_deref(),
|
||||
Some("http://localhost:3000/runs/retry")
|
||||
|
|
@ -463,7 +455,7 @@ mod tests {
|
|||
let outcome = retry_run(&store, &RetryRunInput {
|
||||
source_run_id,
|
||||
new_run_id: RunId::new(),
|
||||
provenance: Some(provenance("retry-user")),
|
||||
provenance: provenance("retry-user"),
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
@ -517,7 +509,7 @@ mod tests {
|
|||
let err = retry_run(&store, &RetryRunInput {
|
||||
source_run_id: run_id,
|
||||
new_run_id: RunId::new(),
|
||||
provenance: None,
|
||||
provenance: provenance("retry-user"),
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
@ -535,7 +527,7 @@ mod tests {
|
|||
let err = retry_run(&store, &RetryRunInput {
|
||||
source_run_id: fixtures::RUN_1,
|
||||
new_run_id: RunId::new(),
|
||||
provenance: None,
|
||||
provenance: provenance("retry-user"),
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -1128,7 +1128,9 @@ mod tests {
|
|||
use fabro_store::Database;
|
||||
use fabro_types::settings::run::RunMode;
|
||||
use fabro_types::settings::{InterpString, ModelRef};
|
||||
use fabro_types::{BilledModelUsage, ManifestPath, StageTiming, WorkflowSettings, fixtures};
|
||||
use fabro_types::{
|
||||
BilledModelUsage, ManifestPath, StageTiming, WorkflowSettings, fixtures, test_support,
|
||||
};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
use super::*;
|
||||
|
|
@ -1415,7 +1417,7 @@ reasoning = false
|
|||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
},
|
||||
|
|
@ -1837,7 +1839,7 @@ reasoning = false
|
|||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -204,6 +204,7 @@ mod tests {
|
|||
use chrono::Utc;
|
||||
use fabro_types::{
|
||||
Checkpoint, CheckpointRecord, Graph, RunDiff, RunSpec, WorkflowSettings, fixtures,
|
||||
test_support,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
|
@ -248,7 +249,7 @@ mod tests {
|
|||
automation: None,
|
||||
source_directory: None,
|
||||
labels: HashMap::new(),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
git: None,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ use fabro_interview::AutoApproveInterviewer;
|
|||
use fabro_sandbox::SandboxSpec;
|
||||
use fabro_store::Database;
|
||||
use fabro_types::settings::run::RunModelControls;
|
||||
use fabro_types::{Principal, RunId, SystemActorKind, WorkflowSettings, fixtures, format_blob_ref};
|
||||
use fabro_types::{
|
||||
Principal, RunId, SystemActorKind, WorkflowSettings, fixtures, format_blob_ref, test_support,
|
||||
};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
use super::*;
|
||||
|
|
@ -165,7 +167,7 @@ fn persisted_workflow(graph: Graph, source: String, run_dir: &Path, run_id: RunI
|
|||
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
|
||||
}),
|
||||
labels: HashMap::new(),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
|
|
@ -208,7 +210,7 @@ async fn seed_created_and_starting(
|
|||
workflow_slug: run_options.workflow_slug.clone(),
|
||||
automation: None,
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
git: run_options.pre_run_git.clone(),
|
||||
fork_source_ref: run_options.fork_source_ref.clone(),
|
||||
|
|
|
|||
|
|
@ -651,7 +651,7 @@ mod tests {
|
|||
use fabro_types::run_event::{MetadataSnapshotFailureKind, MetadataSnapshotPhase};
|
||||
use fabro_types::{
|
||||
BilledTokenCounts, EventBody, RunBlobId, RunEvent, RunId, RunSpec, StageCompletion,
|
||||
WorkflowSettings, first_event_seq, fixtures,
|
||||
WorkflowSettings, first_event_seq, fixtures, test_support,
|
||||
};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
|
|
@ -739,7 +739,7 @@ mod tests {
|
|||
workflow_slug: Some("metadata".to_string()),
|
||||
automation: None,
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
|
|
@ -856,7 +856,7 @@ mod tests {
|
|||
automation: None,
|
||||
source_directory: None,
|
||||
labels: HashMap::new(),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
git: None,
|
||||
|
|
|
|||
|
|
@ -648,7 +648,7 @@ mod tests {
|
|||
use fabro_sandbox::SandboxSpec;
|
||||
use fabro_store::Database;
|
||||
use fabro_types::settings::run::RunModelControls;
|
||||
use fabro_types::{EventBody, RunEvent, RunId, WorkflowSettings, fixtures};
|
||||
use fabro_types::{EventBody, RunEvent, RunId, WorkflowSettings, fixtures, test_support};
|
||||
use fabro_vault::{SecretType, Vault};
|
||||
use object_store::memory::InMemory;
|
||||
use tokio::fs::{create_dir_all, write};
|
||||
|
|
@ -773,7 +773,7 @@ mod tests {
|
|||
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
|
||||
}),
|
||||
labels: HashMap::new(),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ mod tests {
|
|||
|
||||
use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};
|
||||
use fabro_store::{Database, RunDatabase};
|
||||
use fabro_types::fixtures;
|
||||
use fabro_types::{fixtures, test_support};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
use super::*;
|
||||
|
|
@ -148,7 +148,7 @@ mod tests {
|
|||
("env".to_string(), "test".to_string()),
|
||||
("team".to_string(), "workflow".to_string()),
|
||||
]),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
|
|
|
|||
|
|
@ -680,7 +680,7 @@ mod tests {
|
|||
use fabro_store::Database;
|
||||
use fabro_types::{
|
||||
BilledTokenCounts, RunProjection, RunSpec, SuccessReason, WorkflowSettings,
|
||||
first_event_seq, fixtures,
|
||||
first_event_seq, fixtures, test_support,
|
||||
};
|
||||
use fabro_vault::{SecretType, Vault};
|
||||
use futures::stream;
|
||||
|
|
@ -823,7 +823,7 @@ mod tests {
|
|||
automation: None,
|
||||
source_directory: None,
|
||||
labels: HashMap::new(),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
git: None,
|
||||
|
|
@ -1148,7 +1148,7 @@ mod tests {
|
|||
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
|
||||
}),
|
||||
labels: HashMap::new(),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
|
|
@ -1219,7 +1219,7 @@ mod tests {
|
|||
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
|
||||
}),
|
||||
labels: HashMap::new(),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
|
|
@ -1575,7 +1575,7 @@ mod tests {
|
|||
source_directory: Some(tmp.path().display().to_string()),
|
||||
git: None,
|
||||
labels: std::collections::HashMap::new(),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
|
|
@ -1704,7 +1704,7 @@ mod tests {
|
|||
source_directory: Some("/tmp/project".to_string()),
|
||||
git: None,
|
||||
labels: HashMap::new(),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
|
|
@ -1722,7 +1722,7 @@ mod tests {
|
|||
workflow_slug: run_spec.workflow_slug.clone(),
|
||||
automation: None,
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
|
|
@ -1875,7 +1875,7 @@ mod tests {
|
|||
source_directory: None,
|
||||
git: None,
|
||||
labels: HashMap::new(),
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
|
|
@ -1893,7 +1893,7 @@ mod tests {
|
|||
workflow_slug: None,
|
||||
automation: None,
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
provenance: test_support::test_run_provenance(),
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue