mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Integrated into release/v3.8.45
This commit is contained in:
committed by
GitHub
parent
7a098a0d85
commit
bb62c2a618
@@ -540,7 +540,7 @@ the stale-enforcement added in Fase 6A.3.
|
||||
18. Every bug fix must be validated before shipping: a failing-then-passing unit/integration test (TDD) OR a documented live test on the production VPS (192.168.0.15). A fix without either is not merged. See Testing → "Bug fix / issue triage protocol" for the full decision tree.
|
||||
19. Never develop on the shared main checkout. Every development task runs in its own git worktree on its own dedicated branch, and you MUST confirm the base branch with the operator (e.g. via `AskUserQuestion`) before creating the worktree/branch — never assume `main` or the currently checked-out branch. A `git checkout` in the shared checkout silently destroys other sessions' uncommitted work. Tear down only the worktrees/branches you created (by name, never `fix/*`/`feat/*` wildcards), leave other sessions' worktrees untouched, and end on the branch you started on (the active `release/vX.Y.Z`, never `main`). See Git Workflow → "Worktree isolation".
|
||||
20. PII redaction/sanitization is **opt-in — never on by default**. OmniRoute proxies for self-hosted/local LLMs where the operator owns the data, so mutating request/response payloads by default would silently corrupt legitimate traffic. The two data-mutating PII feature flags **MUST** keep `defaultValue: "false"` in `src/shared/constants/featureFlagDefinitions.ts`: `PII_REDACTION_ENABLED` (request-side) and `PII_RESPONSE_SANITIZATION` (response + streaming). All three application points — `src/lib/guardrails/piiMasker.ts` (request guardrail), `src/lib/piiSanitizer.ts` (response), `src/lib/streamingPiiTransform.ts` (SSE) — are gated on these flags; with both off the `pii-masker` guardrail still runs but never mutates payloads (data passes through untouched). Flipping either default to `"true"` requires explicit operator approval. The regression guard is `tests/unit/pii-opt-in-default.test.ts` (asserts both definition defaults + behavioral pass-through). Opt-in is per-operator via env or the settings/DB override (`src/lib/db/featureFlags.ts`), never a silent default. See `docs/security/GUARDRAILS.md`.
|
||||
21. **Release-freeze — the release branch is frozen to campaign merges while a `/generate-release` is running.** `/generate-release` opens a marker issue labeled `release-freeze` at the start of reconciliation (Phase 0a) and closes it once the release PR squash-merges to `main`. Before merging **any** PR into the active `release/vX.Y.Z` branch, every campaign workflow (`/review-issues`, `/review-prs`, `/implement-features`, `/green-prs`, `/port-upstream-*`) **MUST** check `gh issue list --repo diegosouzapw/OmniRoute --label release-freeze --state open` — if a freeze is active, **HOLD the merge** (leave the PR ready and open; do NOT merge to the release branch), tell the operator, and resume once the freeze lifts. This is a **coordination signal, not a permission lock**: the release captain and the campaign sessions share the `diegosouzapw` identity, so a GitHub branch-protection lock cannot distinguish them — only this honored marker prevents the mid-release commit races that forced full CHANGELOG re-reconciliation in v3.8.40/v3.8.41 (a parallel campaign advanced `release/vX.Y.Z` by 34 commits mid-run). The release captain's own reconciliation/cycle-open pushes are exempt — they _are_ the release. Fixes that must land during a freeze (a homologation finding) follow the post-merge read-only rule: land on `main` first via `fix/release-vX.Y.Z-*`. **⛔ ONLY `/generate-release` may raise a release-freeze, and ONLY at its Phase 0a (start of generating a new version) — lifted at Phase 12c after the squash-merge to `main`.** No campaign, session, or agent may open a `release-freeze` marker at any other time — a freeze is **never** a mid-development coordination tool. If a session ever believes a freeze is genuinely, unavoidably necessary outside the `/generate-release` flow, it **MUST first ask the operator (`diegosouzapw`) in chat, explicitly alert "estou criando um freeze" and get an explicit yes** — never open, extend, or re-open a `release-freeze` autonomously. Conversely, do **not** close/lift an active `/generate-release` freeze to unblock campaign merges: it protects the captain's single clean CI run and auto-lifts at Phase 12c — closing it early re-triggers the exact commit race it prevents. Verify a freeze is legitimate before acting on it: an open `release-freeze` whose title/body references an **OPEN** release PR (`gh pr view <N> --json state`) is the authorized captain freeze — hold, don't touch.
|
||||
21. **Release-freeze — the FROZEN release branch belongs to the release captain; development does NOT stop (parallel-cycle model, 2026-07-04).** `/generate-release` opens a marker issue labeled `release-freeze` at the start of reconciliation (Phase 0a), **immediately cuts the next cycle's branch `release/vX+1` from the frozen tip (Phase 0a.0b — bump + living release PR + re-home of open PRs)**, and closes the freeze once the release PR squash-merges to `main`. Before merging **any** PR, every campaign workflow (`/review-issues`, `/review-prs`, `/implement-features`, `/green-prs`, `/port-upstream-*`) **MUST** check `gh issue list --repo diegosouzapw/OmniRoute --label release-freeze --state open` — if a freeze is active: **NEVER merge into the frozen `release/vX.Y.Z` named in the freeze title**; instead resolve the ACTIVE development branch (the **highest** `release/v*` by semver — normally `release/vX+1`, announced in a freeze-issue comment) and **retarget the PR there** (`gh pr edit <N> --base release/vX+1`, then VERIFY with `gh pr view <N> --json baseRefName` — the edit fails silently) and merge normally. **HOLD only when the highest release/v\* branch IS the frozen one** (the short window before 0a.0b completes, or a pre-parallel-cycle release) — in that case leave the PR ready and open, tell the operator, and resume when the next branch appears or the freeze lifts. The just-shipped fixes reach `release/vX+1` via the Phase 5 sync-back (`scripts/release/sync-next-cycle.mjs`); do not try to sync mid-release. This is a **coordination signal, not a permission lock**: the release captain and the campaign sessions share the `diegosouzapw` identity, so a GitHub branch-protection lock cannot distinguish them — only this honored marker prevents the mid-release commit races that forced full CHANGELOG re-reconciliation in v3.8.40/v3.8.41 (a parallel campaign advanced `release/vX.Y.Z` by 34 commits mid-run). The release captain's own reconciliation/cycle-open pushes are exempt — they _are_ the release. Fixes that must land during a freeze (a homologation finding) follow the post-merge read-only rule: land on `main` first via `fix/release-vX.Y.Z-*`. **⛔ ONLY `/generate-release` may raise a release-freeze, and ONLY at its Phase 0a (start of generating a new version) — lifted at Phase 12c after the squash-merge to `main`.** No campaign, session, or agent may open a `release-freeze` marker at any other time — a freeze is **never** a mid-development coordination tool. If a session ever believes a freeze is genuinely, unavoidably necessary outside the `/generate-release` flow, it **MUST first ask the operator (`diegosouzapw`) in chat, explicitly alert "estou criando um freeze" and get an explicit yes** — never open, extend, or re-open a `release-freeze` autonomously. Conversely, do **not** close/lift an active `/generate-release` freeze to unblock campaign merges: it protects the captain's single clean CI run and auto-lifts at Phase 12c — closing it early re-triggers the exact commit race it prevents. Verify a freeze is legitimate before acting on it: an open `release-freeze` whose title/body references an **OPEN** release PR (`gh pr view <N> --json state`) is the authorized captain freeze — hold, don't touch.
|
||||
22. **Cross-session safety — this repo is worked by MANY parallel sessions/agents at once; never step on another's in-flight work.** Two absolute bans, both recurring incidents (this rule exists because they keep happening):
|
||||
- **(a) Never `git stash` / `git stash pop` — ANYWHERE in this repo, including inside an isolated worktree, and including inside any subagent you dispatch.** `git stash` operates on the **shared repository object store**, not the per-worktree working tree — so a stash pushed or popped in one session can silently clobber or resurrect another parallel session's uncommitted changes. This is not hypothetical: 2026-07-02 a `#5923` quotaCache change leaked into the unrelated `#2296` worktree via a global `stash pop`, and the same class reincided through a **subagent**. To compare working changes against a base ref **without** stashing, use `git show <ref>:<path>` or `git diff <ref> -- <path>`; to confirm a typecheck/lint error is pre-existing on the base, inspect the base ref directly (`git show origin/release/vX.Y.Z:<path>`) — never stash your tree away to "get it clean". **Put this ban verbatim in the prompt of every subagent that touches git** (agents don't inherit this file's context — the recurrence was a subagent).
|
||||
- **(b) Never merge, push, rebase, or force-push a PR / branch / worktree that another session is actively working.** An open PR whose head is a live fix worktree in `.claude/worktrees/` you did **not** create (e.g. `fix-5852`/`fix-5923` carrying fresh commits, even when they share your `diegosouzapw` identity), or any branch another session owns, is **off-limits — HOLD**, and let the owning session merge it. **Before** merging or pushing to any PR you did not create *this* session, run `git worktree list` to check for a matching in-flight worktree and re-check `gh pr view <N> --json state,headRefOid`. Only the owning session merges its own in-flight PR; mid-flight merges race the owner and re-trigger the exact commit/CHANGELOG races Rule #19 and Rule #21 guard against. (Reinforces Rule #19.)
|
||||
|
||||
195
scripts/release/sync-next-cycle.mjs
Normal file
195
scripts/release/sync-next-cycle.mjs
Normal file
@@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env node
|
||||
// scripts/release/sync-next-cycle.mjs
|
||||
//
|
||||
// Parallel-cycle sync-back (generate-release Phase 5, step 20).
|
||||
// Merges origin/main (which carries the just-shipped release's closing fixes +
|
||||
// finalized CHANGELOG) into the live next-cycle branch release/v<NEXT> that was
|
||||
// cut at the freeze (Phase 0a.0b) — resolving CHANGELOG.md with the anti-eat
|
||||
// protocol: main's CHANGELOG wins VERBATIM, and the next cycle's own
|
||||
// `## [<NEXT>] — TBD` section (with any bullets it already accumulated) is
|
||||
// re-inserted on top. Design: _tasks/release-flow/2026-07-04_proposta-ciclo-paralelo-v2.md
|
||||
//
|
||||
// Usage: node scripts/release/sync-next-cycle.mjs <nextVersion> e.g. 3.8.45
|
||||
//
|
||||
// Idempotent: safe to re-run after resolving any conflicts it could not
|
||||
// auto-resolve (it detects an in-progress merge in its worktree and resumes).
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
/**
|
||||
* Pure: insert (or replace) the next cycle's section into main's CHANGELOG.
|
||||
* - `mainChangelog`: the full CHANGELOG.md content from origin/main (wins verbatim).
|
||||
* - `nextSection`: the `## [<next>] — …` section text captured from the cycle
|
||||
* branch BEFORE the merge (header line through the line before the next `## [`
|
||||
* heading), or null/empty when the cycle has no section yet (a fresh `— TBD`
|
||||
* placeholder is synthesized).
|
||||
* The section lands right after the `## [Unreleased]` block (mirroring the
|
||||
* layout every cycle-open produces), before the first `## [x.y.z]` heading.
|
||||
*/
|
||||
export function insertNextSection(mainChangelog, nextSection, nextVersion) {
|
||||
const lines = mainChangelog.split("\n");
|
||||
const isVersionHeading = (l) => /^## \[\d+\.\d+\.\d+\]/.test(l);
|
||||
|
||||
// Drop any pre-existing copy of the next section from main's content (it
|
||||
// normally has none — main only learns about <next> at the NEXT release).
|
||||
const startIdx = lines.findIndex((l) => l.startsWith(`## [${nextVersion}]`));
|
||||
if (startIdx !== -1) {
|
||||
let endIdx = lines.length;
|
||||
for (let i = startIdx + 1; i < lines.length; i++) {
|
||||
if (isVersionHeading(lines[i]) || lines[i].startsWith("## [Unreleased]")) {
|
||||
endIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
lines.splice(startIdx, endIdx - startIdx);
|
||||
}
|
||||
|
||||
const section =
|
||||
nextSection && nextSection.trim().length > 0
|
||||
? nextSection.replace(/\s+$/, "")
|
||||
: `## [${nextVersion}] — TBD`;
|
||||
|
||||
// Insert before the first released-version heading.
|
||||
const firstVersionIdx = lines.findIndex(isVersionHeading);
|
||||
const insertAt = firstVersionIdx === -1 ? lines.length : firstVersionIdx;
|
||||
lines.splice(insertAt, 0, ...section.split("\n"), "", "---", "");
|
||||
|
||||
// Collapse accidental duplicate separators introduced by the splice.
|
||||
return lines
|
||||
.join("\n")
|
||||
.replace(/\n---\n\n---\n/g, "\n---\n")
|
||||
.replace(/\n{4,}/g, "\n\n\n");
|
||||
}
|
||||
|
||||
/** Pure: extract the `## [<version>]` section (header included, next heading excluded). */
|
||||
export function extractSection(changelog, version) {
|
||||
const lines = changelog.split("\n");
|
||||
const start = lines.findIndex((l) => l.startsWith(`## [${version}]`));
|
||||
if (start === -1) return null;
|
||||
let end = lines.length;
|
||||
for (let i = start + 1; i < lines.length; i++) {
|
||||
if (/^## \[/.test(lines[i])) {
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Trim a trailing `---` separator so re-insertion controls its own framing.
|
||||
let sectionLines = lines.slice(start, end);
|
||||
while (
|
||||
sectionLines.length &&
|
||||
(sectionLines[sectionLines.length - 1].trim() === "---" ||
|
||||
sectionLines[sectionLines.length - 1].trim() === "")
|
||||
) {
|
||||
sectionLines.pop();
|
||||
}
|
||||
return sectionLines.join("\n");
|
||||
}
|
||||
|
||||
function git(args, opts = {}) {
|
||||
return execFileSync("git", args, { encoding: "utf8", ...opts }).trim();
|
||||
}
|
||||
|
||||
function main() {
|
||||
const NEXT = process.argv[2];
|
||||
if (!/^\d+\.\d+\.\d+$/.test(NEXT || "")) {
|
||||
console.error("usage: node scripts/release/sync-next-cycle.mjs <nextVersion> (e.g. 3.8.45)");
|
||||
process.exit(2);
|
||||
}
|
||||
const ROOT = git(["rev-parse", "--show-toplevel"]);
|
||||
const BRANCH = `release/v${NEXT}`;
|
||||
const WT = path.join(ROOT, ".claude", "worktrees", `sync-next-${NEXT}`);
|
||||
|
||||
git(["fetch", "origin", "main", BRANCH], { cwd: ROOT, stdio: ["ignore", "pipe", "pipe"] });
|
||||
const prevVersion = JSON.parse(git(["show", "origin/main:package.json"], { cwd: ROOT })).version;
|
||||
console.log(`[sync-next-cycle] main is v${prevVersion} → syncing into ${BRANCH}`);
|
||||
|
||||
// Worktree (idempotent — reuse if a previous run left it for conflict resolution).
|
||||
if (!fs.existsSync(WT)) {
|
||||
git(["worktree", "add", WT, BRANCH], { cwd: ROOT, stdio: ["ignore", "pipe", "pipe"] });
|
||||
}
|
||||
const mergeHeadPath = git(["rev-parse", "--path-format=absolute", "--git-path", "MERGE_HEAD"], {
|
||||
cwd: WT,
|
||||
});
|
||||
const mergeInProgress = fs.existsSync(mergeHeadPath);
|
||||
if (!mergeInProgress) {
|
||||
git(["pull", "--ff-only", "origin", BRANCH], { cwd: WT, stdio: ["ignore", "pipe", "pipe"] });
|
||||
}
|
||||
|
||||
// Capture the cycle's own section BEFORE the merge touches the file.
|
||||
const cycleChangelog = fs.readFileSync(path.join(WT, "CHANGELOG.md"), "utf8");
|
||||
const nextSection = extractSection(cycleChangelog, NEXT);
|
||||
|
||||
if (!mergeInProgress) {
|
||||
try {
|
||||
execFileSync("git", ["merge", "--no-commit", "--no-ff", "origin/main"], {
|
||||
cwd: WT,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
} catch {
|
||||
// Conflicts are expected (CHANGELOG at minimum) — handled below.
|
||||
}
|
||||
}
|
||||
|
||||
// Anti-CHANGELOG-eat: main's CHANGELOG verbatim + the cycle's section on top.
|
||||
const mainChangelog = git(["show", "origin/main:CHANGELOG.md"], { cwd: WT });
|
||||
const merged = insertNextSection(mainChangelog + "\n", nextSection, NEXT);
|
||||
// Assertions: main's latest section intact + next section present.
|
||||
if (!merged.includes(`## [${prevVersion}]`)) {
|
||||
console.error(`[sync-next-cycle] ABORT: main's ## [${prevVersion}] section missing after re-insertion`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!merged.includes(`## [${NEXT}]`)) {
|
||||
console.error(`[sync-next-cycle] ABORT: ## [${NEXT}] section missing after re-insertion`);
|
||||
process.exit(1);
|
||||
}
|
||||
fs.writeFileSync(path.join(WT, "CHANGELOG.md"), merged);
|
||||
git(["add", "CHANGELOG.md"], { cwd: WT });
|
||||
|
||||
// i18n mirrors: regenerate instead of merging them one by one.
|
||||
const mirrors = git(["diff", "--name-only", "--diff-filter=U"], { cwd: WT })
|
||||
.split("\n")
|
||||
.filter((f) => f.startsWith("docs/i18n/") && f.endsWith("CHANGELOG.md"));
|
||||
for (const m of mirrors) {
|
||||
execFileSync("git", ["checkout", "origin/main", "--", m], { cwd: WT });
|
||||
execFileSync("git", ["add", m], { cwd: WT });
|
||||
}
|
||||
try {
|
||||
execFileSync("npm", ["run", "release:sync-changelog-i18n", "--", NEXT, prevVersion], {
|
||||
cwd: WT,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
execFileSync("git", ["add", "-A", "docs/i18n"], { cwd: WT });
|
||||
} catch (e) {
|
||||
console.warn("[sync-next-cycle] i18n mirror resync failed (resolve manually):", e.message);
|
||||
}
|
||||
|
||||
// Anything still conflicted is for the human (migrations, lockfile, code).
|
||||
const unresolved = git(["diff", "--name-only", "--diff-filter=U"], { cwd: WT })
|
||||
.split("\n")
|
||||
.filter(Boolean);
|
||||
if (unresolved.length) {
|
||||
console.error(
|
||||
`[sync-next-cycle] ${unresolved.length} conflict(s) need manual resolution in ${WT}:\n` +
|
||||
unresolved.map((f) => ` ✗ ${f}`).join("\n") +
|
||||
`\n → resolve + git add, then re-run this script (it resumes the merge).` +
|
||||
`\n → migrations: renumber per the cross-PR collision precedent; lockfile: npm install.`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
git(["commit", "-m", `chore(release): sync main (v${prevVersion} close) into ${BRANCH} — parallel-cycle sync-back`], { cwd: WT });
|
||||
git(["push", "origin", BRANCH], { cwd: WT });
|
||||
|
||||
const left = git(["rev-list", "--count", `${BRANCH}..origin/main`], { cwd: WT });
|
||||
console.log(`[sync-next-cycle] pushed. origin/main commits not in ${BRANCH}: ${left} (expected 0)`);
|
||||
|
||||
git(["worktree", "remove", "--force", WT], { cwd: ROOT });
|
||||
console.log("[sync-next-cycle] done — worktree removed.");
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] || "").href) main();
|
||||
@@ -39,15 +39,20 @@
|
||||
"incremental": true,
|
||||
"incrementalFile": "reports/mutation/stryker-incremental.json",
|
||||
"testRunner": "tap",
|
||||
"plugins": ["@stryker-mutator/tap-runner"],
|
||||
"plugins": [
|
||||
"@stryker-mutator/tap-runner"
|
||||
],
|
||||
"tap": {
|
||||
"testFiles": [
|
||||
"tests/unit/account-fallback-anthropic-quota.test.ts",
|
||||
"tests/unit/account-fallback-retry-after-json.test.ts",
|
||||
"tests/unit/account-fallback-route-restriction-403.test.ts",
|
||||
"tests/unit/account-fallback-service.test.ts",
|
||||
"tests/unit/accountfallback-ratelimit-400-4976.test.ts",
|
||||
"tests/unit/antigravity-429-quota-tdd.test.ts",
|
||||
"tests/unit/api-key-rotator-health.test.ts",
|
||||
"tests/unit/appearance-widget-settings-schema.test.ts",
|
||||
"tests/unit/auth-antigravity-account-retry-v2.test.ts",
|
||||
"tests/unit/auth-clear-account-error.test.ts",
|
||||
"tests/unit/auth-disable-cooling-2997.test.ts",
|
||||
"tests/unit/auth-extract-api-key.test.ts",
|
||||
@@ -55,6 +60,9 @@
|
||||
"tests/unit/auth-ollama-cloud-per-model-403-3027.test.ts",
|
||||
"tests/unit/auth-opencode-zen-noauth-fallback.test.ts",
|
||||
"tests/unit/auth-terminal-status.test.ts",
|
||||
"tests/unit/authz/discovery-routes-local-only.test.ts",
|
||||
"tests/unit/authz/route-guard-local-prefix.test.ts",
|
||||
"tests/unit/authz/route-guard-version-get-exemption.test.ts",
|
||||
"tests/unit/authz/routeGuard.test.ts",
|
||||
"tests/unit/auto-combo-context-advertising.test.ts",
|
||||
"tests/unit/auto-combo-engine.test.ts",
|
||||
@@ -62,6 +70,7 @@
|
||||
"tests/unit/build/check-circular-deps.test.ts",
|
||||
"tests/unit/cache-sweeps.test.ts",
|
||||
"tests/unit/cc-compatible-provider.test.ts",
|
||||
"tests/unit/chat-combo-live-test.test.ts",
|
||||
"tests/unit/chat-context-relay.test.ts",
|
||||
"tests/unit/chat-cooldown-aware-retry.test.ts",
|
||||
"tests/unit/chat-helpers.test.ts",
|
||||
@@ -69,6 +78,7 @@
|
||||
"tests/unit/chat-route-edge-cases.test.ts",
|
||||
"tests/unit/chatcore-compression-integration.test.ts",
|
||||
"tests/unit/chatcore-executor-helpers.test.ts",
|
||||
"tests/unit/chatcore-executor-proxy.test.ts",
|
||||
"tests/unit/chatcore-extracted-modules-3821.test.ts",
|
||||
"tests/unit/chatcore-headers.test.ts",
|
||||
"tests/unit/chatcore-imports-cleanly.test.ts",
|
||||
@@ -77,15 +87,20 @@
|
||||
"tests/unit/chatcore-memory-skills-injection.test.ts",
|
||||
"tests/unit/chatcore-non-streaming-sse.test.ts",
|
||||
"tests/unit/chatcore-passthrough-tool-names.test.ts",
|
||||
"tests/unit/chatcore-request-format.test.ts",
|
||||
"tests/unit/chatcore-sanitization.test.ts",
|
||||
"tests/unit/chatcore-semantic-cache-store.test.ts",
|
||||
"tests/unit/chatcore-semantic-cache.test.ts",
|
||||
"tests/unit/chatcore-strip-stale-headers.test.ts",
|
||||
"tests/unit/chatcore-telemetry-helpers.test.ts",
|
||||
"tests/unit/chatcore-translation-paths.test.ts",
|
||||
"tests/unit/chatcore-upstream-timeouts.test.ts",
|
||||
"tests/unit/check-error-helper.test.ts",
|
||||
"tests/unit/check-route-guard-membership.test.ts",
|
||||
"tests/unit/check-test-discovery.test.ts",
|
||||
"tests/unit/circuit-breaker-failure-kind.test.ts",
|
||||
"tests/unit/circuit-breaker-registry-cap.test.ts",
|
||||
"tests/unit/circuit-breaker-stream-controller-4602.test.ts",
|
||||
"tests/unit/claude-code-parity.test.ts",
|
||||
"tests/unit/claude-effort-suffix-strip.test.ts",
|
||||
"tests/unit/claude-oauth-provider.test.ts",
|
||||
@@ -99,33 +114,45 @@
|
||||
"tests/unit/codex-stream-false.test.ts",
|
||||
"tests/unit/collect-metrics-module-coverage.test.ts",
|
||||
"tests/unit/combo-499-abort.test.ts",
|
||||
"tests/unit/combo-account-allowlist-3266.test.ts",
|
||||
"tests/unit/combo-auto-candidate-expansion.test.ts",
|
||||
"tests/unit/combo-cache-invalidation.test.ts",
|
||||
"tests/unit/combo-config.test.ts",
|
||||
"tests/unit/combo-context-relay.test.ts",
|
||||
"tests/unit/combo-headroom-strategy.test.ts",
|
||||
"tests/unit/combo-health-autopilot.test.ts",
|
||||
"tests/unit/combo-health-dashboard.test.ts",
|
||||
"tests/unit/combo-health-route.test.ts",
|
||||
"tests/unit/combo-hedging.test.ts",
|
||||
"tests/unit/combo-max-depth-config.test.ts",
|
||||
"tests/unit/combo-model-lockout-honors-reset-1308.test.ts",
|
||||
"tests/unit/combo-omnimodel-tag-stripping.test.ts",
|
||||
"tests/unit/combo-param-validation-fallback-4519.test.ts",
|
||||
"tests/unit/combo-prescreen.test.ts",
|
||||
"tests/unit/combo-priority-quota-exhaustion-cutoff-5923.test.ts",
|
||||
"tests/unit/combo-provider-cooldown.test.ts",
|
||||
"tests/unit/combo-provider-diversity-wiring.test.ts",
|
||||
"tests/unit/combo-quality-validator-reasoning.test.ts",
|
||||
"tests/unit/combo-priority-quota-exhaustion-cutoff-5923.test.ts",
|
||||
"tests/unit/combo-quota-share-cooldown-wait.test.ts",
|
||||
"tests/unit/combo-quota-soft-penalty.test.ts",
|
||||
"tests/unit/combo-round-robin-streaming-lock-3811.test.ts",
|
||||
"tests/unit/combo-routing-engine.test.ts",
|
||||
"tests/unit/combo-scoring-inspector.test.ts",
|
||||
"tests/unit/combo-selected-connection-success.test.ts",
|
||||
"tests/unit/combo-sessionless-pin-3825.test.ts",
|
||||
"tests/unit/combo-strategies.test.ts",
|
||||
"tests/unit/combo-strategy-fallbacks.test.ts",
|
||||
"tests/unit/combo-stream-readiness-fallback.test.ts",
|
||||
"tests/unit/combo-streaming-empty-content-failover.test.ts",
|
||||
"tests/unit/combo-strict-random-distribution-3959.test.ts",
|
||||
"tests/unit/combo-target-defensive-modelstr.test.ts",
|
||||
"tests/unit/combo/auto-quota-cutoff.test.ts",
|
||||
"tests/unit/combo/auto-status-penalty-4540.test.ts",
|
||||
"tests/unit/combo/combo-exhausted-skip.test.ts",
|
||||
"tests/unit/combo/effective-max-concurrency.test.ts",
|
||||
"tests/unit/complexity-aware-scoring-wiring.test.ts",
|
||||
"tests/unit/context-pinning-tool-calls.test.ts",
|
||||
"tests/unit/cooldown-epoch-string-3954.test.ts",
|
||||
"tests/unit/correctness/combo.property.test.ts",
|
||||
"tests/unit/correctness/sanitizers.property.test.ts",
|
||||
"tests/unit/custom-model-target-format.test.ts",
|
||||
@@ -136,17 +163,29 @@
|
||||
"tests/unit/error-message-sanitization.test.ts",
|
||||
"tests/unit/executor-antigravity.test.ts",
|
||||
"tests/unit/executor-web-cookie-sweep.test.ts",
|
||||
"tests/unit/format-provider-error-cause.test.ts",
|
||||
"tests/unit/gemini-web-missing-browser-3516.test.ts",
|
||||
"tests/unit/grok-cli-oauth.test.ts",
|
||||
"tests/unit/guardrails-api-3496.test.ts",
|
||||
"tests/unit/headroom-proxy-lifecycle.test.ts",
|
||||
"tests/unit/livews-forward-backoff-4604.test.ts",
|
||||
"tests/unit/management-auth-hardening.test.ts",
|
||||
"tests/unit/mark-account-unavailable-numeric-epoch-guard.test.ts",
|
||||
"tests/unit/memory-embedding-remote.test.ts",
|
||||
"tests/unit/memory-embedding-transformers.test.ts",
|
||||
"tests/unit/middleware-header-strip-5849.test.ts",
|
||||
"tests/unit/model-cooldowns-route-auth.test.ts",
|
||||
"tests/unit/model-cooldowns-route.test.ts",
|
||||
"tests/unit/model-lockout-decay.test.ts",
|
||||
"tests/unit/model-lockout-max-cooldown.test.ts",
|
||||
"tests/unit/no-memory-header.test.ts",
|
||||
"tests/unit/non-streaming-sse-terminal-typescan-4459.test.ts",
|
||||
"tests/unit/oauth-providers-config.test.ts",
|
||||
"tests/unit/oauth-redirect-uri-mismatch.test.ts",
|
||||
"tests/unit/observability-fase04.test.ts",
|
||||
"tests/unit/observability-payloads.test.ts",
|
||||
"tests/unit/openapi-security-tiers.test.ts",
|
||||
"tests/unit/persist-429-cooldown-account-fallback.test.ts",
|
||||
"tests/unit/plan3-p0.test.ts",
|
||||
"tests/unit/plugin-sandbox-permissions.test.ts",
|
||||
"tests/unit/plugins-route-error-sanitization.test.ts",
|
||||
@@ -164,6 +203,7 @@
|
||||
"tests/unit/quota-streaming-consumption-usd.test.ts",
|
||||
"tests/unit/rate-limit-enhanced.test.ts",
|
||||
"tests/unit/rate-limit-manager.test.ts",
|
||||
"tests/unit/rate-limit-queue-timeout-lockout.test.ts",
|
||||
"tests/unit/responses-handler.test.ts",
|
||||
"tests/unit/route-explainability.test.ts",
|
||||
"tests/unit/route-guard-plugins-local-only.test.ts",
|
||||
@@ -172,11 +212,13 @@
|
||||
"tests/unit/router-strategies.test.ts",
|
||||
"tests/unit/service-combo-metrics.test.ts",
|
||||
"tests/unit/services-branch-hardening.test.ts",
|
||||
"tests/unit/services/bifrost-route-guard.test.ts",
|
||||
"tests/unit/services/combo-metrics-memory.test.ts",
|
||||
"tests/unit/settings/authz-bypass.test.ts",
|
||||
"tests/unit/skip-provider-breaker-consumer-2743.test.ts",
|
||||
"tests/unit/sse-auth.test.ts",
|
||||
"tests/unit/strict-random-deck.test.ts",
|
||||
"tests/unit/strip-reasoning-header.test.ts",
|
||||
"tests/unit/system-role-extraction.test.ts",
|
||||
"tests/unit/t23-t24-fallback-resilience.test.ts",
|
||||
"tests/unit/tag-routing.test.ts",
|
||||
@@ -184,53 +226,11 @@
|
||||
"tests/unit/token-refresh-race-comprehensive.test.ts",
|
||||
"tests/unit/token-refresh-service.test.ts",
|
||||
"tests/unit/tools-filter-anthropic-format.test.ts",
|
||||
"tests/unit/usage-service-hardening.test.ts",
|
||||
"tests/unit/validate-response-quality.test.ts",
|
||||
"tests/unit/accountfallback-ratelimit-400-4976.test.ts",
|
||||
"tests/unit/antigravity-429-quota-tdd.test.ts",
|
||||
"tests/unit/auth-antigravity-account-retry-v2.test.ts",
|
||||
"tests/unit/middleware-header-strip-5849.test.ts",
|
||||
"tests/unit/authz/discovery-routes-local-only.test.ts",
|
||||
"tests/unit/authz/route-guard-local-prefix.test.ts",
|
||||
"tests/unit/authz/route-guard-version-get-exemption.test.ts",
|
||||
"tests/unit/chat-combo-live-test.test.ts",
|
||||
"tests/unit/chatcore-executor-proxy.test.ts",
|
||||
"tests/unit/chatcore-request-format.test.ts",
|
||||
"tests/unit/chatcore-semantic-cache-store.test.ts",
|
||||
"tests/unit/chatcore-upstream-timeouts.test.ts",
|
||||
"tests/unit/circuit-breaker-registry-cap.test.ts",
|
||||
"tests/unit/circuit-breaker-stream-controller-4602.test.ts",
|
||||
"tests/unit/clinepass-provider.test.ts",
|
||||
"tests/unit/codex-session-affinity-reset-aware-5903.test.ts",
|
||||
"tests/unit/combo-account-allowlist-3266.test.ts",
|
||||
"tests/unit/combo-headroom-strategy.test.ts",
|
||||
"tests/unit/combo-model-lockout-honors-reset-1308.test.ts",
|
||||
"tests/unit/combo-param-validation-fallback-4519.test.ts",
|
||||
"tests/unit/combo-priority-quota-exhaustion-cutoff-5923.test.ts",
|
||||
"tests/unit/combo-quota-share-cooldown-wait.test.ts",
|
||||
"tests/unit/combo-selected-connection-success.test.ts",
|
||||
"tests/unit/combo-stream-readiness-fallback.test.ts",
|
||||
"tests/unit/combo-strict-random-distribution-3959.test.ts",
|
||||
"tests/unit/combo/auto-quota-cutoff.test.ts",
|
||||
"tests/unit/combo/auto-status-penalty-4540.test.ts",
|
||||
"tests/unit/combo/combo-exhausted-skip.test.ts",
|
||||
"tests/unit/combo/effective-max-concurrency.test.ts",
|
||||
"tests/unit/cooldown-epoch-string-3954.test.ts",
|
||||
"tests/unit/format-provider-error-cause.test.ts",
|
||||
"tests/unit/grok-cli-oauth.test.ts",
|
||||
"tests/unit/headroom-proxy-lifecycle.test.ts",
|
||||
"tests/unit/livews-forward-backoff-4604.test.ts",
|
||||
"tests/unit/management-auth-hardening.test.ts",
|
||||
"tests/unit/mark-account-unavailable-numeric-epoch-guard.test.ts",
|
||||
"tests/unit/model-lockout-max-cooldown.test.ts",
|
||||
"tests/unit/no-memory-header.test.ts",
|
||||
"tests/unit/non-streaming-sse-terminal-typescan-4459.test.ts",
|
||||
"tests/unit/openapi-security-tiers.test.ts",
|
||||
"tests/unit/rate-limit-queue-timeout-lockout.test.ts",
|
||||
"tests/unit/strip-reasoning-header.test.ts",
|
||||
"tests/unit/tproxy-route.test.ts",
|
||||
"tests/unit/types-barrel-model-cooldown.test.ts",
|
||||
"tests/unit/upstream-retry-hints-toggle.test.ts"
|
||||
"tests/unit/upstream-retry-hints-toggle.test.ts",
|
||||
"tests/unit/usage-service-hardening.test.ts",
|
||||
"tests/unit/validate-response-quality.test.ts"
|
||||
],
|
||||
"nodeArgs": [
|
||||
"--import",
|
||||
@@ -339,7 +339,11 @@
|
||||
".worktrees",
|
||||
".stryker-tmp"
|
||||
],
|
||||
"reporters": ["progress", "html", "json"],
|
||||
"reporters": [
|
||||
"progress",
|
||||
"html",
|
||||
"json"
|
||||
],
|
||||
"htmlReporter": {
|
||||
"fileName": "reports/mutation/mutation.html"
|
||||
},
|
||||
|
||||
86
tests/unit/sync-next-cycle.test.ts
Normal file
86
tests/unit/sync-next-cycle.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
insertNextSection,
|
||||
extractSection,
|
||||
} from "../../scripts/release/sync-next-cycle.mjs";
|
||||
|
||||
// Pure-function guards for the parallel-cycle sync-back (generate-release
|
||||
// Phase 5 step 20): main's CHANGELOG must win VERBATIM and the next cycle's
|
||||
// section must be re-inserted on top without eating any of main's bullets
|
||||
// (anti-CHANGELOG-eat protocol, design doc
|
||||
// _tasks/release-flow/2026-07-04_proposta-ciclo-paralelo-v2.md).
|
||||
|
||||
const MAIN = `# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
---
|
||||
|
||||
## [3.8.44] — 2026-07-04
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(a):** shipped feature one. (#1)
|
||||
- **feat(b):** shipped feature two. (#2)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| --- | --- |
|
||||
| [@x](https://github.com/x) | #1 |
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
- **old:** bullet. (#0)
|
||||
`;
|
||||
|
||||
const CYCLE_SECTION = `## [3.8.45] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(new):** cycle bullet accumulated before the sync. (#9)`;
|
||||
|
||||
test("insertNextSection puts the cycle section before main's latest version, keeping main intact", () => {
|
||||
const out = insertNextSection(MAIN, CYCLE_SECTION, "3.8.45");
|
||||
const i45 = out.indexOf("## [3.8.45]");
|
||||
const i44 = out.indexOf("## [3.8.44]");
|
||||
const i43 = out.indexOf("## [3.8.43]");
|
||||
assert.ok(i45 !== -1 && i44 !== -1 && i43 !== -1, "all three sections present");
|
||||
assert.ok(i45 < i44 && i44 < i43, "ordering: 3.8.45 → 3.8.44 → 3.8.43");
|
||||
// main's content survives verbatim (no bullet eaten)
|
||||
assert.ok(out.includes("shipped feature one"), "main bullet 1 intact");
|
||||
assert.ok(out.includes("shipped feature two"), "main bullet 2 intact");
|
||||
assert.ok(out.includes("| [@x](https://github.com/x) | #1 |"), "contributors row intact");
|
||||
// the cycle's accumulated bullet survives
|
||||
assert.ok(out.includes("cycle bullet accumulated before the sync"), "cycle bullet intact");
|
||||
});
|
||||
|
||||
test("insertNextSection synthesizes a TBD placeholder when the cycle has no section yet", () => {
|
||||
const out = insertNextSection(MAIN, null, "3.8.45");
|
||||
assert.ok(out.includes("## [3.8.45] — TBD"));
|
||||
assert.ok(out.indexOf("## [3.8.45]") < out.indexOf("## [3.8.44]"));
|
||||
});
|
||||
|
||||
test("insertNextSection replaces a pre-existing copy of the next section instead of duplicating", () => {
|
||||
const withNext = insertNextSection(MAIN, CYCLE_SECTION, "3.8.45");
|
||||
const again = insertNextSection(withNext, CYCLE_SECTION, "3.8.45");
|
||||
const count = (again.match(/## \[3\.8\.45\]/g) || []).length;
|
||||
assert.equal(count, 1, "exactly one [3.8.45] heading after re-run (idempotent)");
|
||||
});
|
||||
|
||||
test("extractSection returns header through the line before the next heading, trimming the separator", () => {
|
||||
const withNext = insertNextSection(MAIN, CYCLE_SECTION, "3.8.45");
|
||||
const section = extractSection(withNext, "3.8.45");
|
||||
assert.ok(section, "section found");
|
||||
assert.ok(section.startsWith("## [3.8.45]"));
|
||||
assert.ok(section.includes("cycle bullet accumulated"));
|
||||
assert.ok(!section.includes("## [3.8.44]"), "does not bleed into the next section");
|
||||
assert.ok(!section.trimEnd().endsWith("---"), "trailing separator trimmed");
|
||||
});
|
||||
|
||||
test("extractSection returns null when the version has no section", () => {
|
||||
assert.equal(extractSection(MAIN, "9.9.9"), null);
|
||||
});
|
||||
Reference in New Issue
Block a user