Files
compound-engineering-plugin…/tests/ce-babysit-pr-snapshot.test.ts

5113 lines
243 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, test, beforeEach, setDefaultTimeout } from "bun:test"
import { spawn, spawnSync } from "node:child_process"
import { chmodSync, existsSync, mkdtempSync, writeFileSync, readFileSync, renameSync } from "node:fs"
import { tmpdir } from "node:os"
import path from "node:path"
setDefaultTimeout(30_000)
// Regression tests for the ce-babysit-pr pr-snapshot claim->act->confirm engine.
// Exercised via --fetch-file (no live PR), following the tests/*-validator.test.ts
// spawnSync pattern. Locks in the ce-code-review fixes: crash-safety, needs-human
// silencing + open_needs_human visibility, checks_terminal, key-collision, null-head.
const SCRIPT = path.join(import.meta.dir, "..", "skills", "ce-babysit-pr", "scripts", "pr-snapshot")
const ORDINARY_TEST_BUDGET_SECONDS = "28800"
const EXPIRING_TEST_INVOCATION = ["--start-invocation", "--invocation-budget-seconds", "1"]
function fetchFile(dir: string, name: string, obj: unknown): string {
const p = path.join(dir, name)
const fixture = obj && typeof obj === "object" && !Array.isArray(obj)
? { ...(obj as Record<string, unknown>) }
: obj
if (fixture && typeof fixture === "object" && Array.isArray(fixture.threads)) {
fixture.threads = fixture.threads.map((thread: any) => ({
...thread,
url: thread.url ?? `https://example.test/thread/${thread.thread_id}`,
}))
}
writeFileSync(p, JSON.stringify(fixture))
return p
}
function residualFile(dir: string, name: string, sourceId: string, kind: "thread" | "comment" | "review" | "check" | "currency"): { path: string; value: any } {
const value = {
type: "needs-human",
sources: [{ id: sourceId, kind }],
decision_context: {
quoted_feedback: "Choose the intended behavior.",
investigation: "Inspected the affected path.",
decision_reason: "Both behaviors are plausible.",
options: [{ option: "Keep", tradeoff: "Preserves compatibility." }],
recommendation: null,
},
thread_urls: kind === "thread" ? [`https://example.test/thread/${sourceId}`] : [],
}
return { path: fetchFile(dir, name, value), value }
}
function persistedInvocationArgs(stateDir: string): string[] {
if (!existsSync(path.join(stateDir, "state.json"))) return []
const state = JSON.parse(readFileSync(path.join(stateDir, "state.json"), "utf8"))
if (!state.invocation_id || !state.started_at || !state.invocation_budget_seconds) return []
return ["--invocation-id", state.invocation_id, "--session-started-at", state.started_at,
"--invocation-budget-seconds", String(state.invocation_budget_seconds)]
}
function snapshot(stateDir: string, fetch: string, extra: string[] = []): any {
const hasInvocationMode = extra.includes("--start-invocation")
|| extra.includes("--reset-session")
|| extra.includes("--continue-invocation")
|| extra.includes("--invocation-id")
const persistedArgs = !hasInvocationMode ? persistedInvocationArgs(stateDir) : []
const startsInvocation = (!hasInvocationMode && persistedArgs.length === 0)
|| extra.includes("--start-invocation")
|| extra.includes("--reset-session")
const budgetArgs = startsInvocation && !extra.includes("--invocation-budget-seconds")
? ["--invocation-budget-seconds", ORDINARY_TEST_BUDGET_SECONDS]
: []
const r = spawnSync(
"python3",
[SCRIPT, "snapshot", "--pr", "1", "--repo", "o/r", "--state-dir", stateDir, "--fetch-file", fetch,
...(hasInvocationMode ? [] : persistedArgs.length > 0 ? persistedArgs : ["--start-invocation"]),
...budgetArgs, ...extra],
{ encoding: "utf8" },
)
expect(r.status, r.stderr).toBe(0)
return JSON.parse(r.stdout)
}
function currentInvocationArgs(stateDir: string, fetch: string): string[] {
const persistedArgs = persistedInvocationArgs(stateDir)
if (persistedArgs.length > 0) return persistedArgs
const started = snapshot(stateDir, fetch, ["--start-invocation",
"--invocation-budget-seconds", ORDINARY_TEST_BUDGET_SECONDS])
return ["--invocation-id", started.invocation_id, "--session-started-at", started.invocation_started_at,
"--invocation-budget-seconds", String(started.invocation_budget_seconds)]
}
function mark(stateDir: string, args: string[]): void {
// Model a successful at-mark refetch from the last observed state unless the test supplies an
// explicit post-action fetch. Decision marks fail closed when any covered thread is absent.
const persisted = existsSync(path.join(stateDir, "state.json"))
? JSON.parse(readFileSync(path.join(stateDir, "state.json"), "utf8"))
: {}
const extra = args.includes("--fetch-file")
? []
: ["--fetch-file", fetchFile(path.dirname(stateDir), "mark-current.json", {
threads: Object.values(persisted.threads ?? {}),
})]
const needsResidual = args.includes("--disposition")
&& args[args.indexOf("--disposition") + 1] === "needs-human"
&& (args.includes("--thread") || args.includes("--comment") || args.includes("--check"))
&& !args.includes("--residual-file")
const sourceId = args.includes("--thread") ? args[args.indexOf("--thread") + 1]!
: args.includes("--comment") ? args[args.indexOf("--comment") + 1]!
: args.includes("--check") ? args[args.indexOf("--check") + 1]!
: ""
const kind = args.includes("--thread") ? "thread"
: args.includes("--check") ? "check"
: sourceId.startsWith("PRR_") ? "review" : "comment"
const residualArgs = needsResidual
? ["--residual-file", residualFile(path.dirname(stateDir), `residual-${sourceId}.json`, sourceId, kind).path]
: []
const r = spawnSync("python3", [SCRIPT, "mark", "--state-dir", stateDir,
...persistedInvocationArgs(stateDir), ...args, ...residualArgs, ...extra], { encoding: "utf8" })
expect(r.status, r.stderr).toBe(0)
}
function markCurrency(stateDir: string, key: string, disposition: string, fingerprint?: string): void {
if (disposition === "needs-human") {
const args = ["--disposition", "needs-human", "--residual-file",
residualFile(path.dirname(stateDir), `currency-${key}.json`, key, "currency").path]
if (fingerprint) args.push("--semantic-conflict-fingerprint", fingerprint)
mark(stateDir, args)
return
}
const args = ["--currency-key", key, "--currency-disposition", disposition]
mark(stateDir, args)
}
function markCurrencyOutcome(stateDir: string, key: string, outcome: string): void {
const args = ["--currency-key", key, "--currency-outcome", outcome]
const item = JSON.parse(readFileSync(path.join(stateDir, "state.json"), "utf8"))
.branch_currency_state?.items?.[key]
if (outcome === "ambiguous"
|| (outcome === "proven-no-mutation" && Number(item?.retry_count ?? 0) >= 1)) {
args.push("--residual-file", residualFile(path.dirname(stateDir), `currency-outcome-${key}.json`, key, "currency").path)
}
mark(stateDir, args)
}
function markCurrencyInspection(stateDir: string, key: string, fingerprint: string): void {
const state = JSON.parse(readFileSync(path.join(stateDir, "state.json"), "utf8"))
const args = ["--currency-key", key, "--currency-inspected-fingerprint", fingerprint]
if (state.branch_currency_state?.semantic_parks?.[fingerprint]) {
args.push("--residual-file", residualFile(
path.dirname(stateDir), `currency-inspection-${key}.json`, key, "currency").path)
}
mark(stateDir, args)
}
function markDecisionAnswered(stateDir: string, decisionId?: string): void {
const current = JSON.parse(readFileSync(path.join(stateDir, "state.json"), "utf8"))
.human_decisions?.[0]?.id
const answerPath = path.join(path.dirname(stateDir), "human-answer.md")
writeFileSync(answerPath, "Option 2: regenerate the fixture from current source.\n")
mark(stateDir, ["--answer-decision", decisionId ?? current, "--answer-file", answerPath])
}
function expectCurrentDecision(actual: any[], residual: any): void {
expect(actual).toHaveLength(1)
expect(actual[0]).toEqual(residual)
}
function watch(stateDir: string, fetch: string, extra: string[] = []): any {
const invocationArgs = extra.includes("--invocation-id") ? [] : currentInvocationArgs(stateDir, fetch)
const r = spawnSync(
"python3",
[SCRIPT, "watch", "--pr", "1", "--repo", "o/r", "--state-dir", stateDir, "--fetch-file", fetch,
"--interval", "0.1", ...invocationArgs, ...extra],
{ encoding: "utf8", timeout: 5000 },
)
expect(r.status, r.stderr).toBe(0)
return JSON.parse(r.stdout.trim().split("\n").pop()!) // the wake sentinel is the final line
}
function startWatch(stateDir: string, fetch: string, extra: string[] = []) {
const invocationArgs = extra.includes("--invocation-id") ? [] : currentInvocationArgs(stateDir, fetch)
const child = spawn(
"python3",
[SCRIPT, "watch", "--pr", "1", "--repo", "o/r", "--state-dir", stateDir, "--fetch-file", fetch,
"--interval", "0.05", ...invocationArgs, ...extra],
{ stdio: ["ignore", "pipe", "pipe"] },
)
let stdout = ""
let stderr = ""
child.stdout.on("data", (chunk) => { stdout += chunk })
child.stderr.on("data", (chunk) => { stderr += chunk })
const result = new Promise<{ code: number | null; stdout: string; stderr: string }>((resolve) => {
child.on("close", (code) => resolve({ code, stdout, stderr }))
})
return { child, result }
}
async function waitForWatchGeneration(stateDir: string, previous: string | null = null): Promise<string> {
const deadline = Date.now() + 5000
while (Date.now() < deadline) {
try {
const generation = JSON.parse(readFileSync(path.join(stateDir, "state.json"), "utf8")).watch_generation
if (typeof generation === "string" && generation !== previous) return generation
} catch {
// The first watcher may still be creating state.json.
}
await Bun.sleep(20)
}
throw new Error(`watch generation did not advance from ${previous}`)
}
function wakeReason(snapshotValue: unknown, settleSeconds = 0): string | null {
const r = spawnSync(
"python3",
[
"-c",
`import json; from importlib.machinery import SourceFileLoader; ` +
`m=SourceFileLoader('prs', ${JSON.stringify(SCRIPT)}).load_module(); ` +
`print(json.dumps(m._wake_reason(json.loads(${JSON.stringify(JSON.stringify(snapshotValue))}), ${settleSeconds})))`,
],
{ encoding: "utf8" },
)
expect(r.status, r.stderr).toBe(0)
return JSON.parse(r.stdout.trim())
}
function extractFeedback(view: unknown): any[] {
const r = spawnSync(
"python3",
[
"-c",
`import json; from importlib.machinery import SourceFileLoader; ` +
`m=SourceFileLoader('prs', ${JSON.stringify(SCRIPT)}).load_module(); ` +
`print(json.dumps(m._extract_feedback(json.loads(${JSON.stringify(JSON.stringify(view))}))))`,
],
{ encoding: "utf8" },
)
expect(r.status, r.stderr).toBe(0)
return JSON.parse(r.stdout.trim())
}
function eyesReactionIdentities(pages: unknown): string[] {
const r = spawnSync(
"python3",
[
"-c",
`import json; from importlib.machinery import SourceFileLoader; ` +
`m=SourceFileLoader('prs', ${JSON.stringify(SCRIPT)}).load_module(); ` +
`print(json.dumps(m._eyes_reaction_identities(json.loads(${JSON.stringify(JSON.stringify(pages))}))))`,
],
{ encoding: "utf8" },
)
expect(r.status, r.stderr).toBe(0)
return JSON.parse(r.stdout.trim())
}
function probeBaseIdentity(options: {
refStatus?: number
refError?: string
refOid?: string
gitStatus?: number
gitOutput?: string
gitTimeout?: boolean
gitOSError?: boolean
historicalOid?: string
graphqlOid?: string | null
headOid?: string
mergeable?: string
mergeStateStatus?: string
mergeCommitOid?: string | null
parentOids?: string[]
host?: string
}): { base: any; calls: string[][] } {
const values = {
refStatus: 0,
refError: "not found",
refOid: "2".repeat(40),
gitStatus: 1,
gitOutput: "",
gitTimeout: false,
gitOSError: false,
historicalOid: "1".repeat(40),
graphqlOid: "2".repeat(40) as string | null,
headOid: "3".repeat(40),
mergeable: "MERGEABLE",
mergeStateStatus: "CLEAN",
mergeCommitOid: "4".repeat(40) as string | null,
parentOids: ["2".repeat(40), "3".repeat(40)],
host: "ghe.acme.test",
...options,
}
const r = spawnSync("python3", ["-c", `
import json, subprocess
from importlib.machinery import SourceFileLoader
m = SourceFileLoader("prs", ${JSON.stringify(SCRIPT)}).load_module()
values = json.loads(${JSON.stringify(JSON.stringify(values))})
calls = []
class Result: pass
def run(cmd):
calls.append(cmd)
result = Result()
result.returncode = values["refStatus"]
result.stderr = values["refError"] if result.returncode else ""
result.stdout = values["refOid"] + "\\n" if result.returncode == 0 else ""
return result
def run_git(cmd):
calls.append(cmd)
if values["gitTimeout"]:
raise subprocess.TimeoutExpired(cmd, 30)
if values["gitOSError"]:
raise FileNotFoundError("git")
result = Result()
result.returncode = values["gitStatus"]
result.stderr = "git ref probe failed" if result.returncode else ""
result.stdout = values["gitOutput"]
return result
m._run = run
m._run_git = run_git
potential = None if values["mergeCommitOid"] is None else {
"oid": values["mergeCommitOid"],
"parents": {"nodes": [{"oid": oid} for oid in values["parentOids"]]},
}
identity = {
"baseRefOid": values["historicalOid"],
"headRefOid": values["headOid"],
"mergeable": values["mergeable"],
"mergeStateStatus": values["mergeStateStatus"],
"baseRef": {"target": {"oid": values["graphqlOid"]}},
"potentialMergeCommit": potential,
}
base = m.fetch_base_ref("o", "r", "main", identity, values["host"])
print(json.dumps({"base": base, "calls": calls}))
`], { encoding: "utf8" })
expect(r.status, r.stderr).toBe(0)
return JSON.parse(r.stdout)
}
function probeAwaitingApproval(response: { status: number; stdout?: string }): number | null {
const r = spawnSync(
"python3",
[
"-c",
`import json
from importlib.machinery import SourceFileLoader
m = SourceFileLoader("prs", ${JSON.stringify(SCRIPT)}).load_module()
class Result: pass
result = Result()
result.returncode = ${JSON.stringify(response.status)}
result.stdout = ${JSON.stringify(response.stdout ?? "")}
m._run = lambda _cmd: result
print(json.dumps(m.fetch_awaiting_approval("o", "r", "head")))`,
],
{ encoding: "utf8" },
)
expect(r.status, r.stderr).toBe(0)
return JSON.parse(r.stdout.trim())
}
function probeChain(options: {
pr?: number
url?: string
baseRef?: string
headRef?: string
stackView: { status: number; stdout?: unknown; stderr?: string }
graphql: { status: number; stdout?: unknown; stderr?: string }
defaultBranch?: { status: number; stdout?: unknown; stderr?: string }
openPrs?: unknown[]
}): { chain: any; calls: string[] } {
const payload = {
pr: options.pr ?? 42,
url: options.url ?? "https://github.com/o/r/pull/42",
base_ref: options.baseRef ?? "main",
head_ref: options.headRef ?? "feature",
stack_view: options.stackView,
graphql: options.graphql,
default_branch: options.defaultBranch ?? { status: 0, stdout: "main\n" },
open_prs: options.openPrs ?? [],
}
const python = `
import json
from importlib.machinery import SourceFileLoader
m = SourceFileLoader("prs", ${JSON.stringify(SCRIPT)}).load_module()
p = json.loads(${JSON.stringify(JSON.stringify(payload))})
calls = []
class Result:
pass
def fake(cmd):
calls.append(" ".join(cmd))
if cmd[:4] == ["gh", "stack", "view", "--json"]:
cfg = p["stack_view"]
elif cmd[:3] == ["gh", "api", "graphql"]:
cfg = p["graphql"]
elif cmd[:2] == ["gh", "api"]:
cfg = p["default_branch"]
else:
cfg = {"status": 0, "stdout": p["open_prs"]}
result = Result()
result.returncode = cfg["status"]
value = cfg.get("stdout")
result.stdout = value if isinstance(value, str) else json.dumps(value)
result.stderr = cfg.get("stderr", "")
return result
m._run = fake
chain = m.fetch_pr_chain(p["pr"], "o/r", p["url"], p["base_ref"], p["head_ref"], "o", "r", None)
print(json.dumps({"chain": chain, "calls": calls}))
`
const r = spawnSync(
"python3",
["-c", python],
{ encoding: "utf8" },
)
expect(r.status, r.stderr).toBe(0)
return JSON.parse(r.stdout.trim())
}
const CODEX_WRAPPER = `
### 💡 Codex Review
Here are some automated review suggestions for this pull request.
**Reviewed commit:** \`50ffb4dd99\`
<details> <summary>ℹ️ About Codex in GitHub</summary>
<br/>
[Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
</details>`
const FAILING = {
pr_state: "OPEN",
mergeable: "MERGEABLE",
merge_state_status: "BLOCKED",
review_decision: "REVIEW_REQUIRED",
head_sha: "s1",
base: {
host: "github.com",
repository: "o/r",
ref: "main",
oid: "base-1",
pr_oid: "base-1",
freshness: "current",
},
url: "http://x/1",
checks: [{ key: "CI/test", name: "test", status: "COMPLETED", conclusion: "FAILURE", details_url: "u" }],
threads: [{ thread_id: "T1", last_comment_id: "C1", last_comment_at: "t1" }],
}
function currencyFixture(overrides: Record<string, unknown> = {}): Record<string, unknown> {
const fixture = {
...FAILING,
mergeable: "MERGEABLE",
merge_state_status: "BEHIND",
base: {
host: "github.com",
repository: "o/r",
ref: "main",
oid: "base-1",
pr_oid: "base-1",
freshness: "current",
},
host_branch_update_capability: true,
pr_chain: {
manager_status: "absent",
manager_source: null,
relationship_status: "independent",
default_branch: "main",
parent_prs: [],
dependent_prs: [],
},
...overrides,
}
if (overrides.base && typeof overrides.base === "object") {
const base = overrides.base as Record<string, unknown>
fixture.base = {
pr_oid: base.oid,
freshness: "current",
...base,
} as typeof fixture.base
}
return fixture
}
function quietCurrencyFixture(overrides: Record<string, unknown> = {}): Record<string, unknown> {
return currencyFixture({
review_decision: "APPROVED",
checks: [{ key: "CI/test", name: "test", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }],
threads: [],
...overrides,
})
}
describe("ce-babysit-pr pr-snapshot engine", () => {
let dir: string
let state: string
beforeEach(() => {
dir = mkdtempSync(path.join(tmpdir(), "prsnap-"))
state = path.join(dir, "state")
})
test("branch currency: complete remote base identity creates one stable normal-base observation", () => {
const fetch = fetchFile(dir, "currency-stable.json", currencyFixture())
const first = snapshot(state, fetch)
const second = snapshot(state, fetch)
expect(first.branch_currency).toMatchObject({
disposition: "open",
route: "normal-base",
host: "github.com",
base_repository: "o/r",
base_ref: "main",
base_oid: "base-1",
head_sha: "s1",
status: "BEHIND",
host_branch_update_capability: true,
})
expect(second.branch_currency.key).toBe(first.branch_currency.key)
expect(second.branch_currency.disposition).toBe("open")
expect(second.mergeability_certain).toBe(true)
expect(second.branch_currency_blocker).toEqual({
key: second.branch_currency.key,
disposition: "open",
recovery_state: null,
})
})
test("branch currency: an old state file gains safe defaults without consuming the unseen item", () => {
const fetch = fetchFile(dir, "currency-migration.json", currencyFixture())
snapshot(state, fetch)
const statePath = path.join(state, "state.json")
const legacy = JSON.parse(readFileSync(statePath, "utf8"))
delete legacy.branch_currency_state
writeFileSync(statePath, JSON.stringify(legacy))
const migrated = snapshot(state, fetch)
expect(migrated.branch_currency.disposition).toBe("open")
const persisted = JSON.parse(readFileSync(statePath, "utf8"))
expect(persisted.branch_currency_state.current_key).toBe(migrated.branch_currency.key)
})
test("branch currency: UNKNOWN mergeability re-polls without creating or consuming an item", () => {
const uncertain = currencyFixture({
mergeable: "UNKNOWN",
merge_state_status: "UNKNOWN",
})
const value = snapshot(state, fetchFile(dir, "currency-unknown.json", uncertain))
expect(value.mergeability_certain).toBe(false)
expect(value.branch_currency).toBeNull()
expect(value.branch_currency_blocker).toBeNull()
const persisted = JSON.parse(readFileSync(path.join(state, "state.json"), "utf8"))
expect(persisted.branch_currency_state.current_key).toBeNull()
expect(persisted.branch_currency_state.items).toEqual({})
})
test("branch currency: wake precedence favors review and failing CI, while passive checks do not delay maintenance", () => {
const open = snapshot(state, fetchFile(dir, "currency-wake.json", quietCurrencyFixture({
checks: [{ key: "CI/test", name: "test", status: "IN_PROGRESS", conclusion: null, details_url: "u" }],
review_in_progress: true,
})))
expect(open.branch_currency.attention).toBe("claim")
expect(wakeReason(open)).toBe("branch-currency")
expect(wakeReason({ ...open, counts: { ...open.counts, threads: 1 } })).toBe("actionable")
expect(wakeReason({
...open,
open_needs_human: 1,
needs_human_ids: ["parked-review-decision"],
})).toBe("branch-currency")
expect(wakeReason({ ...open, blocked_external: true })).toBe("branch-currency")
expect(wakeReason({
...open,
counts: { ...open.counts, ci: 0 },
has_failing_checks: true,
checks_terminal: true,
})).toBe("blocked-failing")
}, 15000)
test("branch currency: claim re-entry reconciles, permits one proven-no-mutation retry, and never retries ambiguity", () => {
const fetch = fetchFile(dir, "currency-lifecycle.json", quietCurrencyFixture())
const first = snapshot(state, fetch)
markCurrency(state, first.branch_currency.key, "claimed")
const sameInvocation = snapshot(state, fetch)
expect(sameInvocation.branch_currency).toMatchObject({
disposition: "claimed",
attention: null,
retry_count: 0,
mutation_consumed: false,
})
const resumed = snapshot(state, fetch, ["--start-invocation",
"--invocation-budget-seconds", ORDINARY_TEST_BUDGET_SECONDS])
expect(resumed.branch_currency.attention).toBe("reconcile")
expect(wakeReason(resumed)).toBe("branch-currency")
markCurrencyOutcome(state, resumed.branch_currency.key, "proven-no-mutation")
const backingOff = snapshot(state, fetch)
expect(backingOff.branch_currency).toMatchObject({
disposition: "open",
attention: null,
retry_count: 1,
recovery_state: "retry-authorized",
})
expect(backingOff.branch_currency.retry_wait_seconds).toBeGreaterThan(0)
const earlyRetry = spawnSync("python3", [SCRIPT, "mark", "--state-dir", state,
...persistedInvocationArgs(state), "--currency-key", backingOff.branch_currency.key,
"--currency-disposition", "claimed"], { encoding: "utf8" })
expect(earlyRetry.status).not.toBe(0)
const statePath = path.join(state, "state.json")
const afterBackoff = JSON.parse(readFileSync(statePath, "utf8"))
afterBackoff.branch_currency_state.items[backingOff.branch_currency.key].retry_not_before = "2000-01-01T00:00:00Z"
writeFileSync(statePath, JSON.stringify(afterBackoff))
const retry = snapshot(state, fetch)
expect(retry.branch_currency.attention).toBe("claim")
markCurrency(state, retry.branch_currency.key, "claimed")
const missingExhaustedResidual = spawnSync("python3", [SCRIPT, "mark", "--state-dir", state,
...persistedInvocationArgs(state), "--currency-key", retry.branch_currency.key,
"--currency-outcome", "proven-no-mutation"], { encoding: "utf8" })
expect(missingExhaustedResidual.status).not.toBe(0)
expect(missingExhaustedResidual.stderr).toMatch(/residual-file/)
expect(snapshot(state, fetch).branch_currency).toMatchObject({
disposition: "claimed",
retry_count: 1,
})
markCurrencyOutcome(state, retry.branch_currency.key, "proven-no-mutation")
const exhausted = snapshot(state, fetch)
expect(exhausted.branch_currency).toMatchObject({
disposition: "open",
attention: null,
retry_count: 1,
recovery_state: "retry-exhausted",
})
expectCurrentDecision(exhausted.needs_human_residuals,
residualFile(dir, "currency-exhausted-expected.json", retry.branch_currency.key, "currency").value)
const exhaustedAfterCapabilityDrift = snapshot(state, fetchFile(
dir, "currency-exhausted-capability-drift.json", quietCurrencyFixture({
host_branch_update_capability: false,
})))
expect(exhaustedAfterCapabilityDrift.branch_currency).toMatchObject({
recovery_state: "retry-exhausted",
attention: null,
})
expectCurrentDecision(exhaustedAfterCapabilityDrift.needs_human_residuals,
residualFile(dir, "currency-exhausted-drift-expected.json", retry.branch_currency.key, "currency").value)
const unsafeReplay = spawnSync("python3", [SCRIPT, "mark", "--state-dir", state,
...persistedInvocationArgs(state), "--currency-key", exhausted.branch_currency.key,
"--currency-disposition", "claimed"], { encoding: "utf8" })
expect(unsafeReplay.status).not.toBe(0)
expect(unsafeReplay.stderr).toMatch(/current human decision to be answered/)
const ambiguousState = path.join(dir, "currency-ambiguous")
const ambiguous = snapshot(ambiguousState, fetch)
markCurrency(ambiguousState, ambiguous.branch_currency.key, "claimed")
const missingAmbiguousResidual = spawnSync("python3", [SCRIPT, "mark", "--state-dir", ambiguousState,
...persistedInvocationArgs(ambiguousState), "--currency-key", ambiguous.branch_currency.key,
"--currency-outcome", "ambiguous"], { encoding: "utf8" })
expect(missingAmbiguousResidual.status).not.toBe(0)
expect(missingAmbiguousResidual.stderr).toMatch(/residual-file/)
expect(snapshot(ambiguousState, fetch)).toMatchObject({
needs_human_residuals: [],
branch_currency: { recovery_state: "claimed" },
})
markCurrencyOutcome(ambiguousState, ambiguous.branch_currency.key, "ambiguous")
const ambiguousParked = snapshot(ambiguousState, fetch)
expect(ambiguousParked.branch_currency).toMatchObject({
disposition: "claimed",
attention: null,
recovery_state: "ambiguous",
mutation_consumed: false,
mutation_requires_answer: true,
})
expectCurrentDecision(ambiguousParked.needs_human_residuals,
residualFile(dir, "currency-ambiguous-expected.json", ambiguous.branch_currency.key, "currency").value)
markDecisionAnswered(ambiguousState, ambiguousParked.human_decisions[0].decision_id)
const ambiguousAnswered = snapshot(ambiguousState, fetch)
expect(ambiguousAnswered.branch_currency).toMatchObject({
disposition: "open",
attention: "claim",
recovery_state: "ambiguous",
mutation_requires_answer: true,
})
markCurrency(ambiguousState, ambiguous.branch_currency.key, "claimed")
const ambiguousConsumed = snapshot(ambiguousState, fetch)
expect(ambiguousConsumed.branch_currency).toMatchObject({
disposition: "claimed",
attention: null,
mutation_requires_answer: false,
})
expect(ambiguousConsumed.answered_human_decisions).toEqual([])
})
test("branch currency: grouped invalidation cannot turn decision-bound evidence into mutation authority", () => {
const sd = path.join(dir, "currency-grouped-invalidation")
const fixture = quietCurrencyFixture({
threads: [{ thread_id: "T1", last_comment_id: "C1", last_comment_at: "t1" }],
})
const fetch = fetchFile(dir, "currency-grouped-1.json", fixture)
const first = snapshot(sd, fetch)
markCurrency(sd, first.branch_currency.key, "claimed")
const resumed = snapshot(sd, fetch, ["--start-invocation",
"--invocation-budget-seconds", ORDINARY_TEST_BUDGET_SECONDS])
markCurrencyOutcome(sd, resumed.branch_currency.key, "proven-no-mutation")
const statePath = path.join(sd, "state.json")
const afterBackoff = JSON.parse(readFileSync(statePath, "utf8"))
afterBackoff.branch_currency_state.items[first.branch_currency.key].retry_not_before = "2000-01-01T00:00:00Z"
writeFileSync(statePath, JSON.stringify(afterBackoff))
const retry = snapshot(sd, fetch)
markCurrency(sd, retry.branch_currency.key, "claimed")
const grouped = {
...residualFile(dir, "currency-grouped-unused.json", retry.branch_currency.key, "currency").value,
sources: [
{ id: retry.branch_currency.key, kind: "currency" },
{ id: "T1", kind: "thread" },
],
thread_urls: ["https://example.test/thread/T1"],
}
const groupedPath = fetchFile(dir, "currency-grouped-residual.json", grouped)
mark(sd, ["--currency-key", retry.branch_currency.key,
"--currency-outcome", "proven-no-mutation", "--residual-file", groupedPath])
const parked = snapshot(sd, fetch)
expectCurrentDecision(parked.needs_human_residuals, grouped)
expect(parked.branch_currency).toMatchObject({
recovery_state: "retry-exhausted",
mutation_requires_answer: true,
attention: null,
})
const changedFetch = fetchFile(dir, "currency-grouped-2.json", {
...fixture,
threads: [{ thread_id: "T1", last_comment_id: "C2", last_comment_at: "t2" }],
})
const reopened = snapshot(sd, changedFetch)
expect(reopened.needs_human_residuals).toEqual([])
expect(reopened.actionable.threads.map((item: any) => item.thread_id)).toEqual(["T1"])
expect(reopened.branch_currency).toMatchObject({
recovery_state: "retry-exhausted",
mutation_requires_answer: true,
attention: "decide",
})
expect(wakeReason({ ...reopened, counts: { ...reopened.counts, threads: 0 } }))
.toBe("branch-currency")
const unsafeClaim = spawnSync("python3", [SCRIPT, "mark", "--state-dir", sd,
...persistedInvocationArgs(sd), "--currency-key", reopened.branch_currency.key,
"--currency-disposition", "claimed"], { encoding: "utf8" })
expect(unsafeClaim.status).not.toBe(0)
expect(unsafeClaim.stderr).toMatch(/exact human answer/)
const currencyOnly = residualFile(
dir, "currency-grouped-reopened.json", reopened.branch_currency.key, "currency")
mark(sd, ["--disposition", "needs-human", "--residual-file", currencyOnly.path])
const reparked = snapshot(sd, changedFetch)
markDecisionAnswered(sd, reparked.human_decisions[0].decision_id)
const answered = snapshot(sd, changedFetch)
expect(answered.branch_currency).toMatchObject({
mutation_requires_answer: true,
attention: "claim",
})
markCurrency(sd, answered.branch_currency.key, "claimed")
expect(JSON.parse(readFileSync(statePath, "utf8"))
.branch_currency_state.items[answered.branch_currency.key].mutation_requires_answer).toBe(false)
}, 30000)
test("branch currency: every grouped decision removal reactivates exact claimed evidence", () => {
const parkGroupedAmbiguous = (label: string) => {
const sd = path.join(dir, `currency-grouped-${label}`)
const fixture = quietCurrencyFixture({
threads: [{ thread_id: "T1", last_comment_id: "C1", last_comment_at: "t1" }],
})
const fetch = fetchFile(dir, `currency-grouped-${label}-1.json`, fixture)
const observed = snapshot(sd, fetch)
markCurrency(sd, observed.branch_currency.key, "claimed")
const grouped = {
...residualFile(
dir, `currency-grouped-${label}-unused.json`, observed.branch_currency.key, "currency").value,
sources: [
{ id: observed.branch_currency.key, kind: "currency" },
{ id: "T1", kind: "thread" },
],
thread_urls: ["https://example.test/thread/T1"],
}
mark(sd, ["--currency-key", observed.branch_currency.key,
"--currency-outcome", "ambiguous", "--residual-file",
fetchFile(dir, `currency-grouped-${label}-residual.json`, grouped)])
const parked = snapshot(sd, fetch)
expect(parked.branch_currency).toMatchObject({
disposition: "claimed",
recovery_state: "ambiguous",
mutation_requires_answer: true,
attention: null,
})
return { sd, fixture, fetch, key: observed.branch_currency.key }
}
for (const route of ["explicit", "reconciled", "head-moved-reconciled"] as const) {
const current = parkGroupedAmbiguous(route)
const nextFetch = route === "explicit" ? current.fetch : fetchFile(
dir, `currency-grouped-${route}-2.json`, {
...current.fixture,
...(route === "head-moved-reconciled" ? { head_sha: "s2" } : {}),
threads: [{ thread_id: "T1", last_comment_id: "C2", last_comment_at: "t2" }],
})
if (route === "explicit") {
mark(current.sd, ["--thread", "T1", "--disposition", "open"])
}
const reopened = snapshot(current.sd, nextFetch)
expect(reopened.needs_human_residuals).toEqual([])
expect(reopened.actionable.threads.map((item: any) => item.thread_id)).toEqual(["T1"])
expect(reopened.branch_currency).toMatchObject({
key: current.key,
disposition: "open",
recovery_state: "ambiguous",
mutation_requires_answer: true,
attention: "decide",
})
const unsafeClaim = spawnSync("python3", [SCRIPT, "mark", "--state-dir", current.sd,
...persistedInvocationArgs(current.sd), "--currency-key", current.key,
"--currency-disposition", "claimed"], { encoding: "utf8" })
expect(unsafeClaim.status).not.toBe(0)
expect(unsafeClaim.stderr).toMatch(/exact human answer/)
}
}, 30000)
test("branch currency: mutation observation consumes the attempt and remains reconciliation-only", () => {
const fetch = fetchFile(dir, "currency-consumed.json", quietCurrencyFixture())
const observed = snapshot(state, fetch)
markCurrency(state, observed.branch_currency.key, "claimed")
markCurrencyOutcome(state, observed.branch_currency.key, "mutation-observed")
const current = snapshot(state, fetch)
expect(current.branch_currency).toMatchObject({
disposition: "claimed",
attention: null,
mutation_consumed: true,
recovery_state: "mutation-observed",
})
expect(wakeReason({
...current,
mergeable: "MERGEABLE",
merge_state_status: "CLEAN",
checks_terminal: true,
has_failing_checks: false,
review_in_progress: false,
quiet_seconds: 2000,
}, 0)).toBeNull()
const invalidRetry = spawnSync("python3", [SCRIPT, "mark", "--state-dir", state,
...persistedInvocationArgs(state), "--currency-key", observed.branch_currency.key,
"--currency-outcome", "proven-no-mutation"], { encoding: "utf8" })
expect(invalidRetry.status).not.toBe(0)
const directReopen = spawnSync("python3", [SCRIPT, "mark", "--state-dir", state,
...persistedInvocationArgs(state), "--currency-key", observed.branch_currency.key,
"--currency-disposition", "open"], { encoding: "utf8" })
expect(directReopen.status).not.toBe(0)
expect(snapshot(state, fetch).branch_currency).toMatchObject({
disposition: "claimed",
mutation_consumed: true,
recovery_state: "mutation-observed",
})
const resumed = snapshot(state, fetch, ["--start-invocation",
"--invocation-budget-seconds", ORDINARY_TEST_BUDGET_SECONDS])
expect(resumed.branch_currency.attention).toBe("reconcile")
}, 20000)
test("branch currency: async evidence movement re-wakes a same-invocation mutation claim once", () => {
const behind = fetchFile(dir, "currency-async-mutation-behind.json", quietCurrencyFixture())
const observed = snapshot(state, behind)
markCurrency(state, observed.branch_currency.key, "claimed")
markCurrencyOutcome(state, observed.branch_currency.key, "mutation-observed")
const unchanged = snapshot(state, behind)
expect(unchanged.branch_currency).toMatchObject({
key: observed.branch_currency.key,
disposition: "claimed",
attention: null,
reconciliation_only: true,
})
const updated = fetchFile(dir, "currency-async-mutation-clean.json", quietCurrencyFixture({
head_sha: "s2",
mergeable: "MERGEABLE",
merge_state_status: "CLEAN",
}))
const moved = snapshot(state, updated)
expect(moved.branch_currency).toMatchObject({
key: observed.branch_currency.key,
disposition: "claimed",
attention: "reconcile",
reconciliation_only: true,
mutation_consumed: true,
})
expect(wakeReason(moved)).toBe("branch-currency")
markCurrency(state, observed.branch_currency.key, "confirmed")
const confirmed = snapshot(state, updated)
expect(confirmed.branch_currency).toBeNull()
expect(confirmed.branch_currency_blocker).toBeNull()
expect(wakeReason({ ...confirmed, quiet_seconds: 0 }, 300)).toBeNull()
}, 15000)
test("branch currency: answering ambiguity re-evaluates moved evidence without reviving the claim", () => {
const behind = fetchFile(dir, "currency-async-ambiguous-behind.json", quietCurrencyFixture())
const observed = snapshot(state, behind)
markCurrency(state, observed.branch_currency.key, "claimed")
markCurrencyOutcome(state, observed.branch_currency.key, "ambiguous")
const parked = snapshot(state, behind)
expect(parked.branch_currency.attention).toBeNull()
const decisionId = parked.human_decisions[0].decision_id
const movedFetch = fetchFile(dir, "currency-async-ambiguous-clean.json", quietCurrencyFixture({
head_sha: "s2",
mergeable: "MERGEABLE",
merge_state_status: "CLEAN",
}))
expect(snapshot(state, movedFetch).branch_currency.attention).toBeNull()
markDecisionAnswered(state, decisionId)
const answered = snapshot(state, movedFetch)
expect(answered.branch_currency).toBeNull()
expect(answered.branch_currency_blocker).toBeNull()
expect(answered.answered_human_decisions).toEqual([])
}, 15000)
test("branch currency: a BEHIND capability decision retains its answer gate when evidence moves", () => {
for (const capability of [false, "unknown"] as const) {
const capabilityState = path.join(dir, `currency-capability-${capability}`)
const unavailable = fetchFile(dir, `currency-capability-${capability}-off.json`, quietCurrencyFixture({
host_branch_update_capability: capability,
}))
const observed = snapshot(capabilityState, unavailable)
markCurrency(capabilityState, observed.branch_currency.key, "needs-human")
const parked = snapshot(capabilityState, unavailable)
expect(parked.branch_currency).toMatchObject({ disposition: "open", attention: null })
expect(parked.open_needs_human).toBe(1)
const enabled = snapshot(capabilityState, fetchFile(dir, `currency-capability-${capability}-on.json`, quietCurrencyFixture({
host_branch_update_capability: true,
})))
expect(enabled.branch_currency).toMatchObject({
key: observed.branch_currency.key,
disposition: "open",
attention: "decide",
host_branch_update_capability: true,
mutation_requires_answer: true,
})
expect(wakeReason(enabled)).toBe("branch-currency")
const unsafeClaim = spawnSync("python3", [SCRIPT, "mark", "--state-dir", capabilityState,
...persistedInvocationArgs(capabilityState), "--currency-key", enabled.branch_currency.key,
"--currency-disposition", "claimed"], { encoding: "utf8" })
expect(unsafeClaim.status).not.toBe(0)
expect(unsafeClaim.stderr).toMatch(/exact human answer/)
}
}, 15000)
test("branch currency decisions use the canonical typed residual and re-actionize as one source", () => {
const currencyState = path.join(dir, "currency-canonical-residual")
const unavailable = fetchFile(dir, "currency-canonical-off.json", quietCurrencyFixture({
host_branch_update_capability: false,
}))
const observed = snapshot(currencyState, unavailable)
const residual = residualFile(
dir, "currency-canonical-residual.json", observed.branch_currency.key, "currency")
mark(currencyState, ["--disposition", "needs-human", "--residual-file", residual.path])
const parked = snapshot(currencyState, unavailable)
expectCurrentDecision(parked.needs_human_residuals, residual.value)
expect(parked.open_needs_human).toBe(1)
expect(parked.branch_currency.disposition).toBe("open")
expect(parked.branch_currency.mutation_requires_answer).toBe(true)
expect(wakeReason(parked)).toBe("needs-human")
const enabled = snapshot(currencyState, fetchFile(dir, "currency-canonical-on.json", quietCurrencyFixture({
host_branch_update_capability: true,
})))
expect(enabled.needs_human_residuals).toEqual([])
expect(enabled.open_needs_human).toBe(0)
expect(enabled.branch_currency).toMatchObject({
disposition: "open",
attention: "decide",
mutation_requires_answer: true,
})
}, 15000)
test("branch currency: grouped capability decisions cannot turn sibling activity into mutation authority", () => {
const decisionState = path.join(dir, "currency-capability-grouped")
const fixture = quietCurrencyFixture({
host_branch_update_capability: "unknown",
threads: [{ thread_id: "T1", last_comment_id: "C1", last_comment_at: "t1" }],
})
const first = fetchFile(dir, "currency-capability-grouped-1.json", fixture)
const observed = snapshot(decisionState, first)
const grouped = {
...residualFile(
dir, "currency-capability-grouped-unused.json",
observed.branch_currency.key, "currency").value,
sources: [
{ id: observed.branch_currency.key, kind: "currency" },
{ id: "T1", kind: "thread" },
],
thread_urls: ["https://example.test/thread/T1"],
}
mark(decisionState, ["--disposition", "needs-human", "--residual-file",
fetchFile(dir, "currency-capability-grouped-residual.json", grouped),
"--fetch-file", first])
const statePath = path.join(decisionState, "state.json")
const preOwnerState = JSON.parse(readFileSync(statePath, "utf8"))
preOwnerState.branch_currency_state.items[observed.branch_currency.key]
.mutation_requires_answer = false
const currencySource = preOwnerState.human_decisions[0].sources.find(
(source: any) => source.kind === "currency")
currencySource.observation.mutation_requires_answer = false
preOwnerState.human_decisions[0].id = "decision:pre-shared-owner"
writeFileSync(statePath, JSON.stringify(preOwnerState))
const parked = snapshot(decisionState, first)
expectCurrentDecision(parked.needs_human_residuals, grouped)
expect(parked.branch_currency).toMatchObject({
host_branch_update_capability: "unknown",
mutation_requires_answer: true,
attention: null,
})
const moved = fetchFile(dir, "currency-capability-grouped-2.json", {
...fixture,
threads: [{ thread_id: "T1", last_comment_id: "C2", last_comment_at: "t2" }],
})
const reopened = snapshot(decisionState, moved)
expect(reopened.needs_human_residuals).toEqual([])
expect(reopened.actionable.threads.map((item: any) => item.thread_id)).toEqual(["T1"])
expect(reopened.branch_currency).toMatchObject({
host_branch_update_capability: "unknown",
mutation_requires_answer: true,
attention: "decide",
})
const unsafeClaim = spawnSync("python3", [SCRIPT, "mark", "--state-dir", decisionState,
...persistedInvocationArgs(decisionState), "--currency-key", reopened.branch_currency.key,
"--currency-disposition", "claimed"], { encoding: "utf8" })
expect(unsafeClaim.status).not.toBe(0)
expect(unsafeClaim.stderr).toMatch(/exact human answer/)
const currencyOnly = residualFile(
dir, "currency-capability-grouped-reopened.json", reopened.branch_currency.key, "currency")
mark(decisionState, ["--disposition", "needs-human", "--residual-file", currencyOnly.path])
const reparked = snapshot(decisionState, moved)
markDecisionAnswered(decisionState, reparked.human_decisions[0].decision_id)
const answered = snapshot(decisionState, moved)
expect(answered.branch_currency).toMatchObject({
mutation_requires_answer: true,
attention: "claim",
})
markCurrency(decisionState, answered.branch_currency.key, "claimed")
expect(snapshot(decisionState, moved).branch_currency).toMatchObject({
disposition: "claimed",
mutation_requires_answer: false,
attention: null,
})
}, 20000)
test("branch currency cannot create a human decision without a complete typed residual", () => {
const currencyState = path.join(dir, "currency-residual-required")
const fetch = fetchFile(dir, "currency-residual-required.json", quietCurrencyFixture({
host_branch_update_capability: false,
}))
const observed = snapshot(currencyState, fetch)
const result = spawnSync("python3", [SCRIPT, "mark", "--state-dir", currencyState,
...persistedInvocationArgs(currencyState), "--disposition", "needs-human"],
{ encoding: "utf8" })
expect(result.status).not.toBe(0)
expect(result.stderr).toMatch(/residual-file/)
expect(snapshot(currencyState, fetch).branch_currency.disposition).toBe("open")
}, 15000)
test("a completed branch-currency source cannot be republished as a human decision", () => {
const currencyState = path.join(dir, "currency-completed-residual")
const fetch = fetchFile(dir, "currency-completed-residual.json", quietCurrencyFixture())
const observed = snapshot(currencyState, fetch)
markCurrency(currencyState, observed.branch_currency.key, "claimed")
markCurrency(currencyState, observed.branch_currency.key, "confirmed")
const residual = residualFile(
dir, "currency-completed-residual-payload.json", observed.branch_currency.key, "currency")
const result = spawnSync("python3", [SCRIPT, "mark", "--state-dir", currencyState,
...persistedInvocationArgs(currencyState), "--disposition", "needs-human",
"--residual-file", residual.path], { encoding: "utf8" })
expect(result.status).not.toBe(0)
expect(result.stderr).toMatch(/not current decision evidence/)
expect(snapshot(currencyState, fetch).needs_human_residuals).toEqual([])
}, 15000)
test("dispatch cannot erase a source covered by a canonical decision residual", () => {
const decisionState = path.join(dir, "covered-dispatch")
const fetch = fetchFile(dir, "covered-dispatch.json", {
...FAILING,
checks: [],
threads: [],
feedback: [{ id: "IC_decision", kind: "comment", author: "reviewer", edit_id: "v1" }],
})
snapshot(decisionState, fetch)
const residual = residualFile(dir, "covered-dispatch-residual.json", "IC_decision", "comment")
mark(decisionState, ["--disposition", "needs-human", "--residual-file", residual.path])
const dispatched = spawnSync("python3", [SCRIPT, "mark", "--state-dir", decisionState,
...persistedInvocationArgs(decisionState), "--comment", "IC_decision",
"--disposition", "dispatched"], { encoding: "utf8" })
expect(dispatched.status).not.toBe(0)
expect(dispatched.stderr).toMatch(/current human decision/)
expectCurrentDecision(snapshot(decisionState, fetch).needs_human_residuals, residual.value)
}, 15000)
test("branch currency: standing confirmed and needs-human residuals stay quiet, and max-runtime outranks new work", () => {
const fetch = fetchFile(dir, "currency-standing.json", quietCurrencyFixture())
for (const disposition of ["confirmed", "needs-human"]) {
const residualState = path.join(dir, `currency-standing-${disposition}`)
const observed = snapshot(residualState, fetch)
if (disposition === "confirmed") {
markCurrency(residualState, observed.branch_currency.key, "claimed")
markCurrency(residualState, observed.branch_currency.key, disposition)
} else {
markCurrency(residualState, observed.branch_currency.key, disposition, "semantic-v1")
}
const residualPath = path.join(residualState, "state.json")
const expiredResidual = JSON.parse(readFileSync(residualPath, "utf8"))
expiredResidual.started_at = "2000-01-01T00:00:00Z"
expiredResidual.invocation_budget_seconds = 1
writeFileSync(residualPath, JSON.stringify(expiredResidual))
expect(watch(residualState, fetch).reason).toBe("max-runtime")
}
const budgetState = path.join(dir, "currency-budget")
snapshot(budgetState, fetch)
const statePath = path.join(budgetState, "state.json")
const expired = JSON.parse(readFileSync(statePath, "utf8"))
expired.started_at = "2000-01-01T00:00:00Z"
expired.invocation_budget_seconds = 1
writeFileSync(statePath, JSON.stringify(expired))
expect(watch(budgetState, fetch).reason).toBe("max-runtime")
const lateClaim = spawnSync("python3", [SCRIPT, "mark", "--state-dir", budgetState,
...persistedInvocationArgs(budgetState), "--currency-key",
expired.branch_currency_state.current_key, "--currency-disposition", "claimed"],
{ encoding: "utf8" })
expect(lateClaim.status).not.toBe(0)
}, 20000)
// Active-watch-capability budget: the 8h cap is spent in active time, not raw wall-clock.
// A suspended machine (laptop asleep) is excluded; the 3-day backstop stays wall-clock.
const FAILING_ACTIONABLE = {
pr_state: "OPEN", mergeable: "MERGEABLE", merge_state_status: "UNSTABLE", review_decision: null,
head_sha: "h1", url: "https://github.com/o/r/pull/1", threads: [],
checks: [{ key: "CI/test", name: "test", status: "COMPLETED", conclusion: "FAILURE", details_url: "u" }],
feedback: [], awaiting_approval: 0,
}
const isoAgo = (seconds: number) =>
new Date(Date.now() - seconds * 1000).toISOString().replace(/\.\d+Z$/, "Z")
function patchState(stateDir: string, patch: Record<string, unknown>): void {
const p = path.join(stateDir, "state.json")
writeFileSync(p, JSON.stringify({ ...JSON.parse(readFileSync(p, "utf8")), ...patch }))
}
function readState(stateDir: string): any {
return JSON.parse(readFileSync(path.join(stateDir, "state.json"), "utf8"))
}
test("active-time budget: a suspended span (stale activity heartbeat) is excluded from the 8h cap", () => {
// Covers AE1. started_at and last_activity both ~6h stale (machine was suspended), budget 8h.
const fetch = fetchFile(dir, "active-suspend.json", FAILING_ACTIONABLE)
snapshot(state, fetch)
patchState(state, { started_at: isoAgo(6 * 3600), last_activity_at: isoAgo(6 * 3600),
dead_time_seconds: 0 })
// The watch arms, measures the ~6h activity gap, charges it to dead time, and does NOT max-runtime.
expect(watch(state, fetch).reason).toBe("actionable")
const after = readState(state)
// ~6h minus the 15-min threshold is charged to dead time (excluded from the active budget).
expect(after.dead_time_seconds).toBeGreaterThan(6 * 3600 - 15 * 60 - 60)
expect(after.dead_time_seconds).toBeLessThan(6 * 3600)
}, 20000)
test("active-time budget: steady sub-threshold polling accrues no dead time", () => {
// Covers AE2. A recent heartbeat (well under the 15-min threshold) never registers as suspend.
const fetch = fetchFile(dir, "active-steady.json", FAILING_ACTIONABLE)
snapshot(state, fetch)
patchState(state, { started_at: isoAgo(2 * 3600), last_activity_at: isoAgo(30),
dead_time_seconds: 0 })
expect(watch(state, fetch).reason).toBe("actionable")
expect(readState(state).dead_time_seconds).toBe(0)
}, 20000)
test("3-day backstop: raw wall-clock expiry fires even when active elapsed is ~0", () => {
// Covers AE4. started_at 4 days ago, dead_time ~4 days (active ~0), heartbeat fresh so the poll
// adds nothing. The 8h active cap is nowhere near hit, but the wall-clock backstop terminates.
const fetch = fetchFile(dir, "backstop.json", FAILING_ACTIONABLE)
snapshot(state, fetch)
patchState(state, { started_at: isoAgo(4 * 86400), last_activity_at: isoAgo(10),
dead_time_seconds: 4 * 86400 })
const wake = watch(state, fetch)
expect(wake.reason).toBe("max-runtime")
expect(wake.max_runtime_ceiling).toBe("backstop")
}, 20000)
test("active cap: active elapsed past the 8h budget fires max-runtime with the active-budget ceiling", () => {
// started_at 9h ago, no dead time, wall-clock < 3-day backstop -> the active budget is the ceiling.
const fetch = fetchFile(dir, "active-cap.json", FAILING_ACTIONABLE)
snapshot(state, fetch)
patchState(state, { started_at: isoAgo(9 * 3600), last_activity_at: isoAgo(10),
dead_time_seconds: 0 })
const wake = watch(state, fetch)
expect(wake.reason).toBe("max-runtime")
expect(wake.max_runtime_ceiling).toBe("active-budget")
}, 20000)
test("legacy state without active-time fields migrates on load", () => {
// Covers U1. A pre-existing state file lacking the new fields gains them with safe defaults.
const fetch = fetchFile(dir, "migrate.json", quietCurrencyFixture())
snapshot(state, fetch)
const legacy = readState(state)
delete legacy.last_activity_at
delete legacy.dead_time_seconds
delete legacy.invocation_backstop_seconds
writeFileSync(path.join(state, "state.json"), JSON.stringify(legacy))
snapshot(state, fetch)
const migrated = readState(state)
expect(migrated.dead_time_seconds).toBe(0)
expect(migrated.last_activity_at).toBeTruthy()
expect(migrated.invocation_backstop_seconds).toBe(3 * 24 * 60 * 60)
}, 20000)
test("legacy migration does not refund pre-migration time: an expired old invocation still max-runtimes", () => {
// Regression (Cursor/Codex): seeding last_activity to the OLD started_at made the first poll
// charge the whole historical invocation as one suspend gap, so a 9h-old 8h run read as ~15 min
// active and never expired. Seeding to load time keeps it on wall-clock -> it must still expire.
const fetch = fetchFile(dir, "legacy-expire.json", FAILING_ACTIONABLE)
snapshot(state, fetch)
const legacy = readState(state)
legacy.started_at = isoAgo(9 * 3600)
legacy.invocation_budget_seconds = 8 * 3600
delete legacy.last_activity_at
delete legacy.dead_time_seconds
delete legacy.invocation_backstop_seconds
writeFileSync(path.join(state, "state.json"), JSON.stringify(legacy))
const wake = watch(state, fetch)
expect(wake.reason).toBe("max-runtime")
expect(wake.max_runtime_ceiling).toBe("active-budget")
// The historical span was not laundered into dead time.
expect(readState(state).dead_time_seconds).toBe(0)
}, 20000)
test("re-arm preserves accumulated dead time and the backstop (no reset, no extend)", () => {
// Covers AE5. A continue-invocation re-arm keeps the accumulated dead time rather than resetting.
const fetch = fetchFile(dir, "rearm.json", quietCurrencyFixture())
snapshot(state, fetch)
patchState(state, { dead_time_seconds: 1234, last_activity_at: isoAgo(10) })
const inv = persistedInvocationArgs(state)
const r = spawnSync("python3", [SCRIPT, "snapshot", "--pr", "1", "--repo", "o/r",
"--state-dir", state, "--fetch-file", fetch, "--continue-invocation", ...inv],
{ encoding: "utf8" })
expect(r.status, r.stderr).toBe(0)
const after = readState(state)
expect(after.dead_time_seconds).toBe(1234)
expect(after.invocation_backstop_seconds).toBe(3 * 24 * 60 * 60)
}, 20000)
test("checkpoint mode: an agent snapshot with a stale heartbeat never accumulates dead time", () => {
// KTD4 scope guard: only the in-session watch (watch_generation) accumulates. A plain agent
// snapshot bumps the heartbeat with accumulate=False, so checkpoint/durable runs stay wall-clock.
const fetch = fetchFile(dir, "checkpoint.json", quietCurrencyFixture())
snapshot(state, fetch)
patchState(state, { started_at: isoAgo(6 * 3600), last_activity_at: isoAgo(6 * 3600),
dead_time_seconds: 0 })
const observed = snapshot(state, fetch) // plain (non-watch) agent snapshot
expect(readState(state).dead_time_seconds).toBe(0)
// Wall-clock retained: elapsed reflects the full ~6h with nothing refunded.
expect(observed.invocation_elapsed_seconds).toBeGreaterThan(6 * 3600 - 120)
}, 20000)
test("an agent mark bumps the activity heartbeat without accumulating dead time", () => {
// A long tick that only marks keeps the heartbeat fresh, so the next watch poll charges nothing.
const fetch = fetchFile(dir, "markbump.json", FAILING_ACTIONABLE)
snapshot(state, fetch)
patchState(state, { last_activity_at: isoAgo(6 * 3600), dead_time_seconds: 0 })
mark(state, ["--check", "CI/test"])
const after = readState(state)
expect(after.dead_time_seconds).toBe(0)
expect(new Date(after.last_activity_at).getTime()).toBeGreaterThan(Date.now() - 60 * 1000)
}, 20000)
test("clock-backward safety: a future heartbeat/anchor never produces negative accounting", () => {
const fetch = fetchFile(dir, "clockback.json", FAILING_ACTIONABLE)
snapshot(state, fetch)
patchState(state, { started_at: new Date(Date.now() + 3600 * 1000).toISOString(),
last_activity_at: new Date(Date.now() + 3600 * 1000).toISOString(), dead_time_seconds: 0 })
const wake = watch(state, fetch)
expect(wake.reason).not.toBe("max-runtime")
const after = readState(state)
expect(after.dead_time_seconds).toBeGreaterThanOrEqual(0)
}, 20000)
test("continue-invocation adopting a new id into a used state dir resets the active-time clock", () => {
// Regression: adopting a fresh invocation must not inherit a prior invocation's dead time.
const fetch = fetchFile(dir, "adopt.json", quietCurrencyFixture())
snapshot(state, fetch)
patchState(state, { dead_time_seconds: 5000, last_activity_at: isoAgo(6 * 3600) })
const prior = readState(state)
const r = spawnSync("python3", [SCRIPT, "snapshot", "--pr", "1", "--repo", "o/r",
"--state-dir", state, "--fetch-file", fetch, "--continue-invocation",
"--invocation-id", "adopted-new-id", "--session-started-at", prior.started_at,
"--invocation-budget-seconds", String(prior.invocation_budget_seconds)],
{ encoding: "utf8" })
expect(r.status, r.stderr).toBe(0)
const after = readState(state)
expect(after.invocation_id).toBe("adopted-new-id")
expect(after.dead_time_seconds).toBe(0)
}, 20000)
test("managed-stack continuation carries accumulated dead time to the next layer's state dir", () => {
// Codex P2: the shared active-time budget spans stack layers, but dead time is per-state-dir.
// --continue-dead-time-seconds threads the prior layer's excluded-suspend total into the new
// layer so it is not re-counted as active. Absent the arg, an adopt still resets to 0.
const fetch = fetchFile(dir, "stack-carry.json", quietCurrencyFixture())
snapshot(state, fetch)
const prior = readState(state)
const cont = (stateDir: string, extra: string[]) => spawnSync("python3",
[SCRIPT, "snapshot", "--pr", "1", "--repo", "o/r", "--state-dir", stateDir, "--fetch-file", fetch,
"--continue-invocation", "--invocation-id", prior.invocation_id,
"--session-started-at", prior.started_at,
"--invocation-budget-seconds", String(prior.invocation_budget_seconds), ...extra],
{ encoding: "utf8" })
const layer2 = path.join(dir, "layer2")
const r = cont(layer2, ["--continue-dead-time-seconds", "4200"])
expect(r.status, r.stderr).toBe(0)
expect(readState(layer2).dead_time_seconds).toBe(4200)
const layer3 = path.join(dir, "layer3")
const r2 = cont(layer3, [])
expect(r2.status, r2.stderr).toBe(0)
expect(readState(layer3).dead_time_seconds).toBe(0)
}, 20000)
test("carry arg on a same-id re-continue raises the dead-time floor without clobbering accumulation", () => {
// Bugbot: the carry must be honored on a later same-id re-continue (early-return path), not only
// the first adopt. It sets a monotonic floor — raises 0 to the carry, never lowers a larger value.
const fetch = fetchFile(dir, "recont.json", quietCurrencyFixture())
snapshot(state, fetch)
const prior = readState(state)
const layer = path.join(dir, "recont-layer")
const cont = (extra: string[]) => spawnSync("python3",
[SCRIPT, "snapshot", "--pr", "1", "--repo", "o/r", "--state-dir", layer, "--fetch-file", fetch,
"--continue-invocation", "--invocation-id", prior.invocation_id,
"--session-started-at", prior.started_at,
"--invocation-budget-seconds", String(prior.invocation_budget_seconds), ...extra],
{ encoding: "utf8" })
expect(cont([]).status).toBe(0) // first continue (adopt), no carry -> 0
expect(readState(layer).dead_time_seconds).toBe(0)
expect(cont(["--continue-dead-time-seconds", "3000"]).status).toBe(0) // re-continue raises floor
expect(readState(layer).dead_time_seconds).toBe(3000)
expect(cont(["--continue-dead-time-seconds", "1000"]).status).toBe(0) // lower carry never clobbers
expect(readState(layer).dead_time_seconds).toBe(3000)
}, 20000)
test("branch currency: a carried semantic park wakes only for inspection and unchanged evidence stays parked", () => {
const dirty = quietCurrencyFixture({ mergeable: "CONFLICTING", merge_state_status: "DIRTY" })
const original = snapshot(state, fetchFile(dir, "currency-inspect-1.json", dirty))
markCurrency(state, original.branch_currency.key, "needs-human", "conflict-v1")
const moved = snapshot(state, fetchFile(dir, "currency-inspect-2.json", quietCurrencyFixture({
mergeable: "CONFLICTING",
merge_state_status: "DIRTY",
base: { host: "github.com", repository: "o/r", ref: "main", oid: "base-2" },
})))
expect(moved.branch_currency.attention).toBe("inspect")
expect(wakeReason(moved)).toBe("branch-currency")
const prematureClaim = spawnSync("python3", [SCRIPT, "mark", "--state-dir", state,
...persistedInvocationArgs(state), "--currency-key", moved.branch_currency.key,
"--currency-disposition", "claimed"], { encoding: "utf8" })
expect(prematureClaim.status).not.toBe(0)
const missingInspectionResidual = spawnSync("python3", [SCRIPT, "mark", "--state-dir", state,
...persistedInvocationArgs(state), "--currency-key", moved.branch_currency.key,
"--currency-inspected-fingerprint", "conflict-v1"], { encoding: "utf8" })
expect(missingInspectionResidual.status).not.toBe(0)
expect(missingInspectionResidual.stderr).toMatch(/residual-file/)
expect(snapshot(state, fetchFile(dir, "currency-inspect-missing-residual.json", quietCurrencyFixture({
mergeable: "CONFLICTING",
merge_state_status: "DIRTY",
base: { host: "github.com", repository: "o/r", ref: "main", oid: "base-2" },
}))).branch_currency).toMatchObject({ disposition: "open", attention: "inspect" })
markCurrencyInspection(state, moved.branch_currency.key, "conflict-v1")
const unchanged = snapshot(state, fetchFile(dir, "currency-inspect-same.json", quietCurrencyFixture({
mergeable: "CONFLICTING",
merge_state_status: "DIRTY",
base: { host: "github.com", repository: "o/r", ref: "main", oid: "base-2" },
})))
expect(unchanged.branch_currency).toMatchObject({
disposition: "open",
attention: null,
recovery_state: "semantic-unchanged",
mutation_requires_answer: true,
})
const unchangedAfterCapabilityDrift = snapshot(state, fetchFile(
dir, "currency-inspect-capability-drift.json", quietCurrencyFixture({
mergeable: "CONFLICTING",
merge_state_status: "DIRTY",
host_branch_update_capability: false,
base: { host: "github.com", repository: "o/r", ref: "main", oid: "base-2" },
})))
expect(unchangedAfterCapabilityDrift.branch_currency).toMatchObject({
disposition: "open",
attention: null,
recovery_state: "semantic-unchanged",
})
expect(unchangedAfterCapabilityDrift.open_needs_human).toBe(1)
const changed = snapshot(state, fetchFile(dir, "currency-inspect-3.json", quietCurrencyFixture({
mergeable: "CONFLICTING",
merge_state_status: "DIRTY",
base: { host: "github.com", repository: "o/r", ref: "main", oid: "base-3" },
})))
expect(changed.branch_currency.attention).toBe("inspect")
markCurrencyInspection(state, changed.branch_currency.key, "conflict-v2")
expect(snapshot(state, fetchFile(dir, "currency-inspect-changed.json", quietCurrencyFixture({
mergeable: "CONFLICTING",
merge_state_status: "DIRTY",
base: { host: "github.com", repository: "o/r", ref: "main", oid: "base-3" },
}))).branch_currency).toMatchObject({
disposition: "open",
attention: "claim",
inspection_result: "changed",
})
const persisted = JSON.parse(readFileSync(path.join(state, "state.json"), "utf8"))
expect(persisted.branch_currency_state.semantic_parks).toEqual({})
const nextBase = snapshot(state, fetchFile(dir, "currency-inspect-4.json", quietCurrencyFixture({
mergeable: "CONFLICTING",
merge_state_status: "DIRTY",
base: { host: "github.com", repository: "o/r", ref: "main", oid: "base-4" },
})))
expect(nextBase.branch_currency).toMatchObject({
disposition: "open",
attention: "claim",
parked_semantic_fingerprints: [],
})
})
test("branch currency: unresolved claims survive base and expected head movement until reconciled", () => {
const first = snapshot(state, fetchFile(dir, "currency-key-1.json", currencyFixture()))
markCurrency(state, first.branch_currency.key, "claimed")
expect(snapshot(state, fetchFile(dir, "currency-key-1b.json", currencyFixture())).branch_currency.disposition).toBe("claimed")
const movedWhileClaimed = snapshot(state, fetchFile(dir, "currency-key-2.json", currencyFixture({
base: { host: "github.com", repository: "o/r", ref: "main", oid: "base-2" },
})))
expect(movedWhileClaimed.branch_currency).toMatchObject({
key: first.branch_currency.key,
disposition: "claimed",
reconciliation_only: true,
})
expect(movedWhileClaimed.branch_currency_blocker.key).toBe(first.branch_currency.key)
const resumed = snapshot(state, fetchFile(dir, "currency-key-2-resumed.json", currencyFixture({
base: { host: "github.com", repository: "o/r", ref: "main", oid: "base-2" },
})), ["--start-invocation", "--invocation-budget-seconds", ORDINARY_TEST_BUDGET_SECONDS])
expect(resumed.branch_currency.attention).toBe("reconcile")
markCurrencyOutcome(state, first.branch_currency.key, "proven-no-mutation")
const statePath = path.join(state, "state.json")
const retryState = JSON.parse(readFileSync(statePath, "utf8"))
retryState.branch_currency_state.items[first.branch_currency.key].retry_not_before = "2000-01-01T00:00:00Z"
writeFileSync(statePath, JSON.stringify(retryState))
const movedBase = snapshot(state, fetchFile(dir, "currency-key-2-open.json", currencyFixture({
base: { host: "github.com", repository: "o/r", ref: "main", oid: "base-2" },
})))
expect(movedBase.branch_currency.key).not.toBe(first.branch_currency.key)
expect(movedBase.branch_currency.disposition).toBe("open")
markCurrency(state, movedBase.branch_currency.key, "claimed")
markCurrencyOutcome(state, movedBase.branch_currency.key, "mutation-observed")
const movedHead = snapshot(state, fetchFile(dir, "currency-key-3.json", currencyFixture({
head_sha: "s2",
mergeable: "MERGEABLE",
merge_state_status: "CLEAN",
base: { host: "github.com", repository: "o/r", ref: "main", oid: "base-2" },
})))
expect(movedHead.branch_currency).toMatchObject({
key: movedBase.branch_currency.key,
disposition: "claimed",
mutation_consumed: true,
reconciliation_only: true,
})
expect(movedHead.branch_currency_blocker.key).toBe(movedBase.branch_currency.key)
markCurrency(state, movedBase.branch_currency.key, "confirmed")
const confirmed = snapshot(state, fetchFile(dir, "currency-key-3-confirmed.json", currencyFixture({
head_sha: "s2",
mergeable: "MERGEABLE",
merge_state_status: "CLEAN",
base: { host: "github.com", repository: "o/r", ref: "main", oid: "base-2" },
})))
expect(confirmed.branch_currency).toBeNull()
expect(confirmed.branch_currency_blocker).toBeNull()
const persisted = JSON.parse(readFileSync(path.join(state, "state.json"), "utf8"))
expect(persisted.branch_currency_state.current_key).toBeNull()
}, 20000)
test("branch currency: a decision is invocation-fenced and changed observations invalidate it", () => {
const dirty = currencyFixture({ mergeable: "CONFLICTING", merge_state_status: "DIRTY" })
const original = snapshot(state, fetchFile(dir, "currency-residual-head-1.json", dirty))
markCurrency(state, original.branch_currency.key, "needs-human", "conflict-v1")
const parked = snapshot(state, fetchFile(dir, "currency-residual-head-parked.json", dirty))
expect(parked.needs_human_residuals).toHaveLength(1)
expect(parked.branch_currency).toMatchObject({ disposition: "open", attention: null })
const stale = spawnSync("python3", [SCRIPT, "mark", "--state-dir", state,
"--invocation-id", "stale", "--session-started-at", parked.invocation_started_at,
"--invocation-budget-seconds", String(parked.invocation_budget_seconds),
"--currency-key", original.branch_currency.key, "--currency-disposition", "open"],
{ encoding: "utf8" })
expect(stale.status).not.toBe(0)
const moved = snapshot(state, fetchFile(dir, "currency-residual-head-2.json", currencyFixture({
head_sha: "s2",
mergeable: "CONFLICTING",
merge_state_status: "DIRTY",
})))
expect(moved.needs_human_residuals).toEqual([])
expect(moved.open_needs_human).toBe(0)
expect(moved.branch_currency).toMatchObject({
disposition: "open",
attention: "inspect",
parked_semantic_fingerprints: ["conflict-v1"],
})
expect(moved.branch_currency.key).not.toBe(original.branch_currency.key)
}, 15000)
test("a generic answer consumes a currency decision and prevents exact re-parking", () => {
const dirty = currencyFixture({ mergeable: "CONFLICTING", merge_state_status: "DIRTY" })
const observed = snapshot(state, fetchFile(dir, "currency-answer.json", dirty))
markCurrency(state, observed.branch_currency.key, "needs-human", "conflict-v1")
const parked = snapshot(state, fetchFile(dir, "currency-answer-parked.json", dirty))
const decisionId = parked.human_decisions[0].decision_id
const wrong = spawnSync("python3", [SCRIPT, "mark", "--state-dir", state,
...persistedInvocationArgs(state), "--answer-decision", "decision:wrong",
"--answer-file", fetchFile(dir, "wrong-answer.txt", "Option 2")], { encoding: "utf8" })
expect(wrong.status).not.toBe(0)
markDecisionAnswered(state, decisionId)
const resumed = snapshot(state, fetchFile(dir, "currency-answer-resumed.json", dirty))
expect(resumed.needs_human_residuals).toEqual([])
expect(resumed.answered_human_decisions).toHaveLength(1)
expect(resumed.answered_human_decisions[0]).toMatchObject({
id: decisionId,
answer: "Option 2: regenerate the fixture from current source.",
})
expect(resumed.branch_currency).toMatchObject({ disposition: "open", attention: "claim" })
const residual = residualFile(dir, "currency-repark.json", observed.branch_currency.key, "currency")
const repark = spawnSync("python3", [SCRIPT, "mark", "--state-dir", state,
...persistedInvocationArgs(state), "--disposition", "needs-human",
"--residual-file", residual.path, "--semantic-conflict-fingerprint", "conflict-v1"],
{ encoding: "utf8" })
expect(repark.status).not.toBe(0)
expect(repark.stderr).toMatch(/already have a recorded human answer/)
}, 15000)
test("a decision answer rejects every source and currency action modifier atomically", () => {
const dirty = currencyFixture({ mergeable: "CONFLICTING", merge_state_status: "DIRTY" })
const observed = snapshot(state, fetchFile(dir, "answer-action-exclusive.json", dirty))
markCurrency(state, observed.branch_currency.key, "needs-human", "conflict-v1")
const parked = snapshot(state, fetchFile(dir, "answer-action-exclusive-parked.json", dirty))
const decisionId = parked.human_decisions[0].decision_id
const answerPath = fetchFile(dir, "answer-action-exclusive.txt", "Option 2")
const residualPath = residualFile(
dir, "answer-action-exclusive-residual.json", observed.branch_currency.key, "currency").path
const modifiers = [
["--check", "CI/test"],
["--thread", "thread-1"],
["--comment", "comment-1"],
["--residual-file", residualPath],
["--acted-edit-id", "edit-1"],
["--currency-key", observed.branch_currency.key],
["--currency-disposition", "open"],
["--semantic-conflict-fingerprint", "conflict-v1"],
["--currency-outcome", "ambiguous"],
["--currency-inspected-fingerprint", "conflict-v1"],
["--disposition", "open"],
]
for (const modifier of modifiers) {
const before = readFileSync(path.join(state, "state.json"), "utf8")
const rejected = spawnSync("python3", [SCRIPT, "mark", "--state-dir", state,
...persistedInvocationArgs(state), "--answer-decision", decisionId,
"--answer-file", answerPath, ...modifier], { encoding: "utf8" })
expect(rejected.status, modifier.join(" ")).not.toBe(0)
expect(rejected.stderr).toMatch(/answer marks cannot be combined with source actions/)
expect(readFileSync(path.join(state, "state.json"), "utf8")).toBe(before)
}
}, 15000)
test("branch currency: dependents do not block a normal-base root, but managed, open-parent, and uncertain routes do", () => {
const dependentRoot = snapshot(path.join(dir, "currency-dependent-root"), fetchFile(dir, "currency-dependent-root.json", currencyFixture({
pr_chain: {
manager_status: "absent",
relationship_status: "dependent",
default_branch: "main",
parent_prs: [],
dependent_prs: [{ number: 2, state: "OPEN", baseRefName: "feature", headRefName: "child" }],
},
})))
expect(dependentRoot.branch_currency.route).toBe("normal-base")
const exclusions = [
{ manager_status: "confirmed", relationship_status: "independent", default_branch: "main", parent_prs: [], dependent_prs: [] },
{ manager_status: "absent", relationship_status: "dependent", default_branch: "main", parent_prs: [{ number: 3, state: "OPEN" }], dependent_prs: [] },
{ manager_status: "probe-error", relationship_status: "independent", default_branch: "main", parent_prs: [], dependent_prs: [] },
{ manager_status: "absent", relationship_status: "probe-error", default_branch: "main", parent_prs: [], dependent_prs: [] },
{ manager_status: "absent", relationship_status: "independent", default_branch: null, parent_prs: [], dependent_prs: [] },
]
for (const [index, prChain] of exclusions.entries()) {
const value = snapshot(path.join(dir, `currency-excluded-${index}`), fetchFile(dir, `currency-excluded-${index}.json`, currencyFixture({ pr_chain: prChain })))
expect(value.branch_currency).toBeNull()
}
}, 15000)
test("live fetch binds mergeability to GraphQL base/head parents plus the host-qualified exact ref", () => {
const python = `
import json
from importlib.machinery import SourceFileLoader
m = SourceFileLoader("prs", ${JSON.stringify(SCRIPT)}).load_module()
calls = []
historical_oid = "1111111111111111111111111111111111111111"
reported_base_oid = "2222222222222222222222222222222222222222"
current_base_oid = reported_base_oid
head_oid = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
identity_head_oid = head_oid
identity_base_ref = "main"
ref_probe_status = 0
class Result: pass
def checked(cmd, label):
calls.append(cmd)
result = Result()
result.returncode = 0
result.stderr = ""
result.stdout = json.dumps({
"state": "OPEN", "mergeable": "MERGEABLE", "mergeStateStatus": "BEHIND",
"reviewDecision": "APPROVED", "headRefOid": head_oid,
"baseRefOid": historical_oid,
"baseRefName": "main", "headRefName": "feature", "number": 7,
"url": "https://ghe.acme.test/o/r/pull/7", "statusCheckRollup": [],
"author": {"login": "author"}, "comments": [], "reviews": []})
return result
def run(cmd):
calls.append(cmd)
result = Result()
if "graphql" in cmd:
result.returncode = 0
result.stderr = ""
result.stdout = json.dumps({"data": {"repository": {"pullRequest": {
"mergeable": "MERGEABLE", "mergeStateStatus": "BEHIND",
"headRefOid": identity_head_oid, "baseRefOid": historical_oid,
"baseRefName": identity_base_ref,
"viewerCanUpdateBranch": True,
"baseRef": {"target": {"oid": reported_base_oid}},
"potentialMergeCommit": {"oid": "4444444444444444444444444444444444444444",
"parents": {"nodes": [{"oid": reported_base_oid}, {"oid": head_oid}]}}
}}}})
return result
result.returncode = ref_probe_status
result.stderr = "base ref probe failed" if result.returncode else ""
result.stdout = current_base_oid + "\\n" if result.returncode == 0 else ""
return result
m._run_checked = checked
m._run = run
m.fetch_eyes_reactors = lambda *args: []
m.fetch_threads = lambda *args: []
m.fetch_awaiting_approval = lambda *args: 0
m.fetch_pr_chain = lambda *args: {"manager_status": "absent", "relationship_status": "independent",
"default_branch": "main", "parent_prs": [], "dependent_prs": []}
current = m.fetch(7, "ghe.acme.test/o/r")
identity_head_oid = head_oid.upper()
same_head_mixed_case = m.fetch(7, "ghe.acme.test/o/r")
identity_head_oid = "6666666666666666666666666666666666666666"
head_race = m.fetch(7, "ghe.acme.test/o/r")
identity_head_oid = head_oid
current_base_oid = "5555555555555555555555555555555555555555"
race = m.fetch(7, "ghe.acme.test/o/r")
ref_probe_status = 1
probe_error = m.fetch(7, "ghe.acme.test/o/r")
print(json.dumps({"current": current, "same_head_mixed_case": same_head_mixed_case,
"head_race": head_race, "race": race,
"probe_error": probe_error, "calls": calls}))
`
const r = spawnSync("python3", ["-c", python], { encoding: "utf8" })
expect(r.status, r.stderr).toBe(0)
const result = JSON.parse(r.stdout)
expect(result.calls[0].join(" ")).toContain("baseRefOid")
const refCalls = result.calls.filter((call: string[]) => call.includes("repos/o/r/git/ref/heads/main"))
expect(refCalls).toHaveLength(5)
for (const call of refCalls) {
expect(call).toContain("--hostname")
expect(call).toContain("ghe.acme.test")
}
const identityCalls = result.calls.filter((call: string[]) => call.includes("graphql") && call.join(" ").includes("potentialMergeCommit"))
expect(identityCalls).toHaveLength(5)
for (const call of identityCalls) {
expect(call).toContain("--hostname")
expect(call).toContain("ghe.acme.test")
}
expect(result.current.base).toEqual({
host: "ghe.acme.test",
repository: "o/r",
ref: "main",
oid: "2222222222222222222222222222222222222222",
graphql_oid: "2222222222222222222222222222222222222222",
historical_oid: "1111111111111111111111111111111111111111",
merge_commit_oid: "4444444444444444444444444444444444444444",
merge_parent_oids: ["2222222222222222222222222222222222222222", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"],
identity: "current",
})
expect(result.current.host_branch_update_capability).toBe(true)
expect(result.same_head_mixed_case.base.identity).toBe("current")
expect(result.head_race.head_sha).toBe("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
expect(result.head_race.base.identity).toBe("race")
expect(result.head_race.host_branch_update_capability).toBe("unknown")
expect(result.race.base.identity).toBe("race")
expect(result.race.base.oid).toBe("5555555555555555555555555555555555555555")
expect(result.race.host_branch_update_capability).toBe("unknown")
expect(result.probe_error.base.identity).toBe("probe-error")
expect(result.probe_error.base.oid).toBeNull()
expect(result.probe_error.host_branch_update_capability).toBe("unknown")
})
test("source marks reject a residual payload unless the disposition is needs-human", () => {
const fetch = fetchFile(dir, "residual-dispatched.json", { ...FAILING, threads: [] })
snapshot(state, fetch)
const statePath = path.join(state, "state.json")
const before = readFileSync(statePath, "utf8")
const cases = [
{ flag: "--check", id: "CI/test", kind: "check" as const },
{ flag: "--thread", id: "T1", kind: "thread" as const },
{ flag: "--comment", id: "I1", kind: "comment" as const },
]
for (const item of cases) {
const residual = residualFile(
dir, `rejected-${item.kind}.json`, item.id, item.kind)
const result = spawnSync("python3", [SCRIPT, "mark", "--state-dir", state,
...persistedInvocationArgs(state), item.flag, item.id,
"--residual-file", residual.path], { encoding: "utf8" })
expect(result.status).not.toBe(0)
expect(result.stderr).toContain("--residual-file requires --disposition needs-human")
expect(readFileSync(statePath, "utf8")).toBe(before)
}
})
test("currency marks reject a residual payload when the action does not need a decision", () => {
const fetch = fetchFile(dir, "currency-residual-claimed.json", quietCurrencyFixture())
const observed = snapshot(state, fetch)
const statePath = path.join(state, "state.json")
const before = readFileSync(statePath, "utf8")
const residual = residualFile(
dir, "rejected-currency-claimed.json", observed.branch_currency.key, "currency")
const result = spawnSync("python3", [SCRIPT, "mark", "--state-dir", state,
...persistedInvocationArgs(state), "--currency-key", observed.branch_currency.key,
"--currency-disposition", "claimed", "--residual-file", residual.path],
{ encoding: "utf8" })
expect(result.status).not.toBe(0)
expect(result.stderr).toContain(
"--residual-file requires a currency outcome that needs a human decision")
expect(readFileSync(statePath, "utf8")).toBe(before)
})
test("live fetch preserves a legacy StatusContext creation identity", () => {
const python = `
import json
from importlib.machinery import SourceFileLoader
m = SourceFileLoader("prs_status", ${JSON.stringify(SCRIPT)}).load_module()
class Result: pass
def checked(cmd, label):
result = Result()
result.returncode = 0
result.stderr = ""
result.stdout = json.dumps({
"state": "OPEN", "mergeable": "MERGEABLE", "mergeStateStatus": "CLEAN",
"reviewDecision": None, "headRefOid": "a" * 40, "baseRefOid": "b" * 40,
"baseRefName": "main", "headRefName": "feature", "number": 7,
"url": "https://github.com/o/r/pull/7", "author": {"login": "author"},
"comments": [], "reviews": [],
"statusCheckRollup": [{"__typename": "StatusContext", "context": "legacy-ci",
"state": "FAILURE", "targetUrl": "https://ci.example.test/build",
"createdAt": "2026-08-20T04:00:00Z", "description": "failed"}]})
return result
m._run_checked = checked
m.fetch_pr_merge_identity = lambda *args: None
m.fetch_base_ref = lambda *args: {"identity": "current", "oid": "b" * 40}
m.fetch_eyes_reactors = lambda *args: []
m.fetch_threads = lambda *args: []
m.fetch_awaiting_approval = lambda *args: 0
m.fetch_pr_chain = lambda *args: {"manager_status": "absent", "relationship_status": "independent",
"default_branch": "main", "parent_prs": [], "dependent_prs": []}
print(json.dumps(m.fetch(7, "o/r")["checks"][0]))
`
const r = spawnSync("python3", ["-c", python], { encoding: "utf8" })
expect(r.status, r.stderr).toBe(0)
expect(JSON.parse(r.stdout)).toEqual({
key: "legacy-ci",
name: "legacy-ci",
status: "COMPLETED",
conclusion: "FAILURE",
details_url: "https://ci.example.test/build",
created_at: "2026-08-20T04:00:00Z",
started_at: null,
completed_at: null,
})
})
test("base-ref freshness blocks readiness, resets quiet on current-to-stale, and fails closed on probe error", () => {
const clean = {
...FAILING,
merge_state_status: "CLEAN",
review_decision: "APPROVED",
checks: [{ key: "CI/test", name: "test", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }],
threads: [],
}
const currentFile = fetchFile(dir, "base-current.json", clean)
snapshot(state, currentFile)
const statePath = path.join(state, "state.json")
const settled = JSON.parse(readFileSync(statePath, "utf8"))
settled.last_change_at = "2026-07-17T12:00:00+00:00"
writeFileSync(statePath, JSON.stringify(settled))
const current = snapshot(state, currentFile)
expect(current.base_ref_blocker).toBeNull()
expect(current.mergeability_certain).toBe(true)
expect(current.changed_this_tick).toBe(false)
expect(current.quiet_seconds).toBeGreaterThan(60)
expect(wakeReason(current, 0)).toBe("merge-ready")
const stale = snapshot(state, fetchFile(dir, "base-stale.json", {
...clean,
base: {
host: "github.com",
repository: "o/r",
ref: "main",
oid: "base-2",
pr_oid: "base-1",
freshness: "stale",
},
}))
expect(stale.base_ref_blocker).toBe("stale")
expect(stale.mergeability_certain).toBe(false)
expect(stale.changed_this_tick).toBe(true)
expect(stale.quiet_seconds).toBeLessThan(2)
expect(stale.branch_currency).toBeNull()
expect(wakeReason(stale, 0)).toBe("base-ref-blocked")
const probeError = snapshot(state, fetchFile(dir, "base-probe-error.json", {
...clean,
base: {
host: "github.com",
repository: "o/r",
ref: "main",
oid: null,
pr_oid: "base-1",
freshness: "probe-error",
},
}))
expect(probeError.base_ref_blocker).toBe("probe-error")
expect(probeError.mergeability_certain).toBe(false)
expect(probeError.branch_currency).toBeNull()
expect(wakeReason(probeError, 0)).toBe("base-ref-blocked")
})
test("historical base movement does not block a merge computation proven against the current base", () => {
const historicalBase = "60a8e4348581471105797264808676f1f562bea5"
const liveBase = "5c8913cd7466b57bed5aee0d9809bf90b9e83115"
const head = "d0108be80bf04447ee768dfb6c925301c4cdc74f"
const clean = {
...FAILING,
head_sha: head,
merge_state_status: "CLEAN",
review_decision: "APPROVED",
checks: [{ key: "CI/test", name: "test", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }],
threads: [],
base: {
host: "github.com",
repository: "Esper-Labs/nugget",
ref: "main",
oid: liveBase,
graphql_oid: liveBase,
historical_oid: historicalBase,
merge_commit_oid: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
merge_parent_oids: [liveBase, head],
identity: "current",
},
}
const current = snapshot(state, fetchFile(dir, "historical-base-current.json", clean))
expect(current.base_ref_blocker).toBeNull()
expect(current.mergeability_certain).toBe(true)
expect(current.base.historical_oid).toBe(historicalBase)
expect(current.base.oid).toBe(liveBase)
expect(wakeReason(current, 0)).toBe("merge-ready")
})
test("base identity fails closed for races, pending merge generation, malformed refs, and deleted refs", () => {
const race = probeBaseIdentity({ refOid: "5".repeat(40) }).base
const pending = probeBaseIdentity({ mergeCommitOid: null }).base
expect(race.identity).toBe("race")
expect(pending.identity).toBe("mergeability-pending")
expect(probeBaseIdentity({ refOid: "not-a-sha" }).base.identity).toBe("probe-error")
expect(probeBaseIdentity({ refStatus: 1 }).base.identity).toBe("probe-error")
expect(probeBaseIdentity({ parentOids: ["2".repeat(40)] }).base.identity).toBe("probe-error")
expect(probeBaseIdentity({ parentOids: ["not-an-oid", "3".repeat(40)] }).base.identity).toBe("probe-error")
expect(probeBaseIdentity({ parentOids: ["3".repeat(40), "2".repeat(40)] }).base.identity).toBe("race")
expect(probeBaseIdentity({ parentOids: ["5".repeat(40), "3".repeat(40)] }).base.identity).toBe("race")
expect(probeBaseIdentity({
mergeable: "CONFLICTING",
mergeStateStatus: "CLEAN",
mergeCommitOid: null,
}).base.identity).toBe("mergeability-pending")
for (const [name, base] of [["race", race], ["pending", pending]] as const) {
const value = snapshot(path.join(dir, `base-${name}`), fetchFile(dir, `base-${name}.json`, {
...FAILING,
head_sha: "3".repeat(40),
merge_state_status: "CLEAN",
review_decision: "APPROVED",
checks: [{ key: "CI/test", name: "test", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }],
threads: [],
base,
}))
expect(value.base_ref_blocker).toBe(base.identity)
expect(value.mergeability_certain).toBe(false)
expect(wakeReason(value, 0)).toBe("base-ref-blocked")
}
})
test("base identity accepts SHA-1 and SHA-256 object IDs and routes GitHub Enterprise probes", () => {
for (const length of [40, 64]) {
const baseOid = "a".repeat(length)
const headOid = "b".repeat(length)
const result = probeBaseIdentity({
refOid: baseOid,
graphqlOid: baseOid,
headOid,
historicalOid: "c".repeat(length),
mergeCommitOid: "d".repeat(length),
parentOids: [baseOid, headOid],
})
expect(result.base.identity).toBe("current")
expect(result.calls[0]).toContain("--hostname")
expect(result.calls[0]).toContain("ghe.acme.test")
}
})
test("private REST ref 404 falls back to an exact non-interactive Git ref probe", () => {
const baseOid = "a".repeat(40)
const headOid = "b".repeat(40)
const result = probeBaseIdentity({
refStatus: 1,
refError: "gh: Not Found (HTTP 404)",
gitStatus: 0,
gitOutput: `${baseOid}\trefs/heads/main\n`,
graphqlOid: baseOid,
headOid,
mergeCommitOid: "d".repeat(40),
parentOids: [baseOid, headOid],
})
expect(result.base.identity).toBe("current")
expect(result.base.oid).toBe(baseOid)
expect(result.calls).toHaveLength(2)
expect(result.calls[1]).toEqual([
"git", "-c", "core.askPass=",
"-c", "credential.helper=",
"-c", "credential.helper=!gh auth git-credential",
"ls-remote", "--exit-code", "--refs",
"https://ghe.acme.test/o/r.git", "refs/heads/main",
])
expect(probeBaseIdentity({
refStatus: 1,
refError: "gh: Not Found (HTTP 404)",
gitStatus: 1,
}).base.identity).toBe("probe-error")
expect(probeBaseIdentity({
refStatus: 1,
refError: "gh: Not Found (HTTP 404)",
gitStatus: 0,
gitOutput: `${baseOid}\trefs/heads/not-main\n`,
}).base.identity).toBe("probe-error")
expect(probeBaseIdentity({
refStatus: 1,
refError: "gh: Not Found (HTTP 404)",
gitStatus: 0,
gitOutput: `not-an-oid\trefs/heads/main\n`,
}).base.identity).toBe("probe-error")
expect(probeBaseIdentity({
refStatus: 1,
refError: "gh: Not Found (HTTP 404)",
gitStatus: 0,
gitOutput: `${baseOid}\trefs/heads/main\n${baseOid}\trefs/heads/main\n`,
}).base.identity).toBe("probe-error")
expect(probeBaseIdentity({
refStatus: 1,
refError: "gh: Not Found (HTTP 404)",
gitTimeout: true,
}).base.identity).toBe("probe-error")
expect(probeBaseIdentity({
refStatus: 1,
refError: "gh: Not Found (HTTP 404)",
gitOSError: true,
}).base.identity).toBe("probe-error")
const forbidden = probeBaseIdentity({
refStatus: 1,
refError: "gh: Forbidden (HTTP 403)",
gitStatus: 0,
gitOutput: `${baseOid}\trefs/heads/main\n`,
})
expect(forbidden.base.identity).toBe("probe-error")
expect(forbidden.calls).toHaveLength(1)
})
test("private ref fallback clears configured helpers and delegates credentials to gh auth", () => {
const credentialDir = mkdtempSync(path.join(tmpdir(), "ce-babysit-pr-credential-"))
const fakeGh = path.join(credentialDir, "gh")
const poisonHelper = path.join(credentialDir, "poison-helper")
const poisonMarker = path.join(credentialDir, "poison-invoked")
writeFileSync(fakeGh, `#!/bin/sh
if [ "$1 $2 $3" != "auth git-credential get" ]; then
exit 92
fi
printf 'username=oauth-user\\npassword=session-token\\n'
`)
writeFileSync(poisonHelper, `#!/bin/sh
: > "$PR_SNAPSHOT_POISON_MARKER"
exit 91
`)
chmodSync(fakeGh, 0o755)
chmodSync(poisonHelper, 0o755)
writeFileSync(path.join(credentialDir, ".gitconfig"), `[credential]
\thelper = !${poisonHelper}
`)
const result = spawnSync("git", [
"-c", "core.askPass=",
"-c", "credential.helper=",
"-c", "credential.helper=!gh auth git-credential",
"credential", "fill",
], {
encoding: "utf8",
input: "protocol=https\nhost=ghe.acme.test\n\n",
env: {
...process.env,
HOME: credentialDir,
GIT_CONFIG_NOSYSTEM: "1",
GIT_TERMINAL_PROMPT: "0",
GCM_INTERACTIVE: "never",
GIT_ASKPASS: "",
SSH_ASKPASS: "",
PATH: `${credentialDir}:${process.env.PATH ?? ""}`,
PR_SNAPSHOT_POISON_MARKER: poisonMarker,
},
})
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain("username=oauth-user")
expect(result.stdout).toContain("password=session-token")
expect(existsSync(poisonMarker)).toBe(false)
})
test("Git ref probes are bounded and non-interactive", () => {
const r = spawnSync("python3", ["-c", `
import json, os
from importlib.machinery import SourceFileLoader
m = SourceFileLoader("pr_snapshot", ${JSON.stringify(SCRIPT)}).load_module()
observed = {}
class Result:
returncode = 1
stdout = ""
stderr = ""
def run(cmd, **kwargs):
observed.update(kwargs)
observed["inherited"] = kwargs["env"].get("PR_SNAPSHOT_TEST_ENV")
return Result()
m.subprocess.run = run
os.environ["PR_SNAPSHOT_TEST_ENV"] = "preserved"
m._run_git(["git", "ls-remote"])
print(json.dumps({
"prompt": observed["env"].get("GIT_TERMINAL_PROMPT"),
"gcm": observed["env"].get("GCM_INTERACTIVE"),
"askpass": observed["env"].get("GIT_ASKPASS"),
"ssh_askpass": observed["env"].get("SSH_ASKPASS"),
"inherited": observed["inherited"],
"timeout": observed["timeout"],
}))
`], { encoding: "utf8" })
expect(r.status, r.stderr).toBe(0)
expect(JSON.parse(r.stdout)).toEqual({
prompt: "0",
gcm: "never",
askpass: "",
ssh_askpass: "",
inherited: "preserved",
timeout: 30,
})
})
test("DIRTY conflict state does not require a generated test merge commit", () => {
const baseOid = "a".repeat(40)
const result = probeBaseIdentity({
refOid: baseOid,
graphqlOid: baseOid,
mergeable: "CONFLICTING",
mergeStateStatus: "DIRTY",
mergeCommitOid: null,
})
expect(result.base.identity).toBe("current")
expect(result.base.merge_commit_oid).toBeNull()
})
test("BEHIND emits branch currency when current merge identity is proven despite historical base movement", () => {
const historicalBase = "1".repeat(40)
const liveBase = "2".repeat(40)
const head = "3".repeat(40)
const behind = snapshot(state, fetchFile(dir, "behind-historical-base.json", quietCurrencyFixture({
head_sha: head,
base: {
host: "github.com",
repository: "o/r",
ref: "main",
oid: liveBase,
graphql_oid: liveBase,
historical_oid: historicalBase,
merge_commit_oid: "4".repeat(40),
merge_parent_oids: [liveBase, head],
identity: "current",
},
})))
expect(behind.base_ref_blocker).toBeNull()
expect(behind.mergeability_certain).toBe(true)
expect(behind.branch_currency).toMatchObject({ status: "BEHIND", base_oid: liveBase, head_sha: head })
expect(wakeReason(behind, 0)).toBe("branch-currency")
})
test("watch does not turn ordinary historical base movement into a standing base-ref residual", () => {
const historicalBase = "1".repeat(40)
const liveBase = "2".repeat(40)
const head = "3".repeat(40)
const clean = {
...FAILING,
head_sha: head,
merge_state_status: "CLEAN",
review_decision: "APPROVED",
checks: [{ key: "CI/test", name: "test", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }],
threads: [],
base: {
host: "github.com",
repository: "o/r",
ref: "main",
oid: liveBase,
graphql_oid: liveBase,
historical_oid: historicalBase,
merge_commit_oid: "4".repeat(40),
merge_parent_oids: [liveBase, head],
identity: "current",
},
}
const cleanFile = fetchFile(dir, "base-moved-current.json", clean)
const cleanState = path.join(dir, "base-moved-current")
snapshot(cleanState, cleanFile)
expect(watch(cleanState, cleanFile, ["--settle-seconds", "0"]).reason).toBe("merge-ready")
}, 15000)
test("first snapshot: thread + failing check are actionable; checks terminal", () => {
const d = snapshot(state, fetchFile(dir, "a.json", FAILING))
expect(d.counts.threads).toBe(1)
expect(d.counts.ci).toBe(1)
expect(d.has_failing_checks).toBe(true)
expect(d.checks_terminal).toBe(true)
})
test("crash-safety: un-marked items stay actionable on the next tick", () => {
const f = fetchFile(dir, "a.json", FAILING)
const first = snapshot(state, f)
const second = snapshot(state, f)
expect(second.counts.threads).toBe(first.counts.threads)
expect(second.counts.ci).toBe(first.counts.ci)
})
test("needs-human thread: silenced despite the resolver's own reply moving identity, but stays visible via open_needs_human", () => {
snapshot(state, fetchFile(dir, "a.json", FAILING))
// The resolver posts decision_context, moving the thread's last-comment identity.
const replied = { ...FAILING, threads: [{ thread_id: "T1", last_comment_id: "C2", last_comment_at: "t2" }] }
const repliedFetch = fetchFile(dir, "b.json", replied)
mark(state, ["--thread", "T1", "--disposition", "needs-human", "--fetch-file", repliedFetch])
const d = snapshot(state, repliedFetch)
expect(d.counts.threads).toBe(0) // no re-actionize (the P1 fix)
expect(d.open_needs_human).toBe(1) // still blocks merge-ready
})
test("needs-human residuals retain stable source identity across resumable ticks", () => {
const sd = path.join(dir, "durable-needs-human")
snapshot(sd, fetchFile(dir, "durable-1.json", FAILING))
const residual = residualFile(dir, "durable-residual.json", "T1", "thread")
const replied = { ...FAILING, threads: [{ thread_id: "T1", last_comment_id: "C2", last_comment_at: "t2" }] }
const repliedFetch = fetchFile(dir, "durable-2.json", replied)
mark(sd, ["--thread", "T1", "--disposition", "needs-human",
"--residual-file", residual.path, "--fetch-file", repliedFetch])
expectCurrentDecision(
snapshot(sd, repliedFetch).needs_human_residuals, residual.value)
const resumed = snapshot(sd, fetchFile(dir, "durable-3.json", replied), ["--start-invocation"])
expectCurrentDecision(resumed.needs_human_residuals, residual.value)
})
test("legacy parked items without a complete residual are re-actionized", () => {
const sd = path.join(dir, "legacy-needs-human")
const fetch = fetchFile(dir, "legacy-needs-human.json", FAILING)
snapshot(sd, fetch)
const statePath = path.join(sd, "state.json")
const legacy = JSON.parse(readFileSync(statePath, "utf8"))
legacy.threads.T1.disposition = "needs-human"
delete legacy.threads.T1.needs_human_residual
delete legacy.needs_human_residuals
writeFileSync(statePath, JSON.stringify(legacy))
const migrated = snapshot(sd, fetch)
expect(migrated.needs_human_residuals).toEqual([])
expect(migrated.open_needs_human).toBe(0)
expect(migrated.counts.threads).toBe(1)
})
test("one residual mark parks a complete group before any snapshot can observe partial state", () => {
const sd = path.join(dir, "grouped-needs-human")
const feedback = {
...FAILING,
checks: [],
threads: [
{ thread_id: "T1", last_comment_id: "C1", last_comment_at: "t1" },
{ thread_id: "T2", last_comment_id: "D1", last_comment_at: "t1" },
],
}
snapshot(sd, fetchFile(dir, "grouped-1.json", feedback))
const grouped = {
...residualFile(dir, "unused.json", "T1", "thread").value,
sources: [{ id: "T1", kind: "thread" }, { id: "T2", kind: "thread" }],
thread_urls: ["https://example.test/thread/T1", "https://example.test/thread/T2"],
}
const groupedPath = fetchFile(dir, "grouped-residual.json", grouped)
mark(sd, ["--disposition", "needs-human", "--residual-file", groupedPath])
const parked = snapshot(sd, fetchFile(dir, "grouped-2.json", feedback))
expectCurrentDecision(parked.needs_human_residuals, grouped)
expect(parked.actionable.threads).toEqual([])
const parkedState = JSON.parse(readFileSync(path.join(sd, "state.json"), "utf8"))
expect(parkedState.threads.T1.disposition).toBe("open")
expect(parkedState.threads.T2.disposition).toBe("open")
// A repeated exact persistence is idempotent rather than re-baselining the group.
mark(sd, ["--disposition", "needs-human", "--residual-file", groupedPath])
expect(snapshot(sd, fetchFile(dir, "grouped-idempotent.json", feedback)).needs_human_residuals)
.toEqual([grouped])
const answered = {
...feedback,
threads: [
{ thread_id: "T1", last_comment_id: "C2", last_comment_at: "t2" },
{ thread_id: "T2", last_comment_id: "D1", last_comment_at: "t1" },
],
}
const reopened = snapshot(sd, fetchFile(dir, "grouped-3.json", answered))
expect(reopened.needs_human_residuals).toEqual([])
expect(reopened.open_needs_human).toBe(0)
expect(reopened.actionable.threads.map((item: any) => item.thread_id).sort()).toEqual(["T1", "T2"])
})
test("a missing check invalidates its residual and cannot leave same-head dispatch behind", () => {
const sd = path.join(dir, "ci-needs-human")
const fetch = fetchFile(dir, "ci-needs-human-1.json", { ...FAILING, threads: [] })
snapshot(sd, fetch)
const residual = residualFile(dir, "ci-residual.json", "CI/test", "check")
mark(sd, ["--disposition", "needs-human", "--residual-file", residual.path])
const parked = snapshot(sd, fetch)
expectCurrentDecision(parked.needs_human_residuals, residual.value)
expect(parked.open_needs_human).toBe(1)
expect(parked.counts.ci).toBe(0)
const missing = snapshot(sd, fetchFile(dir, "ci-needs-human-missing.json", {
...FAILING, checks: [], threads: [],
}))
expect(missing.needs_human_residuals).toEqual([])
expect(missing.open_needs_human).toBe(0)
const missingState = JSON.parse(readFileSync(path.join(sd, "state.json"), "utf8"))
expect(missingState.ci_dispatched.s1 ?? []).not.toContain("CI/test")
const reappeared = snapshot(sd, fetchFile(dir, "ci-needs-human-reappeared.json", {
...FAILING, threads: [],
}))
expect(reappeared.counts.ci).toBe(1)
expect(reappeared.actionable.ci.map((item: any) => item.key)).toEqual(["CI/test"])
})
test("a changed same-head check run invalidates its residual and reopens investigation", () => {
const sd = path.join(dir, "ci-needs-human-rerun")
const firstFailure = {
...FAILING,
threads: [],
checks: [{
key: "CI/test", name: "test", status: "COMPLETED", conclusion: "FAILURE",
details_url: "https://example.test/runs/1", started_at: "t1", completed_at: "t2",
}],
}
const firstFetch = fetchFile(dir, "ci-needs-human-rerun-1.json", firstFailure)
snapshot(sd, firstFetch)
const residual = residualFile(dir, "ci-rerun-residual.json", "CI/test", "check")
mark(sd, ["--disposition", "needs-human", "--residual-file", residual.path])
expectCurrentDecision(snapshot(sd, firstFetch).needs_human_residuals, residual.value)
const rerunFailure = {
...firstFailure,
checks: [{
key: "CI/test", name: "test", status: "COMPLETED", conclusion: "TIMED_OUT",
details_url: "https://example.test/runs/2", started_at: "t3", completed_at: "t4",
}],
}
const reopened = snapshot(sd, fetchFile(dir, "ci-needs-human-rerun-2.json", rerunFailure))
expect(reopened.needs_human_residuals).toEqual([])
expect(reopened.open_needs_human).toBe(0)
expect(reopened.actionable.ci.map((item: any) => item.key)).toEqual(["CI/test"])
const reopenedState = JSON.parse(readFileSync(path.join(sd, "state.json"), "utf8"))
expect(reopenedState.ci_dispatched.s1 ?? []).not.toContain("CI/test")
expect(reopenedState.needs_human_check_observations).toBeUndefined()
})
test("a same-key failing check on a new head invalidates its old decision", () => {
const sd = path.join(dir, "ci-needs-human-new-head")
const firstFetch = fetchFile(dir, "ci-needs-human-new-head-1.json", {
...FAILING, threads: [],
})
snapshot(sd, firstFetch)
const residual = residualFile(dir, "ci-new-head-residual.json", "CI/test", "check")
mark(sd, ["--disposition", "needs-human", "--residual-file", residual.path])
expectCurrentDecision(snapshot(sd, firstFetch).needs_human_residuals, residual.value)
const reopened = snapshot(sd, fetchFile(dir, "ci-needs-human-new-head-2.json", {
...FAILING, head_sha: "s2", threads: [],
}))
expect(reopened.needs_human_residuals).toEqual([])
expect(reopened.actionable.ci.map((item: any) => item.key)).toEqual(["CI/test"])
})
test("legacy check migration transfers suppression to the canonical decision", () => {
const sd = path.join(dir, "ci-needs-human-legacy-migration")
const fetch = fetchFile(dir, "ci-needs-human-legacy-migration.json", {
...FAILING, threads: [],
})
snapshot(sd, fetch)
const residual = residualFile(dir, "ci-legacy-migration-residual.json", "CI/test", "check")
const legacy = readState(sd)
delete legacy.human_decisions
delete legacy.answered_human_decisions
legacy.needs_human_residuals = [residual.value]
legacy.needs_human_check_observations = {
"CI/test": {
conclusion: "FAILURE",
details_url: "u",
created_at: null,
started_at: null,
completed_at: null,
},
}
legacy.ci_dispatched = { s1: ["CI/test"] }
writeFileSync(path.join(sd, "state.json"), JSON.stringify(legacy))
const migrated = snapshot(sd, fetch)
expectCurrentDecision(migrated.needs_human_residuals, residual.value)
expect(migrated.actionable.ci).toEqual([])
expect(readState(sd).ci_dispatched.s1).not.toContain("CI/test")
markDecisionAnswered(sd, migrated.human_decisions[0].decision_id)
const answered = snapshot(sd, fetch)
expect(answered.actionable.ci.map((item: any) => item.key)).toEqual(["CI/test"])
expect(answered.answered_human_decisions).toHaveLength(1)
const staleSd = path.join(dir, "ci-needs-human-stale-legacy-migration")
snapshot(staleSd, fetch)
const stale = readState(staleSd)
delete stale.human_decisions
stale.needs_human_residuals = [residual.value]
stale.needs_human_check_observations = {
"CI/test": {
conclusion: "TIMED_OUT",
details_url: "u",
created_at: null,
started_at: null,
completed_at: null,
},
}
stale.ci_dispatched = { s1: ["CI/test"] }
writeFileSync(path.join(staleSd, "state.json"), JSON.stringify(stale))
const failedOpen = snapshot(staleSd, fetch)
expect(failedOpen.needs_human_residuals).toEqual([])
expect(failedOpen.actionable.ci.map((item: any) => item.key)).toEqual(["CI/test"])
})
test("a new legacy status creation on the same head reopens investigation", () => {
const sd = path.join(dir, "ci-needs-human-legacy-status")
const legacyFailure = (created_at: string) => ({
...FAILING,
threads: [],
checks: [{
key: "legacy-ci", name: "legacy-ci", status: "COMPLETED", conclusion: "FAILURE",
details_url: "https://ci.example.test/build", created_at,
started_at: null, completed_at: null,
}],
})
const firstFetch = fetchFile(dir, "ci-needs-human-legacy-1.json", legacyFailure("t1"))
snapshot(sd, firstFetch)
const residual = residualFile(dir, "ci-legacy-residual.json", "legacy-ci", "check")
mark(sd, ["--disposition", "needs-human", "--residual-file", residual.path])
expectCurrentDecision(snapshot(sd, firstFetch).needs_human_residuals, residual.value)
const reopened = snapshot(sd, fetchFile(
dir, "ci-needs-human-legacy-2.json", legacyFailure("t2")))
expect(reopened.needs_human_residuals).toEqual([])
expect(reopened.actionable.ci.map((item: any) => item.key)).toEqual(["legacy-ci"])
})
test("mixed grouped invalidation reopens review and CI in the same snapshot", () => {
const sd = path.join(dir, "mixed-needs-human")
const feedback = {
...FAILING,
threads: [{ thread_id: "T1", last_comment_id: "C1", last_comment_at: "t1" }],
}
snapshot(sd, fetchFile(dir, "mixed-1.json", feedback))
const grouped = {
...residualFile(dir, "mixed-unused.json", "T1", "thread").value,
sources: [{ id: "T1", kind: "thread" }, { id: "CI/test", kind: "check" }],
}
const groupedPath = fetchFile(dir, "mixed-residual.json", grouped)
mark(sd, ["--disposition", "needs-human", "--residual-file", groupedPath])
const parked = snapshot(sd, fetchFile(dir, "mixed-2.json", feedback))
expectCurrentDecision(parked.needs_human_residuals, grouped)
expect(parked.counts.threads).toBe(0)
expect(parked.counts.ci).toBe(0)
const answered = {
...feedback,
threads: [{ thread_id: "T1", last_comment_id: "C2", last_comment_at: "t2" }],
}
const reopened = snapshot(sd, fetchFile(dir, "mixed-3.json", answered))
expect(reopened.needs_human_residuals).toEqual([])
expect(reopened.actionable.threads.map((item: any) => item.thread_id)).toEqual(["T1"])
expect(reopened.actionable.ci.map((item: any) => item.key)).toEqual(["CI/test"])
})
test("a decision atomically replaces ordinary dispatch as the suppression owner", () => {
const sd = path.join(dir, "decision-after-dispatch")
const fixture = {
...FAILING,
threads: [],
feedback: [{ id: "IC_1", kind: "comment", author: "reviewer", edit_id: "e1" }],
}
const fetch = fetchFile(dir, "decision-after-dispatch.json", fixture)
snapshot(sd, fetch)
mark(sd, ["--comment", "IC_1", "--disposition", "dispatched"])
mark(sd, ["--check", "CI/test", "--disposition", "dispatched"])
expect(snapshot(sd, fetch).counts).toMatchObject({ comments: 0, ci: 0 })
const grouped = {
...residualFile(dir, "decision-after-dispatch-unused.json", "IC_1", "comment").value,
sources: [{ id: "IC_1", kind: "comment" }, { id: "CI/test", kind: "check" }],
}
const groupedPath = fetchFile(dir, "decision-after-dispatch-residual.json", grouped)
mark(sd, ["--disposition", "needs-human", "--residual-file", groupedPath])
const parked = snapshot(sd, fetch)
expectCurrentDecision(parked.needs_human_residuals, grouped)
expect(parked.counts).toMatchObject({ comments: 0, ci: 0 })
const parkedState = JSON.parse(readFileSync(path.join(sd, "state.json"), "utf8"))
expect(parkedState.feedback.IC_1.disposition).toBe("open")
expect(parkedState.ci_dispatched.s1).not.toContain("CI/test")
markDecisionAnswered(sd, parked.human_decisions[0].decision_id)
const answered = snapshot(sd, fetch)
expect(answered.actionable.comments.map((item: any) => item.id)).toEqual(["IC_1"])
expect(answered.actionable.ci.map((item: any) => item.key)).toEqual(["CI/test"])
})
test("a check-only human answer reactivates the exact source and clears after consumption", () => {
const sd = path.join(dir, "check-only-answer")
const fetch = fetchFile(dir, "check-only-answer.json", { ...FAILING, threads: [] })
snapshot(sd, fetch)
const residual = residualFile(dir, "check-only-answer-residual.json", "CI/test", "check")
mark(sd, ["--disposition", "needs-human", "--residual-file", residual.path])
const parked = snapshot(sd, fetch)
expectCurrentDecision(parked.needs_human_residuals, residual.value)
const decisionId = parked.human_decisions[0].decision_id
markDecisionAnswered(sd, decisionId)
const answered = snapshot(sd, fetch)
expect(answered.needs_human_residuals).toEqual([])
expect(answered.actionable.ci.map((item: any) => item.key)).toEqual(["CI/test"])
expect(answered.answered_human_decisions).toHaveLength(1)
mark(sd, ["--check", "CI/test", "--disposition", "dispatched"])
const consumed = snapshot(sd, fetch)
expect(consumed.actionable.ci).toEqual([])
expect(consumed.answered_human_decisions).toEqual([])
})
test("one answer reactivates every source in a mixed decision atomically", () => {
const sd = path.join(dir, "mixed-answer")
const fixture = {
...FAILING,
threads: [{ thread_id: "T1", last_comment_id: "C1", last_comment_at: "t1" }],
}
const fetch = fetchFile(dir, "mixed-answer.json", fixture)
snapshot(sd, fetch)
const residual = {
...residualFile(dir, "mixed-answer-unused.json", "T1", "thread").value,
sources: [{ id: "T1", kind: "thread" }, { id: "CI/test", kind: "check" }],
}
const residualPath = fetchFile(dir, "mixed-answer-residual.json", residual)
mark(sd, ["--disposition", "needs-human", "--residual-file", residualPath,
"--fetch-file", fetch])
const parked = snapshot(sd, fetch)
expectCurrentDecision(parked.needs_human_residuals, residual)
const decisionId = parked.human_decisions[0].decision_id
const ordinaryBeforeAnswer = readState(sd)
markDecisionAnswered(sd, decisionId)
const ordinaryAfterAnswer = readState(sd)
expect(ordinaryAfterAnswer.threads.T1).toEqual(ordinaryBeforeAnswer.threads.T1)
expect(ordinaryAfterAnswer.ci_dispatched).toEqual(ordinaryBeforeAnswer.ci_dispatched)
const answered = snapshot(sd, fetch)
expect(answered.actionable.threads.map((item: any) => item.thread_id)).toEqual(["T1"])
expect(answered.actionable.ci.map((item: any) => item.key)).toEqual(["CI/test"])
expect(answered.open_needs_human).toBe(0)
})
test("one answered source prevents a mixed residual from re-parking its unchanged evidence", () => {
const sd = path.join(dir, "mixed-answer-repark")
const fixture = (commentId: string) => ({
...FAILING,
threads: [{ thread_id: "T1", last_comment_id: commentId, last_comment_at: commentId }],
})
const first = fetchFile(dir, "mixed-answer-repark-1.json", fixture("C1"))
snapshot(sd, first)
const original = {
...residualFile(dir, "mixed-answer-repark-unused.json", "T1", "thread").value,
sources: [{ id: "T1", kind: "thread" }, { id: "CI/test", kind: "check" }],
}
const originalPath = fetchFile(dir, "mixed-answer-repark-original.json", original)
mark(sd, ["--disposition", "needs-human", "--residual-file", originalPath,
"--fetch-file", first])
const parked = snapshot(sd, first)
markDecisionAnswered(sd, parked.human_decisions[0].decision_id)
const moved = fetchFile(dir, "mixed-answer-repark-2.json", fixture("C2"))
const actionable = snapshot(sd, moved)
expect(actionable.answered_human_decisions).toHaveLength(1)
const repeated = {
...original,
decision_context: { ...original.decision_context, investigation: "Inspected the new reply." },
}
const repeatedPath = fetchFile(dir, "mixed-answer-repark-repeated.json", repeated)
const result = spawnSync("python3", [SCRIPT, "mark", "--state-dir", sd,
...persistedInvocationArgs(sd), "--disposition", "needs-human",
"--residual-file", repeatedPath, "--fetch-file", moved], { encoding: "utf8" })
expect(result.status).not.toBe(0)
expect(result.stderr).toContain("source observations already have a recorded human answer")
expect(readState(sd).human_decisions).toEqual([])
})
test("a thread decision fails closed when its post-reply observation cannot be fetched", () => {
const sd = path.join(dir, "missing-post-reply-baseline")
const first = fetchFile(dir, "missing-post-reply-baseline-1.json", {
...FAILING,
checks: [],
threads: [{ thread_id: "T1", last_comment_id: "C1", last_comment_at: "t1" }],
})
snapshot(sd, first)
const residual = residualFile(dir, "missing-post-reply-baseline-residual.json", "T1", "thread")
const empty = fetchFile(dir, "missing-post-reply-baseline-empty.json", { threads: [] })
const result = spawnSync("python3", [SCRIPT, "mark", "--state-dir", sd,
...persistedInvocationArgs(sd), "--disposition", "needs-human",
"--residual-file", residual.path, "--fetch-file", empty], { encoding: "utf8" })
expect(result.status).not.toBe(0)
expect(result.stderr).toContain("cannot freeze the post-reply thread observation: T1")
expect(readState(sd).human_decisions).toEqual([])
})
test("a legacy currency answer never claims current grouped sibling evidence", () => {
const sd = path.join(dir, "legacy-currency-answer")
const fixture = quietCurrencyFixture({
checks: [{ key: "CI/test", name: "test", status: "COMPLETED", conclusion: "FAILURE", details_url: "old" }],
threads: [{ thread_id: "T1", last_comment_id: "C1", last_comment_at: "t1" }],
})
const first = fetchFile(dir, "legacy-currency-answer-1.json", fixture)
const observed = snapshot(sd, first)
const residual = {
...residualFile(
dir, "legacy-currency-answer-unused.json", observed.branch_currency.key, "currency").value,
sources: [
{ id: observed.branch_currency.key, kind: "currency" },
{ id: "T1", kind: "thread" },
{ id: "CI/test", kind: "check" },
],
thread_urls: ["https://example.test/thread/T1"],
}
const legacy = readState(sd)
delete legacy.human_decisions
delete legacy.answered_human_decisions
legacy.branch_currency_state.items[observed.branch_currency.key].mutation_requires_answer = true
legacy.branch_currency_state.items[observed.branch_currency.key].answered_decision = {
answer: "Option 2: keep the current source.",
residual,
recorded_at: "2026-01-01T00:00:00Z",
}
legacy.threads.T1.disposition = "needs-human"
legacy.threads.T1.acted_identity = ["C1", "t1"]
legacy.ci_dispatched.s1 = ["CI/test"]
writeFileSync(path.join(sd, "state.json"), JSON.stringify(legacy))
const moved = fetchFile(dir, "legacy-currency-answer-2.json", {
...fixture,
checks: [{ key: "CI/test", name: "test", status: "COMPLETED", conclusion: "FAILURE", details_url: "new" }],
threads: [{ thread_id: "T1", last_comment_id: "C2", last_comment_at: "t2" }],
})
const migrated = snapshot(sd, moved)
expect(migrated.answered_human_decisions).toHaveLength(1)
expect(migrated.answered_human_decisions[0]).toMatchObject({
answer: "Option 2: keep the current source.",
residual,
})
expect(migrated.answered_human_decisions[0].sources).toEqual([
expect.objectContaining({ kind: "currency", id: observed.branch_currency.key }),
])
expect(migrated.actionable.threads.map((item: any) => item.thread_id)).toEqual(["T1"])
expect(migrated.actionable.ci.map((item: any) => item.key)).toEqual(["CI/test"])
expect(readState(sd).branch_currency_state.items[observed.branch_currency.key]
.answered_decision).toBeUndefined()
const authorized = snapshot(sd, moved)
expect(authorized.branch_currency).toMatchObject({
attention: "claim",
mutation_requires_answer: true,
})
const siblings = {
...residual,
sources: [{ id: "T1", kind: "thread" }, { id: "CI/test", kind: "check" }],
}
mark(sd, ["--disposition", "needs-human", "--residual-file",
fetchFile(dir, "legacy-currency-answer-siblings.json", siblings), "--fetch-file", moved])
expect(snapshot(sd, moved).needs_human_residuals).toEqual([siblings])
markCurrency(sd, authorized.branch_currency.key, "claimed")
const consumed = snapshot(sd, moved)
expect(consumed.branch_currency).toMatchObject({
disposition: "claimed",
mutation_requires_answer: false,
})
expect(consumed.answered_human_decisions).toEqual([])
})
test("needs-human marks fail closed without the typed residual payload", () => {
const sd = path.join(dir, "missing-needs-human-residual")
const fetch = fetchFile(dir, "missing-residual.json", FAILING)
snapshot(sd, fetch)
const r = spawnSync("python3", [SCRIPT, "mark", "--state-dir", sd,
...persistedInvocationArgs(sd), "--thread", "T1", "--disposition", "needs-human",
"--fetch-file", fetch], { encoding: "utf8" })
expect(r.status).not.toBe(0)
expect(r.stderr).toMatch(/residual-file/i)
})
test("needs-human marks validate the complete residual schema", () => {
const sd = path.join(dir, "malformed-needs-human-residual")
const fetch = fetchFile(dir, "malformed-residual.json", FAILING)
snapshot(sd, fetch)
const valid = residualFile(dir, "valid-residual.json", "T1", "thread").value
const malformed = [
{ ...valid, decision_context: undefined },
{ ...valid, decision_context: { ...valid.decision_context, options: [] } },
{ ...valid, decision_context: { ...valid.decision_context, recommendation: undefined } },
{ ...valid, thread_urls: [] },
{ ...valid, sources: [{ id: "CI/test", kind: "check" }],
thread_urls: ["https://example.test/thread/T1"] },
{ ...valid, sources: [{ id: "T1", kind: "thread" }, { id: "T2", kind: "thread" }] },
]
for (const [index, value] of malformed.entries()) {
const residualPath = fetchFile(dir, `malformed-${index}.json`, value)
const r = spawnSync("python3", [SCRIPT, "mark", "--state-dir", sd,
...persistedInvocationArgs(sd), "--thread", "T1", "--disposition", "needs-human",
"--residual-file", residualPath, "--fetch-file", fetch], { encoding: "utf8" })
expect(r.status).not.toBe(0)
expect(r.stderr).toMatch(/invalid --residual-file/i)
}
})
test("a decision mark binds every thread URL to its authoritative source", () => {
const sd = path.join(dir, "wrong-thread-url")
const fetch = fetchFile(dir, "wrong-thread-url.json", FAILING)
snapshot(sd, fetch)
const residual = residualFile(dir, "wrong-thread-url-residual.json", "T1", "thread").value
residual.thread_urls = ["https://example.test/thread/T2"]
const rejected = spawnSync("python3", [SCRIPT, "mark", "--state-dir", sd,
...persistedInvocationArgs(sd), "--thread", "T1", "--disposition", "needs-human",
"--residual-file", fetchFile(dir, "wrong-thread-url-payload.json", residual),
"--fetch-file", fetch], { encoding: "utf8" })
expect(rejected.status).not.toBe(0)
expect(rejected.stderr).toMatch(/thread_urls must match the authoritative URL/)
expect(JSON.parse(readFileSync(path.join(sd, "state.json"), "utf8")).human_decisions).toEqual([])
})
test("mark --check silences it; a new head SHA re-actionizes", () => {
const f = fetchFile(dir, "a.json", FAILING)
snapshot(state, f)
mark(state, ["--check", "CI/test"])
expect(snapshot(state, f).counts.ci).toBe(0)
const newHead = { ...FAILING, head_sha: "s2" }
expect(snapshot(state, fetchFile(dir, "c.json", newHead)).counts.ci).toBe(1)
})
test("checks_terminal is false while a check is IN_PROGRESS; all_checks_ok stays false", () => {
const inprog = {
...FAILING,
checks: [{ key: "CI/test", name: "test", status: "IN_PROGRESS", conclusion: null, details_url: "u" }],
}
const d = snapshot(state, fetchFile(dir, "ip.json", inprog))
expect(d.checks_terminal).toBe(false)
expect(d.all_checks_ok).toBe(false)
expect(d.has_failing_checks).toBe(false)
})
test("clean + terminal + approved: all_checks_ok true, mergeStateStatus passthrough, no open needs-human", () => {
const clean = {
...FAILING,
merge_state_status: "CLEAN",
review_decision: "APPROVED",
checks: [{ key: "CI/test", name: "test", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }],
threads: [],
}
const d = snapshot(state, fetchFile(dir, "cl.json", clean))
expect(d.all_checks_ok).toBe(true)
expect(d.checks_terminal).toBe(true)
expect(d.merge_state_status).toBe("CLEAN")
expect(d.open_needs_human).toBe(0)
})
test("gh stack view is the first probe and a target match supplies managed freshness without GraphQL", () => {
const { chain, calls } = probeChain({
stackView: {
status: 0,
stdout: {
trunk: "main",
currentBranch: "feature",
branches: [
{ name: "parent", needsRebase: false, pr: { number: 41, url: "https://github.com/o/r/pull/41", state: "OPEN" } },
{ name: "feature", isCurrent: true, needsRebase: true, pr: { number: 42, url: "https://GITHUB.COM/O/R/pull/42/", state: "OPEN" } },
{ name: "child", needsRebase: false, pr: { number: 43, url: "https://github.com/o/r/pull/43", state: "OPEN", isDraft: true } },
],
},
},
graphql: { status: 1, stderr: "must not run" },
})
expect(calls[0]).toBe("gh stack view --json")
expect(calls.some((call) => call.includes("api graphql"))).toBe(false)
expect(chain.manager_status).toBe("confirmed")
expect(chain.manager_source).toBe("gh-stack")
expect(chain.relationship_status).toBe("dependent")
expect(chain.target_position).toBe(2)
expect(chain.target_needs_rebase).toBe(true)
expect(chain.entries[2].isDraft).toBe(true)
expect(chain.dependent_prs[0].isDraft).toBe(true)
})
test("a successful view of another local stack falls back to GraphQL instead of misclassifying the target", () => {
const { chain, calls } = probeChain({
stackView: {
status: 0,
stdout: { trunk: "main", currentBranch: "other", branches: [{ name: "other", pr: { number: 7 } }] },
},
graphql: {
status: 0,
stdout: {
data: { repository: { pullRequest: {
stackEntry: { position: 2 },
stack: {
id: "STACK_1", number: 99, size: 3, baseRefName: "main",
entries: { nodes: [
{ position: 1, pullRequest: { number: 41, url: "https://github.com/o/r/pull/41", state: "OPEN", headRefName: "parent" } },
{ position: 2, pullRequest: { number: 42, url: "https://github.com/o/r/pull/42", state: "OPEN", headRefName: "feature" } },
{ position: 3, pullRequest: { number: 43, url: "https://github.com/o/r/pull/43", state: "OPEN", headRefName: "child" } },
] },
},
} } },
},
},
})
expect(calls.some((call) => call.includes("api graphql"))).toBe(true)
expect(chain.manager_status).toBe("confirmed")
expect(chain.manager_source).toBe("graphql")
expect(chain.target_position).toBe(2)
expect(chain.target_needs_rebase).toBeNull()
})
test("a local stack entry with the target number in another repository falls back to GraphQL", () => {
const { chain, calls } = probeChain({
stackView: {
status: 0,
stdout: {
trunk: "main",
currentBranch: "feature",
branches: [
{
name: "feature",
isCurrent: true,
needsRebase: true,
pr: { number: 42, url: "https://github.com/another/repository/pull/42", state: "OPEN" },
},
],
},
},
graphql: {
status: 0,
stdout: {
data: { repository: {
defaultBranchRef: { name: "main" },
pullRequest: {
stackEntry: { position: 1 },
stack: {
id: "STACK_2", number: 100, size: 1, baseRefName: "main",
entries: { nodes: [
{ position: 1, pullRequest: { number: 42, url: "https://github.com/o/r/pull/42", state: "OPEN", headRefName: "feature" } },
] },
},
},
} },
},
},
})
expect(calls.some((call) => call.includes("api graphql"))).toBe(true)
expect(chain.manager_status).toBe("confirmed")
expect(chain.manager_source).toBe("graphql")
expect(chain.target_needs_rebase).toBeNull()
})
test("successful null GraphQL stack classifies an ordinary manual dependency chain", () => {
const { chain, calls } = probeChain({
baseRef: "parent",
headRef: "feature",
stackView: { status: 1, stderr: "no current stack" },
graphql: { status: 0, stdout: { data: { repository: {
defaultBranchRef: { name: "main" },
pullRequest: { stackEntry: null, stack: null },
} } } },
openPrs: [
{ number: 41, url: "https://github.com/o/r/pull/41", state: "MERGED", baseRefName: "main", headRefName: "parent" },
{ number: 42, url: "https://github.com/o/r/pull/42", state: "OPEN", baseRefName: "parent", headRefName: "feature" },
{ number: 43, url: "https://github.com/o/r/pull/43", state: "OPEN", baseRefName: "feature", headRefName: "child" },
],
})
expect(chain.manager_status).toBe("absent")
expect(calls.some((call) => call.includes("gh pr list") && call.includes("--state all") && call.includes("--head parent"))).toBe(true)
expect(calls.some((call) => call.includes("gh pr list") && call.includes("--state open") && call.includes("--base feature"))).toBe(true)
expect(chain.relationship_status).toBe("dependent")
expect(chain.parent_prs.map((pr: any) => pr.number)).toEqual([41])
expect(chain.dependent_prs.map((pr: any) => pr.number)).toEqual([43])
})
test("a PR based on the default branch ignores unrelated PRs whose head has the default-branch name", () => {
const { chain, calls } = probeChain({
baseRef: "main",
headRef: "feature",
stackView: { status: 1, stderr: "no current stack" },
graphql: { status: 0, stdout: { data: { repository: {
defaultBranchRef: { name: "main" },
pullRequest: { stackEntry: null, stack: null },
} } } },
openPrs: [
{ number: 500, url: "https://github.com/o/r/pull/500", state: "OPEN", baseRefName: "main", headRefName: "main" },
{ number: 320, url: "https://github.com/o/r/pull/320", state: "OPEN", baseRefName: "main", headRefName: "main" },
],
})
expect(calls.some((call) => call.includes("gh pr list") && call.includes("--head main"))).toBe(false)
expect(calls.some((call) => call.includes("gh pr list") && call.includes("--base feature"))).toBe(true)
expect(chain.manager_status).toBe("absent")
expect(chain.relationship_status).toBe("independent")
expect(chain.parent_prs).toEqual([])
expect(chain.dependent_prs).toEqual([])
})
test("manager probe failure remains unknown and never collapses to absent", () => {
const { chain, calls } = probeChain({
stackView: { status: 1, stderr: "no current stack" },
graphql: { status: 1, stderr: "gh: HTTP 401: Bad credentials" },
})
expect(chain.manager_status).toBe("probe-error")
expect(chain.manager_status).not.toBe("absent")
expect(calls.filter((call) => call.startsWith("gh api ")).length).toBe(1)
})
test("unavailable stack fields fall back to the default branch and manual-chain classification", () => {
const { chain, calls } = probeChain({
baseRef: "parent",
headRef: "feature",
stackView: { status: 1, stderr: "no current stack" },
graphql: { status: 1, stderr: "gh: Field 'stackEntry' doesn't exist on type 'PullRequest'" },
defaultBranch: { status: 0, stdout: "main\n" },
openPrs: [
{ number: 41, url: "https://github.com/o/r/pull/41", state: "OPEN", baseRefName: "main", headRefName: "parent" },
{ number: 43, url: "https://github.com/o/r/pull/43", state: "OPEN", baseRefName: "feature", headRefName: "child" },
],
})
expect(calls).toContain("gh api repos/o/r --jq .default_branch")
expect(chain.manager_status).toBe("absent")
expect(chain.relationship_status).toBe("dependent")
expect(chain.parent_prs.map((pr: any) => pr.number)).toEqual([41])
expect(chain.dependent_prs.map((pr: any) => pr.number)).toEqual([43])
})
test("stack fields unavailable with no default-branch fallback remains a manager probe error", () => {
const { chain } = probeChain({
stackView: { status: 1, stderr: "no current stack" },
graphql: { status: 1, stderr: "GraphQL: Cannot query field \"stack\" on type \"PullRequest\"." },
defaultBranch: { status: 1, stderr: "network unavailable" },
})
expect(chain.manager_status).toBe("probe-error")
})
test("colliding check keys are disambiguated (both failing checks surface, neither shadows)", () => {
const collide = {
...FAILING,
checks: [
{ key: "test", name: "test", status: "COMPLETED", conclusion: "FAILURE", details_url: "u1" },
{ key: "test", name: "test", status: "COMPLETED", conclusion: "FAILURE", details_url: "u2" },
],
}
expect(snapshot(state, fetchFile(dir, "co.json", collide)).counts.ci).toBe(2)
})
test("transient null head falls back to the last known head — no ci_dispatched wipe / re-dispatch thrash", () => {
const f = fetchFile(dir, "a.json", FAILING)
snapshot(state, f)
mark(state, ["--check", "CI/test"])
const nullHead = { ...FAILING, head_sha: null }
const d = snapshot(state, fetchFile(dir, "nh.json", nullHead))
expect(d.head_changed).toBe(false)
expect(d.counts.ci).toBe(0) // still silenced
})
// --- trajectory: deterministic cross-tick facts for non-convergence detection ---
const GREEN_CHECK = { key: "CI/test", name: "test", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }
const RED_CHECK = { key: "CI/test", name: "test", status: "COMPLETED", conclusion: "FAILURE", details_url: "u" }
test("check recurrence: fail -> clear -> fail on a NEW head increments recur (ping-pong signal)", () => {
snapshot(state, fetchFile(dir, "r1.json", { ...FAILING, head_sha: "s1", checks: [RED_CHECK] }))
snapshot(state, fetchFile(dir, "r2.json", { ...FAILING, head_sha: "s2", checks: [GREEN_CHECK] }))
const d = snapshot(state, fetchFile(dir, "r3.json", { ...FAILING, head_sha: "s3", checks: [RED_CHECK] }))
expect(d.trajectory.check_recur_max).toBe(1)
expect(d.trajectory.recurring_checks).toEqual([{ key: "CI/test", recur: 1 }])
})
test("same-head flapping is NOT recurrence (flaky, not ping-pong)", () => {
const f = { ...FAILING, head_sha: "s1" }
snapshot(state, fetchFile(dir, "f1.json", { ...f, checks: [RED_CHECK] }))
snapshot(state, fetchFile(dir, "f2.json", { ...f, checks: [GREEN_CHECK] }))
const d = snapshot(state, fetchFile(dir, "f3.json", { ...f, checks: [RED_CHECK] }))
expect(d.trajectory.check_recur_max).toBe(0)
})
test("review backlog trend rises and new-thread arrivals are counted (treadmill signal)", () => {
const th = (ids: string[]) => ids.map((id) => ({ thread_id: id, last_comment_id: `c-${id}`, last_comment_at: id }))
snapshot(state, fetchFile(dir, "t1.json", { ...FAILING, checks: [], threads: th(["T1"]) }))
snapshot(state, fetchFile(dir, "t2.json", { ...FAILING, checks: [], threads: th(["T1", "T2"]) }))
const d = snapshot(state, fetchFile(dir, "t3.json", { ...FAILING, checks: [], threads: th(["T1", "T2", "T3", "T4"]) }))
expect(d.trajectory.unresolved_trend).toBe("rising")
expect(d.trajectory.new_threads_this_tick).toBe(2) // T3, T4 are new this tick
expect(d.trajectory.unresolved_threads).toBe(4)
})
test("check_recur_max does not stay elevated after the recurring check leaves CI (stale-key prune)", () => {
snapshot(state, fetchFile(dir, "p1.json", { ...FAILING, head_sha: "s1", checks: [RED_CHECK] }))
snapshot(state, fetchFile(dir, "p2.json", { ...FAILING, head_sha: "s2", checks: [GREEN_CHECK] }))
expect(snapshot(state, fetchFile(dir, "p3.json", { ...FAILING, head_sha: "s3", checks: [RED_CHECK] })).trajectory.check_recur_max).toBe(1)
// CI/test is gone from the run (renamed/removed); its recurrence must not linger.
const other = { key: "CI/other", name: "other", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }
const d = snapshot(state, fetchFile(dir, "p4.json", { ...FAILING, head_sha: "s4", checks: [other] }))
expect(d.trajectory.check_recur_max).toBe(0)
})
test("heads_since_progress climbs on a persistent failure across heads, but resets on progressive migration", () => {
// Same check red across three new heads with nothing clearing = a stall.
snapshot(state, fetchFile(dir, "s1.json", { ...FAILING, head_sha: "h1", checks: [RED_CHECK], threads: [] }))
expect(snapshot(state, fetchFile(dir, "s2.json", { ...FAILING, head_sha: "h2", checks: [RED_CHECK], threads: [] })).trajectory.heads_since_progress).toBe(1)
expect(snapshot(state, fetchFile(dir, "s3.json", { ...FAILING, head_sha: "h3", checks: [RED_CHECK], threads: [] })).trajectory.heads_since_progress).toBe(2)
// A different check now fails (A cleared, B appeared) = progressive migration, not a stall -> reset.
const other = { key: "CI/other", name: "other", status: "COMPLETED", conclusion: "FAILURE", details_url: "u" }
expect(snapshot(state, fetchFile(dir, "s4.json", { ...FAILING, head_sha: "h4", checks: [other], threads: [] })).trajectory.heads_since_progress).toBe(0)
})
test("parking a thread counts as progress: it leaves the non-parked problem set, so no-progress resets", () => {
const withThread = (headSha: string) => ({
...FAILING,
head_sha: headSha,
checks: [RED_CHECK],
threads: [{ thread_id: "T1", last_comment_id: "c1", last_comment_at: "t1" }],
})
snapshot(state, fetchFile(dir, "pk1.json", withThread("h1"))) // problems: {CI/test, T1}
mark(state, ["--thread", "T1", "--disposition", "needs-human"])
// New head, CI/test still red, T1 now parked (excluded from problems) -> total drops 2->1 = a new low.
const d = snapshot(state, fetchFile(dir, "pk2.json", withThread("h2")))
expect(d.open_needs_human).toBe(1)
expect(d.trajectory.heads_since_progress).toBe(0) // progress was made (a problem left the set), despite the head change
})
test("a rerun (IN_PROGRESS) is not a clear — no false recurrence when it fails again", () => {
snapshot(state, fetchFile(dir, "ir1.json", { ...FAILING, head_sha: "s1", checks: [RED_CHECK] }))
const rerun = { ...FAILING, head_sha: "s2", checks: [{ key: "CI/test", name: "test", status: "IN_PROGRESS", conclusion: null, details_url: "u" }] }
snapshot(state, fetchFile(dir, "ir2.json", rerun))
const d = snapshot(state, fetchFile(dir, "ir3.json", { ...FAILING, head_sha: "s3", checks: [RED_CHECK] }))
expect(d.trajectory.check_recur_max).toBe(0)
})
test("mark --disposition open re-actionizes a parked needs-human thread (the re-open path)", () => {
const f = fetchFile(dir, "ro.json", FAILING)
snapshot(state, f)
mark(state, ["--thread", "T1", "--disposition", "needs-human"])
expect(snapshot(state, f).open_needs_human).toBe(1) // parked, not actionable
mark(state, ["--thread", "T1", "--disposition", "open"])
const d = snapshot(state, f)
expect(d.counts.threads).toBe(1) // re-opened -> actionable again
expect(d.open_needs_human).toBe(0)
})
test("a dispatched thread reactivates when a later reviewer comment moves its identity, but not on our own reply (acted_identity baseline)", () => {
// The false-green fix: a dispatched-but-unresolved thread with fresh reviewer activity must
// return to actionable, or it stays hidden from counts.threads and lets merge-ready fire.
const sd = path.join(dir, "react")
const thr = (cid: string) => ({
pr_state: "OPEN", mergeable: "MERGEABLE", merge_state_status: "CLEAN", review_decision: null,
head_sha: "s1", url: "http://x/1", checks: [],
threads: [{ thread_id: "T1", last_comment_id: cid, last_comment_at: cid }],
})
snapshot(sd, fetchFile(dir, "r1.json", thr("C1"))) // open -> actionable
mark(sd, ["--thread", "T1", "--disposition", "dispatched"])
// first post-action observation adopts the current identity (our reply) as baseline -> silenced
expect(snapshot(sd, fetchFile(dir, "r2.json", thr("C1"))).counts.threads).toBe(0)
// same identity on a later tick -> still silenced (our own reply does not re-trigger)
expect(snapshot(sd, fetchFile(dir, "r3.json", thr("C1"))).counts.threads).toBe(0)
// a genuine reviewer reply moves the identity to C2 -> reactivated
expect(snapshot(sd, fetchFile(dir, "r4.json", thr("C2"))).counts.threads).toBe(1)
})
test("thread activity invalidates a decision but is never recorded as the human answer", () => {
const sd = path.join(dir, "nhreact")
const thr = (cid: string) => ({
...FAILING, checks: [], threads: [{ thread_id: "T1", last_comment_id: cid, last_comment_at: cid }],
})
snapshot(sd, fetchFile(dir, "nh1.json", thr("C1")))
const replied = fetchFile(dir, "nh2.json", thr("C2"))
mark(sd, ["--thread", "T1", "--disposition", "needs-human", "--fetch-file", replied])
const d1 = snapshot(sd, replied)
expect(d1.counts.threads).toBe(0)
expect(d1.open_needs_human).toBe(1) // still parked, blocks merge-ready
// Remote activity changes the question's evidence; it does not guess that this was the answer.
const d2 = snapshot(sd, fetchFile(dir, "nh3.json", thr("C3")))
expect(d2.counts.threads).toBe(1)
expect(d2.open_needs_human).toBe(0)
expect(d2.answered_human_decisions).toEqual([])
})
test("blocked_external waits for other running checks — does not fire while a check is still IN_PROGRESS", () => {
const RUNNING = { key: "CI/b", name: "b", status: "IN_PROGRESS", conclusion: null, details_url: "u" }
const GREEN = { key: "CI/a", name: "a", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }
// awaiting approval + a still-running check -> NOT blocked_external yet (that check could fail)
const running = { ...FAILING, threads: [], checks: [RUNNING], awaiting_approval: 1 }
expect(snapshot(path.join(dir, "be1"), fetchFile(dir, "be1.json", running)).blocked_external).toBe(false)
// awaiting approval + all other checks terminal -> blocked_external
const terminal = { ...FAILING, threads: [], checks: [GREEN], awaiting_approval: 1 }
expect(snapshot(path.join(dir, "be2"), fetchFile(dir, "be2.json", terminal)).blocked_external).toBe(true)
})
test("a dispatched (handled) top-level comment does not inflate heads_since_progress across heads", () => {
// A handled comment never drops out of the fetch, so counting it as an open problem would keep
// heads_since_progress climbing forever and falsely trip non-convergence on unrelated later work.
const sd = path.join(dir, "stall")
const fb = (head: string) => ({
...FAILING, head_sha: head, checks: [], threads: [], feedback: [{ id: "IC_1", kind: "comment", author: "r", edit_id: "h" }],
})
snapshot(sd, fetchFile(dir, "st1.json", fb("s1"))) // IC_1 open -> a problem
mark(sd, ["--comment", "IC_1", "--disposition", "dispatched", "--acted-edit-id", "h"])
const d = snapshot(sd, fetchFile(dir, "st2.json", fb("s2"))) // dispatched + head moved -> handled, progress
expect(d.trajectory.heads_since_progress).toBe(0)
})
test("a watch poll does not consume new_threads_this_tick — the agent's tick still sees the new arrival", () => {
// The watch's waking poll persists change-detection state but must NOT roll the trajectory, or it
// marks the just-arrived thread "seen" and the agent's real tick reads 0 new arrivals — hiding a
// review-bot treadmill from the non-convergence trigger.
const sd = path.join(dir, "trajwatch")
const noThreads = { ...FAILING, checks: [], threads: [] }
snapshot(sd, fetchFile(dir, "tw1.json", noThreads)) // agent tick: baseline, no threads
const withThread = { ...FAILING, checks: [], threads: [{ thread_id: "T1", last_comment_id: "C1", last_comment_at: "C1" }] }
expect(watch(sd, fetchFile(dir, "tw2.json", withThread)).reason).toBe("actionable") // a poll wakes on the new thread
// the agent's real tick then still counts T1 as newly arrived (the poll didn't mark it seen)
expect(snapshot(sd, fetchFile(dir, "tw3.json", withThread)).trajectory.new_threads_this_tick).toBe(1)
}, 15000)
test("heads_since_progress counts head moves across AGENT ticks even when a poll observed the new head first (C2)", () => {
const sd = path.join(dir, "hspwatch")
const failAt = (head: string) => ({ ...FAILING, head_sha: head, threads: [], checks: [{ key: "CI/x", name: "x", status: "COMPLETED", conclusion: "FAILURE", details_url: "u" }] })
snapshot(sd, fetchFile(dir, "hw1.json", failAt("s1"))) // agent tick: persistent failure at head s1
watch(sd, fetchFile(dir, "hw2.json", failAt("s2"))) // a poll observes+persists head s2 (no trajectory roll)
const d = snapshot(sd, fetchFile(dir, "hw3.json", failAt("s2"))) // agent tick at s2
expect(d.trajectory.heads_since_progress).toBe(1) // head moved s1->s2 between agent ticks; not starved by the poll
}, 15000)
test("check recurrence catches a CLEAR observed only on a watch poll (C1)", () => {
const sd = path.join(dir, "recurwatch")
const RED = { key: "CI/x", name: "x", status: "COMPLETED", conclusion: "FAILURE", details_url: "u" }
const GREEN = { key: "CI/x", name: "x", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }
snapshot(sd, fetchFile(dir, "rw1.json", { ...FAILING, head_sha: "s1", threads: [], checks: [RED] }),
EXPIRING_TEST_INVOCATION) // fail h1; the watch below deliberately expires
watch(sd, fetchFile(dir, "rw2.json", { ...FAILING, head_sha: "s2", threads: [], checks: [GREEN] })) // a poll observes the CLEAR
const d = snapshot(sd, fetchFile(dir, "rw3.json", { ...FAILING, head_sha: "s3", threads: [], checks: [RED] })) // fail h3
expect(d.trajectory.check_recur_max).toBe(1) // fail -> clear(seen only on a poll) -> fail = recurrence
}, 15000)
test("snapshot refuses to inherit an old budget without an explicit invocation boundary", () => {
const sd = path.join(dir, "sess")
snapshot(sd, fetchFile(dir, "se1.json", FAILING))
// simulate resuming days later against persisted state: backdate started_at
const statePath = path.join(sd, "state.json")
const st = JSON.parse(readFileSync(statePath, "utf8"))
st.started_at = "2020-01-01T00:00:00Z"
writeFileSync(statePath, JSON.stringify(st))
const bare = spawnSync("python3", [SCRIPT, "snapshot", "--pr", "1", "--repo", "o/r", "--state-dir", sd,
"--fetch-file", fetchFile(dir, "se2.json", FAILING)], { encoding: "utf8" })
expect(bare.status).not.toBe(0)
expect(bare.stderr).toContain("requires --start-invocation or --invocation-id")
const fresh = snapshot(sd, fetchFile(dir, "se3.json", FAILING), ["--start-invocation"])
expect(fresh.invocation_elapsed_seconds).toBeLessThan(10)
})
test("a new invocation clock starts after a slow first fetch", () => {
const sd = path.join(dir, "slow-first-fetch")
const python = `
import json
from datetime import datetime, timedelta, timezone
from importlib.machinery import SourceFileLoader
from types import SimpleNamespace
m = SourceFileLoader("prs", ${JSON.stringify(SCRIPT)}).load_module()
before_fetch = datetime(2026, 1, 1, tzinfo=timezone.utc)
after_fetch = before_fetch + timedelta(seconds=61)
clock_reads = 0
def fake_now():
global clock_reads
clock_reads += 1
return before_fetch if clock_reads == 1 else after_fetch
m._now = fake_now
m._fetch_snapshot = lambda args: json.loads(${JSON.stringify(JSON.stringify(FAILING))})
args = SimpleNamespace(state_dir=${JSON.stringify(sd)}, pr=1, repo="o/r", fetch_file=None,
reset_session=False, start_invocation=True, continue_invocation=False,
invocation_id=None, session_started_at=None,
invocation_budget_seconds=28800)
m.cmd_snapshot(args)
`
const r = spawnSync("python3", ["-c", python], { encoding: "utf8" })
expect(r.status, r.stderr).toBe(0)
const value = JSON.parse(r.stdout)
expect(value.invocation_started_at).toBe("2026-01-01T00:01:01+00:00")
expect(value.invocation_elapsed_seconds).toBe(0)
})
test("a new invocation defaults to one fixed eight-hour budget", () => {
const sd = path.join(dir, "default-invocation-budget")
const r = spawnSync("python3", [
SCRIPT, "snapshot", "--pr", "1", "--repo", "o/r", "--state-dir", sd,
"--fetch-file", fetchFile(dir, "default-invocation-budget.json", FAILING),
"--start-invocation",
], { encoding: "utf8" })
expect(r.status, r.stderr).toBe(0)
const value = JSON.parse(r.stdout)
expect(value.invocation_budget_seconds).toBe(28_800)
expect(value.invocation_remaining_seconds).toBeGreaterThan(28_790)
})
test("a new invocation preserves PR history but receives one fresh fixed eight-hour budget", () => {
const sd = path.join(dir, "invocation-boundary")
const current = {
...FAILING,
threads: [{ thread_id: "T1", last_comment_id: "C1", last_comment_at: "C1" }],
checks: [{ key: "CI/test", name: "test", status: "IN_PROGRESS", conclusion: null, details_url: "u" }],
}
const initial = snapshot(sd, fetchFile(dir, "ib1.json", current), [
"--start-invocation", "--invocation-budget-seconds", "28800",
])
mark(sd, ["--thread", "T1", "--disposition", "dispatched"])
const statePath = path.join(sd, "state.json")
const old = JSON.parse(readFileSync(statePath, "utf8"))
old.state_created_at = new Date(Date.now() - 8 * 60 * 60 * 1000).toISOString()
old.started_at = new Date(Date.now() - 6 * 60 * 60 * 1000).toISOString()
old.trajectory.stream_series = ["review", "ci"]
writeFileSync(statePath, JSON.stringify(old))
// A later explicit skill invocation does not opt into the old clock. Durable review and
// trajectory state survives, while the invocation clock starts near zero by default.
const fresh = snapshot(sd, fetchFile(dir, "ib2.json", current), [
"--start-invocation", "--invocation-budget-seconds", "28800",
])
expect(fresh.invocation_id).not.toBe(initial.invocation_id)
expect(fresh.invocation_elapsed_seconds).toBeLessThanOrEqual(1)
expect(fresh.persisted_state_age_seconds).toBeGreaterThan(28_700)
expect(fresh.counts.threads).toBe(0)
expect(JSON.parse(readFileSync(statePath, "utf8")).trajectory.stream_series).toEqual(["review", "ci"])
// Re-arms present the invocation token and preserve its fixed anchor. Put that anchor one
// second before the real eight-hour cap so the watch proves it stops against the same budget.
const almostExpired = new Date(Date.now() - 28_799_000).toISOString().replace("Z", "+00:00")
const persisted = JSON.parse(readFileSync(statePath, "utf8"))
persisted.started_at = almostExpired
writeFileSync(statePath, JSON.stringify(persisted))
const rearmed = snapshot(sd, fetchFile(dir, "ib3.json", current), [
"--invocation-id", fresh.invocation_id,
"--session-started-at", almostExpired,
"--invocation-budget-seconds", "28800",
])
expect(rearmed.invocation_id).toBe(fresh.invocation_id)
expect(rearmed.invocation_started_at).toBe(almostExpired)
const wake = watch(sd, fetchFile(dir, "ib4.json", current), [
"--invocation-id", fresh.invocation_id,
"--session-started-at", almostExpired,
"--invocation-budget-seconds", "28800",
])
expect(wake.reason).toBe("max-runtime")
expect(wake.invocation_elapsed_seconds).toBeGreaterThanOrEqual(28_800)
expect(wake.invocation_budget_seconds).toBe(28_800)
expect(wake.invocation_started_at).toBe(almostExpired)
}, 10000)
test("an invocation session start carries into a new managed-stack layer state dir", () => {
const started = new Date(Date.now() - 3_600_000).toISOString()
const d = snapshot(
path.join(dir, "next-layer"),
fetchFile(dir, "next-layer.json", FAILING),
["--continue-invocation", "--invocation-id", "managed-stack-invocation",
"--session-started-at", started, "--invocation-budget-seconds", "28800"],
)
expect(new Date(d.invocation_started_at).getTime()).toBe(new Date(started).getTime())
expect(d.invocation_elapsed_seconds).toBeGreaterThan(3_500)
})
test("re-arming watch preserves the invocation budget instead of resetting it", () => {
const sd = path.join(dir, "watch-budget")
const started = new Date(Date.now() - 10_000).toISOString()
const waiting = {
...FAILING,
threads: [],
checks: [{ key: "CI/test", name: "test", status: "IN_PROGRESS", conclusion: null, details_url: "u" }],
}
snapshot(sd, fetchFile(dir, "watch-budget-snapshot.json", waiting), [
"--continue-invocation", "--invocation-id", "watch-budget-invocation",
"--session-started-at", started, "--invocation-budget-seconds", "1",
])
const wake = watch(
sd,
fetchFile(dir, "watch-budget-watch.json", waiting),
["--invocation-id", "watch-budget-invocation", "--session-started-at", started,
"--invocation-budget-seconds", "1"],
)
expect(wake.reason).toBe("max-runtime")
}, 15000)
test("a re-arm cannot extend the fixed invocation budget", () => {
const sd = path.join(dir, "watch-budget-extension")
const started = snapshot(sd, fetchFile(dir, "watch-budget-extension-start.json", FAILING), [
"--start-invocation", "--invocation-budget-seconds", "28800",
])
const r = spawnSync("python3", [
SCRIPT, "snapshot", "--pr", "1", "--repo", "o/r", "--state-dir", sd,
"--fetch-file", fetchFile(dir, "watch-budget-extension-resume.json", FAILING),
"--invocation-id", started.invocation_id,
"--session-started-at", started.invocation_started_at,
"--invocation-budget-seconds", "57600",
], { encoding: "utf8" })
expect(r.status).not.toBe(0)
expect(r.stderr).toContain("does not match the persisted fixed budget")
expect(JSON.parse(readFileSync(path.join(sd, "state.json"), "utf8")).invocation_budget_seconds).toBe(28800)
})
test("the fixed cap outranks newly actionable work instead of allowing another round", () => {
const sd = path.join(dir, "budget-outranks-actionable")
const started = new Date(Date.now() - 2_000).toISOString()
snapshot(sd, fetchFile(dir, "budget-outranks-actionable-start.json", FAILING), [
"--continue-invocation", "--invocation-id", "expired-actionable-invocation",
"--session-started-at", started, "--invocation-budget-seconds", "1",
])
const wake = watch(sd, fetchFile(dir, "budget-outranks-actionable-watch.json", FAILING), [
"--invocation-id", "expired-actionable-invocation",
"--session-started-at", started, "--invocation-budget-seconds", "1",
])
expect(wake.reason).toBe("max-runtime")
expect(wake.invocation_elapsed_seconds).toBeGreaterThanOrEqual(2)
})
test("the fixed cap fetches terminal PR state before emitting max-runtime", async () => {
const sd = path.join(dir, "budget-terminal-precedence")
const waiting = {
...FAILING,
threads: [],
checks: [{ key: "CI/test", name: "test", status: "IN_PROGRESS", conclusion: null, details_url: "u" }],
}
const fetch = fetchFile(dir, "budget-terminal-precedence.json", waiting)
const started = snapshot(sd, fetch, ["--start-invocation", "--invocation-budget-seconds", "1"])
const watcher = startWatch(sd, fetch, [
"--interval", "5",
"--invocation-id", started.invocation_id,
"--session-started-at", started.invocation_started_at,
"--invocation-budget-seconds", "1",
])
await waitForWatchGeneration(sd)
const replacement = fetchFile(dir, "budget-terminal-precedence-closed.json", {
...waiting,
pr_state: "CLOSED",
})
renameSync(replacement, fetch)
const result = await watcher.result
expect(result.code, result.stderr).toBe(0)
const wake = JSON.parse(result.stdout.trim().split("\n").pop()!)
expect(wake.reason).toBe("terminal")
expect(wake.pr_state).toBe("CLOSED")
}, 5000)
test("the fixed cap preserves a merge-ready result from its final refresh", async () => {
const sd = path.join(dir, "budget-ready-precedence")
const waiting = {
...FAILING,
threads: [],
checks: [{ key: "CI/test", name: "test", status: "IN_PROGRESS", conclusion: null, details_url: "u" }],
}
const fetch = fetchFile(dir, "budget-ready-precedence.json", waiting)
const started = snapshot(sd, fetch, ["--start-invocation", "--invocation-budget-seconds", "1"])
const watcher = startWatch(sd, fetch, [
"--interval", "5", "--settle-seconds", "0",
"--invocation-id", started.invocation_id,
"--session-started-at", started.invocation_started_at,
"--invocation-budget-seconds", "1",
])
await waitForWatchGeneration(sd)
const replacement = fetchFile(dir, "budget-ready-precedence-clean.json", {
...waiting,
mergeable: "MERGEABLE",
merge_state_status: "CLEAN",
checks: [{ key: "CI/test", name: "test", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }],
})
renameSync(replacement, fetch)
const result = await watcher.result
expect(result.code, result.stderr).toBe(0)
expect(JSON.parse(result.stdout.trim()).reason).toBe("merge-ready")
}, 5000)
test("watch cannot start or reset an invocation budget", () => {
const sd = path.join(dir, "watch-cannot-reset")
const waiting = {
...FAILING,
threads: [],
checks: [{ key: "CI/test", name: "test", status: "IN_PROGRESS", conclusion: null, details_url: "u" }],
}
const started = snapshot(sd, fetchFile(dir, "watch-cannot-reset-start.json", waiting), ["--start-invocation"])
const r = spawnSync("python3", [
SCRIPT, "watch", "--pr", "1", "--repo", "o/r", "--state-dir", sd,
"--fetch-file", fetchFile(dir, "watch-cannot-reset-watch.json", waiting),
"--invocation-id", started.invocation_id,
"--session-started-at", started.invocation_started_at,
"--invocation-budget-seconds", String(started.invocation_budget_seconds),
"--reset-session",
], { encoding: "utf8" })
expect(r.status).not.toBe(0)
expect(r.stderr).toContain("unrecognized arguments: --reset-session")
})
test("mark requires the current invocation tuple before mutating dispositions", () => {
const sd = path.join(dir, "mark-invocation-fence")
const fetch = fetchFile(dir, "mark-invocation-fence.json", FAILING)
snapshot(sd, fetch)
const oldInvocation = persistedInvocationArgs(sd)
snapshot(sd, fetch, ["--start-invocation"])
const tokenless = spawnSync("python3", [SCRIPT, "mark", "--state-dir", sd,
"--thread", "T1", "--disposition", "dispatched"], { encoding: "utf8" })
expect(tokenless.status).not.toBe(0)
const stale = spawnSync("python3", [SCRIPT, "mark", "--state-dir", sd, ...oldInvocation,
"--thread", "T1", "--disposition", "dispatched"], { encoding: "utf8" })
expect(stale.status).not.toBe(0)
const persisted = JSON.parse(readFileSync(path.join(sd, "state.json"), "utf8"))
expect(persisted.threads?.T1?.disposition).not.toBe("dispatched")
mark(sd, ["--thread", "T1", "--disposition", "dispatched"])
const updated = JSON.parse(readFileSync(path.join(sd, "state.json"), "utf8"))
expect(updated.threads.T1.disposition).toBe("dispatched")
})
test("clearing a fork approval gate is movement (resets the settle clock so merge-ready waits for check-runs)", () => {
const sd = path.join(dir, "appr")
const gated = { ...FAILING, merge_state_status: "CLEAN", review_decision: "APPROVED", checks: [], threads: [], awaiting_approval: 1 }
snapshot(sd, fetchFile(dir, "ap1.json", gated)) // first tick
expect(snapshot(sd, fetchFile(dir, "ap2.json", gated)).changed_this_tick).toBe(false) // stable gate, no movement
// approval clears (no check-runs created yet) -> registered as movement so quiet resets
expect(snapshot(sd, fetchFile(dir, "ap3.json", { ...gated, awaiting_approval: 0 })).changed_this_tick).toBe(true)
})
test("mark --thread captures the acted baseline at mark time (closes the reviewer-reply race)", () => {
const sd = path.join(dir, "atmark")
const thr = (cid: string) => ({
...FAILING, checks: [], threads: [{ thread_id: "T1", last_comment_id: cid, last_comment_at: cid }],
})
snapshot(sd, fetchFile(dir, "am1.json", thr("C1")))
// our decision_context reply is C2; marking WITH the current fetch captures C2 as the baseline now
mark(sd, ["--thread", "T1", "--disposition", "needs-human", "--fetch-file", fetchFile(dir, "am2.json", thr("C2"))])
// a reviewer reply that raced in (C3) before the next snapshot -> reactivated, not swallowed as baseline
const d = snapshot(sd, fetchFile(dir, "am3.json", thr("C3")))
expect(d.counts.threads).toBe(1) // C3 != the C2 baseline captured at mark -> reopened
expect(d.open_needs_human).toBe(0)
})
test("mark --comment needs-human with --acted-edit-id captures the exact observation at mark time", () => {
const sd = path.join(dir, "cmark")
const fb = (edit: string) => ({
...FAILING, merge_state_status: "CLEAN", review_decision: "APPROVED", checks: [], threads: [],
feedback: [{ id: "IC_1", kind: "comment", author: "reviewer", edit_id: edit }],
})
snapshot(sd, fetchFile(dir, "cm1.json", fb("h1")))
// park needs-human with the snapshot-time edit_id as the explicit baseline
mark(sd, ["--comment", "IC_1", "--disposition", "needs-human", "--acted-edit-id", "h1"])
// an edit that races in (h2) before the next snapshot -> reactivated, not swallowed as baseline
const raced = snapshot(sd, fetchFile(dir, "cm2.json", fb("h2")))
expect(raced.counts.comments).toBe(1)
expect(raced.open_needs_human).toBe(0)
// on a dispatched mark the same flag is stored but never read: an edit stays silenced
mark(sd, ["--comment", "IC_1", "--disposition", "dispatched", "--acted-edit-id", "h2"])
expect(snapshot(sd, fetchFile(dir, "cm3.json", fb("h3"))).counts.comments).toBe(0)
})
test("a covered comment edit invalidates the decision without recording an answer", () => {
const sd = path.join(dir, "nhedit")
const fb = (edit: string) => ({
...FAILING, merge_state_status: "CLEAN", review_decision: "APPROVED", checks: [], threads: [],
feedback: [{ id: "IC_q", kind: "comment", author: "reviewer", edit_id: edit }],
})
snapshot(sd, fetchFile(dir, "nh1.json", fb("q1")))
mark(sd, ["--comment", "IC_q", "--disposition", "needs-human"]) // lazy baseline
const parked = snapshot(sd, fetchFile(dir, "nh2.json", fb("q1")))
expect(parked.counts.comments).toBe(0)
expect(parked.open_needs_human).toBe(1) // parked -> blocks merge-ready
const changed = snapshot(sd, fetchFile(dir, "nh3.json", fb("q2")))
expect(changed.counts.comments).toBe(1)
expect(changed.open_needs_human).toBe(0)
expect(changed.answered_human_decisions).toEqual([])
})
test("a dispatched thread reactivates when an EARLIER comment is edited (same last_comment_id, bumped last_comment_at)", () => {
// fetch_threads sets last_comment_at = max edit/create time across the whole thread, so an edit
// to an earlier comment (last_comment_id unchanged) still moves the identity and reopens it.
const sd = path.join(dir, "editearlier")
const thr = (at: string) => ({
...FAILING, checks: [], threads: [{ thread_id: "T1", last_comment_id: "R1", last_comment_at: at }],
})
snapshot(sd, fetchFile(dir, "ee1.json", thr("t1")))
mark(sd, ["--thread", "T1", "--disposition", "dispatched"]) // lazy baseline
expect(snapshot(sd, fetchFile(dir, "ee2.json", thr("t1"))).counts.threads).toBe(0) // baseline (R1,t1) -> silenced
// reviewer edits an earlier comment: last_comment_id stays R1 but the thread's max edit time bumps
expect(snapshot(sd, fetchFile(dir, "ee3.json", thr("t2"))).counts.threads).toBe(1) // reactivated
})
test("a dispatched top-level comment does NOT reactivate when its body is edited; a new comment id still does (#1309)", () => {
// Status bots (changeset-bot, CodeRabbit, Codecov) rewrite their own comment bodies on every
// push. Edit-keyed reactivation re-actionized the handled comment on every rewrite, so
// counts.comments never reached 0 and merge-ready could never fire. A marked comment stays
// silenced across edits; a genuinely new request is a new comment id and stays actionable.
const sd = path.join(dir, "editfb")
const fb = (feedback: object[]) => ({
...FAILING, merge_state_status: "CLEAN", review_decision: "APPROVED", checks: [], threads: [],
feedback,
})
const bot = (edit: string) => ({ id: "IC_1", kind: "comment", author: "changeset-bot", edit_id: edit })
snapshot(sd, fetchFile(dir, "e1.json", fb([bot("h1")]))) // actionable
mark(sd, ["--comment", "IC_1", "--disposition", "dispatched"])
expect(snapshot(sd, fetchFile(dir, "e2.json", fb([bot("h1")]))).counts.comments).toBe(0) // same body -> silenced
// bot rewrites its status comment on the next push -> STAYS silenced, but the edit is still
// review activity: it must reset the settle clock so merge-ready cannot fire off an old quiet
// window right after fresh edits (edit_id is part of _change_sig even though it no longer
// reopens the item)
patchState(sd, { last_change_at: isoAgo(60 * 60) })
const edited = snapshot(sd, fetchFile(dir, "e3.json", fb([bot("h2")])))
expect(edited.counts.comments).toBe(0)
expect(edited.changed_this_tick).toBe(true)
expect(edited.quiet_seconds).toBeLessThan(2)
// an unchanged tick after the edit settles normally
patchState(sd, { last_change_at: isoAgo(60 * 60) })
const settled = snapshot(sd, fetchFile(dir, "e3b.json", fb([bot("h2")])))
expect(settled.changed_this_tick).toBe(false)
expect(settled.quiet_seconds).toBeGreaterThan(60)
expect(snapshot(sd, fetchFile(dir, "e4.json", fb([bot("h3")]))).counts.comments).toBe(0)
// a brand-new comment is a new id -> actionable; the handled one stays out of the count
const next = snapshot(sd, fetchFile(dir, "e5.json", fb([bot("h3"), { id: "IC_2", kind: "comment", author: "reviewer", edit_id: "x1" }])))
expect(next.counts.comments).toBe(1)
expect(next.actionable.comments.map((c: any) => c.id)).toEqual(["IC_2"])
// explicit re-open still works
mark(sd, ["--comment", "IC_1", "--disposition", "open"])
expect(snapshot(sd, fetchFile(dir, "e6.json", fb([bot("h3"), { id: "IC_2", kind: "comment", author: "reviewer", edit_id: "x1" }]))).counts.comments).toBe(2)
})
test("a fork-PR workflow awaiting maintainer approval blocks 'all_checks_ok' and flags blocked_external", () => {
const gated = {
...FAILING,
merge_state_status: "UNSTABLE",
review_decision: "",
checks: [{ key: "Track", name: "Track", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }],
threads: [],
awaiting_approval: 1, // real CI hasn't run — awaiting a base-repo maintainer's approval
}
const d = snapshot(state, fetchFile(dir, "aa.json", gated))
expect(d.checks_awaiting_approval).toBe(1)
expect(d.has_failing_checks).toBe(false)
expect(d.all_checks_ok).toBe(false) // not "ok" — the gated CI is invisible to the rollup
expect(d.blocked_external).toBe(true)
})
test("an approval probe failure is unknown rather than a proven-clear gate", () => {
expect(probeAwaitingApproval({ status: 1 })).toBeNull()
expect(probeAwaitingApproval({ status: 0, stdout: "not-a-count" })).toBeNull()
expect(probeAwaitingApproval({ status: 0, stdout: "0" })).toBe(0)
})
test("approval review-drain clock is head-scoped and resets only on external review movement", () => {
const sd = path.join(dir, "approval-drain-state")
const gated = {
...FAILING,
head_sha: "gated-h1",
merge_state_status: "UNSTABLE",
checks: [{ key: "Track", name: "Track", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }],
threads: [],
feedback: [],
awaiting_approval: 1,
}
const first = snapshot(sd, fetchFile(dir, "approval-drain-first.json", gated))
expect(first.blocked_external).toBe(true)
expect(first.blocked_external_review_quiet_seconds).toBeLessThan(2)
const startedAt = first.blocked_external_review_last_activity_at
patchState(sd, { blocked_external_review_last_activity_at: isoAgo(10 * 60) })
const unchanged = snapshot(sd, fetchFile(dir, "approval-drain-unchanged.json", gated))
expect(unchanged.blocked_external_review_quiet_seconds).toBeGreaterThanOrEqual(9 * 60)
const activityBeforeProbeFailure = unchanged.blocked_external_review_last_activity_at
const unknown = snapshot(sd, fetchFile(dir, "approval-drain-probe-failed.json", {
...gated,
awaiting_approval: null,
}))
expect(unknown.checks_awaiting_approval).toBe(1)
expect(unknown.blocked_external).toBe(true)
expect(unknown.blocked_external_review_last_activity_at).toBe(activityBeforeProbeFailure)
const recovered = snapshot(sd, fetchFile(dir, "approval-drain-probe-recovered.json", gated))
expect(recovered.blocked_external_review_last_activity_at).toBe(activityBeforeProbeFailure)
expect(recovered.blocked_external_review_quiet_seconds).toBeGreaterThanOrEqual(9 * 60)
const persistentFeedback = {
...gated,
feedback: [{ id: "C1", kind: "comment", author: "reviewer", edit_id: "e1" }],
}
snapshot(sd, fetchFile(dir, "approval-drain-feedback-baseline.json", persistentFeedback))
patchState(sd, { blocked_external_review_last_activity_at: isoAgo(10 * 60) })
mark(sd, ["--comment", "C1", "--disposition", "dispatched", "--acted-edit-id", "e1"])
const dispositionOnly = snapshot(sd, fetchFile(dir, "approval-drain-disposition.json", persistentFeedback))
expect(dispositionOnly.blocked_external_review_quiet_seconds).toBeGreaterThanOrEqual(9 * 60)
const newFeedback = {
...persistentFeedback,
feedback: [
{ id: "C1", kind: "comment", author: "reviewer", edit_id: "e1" },
{ id: "C2", kind: "comment", author: "reviewer", edit_id: "e2" },
],
}
const moved = snapshot(sd, fetchFile(dir, "approval-drain-feedback.json", newFeedback))
expect(moved.blocked_external).toBe(false)
expect(moved.blocked_external_review_quiet_seconds).toBeLessThan(2)
expect(moved.blocked_external_review_last_activity_at).not.toBe(startedAt)
const newHead = snapshot(sd, fetchFile(dir, "approval-drain-head.json", {
...gated,
head_sha: "gated-h2",
}))
expect(newHead.blocked_external_review_quiet_seconds).toBeLessThan(2)
const cleared = snapshot(sd, fetchFile(dir, "approval-drain-cleared.json", {
...gated,
head_sha: "gated-h2",
awaiting_approval: 0,
}))
expect(cleared.blocked_external_review_last_activity_at).toBeNull()
expect(cleared.blocked_external_review_quiet_seconds).toBe(0)
})
test("external review movement resets a confirmed approval drain when the current probe is unknown", () => {
const sd = path.join(dir, "approval-drain-unknown-movement")
const gated = {
...FAILING,
head_sha: "gated-h1",
merge_state_status: "UNSTABLE",
checks: [{ key: "Track", name: "Track", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }],
threads: [],
feedback: [],
awaiting_approval: 1,
}
snapshot(sd, fetchFile(dir, "approval-drain-unknown-start.json", gated))
patchState(sd, { blocked_external_review_last_activity_at: isoAgo(10 * 60) })
const withExternalMovement = {
...gated,
feedback: [{ id: "C1", kind: "comment", author: "reviewer", edit_id: "e1" }],
awaiting_approval: null,
}
const moved = snapshot(sd, fetchFile(dir, "approval-drain-unknown-moved.json", withExternalMovement))
expect(moved.checks_awaiting_approval).toBe(1)
expect(moved.blocked_external_review_moved_this_tick).toBe(true)
expect(moved.blocked_external_review_quiet_seconds).toBeLessThan(2)
const resetAt = moved.blocked_external_review_last_activity_at
const unchanged = snapshot(sd, fetchFile(dir, "approval-drain-unknown-unchanged.json", withExternalMovement))
expect(unchanged.blocked_external_review_moved_this_tick).toBe(false)
expect(unchanged.blocked_external_review_last_activity_at).toBe(resetAt)
const cleared = snapshot(sd, fetchFile(dir, "approval-drain-unknown-cleared.json", {
...withExternalMovement,
awaiting_approval: 0,
}))
expect(cleared.checks_awaiting_approval).toBe(0)
expect(cleared.blocked_external_review_last_activity_at).toBeNull()
})
test("approval drain ignores the resolver reply baseline but wakes for a later reviewer reply", () => {
const gated = (cid: string) => ({
...FAILING,
head_sha: "gated-h1",
merge_state_status: "UNSTABLE",
checks: [{ key: "Track", name: "Track", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }],
threads: [{ thread_id: "T1", last_comment_id: cid, last_comment_at: cid }],
feedback: [],
awaiting_approval: 1,
})
const drainedState = path.join(dir, "approval-drain-resolver-reply")
snapshot(drainedState, fetchFile(dir, "approval-drain-reviewer-c1.json", gated("C1")))
const resolverReply = fetchFile(dir, "approval-drain-resolver-c2.json", gated("C2"))
mark(drainedState, ["--thread", "T1", "--disposition", "dispatched", "--fetch-file", resolverReply])
patchState(drainedState, { blocked_external_review_last_activity_at: isoAgo(10) })
const resolverObserved = snapshot(drainedState, resolverReply)
expect(resolverObserved.blocked_external_review_moved_this_tick).toBe(false)
expect(resolverObserved.blocked_external_review_quiet_seconds).toBeGreaterThanOrEqual(1)
const drained = watch(drainedState, resolverReply, ["--blocked-external-drain-seconds", "1"])
expect(drained.reason).toBe("blocked-external-drained")
expect(drained.blocked_external_review_quiet_seconds).toBeGreaterThanOrEqual(1)
const wakeState = path.join(dir, "approval-drain-later-reviewer")
snapshot(wakeState, fetchFile(dir, "approval-drain-reviewer-start.json", gated("C1")))
mark(wakeState, ["--thread", "T1", "--disposition", "dispatched", "--fetch-file", resolverReply])
patchState(wakeState, { blocked_external_review_last_activity_at: isoAgo(10) })
const reviewerReply = fetchFile(dir, "approval-drain-reviewer-c3.json", gated("C3"))
const reviewerObserved = snapshot(wakeState, reviewerReply)
expect(reviewerObserved.blocked_external_review_moved_this_tick).toBe(true)
expect(reviewerObserved.blocked_external_review_quiet_seconds).toBeLessThan(2)
expect(reviewerObserved.counts.threads).toBe(1)
expect(wakeReason(reviewerObserved)).toBe("actionable")
})
test("approval drain ignores loop-owned thread resolution but wakes for a needs-human removal", () => {
const gated = {
...FAILING,
head_sha: "gated-h1",
merge_state_status: "UNSTABLE",
checks: [{ key: "Track", name: "Track", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }],
threads: [{ thread_id: "T1", last_comment_id: "C1", last_comment_at: "C1" }],
feedback: [],
awaiting_approval: 1,
}
const resolved = fetchFile(dir, "approval-drain-resolved.json", { ...gated, threads: [] })
const dispatchedState = path.join(dir, "approval-drain-dispatched-resolution")
snapshot(dispatchedState, fetchFile(dir, "approval-drain-dispatched-start.json", gated))
mark(dispatchedState, ["--thread", "T1", "--disposition", "dispatched", "--fetch-file",
fetchFile(dir, "approval-drain-dispatched-baseline.json", gated)])
patchState(dispatchedState, { blocked_external_review_last_activity_at: isoAgo(10) })
const loopResolved = snapshot(dispatchedState, resolved)
expect(loopResolved.blocked_external_review_moved_this_tick).toBe(false)
expect(loopResolved.blocked_external_review_quiet_seconds).toBeGreaterThanOrEqual(1)
const needsHumanState = path.join(dir, "approval-drain-needs-human-removal")
snapshot(needsHumanState, fetchFile(dir, "approval-drain-needs-human-start.json", gated))
mark(needsHumanState, ["--thread", "T1", "--disposition", "needs-human", "--fetch-file",
fetchFile(dir, "approval-drain-needs-human-baseline.json", gated)])
patchState(needsHumanState, { blocked_external_review_last_activity_at: isoAgo(10) })
const externallyRemoved = snapshot(needsHumanState, resolved)
expect(externallyRemoved.blocked_external_review_moved_this_tick).toBe(true)
expect(externallyRemoved.blocked_external_review_quiet_seconds).toBeLessThan(2)
})
test("approval review-drain wakes terminally after its selected quiet bound", () => {
const sd = path.join(dir, "approval-drain-expiry")
const gated = {
...FAILING,
head_sha: "gated-h1",
merge_state_status: "UNSTABLE",
checks: [{ key: "Track", name: "Track", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }],
threads: [],
feedback: [],
awaiting_approval: 1,
}
snapshot(sd, fetchFile(dir, "approval-drain-expiry-first.json", gated))
patchState(sd, { blocked_external_review_last_activity_at: isoAgo(10) })
const expired = watch(sd, fetchFile(dir, "approval-drain-expiry-watch.json", gated), [
"--blocked-external-drain-seconds", "1",
])
expect(expired.reason).toBe("blocked-external-drained")
expect(expired.blocked_external_review_quiet_seconds).toBeGreaterThanOrEqual(1)
expect(expired.blocked_external_drain_seconds).toBe(1)
})
test("the invocation budget outranks an expired approval review-drain", () => {
const sd = path.join(dir, "approval-drain-budget")
const gated = {
...FAILING,
head_sha: "gated-h1",
merge_state_status: "UNSTABLE",
checks: [{ key: "Track", name: "Track", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }],
threads: [],
feedback: [],
awaiting_approval: 1,
}
snapshot(sd, fetchFile(dir, "approval-drain-budget-first.json", gated))
patchState(sd, {
started_at: isoAgo(9 * 3600),
last_activity_at: isoAgo(10),
dead_time_seconds: 0,
blocked_external_review_last_activity_at: isoAgo(10),
})
const expired = watch(sd, fetchFile(dir, "approval-drain-budget-watch.json", gated), [
"--blocked-external-drain-seconds", "1",
])
expect(expired.reason).toBe("max-runtime")
})
test("new review feedback outranks an expired approval review-drain", () => {
const sd = path.join(dir, "approval-drain-feedback-wake")
const gated = {
...FAILING,
head_sha: "gated-h1",
merge_state_status: "UNSTABLE",
checks: [{ key: "Track", name: "Track", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }],
threads: [],
feedback: [],
awaiting_approval: 1,
}
snapshot(sd, fetchFile(dir, "approval-drain-feedback-first.json", gated))
patchState(sd, { blocked_external_review_last_activity_at: isoAgo(10) })
const withFeedback = {
...gated,
feedback: [{ id: "C1", kind: "comment", author: "reviewer", edit_id: "e1" }],
}
const wake = watch(sd, fetchFile(dir, "approval-drain-feedback-watch.json", withFeedback), [
"--blocked-external-drain-seconds", "1",
])
expect(wake.reason).toBe("feedback-candidate")
})
test("a review lifecycle starting during an approval drain wakes for a longer bound", () => {
const sd = path.join(dir, "approval-drain-signal")
const gated = {
...FAILING,
head_sha: "gated-h1",
merge_state_status: "UNSTABLE",
checks: [{ key: "Track", name: "Track", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }],
threads: [],
feedback: [],
awaiting_approval: 1,
review_in_progress: false,
review_signal_count: 0,
review_signal_identities: [],
}
snapshot(sd, fetchFile(dir, "approval-drain-signal-first.json", gated))
const withSignal = {
...gated,
review_in_progress: true,
review_signal_count: 1,
review_signal_identities: ["review-bot"],
}
const wake = watch(sd, fetchFile(dir, "approval-drain-signal-watch.json", withSignal), [
"--blocked-external-drain-seconds", "300",
])
expect(wake.reason).toBe("blocked-external")
})
test("an empty statusCheckRollup (no check-runs yet) is not ok — checks_present false blocks a pipeline false-success", () => {
const noChecks = { ...FAILING, merge_state_status: "CLEAN", review_decision: "APPROVED", checks: [], threads: [] }
const d = snapshot(state, fetchFile(dir, "nc.json", noChecks))
expect(d.checks_present).toBe(false)
expect(d.all_checks_ok).toBe(false) // no observed checks -> not "ok"; the pipeline stop must not exit-success
expect(d.checks_terminal).toBe(true) // vacuously terminal on an empty set — exactly why checks_present is needed
})
test("_resolve_repo_ref parses the host from the PR URL so gh api targets GHE, not github.com", () => {
const r = spawnSync(
"python3",
[
"-c",
`from importlib.machinery import SourceFileLoader; ` +
`m=SourceFileLoader('prs', ${JSON.stringify(SCRIPT)}).load_module(); ` +
`print(m._resolve_repo_ref('', 'https://ghe.acme.com/o/r/pull/5')); ` +
`print(m._host_args('ghe.acme.com')); print(m._host_args(None))`,
],
{ encoding: "utf8" },
)
expect(r.status, r.stderr).toBe(0)
const lines = r.stdout.trim().split("\n")
expect(lines[0]).toBe("('o', 'r', 'ghe.acme.com')")
expect(lines[1]).toBe("['--hostname', 'ghe.acme.com']")
expect(lines[2]).toBe("[]")
})
test("cross-stream alternation: ci-only then review-only then ci-only ticks flip (churn signal)", () => {
const th = (ids: string[]) => ids.map((id) => ({ thread_id: id, last_comment_id: `c-${id}`, last_comment_at: id }))
snapshot(state, fetchFile(dir, "a1.json", { ...FAILING, head_sha: "s1", checks: [RED_CHECK], threads: [] }))
snapshot(state, fetchFile(dir, "a2.json", { ...FAILING, head_sha: "s2", checks: [GREEN_CHECK], threads: th(["T1"]) }))
const d = snapshot(state, fetchFile(dir, "a3.json", { ...FAILING, head_sha: "s3", checks: [RED_CHECK], threads: [] }))
expect(d.trajectory.stream_alternations).toBe(2) // ci -> review -> ci
})
test("non-thread feedback: a top-level comment / review body is actionable, mark --comment silences it, needs-human blocks ready", () => {
const withFeedback = {
...FAILING,
merge_state_status: "CLEAN",
review_decision: "APPROVED",
checks: [GREEN_CHECK],
threads: [],
feedback: [
{ id: "IC_1", kind: "comment", author: "reviewer" },
{ id: "PRR_1", kind: "review", author: "coderabbit", state: "COMMENTED" },
],
}
const f = fetchFile(dir, "fb.json", withFeedback)
const d = snapshot(state, f)
expect(d.counts.comments).toBe(2) // both surfaced as feedback candidates with no inline thread
expect(d.actionable.comments.map((c: any) => c.id).sort()).toEqual(["IC_1", "PRR_1"])
mark(state, ["--comment", "IC_1", "--disposition", "dispatched"])
mark(state, ["--comment", "PRR_1", "--disposition", "needs-human"])
const d2 = snapshot(state, f)
expect(d2.counts.comments).toBe(0) // dispatched item silenced; needs-human item parked, not actionable
expect(d2.open_needs_human).toBe(1) // parked comment blocks merge-ready just like a parked thread
})
test("_extract_feedback surfaces every non-empty body, the PR author's included", () => {
const v = {
author: { login: "me" },
comments: [
// The PR author's own top-level ask is how a human requests a change on an agent-opened
// PR. Excluding it here kept the tick from ever invoking ce-resolve-pr-feedback for it.
{ id: "c-me", author: { login: "me" }, body: "please rename check_id to validate_id" },
{ id: "c-cov", author: { login: "codecov[bot]" }, body: "coverage -0.1%" },
{ id: "c-wrapper", author: { login: "chatgpt-codex-connector" }, body: CODEX_WRAPPER },
{ id: "c-near-match", author: { login: "chatgpt-codex-connector" }, body: `${CODEX_WRAPPER}\n\nP1: Preserve this appended actionable finding.` },
{ id: "c-claude", author: { login: "github-actions" }, body: "<!-- claude-review-summary -->\n## Claude Review\nBLOCKING: regenerate code" },
{ id: "c-ghost", author: null, body: "feedback from an unavailable account" },
{ id: "c-empty", author: { login: "octo-reviewer" }, body: " " }, // empty -> excluded
],
reviews: [
{ id: "r-wrapper", author: { login: "chatgpt-codex-connector" }, body: CODEX_WRAPPER.replace("50ffb4dd99", "1f95273c71"), state: "COMMENTED" },
{ id: "r-codex", author: { login: "chatgpt-codex-connector" }, body: `### 💡 Codex Review\n\nhttps://github.com/o/r/blob/abc/file.ts#L1-L2\n**P2 Block archiving core questions**\n\nAdd the invariant guard.\n\n<details> <summary>ℹ️ About Codex in GitHub</summary></details>`, state: "COMMENTED" },
{ id: "r-cr", author: { login: "coderabbitai[bot]" }, body: "Actionable comments posted: 1\n\nInline review comments failed to post. Fix the custom agent ID path.", state: "COMMENTED" },
{ id: "r-empty", author: { login: "octo-reviewer" }, body: "", state: "APPROVED" }, // empty body -> excluded
],
}
expect(extractFeedback(v).map((f: any) => f.id).sort()).toEqual([
"c-claude", "c-cov", "c-ghost", "c-me", "c-near-match", "c-wrapper", "r-codex", "r-cr", "r-wrapper",
])
})
test("watch: wakes on actionable backlog, terminal, and merge-ready-after-settle; times out on clean-not-settled", () => {
const GREEN = { key: "CI/test", name: "test", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }
// actionable backlog (FAILING has an unresolved thread + a failing check) -> wake
expect(watch(path.join(dir, "w1"), fetchFile(dir, "wa.json", FAILING)).reason).toBe("actionable")
// terminal PR -> wake regardless of backlog
const term = fetchFile(dir, "wt.json", { ...FAILING, pr_state: "CLOSED", threads: [], checks: [] })
expect(watch(path.join(dir, "w2"), term).reason).toBe("terminal")
// clean + green but not yet settled (settle 300 > quiet ~0) -> keep watching -> times out
const clean = { ...FAILING, merge_state_status: "CLEAN", review_decision: "APPROVED", threads: [], checks: [GREEN] }
const cf = fetchFile(dir, "wc.json", clean)
const unsettledDir = path.join(dir, "w3")
snapshot(unsettledDir, cf, EXPIRING_TEST_INVOCATION)
expect(watch(unsettledDir, cf, ["--settle-seconds", "300"]).reason).toBe("max-runtime")
// same clean state with a zero settle window -> merge-ready wake
expect(watch(path.join(dir, "w4"), cf, ["--settle-seconds", "0"]).reason).toBe("merge-ready")
}, 15000) // spawns 4 watch subprocesses incl. a max-runtime timeout -> explicit timeout over Bun's 5s default
test("watch: a newer valid watcher supersedes the old watcher and owns the only wake", async () => {
const sd = path.join(dir, "watch-owner")
const running = {
...FAILING,
threads: [],
checks: [{ key: "CI/test", name: "test", status: "IN_PROGRESS", conclusion: null, details_url: "u" }],
}
const fetch = fetchFile(dir, "watch-owner.json", running)
snapshot(sd, fetch)
const beforeTakeover = JSON.parse(readFileSync(path.join(sd, "state.json"), "utf8"))
const oldWatch = startWatch(sd, fetch)
const oldGeneration = await waitForWatchGeneration(sd)
const newWatch = startWatch(sd, fetch)
await waitForWatchGeneration(sd, oldGeneration)
const afterTakeover = JSON.parse(readFileSync(path.join(sd, "state.json"), "utf8"))
expect(afterTakeover.started_at).toBe(beforeTakeover.started_at)
expect(afterTakeover.last_change_at).toBe(beforeTakeover.last_change_at)
const nextFetch = `${fetch}.next`
writeFileSync(nextFetch, JSON.stringify({
...running,
threads: [{ thread_id: "T-new", last_comment_id: "C1", last_comment_at: "t1" }],
}))
renameSync(nextFetch, fetch)
const [oldResult, newResult] = await Promise.all([oldWatch.result, newWatch.result])
expect(oldResult.code, oldResult.stderr).toBe(0)
expect(newResult.code, newResult.stderr).toBe(0)
const wakes = [oldResult.stdout, newResult.stdout]
.flatMap((output) => output.trim() ? output.trim().split("\n") : [])
.map((line) => JSON.parse(line))
.filter((event) => event.event === "BABYSIT_WAKE")
expect(wakes).toHaveLength(1)
expect(wakes[0].reason).toBe("actionable")
expect(wakes[0].watch_generation).toEqual(expect.any(String))
expect(oldResult.stdout).toBe("")
const persisted = JSON.parse(readFileSync(path.join(sd, "state.json"), "utf8"))
expect(persisted.watch_generation).toBe(wakes[0].watch_generation)
expect(snapshot(sd, fetch).watch_generation).toBe(wakes[0].watch_generation)
}, 15000)
test("watch: a replacement that fails preflight leaves the existing watcher active", async () => {
const sd = path.join(dir, "watch-preflight")
const running = {
...FAILING,
threads: [],
checks: [{ key: "CI/test", name: "test", status: "IN_PROGRESS", conclusion: null, details_url: "u" }],
}
const fetch = fetchFile(dir, "watch-preflight.json", running)
snapshot(sd, fetch)
const existingWatch = startWatch(sd, fetch)
const activeGeneration = await waitForWatchGeneration(sd)
const invalidFetch = path.join(dir, "invalid-watch-preflight.json")
writeFileSync(invalidFetch, "not json")
const failedReplacement = startWatch(sd, invalidFetch)
const failedResult = await failedReplacement.result
expect(failedResult.code).not.toBe(0)
expect(JSON.parse(readFileSync(path.join(sd, "state.json"), "utf8")).watch_generation).toBe(activeGeneration)
const nextFetch = `${fetch}.next`
writeFileSync(nextFetch, JSON.stringify({
...running,
threads: [{ thread_id: "T-after-failure", last_comment_id: "C1", last_comment_at: "t1" }],
}))
renameSync(nextFetch, fetch)
const existingResult = await existingWatch.result
expect(existingResult.code, existingResult.stderr).toBe(0)
const wake = JSON.parse(existingResult.stdout.trim())
expect(wake.reason).toBe("actionable")
expect(wake.watch_generation).toBe(activeGeneration)
}, 15000)
test("watch: an existing stop file wakes before reservation or preflight", () => {
const sd = path.join(dir, "watch-stopped-before-arm")
const fetch = fetchFile(dir, "watch-stopped-before-arm.json", {
...FAILING,
head_sha: "incumbent-head",
threads: [],
checks: [{ key: "CI/test", name: "test", status: "IN_PROGRESS", conclusion: null, details_url: "u" }],
})
snapshot(sd, fetch)
const statePath = path.join(sd, "state.json")
const incumbent = JSON.parse(readFileSync(statePath, "utf8"))
incumbent.watch_generation = "incumbent-generation"
incumbent.watch_pid = 999999
incumbent.watch_process_identity = "incumbent-identity"
const before = JSON.stringify(incumbent)
writeFileSync(statePath, before)
const stopFile = path.join(dir, "watch-stopped-before-arm.stop")
writeFileSync(stopFile, "stop")
const missingFetch = path.join(dir, "watch-stopped-before-arm-must-not-fetch.json")
const r = spawnSync(
"python3",
[SCRIPT, "watch", "--pr", "1", "--repo", "o/r", "--state-dir", sd,
"--fetch-file", missingFetch, "--stop-file", stopFile,
"--invocation-id", incumbent.invocation_id,
"--session-started-at", incumbent.started_at,
"--invocation-budget-seconds", String(incumbent.invocation_budget_seconds)],
{ encoding: "utf8", timeout: 5000 },
)
expect(r.status, r.stderr).toBe(0)
expect(JSON.parse(r.stdout.trim())).toMatchObject({
event: "BABYSIT_WAKE",
reason: "stop-signal",
watch_generation: "incumbent-generation",
})
expect(readFileSync(statePath, "utf8")).toBe(before)
expect(existsSync(path.join(sd, "watch-candidate.json"))).toBe(false)
})
test("watch: a newer invocation supersedes an older candidate with a slow preflight", async () => {
const sd = path.join(dir, "watch-candidate-order")
const running = {
...FAILING,
threads: [],
checks: [{ key: "CI/test", name: "test", status: "IN_PROGRESS", conclusion: null, details_url: "u" }],
}
const initial = fetchFile(dir, "watch-candidate-initial.json", running)
snapshot(sd, initial)
const slowFetch = path.join(dir, "watch-candidate-slow.fifo")
const mkfifo = spawnSync("mkfifo", [slowFetch], { encoding: "utf8" })
expect(mkfifo.status, mkfifo.stderr).toBe(0)
const olderCandidate = startWatch(sd, slowFetch)
await Bun.sleep(200) // the older invocation is blocked in its first fetch
const fastFetch = fetchFile(dir, "watch-candidate-fast.json", running)
const newerCandidate = startWatch(sd, fastFetch)
const activeGeneration = await waitForWatchGeneration(sd)
const olderStopped = await Promise.race([
olderCandidate.result.then(() => true),
Bun.sleep(1000).then(() => false),
])
if (!olderStopped) {
olderCandidate.child.kill("SIGKILL")
newerCandidate.child.kill("SIGTERM")
await Promise.all([olderCandidate.result, newerCandidate.result])
}
expect(olderStopped).toBe(true)
const nextFetch = `${fastFetch}.next`
writeFileSync(nextFetch, JSON.stringify({
...running,
threads: [{ thread_id: "T-candidate", last_comment_id: "C1", last_comment_at: "t1" }],
}))
renameSync(nextFetch, fastFetch)
const newerResult = await newerCandidate.result
expect(newerResult.code, newerResult.stderr).toBe(0)
const wake = JSON.parse(newerResult.stdout.trim())
expect(wake.watch_generation).toBe(activeGeneration)
}, 15000)
test("watch: an explicit invocation replacement emits a non-action supersession wake", async () => {
const sd = path.join(dir, "watch-invocation-superseded")
const running = {
...FAILING,
threads: [],
checks: [{ key: "CI/test", name: "test", status: "IN_PROGRESS", conclusion: null, details_url: "u" }],
}
const fetch = fetchFile(dir, "watch-invocation-superseded.json", running)
const initial = snapshot(sd, fetch, ["--start-invocation", "--invocation-budget-seconds", "28800"])
const oldWatch = startWatch(sd, fetch)
await waitForWatchGeneration(sd)
const replacement = snapshot(sd, fetch, ["--start-invocation", "--invocation-budget-seconds", "12345"])
const replacementClock = {
invocation_id: replacement.invocation_id,
started_at: replacement.invocation_started_at,
invocation_budget_seconds: replacement.invocation_budget_seconds,
}
const result = await oldWatch.result
expect(result.code, result.stderr).toBe(0)
const wake = JSON.parse(result.stdout.trim())
expect(wake).toEqual({
event: "BABYSIT_WAKE",
reason: "invocation-superseded",
watch_generation: expect.any(String),
superseded_invocation_id: initial.invocation_id,
current_invocation_id: replacement.invocation_id,
})
const persisted = JSON.parse(readFileSync(path.join(sd, "state.json"), "utf8"))
expect({
invocation_id: persisted.invocation_id,
started_at: persisted.started_at,
invocation_budget_seconds: persisted.invocation_budget_seconds,
}).toEqual(replacementClock)
}, 15000)
test("watch: an expired old budget cannot emit max-runtime after invocation replacement", async () => {
const sd = path.join(dir, "watch-expired-invocation-superseded")
const running = {
...FAILING,
threads: [],
checks: [{ key: "CI/test", name: "test", status: "IN_PROGRESS", conclusion: null, details_url: "u" }],
}
const fetch = fetchFile(dir, "watch-expired-invocation-superseded.json", running)
const initial = snapshot(sd, fetch, EXPIRING_TEST_INVOCATION)
const oldWatch = startWatch(sd, fetch, [
"--interval", "5",
"--invocation-id", initial.invocation_id,
"--session-started-at", initial.invocation_started_at,
"--invocation-budget-seconds", "1",
])
await waitForWatchGeneration(sd)
const replacement = snapshot(sd, fetch, ["--start-invocation", "--invocation-budget-seconds", "28800"])
const result = await oldWatch.result
expect(result.code, result.stderr).toBe(0)
expect(JSON.parse(result.stdout.trim())).toEqual({
event: "BABYSIT_WAKE",
reason: "invocation-superseded",
watch_generation: expect.any(String),
superseded_invocation_id: initial.invocation_id,
current_invocation_id: replacement.invocation_id,
})
}, 15000)
test("watch: takeover interrupts an old watcher blocked in its next fetch", async () => {
const sd = path.join(dir, "watch-blocked-fetch")
const running = {
...FAILING,
threads: [],
checks: [{ key: "CI/test", name: "test", status: "IN_PROGRESS", conclusion: null, details_url: "u" }],
}
const oldFetch = fetchFile(dir, "watch-blocked-old.json", running)
snapshot(sd, oldFetch)
const oldWatch = startWatch(sd, oldFetch, ["--interval", "0.2"])
const oldGeneration = await waitForWatchGeneration(sd)
const fifo = `${oldFetch}.fifo`
const mkfifo = spawnSync("mkfifo", [fifo], { encoding: "utf8" })
expect(mkfifo.status, mkfifo.stderr).toBe(0)
renameSync(fifo, oldFetch)
await Bun.sleep(300) // the old generation is now blocked opening the FIFO for its next fetch
const replacementFetch = fetchFile(dir, "watch-blocked-new.json", running)
const replacement = startWatch(sd, replacementFetch)
await waitForWatchGeneration(sd, oldGeneration)
const stoppedPromptly = await Promise.race([
oldWatch.result.then(() => true),
Bun.sleep(1000).then(() => false),
])
if (!stoppedPromptly) oldWatch.child.kill("SIGKILL")
expect(stoppedPromptly).toBe(true)
replacement.child.kill("SIGTERM")
await replacement.result
}, 15000)
test("watch: an in-flight poll cannot persist after its generation becomes stale", async () => {
const sd = path.join(dir, "watch-stale-poll")
const running = {
...FAILING,
head_sha: "current-head",
threads: [],
checks: [{ key: "CI/test", name: "test", status: "IN_PROGRESS", conclusion: null, details_url: "u" }],
}
const fetch = fetchFile(dir, "watch-stale-poll.json", running)
snapshot(sd, fetch)
const oldWatch = startWatch(sd, fetch, ["--interval", "0.2"])
await waitForWatchGeneration(sd)
const fifo = `${fetch}.fifo`
const mkfifo = spawnSync("mkfifo", [fifo], { encoding: "utf8" })
expect(mkfifo.status, mkfifo.stderr).toBe(0)
renameSync(fifo, fetch)
await Bun.sleep(300)
const statePath = path.join(sd, "state.json")
const replacementState = JSON.parse(readFileSync(statePath, "utf8"))
replacementState.watch_generation = "replacement-generation"
const nextState = `${statePath}.next`
writeFileSync(nextState, JSON.stringify(replacementState))
renameSync(nextState, statePath)
writeFileSync(fetch, JSON.stringify({ ...running, head_sha: "stale-head" }))
const result = await oldWatch.result
expect(result.code, result.stderr).toBe(0)
const persisted = JSON.parse(readFileSync(statePath, "utf8"))
expect(persisted.watch_generation).toBe("replacement-generation")
expect(persisted.head_sha).toBe("current-head")
}, 15000)
test("watch: preflight stays read-only until activation fences incumbent persistence", () => {
const sd = path.join(dir, "watch-preflight-fence")
const base = fetchFile(dir, "watch-preflight-fence-base.json", {
...FAILING,
head_sha: "base-head",
threads: [],
checks: [{ key: "CI/test", name: "test", status: "IN_PROGRESS", conclusion: null, details_url: "u" }],
})
const incumbent = fetchFile(dir, "watch-preflight-fence-incumbent.json", {
...FAILING,
head_sha: "incumbent-head",
threads: [],
checks: [{ key: "CI/test", name: "test", status: "IN_PROGRESS", conclusion: null, details_url: "u" }],
})
const successor = fetchFile(dir, "watch-preflight-fence-successor.json", {
...FAILING,
head_sha: "successor-head",
threads: [],
checks: [{ key: "CI/test", name: "test", status: "IN_PROGRESS", conclusion: null, details_url: "u" }],
})
snapshot(sd, base)
const statePath = path.join(sd, "state.json")
const active = JSON.parse(readFileSync(statePath, "utf8"))
active.watch_generation = "incumbent-generation"
writeFileSync(statePath, JSON.stringify(active))
const python = `
import json, subprocess, time
from importlib.machinery import SourceFileLoader
from types import SimpleNamespace
m = SourceFileLoader("prs", ${JSON.stringify(SCRIPT)}).load_module()
state_dir = ${JSON.stringify(sd)}
state_path = ${JSON.stringify(statePath)}
invocation = json.load(open(state_path))
args = SimpleNamespace(state_dir=state_dir, pr=1, repo="o/r",
fetch_file=${JSON.stringify(successor)}, reset_session=False,
start_invocation=False, continue_invocation=False,
invocation_id=invocation["invocation_id"],
session_started_at=invocation["started_at"],
invocation_budget_seconds=invocation["invocation_budget_seconds"])
generation = "successor-generation"
m._reserve_watch_candidate(args, generation)
cur = m._fetch_snapshot(args)
assert json.load(open(state_path))["head_sha"] == "base-head", "preflight mutated persisted state"
original_diff = m.diff
child = None
def diff_with_incumbent_race(state, current, now=None, advance_trajectory=True):
global child
child_code = '''
import json
from importlib.machinery import SourceFileLoader
from types import SimpleNamespace
m = SourceFileLoader("prs_child", ${JSON.stringify(SCRIPT)}).load_module()
invocation = json.load(open(${JSON.stringify(statePath)}))
args = SimpleNamespace(state_dir=${JSON.stringify(sd)}, pr=1, repo="o/r",
fetch_file=${JSON.stringify(incumbent)}, reset_session=False,
start_invocation=False, continue_invocation=False,
invocation_id=invocation["invocation_id"],
session_started_at=invocation["started_at"],
invocation_budget_seconds=invocation["invocation_budget_seconds"])
try:
m._run_snapshot(args, m._now(), advance_trajectory=False,
watch_generation="incumbent-generation")
except m._WatchSuperseded:
pass
else:
raise SystemExit("stale incumbent persist was not rejected")
'''
child = subprocess.Popen(["python3", "-c", child_code], stdout=subprocess.PIPE,
stderr=subprocess.PIPE, text=True)
time.sleep(0.2)
assert child.poll() is None, "incumbent was not blocked by atomic activation"
return original_diff(state, current, now, advance_trajectory=advance_trajectory)
m.diff = diff_with_incumbent_race
previous, actionable = m._activate_watch(args, generation, m._now(), cur)
stdout, stderr = child.communicate(timeout=5)
assert child.returncode == 0, stderr
persisted = json.load(open(state_path))
print(json.dumps({"generation": persisted["watch_generation"], "head": persisted["head_sha"]}))
`
const r = spawnSync("python3", ["-c", python], { encoding: "utf8", timeout: 10000 })
expect(r.status, r.stderr).toBe(0)
expect(JSON.parse(r.stdout)).toEqual({ generation: "successor-generation", head: "successor-head" })
})
test("watch: PID identity must still match before a replaced watcher is signaled", () => {
const python = `
import json
from importlib.machinery import SourceFileLoader
m = SourceFileLoader("prs", ${JSON.stringify(SCRIPT)}).load_module()
signals = []
m.os.kill = lambda pid, sig: signals.append([pid, sig])
m._process_identity = lambda pid: {123: "different", 124: None, 125: "same"}.get(pid)
for pid, identity in ((123, "old"), (124, "old"), (125, "same")):
m._terminate_replaced_watch({"pid": pid, "process_identity": identity})
print(json.dumps(signals))
`
const r = spawnSync("python3", ["-c", python], { encoding: "utf8" })
expect(r.status, r.stderr).toBe(0)
expect(JSON.parse(r.stdout)).toEqual([[125, 15]])
})
test("watch: takeover interrupts and reaps an active fetch subprocess", () => {
const childPid = path.join(dir, "watch-fetch-child.pid")
const python = `
import os, signal, subprocess, threading, time
from importlib.machinery import SourceFileLoader
from types import SimpleNamespace
m = SourceFileLoader("prs", ${JSON.stringify(SCRIPT)}).load_module()
pid_file = ${JSON.stringify(childPid)}
def fake_snapshot(args, now, advance_trajectory=True, watch_generation=None):
subprocess.run(["sh", "-c", "echo $$ > " + pid_file + "; exec sleep 30"], check=True)
return {"counts": {}, "pr_state": "OPEN", "session_seconds": 0}
def stop_when_child_starts():
deadline = time.time() + 5
while time.time() < deadline and not os.path.exists(pid_file):
time.sleep(0.01)
os.kill(os.getpid(), signal.SIGTERM)
m._run_snapshot = fake_snapshot
m._fetch_snapshot = lambda args: {}
m._reserve_watch_candidate = lambda args, generation: {}
m._clear_watch_candidate = lambda args, generation: None
m._activate_watch = lambda args, generation, now, cur: (
{}, {"counts": {}, "pr_state": "OPEN", "session_seconds": 0})
m._terminate_replaced_watch = lambda previous: None
m._watch_is_current = lambda args, generation: True
m._wake_reason = lambda actionable, settle_seconds: None
threading.Thread(target=stop_when_child_starts, daemon=True).start()
args = SimpleNamespace(reset_session=False, stop_file=None, settle_seconds=300, max_runtime=0,
interval=0.01, state_dir=${JSON.stringify(dir)}, pr=1, repo="o/r")
started = time.time()
m.cmd_watch(args)
pid = int(open(pid_file).read())
alive = True
try:
os.kill(pid, 0)
except ProcessLookupError:
alive = False
print(f"{alive} {time.time() - started:.3f}")
`
const r = spawnSync("python3", ["-c", python], { encoding: "utf8", timeout: 5000 })
expect(r.status, r.stderr).toBe(0)
const [alive, elapsed] = r.stdout.trim().split(" ")
expect(alive).toBe("False")
expect(Number(elapsed)).toBeLessThan(2)
})
test("watch: stale teardown ignores a late takeover SIGTERM and ordinary teardown restores it", () => {
const python = `
import json, os, signal, threading, time
from importlib.machinery import SourceFileLoader
from types import SimpleNamespace
m = SourceFileLoader("prs", ${JSON.stringify(SCRIPT)}).load_module()
args = SimpleNamespace(reset_session=False, stop_file=None, settle_seconds=300, max_runtime=0,
interval=0.01, state_dir=${JSON.stringify(dir)}, pr=1, repo="o/r")
actionable = {"counts": {}, "pr_state": "OPEN", "session_seconds": 0}
m._reserve_watch_candidate = lambda args, generation: {}
m._clear_watch_candidate = lambda args, generation: None
m._fetch_snapshot = lambda args: {}
m._activate_watch = lambda args, generation, now, cur: ({}, actionable)
m._terminate_replaced_watch = lambda previous: None
m._emit_wake_if_current = lambda *args, **kwargs: True
real_signal = signal.signal
signal_calls = 0
final_handler_installed = threading.Event()
def track_signal(signum, handler):
global signal_calls
result = real_signal(signum, handler)
if signum == signal.SIGTERM:
signal_calls += 1
if signal_calls == 2:
final_handler_installed.set()
time.sleep(0.2)
return result
def send_late_takeover_signal():
if not final_handler_installed.wait(2):
os._exit(2)
os.kill(os.getpid(), signal.SIGTERM)
m.signal.signal = track_signal
m._watch_is_current = lambda args, generation: False
sender = threading.Thread(target=send_late_takeover_signal)
sender.start()
m.cmd_watch(args)
sender.join(timeout=2)
assert not sender.is_alive()
m.signal.signal = real_signal
def caller_handler(_signum, _frame):
pass
real_signal(signal.SIGTERM, caller_handler)
m._watch_is_current = lambda args, generation: True
m._wake_reason = lambda actionable, settle_seconds: "actionable"
m.cmd_watch(args)
print(json.dumps({"ordinary_restored": signal.getsignal(signal.SIGTERM) is caller_handler}))
`
const r = spawnSync("python3", ["-c", python], { encoding: "utf8", timeout: 5000 })
expect(r.status, r.stderr).toBe(0)
expect(JSON.parse(r.stdout)).toEqual({ ordinary_restored: true })
})
test("fetch_threads follows every GraphQL page before returning unresolved threads", () => {
const python = `
import json
from importlib.machinery import SourceFileLoader
m = SourceFileLoader("prs", ${JSON.stringify(SCRIPT)}).load_module()
calls = []
class Result: pass
def fake(args, label):
calls.append(args)
second = any(arg == "cursor=page-2" for arg in args)
node = {"id": "T2" if second else "T1", "isResolved": False, "path": "x", "line": 1,
"comments": {"nodes": [{"id": "C2" if second else "C1", "createdAt": "t2" if second else "t1", "lastEditedAt": None}]}}
page = {"nodes": [node], "pageInfo": {"hasNextPage": not second, "endCursor": None if second else "page-2"}}
result = Result()
result.returncode = 0
result.stderr = ""
result.stdout = json.dumps({"data": {"repository": {"pullRequest": {"reviewThreads": page}}}})
return result
m._run_checked = fake
threads = m.fetch_threads(1, "o", "r")
print(json.dumps({"ids": [t["thread_id"] for t in threads], "calls": calls}))
`
const r = spawnSync("python3", ["-c", python], { encoding: "utf8" })
expect(r.status, r.stderr).toBe(0)
const result = JSON.parse(r.stdout)
expect(result.ids).toEqual(["T1", "T2"])
expect(result.calls).toHaveLength(2)
expect(result.calls[1]).toContain("cursor=page-2")
})
test("watch: managed target freshness defers to GitHub — stale on trunk drift with CLEAN is merge-ready, stale without CLEAN blocks", () => {
const GREEN = { key: "CI/test", name: "test", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }
const managedStale = {
...FAILING,
merge_state_status: "CLEAN",
review_decision: "APPROVED",
checks: [GREEN],
threads: [],
pr_chain: {
manager_status: "confirmed",
manager_source: "gh-stack",
relationship_status: "dependent",
target_needs_rebase: true,
upstack_needs_rebase: [],
},
}
const clean = snapshot(path.join(dir, "stack-stale"), fetchFile(dir, "stack-stale.json", managedStale))
expect(clean.stack_blocker).toBeNull()
expect(wakeReason(clean)).toBe("merge-ready")
const notClean = { ...managedStale, merge_state_status: "BLOCKED" }
const blocked = snapshot(path.join(dir, "stack-stale-blocked"), fetchFile(dir, "stack-stale-blocked.json", notClean))
expect(blocked.stack_blocker).toBe("target-needs-rebase")
expect(wakeReason(blocked)).toBe("stack-blocked")
}, 15000)
test("watch: unknown managed freshness blocks ready only when GitHub is not CLEAN, while stale upstack alone still permits ready-as-next", () => {
const GREEN = { key: "CI/test", name: "test", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }
const base = {
...FAILING,
merge_state_status: "CLEAN",
review_decision: "APPROVED",
checks: [GREEN],
threads: [],
}
const unknown = {
...base,
pr_chain: {
manager_status: "confirmed",
manager_source: "graphql",
relationship_status: "dependent",
target_needs_rebase: null,
upstack_needs_rebase: [],
},
}
expect(wakeReason(snapshot(path.join(dir, "stack-unknown"), fetchFile(dir, "stack-unknown.json", unknown)))).toBe("merge-ready")
const unknownNotClean = { ...unknown, merge_state_status: "BLOCKED" }
expect(wakeReason(snapshot(path.join(dir, "stack-unknown-blocked"), fetchFile(dir, "stack-unknown-blocked.json", unknownNotClean)))).toBe("stack-blocked")
const readyAsNext = {
...base,
pr_chain: {
manager_status: "confirmed",
manager_source: "gh-stack",
relationship_status: "dependent",
target_needs_rebase: false,
upstack_needs_rebase: [{ number: 43, position: 3 }],
},
}
expect(wakeReason(snapshot(path.join(dir, "stack-up"), fetchFile(dir, "stack-up.json", readyAsNext)))).toBe("merge-ready")
}, 15000)
test("watch: manager probe error is a residual, not an unmanaged merge-ready fallback", () => {
const GREEN = { key: "CI/test", name: "test", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }
const probeError = {
...FAILING,
merge_state_status: "CLEAN",
review_decision: "APPROVED",
checks: [GREEN],
threads: [],
pr_chain: {
manager_status: "probe-error",
manager_source: null,
relationship_status: "independent",
target_needs_rebase: null,
upstack_needs_rebase: [],
},
}
expect(wakeReason(snapshot(path.join(dir, "stack-error"), fetchFile(dir, "stack-error.json", probeError)))).toBe("stack-blocked")
}, 15000)
test("watch: unresolved ordinary relationship classification also blocks an independent-readiness claim", () => {
const GREEN = { key: "CI/test", name: "test", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }
const relationshipError = {
...FAILING,
merge_state_status: "CLEAN",
review_decision: "APPROVED",
checks: [GREEN],
threads: [],
pr_chain: {
manager_status: "absent",
manager_source: null,
relationship_status: "probe-error",
target_needs_rebase: null,
upstack_needs_rebase: [],
},
}
expect(wakeReason(snapshot(path.join(dir, "relationship-error"), fetchFile(dir, "relationship-error.json", relationshipError)))).toBe("stack-blocked")
}, 15000)
test("watch: labels a comments-only wake as a feedback candidate while CI is running", () => {
const RUNNING = { key: "CI/test", name: "test", status: "IN_PROGRESS", conclusion: null, details_url: "u" }
const candidate = {
...FAILING,
threads: [],
checks: [RUNNING],
feedback: [{ id: "IC_status", kind: "comment", author: "review-bot", edit_id: "status-v1" }],
}
expect(watch(path.join(dir, "wfc"), fetchFile(dir, "wfc.json", candidate)).reason).toBe("feedback-candidate")
}, 15000)
test("watch: an in-progress review signal blocks until the 15-minute stale-review check", () => {
const base = {
...FAILING,
merge_state_status: "CLEAN",
review_decision: "APPROVED",
threads: [],
checks: [GREEN_CHECK],
counts: { threads: 0, ci: 0, comments: 0 },
checks_terminal: true,
has_failing_checks: false,
checks_awaiting_approval: 0,
open_needs_human: 0,
stack_blocker: null,
}
expect(wakeReason({ ...base, review_in_progress: true, quiet_seconds: 899 }, 0)).toBeNull()
expect(wakeReason({ ...base, review_in_progress: true, quiet_seconds: 900 }, 0)).toBe("merge-ready")
expect(wakeReason({ ...base, review_in_progress: false, quiet_seconds: 0 }, 0)).toBe("merge-ready")
})
test("snapshot: remembers an incomplete current-head review after the eyes signal disappears", () => {
const base = {
...FAILING,
merge_state_status: "CLEAN",
review_decision: "APPROVED",
threads: [],
checks: [GREEN_CHECK],
}
snapshot(state, fetchFile(dir, "signal-absent.json", { ...base, review_in_progress: false }))
const statePath = path.join(state, "state.json")
const prior = JSON.parse(readFileSync(statePath, "utf8"))
prior.last_change_at = "2026-07-17T12:00:00+00:00"
writeFileSync(statePath, JSON.stringify(prior))
const started = snapshot(state, fetchFile(dir, "signal-started.json", { ...base, review_in_progress: true }))
expect(started.review_signal_seen_on_head).toBe(true)
expect(started.review_signal_first_seen_at).toBe(started.review_signal_last_changed_at)
expect(started.review_signal_first_seen_at).not.toBe(prior.last_change_at)
const firstSeenAt = started.review_signal_first_seen_at
const disappeared = snapshot(state, fetchFile(dir, "signal-disappeared.json", { ...base, review_in_progress: false }))
expect(disappeared.review_in_progress).toBe(false)
expect(disappeared.review_signal_seen_on_head).toBe(true)
expect(disappeared.review_signal_first_seen_at).toBe(firstSeenAt)
expect(disappeared.review_signal_last_changed_at).not.toBe(firstSeenAt)
expect(disappeared.changed_this_tick).toBe(true)
expect(disappeared.quiet_seconds).toBeLessThan(2)
const nextHead = snapshot(state, fetchFile(dir, "signal-new-head.json", {
...base,
head_sha: "s2",
review_in_progress: false,
}))
expect(nextHead.review_signal_seen_on_head).toBe(false)
expect(nextHead.review_signal_first_seen_at).toBeNull()
expect(nextHead.review_signal_last_changed_at).toBeNull()
})
test("snapshot: eyes identity changes reset quiet time even when the count stays fixed", () => {
expect(eyesReactionIdentities([[
{ content: "eyes", user: { node_id: "U_bot_b" } },
{ content: "eyes", user: { node_id: "U_bot_a" } },
{ content: "+1", user: { node_id: "U_other" } },
]])).toEqual(["U_bot_a", "U_bot_b"])
const base = {
...FAILING,
merge_state_status: "CLEAN",
review_decision: "APPROVED",
threads: [],
checks: [GREEN_CHECK],
review_in_progress: true,
}
const first = snapshot(state, fetchFile(dir, "signal-count-one.json", {
...base,
review_signal_identities: ["U_bot_a"],
}))
expect(first.review_signal_count).toBe(1)
expect(first.review_signal_identities).toEqual(["U_bot_a"])
const statePath = path.join(state, "state.json")
const prior = JSON.parse(readFileSync(statePath, "utf8"))
prior.last_change_at = "2026-07-17T12:00:00+00:00"
prior.review_signal_last_changed_at = prior.last_change_at
writeFileSync(statePath, JSON.stringify(prior))
const swapped = snapshot(state, fetchFile(dir, "signal-reviewer-swapped.json", {
...base,
review_signal_identities: ["U_bot_b"],
}))
expect(swapped.review_in_progress).toBe(true)
expect(swapped.review_signal_count).toBe(1)
expect(swapped.review_signal_identities).toEqual(["U_bot_b"])
expect(swapped.review_signal_last_changed_at).not.toBe(prior.review_signal_last_changed_at)
expect(swapped.changed_this_tick).toBe(true)
expect(swapped.quiet_seconds).toBeLessThan(2)
const persisted = JSON.parse(readFileSync(statePath, "utf8"))
persisted.last_change_at = "2026-07-17T12:00:00+00:00"
persisted.review_signal_last_changed_at = persisted.last_change_at
writeFileSync(statePath, JSON.stringify(persisted))
const unchanged = snapshot(state, fetchFile(dir, "signal-reviewer-unchanged.json", {
...base,
review_signal_identities: ["U_bot_b"],
}))
expect(unchanged.review_in_progress).toBe(true)
expect(unchanged.review_signal_count).toBe(1)
expect(unchanged.changed_this_tick).toBe(false)
expect(unchanged.quiet_seconds).toBeGreaterThan(60)
})
test("snapshot: a count-only legacy review signal migrates to reactor identities", () => {
const base = {
...FAILING,
merge_state_status: "CLEAN",
review_decision: "APPROVED",
threads: [],
checks: [GREEN_CHECK],
review_in_progress: true,
review_signal_count: 1,
}
snapshot(state, fetchFile(dir, "signal-legacy-count.json", base))
const statePath = path.join(state, "state.json")
const legacy = JSON.parse(readFileSync(statePath, "utf8"))
delete legacy.review_signal_identities
legacy.last_change_at = "2026-07-17T12:00:00+00:00"
legacy.review_signal_last_changed_at = legacy.last_change_at
writeFileSync(statePath, JSON.stringify(legacy))
const migrated = snapshot(state, fetchFile(dir, "signal-identity-aware.json", {
...base,
review_signal_identities: ["U_bot_a"],
}))
expect(migrated.review_in_progress).toBe(true)
expect(migrated.review_signal_count).toBe(1)
expect(migrated.review_signal_identities).toEqual(["U_bot_a"])
expect(migrated.review_signal_seen_on_head).toBe(true)
expect(migrated.changed_this_tick).toBe(true)
expect(migrated.quiet_seconds).toBeLessThan(2)
})
test("watch: a no-check MERGEABLE/CLEAN PR still reaches merge-ready (the >=1-check guard is pipeline-only)", () => {
// A repo with no configured checks: all_checks_ok is false (no observed check), but the
// interactive merge-ready wake must still fire for a CLEAN/MERGEABLE PR with no backlog.
const nochecks = { ...FAILING, merge_state_status: "CLEAN", review_decision: "APPROVED", threads: [], checks: [] }
expect(watch(path.join(dir, "nc1"), fetchFile(dir, "nc1.json", nochecks), ["--settle-seconds", "0"]).reason).toBe("merge-ready")
}, 15000)
test("watch: a dispatched terminal-red check present at arm is a standing residual — kept watching, not re-woken", () => {
// A failing check ce-debug marked dispatched leaves counts.ci == 0 while has_failing_checks stays
// true. It was already surfaced when it was dispatched, so it is in the watch's arm-time baseline
// and must NOT re-wake the loop (that was the pre-gating behavior); the watch keeps running for
// other streams. `blocked-failing` only fires on a *later* transition to terminal-red (e.g. a
// rerun completing red) — the same wake-on-new path the parked-needs-human test exercises.
const red = { ...FAILING, threads: [], checks: [{ key: "CI/test", name: "test", status: "COMPLETED", conclusion: "FAILURE", details_url: "u" }] }
const rf = fetchFile(dir, "wbf.json", red)
const sd = path.join(dir, "wbf")
snapshot(sd, rf, EXPIRING_TEST_INVOCATION) // this standing-residual watch deliberately expires
mark(sd, ["--check", "CI/test"]) // now dispatched -> counts.ci == 0, terminal-red residual, already surfaced
expect(watch(sd, rf).reason).toBe("max-runtime")
}, 15000)
test("watch: a parked needs-human does not wake or end the loop — it keeps watching the other streams", () => {
// The stop-vs-residual fix: a standing needs-human present at arm time must NOT re-wake the
// detector (that would busy-wake / falsely terminate the self-sustaining watch); the watch keeps
// polling for new work and only wakes when something genuinely new arrives.
const sd = path.join(dir, "nhwatch")
const base = (extra: any[] = []) => ({
pr_state: "OPEN", mergeable: "MERGEABLE", merge_state_status: "CLEAN", review_decision: null,
head_sha: "s1", url: "http://x/1", checks: [],
threads: [{ thread_id: "T1", last_comment_id: "C1", last_comment_at: "C1" }, ...extra],
})
snapshot(sd, fetchFile(dir, "nhw1.json", base()), EXPIRING_TEST_INVOCATION)
mark(sd, ["--thread", "T1", "--disposition", "needs-human"])
// parked needs-human, nothing else actionable -> keeps watching, times out (does NOT wake needs-human)
expect(watch(sd, fetchFile(dir, "nhw2.json", base())).reason).toBe("max-runtime")
// The capped invocation is over. A later explicit invocation preserves the parked disposition,
// while a new actionable thread still wakes that new invocation.
const withNew = fetchFile(dir, "nhw3.json", base([{ thread_id: "T2", last_comment_id: "D1", last_comment_at: "D1" }]))
snapshot(sd, withNew, ["--start-invocation"])
expect(watch(sd, withNew).reason).toBe("actionable")
}, 15000)
// Incident: two CLEAN PRs received `git merge origin/main` after a sibling merged, one pushed
// (restarting green CI). Ordinary base movement with CLEAN never yields a currency item, so the
// engine flags a two-parent head whose second parent is the base tip and no claimed item
// produced it. The wake is a distinct reason so the tick reports it as a defect, not maintenance.
test("unrequested base merge: a base-parent merge head with no claimed currency item is flagged and wakes once", () => {
const base = { host: "github.com", repository: "o/r", ref: "main",
oid: "2222222222222222222222222222222222222222", graphql_oid: "2222222222222222222222222222222222222222",
historical_oid: "1111111111111111111111111111111111111111", identity: "current" }
const clean = {
...FAILING, merge_state_status: "CLEAN", review_decision: "APPROVED",
checks: [{ key: "CI/test", name: "test", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }],
threads: [], base, head_sha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
head_parents: ["9999999999999999999999999999999999999999"],
}
const first = snapshot(state, fetchFile(dir, "ubm-1.json", clean))
expect(first.unrequested_base_merge).toBeNull()
// A plain new commit (single parent) is not flagged.
const plain = snapshot(state, fetchFile(dir, "ubm-2.json", { ...clean,
head_sha: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
head_parents: ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"] }))
expect(plain.unrequested_base_merge).toBeNull()
// A merge whose second parent is the base tip, with no currency item ever claimed → flagged.
const merged = snapshot(state, fetchFile(dir, "ubm-3.json", { ...clean,
head_sha: "cccccccccccccccccccccccccccccccccccccccc",
head_parents: ["bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "2222222222222222222222222222222222222222"] }))
expect(merged.unrequested_base_merge).toEqual(expect.objectContaining({
head: "cccccccccccccccccccccccccccccccccccccccc",
base_parent: "2222222222222222222222222222222222222222",
}))
expect(wakeReason(merged)).toBe("unrequested-base-merge")
// Persists on the same head across polls; cleared by the next head.
const again = snapshot(state, fetchFile(dir, "ubm-3.json", { ...clean,
head_sha: "cccccccccccccccccccccccccccccccccccccccc",
head_parents: ["bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "2222222222222222222222222222222222222222"] }))
expect(again.unrequested_base_merge?.head).toBe("cccccccccccccccccccccccccccccccccccccccc")
const next = snapshot(state, fetchFile(dir, "ubm-4.json", { ...clean,
head_sha: "dddddddddddddddddddddddddddddddddddddddd",
head_parents: ["cccccccccccccccccccccccccccccccccccccccc"] }))
expect(next.unrequested_base_merge).toBeNull()
expect(wakeReason(next)).not.toBe("unrequested-base-merge")
})
// Observed live (tmchow/pr-stack-test, 2026-08-17): after a squash-merge to main GitHub kept
// MERGEABLE/CLEAN but left potentialMergeCommit on the old base for 20+ minutes, so `race`
// never cleared and every harness idled to budget. The degrade accepts the cached verdict once
// head and live base have been stable that long, and discloses it.
test("stale merge computation: a head-matching race degrades to stale-computation after the bounded window", () => {
const race = {
...FAILING, merge_state_status: "CLEAN", review_decision: "APPROVED",
checks: [{ key: "CI/test", name: "test", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }],
threads: [], head_sha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
base: { host: "github.com", repository: "o/r", ref: "main",
oid: "2222222222222222222222222222222222222222", graphql_oid: "2222222222222222222222222222222222222222",
historical_oid: "1111111111111111111111111111111111111111",
merge_parent_oids: ["1111111111111111111111111111111111111111", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"],
identity: "race", stale_computation_candidate: true },
}
const file = fetchFile(dir, "stale-race.json", race)
const first = snapshot(state, file)
expect(first.base_ref_blocker).toBe("race")
expect(first.mergeability_certain).toBe(false)
expect(wakeReason(first)).toBe("base-ref-blocked")
const statePath = path.join(state, "state.json")
const persisted = JSON.parse(readFileSync(statePath, "utf8"))
expect(persisted.stale_merge_computation?.key).toBe(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:2222222222222222222222222222222222222222")
persisted.stale_merge_computation.first_seen_at = "2026-07-17T12:00:00+00:00"
persisted.last_change_at = "2026-07-17T12:00:00+00:00"
writeFileSync(statePath, JSON.stringify(persisted))
const later = snapshot(state, file)
expect(later.base.identity).toBe("stale-computation")
expect(later.base.merge_computation_stale).toBe(true)
expect(later.base_ref_blocker).toBeNull()
expect(later.mergeability_certain).toBe(true)
expect(wakeReason(later)).toBe("merge-ready")
// Presentation-only degrade must not reset the quiet clock (#1562).
expect(JSON.parse(readFileSync(statePath, "utf8")).last_change_at).toBe("2026-07-17T12:00:00+00:00")
expect(wakeReason(later, 300)).toBe("merge-ready")
// A pending (unclassified) head forces uncertainty even through the stale-computation degrade.
const pendingStale = snapshot(state, fetchFile(dir, "stale-race-pending.json", { ...race,
head_sha: "cccccccccccccccccccccccccccccccccccccccc", head_parents: null,
base: { ...race.base, merge_parent_oids: ["1111111111111111111111111111111111111111", "cccccccccccccccccccccccccccccccccccccccc"] } }))
expect(pendingStale.unrequested_base_merge_pending).toBe(true)
const sp = JSON.parse(readFileSync(statePath, "utf8"))
sp.stale_merge_computation.first_seen_at = "2026-07-17T12:00:00+00:00"
writeFileSync(statePath, JSON.stringify(sp))
const pendingStale2 = snapshot(state, fetchFile(dir, "stale-race-pending.json", { ...race,
head_sha: "cccccccccccccccccccccccccccccccccccccccc", head_parents: null,
base: { ...race.base, merge_parent_oids: ["1111111111111111111111111111111111111111", "cccccccccccccccccccccccccccccccccccccccc"] } }))
expect(pendingStale2.base_ref_blocker).toBe("mergeability-pending")
expect(pendingStale2.mergeability_certain).toBe(false)
// A base parent that does not match the head is a genuine race, never degraded.
const genuine = snapshot(path.join(dir, "genuine-race"), fetchFile(dir, "genuine-race.json", {
...race, base: { ...race.base, stale_computation_candidate: false } }))
expect(genuine.base_ref_blocker).toBe("race")
// A moved head resets the window.
const moved = snapshot(state, fetchFile(dir, "stale-race-moved.json", { ...race,
head_sha: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
base: { ...race.base, merge_parent_oids: ["1111111111111111111111111111111111111111", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"] } }))
expect(moved.base_ref_blocker).toBe("race")
})
test("genuine base identity transitions still change the settle signature", () => {
const frozen = "2026-07-17T12:00:00+00:00"
const baseCore = {
host: "github.com", repository: "o/r", ref: "main",
oid: "2222222222222222222222222222222222222222",
graphql_oid: "2222222222222222222222222222222222222222",
historical_oid: "1111111111111111111111111111111111111111",
merge_parent_oids: ["2222222222222222222222222222222222222222", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"],
}
const green = {
...FAILING, merge_state_status: "CLEAN", review_decision: "APPROVED",
checks: [{ key: "CI/test", name: "test", status: "COMPLETED", conclusion: "SUCCESS", details_url: "u" }],
threads: [], head_sha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
base: { ...baseCore, identity: "probe-error" },
}
const file = fetchFile(dir, "identity-steps.json", green)
snapshot(state, file)
const statePath = path.join(state, "state.json")
const identities = ["race", "mergeability-pending", "current"]
for (const identity of identities) {
const persisted = JSON.parse(readFileSync(statePath, "utf8"))
persisted.last_change_at = frozen
writeFileSync(statePath, JSON.stringify(persisted))
snapshot(state, fetchFile(dir, `identity-${identity}.json`, {
...green, base: { ...baseCore, identity },
}))
expect(JSON.parse(readFileSync(statePath, "utf8")).last_change_at).not.toBe(frozen)
}
})
// Pipelined stack traversal: the watcher polls the active (upper) layer and keeps probing the
// lower layers it was told about. A NEW thread / comment / failing check / head below wakes
// `downstack-actionable` so the walk returns to the lowest unsettled layer; what was already
// there at arm time (parked, dispatched) does not.
test("downstack probe: a new lower-layer item wakes downstack-actionable; the arm-time baseline does not", async () => {
const quietUpper = {
...FAILING, merge_state_status: "BLOCKED", review_decision: null,
checks: [{ key: "CI/test", name: "test", status: "IN_PROGRESS", conclusion: null, details_url: "u" }],
threads: [], head_sha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
}
const upperFile = fetchFile(dir, "upper.json", quietUpper)
const lowerQuiet = { pr_state: "OPEN", head_sha: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", threads: [], feedback: [],
checks: [{ key: "CI/test", status: "COMPLETED", conclusion: "SUCCESS" }] }
const lowerFile = fetchFile(dir, "lower.json", lowerQuiet)
// Baseline: an already-open lower thread at arm time is not a wake.
writeFileSync(lowerFile, JSON.stringify({ ...lowerQuiet, threads: [{ thread_id: "OLD", last_comment_id: "c", last_comment_at: "t" }] }))
snapshot(state, upperFile, ["--start-invocation"])
const { child, result } = startWatch(state, upperFile, ["--downstack-pr", "7", "--downstack-fetch-file", `7=${lowerFile}`,
"--interval", "0.2"])
await waitForWatchGeneration(state)
await Bun.sleep(600)
expect(child.exitCode).toBeNull() // still watching: baseline item did not wake
// A new lower-layer thread arrives.
writeFileSync(lowerFile, JSON.stringify({ ...lowerQuiet, threads: [
{ thread_id: "OLD", last_comment_id: "c", last_comment_at: "t" },
{ thread_id: "NEW", last_comment_id: "c2", last_comment_at: "t2" }] }))
const out = await Promise.race([result, Bun.sleep(4000).then(() => null)])
expect(out).not.toBeNull()
const wake = JSON.parse(out!.stdout.trim().split("\n").pop()!)
expect(wake.reason).toBe("downstack-actionable")
expect(wake.downstack_prs).toEqual([7])
})
test("downstack probe: a lower-layer head change alone wakes; an unrequested-base-merge residual does not re-wake after re-arm", async () => {
const quietUpper = {
...FAILING, merge_state_status: "BLOCKED", review_decision: null,
checks: [{ key: "CI/test", name: "test", status: "IN_PROGRESS", conclusion: null, details_url: "u" }],
threads: [], head_sha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
}
const upperFile = fetchFile(dir, "upper2.json", quietUpper)
const lowerQuiet = { pr_state: "OPEN", head_sha: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", threads: [], feedback: [],
checks: [{ key: "CI/test", status: "COMPLETED", conclusion: "SUCCESS" }] }
const lowerFile = fetchFile(dir, "lower2.json", lowerQuiet)
snapshot(state, upperFile, ["--start-invocation"])
const { child, result } = startWatch(state, upperFile, ["--downstack-pr", "7", "--downstack-fetch-file", `7=${lowerFile}`, "--interval", "0.2"])
await waitForWatchGeneration(state)
await Bun.sleep(400)
expect(child.exitCode).toBeNull()
// Loss of quiescence with no new event: the lower layer turns DIRTY on the same head.
writeFileSync(lowerFile, JSON.stringify({ ...lowerQuiet, merge_state_status: "DIRTY" }))
const out = await Promise.race([result, Bun.sleep(4000).then(() => null)])
expect(out).not.toBeNull()
expect(JSON.parse(out!.stdout.trim().split("\n").pop()!).reason).toBe("downstack-actionable")
// Loss of manager confirmation on the same head wakes; so does an edit in an existing thread.
const s4 = path.join(dir, "ds-mgr")
const lower4 = fetchFile(dir, "lower4.json", { ...lowerQuiet, pr_chain: { manager_status: "confirmed", target_needs_rebase: false },
threads: [{ thread_id: "T", last_comment_id: "c", last_comment_at: "t1" }] })
snapshot(s4, upperFile, ["--start-invocation"])
const w4 = startWatch(s4, upperFile, ["--downstack-pr", "7", "--downstack-fetch-file", `7=${lower4}`, "--interval", "0.2"])
await waitForWatchGeneration(s4)
await Bun.sleep(400)
writeFileSync(lower4, JSON.stringify({ ...lowerQuiet, pr_chain: { manager_status: "probe-error", target_needs_rebase: null },
threads: [{ thread_id: "T", last_comment_id: "c", last_comment_at: "t1" }] }))
const out4 = await Promise.race([w4.result, Bun.sleep(4000).then(() => null)])
expect(out4).not.toBeNull()
expect(JSON.parse(out4!.stdout.trim().split("\n").pop()!).reason).toBe("downstack-actionable")
const s5 = path.join(dir, "ds-edit")
const lower5 = fetchFile(dir, "lower5.json", { ...lowerQuiet, threads: [{ thread_id: "T", last_comment_id: "c", last_comment_at: "t1" }] })
snapshot(s5, upperFile, ["--start-invocation"])
const w5 = startWatch(s5, upperFile, ["--downstack-pr", "7", "--downstack-fetch-file", `7=${lower5}`, "--interval", "0.2"])
await waitForWatchGeneration(s5)
await Bun.sleep(400)
writeFileSync(lower5, JSON.stringify({ ...lowerQuiet, threads: [{ thread_id: "T", last_comment_id: "c", last_comment_at: "t2" }] }))
const out5 = await Promise.race([w5.result, Bun.sleep(4000).then(() => null)])
expect(out5).not.toBeNull()
expect(JSON.parse(out5!.stdout.trim().split("\n").pop()!).reason).toBe("downstack-actionable")
// And a head change alone.
const s3 = path.join(dir, "ds-head")
const lower3 = fetchFile(dir, "lower3.json", lowerQuiet)
snapshot(s3, upperFile, ["--start-invocation"])
const w3 = startWatch(s3, upperFile, ["--downstack-pr", "7", "--downstack-fetch-file", `7=${lower3}`, "--interval", "0.2"])
await waitForWatchGeneration(s3)
await Bun.sleep(400)
writeFileSync(lower3, JSON.stringify({ ...lowerQuiet, head_sha: "cccccccccccccccccccccccccccccccccccccccc" }))
const out3 = await Promise.race([w3.result, Bun.sleep(4000).then(() => null)])
expect(out3).not.toBeNull()
expect(JSON.parse(out3!.stdout.trim().split("\n").pop()!).reason).toBe("downstack-actionable")
// Residual suppression: an unrequested_base_merge present at arm time is in the baseline.
const flagged = { ...quietUpper, checks: [], head_sha: "dddddddddddddddddddddddddddddddddddddddd",
base: { host: "github.com", repository: "o/r", ref: "main", oid: "2222222222222222222222222222222222222222",
graphql_oid: "2222222222222222222222222222222222222222", identity: "current" },
head_parents: ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "2222222222222222222222222222222222222222"] }
const s2 = path.join(dir, "ubm-rearm")
snapshot(s2, fetchFile(dir, "ubm-r1.json", { ...quietUpper, checks: [] }), ["--start-invocation"])
const flaggedFile = fetchFile(dir, "ubm-r2.json", flagged)
expect(snapshot(s2, flaggedFile).unrequested_base_merge).not.toBeNull()
const w2 = startWatch(s2, flaggedFile, ["--interval", "0.2"])
await waitForWatchGeneration(s2)
await Bun.sleep(700)
expect(w2.child.exitCode).toBeNull() // did not immediately re-wake on the already-surfaced residual
w2.child.kill()
await w2.result
})
test("unrequested base merge: a claimed DIRTY repair that observed its mutation is not flagged", () => {
const fixture = currencyFixture({ merge_state_status: "DIRTY", mergeable: "CONFLICTING",
head_sha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
base: { host: "github.com", repository: "o/r", ref: "main",
oid: "2222222222222222222222222222222222222222", graphql_oid: "2222222222222222222222222222222222222222",
identity: "current" } }) as any
const dirty = snapshot(state, fetchFile(dir, "ubm-dirty-1.json", fixture))
const key = dirty.branch_currency?.key
expect(key).toBeTruthy()
markCurrency(state, key, "claimed")
markCurrencyOutcome(state, key, "mutation-observed")
const baseOid = String(fixture.base.oid)
const repaired = snapshot(state, fetchFile(dir, "ubm-dirty-2.json", { ...fixture,
merge_state_status: "CLEAN", mergeable: "MERGEABLE",
head_sha: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
head_parents: [String(fixture.head_sha), baseOid] }))
expect(repaired.unrequested_base_merge).toBeNull()
// Control: the same merge head on a fresh state with no claim is flagged.
const control = path.join(dir, "ubm-control-state")
snapshot(control, fetchFile(dir, "ubm-dirty-1.json", fixture))
const flagged = snapshot(control, fetchFile(dir, "ubm-dirty-3.json", { ...fixture,
merge_state_status: "CLEAN", mergeable: "MERGEABLE",
head_sha: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
head_parents: [String(fixture.head_sha), baseOid] }))
expect(flagged.unrequested_base_merge?.base_parent).toBe(baseOid)
// A transient parents-probe failure on the head-change tick keeps the head unclassified and
// retryable: detection fires on the first later snapshot that carries parents.
const s3 = path.join(dir, "ubm-retry")
snapshot(s3, fetchFile(dir, "ubm-t1.json", fixture), ["--start-invocation"])
const noParents = snapshot(s3, fetchFile(dir, "ubm-t2.json", { ...fixture,
merge_state_status: "CLEAN", mergeable: "MERGEABLE",
head_sha: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", head_parents: null }))
expect(noParents.unrequested_base_merge).toBeNull()
expect(noParents.unrequested_base_merge_pending).toBe(true)
expect(noParents.base_ref_blocker).toBe("mergeability-pending")
expect(noParents.mergeability_certain).toBe(false)
const recovered = snapshot(s3, fetchFile(dir, "ubm-t3.json", { ...fixture,
merge_state_status: "CLEAN", mergeable: "MERGEABLE",
head_sha: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
head_parents: [String(fixture.head_sha), baseOid] }))
expect(recovered.unrequested_base_merge?.base_parent).toBe(baseOid)
expect(recovered.unrequested_base_merge_pending).toBe(false)
// A consumed item for base A does not exempt a merge of the same head with a newer base B.
const s6 = path.join(dir, "ubm-newer-base")
snapshot(s6, fetchFile(dir, "ubm-nb1.json", fixture))
const k6 = snapshot(s6, fetchFile(dir, "ubm-nb1.json", fixture)).branch_currency?.key
markCurrency(s6, k6, "claimed"); markCurrencyOutcome(s6, k6, "mutation-observed")
const newerBase = "3333333333333333333333333333333333333333"
const withB = snapshot(s6, fetchFile(dir, "ubm-nb2.json", { ...fixture,
merge_state_status: "CLEAN", mergeable: "MERGEABLE",
base: { ...fixture.base, oid: newerBase, graphql_oid: newerBase },
head_sha: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
head_parents: [String(fixture.head_sha), newerBase] }))
expect(withB.unrequested_base_merge?.base_parent).toBe(newerBase)
// A consumed item from an earlier head does not exempt a later, unrelated base merge.
const stale = snapshot(state, fetchFile(dir, "ubm-dirty-4.json", { ...fixture,
merge_state_status: "CLEAN", mergeable: "MERGEABLE",
head_sha: "ffffffffffffffffffffffffffffffffffffffff",
head_parents: ["eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", baseOid] }))
expect(stale.unrequested_base_merge?.base_parent).toBe(baseOid)
})
})
// UTF-8 gh/git output under a non-UTF-8 locale (issue #1346)
//
// _run() used subprocess.run(..., text=True) with no encoding=, so Python decoded
// stdout with the locale encoding (cp1252 on Windows, ascii under C). gh emits UTF-8;
// a curly quote (U+201D, last byte 0x9d) crashed the reader thread and left stdout None.
// Forcing C + PYTHONUTF8=0 reproduces that decode failure on UTF-8 CI.
describe("pr-snapshot _run pins UTF-8 under a non-UTF-8 locale (#1346)", () => {
const NON_UTF8_LOCALE = {
...process.env,
LC_ALL: "C",
LANG: "C",
LC_CTYPE: "C",
PYTHONUTF8: "0",
PYTHONCOERCECLOCALE: "0",
}
test("_run decodes UTF-8 curly-quote stdout instead of raising UnicodeDecodeError", () => {
const python = `
import json, sys
from importlib.machinery import SourceFileLoader
m = SourceFileLoader("prs", ${JSON.stringify(SCRIPT)}).load_module()
child = [sys.executable, "-c",
"import sys; sys.stdout.buffer.write(b'{\\"title\\": \\"hello \\\\xe2\\\\x80\\\\x9d world\\"}')"]
r = m._run(child)
print(json.dumps({"returncode": r.returncode, "stdout": r.stdout, "title": json.loads(r.stdout)["title"]}))
`
const r = spawnSync("python3", ["-c", python], { encoding: "utf8", env: NON_UTF8_LOCALE })
expect(r.status, r.stderr).toBe(0)
expect(JSON.parse(r.stdout)).toEqual({
returncode: 0,
stdout: '{"title": "hello \u201d world"}',
title: "hello \u201d world",
})
})
})