feat: select versioned Ebox renderer port

This commit is contained in:
Kinneyzhang 2026-09-01 00:27:13 +08:00
parent 24ca887ae4
commit d5ccb30125
8 changed files with 545 additions and 6 deletions

View File

@ -1,8 +1,8 @@
EMACS ?= emacs
LOAD_PATH = -L . -L examples -L scripts -L ../ebox -L ../tp -L ../ecss
SOURCES = etaf-view.el etaf-compiler.el etaf-component.el etaf-reactive.el etaf-observer.el etaf-context.el etaf-theme-tp.el etaf-resource.el etaf-data.el etaf-renderer.el etaf-runtime.el etaf-behavior.el etaf-actions.el etaf-events.el etaf-performance.el etaf.el scripts/emacs-gui-verifier.el
SOURCES = etaf-view.el etaf-compiler.el etaf-component.el etaf-reactive.el etaf-observer.el etaf-context.el etaf-theme-tp.el etaf-resource.el etaf-data.el etaf-render-port.el etaf-renderer.el etaf-runtime.el etaf-behavior.el etaf-actions.el etaf-events.el etaf-performance.el etaf.el scripts/emacs-gui-verifier.el
EXAMPLES = examples/etaf-counter-example.el examples/etaf-data-example.el examples/etaf-resource-example.el
TESTS = tests/etaf-tests.el tests/etaf-compiler-tests.el tests/etaf-component-frontends-tests.el tests/etaf-resource-tests.el tests/etaf-data-tests.el tests/etaf-theme-tp-tests.el tests/etaf-examples-tests.el tests/etaf-observer-tests.el tests/etaf-performance-tests.el tests/etaf-gui-verifier-tests.el tests/etaf-m0a-current-characterization-tests.el tests/etaf-interaction-contract-tests.el tests/etaf-m0b-component-manifest-tests.el
TESTS = tests/etaf-tests.el tests/etaf-compiler-tests.el tests/etaf-component-frontends-tests.el tests/etaf-resource-tests.el tests/etaf-data-tests.el tests/etaf-theme-tp-tests.el tests/etaf-examples-tests.el tests/etaf-observer-tests.el tests/etaf-performance-tests.el tests/etaf-gui-verifier-tests.el tests/etaf-m0a-current-characterization-tests.el tests/etaf-interaction-contract-tests.el tests/etaf-m0b-component-manifest-tests.el tests/etaf-render-port-tests.el
.PHONY: test compile load checkdoc docs-check check clean

View File

@ -246,7 +246,7 @@ Strings are the smallest text View and lower to Ebox content. Nested `text` View
`grid` reuses Ebox's public two-dimensional layout contract. Its properties include track templates, `auto`/fractional/`minmax` tracks, gaps, row/column placement, spans, auto-flow, and item alignment. Ebox owns track measurement and placement; ETAF only maps the `grid` Host into that node. Ebox's optional native reflow backend is not required for correctness; a Grid tree uses the ordinary Ebox renderer when that backend does not support the node.
ETAF's Renderer is the only framework module that calls Ebox. It uses Ebox public constructors, property readers, host-reference queries, and publication APIs. Ebox does not know about Components, slots, Actions, Context, Behaviors, or Data.
ETAF's Renderer is the only framework module that lowers View semantics into Ebox nodes, properties, and Host queries; `etaf-render-port.el` is the only module that probes the versioned Ebox framework SPI and selects a publication route. Both use only public Ebox APIs, while Runtime reads the already-selected immutable port instead of guessing the Ebox version. Ebox does not know about Components, slots, Actions, Context, Behaviors, or Data.
The public View grammar does not accept raw Ebox Nodes. Framework integrations construct canonical TextNode and BoxNode values through Ebox's typed integration port, while ordinary ETAF applications stay on Hosts and Components. This keeps measurement, identity, and rollback ownership inside one lowering path.

View File

@ -241,7 +241,7 @@ text · fragment · container · row · column · stack · flex · grid · space
`grid` 复用 Ebox 的公共二维布局契约,支持轨道模板、`auto`/分数/`minmax` 轨道、间距、行列放置、跨度、自动流向和 item 对齐。轨道测量和放置由 Ebox 负责ETAF 只把 `grid` Host 映射到这个节点。Ebox 的可选 native reflow 后端不参与正确性保证;如果该后端不支持 GridGrid 树会使用普通 Ebox 渲染器。
ETAF 的 Renderer 是唯一调用 Ebox 的框架模块,并且只使用 Ebox 公共构造器、属性读取器、Host 引用查询和发布 API。Ebox 不理解 Component、slot、Action、Context、Behavior 或 Data。
ETAF 的 Renderer 是唯一把 View 语义 lower 为 Ebox 节点、属性与 Host 查询的框架模块;`etaf-render-port.el` 是唯一探测 versioned Ebox framework SPI 并选择发布路径的模块。两者都只使用 Ebox 公共 APIRuntime 只读取已经选定的 immutable port不再自行猜测 Ebox 版本。Ebox 不理解 Component、slot、Action、Context、Behavior 或 Data。
公共 View 语法不接受裸 Ebox Node。框架集成层通过 Ebox 的 typed integration port 构造规范的 TextNode 和 BoxNode普通 ETAF 应用只使用 Host 与 Component。这样 measurement、identity 和 rollback 始终由同一条 lowering 路径负责。

371
etaf-render-port.el Normal file
View File

@ -0,0 +1,371 @@
;;; etaf-render-port.el --- Versioned Ebox renderer port -*- lexical-binding: t; -*-
;; SPDX-License-Identifier: GPL-3.0-or-later
;;; Commentary:
;; This file is ETAF's only Ebox framework-SPI bootstrap owner. It probes the
;; additive Ebox v2 provider once, selects one immutable port, and exposes that
;; selection to downstream ETAF code without repeated `featurep' or `fboundp'
;; protocol guesses. A completely absent v2 provider receives a complete v1
;; fallback; a present but broken or incompatible provider fails closed.
;;; Code:
(require 'cl-lib)
(require 'subr-x)
(require 'ebox)
(define-error 'etaf-spi-bootstrap-error
"Malformed Ebox framework SPI provider")
(define-error 'etaf-spi-incompatible-error
"Incompatible Ebox framework SPI provider"
'etaf-spi-bootstrap-error)
(defconst etaf-render-port--required-spi-version 2
"Ebox framework SPI version consumed by this ETAF build.")
(defconst etaf-render-port--required-schema-version
'ebox-framework-spi-schema/v2
"Ebox provider schema consumed by this ETAF build.")
(defconst etaf-render-port--required-capabilities
'(initial-paired-stage-rollback
update-paired-stage-rollback
combined-participant-ordering
same-object-legacy-report)
"Capabilities required from an Ebox framework SPI v2 provider.")
(defconst etaf-render-port--required-tp-protocol
'tp-transaction-protocol-v1+v2
"TP transaction protocol required by the Ebox framework SPI.")
(defconst etaf-render-port--required-stage-order
'(ebox-mirror/native framework-stage)
"Required stage order inside the combined Ebox participant.")
(defconst etaf-render-port--required-rollback-order
'(framework-rollback ebox-mirror/native tp)
"Required rollback order inside the combined Ebox participant.")
(cl-defstruct
(etaf-render-port
(:constructor etaf-render-port--create)
(:conc-name etaf-render-port--))
"Immutable selected Ebox rendering capability."
(route nil :read-only t)
(spi-version nil :read-only t)
(schema-version nil :read-only t)
(capabilities nil :read-only t)
(tp-protocol nil :read-only t)
(initial-function nil :read-only t)
(update-function nil :read-only t)
(bootstrap-outcome nil :read-only t)
(provider nil :read-only t))
(defun etaf-render-port-route (port)
"Return selected PORT route, either `v1' or `v2'."
(etaf-render-port--route port))
(defun etaf-render-port-spi-version (port)
"Return PORT's selected framework SPI version."
(etaf-render-port--spi-version port))
(defun etaf-render-port-schema-version (port)
"Return PORT's selected provider schema version."
(etaf-render-port--schema-version port))
(defun etaf-render-port-capabilities (port)
"Return a defensive copy of PORT's capabilities."
(copy-sequence (etaf-render-port--capabilities port)))
(defun etaf-render-port-tp-protocol (port)
"Return PORT's selected TP transaction protocol."
(etaf-render-port--tp-protocol port))
(defun etaf-render-port-initial-function (port)
"Return PORT's initial mount function symbol."
(etaf-render-port--initial-function port))
(defun etaf-render-port-update-function (port)
"Return PORT's update function symbol."
(etaf-render-port--update-function port))
(defun etaf-render-port-bootstrap-outcome (port)
"Return PORT's immutable bootstrap outcome tag."
(etaf-render-port--bootstrap-outcome port))
(defun etaf-render-port--bootstrap-error (reason &optional detail)
"Signal a fail-closed bootstrap error for REASON and DETAIL."
(signal 'etaf-spi-bootstrap-error
(list :reason reason :detail detail)))
(defun etaf-render-port--ensure-accessors ()
"Require every public Ebox v2 record accessor before reading a provider."
(dolist
(function
'(ebox-framework-spi-provider-p
ebox-framework-spi-provider-spi-version
ebox-framework-spi-provider-schema-version
ebox-framework-spi-provider-capabilities
ebox-framework-spi-provider-tp-protocol
ebox-framework-spi-provider-stage-order
ebox-framework-spi-provider-rollback-order
ebox-framework-spi-provider-report-semantics
ebox-framework-spi-provider-initial-operation
ebox-framework-spi-provider-update-operation
ebox-framework-spi-operation-p
ebox-framework-spi-operation-kind
ebox-framework-spi-operation-function
ebox-framework-spi-operation-argument-schema
ebox-framework-spi-operation-result-schema
ebox-framework-spi-operation-paired-stage-rollback-p))
(unless (fboundp function)
(etaf-render-port--bootstrap-error
'missing-provider-accessor function))))
(defun etaf-render-port--operation-snapshot (operation label)
"Return validated immutable field snapshot for OPERATION named LABEL."
(unless (ebox-framework-spi-operation-p operation)
(etaf-render-port--bootstrap-error
'malformed-operation (list :label label :value operation)))
(let ((kind (ebox-framework-spi-operation-kind operation))
(function (ebox-framework-spi-operation-function operation))
(arguments
(ebox-framework-spi-operation-argument-schema operation))
(result (ebox-framework-spi-operation-result-schema operation))
(paired
(ebox-framework-spi-operation-paired-stage-rollback-p operation)))
(unless (and (symbolp kind)
(symbolp function)
(fboundp function)
(proper-list-p arguments)
(cl-every #'symbolp arguments)
(symbolp result)
(memq paired '(nil t)))
(etaf-render-port--bootstrap-error
'malformed-operation-fields
(list :label label :kind kind :function function
:arguments arguments :result result :paired paired)))
(list :kind kind :function function
:arguments (copy-sequence arguments)
:result result :paired paired)))
(defun etaf-render-port--provider-snapshot (provider)
"Return a validated field snapshot of Ebox v2 PROVIDER."
(etaf-render-port--ensure-accessors)
(unless (ebox-framework-spi-provider-p provider)
(etaf-render-port--bootstrap-error 'non-provider-record provider))
(let ((spi-version (ebox-framework-spi-provider-spi-version provider))
(schema-version
(ebox-framework-spi-provider-schema-version provider))
(capabilities
(ebox-framework-spi-provider-capabilities provider))
(tp-protocol (ebox-framework-spi-provider-tp-protocol provider))
(stage-order (ebox-framework-spi-provider-stage-order provider))
(rollback-order
(ebox-framework-spi-provider-rollback-order provider))
(report-semantics
(ebox-framework-spi-provider-report-semantics provider)))
(unless (and (integerp spi-version)
(> spi-version 0)
(symbolp schema-version)
(proper-list-p capabilities)
(cl-every #'symbolp capabilities)
(symbolp tp-protocol)
(proper-list-p stage-order)
(cl-every #'symbolp stage-order)
(proper-list-p rollback-order)
(cl-every #'symbolp rollback-order)
(symbolp report-semantics))
(etaf-render-port--bootstrap-error
'malformed-provider-fields
(list :spi-version spi-version :schema-version schema-version
:capabilities capabilities :tp-protocol tp-protocol
:stage-order stage-order :rollback-order rollback-order
:report-semantics report-semantics)))
(list
:provider provider
:spi-version spi-version
:schema-version schema-version
:capabilities (copy-sequence capabilities)
:tp-protocol tp-protocol
:stage-order (copy-sequence stage-order)
:rollback-order (copy-sequence rollback-order)
:report-semantics report-semantics
:initial
(etaf-render-port--operation-snapshot
(ebox-framework-spi-provider-initial-operation provider) 'initial)
:update
(etaf-render-port--operation-snapshot
(ebox-framework-spi-provider-update-operation provider) 'update))))
(defun etaf-render-port--incompatibilities (snapshot)
"Return deterministic compatibility failures in provider SNAPSHOT."
(let (failures)
(unless (eql (plist-get snapshot :spi-version)
etaf-render-port--required-spi-version)
(push (list :spi-version (plist-get snapshot :spi-version)) failures))
(unless (eq (plist-get snapshot :schema-version)
etaf-render-port--required-schema-version)
(push (list :schema-version (plist-get snapshot :schema-version))
failures))
(let ((capabilities (plist-get snapshot :capabilities)))
(dolist (capability etaf-render-port--required-capabilities)
(unless (memq capability capabilities)
(push (list :missing-capability capability) failures))))
(unless (eq (plist-get snapshot :tp-protocol)
etaf-render-port--required-tp-protocol)
(push (list :tp-protocol (plist-get snapshot :tp-protocol)) failures))
(unless (equal (plist-get snapshot :stage-order)
etaf-render-port--required-stage-order)
(push (list :stage-order (plist-get snapshot :stage-order)) failures))
(unless (equal (plist-get snapshot :rollback-order)
etaf-render-port--required-rollback-order)
(push (list :rollback-order (plist-get snapshot :rollback-order))
failures))
(unless (eq (plist-get snapshot :report-semantics)
'same-object-legacy-report)
(push (list :report-semantics
(plist-get snapshot :report-semantics))
failures))
(dolist
(expectation
`((:initial initial ebox-framework-spi-initial
(buffer canonical-input framework-stage framework-rollback))
(:update update ebox-framework-spi-update
(buffer canonical-input-or-candidate
framework-stage framework-rollback))))
(let* ((slot (nth 0 expectation))
(operation (plist-get snapshot slot)))
(unless (and (eq (plist-get operation :kind) (nth 1 expectation))
(eq (plist-get operation :function) (nth 2 expectation))
(equal (plist-get operation :arguments)
(nth 3 expectation))
(eq (plist-get operation :result)
'ebox-legacy-report/same-object)
(eq (plist-get operation :paired) t))
(push (list slot operation) failures))))
(nreverse failures)))
(defun etaf-render-port--validate-framework-pair
(framework-stage framework-rollback)
"Validate FRAMEWORK-STAGE and FRAMEWORK-ROLLBACK as one required pair."
(unless (functionp framework-stage)
(signal 'wrong-type-argument (list 'functionp framework-stage)))
(unless (functionp framework-rollback)
(signal 'wrong-type-argument (list 'functionp framework-rollback)))
t)
(defun etaf-render-port--v1-initial
(buffer input framework-stage framework-rollback)
"Publish INPUT initially to BUFFER through legacy Ebox.
FRAMEWORK-STAGE runs after publication; FRAMEWORK-ROLLBACK performs contained
manual framework cleanup if staging fails."
(etaf-render-port--validate-framework-pair
framework-stage framework-rollback)
(let ((result (ebox-render-to-buffer buffer input))
stage-entered)
(condition-case primary
(progn
(setq stage-entered t)
(funcall framework-stage nil)
result)
((error quit)
(when stage-entered
(let ((inhibit-quit t) (quit-flag nil))
(condition-case nil
(funcall framework-rollback nil)
((error quit) nil))))
(signal (car primary) (cdr primary))))))
(defun etaf-render-port--v1-update
(buffer input framework-stage framework-rollback)
"Update BUFFER from INPUT through the legacy Ebox callback pair.
FRAMEWORK-STAGE and FRAMEWORK-ROLLBACK retain their existing Ebox meanings."
(etaf-render-port--validate-framework-pair
framework-stage framework-rollback)
(ebox-commit buffer input framework-stage framework-rollback))
(defun etaf-render-port--v1-fallback ()
"Return the complete immutable fallback port for an absent Ebox v2 SPI."
(etaf-render-port--create
:route 'v1
:spi-version 1
:schema-version 'etaf-ebox-v1-fallback/v1
:capabilities
'(initial-manual-cleanup update-paired-stage-rollback
same-object-update-report)
:tp-protocol 'tp-transaction-protocol-v1
:initial-function 'etaf-render-port--v1-initial
:update-function 'etaf-render-port--v1-update
:bootstrap-outcome 'v2-absent-v1-selected))
(defun etaf-render-port--v2-port (snapshot)
"Return an immutable selected v2 port from compatible SNAPSHOT."
(let ((failures (etaf-render-port--incompatibilities snapshot)))
(when failures
(signal 'etaf-spi-incompatible-error
(list :incompatibilities failures)))
(etaf-render-port--create
:route 'v2
:spi-version (plist-get snapshot :spi-version)
:schema-version (plist-get snapshot :schema-version)
:capabilities (copy-sequence (plist-get snapshot :capabilities))
:tp-protocol (plist-get snapshot :tp-protocol)
:initial-function
(plist-get (plist-get snapshot :initial) :function)
:update-function
(plist-get (plist-get snapshot :update) :function)
:bootstrap-outcome 'valid-v2-selected
:provider (plist-get snapshot :provider))))
(defun etaf-render-port--bootstrap ()
"Probe Ebox exactly once and return one immutable selected render port."
(let ((feature-present-p (featurep 'ebox-framework-spi-v2))
(predicate-present-p (fboundp 'ebox-framework-spi-capabilities)))
(cond
((and (not feature-present-p) (not predicate-present-p))
(etaf-render-port--v1-fallback))
((not feature-present-p)
(etaf-render-port--bootstrap-error 'predicate-without-v2-feature))
((not predicate-present-p)
(etaf-render-port--bootstrap-error 'v2-feature-without-predicate))
(t
(condition-case condition
(etaf-render-port--v2-port
(etaf-render-port--provider-snapshot
(ebox-framework-spi-capabilities)))
((etaf-spi-incompatible-error etaf-spi-bootstrap-error)
(signal (car condition) (cdr condition)))
((error quit)
(etaf-render-port--bootstrap-error
'provider-predicate-failure condition)))))))
(defconst etaf-render-port--selected-port
(etaf-render-port--bootstrap)
"Process-wide immutable Ebox render port selected during ETAF bootstrap.")
(defun etaf-render-port-selected ()
"Return the process-wide immutable Ebox render port."
etaf-render-port--selected-port)
(defun etaf-render-port-initial
(buffer input framework-stage framework-rollback)
"Run selected initial operation for BUFFER and canonical INPUT.
FRAMEWORK-STAGE and FRAMEWORK-ROLLBACK are one required callback pair."
(funcall (etaf-render-port-initial-function
etaf-render-port--selected-port)
buffer input framework-stage framework-rollback))
(defun etaf-render-port-update
(buffer input framework-stage framework-rollback)
"Run selected update operation for BUFFER and INPUT.
FRAMEWORK-STAGE and FRAMEWORK-ROLLBACK are one required callback pair."
(funcall (etaf-render-port-update-function
etaf-render-port--selected-port)
buffer input framework-stage framework-rollback))
(provide 'etaf-render-port)
;;; etaf-render-port.el ends here

View File

@ -15,6 +15,7 @@
(require 'etaf-view)
(require 'etaf-component)
(require 'etaf-reactive)
(require 'etaf-render-port)
(require 'etaf-renderer)
(require 'etaf-context)
(require 'etaf-behavior)
@ -5575,7 +5576,7 @@ RENDERED-IDENTITIES names the Component render participants."
candidate (etaf--semantic-host-host-ref (car change))
(etaf--runtime-lower-semantic-input
runtime candidate-generation (nth 3 change)))))
(ebox-commit
(etaf-render-port-update
(etaf-runtime-buffer runtime) candidate
(lambda (_report)
(etaf--runtime-participant-publish participant))
@ -5657,7 +5658,7 @@ RENDERED-IDENTITIES names the Component render participants."
(let ((candidate (ebox-candidate-begin
(etaf-runtime-buffer runtime))))
(ebox-candidate-replace-root candidate next-root)
(ebox-commit
(etaf-render-port-update
(etaf-runtime-buffer runtime) candidate
(lambda (_report)
(etaf--runtime-participant-publish participant))

View File

@ -35,6 +35,7 @@
(require 'etaf-context)
(require 'etaf-resource)
(require 'etaf-data)
(require 'etaf-render-port)
(require 'etaf-renderer)
(etaf--prefer-local-files)
(require 'etaf-runtime)

View File

@ -0,0 +1,162 @@
;;; etaf-render-port-tests.el --- M3a renderer-port gates -*- lexical-binding: t; -*-
;; SPDX-License-Identifier: GPL-3.0-or-later
(require 'cl-lib)
(require 'ert)
(require 'etaf-render-port)
(defconst etaf-render-port-test--root
(file-name-directory
(directory-file-name
(file-name-directory (or load-file-name buffer-file-name))))
"ETAF package root used by static bootstrap-owner checks.")
(ert-deftest etaf-render-port-selects-valid-v2-immutably ()
"A valid provider produces one immutable v2 selected port."
(let* ((port (etaf-render-port--bootstrap))
(capabilities (etaf-render-port-capabilities port)))
(should (etaf-render-port-p port))
(should (eq (etaf-render-port-route port) 'v2))
(should (= (etaf-render-port-spi-version port) 2))
(should (eq (etaf-render-port-schema-version port)
'ebox-framework-spi-schema/v2))
(should (eq (etaf-render-port-tp-protocol port)
'tp-transaction-protocol-v1+v2))
(should (eq (etaf-render-port-initial-function port)
'ebox-framework-spi-initial))
(should (eq (etaf-render-port-update-function port)
'ebox-framework-spi-update))
(should (eq (etaf-render-port-bootstrap-outcome port)
'valid-v2-selected))
(setcar capabilities 'mutated)
(should (eq (car (etaf-render-port-capabilities port))
'initial-paired-stage-rollback))
(should-error
(eval `(setf (etaf-render-port--route ',port) 'broken)))))
(ert-deftest etaf-render-port-falls-back-only-when-v2-is-fully-absent ()
"Only complete feature/predicate absence selects the immutable v1 port."
(let ((original-featurep (symbol-function 'featurep)))
(cl-letf (((symbol-function 'featurep)
(lambda (feature)
(and (not (eq feature 'ebox-framework-spi-v2))
(funcall original-featurep feature))))
((symbol-function 'ebox-framework-spi-capabilities) nil))
(let ((port (etaf-render-port--bootstrap)))
(should (eq (etaf-render-port-route port) 'v1))
(should (= (etaf-render-port-spi-version port) 1))
(should (eq (etaf-render-port-initial-function port)
'etaf-render-port--v1-initial))
(should (eq (etaf-render-port-update-function port)
'etaf-render-port--v1-update))
(should (eq (etaf-render-port-bootstrap-outcome port)
'v2-absent-v1-selected))))))
(ert-deftest etaf-render-port-rejects-half-present-v2 ()
"Feature-only and predicate-only providers fail instead of downgrading."
(cl-letf (((symbol-function 'ebox-framework-spi-capabilities) nil))
(should-error (etaf-render-port--bootstrap)
:type 'etaf-spi-bootstrap-error))
(let ((original-featurep (symbol-function 'featurep)))
(cl-letf (((symbol-function 'featurep)
(lambda (feature)
(and (not (eq feature 'ebox-framework-spi-v2))
(funcall original-featurep feature)))))
(should-error (etaf-render-port--bootstrap)
:type 'etaf-spi-bootstrap-error))))
(ert-deftest etaf-render-port-rejects-broken-v2-provider ()
"Throwing, non-record, and malformed providers fail closed."
(cl-letf
(((symbol-function 'ebox-framework-spi-capabilities)
(lambda () (error "injected provider failure"))))
(should-error (etaf-render-port--bootstrap)
:type 'etaf-spi-bootstrap-error))
(cl-letf
(((symbol-function 'ebox-framework-spi-capabilities)
(lambda () 'not-a-provider)))
(should-error (etaf-render-port--bootstrap)
:type 'etaf-spi-bootstrap-error))
(let ((provider (ebox-framework-spi-capabilities)))
(cl-letf
(((symbol-function 'ebox-framework-spi-capabilities)
(lambda () provider))
((symbol-function 'ebox-framework-spi-provider-capabilities)
(lambda (_provider) "not-a-capability-list")))
(should-error (etaf-render-port--bootstrap)
:type 'etaf-spi-bootstrap-error))))
(ert-deftest etaf-render-port-rejects-incompatible-v2-provider ()
"Version, required-capability, and TP protocol mismatches are incompatible."
(let ((provider (ebox-framework-spi-capabilities)))
(cl-letf
(((symbol-function 'ebox-framework-spi-capabilities)
(lambda () provider))
((symbol-function 'ebox-framework-spi-provider-spi-version)
(lambda (_provider) 99)))
(should-error (etaf-render-port--bootstrap)
:type 'etaf-spi-incompatible-error))
(cl-letf
(((symbol-function 'ebox-framework-spi-capabilities)
(lambda () provider))
((symbol-function 'ebox-framework-spi-provider-capabilities)
(lambda (_provider)
'(initial-paired-stage-rollback
update-paired-stage-rollback
same-object-legacy-report))))
(should-error (etaf-render-port--bootstrap)
:type 'etaf-spi-incompatible-error))
(cl-letf
(((symbol-function 'ebox-framework-spi-capabilities)
(lambda () provider))
((symbol-function 'ebox-framework-spi-provider-tp-protocol)
(lambda (_provider) 'tp-transaction-protocol-v0)))
(should-error (etaf-render-port--bootstrap)
:type 'etaf-spi-incompatible-error))))
(ert-deftest etaf-render-port-v1-initial-runs-manual-framework-cleanup ()
"A failed legacy initial stage invokes its paired cleanup exactly once."
(let (trace)
(cl-letf (((symbol-function 'ebox-render-to-buffer)
(lambda (&rest _arguments)
(push 'render trace)
'rendered-buffer)))
(should-error
(etaf-render-port--v1-initial
'buffer 'input
(lambda (_report)
(push 'stage trace)
(error "injected v1 initial failure"))
(lambda (_report) (push 'rollback trace))))
(should (equal (nreverse trace) '(render stage rollback))))))
(ert-deftest etaf-render-port-is-the-only-protocol-probe-owner ()
"No downstream ETAF module probes Ebox framework SPI protocol state."
(dolist (file (directory-files etaf-render-port-test--root t "\\.el\\'"))
(unless (string= (file-name-nondirectory file) "etaf-render-port.el")
(with-temp-buffer
(insert-file-contents file)
(let ((source (buffer-string)))
(should-not (string-match-p "ebox-framework-spi" source))
(should-not (string-match-p "ebox-framework-spi-v2" source)))))))
(ert-deftest etaf-render-port-owns-runtime-update-routing ()
"Runtime updates use the selected port instead of calling Ebox directly."
(with-temp-buffer
(insert-file-contents
(expand-file-name "etaf-runtime.el" etaf-render-port-test--root))
(let ((source (buffer-string)))
(should (string-match-p "(etaf-render-port-update" source))
(should-not (string-match-p "(ebox-commit" source)))))
(ert-deftest etaf-render-port-selected-port-is-process-stable ()
"Every downstream read returns the one bootstrap-selected port identity."
(should (eq (etaf-render-port-selected)
(etaf-render-port-selected)))
(should (memq (etaf-render-port-route (etaf-render-port-selected))
'(v1 v2))))
(provide 'etaf-render-port-tests)
;;; etaf-render-port-tests.el ends here

View File

@ -8,6 +8,10 @@
(:file "etaf-performance.el" :form condition-case :conditions (error) :owner etaf-performance :policy generic-containment)
(:file "etaf-reactive.el" :form condition-case :conditions (error quit) :owner etaf-reactive :policy generic-containment)
(:file "etaf-reactive.el" :form condition-case :conditions (error quit) :owner etaf-reactive :policy generic-containment)
(:file "etaf-render-port.el" :form condition-case :conditions (error quit) :owner etaf-render-port :policy generic-containment)
(:file "etaf-render-port.el" :form condition-case :conditions (error quit) :owner etaf-render-port :policy generic-containment)
(:file "etaf-render-port.el" :form condition-case :conditions (error quit) :owner etaf-render-port :policy generic-containment)
(:file "etaf-render-port.el" :form condition-case :conditions (etaf-spi-incompatible-error etaf-spi-bootstrap-error) :owner etaf-render-port :policy specific-compatibility)
(:file "etaf-resource.el" :form condition-case :conditions (error) :owner etaf-resource :policy generic-containment)
(:file "etaf-resource.el" :form condition-case :conditions (error) :owner etaf-resource :policy generic-containment)
(:file "etaf-runtime.el" :form condition-case :conditions (error) :owner etaf-runtime :policy generic-containment)