Skip to content

Commit 5dfe67f

Browse files
committed
test(web): stabilize multi-locale browser flows
1 parent 36897b1 commit 5dfe67f

File tree

3 files changed

+74
-53
lines changed

3 files changed

+74
-53
lines changed

web/scripts/browser-flows.sh

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,32 @@ set -euo pipefail
33

44
BASE_URL="${BASE_URL:-${1:-http://127.0.0.1:3002}}"
55
LOCALE="${LOCALE:-zh}"
6+
SESSION_NAME="${SESSION_NAME:-learn-claude-code-flows-${LOCALE}}"
67

78
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
89
source "$ROOT_DIR/scripts/browser-test-lib.sh"
910

11+
agent-browser() {
12+
command agent-browser --session-name "$SESSION_NAME" "$@"
13+
}
14+
1015
trap 'stop_static_server_if_started; agent-browser close >/dev/null 2>&1 || true' EXIT
1116

17+
locale_text() {
18+
local key="$1"
19+
case "$LOCALE:$key" in
20+
zh:deep_dive) echo '深入探索' ;;
21+
en:deep_dive) echo 'Deep Dive' ;;
22+
ja:deep_dive) echo '深掘り' ;;
23+
24+
zh:bridge_control_plane) echo '工具控制平面' ;;
25+
en:bridge_control_plane) echo 'Tool Control Plane' ;;
26+
ja:bridge_control_plane) echo 'ツール制御プレーン' ;;
27+
28+
*) echo "Unknown locale text key: ${LOCALE}:${key}" >&2; return 1 ;;
29+
esac
30+
}
31+
1232
wait_page() {
1333
agent-browser wait --load networkidle >/dev/null 2>&1 || agent-browser wait 600 >/dev/null 2>&1 || true
1434
agent-browser wait 1200 >/dev/null 2>&1 || true
@@ -171,84 +191,86 @@ flow_home_to_s01() {
171191
click_link_by_href "/${LOCALE}/s01/"
172192
wait_page
173193
assert_url_contains "/${LOCALE}/s01/"
174-
assert_body_contains 'Agent 循环'
194+
assert_body_contains 's01'
175195
assert_no_overflow
176196
assert_no_page_errors
177197
}
178198

179199
flow_home_to_timeline() {
180200
open_page "/${LOCALE}/timeline/"
181201
assert_url_contains "/${LOCALE}/timeline/"
182-
assert_body_contains '按 4 个阶段渐进搭建'
202+
assert_body_contains 's01'
203+
assert_body_contains 's19'
183204
assert_no_overflow
184205
assert_no_page_errors
185206
}
186207

187208
flow_home_to_layers() {
188209
open_page "/${LOCALE}/layers/"
189210
assert_url_contains "/${LOCALE}/layers/"
190-
assert_body_contains '阶段入口'
211+
assert_body_contains 'P1'
212+
assert_body_contains 's19'
191213
assert_no_overflow
192214
assert_no_page_errors
193215
}
194216

195217
flow_home_to_compare() {
196218
open_page "/${LOCALE}/"
197-
click_link_by_href "/${LOCALE}/compare/" '版本对比'
219+
click_link_by_href "/${LOCALE}/compare/"
198220
wait_page
199221
assert_url_contains "/${LOCALE}/compare/"
200-
assert_body_contains '学习路径对比'
222+
assert_body_contains 's14 -> s15'
201223
assert_no_overflow
202224
assert_no_page_errors
203225
}
204226

205227
flow_compare_default_state() {
206228
open_page "/${LOCALE}/compare"
207-
assert_body_contains '跃迁诊断'
208-
assert_body_contains 'Agent 循环'
209-
assert_body_contains '工具使用'
229+
assert_body_contains 's01'
230+
assert_body_contains 's02'
231+
assert_body_contains 's14 -> s15'
210232
assert_no_overflow
211233
assert_no_page_errors
212234
}
213235

214236
flow_timeline_to_stage_exit() {
215237
open_page "/${LOCALE}/timeline"
216-
click_link_exact '打开阶段收口: s06'
238+
click_link_by_href "/${LOCALE}/s06/"
217239
wait_page
218240
assert_url_contains "/${LOCALE}/s06/"
219-
assert_body_contains '上下文压缩'
241+
assert_body_contains 's06'
220242
assert_no_overflow
221243
assert_no_page_errors
222244
}
223245

224246
flow_layers_to_stage_entry() {
225247
open_page "/${LOCALE}/layers"
226-
click_link_by_href "/${LOCALE}/s15/" '阶段入口'
248+
click_link_by_href "/${LOCALE}/s15/"
227249
wait_page
228250
assert_url_contains "/${LOCALE}/s15/"
229-
assert_body_contains 'Agent 团队'
251+
assert_body_contains 's15'
230252
assert_no_overflow
231253
assert_no_page_errors
232254
}
233255

234256
flow_chapter_to_bridge_doc() {
235257
open_page "/${LOCALE}/s02"
236-
agent-browser --json find text '深入探索' click >/dev/null
258+
agent-browser --json find text "$(locale_text deep_dive)" click >/dev/null
237259
wait_page
238-
click_link_by_href "/${LOCALE}/docs/s02a-tool-control-plane/" '工具控制平面'
260+
click_link_by_href "/${LOCALE}/docs/s02a-tool-control-plane/" "$(locale_text bridge_control_plane)"
239261
wait_page
240262
assert_url_contains "/${LOCALE}/docs/s02a-tool-control-plane/"
241-
assert_body_contains '工具控制平面'
263+
assert_body_contains "$(locale_text bridge_control_plane)"
242264
assert_no_overflow
243265
assert_no_page_errors
244266
}
245267

246268
flow_bridge_doc_home_return() {
247269
open_page "/${LOCALE}/docs/s00f-code-reading-order"
248-
click_link_by_href "/${LOCALE}/" '回到学习主线'
270+
click_link_by_href "/${LOCALE}/"
249271
wait_page
250272
assert_url_contains "/${LOCALE}/"
251-
assert_body_contains '开始学习'
273+
assert_body_contains 's01'
252274
assert_no_overflow
253275
assert_no_page_errors
254276
}
@@ -258,7 +280,7 @@ flow_bridge_doc_back_to_chapter() {
258280
click_link_by_href "/${LOCALE}/s02/" 's02'
259281
wait_page
260282
assert_url_contains "/${LOCALE}/s02/"
261-
assert_body_contains '工具使用'
283+
assert_body_contains 's02'
262284
assert_no_overflow
263285
assert_no_page_errors
264286
}
@@ -281,19 +303,18 @@ flow_compare_preset() {
281303
open_page "/${LOCALE}/compare"
282304
agent-browser --json find text 's14 -> s15' click >/dev/null
283305
agent-browser wait 800 >/dev/null 2>&1 || true
284-
assert_body_contains '跃迁诊断'
285-
assert_body_contains 'Agent 团队'
286-
assert_body_contains '更稳的读法'
306+
assert_body_contains 's14'
307+
assert_body_contains 's15'
287308
assert_no_overflow
288309
assert_no_page_errors
289310
}
290311

291312
flow_chapter_next_navigation() {
292313
open_page "/${LOCALE}/s15"
293-
click_link_by_href "/${LOCALE}/s16/" '下一章'
314+
click_link_by_href "/${LOCALE}/s16/"
294315
wait_page
295316
assert_url_contains "/${LOCALE}/s16/"
296-
assert_body_contains '团队协议'
317+
assert_body_contains 's16'
297318
assert_no_overflow
298319
assert_no_page_errors
299320
}

web/src/data/generated/docs.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -870,7 +870,7 @@
870870
"title": "s12: Task System",
871871
"kind": "chapter",
872872
"filename": "s12-task-system.md",
873-
"content": "# s12: Task System\n\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > [ s12 ]`\n\n> *\"大きな目標を小タスクに分解し、順序付けし、ディスクに記録する\"* -- ファイルベースのタスクグラフ、マルチエージェント協調の基盤。\n>\n> **Harness 層**: 永続タスク -- どの会話よりも長く生きる目標。\n\n## 問題\n\ns03のTodoManagerはメモリ上のフラットなチェックリストに過ぎない: 順序なし、依存関係なし、ステータスは完了か未完了のみ。実際の目標には構造がある -- タスクBはタスクAに依存し、タスクCとDは並行実行でき、タスクEはCとDの両方を待つ。\n\n明示的な関係がなければ、エージェントは何が実行可能で、何がブロックされ、何が同時に走れるかを判断できない。しかもリストはメモリ上にしかないため、コンテキスト圧縮(s06)で消える。\n\n## 主線とどう併読するか\n\n- `s03` からそのまま来たなら、[`data-structures.md`](./data-structures.md) へ戻って `TodoItem` / `PlanState` と `TaskRecord` を分けます。\n- object 境界が混ざり始めたら、[`entity-map.md`](./entity-map.md) で message、task、runtime task、teammate を分離してから戻ります。\n- 次に `s13` を読むなら、[`s13a-runtime-task-model.md`](./s13a-runtime-task-model.md) を横に置いて、durable task と runtime task を同じ言葉で潰さないようにします。\n\n## 解決策\n\nフラットなチェックリストをディスクに永続化する**タスクグラフ**に昇格させる。各タスクは1つのJSONファイルで、ステータス・前方依存(`blockedBy`)・後方依存(`blocks`)を持つ。タスクグラフは常に3つの問いに答える:\n\n- **何が実行可能か?** -- `pending`ステータスで`blockedBy`が空のタスク。\n- **何がブロックされているか?** -- 未完了の依存を待つタスク。\n- **何が完了したか?** -- `completed`のタスク。完了時に後続タスクを自動的にアンブロックする。\n\n```\n.tasks/\n task_1.json {\"id\":1, \"status\":\"completed\"}\n task_2.json {\"id\":2, \"blockedBy\":[1], \"status\":\"pending\"}\n task_3.json {\"id\":3, \"blockedBy\":[1], \"status\":\"pending\"}\n task_4.json {\"id\":4, \"blockedBy\":[2,3], \"status\":\"pending\"}\n\nタスクグラフ (DAG):\n +----------+\n +--> | task 2 | --+\n | | pending | |\n+----------+ +----------+ +--> +----------+\n| task 1 | | task 4 |\n| completed| --> +----------+ +--> | blocked |\n+----------+ | task 3 | --+ +----------+\n | pending |\n +----------+\n\n順序: task 1 は 2 と 3 より先に完了する必要がある\n並行: task 2 と 3 は同時に実行できる\n依存: task 4 は 2 と 3 の両方を待つ\nステータス: pending -> in_progress -> completed\n```\n\nこのタスクグラフは後続の runtime / platform 章の協調バックボーンになる: バックグラウンド実行(`s13`)、マルチエージェントチーム(`s15+`)、worktree 分離(`s18`)はすべてこの durable な構造の恩恵を受ける。\n\n## 仕組み\n\n1. **TaskManager**: タスクごとに1つのJSONファイル、依存グラフ付きCRUD。\n\n```python\nclass TaskManager:\n def __init__(self, tasks_dir: Path):\n self.dir = tasks_dir\n self.dir.mkdir(exist_ok=True)\n self._next_id = self._max_id() + 1\n\n def create(self, subject, description=\"\"):\n task = {\"id\": self._next_id, \"subject\": subject,\n \"status\": \"pending\", \"blockedBy\": [],\n \"blocks\": [], \"owner\": \"\"}\n self._save(task)\n self._next_id += 1\n return json.dumps(task, indent=2)\n```\n\n2. **依存解除**: タスク完了時に、他タスクの`blockedBy`リストから完了IDを除去し、後続タスクをアンブロックする。\n\n```python\ndef _clear_dependency(self, completed_id):\n for f in self.dir.glob(\"task_*.json\"):\n task = json.loads(f.read_text())\n if completed_id in task.get(\"blockedBy\", []):\n task[\"blockedBy\"].remove(completed_id)\n self._save(task)\n```\n\n3. **ステータス遷移 + 依存配線**: `update`がステータス変更と依存エッジを担う。\n\n```python\ndef update(self, task_id, status=None,\n add_blocked_by=None, add_blocks=None):\n task = self._load(task_id)\n if status:\n task[\"status\"] = status\n if status == \"completed\":\n self._clear_dependency(task_id)\n self._save(task)\n```\n\n4. 4つのタスクツールをディスパッチマップに追加する。\n\n```python\nTOOL_HANDLERS = {\n # ...base tools...\n \"task_create\": lambda **kw: TASKS.create(kw[\"subject\"]),\n \"task_update\": lambda **kw: TASKS.update(kw[\"task_id\"], kw.get(\"status\")),\n \"task_list\": lambda **kw: TASKS.list_all(),\n \"task_get\": lambda **kw: TASKS.get(kw[\"task_id\"]),\n}\n```\n\n`s12` 以降、タスクグラフが durable なマルチステップ作業のデフォルトになる。`s03` の Todo は軽量な単一セッション用チェックリストとして残る。\n\n## s06からの変更点\n\n| コンポーネント | Before (s06) | After (s12) |\n|---|---|---|\n| Tools | 5 | 8 (`task_create/update/list/get`) |\n| 計画モデル | フラットチェックリスト (メモリ) | 依存関係付きタスクグラフ (ディスク) |\n| 関係 | なし | `blockedBy` + `blocks` エッジ |\n| ステータス追跡 | 完了か未完了 | `pending` -> `in_progress` -> `completed` |\n| 永続性 | 圧縮で消失 | 圧縮・再起動後も存続 |\n\n## 試してみる\n\n```sh\ncd learn-claude-code\npython agents/s12_task_system.py\n```\n\n1. `Create 3 tasks: \"Setup project\", \"Write code\", \"Write tests\". Make them depend on each other in order.`\n2. `List all tasks and show the dependency graph`\n3. `Complete task 1 and then list tasks to see task 2 unblocked`\n4. `Create a task board for refactoring: parse -> transform -> emit -> test, where transform and emit can run in parallel after parse`\n\n## 教学上の境界\n\nこのリポジトリで本当に重要なのは、完全な製品向け保存層の再現ではありません。\n\n重要なのは:\n\n- durable なタスク記録\n- 明示的な依存エッジ\n- 分かりやすい状態遷移\n- 後続章が再利用できる構造\n\nこの 4 点を自分で実装できれば、タスクシステムの核心はつかめています。\n"
873+
"content": "# s12: Task System\n\n`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > [ s12 ]`\n\n> *\"大きな目標を小タスクに分解し、順序付けし、ディスクに記録する\"* -- ファイルベースのタスクグラフ、マルチエージェント協調の基盤。\n>\n> **Harness 層**: 永続タスク -- どの会話よりも長く生きる目標。\n\n## 問題\n\ns03のTodoManagerはメモリ上のフラットなチェックリストに過ぎない: 順序なし、依存関係なし、ステータスは完了か未完了のみ。実際の目標には構造がある -- タスクBはタスクAに依存し、タスクCとDは並行実行でき、タスクEはCとDの両方を待つ。\n\n明示的な関係がなければ、エージェントは何が実行可能で、何がブロックされ、何が同時に走れるかを判断できない。しかもリストはメモリ上にしかないため、コンテキスト圧縮(s06)で消える。\n\n## 主線とどう併読するか\n\n- `s03` からそのまま来たなら、[`data-structures.md`](./data-structures.md) へ戻って `TodoItem` / `PlanState` と `TaskRecord` を分けます。\n- object 境界が混ざり始めたら、[`entity-map.md`](./entity-map.md) で message、task、runtime task、teammate を分離してから戻ります。\n- 次に `s13` を読むなら、[`s13a-runtime-task-model.md`](./s13a-runtime-task-model.md) を横に置いて、durable task と runtime task を同じ言葉で潰さないようにします。\n\n## 解決策\n\nフラットなチェックリストをディスクに永続化する**タスクグラフ**に昇格させる。各タスクは1つのJSONファイルで、ステータス・前方依存(`blockedBy`)を持つ。タスクグラフは常に3つの問いに答える:\n\n- **何が実行可能か?** -- `pending`ステータスで`blockedBy`が空のタスク。\n- **何がブロックされているか?** -- 未完了の依存を待つタスク。\n- **何が完了したか?** -- `completed`のタスク。完了時に後続タスクを自動的にアンブロックする。\n\n```\n.tasks/\n task_1.json {\"id\":1, \"status\":\"completed\"}\n task_2.json {\"id\":2, \"blockedBy\":[1], \"status\":\"pending\"}\n task_3.json {\"id\":3, \"blockedBy\":[1], \"status\":\"pending\"}\n task_4.json {\"id\":4, \"blockedBy\":[2,3], \"status\":\"pending\"}\n\nタスクグラフ (DAG):\n +----------+\n +--> | task 2 | --+\n | | pending | |\n+----------+ +----------+ +--> +----------+\n| task 1 | | task 4 |\n| completed| --> +----------+ +--> | blocked |\n+----------+ | task 3 | --+ +----------+\n | pending |\n +----------+\n\n順序: task 1 は 2 と 3 より先に完了する必要がある\n並行: task 2 と 3 は同時に実行できる\n依存: task 4 は 2 と 3 の両方を待つ\nステータス: pending -> in_progress -> completed\n```\n\nこのタスクグラフは後続の runtime / platform 章の協調バックボーンになる: バックグラウンド実行(`s13`)、マルチエージェントチーム(`s15+`)、worktree 分離(`s18`)はすべてこの durable な構造の恩恵を受ける。\n\n## 仕組み\n\n1. **TaskManager**: タスクごとに1つのJSONファイル、依存グラフ付きCRUD。\n\n```python\nclass TaskManager:\n def __init__(self, tasks_dir: Path):\n self.dir = tasks_dir\n self.dir.mkdir(exist_ok=True)\n self._next_id = self._max_id() + 1\n\n def create(self, subject, description=\"\"):\n task = {\"id\": self._next_id, \"subject\": subject,\n \"status\": \"pending\", \"blockedBy\": [],\n \"owner\": \"\"}\n self._save(task)\n self._next_id += 1\n return json.dumps(task, indent=2)\n```\n\n2. **依存解除**: タスク完了時に、他タスクの`blockedBy`リストから完了IDを除去し、後続タスクをアンブロックする。\n\n```python\ndef _clear_dependency(self, completed_id):\n for f in self.dir.glob(\"task_*.json\"):\n task = json.loads(f.read_text())\n if completed_id in task.get(\"blockedBy\", []):\n task[\"blockedBy\"].remove(completed_id)\n self._save(task)\n```\n\n3. **ステータス遷移 + 依存配線**: `update`がステータス変更と依存エッジを担う。\n\n```python\ndef update(self, task_id, status=None,\n add_blocked_by=None, remove_blocked_by=None):\n task = self._load(task_id)\n if status:\n task[\"status\"] = status\n if status == \"completed\":\n self._clear_dependency(task_id)\n if add_blocked_by:\n task[\"blockedBy\"] = list(set(task[\"blockedBy\"] + add_blocked_by))\n if remove_blocked_by:\n task[\"blockedBy\"] = [x for x in task[\"blockedBy\"] if x not in remove_blocked_by]\n self._save(task)\n```\n\n4. 4つのタスクツールをディスパッチマップに追加する。\n\n```python\nTOOL_HANDLERS = {\n # ...base tools...\n \"task_create\": lambda **kw: TASKS.create(kw[\"subject\"]),\n \"task_update\": lambda **kw: TASKS.update(kw[\"task_id\"], kw.get(\"status\")),\n \"task_list\": lambda **kw: TASKS.list_all(),\n \"task_get\": lambda **kw: TASKS.get(kw[\"task_id\"]),\n}\n```\n\n`s12` 以降、タスクグラフが durable なマルチステップ作業のデフォルトになる。`s03` の Todo は軽量な単一セッション用チェックリストとして残る。\n\n## s06からの変更点\n\n| コンポーネント | Before (s06) | After (s12) |\n|---|---|---|\n| Tools | 5 | 8 (`task_create/update/list/get`) |\n| 計画モデル | フラットチェックリスト (メモリ) | 依存関係付きタスクグラフ (ディスク) |\n| 関係 | なし | `blockedBy` エッジ |\n| ステータス追跡 | 完了か未完了 | `pending` -> `in_progress` -> `completed` |\n| 永続性 | 圧縮で消失 | 圧縮・再起動後も存続 |\n\n## 試してみる\n\n```sh\ncd learn-claude-code\npython agents/s12_task_system.py\n```\n\n1. `Create 3 tasks: \"Setup project\", \"Write code\", \"Write tests\". Make them depend on each other in order.`\n2. `List all tasks and show the dependency graph`\n3. `Complete task 1 and then list tasks to see task 2 unblocked`\n4. `Create a task board for refactoring: parse -> transform -> emit -> test, where transform and emit can run in parallel after parse`\n\n## 教学上の境界\n\nこのリポジトリで本当に重要なのは、完全な製品向け保存層の再現ではありません。\n\n重要なのは:\n\n- durable なタスク記録\n- 明示的な依存エッジ\n- 分かりやすい状態遷移\n- 後続章が再利用できる構造\n\nこの 4 点を自分で実装できれば、タスクシステムの核心はつかめています。\n"
874874
},
875875
{
876876
"version": "s13",

0 commit comments

Comments
 (0)