Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Patch-ready tip on `main` after the PerfTrace / Codex measurement stack and rela
- **Latency eval harness** — assert phase presence and relative magnitudes in tests (`assert-spans`, multi-tool fixture). (CL-5174, #310)
- **Reasoning effort by agent role** — orchestrator vs task-leaf defaults so high-effort leaves stop multiplying wall time. (CL-5162, #302)
- **Session state under `~/.corbits/projects`** — project key from git toplevel (worktrees share); dual-read migrate from in-repo `.agent-state`; path-restriction exception for the global state root. (CL-5257, #313)
- **Post-upgrade release notes** — on a fresh interactive start after upgrade, show bounded Keep-a-Changelog sections in the session banner; stamp `lastChangelogVersion` in global settings; first install is quiet; `/changelog` and `/changelog full` for on-demand history. Ships `CHANGELOG.md` next to release binaries. (CL-5333, CL-5332, CL-5334)
- **Streaming stall / loop detection** — trailing-window repetition detection; preserve partial streamed output in exec and TUI; partial-capture lifecycle owned by the cycle recorder. (#280, #281)
- **Nested UI polish** — quieter chrome, context meter, task/shell rows, observe-leave behavior. (#312)
- **Approval queue re-eval** — when a grant widens, re-check the pending queue; stored approvals evaluated through `@intx/authz`. (#288, #295)
Expand Down Expand Up @@ -45,7 +46,6 @@ Patch-ready tip on `main` after the PerfTrace / Codex measurement stack and rela

### Planned

- What's-new banner on interactive start after upgrade (CL-4604 — **canceled** as a ticket; still not implemented; see note below)
- Local context estimate for compaction when providers omit usage (CL-4345)
- Image age → rehydratable attachment URI (CL-4349)
- Always-return subagent salvage without a default wall-clock death clock (CL-4401)
Expand Down
2 changes: 1 addition & 1 deletion scripts/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ TAP_REPO="corbitsdev/homebrew-tap" # tap repo (formula)
TAP_SLUG="corbitsdev/tap" # `brew tap' name of TAP_REPO
FORMULA="corbits" # formula / binary name
DESC="Single-process coding agent CLI built on the Interchange runtime"
DOC_FILES=(LICENSE.md README.md GPLv2-AI-Exception.md GPL-2.0.txt) # shipped with the binary
DOC_FILES=(LICENSE.md README.md CHANGELOG.md GPLv2-AI-Exception.md GPL-2.0.txt) # shipped with the binary

# Build matrix: "label|bun-target|kind|deb-arch". kind is macos or linux;
# deb-arch is the Debian architecture for linux targets, "-" for macOS.
Expand Down
163 changes: 163 additions & 0 deletions src/changelog/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { describe, expect, test } from "bun:test";
import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

import {
compareVersions,
decideStartupChangelog,
formatStartupChangelog,
getNewEntries,
parseChangelog,
parseChangelogText,
parseVersionString,
resolveChangelogPath,
} from "./index.js";

const SAMPLE = `# Changelog

## [Unreleased]

### New Features
- not a release

## [0.2.86] - 2026-07-30

### New Features
- Feature A

## [0.2.85] - 2026-07-20

### Fixed
- Bug B

## [0.1.0] - 2026-01-01

### New Features
- Initial
`;

describe("parseChangelogText", () => {
test("parses versioned sections and skips Unreleased", () => {
const entries = parseChangelogText(SAMPLE);
expect(entries.map((e) => `${e.major}.${e.minor}.${e.patch}`)).toEqual([
"0.2.86",
"0.2.85",
"0.1.0",
]);
expect(entries[0]!.content).toContain("## [0.2.86]");
expect(entries[0]!.content).toContain("Feature A");
expect(entries.every((e) => !e.content.includes("Unreleased"))).toBe(true);
});

test("accepts unbracketed version headers", () => {
const entries = parseChangelogText("## 1.2.3\n\n- note\n");
expect(entries).toHaveLength(1);
expect(entries[0]!.major).toBe(1);
expect(entries[0]!.minor).toBe(2);
expect(entries[0]!.patch).toBe(3);
});
});

describe("compareVersions / getNewEntries", () => {
test("orders major.minor.patch", () => {
const a = parseVersionString("0.2.86")!;
const b = parseVersionString("0.2.85")!;
expect(compareVersions(a, b)).toBeGreaterThan(0);
expect(compareVersions(b, a)).toBeLessThan(0);
expect(compareVersions(a, a)).toBe(0);
});

test("returns only newer entries", () => {
const entries = parseChangelogText(SAMPLE);
const newer = getNewEntries(entries, "0.2.85");
expect(newer.map((e) => `${e.major}.${e.minor}.${e.patch}`)).toEqual(["0.2.86"]);
});
});

describe("decideStartupChangelog", () => {
const entries = parseChangelogText(SAMPLE);

test("missing watermark is first install — stamp, no history", () => {
const d = decideStartupChangelog({
entries,
lastChangelogVersion: undefined,
packageVersion: "0.2.86",
});
expect(d).toEqual({ kind: "first_install", stampVersion: "0.2.86" });
});

test("malformed watermark is first install", () => {
const d = decideStartupChangelog({
entries,
lastChangelogVersion: "not-a-version",
packageVersion: "0.2.86",
});
expect(d.kind).toBe("first_install");
});

test("upgrade shows notes and stamps package version", () => {
const d = decideStartupChangelog({
entries,
lastChangelogVersion: "0.2.85",
packageVersion: "0.2.86",
});
expect(d.kind).toBe("upgrade");
if (d.kind === "upgrade") {
expect(d.markdown).toContain("0.2.86");
expect(d.markdown).toContain("Feature A");
expect(d.markdown).not.toContain("0.1.0");
expect(d.stampVersion).toBe("0.2.86");
expect(d.versions).toContain("0.2.86");
}
});

test("current version is quiet", () => {
const d = decideStartupChangelog({
entries,
lastChangelogVersion: "0.2.86",
packageVersion: "0.2.86",
});
expect(d).toEqual({ kind: "current" });
});
});

describe("formatStartupChangelog", () => {
test("caps entry count and marks truncated", () => {
const entries = parseChangelogText(SAMPLE);
const formatted = formatStartupChangelog(entries, { maxEntries: 1 });
expect(formatted.versions).toEqual(["0.2.86"]);
expect(formatted.truncated).toBe(true);
expect(formatted.markdown).toContain("/changelog");
});

test("caps byte size", () => {
const big = parseChangelogText(
`## [9.0.0]\n\n${"x".repeat(200)}\n\n## [8.0.0]\n\n${"y".repeat(200)}\n`,
);
const formatted = formatStartupChangelog(big, { maxEntries: 5, maxBytes: 120 });
expect(Buffer.byteLength(formatted.markdown, "utf8")).toBeLessThanOrEqual(120);
expect(formatted.truncated).toBe(true);
});
});

describe("parseChangelog file + resolveChangelogPath", () => {
test("missing file yields empty", () => {
expect(parseChangelog("/no/such/CHANGELOG.md")).toEqual([]);
});

test("reads a real file; resolve prefers package then cwd", () => {
const dir = mkdtempSync(join(tmpdir(), "corbits-changelog-"));
const path = join(dir, "CHANGELOG.md");
writeFileSync(path, SAMPLE, "utf8");
expect(parseChangelog(path)).toHaveLength(3);
// Package-root candidates win when the worktree has CHANGELOG.md; when they
// do not exist, cwd is used.
const resolved = resolveChangelogPath({
cwd: dir,
execPath: "/nonexistent/bin/corbits",
moduleUrl: `file://${join(dir, "src", "changelog", "index.ts")}`,
});
expect(resolved).toBe(path);
});
});
Loading
Loading