Why: Allow consumers to download an immutable module revision without nested source checkouts or local path replacements. Evidence: SELF package tests and the prior canonical-path rejection are recorded in the Dev dependency-workspace task card; remote consumption is replayed against the committed candidate. Impact: Change the module identity and its Go imports while preserving API behavior. Existing historical submodule revisions remain buildable. Refs: Versioned dependencies U1/U3; feature/dependency-workspace
101 lines
3.1 KiB
Go
101 lines
3.1 KiB
Go
package store
|
||
|
||
import (
|
||
"database/sql"
|
||
"errors"
|
||
"testing"
|
||
"time"
|
||
|
||
corepkg "git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git"
|
||
)
|
||
|
||
// U5 余项(KTD5):同事务 outbox——业务提交与命令投递原子(提交即生效、
|
||
// 回滚即同灭);KTD6 前半——事件提交与 NOTIFY 同事务(通知不早于事件可见)。
|
||
|
||
func TestEnqueueCommandInTxAtomicWithBusiness(t *testing.T) {
|
||
st := openTestStore(t)
|
||
// 业务事务回滚 → 命令随之消失。
|
||
tx, err := st.db.Begin()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := st.setSettingsInTx(tx, "owner_tx", "scope_tx", `{"a":1}`); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := st.EnqueueCommandInTx(tx, "cmd_tx_1", "cancel", "run_tx", "cnv_tx", 0, map[string]string{"o": "owner_tx"}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := tx.Rollback(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, _, err := st.CommandState("cmd_tx_1"); !errors.Is(err, ErrNotFound) {
|
||
t.Fatalf("回滚后命令应消失:%v", err)
|
||
}
|
||
// 提交 → 业务与命令同时可见。
|
||
tx, err = st.db.Begin()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := st.setSettingsInTx(tx, "owner_tx", "scope_tx", `{"a":1}`); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := st.EnqueueCommandInTx(tx, "cmd_tx_2", "hitl", "run_tx", "cnv_tx", 0, nil); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := tx.Commit(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
state, _, err := st.CommandState("cmd_tx_2")
|
||
if err != nil || state != "pending" {
|
||
t.Fatalf("提交后命令应 pending:state=%s err=%v", state, err)
|
||
}
|
||
// 幂等:同 id 重复投递(同事务)不报错不增行。
|
||
tx, _ = st.db.Begin()
|
||
if err := st.EnqueueCommandInTx(tx, "cmd_tx_2", "hitl", "run_tx", "cnv_tx", 0, nil); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := tx.Commit(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got, err := st.ClaimPendingCommands("instX", 16)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
n := 0
|
||
for _, c := range got {
|
||
if c.CommandID == "cmd_tx_2" {
|
||
n++
|
||
}
|
||
}
|
||
if n != 1 {
|
||
t.Fatalf("幂等后应仍只 1 条:%d", n)
|
||
}
|
||
}
|
||
|
||
// setSettingsInTx 测试辅助:在既有事务里写一行设置(代表业务提交)。
|
||
func (s *Store) setSettingsInTx(tx *sql.Tx, owner, scope, vals string) error {
|
||
_, err := tx.Exec(s.q(`INSERT INTO settings (owner, scope, "values") VALUES (?,?,?)
|
||
ON CONFLICT(owner, scope) DO UPDATE SET "values"=excluded."values"`), owner, scope, vals)
|
||
return err
|
||
}
|
||
|
||
// 事件提交与 NOTIFY 同事务:AppendEvents 后事件与通知原子可见
|
||
// (KTD6:通知不早于事件;SQLite 档无 NOTIFY,验证事件落库即可)。
|
||
func TestAppendEventsCommitsWithNotify(t *testing.T) {
|
||
st := openTestStore(t)
|
||
core := []corepkg.StreamEvent{}
|
||
for i := 0; i < 3; i++ {
|
||
core = append(core, corepkg.StreamEvent{ConversationID: "cnv_n", RunID: "r", TS: time.Now().UTC().Format(time.RFC3339Nano), V: 1, Type: corepkg.EvRunPhase, Payload: []byte(`{}`)})
|
||
}
|
||
if _, err := st.AppendEvents("cnv_n", core); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var after int64
|
||
if err := st.db.QueryRow(`SELECT COALESCE(MAX(seq),0) FROM events WHERE conversation_id='cnv_n'`).Scan(&after); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if after != 3 {
|
||
t.Fatalf("事件应 +3:%d", after)
|
||
}
|
||
}
|