Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/desktop-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ jobs:
SWIFT_BUNDLE_ID_INPUT: ${{ github.event.inputs.swift_bundle_id }}
MATRIX_TARGET: ${{ matrix.target }}
MATRIX_PROFILE: ${{ matrix.profile }}
UIQ_DESKTOP_AUTOMATION_MODE: operator-manual
UIQ_DESKTOP_AUTOMATION_REASON: github-workflow-desktop-smoke
steps:
- uses: ./.github/actions/repo-checkout
- uses: ./.github/actions/setup-deps
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,8 @@ jobs:
env:
UIQ_TAURI_APP_PATH: ${{ vars.UIQ_TAURI_APP_PATH || secrets.UIQ_TAURI_APP_PATH || '' }}
UIQ_SWIFT_BUNDLE_ID: ${{ vars.UIQ_SWIFT_BUNDLE_ID || secrets.UIQ_SWIFT_BUNDLE_ID || '' }}
UIQ_DESKTOP_AUTOMATION_MODE: operator-manual
UIQ_DESKTOP_AUTOMATION_REASON: github-workflow-nightly-desktop-regression
steps:
- uses: ./.github/actions/repo-checkout
- uses: ./.github/actions/setup-deps
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/weekly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ jobs:
env:
UIQ_TAURI_APP_PATH: ${{ vars.UIQ_TAURI_APP_PATH || secrets.UIQ_TAURI_APP_PATH || '' }}
UIQ_SWIFT_BUNDLE_ID: ${{ vars.UIQ_SWIFT_BUNDLE_ID || secrets.UIQ_SWIFT_BUNDLE_ID || '' }}
UIQ_DESKTOP_AUTOMATION_MODE: operator-manual
UIQ_DESKTOP_AUTOMATION_REASON: github-workflow-weekly-desktop-regression
steps:
- uses: ./.github/actions/repo-checkout
- uses: ./.github/actions/setup-deps
Expand Down
133 changes: 133 additions & 0 deletions .secrets.baseline
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
{
"version": "1.5.0",
"plugins_used": [
{
"name": "ArtifactoryDetector"
},
{
"name": "AWSKeyDetector"
},
{
"name": "AzureStorageKeyDetector"
},
{
"name": "Base64HighEntropyString",
"limit": 4.5
},
{
"name": "BasicAuthDetector"
},
{
"name": "CloudantDetector"
},
{
"name": "DiscordBotTokenDetector"
},
{
"name": "GitHubTokenDetector"
},
{
"name": "GitLabTokenDetector"
},
{
"name": "HexHighEntropyString",
"limit": 3.0
},
{
"name": "IbmCloudIamDetector"
},
{
"name": "IbmCosHmacDetector"
},
{
"name": "IPPublicDetector"
},
{
"name": "JwtTokenDetector"
},
{
"name": "KeywordDetector",
"keyword_exclude": ""
},
{
"name": "MailchimpDetector"
},
{
"name": "NpmDetector"
},
{
"name": "OpenAIDetector"
},
{
"name": "PrivateKeyDetector"
},
{
"name": "PypiTokenDetector"
},
{
"name": "SendGridDetector"
},
{
"name": "SlackDetector"
},
{
"name": "SoftlayerDetector"
},
{
"name": "SquareOAuthDetector"
},
{
"name": "StripeDetector"
},
{
"name": "TelegramBotTokenDetector"
},
{
"name": "TwilioKeyDetector"
}
],
"filters_used": [
{
"path": "detect_secrets.filters.allowlist.is_line_allowlisted"
},
{
"path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies",
"min_level": 2
},
{
"path": "detect_secrets.filters.heuristic.is_indirect_reference"
},
{
"path": "detect_secrets.filters.heuristic.is_likely_id_string"
},
{
"path": "detect_secrets.filters.heuristic.is_lock_file"
},
{
"path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string"
},
{
"path": "detect_secrets.filters.heuristic.is_potential_uuid"
},
{
"path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign"
},
{
"path": "detect_secrets.filters.heuristic.is_sequential_string"
},
{
"path": "detect_secrets.filters.heuristic.is_swagger_file"
},
{
"path": "detect_secrets.filters.heuristic.is_templated_secret"
},
{
"path": "detect_secrets.filters.regex.should_exclude_file",
"pattern": [
"(^|/)\\.codex/|(^|/)docs/|(^|/)artifacts/|(^|/)(tests?|__tests__|fixtures?)/|(^|/)(pnpm-lock\\.yaml|package-lock\\.json|yarn\\.lock|uv\\.lock)$|(^|/)README\\.md$|(^|/)\\.env\\.example$|(^|/)docker-compose\\.ya?ml$|(^|/)configs/env/contract\\.yaml$|(^|/)apps/api/app/core/settings\\.py$|(^|/)scripts/|(^|/)apps/automation-runner/"
]
}
],
"results": {},
"generated_at": "2026-04-07T08:15:08Z"
}
40 changes: 35 additions & 5 deletions apps/api/app/services/automation_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
import os
import re
import signal
import subprocess
import logging
import random
Expand Down Expand Up @@ -584,17 +585,42 @@ def _terminate_process(
self, process: subprocess.Popen[str], timeout_seconds: float = 3.0
) -> bool:
"""Try terminate first, then force kill the direct child if it does not exit."""
poll = getattr(process, "poll", None)
if callable(poll) and poll() is not None:
return False
pid = self._owned_child_pid(process)
if pid is None:
logger.warning(
"refused to signal process without owned positive pid",
extra={"pid": process.pid},
"missing owned positive pid; falling back to direct child termination",
extra={"pid": getattr(process, "pid", None)},
)
return False
if process.poll() is not None:
return False
try:
process.terminate()
process.wait(timeout=timeout_seconds)
return False
except subprocess.TimeoutExpired:
try:
process.kill()
try:
process.wait(timeout=timeout_seconds)
except subprocess.TimeoutExpired:
logger.warning(
"process did not exit after kill",
extra={"error": "kill timeout"},
)
return True
except OSError as error:
logger.warning(
"failed to kill direct child process",
extra={"error": str(error), "pid": getattr(process, "pid", None)},
)
return False
try:
try:
os.killpg(pid, signal.SIGTERM)
except ProcessLookupError:
return False
except OSError:
process.terminate()
except ProcessLookupError:
return False
Expand All @@ -608,6 +634,10 @@ def _terminate_process(
return False
except subprocess.TimeoutExpired:
try:
os.killpg(pid, signal.SIGKILL)
except ProcessLookupError:
return False
except OSError:
process.kill()
except ProcessLookupError:
return False
Expand Down
11 changes: 8 additions & 3 deletions apps/api/tests/test_backend_coverage_final_sprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,16 +220,21 @@ def test_automation_retry_terminate_and_sync_edge_branches(
class TimeoutProcess:
pid = None

def __init__(self) -> None:
self.terminated = False

def terminate(self) -> None:
return None
self.terminated = True

def kill(self) -> None:
return None
self.terminated = True

def wait(self, timeout: float | None = None) -> int:
raise subprocess.TimeoutExpired("cmd", timeout)

assert automation_service._terminate_process(TimeoutProcess(), timeout_seconds=0.01) is True
process = TimeoutProcess()
assert automation_service._terminate_process(process, timeout_seconds=0.01) is True
assert process.terminated is True
assert warnings

now = datetime.now(timezone.utc)
Expand Down
10 changes: 9 additions & 1 deletion apps/api/tests/test_universal_platform_resume_branches.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def __init__(self, run: RunRecord | None) -> None:
self._lock = RLock()
self.audits: list[tuple[str, str | None, dict[str, object]]] = []
self.last_env: dict[str, str] | None = None
self.materialized_flow_ids: list[str] = []

def get_run(self, run_id: str, requester: str | None = None) -> RunRecord:
if self._run is None or self._run.run_id != run_id:
Expand All @@ -55,7 +56,11 @@ def get_template(self, template_id: str, requester: str | None = None) -> Simple

def get_flow(self, flow_id: str, requester: str | None = None) -> SimpleNamespace:
_ = requester
return SimpleNamespace(flow_id=flow_id, start_url="https://example.com/register")
return SimpleNamespace(
flow_id=flow_id,
session_id="session_1",
start_url="https://example.com/register",
)

def _get_validated_params_snapshot(self, run_id: str) -> dict[str, str]:
_ = run_id
Expand All @@ -64,6 +69,9 @@ def _get_validated_params_snapshot(self, run_id: str) -> dict[str, str]:
def _validate_params(self, template: object, params: object, otp_policy: object) -> None:
_ = (template, params, otp_policy)

def _materialize_replay_bridge(self, flow: SimpleNamespace) -> None:
self.materialized_flow_ids.append(flow.flow_id)

def _build_env(self, start_url: str, params: dict[str, str], otp_value: str) -> dict[str, str]:
env = {"START_URL": start_url, "EMAIL": params["email"], "OTP": otp_value}
self.last_env = env
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ test("ai-input-pack trims transcript/events/html and summarizes HAR entries", ()
transcript: [
{ t: "0", text: " first line " },
{ t: "1", text: "" },
{ t: "2", text: "second line that is very long" },
{ t: "2", text: "later line that is very long" },
],
events: [
{
Expand Down Expand Up @@ -372,7 +372,7 @@ test("ai-input-pack trims transcript/events/html and summarizes HAR entries", ()

assert.equal(packed.payload.transcript.length, 2)
assert.equal(packed.payload.transcript[0]?.text, "first line")
assert.equal(packed.payload.transcript[1]?.text, "secon")
assert.equal(packed.payload.transcript[1]?.text, "later")
assert.equal(packed.payload.eventLog.length, 1)
assert.equal(packed.payload.eventLog[0]?.value, "value")
assert.equal(packed.payload.htmlSnippet, "<div>abc")
Expand Down
33 changes: 26 additions & 7 deletions apps/web/tests/e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ pwTest.beforeEach(async ({ page }) => {
})
return
}
throw new Error(`[frontend-smoke] Unhandled API route: ${route.request().method()} ${url.pathname}`)
throw new Error(
`[frontend-smoke] Unhandled API route: ${route.request().method()} ${url.pathname}`
)
})
await page.route("**/health/**", async (route) => {
const url = new URL(route.request().url())
Expand All @@ -111,7 +113,13 @@ pwTest.beforeEach(async ({ page }) => {
body: JSON.stringify({
uptime_seconds: 120,
task_total: 0,
task_counts: { queued: 0, running: 0, success: 0, failed: 0, cancelled: 0 },
task_counts: {
queued: 0,
running: 0,
success: 0,
failed: 0,
cancelled: 0,
},
metrics: { requests_total: 1, rate_limited: 0 },
}),
})
Expand All @@ -131,7 +139,9 @@ pwTest.beforeEach(async ({ page }) => {
})
return
}
throw new Error(`[frontend-smoke] Unhandled health route: ${route.request().method()} ${url.pathname}`)
throw new Error(
`[frontend-smoke] Unhandled health route: ${route.request().method()} ${url.pathname}`
)
})
await page.addInitScript(() => {
window.localStorage.setItem("ab_onboarding_done", "1")
Expand All @@ -142,9 +152,16 @@ pwTest.beforeEach(async ({ page }) => {
pwTest("@smoke frontend shell and primary navigation", async ({ page }) => {
await page.goto("/")

const primaryNavigation = page.getByRole("navigation", {
name: "Primary navigation",
})
await expect(page.getByRole("heading", { level: 1, name: "ProofTrail" })).toBeVisible()
await expect(page.getByRole("tablist", { name: "Primary navigation" })).toBeVisible()
await expect(page.getByRole("tab", { name: "Quick Launch" })).toHaveAttribute("aria-selected", "true")
await expect(primaryNavigation).toBeVisible()
await expect(primaryNavigation.getByRole("tablist")).toBeVisible()
await expect(page.getByRole("tab", { name: "Quick Launch" })).toHaveAttribute(
"aria-selected",
"true"
)
await expect(page.getByRole("tablist", { name: "Command categories" })).toBeVisible()
const sidebarToggle = page.getByLabel("Collapse parameter rail")
await expect(sidebarToggle).toHaveAttribute("aria-expanded", "true")
Expand All @@ -160,7 +177,7 @@ pwTest("@smoke frontend shell and primary navigation", async ({ page }) => {
await taskCenterTab.click()
await expect(taskCenterTab).toHaveAttribute("aria-selected", "true")
await expect(taskCenterTab).toHaveAttribute("aria-controls", "app-view-tasks-panel")
const taskCenterPanel = page.locator('section#app-view-tasks-panel')
const taskCenterPanel = page.locator("section#app-view-tasks-panel")
await expect(taskCenterPanel).toHaveAttribute("role", "tabpanel")
await expect(taskCenterPanel).toHaveAttribute("aria-labelledby", "console-tab-tasks")

Expand All @@ -181,6 +198,8 @@ pwTest("@smoke frontend shell and primary navigation", async ({ page }) => {
await expect(flowDraftTab).toHaveAttribute("aria-selected", "true")
await expect(page.getByRole("heading", { name: "Key outcome and next action" })).toBeVisible()
await expect(
page.getByText("Advanced workshop (optional): system diagnostics, flow editing, and debugging evidence")
page.getByText(
"Advanced workshop (optional): system diagnostics, flow editing, and debugging evidence"
)
).toBeVisible()
})
Loading
Loading