Files
kilocode/.github/docs-sync/selftest.mjs
T

4686 lines
187 KiB
JavaScript
Raw 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.
// kilocode_change - new file
/**
* Offline self-check for the docs-sync failure paths (S4).
* Plain node:assert, no network, no LLM, no new dependency.
* Run: node .github/docs-sync/selftest.mjs
*/
import assert from "node:assert/strict"
import { execFileSync, spawn, spawnSync } from "node:child_process"
import fs from "node:fs"
import os from "node:os"
import path from "node:path"
import { fileURLToPath } from "node:url"
import { sleepSync } from "./lib.mjs"
import { mergeOrFallback, DEFAULT_BRANCH } from "./prepare-branch.mjs"
import { isSurfaceBranch, surfaceNameFromBranch } from "./surfaces.mjs"
import { applyCap, pickWatermark } from "./watermark.mjs"
import {
computeUncovered,
computeProcessedThrough,
routeRows,
dropLegacySkipped,
noDiffReport,
LEARNINGS_FILE,
nonContentFiles,
resolveLearnedThrough,
patchProcessedThrough,
renderBody,
extractSectionRows,
surfaceBranch,
} from "./upsert-pr.mjs"
import {
revertTitleKind,
parseRevertTargets,
computeRevertAnnotations,
applyRevertAnnotations,
unannotatedRevertSignals,
} from "./reverts.mjs"
import {
parseLearnings,
renderLearnings,
parseLearnedThrough,
patchMarkerIntoBody,
parseDelta,
validateDelta,
applyDelta,
isTrustedComment,
promptBlock,
} from "./learn.mjs"
const HERE = path.dirname(fileURLToPath(import.meta.url))
const EDIT_SCRIPT = path.join(HERE, "edit.mjs")
const TRIAGE_SCRIPT = path.join(HERE, "triage.mjs")
const COLLECT_SCRIPT = path.join(HERE, "collect.mjs")
const LEARN_SCRIPT = path.join(HERE, "learn.mjs")
const UPSERT_SCRIPT = path.join(HERE, "upsert-pr.mjs")
const PREP_SCRIPT = path.join(HERE, "prepare-branch.mjs")
const temps = []
function mktemp(prefix) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix))
temps.push(dir)
return dir
}
function cleanup() {
for (const dir of temps.splice(0)) {
try {
fs.rmSync(dir, { recursive: true, force: true })
} catch {
// best-effort
}
}
}
function writeExecutable(filePath, body) {
fs.writeFileSync(filePath, body, { mode: 0o755 })
}
function makeStubKiloDir({ mode, callLog, stderrText = "event stream disconnected" }) {
const dir = mktemp("docs-sync-kilo-")
const kiloPath = path.join(dir, "kilo")
// mode: "stderr-exit0" | "record" | "partial-triage" | "mixed-triage" | "write-edit-summary"
const script = `#!/usr/bin/env node
const fs = require("node:fs");
const path = require("node:path");
const mode = ${JSON.stringify(mode)};
const callLog = ${JSON.stringify(callLog ?? "")};
const stderrText = ${JSON.stringify(stderrText)};
if (callLog) {
fs.appendFileSync(callLog, JSON.stringify({ argv: process.argv.slice(2), cwd: process.cwd() }) + "\\n");
}
if (mode === "stderr-exit0") {
process.stderr.write(stderrText + "\\n");
process.exit(0);
}
if (mode === "record") {
process.stderr.write("recorded\\n");
process.exit(0);
}
// Parse -f chunk/batch file from args for triage stubs
const args = process.argv.slice(2);
const fIdx = args.indexOf("-f");
const fileArg = fIdx >= 0 ? args[fIdx + 1] : null;
let chunk = [];
if (fileArg && fs.existsSync(fileArg)) {
try { chunk = JSON.parse(fs.readFileSync(fileArg, "utf8")); } catch { chunk = []; }
}
if (mode === "write-edit-summary") {
// Success path: write the batch summary so edit.mjs returns true, while still
// emitting stderr so selftest can assert runKilo persisted it unconditionally.
process.stderr.write(stderrText + "\\n");
const m = fileArg && String(fileArg).match(/edit-batch-(\\d+)\\.json/);
const index = m ? m[1] : "0";
const summary = chunk.map((d) => ({
pr: d.number,
url: d.url,
action: "skipped",
reason: "selftest stub",
}));
fs.mkdirSync("docs-sync-out", { recursive: true });
fs.writeFileSync("docs-sync-out/edit-summary-" + index + ".json", JSON.stringify(summary));
process.exit(0);
}
if (mode === "partial-triage") {
// Classify only a proper subset (first URL) of the chunk.
const owned = chunk.slice(0, Math.max(0, chunk.length - 1));
const entries = owned.map((d) => ({
pr: d.number,
url: d.url,
docs_worthy: false,
reason: "genuine not worthy",
target_sections: [],
priority: "medium",
}));
if (entries.length === 0 && chunk.length > 0) {
// single-PR chunk: still leave one missing by emitting empty-ish foreign-only
process.stdout.write("[]\\n");
} else {
process.stdout.write(JSON.stringify(entries) + "\\n");
}
process.exit(0);
}
if (mode === "mixed-triage") {
// Half docs_worthy true, half fail (no output for second half — but we return
// only some entries so backfill marks the rest pending). Actually: return
// docs_worthy:true for first half of chunk URLs so worthy > 0.
const half = Math.ceil(chunk.length / 2);
const entries = chunk.slice(0, half).map((d) => ({
pr: d.number,
url: d.url,
docs_worthy: true,
reason: "needs docs",
target_sections: ["overview"],
priority: "high",
}));
process.stdout.write(JSON.stringify(entries) + "\\n");
process.exit(0);
}
if (mode === "triage-embed-env-secret") {
// Valid triage JSON with a secret env value embedded in a string field
// (stdout is persisted to triage-raw-*.txt; must be redacted at capture).
const secret = process.env.KILO_API_KEY || "missing-secret";
const entries = chunk.map((d) => ({
pr: d.number,
url: d.url,
docs_worthy: true,
reason: "needs docs; diagnostic=" + secret,
target_sections: ["overview"],
priority: "high",
}));
process.stdout.write(JSON.stringify(entries) + "\\n");
process.exit(0);
}
if (mode === "extraction-delta") {
const deltaFile = "docs-sync-out/extraction-delta.json";
if (fs.existsSync(deltaFile)) {
const delta = JSON.parse(fs.readFileSync(deltaFile, "utf8"));
process.stdout.write(JSON.stringify(delta) + "\\n");
} else {
process.stdout.write('{"add":[],"remove":[]}' + "\\n");
}
process.exit(0);
}
process.stderr.write("unknown stub mode\\n");
process.exit(1);
`
writeExecutable(kiloPath, script)
return dir
}
function gitIn(cwd, args, env = {}) {
return execFileSync("git", args, {
cwd,
env: { ...process.env, ...env },
stdio: ["ignore", "pipe", "pipe"],
encoding: "utf8",
})
.toString()
.trim()
}
function makeGitRunner(cwd, env = {}) {
return (args) => gitIn(cwd, args, env)
}
function initRepoWithIdentity(dir) {
gitIn(dir, ["init", "-b", "main"])
gitIn(dir, ["config", "user.name", "docs-sync-selftest"])
gitIn(dir, ["config", "user.email", "docs-sync-selftest@example.com"])
gitIn(dir, ["config", "commit.gpgsign", "false"])
}
// ---------------------------------------------------------------------------
// Case 1 — Defect A: mergeOrFallback
// ---------------------------------------------------------------------------
function case1_mergeOrFallback() {
console.log("case 1: Defect A (mergeOrFallback)")
// 1a — identity configured + clean merge → mode=update
{
const dir = mktemp("docs-sync-merge-clean-")
initRepoWithIdentity(dir)
fs.writeFileSync(path.join(dir, "a.txt"), "base\n")
gitIn(dir, ["add", "a.txt"])
gitIn(dir, ["commit", "-m", "base"])
gitIn(dir, ["checkout", "-b", DEFAULT_BRANCH])
fs.writeFileSync(path.join(dir, "b.txt"), "on branch\n")
gitIn(dir, ["add", "b.txt"])
gitIn(dir, ["commit", "-m", "branch commit"])
// Advance main without conflict
gitIn(dir, ["checkout", "main"])
fs.writeFileSync(path.join(dir, "c.txt"), "on main\n")
gitIn(dir, ["add", "c.txt"])
gitIn(dir, ["commit", "-m", "main advance"])
gitIn(dir, ["update-ref", "refs/remotes/origin/main", "main"])
gitIn(dir, ["checkout", DEFAULT_BRANCH])
const result = mergeOrFallback({ branch: DEFAULT_BRANCH, git: makeGitRunner(dir) })
assert.equal(result.mode, "update")
assert.equal(result.branch, DEFAULT_BRANCH)
// merge brought c.txt in
assert.ok(fs.existsSync(path.join(dir, "c.txt")))
}
// 1b — genuine conflict → mode=conflict, abort succeeds, original branch untouched
{
const dir = mktemp("docs-sync-merge-conflict-")
initRepoWithIdentity(dir)
fs.writeFileSync(path.join(dir, "conflict.txt"), "base\n")
gitIn(dir, ["add", "conflict.txt"])
gitIn(dir, ["commit", "-m", "base"])
const baseSha = gitIn(dir, ["rev-parse", "HEAD"])
gitIn(dir, ["checkout", "-b", DEFAULT_BRANCH])
fs.writeFileSync(path.join(dir, "conflict.txt"), "branch side\n")
gitIn(dir, ["add", "conflict.txt"])
gitIn(dir, ["commit", "-m", "branch edit"])
const branchShaBefore = gitIn(dir, ["rev-parse", "HEAD"])
gitIn(dir, ["checkout", "main"])
fs.writeFileSync(path.join(dir, "conflict.txt"), "main side\n")
gitIn(dir, ["add", "conflict.txt"])
gitIn(dir, ["commit", "-m", "main edit"])
gitIn(dir, ["update-ref", "refs/remotes/origin/main", "main"])
gitIn(dir, ["checkout", DEFAULT_BRANCH])
const result = mergeOrFallback({ branch: DEFAULT_BRANCH, git: makeGitRunner(dir) })
assert.equal(result.mode, "conflict")
assert.ok(result.branch.startsWith(`${DEFAULT_BRANCH}-`))
// Original rolling branch tip unchanged
const branchShaAfter = gitIn(dir, ["rev-parse", DEFAULT_BRANCH])
assert.equal(branchShaAfter, branchShaBefore)
// No merge in progress
let mergeHead = true
try {
gitIn(dir, ["rev-parse", "-q", "--verify", "MERGE_HEAD"])
} catch {
mergeHead = false
}
assert.equal(mergeHead, false)
void baseSha
}
// 1c — identity-less / non-conflict merge failure → throws (does not fake conflict)
{
const dir = mktemp("docs-sync-merge-noid-")
initRepoWithIdentity(dir)
fs.writeFileSync(path.join(dir, "a.txt"), "base\n")
gitIn(dir, ["add", "a.txt"])
gitIn(dir, ["commit", "-m", "base"])
gitIn(dir, ["checkout", "-b", DEFAULT_BRANCH])
fs.writeFileSync(path.join(dir, "b.txt"), "branch\n")
gitIn(dir, ["add", "b.txt"])
gitIn(dir, ["commit", "-m", "branch"])
gitIn(dir, ["checkout", "main"])
fs.writeFileSync(path.join(dir, "c.txt"), "main\n")
gitIn(dir, ["add", "c.txt"])
gitIn(dir, ["commit", "-m", "main"])
gitIn(dir, ["update-ref", "refs/remotes/origin/main", "main"])
gitIn(dir, ["checkout", DEFAULT_BRANCH])
// Strip identity so merge cannot create a commit
gitIn(dir, ["config", "--unset", "user.name"])
gitIn(dir, ["config", "--unset", "user.email"])
const env = {
GIT_CONFIG_GLOBAL: "/dev/null",
GIT_CONFIG_SYSTEM: "/dev/null",
GIT_CONFIG_NOSYSTEM: "1",
}
const git = (args) =>
execFileSync("git", ["-c", "user.useConfigOnly=true", ...args], {
cwd: dir,
env: { ...process.env, ...env },
stdio: ["ignore", "pipe", "pipe"],
encoding: "utf8",
})
.toString()
.trim()
assert.throws(
() => mergeOrFallback({ branch: DEFAULT_BRANCH, git }),
(err) => {
// Must throw the original merge error, not a merge --abort failure
const msg = String(err?.stderr ?? err?.message ?? err)
assert.ok(!/no merge to abort/i.test(msg), `should not reach merge --abort: ${msg}`)
return true
},
)
}
}
// ---------------------------------------------------------------------------
// Helpers to run edit.mjs / triage.mjs as child processes
// ---------------------------------------------------------------------------
function setupEditCwd(worthy, triage) {
const cwd = mktemp("docs-sync-edit-")
fs.mkdirSync(path.join(cwd, "docs-sync-out"), { recursive: true })
fs.writeFileSync(path.join(cwd, "docs-sync-out", "worthy.json"), JSON.stringify(worthy, null, 2))
fs.writeFileSync(path.join(cwd, "docs-sync-out", "triage.json"), JSON.stringify(triage, null, 2))
return cwd
}
function setupTriageCwd(digest) {
const cwd = mktemp("docs-sync-triage-")
fs.mkdirSync(path.join(cwd, "docs-sync-out"), { recursive: true })
fs.writeFileSync(path.join(cwd, "docs-sync-out", "digest.json"), JSON.stringify(digest, null, 2))
return cwd
}
function runNodeScript(scriptPath, { cwd, env = {}, kiloDir, args = [] }) {
const pathEnv = [kiloDir, process.env.PATH].filter(Boolean).join(path.delimiter)
const result = spawnSync(process.execPath, [scriptPath, ...args], {
cwd,
env: {
...process.env,
...env,
PATH: pathEnv,
DOCS_SYNC_BACKOFF_MS: env.DOCS_SYNC_BACKOFF_MS ?? "0",
},
encoding: "utf8",
timeout: 60_000,
})
return {
status: result.status,
stdout: String(result.stdout ?? ""),
stderr: String(result.stderr ?? ""),
output: `${result.stdout ?? ""}${result.stderr ?? ""}`,
error: result.error,
}
}
function samplePr(n, { merged_at, repo = "Kilo-Org/cloud" } = {}) {
return {
repo,
number: n,
title: `feat: sample ${n}`,
url: `https://github.com/${repo}/pull/${n}`,
author: "dev",
merged_at: merged_at ?? "2026-07-20T12:00:00.000Z",
labels: [],
body: "body",
files: [],
files_total: 1,
patch_excerpt: "",
}
}
// ---------------------------------------------------------------------------
// Case 2 — Defect B: edit.mjs with stub kilo (exit 0 + stderr)
// ---------------------------------------------------------------------------
function case2_defectB() {
console.log("case 2: Defect B (edit.mjs stderr-on-exit-0)")
const prs = [1, 2, 3, 4, 5].map((n) => samplePr(n))
const worthy = prs
const triage = prs.map((p) => ({
pr: p.number,
url: p.url,
docs_worthy: true,
reason: "needs docs",
target_sections: ["overview"],
priority: "high",
}))
const cwd = setupEditCwd(worthy, triage)
const stderrText = "event stream disconnected DIAG-CASE2"
const kiloDir = makeStubKiloDir({ mode: "stderr-exit0", stderrText })
const started = Date.now()
const result = runNodeScript(EDIT_SCRIPT, {
cwd,
kiloDir,
env: {
EDIT_MODEL: "test/model",
DOCS_SYNC_BACKOFF_MS: "0",
// Enough budget for 3 attempts × tiny timeout
EDIT_BUDGET_MINUTES: "5",
EDIT_BATCH_TIMEOUT_MINUTES: "1",
},
})
const elapsed = Date.now() - started
assert.equal(result.status, 0, `edit.mjs exit: ${result.output}`)
// Backoff collapsed — 3 attempts without 60s+300s waits
assert.ok(elapsed < 15_000, `backoff should collapse with DOCS_SYNC_BACKOFF_MS=0; elapsed=${elapsed}ms`)
assert.match(result.output, /stderr tail:/)
assert.match(result.output, /DIAG-CASE2|event stream disconnected/)
assert.match(result.output, /attempt 1/)
assert.match(result.output, /attempt 2/)
// 3 attempts
assert.match(result.output, /attempt 3|failed after up to 3 attempts/)
const summary = JSON.parse(fs.readFileSync(path.join(cwd, ".docs-sync-summary.json"), "utf8"))
assert.equal(summary.length, 5)
for (const e of summary) {
assert.equal(e.action, "pending", `expected pending, got ${JSON.stringify(e)}`)
}
const uncovered = computeUncovered({ worthy, summary, triage })
assert.equal(uncovered.length, 5)
for (const u of uncovered) {
assert.ok(u.reason, "uncovered reason present")
}
}
// ---------------------------------------------------------------------------
// Case 2b — AC4a: every docs-sync kilo run argv carries --auto
// ---------------------------------------------------------------------------
/** Slice `args: [` … matching `]` from source (newlines allowed inside). */
function extractArgsArraySlice(source) {
const start = source.indexOf("args: [")
assert.ok(start >= 0, "args: [ not found in source")
let i = start + "args: ".length
assert.equal(source[i], "[")
let depth = 0
for (; i < source.length; i++) {
const ch = source[i]
if (ch === "[") depth++
else if (ch === "]") {
depth--
if (depth === 0) return source.slice(start, i + 1)
}
}
throw new assert.AssertionError({ message: "unclosed args: [ array in source" })
}
/** Label → kilo-stderr filename rule (must match lib.mjs runKilo). */
function kiloStderrLogName(label) {
return `kilo-stderr-${String(label).replace(/[^A-Za-z0-9._-]/g, "-")}.log`
}
function case2b_autoFlag() {
console.log("case 2b: AC4a (--auto on every docs-sync kilo run)")
// (i) region-scoped static check on triage.mjs / edit.mjs argv arrays
for (const name of ["triage.mjs", "edit.mjs"]) {
const src = fs.readFileSync(path.join(HERE, name), "utf8")
const slice = extractArgsArraySlice(src)
assert.ok(slice.includes('"--auto"'), `${name} args array must contain "--auto"; got:\n${slice}`)
}
// (ii) Fix verify failures step: join the run: | block and require --auto on kilo run
{
const yml = fs.readFileSync(path.join(HERE, "..", "workflows", "docs-sync.yml"), "utf8")
const stepIdx = yml.indexOf("Fix verify failures")
assert.ok(stepIdx >= 0, "Fix verify failures step missing")
const afterStep = yml.slice(stepIdx)
const runIdx = afterStep.indexOf("run: |")
assert.ok(runIdx >= 0, "run: | missing after Fix verify failures")
const blockStart = stepIdx + runIdx + "run: |".length
const rest = yml.slice(blockStart)
// Block ends at next unindented step key or EOF — collect indented lines
const lines = []
for (const line of rest.split("\n")) {
if (line === "") {
lines.push(line)
continue
}
// stop at next top-level list item under steps (two-space + "- ")
if (/^ {0,6}- name:/.test(line) || (/^\S/.test(line) && lines.length > 0)) break
lines.push(line)
}
// Join continuation backslashes then collapse whitespace for the kilo run line
const joined = lines
.map((l) => l.replace(/^\s+/, ""))
.join("\n")
.replace(/\\\n/g, " ")
.replace(/\s+/g, " ")
assert.match(joined, /kilo run\b/, `expected kilo run in Fix verify block:\n${joined}`)
const kiloCmd = joined.match(/kilo run\b[^|]*/)?.[0] ?? ""
assert.ok(
/\s--auto\b/.test(kiloCmd) || /kilo run\s+--auto\b/.test(kiloCmd),
`Fix verify kilo run must contain --auto; got: ${kiloCmd}`,
)
// The step runs under `set -o pipefail` + the default `bash -e`, so an
// unguarded kilo pipeline aborts the block before verify2.log is written
// once the CLI exits nonzero on a mid-stream error. The rebuild must decide
// this step's outcome, not the agent's exit code.
// Window is the end of the kilo pipeline → the rebuild, so a comment
// elsewhere in the block cannot satisfy the guard assertion.
const teeIdx = joined.indexOf("tee -a docs-sync-out/edit-log.txt")
assert.ok(teeIdx >= 0, `expected the kilo pipeline to tee edit-log.txt:\n${joined}`)
const kiloPipeline = joined.slice(teeIdx, joined.indexOf("bun run", teeIdx))
assert.match(
kiloPipeline,
/\|\|\s*(echo|true)\b/,
`Fix verify kilo pipeline must be guarded (|| echo/true) so bash -e cannot skip the rebuild; got: ${kiloPipeline}`,
)
assert.match(joined, /verify2\.log/, "Fix verify block must still write verify2.log")
}
// (iii) authoritative: real stub invocations with callLog — every argv has --auto
{
const prs = [1, 2, 3, 4, 5].map((n) => samplePr(n))
const worthy = prs
const triage = prs.map((p) => ({
pr: p.number,
url: p.url,
docs_worthy: true,
reason: "needs docs",
target_sections: ["overview"],
priority: "high",
}))
const cwd = setupEditCwd(worthy, triage)
const callLog = path.join(cwd, "kilo-calls.log")
const stderrText = "event stream disconnected DIAG-AUTO"
const kiloDir = makeStubKiloDir({ mode: "stderr-exit0", stderrText, callLog })
const result = runNodeScript(EDIT_SCRIPT, {
cwd,
kiloDir,
env: {
EDIT_MODEL: "test/model",
DOCS_SYNC_BACKOFF_MS: "0",
EDIT_BUDGET_MINUTES: "5",
EDIT_BATCH_TIMEOUT_MINUTES: "1",
},
})
assert.equal(result.status, 0, `edit.mjs exit: ${result.output}`)
assert.ok(fs.existsSync(callLog), "callLog must be written (stub was invoked)")
const lines = fs.readFileSync(callLog, "utf8").trim().split("\n").filter(Boolean)
assert.ok(lines.length > 0, "callCount > 0 required (vacuous empty log forbidden)")
for (const line of lines) {
const { argv } = JSON.parse(line)
assert.ok(
Array.isArray(argv) && argv.includes("--auto"),
`every kilo argv must include --auto; got ${JSON.stringify(argv)}`,
)
}
}
}
// ---------------------------------------------------------------------------
// Case 2c — full child stderr always written (success and failure paths)
// ---------------------------------------------------------------------------
function case2c_stderrLogAlways() {
console.log("case 2c: unconditional kilo-stderr-*.log")
const prs = [1, 2, 3, 4, 5].map((n) => samplePr(n))
const worthy = prs
const triage = prs.map((p) => ({
pr: p.number,
url: p.url,
docs_worthy: true,
reason: "needs docs",
target_sections: ["overview"],
priority: "high",
}))
// Failure path: stub exits 0 without summary (same mode as case 2)
{
const cwd = setupEditCwd(worthy, triage)
const stderrText = "FAILPATH-STDERR-MARKER"
const kiloDir = makeStubKiloDir({ mode: "stderr-exit0", stderrText })
const result = runNodeScript(EDIT_SCRIPT, {
cwd,
kiloDir,
env: {
EDIT_MODEL: "test/model",
DOCS_SYNC_BACKOFF_MS: "0",
EDIT_BUDGET_MINUTES: "5",
EDIT_BATCH_TIMEOUT_MINUTES: "1",
},
})
assert.equal(result.status, 0, result.output)
const logName = kiloStderrLogName("edit batch 0 attempt 1")
const logPath = path.join(cwd, "docs-sync-out", logName)
assert.equal(logName, "kilo-stderr-edit-batch-0-attempt-1.log")
assert.ok(fs.existsSync(logPath), `expected ${logPath} on failure path`)
assert.match(fs.readFileSync(logPath, "utf8"), /FAILPATH-STDERR-MARKER/)
}
// Success path: stub writes summary (today's path that discarded stderr)
{
const cwd = setupEditCwd(worthy, triage)
const stderrText = "SUCCESSPATH-STDERR-MARKER"
const kiloDir = makeStubKiloDir({ mode: "write-edit-summary", stderrText })
const result = runNodeScript(EDIT_SCRIPT, {
cwd,
kiloDir,
env: {
EDIT_MODEL: "test/model",
DOCS_SYNC_BACKOFF_MS: "0",
EDIT_BUDGET_MINUTES: "5",
EDIT_BATCH_TIMEOUT_MINUTES: "1",
},
})
assert.equal(result.status, 0, result.output)
assert.ok(
fs.existsSync(path.join(cwd, "docs-sync-out", "edit-summary-0.json")),
"stub must write summary (success path)",
)
const logName = kiloStderrLogName("edit batch 0 attempt 1")
const logPath = path.join(cwd, "docs-sync-out", logName)
assert.ok(fs.existsSync(logPath), `expected ${logPath} on success path`)
assert.match(fs.readFileSync(logPath, "utf8"), /SUCCESSPATH-STDERR-MARKER/)
}
}
// ---------------------------------------------------------------------------
// Case 2d — redact secret env values from captured kilo stderr (artifact-safe)
// ---------------------------------------------------------------------------
function case2d_redactEnvSecrets() {
console.log("case 2d: redact env secrets from kilo stderr capture")
const prs = [1, 2, 3, 4, 5].map((n) => samplePr(n))
const worthy = prs
const triage = prs.map((p) => ({
pr: p.number,
url: p.url,
docs_worthy: true,
reason: "needs docs",
target_sections: ["overview"],
priority: "high",
}))
const cwd = setupEditCwd(worthy, triage)
const secret = "selftest-secret-value-12345"
const stderrText = `leak before ${secret} after`
const kiloDir = makeStubKiloDir({ mode: "stderr-exit0", stderrText })
const result = runNodeScript(EDIT_SCRIPT, {
cwd,
kiloDir,
env: {
EDIT_MODEL: "test/model",
DOCS_SYNC_BACKOFF_MS: "0",
EDIT_BUDGET_MINUTES: "5",
EDIT_BATCH_TIMEOUT_MINUTES: "1",
KILO_API_KEY: secret,
},
})
assert.equal(result.status, 0, `edit.mjs exit: ${result.output}`)
const logName = kiloStderrLogName("edit batch 0 attempt 1")
const logPath = path.join(cwd, "docs-sync-out", logName)
assert.ok(fs.existsSync(logPath), `expected ${logPath}`)
const logBody = fs.readFileSync(logPath, "utf8")
assert.ok(!logBody.includes(secret), `persisted stderr must not contain secret; got: ${logBody}`)
assert.ok(logBody.includes("leak before *** after"), `persisted stderr must redact to exact line; got: ${logBody}`)
// Console stderr-tail region must also be redacted (not only the artifact file).
const tailIdx = result.output.indexOf("stderr tail:")
assert.ok(tailIdx >= 0, `expected stderr tail: in output; got: ${result.output}`)
const tailRegion = result.output.slice(tailIdx)
assert.ok(!tailRegion.includes(secret), `console stderr tail must not contain secret; got: ${tailRegion}`)
}
// ---------------------------------------------------------------------------
// Case 2e — longer secret first when a shorter env value is a prefix
// ---------------------------------------------------------------------------
function case2e_prefixSecretOrdering() {
console.log("case 2e: prefix-secret ordering (longer value redacted first)")
const prs = [1, 2, 3, 4, 5].map((n) => samplePr(n))
const worthy = prs
const triage = prs.map((p) => ({
pr: p.number,
url: p.url,
docs_worthy: true,
reason: "needs docs",
target_sections: ["overview"],
priority: "high",
}))
const cwd = setupEditCwd(worthy, triage)
const shortSecret = "abcdefgh"
const longSecret = "abcdefghIJKL-tail"
const stderrText = `leak: ${longSecret} end`
const kiloDir = makeStubKiloDir({ mode: "stderr-exit0", stderrText })
const result = runNodeScript(EDIT_SCRIPT, {
cwd,
kiloDir,
env: {
EDIT_MODEL: "test/model",
DOCS_SYNC_BACKOFF_MS: "0",
EDIT_BUDGET_MINUTES: "5",
EDIT_BATCH_TIMEOUT_MINUTES: "1",
A_KEY: shortSecret,
B_TOKEN: longSecret,
},
})
assert.equal(result.status, 0, `edit.mjs exit: ${result.output}`)
const logName = kiloStderrLogName("edit batch 0 attempt 1")
const logPath = path.join(cwd, "docs-sync-out", logName)
assert.ok(fs.existsSync(logPath), `expected ${logPath}`)
const logBody = fs.readFileSync(logPath, "utf8")
assert.ok(!logBody.includes("IJKL-tail"), `must not leak prefix remainder; got: ${logBody}`)
assert.ok(logBody.includes("leak: *** end"), `expected full long secret redacted; got: ${logBody}`)
}
// ---------------------------------------------------------------------------
// Case 2f — redact secret values from captured kilo stdout (triage-raw artifact)
// ---------------------------------------------------------------------------
function case2f_redactStdout() {
console.log("case 2f: redact env secrets from kilo stdout (triage-raw)")
const digest = [samplePr(501), samplePr(502)]
const cwd = setupTriageCwd(digest)
const secret = "selftest-stdout-secret-99999"
const kiloDir = makeStubKiloDir({ mode: "triage-embed-env-secret" })
const summaryFile = path.join(cwd, "step-summary.md")
fs.writeFileSync(summaryFile, "")
const result = runNodeScript(TRIAGE_SCRIPT, {
cwd,
kiloDir,
env: {
TRIAGE_MODEL: "test/model",
DOCS_SYNC_BACKOFF_MS: "0",
TRIAGE_BUDGET_MINUTES: "30",
GITHUB_STEP_SUMMARY: summaryFile,
KILO_API_KEY: secret,
},
})
assert.equal(result.status, 0, `triage.mjs exit: ${result.output}`)
const rawFiles = fs.readdirSync(path.join(cwd, "docs-sync-out")).filter((f) => f.startsWith("triage-raw-"))
assert.ok(rawFiles.length > 0, "expected triage-raw-*.txt artifact")
for (const f of rawFiles) {
const body = fs.readFileSync(path.join(cwd, "docs-sync-out", f), "utf8")
assert.ok(!body.includes(secret), `triage-raw must not contain secret; ${f}: ${body}`)
}
const triage = JSON.parse(fs.readFileSync(path.join(cwd, "docs-sync-out", "triage.json"), "utf8"))
assert.ok(triage.length >= 1, "triage must still parse after redaction")
assert.ok(
triage.some((e) => e.docs_worthy === true || e.pending === true || e.docs_worthy === false),
"triage entries must be structured",
)
}
// ---------------------------------------------------------------------------
// Case 2g — redact-stream.mjs line-wise filter (including partial last line)
// ---------------------------------------------------------------------------
function case2g_redactStream() {
console.log("case 2g: redact-stream.mjs stdin filter")
const secret = "stream-secret-value-xyz"
const filterPath = path.join(HERE, "redact-stream.mjs")
assert.ok(fs.existsSync(filterPath), `expected ${filterPath}`)
const input = `leak ${secret} after\npartial-${secret}`
const result = spawnSync(process.execPath, [filterPath], {
env: { ...process.env, KILO_API_KEY: secret },
input,
encoding: "utf8",
timeout: 10_000,
})
assert.equal(result.status, 0, `redact-stream exit: ${result.stderr || result.error}`)
assert.equal(result.stdout, "leak *** after\npartial-***")
}
// ---------------------------------------------------------------------------
// Case 2h — pending causes reach the rolling PR free of ANSI escapes
// ---------------------------------------------------------------------------
function case2h_pendingCauseIsReadable() {
console.log("case 2h: pending cause has no ANSI escapes")
const prs = [1, 2, 3, 4, 5].map((n) => samplePr(n))
const worthy = prs
const triage = prs.map((p) => ({
pr: p.number,
url: p.url,
docs_worthy: true,
reason: "needs docs",
target_sections: ["overview"],
priority: "high",
}))
const cwd = setupEditCwd(worthy, triage)
// Verbatim shape of a real kilo TUI stderr line (see PR #12521's pending table).
const ESC = "\u001b"
const stderrText = `${ESC}[0m→ ${ESC}[0mRead packages/kilo-docs/AGENTS.md${ESC}[2K${ESC}[1G done`
const kiloDir = makeStubKiloDir({ mode: "stderr-exit0", stderrText })
const result = runNodeScript(EDIT_SCRIPT, {
cwd,
kiloDir,
env: {
EDIT_MODEL: "test/model",
DOCS_SYNC_BACKOFF_MS: "0",
EDIT_BUDGET_MINUTES: "5",
EDIT_BATCH_TIMEOUT_MINUTES: "1",
},
})
assert.equal(result.status, 0, `edit.mjs exit: ${result.output}`)
const summary = JSON.parse(fs.readFileSync(path.join(cwd, ".docs-sync-summary.json"), "utf8"))
assert.equal(summary.length, 5)
for (const e of summary) {
assert.equal(e.action, "pending", `expected pending, got ${JSON.stringify(e)}`)
assert.ok(!e.reason.includes(ESC), `pending reason must not contain ANSI escapes: ${JSON.stringify(e.reason)}`)
// Non-vacuous: the diagnostic text itself must survive the strip.
assert.match(e.reason, /Read packages\/kilo-docs\/AGENTS\.md/)
}
// The raw artifact log keeps the escapes — it is the debugging record.
const rawLog = fs.readFileSync(path.join(cwd, "docs-sync-out", "kilo-stderr-edit-batch-0-attempt-1.log"), "utf8")
assert.ok(rawLog.includes(ESC), "persisted stderr log must stay raw")
}
// ---------------------------------------------------------------------------
// Case 2i — wall-clock budgets can actually fit work
// ---------------------------------------------------------------------------
/**
* The pre-unit gates in triage.mjs/edit.mjs refuse to start a chunk/batch unless
* a whole per-unit timeout remains, so a budget below that timeout silently runs
* ZERO units and defers every PR. Run 30306629290 hit the weaker form of this:
* 8 of 11 chunks and 4 of 11 batches ran, the rest deferred untried. Assert the
* workflow sets both budgets and that each fits at least two units.
*/
function case2i_budgetsFitWork() {
console.log("case 2i: triage/edit budgets fit at least two units")
const yml = fs.readFileSync(path.join(HERE, "..", "workflows", "docs-sync.yml"), "utf8")
const readEnvNumber = (key) => {
const m = yml.match(new RegExp(`^\\s*${key}:\\s*"?(\\d+)"?\\s*$`, "m"))
assert.ok(m, `${key} must be set in docs-sync.yml (default is too small to drain a backlog)`)
return Number(m[1])
}
// Per-unit timeouts are script constants, not workflow env; read them from source.
const triageSrc = fs.readFileSync(path.join(HERE, "triage.mjs"), "utf8")
const chunkMin = Number(triageSrc.match(/CHUNK_TIMEOUT_MS = (\d+) \* 60 \* 1000/)?.[1])
assert.ok(Number.isFinite(chunkMin), "could not read CHUNK_TIMEOUT_MS from triage.mjs")
const editSrc = fs.readFileSync(path.join(HERE, "edit.mjs"), "utf8")
const batchMin = Number(editSrc.match(/EDIT_BATCH_TIMEOUT_MINUTES\) \|\| (\d+)/)?.[1])
assert.ok(Number.isFinite(batchMin), "could not read EDIT_BATCH_TIMEOUT_MINUTES default from edit.mjs")
const triageBudget = readEnvNumber("TRIAGE_BUDGET_MINUTES")
const editBudget = readEnvNumber("EDIT_BUDGET_MINUTES")
assert.ok(
triageBudget >= 2 * chunkMin,
`TRIAGE_BUDGET_MINUTES=${triageBudget} must be >= 2x chunk timeout (${chunkMin}m)`,
)
assert.ok(editBudget >= 2 * batchMin, `EDIT_BUDGET_MINUTES=${editBudget} must be >= 2x batch timeout (${batchMin}m)`)
// The job timeout must outlast both budgets plus the non-LLM steps.
const jobTimeout = Number(yml.match(/^\s*timeout-minutes:\s*(\d+)\s*$/m)?.[1])
assert.ok(Number.isFinite(jobTimeout), "could not read job timeout-minutes")
assert.ok(
jobTimeout > triageBudget + editBudget,
`job timeout-minutes=${jobTimeout} must exceed triage+edit budgets (${triageBudget}+${editBudget})`,
)
}
// ---------------------------------------------------------------------------
// Case 3 — watermark invariant
// ---------------------------------------------------------------------------
function case3_watermark() {
console.log("case 3: watermark invariant")
const now = "2026-07-27T12:00:00.000Z"
const nowMs = Date.parse(now)
const prA = samplePr(10, { merged_at: "2026-07-20T10:00:00.000Z" })
const prB = samplePr(11, { merged_at: "2026-07-22T15:30:00.000Z" })
const prC = samplePr(12, { merged_at: "2026-07-25T08:00:00.000Z" })
const digest = [prA, prB, prC]
// all covered → processed-through === now
{
const worthy = [prA, prB]
const summary = [
{ pr: 10, url: prA.url, action: "updated packages/kilo-docs/pages/x.md", reason: "" },
{ pr: 11, url: prB.url, action: "skipped", reason: "already documented" },
]
const triage = [
{ pr: 10, url: prA.url, docs_worthy: true, pending: false, reason: "ok" },
{ pr: 11, url: prB.url, docs_worthy: true, pending: false, reason: "ok" },
]
const uncovered = computeUncovered({ worthy, summary, triage })
assert.equal(uncovered.length, 0)
const through = computeProcessedThrough({ uncovered, digest, now })
assert.equal(through, now)
}
// one uncovered → merged_at − 1 ms, strictly < now
{
const worthy = [prA, prB]
const summary = [
{ pr: 10, url: prA.url, action: "updated x", reason: "" },
{ pr: 11, url: prB.url, action: "pending", reason: "edit batch 0: exit 0" },
]
const uncovered = computeUncovered({ worthy, summary, triage: [] })
assert.equal(uncovered.length, 1)
assert.equal(uncovered[0].url, prB.url)
const through = computeProcessedThrough({ uncovered, digest, now })
const expected = new Date(Date.parse(prB.merged_at) - 1).toISOString()
assert.equal(through, expected)
assert.ok(Date.parse(through) < nowMs)
}
// several uncovered → earliest merge time wins
{
const worthy = [prA, prB, prC]
const summary = [
{ pr: 10, url: prA.url, action: "pending", reason: "fail" },
{ pr: 12, url: prC.url, action: "pending", reason: "fail" },
]
// prB missing from summary entirely
const uncovered = computeUncovered({ worthy, summary, triage: [] })
assert.ok(uncovered.length >= 2)
const through = computeProcessedThrough({ uncovered, digest, now })
// earliest among A, B, C that are uncovered — A is earliest
const times = uncovered
.map((u) => digest.find((d) => d.url === u.url)?.merged_at)
.filter(Boolean)
.map((t) => Date.parse(t))
const earliest = Math.min(...times)
assert.equal(through, new Date(earliest - 1).toISOString())
}
// summary missing/truncated while worthy non-empty → every worthy URL held back
{
const worthy = [prA, prB]
const uncovered = computeUncovered({ worthy, summary: [], triage: [] })
assert.equal(uncovered.length, 2)
const through = computeProcessedThrough({ uncovered, digest, now })
assert.equal(through, new Date(Date.parse(prA.merged_at) - 1).toISOString())
}
// noDiffReport three arms
{
const uncovered = [{ url: prA.url, reason: "edit batch failed" }]
const arm1 = noDiffReport({ uncovered, sinceOverride: true })
assert.ok(arm1.summary.includes(prA.url))
assert.ok(arm1.warning, "override + uncovered → warning present")
const arm2 = noDiffReport({ uncovered: [], sinceOverride: true })
assert.equal(arm2.warning, null, "override + empty uncovered → warning absent")
const arm3 = noDiffReport({ uncovered, sinceOverride: false })
assert.equal(arm3.warning, null, "scheduled + uncovered → warning absent")
}
// triage pending:true backfill rows land in uncovered (consumption)
{
const triage = [
{
pr: 99,
url: "https://github.com/Kilo-Org/cloud/pull/99",
docs_worthy: false,
pending: true,
reason: "not classified by triage",
},
]
const uncovered = computeUncovered({ worthy: [], summary: [], triage })
assert.equal(uncovered.length, 1)
assert.equal(uncovered[0].url, triage[0].url)
const through = computeProcessedThrough({
uncovered,
digest: [{ url: triage[0].url, merged_at: "2026-07-21T00:00:00.000Z" }],
now,
})
assert.equal(through, new Date(Date.parse("2026-07-21T00:00:00.000Z") - 1).toISOString())
}
// fallback field (post-plan repair)
{
const uncovered = [{ url: "https://github.com/Kilo-Org/cloud/pull/50", reason: "missing" }]
const fallback = "2026-07-17T00:00:00.000Z"
// unresolved merged_at + parseable fallback → hold at fallback, warn
const prevWarn = console.warn
const warnings = []
console.warn = (...a) => warnings.push(a.join(" "))
try {
const through = computeProcessedThrough({ uncovered, digest: [], now, fallback })
assert.equal(through, new Date(fallback).toISOString())
assert.ok(Date.parse(through) < nowMs)
assert.ok(warnings.some((w) => w.includes("::warning::")))
} finally {
console.warn = prevWarn
}
// unresolved + unparseable/missing fallback → throws
assert.throws(() => computeProcessedThrough({ uncovered, digest: [], now }), /fallback|SINCE|refusing/i)
assert.throws(
() => computeProcessedThrough({ uncovered, digest: [], now, fallback: "not-a-date" }),
/fallback|SINCE|refusing/i,
)
// resolved merged_at ignores fallback
const throughResolved = computeProcessedThrough({
uncovered: [{ url: prA.url, reason: "x" }],
digest: [prA],
now,
fallback: "2020-01-01T00:00:00.000Z",
})
assert.equal(throughResolved, new Date(Date.parse(prA.merged_at) - 1).toISOString())
// empty uncovered ignores fallback
const throughEmpty = computeProcessedThrough({
uncovered: [],
digest: [],
now,
fallback: "2020-01-01T00:00:00.000Z",
})
assert.equal(throughEmpty, now)
}
}
// ---------------------------------------------------------------------------
// Case 4 — routing and round trip
// ---------------------------------------------------------------------------
function case4_routing() {
console.log("case 4: routing and round trip")
const summary = [
{ pr: 1, url: "https://github.com/Kilo-Org/cloud/pull/1", action: "updated pages/a.md", reason: "" },
{ pr: 2, url: "https://github.com/Kilo-Org/cloud/pull/2", action: "skipped", reason: "already documented" },
{ pr: 3, url: "https://github.com/Kilo-Org/cloud/pull/3", action: "pending", reason: "edit batch 1: exit 0" },
]
const triage = [
{
pr: 4,
url: "https://github.com/Kilo-Org/cloud/pull/4",
docs_worthy: false,
pending: false,
reason: "chore only",
},
{
pr: 5,
url: "https://github.com/Kilo-Org/cloud/pull/5",
docs_worthy: false,
pending: true,
reason: "triage failed to classify this PR",
},
]
const worthy = [
{ number: 1, url: summary[0].url },
{ number: 2, url: summary[1].url },
{ number: 3, url: summary[2].url },
]
const uncovered = computeUncovered({ worthy, summary, triage })
const { changesRows, pendingRows, skippedRows } = routeRows({ summary, triage, uncovered })
// pending appears in neither Changes nor Considered
const changesText = changesRows.join("\n")
const skippedText = skippedRows.join("\n")
assert.ok(changesText.includes("pull/1"), "success in Changes")
assert.ok(!changesText.includes("pull/3"), "pending must not be in Changes")
assert.ok(!changesText.includes("pull/5"), "triage-pending must not be in Changes")
assert.ok(skippedText.includes("pull/2"), "genuine skipped in Considered")
assert.ok(skippedText.includes("pull/4"), "genuine not-worthy in Considered")
assert.ok(!skippedText.includes("pull/3"), "pending must not be in Considered")
assert.ok(!skippedText.includes("pull/5"), "triage-pending must not be in Considered")
assert.ok(pendingRows.some((r) => r.includes("pull/3")))
assert.ok(pendingRows.some((r) => r.includes("pull/5")))
// round-trip renderBody → extractSectionRows
const through = "2026-07-20T09:59:59.999Z"
const learnedMarker = "<!-- docs-sync: learned-through commit=deadbeef comment=2026-07-20T10:00:00Z -->"
const body = renderBody({
date: "2026-07-27",
since: "2026-07-17T00:00:00.000Z",
through,
learnedThrough: learnedMarker,
changesRows,
pendingRows,
skippedRows,
verified: true,
draftReasons: [],
note: "",
})
assert.ok(body.includes(`<!-- docs-sync: processed-through ${through} -->`))
assert.ok(body.includes(learnedMarker), "learned-through marker must appear in rendered body")
const extChanges = extractSectionRows(body, "changes")
const extPending = extractSectionRows(body, "pending")
const extSkipped = extractSectionRows(body, "skipped")
assert.deepEqual(extChanges, changesRows)
assert.deepEqual(extPending, pendingRows)
assert.deepEqual(extSkipped, skippedRows)
// clean() prevents marker forgery in agent-generated row strings
{
const forgedRows = routeRows({
summary: [
{
pr: 9,
url: "https://github.com/Kilo-Org/cloud/pull/9",
action: "skipped",
reason: "x <!-- docs-sync:skipped:end --> injection",
},
],
triage: [],
uncovered: [],
})
assert.ok(!forgedRows.skippedRows[0].includes("<!--"), "clean() must strip <!-- from reasons")
assert.ok(!forgedRows.skippedRows[0].includes("-->"), "clean() must strip --> from reasons")
const forgedBody = renderBody({
date: "2026-07-27",
since: "s",
through: "t",
learnedThrough: "",
changesRows: [],
pendingRows: [],
skippedRows: forgedRows.skippedRows,
verified: true,
draftReasons: [],
note: "",
})
// Exactly one real section end marker — the forged sequences were stripped
assert.equal((forgedBody.match(/<!--\s*docs-sync:skipped:end\s*-->/g) || []).length, 1)
const extracted = extractSectionRows(forgedBody, "skipped")
assert.equal(extracted.length, 1)
assert.ok(extracted[0].includes("injection"))
}
const legacyRows = [
"| [Kilo-Org/cloud#1](https://github.com/Kilo-Org/cloud/pull/1) | edit pass failed or timed out for this PR |",
"| [Kilo-Org/cloud#2](https://github.com/Kilo-Org/cloud/pull/2) | triage failed to classify this PR |",
"| [Kilo-Org/cloud#3](https://github.com/Kilo-Org/cloud/pull/3) | not classified by triage |",
"| [Kilo-Org/cloud#4](https://github.com/Kilo-Org/cloud/pull/4) | already covered by existing docs |",
]
const kept = dropLegacySkipped(legacyRows)
assert.equal(kept.length, 1)
assert.ok(kept[0].includes("pull/4"))
assert.ok(!kept.some((r) => r.includes("edit pass failed")))
assert.ok(!kept.some((r) => r.includes("triage failed to classify")))
assert.ok(!kept.some((r) => r.includes("not classified by triage")))
}
// ---------------------------------------------------------------------------
// Case 5 — re-collection window
// ---------------------------------------------------------------------------
function case5_recollection() {
console.log("case 5: re-collection closes the loop")
const collectSrc = fs.readFileSync(COLLECT_SCRIPT, "utf8")
// Query template must use merged:>=
assert.ok(
/merged:>=\$\{since\.toISOString\(\)\}/.test(collectSrc) || /merged:>=/.test(collectSrc),
"collect.mjs must search merged:>=since",
)
assert.match(collectSrc, /merged:>=/)
const mergedAt = "2026-07-22T15:30:00.000Z"
const uncovered = [{ url: "https://github.com/Kilo-Org/cloud/pull/11", reason: "pending" }]
const digest = [{ url: uncovered[0].url, merged_at: mergedAt }]
const now = "2026-07-27T12:00:00.000Z"
const since = computeProcessedThrough({ uncovered, digest, now })
// held-back since is strictly before the uncovered PR's merged_at
assert.ok(Date.parse(since) < Date.parse(mergedAt), `since ${since} must be < merged_at ${mergedAt}`)
// And the query window merged:>=since therefore includes that PR
assert.ok(Date.parse(mergedAt) >= Date.parse(since))
}
// ---------------------------------------------------------------------------
// Case 6 — budgets
// ---------------------------------------------------------------------------
function case6_budgets() {
console.log("case 6: budgets")
// --- edit budget ---
{
// 12 PRs = 3 batches of 5; budget too small for even one batch unit
const prs = Array.from({ length: 12 }, (_, i) => samplePr(100 + i))
const worthy = prs
const triage = prs.map((p) => ({
pr: p.number,
url: p.url,
docs_worthy: true,
reason: "needs docs",
target_sections: [],
priority: "medium",
}))
const cwd = setupEditCwd(worthy, triage)
const callLog = path.join(cwd, "kilo-calls.log")
const kiloDir = makeStubKiloDir({ mode: "record", callLog })
// EDIT_BUDGET_MINUTES must be positive (0 falls through to default 50).
// BATCH_TIMEOUT default would be 15m; set both tiny so left < BATCH_TIMEOUT immediately.
const result = runNodeScript(EDIT_SCRIPT, {
cwd,
kiloDir,
env: {
EDIT_MODEL: "test/model",
DOCS_SYNC_BACKOFF_MS: "0",
EDIT_BUDGET_MINUTES: "0.0001",
EDIT_BATCH_TIMEOUT_MINUTES: "15",
},
})
assert.equal(result.status, 0, result.output)
assert.match(result.output, /deferring \d+ PRs/)
assert.match(result.output, /deferred \d+ PRs due to wall-clock budget/)
const calls = fs.existsSync(callLog) ? fs.readFileSync(callLog, "utf8").trim() : ""
const callCount = calls ? calls.split("\n").filter(Boolean).length : 0
assert.equal(callCount, 0, `kilo must not be invoked for deferred edit batches; got ${callCount}`)
const summary = JSON.parse(fs.readFileSync(path.join(cwd, ".docs-sync-summary.json"), "utf8"))
assert.ok(summary.every((e) => e.action === "pending"))
const uncovered = computeUncovered({ worthy, summary, triage })
assert.equal(uncovered.length, 12)
assert.ok(summary.every((e) => e.action !== "skipped"))
}
// --- triage budget ---
{
// CHUNK_SIZE=25; 30 PRs = 2 chunks; budget too small for a 10m chunk
const digest = Array.from({ length: 30 }, (_, i) => samplePr(200 + i))
const cwd = setupTriageCwd(digest)
const callLog = path.join(cwd, "kilo-calls.log")
const kiloDir = makeStubKiloDir({ mode: "record", callLog })
const summaryFile = path.join(cwd, "step-summary.md")
fs.writeFileSync(summaryFile, "")
const result = runNodeScript(TRIAGE_SCRIPT, {
cwd,
kiloDir,
env: {
TRIAGE_MODEL: "test/model",
DOCS_SYNC_BACKOFF_MS: "0",
TRIAGE_BUDGET_MINUTES: "0.0001",
GITHUB_STEP_SUMMARY: summaryFile,
},
})
assert.equal(result.status, 0, result.output)
assert.match(result.output, /deferring \d+ PRs/)
const calls = fs.existsSync(callLog) ? fs.readFileSync(callLog, "utf8").trim() : ""
const callCount = calls ? calls.split("\n").filter(Boolean).length : 0
assert.equal(callCount, 0, `kilo must not be invoked for deferred triage chunks; got ${callCount}`)
const triage = JSON.parse(fs.readFileSync(path.join(cwd, "docs-sync-out", "triage.json"), "utf8"))
assert.equal(triage.length, 30)
assert.ok(triage.every((e) => e.pending === true))
assert.ok(triage.every((e) => e.docs_worthy === false))
const uncovered = computeUncovered({ worthy: [], summary: [], triage })
assert.equal(uncovered.length, 30)
}
}
// ---------------------------------------------------------------------------
// Case 7 — applyCap both arms
// ---------------------------------------------------------------------------
function case7_cap() {
console.log("case 7: applyCap")
const now = new Date("2026-07-27T12:00:00.000Z")
const old = new Date("2026-06-01T00:00:00.000Z")
const prevLog = console.log
const prevWarn = console.warn
const logs = []
const warnings = []
console.log = (...a) => logs.push(a.join(" "))
console.warn = (...a) => warnings.push(a.join(" "))
try {
// explicit:false + older than 14 days → clamped AND reported
const a = applyCap(old, now, { explicit: false })
assert.equal(a.clamped, true)
assert.ok(a.since.getTime() > old.getTime())
const cap = new Date(now.getTime() - 14 * 24 * 3600 * 1000)
assert.equal(a.since.toISOString(), cap.toISOString())
assert.ok(warnings.some((w) => w.includes("::warning::") && w.includes("clamped")))
// explicit:true + older than 14 days → unchanged, skip reported
logs.length = 0
warnings.length = 0
const b = applyCap(old, now, { explicit: true })
assert.equal(b.clamped, false)
assert.equal(b.since.toISOString(), old.toISOString())
assert.ok(logs.some((l) => /cap skipped|INPUT_SINCE/i.test(l)))
} finally {
console.log = prevLog
console.warn = prevWarn
}
}
// ---------------------------------------------------------------------------
// Case 8 — triage.mjs outputs
// ---------------------------------------------------------------------------
function case8_triage() {
console.log("case 8: triage pass outputs")
// 8a Run A: SINCE_OVERRIDE=true + everything pending → warning present
{
const digest = [samplePr(301), samplePr(302), samplePr(303)]
const cwd = setupTriageCwd(digest)
const kiloDir = makeStubKiloDir({ mode: "stderr-exit0", stderrText: "stream end before idle" })
const summaryFile = path.join(cwd, "step-summary.md")
fs.writeFileSync(summaryFile, "")
const result = runNodeScript(TRIAGE_SCRIPT, {
cwd,
kiloDir,
env: {
TRIAGE_MODEL: "test/model",
DOCS_SYNC_BACKOFF_MS: "0",
TRIAGE_BUDGET_MINUTES: "30",
SINCE_OVERRIDE: "true",
GITHUB_STEP_SUMMARY: summaryFile,
},
})
assert.equal(result.status, 0, result.output)
const triage = JSON.parse(fs.readFileSync(path.join(cwd, "docs-sync-out", "triage.json"), "utf8"))
assert.equal(triage.length, 3)
assert.ok(triage.every((e) => e.pending === true))
const summary = fs.readFileSync(summaryFile, "utf8")
assert.match(summary, /triage pending/)
for (const d of digest) {
assert.ok(summary.includes(d.url), `summary lists ${d.url}`)
}
assert.match(result.output, /::warning::.*since-override/)
}
// 8a Run B: SINCE_OVERRIDE unset → warning absent
{
const digest = [samplePr(311), samplePr(312)]
const cwd = setupTriageCwd(digest)
const kiloDir = makeStubKiloDir({ mode: "stderr-exit0", stderrText: "stream end" })
const summaryFile = path.join(cwd, "step-summary.md")
fs.writeFileSync(summaryFile, "")
const result = runNodeScript(TRIAGE_SCRIPT, {
cwd,
kiloDir,
env: {
TRIAGE_MODEL: "test/model",
DOCS_SYNC_BACKOFF_MS: "0",
TRIAGE_BUDGET_MINUTES: "30",
GITHUB_STEP_SUMMARY: summaryFile,
},
})
assert.equal(result.status, 0, result.output)
const triage = JSON.parse(fs.readFileSync(path.join(cwd, "docs-sync-out", "triage.json"), "utf8"))
assert.ok(triage.every((e) => e.pending === true))
assert.ok(fs.readFileSync(summaryFile, "utf8").includes("triage pending"))
assert.ok(!/::warning::.*since-override/.test(result.output), "override warning must be absent when unset")
}
// 8a Run C: SINCE_OVERRIDE=true with MIXED stub (worthy > 0) → warning ABSENT
{
const digest = [samplePr(321), samplePr(322), samplePr(323), samplePr(324)]
const cwd = setupTriageCwd(digest)
const kiloDir = makeStubKiloDir({ mode: "mixed-triage" })
const summaryFile = path.join(cwd, "step-summary.md")
fs.writeFileSync(summaryFile, "")
const result = runNodeScript(TRIAGE_SCRIPT, {
cwd,
kiloDir,
env: {
TRIAGE_MODEL: "test/model",
DOCS_SYNC_BACKOFF_MS: "0",
TRIAGE_BUDGET_MINUTES: "30",
SINCE_OVERRIDE: "true",
GITHUB_STEP_SUMMARY: summaryFile,
},
})
assert.equal(result.status, 0, result.output)
const triage = JSON.parse(fs.readFileSync(path.join(cwd, "docs-sync-out", "triage.json"), "utf8"))
const worthy = triage.filter((e) => e.docs_worthy === true).length
const pending = triage.filter((e) => e.pending === true).length
assert.ok(worthy > 0, "mixed stub must produce worthy > 0")
assert.ok(pending > 0, "mixed stub must leave some pending")
assert.ok(
!/::warning::.*since-override/.test(result.output),
"override warning must be ABSENT when worthy > 0 (Upsert will run)",
)
}
// 8b — partial classification → missing URLs pending:true + computeUncovered
{
// One chunk of 4 PRs; stub classifies first 3 only
const digest = [samplePr(401), samplePr(402), samplePr(403), samplePr(404)]
const cwd = setupTriageCwd(digest)
const kiloDir = makeStubKiloDir({ mode: "partial-triage" })
const summaryFile = path.join(cwd, "step-summary.md")
fs.writeFileSync(summaryFile, "")
const result = runNodeScript(TRIAGE_SCRIPT, {
cwd,
kiloDir,
env: {
TRIAGE_MODEL: "test/model",
DOCS_SYNC_BACKOFF_MS: "0",
TRIAGE_BUDGET_MINUTES: "30",
GITHUB_STEP_SUMMARY: summaryFile,
},
})
assert.equal(result.status, 0, result.output)
const triage = JSON.parse(fs.readFileSync(path.join(cwd, "docs-sync-out", "triage.json"), "utf8"))
assert.equal(triage.length, 4)
const missing = triage.filter((e) => e.reason === "not classified by triage")
assert.ok(missing.length >= 1, "backfill must mark unclassified URLs")
assert.ok(missing.every((e) => e.pending === true))
const uncovered = computeUncovered({ worthy: [], summary: [], triage })
for (const m of missing) {
assert.ok(
uncovered.some((u) => u.url === m.url),
`${m.url} must appear in computeUncovered`,
)
}
}
}
// ---------------------------------------------------------------------------
// Case 9 — revert interception (title/body/annotations + source-order guards)
// ---------------------------------------------------------------------------
function case9_reverts() {
console.log("case 9: revert interception")
// --- revertTitleKind ---
assert.equal(revertTitleKind("revert(cli): restore opt-in stream idle timeouts"), "conventional")
assert.equal(revertTitleKind('Revert "feat(cli): default stream watchdog"'), "github-native")
assert.equal(revertTitleKind("REVERT: all of it"), "conventional")
assert.equal(revertTitleKind("feat(cli): add x"), null)
assert.equal(revertTitleKind("docs: update y"), null)
assert.equal(revertTitleKind("Reverted behavior docs"), null)
// --- parseRevertTargets ---
const defaultRepo = "Kilo-Org/kilocode"
const body12497 = `The default stream inactivity watchdog introduced by #12249 aborts requests based only on the absence of normalized AI SDK events. That signal cannot distinguish a dead provider stream from long prompt processing, reasoning, buffering, or transport behavior, and the follow-up in #12481 reduces false positives without resolving that ambiguity.
Revert both changes and restore the previous opt-in contract: Kilo does not impose a stream idle timeout unless the provider configuration explicitly sets \`chunkTimeout\`. Explicit provider timeouts continue to use the existing AI SDK and SSE timeout paths. This removes the global heuristic while the underlying stalled-stream source and the required transport-level observability are investigated.
This deliberately restores the possibility that an unconfigured provider stream can remain open indefinitely. A default watchdog should be reintroduced only with evidence that its liveness signal and threshold do not terminate healthy responses.
Reverts #12249 and #12481.
`
{
const targets = parseRevertTargets(body12497, defaultRepo)
assert.equal(targets.length, 2)
assert.deepEqual(
targets.map((t) => ({ repo: t.repo, number: t.number, url: t.url })),
[
{
repo: "Kilo-Org/kilocode",
number: 12249,
url: "https://github.com/Kilo-Org/kilocode/pull/12249",
},
{
repo: "Kilo-Org/kilocode",
number: 12481,
url: "https://github.com/Kilo-Org/kilocode/pull/12481",
},
],
)
}
{
const narrative = "Revert the mistaken fix in #99999 because it broke streams.\n\nReverts #12249."
const targets = parseRevertTargets(narrative, defaultRepo)
assert.equal(targets.length, 1)
assert.equal(targets[0].number, 12249)
assert.ok(!targets.some((t) => t.number === 99999))
}
{
const targets = parseRevertTargets("Reverts Kilo-Org/cloud#42.", defaultRepo)
assert.equal(targets.length, 1)
assert.equal(targets[0].repo, "Kilo-Org/cloud")
assert.equal(targets[0].number, 42)
assert.equal(targets[0].url, "https://github.com/Kilo-Org/cloud/pull/42")
}
{
const targets = parseRevertTargets("Reverts #1, #2.", defaultRepo)
assert.equal(targets.length, 2)
assert.deepEqual(
targets.map((t) => t.number),
[1, 2],
)
}
assert.deepEqual(parseRevertTargets("This reverts commit deadbeefcafe.", defaultRepo), [])
assert.deepEqual(parseRevertTargets("This reverts #5.", defaultRepo), [])
assert.deepEqual(parseRevertTargets("No revert trailer here at all.", defaultRepo), [])
// bulleted / quoted single-line trailers
{
const targets = parseRevertTargets("- Reverts #7.", defaultRepo)
assert.equal(targets.length, 1)
assert.equal(targets[0].number, 7)
}
{
const targets = parseRevertTargets("> Reverts #8.", defaultRepo)
assert.equal(targets.length, 1)
assert.equal(targets[0].number, 8)
}
// word-boundary: prose glued to "Reverts" is not a trailer
assert.deepEqual(parseRevertTargets("Revertsomething #5", defaultRepo), [])
// --- computeRevertAnnotations ---
// Map keys are lowercased; lookups must use .toLowerCase()
const fUrl = "https://github.com/Kilo-Org/kilocode/pull/100"
const r1Url = "https://github.com/Kilo-Org/kilocode/pull/200"
const r2Url = "https://github.com/Kilo-Org/kilocode/pull/300"
const f2Url = "https://github.com/Kilo-Org/kilocode/pull/101"
const mergedAt = "2026-07-20T12:00:00.000Z"
const mergedAt2 = "2026-07-21T12:00:00.000Z"
{
const annotations = computeRevertAnnotations([
{
url: r1Url,
merged_at: mergedAt,
targets: [{ repo: "Kilo-Org/kilocode", number: 100, url: fUrl }],
},
])
assert.equal(annotations.size, 1)
assert.deepEqual(annotations.get(fUrl.toLowerCase()), { url: r1Url, merged_at: mergedAt })
}
{
// two-target signal (#12497-shaped)
const annotations = computeRevertAnnotations([
{
url: r1Url,
merged_at: mergedAt,
targets: [
{ repo: "Kilo-Org/kilocode", number: 100, url: fUrl },
{ repo: "Kilo-Org/kilocode", number: 101, url: f2Url },
],
},
])
assert.equal(annotations.size, 2)
assert.deepEqual(annotations.get(fUrl.toLowerCase()), { url: r1Url, merged_at: mergedAt })
assert.deepEqual(annotations.get(f2Url.toLowerCase()), { url: r1Url, merged_at: mergedAt })
}
{
// revert-of-revert: R1 reverts F, R2 reverts R1 → F not annotated; R1 gets R2
const annotations = computeRevertAnnotations([
{
url: r1Url,
merged_at: mergedAt,
targets: [{ repo: "Kilo-Org/kilocode", number: 100, url: fUrl }],
},
{
url: r2Url,
merged_at: mergedAt2,
targets: [{ repo: "Kilo-Org/kilocode", number: 200, url: r1Url }],
},
])
assert.equal(annotations.has(fUrl.toLowerCase()), false)
assert.deepEqual(annotations.get(r1Url.toLowerCase()), { url: r2Url, merged_at: mergedAt2 })
}
{
const annotations = computeRevertAnnotations([{ url: r1Url, merged_at: mergedAt, targets: [] }])
assert.equal(annotations.size, 0)
}
// --- applyRevertAnnotations ---
{
const digest = [
{ url: fUrl, title: "feat F" },
{ url: "https://github.com/Kilo-Org/kilocode/pull/999", title: "untouched" },
]
const applied = applyRevertAnnotations(digest, [
{
url: r1Url,
merged_at: mergedAt,
targets: [{ repo: "Kilo-Org/kilocode", number: 100, url: fUrl }],
},
])
assert.deepEqual(digest[0].reverted_by, { url: r1Url, merged_at: mergedAt })
assert.equal(digest[1].reverted_by, undefined)
assert.deepEqual(applied, [[fUrl, r1Url]])
}
{
// end-to-end revert-of-revert: F must not gain reverted_by
const digest = [{ url: fUrl, title: "feat F" }]
const applied = applyRevertAnnotations(digest, [
{
url: r1Url,
merged_at: mergedAt,
targets: [{ repo: "Kilo-Org/kilocode", number: 100, url: fUrl }],
},
{
url: r2Url,
merged_at: mergedAt2,
targets: [{ repo: "Kilo-Org/kilocode", number: 200, url: r1Url }],
},
])
assert.equal(digest[0].reverted_by, undefined)
assert.deepEqual(applied, [])
}
{
// case-insensitive url matching: lowercase signal target vs canonical digest entry
const canonical = "https://github.com/Kilo-Org/kilocode/pull/12249"
const lowerTarget = "https://github.com/kilo-org/kilocode/pull/12249"
const reverter = "https://github.com/Kilo-Org/kilocode/pull/12497"
const digest = [{ url: canonical, title: "feat stream" }]
const applied = applyRevertAnnotations(digest, [
{
url: reverter,
merged_at: mergedAt,
targets: [{ repo: "kilo-org/kilocode", number: 12249, url: lowerTarget }],
},
])
assert.deepEqual(digest[0].reverted_by, { url: reverter, merged_at: mergedAt })
assert.deepEqual(applied, [[canonical, reverter]])
}
// --- unannotatedRevertSignals ---
{
// partial coverage: F in-digest, G pre-window → only G missed; chains empty
const sUrl = "https://github.com/Kilo-Org/kilocode/pull/500"
const gUrl = "https://github.com/Kilo-Org/kilocode/pull/102"
const signals = [
{
url: sUrl,
merged_at: mergedAt,
targets: [
{ repo: "Kilo-Org/kilocode", number: 100, url: fUrl },
{ repo: "Kilo-Org/kilocode", number: 102, url: gUrl },
],
},
]
const result = unannotatedRevertSignals(signals, [[fUrl, sUrl]])
assert.deepEqual(result, { missed: [{ url: sUrl, targets: [gUrl] }], unparsed: [], chains: [] })
}
{
// fully covered signal (no chain) → all three buckets empty
const signals = [
{
url: r1Url,
merged_at: mergedAt,
targets: [
{ repo: "Kilo-Org/kilocode", number: 100, url: fUrl },
{ repo: "Kilo-Org/kilocode", number: 101, url: f2Url },
],
},
]
const result = unannotatedRevertSignals(signals, [
[fUrl, r1Url],
[f2Url, r1Url],
])
assert.deepEqual(result, { missed: [], unparsed: [], chains: [] })
}
{
// depth-2 chain (#4709/#4759): R1→F, R2→R1 — both in chains; F visible as R1 target
const signals = [
{
url: r1Url,
merged_at: mergedAt,
targets: [{ repo: "Kilo-Org/kilocode", number: 100, url: fUrl }],
},
{
url: r2Url,
merged_at: mergedAt2,
targets: [{ repo: "Kilo-Org/kilocode", number: 200, url: r1Url }],
},
]
const result = unannotatedRevertSignals(signals, [])
assert.deepEqual(result, {
missed: [],
unparsed: [],
chains: [
{ url: r1Url, targets: [fUrl] },
{ url: r2Url, targets: [r1Url] },
],
})
}
{
// depth-3 chain: R1→F, R2→R1, R3→R2 — all three in chains; missed/unparsed empty
const r3Url = "https://github.com/Kilo-Org/kilocode/pull/301"
const signals = [
{
url: r1Url,
merged_at: mergedAt,
targets: [{ repo: "Kilo-Org/kilocode", number: 100, url: fUrl }],
},
{
url: r2Url,
merged_at: mergedAt2,
targets: [{ repo: "Kilo-Org/kilocode", number: 200, url: r1Url }],
},
{
url: r3Url,
merged_at: "2026-04-03T00:00:00Z",
targets: [{ repo: "Kilo-Org/kilocode", number: 201, url: r2Url }],
},
]
const result = unannotatedRevertSignals(signals, [])
assert.deepEqual(result, {
missed: [],
unparsed: [],
chains: [
{ url: r1Url, targets: [fUrl] },
{ url: r2Url, targets: [r1Url] },
{ url: r3Url, targets: [r2Url] },
],
})
}
{
// mixed signal: M targets [R1, A, B]; A annotated, B missed; chains lists only R1
const mUrl = "https://github.com/Kilo-Org/kilocode/pull/800"
const aUrl = "https://github.com/Kilo-Org/kilocode/pull/801"
const bUrl = "https://github.com/Kilo-Org/kilocode/pull/802"
const signals = [
{
url: r1Url,
merged_at: mergedAt,
targets: [{ repo: "Kilo-Org/kilocode", number: 100, url: fUrl }],
},
{
url: mUrl,
merged_at: mergedAt2,
targets: [
{ repo: "Kilo-Org/kilocode", number: 200, url: r1Url },
{ repo: "Kilo-Org/kilocode", number: 801, url: aUrl },
{ repo: "Kilo-Org/kilocode", number: 802, url: bUrl },
],
},
]
const result = unannotatedRevertSignals(signals, [[aUrl, mUrl]])
assert.deepEqual(result, {
missed: [{ url: mUrl, targets: [bUrl] }],
unparsed: [],
chains: [
{ url: r1Url, targets: [fUrl] },
{ url: mUrl, targets: [r1Url] },
],
})
}
{
// zero-target signal → unparsed, not missed; chains empty
const emptyUrl = "https://github.com/Kilo-Org/kilocode/pull/400"
const result = unannotatedRevertSignals([{ url: emptyUrl, merged_at: mergedAt, targets: [] }], [])
assert.deepEqual(result, { missed: [], unparsed: [emptyUrl], chains: [] })
}
{
// case-insensitivity: annotated set matches target urls differing only by case
const signalUrl = "https://github.com/Kilo-Org/kilocode/pull/600"
const targetMixed = "https://github.com/Kilo-Org/kilocode/pull/700"
const targetLower = "https://github.com/kilo-org/kilocode/pull/700"
const signals = [
{
url: signalUrl,
merged_at: mergedAt,
targets: [{ repo: "Kilo-Org/kilocode", number: 700, url: targetMixed }],
},
]
const result = unannotatedRevertSignals(signals, [[targetLower, signalUrl]])
assert.deepEqual(result, { missed: [], unparsed: [], chains: [] })
}
{
// live-window lock: #12497-shaped signal with both targets covered
const s12497 = "https://github.com/Kilo-Org/kilocode/pull/12497"
const t12249 = "https://github.com/Kilo-Org/kilocode/pull/12249"
const t12481 = "https://github.com/Kilo-Org/kilocode/pull/12481"
const signals = [
{
url: s12497,
merged_at: mergedAt,
targets: [
{ repo: "Kilo-Org/kilocode", number: 12249, url: t12249 },
{ repo: "Kilo-Org/kilocode", number: 12481, url: t12481 },
],
},
]
const result = unannotatedRevertSignals(signals, [
[t12249, s12497],
[t12481, s12497],
])
assert.deepEqual(result, { missed: [], unparsed: [], chains: [] })
}
// --- source-order guards (case-5 style) ---
{
const collectSrc = fs.readFileSync(COLLECT_SCRIPT, "utf8")
assert.ok(collectSrc.includes("./reverts.mjs"), "collect.mjs must import ./reverts.mjs")
const kindIdx = collectSrc.indexOf("revertTitleKind(item.title")
const dropIdx = collectSrc.indexOf("DROP_TITLE.test")
assert.ok(kindIdx >= 0, "revertTitleKind(item.title call missing")
assert.ok(dropIdx >= 0, "DROP_TITLE.test missing")
assert.ok(kindIdx < dropIdx, "revert interception must run before DROP_TITLE")
const applyIdx = collectSrc.indexOf("applyRevertAnnotations(digest")
const fullIdx = collectSrc.indexOf("digest-full.json")
assert.ok(applyIdx >= 0, "applyRevertAnnotations(digest call missing")
assert.ok(fullIdx >= 0, "digest-full.json write missing")
assert.ok(applyIdx < fullIdx, "annotations must be applied before digests are written")
assert.ok(
collectSrc.includes('if (revertTitleKind(item.title ?? "")) {'),
'collect must use exact intercept line if (revertTitleKind(item.title ?? "")) {',
)
}
// --- prompt-text guards ---
{
const triagePrompt = fs.readFileSync(path.join(HERE, "triage-prompt.md"), "utf8")
assert.ok(triagePrompt.includes("reverted_by"), "triage-prompt must mention reverted_by")
assert.ok(triagePrompt.includes("stream-liveness"), "triage-prompt must mention stream-liveness")
assert.ok(
triagePrompt.includes("reverted by https://github.com/Kilo-Org/kilocode/pull/12497"),
"triage-prompt must contain exact cite example",
)
const editPrompt = fs.readFileSync(path.join(HERE, "edit-prompt.md"), "utf8")
assert.ok(editPrompt.includes("reverted_by"), "edit-prompt must mention reverted_by")
assert.ok(editPrompt.includes("current source tree"), "edit-prompt must mention current source tree")
assert.ok(editPrompt.includes("Skipping is a normal outcome"), "edit-prompt must mention skipping outcome")
assert.ok(editPrompt.includes("Existence alone is not enough"), "edit-prompt must mention existence guard")
}
}
// ---------------------------------------------------------------------------
// Case 10 — learnings extraction, validation, injection, upsert safety
// ---------------------------------------------------------------------------
function case10_learnings() {
console.log("case 10: learnings")
// --- helpers for extraction runs ---
function writeFixture(cwd, data) {
const f = path.join(cwd, "fixture.json")
fs.writeFileSync(f, JSON.stringify(data, null, 2))
return f
}
function writeExtractionDelta(cwd, delta) {
fs.mkdirSync(path.join(cwd, "docs-sync-out"), { recursive: true })
fs.writeFileSync(path.join(cwd, "docs-sync-out", "extraction-delta.json"), JSON.stringify(delta, null, 2))
}
// Prepare a git repo for learn.mjs tests: set origin refs, create docs-sync-out.
function setupLearnRepo(dir, branch = "docs/auto-sync") {
fs.mkdirSync(path.join(dir, "docs-sync-out"), { recursive: true })
gitIn(dir, ["update-ref", "refs/remotes/origin/main", "main"])
gitIn(dir, ["update-ref", `refs/remotes/origin/${branch}`, branch])
return dir
}
const githubBotEmail = "41898282+github-actions[bot]@users.noreply.github.com"
const kiloconnectBotEmail = "240665456+kiloconnect[bot]@users.noreply.github.com"
// 10a — three commit classes
{
console.log(" 10a — three commit classes")
const dir = mktemp("docs-sync-learn-a-")
initRepoWithIdentity(dir)
fs.writeFileSync(path.join(dir, "base.txt"), "base\n")
gitIn(dir, ["add", "base.txt"])
gitIn(dir, ["commit", "-m", "base"])
// Branch
gitIn(dir, ["checkout", "-b", "docs/auto-sync"])
// 1. kiloconnect[bot] commit that touches packages/kilo-docs/pages/x.md
gitIn(dir, ["config", "user.email", kiloconnectBotEmail])
fs.mkdirSync(path.join(dir, "packages", "kilo-docs", "pages"), { recursive: true })
fs.writeFileSync(path.join(dir, "packages", "kilo-docs", "pages", "x.md"), "# x\n")
gitIn(dir, ["add", "packages/kilo-docs/pages/x.md"])
gitIn(dir, ["commit", "-m", "docs: add x page"])
const kiloconnectSha = gitIn(dir, ["rev-parse", "HEAD"])
// 2. github-actions[bot] commit
gitIn(dir, ["config", "user.email", githubBotEmail])
fs.writeFileSync(path.join(dir, "packages", "kilo-docs", "pages", "y.md"), "# y\n")
gitIn(dir, ["add", "packages/kilo-docs/pages/y.md"])
gitIn(dir, ["commit", "-m", "docs: add y page"])
// 3. Merge commit (non-merge filter)
gitIn(dir, ["config", "user.email", "someone@example.com"])
gitIn(dir, ["checkout", "-b", "tmp-merge"])
fs.writeFileSync(path.join(dir, "z.txt"), "z\n")
gitIn(dir, ["add", "z.txt"])
gitIn(dir, ["commit", "-m", "tmp"])
gitIn(dir, ["checkout", "docs/auto-sync"])
gitIn(dir, ["merge", "--no-ff", "tmp-merge", "-m", "merge tmp"])
// 4. commit reachable from main
gitIn(dir, ["checkout", "main"])
fs.writeFileSync(path.join(dir, "main-only.txt"), "main only\n")
gitIn(dir, ["add", "main-only.txt"])
gitIn(dir, ["commit", "-m", "main only"])
gitIn(dir, ["checkout", "docs/auto-sync"])
const tip = gitIn(dir, ["rev-parse", "HEAD"])
// Setup origin refs (needed by learn.mjs git commands)
const cwd = setupLearnRepo(dir)
const fixturePath = writeFixture(dir, {
pr: { number: 1, head: { ref: "docs/auto-sync" }, body: "", user: { login: "github-actions[bot]" } },
comments: [],
})
const callLog = path.join(dir, "kilo-calls.log")
const kiloDir = makeStubKiloDir({ mode: "extraction-delta", callLog })
writeExtractionDelta(dir, { add: [], remove: [] })
const result = runNodeScript(LEARN_SCRIPT, {
cwd: dir,
kiloDir,
env: {
TRIAGE_MODEL: "test/model",
DOCS_SYNC_FIXTURE: fixturePath,
LEARNINGS_BUDGET_MINUTES: "1",
DOCS_SYNC_BACKOFF_MS: "0",
},
})
// Assert: input file written, exactly one correction — kiloconnect only.
// github-actions[bot] commit excluded (criterion 5), merge commit excluded
// via --no-merges, main-reachable commit excluded by origin/main range (criterion 6).
const inputFile = path.join(dir, "docs-sync-out", "learnings-input.json")
assert.ok(fs.existsSync(inputFile), `expected ${inputFile}`)
const input = JSON.parse(fs.readFileSync(inputFile, "utf8"))
assert.equal(input.corrections.length, 1, "exactly one correction (kiloconnect commit)")
assert.equal(
input.corrections[0].source,
`commit:${kiloconnectSha.slice(0, 7)}`,
"correction must be kiloconnect commit only",
)
}
// 10rz — timestamp correlation with different timezone offsets
// A comment must map to the chronologically earliest eligible commit even when
// timestamps use different timezone offsets (Z vs +05:00).
{
console.log(" 10rz — timestamp correlation")
const dir = mktemp("docs-sync-learn-rz-")
initRepoWithIdentity(dir)
fs.writeFileSync(path.join(dir, "base.txt"), "base\n")
gitIn(dir, ["add", "base.txt"])
gitIn(dir, ["commit", "-m", "base"])
gitIn(dir, ["checkout", "-b", "docs/auto-sync"])
// Commit A: non-UTC offset, chronologically earliest (UTC 08:00)
// iso = 2026-08-03T13:00:00+05:00
fs.mkdirSync(path.join(dir, "packages", "kilo-docs", "pages"), { recursive: true })
fs.writeFileSync(path.join(dir, "packages", "kilo-docs", "pages", "x.md"), "# x\n")
gitIn(dir, ["add", "packages/kilo-docs/pages/x.md"])
gitIn(dir, ["commit", "-m", "first edit x", "--author", `kiloconnect[bot] <${kiloconnectBotEmail}>`], {
GIT_COMMITTER_DATE: "2026-08-03T13:00:00+05:00",
})
const shaA = gitIn(dir, ["rev-parse", "HEAD"])
// Commit B: UTC offset, chronologically later (UTC 09:00)
// iso = 2026-08-03T09:00:00Z — string comparison would pick this as "earlier" (09 < 13)
// but chronologically A is earlier (08:00 < 09:00)
fs.writeFileSync(path.join(dir, "packages", "kilo-docs", "pages", "x.md"), "# x\n## edit\n")
gitIn(dir, ["add", "packages/kilo-docs/pages/x.md"])
gitIn(dir, ["commit", "-m", "second edit x", "--author", `kiloconnect[bot] <${kiloconnectBotEmail}>`], {
GIT_COMMITTER_DATE: "2026-08-03T09:00:00Z",
})
const shaB = gitIn(dir, ["rev-parse", "HEAD"])
// Seed LEARNINGS.md so existing entries are non-empty but irrelevant
const existing = [
{ id: "pre", rule: "Pre-existing rule.", scope: "both", source: "commit:0000000", date: "2026-01-01" },
]
const learningsPath = path.join(dir, "packages", "kilo-docs", "LEARNINGS.md")
fs.mkdirSync(path.dirname(learningsPath), { recursive: true })
fs.writeFileSync(learningsPath, renderLearnings(existing))
gitIn(dir, ["add", "packages/kilo-docs/LEARNINGS.md"])
gitIn(dir, ["commit", "-m", "seed learnings", "--author", `kiloconnect[bot] <${kiloconnectBotEmail}>`])
const cwd = setupLearnRepo(dir)
// Comment at UTC 05:30 on x.md — before both commits chronologically.
// 05:30 < 08:00 (A) and 05:30 < 09:00 (B) → both eligible
// The chronologically earliest eligible commit is A (08:00).
const fixturePath = writeFixture(cwd, {
pr: { number: 1, head: { ref: "docs/auto-sync" }, body: "", user: { login: "github-actions[bot]" } },
comments: [
{
id: 101,
created_at: "2026-08-03T05:30:00Z",
path: "packages/kilo-docs/pages/x.md",
body: "Please fix the docs.",
author_association: "MEMBER",
user: { login: "maintainer" },
},
],
})
const kiloDir = makeStubKiloDir({ mode: "extraction-delta", callLog: path.join(cwd, "kilo-calls.log") })
writeExtractionDelta(cwd, { add: [], remove: [] })
const result = runNodeScript(LEARN_SCRIPT, {
cwd,
kiloDir,
env: {
TRIAGE_MODEL: "test/model",
DOCS_SYNC_FIXTURE: fixturePath,
LEARNINGS_BUDGET_MINUTES: "1",
DOCS_SYNC_BACKOFF_MS: "0",
},
})
// Read learnings-input.json to inspect the correlation result
const inputFile = path.join(cwd, "docs-sync-out", "learnings-input.json")
assert.ok(fs.existsSync(inputFile), "learnings-input.json must exist")
const input = JSON.parse(fs.readFileSync(inputFile, "utf8"))
// Both commits must appear as corrections
const commitA = input.corrections.find((c) => c.source === `commit:${shaA.slice(0, 7)}`)
const commitB = input.corrections.find((c) => c.source === `commit:${shaB.slice(0, 7)}`)
assert.ok(commitA, "commit A must be in corrections")
assert.ok(commitB, "commit B must be in corrections")
// Comment must be associated with commit A (chronologically earliest)
assert.ok(commitA.comment, "commit A must have the comment associated")
assert.equal(commitA.comment.path, "packages/kilo-docs/pages/x.md")
assert.equal(commitB.comment, undefined, "commit B must not have the comment associated")
// No standalone comment candidate — the comment was correlated, not orphaned
const standalone = input.corrections.filter((c) => c.source && c.source.startsWith("comment:"))
assert.equal(standalone.length, 0, "comment must be associated, not standalone")
}
// 10b — watermark suppression (no model call when marker covers all)
{
console.log(" 10b — watermark suppression")
const dir = mktemp("docs-sync-learn-b-")
initRepoWithIdentity(dir)
fs.writeFileSync(path.join(dir, "base.txt"), "base\n")
gitIn(dir, ["add", "base.txt"])
gitIn(dir, ["commit", "-m", "base"])
gitIn(dir, ["checkout", "-b", "docs/auto-sync"])
// corrective commit
fs.mkdirSync(path.join(dir, "packages", "kilo-docs", "pages"), { recursive: true })
fs.writeFileSync(path.join(dir, "packages", "kilo-docs", "pages", "x.md"), "# x\n")
gitIn(dir, ["add", "packages/kilo-docs/pages/x.md"])
gitIn(dir, ["commit", "-m", "docs update", "--author", `kiloconnect[bot] <${kiloconnectBotEmail}>`])
// Write LEARNINGS.md on the branch first, so the marker can point to the tip after it
const existing = [
{ id: "test", rule: "existing rule", scope: "both", source: "commit:0000000", date: "2026-01-01" },
]
const learningsPath = path.join(dir, "packages", "kilo-docs", "LEARNINGS.md")
const learningsContent = renderLearnings(existing)
fs.mkdirSync(path.dirname(learningsPath), { recursive: true })
fs.writeFileSync(learningsPath, learningsContent)
gitIn(dir, ["add", "packages/kilo-docs/LEARNINGS.md"])
gitIn(dir, ["commit", "-m", "seed learnings"])
const tip = gitIn(dir, ["rev-parse", "HEAD"])
const tipDate = new Date().toISOString()
const cwd = setupLearnRepo(dir)
const body = `<!-- docs-sync: learned-through commit=${tip} comment=${tipDate} -->`
const fixturePath = writeFixture(cwd, {
pr: { number: 1, head: { ref: "docs/auto-sync" }, body, user: { login: "github-actions[bot]" } },
comments: [],
})
const callLog = path.join(cwd, "kilo-calls.log")
const kiloDir = makeStubKiloDir({ mode: "extraction-delta", callLog })
writeExtractionDelta(cwd, { add: [], remove: [] })
fs.writeFileSync(path.join(cwd, "docs-sync-out", "learnings.json"), JSON.stringify(existing, null, 2))
const result = runNodeScript(LEARN_SCRIPT, {
cwd,
kiloDir,
env: {
TRIAGE_MODEL: "test/model",
DOCS_SYNC_FIXTURE: fixturePath,
LEARNINGS_BUDGET_MINUTES: "1",
DOCS_SYNC_BACKOFF_MS: "0",
},
})
// Assert: stub never invoked, learnings.json unchanged, no model call
const calls = fs.existsSync(callLog) ? fs.readFileSync(callLog, "utf8").trim() : ""
assert.equal(calls, "", `kilo must not be invoked when marker covers all; got ${calls}`)
const out = JSON.parse(fs.readFileSync(path.join(cwd, "docs-sync-out", "learnings.json"), "utf8"))
assert.deepEqual(out, existing, "learnings.json must equal existing entries")
// Run apply and prove LEARNINGS.md is byte-unchanged
const lp = path.join(cwd, "packages", "kilo-docs", "LEARNINGS.md")
const before = fs.readFileSync(lp, "utf8")
runNodeScript(LEARN_SCRIPT, {
cwd,
kiloDir,
args: ["--apply"],
env: { DOCS_SYNC_BACKOFF_MS: "0" },
})
const after = fs.readFileSync(lp, "utf8")
assert.equal(after, before, "LEARNINGS.md must be byte-unchanged after apply")
}
// 10c — rerun idempotency
{
console.log(" 10c — rerun idempotency")
const dir = mktemp("docs-sync-learn-c-")
initRepoWithIdentity(dir)
fs.writeFileSync(path.join(dir, "base.txt"), "base\n")
gitIn(dir, ["add", "base.txt"])
gitIn(dir, ["commit", "-m", "base"])
gitIn(dir, ["checkout", "-b", "docs/auto-sync"])
fs.mkdirSync(path.join(dir, "packages", "kilo-docs", "pages"), { recursive: true })
fs.writeFileSync(path.join(dir, "packages", "kilo-docs", "pages", "x.md"), "# x\n")
gitIn(dir, ["add", "packages/kilo-docs/pages/x.md"])
gitIn(dir, ["commit", "-m", "docs update", "--author", `kiloconnect[bot] <${kiloconnectBotEmail}>`])
let tip = gitIn(dir, ["rev-parse", "HEAD"])
const add = [
{
id: "new-rule",
rule: "A new rule from testing.",
scope: "both",
source: `commit:${tip.slice(0, 7)}`,
date: "2026-08-03",
},
]
let firstLEARNINGS
// First run
{
const cwd = setupLearnRepo(dir)
const fixturePath = writeFixture(cwd, {
pr: { number: 1, head: { ref: "docs/auto-sync" }, body: "", user: { login: "github-actions[bot]" } },
comments: [],
})
const kiloDir = makeStubKiloDir({ mode: "extraction-delta", callLog: path.join(cwd, "kilo-calls.log") })
writeExtractionDelta(cwd, { add, remove: [] })
const result = runNodeScript(LEARN_SCRIPT, {
cwd,
kiloDir,
env: {
TRIAGE_MODEL: "test/model",
DOCS_SYNC_FIXTURE: fixturePath,
LEARNINGS_BUDGET_MINUTES: "1",
DOCS_SYNC_BACKOFF_MS: "0",
},
})
const out1 = JSON.parse(fs.readFileSync(path.join(cwd, "docs-sync-out", "learnings.json"), "utf8"))
assert.equal(out1.length, 1)
assert.equal(out1[0].id, "new-rule")
// Write LEARNINGS.md on the branch so second run sees existing entries
const learningsPath = path.join(dir, "packages", "kilo-docs", "LEARNINGS.md")
fs.mkdirSync(path.dirname(learningsPath), { recursive: true })
fs.writeFileSync(learningsPath, renderLearnings(out1))
gitIn(dir, ["add", "packages/kilo-docs/LEARNINGS.md"])
gitIn(dir, ["commit", "-m", "seed learnings"])
tip = gitIn(dir, ["rev-parse", "HEAD"])
firstLEARNINGS = fs.readFileSync(path.join(dir, "packages", "kilo-docs", "LEARNINGS.md"), "utf8")
}
// Second run with marker covering first run's result
{
const cwd = setupLearnRepo(dir)
const body = `<!-- docs-sync: learned-through commit=${tip} comment=2026-08-03T12:00:00Z -->`
const fixturePath = writeFixture(cwd, {
pr: { number: 1, head: { ref: "docs/auto-sync" }, body, user: { login: "github-actions[bot]" } },
comments: [],
})
const callLog2 = path.join(cwd, "kilo-calls-run2.log")
const kiloDir2 = makeStubKiloDir({ mode: "extraction-delta", callLog: callLog2 })
writeExtractionDelta(cwd, { add, remove: [] })
const result = runNodeScript(LEARN_SCRIPT, {
cwd,
kiloDir: kiloDir2,
env: {
TRIAGE_MODEL: "test/model",
DOCS_SYNC_FIXTURE: fixturePath,
LEARNINGS_BUDGET_MINUTES: "1",
DOCS_SYNC_BACKOFF_MS: "0",
},
})
const calls2 = fs.existsSync(callLog2) ? fs.readFileSync(callLog2, "utf8").trim() : ""
assert.equal(calls2, "", "second run must not invoke kilo")
const out2 = JSON.parse(fs.readFileSync(path.join(cwd, "docs-sync-out", "learnings.json"), "utf8"))
assert.equal(out2.length, 1)
assert.equal(out2[0].id, "new-rule")
// Run apply and prove LEARNINGS.md is byte-identical after idempotent rerun
runNodeScript(LEARN_SCRIPT, {
cwd,
kiloDir: kiloDir2,
args: ["--apply"],
env: { DOCS_SYNC_BACKOFF_MS: "0" },
})
const afterApply = fs.readFileSync(path.join(dir, "packages", "kilo-docs", "LEARNINGS.md"), "utf8")
assert.equal(afterApply.length, firstLEARNINGS.length, "LEARNINGS.md must be same length after apply")
assert.equal(afterApply, firstLEARNINGS, "LEARNINGS.md must be byte-identical after apply")
}
}
// 10d — contradiction replacement (applyDelta)
{
console.log(" 10d — contradiction replacement")
const old = [
{ id: "old-rule", rule: "Old rule text.", scope: "both", source: "commit:aaaaaaa", date: "2026-01-01" },
]
const add = [
{ id: "new-rule", rule: "New rule text.", scope: "both", source: "commit:bbbbbbb", date: "2026-02-01" },
]
const delta = { add, remove: ["old-rule"] }
const result = applyDelta(old, delta)
assert.equal(result.length, 1)
assert.equal(result[0].id, "new-rule")
// Third delta touching neither does not bring old back
const again = applyDelta(result, { add: [], remove: [] })
assert.equal(again.length, 1)
assert.equal(again[0].id, "new-rule")
}
// 10e — prompt injection (triage/edit argv carry tagged rules)
{
console.log(" 10e — prompt injection")
const dir = mktemp("docs-sync-learn-e-")
initRepoWithIdentity(dir)
fs.writeFileSync(path.join(dir, "base.txt"), "base\n")
gitIn(dir, ["add", "base.txt"])
gitIn(dir, ["commit", "-m", "base"])
gitIn(dir, ["checkout", "-b", "docs/auto-sync"])
// Add a corrective commit so extraction has a candidate
fs.mkdirSync(path.join(dir, "packages", "kilo-docs", "pages"), { recursive: true })
fs.writeFileSync(path.join(dir, "packages", "kilo-docs", "pages", "x.md"), "# x\n")
gitIn(dir, ["add", "packages/kilo-docs/pages/x.md"])
gitIn(dir, ["commit", "-m", "docs update", "--author", `kiloconnect[bot] <${kiloconnectBotEmail}>`])
const commitSha = gitIn(dir, ["rev-parse", "HEAD"])
const cwd = setupLearnRepo(dir)
const fixturePath = writeFixture(cwd, {
pr: { number: 1, head: { ref: "docs/auto-sync" }, body: "", user: { login: "github-actions[bot]" } },
comments: [],
})
const callLog = path.join(cwd, "kilo-calls.log")
const kiloDir = makeStubKiloDir({ mode: "extraction-delta", callLog })
// Three entries: triage, edit, both — all from the same candidate source
const src = `commit:${commitSha.slice(0, 7)}`
writeExtractionDelta(cwd, {
add: [
{ id: "triage-rule", rule: "Triage-only rule text.", scope: "triage", source: src, date: "2026-08-01" },
{ id: "edit-rule", rule: "Edit-only rule text.", scope: "edit", source: src, date: "2026-08-02" },
{ id: "both-rule", rule: "Both scope rule text.", scope: "both", source: src, date: "2026-08-03" },
],
remove: [],
})
// Run extraction so it writes learnings-<scope>.md blocks (extraction step 13).
// Only extraction writes these files; --apply writes only LEARNINGS.md.
runNodeScript(LEARN_SCRIPT, {
cwd,
kiloDir,
env: {
TRIAGE_MODEL: "test/model",
DOCS_SYNC_FIXTURE: fixturePath,
LEARNINGS_BUDGET_MINUTES: "1",
DOCS_SYNC_BACKOFF_MS: "0",
},
})
const triageBlockPath = path.join(cwd, "docs-sync-out", "learnings-triage.md")
const editBlockPath = path.join(cwd, "docs-sync-out", "learnings-edit.md")
assert.ok(fs.existsSync(triageBlockPath), "learnings-triage.md must exist")
assert.ok(fs.existsSync(editBlockPath), "learnings-edit.md must exist")
// Run triage.mjs against a recording stub. Copy the learnings block into
// its cwd so readLearningsBlock picks it up.
{
const triageCwd = setupTriageCwd([samplePr(1)])
fs.copyFileSync(triageBlockPath, path.join(triageCwd, "docs-sync-out", "learnings-triage.md"))
const triageCallLog = path.join(triageCwd, "triage-calls.log")
const triageKiloDir = makeStubKiloDir({ mode: "record", callLog: triageCallLog })
runNodeScript(TRIAGE_SCRIPT, {
cwd: triageCwd,
kiloDir: triageKiloDir,
env: { TRIAGE_MODEL: "test/model", DOCS_SYNC_BACKOFF_MS: "0" },
})
const logText = fs.readFileSync(triageCallLog, "utf8")
assert.ok(logText.includes("Triage-only rule text"), "triage argv must contain triage rule")
assert.ok(logText.includes("Both scope rule text"), "triage argv must contain both rule")
assert.ok(!logText.includes("Edit-only rule text"), "triage argv must not contain edit-only rule")
}
// Run edit.mjs against a recording stub.
{
const triageEntry = {
pr: 1,
url: "https://github.com/Kilo-Org/cloud/pull/1",
docs_worthy: true,
reason: "needs docs",
target_sections: [],
priority: "medium",
}
const editCwd = setupEditCwd([samplePr(1)], [triageEntry])
fs.copyFileSync(editBlockPath, path.join(editCwd, "docs-sync-out", "learnings-edit.md"))
const editCallLog = path.join(editCwd, "edit-calls.log")
const editKiloDir = makeStubKiloDir({ mode: "record", callLog: editCallLog })
runNodeScript(EDIT_SCRIPT, {
cwd: editCwd,
kiloDir: editKiloDir,
env: { EDIT_MODEL: "test/model", DOCS_SYNC_BACKOFF_MS: "0" },
})
const logText = fs.readFileSync(editCallLog, "utf8")
assert.ok(logText.includes("Edit-only rule text"), "edit argv must contain edit rule")
assert.ok(logText.includes("Both scope rule text"), "edit argv must contain both rule")
assert.ok(!logText.includes("Triage-only rule text"), "edit argv must not contain triage-only rule")
}
}
// 10f — failure path
{
console.log(" 10f — failure path")
const dir = mktemp("docs-sync-learn-f-")
initRepoWithIdentity(dir)
fs.writeFileSync(path.join(dir, "base.txt"), "base\n")
gitIn(dir, ["add", "base.txt"])
gitIn(dir, ["commit", "-m", "base"])
gitIn(dir, ["checkout", "-b", "docs/auto-sync"])
fs.mkdirSync(path.join(dir, "packages", "kilo-docs", "pages"), { recursive: true })
fs.writeFileSync(path.join(dir, "packages", "kilo-docs", "pages", "x.md"), "# x\n")
gitIn(dir, ["add", "packages/kilo-docs/pages/x.md"])
gitIn(dir, ["commit", "-m", "docs update", "--author", `kiloconnect[bot] <${kiloconnectBotEmail}>`])
const existing = [
{ id: "test", rule: "existing rule", scope: "both", source: "commit:0000000", date: "2026-01-01" },
]
// Write LEARNINGS.md on the branch so learn.mjs reads it as existing entries
const learningsPath = path.join(dir, "packages", "kilo-docs", "LEARNINGS.md")
fs.mkdirSync(path.dirname(learningsPath), { recursive: true })
fs.writeFileSync(learningsPath, renderLearnings(existing))
gitIn(dir, ["add", "packages/kilo-docs/LEARNINGS.md"])
gitIn(dir, ["commit", "-m", "seed learnings"])
const cwd = setupLearnRepo(dir)
const fixturePath = writeFixture(cwd, {
pr: { number: 1, head: { ref: "docs/auto-sync" }, body: "", user: { login: "github-actions[bot]" } },
comments: [],
})
// Stub exits 0 with garbage stdout (stderr-exit0 mode)
const stderrText = "some fake error stream"
const kiloDir = makeStubKiloDir({ mode: "stderr-exit0", stderrText })
const outputFile = path.join(cwd, "gh-output-f")
const result = runNodeScript(LEARN_SCRIPT, {
cwd,
kiloDir,
env: {
TRIAGE_MODEL: "test/model",
DOCS_SYNC_FIXTURE: fixturePath,
LEARNINGS_BUDGET_MINUTES: "1",
DOCS_SYNC_BACKOFF_MS: "0",
GITHUB_OUTPUT: outputFile,
},
})
assert.equal(result.status, 0, "learn.mjs must exit 0 on failure")
const out = JSON.parse(fs.readFileSync(path.join(cwd, "docs-sync-out", "learnings.json"), "utf8"))
assert.deepEqual(out, existing, "learnings.json must equal existing entries on failure")
// No marker PATCH file
assert.ok(!fs.existsSync(`${fixturePath}.patched`), "no marker PATCH on failure")
// No learned_through output
if (fs.existsSync(outputFile)) {
const ghOut = fs.readFileSync(outputFile, "utf8")
assert.ok(!ghOut.includes("learned_through="), "GITHUB_OUTPUT must not contain learned_through on failure")
}
// Run apply and prove LEARNINGS.md is byte-unchanged
const lp = path.join(cwd, "packages", "kilo-docs", "LEARNINGS.md")
const before = fs.readFileSync(lp, "utf8")
runNodeScript(LEARN_SCRIPT, {
cwd,
kiloDir,
args: ["--apply"],
env: { DOCS_SYNC_BACKOFF_MS: "0" },
})
const after = fs.readFileSync(lp, "utf8")
assert.equal(after, before, "LEARNINGS.md must be byte-unchanged after apply on failure")
}
// 10g — general-rule check (validateDelta rejections)
{
console.log(" 10g — general-rule check")
const existing = []
const sources = ["commit:aaaaaaa"]
const entryWithPR = {
id: "bad-pr",
rule: "See #12716 for details",
scope: "both",
source: "commit:aaaaaaa",
date: "2026-08-03",
}
const entryWithURL = {
id: "bad-url",
rule: "Check https://example.com",
scope: "both",
source: "commit:aaaaaaa",
date: "2026-08-03",
}
const entryWithPerson = {
id: "bad-person",
rule: "Ask @emilieschario",
scope: "both",
source: "commit:aaaaaaa",
date: "2026-08-03",
}
const entryWithPage = {
id: "bad-page",
rule: "Edit packages/kilo-docs/pages/x.md",
scope: "both",
source: "commit:aaaaaaa",
date: "2026-08-03",
}
const entryBadSource = {
id: "bad-source",
rule: "A valid sentence.",
scope: "both",
source: "invented",
date: "2026-08-03",
}
const entryBadScope = {
id: "bad-scope",
rule: "A valid sentence.",
scope: "wrong",
source: "commit:aaaaaaa",
date: "2026-08-03",
}
const entryCollide = {
id: "existing-id",
rule: "A valid sentence.",
scope: "both",
source: "commit:aaaaaaa",
date: "2026-08-03",
}
const entryBadDate = {
id: "bad-date",
rule: "A valid sentence.",
scope: "both",
source: "commit:aaaaaaa",
date: "not-a-date",
}
const entryRemoveUnknown = {
id: "valid",
rule: "A valid sentence.",
scope: "both",
source: "commit:aaaaaaa",
date: "2026-08-03",
}
const existingWithId = [
{ id: "existing-id", rule: "Existing rule.", scope: "both", source: "commit:aaaaaaa", date: "2026-01-01" },
]
// PR number
{
const { rejected } = validateDelta(
{ add: [entryWithPR], remove: [] },
{ existing, candidateSources: sources, deletedInWindow: [] },
)
assert.equal(rejected.length, 1, "PR number must be rejected")
assert.ok(rejected[0].reason, "rejection must carry a reason")
}
// URL — docs-check-links.yml link-checks LEARNINGS.md with fail:true, so
// a URL in a rule would break CI on every bot commit.
{
const { rejected } = validateDelta(
{ add: [entryWithURL], remove: [] },
{ existing, candidateSources: sources, deletedInWindow: [] },
)
assert.equal(rejected.length, 1, "URL must be rejected")
}
// Person
{
const { rejected } = validateDelta(
{ add: [entryWithPerson], remove: [] },
{ existing, candidateSources: sources, deletedInWindow: [] },
)
assert.equal(rejected.length, 1, "person reference must be rejected")
}
// Docs page
{
const { rejected } = validateDelta(
{ add: [entryWithPage], remove: [] },
{ existing, candidateSources: sources, deletedInWindow: [] },
)
assert.equal(rejected.length, 1, "docs page reference must be rejected")
}
// Bad source
{
const { rejected } = validateDelta(
{ add: [entryBadSource], remove: [] },
{ existing, candidateSources: sources, deletedInWindow: [] },
)
assert.equal(rejected.length, 1, "invented source must be rejected")
}
// Bad scope
{
const { rejected } = validateDelta(
{ add: [entryBadScope], remove: [] },
{ existing, candidateSources: sources, deletedInWindow: [] },
)
assert.equal(rejected.length, 1, "bad scope must be rejected")
}
// Colliding id
{
const { rejected } = validateDelta(
{ add: [entryCollide], remove: [] },
{ existing: existingWithId, candidateSources: sources, deletedInWindow: [] },
)
assert.equal(rejected.length, 1, "colliding id must be rejected")
}
// Remove unknown id
{
const { rejected } = validateDelta(
{ add: [entryRemoveUnknown], remove: ["unknown-id"] },
{ existing, candidateSources: sources, deletedInWindow: [] },
)
assert.equal(rejected.length, 1, "remove of unknown id must be rejected")
}
// Bad date
{
const { rejected } = validateDelta(
{ add: [entryBadDate], remove: [] },
{ existing, candidateSources: sources, deletedInWindow: [] },
)
assert.equal(rejected.length, 1, "bad date must be rejected")
}
}
// 10h — comment trust (isTrustedComment)
{
console.log(" 10h — comment trust")
assert.equal(isTrustedComment({ author_association: "OWNER", user: { login: "owner-user" } }), true)
assert.equal(isTrustedComment({ author_association: "MEMBER", user: { login: "emilieschario" } }), true)
assert.equal(isTrustedComment({ author_association: "COLLABORATOR", user: { login: "collab-user" } }), true)
assert.equal(isTrustedComment({ author_association: "CONTRIBUTOR", user: { login: "kilo-code-bot[bot]" } }), false)
assert.equal(isTrustedComment({ author_association: "NONE", user: { login: "rando" } }), false)
// MEMBER whose login ends in [bot]
assert.equal(isTrustedComment({ author_association: "MEMBER", user: { login: "some-bot[bot]" } }), false)
}
// 10i — draft gate (nonContentFiles)
{
console.log(" 10i — draft gate")
const files = ["packages/kilo-docs/LEARNINGS.md", "packages/kilo-docs/pages/a.md"]
const result = nonContentFiles(files)
assert.equal(result.length, 0, "LEARNINGS.md and pages must not trigger the draft gate")
// Still flags non-content
const withConfig = ["packages/kilo-docs/next.config.js"]
const flagged = nonContentFiles(withConfig)
assert.equal(flagged.length, 1, "next.config.js must still trigger the gate")
assert.equal(flagged[0], "packages/kilo-docs/next.config.js")
}
// 10j — no --auto on extraction call
{
console.log(" 10j — no --auto on extraction call")
const src = fs.readFileSync(LEARN_SCRIPT, "utf8")
// Find the extraction-mode runKilo args array
const argsStart = src.indexOf("runKilo({")
assert.ok(argsStart >= 0, "runKilo call must exist in learn.mjs")
const argsBlock = src.slice(argsStart, src.indexOf("})", argsStart) + 2)
assert.ok(!argsBlock.includes("--auto"), "extraction runKilo must not include --auto")
assert.ok(argsBlock.includes("-f"), "extraction runKilo must include -f")
}
// 10k — hand-mangled file (parseLearnings)
{
console.log(" 10k — hand-mangled file")
const text = `# header
<!-- docs-sync:learnings:start -->
- Valid rule. <!-- id=valid-rule scope=both source=commit:aaaaaaa date=2026-08-03 -->
- Broken meta. <!-- id=broken-rule scope=not-a-scope source=bad date=bad -->
Just prose, not a rule line.
<!-- docs-sync:learnings:end -->`
const entries = parseLearnings(text)
assert.equal(entries.length, 1, "only valid entry must parse")
assert.equal(entries[0].id, "valid-rule")
}
// 10l — first-run fallback (main when branch has none)
// Prove the git commands learn.mjs relies on: when the branch file is absent,
// git show origin/<branch>:packages/kilo-docs/LEARNINGS.md fails, and
// git show origin/main:packages/kilo-docs/LEARNINGS.md returns the main's entries.
{
console.log(" 10l — empty file fallback")
const dir = mktemp("docs-sync-learn-l-")
initRepoWithIdentity(dir)
// Write LEARNINGS.md on main
const entries = [
{ id: "test", rule: "Test rule text.", scope: "both", source: "commit:aaaaaaa", date: "2026-01-01" },
]
const lp = path.join(dir, "packages", "kilo-docs", "LEARNINGS.md")
fs.mkdirSync(path.dirname(lp), { recursive: true })
fs.writeFileSync(lp, renderLearnings(entries))
gitIn(dir, ["add", "packages/kilo-docs/LEARNINGS.md"])
gitIn(dir, ["commit", "-m", "main learnings"])
// Branch from main, then remove LEARNINGS.md
gitIn(dir, ["checkout", "-b", "docs/auto-sync"])
fs.rmSync(lp)
gitIn(dir, ["add", "packages/kilo-docs/LEARNINGS.md"])
gitIn(dir, ["commit", "-m", "remove learnings on branch"])
// Set up origin refs so git show origin/<ref> resolves
setupLearnRepo(dir)
// git show on branch must fail — file absent at that ref
let branchFailed = false
try {
gitIn(dir, ["show", "origin/docs/auto-sync:packages/kilo-docs/LEARNINGS.md"])
} catch {
branchFailed = true
}
assert.ok(branchFailed, "git show on branch must fail when LEARNINGS.md absent")
// git show on main must succeed with the main's entries
const mainContent = gitIn(dir, ["show", "origin/main:packages/kilo-docs/LEARNINGS.md"])
const parsed = parseLearnings(mainContent)
assert.equal(parsed.length, 1, "main fallback must return the main's entries")
assert.equal(parsed[0].id, "test")
// Also verify: empty parse and render (unit coverage of the empty case)
const empty = parseLearnings("")
assert.equal(empty.length, 0)
assert.deepEqual(empty, [])
const rendered = renderLearnings([])
assert.ok(rendered.includes("<!-- docs-sync:learnings:start -->"))
assert.ok(rendered.includes("<!-- docs-sync:learnings:end -->"))
}
// 10m — empty delta advances marker with no file change
{
console.log(" 10m — empty delta marker advance")
const dir = mktemp("docs-sync-learn-m-")
initRepoWithIdentity(dir)
fs.writeFileSync(path.join(dir, "base.txt"), "base\n")
gitIn(dir, ["add", "base.txt"])
gitIn(dir, ["commit", "-m", "base"])
gitIn(dir, ["checkout", "-b", "docs/auto-sync"])
fs.mkdirSync(path.join(dir, "packages", "kilo-docs", "pages"), { recursive: true })
fs.writeFileSync(path.join(dir, "packages", "kilo-docs", "pages", "x.md"), "# x\n")
gitIn(dir, ["add", "packages/kilo-docs/pages/x.md"])
gitIn(dir, ["commit", "-m", "docs update", "--author", `kiloconnect[bot] <${kiloconnectBotEmail}>`])
const tip = gitIn(dir, ["rev-parse", "HEAD"])
const cwd = setupLearnRepo(dir)
const fixturePath = writeFixture(cwd, {
pr: { number: 1, head: { ref: "docs/auto-sync" }, body: "", user: { login: "github-actions[bot]" } },
comments: [],
})
const kiloDir = makeStubKiloDir({ mode: "extraction-delta", callLog: path.join(cwd, "kilo-calls.log") })
writeExtractionDelta(cwd, { add: [], remove: [] })
const existing = []
fs.writeFileSync(path.join(cwd, "docs-sync-out", "learnings.json"), JSON.stringify(existing, null, 2))
const learningsPath = path.join(cwd, "packages", "kilo-docs", "LEARNINGS.md")
fs.mkdirSync(path.dirname(learningsPath), { recursive: true })
fs.writeFileSync(learningsPath, renderLearnings(existing))
const before = fs.readFileSync(learningsPath, "utf8")
const outputFile = path.join(cwd, "gh-output-m")
const result = runNodeScript(LEARN_SCRIPT, {
cwd,
kiloDir,
env: {
TRIAGE_MODEL: "test/model",
DOCS_SYNC_FIXTURE: fixturePath,
LEARNINGS_BUDGET_MINUTES: "1",
DOCS_SYNC_BACKOFF_MS: "0",
GITHUB_OUTPUT: outputFile,
},
})
// learnings.json unchanged
const out = JSON.parse(fs.readFileSync(path.join(cwd, "docs-sync-out", "learnings.json"), "utf8"))
assert.deepEqual(out, existing)
// No learned_through output (empty delta route omits it per G5 table)
if (fs.existsSync(outputFile)) {
const ghOut = fs.readFileSync(outputFile, "utf8")
assert.ok(!ghOut.includes("learned_through="), "GITHUB_OUTPUT must not contain learned_through on empty delta")
}
const patched = `${fixturePath}.patched`
assert.ok(fs.existsSync(patched), "marker PATCH file must be written for empty delta")
const markerText = fs.readFileSync(patched, "utf8")
assert.ok(markerText.includes(tip), "marker PATCH must contain tip SHA")
runNodeScript(LEARN_SCRIPT, {
cwd,
kiloDir,
args: ["--apply"],
env: { DOCS_SYNC_BACKOFF_MS: "0" },
})
assert.equal(fs.readFileSync(learningsPath, "utf8"), before, "LEARNINGS.md must be byte-unchanged after apply")
// patchMarkerIntoBody: existing marker → replaced in place
{
const oldBody = "some text\n<!-- docs-sync: learned-through commit=old comment=old -->\nmore text\n"
const newLine = "<!-- docs-sync: learned-through commit=abc comment=2026-01-01 -->"
const patched = patchMarkerIntoBody(oldBody, newLine)
assert.ok(patched.includes(newLine), "new marker must be in body")
assert.ok(!patched.includes("commit=old"), "old marker must be gone")
assert.equal(
(patched.match(/<!--\s*docs-sync:\s*learned-through/g) || []).length,
1,
"exactly one marker after replace",
)
}
// patchMarkerIntoBody: no marker → appended
{
const oldBody = "no marker here\n"
const newLine = "<!-- docs-sync: learned-through commit=abc comment=2026-01-01 -->"
const patched = patchMarkerIntoBody(oldBody, newLine)
assert.ok(patched.includes(newLine))
}
}
// 10n — non-empty delta routes through upsert (not learn.mjs PATCH)
{
console.log(" 10n — non-empty delta marker route")
const dir = mktemp("docs-sync-learn-n-")
initRepoWithIdentity(dir)
fs.writeFileSync(path.join(dir, "base.txt"), "base\n")
gitIn(dir, ["add", "base.txt"])
gitIn(dir, ["commit", "-m", "base"])
gitIn(dir, ["checkout", "-b", "docs/auto-sync"])
fs.mkdirSync(path.join(dir, "packages", "kilo-docs", "pages"), { recursive: true })
fs.writeFileSync(path.join(dir, "packages", "kilo-docs", "pages", "x.md"), "# x\n")
gitIn(dir, ["add", "packages/kilo-docs/pages/x.md"])
gitIn(dir, ["commit", "-m", "docs update", "--author", `kiloconnect[bot] <${kiloconnectBotEmail}>`])
const source = `commit:${gitIn(dir, ["rev-parse", "HEAD"]).slice(0, 7)}`
const tip = gitIn(dir, ["rev-parse", "HEAD"])
const cwd = setupLearnRepo(dir)
const fixturePath = writeFixture(cwd, {
pr: { number: 1, head: { ref: "docs/auto-sync" }, body: "", user: { login: "github-actions[bot]" } },
comments: [],
})
const kiloDir = makeStubKiloDir({ mode: "extraction-delta", callLog: path.join(cwd, "kilo-calls.log") })
writeExtractionDelta(cwd, {
add: [
{ id: "new-rule", rule: "A new rule.", scope: "both", source: `commit:${tip.slice(0, 7)}`, date: "2026-08-03" },
],
remove: [],
})
const summaryFile = path.join(cwd, "step-summary.md")
fs.writeFileSync(summaryFile, "")
const outputFile = path.join(cwd, "gh-output")
const result = runNodeScript(LEARN_SCRIPT, {
cwd,
kiloDir,
env: {
TRIAGE_MODEL: "test/model",
DOCS_SYNC_FIXTURE: fixturePath,
LEARNINGS_BUDGET_MINUTES: "1",
DOCS_SYNC_BACKOFF_MS: "0",
GITHUB_STEP_SUMMARY: summaryFile,
GITHUB_OUTPUT: outputFile,
},
})
// No marker PATCH file
assert.ok(!fs.existsSync(`${fixturePath}.patched`), "non-empty delta must not PATCH marker")
// Assert learned_through was written to GITHUB_OUTPUT (non-empty delta route)
const ghOut = fs.readFileSync(outputFile, "utf8")
assert.ok(ghOut.includes("learned_through="), "GITHUB_OUTPUT must contain learned_through on non-empty delta")
assert.ok(ghOut.includes(`commit=${tip}`), "learned_through must contain tip SHA")
// renderBody with learnedThrough marker
const marker = "<!-- docs-sync: learned-through commit=abc comment=2026-01-01 -->"
const body1 = renderBody({
date: "2026-08-03",
since: "2026-07-01T00:00:00.000Z",
through: "2026-08-03T00:00:00.000Z",
learnedThrough: marker,
changesRows: [],
pendingRows: [],
skippedRows: [],
verified: true,
draftReasons: [],
note: "",
})
assert.ok(body1.includes(marker), "renderBody must emit marker when given")
// renderBody with parameter omitted → no marker
const body2 = renderBody({
date: "2026-08-03",
since: "2026-07-01T00:00:00.000Z",
through: "2026-08-03T00:00:00.000Z",
changesRows: [],
pendingRows: [],
skippedRows: [],
verified: true,
draftReasons: [],
note: "",
})
assert.ok(!body2.includes("learned-through"), "renderBody without learnedThrough must emit no marker")
// Still round-trips
const extChanges = extractSectionRows(body2, "changes")
assert.deepEqual(extChanges, [])
}
// 10o — deleted-in-window rejection
{
console.log(" 10o — deleted-in-window rejection")
// Normalized match catches near-identical wording
{
const delta = {
add: [
{
id: "dup",
rule: "do not document experimental features!",
scope: "both",
source: "commit:aaaaaaa",
date: "2026-08-03",
},
],
remove: [],
}
const { rejected } = validateDelta(delta, {
existing: [],
candidateSources: ["commit:aaaaaaa"],
deletedInWindow: ["Do not document experimental features."],
})
assert.equal(rejected.length, 1, "normalized match must reject deleted rule")
}
// Different meaning on same topic — NOT rejected (accepted limit)
{
const delta = {
add: [
{
id: "good",
rule: "Document experimental features in a separate section.",
scope: "both",
source: "commit:aaaaaaa",
date: "2026-08-03",
},
],
remove: [],
}
const { rejected } = validateDelta(delta, {
existing: [],
candidateSources: ["commit:aaaaaaa"],
deletedInWindow: ["Do not document experimental features."],
})
assert.equal(rejected.length, 0, "different rule on same topic must not be rejected")
}
}
// 10p — resolveLearnedThrough pure function and anti-drift assertions
{
console.log(" 10p — resolveLearnedThrough + anti-drift")
// Unit tests on the pure export
// env value set wins over body marker
assert.equal(
resolveLearnedThrough({
envValue: "<!-- docs-sync: learned-through commit=env commit=env -->",
prBody: "<!-- docs-sync: learned-through commit=body comment=body -->",
}),
"<!-- docs-sync: learned-through commit=env commit=env -->",
)
// env unset, body marker present → body wins
assert.equal(
resolveLearnedThrough({ envValue: "", prBody: "<!-- docs-sync: learned-through commit=body comment=body -->" }),
"<!-- docs-sync: learned-through commit=body comment=body -->",
)
// both absent → empty string
assert.equal(resolveLearnedThrough({ envValue: "", prBody: "" }), "")
// env set to whitespace → treated as unset
assert.equal(
resolveLearnedThrough({
envValue: " ",
prBody: "<!-- docs-sync: learned-through commit=body comment=body -->",
}),
"<!-- docs-sync: learned-through commit=body comment=body -->",
)
// renderBody emits no marker for ""
const bodyEmpty = renderBody({
date: "d",
since: "s",
through: "t",
learnedThrough: "",
changesRows: [],
pendingRows: [],
skippedRows: [],
verified: true,
draftReasons: [],
note: "",
})
assert.ok(!bodyEmpty.includes("learned-through"), "empty learnedThrough must emit no marker")
// Anti-drift: static-source assertions on upsert-pr.mjs
const upsertSrc = fs.readFileSync(path.join(HERE, "upsert-pr.mjs"), "utf8")
// (a) exactly one resolveLearnedThrough( call passing process.env.LEARNED_THROUGH
const calls = upsertSrc.match(/resolveLearnedThrough\(/g) || []
// One in the export definition, one in the call site
assert.ok(calls.length >= 2, `expected at least 2 resolveLearnedThrough( occurrences; got ${calls.length}`)
assert.ok(
upsertSrc.includes("process.env.LEARNED_THROUGH"),
"resolveLearnedThrough must receive process.env.LEARNED_THROUGH",
)
assert.ok(upsertSrc.includes("prBody"), "resolveLearnedThrough must receive prBody")
// (b) prBody is function-scoped and read before the body is rendered
const prBodyIdx = upsertSrc.indexOf('let prBody = ""')
assert.ok(prBodyIdx >= 0, 'prBody must be declared at function scope with let prBody = ""')
// (c) the renderBody({ argument object contains learnedThrough + surfaceBlock
const renderBodyIdx = upsertSrc.indexOf("const body = renderBody({")
assert.ok(renderBodyIdx >= 0, "renderBody call must exist")
assert.ok(prBodyIdx < renderBodyIdx, "let prBody must appear before the renderBody call")
const afterRenderBody = upsertSrc.slice(renderBodyIdx)
const renderBodyArgsEnd = afterRenderBody.indexOf("})")
const renderBodyArgs = afterRenderBody.slice(0, renderBodyArgsEnd)
assert.ok(renderBodyArgs.includes("learnedThrough"), "renderBody call must pass learnedThrough")
assert.ok(renderBodyArgs.includes("surfaceBlock"), "renderBody call must pass the surface block")
}
// 10q — dry run makes no live write
{
console.log(" 10q — dry run no live write")
const dir = mktemp("docs-sync-learn-q-")
initRepoWithIdentity(dir)
fs.writeFileSync(path.join(dir, "base.txt"), "base\n")
gitIn(dir, ["add", "base.txt"])
gitIn(dir, ["commit", "-m", "base"])
gitIn(dir, ["checkout", "-b", "docs/auto-sync"])
fs.mkdirSync(path.join(dir, "packages", "kilo-docs", "pages"), { recursive: true })
fs.writeFileSync(path.join(dir, "packages", "kilo-docs", "pages", "x.md"), "# x\n")
gitIn(dir, ["add", "packages/kilo-docs/pages/x.md"])
gitIn(dir, ["commit", "-m", "docs update", "--author", `kiloconnect[bot] <${kiloconnectBotEmail}>`])
// DRY_RUN=true
{
const cwd = setupLearnRepo(dir)
const fixturePath = writeFixture(cwd, {
pr: { number: 1, head: { ref: "docs/auto-sync" }, body: "", user: { login: "github-actions[bot]" } },
comments: [],
})
const kiloDir = makeStubKiloDir({ mode: "extraction-delta", callLog: path.join(cwd, "kilo-calls.log") })
writeExtractionDelta(cwd, { add: [], remove: [] })
const outputFile = path.join(cwd, "gh-output-q-dry")
const result = runNodeScript(LEARN_SCRIPT, {
cwd,
kiloDir,
env: {
TRIAGE_MODEL: "test/model",
DOCS_SYNC_FIXTURE: fixturePath,
LEARNINGS_BUDGET_MINUTES: "1",
DOCS_SYNC_BACKOFF_MS: "0",
DRY_RUN: "true",
GITHUB_OUTPUT: outputFile,
},
})
assert.ok(!fs.existsSync(`${fixturePath}.patched`), "DRY_RUN must suppress marker PATCH")
if (fs.existsSync(outputFile)) {
const ghOut = fs.readFileSync(outputFile, "utf8")
assert.ok(!ghOut.includes("learned_through="), "GITHUB_OUTPUT must not contain learned_through on DRY_RUN")
}
assert.ok(result.stdout.includes("marker PATCH suppressed"), "stdout must log marker suppression for DRY_RUN")
assert.ok(
result.stdout.includes("would have written marker"),
"stdout must log the suppressed marker for DRY_RUN",
)
}
// LEARNINGS_NO_PATCH=1 (same mechanism)
{
const cwd = setupLearnRepo(dir)
const fixturePath = writeFixture(cwd, {
pr: { number: 1, head: { ref: "docs/auto-sync" }, body: "", user: { login: "github-actions[bot]" } },
comments: [],
})
const kiloDir = makeStubKiloDir({ mode: "extraction-delta", callLog: path.join(cwd, "kilo-calls.log") })
writeExtractionDelta(cwd, { add: [], remove: [] })
const outputFile = path.join(cwd, "gh-output-q-nopatch")
const result = runNodeScript(LEARN_SCRIPT, {
cwd,
kiloDir,
env: {
TRIAGE_MODEL: "test/model",
DOCS_SYNC_FIXTURE: fixturePath,
LEARNINGS_BUDGET_MINUTES: "1",
DOCS_SYNC_BACKOFF_MS: "0",
LEARNINGS_NO_PATCH: "1",
GITHUB_OUTPUT: outputFile,
},
})
assert.ok(!fs.existsSync(`${fixturePath}.patched`), "LEARNINGS_NO_PATCH must suppress marker PATCH")
if (fs.existsSync(outputFile)) {
const ghOut = fs.readFileSync(outputFile, "utf8")
assert.ok(
!ghOut.includes("learned_through="),
"GITHUB_OUTPUT must not contain learned_through on LEARNINGS_NO_PATCH",
)
}
assert.ok(
result.stdout.includes("marker PATCH suppressed"),
"stdout must log marker suppression for LEARNINGS_NO_PATCH",
)
assert.ok(
result.stdout.includes("would have written marker"),
"stdout must log the suppressed marker for LEARNINGS_NO_PATCH",
)
}
// DRY_RUN=true with non-empty delta (suppresses learned_through output)
{
const cwd = setupLearnRepo(dir)
const tipSource = `commit:${gitIn(dir, ["rev-parse", "HEAD"]).slice(0, 7)}`
const fixturePath = writeFixture(cwd, {
pr: { number: 1, head: { ref: "docs/auto-sync" }, body: "", user: { login: "github-actions[bot]" } },
comments: [],
})
const kiloDir = makeStubKiloDir({ mode: "extraction-delta", callLog: path.join(cwd, "kilo-calls.log") })
writeExtractionDelta(cwd, {
add: [
{
id: "dry-suppress",
rule: "A rule suppressed under dry run.",
scope: "both",
source: tipSource,
date: "2026-08-03",
},
],
remove: [],
})
const outputFile = path.join(cwd, "gh-output-q-dry-nonempty")
const result = runNodeScript(LEARN_SCRIPT, {
cwd,
kiloDir,
env: {
TRIAGE_MODEL: "test/model",
DOCS_SYNC_FIXTURE: fixturePath,
LEARNINGS_BUDGET_MINUTES: "1",
DOCS_SYNC_BACKOFF_MS: "0",
DRY_RUN: "true",
GITHUB_OUTPUT: outputFile,
},
})
if (fs.existsSync(outputFile)) {
const ghOut = fs.readFileSync(outputFile, "utf8")
assert.ok(
!ghOut.includes("learned_through="),
"GITHUB_OUTPUT must not contain learned_through on DRY_RUN non-empty delta",
)
}
assert.ok(
result.stdout.includes("learned-through output suppressed"),
"stdout must log learned-through output suppression for DRY_RUN non-empty delta",
)
}
// LEARNINGS_NO_PATCH=1 with non-empty delta
{
const cwd = setupLearnRepo(dir)
const tipSource = `commit:${gitIn(dir, ["rev-parse", "HEAD"]).slice(0, 7)}`
const fixturePath = writeFixture(cwd, {
pr: { number: 1, head: { ref: "docs/auto-sync" }, body: "", user: { login: "github-actions[bot]" } },
comments: [],
})
const kiloDir = makeStubKiloDir({ mode: "extraction-delta", callLog: path.join(cwd, "kilo-calls.log") })
writeExtractionDelta(cwd, {
add: [
{
id: "nopatch-suppress",
rule: "A rule suppressed under no-patch.",
scope: "both",
source: tipSource,
date: "2026-08-03",
},
],
remove: [],
})
const outputFile = path.join(cwd, "gh-output-q-nopatch-nonempty")
const result = runNodeScript(LEARN_SCRIPT, {
cwd,
kiloDir,
env: {
TRIAGE_MODEL: "test/model",
DOCS_SYNC_FIXTURE: fixturePath,
LEARNINGS_BUDGET_MINUTES: "1",
DOCS_SYNC_BACKOFF_MS: "0",
LEARNINGS_NO_PATCH: "1",
GITHUB_OUTPUT: outputFile,
},
})
if (fs.existsSync(outputFile)) {
const ghOut = fs.readFileSync(outputFile, "utf8")
assert.ok(
!ghOut.includes("learned_through="),
"GITHUB_OUTPUT must not contain learned_through on LEARNINGS_NO_PATCH non-empty delta",
)
}
assert.ok(
result.stdout.includes("learned-through output suppressed"),
"stdout must log learned-through output suppression for LEARNINGS_NO_PATCH non-empty delta",
)
}
}
// 10s — a failed API call must not disable the existing learnings
// The learn step is continue-on-error, and triage and edit read only the two prompt
// artifacts. So learn.mjs must write them before the first call that can throw.
{
console.log(" 10s — prompt artifacts survive an API failure")
const dir = mktemp("docs-sync-learn-s-")
const learningsPath = path.join(dir, "packages", "kilo-docs", "LEARNINGS.md")
fs.mkdirSync(path.dirname(learningsPath), { recursive: true })
const seeded = [
{
id: "seeded-rule",
rule: "Do not document features behind experimental flags.",
scope: "both",
source: "commit:aaaaaaa",
date: "2026-08-01",
},
]
fs.writeFileSync(learningsPath, renderLearnings(seeded))
// No DOCS_SYNC_FIXTURE and an empty GITHUB_REPOSITORY: repo() throws inside
// extract(). It stands for any API failure before the artifacts exist.
const failEnv = {
TRIAGE_MODEL: "test/model",
GITHUB_REPOSITORY: "",
GITHUB_OUTPUT: path.join(dir, "gh-output-s"),
GITHUB_STEP_SUMMARY: path.join(dir, "gh-summary-s"),
DOCS_SYNC_BACKOFF_MS: "0",
}
const result = runNodeScript(LEARN_SCRIPT, { cwd: dir, env: failEnv })
assert.notEqual(result.status, 0, "extraction must fail without GITHUB_REPOSITORY")
const triagePath = path.join(dir, "docs-sync-out", "learnings-triage.md")
const editPath = path.join(dir, "docs-sync-out", "learnings-edit.md")
for (const f of [triagePath, editPath]) {
assert.ok(fs.existsSync(f), `${path.basename(f)} must survive the failure`)
assert.ok(
fs.readFileSync(f, "utf8").includes("Do not document features behind experimental flags."),
`${path.basename(f)} must carry the checked-out rule`,
)
}
// An empty file must clear the stale block, not leave the earlier rule in place.
fs.writeFileSync(learningsPath, renderLearnings([]))
runNodeScript(LEARN_SCRIPT, { cwd: dir, env: failEnv })
assert.ok(!fs.existsSync(triagePath), "an empty learnings file must remove learnings-triage.md")
assert.ok(!fs.existsSync(editPath), "an empty learnings file must remove learnings-edit.md")
}
// 10t — the direct marker PATCH must not overwrite a concurrent body edit
// The body read at step 1 predates the extraction call, so learn.mjs must re-read
// the body immediately before the PATCH.
{
console.log(" 10t — marker PATCH preserves a concurrent body edit")
const dir = mktemp("docs-sync-learn-t-")
initRepoWithIdentity(dir)
fs.writeFileSync(path.join(dir, "base.txt"), "base\n")
gitIn(dir, ["add", "base.txt"])
gitIn(dir, ["commit", "-m", "base"])
gitIn(dir, ["checkout", "-b", "docs/auto-sync/cli"])
// github-actions[bot] authored the only branch commit, so there is no candidate
// correction and no model call. The run goes straight to the direct marker PATCH.
const learningsPath = path.join(dir, "packages", "kilo-docs", "LEARNINGS.md")
fs.mkdirSync(path.dirname(learningsPath), { recursive: true })
fs.writeFileSync(learningsPath, renderLearnings([]))
gitIn(dir, ["add", "packages/kilo-docs/LEARNINGS.md"])
gitIn(dir, ["commit", "-m", "seed learnings", "--author", `github-actions[bot] <${githubBotEmail}>`])
gitIn(dir, ["remote", "add", "origin", dir]) // learn.mjs fetches origin itself
const cwd = setupLearnRepo(dir, "docs/auto-sync/cli")
const tip = gitIn(dir, ["rev-parse", "HEAD"])
// Stub GitHub API. The second read of the pull request returns the maintainer edit.
const serverDir = mktemp("docs-sync-api-t-")
const portFile = path.join(serverDir, "port")
const patchFile = path.join(serverDir, "patch.json")
const serverScript = path.join(serverDir, "server.cjs")
fs.writeFileSync(
serverScript,
`const fs = require("node:fs")
const http = require("node:http")
let reads = 0
const json = (res, data) => {
res.writeHead(200, { "content-type": "application/json" })
res.end(JSON.stringify(data))
}
const server = http.createServer((req, res) => {
let raw = ""
req.on("data", (c) => (raw += c))
req.on("end", () => {
if (req.method === "PATCH") return fs.writeFileSync(process.env.PATCH_FILE, raw), json(res, {})
if (req.url.startsWith("/search/issues")) return json(res, { items: [{ number: 1 }] })
if (req.url.includes("/comments")) return json(res, [])
if (req.url.includes("/pulls/1")) {
const body = reads++ === 0 ? process.env.BODY_BEFORE : process.env.BODY_AFTER
return json(res, {
number: 1,
body,
head: { ref: "docs/auto-sync/cli" },
user: { login: "github-actions[bot]" },
})
}
json(res, {})
})
})
server.listen(0, "127.0.0.1", () => fs.writeFileSync(process.env.PORT_FILE, String(server.address().port)))
`,
)
const bodyBefore = "Rolling PR body.\n<!-- docs-sync: learned-through commit=old comment=none -->\n"
const humanEdit = "A maintainer edited the body while extraction ran."
const child = spawn(process.execPath, [serverScript], {
stdio: "ignore",
env: {
...process.env,
PORT_FILE: portFile,
PATCH_FILE: patchFile,
BODY_BEFORE: bodyBefore,
BODY_AFTER: bodyBefore + humanEdit + "\n",
},
})
try {
let port = ""
for (let i = 0; i < 100 && !port; i++) {
if (fs.existsSync(portFile)) port = fs.readFileSync(portFile, "utf8").trim()
else sleepSync(50)
}
assert.ok(port, "the stub API server must report a port")
const result = runNodeScript(LEARN_SCRIPT, {
cwd,
env: {
TRIAGE_MODEL: "test/model",
GITHUB_REPOSITORY: "acme/repo",
GH_TOKEN: "stub-token",
DOCS_SYNC_API_BASE: `http://127.0.0.1:${port}`,
GITHUB_OUTPUT: path.join(dir, "gh-output-t"),
GITHUB_STEP_SUMMARY: path.join(dir, "gh-summary-t"),
LEARNINGS_BUDGET_MINUTES: "1",
DOCS_SYNC_BACKOFF_MS: "0",
},
})
assert.equal(result.status, 0, `learn.mjs must succeed against the stub API: ${result.output}`)
assert.ok(fs.existsSync(patchFile), "the run must PATCH the pull request body")
const patchedBody = JSON.parse(fs.readFileSync(patchFile, "utf8")).body
assert.ok(patchedBody.includes(humanEdit), "the concurrent body edit must survive the marker PATCH")
assert.ok(patchedBody.includes(tip), "the PATCH must carry the new tip SHA")
assert.ok(!patchedBody.includes("commit=old"), "the old marker must be replaced")
assert.equal(
(patchedBody.match(/<!--\s*docs-sync:\s*learned-through/g) || []).length,
1,
"exactly one marker after the PATCH",
)
} finally {
child.kill()
}
}
// 10u — one model response cannot repeat an id or a rule
{
console.log(" 10u — duplicates inside one delta")
const base = { scope: "both", source: "commit:aaaaaaa", date: "2026-08-03" }
const ctx = { existing: [], candidateSources: ["commit:aaaaaaa"], deletedInWindow: [] }
const dupId = validateDelta(
{
add: [
{ ...base, id: "same-id", rule: "Do not document features behind experimental flags." },
{ ...base, id: "same-id", rule: "Keep the release notes short." },
],
remove: [],
},
ctx,
)
assert.equal(dupId.add.length, 1, "a repeated id must be rejected")
assert.equal(dupId.add[0].rule, "Do not document features behind experimental flags.")
assert.equal(dupId.rejected.length, 1)
assert.ok(dupId.rejected[0].reason.includes("earlier addition"), "the reason must name the earlier addition")
const dupText = validateDelta(
{
add: [
{ ...base, id: "rule-one", rule: "Do not document features behind experimental flags." },
{ ...base, id: "rule-two", rule: "Do not document features behind experimental flags!" },
],
remove: [],
},
ctx,
)
assert.equal(dupText.add.length, 1, "a repeated rule text must be rejected")
assert.equal(dupText.rejected.length, 1)
assert.ok(dupText.rejected[0].reason.includes("earlier addition"), "the reason must name the earlier addition")
}
// Prompt block format
{
console.log(" 10 — promptBlock format")
const entries = [
{ id: "r1", rule: "First rule.", scope: "triage", source: "commit:aaaaaaa", date: "2026-08-01" },
{ id: "r2", rule: "Second rule.", scope: "edit", source: "commit:bbbbbbb", date: "2026-08-02" },
{ id: "r3", rule: "Both rule.", scope: "both", source: "commit:ccccccc", date: "2026-08-03" },
]
const triageBlock = promptBlock(entries, "triage")
assert.ok(triageBlock.includes("First rule"), "triage block must include triage-scoped rule")
assert.ok(triageBlock.includes("Both rule"), "triage block must include both-scoped rule")
assert.ok(!triageBlock.includes("Second rule"), "triage block must not include edit-only rule")
assert.ok(triageBlock.includes("## Learnings from maintainer corrections"))
const editBlock = promptBlock(entries, "edit")
assert.ok(editBlock.includes("Second rule"), "edit block must include edit-scoped rule")
assert.ok(editBlock.includes("Both rule"), "edit block must include both-scoped rule")
assert.ok(!editBlock.includes("First rule"), "edit block must not include triage-only rule")
// Empty: no matching entries
const emptyBlock = promptBlock(
[{ id: "r1", rule: "R.", scope: "edit", source: "commit:aa", date: "2026-01-01" }],
"triage",
)
assert.equal(emptyBlock, "")
}
// parseLearnedThrough
{
console.log(" 10 — parseLearnedThrough")
const body = "stuff\n<!-- docs-sync: learned-through commit=abc1234 comment=2026-08-03T12:00:00Z -->\nmore"
const parsed = parseLearnedThrough(body)
assert.equal(parsed.commit, "abc1234")
assert.equal(parsed.comment, "2026-08-03T12:00:00Z")
// none values → null
const noneBody = "<!-- docs-sync: learned-through commit=none comment=none -->"
const noneParsed = parseLearnedThrough(noneBody)
assert.equal(noneParsed.commit, null)
assert.equal(noneParsed.comment, null)
// absent → both null
const absent = parseLearnedThrough("no marker")
assert.equal(absent.commit, null)
assert.equal(absent.comment, null)
}
// renderLearnings deterministic order
{
console.log(" 10 — renderLearnings deterministic order")
const entries = [
{ id: "b", rule: "B rule.", scope: "both", source: "commit:bb", date: "2026-08-02" },
{ id: "a", rule: "A rule.", scope: "both", source: "commit:aa", date: "2026-08-01" },
{ id: "c", rule: "C rule.", scope: "both", source: "commit:cc", date: "2026-08-01" },
]
const r1 = renderLearnings(entries)
const r2 = renderLearnings(entries)
assert.equal(r1, r2, "renderLearnings must be deterministic")
// Order: date ascending, then id ascending. So a before b before c (a.date < c.date, both before b)
const aPos = r1.indexOf("A rule")
const cPos = r1.indexOf("C rule")
const bPos = r1.indexOf("B rule")
assert.ok(aPos < cPos, "a (earlier date) must come before c")
assert.ok(cPos < bPos, "c (same date as a but later id) must come before b (later date)")
}
}
// Case 11 — created per-surface PRs get an assignee and a review request
function case11_prOwner() {
console.log("case 11 — per-surface assignee and reviewer calls")
const src = fs.readFileSync(UPSERT_SCRIPT, "utf8")
// The single rolling owner is gone; reviewers come from the surface map.
assert.ok(!src.includes("DOCS_OWNER"), "the single DOCS_OWNER constant must be removed")
assert.ok(src.includes("computeSurfaceReviewers"), "upsert must compute per-surface reviewers")
assert.ok(src.includes("groupBySurface"), "upsert must group docs changes by surface")
assert.equal(surfaceBranch("cli"), "docs/auto-sync/cli")
assert.equal(surfaceBranch("other"), "docs/auto-sync/other")
// Created PRs still POST assignees and requested_reviewers, best-effort.
assert.ok(src.includes("/assignees`, {"), "created PR must POST assignees")
assert.ok(src.includes("/requested_reviewers`, {"), "created PR must POST requested_reviewers")
const assignIdx = src.indexOf("/assignees`, {")
const reviewIdx = src.indexOf("/requested_reviewers`, {")
const tryIdx = src.lastIndexOf("try {", assignIdx)
const catchIdx = src.indexOf("} catch", assignIdx)
assert.ok(tryIdx >= 0 && catchIdx > reviewIdx, "both POSTs must sit in one try/catch")
// prepare-branch must ignore legacy rolling PRs entirely: it recognizes only
// per-surface branches, never comments on a legacy PR, and never closes one.
const prepSrc = fs.readFileSync(PREP_SCRIPT, "utf8")
assert.ok(prepSrc.includes("isSurfaceBranch"), "prepare-branch must recognize per-surface branches")
assert.ok(!prepSrc.includes('state: "closed"'), "prepare-branch must not close a legacy PR")
assert.ok(!prepSrc.includes("/issues/"), "prepare-branch must not comment on a legacy PR")
}
// ---------------------------------------------------------------------------
// Case 12/13 — per-surface segmentation against a stub API
// ---------------------------------------------------------------------------
const SURFACE_NOW = "2026-09-01T00:00:00.000Z"
const SURFACE_PREFIX = "docs/auto-sync/"
const GITHUB_BOT_EMAIL = "41898282+github-actions[bot]@users.noreply.github.com"
const SURFACE_PAGE_FILES = {
cli: "packages/kilo-docs/pages/getting-started/new.md",
vscode: "packages/kilo-docs/pages/code-with-ai/platforms/vscode/new.md",
gateway: "packages/kilo-docs/pages/gateway/new.md",
other: "packages/kilo-docs/pages/community/new.md",
}
// Stub GitHub API. Routes are narrow: search, one pull read, commits by path,
// collaborator permission, and the write endpoints we capture. No network.
const SURFACE_STUB_SERVER = `
const fs = require("node:fs")
const http = require("node:http")
const config = JSON.parse(fs.readFileSync(process.env.STUB_CONFIG_FILE, "utf8"))
let nextPr = 100
function log(entry) { fs.appendFileSync(process.env.STUB_LOG_FILE, JSON.stringify(entry) + "\\n") }
function send(res, status, data) {
res.writeHead(status, { "content-type": "application/json" })
res.end(JSON.stringify(data))
}
const server = http.createServer((req, res) => {
let raw = ""
req.on("data", (c) => (raw += c))
req.on("end", () => {
let body = null
if (raw) { try { body = JSON.parse(raw) } catch (e) { body = raw } }
const path = String(req.url).split("?")[0]
log({ method: req.method, url: req.url, body, auth: req.headers.authorization })
if (req.method === "GET" && path === "/search/issues") return send(res, 200, { items: config.openPrs || [] })
const pullMatch = path.match(/^\\/repos\\/[^/]+\\/[^/]+\\/pulls\\/(\\d+)$/)
if (req.method === "GET" && pullMatch) {
const n = Number(pullMatch[1])
const pr = (config.openPrs || []).find((p) => p.number === n) || {}
return send(res, 200, { number: n, head: { ref: pr.head || "" }, body: pr.body || "", html_url: "https://example.test/pull/" + n })
}
if (req.method === "GET" && /\\/pulls\\/\\d+\\/comments$/.test(path)) return send(res, 200, [])
if (req.method === "GET" && path.endsWith("/commits")) {
const query = String(req.url).split("?")[1] || ""
const m = query.match(/path=([^&]*)/)
const prefix = m ? decodeURIComponent(m[1]) : ""
// A repo-qualified key wins; a bare prefix keeps the existing cases working.
const repoMatch = path.match(/^\\/repos\\/(.+)\\/commits$/)
const key = repoMatch ? repoMatch[1] + "|" + prefix : prefix
if ((config.failCommits || []).includes(key) || (config.failCommits || []).includes(prefix)) return send(res, 403, { message: "Resource not accessible by integration" })
return send(res, 200, (config.commits || {})[key] ?? (config.commits || {})[prefix] ?? [])
}
if (req.method === "GET" && path.indexOf("/collaborators/") >= 0 && path.endsWith("/permission")) {
const login = decodeURIComponent(path.split("/collaborators/")[1].replace("/permission", ""))
return send(res, 200, { permission: (config.permissions || {})[login] || "write" })
}
if (req.method === "POST" && /\\/pulls$/.test(path)) {
const head = body && body.head
if (config.failHead && head === config.failHead) return send(res, 400, { message: "stub rejected " + head })
const number = nextPr++
const url = "https://example.test/pull/" + number
log({ kind: "create", head: head, number: number, url: url, body: body })
return send(res, 201, { number: number, html_url: url, head: { ref: head } })
}
if (req.method === "PATCH" && pullMatch) {
const n = Number(pullMatch[1])
if ((config.failPatch || []).includes(n)) return send(res, 422, { message: "stub rejected patch " + n })
return send(res, 200, { number: n, html_url: "https://example.test/pull/" + n })
}
if (req.method === "POST" && path.endsWith("/labels")) return send(res, 201, {})
if (req.method === "POST" && path.endsWith("/assignees")) return send(res, 200, {})
if (req.method === "POST" && path.endsWith("/requested_reviewers")) return send(res, 200, {})
send(res, 404, { message: "unhandled " + req.method + " " + req.url })
})
})
server.listen(0, "127.0.0.1", function () { fs.writeFileSync(process.env.STUB_PORT_FILE, String(server.address().port)) })
`
function stubCommit(login, daysAgo) {
const at = Date.parse(SURFACE_NOW) - daysAgo * 86_400_000
return { author: { login, type: "User" }, commit: { author: { date: new Date(at).toISOString() } } }
}
function startSurfaceStub(config) {
const dir = mktemp("docs-sync-stub-")
const portFile = path.join(dir, "port")
const logFile = path.join(dir, "requests.jsonl")
const configFile = path.join(dir, "config.json")
fs.writeFileSync(configFile, JSON.stringify(config, null, 2))
const script = path.join(dir, "server.cjs")
fs.writeFileSync(script, SURFACE_STUB_SERVER)
const child = spawn(process.execPath, [script], {
stdio: "ignore",
env: { ...process.env, STUB_PORT_FILE: portFile, STUB_LOG_FILE: logFile, STUB_CONFIG_FILE: configFile },
})
let port = ""
for (let i = 0; i < 200 && !port; i++) {
if (fs.existsSync(portFile)) port = fs.readFileSync(portFile, "utf8").trim()
else sleepSync(25)
}
if (!port) {
child.kill()
throw new Error("stub API did not report a port")
}
return { dir, logFile, port, child }
}
function readStubLog(logFile) {
if (!fs.existsSync(logFile)) return []
return fs
.readFileSync(logFile, "utf8")
.trim()
.split("\n")
.filter(Boolean)
.map((l) => JSON.parse(l))
}
// Temp repo with a bare origin, main carrying base docs, and the integration
// branch checked out with the working-tree docs changes that span two surfaces
// plus one unmapped file.
function setupSurfaceRepo() {
const root = mktemp("docs-sync-surface-")
const originDir = path.join(root, "origin.git")
gitIn(root, ["init", "--bare", "origin.git"])
const repoDir = path.join(root, "repo")
fs.mkdirSync(repoDir)
initRepoWithIdentity(repoDir)
for (const section of ["getting-started", "gateway", "community"]) {
const p = path.join(repoDir, "packages", "kilo-docs", "pages", section, "base.md")
fs.mkdirSync(path.dirname(p), { recursive: true })
fs.writeFileSync(p, "# base\n")
}
gitIn(repoDir, ["add", "packages/kilo-docs"])
gitIn(repoDir, ["commit", "-m", "base docs"])
gitIn(repoDir, ["remote", "add", "origin", originDir])
gitIn(repoDir, ["push", "-q", "origin", "main"])
// The integration branch is local; surface branches are pushed instead.
gitIn(repoDir, ["checkout", "-q", "-b", "docs/auto-sync"])
for (const file of Object.values(SURFACE_PAGE_FILES)) {
const p = path.join(repoDir, file)
fs.mkdirSync(path.dirname(p), { recursive: true })
fs.writeFileSync(p, "# new\n")
}
return { root, repoDir }
}
function runUpsert(repoDir, root, port, extraEnv = {}) {
return runNodeScript(UPSERT_SCRIPT, {
cwd: repoDir,
env: {
GITHUB_REPOSITORY: "acme/repo",
GH_TOKEN: "stub-token",
DOCS_SYNC_API_BASE: `http://127.0.0.1:${port}`,
PROCESSED_THROUGH: SURFACE_NOW,
SINCE: "2026-08-01T00:00:00.000Z",
VERIFIED: "true",
BRANCH: "docs/auto-sync",
PREP_MODE: "update",
GITHUB_OUTPUT: path.join(root, "gh-output"),
GITHUB_STEP_SUMMARY: path.join(root, "gh-summary"),
...extraEnv,
},
})
}
function case12_surfaceSegmentation() {
console.log("case 12 — per-surface segmentation")
const stub = startSurfaceStub({
openPrs: [],
commits: {
"packages/opencode/": [stubCommit("alice", 1), stubCommit("bob", 5)],
"packages/kilo-vscode/": [stubCommit("erin", 1), stubCommit("frank", 3)],
"packages/kilo-gateway/": [stubCommit("carol", 2), stubCommit("dave", 4)],
},
permissions: { alice: "write", bob: "write", erin: "write", frank: "write", carol: "write", dave: "write" },
})
try {
const { root, repoDir } = setupSurfaceRepo()
const result = runUpsert(repoDir, root, stub.port)
assert.equal(result.status, 0, `upsert must exit 0: ${result.output}`)
const entries = readStubLog(stub.logFile)
const creates = entries.filter((e) => e.kind === "create")
assert.equal(
creates.length,
4,
`exactly four PRs must be created; got ${JSON.stringify(creates)}\n${result.output}`,
)
const byHead = new Map(creates.map((c) => [c.head, c]))
assert.deepEqual([...byHead.keys()].sort(), [
`${SURFACE_PREFIX}cli`,
`${SURFACE_PREFIX}gateway`,
`${SURFACE_PREFIX}other`,
`${SURFACE_PREFIX}vscode`,
])
const assignees = entries.filter((e) => e.method === "POST" && e.url.endsWith("/assignees"))
const reviews = entries.filter((e) => e.method === "POST" && e.url.endsWith("/requested_reviewers"))
const pairFor = (name) => {
const create = byHead.get(`${SURFACE_PREFIX}${name}`)
const a = assignees.find((e) => e.url.includes(`/issues/${create.number}/`))
const r = reviews.find((e) => e.url.includes(`/pulls/${create.number}/`))
assert.ok(a, `assignee POST for ${name}`)
assert.ok(r, `reviewer POST for ${name}`)
return { assignees: a.body.assignees, reviewers: r.body.reviewers }
}
assert.deepEqual(pairFor("cli"), { assignees: ["alice", "bob"], reviewers: ["alice", "bob"] })
assert.deepEqual(pairFor("vscode"), { assignees: ["erin", "frank"], reviewers: ["erin", "frank"] })
assert.deepEqual(pairFor("gateway"), { assignees: ["carol", "dave"], reviewers: ["carol", "dave"] })
assert.deepEqual(pairFor("other"), {
assignees: ["lambertjosh", "intentionally-left-nil"],
reviewers: ["lambertjosh", "intentionally-left-nil"],
})
for (const create of creates) {
const body = create.body.body
assert.match(body, /Derived from the repository layout/, `derivation in ${create.head}`)
assert.match(body, /`packages\/kilo-docs\/pages\/community\/`/, `other paths in ${create.head}`)
assert.match(body, /\.github\/docs-sync\/surfaces\.json/, `map file in ${create.head}`)
}
assert.match(byHead.get(`${SURFACE_PREFIX}cli`).body.body, /half-life 180 days/)
assert.match(byHead.get(`${SURFACE_PREFIX}gateway`).body.body, /half-life 180 days/)
assert.match(byHead.get(`${SURFACE_PREFIX}other`).body.body, /fixed reviewers/i)
// Each surface branch carries exactly its own changed file.
const branchFiles = new Map()
for (const name of Object.keys(SURFACE_PAGE_FILES)) {
const head = `${SURFACE_PREFIX}${name}`
const diff = gitIn(repoDir, ["diff", "--name-only", "origin/main", `origin/${head}`])
branchFiles.set(name, diff.split("\n").filter(Boolean))
}
for (const [name, file] of Object.entries(SURFACE_PAGE_FILES)) {
assert.deepEqual(branchFiles.get(name), [file], `branch ${name} must carry only its own file`)
}
const flat = [...branchFiles.values()].flat()
assert.equal(flat.length, 4, "four changed files total")
assert.equal(new Set(flat).size, 4, "no changed file may appear in two PRs")
} finally {
stub.child.kill()
}
}
function case13_surfaceFailureIsolated() {
console.log("case 13 — one surface's failure is isolated")
const stub = startSurfaceStub({
openPrs: [],
failHead: `${SURFACE_PREFIX}cli`,
commits: { "packages/kilo-gateway/": [stubCommit("carol", 2), stubCommit("dave", 4)] },
permissions: { carol: "write", dave: "write" },
})
try {
const { root, repoDir } = setupSurfaceRepo()
const result = runUpsert(repoDir, root, stub.port)
assert.equal(result.status, 0, `run must exit 0 when one surface fails: ${result.output}`)
assert.match(result.output, /surface cli failed/, "the failure must be warned, not thrown")
const creates = readStubLog(stub.logFile).filter((e) => e.kind === "create")
assert.equal(creates.length, 3, "the other three surfaces must still create PRs")
assert.deepEqual([...new Set(creates.map((c) => c.head))].sort(), [
`${SURFACE_PREFIX}gateway`,
`${SURFACE_PREFIX}other`,
`${SURFACE_PREFIX}vscode`,
])
} finally {
stub.child.kill()
}
}
function case14_prepareBranchSurfaces() {
console.log("case 14 — prepare-branch merges surface branches without a ref collision")
const root = mktemp("docs-sync-prep-")
const originDir = path.join(root, "origin.git")
gitIn(root, ["init", "--bare", "origin.git"])
const repoDir = path.join(root, "repo")
fs.mkdirSync(repoDir)
initRepoWithIdentity(repoDir)
const write = (rel, text) => {
const p = path.join(repoDir, rel)
fs.mkdirSync(path.dirname(p), { recursive: true })
fs.writeFileSync(p, text)
}
write("packages/kilo-docs/pages/getting-started/base.md", "# base\n")
gitIn(repoDir, ["add", "packages/kilo-docs"])
gitIn(repoDir, ["commit", "-m", "base"])
gitIn(repoDir, ["remote", "add", "origin", originDir])
gitIn(repoDir, ["push", "-q", "origin", "main"])
// Durability integration branch.
gitIn(repoDir, ["checkout", "-q", "-b", "docs/auto-sync-integration"])
write("packages/kilo-docs/pages/getting-started/integ.md", "# integ\n")
gitIn(repoDir, ["add", "packages/kilo-docs"])
gitIn(repoDir, ["commit", "-m", "integ"])
gitIn(repoDir, ["push", "-q", "origin", "docs/auto-sync-integration"])
// Surface branch carrying a human commit.
gitIn(repoDir, ["checkout", "-q", "main"])
gitIn(repoDir, ["checkout", "-q", "-b", "docs/auto-sync/cli"])
write("packages/kilo-docs/pages/getting-started/human.md", "# human\n")
gitIn(repoDir, ["add", "packages/kilo-docs"])
gitIn(repoDir, ["commit", "-m", "human"])
gitIn(repoDir, ["push", "-q", "origin", "docs/auto-sync/cli"])
// actions/checkout leaves other branches as remote-tracking refs; drop the
// local surface branch so only the integration branch name is a local head.
gitIn(repoDir, ["checkout", "-q", "main"])
gitIn(repoDir, ["branch", "-D", "docs/auto-sync/cli"])
const stub = startSurfaceStub({ openPrs: [{ number: 7, head: "docs/auto-sync/cli", body: "" }] })
try {
const outputFile = path.join(root, "gh-output")
const result = runNodeScript(PREP_SCRIPT, {
cwd: repoDir,
env: {
GITHUB_REPOSITORY: "acme/repo",
GH_TOKEN: "stub-token",
DOCS_SYNC_API_BASE: `http://127.0.0.1:${stub.port}`,
GITHUB_OUTPUT: outputFile,
},
})
assert.equal(result.status, 0, `prepare-branch must exit 0: ${result.output}`)
const out = fs.readFileSync(outputFile, "utf8")
assert.match(out, /branch=docs\/auto-sync\n/, "integration branch output")
assert.match(out, /mode=update\n/, "mode output")
assert.ok(
fs.existsSync(path.join(repoDir, "packages/kilo-docs/pages/getting-started/human.md")),
"the surface branch's human commit must be merged into the integration tree",
)
assert.ok(
fs.existsSync(path.join(repoDir, "packages/kilo-docs/pages/getting-started/integ.md")),
"the durability content must be present",
)
} finally {
stub.child.kill()
}
}
function case15_surfaceUpdate() {
console.log("case 15 — updating an open surface PR preserves human commits")
const stub = startSurfaceStub({
openPrs: [
{
number: 42,
head: `${SURFACE_PREFIX}cli`,
body: "old body\n<!-- docs-sync:changes:start -->\n| old change | [acme#1](https://example.test/1) |\n<!-- docs-sync:changes:end -->\n",
},
],
commits: {
"packages/opencode/": [stubCommit("alice", 1), stubCommit("bob", 5)],
"packages/kilo-gateway/": [stubCommit("carol", 2), stubCommit("dave", 4)],
},
permissions: { alice: "write", bob: "write", carol: "write", dave: "write" },
})
try {
const { root, repoDir } = setupSurfaceRepo()
// A human commit on the open cli PR branch.
gitIn(repoDir, ["checkout", "-q", "-b", "human-tmp", "origin/main"])
const human = path.join(repoDir, "packages/kilo-docs/pages/getting-started/human.md")
fs.mkdirSync(path.dirname(human), { recursive: true })
fs.writeFileSync(human, "# human\n")
gitIn(repoDir, ["add", "packages/kilo-docs/pages/getting-started/human.md"])
gitIn(repoDir, ["commit", "-m", "human edit"])
gitIn(repoDir, ["push", "-q", "origin", "HEAD:refs/heads/docs/auto-sync/cli"])
gitIn(repoDir, ["checkout", "-q", "docs/auto-sync"])
gitIn(repoDir, ["branch", "-D", "human-tmp"])
const result = runUpsert(repoDir, root, stub.port)
assert.equal(result.status, 0, `upsert must exit 0: ${result.output}`)
const entries = readStubLog(stub.logFile)
const creates = entries.filter((e) => e.kind === "create")
assert.equal(creates.length, 3, "only the three other surfaces are created")
assert.ok(!creates.some((c) => c.head === `${SURFACE_PREFIX}cli`), "cli must be updated, not re-created")
const patched = entries.find((e) => e.method === "PATCH" && e.url.includes("/pulls/42"))
assert.ok(patched, "the open cli PR must be PATCHed")
assert.match(patched.body.body, /old change/, "existing rows must be carried forward")
// The updated cli branch keeps the human commit and adds the new cli file.
const diff = gitIn(repoDir, ["diff", "--name-only", "origin/main", `origin/${SURFACE_PREFIX}cli`])
assert.deepEqual(diff.split("\n").filter(Boolean).sort(), [
"packages/kilo-docs/pages/getting-started/human.md",
"packages/kilo-docs/pages/getting-started/new.md",
])
} finally {
stub.child.kill()
}
}
function case16_surfaceDeletion() {
console.log("case 16 — a deleted doc file reaches its surface PR")
const stub = startSurfaceStub({
openPrs: [],
commits: {
"packages/opencode/": [stubCommit("alice", 1)],
"packages/kilo-gateway/": [stubCommit("carol", 2), stubCommit("dave", 4)],
},
permissions: { alice: "write", carol: "write", dave: "write" },
})
try {
const { root, repoDir } = setupSurfaceRepo()
// The integration tree removes a page that still exists on origin/main.
const gone = path.join(repoDir, "packages/kilo-docs/pages/gateway/base.md")
assert.ok(fs.existsSync(gone), "fixture must start with the gateway base page")
fs.rmSync(gone)
gitIn(repoDir, ["rm", "-q", "packages/kilo-docs/pages/gateway/base.md"])
gitIn(repoDir, ["commit", "-m", "remove gateway base page"])
const result = runUpsert(repoDir, root, stub.port)
assert.equal(result.status, 0, `upsert must exit 0 with a deletion: ${result.output}`)
assert.ok(!/surface gateway failed/.test(result.output), `a deletion must not fail its surface: ${result.output}`)
const creates = readStubLog(stub.logFile).filter((e) => e.kind === "create")
const gateway = creates.find((c) => c.head === `${SURFACE_PREFIX}gateway`)
assert.ok(gateway, "the gateway surface PR must still be created")
const status = gitIn(repoDir, ["diff", "--name-status", "origin/main", `origin/${SURFACE_PREFIX}gateway`])
const lines = status.split("\n").filter(Boolean)
assert.ok(
lines.includes("D\tpackages/kilo-docs/pages/gateway/base.md"),
`the gateway branch must carry the deletion; got:\n${status}`,
)
assert.ok(
lines.includes("A\tpackages/kilo-docs/pages/gateway/new.md"),
`the gateway branch must still add its new page; got:\n${status}`,
)
} finally {
stub.child.kill()
}
}
// ---------------------------------------------------------------------------
// Case 17 — learnings read every open surface PR
// ---------------------------------------------------------------------------
function case17_learnAllSurfaces() {
console.log("case 17 — learnings cover every surface PR")
const humanBotEmail = "240665456+kiloconnect[bot]@users.noreply.github.com"
const dir = mktemp("docs-sync-learn-multi-")
initRepoWithIdentity(dir)
fs.writeFileSync(path.join(dir, "base.txt"), "base\n")
gitIn(dir, ["add", "base.txt"])
gitIn(dir, ["commit", "-m", "base"])
const addDoc = (rel, text) => {
const p = path.join(dir, rel)
fs.mkdirSync(path.dirname(p), { recursive: true })
fs.writeFileSync(p, text)
}
gitIn(dir, ["checkout", "-q", "-b", "docs/auto-sync/cli"])
addDoc("packages/kilo-docs/pages/cli.md", "# cli\n")
gitIn(dir, ["add", "packages/kilo-docs"])
gitIn(dir, ["commit", "-m", "cli edit", "--author", `kiloconnect[bot] <${humanBotEmail}>`])
const cliSha = gitIn(dir, ["rev-parse", "HEAD"])
gitIn(dir, ["checkout", "-q", "main"])
gitIn(dir, ["checkout", "-q", "-b", "docs/auto-sync/vscode"])
addDoc("packages/kilo-docs/pages/vscode.md", "# vscode\n")
gitIn(dir, ["add", "packages/kilo-docs"])
gitIn(dir, ["commit", "-m", "vscode edit", "--author", `kiloconnect[bot] <${humanBotEmail}>`])
const vscodeSha = gitIn(dir, ["rev-parse", "HEAD"])
// origin refs, as actions/checkout would leave them.
gitIn(dir, ["update-ref", "refs/remotes/origin/main", "main"])
gitIn(dir, ["update-ref", "refs/remotes/origin/docs/auto-sync/cli", "docs/auto-sync/cli"])
gitIn(dir, ["update-ref", "refs/remotes/origin/docs/auto-sync/vscode", "docs/auto-sync/vscode"])
fs.mkdirSync(path.join(dir, "docs-sync-out"), { recursive: true })
const prs = () => [
{ number: 1, head: { ref: "docs/auto-sync/cli" }, body: "", user: { login: "github-actions[bot]" } },
{ number: 2, head: { ref: "docs/auto-sync/vscode" }, body: "", user: { login: "github-actions[bot]" } },
]
// First run: both branches' corrections must reach the model input.
{
const fixturePath = path.join(dir, "fixture.json")
fs.writeFileSync(fixturePath, JSON.stringify({ prs: prs(), comments: [] }, null, 2))
const callLog = path.join(dir, "kilo-calls.log")
const kiloDir = makeStubKiloDir({ mode: "extraction-delta", callLog })
fs.writeFileSync(path.join(dir, "docs-sync-out", "extraction-delta.json"), JSON.stringify({ add: [], remove: [] }))
const result = runNodeScript(LEARN_SCRIPT, {
cwd: dir,
kiloDir,
env: {
TRIAGE_MODEL: "test/model",
DOCS_SYNC_FIXTURE: fixturePath,
LEARNINGS_BUDGET_MINUTES: "1",
DOCS_SYNC_BACKOFF_MS: "0",
},
})
assert.equal(result.status, 0, `multi-surface extraction must exit 0: ${result.output}`)
const input = JSON.parse(fs.readFileSync(path.join(dir, "docs-sync-out", "learnings-input.json"), "utf8"))
const sources = input.corrections.map((c) => c.source).sort()
assert.deepEqual(
sources,
[`commit:${cliSha.slice(0, 7)}`, `commit:${vscodeSha.slice(0, 7)}`].sort(),
`every surface branch's correction must be a candidate; got ${JSON.stringify(sources)}`,
)
const patched = fs.readFileSync(`${fixturePath}.patched`, "utf8")
assert.ok(
patched.includes(cliSha) && patched.includes(vscodeSha),
`the marker must list every learned tip; got ${patched}`,
)
}
// Second run: the union watermark covers both branches, so nothing is re-learned.
{
const marker = fs.readFileSync(path.join(dir, "fixture.json.patched"), "utf8")
const fixturePath = path.join(dir, "fixture2.json")
fs.writeFileSync(
fixturePath,
JSON.stringify({ prs: prs().map((p) => ({ ...p, body: marker })), comments: [] }, null, 2),
)
const callLog2 = path.join(dir, "kilo-calls2.log")
const kiloDir2 = makeStubKiloDir({ mode: "extraction-delta", callLog: callLog2 })
const result = runNodeScript(LEARN_SCRIPT, {
cwd: dir,
kiloDir: kiloDir2,
env: {
TRIAGE_MODEL: "test/model",
DOCS_SYNC_FIXTURE: fixturePath,
LEARNINGS_BUDGET_MINUTES: "1",
DOCS_SYNC_BACKOFF_MS: "0",
},
})
assert.equal(result.status, 0, `second multi-surface run must exit 0: ${result.output}`)
const calls = fs.existsSync(callLog2) ? fs.readFileSync(callLog2, "utf8").trim() : ""
assert.equal(calls, "", "a marker covering every tip must suppress the model call")
}
// A PR whose branch is gone must not block learning from the others.
{
const fixturePath = path.join(dir, "fixture3.json")
fs.writeFileSync(
fixturePath,
JSON.stringify(
{
prs: [
{ number: 3, head: { ref: "docs/auto-sync/gone" }, body: "", user: { login: "github-actions[bot]" } },
{ number: 4, head: { ref: "docs/auto-sync/cli" }, body: "", user: { login: "github-actions[bot]" } },
],
comments: [],
},
null,
2,
),
)
const callLog3 = path.join(dir, "kilo-calls3.log")
const kiloDir3 = makeStubKiloDir({ mode: "extraction-delta", callLog: callLog3 })
fs.writeFileSync(path.join(dir, "docs-sync-out", "extraction-delta.json"), JSON.stringify({ add: [], remove: [] }))
const result = runNodeScript(LEARN_SCRIPT, {
cwd: dir,
kiloDir: kiloDir3,
env: {
TRIAGE_MODEL: "test/model",
DOCS_SYNC_FIXTURE: fixturePath,
LEARNINGS_BUDGET_MINUTES: "1",
DOCS_SYNC_BACKOFF_MS: "0",
},
})
assert.equal(result.status, 0, `an unreadable branch must not fail the run: ${result.output}`)
const input = JSON.parse(fs.readFileSync(path.join(dir, "docs-sync-out", "learnings-input.json"), "utf8"))
assert.ok(
input.corrections.some((c) => c.source === `commit:${cliSha.slice(0, 7)}`),
"the readable branch must still be learned",
)
}
}
// ---------------------------------------------------------------------------
// Case 18 — a lingering legacy ref is deleted without an open legacy PR
// ---------------------------------------------------------------------------
function case18_legacyRefDeletedWithoutPr() {
console.log("case 18 — a lingering legacy ref is deleted without an open legacy PR")
const root = mktemp("docs-sync-prep-legacy-")
const originDir = path.join(root, "origin.git")
gitIn(root, ["init", "--bare", "origin.git"])
const repoDir = path.join(root, "repo")
fs.mkdirSync(repoDir)
initRepoWithIdentity(repoDir)
const write = (rel, text) => {
const p = path.join(repoDir, rel)
fs.mkdirSync(path.dirname(p), { recursive: true })
fs.writeFileSync(p, text)
}
write("packages/kilo-docs/pages/getting-started/base.md", "# base\n")
gitIn(repoDir, ["add", "packages/kilo-docs"])
gitIn(repoDir, ["commit", "-m", "base"])
gitIn(repoDir, ["remote", "add", "origin", originDir])
gitIn(repoDir, ["push", "-q", "origin", "main"])
gitIn(repoDir, ["checkout", "-q", "-b", "docs/auto-sync-integration"])
write("packages/kilo-docs/pages/getting-started/integ.md", "# integ\n")
gitIn(repoDir, ["add", "packages/kilo-docs"])
gitIn(repoDir, ["commit", "-m", "integ"])
gitIn(repoDir, ["push", "-q", "origin", "docs/auto-sync-integration"])
// A legacy docs/auto-sync branch lingers on origin with no open legacy PR.
// It must be pushed from a temp branch: origin (like a local repo) refuses to
// hold both `docs/auto-sync` and `docs/auto-sync/cli`.
gitIn(repoDir, ["checkout", "-q", "-b", "legacy-tmp", "main"])
write("packages/kilo-docs/pages/getting-started/legacy.md", "# legacy\n")
gitIn(repoDir, ["add", "packages/kilo-docs"])
gitIn(repoDir, ["commit", "-m", "legacy"])
const legacySha = gitIn(repoDir, ["rev-parse", "HEAD"])
gitIn(repoDir, ["push", "-q", "origin", "HEAD:refs/heads/docs/auto-sync"])
// actions/checkout leaves the remote-tracking ref for every origin branch.
gitIn(repoDir, ["update-ref", "refs/remotes/origin/docs/auto-sync", legacySha])
gitIn(repoDir, ["checkout", "-q", "main"])
gitIn(repoDir, ["branch", "-D", "legacy-tmp"])
const stub = startSurfaceStub({ openPrs: [{ number: 7, head: "docs/auto-sync/cli", body: "" }] })
try {
const outputFile = path.join(root, "gh-output")
const result = runNodeScript(PREP_SCRIPT, {
cwd: repoDir,
env: {
GITHUB_REPOSITORY: "acme/repo",
GH_TOKEN: "stub-token",
DOCS_SYNC_API_BASE: `http://127.0.0.1:${stub.port}`,
GITHUB_OUTPUT: outputFile,
},
})
assert.equal(result.status, 0, `prepare-branch must exit 0: ${result.output}`)
const remote = gitIn(repoDir, ["ls-remote", "--heads", "origin", "docs/auto-sync"])
assert.equal(remote, "", `the lingering legacy remote branch must be deleted; got:\n${remote}`)
assert.throws(
() => gitIn(repoDir, ["rev-parse", "--verify", "refs/remotes/origin/docs/auto-sync"]),
"the stale remote-tracking ref must be removed",
)
} finally {
stub.child.kill()
}
}
// ---------------------------------------------------------------------------
// Case 19 — watermark pick and processed-through refresh helpers
// ---------------------------------------------------------------------------
function case19_watermarkAndMarkerHelpers() {
console.log("case 19 — watermark pick and processed-through refresh")
const mk = (iso, number) => ({
number,
user: { login: "github-actions[bot]" },
body: `x\n<!-- docs-sync: processed-through ${iso} -->\n`,
})
assert.equal(pickWatermark([]), null)
assert.equal(
pickWatermark([
{ number: 1, user: { login: "human" }, body: "<!-- docs-sync: processed-through 2026-01-01T00:00:00.000Z -->" },
]),
null,
"an untrusted author's marker must be ignored",
)
// The search is updated-desc, so the first trusted entry is the latest run's
// marker even when a later entry carries a larger (stale) one.
const picked = pickWatermark([mk("2026-08-01T00:00:00.000Z", 2), mk("2026-09-01T00:00:00.000Z", 1)])
assert.equal(picked.number, 2)
assert.equal(picked.marker.toISOString(), "2026-08-01T00:00:00.000Z")
// patchProcessedThrough replaces in place, appends when absent, exactly one marker.
const replaced = patchProcessedThrough(
"body\n<!-- docs-sync: processed-through 2020-01-01T00:00:00.000Z -->\n",
"new",
)
assert.ok(replaced.includes("<!-- docs-sync: processed-through new -->"))
assert.ok(!replaced.includes("processed-through 2020"))
assert.equal((replaced.match(/<!--\s*docs-sync:\s*processed-through/g) || []).length, 1)
const appended = patchProcessedThrough("body only\n", "new")
assert.ok(appended.includes("<!-- docs-sync: processed-through new -->"))
// Anti-drift: the watermark query orders by updated, and upsert refreshes the
// marker on a skipped surface's open PR.
const wmSrc = fs.readFileSync(path.join(HERE, "watermark.mjs"), "utf8")
assert.ok(wmSrc.includes("sort:updated-desc"), "watermark query must sort by updated-desc")
const upsertSrc = fs.readFileSync(UPSERT_SCRIPT, "utf8")
assert.ok(
/patchProcessedThrough\(openPr\.body/.test(upsertSrc),
"upsert must refresh a skipped surface's processed-through marker",
)
}
// ---------------------------------------------------------------------------
// Case 20 — a skipped surface's open PR gets the current marker
// ---------------------------------------------------------------------------
function case20_upsertRefreshesSkippedMarker() {
console.log("case 20 — a skipped surface's open PR gets the current marker")
const stub = startSurfaceStub({
openPrs: [
{
number: 42,
head: `${SURFACE_PREFIX}gateway`,
body: "gateway body\n<!-- docs-sync: processed-through 2020-01-01T00:00:00.000Z -->\n<!-- docs-sync: learned-through commit=oldtip comment=none -->\n",
},
],
})
try {
const { root, repoDir } = setupSurfaceRepo()
// Make cli the only changed surface: the gateway PR has no changed files
// this run and would otherwise keep its stale markers.
for (const name of ["vscode", "gateway", "other"]) {
fs.rmSync(path.join(repoDir, SURFACE_PAGE_FILES[name]))
}
const learned = "<!-- docs-sync: learned-through commit=newtip comment=2026-09-01T00:00:00Z -->"
const result = runUpsert(repoDir, root, stub.port, { LEARNED_THROUGH: learned })
assert.equal(result.status, 0, `upsert must exit 0: ${result.output}`)
const patched = readStubLog(stub.logFile).find((e) => e.method === "PATCH" && e.url.includes("/pulls/42"))
assert.ok(patched, "the skipped gateway PR must be PATCHed with the current marker")
assert.ok(
patched.body.body.includes(`<!-- docs-sync: processed-through ${SURFACE_NOW} -->`),
`the skipped surface's marker must advance; got ${patched.body.body}`,
)
assert.ok(!patched.body.body.includes("processed-through 2020"), "the stale marker must be replaced")
assert.ok(patched.body.body.includes(learned), "the learned-through marker must also advance")
assert.ok(!patched.body.body.includes("commit=oldtip"), "the stale learned-through marker must be replaced")
} finally {
stub.child.kill()
}
}
// ---------------------------------------------------------------------------
// Case 21 — a fixture that spans two product surfaces plus one unmapped file
// ---------------------------------------------------------------------------
// The owner request's acceptance case: two product surfaces plus one unmapped
// file must produce exactly three PRs with the right assignees and reviewers,
// and no changed file may appear in two PRs. This case also prints the proof a
// PR body quotes: the computed surface map, the two assignees chosen per
// surface, the reviewers requested, and the title of each PR the run opens.
function case21_twoSurfacesOneUnmapped() {
console.log("case 21 — two product surfaces plus one unmapped file")
const stub = startSurfaceStub({
openPrs: [],
commits: {
"packages/opencode/": [stubCommit("alice", 1), stubCommit("bob", 5)],
"packages/kilo-vscode/": [stubCommit("erin", 1), stubCommit("frank", 3)],
},
permissions: { alice: "write", bob: "write", erin: "write", frank: "write" },
})
try {
const { root, repoDir } = setupSurfaceRepo()
// Narrow the fixture to the two product surfaces (cli, vscode) plus the
// unmapped community page: drop the third product surface's file.
fs.rmSync(path.join(repoDir, SURFACE_PAGE_FILES.gateway))
const result = runUpsert(repoDir, root, stub.port)
assert.equal(result.status, 0, `upsert must exit 0: ${result.output}`)
const entries = readStubLog(stub.logFile)
const creates = entries.filter((e) => e.kind === "create")
assert.equal(
creates.length,
3,
`exactly three PRs must be created; got ${JSON.stringify(creates)}\n${result.output}`,
)
const byHead = new Map(creates.map((c) => [c.head, c]))
assert.deepEqual([...byHead.keys()].sort(), [
`${SURFACE_PREFIX}cli`,
`${SURFACE_PREFIX}other`,
`${SURFACE_PREFIX}vscode`,
])
const assignees = entries.filter((e) => e.method === "POST" && e.url.endsWith("/assignees"))
const reviews = entries.filter((e) => e.method === "POST" && e.url.endsWith("/requested_reviewers"))
const pairFor = (name) => {
const create = byHead.get(`${SURFACE_PREFIX}${name}`)
const a = assignees.find((e) => e.url.includes(`/issues/${create.number}/`))
const r = reviews.find((e) => e.url.includes(`/pulls/${create.number}/`))
assert.ok(a, `assignee POST for ${name}`)
assert.ok(r, `reviewer POST for ${name}`)
return { assignees: a.body.assignees, reviewers: r.body.reviewers }
}
assert.deepEqual(pairFor("cli"), { assignees: ["alice", "bob"], reviewers: ["alice", "bob"] })
assert.deepEqual(pairFor("vscode"), { assignees: ["erin", "frank"], reviewers: ["erin", "frank"] })
assert.deepEqual(pairFor("other"), {
assignees: ["lambertjosh", "intentionally-left-nil"],
reviewers: ["lambertjosh", "intentionally-left-nil"],
})
assert.match(byHead.get(`${SURFACE_PREFIX}other`).body.body, /fixed reviewers/i)
// Each surface branch carries exactly its own changed file; no file twice.
const branchFiles = new Map()
for (const name of ["cli", "vscode", "other"]) {
const head = `${SURFACE_PREFIX}${name}`
const diff = gitIn(repoDir, ["diff", "--name-only", "origin/main", `origin/${head}`])
branchFiles.set(name, diff.split("\n").filter(Boolean))
}
assert.deepEqual(branchFiles.get("cli"), [SURFACE_PAGE_FILES.cli])
assert.deepEqual(branchFiles.get("vscode"), [SURFACE_PAGE_FILES.vscode])
assert.deepEqual(branchFiles.get("other"), [SURFACE_PAGE_FILES.other])
const flat = [...branchFiles.values()].flat()
assert.equal(flat.length, 3, "three changed files total")
assert.equal(new Set(flat).size, 3, "no changed file may appear in two PRs")
// The proof a PR body quotes, from this run.
console.log(" computed surface map (changed file -> surface):")
for (const [name, files] of branchFiles) {
for (const file of files) console.log(` ${file} -> ${name}`)
}
for (const name of ["cli", "vscode", "other"]) {
const create = byHead.get(`${SURFACE_PREFIX}${name}`)
const pair = pairFor(name)
console.log(` pull request: ${create.head}`)
console.log(` title: ${create.body.title}`)
console.log(` assignees: ${pair.assignees.join(", ")}`)
console.log(` requested reviewers: ${pair.reviewers.join(", ")}`)
}
} finally {
stub.child.kill()
}
}
// Write a docs page that routes to a cloud surface. The mobile page
// (packages/kilo-docs/pages/code-with-ai/platforms/mobile.md) routes to
// cloud-mobile, whose reviewers come from Kilo-Org/cloud history.
function addCloudPage(repoDir, rel) {
const p = path.join(repoDir, rel)
fs.mkdirSync(path.dirname(p), { recursive: true })
fs.writeFileSync(p, "# new\n")
}
const MOBILE_PAGE = "packages/kilo-docs/pages/code-with-ai/platforms/mobile.md"
// The create entry plus the assignee/reviewer POSTs for one surface head.
function createdPair(entries, head) {
const create = entries.filter((e) => e.kind === "create").find((c) => c.head === head)
assert.ok(create, `${head} PR must be created`)
const a = entries.find(
(e) => e.method === "POST" && e.url.endsWith("/assignees") && e.url.includes(`/issues/${create.number}/`),
)
const r = entries.find(
(e) => e.method === "POST" && e.url.endsWith("/requested_reviewers") && e.url.includes(`/pulls/${create.number}/`),
)
assert.ok(a, `assignee POST for ${head}`)
assert.ok(r, `reviewer POST for ${head}`)
return { create, assignees: a.body.assignees, reviewers: r.body.reviewers }
}
function case22_cloudSurfacePrFromCloudHistory() {
console.log("case 22 — a cloud docs page ranks reviewers from the cloud repo")
const stub = startSurfaceStub({
openPrs: [],
commits: { "Kilo-Org/cloud|apps/mobile/": [stubCommit("nina", 1), stubCommit("omar", 2)] },
permissions: { nina: "write", omar: "write" },
})
try {
const { root, repoDir } = setupSurfaceRepo()
addCloudPage(repoDir, MOBILE_PAGE)
const result = runUpsert(repoDir, root, stub.port, { CLOUD_REPO_TOKEN: "cloud-token" })
assert.equal(result.status, 0, `upsert must exit 0: ${result.output}`)
const entries = readStubLog(stub.logFile)
const history = entries.find(
(e) => e.method === "GET" && e.url.startsWith("/repos/Kilo-Org/cloud/commits?path=apps%2Fmobile%2F"),
)
assert.ok(history, `cloud history must be read through the API; got ${JSON.stringify(entries.map((e) => e.url))}`)
assert.equal(history.auth, "Bearer cloud-token", "the cloud history must be read with the cloud token")
const { create, assignees, reviewers } = createdPair(entries, `${SURFACE_PREFIX}cloud-mobile`)
assert.deepEqual(assignees, ["nina", "omar"], "cloud-mobile assignees come from cloud-repo history")
assert.deepEqual(reviewers, ["nina", "omar"], "cloud-mobile reviewers come from cloud-repo history")
assert.match(create.body.body, /ranked from `Kilo-Org\/cloud` git history over `apps\/mobile\/`/)
assert.ok(create.body.body.includes("Kilo-Org/cloud"), "the PR body must name the cloud repo")
assert.ok(create.body.body.includes("CROSS_REPO_ACCESS_TOKEN"), "the PR body must name the cloud token secret")
console.log(` cloud-mobile PR reviewers: ${reviewers.join(", ")}`)
} finally {
stub.child.kill()
}
}
function case23_cloudHistoryUnreachableFallsBack() {
console.log("case 23 — an unreachable cloud history still opens the surface PR")
const stub = startSurfaceStub({
openPrs: [],
commits: { "Kilo-Org/cloud|apps/mobile/": [stubCommit("nina", 1)] },
permissions: { nina: "write" },
failCommits: ["Kilo-Org/cloud|apps/mobile/"],
})
try {
const { root, repoDir } = setupSurfaceRepo()
addCloudPage(repoDir, MOBILE_PAGE)
const result = runUpsert(repoDir, root, stub.port, { CLOUD_REPO_TOKEN: "cloud-token" })
assert.equal(result.status, 0, `run must exit 0: ${result.output}`)
const { create, reviewers } = createdPair(readStubLog(stub.logFile), `${SURFACE_PREFIX}cloud-mobile`)
assert.deepEqual(
reviewers,
["lambertjosh", "intentionally-left-nil"],
"an unreachable cloud history must fall back to the fixed other pair",
)
assert.match(create.body.body, /fixed `other` reviewers/i, "the body must name the fallback")
assert.ok(create.body.body.includes("CROSS_REPO_ACCESS_TOKEN"), "the body must name the required secret")
assert.ok(create.body.body.includes("Kilo-Org/cloud"), "the body must name the cloud repo")
console.log(` unreachable cloud history -> reviewers: ${reviewers.join(", ")}`)
} finally {
stub.child.kill()
}
}
// ---------------------------------------------------------------------------
// Case 24 — a dated legacy PR and its branch are ignored end to end
// ---------------------------------------------------------------------------
function case24_legacyDatedPrIgnored() {
console.log("case 24 — a dated legacy auto-sync PR is ignored, not reused or counted")
const root = mktemp("docs-sync-legacy-dated-")
const originDir = path.join(root, "origin.git")
gitIn(root, ["init", "--bare", "origin.git"])
const repoDir = path.join(root, "repo")
fs.mkdirSync(repoDir)
initRepoWithIdentity(repoDir)
const write = (rel, text) => {
const p = path.join(repoDir, rel)
fs.mkdirSync(path.dirname(p), { recursive: true })
fs.writeFileSync(p, text)
}
write("packages/kilo-docs/pages/getting-started/base.md", "# base\n")
gitIn(repoDir, ["add", "packages/kilo-docs"])
gitIn(repoDir, ["commit", "-m", "base"])
gitIn(repoDir, ["remote", "add", "origin", originDir])
gitIn(repoDir, ["push", "-q", "origin", "main"])
// Durability integration branch: the base prepare-branch must choose.
gitIn(repoDir, ["checkout", "-q", "-b", "docs/auto-sync-integration"])
write("packages/kilo-docs/pages/getting-started/integ.md", "# integ\n")
gitIn(repoDir, ["add", "packages/kilo-docs"])
gitIn(repoDir, ["commit", "-m", "integ"])
gitIn(repoDir, ["push", "-q", "origin", "docs/auto-sync-integration"])
// A real surface branch and its open PR.
gitIn(repoDir, ["checkout", "-q", "-b", "docs/auto-sync/cli", "main"])
write("packages/kilo-docs/pages/getting-started/cli-surface.md", "# cli surface\n")
gitIn(repoDir, ["add", "packages/kilo-docs"])
gitIn(repoDir, ["commit", "-m", "cli surface"])
gitIn(repoDir, ["push", "-q", "origin", "docs/auto-sync/cli"])
// A dated legacy branch carrying a file that is not on main.
gitIn(repoDir, ["checkout", "-q", "-b", "legacy-tmp", "main"])
write("packages/kilo-docs/pages/getting-started/dated.md", "# dated legacy\n")
gitIn(repoDir, ["add", "packages/kilo-docs"])
gitIn(repoDir, ["commit", "-m", "dated legacy"])
gitIn(repoDir, ["push", "-q", "origin", "HEAD:refs/heads/docs/auto-sync-2026-09-11"])
gitIn(repoDir, ["checkout", "-q", "main"])
gitIn(repoDir, ["branch", "-D", "legacy-tmp"])
// A ref may not be a path prefix of another, so the local surface branch must
// be gone for prepare-branch to check out the bare integration ref.
gitIn(repoDir, ["branch", "-D", "docs/auto-sync/cli"])
const openPrs = [
{ number: 14043, head: "docs/auto-sync-2026-09-11", body: "" },
{ number: 77, head: "docs/auto-sync/cli", body: "" },
]
const prepStub = startSurfaceStub({ openPrs })
try {
const outputFile = path.join(root, "gh-output")
const result = runNodeScript(PREP_SCRIPT, {
cwd: repoDir,
env: {
GITHUB_REPOSITORY: "acme/repo",
GH_TOKEN: "stub-token",
DOCS_SYNC_API_BASE: `http://127.0.0.1:${prepStub.port}`,
GITHUB_OUTPUT: outputFile,
},
})
assert.equal(result.status, 0, `prepare-branch must exit 0: ${result.output}`)
const out = fs.readFileSync(outputFile, "utf8")
assert.match(out, /branch=docs\/auto-sync\n/, "integration branch output")
assert.match(out, /mode=update\n/, "mode output")
const prepLog = readStubLog(prepStub.logFile)
assert.ok(
!prepLog.some((e) => e.url.includes("/issues/14043/comments")),
"prepare-branch must not comment on the dated legacy PR",
)
assert.ok(
!prepLog.some((e) => e.method === "PATCH" && e.url.includes("/pulls/14043")),
"prepare-branch must not close the dated legacy PR",
)
assert.ok(
!fs.existsSync(path.join(repoDir, "packages/kilo-docs/pages/getting-started/dated.md")),
"the dated branch's file must not be reused as an integration base",
)
assert.ok(
fs.existsSync(path.join(repoDir, "packages/kilo-docs/pages/getting-started/cli-surface.md")),
"the cli surface branch must be merged into the integration tree",
)
} finally {
prepStub.child.kill()
}
// Upsert against only the dated legacy PR still open: the dated head is not
// counted as an existing surface PR, so the cli surface gets a fresh PR.
write("packages/kilo-docs/pages/getting-started/new.md", "# new\n")
const upsertStub = startSurfaceStub({ openPrs: [{ number: 14043, head: "docs/auto-sync-2026-09-11", body: "" }] })
try {
const result = runUpsert(repoDir, root, upsertStub.port)
assert.equal(result.status, 0, `upsert must exit 0: ${result.output}`)
const log = readStubLog(upsertStub.logFile)
assert.ok(
!log.some((e) => e.method === "PATCH" && e.url.includes("/pulls/14043")),
"upsert must not PATCH the dated legacy PR",
)
const creates = log.filter((e) => e.kind === "create")
assert.ok(
creates.some((c) => c.head === `${SURFACE_PREFIX}cli`),
`a fresh cli surface PR must be created; got ${JSON.stringify(creates.map((c) => c.head))}`,
)
} finally {
upsertStub.child.kill()
}
}
// ---------------------------------------------------------------------------
// Case 25 — only `docs/auto-sync/<surface>` heads are surface branches
// ---------------------------------------------------------------------------
function case25_surfaceBranchRecognized() {
console.log("case 25 — surface branches are recognized, dated and bare refs are not")
assert.equal(isSurfaceBranch("docs/auto-sync/cli"), true, "a surface branch is recognized")
assert.equal(surfaceNameFromBranch("docs/auto-sync/vscode"), "vscode", "the surface name is derived")
assert.equal(isSurfaceBranch("docs/auto-sync-2026-09-11"), false, "a dated legacy branch is not a surface")
assert.equal(isSurfaceBranch("docs/auto-sync"), false, "the bare integration ref is not a surface")
assert.equal(isSurfaceBranch("docs/auto-sync/"), false, "an empty surface name is not a surface")
console.log(" dated and bare auto-sync refs are not surface branches")
}
// ---------------------------------------------------------------------------
// Case 26/27 — learn.mjs against the live API path
// ---------------------------------------------------------------------------
/**
* Repo with a bare origin, `main`, and one bot-authored commit per branch. The
* only commits past main are the sync job's own, so every branch yields no
* candidate correction and the run takes the direct marker-PATCH route.
*/
function setupLearnLiveRepo(branches) {
const root = mktemp("docs-sync-learn-live-")
const originDir = path.join(root, "origin.git")
gitIn(root, ["init", "--bare", "origin.git"])
const repoDir = path.join(root, "repo")
fs.mkdirSync(repoDir)
initRepoWithIdentity(repoDir)
const write = (rel, text) => {
const p = path.join(repoDir, rel)
fs.mkdirSync(path.dirname(p), { recursive: true })
fs.writeFileSync(p, text)
}
write("packages/kilo-docs/LEARNINGS.md", renderLearnings([]))
gitIn(repoDir, ["add", "packages/kilo-docs"])
gitIn(repoDir, ["commit", "-m", "base"])
gitIn(repoDir, ["remote", "add", "origin", originDir])
gitIn(repoDir, ["push", "-q", "origin", "main"])
gitIn(repoDir, ["update-ref", "refs/remotes/origin/main", "main"])
for (const branch of branches) {
gitIn(repoDir, ["checkout", "-q", "-b", branch, "main"])
write(`packages/kilo-docs/pages/${branch.replaceAll("/", "-")}.md`, `# ${branch}\n`)
gitIn(repoDir, ["add", "packages/kilo-docs"])
gitIn(repoDir, ["commit", "-m", `docs ${branch}`, "--author", `github-actions[bot] <${GITHUB_BOT_EMAIL}>`])
gitIn(repoDir, ["push", "-q", "origin", branch])
gitIn(repoDir, ["update-ref", `refs/remotes/origin/${branch}`, branch])
gitIn(repoDir, ["checkout", "-q", "main"])
gitIn(repoDir, ["branch", "-D", branch])
}
fs.mkdirSync(path.join(repoDir, "docs-sync-out"), { recursive: true })
return { root, repoDir }
}
function runLearnLive(repoDir, root, port) {
return runNodeScript(LEARN_SCRIPT, {
cwd: repoDir,
env: {
TRIAGE_MODEL: "test/model",
GITHUB_REPOSITORY: "acme/repo",
GH_TOKEN: "stub-token",
DOCS_SYNC_API_BASE: `http://127.0.0.1:${port}`,
GITHUB_OUTPUT: path.join(root, "gh-output"),
GITHUB_STEP_SUMMARY: path.join(root, "gh-summary"),
LEARNINGS_BUDGET_MINUTES: "1",
DOCS_SYNC_BACKOFF_MS: "0",
},
})
}
// ---------------------------------------------------------------------------
// Case 26 — a legacy dated auto-sync PR is not a learnings target
// ---------------------------------------------------------------------------
function case26_learnIgnoresLegacyPr() {
console.log("case 26 — learn.mjs ignores a legacy dated auto-sync PR")
const { root, repoDir } = setupLearnLiveRepo(["docs/auto-sync/cli", "docs/auto-sync-2026-09-11"])
const stub = startSurfaceStub({
openPrs: [
{ number: 14043, head: "docs/auto-sync-2026-09-11", body: "" },
{ number: 77, head: "docs/auto-sync/cli", body: "" },
],
})
try {
const result = runLearnLive(repoDir, root, stub.port)
assert.equal(result.status, 0, `learn.mjs must exit 0: ${result.output}`)
const log = readStubLog(stub.logFile)
assert.ok(
!log.some((e) => e.url.includes("/pulls/14043/comments")),
"the legacy dated PR must not be read as a learning target",
)
assert.ok(
!log.some((e) => e.method === "PATCH" && e.url.includes("/pulls/14043")),
"the legacy dated PR body must not be PATCHed",
)
assert.ok(
log.some((e) => e.method === "PATCH" && e.url.includes("/pulls/77")),
"the surface PR must still get its marker refreshed",
)
} finally {
stub.child.kill()
}
}
// ---------------------------------------------------------------------------
// Case 27 — one failed marker PATCH does not skip the remaining targets
// ---------------------------------------------------------------------------
function case27_patchFailureIsolated() {
console.log("case 27 — one failed marker PATCH does not abort the rest")
const { root, repoDir } = setupLearnLiveRepo(["docs/auto-sync/cli", "docs/auto-sync/vscode"])
const stub = startSurfaceStub({
openPrs: [
{ number: 1, head: "docs/auto-sync/cli", body: "" },
{ number: 2, head: "docs/auto-sync/vscode", body: "" },
],
failPatch: [1],
})
try {
const result = runLearnLive(repoDir, root, stub.port)
assert.equal(result.status, 0, `a failed marker PATCH must not fail the run: ${result.output}`)
const log = readStubLog(stub.logFile)
assert.ok(
log.some((e) => e.method === "PATCH" && e.url.includes("/pulls/2")),
"the remaining target's marker must still be published",
)
} finally {
stub.child.kill()
}
}
// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------
function main() {
const cases = [
case1_mergeOrFallback,
case2_defectB,
case2b_autoFlag,
case2c_stderrLogAlways,
case2d_redactEnvSecrets,
case2e_prefixSecretOrdering,
case2f_redactStdout,
case2g_redactStream,
case2h_pendingCauseIsReadable,
case2i_budgetsFitWork,
case3_watermark,
case4_routing,
case5_recollection,
case6_budgets,
case7_cap,
case8_triage,
case9_reverts,
case10_learnings,
case11_prOwner,
case12_surfaceSegmentation,
case13_surfaceFailureIsolated,
case14_prepareBranchSurfaces,
case15_surfaceUpdate,
case16_surfaceDeletion,
case17_learnAllSurfaces,
case18_legacyRefDeletedWithoutPr,
case19_watermarkAndMarkerHelpers,
case20_upsertRefreshesSkippedMarker,
case21_twoSurfacesOneUnmapped,
case22_cloudSurfacePrFromCloudHistory,
case23_cloudHistoryUnreachableFallsBack,
case24_legacyDatedPrIgnored,
case25_surfaceBranchRecognized,
case26_learnIgnoresLegacyPr,
case27_patchFailureIsolated,
]
let failed = 0
for (const fn of cases) {
try {
fn()
console.log(` ok: ${fn.name}`)
} catch (err) {
failed++
console.error(` FAIL: ${fn.name}`)
console.error(err)
} finally {
cleanup()
}
}
if (failed > 0) {
console.error(`\nselftest: ${failed} case(s) failed`)
process.exit(1)
}
console.log("\nselftest: all cases passed")
}
main()