diff --git a/agents/rocko/work/queue-79/mutants.sh b/agents/rocko/work/queue-79/mutants.sh new file mode 100755 index 00000000..0d925699 --- /dev/null +++ b/agents/rocko/work/queue-79/mutants.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Row 79 (#1573): rerun the untyped-list mutants against a tree. Usage: +# mutants.sh . Each mutant edits untypedActions in +# packages/harness/src/tools.mjs, runs the harness tools test, and restores +# the file from a saved copy, so the tree need not be committed. Prints +# "killed" or "SURVIVED" per mutant; exit 1 if any survives, a sed doesn't +# change the file, or the unmutated run fails. untyped-within-only stands in +# for Dewey's mutant of that name (row 73 comment 27506). +set -euo pipefail +TREE="$(cd "${1:?usage: mutants.sh }" && pwd)" +cd "$TREE" +SRC=packages/harness/src/tools.mjs +TESTS=(packages/harness/tests/tools.test.mjs) +run_tests() { node --test "${TESTS[@]}" >"$1" 2>&1; } + +# name | sed expression +MUTANTS=( + 'untyped-within-only|s/^ return \[\.\.\.resolved\.limits\.authority\.withinRole, \.\.\.resolved\.limits\.authority\.crossRole\]\.filter/ return [...resolved.limits.authority.withinRole].filter/' + 'untyped-cross-first|s/^ return \[\.\.\.resolved\.limits\.authority\.withinRole, \.\.\.resolved\.limits\.authority\.crossRole\]\.filter/ return [...resolved.limits.authority.crossRole, ...resolved.limits.authority.withinRole].filter/' + 'untyped-keeps-typed|s/\.filter((a) => !typed\.has(a));$/.filter((a) => a === "message.send" || !typed.has(a));/' +) + +OUT="${OUT:-$TREE/.mutants}" +mkdir -p "$OUT" +run_tests "$OUT/baseline.log" || { echo "baseline run failed: $OUT/baseline.log" >&2; exit 1; } +echo "baseline: pass" +status=0 +for m in "${MUTANTS[@]}"; do + IFS='|' read -r name expr <<<"$m" + cp -p "$SRC" "$OUT/orig" + sed -i -e "$expr" "$SRC" + if cmp -s "$OUT/orig" "$SRC"; then + echo "$name: NOT APPLIED (sed matched nothing)"; status=1; continue + fi + diff -u "$OUT/orig" "$SRC" >"$OUT/$name.diff" || true + if run_tests "$OUT/$name.log"; then + echo "$name: SURVIVED"; status=1 + else + echo "$name: killed ($(grep -E '^✖' "$OUT/$name.log" | grep -v 'failing tests' | sed 's/ ([0-9.]*ms)$//' | sort -u | tr '\n' ';'))" + fi + cp -p "$OUT/orig" "$SRC" + cmp -s "$OUT/orig" "$SRC" || { echo "restore failed: $SRC" >&2; exit 1; } + rm -f "$OUT/orig" +done +exit "$status" diff --git a/packages/business/README.md b/packages/business/README.md index 9c3e8d1f..ca372c5e 100644 --- a/packages/business/README.md +++ b/packages/business/README.md @@ -34,7 +34,7 @@ in this repository, or `$MOSAIC_ROLES_DIR` if set. "network": "api-only", "authority": { "withinRole": ["task.update.assigned", "git.push.working", "review.request", "message.send"], - "crossRole": ["task.reassign", "task.scope.change"] + "crossRole": ["task.scope.change"] }, "credentials": [ { "service": "gitea", "scopes": ["write:issue", "write:repository", "read:user"] }, diff --git a/packages/harness/tests/tools.test.mjs b/packages/harness/tests/tools.test.mjs index 31606131..3fe7dd1e 100644 --- a/packages/harness/tests/tools.test.mjs +++ b/packages/harness/tests/tools.test.mjs @@ -30,9 +30,7 @@ test("a coder gets no launch, no resolve_decision, and no task tools without a t const tools = typedTools({ resolved: coder }); assert.deepEqual(names(tools), ["send_message", "raise_decision", "list_agents", "inbox", "trail"]); // Held actions without a tool are listed, so the manifest shows them. - const untyped = untypedActions(coder, tools); - assert.ok(untyped.includes("git.push.working")); - assert.ok(!untyped.includes("message.send")); + assert.deepEqual(untypedActions(coder, tools), ["task.update.assigned", "git.push.working", "review.request", "task.scope.change"]); }); test("a coder with a tracker gets its own task tools and no task_reassign", (t) => { diff --git a/packages/tasks/README.md b/packages/tasks/README.md index 61b36664..d47f5de6 100644 --- a/packages/tasks/README.md +++ b/packages/tasks/README.md @@ -190,10 +190,10 @@ Details that aren't obvious from the table: - `task.close` moves the task into the done bucket. `verdict` is a citation of the review verdict in the queue. The adapter records it and doesn't check it. Checking it against the queue is a follow-up. -- A coder role's `crossRole` grant of `task.reassign` can't be used. The - writer check refuses it before authority is looked at. If a coder - should hand work on, that's a change to the field table, not to the - role file. +- The coder role holds no `task.reassign` grant (row 73). The writer + check refuses a non-pm `task.reassign` before authority is looked at + anyway. If a coder should hand work on, that's a change to the field + table, not to the role file. Verbs for one business run one at a time, in the same queue as the poll, so a poll never reads between two writes of a verb. diff --git a/packages/tasks/live/run.mjs b/packages/tasks/live/run.mjs index eed1513e..0c8d7e97 100644 --- a/packages/tasks/live/run.mjs +++ b/packages/tasks/live/run.mjs @@ -29,7 +29,7 @@ const AUTHORITY = { withinRole: ['task.create', 'task.assign', 'task.reassign', 'task.schedule', 'task.update.assigned', 'task.close', 'message.send'], crossRole: ['task.priority.change', 'task.scope.change'], }, - coder: { withinRole: ['task.update.assigned', 'message.send'], crossRole: ['task.reassign', 'task.scope.change'] }, + coder: { withinRole: ['task.update.assigned', 'git.push.working', 'review.request', 'message.send'], crossRole: ['task.scope.change'] }, reviewer: { withinRole: ['task.update.assigned', 'message.send'], crossRole: [] }, }; const ref = (x) => (x.file ? { file: x.file, expires: x.expires } : { env: x.env, expires: x.expires });