Add domain states and event runtime (T-002)
This commit is contained in:
@@ -0,0 +1,51 @@
|
|||||||
|
package application
|
||||||
|
|
||||||
|
// EventType identifies an application event consumed by UI adapters.
|
||||||
|
type EventType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
EventCatalogRefreshed EventType = "CatalogRefreshed"
|
||||||
|
EventCatalogRejected EventType = "CatalogRejected"
|
||||||
|
EventDownloadStarted EventType = "DownloadStarted"
|
||||||
|
EventDownloadProgress EventType = "DownloadProgress"
|
||||||
|
EventDownloadPaused EventType = "DownloadPaused"
|
||||||
|
EventDownloadCompleted EventType = "DownloadCompleted"
|
||||||
|
EventDownloadFailed EventType = "DownloadFailed"
|
||||||
|
EventInstallCompleted EventType = "InstallCompleted"
|
||||||
|
EventInstallRolledBack EventType = "InstallRolledBack"
|
||||||
|
EventAppStarted EventType = "AppStarted"
|
||||||
|
EventAppExited EventType = "AppExited"
|
||||||
|
EventLicenseChanged EventType = "LicenseChanged"
|
||||||
|
)
|
||||||
|
|
||||||
|
var validEventTypes = map[EventType]struct{}{
|
||||||
|
EventCatalogRefreshed: {},
|
||||||
|
EventCatalogRejected: {},
|
||||||
|
EventDownloadStarted: {},
|
||||||
|
EventDownloadProgress: {},
|
||||||
|
EventDownloadPaused: {},
|
||||||
|
EventDownloadCompleted: {},
|
||||||
|
EventDownloadFailed: {},
|
||||||
|
EventInstallCompleted: {},
|
||||||
|
EventInstallRolledBack: {},
|
||||||
|
EventAppStarted: {},
|
||||||
|
EventAppExited: {},
|
||||||
|
EventLicenseChanged: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid reports whether eventType is part of the documented event contract.
|
||||||
|
func (eventType EventType) Valid() bool {
|
||||||
|
_, ok := validEventTypes[eventType]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event is the common envelope delivered from background use cases to adapters.
|
||||||
|
//
|
||||||
|
// Payload is event-specific and will be replaced by concrete payload types as
|
||||||
|
// the corresponding use cases are implemented.
|
||||||
|
type Event struct {
|
||||||
|
Type EventType
|
||||||
|
RequestID string
|
||||||
|
AppID string
|
||||||
|
Payload any
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package application
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestEventTypeValid(t *testing.T) {
|
||||||
|
eventTypes := []EventType{
|
||||||
|
EventCatalogRefreshed,
|
||||||
|
EventCatalogRejected,
|
||||||
|
EventDownloadStarted,
|
||||||
|
EventDownloadProgress,
|
||||||
|
EventDownloadPaused,
|
||||||
|
EventDownloadCompleted,
|
||||||
|
EventDownloadFailed,
|
||||||
|
EventInstallCompleted,
|
||||||
|
EventInstallRolledBack,
|
||||||
|
EventAppStarted,
|
||||||
|
EventAppExited,
|
||||||
|
EventLicenseChanged,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, eventType := range eventTypes {
|
||||||
|
if !eventType.Valid() {
|
||||||
|
t.Errorf("event type %q should be valid", eventType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if EventType("Unknown").Valid() {
|
||||||
|
t.Fatal("unknown event type should be invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidEvent indicates that an event is not part of the event contract.
|
||||||
|
ErrInvalidEvent = errors.New("invalid application event")
|
||||||
|
// ErrRuntimeClosed indicates that the runtime no longer accepts events.
|
||||||
|
ErrRuntimeClosed = errors.New("application runtime closed")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Runtime is the minimal event bus shared by background use cases and adapters.
|
||||||
|
type Runtime struct {
|
||||||
|
events chan Event
|
||||||
|
done chan struct{}
|
||||||
|
closeOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRuntime creates an event runtime with the requested queue capacity.
|
||||||
|
func NewRuntime(buffer int) *Runtime {
|
||||||
|
if buffer < 0 {
|
||||||
|
panic("application runtime buffer must not be negative")
|
||||||
|
}
|
||||||
|
return &Runtime{
|
||||||
|
events: make(chan Event, buffer),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish queues an event or returns when the context/runtime is closed.
|
||||||
|
func (runtime *Runtime) Publish(ctx context.Context, event Event) error {
|
||||||
|
if !event.Type.Valid() {
|
||||||
|
return fmt.Errorf("%w: unknown type %q", ErrInvalidEvent, event.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-runtime.done:
|
||||||
|
return ErrRuntimeClosed
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-runtime.done:
|
||||||
|
return ErrRuntimeClosed
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case runtime.events <- event:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events exposes the read-only event stream to adapters.
|
||||||
|
func (runtime *Runtime) Events() <-chan Event {
|
||||||
|
return runtime.events
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done is closed when the runtime stops accepting events.
|
||||||
|
func (runtime *Runtime) Done() <-chan struct{} {
|
||||||
|
return runtime.done
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close stops future publishes. It is safe to call more than once.
|
||||||
|
func (runtime *Runtime) Close() {
|
||||||
|
runtime.closeOnce.Do(func() {
|
||||||
|
close(runtime.done)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRuntimePublish(t *testing.T) {
|
||||||
|
runtime := NewRuntime(1)
|
||||||
|
event := Event{
|
||||||
|
Type: EventDownloadStarted,
|
||||||
|
RequestID: "request-1",
|
||||||
|
AppID: "json-parser",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runtime.Publish(context.Background(), event); err != nil {
|
||||||
|
t.Fatalf("Publish() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := <-runtime.Events()
|
||||||
|
if got != event {
|
||||||
|
t.Fatalf("Events() got %#v, want %#v", got, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRuntimeRejectsInvalidEvent(t *testing.T) {
|
||||||
|
runtime := NewRuntime(1)
|
||||||
|
|
||||||
|
err := runtime.Publish(context.Background(), Event{Type: EventType("Unknown")})
|
||||||
|
if !errors.Is(err, ErrInvalidEvent) {
|
||||||
|
t.Fatalf("Publish() error = %v, want %v", err, ErrInvalidEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRuntimePublishHonorsContextCancellation(t *testing.T) {
|
||||||
|
runtime := NewRuntime(1)
|
||||||
|
if err := runtime.Publish(context.Background(), Event{Type: EventDownloadStarted}); err != nil {
|
||||||
|
t.Fatalf("first Publish() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
err := runtime.Publish(ctx, Event{Type: EventDownloadProgress})
|
||||||
|
if !errors.Is(err, context.Canceled) {
|
||||||
|
t.Fatalf("blocked Publish() error = %v, want %v", err, context.Canceled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRuntimeClose(t *testing.T) {
|
||||||
|
runtime := NewRuntime(1)
|
||||||
|
runtime.Close()
|
||||||
|
runtime.Close()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-runtime.Done():
|
||||||
|
default:
|
||||||
|
t.Fatal("Done() should be closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := runtime.Publish(context.Background(), Event{Type: EventDownloadStarted})
|
||||||
|
if !errors.Is(err, ErrRuntimeClosed) {
|
||||||
|
t.Fatalf("Publish() error = %v, want %v", err, ErrRuntimeClosed)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AppStatus describes the user-visible lifecycle state of one catalog app.
|
||||||
|
type AppStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusNotInstalled AppStatus = "not_installed"
|
||||||
|
StatusQueued AppStatus = "queued"
|
||||||
|
StatusDownloading AppStatus = "downloading"
|
||||||
|
StatusVerifying AppStatus = "verifying"
|
||||||
|
StatusExtracting AppStatus = "extracting"
|
||||||
|
StatusInstalling AppStatus = "installing"
|
||||||
|
StatusInstalled AppStatus = "installed"
|
||||||
|
StatusUpdateAvailable AppStatus = "update_available"
|
||||||
|
StatusRunning AppStatus = "running"
|
||||||
|
StatusFailed AppStatus = "failed"
|
||||||
|
StatusRollbackPending AppStatus = "rollback_pending"
|
||||||
|
StatusIncompatible AppStatus = "incompatible"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidStatus indicates that a transition contains an unknown status.
|
||||||
|
ErrInvalidStatus = errors.New("invalid app status")
|
||||||
|
// ErrInvalidTransition indicates that two valid states cannot transition directly.
|
||||||
|
ErrInvalidTransition = errors.New("invalid app status transition")
|
||||||
|
)
|
||||||
|
|
||||||
|
var validStatuses = map[AppStatus]struct{}{
|
||||||
|
StatusNotInstalled: {},
|
||||||
|
StatusQueued: {},
|
||||||
|
StatusDownloading: {},
|
||||||
|
StatusVerifying: {},
|
||||||
|
StatusExtracting: {},
|
||||||
|
StatusInstalling: {},
|
||||||
|
StatusInstalled: {},
|
||||||
|
StatusUpdateAvailable: {},
|
||||||
|
StatusRunning: {},
|
||||||
|
StatusFailed: {},
|
||||||
|
StatusRollbackPending: {},
|
||||||
|
StatusIncompatible: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
type statusTransition struct {
|
||||||
|
from AppStatus
|
||||||
|
to AppStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowedTransitions = map[statusTransition]struct{}{
|
||||||
|
{StatusNotInstalled, StatusQueued}: {},
|
||||||
|
{StatusNotInstalled, StatusIncompatible}: {},
|
||||||
|
{StatusQueued, StatusDownloading}: {},
|
||||||
|
{StatusQueued, StatusNotInstalled}: {},
|
||||||
|
{StatusQueued, StatusInstalled}: {},
|
||||||
|
{StatusQueued, StatusUpdateAvailable}: {},
|
||||||
|
{StatusQueued, StatusFailed}: {},
|
||||||
|
{StatusDownloading, StatusQueued}: {},
|
||||||
|
{StatusDownloading, StatusVerifying}: {},
|
||||||
|
{StatusDownloading, StatusNotInstalled}: {},
|
||||||
|
{StatusDownloading, StatusInstalled}: {},
|
||||||
|
{StatusDownloading, StatusUpdateAvailable}: {},
|
||||||
|
{StatusDownloading, StatusFailed}: {},
|
||||||
|
{StatusVerifying, StatusExtracting}: {},
|
||||||
|
{StatusVerifying, StatusFailed}: {},
|
||||||
|
{StatusExtracting, StatusInstalling}: {},
|
||||||
|
{StatusExtracting, StatusFailed}: {},
|
||||||
|
{StatusInstalling, StatusInstalled}: {},
|
||||||
|
{StatusInstalling, StatusRollbackPending}: {},
|
||||||
|
{StatusInstalling, StatusFailed}: {},
|
||||||
|
{StatusInstalled, StatusUpdateAvailable}: {},
|
||||||
|
{StatusInstalled, StatusRunning}: {},
|
||||||
|
{StatusInstalled, StatusQueued}: {},
|
||||||
|
{StatusInstalled, StatusIncompatible}: {},
|
||||||
|
{StatusUpdateAvailable, StatusQueued}: {},
|
||||||
|
{StatusUpdateAvailable, StatusRunning}: {},
|
||||||
|
{StatusUpdateAvailable, StatusInstalled}: {},
|
||||||
|
{StatusUpdateAvailable, StatusIncompatible}: {},
|
||||||
|
{StatusRunning, StatusInstalled}: {},
|
||||||
|
{StatusRunning, StatusUpdateAvailable}: {},
|
||||||
|
{StatusFailed, StatusQueued}: {},
|
||||||
|
{StatusFailed, StatusNotInstalled}: {},
|
||||||
|
{StatusFailed, StatusInstalled}: {},
|
||||||
|
{StatusFailed, StatusUpdateAvailable}: {},
|
||||||
|
{StatusFailed, StatusRollbackPending}: {},
|
||||||
|
{StatusRollbackPending, StatusInstalled}: {},
|
||||||
|
{StatusRollbackPending, StatusFailed}: {},
|
||||||
|
{StatusIncompatible, StatusNotInstalled}: {},
|
||||||
|
{StatusIncompatible, StatusInstalled}: {},
|
||||||
|
{StatusIncompatible, StatusUpdateAvailable}: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid reports whether status is one of the documented domain states.
|
||||||
|
func (status AppStatus) Valid() bool {
|
||||||
|
_, ok := validStatuses[status]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanTransition reports whether from can move directly to to.
|
||||||
|
func CanTransition(from, to AppStatus) bool {
|
||||||
|
return ValidateTransition(from, to) == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateTransition verifies a direct state change.
|
||||||
|
func ValidateTransition(from, to AppStatus) error {
|
||||||
|
if !from.Valid() {
|
||||||
|
return fmt.Errorf("%w: %q", ErrInvalidStatus, from)
|
||||||
|
}
|
||||||
|
if !to.Valid() {
|
||||||
|
return fmt.Errorf("%w: %q", ErrInvalidStatus, to)
|
||||||
|
}
|
||||||
|
if _, ok := allowedTransitions[statusTransition{from: from, to: to}]; !ok {
|
||||||
|
return &TransitionError{From: from, To: to}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransitionError describes a rejected direct state change.
|
||||||
|
type TransitionError struct {
|
||||||
|
From AppStatus
|
||||||
|
To AppStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *TransitionError) Error() string {
|
||||||
|
return fmt.Sprintf("%s: %s -> %s", ErrInvalidTransition, err.From, err.To)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap allows callers to use errors.Is with ErrInvalidTransition.
|
||||||
|
func (err *TransitionError) Unwrap() error {
|
||||||
|
return ErrInvalidTransition
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAppStatusValid(t *testing.T) {
|
||||||
|
statuses := []AppStatus{
|
||||||
|
StatusNotInstalled,
|
||||||
|
StatusQueued,
|
||||||
|
StatusDownloading,
|
||||||
|
StatusVerifying,
|
||||||
|
StatusExtracting,
|
||||||
|
StatusInstalling,
|
||||||
|
StatusInstalled,
|
||||||
|
StatusUpdateAvailable,
|
||||||
|
StatusRunning,
|
||||||
|
StatusFailed,
|
||||||
|
StatusRollbackPending,
|
||||||
|
StatusIncompatible,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, status := range statuses {
|
||||||
|
if !status.Valid() {
|
||||||
|
t.Errorf("status %q should be valid", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if AppStatus("unknown").Valid() {
|
||||||
|
t.Fatal("unknown status should be invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateTransition(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
from AppStatus
|
||||||
|
to AppStatus
|
||||||
|
want error
|
||||||
|
}{
|
||||||
|
{name: "queue install", from: StatusNotInstalled, to: StatusQueued},
|
||||||
|
{name: "start download", from: StatusQueued, to: StatusDownloading},
|
||||||
|
{name: "pause download", from: StatusDownloading, to: StatusQueued},
|
||||||
|
{name: "finish install", from: StatusInstalling, to: StatusInstalled},
|
||||||
|
{name: "start app", from: StatusInstalled, to: StatusRunning},
|
||||||
|
{name: "recover rollback", from: StatusRollbackPending, to: StatusInstalled},
|
||||||
|
{name: "skip install pipeline", from: StatusNotInstalled, to: StatusInstalled, want: ErrInvalidTransition},
|
||||||
|
{name: "download while running", from: StatusRunning, to: StatusDownloading, want: ErrInvalidTransition},
|
||||||
|
{name: "extract incompatible app", from: StatusIncompatible, to: StatusExtracting, want: ErrInvalidTransition},
|
||||||
|
{name: "same status", from: StatusInstalled, to: StatusInstalled, want: ErrInvalidTransition},
|
||||||
|
{name: "unknown source", from: AppStatus("unknown"), to: StatusQueued, want: ErrInvalidStatus},
|
||||||
|
{name: "unknown target", from: StatusQueued, to: AppStatus("unknown"), want: ErrInvalidStatus},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
err := ValidateTransition(test.from, test.to)
|
||||||
|
if test.want == nil {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ValidateTransition(%q, %q) error = %v", test.from, test.to, err)
|
||||||
|
}
|
||||||
|
if !CanTransition(test.from, test.to) {
|
||||||
|
t.Fatalf("CanTransition(%q, %q) = false, want true", test.from, test.to)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.Is(err, test.want) {
|
||||||
|
t.Fatalf("ValidateTransition(%q, %q) error = %v, want %v", test.from, test.to, err, test.want)
|
||||||
|
}
|
||||||
|
if CanTransition(test.from, test.to) {
|
||||||
|
t.Fatalf("CanTransition(%q, %q) = true, want false", test.from, test.to)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,13 +15,13 @@
|
|||||||
- 日期:2026-07-16
|
- 日期:2026-07-16
|
||||||
- 阶段:MVP 起步(Phase 0 工程骨架建设中)
|
- 阶段:MVP 起步(Phase 0 工程骨架建设中)
|
||||||
- 技术栈:根 Go workspace 纳入 core/app-modern/app-win7 三模块;`app-win7/go.work` 隔离 Go 1.20.14 构建;Gio UI 与平台 stub 待后续 Phase 0 任务接入
|
- 技术栈:根 Go workspace 纳入 core/app-modern/app-win7 三模块;`app-win7/go.work` 隔离 Go 1.20.14 构建;Gio UI 与平台 stub 待后续 Phase 0 任务接入
|
||||||
- 生产代码:已有最小 core 包与 modern/win7 命令入口;尚无业务功能
|
- 生产代码:core 已有软件状态模型、迁移规则、application Event 合约与 Runtime 事件总线骨架;modern/win7 仍为无 UI 命令入口
|
||||||
- 测试:core 已有最小真实单元测试
|
- 测试:core 已覆盖状态合法性/非法迁移、事件类型、发布/取消/关闭
|
||||||
- 数据:无;Catalog 清单与测试样例待建
|
- 数据:无;Catalog 清单与测试样例待建
|
||||||
- 标准启动路径:`./init.sh` / `./init.ps1`(同步依赖、验证 core、打印 modern/win7 双目标构建命令)
|
- 标准启动路径:`./init.sh` / `./init.ps1`(同步依赖、验证 core、打印 modern/win7 双目标构建命令)
|
||||||
- 标准验证路径:`go -C core vet ./...` + `go -C core test -count=1 ./...`;双目标构建命令见下文
|
- 标准验证路径:`go -C core vet ./...` + `go -C core test -count=1 ./...`;双目标构建命令见下文
|
||||||
- 版本管理:git 已初始化,main 分支,远端 origin 为 Gitea `opc/soft_quay`;harness 文档已提交
|
- 版本管理:git 已初始化,main 分支,远端 origin 为 Gitea `opc/soft_quay`;harness 文档已提交
|
||||||
- 当前 blocker:无;下一步按路线图落成并领取 T-002
|
- 当前 blocker:无;下一步按路线图落成并领取 T-003
|
||||||
|
|
||||||
## 当前目录要点
|
## 当前目录要点
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
| `docs/` | 已有 | harness coding 文档集(本次初始化完成) |
|
| `docs/` | 已有 | harness coding 文档集(本次初始化完成) |
|
||||||
| `docs/tasks/` | 已有 | 任务目录;T-001 已完成,T-002 待按路线图落成 |
|
| `docs/tasks/` | 已有 | 任务目录;T-001 已完成,T-002 待按路线图落成 |
|
||||||
| `scripts/` | 已有 | harness 治理脚本(validate_agent_context 等);构建脚本待建 |
|
| `scripts/` | 已有 | harness 治理脚本(validate_agent_context 等);构建脚本待建 |
|
||||||
| `core/` | 已建 | Go 1.20 兼容共享模块;当前仅最小包与测试 |
|
| `core/` | 已建 | Go 1.20 兼容共享模块;已有 domain 状态模型与 application 事件 runtime |
|
||||||
| `app-modern/` | 已建 | Go 1.25.0 + Gio v0.10.1 模块;当前为无 UI 命令入口 |
|
| `app-modern/` | 已建 | Go 1.25.0 + Gio v0.10.1 模块;当前为无 UI 命令入口 |
|
||||||
| `app-win7/` | 已建 | Go 1.20 + Gio v0.6.0 模块;当前为无 UI 命令入口 |
|
| `app-win7/` | 已建 | Go 1.20 + Gio v0.6.0 模块;当前为无 UI 命令入口 |
|
||||||
| `schemas/` | 待建 | 协议 JSON Schema(T-201) |
|
| `schemas/` | 待建 | 协议 JSON Schema(T-201) |
|
||||||
@@ -40,9 +40,9 @@
|
|||||||
|
|
||||||
任务状态以 `docs/tasks/` 各任务文件 frontmatter 的 `status` 为准。本节只写项目级摘要:
|
任务状态以 `docs/tasks/` 各任务文件 frontmatter 的 `status` 为准。本节只写项目级摘要:
|
||||||
|
|
||||||
- 已完成:`T-001 初始化 monorepo 骨架`。
|
- 已完成:`T-001 初始化 monorepo 骨架`、`T-002 建立 domain 状态模型与事件总线`。
|
||||||
- 正在进行:无。
|
- 正在进行:无。
|
||||||
- 下一个可领取任务:按路线图落成并领取 `T-002 建立 domain 状态模型与事件总线`。
|
- 下一个可领取任务:按路线图落成并领取 `T-003 Gio 空窗口 + 平台层 stub`。
|
||||||
|
|
||||||
## 当前可运行内容
|
## 当前可运行内容
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
id: T-002
|
||||||
|
title: 建立 domain 状态模型与 application 事件总线
|
||||||
|
phase: 0
|
||||||
|
deps: [T-001]
|
||||||
|
status: DONE
|
||||||
|
created: 2026-07-16
|
||||||
|
issue: null
|
||||||
|
context_ref: cf9fc01c68476ba3ee4f69525958f2e62ba2d6ce
|
||||||
|
claim_branch: null
|
||||||
|
work_branch: agent/codex/T-002
|
||||||
|
write_paths:
|
||||||
|
- docs/tasks/T-002.md
|
||||||
|
- core/domain/
|
||||||
|
- core/application/
|
||||||
|
- docs/current-state.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## 问题 / 背景
|
||||||
|
|
||||||
|
T-001 只建立了可编译骨架。后续清单、下载、安装、启动和 UI 都需要共享、稳定的软件状态模型,后台任务也需要通过统一事件通道与 UI 解耦。
|
||||||
|
|
||||||
|
## 方案
|
||||||
|
|
||||||
|
1. 在 `core/domain` 定义 `docs/04-architecture.md` 已列出的 12 个软件状态及显式迁移表。
|
||||||
|
2. 提供状态合法性与迁移校验 API,非法迁移返回包含起止状态的稳定错误。
|
||||||
|
3. 在 `core/application` 定义 `docs/api.md` 已列出的事件类型与通用 `Event` 信封。
|
||||||
|
4. 建立带缓冲事件通道、上下文取消与关闭信号的 `Runtime` 骨架;关闭后拒绝新事件。
|
||||||
|
5. 使用表驱动测试覆盖合法/非法状态迁移、未知状态、事件发布、取消与关闭。
|
||||||
|
|
||||||
|
## 验收要点
|
||||||
|
|
||||||
|
- 软件状态枚举与架构文档一致,不新增未定义状态。
|
||||||
|
- 表驱动测试至少覆盖 `not_installed → installed`、`running → downloading` 等非法迁移。
|
||||||
|
- application 事件类型与 `docs/api.md` 一致;未知类型不能发布。
|
||||||
|
- runtime 可以发布/消费事件,阻塞发布可由 context 取消,关闭后返回稳定错误。
|
||||||
|
- `cd core && go vet ./... && go test -count=1 ./...` 通过。
|
||||||
|
- modern 与 win7 两个目标继续可编译。
|
||||||
|
|
||||||
|
## 边界(不改什么)
|
||||||
|
|
||||||
|
- 不实现清单、下载、安装、授权等具体用例。
|
||||||
|
- 不引入 Gio、Windows API 或第三方依赖。
|
||||||
|
- 不修改 app UI 与平台层。
|
||||||
|
|
||||||
|
## 协作约束
|
||||||
|
|
||||||
|
未启用 Gitea;本任务在 `agent/codex/T-002` 分支串行执行。新增写路径前必须确认不越过本文件声明范围。
|
||||||
|
|
||||||
|
## 执行记录
|
||||||
|
|
||||||
|
- 2026-07-16:在 `core/domain/status.go` 落地架构文档中的 12 个软件状态、显式迁移表、状态合法性与迁移校验错误。
|
||||||
|
- 2026-07-16:表驱动测试覆盖正常下载/安装/运行/回滚路径,并覆盖跳过安装链、运行中下载、未知状态和同状态迁移等非法情况。
|
||||||
|
- 2026-07-16:在 `core/application` 落地 `docs/api.md` 已定义的 12 个事件类型、通用 Event 信封及支持缓冲、context 取消、幂等关闭的 Runtime 事件总线骨架。
|
||||||
|
- 验证通过:`cd core && go vet ./... && go test -count=1 ./...`。
|
||||||
|
- 验证通过:Go 1.20.14 + `GOWORK=off` 执行 core vet/test,确认共享核心兼容基线。
|
||||||
|
- 验证通过:modern Go 1.25.0 与 win7 Go 1.20.14 两个 Windows amd64 目标重新编译。
|
||||||
|
- 验证通过:`python scripts/validate_agent_context.py`、`python scripts/validate_harness_governance.py`。
|
||||||
Reference in New Issue
Block a user