|
|
@@ -149,7 +149,7 @@ def run(ctx):
|
|
|
# list page is iconed, and the dashboard pile's goal cards carry
|
|
|
# their own accent styling.
|
|
|
home_html = http_get_page(auth, "/")
|
|
|
- check('<a href="/goals">Goals</a>' in home_html,
|
|
|
+ check('<a class="nav-goal" href="/goals">' in home_html,
|
|
|
"the nav carries the Goals entry beside Tasks")
|
|
|
check(any(p["item_type"] == "goal" for p in
|
|
|
slot_public(auth, "/", "dashboard")["pinned"]),
|
|
|
@@ -514,6 +514,130 @@ def run(ctx):
|
|
|
"consumed watermark never re-fires"),
|
|
|
"the consumed watermark never re-fires a resume")
|
|
|
|
|
|
+ print("== S6: repeated-failure signature in the resume notice ==")
|
|
|
+ # The fail-loop shape (web-db health plan A1/A2): worker tasks
|
|
|
+ # failing with IDENTICAL summary signatures since the park. The
|
|
|
+ # notice must name the cluster and forbid re-cloning; distinct
|
|
|
+ # signatures, or failures older than the park, must NOT cluster.
|
|
|
+ check(wait_for(run_park_second_passed, timeout=30,
|
|
|
+ what="S6 park second passes") is not None,
|
|
|
+ "the S6 park stamp is in a strictly older second")
|
|
|
+ shared_sig = ("curl exited 22: JSON parse error: Invalid UTF-8 middle byte"
|
|
|
+ " 0x5c — the clamped plan slice cuts a multi-byte character"
|
|
|
+ " mid-character at byte 3000 of the shared goal plan")
|
|
|
+ conn = db()
|
|
|
+ for tail in (" (clone attempt one)", " (clone attempt two)"):
|
|
|
+ conn.execute("insert into tasks (project_id, goal_id, kind, title, status,"
|
|
|
+ " priority, summary, completed_at, created_at) values"
|
|
|
+ " (1, ?, 'task', 'Flange clone', 'failed', 5, ?,"
|
|
|
+ " strftime('%s','now'), strftime('%s','now','-30 seconds'))",
|
|
|
+ (goal1, shared_sig + tail))
|
|
|
+ clone_ids = [int(r[0]) for r in conn.execute(
|
|
|
+ "select id from tasks where title = 'Flange clone'").fetchall()]
|
|
|
+ conn.commit()
|
|
|
+ conn.close()
|
|
|
+ check(len(clone_ids) == 2,
|
|
|
+ "the S6 fixture lands two same-signature failed workers")
|
|
|
+ s6_before = len(fake.goal_assignments)
|
|
|
+ check(wait_for(lambda: len(fake.goal_assignments) > s6_before or False,
|
|
|
+ timeout=60, what="S6 repeated-failure resume fires") is not None,
|
|
|
+ "the repeated failures resume the parked agent")
|
|
|
+ s6_notice = fake.goal_assignments[-1].get("resume_notice") or ""
|
|
|
+ check("REPEATED FAILURE" in s6_notice
|
|
|
+ and "do NOT recreate the same task again" in s6_notice
|
|
|
+ and "change the approach, narrow the mandate, or split the scope"
|
|
|
+ in s6_notice,
|
|
|
+ "the notice carries the repeated-failure instruction")
|
|
|
+ check(("#%d" % clone_ids[0]) in s6_notice
|
|
|
+ and ("#%d" % clone_ids[1]) in s6_notice,
|
|
|
+ "the repeated-failure section names both cluster members")
|
|
|
+ check("Invalid UTF-8 middle byte 0x5c" in s6_notice,
|
|
|
+ "the section names the shared signature's first line")
|
|
|
+ check(wait_for(lambda: task_state(auth, run_task)["status"] == "suspended"
|
|
|
+ or False, timeout=60, what="S6 agent re-parks") is not None,
|
|
|
+ "the S6 agent re-parks")
|
|
|
+ # Negative round: one pre-park failure sharing the signature (the
|
|
|
+ # watermark excludes it) plus one NEW failure with a distinct
|
|
|
+ # signature — neither forms a ≥2 cluster, so no section renders.
|
|
|
+ check(wait_for(run_park_second_passed, timeout=30,
|
|
|
+ what="S6 second park second passes") is not None,
|
|
|
+ "the S6 negative-round park stamp is strictly older")
|
|
|
+ conn = db()
|
|
|
+ conn.execute("insert into tasks (project_id, goal_id, kind, title, status,"
|
|
|
+ " priority, summary, completed_at, created_at) values"
|
|
|
+ " (1, ?, 'task', 'Stale signature clone', 'failed', 5, ?,"
|
|
|
+ " strftime('%s','now','-120 seconds'),"
|
|
|
+ " strftime('%s','now','-150 seconds'))",
|
|
|
+ (goal1, shared_sig + " (stale pre-park clone)"))
|
|
|
+ stale_id = conn.execute(
|
|
|
+ "select id from tasks where title = 'Stale signature clone'"
|
|
|
+ ).fetchone()[0]
|
|
|
+ conn.execute("insert into tasks (project_id, goal_id, kind, title, status,"
|
|
|
+ " priority, summary, completed_at, created_at) values"
|
|
|
+ " (1, ?, 'task', 'Distinct failure', 'failed', 5,"
|
|
|
+ " 'tests failed: the flange widget lacks a widget prop',"
|
|
|
+ " strftime('%s','now'), strftime('%s','now','-30 seconds'))",
|
|
|
+ (goal1,))
|
|
|
+ distinct_id = conn.execute(
|
|
|
+ "select id from tasks where title = 'Distinct failure'"
|
|
|
+ ).fetchone()[0]
|
|
|
+ conn.commit()
|
|
|
+ conn.close()
|
|
|
+ s6_more = len(fake.goal_assignments)
|
|
|
+ check(wait_for(lambda: len(fake.goal_assignments) > s6_more or False,
|
|
|
+ timeout=60, what="S6 negative-round resume fires") is not None,
|
|
|
+ "the negative-round failures resume the agent")
|
|
|
+ s6_next = fake.goal_assignments[-1].get("resume_notice") or ""
|
|
|
+ check("REPEATED FAILURE" not in s6_next,
|
|
|
+ "no repeated-failure section without a same-signature pair"
|
|
|
+ " newer than the park")
|
|
|
+ check(("#%d" % distinct_id) in s6_next and ("#%d" % stale_id) in s6_next,
|
|
|
+ "the negative-round notice still reports both failures as statuses")
|
|
|
+ check(wait_for(lambda: task_state(auth, run_task)["status"] == "suspended"
|
|
|
+ or False, timeout=60, what="S6 negative agent re-parks")
|
|
|
+ is not None,
|
|
|
+ "the S6 negative round re-parks")
|
|
|
+
|
|
|
+ print("== S5: the goal page's aggregate LLM usage stat line ==")
|
|
|
+ # usage_summary_for_goal sums the usage events across every task
|
|
|
+ # of the goal (the agent's run plus its workers); a goal with no
|
|
|
+ # usage renders no line.
|
|
|
+ conn = db()
|
|
|
+ conn.execute("insert into goals (project_id, title, brief, branch_name,"
|
|
|
+ " status, created_at) values (1, 'Usage goal', 'Sum the usage.',"
|
|
|
+ " 'goal/usage', 'active', strftime('%s','now'))")
|
|
|
+ usage_goal = conn.execute(
|
|
|
+ "select id from goals where title = 'Usage goal'").fetchone()[0]
|
|
|
+ for title, payloads in (
|
|
|
+ ("Usage worker one", ({"prompt_tokens": 1000000, "completion_tokens": 5000,
|
|
|
+ "elapsed_ms": 3600000},
|
|
|
+ {"prompt_tokens": 500000, "completion_tokens": 2500,
|
|
|
+ "elapsed_ms": 1800000})),
|
|
|
+ ("Usage worker two", ({"prompt_tokens": 2500000, "completion_tokens": 12000,
|
|
|
+ "elapsed_ms": 5400000},))):
|
|
|
+ conn.execute("insert into tasks (project_id, goal_id, kind, title, status,"
|
|
|
+ " priority, created_at) values (1, ?, 'task', ?, 'done', 5,"
|
|
|
+ " strftime('%s','now'))", (usage_goal, title))
|
|
|
+ worker = conn.execute(
|
|
|
+ "select id from tasks where title = ?", (title,)).fetchone()[0]
|
|
|
+ for payload in payloads:
|
|
|
+ conn.execute(
|
|
|
+ "insert into events (kind, actor, scope_type, scope_id, payload,"
|
|
|
+ " created_at) values ('usage', 'harness', 'task', ?, ?,"
|
|
|
+ " strftime('%s','now'))",
|
|
|
+ (worker, json.dumps(payload)))
|
|
|
+ conn.commit()
|
|
|
+ conn.close()
|
|
|
+ state = goal_state(usage_goal)
|
|
|
+ check(state.get("usage_line") == "LLM: 3 calls · 4.0M in / 19.5K out tok · 3h 0m",
|
|
|
+ "the goal state sums usage across its tasks (%r)" % state.get("usage_line"))
|
|
|
+ goal_page = http_get_page(auth, "/goals/%d" % usage_goal)
|
|
|
+ check('stm-if="goal.usage_line.length > 0"' in goal_page
|
|
|
+ and 'stm-text="goal.usage_line"' in goal_page,
|
|
|
+ "the goal page renders the usage stat line in the status head")
|
|
|
+ check(goal_state(goal_elsewhere).get("usage_line") == "",
|
|
|
+ "a goal whose tasks recorded no usage carries no stat line")
|
|
|
+
|
|
|
print("== plan 12: completion review, push state, accept ==")
|
|
|
# The daemon exposes goal-branch push state on /status; the web
|
|
|
# caches it on the goal when it probes the goal agent's saddle.
|