// T1 MySQL 冒烟可执行(方案 §5 M2 / §6.1 T1)——`moon run --target wasm repository-mysql/smoke`

// 说明:moon test 的 Windows socket 读路径挂死(2026-09-05 实测,见仓根 MAINTAINING.md §4 代决策 D-M2-2),

// T1 走 moon run 可执行通道;断言失败 abort → 非零退出(R6:连不上=fail,不是 skip)。

// SKIP_MYSQL=1 开发机跳过。红线 R6:9xxxxx 段 + 测前测后自清理 + 凭据只走 JEFFLOW_DB_* env。



///|

fn skip_mysql() -> Bool {

  @env.get_env_var("SKIP_MYSQL") is Some("1")

}



///|

fn check(cond : Bool, label : String) -> Unit {

  if cond {

    println("  PASS \{label}")

  } else {

    println("  FAIL \{label}")

    abort("T1 assertion failed: \{label}")

  }

}



///|

/// 9xxxxx 段自清理(只动 jeeflow 库 wf_* 表)

async fn clean_9x(repo : @repo.MysqlRepository) -> Unit raise {

  // 先按外键清子行:add_task_actor/抄送行的 id 是雪花(不在 9xxxxx 段),

  // 只有按 process_task_id/process_instance_id 反查才扫得到(issues/115 转办会新增这类行)。

  // 必须在下面按 id 段删主行**之前**做,否则子查询已经扫不到。

  let sub_conn = repo.open_raw()

  let _ = @repo.q(

    sub_conn,

    "DELETE FROM wf_process_task_actor WHERE process_task_id IN (SELECT id FROM wf_process_task WHERE id >= 990000 AND id < 1000000)",

    [],

  )

  let _ = @repo.q(

    sub_conn,

    "DELETE FROM wf_process_cc_instance WHERE process_instance_id IN (SELECT id FROM wf_process_instance WHERE id >= 990000 AND id < 1000000)",

    [],

  )

  sub_conn.close()

  let tables = [

    "wf_process_task_actor",

    "wf_process_task",

    "wf_process_cc_instance",

    "wf_process_instance",

    "wf_process_define",

    "wf_process_design_his",

    "wf_process_design",

    "wf_process_surrogate",

  ]

  for t in tables {

    let conn = repo.open_raw()

    let _ = @repo.q(conn, "DELETE FROM \{t} WHERE id >= 990000 AND id < 1000000", [])

    conn.close()

  }

  // 兜底:清理 operator/user9x 标记的历史残留(早前崩溃运行可能落雪花 id)

  let conn = repo.open_raw()

  let _ = @repo.q(

    conn,

    "DELETE FROM wf_process_task_actor WHERE process_task_id IN (SELECT id FROM wf_process_task WHERE process_instance_id IN (SELECT id FROM wf_process_instance WHERE operator = 'user9x'))",

    [],

  )

  let _ = @repo.q(conn, "DELETE FROM wf_process_task WHERE process_instance_id IN (SELECT id FROM wf_process_instance WHERE operator = 'user9x')", [])

  let _ = @repo.q(conn, "DELETE FROM wf_process_cc_instance WHERE process_instance_id IN (SELECT id FROM wf_process_instance WHERE operator = 'user9x')", [])

  let _ = @repo.q(conn, "DELETE FROM wf_process_instance WHERE operator = 'user9x'", [])

  conn.close()

}



///|

async fn save_define_9x(repo : @repo.MysqlRepository, id : Int64, name : String, content : String) -> Unit raise {

  let conn = repo.open_raw()

  let _ = @repo.x(

    conn,

    "INSERT INTO wf_process_define (id, name, display_name, type, state, content, version, create_time, create_user) VALUES (?, ?, ?, 'approval', 1, ?, 1, NOW(), 't1')",

    [@moondb.Int64(id), @moondb.Text(name), @moondb.Text(name), @moondb.Text(content)],

  )

  conn.close()

}



///|

const SIMPLE_FLOW : String =

  $|{"name": "t1-simple", "displayName": "T1Simple", "type": "approval",

  $|"nodes": [

  $|  {"id": "start", "type": "snaker:start", "text": {"value": "S"}},

  $|  {"id": "apply", "type": "snaker:task", "text": {"value": "A"},

  $|   "properties": {"assignee": "applicant"}},

  $|  {"id": "end", "type": "snaker:end", "text": {"value": "E"}}

  $|],

  $|"edges": [

  $|  {"id": "e1", "sourceNodeId": "start", "targetNodeId": "apply"},

  $|  {"id": "e2", "sourceNodeId": "apply", "targetNodeId": "end"}

  $|]}



///|

fn make_mysql_engine(repo : @repo.MysqlRepository) -> @engine.Engine[@repo.MysqlRepository, @spi.NoExtRepository] {

  let ctx = @spi.Ctx::new(repo, @spi.NoExtRepository::new())

  // 9xxxxx 段计数器:实例/任务 id 全落测试段(R6 可清理)

  let counter : Ref[Int64] = Ref(990100L)

  let ctx2 = ctx.with_id_generator(fn() {

    let v = counter.val

    counter.val = v + 1L

    v

  })

  @engine.Engine::make(ctx2)

}



///|

/// T1-M1:分页五键(C22)+ id DESC + operator 过滤

async fn t1_m1_page() -> Unit raise {

  let repo = @repo.MysqlRepository::from_env()

  clean_9x(repo)

  save_define_9x(repo, 990001L, "t1-page", SIMPLE_FLOW)

  let conn = repo.open_raw()

  for i in 1..<3 {

    let _ = @repo.x(

      conn,

      "INSERT INTO wf_process_instance (id, process_define_id, state, operator, variable, create_time, create_user) VALUES (?, 990001, 10, 'user9x', '{}', NOW(), 't1')",

      [@moondb.Int64(990010L + i.to_int64())],

    )

  }

  conn.close()

  let query = @model.PageQuery::new(1L, 1L)

  query.set_operator(Some("user9x"))

  let page = repo.page_instances(query)

  check(page.page_num() == 1L, "pageNum=1")

  check(page.page_size() == 1L, "pageSize=1")

  check(page.record_count() == 2L, "recordCount=2")

  check(page.total_page() == 2L, "totalPage=2(五键数学)")

  check(page.rows().length() == 1, "rows 截断为 pageSize")

  check(page.rows()[0].id == 990012L, "ORDER BY id DESC 首行=最新")

  clean_9x(repo)

}



///|

/// T1-M2:hydrate 参与人/变量/DATETIME 归一 + 引擎全链落库

async fn t1_m2_hydrate() -> Unit raise {

  let repo = @repo.MysqlRepository::from_env()

  clean_9x(repo)

  save_define_9x(repo, 990001L, "t1-hydrate", SIMPLE_FLOW)

  let engine = make_mysql_engine(repo)

  let args = @json.FlowData::new()

  args.insert_str("f_note", "你好-数据")

  let inst = engine.start_async(990001L, "user9x", args)

  check(inst.instance_id >= 990000L, "9xxxxx 段实例 id")

  let tasks = repo.find_doing_tasks(inst.instance_id, [])

  check(tasks.length() == 1, "发起后 1 个 DOING 任务")

  let task = tasks[0]

  check(task.actor_ids.contains("user9x"), "参与人水合(is_allowed 依赖)")

  check(task.is_allowed("user9x"), "处理人判定通过")

  match task.create_time {

    Some(t) => check(t.length() <= 19, "DATETIME 归一无毫秒尾巴(C7)")

    None => check(false, "create_time 应有值")

  }

  let _ = engine.execute_task_async(task.task_id, "user9x", @json.FlowData::new())

  let inst2 = repo.find_instance_by_id(inst.instance_id).unwrap()

  check(inst2.state == 20, "全链完成后实例 state=20")

  check(inst2.variables.get_str("autoGenTitle") is Some(_), "实例变量 JSON hydrate 回读")

  let done = repo.find_history_tasks(inst.instance_id)

  let mut all_finished = true

  for t in done {

    if t.task_state != 20 {

      all_finished = false

    }

  }

  check(all_finished, "任务落库 state=20 且 hydrate 正确")

  clean_9x(repo)

}



///|

/// T1-M3:m_ 三段式过滤真实走 SQL

async fn t1_m3_mparams() -> Unit raise {

  let repo = @repo.MysqlRepository::from_env()

  clean_9x(repo)

  save_define_9x(repo, 990001L, "t1-like-target-xyz", SIMPLE_FLOW)

  save_define_9x(repo, 990002L, "unrelated", SIMPLE_FLOW)

  let query = @model.PageQuery::new(1L, 10L)

  let args : Map[String, Json] = Map([])

  args["m_LIKE_name"] = @json.string_of("t1-like-target")

  let filters = @query.parse_m_params(args)

  for f in filters {

    query.add_filter(f)

  }

  let page = repo.page_defines(query)

  check(page.record_count() == 1L, "m_LIKE_name 命中 1 条")

  check(page.rows()[0].name == "t1-like-target-xyz", "过滤值正确")

  clean_9x(repo)

}



///|

/// T1-M9(issues/197/audit8 R31):`page_defines` 的条件与 `order_by` 真落进 SQL。

/// 夹具刻意让「版本最大」与「最后插入」不是同一行 ⇒ 只认硬编码 `ORDER BY pd.id DESC`

/// 的实现必然红;条件整档没下发时 record_count 会是 3 不是 2。

/// 纯文本档(白名单/回落/注入形状)在 repository-mysql/repo/i197_define_order_sql_test.mbt,

/// 这一格守的是「impl 到底有没有调那个函数」。

async fn t1_m9_define_order_by() -> Unit raise {

  let repo = @repo.MysqlRepository::from_env()

  clean_9x(repo)

  let conn = repo.open_raw()

  let ins =

    "INSERT INTO wf_process_define (id, name, display_name, type, state, content, version, create_time, create_user) VALUES (?, ?, ?, 'approval', 1, '{}', ?, NOW(), 't1')"

  let _ = @repo.x(conn, ins, [

    @moondb.Int64(990011L), @moondb.Text("t1-i197-a"), @moondb.Text("t1-i197-a"), @moondb.Int64(7L),

  ])

  let _ = @repo.x(conn, ins, [

    @moondb.Int64(990012L), @moondb.Text("t1-i197-a"), @moondb.Text("t1-i197-a"), @moondb.Int64(3L),

  ])

  let _ = @repo.x(conn, ins, [

    @moondb.Int64(990013L), @moondb.Text("t1-i197-b"), @moondb.Text("t1-i197-b"), @moondb.Int64(5L),

  ])

  conn.close()



  // 正向:同名两行+版本倒序 ⇒ 首行必须是版本最大的 990011(它的 id 反而是三行里最小的)

  let q = @model.PageQuery::new(1L, 1L)

  q.add_filter(@model.QueryFilter::make("t", @model.Eq, "name", "t1-i197-a"))

  q.set_order_by("t.version desc")

  let p = repo.page_defines(q)

  check(p.record_count() == 2L, "T1-M9 条件下发:t1-i197-a 只 2 行(实得 \{p.record_count()})")

  check(p.rows().length() == 1 && p.rows()[0].id == 990011L, "T1-M9 order_by 下发:首行=版本最大 990011")

  check(p.rows().length() == 1 && p.rows()[0].version == 7, "T1-M9 首行 version=7")



  // 缺省序档:不给 order_by ⇒ id 倒序(与内存腿缺省同档),首行 990012

  let q2 = @model.PageQuery::new(1L, 1L)

  q2.add_filter(@model.QueryFilter::make("t", @model.Eq, "name", "t1-i197-a"))

  let p2 = repo.page_defines(q2)

  check(p2.rows().length() == 1 && p2.rows()[0].id == 990012L, "T1-M9 缺省序=id 倒序 ⇒ 首行 990012")



  // 负向:查无此名 ⇒ 空页(条件不下发时这里会放出别名的行)

  let q3 = @model.PageQuery::new(1L, 10L)

  q3.add_filter(@model.QueryFilter::make("t", @model.Eq, "name", "t1-i197-missing"))

  q3.set_order_by("t.version desc")

  let p3 = repo.page_defines(q3)

  check(p3.record_count() == 0L && p3.rows().is_empty(), "T1-M9 查无此名 ⇒ 空页")



  // 白名单外 ⇒ 回落 id 倒序,且不把攻击串拼进去(能跑完本身即证明语法没被破坏)

  let q4 = @model.PageQuery::new(1L, 1L)

  q4.add_filter(@model.QueryFilter::make("t", @model.Eq, "name", "t1-i197-a"))

  q4.set_order_by("t.password desc; DROP TABLE wf_process_define")

  let p4 = repo.page_defines(q4)

  check(p4.record_count() == 2L, "T1-M9 非法列+攻击串:条件照旧生效,count=2(实得 \{p4.record_count()})")

  check(p4.rows().length() == 1 && p4.rows()[0].id == 990012L, "T1-M9 回落档首行=id 最大 990012")

  // 攻击串没被拼进去 ⇒ 表还在、行还在(DROP 若真执行,下面两条一条也活不到)

  let conn2 = repo.open_raw()

  let alive = @repo.q(conn2, "SELECT COUNT(*) AS cnt FROM wf_process_define WHERE id IN (990011, 990012, 990013)", [])

  conn2.close()

  check(!alive.is_empty() && @repo.row_i64(alive[0], "cnt") == 3L, "T1-M9 注入未落地:三条夹具行仍在")

  clean_9x(repo)

}



/// 事务体:两条 INSERT 后主动抛错。抽成具名 async fn 是因为内联 `fn() { … }` 闭包
/// 在本工具链下两头堵——不标 async 吃 deprecated_syntax,标了 `async fn()` 又把效应推成
/// 开放 Error、与 execute_in_tx 声明的闭集 raise JeeflowError 不匹配。
async fn t1_m4_boom_tx(repo : @repo.MysqlRepository) -> Unit raise {
  let conn = repo.current_tx_conn().unwrap()
  let _ = @repo.x(
    conn,
    "INSERT INTO wf_process_instance (id, process_define_id, state, operator, variable, create_time, create_user) VALUES (990020, 990001, 10, 'user9x', '{}', NOW(), 't1')",
    [],
  )
  let _ = @repo.x(
    conn,
    "INSERT INTO wf_process_task (id, process_instance_id, task_name, display_name, task_state, create_time, create_user) VALUES (990021, 990020, 'apply', 'A', 10, NOW(), 't1')",
    [],
  )
  raise @error.Business("boom-in-tx")
}

///|
///|

/// T1-M4:事务回滚无半完成实例(§6.2 语义级)

async fn t1_m4_tx_rollback() -> Unit raise {

  let repo = @repo.MysqlRepository::from_env()

  clean_9x(repo)

  let tpl = @repo.MysqlTxTemplate::from_env()

  // 事务模板现在直接 raise:旧 `outcome is Err(_)` 物化成 Bool。
  // 判据不空转——若模板把回调异常吞了(回滚语义退化成"静默成功"),false ⇒ 这格红。
  let raised = try {
    tpl.execute_in_tx(t1_m4_boom_tx)
    false
  } catch {
    _ => true
  }
  check(raised, "回调抛错外传")
  let conn = repo.open_raw()

  let inst_rows = @repo.q(conn, "SELECT COUNT(*) AS c FROM wf_process_instance WHERE id = 990020", [])

  let task_rows = @repo.q(conn, "SELECT COUNT(*) AS c FROM wf_process_task WHERE id = 990021", [])

  conn.close()

  check(@repo.row_i64(inst_rows[0], "c") == 0L, "实例行回滚净")

  check(@repo.row_i64(task_rows[0], "c") == 0L, "任务行回滚净")

  clean_9x(repo)

}



///|

/// T1-M5:并发办理只一次成功(引擎读-改-写守卫,§6.2)

async fn t1_m5_idempotent() -> Unit raise {

  let repo = @repo.MysqlRepository::from_env()

  clean_9x(repo)

  save_define_9x(repo, 990001L, "t1-idem", SIMPLE_FLOW)

  let engine = make_mysql_engine(repo)

  let inst = engine.start_async(990001L, "user9x", @json.FlowData::new())

  let tasks = repo.find_doing_tasks(inst.instance_id, [])

  check(tasks.length() == 1, "办理前 1 个 DOING")

  // 正向第一次:旧 `is Ok(_)`
  let first_ok = try {
    let _ = engine.execute_task_async(tasks[0].task_id, "user9x", @json.FlowData::new())
    true
  } catch {
    _ => false
  }
  check(first_ok, "第一次办理成功")
  // 负向第二次:守卫必须报 99999999;没抛错 ⇒ Some? 不,None 臂 ⇒ code_ok 恒 false
  let second_code = try {
    let _ = engine.execute_task_async(tasks[0].task_id, "user9x", @json.FlowData::new())
    None
  } catch {
    e => Some(e.code())
  }
  let code_ok = match second_code {
    Some(c) => c == @error.ERR_BUSINESS
    None => false
  }
  check(code_ok, "第二次办理 99999999(任务已办理守卫)")
  clean_9x(repo)

}



///|

///|

/// T1-I110:SQL 仓 find_instance_by_id 水合任务 → detail 任务列表非空(issues/110)

/// 修复前:find_instance_by_id 只查 wf_process_instance 单表,tasks 硬编码 [],

/// 门面 processInstance/detail 的 tasks/activeTaskList 恒空(L2-12 门禁真根因)。

/// 对齐 Java findTasksByInstanceId / PHP PdoProcessRepository / C# issues/89 聚合水合。

/// 真实 MySQL:引擎发起 doing 实例(落 9903xx 段)→ 断言 find_instance_by_id 水合任务

/// 非空 + 带 actor_ids,且门面 detail 消费(遍历 inst.tasks 组 tasks/activeTaskList)非空。

async fn t1_i110_hydrate() -> Unit raise {

  let repo = @repo.MysqlRepository::from_env()

  clean_9x(repo)

  save_define_9x(repo, 990301L, "t1-i110", SIMPLE_FLOW)

  // 独立 id 段 9903xx:实例/任务 id 落测试段(R6 可清理)

  let counter : Ref[Int64] = Ref(990301L)

  let ctx = @spi.Ctx::new(repo, @spi.NoExtRepository::new())

  let ctx2 = ctx.with_id_generator(fn() {

    let v = counter.val

    counter.val = v + 1L

    v

  })

  let engine = @engine.Engine::make(ctx2)

  let inst = engine.start_async(990301L, "user9x", @json.FlowData::new())

  check(inst.instance_id >= 990300L, "9xxxxx 段实例 id")



  // ① 仓储层:find_instance_by_id 水合任务 + actor_ids(修复前恒空)

  let loaded = repo.find_instance_by_id(inst.instance_id).unwrap()

  check(!loaded.tasks.is_empty(), "find_instance_by_id 水合任务非空")

  let mut actors_ok = true

  for t in loaded.tasks {

    if t.actor_ids.is_empty() {

      actors_ok = false

    }

  }

  check(actors_ok, "水合任务带参与者 actor_ids")



  // ② 门面 detail 消费口径:遍历 inst.tasks 组 tasks/activeTaskList

  let mut active_count = 0

  for t in loaded.tasks {

    if t.is_doing() {

      active_count += 1

    }

  }

  check(loaded.tasks.length() > 0, "detail tasks 非空")

  check(active_count > 0, "detail activeTaskList 非空")

  clean_9x(repo)

}



///|

/// NULL 安全单元格读取( 与 "" 必须可分辨:审计列断言读的就是这一格)

fn cell(r : @moondb.Row, col : String) -> String {

  match r.by_name(col) {

    None => "<无此列>"

    Some(v) =>

      match v {

        @moondb.Null => ""

        @moondb.Text(s) => s

        @moondb.Blob(_) => ""

        @moondb.Int64(n) => n.to_string()

        @moondb.Int(n) => n.to_string()

        @moondb.Double(d) => d.to_string()

        @moondb.Bool(b) => if b { "1" } else { "0" }

      }

  }

}



///|

/// 库里单列读回(不走仓储水合,直接 SELECT 列名——要钉的就是这一格)

async fn select_cell(

  repo : @repo.MysqlRepository,

  sql : String,

  params : Array[@moondb.Value],

  col : String

) -> String raise {

  let conn = repo.open_raw()

  let rows = @repo.q(conn, sql, params)

  conn.close()

  if rows.is_empty() {

    return "<无行>"

  }

  cell(rows[0], col)

}



///|

/// T1-I114(仓储级):update_instance / update_task 必须把 update_user **真写进 SQL**。

/// 缺陷本体(本栈独有):两条 UPDATE 语句历史上只有 state/variable/update_time(+operator/

/// finish_time),内存仓是整行替换所以门面级测试全绿也照不出来,SQL 仓审计列静默丢失

/// → 双仓答案不一致。末跳 `update_user=None → 库里 ` 是对照组,专门证伪

/// "这列从来没被写过"式的假绿。门面级(鉴权 + 三件留痕 + 出口)真机用例见

/// demo/cmd/t1_mysql。

async fn t1_i114_update_user_lands() -> Unit raise {

  let repo = @repo.MysqlRepository::from_env()

  clean_9x(repo)

  save_define_9x(repo, 990401L, "t1-i114", SIMPLE_FLOW)

  let engine = make_mysql_engine(repo)

  let inst = engine.start_async(990401L, "user9x", @json.FlowData::new())

  let tasks = repo.find_doing_tasks(inst.instance_id, [])

  check(tasks.length() == 1, "I114 发起后 1 个进行中任务")

  let task = tasks[0]

  let iid_sql = "SELECT update_user FROM wf_process_instance WHERE id = ?"

  let tid_sql = "SELECT update_user FROM wf_process_task WHERE id = ?"

  // 只动审计列,走的就是被修的那两条 UPDATE 语句

  inst.update_user = Some("i114-revoker")

  repo.update_instance(inst)

  check(

    select_cell(repo, iid_sql, [@moondb.Int64(inst.instance_id)], "update_user") == "i114-revoker",

    "I114 库里 wf_process_instance.update_user 真被写入(列确在 UPDATE 语句里)",

  )

  task.update_user = Some("i114-revoker")

  repo.update_task(task)

  check(

    select_cell(repo, tid_sql, [@moondb.Int64(task.task_id)], "update_user") == "i114-revoker",

    "I114 库里 wf_process_task.update_user 真被写入",

  )

  // 对照组:None 必须落 NULL(否则前面的命中可能是"这列从没被写过 + 哨兵恰好同值")

  inst.update_user = None

  repo.update_instance(inst)

  check(

    select_cell(repo, iid_sql, [@moondb.Int64(inst.instance_id)], "update_user") == "",

    "I114 update_user=None 落库 NULL(对照,证伪假绿)",

  )

  clean_9x(repo)

}



///|

/// 库里单列读回(时间列版)。老 `cell()` 对 `@moondb.Blob` 回 ""——

/// MySQL 结果集把 `DATETIME(3)` 投成 Blob,而 I120 要比的正是那一串时间,

/// 故按库内 `value_to_text` 的同规则解码(旁挂新函数,不动既有 `cell()` 的三档标记语义)。

async fn select_cell_dt(

  repo : @repo.MysqlRepository,

  sql : String,

  params : Array[@moondb.Value],

  col : String

) -> String raise {

  let conn = repo.open_raw()

  let rows = @repo.q(conn, sql, params)

  conn.close()

  if rows.is_empty() {

    return "<无行>"

  }

  match rows[0].by_name(col) {

    None => "<无此列>"

    Some(v) =>

      match v {

        @moondb.Null => ""

        @moondb.Text(s) => s

        @moondb.Blob(b) => @utf8.decode_lossy(b[:])

        _ => "<非文本值>"

      }

  }

}



///|

/// DATETIME(3) 文本比较取前 19 位(列带 `.000` 尾巴时不影响判据;C7 同口径)

fn head19(s : String) -> String {

  if s.length() >= 19 { s[0:19].to_owned() } else { s }

}



///|

/// T1-I120(仓储级):本栈六条写路径的时间列必须由**引擎钟**供给,SQL 侧不许有 `NOW()`。

///

/// 缺陷本体:`update_instance`/`update_task`/`add_task_actor`/`create_cc_instance`/

/// `update_design`/`update_surrogate` 历史上写 `update_time=NOW()` / `create_time=NOW()`,

/// 取的是**数据库会话时区**的墙钟;开发服务器 160 实测 `@@session.time_zone=+08:00`

/// (`NOW()` 比 `UTC_TIMESTAMP()` 晚 8 小时),而引擎写出的 `create_time`/`finish_time`

/// 走它自己那把钟(未注入时 UTC)⇒ 同一行两个基准差 8 小时。外溢到读侧的不是只有审计列:

/// `wf_process_cc_instance.create_time` 会被 `page_cc_instances` SELECT 回并投影给前端,

/// 「抄送我的」列表那一格用的就是 DB 那把钟。

///

/// 夹具设计:注入钟 = 真实 UTC − 12h ⇒ 「引擎钟(注入)」「真实 UTC」「DB +08:00」三方互不相同,

/// 库里读到哪一把当场可分辨;末跳摘掉注入再写一次作**对照组**,读回值必须换成真实 UTC

/// (≠ 注入串),用以证伪"这列根本没被写 / 被写成了常量"式的假绿。

async fn t1_i120_engine_clock_lands() -> Unit raise {

  let repo = @repo.MysqlRepository::from_env()

  clean_9x(repo)

  save_define_9x(repo, 990501L, "t1-i120", SIMPLE_FLOW)

  let injected = @model.format_unix_utc(@model.epoch_secs() - 43200L)

  @model.set_clock(Some(fn() { injected }))



  let engine = make_mysql_engine(repo)

  let inst = engine.start_async(990501L, "user9x", @json.FlowData::new())

  let tasks = repo.find_doing_tasks(inst.instance_id, [])

  check(tasks.length() == 1, "I120 发起后 1 个进行中任务")

  let task = tasks[0]



  inst.update_user = Some("i120")

  repo.update_instance(inst)

  let inst_upd = head19(select_cell_dt(repo, "SELECT update_time FROM wf_process_instance WHERE id = ?", [

    @moondb.Int64(inst.instance_id)

  ], "update_time"))

  check(

    inst_upd == injected,

    "I120 库里 wf_process_instance.update_time = 注入的引擎钟(实际=\{inst_upd},注入串=\{injected})",

  )

  task.update_user = Some("i120")

  repo.update_task(task)

  let task_upd = head19(select_cell_dt(repo, "SELECT update_time FROM wf_process_task WHERE id = ?", [

    @moondb.Int64(task.task_id)

  ], "update_time"))

  check(

    task_upd == injected,

    "I120 库里 wf_process_task.update_time = 注入的引擎钟(实际=\{task_upd},同行 finish_time 同基准)",

  )

  repo.add_task_actor(task.task_id, ["i120-extra"])

  let actor_ct = head19(select_cell_dt(repo, "SELECT create_time FROM wf_process_task_actor WHERE process_task_id = ? AND actor_id = ?", [

    @moondb.Int64(task.task_id), @moondb.Text("i120-extra")

  ], "create_time"))

  check(

    actor_ct == injected,

    "I120 库里 wf_process_task_actor.create_time = 注入的引擎钟(实际=\{actor_ct})",

  )

  repo.create_cc_instance(inst.instance_id, "user9x", ["i120-cc"])

  let cc_ct = head19(select_cell_dt(repo, "SELECT create_time FROM wf_process_cc_instance WHERE process_instance_id = ? AND actor_id = ?", [

    @moondb.Int64(inst.instance_id), @moondb.Text("i120-cc")

  ], "create_time"))

  check(

    cc_ct == injected,

    "I120 抄送行 create_time = 注入的引擎钟(实际=\{cc_ct};该列会被 page_cc_instances 回读投影给前端)",

  )

  let design : @model.ProcessDesign = {

    id: 990502L,

    name: "t1-i120-design",

    display_name: "I120 设计",

    design_type: "approval",

    icon: None,

    is_deployed: 0,

    remark: None,

    create_time: None,

    create_user: Some("user9x"),

    update_time: None,

    update_user: None,

  }

  repo.save_design(design)

  repo.update_design(design)

  let design_upd = head19(select_cell_dt(repo, "SELECT update_time FROM wf_process_design WHERE id = ?", [

    @moondb.Int64(990502L)

  ], "update_time"))

  check(

    design_upd == injected,

    "I120 库里 wf_process_design.update_time = 注入的引擎钟(实际=\{design_upd})",

  )

  let sg : @model.ProcessSurrogate = {

    id: 990503L,

    process_name: "t1-i120",

    operator: "user9x",

    surrogate: "userB",

    start_time: None,

    end_time: None,

    enabled: 1,

    create_time: None,

    create_user: Some("user9x"),

    update_time: None,

    update_user: None,

  }

  repo.save_surrogate(sg)

  repo.update_surrogate(sg)

  let sg_upd = head19(select_cell_dt(repo, "SELECT update_time FROM wf_process_surrogate WHERE id = ?", [

    @moondb.Int64(990503L)

  ], "update_time"))

  check(

    sg_upd == injected,

    "I120 库里 wf_process_surrogate.update_time = 注入的引擎钟(实际=\{sg_upd})",

  )



  // 对照组:摘掉注入 ⇒ 同一列必须换成真实 UTC(不再等于注入串),

  // 否则上面的命中可能只是"常量/从没被写过"。

  @model.set_clock(None)

  repo.update_instance(inst)

  let after_default = head19(select_cell_dt(repo, "SELECT update_time FROM wf_process_instance WHERE id = ?", [

    @moondb.Int64(inst.instance_id)

  ], "update_time"))

  check(

    after_default != injected && after_default.length() >= 19,

    "I120 摘掉注入后 update_time 跟随默认臂真实 UTC(实际=\{after_default},注入串=\{injected})",

  )

  clean_9x(repo)

}



///|

/// ── issues/116 批次 D:委托查询四判据 + 双仓同答案(SQL 仓真机)──

/// 同一份委托台账同时灌进 SQL 仓与内存仓,逐探针比**两侧答案是否相同**(spec 06 §4.5 条款 6)。

/// 四判据(条款 5):① 空 processName 全流程兜底(且精确名优先于兜底)、

/// ② 时间窗 start<=now<=end(NULL=该侧不限,边界含等号)、③ 自委托过滤、④ enabled 只认 1。

/// 条款 1.4:多条命中取 id 最大 —— SQL 侧 ORDER BY id DESC。

const I116_NOW : String = "2026-09-21 10:00:00"



///|

fn sg_row(

  id : Int64,

  operator : String,

  agent : String,

  process_name : String,

  start_time : String?,

  end_time : String?,

  enabled : Int

) -> @model.ProcessSurrogate {

  {

    id: id,

    process_name: process_name,

    operator: operator,

    surrogate: agent,

    start_time: start_time,

    end_time: end_time,

    enabled: enabled,

    create_time: None,

    create_user: Some("i116"),

    update_time: None,

    update_user: None,

  }

}



///|

async fn t1_i116_surrogate_query() -> Unit raise {

  @model.set_clock(Some(fn() { I116_NOW }))

  defer { @model.set_clock(None) }

  let repo = @repo.MysqlRepository::from_env()

  clean_9x(repo)

  let mem = @mem.MemoryRepository::new()

  let rows = [

    sg_row(990501L, "i116-zhang", "i116-li", "i116-flow", Some("2026-09-01 00:00:00"), Some("2026-09-30 23:59:59"), 1),

    sg_row(990502L, "i116-future", "i116-li", "i116-flow", Some("2026-09-25 00:00:00"), None, 1),

    sg_row(990503L, "i116-past", "i116-li", "i116-flow", None, Some("2026-09-20 23:59:59"), 1),

    sg_row(990504L, "i116-off", "i116-li", "i116-flow", None, None, 0),

    sg_row(990505L, "i116-self", "i116-self", "i116-flow", None, None, 1),

    sg_row(990506L, "i116-blank", "i116-li", "", None, None, 1),

    sg_row(990507L, "i116-multi", "i116-old", "i116-flow", None, None, 1),

    sg_row(990508L, "i116-multi", "i116-new", "i116-flow", None, None, 1),

    sg_row(990509L, "i116-edge", "i116-li", "i116-flow", Some(I116_NOW), Some(I116_NOW), 1),

    // 精确名行 id 比兜底行**小**:证明两阶段查询,而不是"一次 ORDER BY id DESC 扫全表"

    sg_row(990510L, "i116-prio", "i116-exact", "i116-flow", None, None, 1),

    sg_row(990511L, "i116-prio", "i116-blank-hit", "", None, None, 1),

    // ── issues/123 条款 1.4:同作用域「旧记录有效 + 新记录不生效」四格 ──

    // 新记录 id 必须更大(ORDER BY id DESC 才落到它)。四格期望 ****:

    // "先滤生效、剩下的才取最新"的旧形状会跳过新的那条、复活旧的那条 ⇒ 这四格是杀手。

    sg_row(990512L, "i123-out", "i123-old-valid", "i116-flow", Some("2026-09-01 00:00:00"), Some("2026-09-30 23:59:59"), 1),

    sg_row(990513L, "i123-out", "i123-new-agent", "i116-flow", Some("2026-09-25 00:00:00"), None, 1),

    sg_row(990514L, "i123-off", "i123-old-valid", "i116-flow", Some("2026-09-01 00:00:00"), Some("2026-09-30 23:59:59"), 1),

    sg_row(990515L, "i123-off", "i123-new-agent", "i116-flow", None, None, 0),

    sg_row(990516L, "i123-dirty", "i123-old-valid", "i116-flow", Some("2026-09-01 00:00:00"), Some("2026-09-30 23:59:59"), 1),

    // 脏值经 sg_row **直落仓储**(不经门面,门面对非 0/1 是判错口径,见 facade 用例)

    sg_row(990517L, "i123-dirty", "i123-new-agent", "i116-flow", None, None, 2),

    sg_row(990518L, "i123-self", "i123-old-valid", "i116-flow", Some("2026-09-01 00:00:00"), Some("2026-09-30 23:59:59"), 1),

    sg_row(990519L, "i123-self", "i123-self", "i116-flow", None, None, 1),

    // 最新一条有效 ⇒ 照常取它(证明四格的红不是"委托整体失效")

    sg_row(990520L, "i123-ok", "i123-old-off", "i116-flow", None, None, 0),

    sg_row(990521L, "i123-ok", "i123-new-valid", "i116-flow", Some("2026-09-01 00:00:00"), Some("2026-09-30 23:59:59"), 1),

    // 精确作用域最新一条已过期 ⇒ 判否后**仍**回落全流程作用域那条(Java testSurrogateCrudAndGet 同形)

    sg_row(990522L, "i123-fb", "i123-global", "", None, None, 1),

    sg_row(990523L, "i123-fb", "i123-exact-expired", "i116-flow", None, Some("2026-09-20 23:59:59"), 1),

  ]

  for r in rows {

    repo.save_surrogate(r)

    mem.save_surrogate(r)

  }

  let probes : Array[(String, String, String)] = [

    ("i116-zhang", "i116-flow", "i116-li"),

    ("i116-future", "i116-flow", ""),

    ("i116-past", "i116-flow", ""),

    ("i116-off", "i116-flow", ""),

    ("i116-self", "i116-flow", ""),

    ("i116-blank", "any-unlisted-flow", "i116-li"),

    ("i116-multi", "i116-flow", "i116-new"),

    ("i116-edge", "i116-flow", "i116-li"),

    ("i116-nobody", "i116-flow", ""),

    ("i116-prio", "i116-flow", "i116-exact"),

    // 负向:带名字的委托**不得跨流程命中**(兜底只属于 process_name 为空的那条台账;

    // 引擎侧同判据见 core/engine/surrogate_test.mbt "有名字的委托不跨流程命中")

    ("i116-zhang", "other-flow", ""),

    // ── issues/123 条款 1.4:各作用域先取**最新一条**再由四判据裁决;旧记录不得复活 ──

    ("i123-out", "i116-flow", ""), // 最新一条窗外

    ("i123-off", "i116-flow", ""), // 最新一条 enabled=0

    ("i123-dirty", "i116-flow", ""), // 最新一条 enabled=2 脏值(只认 1)

    ("i123-self", "i116-flow", ""), // 最新一条自委托

    ("i123-ok", "i116-flow", "i123-new-valid"), // 正向:最新一条有效 ⇒ 取它,旧的停用那条不算

    ("i123-fb", "i116-flow", "i123-global"), // 精确作用域判否后仍回落全流程作用域

  ]

  for p in probes {

    let (op, name, want) = p

    let sql_hit = repo.get_surrogate(op, name, I116_NOW)

    let mem_hit = mem.get_surrogate(op, name, I116_NOW)

    let sql_v = match sql_hit {

      Some(s) => s.surrogate

      None => ""

    }

    let mem_v = match mem_hit {

      Some(s) => s.surrogate

      None => ""

    }

    check(sql_v == want, "I116 SQL 仓 \{op}@\{name} → \{want}(实际=\{sql_v})")

    check(mem_v == want, "I116 内存仓 \{op}@\{name} → \{want}(实际=\{mem_v})")

    check(

      sql_v == mem_v,

      "I116 条款6 双仓同答案 \{op}@\{name}:SQL=\{sql_v} 内存=\{mem_v}",

    )

  }

  // 兜底不得跨授权人

  check(repo.get_surrogate("i116-nobody", "any-unlisted-flow", I116_NOW) is None, "I116 兜底仅按授权人")

  clean_9x(repo)

}



///|

/// ── issues/117:我已办状态集合 = task_state <> 10,且空 operator 不得返回全库 ──

/// 造 20/30/40/99/10 五条行,SQL 仓与内存仓必须给出**同一 id 集合**(条款 6 通则)。

async fn t1_i117_done_state_set() -> Unit raise {

  @model.set_clock(Some(fn() { I116_NOW }))

  defer { @model.set_clock(None) }

  let repo = @repo.MysqlRepository::from_env()

  clean_9x(repo)

  save_define_9x(repo, 990601L, "t1-i117", SIMPLE_FLOW)

  let conn = repo.open_raw()

  let _ = @repo.x(

    conn,

    "INSERT INTO wf_process_instance (id, process_define_id, state, operator, variable, create_time, create_user) VALUES (990600, 990601, 10, 'i117-applicant', '{}', NOW(), 'i117')",

    [],

  )

  conn.close()

  let specs : Array[(Int64, Int, String?)] = [

    (990611L, 20, Some("i117-leader")),

    (990612L, 30, Some("i117-leader")),

    (990613L, 40, Some("i117-leader")),

    (990614L, 99, Some("i117-leader")),

    (990615L, 10, None),

  ]

  let mem = @mem.MemoryRepository::new()

  let mem_define : @model.ProcessDefine = {

    id: 990601L,

    name: "t1-i117",

    display_name: "T1I117",

    define_type: "approval",

    state: 1,

    content: SIMPLE_FLOW,

    version: 1,

    create_time: None,

    create_user: None,

    update_time: None,

    update_user: None,

  }

  mem.save_define(mem_define)

  for sp in specs {

    let (tid, state, actor) = sp

    let t : @model.ProcessTask = {

      task_id: tid,

      process_instance_id: 990600L,

      task_name: "n\{state}",

      display_name: "节点\{state}",

      task_type: 0,

      perform_type: 0,

      task_state: state,

      actor_id: actor,

      actor_ids: ["i117-leader"],

      finish_time: if state == 20 { Some(I116_NOW) } else { None },

      expire_time: None,

      form_key: None,

      parent_task_id: None,

      variables: @json.FlowData::new(),

      create_time: Some(I116_NOW),

      // create_user 是发起人:已办绝不按它放行(issues/107 / Rust 那格违约点)

      create_user: Some("i117-applicant"),

      update_time: None,

      update_user: None,

    }

    repo.save_task(t)

    mem.save_task(t)

  }

  let done_ids = ["990611", "990612", "990613", "990614"]

  // SQL 仓正向

  let q = @model.PageQuery::new(1L, 20L)

  q.set_operator(Some("i117-leader"))

  let page = repo.page_done_tasks(q)

  check(page.record_count() == 4L, "I117 SQL 仓已办 4 条(实际=\{page.record_count()})")

  for want in done_ids {

    check(page.rows().exists(r => r.id.to_string() == want), "I117 SQL 仓含任务行 \{want}")

  }

  check(!page.rows().exists(r => r.id.to_string() == "990615"), "I117 SQL 仓 DOING(10) 不进已办")

  // 内存仓同答案

  let mpage = mem.page_done_tasks(q)

  check(mpage.record_count() == page.record_count(), "I117 条款6 双仓条数一致")

  for want in done_ids {

    check(mpage.rows().exists(r => r.id.to_string() == want), "I117 内存仓含任务行 \{want}")

  }

  check(!mpage.rows().exists(r => r.id.to_string() == "990615"), "I117 内存仓 DOING(10) 不进已办")

  // 判据生效自证:库里 task_state <> 10 确实筛出这 4 行(不是"=20 蓙对")

  let cand = select_cell(

    repo,

    "SELECT COUNT(*) AS cnt FROM wf_process_task WHERE process_instance_id = 990600 AND task_state <> 10",

    [],

    "cnt",

  )

  check(cand == "4", "I117 裸 SQL 候选集 = 4(证 <>10 判据真在筛,实际=\{cand})")

  // 负向:空 operator 不得返回全库

  let q_none = @model.PageQuery::new(1L, 20L)

  check(repo.page_done_tasks(q_none).record_count() == 0L, "I117 SQL 仓无 operator → 空页(不得全库)")

  check(mem.page_done_tasks(q_none).record_count() == 0L, "I117 内存仓无 operator → 空页(不得全库)")

  let q_blank = @model.PageQuery::new(1L, 20L)

  q_blank.set_operator(Some(""))

  check(repo.page_done_tasks(q_blank).record_count() == 0L, "I117 SQL 仓 operator='' → 空页")

  check(mem.page_done_tasks(q_blank).record_count() == 0L, "I117 内存仓 operator='' → 空页")

  // 负向:发起人(create_user)看不到

  let q_creator = @model.PageQuery::new(1L, 20L)

  q_creator.set_operator(Some("i117-applicant"))

  check(repo.page_done_tasks(q_creator).record_count() == 0L, "I117 SQL 仓 create_user 不放行")

  check(mem.page_done_tasks(q_creator).record_count() == 0L, "I117 内存仓 create_user 不放行")

  // 回归:分页五键在判据收紧后仍成立

  let q_page = @model.PageQuery::new(1L, 2L)

  q_page.set_operator(Some("i117-leader"))

  let pp = repo.page_done_tasks(q_page)

  check(

    pp.record_count() == 4L && pp.total_page() == 2L && pp.rows().length() == 2,

    "I117 分页五键回归(recordCount=4/totalPage=2/rows=2)",

  )

  clean_9x(repo)

}



// ── T1-M6/M7/M8:issues/144 的事务归属档(方案 b 的验收判据)──────────────────

// 三档各自测一件事:并发请求不得互相提交(M6)、同一实例内嵌套仍属同一事务(M7)、

// 归属边界的两个方向(同实例族拷贝共用槽 / 派生请求实例独立槽)(M8)。



///|

/// 夹具实例行(9xxxxx 段,测后 clean_9x)。走**真实仓储写路径** `save_instance`,

/// 而不是手写 SQL:本案要测的就是 open_or_tx() 那 42 个调用点把读写汇到哪条连接上。

fn m6_inst(iid : Int64) -> @model.ProcessInstance {

  {

    instance_id: iid,

    parent_id: None,

    define_id: 990001L,

    state: 10,

    parent_node_name: None,

    business_no: None,

    operator: "t1m6",

    expire_time: None,

    variables: @json.FlowData::new(),

    tasks: [],

    create_time: None,

    create_user: Some("t1"),

    update_time: None,

    update_user: None,

    define_info: None,

  }

}



///|

/// 持久态读数:另开一条连接,绕开事务可见性——读到的就是"已提交/已回滚"的终态

async fn m6_row_count(base : @repo.MysqlRepository, iid : Int64) -> Int64 raise {

  let conn = base.open_raw()

  let rows = @repo.q(

    conn,

    "SELECT COUNT(*) AS c FROM wf_process_instance WHERE id = ?",

    [@moondb.Int64(iid)],

  )

  conn.close()

  @repo.row_i64(rows[0], "c")

}



///|

/// A 支:事务里写一行 → 让出事件循环 → 抛错(模板 ROLLBACK)

async fn m6_body_a(r : @repo.MysqlRepository) -> Unit raise {

  r.save_instance(m6_inst(990030L))

  // 真实系统里任何一次 DB 往返都是 await,这里用 sleep 制造同一个让出点

  let _ = @async.sleep(60)

  raise @error.Business("A-boom-in-tx")

}



///|

/// B 支:在 A 的让出窗口里进模板,写完并成功返回

async fn m6_body_b(r : @repo.MysqlRepository) -> Unit raise {

  r.save_instance(m6_inst(990031L))

}



///|

/// T1-M6 并发原子档(案文 §5 第一条):X2 那格必须从 `has400=0` 翻成 1,

/// 否则"跨请求共用同一条连接时事务边界不存在"这条结论仍然成立。

async fn t1_m6_tx_scope_isolation() -> Unit raise {

  println("T1-M6 两个并发请求各走一次模板(issues/144 X2 档)...")

  let base = @repo.MysqlRepository::from_env()

  clean_9x(base)

  let ra = base.new_request()

  let rb = base.new_request()

  let a_out = Ref("unset")

  let b_out = Ref("unset")

  let _ = @async.all([

    () => {

      let out = try {

        ra.execute_in_tx(m6_body_a)

        "clean"

      } catch {

        _ => "raised"

      }

      a_out.val = out

    },

    () => {

      // 晚 20ms 进模板:此刻 A 已 BEGIN 并让出——旧的全局槽形状下 B 会复用 A 的连接

      let _ = @async.sleep(20)

      let out = try {

        rb.execute_in_tx(m6_body_b)

        "committed"

      } catch {

        _ => "raised"

      }

      b_out.val = out

    },

  ])

  check(a_out.val == "raised", "T1-M6 A 支回调抛错按预期外传(模板已 ROLLBACK)")

  check(b_out.val == "committed", "T1-M6 B 支独立提交并成功返回(没搭 A 的事务)")

  check(m6_row_count(base, 990030L) == 0L, "T1-M6 A 支自己的行被自己回滚撤掉")

  check(m6_row_count(base, 990031L) == 1L, "T1-M6 B 支已确认的写入还在(X2 的 has400 翻成 1)")

  clean_9x(base)

}



///|

/// 内层:写一行后"成功返回"。若嵌套错误地另开事务,这里就会独立提交,

/// 外层随后回滚就撤不掉它——所以"内层行也不在"同时证伪了两种坏形状。

async fn m7_body_inner(r : @repo.MysqlRepository) -> Unit raise {

  r.save_instance(m6_inst(990033L))

}



async fn m7_body_outer(r : @repo.MysqlRepository) -> Unit raise {

  r.save_instance(m6_inst(990032L))

  let inner = r.tx_template()

  inner.execute_in_tx(m7_body_inner)

  check(r.tx_active(), "T1-M7 嵌套调用返回后本实例仍在同一事务中(未另开连接)")

  raise @error.Business("outer-boom")

}



///|

/// T1-M7 正向对照(案文 §5 第二条):同一实例内嵌套不重复 BEGIN、同属一个事务。

/// 这条是本案要**保住**的既有语义,不是新能力。

async fn t1_m7_nested_tx_reuse() -> Unit raise {

  println("T1-M7 同一实例内嵌套不重复 BEGIN(正向对照)...")

  let base = @repo.MysqlRepository::from_env()

  clean_9x(base)

  let r = base.new_request()

  let raised = try {

    r.execute_in_tx(m7_body_outer)

    false

  } catch {

    _ => true

  }

  check(raised, "T1-M7 外层抛错按预期外传")

  check(!r.tx_active(), "T1-M7 模板收尾清空本实例事务槽(不留悬空连接)")

  check(m6_row_count(base, 990032L) == 0L, "T1-M7 外层写入随外层回滚撤掉")

  check(m6_row_count(base, 990033L) == 0L, "T1-M7 嵌套内层写入与外层同事务、被一起撤掉")

  clean_9x(base)

}



///|

/// T1-M8 归属边界的两个方向:

/// 拷贝共享槽(⇒ Ctx/Facade/Engine 各自持有的那份 R 自动落在同一事务里,这是 b 成立的前提);

/// 派生独立槽(⇒ 请求边界切开,这是 b 修复本案的前提)。

async fn m8_body(r : @repo.MysqlRepository) -> Unit raise {

  let as_copy = r

  as_copy.save_instance(m6_inst(990034L))

  let derived = r.new_request()

  derived.save_instance(m6_inst(990035L))

  check(derived.tx_active() == false, "T1-M8 派生实例看不到母体的事务(新槽)")

  raise @error.Business("m8-boom")

}



async fn t1_m8_tx_ownership_boundary() -> Unit raise {

  println("T1-M8 事务归属边界:拷贝共用槽 / 派生独立槽...")

  let base = @repo.MysqlRepository::from_env()

  clean_9x(base)

  let r = base.new_request()

  let raised = try {

    r.execute_in_tx(m8_body)

    false

  } catch {

    _ => true

  }

  check(raised, "T1-M8 抛错按预期外传")

  check(m6_row_count(base, 990034L) == 0L, "T1-M8 经拷贝写入的行落在事务内(被回滚撤掉)")

  check(m6_row_count(base, 990035L) == 1L, "T1-M8 经派生实例写入的行落在事务外(autocommit 留存)")

  clean_9x(base)

}

///|

/// T1-M10 空转档(audit8 R95,10-16 owner 拍甲补上)——spec/12 §12.4 第三档,专抓

/// 「名义有事务、实际盖不住写」:SPI 声明在不在、模板注册没注册、代码读起来像不像有事务,

/// 全都看不出这个破口,只有跑这一档才现形。三腿各钉一件事:

/// ① 事务内写完,**同一条事务连接**必须读得到——读的是真实仓储路径 `find_instance_by_id`

///    (它内部就是 `open_or_tx()`),不是手摘 `current_tx_conn`:测的是那 42 个调用点把读写汇到

///    哪条连接上,而不是管子本身;

/// ② 提交之前另开一条**独立连接**读不到(`open_raw()` 恒新开)——这腿同时防"事务压根没开、每句

///    autocommit"那种假象把 ① 一起糊过去;

/// ③ 抛错回滚后独立连接仍读不到(终态不存在)。

/// 阳性对照(变异,读数见 notes/R95.md)=把 `open_or_tx` 退回「每句自取新连接」⇒ ① 必红;

/// 去掉回滚臂 ⇒ ③ 必红。

async fn m10_body(r : @repo.MysqlRepository) -> Unit raise {

  r.save_instance(m6_inst(990040L))

  let seen = r.find_instance_by_id(990040L)

  let readable = match seen {

    Some(_) => true

    None => false

  }

  check(readable, "T1-M10 空转档①:事务内在同一条连接上读得到自己刚写的行")

  let other = m6_row_count(r, 990040L)

  check(other == 0L, "T1-M10 空转档②:独立连接在提交前读不到未提交的行(实得 \{other})")

  raise @error.Business("m10-boom-in-tx")

}



///|

async fn t1_m10_tx_read_your_own_writes() -> Unit raise {

  println("T1-M10 空转档:同连接读回+提交前独立连接不可见+回滚后不存在(spec/12 §12.4 第三档)...")

  let base = @repo.MysqlRepository::from_env()

  clean_9x(base)

  let r = base.new_request()

  let raised = try {

    r.execute_in_tx(m10_body)

    false

  } catch {

    _ => true

  }

  check(raised, "T1-M10 抛错按预期外传(模板已 ROLLBACK)")

  check(m6_row_count(base, 990040L) == 0L, "T1-M10 空转档③:回滚后独立连接读不到该行")

  clean_9x(base)

}

///|

async fn t1_all() -> Unit raise {

  println("T1-M1 page five keys...")

  t1_m1_page()

  println("T1-M2 hydrate + engine flow...")

  t1_m2_hydrate()

  println("T1-M3 m_ params...")

  t1_m3_mparams()

  println("T1-M9 page_defines 条件+order_by 真落 SQL(issues/197)...")

  t1_m9_define_order_by()

  println("T1-M4 tx rollback...")

  t1_m4_tx_rollback()

  println("T1-M5 idempotent execute...")

  t1_m5_idempotent()

  println("T1-I110 find_instance_by_id hydrate tasks...")

  t1_i110_hydrate()

  println("T1-I114 update_user 真写进 UPDATE 语句(仓储级)...")

  t1_i114_update_user_lands()

  println("T1-I120 六条写路径时间列 = 引擎钟,SQL 侧无 NOW()(仓储级)...")

  t1_i120_engine_clock_lands()

  println("T1-I116 委托查询四判据 + 双仓同答案...")

  t1_i116_surrogate_query()

  println("T1-I117 我已办状态集合 <>10 + 空 operator 空页...")

  t1_i117_done_state_set()

  println("T1-I152 委托台账分页归属不变式(空归属=空页,双仓同答案)...")

  t1_i152_surrogate_page_ownership()

  t1_m6_tx_scope_isolation()

  t1_m7_nested_tx_reuse()

  t1_m8_tx_ownership_boundary()

  t1_m10_tx_read_your_own_writes()

  println("T1 ALL PASS (wasm → 160)")

  println("门面级 withdraw/transfer 真机对拍另跑:moon run --target wasm demo/cmd/t1_mysql")

}



///|

/// T1-I152(issues/152 ② · spec 06 §4.5 processSurrogate/page 归属不变式,SQL 腿):

/// 归属列 operator 的条件缺键/空串/全空白 ⇒ **空页**(recordCount=0 且 rows=[]),

/// 绝不"这条不加"退化成全库台账;与内存仓同一判据(@model.surrogate_ownership_operator)

/// ⇒ 同一份夹具两仓逐档并排对拍,条数或集合不一致即红(条款 6)。

/// 阳性对照:有效归属条件照常出行——防"一律空页"那种假修。

async fn t1_i152_surrogate_page_ownership() -> Unit raise {

  let repo = @repo.MysqlRepository::from_env()

  clean_9x(repo)

  let mem = @mem.MemoryRepository::new()

  let seed = [

    sg_row(991521L, "i152-own", "i152-ag-a", "i152-flow", None, None, 1),

    sg_row(991522L, "i152-own", "i152-ag-b", "", None, None, 1),

    sg_row(991523L, "i152-other", "i152-ag-c", "i152-flow", None, None, 1),

  ]

  for r in seed {

    repo.save_surrogate(r)

    mem.save_surrogate(r)

  }

  // 阳性对照:有效归属档出行(own=2 / other=1),且双仓条数一致

  let q_own = @model.PageQuery::new(1L, 20L)

  q_own.set_operator(Some("i152-own"))

  let sql_own = repo.page_surrogates(q_own)

  let mem_own = mem.page_surrogates(q_own)

  check(sql_own.record_count() == 2L, "I152 SQL 仓有效归属=2 行(实际=\{sql_own.record_count()})")

  check(mem_own.record_count() == sql_own.record_count(), "I152 条款6 双仓条数一致(有效档)")

  for r in sql_own.rows() {

    check(r.operator == "i152-own", "I152 SQL 仓读回行归属列逐字为 own(实际=\{r.operator})")

  }

  let q_other = @model.PageQuery::new(1L, 20L)

  q_other.set_operator(Some("i152-other"))

  check(repo.page_surrogates(q_other).record_count() == 1L, "I152 SQL 仓他人档=1 行")

  // 负向:缺键 / 空串 / 全空白 ⇒ 空页(不是全库 3 行),双仓同答案

  let q_none = @model.PageQuery::new(1L, 20L)

  check(repo.page_surrogates(q_none).record_count() == 0L, "I152 SQL 仓缺归属键 ⇒ 空页(不得全库 3 行)")

  check(repo.page_surrogates(q_none).rows().is_empty(), "I152 SQL 仓缺键档 rows 也为空")

  check(mem.page_surrogates(q_none).record_count() == 0L, "I152 内存仓缺归属键 ⇒ 空页(同答案)")

  for blank in ["", "   ", " \t\r\n "] {

    let qb = @model.PageQuery::new(1L, 20L)

    qb.set_operator(Some(blank))

    check(repo.page_surrogates(qb).record_count() == 0L, "I152 SQL 仓空白归属档(\{blank}) ⇒ 空页")

    check(mem.page_surrogates(qb).record_count() == 0L, "I152 内存仓空白归属档(\{blank}) ⇒ 空页(条款6)")

  }

  clean_9x(repo)

}



///|

async fn smoke() -> Unit raise {

  if skip_mysql() {

    println("SKIP_MYSQL=1 → T1 skipped (开发机口径;发版机连不上=fail)")

    return

  }

  t1_all()

}



///|

fn main {

  @async.run_async_main(smoke)

}