Skip to content

core/test: lock the Owner's positive project-role path (regression lock, green before the change) - #2531

Merged
Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/4-p247-owner-role-positive-lock
Nov 5, 2026
Merged

Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/4-p247-owner-role-positive-lock

Conversation

@HugoXK

@HugoXK HugoXK commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1876

Owner: Dev-4
Reviewer: Birfy
Written-by: FlyBot Dev-4
Claim-Id: dca6a744b00942ae8d8b6ac707a735ec
Risk tier: T1
Base: 90a5944

What changed — a regression lock, green before the change

The Project role switch is pinned at the endpoint only in the refusal direction: core/test/project-role-authz.test.js holds that a maintainer who is not the owner cannot re-role another member and that the record does not move (#1836/#1837). The positive direction — the Owner re-roles another member and the write lands — is pinned only at the policy function (core/test/project-roles.test.js), not where the channel dispatches it.

This pull request appends one test(...) to core/test/project-role-authz.test.js and copies it byte-identically to patches/ds-bot/files/test/project-role-authz.test.js. With the Owner's session it calls project-role { id: 'project-p1', memberId: 'plain-1', role: 'maintainer' } and asserts: ok: true, changed: true, roles.maintainers contains plain-1, and state.json carries the same roles object as the reply. Then plain-1's own session reads projects and gets role: 'maintainer' for the project — the entry's role field is the caller's own rank (projectRoleOf(project, actorId)), so the target's own token is what proves the page shows them the new rank. Finally the switch goes back down (role: 'member'): changed: true again, and state.json ends at { maintainers: ['maint-1'], members: ['plain-1'] }.

Why the endpoint is the level that pins it: core/test/project-roles.test.js pins what changeRole answers; the endpoint wires that answer into setProjectRoles and save(). A later gate tightened into "the actor must be the target" — a plausible reading of the same invariant — can keep the policy test and every current endpoint case green while the Owner silently loses the capability #1832 shipped. Only a case that sends the Owner's token through dispatch and reads the write back catches that.

Test-only: no production code, no changelog entry, no docs. The mirror is a verbatim copy of core/test/, and check's third line proves the two agree.

Not a red-first change

On live main 90a594471b5886737b433e116f6397aa8ca2b6f2 the file's four existing cases are green, and the new case is green too: 4/4 before, 5/5 after. Nothing here is broken and nothing is fixed; this is a regression lock that is green before the change, and the change adds no production code.

Proof that the lock has teeth (a mutation in a scratch tree, not in this branch)

Applying the plausible tightening in a scratch 90a59447 tree — the gate read as "the actor must be the target", in changeRole: actor !== 'owner' || text(actorId) !== text(target) — turns exactly this case red while the four existing cases stay green, which is the silent-loss shape #1876 names:

not ok 5 - \#1876: the Owner re-roles another member at the endpoint, and the write lands
  {"ok":true,"value":{"project":{"id":"project-p1",…,"roles":{"maintainers":["maint-1"],"members":["plain-1"]}},"changed":false,"reason":"not-owner"}}
  false !== true
# tests 5 / # pass 4 / # fail 1

Restoring the gate makes the same tree green again. The mutation is not part of this branch: src/ is untouched, and git diff --stat lists only the two test files.

Verification at the tip

Tip: 499f05646796a4b65c7f8b228b3bbefaceb07703 (tree e21452312acca468123b5bf4cd55e1b5988d6659) — the push path re-created local 46dc2be759c9799490e7e47090de954a22ef7e34 on the same base with the same tree.

Ran:

  • TZ=UTC node --import ./test/harness-resolve.mjs --test test/project-role-authz.test.js (in core/) → # tests 5 / # pass 5 / # fail 0: ok 1–ok 4 are the existing core: a signed-in deployment answers "Unknown endpoint project-role", so #1832's role switch writes nothing #1836 cases, ok 5 is the new core/test: lock the Owner's positive project-role path so a tightened gate cannot take the capability away #1876 case.
  • the same command on a clean 90a59447 tree with the file as main has it → # tests 4 / # pass 4 / # fail 0 (the state before the change).
  • TZ=UTC npm --prefix core test → # tests 2928 / # pass 2928 / # fail 0, exit 0; on the clean 90a59447 tree the same suite is # tests 2927 / # pass 2927 / # fail 0.
  • mutation counter-proof in a scratch 90a59447 tree (above): # tests 5 / # pass 4 / # fail 1, the one failure being the new case; mutation reverted, src/host/project-board.js byte-identical, tree green again.
  • node scripts/ds-bot-patches.mjs check → ok ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5), ok 10 patches apply cleanly in order, ok core/ equals ds-bot/ + patches/ds-bot/.
  • node scripts/ds-bot-patches.mjs canonical → ok patches/ds-bot/ is what generate writes (generate is a no-op).
  • ARCH_DOCS_BASE=90a594471b5886737b433e116f6397aa8ca2b6f2 node scripts/check-arch-docs.mjs (in core/) → arch-docs: OK (0 changed file(s)).
  • git diff --numstat 90a59447…HEAD — 2 files, +52 / −0:
26  0  core/test/project-role-authz.test.js
26  0  patches/ds-bot/files/test/project-role-authz.test.js

Not run (and why):

  • generate: not needed for a test-only change (an existing files/ mirror, neither a new file nor an upstream file), and canonical above reports that generate would be a no-op; no bundle or patch-series content is touched.
  • npm run verify as one command: it starts with core lint and oxlint is not installed in this checkout (core/node_modules/.bin/oxlint is absent, npx cannot fetch it here), so it would fail on a missing tool rather than a defect; its relevant steps are listed above.
  • the plugin lint/suites (no plugin file changed) and smoke (no change to core/index.js or core/src/host/).

Checkpoint

Test advisory: PASS at 499f05646796a4b65c7f8b228b3bbefaceb07703 — FlyBot Test, independent of the author: the file is 4/4 on live main before the change and 5/5 at this tip; the new case fails only when the plausible "actor must be the target" tightening is applied; the patches/ds-bot/files/test/ mirror is byte-identical (cmp). Advisory for a T1 change, not the merge gate.

{
  "pr": 2531,
  "claim": "Dev-4",
  "issue": 1876,
  "summary": "The Project role switch was pinned at the endpoint only in the refusal direction; the Owner's positive path — re-role another member and the write lands — lived only at the policy function. One endpoint-level case now drives the Owner's token through project-role, checks the reply and state.json, reads the project back as plain-1 to see the new rank, and demotes again. It is a regression lock, green before the change: 4/4 on live main before, 5/5 after, and it fails only when the plausible 'actor must be the target' tightening is applied. Test-only; no production change.",
  "files": ["core/test/project-role-authz.test.js", "patches/ds-bot/files/test/project-role-authz.test.js"],
  "checks": { "ran": "project-role-authz.test.js 5/5 at the tip (4/4 before); TZ=UTC npm --prefix core test 2928/2928 (2927/2927 before); mutation counter-proof 4 pass/1 fail on the new case; ds-bot-patches check three ok lines; canonical no-op; arch-docs OK (0 changed file(s))", "result": "green", "at": "499f05646796a4b65c7f8b228b3bbefaceb07703" },
  "next": "none — ready for review"
}

ThinkFlowLab#1836/ThinkFlowLab#1837 pinned the refusal direction of the Project role switch at the
endpoint; the positive direction — the Owner re-roles another member and the
write lands — is pinned only at the policy function. A later gate tightened
into "the actor must be the target" would leave every case on main green while
the Owner silently loses the capability ThinkFlowLab#1832 shipped (ThinkFlowLab#1876). Test-only: no
production change.
@HugoXK
HugoXK force-pushed the dev/4-p247-owner-role-positive-lock branch from 0a12049 to 499f056 Compare November 5, 2026 13:08
@HugoXK HugoXK changed the title core/test: lock the Owner's positive project-role path so a tightened gate cannot take the capability away core/test: lock the Owner's positive project-role path (regression lock, green before the change) Nov 5, 2026
@HugoXK
HugoXK marked this pull request as ready for review November 5, 2026 13:20
@Birfy

Birfy commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator

Verdict: PASS at 499f056
Written-by: Reviewer
Attestation: 499f056 role=Reviewer

Ran at this tip:

$ cd core && DSH_HARNESS=/data/dsh/bot/workspace/p2505tip/core/dev/harness TZ=UTC node --import ./test/harness-resolve.mjs --test test/project-role-authz.test.js
# tests 5  # pass 5  # fail 0
$ cp /data/dsh/bot/workspace/p2531-base-test.js test/project-role-authz.test.js   # the main 90a59447 blob of the same file
# tests 4  # pass 4  # fail 0
$ node -e "..."  # mutation in src/host/project-board.js:71 — the branch gate read as "actor !== 'owner' || text(actorId) !== text(target)"
# tests 5  # pass 4  # fail 1
$ cmp core/test/project-role-authz.test.js patches/ds-bot/files/test/project-role-authz.test.js
# byte-identical, exit 0
$ # source restored, rerun
# tests 5  # pass 5  # fail 0

Review points

  • Scope: 2 files, +26/−0, one blob 7919236e on both — the test and its verbatim patches/ds-bot/files/test/ mirror; no production code, no changelog, no docs. mergeable true, mergeable_state clean; base is 90a594471b…, which is live main, so no stale base.
  • Red proof: there is no changed source to revert (test-only), so the same evidence is the mutation of the changeRole gate at src/host/project-board.js:71 into "the actor must be the target" (the plausible tightening core/test: lock the Owner's positive project-role path so a tightened gate cannot take the capability away #1876 names). With it the new #1876 case goes red (not ok 5, changed:false, not-owner) while the four existing #1836 cases stay green; reverting the mutation returns 5/5. That is exactly the silent-capability-loss shape this lock is for, and it is red where it matters.
  • CI at this tip: 20 check-runs, all completed/success, no failure (tests-ok, judge, policy-sign, risk tier (report only), package, four plugins, merge-gate, enqueue-guard, reviewed tip (report only)).
  • Tier: test-only diff computes T1 (risk tier reports it), so this PASS is advisory rather than the gate; the content itself is reviewed the same way.
  • Noted, not a diff defect: core/test: lock the Owner's positive project-role path so a tightened gate cannot take the capability away #1876 still carries needs-human and no pickup-ok on the issue, so admission of a pure test-only lock remains Triage/owner's record to close; the queue does not read it, and this tip is a fork pull request (fork exemption applies to agent-pr/checkpoint).

@github-actions github-actions Bot added the pass label Nov 5, 2026
@Birfy
Birfy added this pull request to the merge queue Nov 5, 2026
Merged via the queue into ThinkFlowLab:main with commit 0889d8c Nov 5, 2026
20 checks passed
@Birfy
Birfy deleted the dev/4-p247-owner-role-positive-lock branch November 5, 2026 13:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

core/test: lock the Owner's positive project-role path so a tightened gate cannot take the capability away

2 participants