@tyhld/conductor 0.12.6 → 0.12.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -1
- package/sales-template/setup.sh +28 -6
- package/scripts/dist-source.sh +168 -0
- package/scripts/hooks/skill_sync_notify.py +155 -0
- package/scripts/hooks/test_policy.py +20 -4
- package/scripts/hooks/test_skill_sync_notify.py +205 -0
- package/scripts/update-guard.sh +1014 -0
- package/systemd/guard-sync-install.sh +22 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tyhld/conductor",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.8",
|
|
4
4
|
"description": "采配くん管制の見守りアプリ(conductor-agent)。各PCで常駐し、中央(devlog-tracker)へ定期的に生存報告(heartbeat)を送る常駐CLI。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
"sales-template",
|
|
20
20
|
"scripts/hooks",
|
|
21
21
|
"!scripts/hooks/__pycache__",
|
|
22
|
+
"scripts/update-guard.sh",
|
|
23
|
+
"scripts/dist-source.sh",
|
|
22
24
|
"scripts/codex",
|
|
23
25
|
"!scripts/codex/__pycache__",
|
|
24
26
|
"skills",
|
package/sales-template/setup.sh
CHANGED
|
@@ -571,7 +571,29 @@ mkdir -p "$CLAUDE_DIR"
|
|
|
571
571
|
# Notification を足しても【新規に設置したお客さまの機体には入らなかった】。
|
|
572
572
|
# 下の ⑤-0 で見本と配る中身を機械照合し、外れていれば設置を止める=足し忘れが起こせない。
|
|
573
573
|
GUARD_HOME="$HOME/.tyhld/hooks"
|
|
574
|
-
|
|
574
|
+
|
|
575
|
+
# ─── ★配り元を決める(便 4061dd71) ─────────────────────────────────────────
|
|
576
|
+
# 【なぜ要るか】この台本は「setup.sh から見て .. 」を配り元にしている。お客さまの機では
|
|
577
|
+
# それが公開済みの配布物なので正しい。ところが開発機でリポジトリの sales-template/setup.sh を
|
|
578
|
+
# 打つと、配り元は★職人の作業ツリーになり、作りかけの番人がその機の全現場へ配られる。
|
|
579
|
+
# 【どう直したか】呼び出し元が git の作業場所かどうかで分ける(dist-source.sh)。
|
|
580
|
+
# ・git の作業場所でない=配布物そのもの(★お客さまの機はいつもこちら)。
|
|
581
|
+
# 今までどおりそのまま使う。公開 npm への問い合わせもしない=回線が無くても設置できる。
|
|
582
|
+
# ・git の作業場所=職人の作業ツリー。公開済みの正規版(この機に入っている配布物で、版が
|
|
583
|
+
# 公開 npm のいまの版と一致するもの)へ差し替える。確かめられなければ配らずに止まる。
|
|
584
|
+
. "$SCRIPT_DIR/../scripts/dist-source.sh"
|
|
585
|
+
if ! dist_source_for_setup "$(cd "$SCRIPT_DIR/.." && pwd)"; then
|
|
586
|
+
fail "配り元を確かめられません(${DIST_REASON})。★作業ツリーからは配りません。
|
|
587
|
+
人の手番: npm install -g @tyhld/conductor --@tyhld:registry=https://registry.npmjs.org
|
|
588
|
+
そのうえで、入れた配布物の setup.sh を打ってください。"
|
|
589
|
+
fi
|
|
590
|
+
SETUP_DIST_ROOT="$DIST_ROOT"
|
|
591
|
+
if [[ "$SETUP_DIST_ROOT" != "$(cd "$SCRIPT_DIR/.." && pwd)" ]]; then
|
|
592
|
+
echo "[setup] 配り元: $SETUP_DIST_ROOT(版 ${DIST_VERSION:-不明}=公開 npm のいまの版)"
|
|
593
|
+
echo "[setup] ★この台本の置き場(作業ツリー)からは配りません。"
|
|
594
|
+
fi
|
|
595
|
+
|
|
596
|
+
GUARD_SAMPLE="$SETUP_DIST_ROOT/scripts/hooks/settings.sample.json"
|
|
575
597
|
|
|
576
598
|
# --- 番人一式(正本1本)を固定パスへ配置 ---
|
|
577
599
|
# ★配布元は「正本1本」だけ。以前は sales-template/ に複製を置いていたため、正本が更新されても
|
|
@@ -579,7 +601,7 @@ GUARD_SAMPLE="$SCRIPT_DIR/../scripts/hooks/settings.sample.json"
|
|
|
579
601
|
#
|
|
580
602
|
# 正本の在り処は「setup.sh から見て ../scripts/hooks」。npm 配布物でもリポジトリでも
|
|
581
603
|
# 同じ相対位置になる(package.json の files に scripts/hooks を含めてある)。
|
|
582
|
-
GUARD_SRC_DIR="$
|
|
604
|
+
GUARD_SRC_DIR="$SETUP_DIST_ROOT/scripts/hooks"
|
|
583
605
|
|
|
584
606
|
# ★配る番人一式(段2の3フック+その依存)。scripts/update-guard.sh と同じ集合にする。
|
|
585
607
|
# 1本でも欠けると差し替えた現場の番人が読めず止まる(勤務用コピーの考え方=ADR-002)。
|
|
@@ -761,7 +783,7 @@ echo "[setup] 番人フック($registered_events)を $HOME_SETTINGS に登録し
|
|
|
761
783
|
# --- Codex の職人にも番人を登録する(★ADR-022・Codex を使う機体だけ) -------
|
|
762
784
|
# ★同じ処理を2か所に書かない: 稼働機の更新(update-guard.sh ①-e2)と同じスクリプトを呼ぶ。
|
|
763
785
|
# Codex を使っていない機体では中で何もしない(~/.codex を勝手に作らない)。
|
|
764
|
-
CODEX_HOOK_INSTALL="$
|
|
786
|
+
CODEX_HOOK_INSTALL="$SETUP_DIST_ROOT/scripts/hooks/install-codex-hooks.sh"
|
|
765
787
|
if [[ -f "$CODEX_HOOK_INSTALL" ]]; then
|
|
766
788
|
APPLY=1 GUARD_HOME="$GUARD_HOME" bash "$CODEX_HOOK_INSTALL" \
|
|
767
789
|
|| warn "Codex への番人登録に失敗しました(Claude 側の設置は完了しています)"
|
|
@@ -777,9 +799,9 @@ fi
|
|
|
777
799
|
# 締め出す物の正本は @tyhld/conductor の src/proto-profile.ts(=dist/proto-profile.js)1本で、
|
|
778
800
|
# deny も deniedDomains もそこから生成される。ここに一覧を写さない(写しを2か所に持たない=
|
|
779
801
|
# 片方に足してもう片方を忘れる事故を構造的に起こせなくする)。
|
|
780
|
-
SETTINGS_SRC="$
|
|
802
|
+
SETTINGS_SRC="$SETUP_DIST_ROOT/sales-template/settings.json"
|
|
781
803
|
SETTINGS_DST="$CLAUDE_DIR/settings.json"
|
|
782
|
-
PROTO_GEN="$
|
|
804
|
+
PROTO_GEN="$SETUP_DIST_ROOT/dist/proto-profile.js"
|
|
783
805
|
if [[ "$PROFILE" == "proto" ]]; then
|
|
784
806
|
[[ -f "$PROTO_GEN" ]] || fail "試作の型の正本が見つかりません: $PROTO_GEN
|
|
785
807
|
配布物が壊れている可能性があります。次の1行で入れ直してください:
|
|
@@ -882,7 +904,7 @@ fi
|
|
|
882
904
|
# ★番人と違い、これは安全境界ではなく作業規律なので、見つからないときは中断せず警告して進む
|
|
883
905
|
# (スキルが無くても番人は効いており、安全に穴は空かない)。
|
|
884
906
|
CRAFTSMAN_SKILL_NAME="conductor-craftsman"
|
|
885
|
-
CRAFTSMAN_SKILL_SRC="$
|
|
907
|
+
CRAFTSMAN_SKILL_SRC="$SETUP_DIST_ROOT/skills/$CRAFTSMAN_SKILL_NAME"
|
|
886
908
|
CRAFTSMAN_SKILL_DST="$HOME/.claude/skills/$CRAFTSMAN_SKILL_NAME"
|
|
887
909
|
|
|
888
910
|
if [[ -f "$CRAFTSMAN_SKILL_SRC/SKILL.md" ]]; then
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# shellcheck disable=SC2034
|
|
3
|
+
# ★上の指示(SC2034=使っていない変数)は、このファイル全体に効かせるため最初のコマンドより前に置く。
|
|
4
|
+
# この部品の「返り値」は DIST_ROOT / DIST_VERSION / DIST_PUBLISHED / DIST_REASON の4つで、
|
|
5
|
+
# 読むのは呼び出し元(update-guard.sh / sales-template/setup.sh / systemd/guard-sync-install.sh)。
|
|
6
|
+
# このファイルの中では使い切らないので、指摘だけが出てしまう。
|
|
7
|
+
#
|
|
8
|
+
# dist-source.sh — 「全現場へ配ってよい中身は、どこにあるか」を決める1か所(★便 4061dd71)。
|
|
9
|
+
#
|
|
10
|
+
# 【なぜ要るか(実測 2026-09-16・開発履歴 6b405ae6)】
|
|
11
|
+
# 1時間ごとの番人の自動同期(tyhld-guard-sync.timer)の本体が
|
|
12
|
+
# /home/fshim/projects/conductor/scripts/update-guard.sh
|
|
13
|
+
# =★conductor 職人の【作業ツリー】を指していた。職人が番人を直しかけのブランチに居るあいだに
|
|
14
|
+
# 同期が走ると、作りかけの番人がその機の【全現場】へ配られる。当日は main で変更なしだったため
|
|
15
|
+
# 無事だった(配った番人7本が公開版 0.12.5 と cmp で一致)。無事だったのは偶然である。
|
|
16
|
+
#
|
|
17
|
+
# 【えふさん確定(2026-09-16)】
|
|
18
|
+
# 「① 番人の1時間自動同期 承認 29612ff1後に実施。ただし『公開済みの正規版』を唯一の配布元にする。
|
|
19
|
+
# 作業treeから全現場へ配らない」
|
|
20
|
+
#
|
|
21
|
+
# 【この部品が決めること】★これだけ
|
|
22
|
+
# ・配ってよい中身の置き場(DIST_ROOT)=この機に入っている @tyhld/conductor の配布物
|
|
23
|
+
# ・それが「公開済みの正規版」か(入っている版 == 公開 npm のいまの版)
|
|
24
|
+
# ・違う/確かめられないときは【配らない】と言い切ること
|
|
25
|
+
#
|
|
26
|
+
# 【この部品が決めないこと】
|
|
27
|
+
# ・番人の判定・禁止の中身・鉄則の中身(1文字も触らない)
|
|
28
|
+
# ・配った結果の記録と知らせ方(呼び出し側が便 29612ff1 の仕組みに載せる)
|
|
29
|
+
#
|
|
30
|
+
# 【使い方】
|
|
31
|
+
# . "$(dirname "$0")/dist-source.sh"
|
|
32
|
+
# if dist_source_resolve; then …DIST_ROOT を配り元にしてよい…
|
|
33
|
+
# else …DIST_REASON を記録して配らない…
|
|
34
|
+
# fi
|
|
35
|
+
#
|
|
36
|
+
# 【試験用の入口】★安全境界ではない(環境変数を置ける人は cp も打てる)。
|
|
37
|
+
# CONDUCTOR_DIST_ROOT … 配布物の置き場を名指しする(試験で偽の配布物を指すため)
|
|
38
|
+
# CONDUCTOR_DIST_PUBLISHED … 公開 npm への問い合わせの代わりに使う値(試験で網を切らないため)
|
|
39
|
+
|
|
40
|
+
# ★取得先の宣言は install.sh / setup.sh の「registry-pin」正本と同じ形にする。
|
|
41
|
+
# (公開 npm 1本。手元の設定が何であってもここだけは公開 npm を見る=便 9858450b)
|
|
42
|
+
CONDUCTOR_PKG="${CONDUCTOR_PKG:-@tyhld/conductor}"
|
|
43
|
+
CONDUCTOR_REGISTRY="${CONDUCTOR_REGISTRY:-https://registry.npmjs.org}"
|
|
44
|
+
CONDUCTOR_REGISTRY_ARG="--@tyhld:registry=${CONDUCTOR_REGISTRY}"
|
|
45
|
+
|
|
46
|
+
#: 解決の結果。呼び出し側はこの3つだけ見ればよい。
|
|
47
|
+
DIST_ROOT='' # 配ってよい中身の置き場(確かめられたときだけ入る)
|
|
48
|
+
DIST_VERSION='' # そこに入っている版
|
|
49
|
+
DIST_PUBLISHED='' # 公開 npm のいまの版
|
|
50
|
+
DIST_REASON='' # 配れないときの理由(1語・記録にそのまま載る)
|
|
51
|
+
|
|
52
|
+
# この機に入っている配布物の置き場を探す。見つからなければ空。
|
|
53
|
+
# ★node の版が変わると /home/…/.nvm/versions/node/<版>/… の <版> が変わる。
|
|
54
|
+
# だから固定のパスを覚えない。いま PATH に居る conductor から辿る(版が変わっても付いていく)。
|
|
55
|
+
dist_pkg_root() {
|
|
56
|
+
if [[ -n "${CONDUCTOR_DIST_ROOT:-}" ]]; then
|
|
57
|
+
printf '%s' "$CONDUCTOR_DIST_ROOT"
|
|
58
|
+
return 0
|
|
59
|
+
fi
|
|
60
|
+
local bin real root
|
|
61
|
+
# ★bash で動く書き方(command -v -a は zsh 専用。便 9858450b で踏んだ)。
|
|
62
|
+
bin="$(type -a -p conductor 2>/dev/null | head -n 1)"
|
|
63
|
+
if [[ -n "$bin" ]]; then
|
|
64
|
+
real="$(readlink -f "$bin" 2>/dev/null || printf '%s' "$bin")"
|
|
65
|
+
# <置き場>/dist/cli.js から2つ上がると配布物の根になる。
|
|
66
|
+
root="$(cd "$(dirname "$real")/.." 2>/dev/null && pwd)" || root=''
|
|
67
|
+
if [[ -n "$root" && -f "$root/package.json" ]]; then
|
|
68
|
+
printf '%s' "$root"
|
|
69
|
+
return 0
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
# 予備=npm が言う全体の置き場から辿る。
|
|
73
|
+
local groot
|
|
74
|
+
groot="$(npm root -g 2>/dev/null)"
|
|
75
|
+
if [[ -n "$groot" && -f "$groot/$CONDUCTOR_PKG/package.json" ]]; then
|
|
76
|
+
printf '%s' "$groot/$CONDUCTOR_PKG"
|
|
77
|
+
return 0
|
|
78
|
+
fi
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# 指定の置き場に入っている版(package.json)。読めなければ空。
|
|
82
|
+
dist_installed_version() {
|
|
83
|
+
local pj="$1/package.json"
|
|
84
|
+
[[ -f "$pj" ]] || return 0
|
|
85
|
+
python3 -c 'import json,sys
|
|
86
|
+
try:
|
|
87
|
+
print(json.load(open(sys.argv[1], encoding="utf-8")).get("version") or "")
|
|
88
|
+
except Exception:
|
|
89
|
+
pass' "$pj" 2>/dev/null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# 公開 npm が「いま配っている版」(取れなければ空)。★固定の下限は使わない(便 9858450b)。
|
|
93
|
+
dist_published_version() {
|
|
94
|
+
# ★検査用の入口。空文字を入れれば「問い合わせられなかった」を作れる(網を切らずに確かめる)。
|
|
95
|
+
if [[ -n "${CONDUCTOR_DIST_PUBLISHED+x}" ]]; then
|
|
96
|
+
printf '%s' "$CONDUCTOR_DIST_PUBLISHED"
|
|
97
|
+
return 0
|
|
98
|
+
fi
|
|
99
|
+
local v
|
|
100
|
+
v="$(npm view "$CONDUCTOR_PKG" version "$CONDUCTOR_REGISTRY_ARG" 2>/dev/null | tr -d '[:space:]')"
|
|
101
|
+
if [[ "$v" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
|
102
|
+
printf '%s' "${BASH_REMATCH[0]}"
|
|
103
|
+
fi
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# 配ってよい中身の置き場を決める。
|
|
107
|
+
# 返り 0 … DIST_ROOT を配り元にしてよい
|
|
108
|
+
# 返り 1 … 配ってはいけない(DIST_REASON に理由)
|
|
109
|
+
# 理由の語(記録にそのまま載る)
|
|
110
|
+
# not-installed … 配布物がこの機に入っていない
|
|
111
|
+
# no-version … 入っている配布物の版が読めない
|
|
112
|
+
# unverifiable … 公開 npm に問い合わせられない(回線・取得先の不調)
|
|
113
|
+
# version-drift … 入っている版が公開 npm のいまの版と違う
|
|
114
|
+
dist_source_resolve() {
|
|
115
|
+
DIST_ROOT=''; DIST_VERSION=''; DIST_PUBLISHED=''; DIST_REASON=''
|
|
116
|
+
local root
|
|
117
|
+
root="$(dist_pkg_root)"
|
|
118
|
+
if [[ -z "$root" ]]; then
|
|
119
|
+
DIST_REASON='not-installed'
|
|
120
|
+
return 1
|
|
121
|
+
fi
|
|
122
|
+
DIST_VERSION="$(dist_installed_version "$root")"
|
|
123
|
+
if [[ -z "$DIST_VERSION" ]]; then
|
|
124
|
+
DIST_REASON='no-version'
|
|
125
|
+
return 1
|
|
126
|
+
fi
|
|
127
|
+
DIST_PUBLISHED="$(dist_published_version)"
|
|
128
|
+
if [[ -z "$DIST_PUBLISHED" ]]; then
|
|
129
|
+
# ★ここで「まあいいか」と配らない。問い合わせられない=正規版かどうか分からない、である。
|
|
130
|
+
DIST_REASON='unverifiable'
|
|
131
|
+
return 1
|
|
132
|
+
fi
|
|
133
|
+
if [[ "$DIST_VERSION" != "$DIST_PUBLISHED" ]]; then
|
|
134
|
+
DIST_REASON='version-drift'
|
|
135
|
+
return 1
|
|
136
|
+
fi
|
|
137
|
+
DIST_ROOT="$root"
|
|
138
|
+
return 0
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# 配布物として体裁が揃っているか(配る中身が欠けた置き場を掴まない)。
|
|
142
|
+
# ★ここで見るのは「采配くんの配布物であること」を決める最小の3つだけ。
|
|
143
|
+
# 職人の鉄則(skills/)は安全境界ではなく作業規律で、欠けたときの扱いは ①-d が持っている
|
|
144
|
+
# (便 29612ff1=成功と偽らず控えに残して管制へ知らせる)。ここで落とすと、その扱いが働かない。
|
|
145
|
+
dist_source_complete() {
|
|
146
|
+
local root="$1"
|
|
147
|
+
[[ -f "$root/package.json" ]] || return 1
|
|
148
|
+
[[ -f "$root/scripts/hooks/cc2_guard.py" ]] || return 1
|
|
149
|
+
[[ -f "$root/sales-template/settings.json" ]] || return 1
|
|
150
|
+
return 0
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# 設置台本(setup.sh)のための配り元。★お客さまの機の流れを1ミリも変えないための分岐。
|
|
154
|
+
# ・呼び出し元が【git の作業場所でない】=配布物そのもの(お客さまの機はいつもこちら)。
|
|
155
|
+
# そのまま使う。公開 npm への問い合わせもしない=今までどおり・回線が無くても設置できる。
|
|
156
|
+
# ・呼び出し元が【git の作業場所】=職人の作業ツリー。ここから全現場へ配らせない。
|
|
157
|
+
# 公開済みの正規版へ差し替える(確かめられなければ 1 を返して呼び出し元を止める)。
|
|
158
|
+
# 返り 0 … DIST_ROOT を配り元にしてよい/返り 1 … 配ってはいけない(DIST_REASON に理由)
|
|
159
|
+
dist_source_for_setup() {
|
|
160
|
+
local caller_root="$1"
|
|
161
|
+
DIST_ROOT=''; DIST_VERSION=''; DIST_PUBLISHED=''; DIST_REASON=''
|
|
162
|
+
if ! git -C "$caller_root" rev-parse --git-dir >/dev/null 2>&1; then
|
|
163
|
+
DIST_ROOT="$caller_root"
|
|
164
|
+
DIST_VERSION="$(dist_installed_version "$caller_root")"
|
|
165
|
+
return 0
|
|
166
|
+
fi
|
|
167
|
+
dist_source_resolve
|
|
168
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""skill_sync_notify.py — 職人の鉄則を配り損ねたことを管制へ知らせる(★便 29612ff1)。
|
|
3
|
+
|
|
4
|
+
【なぜ要るか(えふさん確定 2026-09-16)】
|
|
5
|
+
「職人ルールを1時間ごとに自動更新 承認。★更新失敗時に古いルールのまま黙って動かないこと。
|
|
6
|
+
適用version/時刻を確認可能にする」
|
|
7
|
+
|
|
8
|
+
1時間ごとの自動同期が鉄則を配れなかったとき、いちばん困るのは
|
|
9
|
+
【古い鉄則のまま職人が動き続け、誰もそれに気づかない】こと。実際に便 4a5bbb9f で、
|
|
10
|
+
鉄則7を直したのに現場の職人は古い文のまま動いていた(人が APPLY=1 を打つまで)。
|
|
11
|
+
だから配り損ねは、機体の中のログだけでなく【管制からも見える】形にする。
|
|
12
|
+
|
|
13
|
+
【なぜ「確認カード」を選んだか(生存報告・開発履歴と比べて)】
|
|
14
|
+
・生存報告(POST /api/conductor/report)… 送っているのは現場ごとの relay で、項目は
|
|
15
|
+
machine / site / branch / status / lastAction / artisanReady に決まっている。
|
|
16
|
+
「鉄則が古い」を載せる場所が無く、足すと中央側の変更が要る。★中央は触らない方針。
|
|
17
|
+
・開発履歴(devlog)… 書き込みはログインの JWT が要る(MCP 経路)。1時間ごとの
|
|
18
|
+
無人の同期は JWT を持たない。★持たせると、常駐に新しい秘密を増やすことになる。
|
|
19
|
+
・確認カード(POST /api/conductor/hook-requests)… ★これを選んだ。
|
|
20
|
+
- 「この機体で人が1つ操作しないと直らない」を伝えるために作られた口である(ADR-016)。
|
|
21
|
+
- 送るのに要るのは、番人がもう持っている接続情報だけ(hook_datalink が読む)。
|
|
22
|
+
新しい鍵も新しい API も増えない。
|
|
23
|
+
- 答えの選択肢を付けなければ、管制では【お知らせ】として出て「閉じる」だけになる
|
|
24
|
+
(便 927da60e で確かめた描き方)。押せないボタンは出ない。
|
|
25
|
+
- 閉じられるまで出たままなので、★古い鉄則のままであることが外から見え続ける。
|
|
26
|
+
|
|
27
|
+
【何があっても同期を止めない】
|
|
28
|
+
接続情報が無い機体・管制へ届かない機体でも、ここは黙って終わる(exit 0)。
|
|
29
|
+
その場合でも、配り損ねた事実は $HOME/.tyhld/craftsman-skill.last に残っている。
|
|
30
|
+
|
|
31
|
+
【使い方】
|
|
32
|
+
python3 skill_sync_notify.py <直近の試みの控えのパス>
|
|
33
|
+
★控えの1行目が status=failed のときだけカードを出す。ok のときは何もしない。
|
|
34
|
+
"""
|
|
35
|
+
import os
|
|
36
|
+
import sys
|
|
37
|
+
|
|
38
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
39
|
+
|
|
40
|
+
import hook_datalink # noqa: E402
|
|
41
|
+
|
|
42
|
+
#: カードを出したままにする時間(ミリ秒)=60分。管制側の上限(HOOK_MAX_WAIT_MS)と同じ。
|
|
43
|
+
#: ★このスクリプトは待たない。ここで宣言した時間だけ管制にカードが出たままになり、
|
|
44
|
+
#: 配り直しが成功すれば次の回にはもう出さないので、自然に消える。
|
|
45
|
+
#: ★1時間ごとの同期と同じ幅にしてあるので、直らない限りカードは途切れない。
|
|
46
|
+
NOTIFY_WAIT_MS = 60 * 60 * 1000
|
|
47
|
+
|
|
48
|
+
#: 同じカードを積み上げないための鍵の頭。★機体ごと・種類ごとに1枚だけにする。
|
|
49
|
+
#: 管制は同じ鍵の再登録を【寿命の延長】として扱う(upsertHookRequest)。だから
|
|
50
|
+
#: 毎時の失敗でカードが増えず、直ればその回で登録が止まって数分後に自然に閉じる。
|
|
51
|
+
REQUEST_KEY_PREFIX = 'craftsman-skill-stale'
|
|
52
|
+
|
|
53
|
+
#: 知らせの種類ごとの文面(★便 4061dd71 で2種類目を足した)。
|
|
54
|
+
#: 控えの `what=` で選ぶ。無ければ従来どおり鉄則の配り損ね(便 29612ff1)。
|
|
55
|
+
#: ★新しい知らせ方(別の口)は作らない。ここに1行足すだけで同じ確認カードに載る。
|
|
56
|
+
NOTICE_KINDS = {
|
|
57
|
+
'craftsman-skill': {
|
|
58
|
+
'key': 'craftsman-skill-stale',
|
|
59
|
+
'summary': '職人の決まりが古いままです',
|
|
60
|
+
'stopped': '職人の決まり(鉄則)が、新しい版に入れ替わっていません。'
|
|
61
|
+
'職人は古い決まりのまま動きます。',
|
|
62
|
+
},
|
|
63
|
+
'dist-source': {
|
|
64
|
+
'key': 'dist-source-unverified',
|
|
65
|
+
'summary': '安全装置を配れていません',
|
|
66
|
+
'stopped': '配り元(この PC に入っている采配くん本体)が公開済みの正規版だと'
|
|
67
|
+
'確かめられないため、番人・職人の決まり・現場の権限ルールを'
|
|
68
|
+
'1件も配っていません。現場は前に配られたままの状態で動きます。',
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def notice_kind(rec):
|
|
74
|
+
"""控えの what= から文面を選ぶ。知らない種類なら鉄則の配り損ねとして扱う。"""
|
|
75
|
+
return NOTICE_KINDS.get(rec.get('what') or 'craftsman-skill', NOTICE_KINDS['craftsman-skill'])
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def parse_last(path):
|
|
79
|
+
"""直近の試みの控えを {鍵: 値} に読む。読めなければ空。
|
|
80
|
+
|
|
81
|
+
形: `<ISO時刻> status=... reason=... version=... sha=... src=... dst=...`
|
|
82
|
+
"""
|
|
83
|
+
try:
|
|
84
|
+
with open(path, encoding='utf-8') as f:
|
|
85
|
+
line = f.readline().strip()
|
|
86
|
+
except Exception: # noqa: BLE001
|
|
87
|
+
return {}
|
|
88
|
+
if not line:
|
|
89
|
+
return {}
|
|
90
|
+
parts = line.split(' ')
|
|
91
|
+
out = {'at': parts[0]}
|
|
92
|
+
for p in parts[1:]:
|
|
93
|
+
if '=' in p:
|
|
94
|
+
k, v = p.split('=', 1)
|
|
95
|
+
out[k] = v
|
|
96
|
+
return out
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def build_payload(rec, env=None):
|
|
100
|
+
"""カードの中身。★何が起きていて、何をすれば直るかを日本語で書く。"""
|
|
101
|
+
env = os.environ if env is None else env
|
|
102
|
+
machine = env.get('CONDUCTOR_MACHINE') or os.uname().nodename
|
|
103
|
+
reason = rec.get('reason', '不明')
|
|
104
|
+
kind = notice_kind(rec)
|
|
105
|
+
body = '\n'.join([
|
|
106
|
+
f'いま止まっているもの: この PC({machine})で、{kind["stopped"]}',
|
|
107
|
+
'再開のしかた: この PC で次の1行を打ってください。'
|
|
108
|
+
' cd ~/projects/conductor そのあと APPLY=1 bash scripts/update-guard.sh',
|
|
109
|
+
'★このカードは管制からは押せません(この PC での操作が要ります)。',
|
|
110
|
+
f'PC: {machine}/うまくいかなかった理由: {reason}',
|
|
111
|
+
f'最後に試した時刻: {rec.get("at", "不明")}',
|
|
112
|
+
f'配ろうとした版: {rec.get("version", "不明")}',
|
|
113
|
+
])
|
|
114
|
+
return {
|
|
115
|
+
'site': env.get('CONDUCTOR_SITE') or machine,
|
|
116
|
+
'machine': machine,
|
|
117
|
+
# ★sessionId は絶対に空で送らない(空だと管制が 400 で弾く)。
|
|
118
|
+
'sessionId': hook_datalink.stable_session_id('', machine, 'guard-sync'),
|
|
119
|
+
'kind': 'message',
|
|
120
|
+
'question': body,
|
|
121
|
+
'summary': f'{kind["summary"]}({machine})',
|
|
122
|
+
'waitMs': NOTIFY_WAIT_MS,
|
|
123
|
+
# ★機体ごと・種類ごとに固定の鍵。毎時の失敗が同じ1枚に集約される(増殖しない)。
|
|
124
|
+
'requestKey': f'{kind["key"]}:{machine}',
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def run(last_path, cfg=None, datalink=hook_datalink):
|
|
129
|
+
"""返り値: 出したカードのID / None(出さなかった)。"""
|
|
130
|
+
rec = parse_last(last_path)
|
|
131
|
+
if rec.get('status') != 'failed':
|
|
132
|
+
return None
|
|
133
|
+
cfg = datalink.load_config() if cfg is None else cfg
|
|
134
|
+
if cfg is None:
|
|
135
|
+
# 接続情報が無い機体。★控えには残っているので、黙って消えるわけではない。
|
|
136
|
+
return None
|
|
137
|
+
reg = datalink.register(cfg, build_payload(rec))
|
|
138
|
+
if not reg or not reg.get('id'):
|
|
139
|
+
return None
|
|
140
|
+
# ★待たない。押されるまで管制に出たままにする=古い鉄則のままであることが見え続ける。
|
|
141
|
+
return reg['id']
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def main() -> int:
|
|
145
|
+
if len(sys.argv) < 2:
|
|
146
|
+
return 0
|
|
147
|
+
try:
|
|
148
|
+
run(sys.argv[1])
|
|
149
|
+
except Exception: # noqa: BLE001 知らせに失敗しても同期は止めない
|
|
150
|
+
pass
|
|
151
|
+
return 0
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == '__main__':
|
|
155
|
+
sys.exit(main())
|
|
@@ -959,16 +959,32 @@ _conductor = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # …/
|
|
|
959
959
|
_conductor = os.path.dirname(_conductor) # …/conductor
|
|
960
960
|
|
|
961
961
|
check('同期モードがある', 'SYNC_ONLY' in _ug, True)
|
|
962
|
-
check('
|
|
963
|
-
'①-b
|
|
962
|
+
check('同期モードは掃除・タイマー・pnpm 点検を飛ばす',
|
|
963
|
+
'①-b・①-c・①-d2 は省略(同期モード)' in _ug, True)
|
|
964
|
+
# ★職人の鉄則(①-d)は同期モードでも【配る】(えふさん確定 2026-09-16・便 29612ff1)。
|
|
965
|
+
# 番人のコードは1時間で届くのに鉄則だけ人待ちだと、直した鉄則が現場に届かないまま
|
|
966
|
+
# 毎時再生産される(便 4a5bbb9f で実測)。中身の検査は test/craftsman-skill-sync.test.ts。
|
|
967
|
+
check('職人の鉄則は同期モードでも配る',
|
|
968
|
+
_ug.index('①-d 職人の鉄則スキル') < _ug.index('①-b・①-c・①-d2 は省略(同期モード)'), True)
|
|
969
|
+
check('配り損ねたら成功と偽らず管制へ知らせる',
|
|
970
|
+
'craftsman_notify_failure' in _ug and 'skill_sync_notify.py' in _ug, True)
|
|
964
971
|
check('同期モードは権限ルールの整理より前で終わる',
|
|
965
972
|
_ug.index('同期のみ完了しました') < _ug.index('④ 各現場の権限ルール'), True)
|
|
966
973
|
# ★同期モードは「現場の二重登録を外す」(=番人の配線)までは行う(ADR-013)。
|
|
967
974
|
# 人が流し忘れても、すでに配ってしまった現場の二重登録が1時間以内にそろう。
|
|
968
975
|
check('同期モードは現場の二重登録外しまでは行う',
|
|
969
976
|
_ug.index('③ 現場の二重登録を外す') < _ug.index('同期のみ完了しました'), True)
|
|
970
|
-
|
|
971
|
-
|
|
977
|
+
# ★2026-09-16(便 4061dd71)に守り方を移した。旧=「この作業ツリーに未コミットの変更が
|
|
978
|
+
# 無ければ配ってよい」(=コミットさえすれば作りかけでも配れた・そもそも作業ツリーが配り元だった)。
|
|
979
|
+
# 新=配り元を「この機に入っている配布物で、版が公開 npm のいまの版と一致するもの」だけに限る。
|
|
980
|
+
# 中身の検査は test/dist-source-published-only.test.ts。
|
|
981
|
+
check('配り元を決める部品を通している', 'dist-source.sh' in _ug, True)
|
|
982
|
+
check('配り元は公開済みの正規版だけ', 'dist_source_resolve' in _ug, True)
|
|
983
|
+
check('配る中身はすべて配り元から取る',
|
|
984
|
+
all(x in _ug for x in ('GUARD_SRC_DIR="$DIST_ROOT/scripts/hooks"',
|
|
985
|
+
'SETTINGS_SRC="$DIST_ROOT/sales-template/settings.json"',
|
|
986
|
+
'CRAFTSMAN_SKILL_SRC="$DIST_ROOT/skills/conductor-craftsman/SKILL.md"')), True)
|
|
987
|
+
check('確かめられないときは配らずに終わる', 'dist_record failed' in _ug, True)
|
|
972
988
|
|
|
973
989
|
_svc = os.path.join(_conductor, 'systemd', 'tyhld-guard-sync.service')
|
|
974
990
|
_tmr = os.path.join(_conductor, 'systemd', 'tyhld-guard-sync.timer')
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""配り損ねの知らせ(skill_sync_notify.py)の単体テスト。
|
|
3
|
+
|
|
4
|
+
実行: python3 scripts/hooks/test_skill_sync_notify.py
|
|
5
|
+
1件でも期待と違えば exit 1。
|
|
6
|
+
|
|
7
|
+
【何を守っているか(便 29612ff1・えふさん確定 2026-09-16)】
|
|
8
|
+
「★更新失敗時に古いルールのまま黙って動かないこと。適用version/時刻を確認可能にする」
|
|
9
|
+
(a) 成功の控えでは【1件も知らせない】(毎時のカードで管制を埋めない)
|
|
10
|
+
(b) 失敗の控えでは【必ず知らせる】=黙って進まない
|
|
11
|
+
(c) カードの中身に「いま止まっているもの」「再開のしかた」が入っている
|
|
12
|
+
=管制が知らせとして描ける形(ai-kana の hook-notice-text.ts が探す見出し語)
|
|
13
|
+
(d) カードにボタンが1つも出ない形で送る(kind=message・choices なし)
|
|
14
|
+
=押せないボタンをお客さまの画面に置かない(ai-kana の hook-answerable.ts)
|
|
15
|
+
(e) 機体ごとに鍵が固定=毎時の失敗が同じ1枚に集約される(カードが増殖しない)
|
|
16
|
+
(f) 知らせに失敗しても例外を外へ出さない(★同期そのものは止めない)
|
|
17
|
+
(g) 控えが読めない・空・壊れていても落ちない
|
|
18
|
+
"""
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
import tempfile
|
|
22
|
+
|
|
23
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
24
|
+
|
|
25
|
+
import skill_sync_notify as sn # noqa: E402
|
|
26
|
+
|
|
27
|
+
failures = []
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def check(label, got, want):
|
|
31
|
+
ok = got == want
|
|
32
|
+
if not ok:
|
|
33
|
+
failures.append(f'{label}: got={got!r} want={want!r}')
|
|
34
|
+
print(f'{"OK " if ok else "NG "} {label:64} {got}')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def check_true(label, got):
|
|
38
|
+
check(label, bool(got), True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
CFG = {'url': 'https://example.invalid', 'token': 'dummy-token-not-real'}
|
|
42
|
+
OK_LINE = ('2026-09-16T10:00:00+0900 status=ok reason=applied version=0.12.7 '
|
|
43
|
+
'sha=abc123def456 src=/r/skills/x/SKILL.md dst=/h/.claude/skills/x/SKILL.md')
|
|
44
|
+
NG_LINE = ('2026-09-16T11:00:00+0900 status=failed reason=write-failed version=0.12.7 '
|
|
45
|
+
'sha=abc123def456 src=/r/skills/x/SKILL.md dst=/h/.claude/skills/x/SKILL.md')
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class FakeLink:
|
|
49
|
+
"""連絡線の代役。★通信は1度も行わない。"""
|
|
50
|
+
|
|
51
|
+
#: 「既定のまま」と「わざと None を返させる」を区別するための印。
|
|
52
|
+
DEFAULT = object()
|
|
53
|
+
|
|
54
|
+
def __init__(self, cfg=CFG, reg=DEFAULT):
|
|
55
|
+
self.cfg = cfg
|
|
56
|
+
self.reg = {'id': 'req-1'} if reg is FakeLink.DEFAULT else reg
|
|
57
|
+
self.sent = []
|
|
58
|
+
|
|
59
|
+
def load_config(self, env=None):
|
|
60
|
+
return self.cfg
|
|
61
|
+
|
|
62
|
+
def register(self, cfg, payload):
|
|
63
|
+
self.sent.append(payload)
|
|
64
|
+
return self.reg
|
|
65
|
+
|
|
66
|
+
# ★本物と同じ実装を借りる(sessionId が空にならないことを一緒に確かめるため)。
|
|
67
|
+
stable_session_id = staticmethod(sn.hook_datalink.stable_session_id)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def write(tmp, name, text):
|
|
71
|
+
p = os.path.join(tmp, name)
|
|
72
|
+
with open(p, 'w', encoding='utf-8') as f:
|
|
73
|
+
f.write(text)
|
|
74
|
+
return p
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ── (a)(b) 出す・出さない ────────────────────────────────────────────────────
|
|
78
|
+
print('\n【(a)(b) 成功では出さない・失敗では必ず出す】')
|
|
79
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
80
|
+
link = FakeLink()
|
|
81
|
+
check('成功の控え: 知らせない', sn.run(write(tmp, 'ok.last', OK_LINE + '\n'), datalink=link), None)
|
|
82
|
+
check('成功の控え: 1件も送っていない', len(link.sent), 0)
|
|
83
|
+
|
|
84
|
+
link = FakeLink()
|
|
85
|
+
got = sn.run(write(tmp, 'ng.last', NG_LINE + '\n'), datalink=link)
|
|
86
|
+
check('失敗の控え: 知らせた(カードIDが返る)', got, 'req-1')
|
|
87
|
+
check('失敗の控え: 1件だけ送った', len(link.sent), 1)
|
|
88
|
+
|
|
89
|
+
# ★控えが無い・空・壊れている=出さない(落ちない)=(g)
|
|
90
|
+
link = FakeLink()
|
|
91
|
+
check('控えが無い: 落ちずに何もしない', sn.run(os.path.join(tmp, 'none.last'), datalink=link), None)
|
|
92
|
+
check('控えが空: 落ちずに何もしない', sn.run(write(tmp, 'e.last', ''), datalink=link), None)
|
|
93
|
+
check('控えが壊れている: 落ちずに何もしない', sn.run(write(tmp, 'b.last', 'ゴミ\n'), datalink=link), None)
|
|
94
|
+
check('壊れた控えでは1件も送らない', len(link.sent), 0)
|
|
95
|
+
|
|
96
|
+
# ── (c)(d)(e) カードの形 ────────────────────────────────────────────────────
|
|
97
|
+
print('\n【(c)(d)(e) カードの形】')
|
|
98
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
99
|
+
link = FakeLink()
|
|
100
|
+
sn.run(write(tmp, 'ng.last', NG_LINE + '\n'), datalink=link)
|
|
101
|
+
p = link.sent[0]
|
|
102
|
+
q = p['question']
|
|
103
|
+
# (c) 管制が本文として取り出す見出し語(ai-kana: NOTICE_BODY_LABELS)。
|
|
104
|
+
check_true('本文に「いま止まっているもの」がある', 'いま止まっているもの:' in q)
|
|
105
|
+
check_true('本文に「再開のしかた」がある', '再開のしかた:' in q)
|
|
106
|
+
check_true('再開の打ち方が書いてある', 'update-guard.sh' in q)
|
|
107
|
+
# ★「古い決まりのまま動く」ことが本文から分かる=黙って動いていない。
|
|
108
|
+
check_true('古いまま動くと書いてある', '古い' in q)
|
|
109
|
+
# 失敗の理由・時刻・版が読める=(2) 適用version/時刻を確認可能にする。
|
|
110
|
+
check_true('うまくいかなかった理由が載る', 'write-failed' in q)
|
|
111
|
+
check_true('最後に試した時刻が載る', '2026-09-16T11:00:00+0900' in q)
|
|
112
|
+
check_true('配ろうとした版が載る', '0.12.7' in q)
|
|
113
|
+
# (d) ボタンを1つも出さない形(kind=message かつ choices なし)。
|
|
114
|
+
check('種別は message', p['kind'], 'message')
|
|
115
|
+
check('選択肢を付けない(押せないボタンを置かない)', 'choices' in p, False)
|
|
116
|
+
check_true('sessionId が空でない(管制が 400 で弾かない)', bool(p['sessionId'].strip()))
|
|
117
|
+
check_true('見出しが1行ある', bool(p['summary'].strip()))
|
|
118
|
+
check('待つと申告する時間は管制の上限と同じ60分', p['waitMs'], 3_600_000)
|
|
119
|
+
# (e) 鍵が機体ごとに固定。
|
|
120
|
+
check_true('鍵が付いている', p['requestKey'].startswith(sn.REQUEST_KEY_PREFIX + ':'))
|
|
121
|
+
link2 = FakeLink()
|
|
122
|
+
sn.run(write(tmp, 'ng2.last', NG_LINE + '\n'), datalink=link2)
|
|
123
|
+
check('2回目も同じ鍵(カードが増えない)', link2.sent[0]['requestKey'], p['requestKey'])
|
|
124
|
+
# ★控えの中身(パス)以外の余計な物を載せない=秘密は1文字も入らない。
|
|
125
|
+
check('本文にトークンは入らない', CFG['token'] in q, False)
|
|
126
|
+
|
|
127
|
+
# ── ★2種類目=配り元が正規版でない(便 4061dd71)────────────────────────────
|
|
128
|
+
print('\n【★2種類目=配り元の配り損ね(便 4061dd71)】')
|
|
129
|
+
DIST_NG = ('2026-09-16T12:00:00+0900 status=failed reason=version-drift what=dist-source '
|
|
130
|
+
'version=0.12.6 published=0.12.8 root=/n/@tyhld/conductor')
|
|
131
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
132
|
+
link = FakeLink()
|
|
133
|
+
sn.run(write(tmp, 'dist.last', DIST_NG + '\n'), datalink=link)
|
|
134
|
+
p = link.sent[0]
|
|
135
|
+
q = p['question']
|
|
136
|
+
check_true('見出しが配り元の話になっている', '安全装置を配れていません' in p['summary'])
|
|
137
|
+
check_true('本文に「いま止まっているもの」がある', 'いま止まっているもの:' in q)
|
|
138
|
+
check_true('本文に「再開のしかた」がある', '再開のしかた:' in q)
|
|
139
|
+
check_true('何を配れていないかが書いてある', '番人' in q and '権限ルール' in q)
|
|
140
|
+
check_true('うまくいかなかった理由が載る', 'version-drift' in q)
|
|
141
|
+
check_true('鍵が種類ごとに分かれている', p['requestKey'].startswith('dist-source-unverified:'))
|
|
142
|
+
# ★鉄則の配り損ねとは別の鍵=2種類が同じ1枚に潰れない。
|
|
143
|
+
link2 = FakeLink()
|
|
144
|
+
sn.run(write(tmp, 'skill.last', NG_LINE + '\n'), datalink=link2)
|
|
145
|
+
check('鉄則の配り損ねとは別の鍵', p['requestKey'] == link2.sent[0]['requestKey'], False)
|
|
146
|
+
# ★知らない種類は、従来どおり鉄則の配り損ねとして扱う(増やしても壊れない)。
|
|
147
|
+
link3 = FakeLink()
|
|
148
|
+
unknown = DIST_NG.replace('what=dist-source', 'what=まだ無い種類')
|
|
149
|
+
sn.run(write(tmp, 'unknown.last', unknown + '\n'), datalink=link3)
|
|
150
|
+
check_true('知らない種類でも落ちずにカードは出る', bool(link3.sent))
|
|
151
|
+
check_true('知らない種類は従来の文面へ倒れる',
|
|
152
|
+
'職人の決まりが古いままです' in link3.sent[0]['summary'])
|
|
153
|
+
|
|
154
|
+
# ── (f) 知らせに失敗しても同期は止めない ────────────────────────────────────
|
|
155
|
+
print('\n【(f) 知らせに失敗しても同期を止めない】')
|
|
156
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
157
|
+
last = write(tmp, 'ng.last', NG_LINE + '\n')
|
|
158
|
+
|
|
159
|
+
# 接続情報が無い機体。
|
|
160
|
+
check('接続情報が無い: 何もせず終わる', sn.run(last, datalink=FakeLink(cfg=None)), None)
|
|
161
|
+
# 管制が受け取らなかった。
|
|
162
|
+
check('登録が空返事: 何もせず終わる', sn.run(last, datalink=FakeLink(reg=None)), None)
|
|
163
|
+
check('登録にIDが無い: 何もせず終わる', sn.run(last, datalink=FakeLink(reg={})), None)
|
|
164
|
+
|
|
165
|
+
# ★main() は何があっても 0 を返す(update-guard.sh が止まらない)。
|
|
166
|
+
argv = sys.argv
|
|
167
|
+
sys.argv = ['skill_sync_notify.py', last]
|
|
168
|
+
orig_load, orig_reg = sn.hook_datalink.load_config, sn.hook_datalink.register
|
|
169
|
+
try:
|
|
170
|
+
sn.hook_datalink.load_config = lambda env=None: CFG
|
|
171
|
+
|
|
172
|
+
def boom(cfg, payload):
|
|
173
|
+
raise RuntimeError('通信断')
|
|
174
|
+
|
|
175
|
+
sn.hook_datalink.register = boom
|
|
176
|
+
check('通信が落ちても exit 0', sn.main(), 0)
|
|
177
|
+
finally:
|
|
178
|
+
sn.hook_datalink.load_config, sn.hook_datalink.register = orig_load, orig_reg
|
|
179
|
+
sys.argv = argv
|
|
180
|
+
check('引数が無くても exit 0', (lambda: (sys.argv.__setitem__(slice(None), ['x']), sn.main())[1])(), 0)
|
|
181
|
+
sys.argv = argv
|
|
182
|
+
|
|
183
|
+
# ── ★空振り確認 ─────────────────────────────────────────────────────────────
|
|
184
|
+
print('\n【★空振り確認】')
|
|
185
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
186
|
+
# 「status を見ずに必ず出す」ようにしたら (a) が落ちる=(a) は本当に効いている。
|
|
187
|
+
orig_parse = sn.parse_last
|
|
188
|
+
sn.parse_last = lambda p: dict(orig_parse(p), status='failed')
|
|
189
|
+
try:
|
|
190
|
+
link = FakeLink()
|
|
191
|
+
sn.run(write(tmp, 'ok.last', OK_LINE + '\n'), datalink=link)
|
|
192
|
+
check('★空振り: 判定を壊すと成功の控えでも出てしまう', len(link.sent), 1)
|
|
193
|
+
finally:
|
|
194
|
+
sn.parse_last = orig_parse
|
|
195
|
+
link = FakeLink()
|
|
196
|
+
check('★戻したあと: 成功では出ない', sn.run(write(tmp, 'ok2.last', OK_LINE + '\n'), datalink=link), None)
|
|
197
|
+
|
|
198
|
+
print('\n' + '=' * 72)
|
|
199
|
+
if failures:
|
|
200
|
+
print(f'NG {len(failures)} 件:')
|
|
201
|
+
for f in failures:
|
|
202
|
+
print(' -', f)
|
|
203
|
+
sys.exit(1)
|
|
204
|
+
print('すべて期待どおり')
|
|
205
|
+
sys.exit(0)
|