Merge branch 'main' into jzh

This commit is contained in:
JzoNg
2026-03-26 20:10:47 +08:00
18 changed files with 297 additions and 98 deletions

View File

@@ -2,6 +2,9 @@ name: autofix.ci
on:
pull_request:
branches: ["main"]
merge_group:
branches: ["main"]
types: [checks_requested]
push:
branches: ["main"]
permissions:
@@ -12,9 +15,15 @@ jobs:
if: github.repository == 'langgenius/dify'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Complete merge group check
if: github.event_name == 'merge_group'
run: echo "autofix.ci updates pull request branches, not merge group refs."
- if: github.event_name != 'merge_group'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check Docker Compose inputs
if: github.event_name != 'merge_group'
id: docker-compose-changes
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
with:
@@ -24,30 +33,34 @@ jobs:
docker/docker-compose-template.yaml
docker/docker-compose.yaml
- name: Check web inputs
if: github.event_name != 'merge_group'
id: web-changes
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
with:
files: |
web/**
- name: Check api inputs
if: github.event_name != 'merge_group'
id: api-changes
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
with:
files: |
api/**
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
- if: github.event_name != 'merge_group'
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- if: github.event_name != 'merge_group'
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- name: Generate Docker Compose
if: steps.docker-compose-changes.outputs.any_changed == 'true'
if: github.event_name != 'merge_group' && steps.docker-compose-changes.outputs.any_changed == 'true'
run: |
cd docker
./generate_docker_compose
- if: steps.api-changes.outputs.any_changed == 'true'
- if: github.event_name != 'merge_group' && steps.api-changes.outputs.any_changed == 'true'
run: |
cd api
uv sync --dev
@@ -59,13 +72,13 @@ jobs:
uv run ruff format ..
- name: count migration progress
if: steps.api-changes.outputs.any_changed == 'true'
if: github.event_name != 'merge_group' && steps.api-changes.outputs.any_changed == 'true'
run: |
cd api
./cnt_base.sh
- name: ast-grep
if: steps.api-changes.outputs.any_changed == 'true'
if: github.event_name != 'merge_group' && steps.api-changes.outputs.any_changed == 'true'
run: |
# ast-grep exits 1 if no matches are found; allow idempotent runs.
uvx --from ast-grep-cli ast-grep --pattern 'db.session.query($WHATEVER).filter($HERE)' --rewrite 'db.session.query($WHATEVER).where($HERE)' -l py --update-all || true
@@ -95,13 +108,14 @@ jobs:
find . -name "*.py.bak" -type f -delete
- name: Setup web environment
if: steps.web-changes.outputs.any_changed == 'true'
if: github.event_name != 'merge_group' && steps.web-changes.outputs.any_changed == 'true'
uses: ./.github/actions/setup-web
- name: ESLint autofix
if: steps.web-changes.outputs.any_changed == 'true'
if: github.event_name != 'merge_group' && steps.web-changes.outputs.any_changed == 'true'
run: |
cd web
vp exec eslint --concurrency=2 --prune-suppressions --quiet || true
- uses: autofix-ci/action@7a166d7532b277f34e16238930461bf77f9d7ed8 # v1.3.3
- if: github.event_name != 'merge_group'
uses: autofix-ci/action@7a166d7532b277f34e16238930461bf77f9d7ed8 # v1.3.3

View File

@@ -3,6 +3,9 @@ name: Main CI Pipeline
on:
pull_request:
branches: ["main"]
merge_group:
branches: ["main"]
types: [checks_requested]
push:
branches: ["main"]
@@ -50,33 +53,201 @@ jobs:
- 'api/migrations/**'
- '.github/workflows/db-migration-test.yml'
# Run tests in parallel
api-tests:
name: API Tests
# Run tests in parallel while always emitting stable required checks.
api-tests-run:
name: Run API Tests
needs: check-changes
if: needs.check-changes.outputs.api-changed == 'true'
uses: ./.github/workflows/api-tests.yml
secrets: inherit
web-tests:
name: Web Tests
api-tests-skip:
name: Skip API Tests
needs: check-changes
if: needs.check-changes.outputs.api-changed != 'true'
runs-on: ubuntu-latest
steps:
- name: Report skipped API tests
run: echo "No API-related changes detected; skipping API tests."
api-tests:
name: API Tests
if: ${{ always() }}
needs:
- check-changes
- api-tests-run
- api-tests-skip
runs-on: ubuntu-latest
steps:
- name: Finalize API Tests status
env:
TESTS_CHANGED: ${{ needs.check-changes.outputs.api-changed }}
RUN_RESULT: ${{ needs.api-tests-run.result }}
SKIP_RESULT: ${{ needs.api-tests-skip.result }}
run: |
if [[ "$TESTS_CHANGED" == 'true' ]]; then
if [[ "$RUN_RESULT" == 'success' ]]; then
echo "API tests ran successfully."
exit 0
fi
echo "API tests were required but finished with result: $RUN_RESULT" >&2
exit 1
fi
if [[ "$SKIP_RESULT" == 'success' ]]; then
echo "API tests were skipped because no API-related files changed."
exit 0
fi
echo "API tests were not required, but the skip job finished with result: $SKIP_RESULT" >&2
exit 1
web-tests-run:
name: Run Web Tests
needs: check-changes
if: needs.check-changes.outputs.web-changed == 'true'
uses: ./.github/workflows/web-tests.yml
secrets: inherit
web-tests-skip:
name: Skip Web Tests
needs: check-changes
if: needs.check-changes.outputs.web-changed != 'true'
runs-on: ubuntu-latest
steps:
- name: Report skipped web tests
run: echo "No web-related changes detected; skipping web tests."
web-tests:
name: Web Tests
if: ${{ always() }}
needs:
- check-changes
- web-tests-run
- web-tests-skip
runs-on: ubuntu-latest
steps:
- name: Finalize Web Tests status
env:
TESTS_CHANGED: ${{ needs.check-changes.outputs.web-changed }}
RUN_RESULT: ${{ needs.web-tests-run.result }}
SKIP_RESULT: ${{ needs.web-tests-skip.result }}
run: |
if [[ "$TESTS_CHANGED" == 'true' ]]; then
if [[ "$RUN_RESULT" == 'success' ]]; then
echo "Web tests ran successfully."
exit 0
fi
echo "Web tests were required but finished with result: $RUN_RESULT" >&2
exit 1
fi
if [[ "$SKIP_RESULT" == 'success' ]]; then
echo "Web tests were skipped because no web-related files changed."
exit 0
fi
echo "Web tests were not required, but the skip job finished with result: $SKIP_RESULT" >&2
exit 1
style-check:
name: Style Check
uses: ./.github/workflows/style.yml
vdb-tests:
name: VDB Tests
vdb-tests-run:
name: Run VDB Tests
needs: check-changes
if: needs.check-changes.outputs.vdb-changed == 'true'
uses: ./.github/workflows/vdb-tests.yml
db-migration-test:
name: DB Migration Test
vdb-tests-skip:
name: Skip VDB Tests
needs: check-changes
if: needs.check-changes.outputs.vdb-changed != 'true'
runs-on: ubuntu-latest
steps:
- name: Report skipped VDB tests
run: echo "No VDB-related changes detected; skipping VDB tests."
vdb-tests:
name: VDB Tests
if: ${{ always() }}
needs:
- check-changes
- vdb-tests-run
- vdb-tests-skip
runs-on: ubuntu-latest
steps:
- name: Finalize VDB Tests status
env:
TESTS_CHANGED: ${{ needs.check-changes.outputs.vdb-changed }}
RUN_RESULT: ${{ needs.vdb-tests-run.result }}
SKIP_RESULT: ${{ needs.vdb-tests-skip.result }}
run: |
if [[ "$TESTS_CHANGED" == 'true' ]]; then
if [[ "$RUN_RESULT" == 'success' ]]; then
echo "VDB tests ran successfully."
exit 0
fi
echo "VDB tests were required but finished with result: $RUN_RESULT" >&2
exit 1
fi
if [[ "$SKIP_RESULT" == 'success' ]]; then
echo "VDB tests were skipped because no VDB-related files changed."
exit 0
fi
echo "VDB tests were not required, but the skip job finished with result: $SKIP_RESULT" >&2
exit 1
db-migration-test-run:
name: Run DB Migration Test
needs: check-changes
if: needs.check-changes.outputs.migration-changed == 'true'
uses: ./.github/workflows/db-migration-test.yml
db-migration-test-skip:
name: Skip DB Migration Test
needs: check-changes
if: needs.check-changes.outputs.migration-changed != 'true'
runs-on: ubuntu-latest
steps:
- name: Report skipped DB migration tests
run: echo "No migration-related changes detected; skipping DB migration tests."
db-migration-test:
name: DB Migration Test
if: ${{ always() }}
needs:
- check-changes
- db-migration-test-run
- db-migration-test-skip
runs-on: ubuntu-latest
steps:
- name: Finalize DB Migration Test status
env:
TESTS_CHANGED: ${{ needs.check-changes.outputs.migration-changed }}
RUN_RESULT: ${{ needs.db-migration-test-run.result }}
SKIP_RESULT: ${{ needs.db-migration-test-skip.result }}
run: |
if [[ "$TESTS_CHANGED" == 'true' ]]; then
if [[ "$RUN_RESULT" == 'success' ]]; then
echo "DB migration tests ran successfully."
exit 0
fi
echo "DB migration tests were required but finished with result: $RUN_RESULT" >&2
exit 1
fi
if [[ "$SKIP_RESULT" == 'success' ]]; then
echo "DB migration tests were skipped because no migration-related files changed."
exit 0
fi
echo "DB migration tests were not required, but the skip job finished with result: $SKIP_RESULT" >&2
exit 1

View File

@@ -7,6 +7,9 @@ on:
- edited
- reopened
- synchronize
merge_group:
branches: ["main"]
types: [checks_requested]
jobs:
lint:
@@ -15,7 +18,11 @@ jobs:
pull-requests: read
runs-on: ubuntu-latest
steps:
- name: Complete merge group check
if: github.event_name == 'merge_group'
run: echo "Semantic PR title validation is handled on pull requests."
- name: Check title
if: github.event_name == 'pull_request'
uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,11 +1,12 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta
from datetime import timedelta
from decimal import Decimal
from uuid import uuid4
from graphon.nodes.human_input.entities import FormDefinition, UserAction
from libs.datetime_utils import naive_utc_now
from models.account import Account, Tenant, TenantAccountJoin
from models.enums import ConversationFromSource, InvokeFrom
from models.execution_extra_content import HumanInputContent
@@ -117,7 +118,7 @@ def create_human_input_message_fixture(db_session) -> HumanInputMessageFixture:
inputs=[],
user_actions=[UserAction(id=action_id, title=action_text)],
rendered_content="Rendered block",
expiration_time=datetime.utcnow() + timedelta(days=1),
expiration_time=naive_utc_now() + timedelta(days=1),
node_title=node_title,
display_in_ui=True,
)
@@ -129,7 +130,7 @@ def create_human_input_message_fixture(db_session) -> HumanInputMessageFixture:
form_definition=form_definition.model_dump_json(),
rendered_content="Rendered block",
status=HumanInputFormStatus.SUBMITTED,
expiration_time=datetime.utcnow() + timedelta(days=1),
expiration_time=naive_utc_now() + timedelta(days=1),
selected_action_id=action_id,
)
db_session.add(form)

View File

@@ -7,7 +7,7 @@ from __future__ import annotations
from collections.abc import Generator
from dataclasses import dataclass
from datetime import datetime, timedelta
from datetime import timedelta
from decimal import Decimal
from uuid import uuid4
@@ -17,6 +17,7 @@ from sqlalchemy.orm import Session, sessionmaker
from graphon.nodes.human_input.entities import FormDefinition, UserAction
from graphon.nodes.human_input.enums import HumanInputFormStatus
from libs.datetime_utils import naive_utc_now
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
from models.enums import ConversationFromSource, InvokeFrom
from models.execution_extra_content import ExecutionExtraContent, HumanInputContent
@@ -174,7 +175,7 @@ def _create_submitted_form(
action_title: str = "Approve",
node_title: str = "Approval",
) -> HumanInputForm:
expiration_time = datetime.utcnow() + timedelta(days=1)
expiration_time = naive_utc_now() + timedelta(days=1)
form_definition = FormDefinition(
form_content="content",
inputs=[],
@@ -207,7 +208,7 @@ def _create_waiting_form(
workflow_run_id: str,
default_values: dict | None = None,
) -> HumanInputForm:
expiration_time = datetime.utcnow() + timedelta(days=1)
expiration_time = naive_utc_now() + timedelta(days=1)
form_definition = FormDefinition(
form_content="content",
inputs=[],

View File

@@ -26,6 +26,7 @@ from controllers.console.datasets.rag_pipeline.rag_pipeline_workflow import (
RagPipelineWorkflowLastRunApi,
)
from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError
from libs.datetime_utils import naive_utc_now
from services.errors.app import IsDraftWorkflowError, WorkflowHashNotEqualError, WorkflowNotFoundError
from services.errors.llm import InvokeRateLimitError
@@ -372,7 +373,7 @@ class TestPublishedPipelineApis:
workflow = MagicMock(
id="w1",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
)
session = MagicMock()

View File

@@ -1,4 +1,3 @@
from datetime import datetime
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
@@ -25,6 +24,7 @@ from controllers.console.datasets.error import (
)
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
from core.rag.index_processor.constant.index_type import IndexStructureType
from libs.datetime_utils import naive_utc_now
from models.dataset import ChildChunk, DocumentSegment
from models.model import UploadFile
@@ -54,8 +54,8 @@ def _segment():
disabled_by=None,
status="normal",
created_by="u1",
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
created_at=naive_utc_now(),
updated_at=naive_utc_now(),
updated_by="u1",
indexing_at=None,
completed_at=None,

View File

@@ -1,4 +1,3 @@
from datetime import datetime
from io import BytesIO
from unittest.mock import MagicMock, patch
@@ -26,6 +25,7 @@ from controllers.console.workspace.workspace import (
WorkspacePermissionApi,
)
from enums.cloud_plan import CloudPlan
from libs.datetime_utils import naive_utc_now
from models.account import TenantStatus
@@ -44,13 +44,13 @@ class TestTenantListApi:
id="t1",
name="Tenant 1",
status="active",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
)
tenant2 = MagicMock(
id="t2",
name="Tenant 2",
status="active",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
)
with (
@@ -97,13 +97,13 @@ class TestTenantListApi:
id="t1",
name="Tenant 1",
status="active",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
)
tenant2 = MagicMock(
id="t2",
name="Tenant 2",
status="active",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
)
features_t2 = MagicMock()
@@ -152,13 +152,13 @@ class TestTenantListApi:
id="t1",
name="Tenant 1",
status="active",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
)
tenant2 = MagicMock(
id="t2",
name="Tenant 2",
status="active",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
)
features = MagicMock()
@@ -204,7 +204,7 @@ class TestTenantListApi:
id="t1",
name="Tenant",
status="active",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
)
features = MagicMock()
@@ -243,13 +243,13 @@ class TestTenantListApi:
id="t1",
name="Tenant 1",
status="active",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
)
tenant2 = MagicMock(
id="t2",
name="Tenant 2",
status="active",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
)
with (
@@ -305,7 +305,7 @@ class TestWorkspaceListApi:
api = WorkspaceListApi()
method = unwrap(api.get)
tenant = MagicMock(id="t1", name="T", status="active", created_at=datetime.utcnow())
tenant = MagicMock(id="t1", name="T", status="active", created_at=naive_utc_now())
paginate_result = MagicMock(
items=[tenant],
@@ -331,7 +331,7 @@ class TestWorkspaceListApi:
id="t1",
name="T",
status="active",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
)
paginate_result = MagicMock(

View File

@@ -1,7 +1,6 @@
from __future__ import annotations
from contextlib import contextmanager
from datetime import datetime
from types import SimpleNamespace
import pytest
@@ -45,6 +44,7 @@ from core.base.tts.app_generator_tts_publisher import AudioTrunk
from core.workflow.system_variables import build_system_variables
from graphon.enums import BuiltinNodeTypes
from graphon.runtime import GraphRuntimeState, VariablePool
from libs.datetime_utils import naive_utc_now
from models.enums import MessageStatus
from models.model import AppMode, EndUser
from tests.workflow_test_utils import build_test_variable_pool
@@ -76,7 +76,7 @@ def _make_pipeline():
message = SimpleNamespace(
id="message-id",
query="hello",
created_at=datetime.utcnow(),
created_at=naive_utc_now(),
status=MessageStatus.NORMAL,
answer="",
)
@@ -257,7 +257,7 @@ class TestAdvancedChatGenerateTaskPipeline:
node_id="node",
node_type=BuiltinNodeTypes.LLM,
node_title="LLM",
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
node_run_index=1,
)
iter_next = QueueIterationNextEvent(
@@ -273,7 +273,7 @@ class TestAdvancedChatGenerateTaskPipeline:
node_id="node",
node_type=BuiltinNodeTypes.LLM,
node_title="LLM",
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
node_run_index=1,
)
loop_start = QueueLoopStartEvent(
@@ -281,7 +281,7 @@ class TestAdvancedChatGenerateTaskPipeline:
node_id="node",
node_type=BuiltinNodeTypes.LLM,
node_title="LLM",
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
node_run_index=1,
)
loop_next = QueueLoopNextEvent(
@@ -297,7 +297,7 @@ class TestAdvancedChatGenerateTaskPipeline:
node_id="node",
node_type=BuiltinNodeTypes.LLM,
node_title="LLM",
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
node_run_index=1,
)
@@ -360,7 +360,7 @@ class TestAdvancedChatGenerateTaskPipeline:
node_execution_id="exec",
node_id="node",
node_type=BuiltinNodeTypes.LLM,
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
inputs={},
outputs={},
process_data={},
@@ -370,7 +370,7 @@ class TestAdvancedChatGenerateTaskPipeline:
node_execution_id="exec",
node_id="node",
node_type=BuiltinNodeTypes.LLM,
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
inputs={},
outputs={},
process_data={},
@@ -473,7 +473,7 @@ class TestAdvancedChatGenerateTaskPipeline:
node_id="node",
node_type=BuiltinNodeTypes.LLM,
node_title="title",
expiration_time=datetime.utcnow(),
expiration_time=naive_utc_now(),
)
assert list(pipeline._handle_human_input_form_filled_event(filled_event)) == ["filled"]
@@ -591,7 +591,7 @@ class TestAdvancedChatGenerateTaskPipeline:
node_execution_id="exec",
node_id="node",
node_type=BuiltinNodeTypes.LLM,
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
inputs={},
outputs={},
process_data={},

View File

@@ -1,7 +1,6 @@
from __future__ import annotations
from contextlib import contextmanager
from datetime import datetime
from types import SimpleNamespace
import pytest
@@ -47,6 +46,7 @@ from core.base.tts.app_generator_tts_publisher import AudioTrunk
from core.workflow.system_variables import build_system_variables, system_variables_to_mapping
from graphon.enums import BuiltinNodeTypes, WorkflowExecutionStatus
from graphon.runtime import GraphRuntimeState, VariablePool
from libs.datetime_utils import naive_utc_now
from models.enums import CreatorUserRole
from models.model import AppMode, EndUser
from tests.workflow_test_utils import build_test_variable_pool
@@ -192,7 +192,7 @@ class TestWorkflowGenerateTaskPipeline:
node_execution_id="exec",
node_id="node",
node_type=BuiltinNodeTypes.START,
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
inputs={},
outputs={},
process_data={},
@@ -245,7 +245,7 @@ class TestWorkflowGenerateTaskPipeline:
node_execution_id="exec",
node_id="node",
node_type=BuiltinNodeTypes.START,
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
inputs={},
outputs={},
process_data={},
@@ -303,7 +303,7 @@ class TestWorkflowGenerateTaskPipeline:
node_id="node",
node_type=BuiltinNodeTypes.LLM,
node_title="LLM",
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
node_run_index=1,
)
iter_next = QueueIterationNextEvent(
@@ -319,7 +319,7 @@ class TestWorkflowGenerateTaskPipeline:
node_id="node",
node_type=BuiltinNodeTypes.LLM,
node_title="LLM",
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
node_run_index=1,
)
loop_start = QueueLoopStartEvent(
@@ -327,7 +327,7 @@ class TestWorkflowGenerateTaskPipeline:
node_id="node",
node_type=BuiltinNodeTypes.LLM,
node_title="LLM",
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
node_run_index=1,
)
loop_next = QueueLoopNextEvent(
@@ -343,7 +343,7 @@ class TestWorkflowGenerateTaskPipeline:
node_id="node",
node_type=BuiltinNodeTypes.LLM,
node_title="LLM",
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
node_run_index=1,
)
filled_event = QueueHumanInputFormFilledEvent(
@@ -359,7 +359,7 @@ class TestWorkflowGenerateTaskPipeline:
node_id="node",
node_type=BuiltinNodeTypes.LLM,
node_title="title",
expiration_time=datetime.utcnow(),
expiration_time=naive_utc_now(),
)
agent_event = QueueAgentLogEvent(
id="log",
@@ -648,7 +648,7 @@ class TestWorkflowGenerateTaskPipeline:
node_title="title",
node_type=BuiltinNodeTypes.LLM,
node_run_index=1,
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
provider_type="provider",
provider_id="provider-id",
error="error",
@@ -660,7 +660,7 @@ class TestWorkflowGenerateTaskPipeline:
node_title="title",
node_type=BuiltinNodeTypes.LLM,
node_run_index=1,
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
provider_type="provider",
provider_id="provider-id",
)
@@ -685,7 +685,7 @@ class TestWorkflowGenerateTaskPipeline:
node_execution_id="exec-id",
node_id="node",
node_type=BuiltinNodeTypes.START,
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
inputs={},
outputs={},
process_data={},
@@ -836,7 +836,7 @@ class TestWorkflowGenerateTaskPipeline:
node_id="node-id",
node_type=BuiltinNodeTypes.START,
in_loop_id="loop-id",
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
process_data={"k": "v"},
outputs={"out": 1},
)

View File

@@ -1,5 +1,4 @@
from collections.abc import Sequence
from datetime import datetime
from unittest.mock import Mock
from core.app.layers.conversation_variable_persist_layer import ConversationVariablePersistenceLayer
@@ -12,6 +11,7 @@ from graphon.node_events import NodeRunResult
from graphon.runtime.graph_runtime_state_protocol import ReadOnlyGraphRuntimeState
from graphon.variables import StringVariable
from graphon.variables.segments import Segment, StringSegment
from libs.datetime_utils import naive_utc_now
class MockReadOnlyVariablePool:
@@ -48,7 +48,7 @@ def _build_node_run_succeeded_event() -> NodeRunSucceededEvent:
id="node-exec-id",
node_id="assigner",
node_type=BuiltinNodeTypes.LLM,
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
node_run_result=NodeRunResult(
status=WorkflowNodeExecutionStatus.SUCCEEDED,
outputs={},

View File

@@ -274,7 +274,7 @@ def _make_form_definition() -> str:
inputs=[],
user_actions=[UserAction(id="submit", title="Submit")],
rendered_content="<p>hello</p>",
expiration_time=datetime.utcnow(),
expiration_time=naive_utc_now(),
).model_dump_json()

View File

@@ -1,10 +1,10 @@
import queue
from datetime import datetime
from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus
from graphon.graph_engine.orchestration.dispatcher import Dispatcher
from graphon.graph_events import NodeRunSucceededEvent
from graphon.node_events import NodeRunResult
from libs.datetime_utils import naive_utc_now
class StubExecutionCoordinator:
@@ -52,7 +52,7 @@ def test_dispatcher_drains_events_when_paused() -> None:
id="exec-1",
node_id="node-1",
node_type=BuiltinNodeTypes.START,
start_at=datetime.utcnow(),
start_at=naive_utc_now(),
node_run_result=NodeRunResult(status=WorkflowNodeExecutionStatus.SUCCEEDED),
)
event_queue.put(event)

View File

@@ -6,6 +6,7 @@ from typing import Any
from graphon.nodes.human_input.entities import FormInput
from graphon.nodes.human_input.enums import TimeoutUnit
from libs.datetime_utils import naive_utc_now
# Exceptions
@@ -49,7 +50,7 @@ class HumanInputForm:
timeout: int
timeout_unit: TimeoutUnit
form_token: str | None = None
created_at: datetime = field(default_factory=datetime.utcnow)
created_at: datetime = field(default_factory=naive_utc_now)
expires_at: datetime | None = None
submitted_at: datetime | None = None
submitted_data: dict[str, Any] | None = None
@@ -61,7 +62,7 @@ class HumanInputForm:
@property
def is_expired(self) -> bool:
return self.expires_at is not None and datetime.utcnow() > self.expires_at
return self.expires_at is not None and naive_utc_now() > self.expires_at
@property
def is_submitted(self) -> bool:
@@ -70,7 +71,7 @@ class HumanInputForm:
def mark_submitted(self, inputs: dict[str, Any], action: str) -> None:
self.submitted_data = inputs
self.submitted_action = action
self.submitted_at = datetime.utcnow()
self.submitted_at = naive_utc_now()
def submit(self, inputs: dict[str, Any], action: str) -> None:
self.mark_submitted(inputs, action)
@@ -107,7 +108,7 @@ class FormSubmissionData:
form_id: str
inputs: dict[str, Any]
action: str
submitted_at: datetime = field(default_factory=datetime.utcnow)
submitted_at: datetime = field(default_factory=naive_utc_now)
@classmethod
def from_request(cls, form_id: str, request: FormSubmissionRequest) -> FormSubmissionData: # type: ignore

View File

@@ -2,7 +2,7 @@
Unit tests for FormService.
"""
from datetime import datetime, timedelta
from datetime import timedelta
import pytest
@@ -142,7 +142,7 @@ class TestFormService:
# Manually expire the form by modifying expiry time
form = form_service.get_form_by_id("form-123")
form.expires_at = datetime.utcnow() - timedelta(hours=1)
form.expires_at = naive_utc_now() - timedelta(hours=1)
form_service.repository.save(form)
# Should raise FormExpiredError
@@ -227,7 +227,7 @@ class TestFormService:
# Manually expire the form
form = form_service.get_form_by_id("form-123")
form.expires_at = datetime.utcnow() - timedelta(hours=1)
form.expires_at = naive_utc_now() - timedelta(hours=1)
form_service.repository.save(form)
# Try to submit expired form

View File

@@ -14,6 +14,7 @@ from graphon.nodes.human_input.enums import (
FormInputType,
TimeoutUnit,
)
from libs.datetime_utils import naive_utc_now
from .support import FormSubmissionData, FormSubmissionRequest, HumanInputForm
@@ -83,7 +84,7 @@ class TestHumanInputForm:
def test_form_expiry_property_expired(self, sample_form_data):
"""Test is_expired property for expired form."""
# Create form with past expiry
past_time = datetime.utcnow() - timedelta(hours=1)
past_time = naive_utc_now() - timedelta(hours=1)
sample_form_data["created_at"] = past_time
form = HumanInputForm(**sample_form_data)
@@ -111,9 +112,9 @@ class TestHumanInputForm:
"""Test form submit method."""
form = HumanInputForm(**sample_form_data)
submission_time_before = datetime.utcnow()
submission_time_before = naive_utc_now()
form.submit({"input": "test value"}, "submit")
submission_time_after = datetime.utcnow()
submission_time_after = naive_utc_now()
assert form.is_submitted
assert form.submitted_data == {"input": "test value"}
@@ -213,11 +214,11 @@ class TestFormSubmissionData:
def test_submission_data_timestamps(self):
"""Test submission data timestamp handling."""
before_time = datetime.utcnow()
before_time = naive_utc_now()
submission_data = FormSubmissionData(form_id="form-123", inputs={"test": "value"}, action="submit")
after_time = datetime.utcnow()
after_time = naive_utc_now()
assert before_time <= submission_data.submitted_at <= after_time

View File

@@ -6,13 +6,14 @@ Tests are organized by functionality and include edge cases, error handling,
and both positive and negative test scenarios.
"""
from datetime import datetime, timedelta
from datetime import timedelta
from unittest.mock import MagicMock, Mock, create_autospec, patch
import pytest
from sqlalchemy import asc, desc
from core.app.entities.app_invoke_entities import InvokeFrom
from libs.datetime_utils import naive_utc_now
from libs.infinite_scroll_pagination import InfiniteScrollPagination
from models import Account, ConversationVariable
from models.enums import ConversationFromSource
@@ -122,8 +123,8 @@ class ConversationServiceTestDataFactory:
conversation.is_deleted = kwargs.get("is_deleted", False)
conversation.name = kwargs.get("name", "Test Conversation")
conversation.status = kwargs.get("status", "normal")
conversation.created_at = kwargs.get("created_at", datetime.utcnow())
conversation.updated_at = kwargs.get("updated_at", datetime.utcnow())
conversation.created_at = kwargs.get("created_at", naive_utc_now())
conversation.updated_at = kwargs.get("updated_at", naive_utc_now())
for key, value in kwargs.items():
setattr(conversation, key, value)
return conversation
@@ -152,7 +153,7 @@ class ConversationServiceTestDataFactory:
message.conversation_id = conversation_id
message.app_id = app_id
message.query = kwargs.get("query", "Test message content")
message.created_at = kwargs.get("created_at", datetime.utcnow())
message.created_at = kwargs.get("created_at", naive_utc_now())
for key, value in kwargs.items():
setattr(message, key, value)
return message
@@ -181,8 +182,8 @@ class ConversationServiceTestDataFactory:
variable.conversation_id = conversation_id
variable.app_id = app_id
variable.data = {"name": kwargs.get("name", "test_var"), "value": kwargs.get("value", "test_value")}
variable.created_at = kwargs.get("created_at", datetime.utcnow())
variable.updated_at = kwargs.get("updated_at", datetime.utcnow())
variable.created_at = kwargs.get("created_at", naive_utc_now())
variable.updated_at = kwargs.get("updated_at", naive_utc_now())
# Mock to_variable method
mock_variable = Mock()
@@ -302,7 +303,7 @@ class TestConversationServiceHelpers:
"""
# Arrange
mock_conversation = ConversationServiceTestDataFactory.create_conversation_mock()
mock_conversation.updated_at = datetime.utcnow()
mock_conversation.updated_at = naive_utc_now()
# Act
condition = ConversationService._build_filter_condition(
@@ -323,7 +324,7 @@ class TestConversationServiceHelpers:
"""
# Arrange
mock_conversation = ConversationServiceTestDataFactory.create_conversation_mock()
mock_conversation.created_at = datetime.utcnow()
mock_conversation.created_at = naive_utc_now()
# Act
condition = ConversationService._build_filter_condition(
@@ -668,9 +669,9 @@ class TestConversationServiceConversationalVariable:
mock_session_factory.create_session.return_value.__enter__.return_value = mock_session
last_variable = ConversationServiceTestDataFactory.create_conversation_variable_mock(
created_at=datetime.utcnow() - timedelta(hours=1)
created_at=naive_utc_now() - timedelta(hours=1)
)
variable = ConversationServiceTestDataFactory.create_conversation_variable_mock(created_at=datetime.utcnow())
variable = ConversationServiceTestDataFactory.create_conversation_variable_mock(created_at=naive_utc_now())
mock_session.scalar.return_value = last_variable
mock_session.scalars.return_value.all.return_value = [variable]

View File

@@ -15,6 +15,7 @@ from graphon.nodes.human_input.entities import (
UserAction,
)
from graphon.nodes.human_input.enums import FormInputType, HumanInputFormKind, HumanInputFormStatus
from libs.datetime_utils import naive_utc_now
from models.human_input import RecipientType
from services.human_input_service import (
Form,
@@ -51,11 +52,11 @@ def sample_form_record():
inputs=[],
user_actions=[UserAction(id="submit", title="Submit")],
rendered_content="<p>hello</p>",
expiration_time=datetime.utcnow() + timedelta(hours=1),
expiration_time=naive_utc_now() + timedelta(hours=1),
),
rendered_content="<p>hello</p>",
created_at=datetime.utcnow(),
expiration_time=datetime.utcnow() + timedelta(hours=1),
created_at=naive_utc_now(),
expiration_time=naive_utc_now() + timedelta(hours=1),
status=HumanInputFormStatus.WAITING,
selected_action_id=None,
submitted_data=None,
@@ -101,8 +102,8 @@ def test_ensure_form_active_respects_global_timeout(monkeypatch, sample_form_rec
service = HumanInputService(session_factory)
expired_record = dataclasses.replace(
sample_form_record,
created_at=datetime.utcnow() - timedelta(hours=2),
expiration_time=datetime.utcnow() + timedelta(hours=2),
created_at=naive_utc_now() - timedelta(hours=2),
expiration_time=naive_utc_now() + timedelta(hours=2),
)
monkeypatch.setattr(human_input_service_module.dify_config, "HUMAN_INPUT_GLOBAL_TIMEOUT_SECONDS", 3600)
@@ -391,7 +392,7 @@ def test_ensure_form_active_errors(sample_form_record, mock_session_factory):
service = HumanInputService(session_factory)
# Submitted
submitted_record = dataclasses.replace(sample_form_record, submitted_at=datetime.utcnow())
submitted_record = dataclasses.replace(sample_form_record, submitted_at=naive_utc_now())
with pytest.raises(human_input_service_module.FormSubmittedError):
service.ensure_form_active(Form(submitted_record))
@@ -402,7 +403,7 @@ def test_ensure_form_active_errors(sample_form_record, mock_session_factory):
# Expired time
expired_time_record = dataclasses.replace(
sample_form_record, expiration_time=datetime.utcnow() - timedelta(minutes=1)
sample_form_record, expiration_time=naive_utc_now() - timedelta(minutes=1)
)
with pytest.raises(FormExpiredError):
service.ensure_form_active(Form(expired_time_record))
@@ -411,7 +412,7 @@ def test_ensure_form_active_errors(sample_form_record, mock_session_factory):
def test_ensure_not_submitted_raises(sample_form_record, mock_session_factory):
session_factory, _ = mock_session_factory
service = HumanInputService(session_factory)
submitted_record = dataclasses.replace(sample_form_record, submitted_at=datetime.utcnow())
submitted_record = dataclasses.replace(sample_form_record, submitted_at=naive_utc_now())
with pytest.raises(human_input_service_module.FormSubmittedError):
service._ensure_not_submitted(Form(submitted_record))