Skip to content

ENG-3683–3692: CI gates, secret scanning and functional-suite overhaul (combined) - #1163

Merged
aix-ahmet merged 45 commits into
developmentfrom
ENG-3683-3692-ci-and-functional-suite-overhaul
Oct 9, 2026
Merged

aix-ahmet merged 45 commits into
developmentfrom
ENG-3683-3692-ci-and-functional-suite-overhaul

Conversation

@elsheikhams99

@elsheikhams99 elsheikhams99 commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This PR combines ten open PRs into one branch based on development, then fixes the test bugs that the first live functional CI run exposed.

Each original PR is one commit, in dependency order. Its conflicts with the earlier ones are already resolved, so the commits can be reviewed one at a time:

Commit Ticket Original PR What it does
1 ENG-3686 #1139 Removes two committed API keys. Adds gitleaks: a pre-commit hook, a full-tree CI step with a checksum-pinned binary, and repo rules for this SDK's key formats. Adds a unit guard against key literals.
2 ENG-3685 #1140 Introduces tests/functional/_assets.py, one named registry of backend asset ids per environment. The assets fixture resolves each id lazily. A hygiene guard fails on any bare id anywhere else.
3 ENG-3684 #1138 Requires each CI leg to execute at least 80% of the tests it collects. Converts silent skips into failures. Failure helpers move to tests/functional/_helpers.py.
4 ENG-3683 #1137 Runs main.yaml on PRs, with a functional-scope gate and a single required functional-result check. Adds a nightly run on the test backend and a release gate that only counts production runs. Production secrets are expanded only for production runs.
5 ENG-3691 #1158 Every permanent skip must name a ticket (checked statically). Every marker must be registered. Deletes fixtures left orphaned by the v1 removal.
6 ENG-3692 #1157 Test hygiene. ruff-format now covers tests/. Tidies the pytest config. Deletes the leaked notebook.
7 ENG-3687 #1154 Adds functional coverage for the untested v2 resources: utility, file, debugger, eval, resource, issue, agent progress and skill.
8 ENG-3689 #1155 Adds v2 team-agent functional tests to replace the deleted v1 suite.
9 ENG-3688 #1156 Ports the v1 agent lifecycle functional tests to v2: drafts, onboarding, subcomponents, tool kinds and round-trips.
10 ENG-3690 #1162 Adds functional coverage of the agent run-time parameters.

Fixes from the first live CI run

Until now, no CI job had ever run the new functional tests. The run on #1162 surfaced these real bugs:

  • SDK bug: expectedOutput sent as an object. Both run paths, Agent.build_run_payload and ExecutionConfig.to_api_dict, sent a Pydantic class, instance or dict as an object. The backend rejects that with 400 executionParams.expectedOutput must be a string. Both paths now send a string, the same one save() persists, and unit tests cover each case.
  • resolve_multi_action_tool found nothing. It filtered on actions_available, which the backend leaves empty, so every tool was ruled out. It now checks each tool's actual actions, prefers Slack-backed tools, and falls back to any tool that has two or more actions. The snake-case round-trip tests use the same lookup.
  • Smaller test corrections:
    • In-use agent delete: assert the backend's err.agent_is_in_use code.
    • max_inspectors is not persisted by the backend, so the test no longer asserts it.
    • The run-time inspector test is now an acceptance check, because inspectors don't appear as steps.
    • Prompt and variable tests no longer mention a "secret", which made the model refuse.
    • The pagination test sorts the results, so its assertion no longer depends on result order.
    • Skill fixture names are unique.
  • CI concurrency: max-parallel: 4 on the functional matrix. The test tenant allows 20 concurrent agent runs, and several PRs running their full matrices at once hit err.agent_concurrency_limit_reached.

Review follow-ups

These are the changes made after the review comment on this PR:

  • Honest outcomes for backend gaps. Tests whose failure comes from the backend are now strict xfails, each naming the gap; the payload assertions still run first. The gaps:
    • run-time variables and prompt are not applied;
    • nested expected_output schemas lose their types;
    • a zero-duration budget hangs instead of blocking.
  • Model connection-type tests. These turned a development skip into an assert. They now xfail with re-pin instructions when the pinned model has no connection_type metadata. An earlier version of this description wrongly called that failure pre-existing; this PR caused it.
  • Test fixes:
    • inspector fields are asserted on the save payload, because the platform does not persist them;
    • the vision test uses an opaque image;
    • the duplicate debugger test is removed;
    • pages 0 and 1 of a sorted search must not overlap.
  • Issue tests never file against production. The issue leg is skipped on production runs.
  • One expectedOutput encoder is now shared by save, run and session runs. A Pydantic instance or dict is now saved as a JSON string. See CHANGELOG.
  • CI:
    • SLACK_TOKEN goes only to the legs that use it, and HF_TOKEN is no longer exported;
    • the job timeout exceeds the leg timeouts;
    • the release gate only counts runs that prove they ran against production.
  • Secret-scanning doc is now generic.
  • Dead allowlist entry and stale docs are cleaned up.
  • Recurring test triggers are created disabled.

Not fixed here (environment or platform)

  • Secrets to create: TEST_COMPOSIO_INTEGRATION_ID and TEST_CONNECTION_ID, each with a _PROD variant. The trigger leg fails without them. SLACK_NIGHTLY_WEBHOOK_URL is needed for the nightly alert.
  • Debugger meta-agent: returns 404 on the test backend.
  • Vision model: VISION_LLM is intermittently unavailable on the test backend.
  • Backend tickets to file:
    • the agent engine ignores run-time variables/prompt;
    • it does not resolve $ref/$defs in expectedOutput schemas;
    • the v2 agent create/update path does not save inspectorTargets/maxInspectors;
    • a zero-duration budget run hangs.
  • SDK bug, not fixed here: edits to team.agents are dropped on save (strict xfail; still needs a ticket).

Notes for reviewers

@elsheikhams99

Copy link
Copy Markdown
Collaborator Author

Verdict: request changes. The SDK change is correct, and CI went from 17 failing legs to 6 with no concurrency failures. But functional-result is still red, partly because of tests this PR adds or tightens. Two items also need resolving before merge: the issue tests file real production issues on every main push, and the secret-scanning doc publishes where the unrotated keys are.

Reviewed: PR #1163 at 7653cacf, 85 files, +8575/−5434. Production code changes are limited to aixplain/_compat.py, aixplain/v2/agent.py and aixplain/v2/session.py. CI evidence comes from run 37388335608.

Blocking

  1. Several red legs are caused by this PR, not the environment. For each one: what fails, why, and the fix.
    • test_model.py::test_sync_model_connection_type
      • Why: ENG-3684 turned the development skip ("Model does not expose connection_type metadata") into an assert, and the pinned sync-only model has no such metadata.
      • Fix: re-pin the model, or use a ticketed skip.
      • The PR body wrongly calls this pre-existing; correct it.
    • test_agent_lifecycle.py::test_agent_round_trips_execution_and_inspector_fields
      • Why: the platform's v2 create/update path does not save inspectorTargets or maxInspectors, so a fetched agent reports [].
      • Fix: assert both on build_save_payload(), as is already done for max_inspectors, and file a platform ticket.
    • test_agent_run_params.py::test_variables_are_substituted_into_instructions and ::test_prompt_override_reaches_model
      • Why: the SDK sends what v1 sent (query: {input, <vars>}; a top-level prompt). The new engine reads only query.input and never reads prompt/criteria, so this is a backend gap.
      • Fix: strict xfail citing a backend ticket, keeping the payload assertions.
    • test_team_agent.py::test_run_team_agent_with_expected_output
      • Why: the engine does not resolve $ref/$defs, so nested Pydantic schemas degrade to list[Any]. The model returned {"result": ["Bruno: 34…"]}.
      • Fix: strict xfail citing a backend ticket. Optional SDK hardening: inline $defs before sending.
    • test_agent_run_params.py::test_attachments_send_image_to_vision_model
      • Why: a fixture bug. RED_PNG_BYTES is a single 50%-transparent red pixel, so the model's "Coral" is correct.
      • Fix: use an opaque 16×16 red PNG.
    • test_agent_run_params.py::test_budget_max_duration_zero_blocks_run
      • Why: the SDK sends maxDurationSeconds: 0 correctly, but the run hangs for 300 s on the backend. The cause is unproven; see request 246-e5a2c5b5-….
      • Fix: strict xfail with a ticket, plus a short timeout=.
    • The debugger scenario is duplicated: test_agent_run_params.py::test_debug_returns_analysis repeats test_debugger.py, so one 404 turns two legs red. Delete the copy.
  2. Production side effect. tests/functional/v2/test_issue.py files two issues that can't be deleted into the production issue system on every push to main (the endpoint is /v1/issue, and the docstring says there's no delete API). It also files two per in-scope PR push and per nightly run on test. Don't file on production, for example by skipping or excluding the leg when IS_PROD.
  3. Secret disclosure.
    • docs/ci/secret-scanning.md publishes, in this public repo, each removed key's file, line, target backend and partial fingerprint, and states they're "not rotated". The keys are also still readable on development until this merges.
    • Rotate both keys and the Zapier token before merging, and move those details to the private ticket.

Should fix

  • Inconsistent expectedOutput encoding. build_run_payload doesn't encode a list (agent.py:2528), while session runs do. build_save_payload still stores a Pydantic instance as a dict (agent.py:2472-2474), which the new engine renders as a Python repr. Use one encoder (_expected_output_to_wire) for save, run and session, and add a CHANGELOG entry for the wire-format fix.
  • Swallowed exception. tests/functional/_helpers.py::resolve_action_source catches every exception from Integration.list_actions() and quietly falls back to a tool, so a broken listing endpoint goes unnoticed. Catch only "empty".
  • Pagination test can no longer see the suspected bug. test_resource.py now compares pages 1 and 2. CI showed pages 0 and 1 aliasing, which suggests a 1-indexed backend despite the SDK documenting 0-indexing. Under the stable sort, also assert that pages 0 and 1 are disjoint.
  • Leg timeouts can't fire. Six legs have timeout: 45, equal to the job's timeout-minutes: 45, so the job kill lands first and skips teardown. Raise the job timeout to about 55.
  • run_response_generation is contradictory. It's documented as ignored in test_agent_run_params.py, but test_team_agent.py still spends a billed, rerun-able team run on it. Drop that test.
  • Dead gitleaks allowlist. The login_response.json exact-value entry is dead now that ENG-3691 deleted the file, and docs/ci/secret-scanning.md still says "8 findings".
  • Stale docs and comments:
    • AGENTS.md points at the deleted tests/mock_responses/, and says "30-minute timeout each".
    • main.yaml:50 says "14 legs"; there are 24.
    • required-checks.md has an HF_TOKEN row that no test reads, and its list of deleted legs omits benchmark.
    • release.yaml says "private repository".
    • The test_team_agent.py xfail has no ticket key.
  • Least privilege. SLACK_TOKEN and HF_TOKEN are exported to all 24 legs, including same-repo PR runs that pip install unreviewed dependencies. Export them only to the legs that use them.
  • Release-gate tag/branch ambiguity. release.yaml matches head_branch == $tag, so a dispatch on a branch named like the tag (which runs on the test backend) would count as a production run. Require an IS_PROD-only job's success, or reject when refs/heads/$TAG exists.
  • Possible leaked trigger. test_trigger.py creates an enabled 2-hourly trigger. With cancel-in-progress on PRs, a cancelled leg can skip teardown and leak it. Create it disabled.

Backend / platform tickets to file (found by this PR's tests)

  1. The engine ignores extra query keys (run-time variables) and the top-level prompt/criteria. The SDK's v1-compatible contract is not honoured.
  2. The engine doesn't resolve $ref/$defs in an expectedOutput JSON schema, so nested Pydantic schemas lose their inner types.
  3. The platform's v2 agent create/update path doesn't save inspectorTargets/maxInspectors.
  4. A run with budget.maxDurationSeconds = 0 hangs IN_PROGRESS instead of returning BLOCKED.
  5. The Debugger meta-agent (696fdccad63e…) isn't deployed on the test backend, and the vision LLM (VISION_LLM) is unavailable there.
  6. SDK: team.agents edits are dropped on save (strict xfail in test_team_agent.py).

What's good

  • SDK fix: expectedOutput is now a string on both run paths, which removed the 400s. No double-encoding (a fetched string passes through unchanged), and stored config isn't mutated. Mutations of either path are caught by the new unit tests.
  • Concurrency: max-parallel: 4 took concurrency-limit hits from 152 to 0, and a unit test guards it.
  • Multi-action tool: the lookup now works; the real cause was the empty actions_available field. No test runs actions on the resolved tool, so there are no side effects.
  • CI security:
    • No script injection; every untrusted value goes through env:.
    • _PROD secrets are empty on PR, schedule and test runs, including when a secret is unset.
    • Least-privilege permissions.
    • The gitleaks download is checksum-pinned and matches upstream, and gitleaks finds no leaks in the tree.
  • Earlier reviews: every Blocking and Should-fix item from the ENG-3687–3692 reviews and the merge plan is resolved, or obsolete because the code was removed. Three are partly resolved: leg/job timeouts, the team.agents ticket key, and the issue tests, which now tag their issues but still file on production.
  • Mechanics:
    • Unit suite: 2720 passed. The only failures are 3 test_client.py tests, from the local urllib3 version.
    • 2991 tests collect, with and without keys.
    • Pinned ruff format is clean, and there are no new pyflakes findings.

Not verified

  • No functional runs were done locally. CI evidence is a single run.
  • Backend root causes were traced against source snapshots, which may differ slightly from what's deployed.
  • The first-round review notes for ENG-3683 to ENG-3686 were not on disk, so the miss-sweep checked those four PRs against the merge plan only.

@aix-ahmet

aix-ahmet commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Code review — PR #1163 (head 9616a2dc), two review passes merged

I reviewed the SDK changes, the CI workflows, the conftests, the CI guards, the asset registry and the new or changed functional tests. I also checked the failures in CI run 37402686371. The expectedOutput encoder change looks correct: state is re-recorded after save(), so storing a dict as a JSON string does not make is_modified misfire.

High

  1. The functional-scope path filter leaves out the shared harness files. .github/workflows/main.yaml:360
    tests/ci_guards.py (imported by tests/conftest.py), tests/cleanup_guards.py (behind resource_tracker) and pytest.ini (--strict-markers, the marker registry) are not in the filter. A PR that touches only these files gets run-functional=false, the matrix is skipped and functional-result passes green. A change that breaks every leg can merge without the gate checking it, which is the fail-open case the gate was added to prevent.

  2. LazyAssets caches failed resolutions for the whole session, transient errors included. tests/functional/_assets.py:357
    One 503, timeout or err.agent_concurrency_limit_reached on the first assets.DEFAULT_LLM read makes every later test in that worker fail with "did not resolve". The @flaky reruns and the timeout reruns replay the cached failure instead of retrying, so one blip turns a whole leg red. Only cache successes, or permanent failures such as a 404.

  3. test_deprecated_files_alias_uploads_and_reads uses vision_agent to read a plain .txt file. tests/functional/v2/test_agent_run_params.py:521
    On the latest run it failed with "model 6a2846019a0a2598b61e12b8 is currently unavailable", which turned agent-run-params red even though the files-alias path works. The module-scoped run_agent (default LLM) covers the same behaviour without the vision model.

  4. _expected_output_to_wire passes other types through unencoded. aixplain/v2/session.py:77
    Anything other than a Pydantic class or instance, a dict or a list goes out unchanged. That includes tuples, ints and bools. The CHANGELOG and the build_run_payload comment say the value is always sent as a string. So execution_params={'expected_output': ('name', 'age')} still gets the 400 executionParams.expectedOutput must be a string this PR set out to remove. Encode tuples like lists, and either str()/json.dumps the rest or raise.

Medium

  1. Any label triggers a full run. .github/workflows/main.yaml:24
    With cancel-in-progress, adding an unrelated label such as bug or needs-review cancels a matrix that is already running (about 25 legs at max-parallel 4) and restarts it. Gate labeled events on github.event.label.name == 'functional-tests'.

  2. multi_action_tool is function-scoped. tests/functional/v2/test_tool.py:22
    The bounded search (up to 5 Tool.search pages plus up to 25 list_actions probes) runs again for each of its three consumers. That is up to about 90 backend calls on a 30-minute leg with a shared rate limit. Resolve the id once at module or session scope and client.Tool.get(id) per test.

  3. _action_count swallows every exception and returns 0. tests/functional/_helpers.py:106
    A broken list_actions (an auth regression or a 500) shows up as "Test fixture missing … Connect Slack". That blames the tenant setup for an SDK or backend regression, and it contradicts resolve_action_source's rule that a broken listing should propagate.

  4. agent-lifecycle leg holds SLACK_TOKEN it never uses. .github/workflows/main.yaml:481
    Its only Slack consumer, test_agent_with_slack_action_tool, is permanently @pytest.mark.skip'd (BUG-1098). The workflow's own rule says "a leg that never uses the token should not hold it". Drop slack: true until the test is un-skipped.

High (second pass)

  1. The new lifecycle test assumes a nested schema the backend doesn't resolve. tests/functional/v2/test_agent_lifecycle.py:415
    test_agent_expected_output_run_returns_json uses the nested People/Person schema ($defs/$ref). This PR marks that same schema as a strict xfail in test_team_agent.py because the backend doesn't resolve it. If the model answers {"result": ["Ana: 31 ..."]}, row["name"] raises TypeError on all 3 flaky attempts and agent-lifecycle goes red. Mark it xfail too, or use a flat schema.

  2. Trigger fixtures now fail on secrets that don't exist yet. tests/functional/v2/test_trigger.py:54
    require_env fails the test when TEST_COMPOSIO_INTEGRATION_ID / TEST_CONNECTION_ID (and their _PROD variants) are missing, and the PR says they haven't been created. Once this merges, every in-scope PR, the nightly and every push to main gets a red functional (trigger) leg. release.yaml's functional-gate then reports MISSING functional (trigger) on every tag, so nothing can be released until the secrets exist. The debugger leg (404 on the test backend) does the same on PRs and the nightly. Create the secrets before merging, or skip until they exist.

  3. The gitleaks rule misses names with a hex-letter suffix. .gitleaks.toml:30
    The aixplain-platform-key rule allows only non-hex characters between the credential name and the key, and under (?i) that also excludes A-F. So TEAM_API_KEY_PROD="<64hex>", AIXPLAIN_API_KEY_DEV=<64hex>, SECRET_KEY = "<64hex>", api_key_test: <64hex> and {"x-aixplain-key": "<64hex>"} don't match (checked with Python re). These are the per-environment names this PR introduces, so a pasted prod key is caught only if gitleaks' generic rule happens to fire.

  4. save() now changes what Agent.get() returns, but the CHANGELOG lists it under "Fixed". aixplain/v2/agent.py:2473
    A dict or Pydantic-instance expected_output is now persisted as a JSON string. After save or fetch, agent.expected_output["color"] raises TypeError for existing user code. Either decode it on read so callers still get a dict, or move the entry under "Breaking".

Medium (second pass)

  1. Dicts and Pydantic instances are encoded differently. aixplain/v2/session.py:76
    Dicts and lists go through json.dumps with the default ensure_ascii=True; Pydantic instances go through model_dump_json, which keeps UTF-8. An Arabic {"الاسم": ...} dict sends "\u0627\u0644...", while the equivalent model sends readable text. Use ensure_ascii=False.

  2. The same small helpers are copied into several test files. test_agent_run_params.py:82 and others
    _unique, _name, _unique_name and _agent_ids are re-defined in several test files even though this PR adds tests/functional/_helpers.py. Move them there.

Low / nits

  1. The expectedOutput encoder lives in session.py but its main user is Agent. aixplain/v2/agent.py:2471, :2522
    Agent imports it lazily inside two methods. Define it once in agent.py, or in a small module both can import, so ownership is clear and the lazy imports go away.

  2. Missing Apache 2.0 license headers. tests/functional/_assets.py, tests/functional/_helpers.py and the new test modules have no header, which AGENTS.md requires for new source files.

Local unit run

On the PR head the unit suite gave 2745 passed and 1 failed: tests/unit/test_ci_guard_wiring.py::test_the_module_table_falls_back_to_reports_under_xdist, run from a worktree under /private/tmp on macOS. The module path came out absolute, so it looks environment-specific, but it may break the pre-commit unit-test hook for macOS developers. ruff format --check tests/ and ruff check aixplain/v2 pass.

Known CI failures already in the PR description

Debugger 404, missing trigger secrets and timeouts are already listed. The VISION_LLM failure is partly finding 3, a test-design problem.

Combined from PR #1139, including its review fixes and the conflict
resolutions against the PRs that come before it in this branch.
Combined from PR #1140, including its review fixes and the conflict
resolutions against the PRs that come before it in this branch.
…ed test

Combined from PR #1138, including its review fixes and the conflict
resolutions against the PRs that come before it in this branch.
Combined from PR #1137, including its review fixes and the conflict
resolutions against the PRs that come before it in this branch.
…, drop dead fixtures

Combined from PR #1158, including its review fixes and the conflict
resolutions against the PRs that come before it in this branch.
Combined from PR #1157, including its review fixes and the conflict
resolutions against the PRs that come before it in this branch.
Combined from PR #1154, including its review fixes and the conflict
resolutions against the PRs that come before it in this branch.
Combined from PR #1155, including its review fixes and the conflict
resolutions against the PRs that come before it in this branch.
Combined from PR #1156, including its review fixes and the conflict
resolutions against the PRs that come before it in this branch.
Combined from PR #1162, including its review fixes and the conflict
resolutions against the PRs that come before it in this branch.
build_run_payload turned a Pydantic class into model_json_schema() and an
instance into model_dump(), both dicts, so executionParams.expectedOutput
reached the backend as an object. The backend rejects that with
"400 executionParams.expectedOutput must be a string", which is how
test_agent_lifecycle.py::test_agent_expected_output_run_returns_json failed
in the first live CI run. Save already sent json.dumps(schema).

Run payloads now JSON-encode a class (the same schema string save persists)
and an instance, as they already did for a dict. Unit tests cover the class
passed through execution_params, the agent's own class, and an instance.
test_delete_agent_in_use_fails expected "cannot be deleted" in the error,
but the backend refuses the delete with the code err.agent_is_in_use
("Failed to delete resource: err.agent_is_in_use" in the first live CI
run). Assert on that code instead of prose the backend never sends.
test_agent_round_trips_execution_and_inspector_fields failed in the first
live CI run with "assert None == 2" on fetched.max_inspectors. The SDK
encodes and decodes the field under the same key (maxInspectors, checked
offline: build_save_payload sends it, from_dict reads it back), so the
None means the backend does not persist it. The test still saves with
max_inspectors=2, so the backend must accept it, and keeps asserting the
fields that do round-trip.
Both tests asked the model to repeat a "secret", and in the first live CI
run GPT-5.4 treated that as a request to leak hidden instructions:
test_prompt_override_reaches_model got "I can't share secrets or hidden
instructions", and test_variables_are_substituted_into_instructions got
the literal "word" back instead of the substituted value.

Each now asks for a random token ("Reply with exactly this code and
nothing else: <token>") and asserts the token appears. The variables test
keeps the placeholder in the instructions, where the SDK rewrites {{x}} to
{x} on save and the backend substitutes the query keys at run time (the
same contract v1's process_variables used), so the SDK path is unchanged.
test_run_time_inspector_runs expected a step for the run-time inspector,
but in the first live CI run the response's steps held only the agent's
own LLM step, so the response exposes no evidence the inspector ran.
Renamed to test_run_time_inspector_accepted per the file's *_accepted
convention: it asserts the inspectors list reaches the payload as given
and that the backend accepts the run. Drops the now-unused
_step_agent_label helper.
test_second_page_does_not_repeat_the_first compared pages 0 and 1 of an
unsorted page_size=1 model search, and in the first live CI run both
returned the same model (6ab3e2f8...). Without sort_by the SDK sends
sort: [{}], so the backend's order is not guaranteed across two calls,
and whether pageNumber counts from 0 or 1 is not something the test
should depend on.

The test now sorts by creation time and compares pages 1 and 2 (distinct
under either convention) with three records each, asserting the two id
sets are disjoint. The SDK sends pageNumber/pageSize exactly as v1 did,
so no SDK change.
The skill_dir fixture named each skill func-test-skill-<epoch seconds>.
Two skills created within the same second (consecutive tests, or a rerun)
share that name, and in the first live CI run the backend refused the
second with err.exist_name, erroring the setup of
TestAgentReflectsSkillUpdate::test_agent_run_reflects_updated_skill_content.
Append a uuid fragment, as the agent names in the same file already do.
elsheikhams99 and others added 28 commits October 9, 2026 15:37
In the first live CI run every caller of resolve_multi_action_tool failed
("Searched 100 tool(s) ... Slack-backed tools with actions available:
none"), and both TestInputActionFieldsRoundTrip tests failed on "Pinned
integration 686432941223092cb4294d3f reports no actions available".

The cause is actions_available, not the tenant: the backend leaves it
unset (None) on search results and integrations. The same run shows the
Slack integration with actions_available=None while
test_tool.py::test_tool_get_parameters passed on that integration's
non-empty list_actions(). Filtering on the field ruled out every tool.

resolve_multi_action_tool now skips only an explicit False and probes
candidates with list_actions(): Slack-backed tools first, then tools of
any other integration, then the rest, bounded to 5 pages and 25 probes.
The AIXPLAIN_TEST_MULTI_ACTION_TOOL_ID override is unchanged, and finding
nothing still fails, naming every probed tool and its action count.

The snake-case round-trip tests use the new resolve_action_source: the
pinned Slack integration when it lists actions, else the resolved
multi-action tool (same list_actions/list_inputs surface), failing loudly
when neither lists any.
The first live run of the functional matrix lost ~40 tests to
"429 err.agent_concurrency_limit_reached" (limit 20 for the test team):
every leg drives agents on the same tenant, several at -n 2, and all legs
started at once. Those failures say nothing about the SDK.

Add max-parallel: 4 to the functional job's strategy, with the reason
beside it, and a unit test in test_ci_matrix_coverage.py that fails if the
cap is dropped or raised above four. required-checks.md notes the cap next
to the concurrency group that bounds runs per PR.
ExecutionConfig.to_api_dict passed execution_params['expected_output'] through unchanged, so a session run with a Pydantic class, instance or dict sent a non-string executionParams.expectedOutput, which the backend rejects with "400 executionParams.expectedOutput must be a string". This is the same bug the previous commit fixes on the direct run path. The session path now encodes it the same way: a class as its JSON schema, an instance as its JSON, and a dict or list with json.dumps.
…ype metadata

ENG-3684 turned the old skip in test_sync_model_connection_type into an
assert, but the pinned sync-only model exposes no connection_type metadata
on the backend, so the model leg went red on a fact no SDK change can fix.

Both connection-type tests now call pytest.xfail when the metadata is
missing, naming the pinned asset and the AIXPLAIN_TEST_SYNC_ONLY_MODEL /
AIXPLAIN_TEST_ASYNC_ONLY_MODEL override. When the metadata is present the
real assertions run unchanged, so the tests start passing by themselves
once the asset carries it or is re-pinned.
expectedOutput was encoded three times, differently. build_run_payload
JSON-encoded a Pydantic class, instance or dict but sent a list raw;
ExecutionConfig.to_api_dict also encoded lists; build_save_payload turned
a class into its schema string but stored a Pydantic instance (and a dict)
as an object, which the agent engine reads back as a Python repr when a
run falls back to the stored value.

All three now call session._expected_output_to_wire (imported lazily in
agent.py, as resolve_attachments already is, to avoid an import cycle):
class -> json.dumps(model_json_schema()), instance -> model_dump_json(),
dict/list -> json.dumps, str/None unchanged. The run path keeps its rule
that an empty string under text/markdown is sent as None.

The save-side change is deliberate. The only test pinning a dict at save,
test_basemodel_instance_is_still_dumped_to_dict, preserved the original
v2 behaviour rather than a backend contract: the save endpoint already
stores the schema string for a class. It now asserts a JSON string, and
new tests cover a dict at save, save/run agreement, a list on the run
path and the text-format empty-string rule. CHANGELOG notes the
wire-format fix and that a fetched agent now carries the string.
test_agent_round_trips_execution_and_inspector_fields failed in CI with
"assert [] == ['output']": the platform's v2 create/update path does not
persist inspectorTargets or maxInspectors, and the read side defaults the
targets to []. That is a backend gap (ticket to be filed), not an SDK bug.

The test now asserts both fields on agent.build_save_payload() -- what the
SDK actually sends -- before saving, with a comment saying why and when to
move them back below the fetch. The output_format and expected_output
round-trip assertions are unchanged.
…lures

Five tests in test_agent_run_params.py were red in CI for reasons that are
not SDK bugs, and one for a fixture bug:

- variables and prompt: the SDK sends the v1-compatible contract (query
  {input, <vars>}; top-level prompt/criteria/identifier), but the agent
  engine reads only query.input. Each test is split into a payload test
  that must pass and an output test under xfail(strict=True,
  raises=AssertionError). A refused run still raises APIError and a
  non-success status calls pytest.fail, so only the output check can be
  the expected failure.
- budget maxDurationSeconds=0: the payload carries the cap, but the run
  hangs IN_PROGRESS on the backend. The payload is asserted first, the run
  gets timeout=60 instead of the 300 s default, and the test is a strict
  xfail on the SDK TimeoutError only.
- attachments: RED_PNG_BYTES was one pixel at 50% alpha, so "coral" was a
  correct answer. It is now a 16x16 8-bit RGB PNG with no alpha channel,
  generated with zlib/struct and decoded to confirm every pixel is
  (255, 0, 0).
- test_debug_returns_analysis duplicated test_debugger.py, so one missing
  meta-agent turned two legs red. Deleted; the module docstring now says
  where debug() is covered and how the xfails are structured.
…s $defs

test_run_team_agent_with_expected_output failed in CI with a pydantic
ValidationError: _People nests _Person, so its JSON schema reaches _Person
through $ref/$defs, which the agent engine does not resolve. The nested
type degrades to a list of anything and the model answered
{"result": ["Bruno: 34 ..."]}.

The test is now xfail(strict=True, raises=pydantic.ValidationError) with a
backend reason (ticket to be filed). The persistence checks, a failed run
and a missing delegation raise AssertionError, which the xfail does not
accept, so only the structured-output validation can be the expected
failure. The flaky mark is dropped: rerunning a known backend gap only
spends three billed team runs to reach the same xfail.
test_run_response_generation_accepted spent a billed, rerun-able team run
to show only that the backend accepts run_response_generation, while
test_agent_run_params.py documents that the agent runtime ignores the flag
(and its JSON-output test already passes it). Two files disagreeing about
the same parameter is worse than one saying it once.

Delete the test and replace its line in the module docstring with a note
pointing at test_agent_run_params.py.
tests/functional/v2/test_issue.py files real issues, and there is no API
to delete one. The issue leg ran on every push to main, against the
production backend, so each merge added two undeletable test reports to
the production issue system.

The issue leg now carries `prod_safe: false` in the matrix. "Run Tests"
receives the leg's path and that key through env: (no more `${{ }}` in
the script body) and, when IS_PROD is true and the key is false, prints a
::notice:: explaining the skip and exits 0 without running pytest. The
leg still runs on PRs, pushes to test and the nightly, against the test
backend.

On production runs `functional (issue)` therefore reports success
without having run, and release.yaml's gate counts it green. That is
deliberate (no production run can exercise it without the side effect)
and is stated in the workflow comment and docs/ci/required-checks.md.

New unit tests run the real step with a stub python: the issue leg runs
pytest when IS_PROD is false and skips with a notice when it is true,
every other leg still runs its exact pytest command on production, and
the step body holds no `${{ }}`. The expression evaluator learned
`matrix.*` lookups for this.
…_TOKEN

SLACK_TOKEN and HF_TOKEN were exported to every functional leg, including
same-repository PR runs, which pip install whatever dependency changes the
PR carries. Only three legs read SLACK_TOKEN (agent-lifecycle, model and
tool, through the slack_token fixture), and nothing under tests/functional
reads HF_TOKEN at all; aixplain/utils/config.py defines it but no SDK or
test path consumes it.

Those three legs now carry `slack: true` in the matrix, and the env step
expands the secret as `matrix.slack && secrets.SLACK_TOKEN || ''`, the same
pattern that gates the _PROD secrets, writing SLACK_TOKEN to GITHUB_ENV
only when it is non-empty. HF_TOKEN is no longer exported.

Unit tests: the `slack` set must equal the legs whose test file uses the
slack_token fixture, and every other leg renders SLACK_TOKEN as empty; the
real env step exports SLACK_TOKEN only when it has a value; nothing under
tests/functional reads HF_TOKEN and main.yaml does not export it. The
secrets table in docs/ci/required-checks.md says the same.
…ase gate

release.yaml's functional gate recognised a manual dispatch on the release
tag with `.event == "workflow_dispatch" and .head_branch == $tag`. A run's
head_branch is a bare name, so a dispatch on a *branch* named exactly like
the tag (which IS_PROD treats as a test-backend run) passed the filter,
and its green legs could satisfy a production release.

main.yaml gains a trivial `production-backend` job whose `if:` is the
functional job's IS_PROD expression (repeated, since a job-level `if:`
cannot read another job's env). The gate now reads each candidate run's
jobs through a jq `jobs_filter` that yields the run's successful job
names only when `production-backend` is among them; a run without it is
logged as a warning and not counted. Checking the real ref inside the run
is robust where a "does refs/heads/$TAG exist now" lookup is not: a branch
deleted after its dispatch would slip past that.

Unit tests: the job's condition matches IS_PROD for every event/ref case,
including a dispatch on a branch named `v0.3.1`; the jobs filter, run on
paginated fixture JSON, drops a run whose production-backend job was
skipped and keeps one where it succeeded on a later page; the gate script
still wires the filter in; the required-checks doc names the new job,
which is listed as not to be required (it is skipped on every PR).
Six legs have `timeout: 45`, which equalled the functional job's
`timeout-minutes: 45`. When a leg hung, the job-level kill landed first,
so the leg showed as cancelled and its teardown never ran -- the per-leg
timeout could never do its job.

Raise the job timeout to 55, above the longest leg timeout, with the
reason beside it, and add a unit test in test_ci_matrix_coverage.py that
fails if the job timeout is not above every leg's.
- AGENTS.md pointed at tests/mock_responses/, which ENG-3691 deleted, and
  said every leg has a 30-minute timeout; legs declare 30 or 45 minutes.
- main.yaml's concurrency comment (and the matching unit-test docstring)
  said the matrix is 14 legs; it is one leg per functional test file, so
  say that instead of a count that rots.
- docs/ci/required-checks.md left `benchmark` out of the deleted v1 legs.
- release.yaml (and a unit-test message) justified `actions: read` by the
  repository being private; it is public. The scope is needed because the
  workflow's deny-all default grants nothing.
docs/ci/secret-scanning.md is published in a public repository, and it
listed each removed credential's file and line, the backend it worked
against, a partial fingerprint of each key and the path of a third token,
alongside the statement that none had been rotated. That is a map for
anyone looking for live credentials in history.

The doc now says only what a reader of this repository needs: committed
credentials were removed in ENG-3686, they remain in history and must be
rotated, rotation is urgent and independent of any PR, and the specifics
are tracked privately in the ENG-3686 ticket. The scanner design, the
repo rules, the gitleaks-vs-detect-secrets rationale and the allowlist
policy are unchanged; the open items are kept, without identifiers.

The same details are removed from the test_secret_scanning.py module
docstring, the .gitleaks.toml header comments, the test_eval.py docstring
(which tied a deleted script to its key and backend) and a
test_agent_lifecycle.py docstring that named the provider of a leaked
MCP URL.
.gitleaks.toml exempted, by exact value, the recorded ECR password in
tests/mock_responses/login_response.json. ENG-3691 deleted that file, so
the entry matches nothing, and the allowlist policy is to remove a dead
exemption: it can only hide something later.

gitleaks dir . with the pinned config still reports no leaks; with the
value allowlists removed it reports only the documentation placeholders
and the hand-written fake keys in unit tests, which is what the updated
docs/ci/secret-scanning.md describes in place of the old "8 findings".
resolve_action_source caught every exception from the pinned
integration's list_actions() and treated it as "no actions", then fell
back to a multi-action tool. A broken listing endpoint therefore went
unnoticed: the snake-case round-trip tests passed on the fallback tool
while the integration path they are meant to cover was down.

Only an empty listing falls back now; an exception propagates and fails
the test with the real error. The docstring says why.
test_second_page_does_not_repeat_the_first compared pages 1 and 2 only,
so it could no longer see the bug it was written around: in the first
live CI run pages 0 and 1 returned the same model. The SDK documents
page_number as 0-based, and a backend that counts from 1 and clamps 0 up
to 1 would alias exactly those two pages.

Under the stable CREATED_AT sort the test now also fetches page 0 and
asserts it is full and shares nothing with page 1, with a message naming
the 1-indexing suspicion. The 1-vs-2 check stays first, so a failure
tells the two problems apart.
test_trigger.py created an enabled trigger every two hours (plus daily,
weekly and monthly ones) and relied on teardown to delete them. PR runs
use cancel-in-progress, so a newer push can cancel a leg before teardown
runs, leaving an active recurring trigger that keeps running its agent on
the shared test backend.

The recurring triggers are now created with enabled=False. What those
tests check -- the schedule_type mapping and notifications -- does not
depend on the trigger being active, and enable/disable is still covered
by test_enable_disable_via_save on a far-future one-off trigger. The
two-hourly test also asserts the fetched trigger is stored disabled, so
a backend that ignored the flag on create would show up rather than
quietly leak an active schedule.
Tuples, numbers and bools went out unencoded and still drew the 400; dicts were
ASCII-escaped while Pydantic instances kept UTF-8. The encoder now lives in
aixplain/v2/_expected_output.py so Agent no longer imports it lazily from
session.py, and the read-back change is listed under Breaking.
One 503, timeout or 429 on the first read of an asset was cached for the whole
session, so every later test in the worker, @flaky reruns included, replayed it.
Successes and definite failures (a 4xx other than 408/409/425/429, an explicit
retryable=False, an undeserializable response, a client that cannot be built)
are still cached; anything else is asked again on the next read.
…Slack leg

- The path filter now covers every module directly under tests/ and pytest.ini,
  so a PR touching only ci_guards.py, cleanup_guards.py or the marker registry
  no longer skips the matrix and passes functional-result green.
- An unrelated label no longer cancels a running matrix: its run queues in the
  PR's group instead. A push or the functional-tests label still cancels.
- agent-lifecycle no longer holds SLACK_TOKEN: its only Slack test is skipped
  (BUG-1098). The guard now ignores unconditionally skipped tests, so it fails
  when that skip is removed without restoring slack: true.
The separator between the credential name and the key could not contain a-f,
so TEAM_API_KEY_PROD="<key>", AIXPLAIN_API_KEY_DEV=<key>, SECRET_KEY = "<key>",
api_key_test: <key> and {"x-aixplain-key": "<key>"} all went unreported. The
rule now needs only a non-hex character right before the key, and aixplain-key
is a credential name. Checked with gitleaks v8.28.0: all five shapes are caught,
a bare sha256 digest is not, and the tree scan stays clean.
…ing listing errors

multi_action_tool re-ran the bounded search (up to 5 Tool.search pages and 25
list_actions probes) for each of its consumers; the id is now resolved once per
module and each test gets a fresh Tool.get. _action_count no longer turns every
exception into 0: a pinned tool's listing error propagates, and a search
candidate is passed over only when the backend refuses that tool with a 4xx.
- test_deprecated_files_alias_uploads_and_reads reads a .txt file, so it runs on
  the default LLM instead of VISION_LLM, whose outages turned the leg red.
- test_agent_expected_output_run_returns_json uses a flat schema: the engine
  does not resolve $ref/$defs (the strict xfail in test_team_agent.py), so the
  nested People schema could fail the leg on all three flaky attempts.
…ers.py

_unique, _name, _unique_name and _agent_ids were redefined in five test files.
They are now unique_name (uuid-suffixed, safe under xdist and reruns) and
agent_ids (id strings, dicts or Agent objects) in the shared helpers module.
AGENTS.md requires it on new source files; _assets.py, _helpers.py and the ten
test modules this PR adds had none. Appended to each module docstring, as in
aixplain/v2/file.py.
…ecrets

TEST_COMPOSIO_INTEGRATION_ID / TEST_CONNECTION_ID (and their _PROD variants)
were never created, so the trigger leg was red on every run and release.yaml
could not release. Neither is a credential:

- the integration is fetched by its marketplace path, composio/gmail, which
  resolves on every backend;
- the connection comes from resolve_trigger_connection, a bounded search for a
  connected tool that lists event-trigger types (Gmail-backed tools first),
  pinnable with AIXPLAIN_TEST_TRIGGER_CONNECTION_ID.

The workflow no longer reads the four secrets, and a unit guard keeps them out.
@aix-ahmet
aix-ahmet force-pushed the ENG-3683-3692-ci-and-functional-suite-overhaul branch from fc6abf8 to 2eed62c Compare October 9, 2026 12:45
@aix-ahmet
aix-ahmet merged commit ef8b34d into development Oct 9, 2026
22 of 29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants