Files
kilocode/.github/docs-sync/selftest.mjs
T
Igor Šćekić 6ec20f2395 fix(docs-sync): address review findings on the learnings step (#12834)
Three defects from the review of #12823.

- A throw after the learn step started left no prompt artifact, so triage and
  edit ran with no learned rule at all. The step is continue-on-error, so the
  run continued and the failure was silent. learn.mjs now writes both prompt
  artifacts from the checked-out file before any fallible work, and replaces
  them once the rolling branch copy loads.
- The direct marker PATCH sent the body read before the extraction call, so it
  overwrote any body edit made in the minutes since. learn.mjs now re-reads the
  body immediately before the PATCH.
- Two additions in one model response could carry one id or one rule text.
  validateDelta now rejects a duplicate of an earlier accepted addition.

Tests: 10s pins the prompt artifacts across a failed API call and the removal of
a stale block. 10t drives learn.mjs against a stub GitHub API whose second read
returns a maintainer edit, and asserts the edit survives the PATCH. 10u pins
both duplicate rejections. DOCS_SYNC_API_BASE is the new selftest-only hook that
points lib.mjs at the stub server.
2026-08-03 21:51:45 +00:00

3500 lines
134 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 { applyCap } from "./watermark.mjs"
import {
computeUncovered,
computeProcessedThrough,
routeRows,
dropLegacySkipped,
noDiffReport,
LEARNINGS_FILE,
nonContentFiles,
resolveLearnedThrough,
renderBody,
extractSectionRows,
} 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 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) {
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/docs/auto-sync", "docs/auto-sync"])
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 at function scope (let prBody before the if block)
const prBodyIdx = upsertSrc.indexOf('let prBody = ""')
assert.ok(prBodyIdx >= 0, 'prBody must be declared at function scope with let prBody = ""')
const ifIdx = upsertSrc.indexOf('if (mode === "update"')
assert.ok(prBodyIdx < ifIdx, 'let prBody must appear before if (mode === "update"...)')
// (c) renderBody({ argument object contains learnedThrough
const renderBodyIdx = upsertSrc.indexOf("const body = renderBody({")
assert.ok(renderBodyIdx >= 0, "renderBody call must exist")
const afterRenderBody = upsertSrc.slice(renderBodyIdx)
const renderBodyArgsEnd = afterRenderBody.indexOf("})")
const renderBodyArgs = afterRenderBody.slice(0, renderBodyArgsEnd)
assert.ok(renderBodyArgs.includes("learnedThrough"), "renderBody call in main() must pass learnedThrough")
}
// 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"])
// 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)
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" },
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 — the created rolling PR gets an assignee and a review request
function case11_prOwner() {
console.log("case 11 — created PR assignee and reviewer")
const src = fs.readFileSync(path.join(HERE, "upsert-pr.mjs"), "utf8")
assert.ok(/const DOCS_OWNER = "\S+"/.test(src), "DOCS_OWNER must be a module constant")
assert.ok(src.includes("/assignees`, {"), "created PR must POST assignees")
assert.ok(src.includes("/requested_reviewers`, {"), "created PR must POST requested_reviewers")
// Both calls belong to the create arm, after the PR exists.
const createIdx = src.indexOf("const pr = await api(`/repos/${repo()}/pulls`")
assert.ok(createIdx >= 0, "create-PR call must exist")
assert.ok(src.indexOf("/assignees`, {") > createIdx, "assignees POST must follow PR creation")
assert.ok(src.indexOf("/requested_reviewers`, {") > createIdx, "reviewer POST must follow PR creation")
// A failure here must not fail the run — the PR is already open.
const ownerIdx = src.indexOf("/assignees`, {")
const tryIdx = src.lastIndexOf("try {", ownerIdx)
const catchIdx = src.indexOf("} catch", ownerIdx)
assert.ok(tryIdx >= 0 && catchIdx > src.indexOf("/requested_reviewers`, {"), "both POSTs must sit in one try/catch")
}
// ---------------------------------------------------------------------------
// 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,
]
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()