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
6 changes: 4 additions & 2 deletions .github/workflows/publish-studio-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ on:
required: true
type: string
thin_bundles:
description: Publish provider-local thin bundles (requires server opt-in)
description: Publish Volcengine thin bundles (BytePlus remains full-only)
required: true
default: false
type: boolean
Expand Down Expand Up @@ -246,9 +246,11 @@ jobs:
matrix:
include:
- provider: volcengine
thin_bundles: ${{ inputs.thin_bundles }}
url_secret: STUDIO_RELEASE_SERVER_URL
key_secret: STUDIO_RELEASE_SERVER_API_KEY
- provider: byteplus
thin_bundles: false
url_secret: BYTEPLUS_STUDIO_RELEASE_SERVER_URL
key_secret: BYTEPLUS_STUDIO_RELEASE_SERVER_API_KEY
runs-on: ubuntu-latest
Expand All @@ -259,7 +261,7 @@ jobs:
RELEASE_SERVER_URL: ${{ secrets[matrix.url_secret] }}
RELEASE_SERVER_API_KEY: ${{ secrets[matrix.key_secret] }}
RELEASE_VERSION: ${{ needs.release-context.outputs.version }}
RELEASE_THIN_BUNDLES: ${{ inputs.thin_bundles }}
RELEASE_THIN_BUNDLES: ${{ matrix.thin_bundles }}

steps:
- name: Validate release server configuration
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/ui/SandboxAgentWorkspace.css
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@
background: hsl(var(--panel));
}

.sandbox-agent-workspace-surface [hidden] {
display: none;
}

.sandbox-agent-workspace-state {
display: grid;
height: 100%;
Expand Down
23 changes: 12 additions & 11 deletions frontend/src/ui/SandboxAgentWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,28 +108,29 @@ export function SandboxAgentWorkspace({
</header>

<div className="sandbox-agent-workspace-surface">
{surface === "main" ? (
<iframe
src={workspace.webuiUrl}
title={t("agentWorkspace.mainTitle", { agent: label })}
allow="clipboard-read; clipboard-write"
hidden={surface !== "main"}
/>
{terminalUrl ? (
<iframe
src={workspace.webuiUrl}
title={t("agentWorkspace.mainTitle", { agent: label })}
allow="clipboard-read; clipboard-write"
src={terminalUrl}
title={t("agentWorkspace.terminalTitle", { agent: label })}
hidden={surface !== "terminal"}
/>
) : terminalLoading ? (
) : surface === "terminal" && terminalLoading ? (
<div className="sandbox-agent-workspace-state" role="status">
{t("agentWorkspace.openingTerminal")}
</div>
) : terminalError ? (
) : surface === "terminal" && terminalError ? (
<div className="sandbox-agent-workspace-state is-error" role="alert">
<p>{terminalError}</p>
<button type="button" onClick={() => void openTerminal()}>
{t("common.tryAgain")}
</button>
</div>
) : terminalUrl ? (
<iframe
src={terminalUrl}
title={t("agentWorkspace.terminalTitle", { agent: label })}
/>
) : null}
</div>
</section>
Expand Down
4 changes: 4 additions & 0 deletions frontend/tests/sandboxSessionPresentation.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ test("sandbox agents expose detail deletion and reusable workspaces", () => {
assert.match(workspaceSource, /t\("agentWorkspace\.main"\)/);
assert.match(workspaceSource, /t\("agentWorkspace\.terminal"\)/);
assert.match(workspaceSource, /sandboxClient\.launchAgentTerminal/);
assert.match(workspaceSource, /hidden=\{surface !== "main"\}/);
assert.match(workspaceSource, /hidden=\{surface !== "terminal"\}/);
assert.doesNotMatch(workspaceSource, /surface === "main"\s*\?\s*\(\s*<iframe/);
assert.match(workspaceStyles, /\.sandbox-agent-workspace-surface \[hidden\][\s\S]*?display: none/);
assert.match(workspaceSource, /size="lg"/);
assert.match(workspaceSource, /gutterSize="lg"/);
assert.match(workspaceSource, /block/);
Expand Down
43 changes: 43 additions & 0 deletions tests/cli/test_frontend_agent_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,3 +444,46 @@ def _target(kind: str, session_id: str, token: str) -> SandboxProxyTarget:

assert response.status_code == 200
assert forwarded_headers[0]["x-hermes-session-token"] == "hermes-session-token"


def test_agent_surface_proxy_reuses_one_http_client(
monkeypatch: pytest.MonkeyPatch,
) -> None:
created = 0
requests = 0
closed = 0

class _Client:
def __init__(self, **_: object) -> None:
nonlocal created
created += 1

async def request(self, _method: str, _url: str, **_: object) -> httpx.Response:
nonlocal requests
requests += 1
return httpx.Response(
200,
content=b"ok",
headers={"content-type": "text/plain"},
)

async def aclose(self) -> None:
nonlocal closed
closed += 1

monkeypatch.setattr("veadk.cli.frontend_agent_proxy.httpx.AsyncClient", _Client)
app = FastAPI()
mount_agent_surface_proxy_routes(
app,
lambda *_args: SandboxProxyTarget(endpoint="https://sandbox.example/"),
)

with TestClient(app) as client:
first = client.get("/web/hermes/sessions/session-1/surface/token-1/api/first")
second = client.get("/web/hermes/sessions/session-1/surface/token-1/api/second")

assert first.status_code == 200
assert second.status_code == 200
assert created == 1
assert requests == 2
assert closed == 1
35 changes: 35 additions & 0 deletions tests/cli/test_studio_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,41 @@ def _urlopen(url: str, *, timeout: int) -> io.BytesIO:
]


def test_stage_byteplus_dependency_wheels_prefers_primary_pypi(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
content = b"downloaded-wheel"
dependency = StudioDependencyWheel(
filename="downloaded.whl",
url="https://files.pythonhosted.org/packages/example/downloaded.whl",
sha256=hashlib.sha256(content).hexdigest(),
)
urls: list[str] = []

def _urlopen(url: str, *, timeout: int) -> io.BytesIO:
urls.append(url)
assert timeout == 60
return io.BytesIO(content)

monkeypatch.setattr(
"veadk.cli.studio_dependencies.studio_dependency_wheels",
lambda _provider, **_kwargs: (dependency,),
)
monkeypatch.setattr(
"veadk.cli.studio_dependencies.urllib.request.urlopen",
_urlopen,
)

staged = stage_studio_dependency_wheels(
tmp_path / "destination",
provider="byteplus",
)

assert staged[0].read_bytes() == content
assert urls == [dependency.url]


def test_write_dependency_manifest_uses_pinned_wheel_metadata(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
Expand Down
98 changes: 97 additions & 1 deletion tests/cli/test_studio_sidecar_prerequisites.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@

import pytest

from veadk.cli.studio_package import write_studio_package
from veadk.cli.studio_package import (
build_local_studio_requirements,
write_studio_package,
)
from veadk.cli.studio_sidecar_prerequisites import (
DEFAULT_SIDECAR_BASE_IMAGE,
SIDECAR_BASE_IMAGE_ENV,
Expand Down Expand Up @@ -52,6 +55,99 @@ def test_write_studio_package_bootstraps_the_preloaded_cli_archive(
assert '--archive "$ROOT_DIR/agentkit-linux-x64.tar.gz"' in run_script


def test_write_studio_update_package_bootstraps_cli_from_remote_artifact(
tmp_path: Path,
) -> None:
package = tmp_path / "package"

write_studio_package(
package,
requirements="veadk-python\n",
site_logo=None,
provider="byteplus",
bundle_agentkit_cli=False,
)

run_script = (package / "run.sh").read_text(encoding="utf-8")
assert "studio_companion --provider byteplus" in run_script
assert "--archive" not in run_script
assert "--runtime-manifest" not in run_script
assert "export VEADK_STUDIO_AGENTKIT_CLI_ARCHIVE=" not in run_script
assert "export VEADK_STUDIO_AGENTKIT_CLI_RUNTIME_MANIFEST=" not in run_script


def test_local_studio_update_skips_full_offline_runtime(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
source_root = tmp_path / "source"
package = tmp_path / "package"
source_root.mkdir()
(source_root / "pyproject.toml").write_text("", encoding="utf-8")
(source_root / "uv.lock").write_text("", encoding="utf-8")
(source_root / "README.md").write_text("", encoding="utf-8")
(source_root / "LICENSE").write_text("", encoding="utf-8")
(source_root / "frontend").mkdir()
(source_root / "frontend" / "package.json").write_text("{}", encoding="utf-8")
(source_root / "frontend" / "package-lock.json").write_text("{}", encoding="utf-8")
(source_root / "veadk").mkdir()

monkeypatch.setattr(
"veadk.cli.studio_package._stage_wheel_source",
lambda _source, _assets, wheel_source: wheel_source.mkdir(parents=True),
)
monkeypatch.setattr(
"veadk.cli.studio_package.shutil.which", lambda _: "/usr/bin/uv"
)

def _build(command: list[str], *, check: bool) -> None:
assert check is True
output_dir = Path(command[-1])
(output_dir / "veadk_python-test-py3-none-any.whl").write_bytes(b"wheel")

monkeypatch.setattr("veadk.cli.studio_package.subprocess.run", _build)
monkeypatch.setattr(
"veadk.cli.studio_package.validate_studio_wheel", lambda *_args: None
)

def _stage_dependencies(destination: Path, **_: object) -> tuple[Path, ...]:
dependency = destination / "dependency-test-py3-none-any.whl"
dependency.write_bytes(b"dependency")
return (dependency,)

monkeypatch.setattr(
"veadk.cli.studio_package.stage_studio_dependency_wheels",
_stage_dependencies,
)
monkeypatch.setattr(
"veadk.cli.studio_package.stage_studio_dependency_sources",
lambda *_args, **_kwargs: pytest.fail("thin update staged source archives"),
)
monkeypatch.setattr(
"veadk.cli.studio_package.stage_studio_agentkit_cli_archive",
lambda *_args, **_kwargs: pytest.fail("thin update staged the CLI archive"),
)
monkeypatch.setattr(
"veadk.cli.studio_package.build_studio_offline_runtime",
lambda *_args, **_kwargs: pytest.fail("thin update built an offline runtime"),
)

requirements = build_local_studio_requirements(
source_root,
package,
provider="byteplus",
offline_runtime=False,
)

assert requirements == (
"./dependency-test-py3-none-any.whl\n./veadk_python-test-py3-none-any.whl\n"
)
assert sorted(path.name for path in package.iterdir()) == [
"dependency-test-py3-none-any.whl",
"veadk_python-test-py3-none-any.whl",
]


def test_project_keeps_native_cli_outside_python_distributions() -> None:
root = Path(__file__).resolve().parents[2]
project = (root / "pyproject.toml").read_text(encoding="utf-8")
Expand Down
18 changes: 17 additions & 1 deletion tests/cli/test_studio_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,9 +449,11 @@ def _build_requirements(
*,
frontend_assets: Path,
provider: str,
offline_runtime: bool,
) -> str:
captured["frontend"] = (frontend_assets / "index.html").read_text()
captured["requirements_provider"] = provider
captured["offline_runtime"] = offline_runtime
return "./veadk.whl\n"

def _write_package(
Expand All @@ -460,11 +462,13 @@ def _write_package(
requirements: str,
site_logo: SiteLogo | None,
provider: str = "volcengine",
bundle_agentkit_cli: bool = True,
) -> None:
package_dir.mkdir(parents=True, exist_ok=True)
(package_dir / "run.sh").write_text("run", encoding="utf-8")
captured["requirements"] = requirements
captured["logo"] = site_logo
captured["bundle_agentkit_cli"] = bundle_agentkit_cli

monkeypatch.setattr(
"veadk.cli.studio_package.build_frontend_assets", _build_frontend
Expand Down Expand Up @@ -508,8 +512,10 @@ def update_application_code_bundle(self, **kwargs: object) -> str:
assert result.exit_code == 0, result.output
assert captured["frontend"] == "built"
assert captured["requirements_provider"] == "volcengine"
assert captured["offline_runtime"] is False
assert captured["requirements"] == "./veadk.whl\n"
assert captured["logo"] == logo
assert captured["bundle_agentkit_cli"] is False
assert captured["scope"] == {
"access_key": "ak",
"secret_key": "sk",
Expand Down Expand Up @@ -745,8 +751,10 @@ def _build_requirements(
*,
frontend_assets: Path,
provider: str,
offline_runtime: bool,
) -> str:
captured["requirements_provider"] = provider
captured["offline_runtime"] = offline_runtime
package_dir.mkdir(parents=True, exist_ok=True)
return "./veadk.whl\n./pydantic.whl\n"

Expand All @@ -761,13 +769,18 @@ def _write_package(
requirements: str,
site_logo: SiteLogo | None,
provider: str,
bundle_agentkit_cli: bool,
) -> None:
from veadk.cli.studio_package import studio_run_script

captured["package_requirements"] = requirements
captured["package_logo"] = site_logo
captured["package_provider"] = provider
run_script = studio_run_script(provider=provider) # type: ignore[arg-type]
captured["bundle_agentkit_cli"] = bundle_agentkit_cli
run_script = studio_run_script(
provider=provider, # type: ignore[arg-type]
bundle_agentkit_cli=bundle_agentkit_cli,
)
captured["run_script"] = run_script
package_dir.mkdir(parents=True, exist_ok=True)
(package_dir / "run.sh").write_text(run_script, encoding="utf-8")
Expand Down Expand Up @@ -805,7 +818,9 @@ def update_application_code_bundle(self, **kwargs: object) -> str:

assert result.exit_code == 0, result.output
assert captured["requirements_provider"] == "byteplus"
assert captured["offline_runtime"] is False
assert captured["package_provider"] == "byteplus"
assert captured["bundle_agentkit_cli"] is False
assert captured["package_requirements"] == "./veadk.whl\n./pydantic.whl\n"
update = captured["update"]
assert isinstance(update, dict)
Expand Down Expand Up @@ -976,6 +991,7 @@ def _write_package(
requirements: str,
site_logo: SiteLogo | None,
provider: str = "volcengine",
bundle_agentkit_cli: bool = True,
) -> None:
package_dir.mkdir(parents=True, exist_ok=True)
captured["logo"] = site_logo
Expand Down
Loading
Loading