diff --git a/.agents/skills/adminive-development/SKILL.md b/.agents/skills/adminive-development/SKILL.md new file mode 100644 index 0000000..84d5b4d --- /dev/null +++ b/.agents/skills/adminive-development/SKILL.md @@ -0,0 +1,208 @@ +--- +name: adminive-development +description: Use when implementing, modifying, debugging, reviewing, or integrating Adminive descriptors, field presentation, Form/Collection views, Composition manifests, JSON/AMIS generation, managed values, HTTP resources, adapters, Gallery frontend, tests, CMake, install/export, or packaging. Do not use for work confined to Structive internals; use the nested structive-development skill there. +--- +# Adminive Development + +## Goal + +Make changes through Adminive's existing semantic layers instead of bypassing them with framework-specific code. The backend remains the business-schema source of truth; adapters translate that truth into JSON, HTTP, AMIS, or frontend renderer contracts. + +## Start by reading the source of truth + +For public behavior or architecture changes, inspect: + +1. `AGENTS.md`. +2. `backend/library/DESIGN.md`. +3. The closest public header and its focused tests. +4. `README.md` for the user-facing contract. +5. If the change reaches `third_party/Structive`, read `third_party/Structive/AGENTS.md` and its skill before editing that subtree. + +Do not infer an API from the Gallery alone. The public headers and tests define the actual contract. + +## Pick the correct layer + +Use this decision table before editing: + +| Need | Owner | +|---|---| +| Native property identity, intrinsic read/write, constraint, synchronization topology | Structive | +| Admin-facing field label/description/control/options/visible condition | Adminive Field Presentation | +| Display/create/edit field selection and logical field layout | Form View | +| Collection columns, query capability, CRUD layout, table/list/cards mode | Collection View | +| Composition of complete Form/Collection/Status components | Composition View | +| Slot source/kind/API contract for an independent frontend | Composition Manifest | +| JSON encoding/decoding protocol | Adminive Core adapter protocol + concrete JSON adapter | +| AMIS renderer JSON | AMIS adapter | +| HTTP route/body/query bridging | httplib/Drogon adapter | +| External prepare/commit/rollback | `Resource_Transaction` | +| In-process mutable consistency | Managed/Structive synchronization | +| Pixel styling/responsive layout for a `frontend` node | frontend renderer | + +If a proposed feature crosses several rows, keep each concern in its owner instead of creating one all-purpose descriptor flag. + +## Common implementation recipes + +### Describe a business object + +Prefer explicit field descriptors: + +```cpp +struct Config { + std::string host; + int port{}; +}; +template <> +struct adminive::Type_Descriptor { + static auto get() { + using T = Config; + return adminive::object( + "config", + "Config", + ADMINIVE_FIELD(T, host).editable().creatable().label("Host").text_input(), + ADMINIVE_FIELD(T, port).editable().creatable().label("Port").number_input() + ); + } +}; +``` + +Do not add the same field list to React. + +### Define Form semantics + +Put view-specific order/grouping in `Type_View_Descriptor`, not in the field descriptor: + +```cpp +template <> +struct adminive::Type_View_Descriptor { + static auto edit() { + using T = Config; + return adminive::edit_form(adminive::vertical( + adminive::use<&T::host>(), + adminive::use<&T::port>() + )); + } +}; +``` + +Keep display/create/edit semantics distinct when they differ. + +### Define Collection semantics + +Start from one collection contract and derive the rendering mode: + +```cpp +const auto base = adminive::collection_view( + adminive::column<&Row::name>("Name").search(), + adminive::column<&Row::score>("Score").sort() +).default_sort("score"); +const auto item = adminive::collection_item<&Row::name>().body<&Row::score>(); +const auto table = base.as_table(); +const auto list = base.as_list(item); +const auto cards = base.as_cards(item, 3); +``` + +`sort()`, `search()`, and `filter()` are backend capabilities. Both httplib and Drogon must pass dynamic query fields to `Collection_Service`, which rejects fields not authorized by the View Schema. + +### Compose complete components + +Use Composition instead of hand-building AMIS containers: + +```cpp +const auto page = adminive::composition_view("device_page", adminive::compose::vertical( + adminive::compose::slot("config"), + adminive::compose::slot("devices") +)); +``` + +Use `compose::frontend(...)` only when the backend intentionally delegates final positioning. For an independent frontend, expose a Composition Manifest with a unique Slot Contract for every referenced slot. + +### Adapt an external value type + +Use `Value_Adapter` when a wrapper has a stable model value. Use `Control_Adapter` only when the renderer itself needs custom behavior. Keep JSON-framework details out of `backend/library`. + +### Adapt enums or reflected aggregates + +- Enum semantics go through `Enum_Adapter`; include `adminive/adapters/magic_enum.hpp` only when that bridge is desired. +- Aggregate reflection goes through `Reflection_Adapter`; use the Boost.PFR bridge only in the service layer. +- Do not make Adminive Core include magic_enum, Boost.PFR, nlohmann JSON, cpp-httplib, or Drogon. + +### Expose a mutable resource + +Use `Resource_Service` through the transport wrapper. Build `Request_Context` only at the outer HTTP bridge. Put persistence/external side effects in `Resource_Transaction`, and preserve rollback behavior when runtime commit or external commit fails. + +### Work on the React Gallery + +- Read `/descriptor`, `/view`, `/data`, `/amis`, and Composition Manifest contracts from the backend. +- Resolve frontend slots from the manifest/renderer registry; never hard-code business slot names or C++ field names into a parallel schema. +- When TypeScript receives `unknown`, narrow it before rendering. Do not silence protocol problems with `as any`. +- Keep `index.html` non-cacheable and content-hashed/versioned static resources immutable. + +## Invariants that must not regress + +- Descriptors validate legal, unique field names. +- Readonly fields can be displayed but are not included in create/edit submit payloads unless the active View permits writing. +- Sensitive fields do not appear in frontend data or default-bearing descriptor output. +- Collection query fields and sort direction are rejected when the View does not authorize them. +- Httplib and Drogon expose the same Collection query contract even though their execution models differ. +- `frontend` composition delegates layout; `vertical/horizontal/flow/grid/list/group/card/tabs` retain backend semantic composition. +- Slot names in a Manifest have exactly one contract. +- AMIS generation translates semantic View/Composition data; it does not invent business layout or permissions. +- Managed synchronization and persistence transaction remain separate mechanisms. + +## Testing workflow + +Use the smallest relevant tests first: + +- Core descriptor/JSON/collection behavior: `Adminive_Core_Adapter_Test` or focused library tests. +- View/Composition/Manifest behavior: `Adminive_View_Schema_Test`. +- Managed behavior: `Adminive_Managed_Test`. +- Value/Object/Reflection/Polymorphic adapters: `Adminive_Advanced_Adapter_Test`. +- Httplib routes: `Adminive_Httplib_Adapter_Test` with a real localhost server/client. +- Drogon routes: `Adminive_Drogon_Adapter_Test`. +- Gallery contract: `Adminive_Gallery_Test`. +- Install/export: `Adminive_Install_Consumer_Test`. +- Source archive: `Adminive_Package_Zip_Test`. + +After focused checks, run the repository gate appropriate to the change. For normal C++ changes: + +```text +cmake -S . -B verification/debug -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON +cmake --build verification/debug +ctest --test-dir verification/debug --output-on-failure +``` + +For release readiness, use `python scripts/verify.py`. If frontend code changed, run from `frontend/`: + +```text +npm run test:unit +npm run build +``` + +Run Playwright when the change affects browser behavior and dependencies are available. + +## Adding a public API + +Before adding a public API: + +1. Confirm an existing semantic layer cannot express the requirement. +2. Prefer additive APIs over changing old signature meaning. +3. Do not create a compatibility implementation for a replaced design unless explicitly required. +4. Add focused tests that prove the old semantics still hold. +5. Add/update independent public-header compile coverage if a public header changes. +6. Update `backend/library/DESIGN.md` and `README.md` when the contract changes. +7. Update the Gallery Capability Case when the feature is user-visible and should be discoverable. + +## Review checklist + +Before handoff, verify: + +- The change is in the correct layer. +- No frontend business schema was duplicated. +- No sensitive/write/query boundary became weaker. +- Httplib/Drogon behavior remains contract-equivalent where they share `Collection_Service`/`Resource_Service`. +- Old implementations were removed rather than wrapped when the design was replaced. +- Tests use `ADMINIVE_CHECK`, not `assert()`. +- CMake paths are local to the owning CMake file. +- Install/export/package contents still match public headers and repo-local Codex guidance. +- Only verification steps actually executed are reported as passing. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ab71f4d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,89 @@ +# Adminive Codex Instructions + +## Mandatory skill usage + +- Use `$adminive-development` before editing Adminive runtime/API/protocol code, adapters, the Gallery frontend, tests, CMake, install/export, or packaging behavior. +- If the task touches `third_party/Structive/`, also read `third_party/Structive/AGENTS.md` and use its `$structive-development` workflow. Structive rules override this file inside that subtree. +- Do not use Git commands or modify repository history. + +## Project purpose + +Adminive projects ordinary C++ business objects into backend-admin capabilities. The backend definition is the source of truth for object descriptors, field presentation, form/collection views, page composition, JSON/HTTP contracts, AMIS schemas, managed mutation, status, and transactions. The React frontend consumes generated contracts and must not maintain a second business-field schema. + +Read `README.md` for usage and `backend/library/DESIGN.md` for architecture before changing public behavior. + +## Architecture boundaries + +- `third_party/Structive`: intrinsic property/schema/constraint/synchronization/runtime-structural capability. It must not learn Adminive UI, HTTP, AMIS, CRUD, or persistence semantics. +- `backend/library`: Adminive Core protocol. Keep it independent of concrete JSON libraries, HTTP frameworks, enum libraries, PFR, and frontend frameworks. +- `backend/service/include/adminive/adapters`: concrete bridges for nlohmann JSON, magic_enum, Boost.PFR, cpp-httplib, and Drogon. +- `backend/service/src`: example runtime, Gallery, persistence sample, and server wiring. Do not move reusable protocol behavior here. +- `frontend`: renderer/documentation client. It consumes descriptors/views/manifests/AMIS; do not duplicate C++ business fields in React. + +When deciding where a feature belongs, preserve this dependency direction: + +```text +Structive -> Adminive Core -> Service adapters -> Example runtime/frontend +``` + +## Compatibility and implementation rules + +- Preserve the existing function signature and semantic contract whenever modifying an existing function. +- If replacing an implementation, delete the old implementation. Do not add compatibility shims unless the design explicitly requires compatibility. +- Do not introduce a second implementation path merely to support old behavior. +- Add validation at the outer protocol/transport boundary when needed; do not repeat equivalent checks in every inner layer. +- Static facts belong in C++20 `constexpr`, concepts, `requires`, or schema validation. Dynamic input belongs in runtime validation. +- `managed writable` is not the same as frontend `editable`/`creatable`. +- `sensitive` fields must not leak through frontend data or default-bearing descriptors. +- Collection query capability is enforced by the backend View Schema. Transport adapters must not silently grant or silently ignore unsupported sort/search/filter fields. +- `frontend` composition delegates final layout to the frontend; other composition kinds retain backend semantic positioning. +- AMIS is an adapter target, not the business description language. +- Synchronization is for in-process mutable consistency. `Resource_Transaction` is for external prepare/commit/rollback. Do not merge those concepts. + +## Coding style + +- C++ uses K&R brace style. +- Do not add meaningless blank lines. +- Keep comments adjacent to the code they explain; do not separate a comment from its code with a blank line. +- Prefer small semantic functions over compatibility wrappers or defensive checks at every layer. +- CMake paths must be relative to the `.cmake`/`CMakeLists.txt` that owns them, normally through `CMAKE_CURRENT_LIST_DIR`. Do not base project paths on `CMAKE_SOURCE_DIR`/`PROJECT_SOURCE_DIR` unless the existing design explicitly requires it. +- Do not change build/output path patterns or add random path components. +- If a PowerShell script is needed, write it for PowerShell 7 and run it with `pwsh`. + +## Tests and verification + +- Tests must use `ADMINIVE_CHECK`, not standard `assert()`, because Release defines `NDEBUG`. +- Public headers must remain independently includable; keep header compile tests up to date when adding/removing public headers. +- Protocol changes require focused unit/protocol tests and transport tests when the behavior is visible over HTTP. +- Changes to install/export/package behavior require install-consumer and package ZIP tests. +- Changes to frontend TypeScript/React require `npm run build`; renderer/protocol logic should also run `npm run test:unit`. Browser-visible behavior should run the Playwright E2E gate when dependencies are available. +- Do not claim a verification step passed unless it was actually executed. + +Useful commands from the repository root: + +```text +cmake -S . -B verification/debug -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON +cmake --build verification/debug +ctest --test-dir verification/debug --output-on-failure +python scripts/verify.py +``` + +For a focused test, build and run the smallest affected target first, then run the broader gate before handoff. + +## Packaging + +- `package_zip` is part of the release contract. Keep `AGENTS.md`, `.agents/skills/`, and the nested Structive guidance in the source archive. +- Generated source ZIP timestamps must use China Standard Time (`UTC+08:00`). +- Do not package build directories, `node_modules`, frontend build output, test reports, temporary verification trees, or stale removed implementations. + +## Review priorities + +When reviewing a change, check in this order: + +1. Public signature/semantic compatibility. +2. Layer ownership and dependency direction. +3. Descriptor/View/Composition/HTTP contract consistency. +4. Sensitive/write/query authorization boundaries. +5. Managed synchronization and transaction behavior. +6. Adapter parity, especially httplib versus Drogon. +7. Independent public-header compilation, install consumer, package contents, frontend build, and E2E coverage. diff --git a/CMakeLists.txt b/CMakeLists.txt index 432d530..2e631ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,9 @@ include("${CMAKE_CURRENT_LIST_DIR}/cmake/AdminiveVerification.cmake") add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/third_party/Structive") add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/backend") set(adminive_package_includes + "/.agents/" "/.gitignore" + "/AGENTS.md" "/backend/" "/cmake/" "/config/" diff --git a/README.md b/README.md index 4d2ad93..998aa10 100644 --- a/README.md +++ b/README.md @@ -445,7 +445,7 @@ options.context_factory = [](const httplib::Request& request) { resource.bind(server, std::move(options)); ``` -这个 Context 只在最外层 HTTP bridge 构造,`Resource_Service` 和 transaction 内层不重复检查 transport。真实 `Adminive_Httplib_Adapter_Test` 会绑定 localhost 临时端口并通过 cpp-httplib Client 验证 descriptor/view/data/AMIS、Context 传递、CRUD、paging、sort/search/filter、非法 query、row reorder 和 item status。 +这个 Context 只在最外层 HTTP bridge 构造,`Resource_Service` 和 transaction 内层不重复检查 transport。`Http_Status_Resource` 与 Drogon 的状态资源保持同一协议,注册 `/descriptor`、`/amis` 和 `/data`,状态 reader 同样可以读取 `Request_Context`。Context factory 或状态 reader 抛出的异常在 transport 边界转换成结构化 HTTP 500,不允许异常穿过 HTTP 回调。真实 `Adminive_Httplib_Adapter_Test` 会绑定 localhost 临时端口并通过 cpp-httplib Client 验证 descriptor/view/data/AMIS、Context 传递、独立 Status Resource、CRUD、paging、sort/search/filter、非法 query、row reorder 和 item status。 ### Drogon 生命周期和异步提交 @@ -466,7 +466,7 @@ options.filters = {"LoginFilter", "AdminPermissionFilter"}; resource.bind(drogon::app(), std::move(options)); ``` -整体状态使用 `Drogon_Status_Resource` 注册 `/descriptor`、`/amis` 和 `/data`。列表 CRUD 使用 `Drogon_Collection_Resource`,和 httplib 的 `Http_Collection_Resource` 共用纯头文件 `Collection_Service`;Drogon/httplib 只负责路由参数、请求体和响应桥接,不再各自实现 CRUD 协议。当前 `Collection_Service` 是线程安全的内存集合服务,尚未定义数据库 repository/transaction 协议;需要把列表直接落数据库时,应在这一层补持久化抽象,而不是把数据库逻辑塞回 Drogon 或 httplib binder。所有 Drogon 路由都会把异常转换成结构化 HTTP 500。 +整体状态使用 `Drogon_Status_Resource` 注册 `/descriptor`、`/amis` 和 `/data`。列表 CRUD 使用 `Drogon_Collection_Resource`,和 httplib 的 `Http_Collection_Resource` 共用纯头文件 `Collection_Service`;Drogon/httplib 只负责路由参数、请求体和响应桥接,不再各自实现 CRUD 协议。两个 transport 都把除 `page/perPage/orderBy/orderDir` 之外的动态 query 字段交给 `Collection_Service`,因此未被 View Schema 声明为 searchable/filterable 的字段会统一返回 400,不能在某个 adapter 中静默忽略。当前 `Collection_Service` 是线程安全的内存集合服务,尚未定义数据库 repository/transaction 协议;需要把列表直接落数据库时,应在这一层补持久化抽象,而不是把数据库逻辑塞回 Drogon 或 httplib binder。所有 Drogon 路由都会把异常转换成结构化 HTTP 500。 ### 描述器驱动状态 diff --git a/backend/library/DESIGN.md b/backend/library/DESIGN.md index 5a5df2b..f3c16a0 100644 --- a/backend/library/DESIGN.md +++ b/backend/library/DESIGN.md @@ -893,7 +893,7 @@ magic_enum Boost.PFR ``` -放 Adapter/bridge 层。 +放 Adapter/bridge 层。同一 Core Service 被多个 transport 复用时,transport 只负责把请求完整翻译成 Core 输入;它们不能各自发明更宽松的协议。例如 Collection GET 中除分页/排序保留参数之外的 query 字段必须交给 `Collection_Service` 统一校验,httplib 与 Drogon 都不能静默丢弃 View Schema 未授权的筛选字段。独立状态资源也应保持 `/descriptor`、`/amis`、`/data` 协议对齐,请求上下文与异常只在 transport 边界处理。 ### 21.4 它属于具体业务项目吗? diff --git a/backend/service/include/adminive/adapters/drogon.hpp b/backend/service/include/adminive/adapters/drogon.hpp index 2374373..f681375 100644 --- a/backend/service/include/adminive/adapters/drogon.hpp +++ b/backend/service/include/adminive/adapters/drogon.hpp @@ -199,15 +199,11 @@ public: query.per_page = read_drogon_size_parameter(request, "perPage", 20); query.order_by = request->getParameter("orderBy"); query.order_dir = request->getParameter("orderDir"); - const auto view = describe_table_view(); - for(const auto& column : view.columns) { - if(!column.searchable && !column.filterable) { + for(const auto& [name, value] : request->getParameters()) { + if(name == "page" || name == "perPage" || name == "orderBy" || name == "orderDir") { continue; } - const std::string value = request->getParameter(column.field); - if(!value.empty()) { - query.fields.insert_or_assign(column.field, value); - } + query.fields.insert_or_assign(name, value); } complete_drogon_request(callback, [service, query = std::move(query)]() mutable { return service->list_response(std::move(query)); diff --git a/backend/service/include/adminive/adapters/httplib.hpp b/backend/service/include/adminive/adapters/httplib.hpp index cb5812b..c9c0a55 100644 --- a/backend/service/include/adminive/adapters/httplib.hpp +++ b/backend/service/include/adminive/adapters/httplib.hpp @@ -69,13 +69,59 @@ public: write_http_response(response, service->amis_response()); }); server.Post(service->path() + "/data", [service, context_factory](const httplib::Request& request, httplib::Response& response) { - const Request_Context context = context_factory ? context_factory(request) : Request_Context{}; - write_http_response(response, service->update_response(request.body, context)); + try { + const Request_Context context = context_factory ? context_factory(request) : Request_Context{}; + write_http_response(response, service->update_response(request.body, context)); + } catch(const std::exception& error) { + write_http_response(response, make_http_error(500, error.what())); + } catch(...) { + write_http_response(response, make_http_error(500, "unknown server error")); + } }); } private: std::shared_ptr service_; }; +template +class Http_Status_Resource { +public: + using Reader = std::function; + Http_Status_Resource(std::string path, Reader reader, std::uint64_t interval = 2000) : state_(std::make_shared(State{std::move(path), std::move(reader), interval, to_status_descriptor_json()})) {} + Json amis_schema() const { + return make_amis_status_service(state_->descriptor, state_->path + "/data", state_->interval); + } + void bind(httplib::Server& server) const { + bind(server, {}); + } + void bind(httplib::Server& server, Httplib_Bind_Options options) const { + const auto state = state_; + const auto context_factory = std::move(options.context_factory); + server.Get(state->path + "/descriptor", [state](const httplib::Request&, httplib::Response& response) { + write_http_response(response, make_http_success(state->descriptor)); + }); + server.Get(state->path + "/amis", [state](const httplib::Request&, httplib::Response& response) { + write_http_response(response, make_http_success(make_amis_status_service(state->descriptor, state->path + "/data", state->interval))); + }); + server.Get(state->path + "/data", [state, context_factory](const httplib::Request& request, httplib::Response& response) { + try { + const Request_Context context = context_factory ? context_factory(request) : Request_Context{}; + write_http_response(response, make_http_success(to_status_json(state->reader(context)))); + } catch(const std::exception& error) { + write_http_response(response, make_http_error(500, error.what())); + } catch(...) { + write_http_response(response, make_http_error(500, "unknown server error")); + } + }); + } +private: + struct State { + std::string path; + Reader reader; + std::uint64_t interval; + Json descriptor; + }; + std::shared_ptr state_; +}; template requires Table_View_Described_Type && std::default_initializable && std::copy_constructible && std::assignable_from class Http_Collection_Resource { diff --git a/backend/service/src/gallery.cpp b/backend/service/src/gallery.cpp index ffef52f..6035bda 100644 --- a/backend/service/src/gallery.cpp +++ b/backend/service/src/gallery.cpp @@ -365,7 +365,7 @@ Json Component_Gallery::documentation() const { cases.push_back(make_doc_case("validation-boundaries", "safety", "Validation 与协议边界", "字段校验、nested path、object validator、readonly 提交、多态 discriminator、非法 Composition/Manifest 和非法 Collection query 都有可重复的标准错误。", "apply_frontend_patch(...); validate_composition_view(...); validate_composition_slot_contracts(...)", examples["protocol_boundaries"], examples["validation_422"], make_gallery_note("协议边界", "在“协议边界”页面可以逐项执行真实请求并查看标准错误。"), string_array({"validation_422", "field_errors", "nested_path", "object_validator", "readonly_reject", "invalid_grid", "invalid_tab", "missing_slot_contract", "duplicate_slot", "invalid_sort", "invalid_filter"}), string_array({"/admin/gallery/boundaries", "/admin/gallery/items?orderBy=color&orderDir=asc", "/admin/gallery/items?score=92"}))); Json status_endpoints = Json::array({"/admin/status", std::string(radio_states_api) + "/{id}/status"}); cases.push_back(make_doc_case("status", "runtime", "Status Polling:overview + item", "状态 Descriptor 与轮询 renderer 分离;完整运行时同时展示 overview status 和 Collection item status。", "make_amis_status_service(descriptor, api, interval); collection.register_status<&T::status>()", status_descriptor, status_amis, status_amis, string_array({"status_polling", "overview_status", "item_status"}), std::move(status_endpoints))); - cases.push_back(make_doc_case("http-adapters", "adapters", "HTTP Adapter:Httplib + Drogon", "Httplib 现在与 Drogon 一样可以通过 context_factory 构造 Request_Context;真实 localhost 黑盒测试覆盖 CRUD、查询、状态和上下文传递。", "resource.bind(server, Httplib_Bind_Options{.context_factory = ...}); Drogon_Bind_Options{...}", Json{{"httplib", "context_factory -> Request_Context"}, {"drogon", "context_factory + executor/filter"}}, Json{{"tests", string_array({"Adminive_Httplib_Adapter_Test", "Adminive_Drogon_Adapter_Test"})}}, make_gallery_note("Transport Adapter", "当前 Gallery 本身运行在 Httplib;Drogon 契约由同一 service 层和独立 adapter test 验证。"), string_array({"httplib", "drogon", "request_context", "real_http_test"}), string_array({"/admin/gallery/config/data"}))); + cases.push_back(make_doc_case("http-adapters", "adapters", "HTTP Adapter:Httplib + Drogon", "Httplib 与 Drogon 都从 transport 构造 Request_Context,并把非保留 Collection query 完整交给同一 Service 校验;Httplib 也提供独立 Http_Status_Resource。真实黑盒测试覆盖 CRUD、严格查询、状态、异常边界和上下文传递。", "resource.bind(server, Httplib_Bind_Options{.context_factory = ...}); Http_Status_Resource(...); Drogon_Bind_Options{...}", Json{{"httplib", "context_factory + strict query + standalone status"}, {"drogon", "context_factory + strict query + executor/filter"}}, Json{{"tests", string_array({"Adminive_Httplib_Adapter_Test", "Adminive_Drogon_Adapter_Test"})}}, make_gallery_note("Transport Adapter", "当前 Gallery 本身运行在 Httplib;两种 transport 对共享 Service 的 query 与错误语义保持一致,Drogon 由独立 adapter test 验证。"), string_array({"httplib", "drogon", "request_context", "strict_query", "standalone_status", "real_http_test"}), string_array({"/admin/gallery/config/data"}))); const Json verification_view = Json{{"ctest_labels", string_array({"unit", "protocol", "http", "install", "package", "header", "concurrency"})}, {"sanitizer_matrix", Json{{"clang_gnu", string_array({"ASan", "UBSan", "TSan"})}, {"msvc", string_array({"ASan"})}, {"unsupported_configuration", "configure_error"}}}, {"configurations", string_array({"Debug", "Release"})}}; const Json verification_runtime = Json{{"headers", "all public Adminive headers compile standalone"}, {"install_consumers", string_array({"Adminive::Core", "Adminive::Default + Httplib"})}, {"package_zip", "exact root files + no stale nested project"}, {"frontend", string_array({"node_test", "playwright_chromium", "playwright_msedge"})}}; cases.push_back(make_doc_case("verification", "engineering", "工程验证 Gate", "Release-safe test checks、公共头独立编译、源码包结构、Core + Default/Httplib 安装消费、真实 HTTP、Debug/Release 和平台真实支持的 Sanitizer 都有正式入口。", "ctest -L header; ctest -L package; ctest -L install; python scripts/verify.py", verification_view, verification_runtime, make_gallery_note("Verification Gate", "测试断言在 Release 下不会被 NDEBUG 清除;不支持的 sanitizer 配置会明确失败或 SKIP,不会假通过。"), string_array({"release_safe_checks", "public_header_compile", "package_zip", "install_consumer", "debug", "release", "asan", "ubsan", "tsan", "frontend_unit", "playwright", "edge"}))); diff --git a/backend/service/tests/drogon_adapter_test.cpp b/backend/service/tests/drogon_adapter_test.cpp index ecbd82f..a8ac8d9 100644 --- a/backend/service/tests/drogon_adapter_test.cpp +++ b/backend/service/tests/drogon_adapter_test.cpp @@ -129,6 +129,25 @@ int main() { }); ADMINIVE_CHECK(response->status == drogon::k200OK); ADMINIVE_CHECK(Json::parse(response->body).at("data").at("total") == 1); + request->parameters = {{"unknown", "value"}}; + response.reset(); + app.handle("/configs", drogon::Get, request, [&response](const drogon::HttpResponsePtr& value) { + response = value; + }); + ADMINIVE_CHECK(response->status == drogon::k400BadRequest); + request->parameters = {{"orderBy", "unknown"}, {"orderDir", "asc"}}; + response.reset(); + app.handle("/configs", drogon::Get, request, [&response](const drogon::HttpResponsePtr& value) { + response = value; + }); + ADMINIVE_CHECK(response->status == drogon::k400BadRequest); + request->parameters = {{"orderBy", "name"}, {"orderDir", "sideways"}}; + response.reset(); + app.handle("/configs", drogon::Get, request, [&response](const drogon::HttpResponsePtr& value) { + response = value; + }); + ADMINIVE_CHECK(response->status == drogon::k400BadRequest); + request->parameters.clear(); response.reset(); app.handle("/configs/1", drogon::Get, request, [&response](const drogon::HttpResponsePtr& value) { response = value; diff --git a/backend/service/tests/fake_drogon/drogon/drogon.h b/backend/service/tests/fake_drogon/drogon/drogon.h index cc1f3a1..46487d9 100644 --- a/backend/service/tests/fake_drogon/drogon/drogon.h +++ b/backend/service/tests/fake_drogon/drogon/drogon.h @@ -72,6 +72,9 @@ public: std::string_view getBody() const noexcept { return body; } + const std::map& getParameters() const noexcept { + return parameters; + } const std::string& getParameter(const std::string& name) const { const auto iterator = parameters.find(name); return iterator == parameters.end() ? empty_parameter_ : iterator->second; diff --git a/backend/service/tests/httplib_adapter_test.cpp b/backend/service/tests/httplib_adapter_test.cpp index 46bdd83..0d65fbd 100644 --- a/backend/service/tests/httplib_adapter_test.cpp +++ b/backend/service/tests/httplib_adapter_test.cpp @@ -2,6 +2,7 @@ #include "adminive/adapters/httplib.hpp" #include "adminive_test.hpp" #include +#include #include #include #include @@ -14,6 +15,9 @@ struct Config { struct Item_Status { std::string state{"ready"}; }; +struct Overview_Status { + std::string user; +}; struct Row { std::string name; bool enabled{true}; @@ -39,6 +43,13 @@ struct Type_Descriptor { } }; template <> +struct Type_Descriptor { + static auto get() { + using T = httplib_adapter_test::Overview_Status; + return object("httplib_overview_status", ADMINIVE_FIELD(T, user)); + } +}; +template <> struct Type_Descriptor { static auto get() { using T = httplib_adapter_test::Row; @@ -77,17 +88,30 @@ int main() { committed_remote = context.remote_address; }; adminive::Http_Resource resource(config, "/config", transaction); + adminive::Http_Status_Resource status("/status", [](const adminive::Request_Context& context) { + if(context.user == "status-error") { + throw std::runtime_error("status failed"); + } + return Overview_Status{context.user}; + }, 25); adminive::Http_Collection_Resource collection("/rows", {Row{"alpha", true, 70}, Row{"beta", false, 90}, Row{"gamma", true, 50}}); collection.register_status<&Row::status>(25); httplib::Server server; - resource.bind(server, adminive::Httplib_Bind_Options{[](const httplib::Request& request) { + adminive::Httplib_Bind_Options options; + options.context_factory = [](const httplib::Request& request) { + const std::string user = request.get_header_value("X-User"); + if(user == "context-error") { + throw std::runtime_error("context failed"); + } adminive::Request_Context context; - context.user = request.get_header_value("X-User"); + context.user = user; context.request_id = request.get_header_value("X-Request-Id"); context.remote_address = request.remote_addr; context.attributes.emplace("transport", "httplib"); return context; - }}); + }; + resource.bind(server, options); + status.bind(server, options); collection.bind(server); const int port = server.bind_to_any_port("127.0.0.1"); ADMINIVE_CHECK(port > 0); @@ -107,6 +131,15 @@ int main() { ADMINIVE_CHECK(committed_user == "alice"); ADMINIVE_CHECK(committed_request_id == "request-42"); ADMINIVE_CHECK(committed_remote == "127.0.0.1"); + httplib::Headers context_error_headers{{"X-User", "context-error"}}; + check_status(client.Post("/config/data", context_error_headers, R"({"name":"not-applied"})", "application/json"), 500); + ADMINIVE_CHECK(config.name == "updated"); + check_status(client.Get("/status/descriptor"), 200); + check_status(client.Get("/status/amis"), 200); + ADMINIVE_CHECK(body_json(client.Get("/status/data", headers)).at("data").at("user").at("value") == "alice"); + httplib::Headers status_error_headers{{"X-User", "status-error"}}; + check_status(client.Get("/status/data", status_error_headers), 500); + check_status(client.Get("/status/data", context_error_headers), 500); ADMINIVE_CHECK(body_json(client.Get("/rows/descriptor")).at("data").at("name") == "httplib_row"); ADMINIVE_CHECK(body_json(client.Get("/rows/view")).at("data").at("kind") == "table"); ADMINIVE_CHECK(body_json(client.Get("/rows?perPage=2&page=1&orderBy=score&orderDir=desc")).at("data").at("items").at(0).at("name") == "beta"); diff --git a/cmake/RunPackageZipTest.cmake b/cmake/RunPackageZipTest.cmake index 451fc71..acfaea9 100644 --- a/cmake/RunPackageZipTest.cmake +++ b/cmake/RunPackageZipTest.cmake @@ -19,6 +19,10 @@ string(REPLACE "\n" ";" archive_entries "${archive_list}") set(root_cmake_count 0) set(root_readme_count 0) set(root_gitignore_count 0) +set(root_agents_count 0) +set(adminive_skill_count 0) +set(structive_agents_count 0) +set(structive_skill_count 0) foreach(entry IN LISTS archive_entries) if(entry MATCHES "^Adminive/") message(FATAL_ERROR "package_zip contains nested stale project entry: ${entry}") @@ -29,6 +33,14 @@ foreach(entry IN LISTS archive_entries) math(EXPR root_readme_count "${root_readme_count} + 1") elseif(entry STREQUAL ".gitignore") math(EXPR root_gitignore_count "${root_gitignore_count} + 1") + elseif(entry STREQUAL "AGENTS.md") + math(EXPR root_agents_count "${root_agents_count} + 1") + elseif(entry STREQUAL ".agents/skills/adminive-development/SKILL.md") + math(EXPR adminive_skill_count "${adminive_skill_count} + 1") + elseif(entry STREQUAL "third_party/Structive/AGENTS.md") + math(EXPR structive_agents_count "${structive_agents_count} + 1") + elseif(entry STREQUAL "third_party/Structive/.agents/skills/structive-development/SKILL.md") + math(EXPR structive_skill_count "${structive_skill_count} + 1") elseif(entry STREQUAL "backend/library/include/adminive/synchronized.hpp" OR entry STREQUAL "backend/service/tests/synchronized_test.cpp") message(FATAL_ERROR "package_zip contains removed synchronized implementation: ${entry}") endif() @@ -36,3 +48,6 @@ endforeach() if(NOT root_cmake_count EQUAL 1 OR NOT root_readme_count EQUAL 1 OR NOT root_gitignore_count EQUAL 1) message(FATAL_ERROR "package_zip root exact-file selection is invalid") endif() +if(NOT root_agents_count EQUAL 1 OR NOT adminive_skill_count EQUAL 1 OR NOT structive_agents_count EQUAL 1 OR NOT structive_skill_count EQUAL 1) + message(FATAL_ERROR "package_zip is missing Codex repository guidance") +endif()