Compare commits

...
Author SHA1 Message Date
gigglewang0417 b8ab27c5ee test(cli/e2e): add member management E2E suite (get/create/delete/set member)
28 tests covering all four member commands:

get member (10 tests)
  - list contains created member
  - required column headers (ID/NAME/EMAIL/ROLE/STATUS)
  - authenticated account appears as owner/active
  - JSON shape: data array with id/email/role/status fields
  - no ANSI codes in non-TTY output
  - pipe-friendly JSON output (ends with newline)
  - -o yaml returns valid YAML
  - -w flag overrides workspace
  - unauthenticated → exit 4

set member (7 tests)
  - promote normal → admin, verify via get member
  - demote admin → normal, verify via get member
  - --role owner rejected client-side (exit 2)
  - missing --role → usage error
  - missing member-id → usage error
  - non-existent UUID → server error
  - unauthenticated → exit 4

create member error paths (5 tests)
  - invalid role (superadmin) rejected client-side (exit 2)
  - --role owner rejected client-side (exit 2)
  - missing --email → usage error
  - missing --role → usage error
  - unauthenticated → exit 4

delete member (6 tests)
  - success: member removed from list
  - cannot delete self → server error
  - missing member-id → usage error
  - non-existent UUID → server error
  - -o json on error → structured error envelope
  - unauthenticated → exit 4

Data lifecycle: beforeAll invites two auto_test+<ts>@dify.ai
members; afterAll cleans them up. No extra env vars required.
2026-06-18 17:39:11 +08:00
gigglewang0417 fadf3acc94 test(cli/e2e): remove 5.70f — test premise was incorrect
5.70f assumed that passing wrong-type inputs to run app would be
intercepted by @accepts(body=) and return HTTP 422 with canonical
ErrorBody details[]. This premise is wrong:

- @accepts(body=) validates the API schema (inputs must be a dict),
  not the workflow-level variable types (num must be a number)
- Wrong-type workflow inputs go through the SSE execution layer,
  returning an error event with status:500, not a canonical 422
- The CLI receives this via decodeStreamError, not classifyResponse,
  so rawResponse is never set and error.server is never populated

The behavior tested by 5.70f (CLI rendering details[] as indented
lines) remains valid as a contract requirement but has no triggerable
E2E path on the current server. Covered by unit tests for renderHuman.
2026-06-17 11:59:56 +08:00
gigglewang0417 2da773e8d8 test(cli/e2e): rewrite 5.70d-h — correct triggers and add V2 hint priority test
Rewrites 5 existing tests and adds 1 new test to align with the V2 ErrorBody spec
(history page: https://km.dify.langgenius.ai/wiki/spaces/Enterprise/history/490602534/):

Trigger fix (5.70d, 5.70g, 5.70h):
  Before: 'run app wrong-type input' → hits SSE execution layer → HTTP 500
  After:  'describe app ZERO' → hits @returns canonical 404 ErrorBody → error.server set
  Result: 5.70d, 5.70g, 5.70h now pass on the current server

Trigger fix (5.70e):
  Before: 'run app wrong-type input' → SSE 500, no details
  After:  direct fetch to GET /apps?page=not-integer → @accepts(query=) returns 422
          with canonical ErrorBody and details[] (bypasses CLI client-side page validation)
  Result: 5.70e now passes on the current server

Forward-looking (5.70f):
  Kept 'run app wrong-type input' trigger — documents that once @accepts covers the
  app run path, details lines will appear; remains red until that server change deploys

New (5.70g2):
  Tests V2 spec correction: hint priority is cliHint ?? server?.hint (CLI wins).
  V1 doc incorrectly said 'server wins'; V2 aligned with codebase.
  Trigger: unauthenticated → 401 AuthExpired → CLI AUTH_LOGIN_HINT appears at
  error.hint regardless of any server-provided hint. Passes on current server.
2026-06-17 10:42:37 +08:00
gigglewang0417 e3e7ff26a5 test(cli/e2e): add 5.70i/5.70j — 404 canonical body and RFC 8628 boundary
5.70i [P1] — /openapi/v1 unknown route 404 carries canonical ErrorBody
             (code, status) and must NOT contain flask-restx route suggestions
             ('did you mean'); pins the ExternalApi._help_on_404 fix from
             PR #37285

5.70j [P1] — device-flow token endpoint returns RFC 8628 {error: string}
             on failure, not the canonical ErrorBody shape; pins the
             explicit RFC 8628 exception documented in PR #37285 and
             confirms zErrorBody.safeParse would fail gracefully on this
             shape (serverError = undefined, generic message, no crash)
2026-06-16 18:09:19 +08:00
gigglewang0417 1bd9cf8cc6 test(cli/e2e): add ErrorBody contract tests for error.server structure
Add 5 new test cases (5.70d-h) that verify the CLI correctly forwards the
canonical ErrorBody from the server when request validation fails (422).

These tests define the E2E contract introduced by the OpenAPI ErrorBody
unification spec. They will be red until the server-side change deploys;
the spec document is at:
  https://km.dify.langgenius.ai/wiki/spaces/Enterprise/pages/490602534/

5.70d [P0] — JSON envelope contains error.server with code='invalid_param',
             status=422, and a non-empty message string
5.70e [P1] — error.server.details array carries field-level error entries
             with the {type, loc, msg} shape from ErrorDetail
5.70f [P1] — human-readable text mode renders each details entry as
             '  - <loc>: <msg> (<type>)' line (no -v required)
5.70g [P1] — text mode header code is server's 'invalid_param', not CLI's
             'server_4xx_other' (server code wins in renderHuman)
5.70h [P1] — JSON envelope error.code is CLI classification code
             ('server_4xx_other'); semantic code is in error.server.code
2026-06-16 17:19:19 +08:00
Stephen ZhouandGitHub 33ef76c5b8 chore: audit deps (#37516) 2026-06-16 09:10:17 +00:00
56a026505e fix(cli/e2e): remove LLM nodes from fixture DSLs and fix test assertions (#37463)
Co-authored-by: yunlu.wen <yunlu.wen@dify.ai>
2026-06-16 08:58:53 +00:00
zyssyz123GitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
dcc0b95e11 fix: issue (#37508)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-06-16 08:53:53 +00:00
lmlmandGitHub f7c90d4873 test: use caplog for mail task logging (#37493) 2026-06-16 08:18:22 +00:00
落尘andGitHub 28cc739a93 fix(jina): handle non-json auth errors (#37502) 2026-06-16 08:16:34 +00:00
落尘andGitHub 4350617694 fix(watercrawl): handle non-json auth errors (#37498) 2026-06-16 08:16:30 +00:00
落尘andGitHub 431d6bb983 fix(watercrawl): accept content type parameters (#37500) 2026-06-16 08:16:24 +00:00
落尘andGitHub fe64c5d4a8 fix(watercrawl): bound result download timeout (#37495) 2026-06-16 08:16:16 +00:00
f9ed81c3f4 fix(api): Agent v2 chat ask_human — resume on timeout + skip input guards on resume (#37492)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 08:11:28 +00:00
41 changed files with 1340 additions and 499 deletions
+31 -11
View File
@@ -53,14 +53,20 @@ class AgentIdPath(BaseModel):
class AgentAppCreatePayload(BaseModel):
name: str = Field(..., min_length=1, description="Agent name")
description: str | None = Field(default=None, description="Agent description (max 400 chars)", max_length=400)
role: str = Field(default="", description="Agent role", max_length=255)
icon_type: IconType | None = Field(default=None, description="Icon type")
icon: str | None = Field(default=None, description="Icon")
icon_background: str | None = Field(default=None, description="Icon background color")
class AgentAppUpdatePayload(UpdateAppPayload):
role: str | None = Field(default=None, description="Agent role", max_length=255)
register_schema_models(
console_ns,
AgentAppCreatePayload,
AgentAppUpdatePayload,
AgentInviteOptionsQuery,
AgentIdPath,
AppListQuery,
@@ -89,20 +95,32 @@ def _serialize_agent_app_detail(app_model) -> dict:
app_setting = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=str(app_model.id))
app_model.access_mode = app_setting.access_mode # type: ignore[attr-defined]
payload = AppDetailWithSite.model_validate(app_model, from_attributes=True).model_dump(mode="json")
agent_id = payload.pop("bound_agent_id", None)
if not agent_id:
agent = _agent_roster_service().get_app_backing_agent(tenant_id=app_model.tenant_id, app_id=app_model.id)
if not agent:
raise AgentNotFoundError()
payload["id"] = agent_id
payload = AppDetailWithSite.model_validate(app_model, from_attributes=True).model_dump(mode="json")
payload.pop("bound_agent_id", None)
payload["app_id"] = str(app_model.id)
payload["id"] = agent.id
payload["role"] = agent.role or ""
return payload
def _serialize_agent_app_pagination(app_pagination) -> dict:
def _serialize_agent_app_pagination(app_pagination, *, tenant_id: str) -> dict:
app_ids = [str(app.id) for app in app_pagination.items]
agents_by_app_id = _agent_roster_service().load_app_backing_agents_by_app_id(
tenant_id=tenant_id,
app_ids=app_ids,
)
payload = AppPagination.model_validate(app_pagination, from_attributes=True).model_dump(mode="json")
for item in payload["data"]:
agent_id = item.pop("bound_agent_id", None)
if agent_id:
item["id"] = agent_id
app_id = item["id"]
item.pop("bound_agent_id", None)
agent = agents_by_app_id.get(app_id)
if agent:
item["app_id"] = app_id
item["id"] = agent.id
item["role"] = agent.role or ""
return payload
@@ -137,7 +155,7 @@ class AgentAppListApi(Resource):
empty = AppPagination(page=args.page, limit=args.limit, total=0, has_more=False, data=[])
return empty.model_dump(mode="json")
return _serialize_agent_app_pagination(app_pagination)
return _serialize_agent_app_pagination(app_pagination, tenant_id=current_tenant_id)
@console_ns.expect(console_ns.models[AgentAppCreatePayload.__name__])
@console_ns.response(201, "Agent app created successfully", console_ns.models[AppDetailWithSite.__name__])
@@ -156,6 +174,7 @@ class AgentAppListApi(Resource):
name=args.name,
description=args.description,
mode="agent",
agent_role=args.role,
icon_type=args.icon_type,
icon=args.icon,
icon_background=args.icon_background,
@@ -177,7 +196,7 @@ class AgentAppApi(Resource):
app_model = _resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
return _serialize_agent_app_detail(app_model)
@console_ns.expect(console_ns.models[UpdateAppPayload.__name__])
@console_ns.expect(console_ns.models[AgentAppUpdatePayload.__name__])
@console_ns.response(200, "Agent app updated successfully", console_ns.models[AppDetailWithSite.__name__])
@console_ns.response(403, "Insufficient permissions")
@console_ns.response(400, "Invalid request parameters")
@@ -188,7 +207,7 @@ class AgentAppApi(Resource):
@with_current_tenant_id
def put(self, tenant_id: str, agent_id: UUID):
app_model = _resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
args = UpdateAppPayload.model_validate(console_ns.payload)
args = AgentAppUpdatePayload.model_validate(console_ns.payload)
args_dict: AppService.ArgsDict = {
"name": args.name,
"description": args.description or "",
@@ -197,6 +216,7 @@ class AgentAppApi(Resource):
"icon_background": args.icon_background or "",
"use_icon_as_answer_icon": args.use_icon_as_answer_icon or False,
"max_active_requests": args.max_active_requests or 0,
"role": args.role,
}
updated = AppService().update_app(app_model, args_dict)
return _serialize_agent_app_detail(updated)
+6
View File
@@ -400,6 +400,9 @@ class AppPartial(ResponseModel):
has_draft_trigger: bool | None = None
# For Agent App type: the roster Agent backing this app (None otherwise).
bound_agent_id: str | None = None
# For Agent App responses exposed through /agent.
app_id: str | None = None
role: str | None = None
is_starred: bool = False
@computed_field(return_type=str | None) # type: ignore
@@ -451,6 +454,9 @@ class AppDetailWithSite(AppDetail):
site: Site | None = None
# For Agent App type: the roster Agent backing this app (None otherwise).
bound_agent_id: str | None = None
# For Agent App responses exposed through /agent.
app_id: str | None = None
role: str | None = None
@computed_field(return_type=str | None) # type: ignore
@property
+27 -14
View File
@@ -246,6 +246,8 @@ class AgentAppGenerator(MessageBasedAppGenerator):
"conversation_id": conversation.id,
"message_id": message.id,
"user_from": UserFrom.ACCOUNT if isinstance(user, Account) else UserFrom.END_USER,
# Resume continues a paused agent run; skip input guards (see _generate_worker).
"is_resume": True,
},
)
worker_thread.start()
@@ -270,6 +272,7 @@ class AgentAppGenerator(MessageBasedAppGenerator):
conversation_id: str,
message_id: str,
user_from: UserFrom,
is_resume: bool = False,
) -> None:
from libs.flask_utils import preserve_flask_contexts
@@ -279,20 +282,30 @@ class AgentAppGenerator(MessageBasedAppGenerator):
message = self._get_message(message_id)
app_config = application_generate_entity.app_config
# Apply app-level input guards (content moderation + annotation
# reply) before reaching the Agent backend, mirroring the EasyUI
# chat / agent-chat runners. These can short-circuit the turn.
app_model = db.session.get(App, app_config.app_id)
if app_model is None:
raise AgentAppGeneratorError("App not found")
handled, query = self._run_input_guards(
application_generate_entity=application_generate_entity,
app_model=app_model,
message=message,
queue_manager=queue_manager,
)
if handled:
return
if is_resume:
# ENG-638: a resume continues a paused agent run; the human's
# reply is threaded in by the runner as deferred_tool_results.
# The query is the replayed paused-turn message, kept only to
# match the suspended snapshot's layers — it is NOT new
# end-user input, so input guards must NOT run. Moderation or an
# annotation match on the replayed query would short-circuit the
# turn and drop the human reply, stranding the ask_human session.
query = application_generate_entity.query or ""
else:
# Apply app-level input guards (content moderation + annotation
# reply) before reaching the Agent backend, mirroring the EasyUI
# chat / agent-chat runners. These can short-circuit the turn.
app_model = db.session.get(App, app_config.app_id)
if app_model is None:
raise AgentAppGeneratorError("App not found")
handled, query = self._run_input_guards(
application_generate_entity=application_generate_entity,
app_model=app_model,
message=message,
queue_manager=queue_manager,
)
if handled:
return
dify_context = DifyRunContext(
tenant_id=app_config.tenant_id,
+7 -5
View File
@@ -118,16 +118,18 @@ class WaterCrawlAPIClient(BaseAPIClient):
response.raise_for_status()
if response.status_code == 204:
return None
if response.headers.get("Content-Type") == "application/json":
content_type = response.headers.get("Content-Type", "")
media_type = content_type.split(";", 1)[0].strip().lower()
if media_type == "application/json":
return response.json() or {}
if response.headers.get("Content-Type") == "application/octet-stream":
if media_type == "application/octet-stream":
return response.content
if response.headers.get("Content-Type") == "text/event-stream":
if media_type == "text/event-stream":
return self.process_eventstream(response)
raise Exception(f"Unknown response type: {response.headers.get('Content-Type')}")
raise Exception(f"Unknown response type: {content_type}")
def get_crawl_requests_list(self, page: int | None = None, page_size: int | None = None):
query_params = {"page": page or 1, "page_size": page_size or 10}
@@ -217,7 +219,7 @@ class WaterCrawlAPIClient(BaseAPIClient):
return event_data["data"]
def download_result(self, result_object: dict[str, Any]):
response = httpx.get(result_object["result"], timeout=None)
response = httpx.get(result_object["result"], timeout=30)
try:
response.raise_for_status()
result_object["result"] = response.json()
+19 -1
View File
@@ -382,7 +382,7 @@ Check if activation token is valid
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [UpdateAppPayload](#updateapppayload)<br> |
| Yes | **application/json**: [AgentAppUpdatePayload](#agentappupdatepayload)<br> |
#### Responses
@@ -11286,6 +11286,7 @@ Default namespace
| icon_background | string | Icon background color | No |
| icon_type | [IconType](#icontype) | Icon type | No |
| name | string | Agent name | Yes |
| role | string | Agent role | No |
#### AgentAppFeaturesPayload
@@ -11304,6 +11305,19 @@ default (the config form sends the full desired feature state on save).
| suggested_questions_after_answer | [AgentSuggestedQuestionsAfterAnswerFeatureConfig](#agentsuggestedquestionsafteranswerfeatureconfig) | Follow-up suggestions config, e.g. {'enabled': true} | No |
| text_to_speech | [AgentTextToSpeechFeatureConfig](#agenttexttospeechfeatureconfig) | Text-to-speech config | No |
#### AgentAppUpdatePayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| description | string | App description (max 400 chars) | No |
| icon | string | Icon | No |
| icon_background | string | Icon background color | No |
| icon_type | [IconType](#icontype) | Icon type | No |
| max_active_requests | integer | Maximum active requests | No |
| name | string | App name | Yes |
| role | string | Agent role | No |
| use_icon_as_answer_icon | boolean | Use icon as answer icon | No |
#### AgentCliToolAuthorizationStatus
Authorization state for Agent-scoped CLI tools.
@@ -12502,6 +12516,7 @@ Enum class for api provider schema type.
| ---- | ---- | ----------- | -------- |
| access_mode | string | | No |
| api_base_url | string | | No |
| app_id | string | | No |
| bound_agent_id | string | | No |
| created_at | integer | | No |
| created_by | string | | No |
@@ -12518,6 +12533,7 @@ Enum class for api provider schema type.
| mode | string | | Yes |
| model_config | [ModelConfig](#modelconfig) | | No |
| name | string | | Yes |
| role | string | | No |
| site | [Site](#site) | | No |
| tags | [ [Tag](#tag) ] | | No |
| tracing | [JSONValue](#jsonvalue) | | No |
@@ -12622,6 +12638,7 @@ AppMCPServer Status Enum
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| access_mode | string | | No |
| app_id | string | | No |
| author_name | string | | No |
| bound_agent_id | string | | No |
| create_user_name | string | | No |
@@ -12639,6 +12656,7 @@ AppMCPServer Status Enum
| mode | string | | Yes |
| model_config | [ModelConfigPartial](#modelconfigpartial) | | No |
| name | string | | Yes |
| role | string | | No |
| tags | [ [Tag](#tag) ] | | No |
| updated_at | integer | | No |
| updated_by | string | | No |
+56 -4
View File
@@ -282,6 +282,7 @@ class AgentRosterService:
app_id: str,
name: str,
description: str = "",
role: str = "",
icon_type: Any = None,
icon: str | None = None,
icon_background: str | None = None,
@@ -298,7 +299,7 @@ class AgentRosterService:
tenant_id=tenant_id,
name=name,
description=description,
role="",
role=role,
icon_type=icon_type,
icon=icon,
icon_background=icon_background,
@@ -341,6 +342,21 @@ class AgentRosterService:
self._session.flush()
return agent
def load_app_backing_agents_by_app_id(self, *, tenant_id: str, app_ids: list[str]) -> dict[str, Agent]:
"""Return active app-backed Agents keyed by Agent App id."""
if not app_ids:
return {}
agents = self._session.scalars(
select(Agent).where(
Agent.tenant_id == tenant_id,
Agent.app_id.in_(app_ids),
Agent.scope == AgentScope.ROSTER,
Agent.source == AgentSource.AGENT_APP,
Agent.status == AgentStatus.ACTIVE,
)
).all()
return {agent.app_id: agent for agent in agents if agent.app_id}
def get_app_backing_agent(self, *, tenant_id: str, app_id: str) -> Agent | None:
"""Return the roster Agent that backs the given Agent App, if any."""
return self._session.scalar(
@@ -444,12 +460,36 @@ class AgentRosterService:
agent.updated_by = account_id
self._session.commit()
@staticmethod
def _visible_version_operations(agent: Agent) -> set[AgentConfigRevisionOperation]:
if agent.source == AgentSource.AGENT_APP:
return {AgentConfigRevisionOperation.SAVE_NEW_VERSION}
return {
AgentConfigRevisionOperation.CREATE_VERSION,
AgentConfigRevisionOperation.SAVE_NEW_VERSION,
AgentConfigRevisionOperation.SAVE_NEW_AGENT,
AgentConfigRevisionOperation.SAVE_TO_ROSTER,
}
def list_agent_versions(self, *, tenant_id: str, agent_id: str) -> list[dict[str, Any]]:
self._get_agent(tenant_id=tenant_id, agent_id=agent_id, roster_only=True)
agent = self._get_agent(tenant_id=tenant_id, agent_id=agent_id, roster_only=True)
visible_version_ids = (
select(AgentConfigRevision.current_snapshot_id)
.where(
AgentConfigRevision.tenant_id == tenant_id,
AgentConfigRevision.agent_id == agent_id,
AgentConfigRevision.operation.in_(self._visible_version_operations(agent)),
)
.subquery()
)
versions = list(
self._session.scalars(
select(AgentConfigSnapshot)
.where(AgentConfigSnapshot.tenant_id == tenant_id, AgentConfigSnapshot.agent_id == agent_id)
.where(
AgentConfigSnapshot.tenant_id == tenant_id,
AgentConfigSnapshot.agent_id == agent_id,
AgentConfigSnapshot.id.in_(select(visible_version_ids.c.current_snapshot_id)),
)
.order_by(AgentConfigSnapshot.version.desc())
).all()
)
@@ -460,7 +500,19 @@ class AgentRosterService:
]
def get_agent_version_detail(self, *, tenant_id: str, agent_id: str, version_id: str) -> dict[str, Any]:
self._get_agent(tenant_id=tenant_id, agent_id=agent_id, roster_only=True)
agent = self._get_agent(tenant_id=tenant_id, agent_id=agent_id, roster_only=True)
visible_revision_id = self._session.scalar(
select(AgentConfigRevision.id)
.where(
AgentConfigRevision.tenant_id == tenant_id,
AgentConfigRevision.agent_id == agent_id,
AgentConfigRevision.current_snapshot_id == version_id,
AgentConfigRevision.operation.in_(self._visible_version_operations(agent)),
)
.limit(1)
)
if not visible_revision_id:
raise AgentVersionNotFoundError()
version = self._get_version(tenant_id=tenant_id, agent_id=agent_id, version_id=version_id)
revisions = list(
self._session.scalars(
+10 -1
View File
@@ -2,7 +2,7 @@ import json
import logging
from collections.abc import Sequence
from datetime import datetime
from typing import Any, Literal, TypedDict, cast, override
from typing import Any, Literal, NotRequired, TypedDict, cast, override
import sqlalchemy as sa
from flask_sqlalchemy.pagination import Pagination
@@ -63,6 +63,7 @@ class CreateAppParams(BaseModel):
name: str = Field(min_length=1)
description: str | None = None
mode: Literal["chat", "agent-chat", "agent", "advanced-chat", "workflow", "completion"]
agent_role: str = Field(default="", max_length=255)
icon_type: str | None = None
icon: str | None = None
icon_background: str | None = None
@@ -90,6 +91,8 @@ class AppService:
filters.append(App.mode == AppMode.AGENT_CHAT)
elif params.mode == "agent":
filters.append(App.mode == AppMode.AGENT)
elif params.mode == "all":
filters.append(App.mode != AppMode.AGENT)
if isinstance(params, AppListParams):
if params.status:
@@ -412,6 +415,7 @@ class AppService:
app_id=app.id,
name=params.name,
description=params.description or "",
role=params.agent_role,
icon_type=icon_type,
icon=params.icon,
icon_background=params.icon_background,
@@ -507,6 +511,7 @@ class AppService:
icon_background: str
use_icon_as_answer_icon: bool
max_active_requests: int
role: NotRequired[str | None]
@staticmethod
def _get_backing_agent_for_update(app: App) -> Agent | None:
@@ -538,6 +543,7 @@ class AppService:
icon_type: IconType | str | None = None,
icon: str | None = None,
icon_background: str | None = None,
role: str | None = None,
account_id: str | None = None,
updated_at: datetime | None = None,
) -> None:
@@ -560,6 +566,8 @@ class AppService:
agent.icon = icon
if icon_background is not None:
agent.icon_background = icon_background
if role is not None:
agent.role = role
agent.updated_by = account_id
if updated_at is not None:
agent.updated_at = updated_at
@@ -594,6 +602,7 @@ class AppService:
icon_type=app.icon_type,
icon=app.icon,
icon_background=app.icon_background,
role=args.get("role"),
account_id=current_user.id,
updated_at=app.updated_at,
)
+8 -2
View File
@@ -43,10 +43,16 @@ class JinaAuth(ApiKeyAuthBase):
def _handle_error(self, response):
if response.status_code in {402, 409, 500}:
error_message = response.json().get("error", "Unknown error occurred")
try:
error_message = response.json().get("error", "Unknown error occurred")
except ValueError:
error_message = response.text or "Unknown error occurred"
raise Exception(f"Failed to authorize. Status code: {response.status_code}. Error: {error_message}")
else:
if response.text:
error_message = json.loads(response.text).get("error", "Unknown error occurred")
try:
error_message = json.loads(response.text).get("error", "Unknown error occurred")
except ValueError:
error_message = response.text
raise Exception(f"Failed to authorize. Status code: {response.status_code}. Error: {error_message}")
raise Exception(f"Unexpected error occurred while trying to authorize. Status code: {response.status_code}")
+8 -2
View File
@@ -43,10 +43,16 @@ class JinaAuth(ApiKeyAuthBase):
def _handle_error(self, response):
if response.status_code in {402, 409, 500}:
error_message = response.json().get("error", "Unknown error occurred")
try:
error_message = response.json().get("error", "Unknown error occurred")
except ValueError:
error_message = response.text or "Unknown error occurred"
raise Exception(f"Failed to authorize. Status code: {response.status_code}. Error: {error_message}")
else:
if response.text:
error_message = json.loads(response.text).get("error", "Unknown error occurred")
try:
error_message = json.loads(response.text).get("error", "Unknown error occurred")
except ValueError:
error_message = response.text
raise Exception(f"Failed to authorize. Status code: {response.status_code}. Error: {error_message}")
raise Exception(f"Unexpected error occurred while trying to authorize. Status code: {response.status_code}")
+8 -2
View File
@@ -37,10 +37,16 @@ class WatercrawlAuth(ApiKeyAuthBase):
def _handle_error(self, response):
if response.status_code in {402, 409, 500}:
error_message = response.json().get("error", "Unknown error occurred")
try:
error_message = response.json().get("error", "Unknown error occurred")
except ValueError:
error_message = response.text or "Unknown error occurred"
raise Exception(f"Failed to authorize. Status code: {response.status_code}. Error: {error_message}")
else:
if response.text:
error_message = json.loads(response.text).get("error", "Unknown error occurred")
try:
error_message = json.loads(response.text).get("error", "Unknown error occurred")
except ValueError:
error_message = response.text
raise Exception(f"Failed to authorize. Status code: {response.status_code}. Error: {error_message}")
raise Exception(f"Unexpected error occurred while trying to authorize. Status code: {response.status_code}")
+16 -2
View File
@@ -95,16 +95,30 @@ def check_and_handle_human_input_timeouts(limit: int = 100) -> None:
timeout_status=HumanInputFormStatus.EXPIRED if is_global else HumanInputFormStatus.TIMEOUT,
reason="global_timeout" if is_global else "node_timeout",
)
assert record.workflow_run_id is not None, "workflow_run_id should not be None for non-test form"
if is_global:
# Global timeout applies only to workflow-owned forms
# (_is_global_timeout requires a workflow_run_id): end the run.
assert record.workflow_run_id is not None, "global timeout requires a workflow_run_id"
_handle_global_timeout(
form_id=record.form_id,
workflow_run_id=record.workflow_run_id,
node_id=record.node_id,
session_factory=session_factory,
)
else:
elif record.workflow_run_id is not None:
# Workflow Agent node / Human Input node form: resume the workflow.
service.enqueue_resume(record.workflow_run_id)
elif record.conversation_id is not None:
# ENG-635: Agent v2 chat ask_human form is conversation-owned (no
# workflow_run_id). Resume the chat turn so the timeout is threaded
# back to the agent run as the ask_human deferred_tool_result
# (status="timeout"), mirroring HumanInputService.submit_form_by_token.
service.enqueue_agent_app_resume(conversation_id=record.conversation_id, form_id=record.form_id)
else:
logger.warning(
"Timed-out form %s has neither workflow_run_id nor conversation_id; skipping resume",
record.form_id,
)
except Exception:
logger.exception(
"Failed to handle timeout for form_id=%s workflow_run_id=%s",
@@ -113,6 +113,7 @@ def _app_detail_obj(**overrides):
"deleted_tools": [],
"site": None,
"bound_agent_id": "00000000-0000-0000-0000-000000000001",
"tenant_id": "tenant-1",
}
data.update(overrides)
return SimpleNamespace(**data)
@@ -195,6 +196,16 @@ def test_agent_app_list_and_create_use_agent_route(
return _app_detail_obj(id="app-created", bound_agent_id="agent-created")
monkeypatch.setattr(roster_controller, "AppService", FakeAppService)
monkeypatch.setattr(
roster_controller.AgentRosterService,
"load_app_backing_agents_by_app_id",
lambda _self, **kwargs: {"app-list": SimpleNamespace(id="agent-list", role="List role")},
)
monkeypatch.setattr(
roster_controller.AgentRosterService,
"get_app_backing_agent",
lambda _self, **kwargs: SimpleNamespace(id="agent-created", role="Created role"),
)
monkeypatch.setattr(
roster_controller.FeatureService,
"get_system_features",
@@ -208,6 +219,8 @@ def test_agent_app_list_and_create_use_agent_route(
assert listed["limit"] == 10
assert listed["total"] == 1
assert listed["data"][0]["id"] == "agent-list"
assert listed["data"][0]["app_id"] == "app-list"
assert listed["data"][0]["role"] == "List role"
assert "bound_agent_id" not in listed["data"][0]
list_call = cast(dict[str, object], captured["list"])
list_params = cast(Any, list_call["params"])
@@ -222,6 +235,8 @@ def test_agent_app_list_and_create_use_agent_route(
assert status == 201
assert created["id"] == "agent-created"
assert created["app_id"] == "app-created"
assert created["role"] == "Created role"
assert "bound_agent_id" not in created
create_call = cast(dict[str, object], captured["create"])
create_params = cast(Any, create_call["params"])
@@ -240,6 +255,11 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id(
"get_agent_app_model",
lambda _self, **kwargs: app_model,
)
monkeypatch.setattr(
roster_controller.AgentRosterService,
"get_app_backing_agent",
lambda _self, **kwargs: SimpleNamespace(id=agent_id, role="Resolved role"),
)
monkeypatch.setattr(
roster_controller.FeatureService,
"get_system_features",
@@ -262,6 +282,8 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id(
detail = unwrap(AgentAppApi.get)(AgentAppApi(), "tenant-1", agent_id)
assert detail["id"] == agent_id
assert detail["app_id"] == "app-1"
assert detail["role"] == "Resolved role"
assert "bound_agent_id" not in detail
with app.test_request_context(
@@ -272,6 +294,8 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id(
assert updated["name"] == "Renamed"
assert updated["id"] == agent_id
assert updated["app_id"] == "app-1"
assert updated["role"] == "Resolved role"
assert "bound_agent_id" not in updated
update_call = cast(dict[str, object], captured["update"])
assert update_call["app"] is app_model
@@ -201,17 +201,18 @@ class TestGenerateWorker:
mocker.patch(f"{MODULE}.AgentAppRunner", return_value=runner)
return runner
def _call(self, generator, mocker: MockerFixture, queue_manager):
def _call(self, generator, mocker: MockerFixture, queue_manager, *, is_resume=False, query="query"):
generator._generate_worker(
flask_app=mocker.MagicMock(),
context=mocker.MagicMock(),
application_generate_entity=mocker.MagicMock(
agent_id="a", agent_config_snapshot_id="s", model_conf=mocker.MagicMock(model="m")
agent_id="a", agent_config_snapshot_id="s", model_conf=mocker.MagicMock(model="m"), query=query
),
queue_manager=queue_manager,
conversation_id="conv",
message_id="msg",
user_from=UserFrom.END_USER,
is_resume=is_resume,
)
def test_happy_path_runs_backend(self, generator, mocker: MockerFixture):
@@ -227,6 +228,20 @@ class TestGenerateWorker:
self._call(generator, mocker, queue_manager)
runner.run.assert_not_called()
def test_resume_skips_input_guards_and_consumes_reply(self, generator, mocker: MockerFixture):
# ENG-638 (review): on resume the replayed query is NOT new end-user input.
# Input guards must be skipped, even if moderation/annotation would match,
# so the run continues and the human reply (deferred_tool_results) is used.
runner = self._wire(generator, mocker, handled=True) # guards WOULD short-circuit
queue_manager = mocker.MagicMock()
self._call(generator, mocker, queue_manager, is_resume=True, query="the approved reply")
generator._run_input_guards.assert_not_called()
runner.run.assert_called_once()
# the replayed paused-turn query flows straight to the runner (snapshot match)
assert runner.run.call_args.kwargs["query"] == "the approved reply"
def test_generate_task_stopped_is_swallowed(self, generator, mocker: MockerFixture):
self._wire(generator, mocker, run_side_effect=GenerateTaskStoppedError())
queue_manager = mocker.MagicMock()
@@ -168,6 +168,13 @@ class TestWaterCrawlAPIClient:
assert client.process_response(_response(200, {"ok": True})) == {"ok": True}
assert client.process_response(_response(200, None)) == {}
def test_process_response_accepts_json_content_type_parameters(self):
client = WaterCrawlAPIClient(api_key="k")
response = _response(200, {"ok": True}, content_type="application/json; charset=utf-8")
assert client.process_response(response) == {"ok": True}
def test_process_response_octet_stream_returns_bytes(self):
client = WaterCrawlAPIClient(api_key="k")
assert (
@@ -242,11 +249,18 @@ class TestWaterCrawlAPIClient:
client = WaterCrawlAPIClient(api_key="k")
response = _response(200, {"markdown": "body"})
monkeypatch.setattr(client_module.httpx, "get", lambda *args, **kwargs: response)
captured = {}
def fake_get(*args, **kwargs):
captured.update(kwargs)
return response
monkeypatch.setattr(client_module.httpx, "get", fake_get)
result = client.download_result({"result": "https://example.com/result.json"})
assert result["result"] == {"markdown": "body"}
assert captured["timeout"] is not None
response.close.assert_called_once()
@@ -29,6 +29,7 @@ from services.agent.composer_validator import ComposerConfigValidator
from services.agent.errors import InvalidComposerConfigError
from services.agent.roster_service import AgentRosterService
from services.agent.workflow_publish_service import WorkflowAgentPublishService
from services.app_service import AppListParams, AppService
from services.entities.agent_entities import AgentSoulConfig, ComposerSavePayload, ComposerSaveStrategy, ComposerVariant
@@ -769,7 +770,7 @@ def test_roster_update_archive_versions_and_detail(monkeypatch):
created_by="account-1",
created_at=revision_created_at,
)
fake_session = FakeSession(scalars=[[listed_version], [revision]])
fake_session = FakeSession(scalar=["visible-revision"], scalars=[[listed_version], [revision]])
agent = Agent(
id="agent-1",
tenant_id="tenant-1",
@@ -825,6 +826,7 @@ def test_roster_create_detail_and_lookup_helpers(monkeypatch):
payload = roster_service.RosterAgentCreatePayload(
name="Analyst",
description="desc",
role="Research assistant",
icon_type="emoji",
icon="A",
icon_background="#fff",
@@ -838,6 +840,7 @@ def test_roster_create_detail_and_lookup_helpers(monkeypatch):
account_id="account-1",
app_id="app-1",
name="Backing Agent",
role="Support agent",
)
found_agent = service._get_agent(tenant_id="tenant-1", agent_id="agent-1")
with pytest.raises(roster_service.AgentNotFoundError):
@@ -849,9 +852,11 @@ def test_roster_create_detail_and_lookup_helpers(monkeypatch):
assert service._load_versions_by_id([]) == {}
assert created.name == "Analyst"
assert created.role == "Research assistant"
assert created.source == AgentSource.ROSTER
assert created.active_config_snapshot_id is not None
assert created.active_config_has_model is False
assert backing_agent.role == "Support agent"
assert backing_agent.active_config_snapshot_id is not None
assert backing_agent.active_config_has_model is False
assert found_agent.id == "agent-1"
@@ -859,6 +864,30 @@ def test_roster_create_detail_and_lookup_helpers(monkeypatch):
assert loaded_versions["version-1"].agent_id == "agent-1"
def test_agent_app_visible_versions_exclude_draft_saves():
agent_app = Agent(source=AgentSource.AGENT_APP)
roster_agent = Agent(source=AgentSource.ROSTER)
agent_app_operations = AgentRosterService._visible_version_operations(agent_app)
roster_operations = AgentRosterService._visible_version_operations(roster_agent)
assert agent_app_operations == {AgentConfigRevisionOperation.SAVE_NEW_VERSION}
assert AgentConfigRevisionOperation.SAVE_CURRENT_VERSION not in agent_app_operations
assert AgentConfigRevisionOperation.CREATE_VERSION in roster_operations
assert AgentConfigRevisionOperation.SAVE_CURRENT_VERSION not in roster_operations
def test_app_list_all_excludes_agent_apps_by_default():
filters = AppService._build_app_list_filters(
"account-1",
"tenant-1",
AppListParams(mode="all"),
)
sql = " ".join(str(filter_) for filter_ in filters)
assert "apps.mode != :mode_1" in sql
def test_validator_dict_helpers_wrap_validation_errors():
valid_soul = ComposerConfigValidator.validate_agent_soul_dict({"prompt": {"system_prompt": "x"}})
valid_node_job = ComposerConfigValidator.validate_node_job_dict({"workflow_prompt": "x"})
@@ -68,6 +68,22 @@ class TestJinaAuth:
auth.validate_credentials()
assert str(exc_info.value) == "Failed to authorize. Status code: 402. Error: Payment required"
@patch("services.auth.jina.jina._http_client.post", autospec=True)
def test_should_handle_http_error_with_non_json_text_response(self, mock_post):
"""Test handling of known HTTP errors with non-JSON text response."""
mock_response = MagicMock()
mock_response.status_code = 402
mock_response.text = "Payment required"
mock_response.json.side_effect = ValueError("Not JSON")
mock_post.return_value = mock_response
credentials = {"auth_type": "bearer", "config": {"api_key": "test_api_key_123"}}
auth = JinaAuth(credentials)
with pytest.raises(Exception) as exc_info:
auth.validate_credentials()
assert str(exc_info.value) == "Failed to authorize. Status code: 402. Error: Payment required"
@patch("services.auth.jina.jina._http_client.post", autospec=True)
def test_should_handle_http_409_error(self, mock_post):
"""Test handling of 409 Conflict error"""
@@ -114,6 +130,22 @@ class TestJinaAuth:
auth.validate_credentials()
assert str(exc_info.value) == "Failed to authorize. Status code: 403. Error: Forbidden"
@patch("services.auth.jina.jina._http_client.post", autospec=True)
def test_should_handle_unexpected_error_with_non_json_text_response(self, mock_post):
"""Test handling of unexpected errors with non-JSON text response."""
mock_response = MagicMock()
mock_response.status_code = 403
mock_response.text = "Forbidden"
mock_response.json.side_effect = Exception("Not JSON")
mock_post.return_value = mock_response
credentials = {"auth_type": "bearer", "config": {"api_key": "test_api_key_123"}}
auth = JinaAuth(credentials)
with pytest.raises(Exception) as exc_info:
auth.validate_credentials()
assert str(exc_info.value) == "Failed to authorize. Status code: 403. Error: Forbidden"
@patch("services.auth.jina.jina._http_client.post", autospec=True)
def test_should_handle_unexpected_error_without_text(self, mock_post):
"""Test handling of unexpected errors without text response"""
@@ -118,6 +118,17 @@ def test_handle_error_statuses_default_unknown_error(jina_module: ModuleType) ->
auth._handle_error(response)
def test_handle_error_statuses_fall_back_to_text_body(jina_module: ModuleType) -> None:
auth = jina_module.JinaAuth(_credentials(api_key="k"))
response = MagicMock()
response.status_code = 402
response.text = "Payment required"
response.json.side_effect = ValueError("Not JSON")
with pytest.raises(Exception, match="Status code: 402.*Payment required"):
auth._handle_error(response)
def test_handle_error_with_text_json_body(jina_module: ModuleType) -> None:
auth = jina_module.JinaAuth(_credentials(api_key="k"))
response = MagicMock()
@@ -128,6 +139,16 @@ def test_handle_error_with_text_json_body(jina_module: ModuleType) -> None:
auth._handle_error(response)
def test_handle_error_with_non_json_text_body(jina_module: ModuleType) -> None:
auth = jina_module.JinaAuth(_credentials(api_key="k"))
response = MagicMock()
response.status_code = 403
response.text = "Forbidden"
with pytest.raises(Exception, match="Status code: 403.*Forbidden"):
auth._handle_error(response)
def test_handle_error_with_text_json_body_missing_error(jina_module: ModuleType) -> None:
auth = jina_module.JinaAuth(_credentials(api_key="k"))
response = MagicMock()
@@ -99,12 +99,25 @@ class TestWatercrawlAuth:
auth_instance.validate_credentials()
assert str(exc_info.value) == f"Failed to authorize. Status code: {status_code}. Error: {error_message}"
@patch("services.auth.watercrawl.watercrawl.httpx.get", autospec=True)
def test_should_handle_http_error_with_non_json_text_response(self, mock_get, auth_instance):
"""Test handling of known HTTP errors with non-JSON text response."""
mock_response = MagicMock()
mock_response.status_code = 402
mock_response.text = "Payment required"
mock_response.json.side_effect = ValueError("Not JSON")
mock_get.return_value = mock_response
with pytest.raises(Exception) as exc_info:
auth_instance.validate_credentials()
assert str(exc_info.value) == "Failed to authorize. Status code: 402. Error: Payment required"
@pytest.mark.parametrize(
("status_code", "response_text", "has_json_error", "expected_error_contains"),
[
(403, '{"error": "Forbidden"}', True, "Failed to authorize. Status code: 403. Error: Forbidden"),
(404, "", True, "Unexpected error occurred while trying to authorize. Status code: 404"),
(401, "Not JSON", True, "Expecting value"), # JSON decode error
(401, "Not JSON", True, "Failed to authorize. Status code: 401. Error: Not JSON"),
],
)
@patch("services.auth.watercrawl.watercrawl.httpx.get", autospec=True)
@@ -63,6 +63,7 @@ class _FakeFormRepo:
return SimpleNamespace(
form_id=form_id,
workflow_run_id=getattr(form, "workflow_run_id", None),
conversation_id=getattr(form, "conversation_id", None),
node_id=getattr(form, "node_id", None),
)
@@ -70,11 +71,15 @@ class _FakeFormRepo:
class _FakeService:
def __init__(self, _session_factory, form_repository=None):
self.enqueued: list[str] = []
self.agent_app_resumed: list[tuple[str, str]] = []
def enqueue_resume(self, workflow_run_id: str | None) -> None:
if workflow_run_id is not None:
self.enqueued.append(workflow_run_id)
def enqueue_agent_app_resume(self, *, conversation_id: str, form_id: str) -> None:
self.agent_app_resumed.append((conversation_id, form_id))
def _build_form(
*,
@@ -84,6 +89,7 @@ def _build_form(
expiration_time: datetime,
workflow_run_id: str | None,
node_id: str,
conversation_id: str | None = None,
) -> SimpleNamespace:
return SimpleNamespace(
id=form_id,
@@ -91,6 +97,7 @@ def _build_form(
created_at=created_at,
expiration_time=expiration_time,
workflow_run_id=workflow_run_id,
conversation_id=conversation_id,
node_id=node_id,
status=HumanInputFormStatus.WAITING,
)
@@ -208,3 +215,45 @@ def test_check_and_handle_human_input_timeouts_omits_global_filter_when_disabled
assert stmt is not None
stmt_text = str(stmt)
assert "created_at <=" not in stmt_text
def test_check_and_handle_human_input_timeouts_routes_conversation_owned_form_to_agent_app_resume(
monkeypatch: pytest.MonkeyPatch,
):
# ENG-635 (review): a conversation-owned Agent v2 chat ask_human form has no
# workflow_run_id. On timeout it must enqueue the Agent App resume (so the
# timeout is threaded back as the ask_human result), instead of asserting on
# workflow_run_id — which previously raised and was swallowed by the except.
now = datetime(2025, 1, 1, 12, 0, 0)
monkeypatch.setattr(task_module, "naive_utc_now", lambda: now)
monkeypatch.setattr(task_module.dify_config, "HUMAN_INPUT_GLOBAL_TIMEOUT_SECONDS", 3600)
monkeypatch.setattr(task_module, "db", SimpleNamespace(engine=object()))
forms = [
_build_form(
form_id="form-chat",
form_kind=HumanInputFormKind.RUNTIME,
created_at=now - timedelta(minutes=5),
expiration_time=now - timedelta(seconds=1),
workflow_run_id=None,
conversation_id="conv-1",
node_id="agent",
),
]
capture: dict[str, Any] = {}
monkeypatch.setattr(task_module, "sessionmaker", lambda *args, **kwargs: _FakeSessionFactory(forms, capture))
repo = _FakeFormRepo(form_map={form.id: form for form in forms})
service = _FakeService(None)
monkeypatch.setattr(task_module, "HumanInputFormSubmissionRepository", lambda: repo)
monkeypatch.setattr(task_module, "HumanInputService", lambda *_args, **_kwargs: service)
monkeypatch.setattr(task_module, "_handle_global_timeout", lambda **_kwargs: None)
task_module.check_and_handle_human_input_timeouts(limit=100)
# Node timeout (conversation forms are never "global"), routed to Agent App resume.
assert repo.calls == [
{"form_id": "form-chat", "timeout_status": HumanInputFormStatus.TIMEOUT, "reason": "node_timeout"}
]
assert service.agent_app_resumed == [("conv-1", "form-chat")]
assert service.enqueued == []
@@ -8,6 +8,7 @@ This module tests the mail sending functionality including:
- Error handling and logging
"""
import logging
import smtplib
from unittest.mock import ANY, MagicMock, patch
@@ -371,8 +372,7 @@ class TestMailTaskRetryLogic:
@patch("tasks.mail_register_task.get_email_i18n_service")
@patch("tasks.mail_register_task.mail")
@patch("tasks.mail_register_task.logger")
def test_mail_task_logs_success(self, mock_logger, mock_mail, mock_email_service):
def test_mail_task_logs_success(self, mock_mail, mock_email_service, caplog):
"""Test that successful mail sends are logged properly."""
# Arrange
mock_mail.is_inited.return_value = True
@@ -380,7 +380,8 @@ class TestMailTaskRetryLogic:
mock_email_service.return_value = mock_service
# Act
send_email_register_mail_task(language="en-US", to="test@example.com", code="123456")
with caplog.at_level(logging.INFO, logger="tasks.mail_register_task"):
send_email_register_mail_task(language="en-US", to="test@example.com", code="123456")
# Assert
mock_service.send_email.assert_called_once_with(
@@ -390,12 +391,14 @@ class TestMailTaskRetryLogic:
template_context={"to": "test@example.com", "code": "123456"},
)
# Verify logging calls
assert mock_logger.info.call_count == 2 # Start and success logs
log_messages = [record.getMessage() for record in caplog.records]
assert len(log_messages) == 2 # Start and success logs
assert "Start email register mail to test@example.com" in log_messages[0]
assert "Send email register mail to test@example.com succeeded" in log_messages[1]
@patch("tasks.mail_register_task.get_email_i18n_service")
@patch("tasks.mail_register_task.mail")
@patch("tasks.mail_register_task.logger")
def test_mail_task_logs_failure(self, mock_logger, mock_mail, mock_email_service):
def test_mail_task_logs_failure(self, mock_mail, mock_email_service, caplog):
"""Test that failed mail sends are logged with exception details."""
# Arrange
mock_mail.is_inited.return_value = True
@@ -404,10 +407,13 @@ class TestMailTaskRetryLogic:
mock_email_service.return_value = mock_service
# Act
send_email_register_mail_task(language="en-US", to="test@example.com", code="123456")
with caplog.at_level(logging.ERROR, logger="tasks.mail_register_task"):
send_email_register_mail_task(language="en-US", to="test@example.com", code="123456")
# Assert
mock_logger.exception.assert_called_once_with("Send email register mail to %s failed", "test@example.com")
assert "Send email register mail to test@example.com failed" in caplog.text
assert len(caplog.records) == 1
assert caplog.records[0].exc_info is not None
@patch("tasks.mail_reset_password_task.get_email_i18n_service")
@patch("tasks.mail_reset_password_task.mail")
@@ -574,8 +580,7 @@ class TestInnerEmailTask:
@patch("tasks.mail_inner_task.get_email_i18n_service")
@patch("tasks.mail_inner_task.mail")
@patch("tasks.mail_inner_task._render_template_with_strategy")
@patch("tasks.mail_inner_task.logger")
def test_inner_email_task_logs_failure(self, mock_logger, mock_render, mock_mail, mock_email_service):
def test_inner_email_task_logs_failure(self, mock_render, mock_mail, mock_email_service, caplog):
"""Test inner email task logs failures properly."""
# Arrange
mock_mail.is_inited.return_value = True
@@ -587,10 +592,13 @@ class TestInnerEmailTask:
to_list = ["user@example.com"]
# Act
send_inner_email_task(to=to_list, subject="Test", body="Body", substitutions={})
with caplog.at_level(logging.ERROR, logger="tasks.mail_inner_task"):
send_inner_email_task(to=to_list, subject="Test", body="Body", substitutions={})
# Assert
mock_logger.exception.assert_called_once()
assert "Send enterprise mail to ['user@example.com'] failed" in caplog.text
assert len(caplog.records) == 1
assert caplog.records[0].exc_info is not None
class TestSendGridIntegration:
@@ -890,9 +898,8 @@ class TestPerformanceAndTiming:
@patch("tasks.mail_register_task.get_email_i18n_service")
@patch("tasks.mail_register_task.mail")
@patch("tasks.mail_register_task.logger")
@patch("tasks.mail_register_task.time")
def test_mail_task_tracks_execution_time(self, mock_time, mock_logger, mock_mail, mock_email_service):
def test_mail_task_tracks_execution_time(self, mock_time, mock_mail, mock_email_service, caplog):
"""Test that mail tasks track and log execution time."""
# Arrange
mock_mail.is_inited.return_value = True
@@ -903,13 +910,14 @@ class TestPerformanceAndTiming:
mock_time.perf_counter.side_effect = [100.0, 100.5] # 0.5 second execution
# Act
send_email_register_mail_task(language="en-US", to="test@example.com", code="123456")
with caplog.at_level(logging.INFO, logger="tasks.mail_register_task"):
send_email_register_mail_task(language="en-US", to="test@example.com", code="123456")
# Assert
assert mock_time.perf_counter.call_count == 2
# Verify latency is logged
success_log_call = mock_logger.info.call_args_list[1]
assert "latency" in str(success_log_call)
assert len(caplog.records) == 2
assert "latency: 0.5" in caplog.records[1].getMessage()
class TestEdgeCasesAndErrorHandling:
@@ -1457,8 +1465,7 @@ class TestLoggingAndMonitoring:
@patch("tasks.mail_register_task.get_email_i18n_service")
@patch("tasks.mail_register_task.mail")
@patch("tasks.mail_register_task.logger")
def test_mail_task_logs_recipient_information(self, mock_logger, mock_mail, mock_email_service):
def test_mail_task_logs_recipient_information(self, mock_mail, mock_email_service, caplog):
"""
Test that mail tasks log recipient information for audit trails.
@@ -1471,17 +1478,16 @@ class TestLoggingAndMonitoring:
mock_email_service.return_value = mock_service
# Act
send_email_register_mail_task(language="en-US", to="audit@example.com", code="123456")
with caplog.at_level(logging.INFO, logger="tasks.mail_register_task"):
send_email_register_mail_task(language="en-US", to="audit@example.com", code="123456")
# Assert
# Check that recipient is logged in start message
start_log_call = mock_logger.info.call_args_list[0]
assert "audit@example.com" in str(start_log_call)
assert "audit@example.com" in caplog.records[0].getMessage()
@patch("tasks.mail_inner_task.get_email_i18n_service")
@patch("tasks.mail_inner_task.mail")
@patch("tasks.mail_inner_task.logger")
def test_inner_email_task_logs_subject_for_tracking(self, mock_logger, mock_mail, mock_email_service):
def test_inner_email_task_logs_subject_for_tracking(self, mock_mail, mock_email_service, caplog):
"""
Test that inner email task logs subject for tracking purposes.
@@ -1494,11 +1500,11 @@ class TestLoggingAndMonitoring:
mock_email_service.return_value = mock_service
# Act
send_inner_email_task(
to=["user@example.com"], subject="Important Notification", body="<p>Body</p>", substitutions={}
)
with caplog.at_level(logging.INFO, logger="tasks.mail_inner_task"):
send_inner_email_task(
to=["user@example.com"], subject="Important Notification", body="<p>Body</p>", substitutions={}
)
# Assert
# Check that subject is logged
start_log_call = mock_logger.info.call_args_list[0]
assert "Important Notification" in str(start_log_call)
assert "Important Notification" in caplog.records[0].getMessage()
+1 -1
View File
@@ -14,7 +14,7 @@ export type TokenStore = {
const DOC_VERSION = 1
type TokenDoc = {
export type TokenDoc = {
version?: number
tokens?: Record<string, Record<string, string>>
}
+8 -64
View File
@@ -6,12 +6,7 @@ app:
mode: advanced-chat
name: echo-bot
use_icon_as_answer_icon: false
dependencies:
- current_identifier: null
type: marketplace
value:
marketplace_plugin_unique_identifier: langgenius/tongyi:0.1.48@966d88dc40611f067311c1c9839139ebc4b55bff471bc5e736dc3e828bc67b46
version: null
dependencies: []
kind: app
version: 0.6.0
workflow:
@@ -68,18 +63,9 @@ workflow:
edges:
- data:
sourceType: start
targetType: llm
id: 1779690795511-llm
source: '1779690795511'
sourceHandle: source
target: llm
targetHandle: target
type: custom
- data:
sourceType: llm
targetType: answer
id: llm-answer
source: llm
id: 1779690795511-answer
source: '1779690795511'
sourceHandle: source
target: answer
targetHandle: target
@@ -87,7 +73,7 @@ workflow:
nodes:
- data:
selected: false
title: 用户输入
title: User Input
type: start
variables:
- default: ''
@@ -107,66 +93,24 @@ workflow:
positionAbsolute:
x: 79
y: 282
selected: true
sourcePosition: right
targetPosition: left
type: custom
width: 242
- data:
context:
enabled: false
variable_selector: []
memory:
query_prompt_template: '{{#sys.query#}}
{{#sys.files#}}'
role_prefix:
assistant: ''
user: ''
window:
enabled: false
size: 10
model:
completion_params:
temperature: 0.7
mode: chat
name: qwen3.6-plus
provider: langgenius/tongyi/tongyi
prompt_template:
- id: 9b866a63-3619-4f5c-a46f-0aed04078587
role: system
text: 'User says: {{{#sys.query#}} Reply exactly: echo:{{#sys.query#}}'
selected: false
title: LLM
type: llm
vision:
enabled: false
height: 88
id: llm
position:
x: 380
y: 282
positionAbsolute:
x: 380
y: 282
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 242
- data:
answer: '{{#llm.text#}}'
answer: 'echo:{{#sys.query#}}'
selected: false
title: 直接回复
title: Reply
type: answer
variables: []
height: 103
id: answer
position:
x: 680
x: 380
y: 282
positionAbsolute:
x: 680
x: 380
y: 282
selected: false
sourcePosition: right
+7 -57
View File
@@ -6,12 +6,7 @@ app:
mode: workflow
name: basic_auto_test
use_icon_as_answer_icon: false
dependencies:
- current_identifier: null
type: marketplace
value:
marketplace_plugin_unique_identifier: langgenius/tongyi:0.1.48@966d88dc40611f067311c1c9839139ebc4b55bff471bc5e736dc3e828bc67b46
version: null
dependencies: []
kind: app
version: 0.6.0
workflow:
@@ -70,20 +65,9 @@ workflow:
isInIteration: false
isInLoop: false
sourceType: start
targetType: llm
id: 1779097154262-source-1779097204645-target
source: '1779097154262'
sourceHandle: source
target: '1779097204645'
targetHandle: target
type: custom
zIndex: 0
- data:
isInLoop: false
sourceType: llm
targetType: end
id: 1779097204645-source-1779171097399-target
source: '1779097204645'
id: 1779097154262-source-1779171097399-target
source: '1779097154262'
sourceHandle: source
target: '1779171097399'
targetHandle: target
@@ -92,7 +76,7 @@ workflow:
nodes:
- data:
selected: true
title: 用户输入
title: User Input
type: start
variables:
- default: ''
@@ -143,49 +127,15 @@ workflow:
targetPosition: left
type: custom
width: 242
- data:
context:
enabled: false
variable_selector: []
model:
completion_params:
temperature: 0.7
mode: chat
name: qwen3.6-plus
provider: langgenius/tongyi/tongyi
prompt_template:
- id: 1ddb3202-d84c-4faf-afe3-424eedc9049a
role: system
text: 'User says:{{#1779097154262.x#}}. Reply exactly: echo:{{#1779097154262.x#}}
'
selected: false
title: LLM
type: llm
vision:
enabled: false
height: 88
id: '1779097204645'
position:
x: 382
y: 282
positionAbsolute:
x: 382
y: 282
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 242
- data:
outputs:
- value_selector:
- '1779097204645'
- text
- '1779097154262'
- x
value_type: string
variable: x
selected: false
title: 输出
title: Output
type: end
height: 88
id: '1779171097399'
+8 -64
View File
@@ -6,12 +6,7 @@ app:
mode: advanced-chat
name: DIFY_E2E_FILE_CHAT
use_icon_as_answer_icon: false
dependencies:
- current_identifier: null
type: marketplace
value:
marketplace_plugin_unique_identifier: langgenius/tongyi:0.1.48@966d88dc40611f067311c1c9839139ebc4b55bff471bc5e736dc3e828bc67b46
version: null
dependencies: []
kind: app
version: 0.6.0
workflow:
@@ -68,18 +63,9 @@ workflow:
edges:
- data:
sourceType: start
targetType: llm
id: 1780453002656-llm
source: '1780453002656'
sourceHandle: source
target: llm
targetHandle: target
type: custom
- data:
sourceType: llm
targetType: answer
id: llm-answer
source: llm
id: 1780453002656-answer
source: '1780453002656'
sourceHandle: source
target: answer
targetHandle: target
@@ -87,7 +73,7 @@ workflow:
nodes:
- data:
selected: false
title: 用户输入
title: User Input
type: start
variables:
- allowed_file_extensions: []
@@ -119,60 +105,18 @@ workflow:
type: custom
width: 242
- data:
context:
enabled: false
variable_selector: []
memory:
query_prompt_template: '{{#sys.query#}}
{{#sys.files#}}'
role_prefix:
assistant: ''
user: ''
window:
enabled: false
size: 10
model:
completion_params:
temperature: 0.7
mode: chat
name: qwen3.6-plus
provider: langgenius/tongyi/tongyi
prompt_template:
- id: ebc516ad-be6b-4a78-af32-77f447305b34
role: system
text: 输出固定内容:""hello
answer: ok
selected: false
title: LLM
type: llm
vision:
enabled: false
height: 88
id: llm
position:
x: 380
y: 282
positionAbsolute:
x: 380
y: 282
selected: true
sourcePosition: right
targetPosition: left
type: custom
width: 242
- data:
answer: '{{#llm.text#}}'
selected: false
title: 直接回复
title: Reply
type: answer
variables: []
height: 103
id: answer
position:
x: 680
x: 380
y: 282
positionAbsolute:
x: 680
x: 380
y: 282
selected: false
sourcePosition: right
+6 -60
View File
@@ -6,12 +6,7 @@ app:
mode: workflow
name: file_auto_test
use_icon_as_answer_icon: false
dependencies:
- current_identifier: null
type: marketplace
value:
marketplace_plugin_unique_identifier: langgenius/tongyi:0.1.48@966d88dc40611f067311c1c9839139ebc4b55bff471bc5e736dc3e828bc67b46
version: null
dependencies: []
kind: app
version: 0.6.0
workflow:
@@ -70,21 +65,9 @@ workflow:
isInIteration: false
isInLoop: false
sourceType: start
targetType: llm
id: 1779693724732-source-1779693759949-target
source: '1779693724732'
sourceHandle: source
target: '1779693759949'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
isInLoop: false
sourceType: llm
targetType: end
id: 1779693759949-source-1779693765299-target
source: '1779693759949'
id: 1779693724732-source-1779693765299-target
source: '1779693724732'
sourceHandle: source
target: '1779693765299'
targetHandle: target
@@ -93,7 +76,7 @@ workflow:
nodes:
- data:
selected: true
title: 用户输入
title: User Input
type: start
variables:
- allowed_file_extensions: []
@@ -140,46 +123,9 @@ workflow:
type: custom
width: 242
- data:
context:
enabled: false
variable_selector: []
model:
completion_params:
temperature: 0.7
mode: chat
name: qwen3.6-plus
provider: langgenius/tongyi/tongyi
prompt_template:
- id: bb929f8f-5fa9-415b-91c3-c30228488dcf
role: system
text: 直接输出内容:hello
outputs: []
selected: false
title: LLM
type: llm
vision:
enabled: false
height: 88
id: '1779693759949'
position:
x: 382
y: 282
positionAbsolute:
x: 382
y: 282
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 242
- data:
outputs:
- value_selector:
- '1779693759949'
- text
value_type: string
variable: x
selected: false
title: 输出
title: Output
type: end
height: 88
id: '1779693765299'
+7 -59
View File
@@ -6,12 +6,7 @@ app:
mode: workflow
name: auto_test_workspace2
use_icon_as_answer_icon: false
dependencies:
- current_identifier: null
type: marketplace
value:
marketplace_plugin_unique_identifier: langgenius/tongyi:0.1.48@966d88dc40611f067311c1c9839139ebc4b55bff471bc5e736dc3e828bc67b46
version: null
dependencies: []
kind: app
version: 0.6.0
workflow:
@@ -70,21 +65,9 @@ workflow:
isInIteration: false
isInLoop: false
sourceType: start
targetType: llm
id: 1780305524693-source-1780305526186-target
source: '1780305524693'
sourceHandle: source
target: '1780305526186'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
isInLoop: false
sourceType: llm
targetType: end
id: 1780305526186-source-1780305600095-target
source: '1780305526186'
id: 1780305524693-source-1780305600095-target
source: '1780305524693'
sourceHandle: source
target: '1780305600095'
targetHandle: target
@@ -93,7 +76,7 @@ workflow:
nodes:
- data:
selected: false
title: 用户输入
title: User Input
type: start
variables: []
height: 73
@@ -109,45 +92,9 @@ workflow:
type: custom
width: 242
- data:
context:
enabled: false
variable_selector: []
model:
completion_params:
temperature: 0.7
mode: chat
name: qwen3.6-plus
provider: langgenius/tongyi/tongyi
prompt_template:
- id: cd753cdd-d950-44bf-99ad-7cb19f42d5b6
role: system
text: 输出内容:hello
outputs: []
selected: false
title: LLM
type: llm
vision:
enabled: false
height: 88
id: '1780305526186'
position:
x: 382
y: 282
positionAbsolute:
x: 382
y: 282
sourcePosition: right
targetPosition: left
type: custom
width: 242
- data:
outputs:
- value_selector:
- '1780305526186'
- text
value_type: string
variable: x
selected: false
title: 输出
title: Output
type: end
height: 88
id: '1780305600095'
@@ -157,6 +104,7 @@ workflow:
positionAbsolute:
x: 684
y: 282
selected: false
sourcePosition: right
targetPosition: left
type: custom
+8 -11
View File
@@ -8,11 +8,13 @@
* withTempConfig) to prevent session state leaking between tests.
*/
import type { TokenDoc } from '@/store/token-store'
import { Buffer } from 'node:buffer'
import { execSync, spawn } from 'node:child_process'
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import yaml from 'js-yaml'
/** Path to the dev entry point — no build required. */
export const BIN = resolve(__dirname, '../../../bin/dev.js')
@@ -208,13 +210,11 @@ function splitHost(host: string): { bare: string, scheme: string } {
}
async function writeFileToken(configDir: string, host: string, email: string, bearer: string): Promise<void> {
const dotParts = `tokens.${host}.${email}`.split('.')
let yaml = ''
for (let i = 0; i < dotParts.length - 1; i++) {
yaml += `${' '.repeat(i) + dotParts[i]}:\n`
const doc: TokenDoc = {
version: 1,
tokens: { [host]: { [email]: bearer } },
}
yaml += `${' '.repeat(dotParts.length - 1) + (dotParts[dotParts.length - 1] ?? '')}: "${bearer}"\n`
await writeFile(join(configDir, 'tokens.yml'), yaml, { mode: 0o600 })
await writeFile(join(configDir, 'tokens.yml'), yaml.dump(doc, { lineWidth: -1, noRefs: true }), { mode: 0o600 })
}
/**
@@ -289,11 +289,8 @@ export async function injectAuth(configDir: string, opts: AuthInjectionOptions):
new Entry('difyctl', account).setPassword(JSON.stringify(opts.bearer))
}
else {
// Fall back to tokens.yml.
// YamlStore.doGet splits the key on '.' and traverses the nested object,
// so "tokens.localhost.user@dify.ai" splits into 4 parts:
// tokens -> localhost -> user@dify -> ai
// The YAML must mirror that exact nesting.
// Fall back to tokens.yml — FileTokenStore uses getTyped<TokenDoc>()
// which expects flat tokens[host][email] with version: 1.
await writeFileToken(configDir, bare, email, opts.bearer)
}
}
@@ -349,9 +349,11 @@ describe('E2E / agent skill — describe app -o json (auth required)', () => {
assertPipeFriendlyJson(r)
})
itWithAuth('[P0] nonexistent app → exit 1 + JSON error envelope', async () => {
itWithAuth('[P0] invalid (non-UUID) app id → exit 2 + usage error envelope', async () => {
// 'app-id-nonexistent-e2e-xyz' is not a valid UUID; describe app rejects it
// client-side via isValidUuid() with usage_invalid_flag (exit 2).
const r = await fx.r(['describe', 'app', 'app-id-nonexistent-e2e-xyz', '-o', 'json'])
expect(r.exitCode).toBe(1)
expect(r.exitCode).toBe(2)
assertErrorEnvelope(r)
})
})
@@ -131,11 +131,12 @@ describe('E2E / difyctl describe app', () => {
// ── Not found ─────────────────────────────────────────────────────────────
it('[P0] non-existent app returns exit code 1 with not-found error (3.83)', async () => {
// Spec 3.83: describe non-existent app → stderr contains not-found, exit code 1.
it('[P0] invalid (non-UUID) app id returns usage error (exit code 2)', async () => {
// NONEXISTENT_ID is not a valid UUID, so the CLI rejects it client-side via
// isValidUuid() before making any network request → usage_invalid_flag (exit 2).
const result = await fx.r(['describe', 'app', NONEXISTENT_ID])
expect(result.exitCode, 'non-existent app should exit with code 1').toBe(1)
expect(result.stderr).toMatch(/not.?found|404|does not exist|server_5xx/i)
expect(result.exitCode, 'invalid UUID should exit with code 2').toBe(2)
expect(result.stderr).toMatch(/uuid|valid|usage_invalid_flag/i)
})
it('[P1] non-existent app in JSON mode outputs JSON error envelope', async () => {
@@ -37,6 +37,7 @@ import { afterEach, beforeEach, describe, expect, inject, it } from 'vitest'
import { ZERO } from '@/util/uuid.js'
import {
assertErrorEnvelope,
assertExitCode,
assertNoAnsi,
assertNonZeroExit,
} from '../../helpers/assert.js'
@@ -141,7 +142,7 @@ describe('E2E / error message standards (spec 5.3)', () => {
// Spec 5.70: submitting a value of the wrong type must fail.
// The workflow app (workflowAppId) expects x as a string; passing a JSON
// number causes the server to reject the request.
// In v1.0 the server returns HTTP 500 for type validation failures.
// After the @accepts/@returns contract unification, the server returns HTTP 422 for request schema failures.
const result = await fx.r([
'run',
'app',
@@ -156,6 +157,206 @@ describe('E2E / error message standards (spec 5.3)', () => {
expect(result.stderr.trim().length).toBeGreaterThan(0)
})
// ── 5.70a/b/c P4 sanitization — 422 error body is clean (no leaks) ────────
it('[P0] 5.70a validation failure message is a plain string, not double-encoded JSON', async () => {
// After the @accepts contract fix, the server aborts with
// abort(422, message="Request validation failed", errors=[...])
// The CLI wraps this into its envelope. The message field must be a plain
// human-readable string — NOT a JSON-serialised string that itself contains
// pydantic error details (which was the double-encoding bug in P4).
const result = await fx.r([
'run',
'app',
E.workflowAppId,
'--inputs',
JSON.stringify({ x: 'hello', num: 'not-a-number', enum_var: 'A', paragraph: 'ok' }),
'-o',
'json',
])
assertNonZeroExit(result)
const envelope = assertErrorEnvelope(result)
// message must be a plain string, not a JSON string (no double encoding)
expect(typeof envelope.error.message).toBe('string')
expect(() => JSON.parse(envelope.error.message)).toThrow()
})
it('[P1] 5.70b validation error response does not leak pydantic version URL', async () => {
// Before the P4 fix, exc.json() included a "url" field pointing to
// https://errors.pydantic.dev/<version>/... — exposing the server's pydantic
// version. The sanitised response must not contain this URL.
const result = await fx.r([
'run',
'app',
E.workflowAppId,
'--inputs',
JSON.stringify({ x: 'hello', num: 'not-a-number', enum_var: 'A', paragraph: 'ok' }),
'-o',
'json',
])
assertNonZeroExit(result)
expect(result.stderr).not.toMatch(/errors\.pydantic\.dev|pydantic\.dev\//)
})
it('[P1] 5.70c validation error response does not echo back user input', async () => {
// Before the P4 fix, exc.json() included the user's original "input" value
// inside the error details. The sanitised response must not repeat the
// submitted value so that sensitive payloads are not reflected to callers.
const sentValue = 'not-a-number-sentinel-12345'
const result = await fx.r([
'run',
'app',
E.workflowAppId,
'--inputs',
JSON.stringify({ x: 'hello', num: sentValue, enum_var: 'A', paragraph: 'ok' }),
'-o',
'json',
])
assertNonZeroExit(result)
expect(result.stderr).not.toContain(sentValue)
})
// ── 5.70d-h ErrorBody contract — error.server structure and rendering priorities ──
// PR #37285 introduces canonical ErrorBody on every /openapi/v1 non-2xx response.
// CLI strict-parses via zErrorBody.safeParse; success → full struct at error.server.
//
// V2 rendering priorities (format.ts, verified against codebase):
// header code : server?.code ?? cliCode — server wins, CLI fallback
// hint : cliHint ?? server?.hint — CLI wins, server fallback (V2 correction)
// details : server?.details[] — " - loc: msg (type)" per entry, no -v
it('[P0] 5.70d JSON envelope contains error.server with canonical code/status/message', async () => {
// Trigger: describe app ZERO — server returns canonical 404 ErrorBody
// { code:"not_found", status:404, message:"app not found" }.
// zErrorBody.safeParse succeeds → error.server is populated on the current server.
const result = await fx.r(['describe', 'app', ZERO, '-o', 'json'])
assertNonZeroExit(result)
const envelope = JSON.parse(result.stderr.trim()) as {
error: { code: string, server?: { code: string, status: number, message: string } }
}
expect(envelope.error.server, 'error.server must be present when server returns canonical ErrorBody').toBeDefined()
expect(typeof envelope.error.server?.code, 'error.server.code must be a string').toBe('string')
expect(envelope.error.server?.code.length).toBeGreaterThan(0)
expect(typeof envelope.error.server?.status, 'error.server.status must be a number').toBe('number')
expect(typeof envelope.error.server?.message, 'error.server.message must be a string').toBe('string')
expect(envelope.error.server?.message.length).toBeGreaterThan(0)
})
it('[P1] 5.70e @accepts query validation returns canonical 422 with details array', async () => {
// Trigger: direct fetch to GET /apps?page=not-integer — @accepts(query=AppListQuery)
// validates page as int and emits canonical 422 ErrorBody with details[].
// Direct fetch is used because the CLI validates --page as integer client-side
// (would exit 2 before hitting the server); this pins the server-side contract.
const res = await fetch(
`${E.host.replace(/\/$/, '')}/openapi/v1/apps?workspace_id=${E.workspaceId}&page=not-an-integer`,
{ headers: { Authorization: `Bearer ${E.token}` }, signal: AbortSignal.timeout(8_000) },
)
expect(res.status).toBe(422)
const body = await res.json() as {
code?: string
status?: number
details?: Array<{ type: string, loc: Array<string | number>, msg: string }>
}
expect(body.code).toBe('invalid_param')
expect(body.status).toBe(422)
expect(Array.isArray(body.details), 'details must be an array').toBe(true)
expect(body.details!.length).toBeGreaterThan(0)
const entry = body.details![0]!
expect(typeof entry.type).toBe('string')
expect(typeof entry.msg).toBe('string')
expect(Array.isArray(entry.loc)).toBe(true)
})
it('[P1] 5.70g rendering priority — header code: server code wins over CLI classification code', async () => {
// renderHuman: headerCode = server?.code ?? e.code (server wins, V2 unchanged)
// When canonical ErrorBody is parsed, the server semantic code replaces the CLI
// classification code ("server_4xx_other") in the human-readable output header.
// Trigger: describe app ZERO → canonical 404; header starts with "not_found:".
const result = await fx.r(['describe', 'app', ZERO])
assertNonZeroExit(result)
expect(result.stderr.trimStart()).not.toMatch(/^server_4xx_other:/)
expect(result.stderr.trimStart()).toMatch(/^not_found:/)
})
it('[P1] 5.70g2 rendering priority — hint: CLI hint wins over server hint (V2 correction)', async () => {
// renderHuman: hint = cliHint ?? server?.hint (CLI wins — V2 spec correction)
// V1 incorrectly documented "server wins"; V2 aligns with codebase: CLI wins.
// Test: 401 AuthExpired — classifyResponse sets c.hint = AUTH_LOGIN_HINT before
// serverError is parsed; CLI hint takes precedence over any server-provided hint.
// Verified on current server (no @accepts deployment required).
const unauthTmp = await withTempConfig()
try {
const result = await run(['get', 'app', '-o', 'json'], { configDir: unauthTmp.configDir })
assertExitCode(result, 4)
const envelope = JSON.parse(result.stderr.trim()) as { error: { hint?: string } }
expect(envelope.error.hint, 'CLI login hint must appear for auth error').toMatch(/auth login/i)
}
finally {
await unauthTmp.cleanup()
}
})
it('[P1] 5.70h JSON envelope: error.code = CLI classification; error.server.code = server semantic code', async () => {
// toEnvelope() sets error.code from HTTP status bucket (e.g. "server_4xx_other")
// while the server's semantic code is separate in error.server.code.
// Agents can branch on error.server.code without parsing human-readable text.
// Trigger: describe app ZERO → canonical 404; error.code="server_4xx_other",
// error.server.code="not_found" — always distinct when ErrorBody is present.
const result = await fx.r(['describe', 'app', ZERO, '-o', 'json'])
assertNonZeroExit(result)
const envelope = JSON.parse(result.stderr.trim()) as {
error: { code: string, server?: { code: string } }
}
expect(envelope.error.code).toBe('server_4xx_other')
expect(envelope.error.server?.code).toBeDefined()
expect(envelope.error.server?.code).not.toBe('server_4xx_other')
})
// ── 5.70i / 5.70j PR #37285 boundary contract ───────────────────────────
it('[P1] 5.70i unknown /openapi/v1 route returns canonical 404 ErrorBody without route suggestions', async () => {
// PR #37285: ExternalApi._help_on_404 suppresses flask-restx route enumeration.
// Previously, an unknown path under /openapi/v1/ returned flask-restx's default
// 404 with a "Did you mean /openapi/v1/apps?" suggestion, leaking the route table.
// After the fix it must return a canonical ErrorBody and contain no suggestions.
const res = await fetch(`${E.host.replace(/\/$/, '')}/openapi/v1/this-path-does-not-exist-e2e`, {
headers: { Authorization: `Bearer ${E.token}` },
signal: AbortSignal.timeout(8_000),
})
expect(res.status).toBe(404)
const body = await res.json() as Record<string, unknown>
// canonical ErrorBody fields must be present
expect(typeof body.code, '404 body must have a string code field').toBe('string')
expect(body.status, '404 body must have status: 404').toBe(404)
// no flask-restx route enumeration in the response
const raw = JSON.stringify(body)
expect(raw).not.toMatch(/did you mean/i)
expect(raw).not.toMatch(/you might want/i)
})
it('[P1] 5.70j device-flow 4xx uses RFC 8628 format, not ErrorBody — zErrorBody parse fails gracefully', async () => {
// PR #37285 explicitly excludes RFC 8628 device-flow endpoints from the
// ErrorBody contract. This test pins that contract:
// - The device/token endpoint returns RFC 8628 {error: string} on failure,
// not the canonical {code, status, message} shape.
// - When the CLI's classifyResponse encounters this, zErrorBody.safeParse
// returns failure → serverError = undefined → generic status-based message,
// no error.server field, no crash.
const res = await fetch(`${E.host.replace(/\/$/, '')}/openapi/v1/oauth/device/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device_code: 'fake-invalid-device-code-e2e-test', client_id: 'difyctl' }),
signal: AbortSignal.timeout(8_000),
})
// device flow errors are 4xx (400 bad_request or 401 expired_token etc.)
expect(res.status).toBeGreaterThanOrEqual(400)
expect(res.status).toBeLessThan(500)
const body = await res.json() as Record<string, unknown>
// RFC 8628 shape: has 'error' string, must NOT have ErrorBody 'code'/'status' pair
expect(typeof body.error, 'RFC 8628 body must have a string error field').toBe('string')
expect(body).not.toHaveProperty('status')
// zErrorBody.safeParse would fail → CLI sets serverError = undefined → generic message
})
// ── 5.76 Failed command + -o yaml → stderr is still JSON envelope ────────
it('[P1] 5.76 failed command with -o yaml still outputs a JSON error envelope on stderr', async () => {
+407
View File
@@ -0,0 +1,407 @@
/**
* E2E: difyctl get/create/delete/set member — Member Management
*
* Data lifecycle:
* beforeAll — generates two random emails, invites them as test fixtures
* afterAll — removes both fixtures (best-effort)
*
* Email format: auto_test+<timestamp>@dify.ai
* No extra env vars required.
*
* JSON response shape (MemberListResponse):
* { page, limit, total, has_more, data: MemberResponse[] }
*
* MemberResponse fields:
* { id, name, email, role, status, avatar?, current: bool }
*/
import type { AuthFixture } from '../../helpers/cli.js'
import { afterAll, beforeAll, describe, expect, inject, it } from 'vitest'
import {
assertErrorEnvelope,
assertExitCode,
assertJson,
assertNoAnsi,
assertNonZeroExit,
} from '../../helpers/assert.js'
import { run, withAuthFixture, withTempConfig } from '../../helpers/cli.js'
import { resolveEnv } from '../../setup/env.js'
// @ts-expect-error — see test/e2e/helpers/vitest-context.ts for explanation
const caps = inject('e2eCapabilities') as import('../../setup/env.js').E2ECapabilities
const E = resolveEnv(caps)
// ── Fixture state ─────────────────────────────────────────────────────────────
let fx: AuthFixture
/** ID of the member used by get / set tests. */
let testMemberId: string
/** ID of the member reserved for the delete-success test. */
let deleteTargetId: string
const ts = Date.now()
const memberEmail = `auto_test+${ts}@dify.ai`
const deleteTargetEmail = `auto_test+${ts + 1}@dify.ai`
// ── Response type helpers ─────────────────────────────────────────────────────
type MemberItem = {
id: string
name: string
email: string
role: string
status: string
current?: boolean
}
type MemberListJson = {
data: MemberItem[]
total: number
page: number
limit: number
has_more: boolean
}
// ── Setup / teardown ──────────────────────────────────────────────────────────
beforeAll(async () => {
fx = await withAuthFixture(E)
// Invite the main test member; capture member_id from response
const createMain = await fx.r([
'create',
'member',
'--email',
memberEmail,
'--role',
'normal',
'-o',
'json',
])
if (createMain.exitCode !== 0) {
throw new Error(
`beforeAll: failed to create test member (${memberEmail}): ${createMain.stderr}`,
)
}
const mainData = JSON.parse(createMain.stdout.trim()) as { member_id?: string }
testMemberId = mainData.member_id as string
if (!testMemberId)
throw new Error(`beforeAll: missing member_id in: ${createMain.stdout}`)
// Invite the delete-target member
const createTarget = await fx.r([
'create',
'member',
'--email',
deleteTargetEmail,
'--role',
'normal',
'-o',
'json',
])
if (createTarget.exitCode !== 0) {
throw new Error(
`beforeAll: failed to create delete-target member (${deleteTargetEmail}): ${createTarget.stderr}`,
)
}
const targetData = JSON.parse(createTarget.stdout.trim()) as { member_id?: string }
deleteTargetId = targetData.member_id as string
if (!deleteTargetId)
throw new Error(`beforeAll: missing member_id in: ${createTarget.stdout}`)
})
afterAll(async () => {
if (testMemberId) {
await fx.r(['delete', 'member', testMemberId, '--yes']).catch(() => {})
}
if (deleteTargetId) {
await fx.r(['delete', 'member', deleteTargetId, '--yes']).catch(() => {})
}
await fx.cleanup()
})
// ── get member ────────────────────────────────────────────────────────────────
describe('E2E / difyctl get member', () => {
it('[P0] member list contains the created test member', async () => {
const result = await fx.r(['get', 'member', '-o', 'json'])
assertExitCode(result, 0)
const data = assertJson<MemberListJson>(result)
const ids = (data.data ?? []).map(m => m.id)
expect(ids, `testMemberId ${testMemberId} must appear in member list`).toContain(testMemberId)
})
it('[P0] default table output contains required column headers', async () => {
const result = await fx.r(['get', 'member'])
assertExitCode(result, 0)
expect(result.stdout).toMatch(/\bID\b/)
expect(result.stdout).toMatch(/\bNAME\b/)
expect(result.stdout).toMatch(/\bEMAIL\b/)
expect(result.stdout).toMatch(/\bROLE\b/)
expect(result.stdout).toMatch(/\bSTATUS\b/)
})
it('[P0] authenticated account appears in member list', async () => {
// The token owner (auto_test@dify.ai) must appear in the member list
const result = await fx.r(['get', 'member', '-o', 'json'])
assertExitCode(result, 0)
const data = assertJson<MemberListJson>(result)
const ownerRow = data.data.find(m => m.email === E.email)
expect(ownerRow, `owner email ${E.email} must be in member list`).toBeDefined()
expect(ownerRow?.role).toBe('owner')
expect(ownerRow?.status).toBe('active')
})
it('[P0] -o json returns valid JSON with data array', async () => {
const result = await fx.r(['get', 'member', '-o', 'json'])
assertExitCode(result, 0)
const data = assertJson<MemberListJson>(result)
expect(Array.isArray(data.data), 'data must be an array').toBe(true)
expect(data.data.length).toBeGreaterThan(0)
})
it('[P0] -o json each member has id, email, role, status fields', async () => {
const result = await fx.r(['get', 'member', '-o', 'json'])
assertExitCode(result, 0)
const data = assertJson<MemberListJson>(result)
const member = data.data[0]!
expect(typeof member.id).toBe('string')
expect(typeof member.email).toBe('string')
expect(typeof member.role).toBe('string')
expect(typeof member.status).toBe('string')
})
it('[P0] output has no ANSI colour codes (non-TTY)', async () => {
const result = await fx.r(['get', 'member'])
assertExitCode(result, 0)
assertNoAnsi(result.stdout, 'stdout')
})
it('[P0] unauthenticated get member returns auth error (exit code 4)', async () => {
const tmp = await withTempConfig()
try {
const result = await run(['get', 'member'], { configDir: tmp.configDir })
assertExitCode(result, 4)
expect(result.stderr).toMatch(/not.?logged.?in|auth.?login/i)
}
finally {
await tmp.cleanup()
}
})
it('[P1] -o yaml returns valid YAML (non-empty, no JSON braces)', async () => {
const result = await fx.r(['get', 'member', '-o', 'yaml'])
assertExitCode(result, 0)
expect(result.stdout.trim().length).toBeGreaterThan(0)
expect(result.stdout.trimStart()).not.toMatch(/^\{/)
})
it('[P1] -o json output is pipe-friendly (no ANSI, ends with newline)', async () => {
const result = await fx.r(['get', 'member', '-o', 'json'])
assertExitCode(result, 0)
assertNoAnsi(result.stdout, 'stdout')
expect(result.stdout.endsWith('\n')).toBe(true)
})
it('[P1] -w overrides the workspace', async () => {
const result = await fx.r(['get', 'member', '-w', E.workspaceId, '-o', 'json'])
assertExitCode(result, 0)
const data = assertJson<MemberListJson>(result)
expect(Array.isArray(data.data)).toBe(true)
})
})
// ── set member ────────────────────────────────────────────────────────────────
describe('E2E / difyctl set member', () => {
it('[P0] owner/admin can promote normal → admin', async () => {
const result = await fx.r(['set', 'member', testMemberId, '--role', 'admin', '-o', 'json'])
assertExitCode(result, 0)
const list = await fx.r(['get', 'member', '-o', 'json'])
const data = assertJson<MemberListJson>(list)
const updated = data.data.find(m => m.id === testMemberId)
expect(updated?.role).toBe('admin')
})
it('[P0] owner/admin can demote admin → normal', async () => {
await fx.r(['set', 'member', testMemberId, '--role', 'admin'])
const result = await fx.r(['set', 'member', testMemberId, '--role', 'normal', '-o', 'json'])
assertExitCode(result, 0)
const list = await fx.r(['get', 'member', '-o', 'json'])
const data = assertJson<MemberListJson>(list)
const updated = data.data.find(m => m.id === testMemberId)
expect(updated?.role).toBe('normal')
})
it('[P0] --role owner is rejected client-side (exit 2, no API call)', async () => {
const result = await fx.r(['set', 'member', testMemberId, '--role', 'owner'])
assertExitCode(result, 2)
expect(result.stderr).toMatch(/invalid|role|owner/i)
})
it('[P0] missing --role returns usage error', async () => {
const result = await fx.r(['set', 'member', testMemberId])
expect(result.exitCode).not.toBe(0)
expect(result.stderr).toMatch(/role|required|missing/i)
})
it('[P0] unauthenticated set member returns auth error (exit 4)', async () => {
const tmp = await withTempConfig()
try {
const result = await run(['set', 'member', testMemberId, '--role', 'normal'], {
configDir: tmp.configDir,
})
assertExitCode(result, 4)
}
finally {
await tmp.cleanup()
}
})
it('[P1] missing member-id returns usage error', async () => {
const result = await fx.r(['set', 'member', '--role', 'normal'])
expect(result.exitCode).not.toBe(0)
expect(result.stderr).toMatch(/missing|required|arg|member/i)
})
it('[P1] non-existent member-id returns server error', async () => {
const result = await fx.r([
'set',
'member',
'00000000-0000-0000-0000-000000000000',
'--role',
'normal',
])
assertNonZeroExit(result)
expect(result.stderr.trim().length).toBeGreaterThan(0)
})
})
// ── create member — error paths ───────────────────────────────────────────────
describe('E2E / difyctl create member (error paths)', () => {
it('[P0] --role with invalid value is rejected client-side (exit 2)', async () => {
const result = await fx.r([
'create',
'member',
'--email',
`auto_test+unused${Date.now()}@dify.ai`,
'--role',
'superadmin',
])
assertExitCode(result, 2)
expect(result.stderr).toMatch(/invalid|role/i)
})
it('[P0] --role owner is rejected client-side (exit 2)', async () => {
const result = await fx.r([
'create',
'member',
'--email',
`auto_test+unused${Date.now()}@dify.ai`,
'--role',
'owner',
])
assertExitCode(result, 2)
expect(result.stderr).toMatch(/invalid|role|owner/i)
})
it('[P0] missing --email returns usage error', async () => {
const result = await fx.r(['create', 'member', '--role', 'normal'])
expect(result.exitCode).not.toBe(0)
expect(result.stderr).toMatch(/email|required|missing/i)
})
it('[P0] missing --role returns usage error', async () => {
const result = await fx.r(['create', 'member', '--email', memberEmail])
expect(result.exitCode).not.toBe(0)
expect(result.stderr).toMatch(/role|required|missing/i)
})
it('[P0] unauthenticated create member returns auth error (exit 4)', async () => {
const tmp = await withTempConfig()
try {
const result = await run(
['create', 'member', '--email', `auto_test+unauth${Date.now()}@dify.ai`, '--role', 'normal'],
{ configDir: tmp.configDir },
)
assertExitCode(result, 4)
}
finally {
await tmp.cleanup()
}
})
})
// ── delete member ─────────────────────────────────────────────────────────────
describe('E2E / difyctl delete member', () => {
it('[P0] owner/admin can remove a member from the workspace', async () => {
const result = await fx.r(['delete', 'member', deleteTargetId, '--yes'])
assertExitCode(result, 0)
const list = await fx.r(['get', 'member', '-o', 'json'])
const data = assertJson<MemberListJson>(list)
const ids = data.data.map(m => m.id)
expect(ids).not.toContain(deleteTargetId)
deleteTargetId = ''
})
it('[P0] attempting to delete self returns server error', async () => {
const list = await fx.r(['get', 'member', '-o', 'json'])
const data = assertJson<MemberListJson>(list)
const self = data.data.find(m => m.email === E.email)
if (!self) {
console.warn('[E2E] could not identify self in member list — skipping')
return
}
const result = await fx.r(['delete', 'member', self.id, '--yes'])
assertNonZeroExit(result)
expect(result.stderr).toMatch(/self|yourself|cannot|not.*allow|400|forbidden/i)
})
it('[P0] missing member-id argument returns usage error', async () => {
const result = await fx.r(['delete', 'member'])
expect(result.exitCode).not.toBe(0)
expect(result.stderr).toMatch(/missing|required|arg|member/i)
})
it('[P0] unauthenticated delete member returns auth error (exit 4)', async () => {
const tmp = await withTempConfig()
try {
const result = await run(
['delete', 'member', '00000000-0000-0000-0000-000000000000', '--yes'],
{ configDir: tmp.configDir },
)
assertExitCode(result, 4)
}
finally {
await tmp.cleanup()
}
})
it('[P1] non-existent member-id returns server error', async () => {
const result = await fx.r([
'delete',
'member',
'00000000-0000-0000-0000-000000000000',
'--yes',
])
assertNonZeroExit(result)
expect(result.stderr.trim().length).toBeGreaterThan(0)
})
it('[P1] -o json outputs structured envelope on error', async () => {
const result = await fx.r([
'delete',
'member',
'00000000-0000-0000-0000-000000000000',
'--yes',
'-o',
'json',
])
assertNonZeroExit(result)
assertErrorEnvelope(result)
})
})
+22 -2
View File
@@ -8,7 +8,7 @@
*
* Staging app prerequisites (specified via DIFY_E2E_* env vars):
* echo-chat — mode=chat, query variable, outputs "echo: {query}"
* echo-workflow — mode=workflow, x variable (required), outputs "echo: {x}"
* echo-workflow — mode=workflow, x variable (required), outputs x directly (no echo prefix)
*/
import type { AuthFixture } from '../../helpers/cli.js'
@@ -263,7 +263,7 @@ describe('E2E / difyctl run app', () => {
JSON.stringify({ x: 'hello', num: 42, enum_var: 'A', paragraph: 'short text' }),
])
assertExitCode(happyResult, 0)
assertStdoutContains(happyResult, 'echo:hello')
assertStdoutContains(happyResult, 'hello') // workflow outputs x directly; echo: prefix removed (no sandbox on server)
// ── 4.1.17: number field receives a string value ─────────────────────
const typedResult = await fx.r([
@@ -289,6 +289,26 @@ describe('E2E / difyctl run app', () => {
})
})
it('[P1] validation failure returns http_status 422 in JSON error envelope', async () => {
// After the @accepts/@returns server contract unification, input schema
// validation failures consistently return HTTP 422 (not 400 or 500).
// This verifies the CLI propagates the unified status code.
const result = await fx.r([
'run',
'app',
E.workflowAppId,
'--inputs',
JSON.stringify({ x: 'hello', num: 'not-a-number', enum_var: 'A', paragraph: 'ok' }),
'-o',
'json',
])
expect(result.exitCode).not.toBe(0)
const envelope = JSON.parse(result.stderr.trim()) as {
error: { code: string, message: string, http_status?: number }
}
expect(envelope.error.http_status, 'validation failure must return http_status 422').toBe(422)
})
// =========================================================================
// Error scenarios
// =========================================================================
@@ -118,7 +118,7 @@ describe('E2E / difyctl run app --conversation', () => {
'invalid-conv-id-xyz-not-exist',
])
assertExitCode(result, 1)
expect(result.stderr).toMatch(/not.?found|conversation|404/i)
expect(result.stderr).toMatch(/not.?found|conversation|404|422|validation/i)
})
// ── Combined flags ──────────────────────────────────────────────────────
@@ -277,7 +277,7 @@ describe('E2E / difyctl run app --stream (specialisation)', () => {
'--stream',
])
expect(result.exitCode, 'wrong-type input should cause non-zero exit').not.toBe(0)
expect(result.stderr).toMatch(/validation|invalid|type|400|server_5xx|must be/i)
expect(result.stderr).toMatch(/validation|invalid|type|422|must be/i)
})
// ── Non-existent app with positional query (4.2.16) ────────────────────
+2
View File
@@ -92,6 +92,8 @@ export default defineConfig({
'test/e2e/suites/framework/**/*.e2e.ts',
// discovery (get app / describe app)
'test/e2e/suites/discovery/**/*.e2e.ts',
// member management (get/create/delete/set member)
'test/e2e/suites/member/**/*.e2e.ts',
// dsl (export / import)
'test/e2e/suites/dsl/**/*.e2e.ts',
// run tests (require valid token)
@@ -18,11 +18,13 @@ export type AgentAppCreatePayload = {
icon_background?: string | null
icon_type?: IconType | null
name: string
role?: string
}
export type AppDetailWithSite = {
access_mode?: string | null
api_base_url?: string | null
app_id?: string | null
bound_agent_id?: string | null
created_at?: number | null
created_by?: string | null
@@ -39,6 +41,7 @@ export type AppDetailWithSite = {
mode: string
model_config?: ModelConfig | null
name: string
role?: string | null
site?: Site | null
tags?: Array<Tag>
tracing?: JsonValue | null
@@ -56,13 +59,14 @@ export type AgentInviteOptionsResponse = {
total: number
}
export type UpdateAppPayload = {
export type AgentAppUpdatePayload = {
description?: string | null
icon?: string | null
icon_background?: string | null
icon_type?: IconType | null
max_active_requests?: number | null
name: string
role?: string | null
use_icon_as_answer_icon?: boolean | null
}
@@ -255,6 +259,7 @@ export type AgentConfigSnapshotDetailResponse = {
export type AppPartial = {
access_mode?: string | null
app_id?: string | null
author_name?: string | null
bound_agent_id?: string | null
create_user_name?: string | null
@@ -272,6 +277,7 @@ export type AppPartial = {
mode: string
model_config?: ModelConfigPartial | null
name: string
role?: string | null
tags?: Array<Tag>
updated_at?: number | null
updated_by?: string | null
@@ -1219,6 +1225,7 @@ export type AppPaginationWritable = {
export type AppDetailWithSiteWritable = {
access_mode?: string | null
api_base_url?: string | null
app_id?: string | null
bound_agent_id?: string | null
created_at?: number | null
created_by?: string | null
@@ -1234,6 +1241,7 @@ export type AppDetailWithSiteWritable = {
mode: string
model_config?: ModelConfig | null
name: string
role?: string | null
site?: SiteWritable | null
tags?: Array<Tag>
tracing?: JsonValue | null
@@ -1245,6 +1253,7 @@ export type AppDetailWithSiteWritable = {
export type AppPartialWritable = {
access_mode?: string | null
app_id?: string | null
author_name?: string | null
bound_agent_id?: string | null
create_user_name?: string | null
@@ -1261,6 +1270,7 @@ export type AppPartialWritable = {
mode: string
model_config?: ModelConfigPartial | null
name: string
role?: string | null
tags?: Array<Tag>
updated_at?: number | null
updated_by?: string | null
@@ -1387,7 +1397,7 @@ export type GetAgentByAgentIdResponses = {
export type GetAgentByAgentIdResponse = GetAgentByAgentIdResponses[keyof GetAgentByAgentIdResponses]
export type PutAgentByAgentIdData = {
body: UpdateAppPayload
body: AgentAppUpdatePayload
path: {
agent_id: string
}
@@ -92,18 +92,20 @@ export const zAgentAppCreatePayload = z.object({
icon_background: z.string().nullish(),
icon_type: zIconType.nullish(),
name: z.string().min(1),
role: z.string().max(255).optional().default(''),
})
/**
* UpdateAppPayload
* AgentAppUpdatePayload
*/
export const zUpdateAppPayload = z.object({
export const zAgentAppUpdatePayload = z.object({
description: z.string().max(400).nullish(),
icon: z.string().nullish(),
icon_background: z.string().nullish(),
icon_type: zIconType.nullish(),
max_active_requests: z.int().nullish(),
name: z.string().min(1),
role: z.string().max(255).nullish(),
use_icon_as_answer_icon: z.boolean().nullish(),
})
@@ -473,6 +475,7 @@ export const zModelConfigPartial = z.object({
*/
export const zAppPartial = z.object({
access_mode: z.string().nullish(),
app_id: z.string().nullish(),
author_name: z.string().nullish(),
bound_agent_id: z.string().nullish(),
create_user_name: z.string().nullish(),
@@ -490,6 +493,7 @@ export const zAppPartial = z.object({
mode: z.string(),
model_config: zModelConfigPartial.nullish(),
name: z.string(),
role: z.string().nullish(),
tags: z.array(zTag).optional(),
updated_at: z.int().nullish(),
updated_by: z.string().nullish(),
@@ -531,6 +535,7 @@ export const zModelConfig = z.object({
export const zAppDetailWithSite = z.object({
access_mode: z.string().nullish(),
api_base_url: z.string().nullish(),
app_id: z.string().nullish(),
bound_agent_id: z.string().nullish(),
created_at: z.int().nullish(),
created_by: z.string().nullish(),
@@ -547,6 +552,7 @@ export const zAppDetailWithSite = z.object({
mode: z.string(),
model_config: zModelConfig.nullish(),
name: z.string(),
role: z.string().nullish(),
site: zSite.nullish(),
tags: z.array(zTag).optional(),
tracing: zJsonValue.nullish(),
@@ -1729,6 +1735,7 @@ export const zMessageInfiniteScrollPaginationResponse = z.object({
*/
export const zAppPartialWritable = z.object({
access_mode: z.string().nullish(),
app_id: z.string().nullish(),
author_name: z.string().nullish(),
bound_agent_id: z.string().nullish(),
create_user_name: z.string().nullish(),
@@ -1745,6 +1752,7 @@ export const zAppPartialWritable = z.object({
mode: z.string(),
model_config: zModelConfigPartial.nullish(),
name: z.string(),
role: z.string().nullish(),
tags: z.array(zTag).optional(),
updated_at: z.int().nullish(),
updated_by: z.string().nullish(),
@@ -1788,6 +1796,7 @@ export const zSiteWritable = z.object({
export const zAppDetailWithSiteWritable = z.object({
access_mode: z.string().nullish(),
api_base_url: z.string().nullish(),
app_id: z.string().nullish(),
bound_agent_id: z.string().nullish(),
created_at: z.int().nullish(),
created_by: z.string().nullish(),
@@ -1803,6 +1812,7 @@ export const zAppDetailWithSiteWritable = z.object({
mode: z.string(),
model_config: zModelConfig.nullish(),
name: z.string(),
role: z.string().nullish(),
site: zSiteWritable.nullish(),
tags: z.array(zTag).optional(),
tracing: zJsonValue.nullish(),
@@ -1880,7 +1890,7 @@ export const zGetAgentByAgentIdPath = z.object({
*/
export const zGetAgentByAgentIdResponse = zAppDetailWithSite
export const zPutAgentByAgentIdBody = zUpdateAppPayload
export const zPutAgentByAgentIdBody = zAgentAppUpdatePayload
export const zPutAgentByAgentIdPath = z.object({
agent_id: z.string(),
@@ -24,6 +24,7 @@ export type CreateAppPayload = {
export type AppDetailWithSite = {
access_mode?: string | null
api_base_url?: string | null
app_id?: string | null
bound_agent_id?: string | null
created_at?: number | null
created_by?: string | null
@@ -40,6 +41,7 @@ export type AppDetailWithSite = {
mode: string
model_config?: ModelConfig | null
name: string
role?: string | null
site?: Site | null
tags?: Array<Tag>
tracing?: JsonValue | null
@@ -1153,6 +1155,7 @@ export type ApiKeyItem = {
export type AppPartial = {
access_mode?: string | null
app_id?: string | null
author_name?: string | null
bound_agent_id?: string | null
create_user_name?: string | null
@@ -1170,6 +1173,7 @@ export type AppPartial = {
mode: string
model_config?: ModelConfigPartial | null
name: string
role?: string | null
tags?: Array<Tag>
updated_at?: number | null
updated_by?: string | null
@@ -2540,6 +2544,7 @@ export type AppPaginationWritable = {
export type AppDetailWithSiteWritable = {
access_mode?: string | null
api_base_url?: string | null
app_id?: string | null
bound_agent_id?: string | null
created_at?: number | null
created_by?: string | null
@@ -2555,6 +2560,7 @@ export type AppDetailWithSiteWritable = {
mode: string
model_config?: ModelConfig | null
name: string
role?: string | null
site?: SiteWritable | null
tags?: Array<Tag>
tracing?: JsonValue | null
@@ -2589,6 +2595,7 @@ export type WorkflowCommentDetailWritable = {
export type AppPartialWritable = {
access_mode?: string | null
app_id?: string | null
author_name?: string | null
bound_agent_id?: string | null
create_user_name?: string | null
@@ -2605,6 +2612,7 @@ export type AppPartialWritable = {
mode: string
model_config?: ModelConfigPartial | null
name: string
role?: string | null
tags?: Array<Tag>
updated_at?: number | null
updated_by?: string | null
@@ -1946,6 +1946,7 @@ export const zModelConfigPartial = z.object({
*/
export const zAppPartial = z.object({
access_mode: z.string().nullish(),
app_id: z.string().nullish(),
author_name: z.string().nullish(),
bound_agent_id: z.string().nullish(),
create_user_name: z.string().nullish(),
@@ -1963,6 +1964,7 @@ export const zAppPartial = z.object({
mode: z.string(),
model_config: zModelConfigPartial.nullish(),
name: z.string(),
role: z.string().nullish(),
tags: z.array(zTag).optional(),
updated_at: z.int().nullish(),
updated_by: z.string().nullish(),
@@ -2004,6 +2006,7 @@ export const zModelConfig = z.object({
export const zAppDetailWithSite = z.object({
access_mode: z.string().nullish(),
api_base_url: z.string().nullish(),
app_id: z.string().nullish(),
bound_agent_id: z.string().nullish(),
created_at: z.int().nullish(),
created_by: z.string().nullish(),
@@ -2020,6 +2023,7 @@ export const zAppDetailWithSite = z.object({
mode: z.string(),
model_config: zModelConfig.nullish(),
name: z.string(),
role: z.string().nullish(),
site: zSite.nullish(),
tags: z.array(zTag).optional(),
tracing: zJsonValue.nullish(),
@@ -3433,6 +3437,7 @@ export const zGeneratedAppResponseWritable = zJsonValue
*/
export const zAppPartialWritable = z.object({
access_mode: z.string().nullish(),
app_id: z.string().nullish(),
author_name: z.string().nullish(),
bound_agent_id: z.string().nullish(),
create_user_name: z.string().nullish(),
@@ -3449,6 +3454,7 @@ export const zAppPartialWritable = z.object({
mode: z.string(),
model_config: zModelConfigPartial.nullish(),
name: z.string(),
role: z.string().nullish(),
tags: z.array(zTag).optional(),
updated_at: z.int().nullish(),
updated_by: z.string().nullish(),
@@ -3492,6 +3498,7 @@ export const zSiteWritable = z.object({
export const zAppDetailWithSiteWritable = z.object({
access_mode: z.string().nullish(),
api_base_url: z.string().nullish(),
app_id: z.string().nullish(),
bound_agent_id: z.string().nullish(),
created_at: z.int().nullish(),
created_by: z.string().nullish(),
@@ -3507,6 +3514,7 @@ export const zAppDetailWithSiteWritable = z.object({
mode: z.string(),
model_config: zModelConfig.nullish(),
name: z.string(),
role: z.string().nullish(),
site: zSiteWritable.nullish(),
tags: z.array(zTag).optional(),
tracing: zJsonValue.nullish(),
+172 -89
View File
@@ -607,6 +607,8 @@ overrides:
is-core-module: npm:@nolyfill/is-core-module@^1.0.39
picomatch@>=4.0.0 <4.0.4: 4.0.4
postcss@<8.5.10: ^8.5.10
postcss-selector-parser@>=6.0.0 <6.1.3: 6.1.4
postcss-selector-parser@>=7.0.0 <7.1.3: 7.1.4
rollup@>=4.0.0 <4.59.0: 4.61.1
safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44
side-channel: npm:@nolyfill/side-channel@^1.0.44
@@ -617,6 +619,9 @@ overrides:
ws@>=8.0.0 <8.20.1: ^8.21.0
yaml@>=2.0.0 <2.8.3: 2.9.0
yauzl@<3.2.1: 3.2.1
'@babel/core@<=7.29.0': ^7.29.1
js-yaml@<=4.1.1: ^4.1.2
tar@<=7.5.15: ^7.5.16
importers:
@@ -1183,7 +1188,7 @@ importers:
version: 11.1.8
jotai:
specifier: 'catalog:'
version: 2.20.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.17)(react@19.2.7)
version: 2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7)
js-audio-recorder:
specifier: 'catalog:'
version: 1.0.7
@@ -1228,13 +1233,13 @@ importers:
version: 1.0.0
next:
specifier: 'catalog:'
version: 16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
version: 16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
next-themes:
specifier: 'catalog:'
version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
nuqs:
specifier: 'catalog:'
version: 2.8.9(next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)
version: 2.8.9(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)
pinyin-pro:
specifier: 'catalog:'
version: 3.28.1
@@ -1394,7 +1399,7 @@ importers:
version: 10.4.4(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))
'@storybook/nextjs-vite':
specifier: 'catalog:'
version: 10.4.4(@babel/core@7.29.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3)
version: 10.4.4(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3)
'@storybook/react':
specifier: 'catalog:'
version: 10.4.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3)
@@ -1532,7 +1537,7 @@ importers:
version: 3.19.3
vinext:
specifier: 'catalog:'
version: 0.1.2(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(@vitejs/plugin-rsc@0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(typescript@6.0.3)
version: 0.1.2(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(@vitejs/plugin-rsc@0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(typescript@6.0.3)
vite:
specifier: npm:@voidzero-dev/vite-plus-core@0.1.24
version: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)'
@@ -1703,56 +1708,81 @@ packages:
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
engines: {node: '>=6.9.0'}
'@babel/compat-data@7.29.0':
resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
'@babel/code-frame@7.29.7':
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
engines: {node: '>=6.9.0'}
'@babel/core@7.29.0':
resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==}
'@babel/compat-data@7.29.7':
resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==}
engines: {node: '>=6.9.0'}
'@babel/core@7.29.7':
resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==}
engines: {node: '>=6.9.0'}
'@babel/generator@7.29.1':
resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==}
engines: {node: '>=6.9.0'}
'@babel/helper-compilation-targets@7.28.6':
resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
'@babel/generator@7.29.7':
resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==}
engines: {node: '>=6.9.0'}
'@babel/helper-compilation-targets@7.29.7':
resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==}
engines: {node: '>=6.9.0'}
'@babel/helper-globals@7.28.0':
resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
engines: {node: '>=6.9.0'}
'@babel/helper-module-imports@7.28.6':
resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
'@babel/helper-globals@7.29.7':
resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
engines: {node: '>=6.9.0'}
'@babel/helper-module-transforms@7.28.6':
resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==}
'@babel/helper-module-imports@7.29.7':
resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==}
engines: {node: '>=6.9.0'}
'@babel/helper-module-transforms@7.29.7':
resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
'@babel/core': ^7.29.1
'@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
'@babel/helper-string-parser@7.29.7':
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.28.5':
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-option@7.27.1':
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
'@babel/helper-validator-identifier@7.29.7':
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
engines: {node: '>=6.9.0'}
'@babel/helpers@7.29.2':
resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==}
'@babel/helper-validator-option@7.29.7':
resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==}
engines: {node: '>=6.9.0'}
'@babel/helpers@7.29.7':
resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.29.2':
resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==}
engines: {node: '>=6.0.0'}
'@babel/parser@7.29.7':
resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/runtime@7.29.2':
resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==}
engines: {node: '>=6.9.0'}
@@ -1761,14 +1791,26 @@ packages:
resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
engines: {node: '>=6.9.0'}
'@babel/template@7.29.7':
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
engines: {node: '>=6.9.0'}
'@babel/traverse@7.29.0':
resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==}
engines: {node: '>=6.9.0'}
'@babel/traverse@7.29.7':
resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==}
engines: {node: '>=6.9.0'}
'@babel/types@7.29.0':
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'}
'@babel/types@7.29.7':
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
engines: {node: '>=6.9.0'}
'@base-ui/react@1.5.0':
resolution: {integrity: sha512-z1gSAlced1yY+iM+mHDEtIkD8UI3Ebs52MuBPxvV6f5hRutk+xvCH/wuB7hDqDzK9JG5FoMz5nhrqtSs1wjt1A==}
engines: {node: '>=14.0.0'}
@@ -5715,6 +5757,7 @@ packages:
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
cssfontparser@1.2.1:
resolution: {integrity: sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==}
@@ -7159,7 +7202,7 @@ packages:
resolution: {integrity: sha512-dnuKfU/GLi8B28RRMjQ3AfoN7kfzP8o41+AX2FmITZqEMY8PHnjABq+VkEooomLwYaGjda+pgy0yFSjaHX/ZPg==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@babel/core': '>=7.0.0'
'@babel/core': ^7.29.1
'@babel/template': '>=7.0.0'
'@types/react': '>=17.0.0'
react: '>=17.0.0'
@@ -7191,9 +7234,6 @@ packages:
js-tokens@9.0.1:
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
js-yaml@4.2.0:
resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==}
@@ -8243,12 +8283,12 @@ packages:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
postcss-selector-parser@6.0.10:
resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
postcss-selector-parser@6.1.4:
resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==}
engines: {node: '>=4'}
postcss-selector-parser@7.1.1:
resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
postcss-selector-parser@7.1.4:
resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==}
engines: {node: '>=4'}
postcss-value-parser@4.2.0:
@@ -8984,8 +9024,8 @@ packages:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
engines: {node: '>=6'}
tar@7.5.11:
resolution: {integrity: sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==}
tar@7.5.16:
resolution: {integrity: sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==}
engines: {node: '>=18'}
tiny-case@1.0.3:
@@ -9955,19 +9995,25 @@ snapshots:
js-tokens: 4.0.0
picocolors: 1.1.1
'@babel/compat-data@7.29.0': {}
'@babel/core@7.29.0':
'@babel/code-frame@7.29.7':
dependencies:
'@babel/code-frame': 7.29.0
'@babel/generator': 7.29.1
'@babel/helper-compilation-targets': 7.28.6
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
'@babel/helpers': 7.29.2
'@babel/parser': 7.29.2
'@babel/template': 7.28.6
'@babel/traverse': 7.29.0
'@babel/types': 7.29.0
'@babel/helper-validator-identifier': 7.29.7
js-tokens: 4.0.0
picocolors: 1.1.1
'@babel/compat-data@7.29.7': {}
'@babel/core@7.29.7':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.7
'@babel/helper-compilation-targets': 7.29.7
'@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
'@babel/helpers': 7.29.7
'@babel/parser': 7.29.7
'@babel/template': 7.29.7
'@babel/traverse': 7.29.7
'@babel/types': 7.29.7
'@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
debug: 4.4.3(supports-color@10.2.2)
@@ -9985,47 +10031,65 @@ snapshots:
'@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
'@babel/helper-compilation-targets@7.28.6':
'@babel/generator@7.29.7':
dependencies:
'@babel/compat-data': 7.29.0
'@babel/helper-validator-option': 7.27.1
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
'@babel/helper-compilation-targets@7.29.7':
dependencies:
'@babel/compat-data': 7.29.7
'@babel/helper-validator-option': 7.29.7
browserslist: 4.28.1
lru-cache: 5.1.1
semver: 6.3.1
'@babel/helper-globals@7.28.0': {}
'@babel/helper-module-imports@7.28.6':
'@babel/helper-globals@7.29.7': {}
'@babel/helper-module-imports@7.29.7':
dependencies:
'@babel/traverse': 7.29.0
'@babel/types': 7.29.0
'@babel/traverse': 7.29.7
'@babel/types': 7.29.7
transitivePeerDependencies:
- supports-color
'@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)':
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-module-imports': 7.28.6
'@babel/helper-validator-identifier': 7.28.5
'@babel/traverse': 7.29.0
'@babel/core': 7.29.7
'@babel/helper-module-imports': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
'@babel/traverse': 7.29.7
transitivePeerDependencies:
- supports-color
'@babel/helper-string-parser@7.27.1': {}
'@babel/helper-string-parser@7.29.7': {}
'@babel/helper-validator-identifier@7.28.5': {}
'@babel/helper-validator-option@7.27.1': {}
'@babel/helper-validator-identifier@7.29.7': {}
'@babel/helpers@7.29.2':
'@babel/helper-validator-option@7.29.7': {}
'@babel/helpers@7.29.7':
dependencies:
'@babel/template': 7.28.6
'@babel/types': 7.29.0
'@babel/template': 7.29.7
'@babel/types': 7.29.7
'@babel/parser@7.29.2':
dependencies:
'@babel/types': 7.29.0
'@babel/parser@7.29.7':
dependencies:
'@babel/types': 7.29.7
'@babel/runtime@7.29.2': {}
'@babel/template@7.28.6':
@@ -10034,6 +10098,12 @@ snapshots:
'@babel/parser': 7.29.2
'@babel/types': 7.29.0
'@babel/template@7.29.7':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
'@babel/traverse@7.29.0':
dependencies:
'@babel/code-frame': 7.29.0
@@ -10046,11 +10116,28 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/traverse@7.29.7':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.7
'@babel/helper-globals': 7.29.7
'@babel/parser': 7.29.7
'@babel/template': 7.29.7
'@babel/types': 7.29.7
debug: 4.4.3(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
'@babel/types@7.29.0':
dependencies:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
'@babel/types@7.29.7':
dependencies:
'@babel/helper-string-parser': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
'@base-ui/react@1.5.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
dependencies:
'@babel/runtime': 7.29.2
@@ -10761,7 +10848,7 @@ snapshots:
dependencies:
'@jsdevtools/ono': 7.1.3
'@types/json-schema': 7.0.15
js-yaml: 4.1.1
js-yaml: 4.2.0
'@hey-api/openapi-ts@0.98.2(magicast@0.5.2)(typescript@6.0.3)':
dependencies:
@@ -10831,7 +10918,7 @@ snapshots:
local-pkg: 1.1.2
pathe: 2.0.3
svgo: 3.3.3
tar: 7.5.11
tar: 7.5.16
transitivePeerDependencies:
- supports-color
@@ -12348,18 +12435,18 @@ snapshots:
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
'@storybook/nextjs-vite@10.4.4(@babel/core@7.29.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3)':
'@storybook/nextjs-vite@10.4.4(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
'@storybook/builder-vite': 10.4.4(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))
'@storybook/react': 10.4.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3)
'@storybook/react-vite': 10.4.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3)
next: 16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
next: 16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))
styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.7)
styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.7)
vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)'
vite-plugin-storybook-nextjs: 3.2.4(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3)
vite-plugin-storybook-nextjs: 3.2.4(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3)
optionalDependencies:
'@types/react': 19.2.17
'@types/react-dom': 19.2.3(@types/react@19.2.17)
@@ -12541,7 +12628,7 @@ snapshots:
'@tailwindcss/typography@0.5.20(tailwindcss@4.3.1)':
dependencies:
postcss-selector-parser: 6.0.10
postcss-selector-parser: 6.1.4
tailwindcss: 4.3.1
'@tailwindcss/vite@4.3.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))':
@@ -13099,13 +13186,13 @@ snapshots:
dependencies:
unpic: 4.2.2
'@unpic/react@1.0.2(next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
'@unpic/react@1.0.2(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
dependencies:
'@unpic/core': 1.0.3
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
optionalDependencies:
next: 16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
next: 16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@upsetjs/venn.js@2.0.0':
optionalDependencies:
@@ -15206,7 +15293,7 @@ snapshots:
eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2)
natural-compare: 1.4.0
nth-check: 2.1.1
postcss-selector-parser: 7.1.1
postcss-selector-parser: 7.1.4
semver: 7.8.4
vue-eslint-parser: 10.4.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)
xml-name-validator: 4.0.0
@@ -15220,7 +15307,7 @@ snapshots:
eslint: 10.5.0(jiti@2.7.0)
natural-compare: 1.4.0
nth-check: 2.1.1
postcss-selector-parser: 7.1.1
postcss-selector-parser: 7.1.4
semver: 7.8.4
vue-eslint-parser: 10.4.0(eslint@10.5.0(jiti@2.7.0))
xml-name-validator: 4.0.0
@@ -16095,10 +16182,10 @@ snapshots:
jiti@2.7.0: {}
jotai@2.20.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.17)(react@19.2.7):
jotai@2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7):
optionalDependencies:
'@babel/core': 7.29.0
'@babel/template': 7.28.6
'@babel/core': 7.29.7
'@babel/template': 7.29.7
'@types/react': 19.2.17
react: 19.2.7
@@ -16114,10 +16201,6 @@ snapshots:
js-tokens@9.0.1: {}
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
js-yaml@4.2.0:
dependencies:
argparse: 2.0.1
@@ -16975,7 +17058,7 @@ snapshots:
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
dependencies:
'@next/env': 16.2.9
'@swc/helpers': 0.5.15
@@ -16984,7 +17067,7 @@ snapshots:
postcss: 8.5.15
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.7)
styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.7)
optionalDependencies:
'@next/swc-darwin-arm64': 16.2.9
'@next/swc-darwin-x64': 16.2.9
@@ -17030,12 +17113,12 @@ snapshots:
dependencies:
boolbase: 1.0.0
nuqs@2.8.9(next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7):
nuqs@2.8.9(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7):
dependencies:
'@standard-schema/spec': 1.0.0
react: 19.2.7
optionalDependencies:
next: 16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
next: 16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
object-assign@4.1.1: {}
@@ -17487,12 +17570,12 @@ snapshots:
possible-typed-array-names@1.1.0: {}
postcss-selector-parser@6.0.10:
postcss-selector-parser@6.1.4:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss-selector-parser@7.1.1:
postcss-selector-parser@7.1.4:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
@@ -17586,7 +17669,7 @@ snapshots:
react-docgen@8.0.3:
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@babel/traverse': 7.29.0
'@babel/types': 7.29.0
'@types/babel__core': 7.20.5
@@ -18387,12 +18470,12 @@ snapshots:
dependencies:
inline-style-parser: 0.2.7
styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.7):
styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.2.7):
dependencies:
client-only: 0.0.1
react: 19.2.7
optionalDependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
stylis@4.3.6: {}
@@ -18447,7 +18530,7 @@ snapshots:
readable-stream: 3.6.2
optional: true
tar@7.5.11:
tar@7.5.16:
dependencies:
'@isaacs/fs-minipass': 4.0.1
chownr: 3.0.0
@@ -18782,9 +18865,9 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
vinext@0.1.2(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(@vitejs/plugin-rsc@0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(typescript@6.0.3):
vinext@0.1.2(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(@vitejs/plugin-rsc@0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(typescript@6.0.3):
dependencies:
'@unpic/react': 1.0.2(next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@unpic/react': 1.0.2(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@vercel/og': 0.8.6
'@vitejs/plugin-react': 6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))
image-size: 2.0.2
@@ -18837,13 +18920,13 @@ snapshots:
- typescript
- utf-8-validate
vite-plugin-storybook-nextjs@3.2.4(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3):
vite-plugin-storybook-nextjs@3.2.4(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3):
dependencies:
'@next/env': 16.0.0
image-size: 2.0.2
magic-string: 0.30.21
module-alias: 2.3.4
next: 16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
next: 16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))
ts-dedent: 2.2.0
vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)'
+5
View File
@@ -27,19 +27,24 @@ packages:
- packages/*
- cli
overrides:
'@babel/core@<=7.29.0': ^7.29.1
'@lexical/code': npm:lexical-code-no-prism@0.41.0
canvas: ^3.2.3
esbuild@<0.27.2: 0.27.2
esbuild@>=0.17.0 <0.28.1: ^0.28.1
esbuild@>=0.27.3 <0.28.1: ^0.28.1
is-core-module: npm:@nolyfill/is-core-module@^1.0.39
js-yaml@<=4.1.1: ^4.1.2
picomatch@>=4.0.0 <4.0.4: 4.0.4
postcss-selector-parser@>=6.0.0 <6.1.3: 6.1.4
postcss-selector-parser@>=7.0.0 <7.1.3: 7.1.4
postcss@<8.5.10: ^8.5.10
rollup@>=4.0.0 <4.59.0: 4.61.1
safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44
side-channel: npm:@nolyfill/side-channel@^1.0.44
solid-js: 1.9.13
string-width: ~8.2.1
tar@<=7.5.15: ^7.5.16
vite: npm:@voidzero-dev/vite-plus-core@0.1.24
vitest: npm:@voidzero-dev/vite-plus-test@0.1.24
ws@>=8.0.0 <8.20.1: ^8.21.0