style: 完成所有文件的lint

This commit is contained in:
claude-code-best
2026-05-01 21:39:30 +08:00
parent d136872cc9
commit 6182015005
1333 changed files with 68255 additions and 77882 deletions

View File

@@ -1,102 +1,102 @@
import { describe, expect, test } from "bun:test";
import { CircularBuffer } from "../CircularBuffer";
import { describe, expect, test } from 'bun:test'
import { CircularBuffer } from '../CircularBuffer'
describe("CircularBuffer", () => {
test("starts empty", () => {
const buf = new CircularBuffer<number>(5);
expect(buf.length()).toBe(0);
expect(buf.toArray()).toEqual([]);
});
describe('CircularBuffer', () => {
test('starts empty', () => {
const buf = new CircularBuffer<number>(5)
expect(buf.length()).toBe(0)
expect(buf.toArray()).toEqual([])
})
test("adds items up to capacity", () => {
const buf = new CircularBuffer<number>(3);
buf.add(1);
buf.add(2);
buf.add(3);
expect(buf.length()).toBe(3);
expect(buf.toArray()).toEqual([1, 2, 3]);
});
test('adds items up to capacity', () => {
const buf = new CircularBuffer<number>(3)
buf.add(1)
buf.add(2)
buf.add(3)
expect(buf.length()).toBe(3)
expect(buf.toArray()).toEqual([1, 2, 3])
})
test("evicts oldest when full", () => {
const buf = new CircularBuffer<number>(3);
buf.add(1);
buf.add(2);
buf.add(3);
buf.add(4);
expect(buf.length()).toBe(3);
expect(buf.toArray()).toEqual([2, 3, 4]);
});
test('evicts oldest when full', () => {
const buf = new CircularBuffer<number>(3)
buf.add(1)
buf.add(2)
buf.add(3)
buf.add(4)
expect(buf.length()).toBe(3)
expect(buf.toArray()).toEqual([2, 3, 4])
})
test("evicts multiple oldest items", () => {
const buf = new CircularBuffer<number>(2);
buf.add(1);
buf.add(2);
buf.add(3);
buf.add(4);
buf.add(5);
expect(buf.toArray()).toEqual([4, 5]);
});
test('evicts multiple oldest items', () => {
const buf = new CircularBuffer<number>(2)
buf.add(1)
buf.add(2)
buf.add(3)
buf.add(4)
buf.add(5)
expect(buf.toArray()).toEqual([4, 5])
})
test("addAll adds multiple items", () => {
const buf = new CircularBuffer<number>(5);
buf.addAll([1, 2, 3]);
expect(buf.toArray()).toEqual([1, 2, 3]);
});
test('addAll adds multiple items', () => {
const buf = new CircularBuffer<number>(5)
buf.addAll([1, 2, 3])
expect(buf.toArray()).toEqual([1, 2, 3])
})
test("addAll with overflow", () => {
const buf = new CircularBuffer<number>(3);
buf.addAll([1, 2, 3, 4, 5]);
expect(buf.toArray()).toEqual([3, 4, 5]);
});
test('addAll with overflow', () => {
const buf = new CircularBuffer<number>(3)
buf.addAll([1, 2, 3, 4, 5])
expect(buf.toArray()).toEqual([3, 4, 5])
})
test("getRecent returns last N items", () => {
const buf = new CircularBuffer<number>(5);
buf.addAll([1, 2, 3, 4, 5]);
expect(buf.getRecent(3)).toEqual([3, 4, 5]);
});
test('getRecent returns last N items', () => {
const buf = new CircularBuffer<number>(5)
buf.addAll([1, 2, 3, 4, 5])
expect(buf.getRecent(3)).toEqual([3, 4, 5])
})
test("getRecent returns fewer when not enough items", () => {
const buf = new CircularBuffer<number>(5);
buf.add(1);
buf.add(2);
expect(buf.getRecent(5)).toEqual([1, 2]);
});
test('getRecent returns fewer when not enough items', () => {
const buf = new CircularBuffer<number>(5)
buf.add(1)
buf.add(2)
expect(buf.getRecent(5)).toEqual([1, 2])
})
test("getRecent works after wraparound", () => {
const buf = new CircularBuffer<number>(3);
buf.addAll([1, 2, 3, 4, 5]);
expect(buf.getRecent(2)).toEqual([4, 5]);
});
test('getRecent works after wraparound', () => {
const buf = new CircularBuffer<number>(3)
buf.addAll([1, 2, 3, 4, 5])
expect(buf.getRecent(2)).toEqual([4, 5])
})
test("clear resets buffer", () => {
const buf = new CircularBuffer<number>(5);
buf.addAll([1, 2, 3]);
buf.clear();
expect(buf.length()).toBe(0);
expect(buf.toArray()).toEqual([]);
});
test('clear resets buffer', () => {
const buf = new CircularBuffer<number>(5)
buf.addAll([1, 2, 3])
buf.clear()
expect(buf.length()).toBe(0)
expect(buf.toArray()).toEqual([])
})
test("works with string type", () => {
const buf = new CircularBuffer<string>(2);
buf.add("a");
buf.add("b");
buf.add("c");
expect(buf.toArray()).toEqual(["b", "c"]);
});
test('works with string type', () => {
const buf = new CircularBuffer<string>(2)
buf.add('a')
buf.add('b')
buf.add('c')
expect(buf.toArray()).toEqual(['b', 'c'])
})
test("capacity=1 keeps only the most recent item", () => {
const buf = new CircularBuffer<number>(1);
buf.add(10);
expect(buf.toArray()).toEqual([10]);
buf.add(20);
expect(buf.toArray()).toEqual([20]);
buf.add(30);
expect(buf.toArray()).toEqual([30]);
expect(buf.getRecent(1)).toEqual([30]);
});
test('capacity=1 keeps only the most recent item', () => {
const buf = new CircularBuffer<number>(1)
buf.add(10)
expect(buf.toArray()).toEqual([10])
buf.add(20)
expect(buf.toArray()).toEqual([20])
buf.add(30)
expect(buf.toArray()).toEqual([30])
expect(buf.getRecent(1)).toEqual([30])
})
test("getRecent on empty buffer returns empty array", () => {
const buf = new CircularBuffer<number>(5);
expect(buf.getRecent(3)).toEqual([]);
});
});
test('getRecent on empty buffer returns empty array', () => {
const buf = new CircularBuffer<number>(5)
expect(buf.getRecent(3)).toEqual([])
})
})

View File

@@ -1,106 +1,106 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
import {
createAbortController,
createChildAbortController,
} from "../abortController";
} from '../abortController'
describe("createAbortController", () => {
test("returns an AbortController that is not aborted", () => {
const controller = createAbortController();
expect(controller.signal.aborted).toBe(false);
});
describe('createAbortController', () => {
test('returns an AbortController that is not aborted', () => {
const controller = createAbortController()
expect(controller.signal.aborted).toBe(false)
})
test("aborting the controller sets signal.aborted", () => {
const controller = createAbortController();
controller.abort();
expect(controller.signal.aborted).toBe(true);
});
test('aborting the controller sets signal.aborted', () => {
const controller = createAbortController()
controller.abort()
expect(controller.signal.aborted).toBe(true)
})
test("abort reason is propagated", () => {
const controller = createAbortController();
controller.abort("custom reason");
expect(controller.signal.reason).toBe("custom reason");
});
test('abort reason is propagated', () => {
const controller = createAbortController()
controller.abort('custom reason')
expect(controller.signal.reason).toBe('custom reason')
})
test("accepts custom maxListeners without error", () => {
const controller = createAbortController(100);
expect(controller.signal.aborted).toBe(false);
});
});
test('accepts custom maxListeners without error', () => {
const controller = createAbortController(100)
expect(controller.signal.aborted).toBe(false)
})
})
describe("createChildAbortController", () => {
test("child is not aborted initially", () => {
const parent = createAbortController();
const child = createChildAbortController(parent);
expect(child.signal.aborted).toBe(false);
expect(parent.signal.aborted).toBe(false);
});
describe('createChildAbortController', () => {
test('child is not aborted initially', () => {
const parent = createAbortController()
const child = createChildAbortController(parent)
expect(child.signal.aborted).toBe(false)
expect(parent.signal.aborted).toBe(false)
})
test("parent abort propagates to child", () => {
const parent = createAbortController();
const child = createChildAbortController(parent);
parent.abort("parent reason");
expect(child.signal.aborted).toBe(true);
expect(child.signal.reason).toBe("parent reason");
});
test('parent abort propagates to child', () => {
const parent = createAbortController()
const child = createChildAbortController(parent)
parent.abort('parent reason')
expect(child.signal.aborted).toBe(true)
expect(child.signal.reason).toBe('parent reason')
})
test("child abort does NOT propagate to parent", () => {
const parent = createAbortController();
const child = createChildAbortController(parent);
child.abort("child reason");
expect(child.signal.aborted).toBe(true);
expect(parent.signal.aborted).toBe(false);
});
test('child abort does NOT propagate to parent', () => {
const parent = createAbortController()
const child = createChildAbortController(parent)
child.abort('child reason')
expect(child.signal.aborted).toBe(true)
expect(parent.signal.aborted).toBe(false)
})
test("already-aborted parent immediately aborts child", () => {
const parent = createAbortController();
parent.abort("pre-abort");
const child = createChildAbortController(parent);
expect(child.signal.aborted).toBe(true);
expect(child.signal.reason).toBe("pre-abort");
});
test('already-aborted parent immediately aborts child', () => {
const parent = createAbortController()
parent.abort('pre-abort')
const child = createChildAbortController(parent)
expect(child.signal.aborted).toBe(true)
expect(child.signal.reason).toBe('pre-abort')
})
test("multiple children are independent", () => {
const parent = createAbortController();
const child1 = createChildAbortController(parent);
const child2 = createChildAbortController(parent);
child1.abort("child1");
expect(child1.signal.aborted).toBe(true);
expect(child2.signal.aborted).toBe(false);
test('multiple children are independent', () => {
const parent = createAbortController()
const child1 = createChildAbortController(parent)
const child2 = createChildAbortController(parent)
child1.abort('child1')
expect(child1.signal.aborted).toBe(true)
expect(child2.signal.aborted).toBe(false)
// Aborting child1 did not affect child2 or parent
expect(parent.signal.aborted).toBe(false);
});
expect(parent.signal.aborted).toBe(false)
})
test("parent abort propagates to all children", () => {
const parent = createAbortController();
const child1 = createChildAbortController(parent);
const child2 = createChildAbortController(parent);
parent.abort("all go down");
expect(child1.signal.aborted).toBe(true);
expect(child2.signal.aborted).toBe(true);
});
test('parent abort propagates to all children', () => {
const parent = createAbortController()
const child1 = createChildAbortController(parent)
const child2 = createChildAbortController(parent)
parent.abort('all go down')
expect(child1.signal.aborted).toBe(true)
expect(child2.signal.aborted).toBe(true)
})
test("grandchild abort propagation", () => {
const grandparent = createAbortController();
const parent = createChildAbortController(grandparent);
const child = createChildAbortController(parent);
grandparent.abort("chain");
expect(parent.signal.aborted).toBe(true);
expect(child.signal.aborted).toBe(true);
});
test('grandchild abort propagation', () => {
const grandparent = createAbortController()
const parent = createChildAbortController(grandparent)
const child = createChildAbortController(parent)
grandparent.abort('chain')
expect(parent.signal.aborted).toBe(true)
expect(child.signal.aborted).toBe(true)
})
test("child abort then parent abort — child stays aborted with original reason", () => {
const parent = createAbortController();
const child = createChildAbortController(parent);
child.abort("child first");
parent.abort("parent later");
expect(child.signal.reason).toBe("child first");
expect(parent.signal.reason).toBe("parent later");
});
test('child abort then parent abort — child stays aborted with original reason', () => {
const parent = createAbortController()
const child = createChildAbortController(parent)
child.abort('child first')
parent.abort('parent later')
expect(child.signal.reason).toBe('child first')
expect(parent.signal.reason).toBe('parent later')
})
test("accepts custom maxListeners for child", () => {
const parent = createAbortController();
const child = createChildAbortController(parent, 200);
expect(child.signal.aborted).toBe(false);
});
});
test('accepts custom maxListeners for child', () => {
const parent = createAbortController()
const child = createChildAbortController(parent, 200)
expect(child.signal.aborted).toBe(false)
})
})

View File

@@ -1,145 +1,141 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
import {
parseArguments,
parseArgumentNames,
generateProgressiveArgumentHint,
substituteArguments,
} from "../argumentSubstitution";
} from '../argumentSubstitution'
// ─── parseArguments ─────────────────────────────────────────────────────
describe("parseArguments", () => {
test("splits simple arguments", () => {
expect(parseArguments("foo bar baz")).toEqual(["foo", "bar", "baz"]);
});
describe('parseArguments', () => {
test('splits simple arguments', () => {
expect(parseArguments('foo bar baz')).toEqual(['foo', 'bar', 'baz'])
})
test("handles quoted strings", () => {
test('handles quoted strings', () => {
expect(parseArguments('foo "hello world" baz')).toEqual([
"foo",
"hello world",
"baz",
]);
});
'foo',
'hello world',
'baz',
])
})
test("handles single-quoted strings", () => {
test('handles single-quoted strings', () => {
expect(parseArguments("foo 'hello world' baz")).toEqual([
"foo",
"hello world",
"baz",
]);
});
'foo',
'hello world',
'baz',
])
})
test("handles escaped quotes inside quoted strings", () => {
test('handles escaped quotes inside quoted strings', () => {
expect(parseArguments('foo "hello \\"world\\"" baz')).toEqual([
"foo",
'foo',
'hello "world"',
"baz",
]);
});
'baz',
])
})
test("returns empty for empty string", () => {
expect(parseArguments("")).toEqual([]);
});
test('returns empty for empty string', () => {
expect(parseArguments('')).toEqual([])
})
test("returns empty for whitespace only", () => {
expect(parseArguments(" ")).toEqual([]);
});
});
test('returns empty for whitespace only', () => {
expect(parseArguments(' ')).toEqual([])
})
})
// ─── parseArgumentNames ─────────────────────────────────────────────────
describe("parseArgumentNames", () => {
test("parses space-separated string", () => {
expect(parseArgumentNames("foo bar baz")).toEqual(["foo", "bar", "baz"]);
});
describe('parseArgumentNames', () => {
test('parses space-separated string', () => {
expect(parseArgumentNames('foo bar baz')).toEqual(['foo', 'bar', 'baz'])
})
test("accepts array input", () => {
expect(parseArgumentNames(["foo", "bar"])).toEqual(["foo", "bar"]);
});
test('accepts array input', () => {
expect(parseArgumentNames(['foo', 'bar'])).toEqual(['foo', 'bar'])
})
test("filters out numeric-only names", () => {
expect(parseArgumentNames("foo 123 bar")).toEqual(["foo", "bar"]);
});
test('filters out numeric-only names', () => {
expect(parseArgumentNames('foo 123 bar')).toEqual(['foo', 'bar'])
})
test("filters out empty strings", () => {
expect(parseArgumentNames(["foo", "", "bar"])).toEqual(["foo", "bar"]);
});
test('filters out empty strings', () => {
expect(parseArgumentNames(['foo', '', 'bar'])).toEqual(['foo', 'bar'])
})
test("returns empty for undefined", () => {
expect(parseArgumentNames(undefined)).toEqual([]);
});
});
test('returns empty for undefined', () => {
expect(parseArgumentNames(undefined)).toEqual([])
})
})
// ─── generateProgressiveArgumentHint ────────────────────────────────────
describe("generateProgressiveArgumentHint", () => {
test("shows remaining arguments", () => {
expect(generateProgressiveArgumentHint(["a", "b", "c"], ["x"])).toBe(
"[b] [c]"
);
});
describe('generateProgressiveArgumentHint', () => {
test('shows remaining arguments', () => {
expect(generateProgressiveArgumentHint(['a', 'b', 'c'], ['x'])).toBe(
'[b] [c]',
)
})
test("returns undefined when all filled", () => {
expect(
generateProgressiveArgumentHint(["a"], ["x"])
).toBeUndefined();
});
test('returns undefined when all filled', () => {
expect(generateProgressiveArgumentHint(['a'], ['x'])).toBeUndefined()
})
test("shows all when none typed", () => {
expect(generateProgressiveArgumentHint(["a", "b"], [])).toBe("[a] [b]");
});
});
test('shows all when none typed', () => {
expect(generateProgressiveArgumentHint(['a', 'b'], [])).toBe('[a] [b]')
})
})
// ─── substituteArguments ────────────────────────────────────────────────
describe("substituteArguments", () => {
test("replaces $ARGUMENTS with full args", () => {
expect(substituteArguments("run $ARGUMENTS", "foo bar")).toBe(
"run foo bar"
);
});
describe('substituteArguments', () => {
test('replaces $ARGUMENTS with full args', () => {
expect(substituteArguments('run $ARGUMENTS', 'foo bar')).toBe('run foo bar')
})
test("replaces indexed $ARGUMENTS[0]", () => {
expect(substituteArguments("run $ARGUMENTS[0]", "foo bar")).toBe("run foo");
});
test('replaces indexed $ARGUMENTS[0]', () => {
expect(substituteArguments('run $ARGUMENTS[0]', 'foo bar')).toBe('run foo')
})
test("replaces shorthand $0, $1", () => {
expect(substituteArguments("$0 and $1", "hello world")).toBe(
"hello and world"
);
});
test('replaces shorthand $0, $1', () => {
expect(substituteArguments('$0 and $1', 'hello world')).toBe(
'hello and world',
)
})
test("replaces out-of-range index with empty string", () => {
expect(substituteArguments("$5", "hello world")).toBe("");
});
test('replaces out-of-range index with empty string', () => {
expect(substituteArguments('$5', 'hello world')).toBe('')
})
test("reuses same placeholder multiple times", () => {
expect(substituteArguments("cmd $0 $1 $0", "alpha beta")).toBe(
"cmd alpha beta alpha"
);
});
test('reuses same placeholder multiple times', () => {
expect(substituteArguments('cmd $0 $1 $0', 'alpha beta')).toBe(
'cmd alpha beta alpha',
)
})
test("replaces named arguments", () => {
expect(
substituteArguments("file: $name", "test.txt", true, ["name"])
).toBe("file: test.txt");
});
test('replaces named arguments', () => {
expect(substituteArguments('file: $name', 'test.txt', true, ['name'])).toBe(
'file: test.txt',
)
})
test("returns content unchanged for undefined args", () => {
expect(substituteArguments("hello", undefined)).toBe("hello");
});
test('returns content unchanged for undefined args', () => {
expect(substituteArguments('hello', undefined)).toBe('hello')
})
test("appends ARGUMENTS when no placeholder found", () => {
expect(substituteArguments("run this", "extra")).toBe(
"run this\n\nARGUMENTS: extra"
);
});
test('appends ARGUMENTS when no placeholder found', () => {
expect(substituteArguments('run this', 'extra')).toBe(
'run this\n\nARGUMENTS: extra',
)
})
test("does not append when appendIfNoPlaceholder is false", () => {
expect(substituteArguments("run this", "extra", false)).toBe("run this");
});
test('does not append when appendIfNoPlaceholder is false', () => {
expect(substituteArguments('run this', 'extra', false)).toBe('run this')
})
test("does not append for empty args string", () => {
expect(substituteArguments("run this", "")).toBe("run this");
});
});
test('does not append for empty args string', () => {
expect(substituteArguments('run this', '')).toBe('run this')
})
})

View File

@@ -1,58 +1,58 @@
import { describe, expect, test } from "bun:test";
import { count, intersperse, uniq } from "../array";
import { describe, expect, test } from 'bun:test'
import { count, intersperse, uniq } from '../array'
describe("intersperse", () => {
test("inserts separator between elements", () => {
const result = intersperse([1, 2, 3], () => 0);
expect(result).toEqual([1, 0, 2, 0, 3]);
});
describe('intersperse', () => {
test('inserts separator between elements', () => {
const result = intersperse([1, 2, 3], () => 0)
expect(result).toEqual([1, 0, 2, 0, 3])
})
test("returns empty array for empty input", () => {
expect(intersperse([], () => 0)).toEqual([]);
});
test('returns empty array for empty input', () => {
expect(intersperse([], () => 0)).toEqual([])
})
test("returns single element without separator", () => {
expect(intersperse([1], () => 0)).toEqual([1]);
});
test('returns single element without separator', () => {
expect(intersperse([1], () => 0)).toEqual([1])
})
test("passes index to separator function", () => {
const result = intersperse(["a", "b", "c"], (i) => `sep-${i}`);
expect(result).toEqual(["a", "sep-1", "b", "sep-2", "c"]);
});
});
test('passes index to separator function', () => {
const result = intersperse(['a', 'b', 'c'], i => `sep-${i}`)
expect(result).toEqual(['a', 'sep-1', 'b', 'sep-2', 'c'])
})
})
describe("count", () => {
test("counts matching elements", () => {
expect(count([1, 2, 3, 4, 5], (x) => x > 3)).toBe(2);
});
describe('count', () => {
test('counts matching elements', () => {
expect(count([1, 2, 3, 4, 5], x => x > 3)).toBe(2)
})
test("returns 0 for empty array", () => {
expect(count([], () => true)).toBe(0);
});
test('returns 0 for empty array', () => {
expect(count([], () => true)).toBe(0)
})
test("returns 0 when nothing matches", () => {
expect(count([1, 2, 3], (x) => x > 10)).toBe(0);
});
test('returns 0 when nothing matches', () => {
expect(count([1, 2, 3], x => x > 10)).toBe(0)
})
test("counts all when everything matches", () => {
expect(count([1, 2, 3], () => true)).toBe(3);
});
});
test('counts all when everything matches', () => {
expect(count([1, 2, 3], () => true)).toBe(3)
})
})
describe("uniq", () => {
test("removes duplicates", () => {
expect(uniq([1, 2, 2, 3, 3, 3])).toEqual([1, 2, 3]);
});
describe('uniq', () => {
test('removes duplicates', () => {
expect(uniq([1, 2, 2, 3, 3, 3])).toEqual([1, 2, 3])
})
test("preserves order of first occurrence", () => {
expect(uniq([3, 1, 2, 1, 3])).toEqual([3, 1, 2]);
});
test('preserves order of first occurrence', () => {
expect(uniq([3, 1, 2, 1, 3])).toEqual([3, 1, 2])
})
test("handles empty array", () => {
expect(uniq([])).toEqual([]);
});
test('handles empty array', () => {
expect(uniq([])).toEqual([])
})
test("works with strings", () => {
expect(uniq(["a", "b", "a"])).toEqual(["a", "b"]);
});
});
test('works with strings', () => {
expect(uniq(['a', 'b', 'a'])).toEqual(['a', 'b'])
})
})

View File

@@ -486,9 +486,7 @@ describe('autonomyRuns', () => {
runs: Array<Record<string, unknown>>
}
file.runs = file.runs.map(run =>
run.runId === runId
? { ...run, ownerProcessId: 2_147_483_647 }
: run,
run.runId === runId ? { ...run, ownerProcessId: 2_147_483_647 } : run,
)
await writeTempFile(tempDir, RUNS_REL, `${JSON.stringify(file, null, 2)}\n`)

View File

@@ -1,117 +1,117 @@
import { describe, expect, test } from "bun:test";
import { createBufferedWriter } from "../bufferedWriter";
import { describe, expect, test } from 'bun:test'
import { createBufferedWriter } from '../bufferedWriter'
describe("createBufferedWriter", () => {
test("immediateMode calls writeFn directly", () => {
const written: string[] = [];
describe('createBufferedWriter', () => {
test('immediateMode calls writeFn directly', () => {
const written: string[] = []
const writer = createBufferedWriter({
writeFn: (c) => written.push(c),
writeFn: c => written.push(c),
immediateMode: true,
});
writer.write("a");
writer.write("b");
expect(written).toEqual(["a", "b"]);
});
})
writer.write('a')
writer.write('b')
expect(written).toEqual(['a', 'b'])
})
test("buffered mode accumulates until flush", () => {
const written: string[] = [];
test('buffered mode accumulates until flush', () => {
const written: string[] = []
const writer = createBufferedWriter({
writeFn: (c) => written.push(c),
});
writer.write("hello ");
writer.write("world");
expect(written).toEqual([]);
writer.flush();
expect(written).toEqual(["hello world"]);
});
writeFn: c => written.push(c),
})
writer.write('hello ')
writer.write('world')
expect(written).toEqual([])
writer.flush()
expect(written).toEqual(['hello world'])
})
test("flush with empty buffer does not call writeFn", () => {
const written: string[] = [];
test('flush with empty buffer does not call writeFn', () => {
const written: string[] = []
const writer = createBufferedWriter({
writeFn: (c) => written.push(c),
});
writer.flush();
expect(written).toEqual([]);
});
writeFn: c => written.push(c),
})
writer.flush()
expect(written).toEqual([])
})
test("flush clears the buffer", () => {
const written: string[] = [];
test('flush clears the buffer', () => {
const written: string[] = []
const writer = createBufferedWriter({
writeFn: (c) => written.push(c),
});
writer.write("data");
writer.flush();
writer.flush(); // second flush should be no-op
expect(written).toEqual(["data"]);
});
writeFn: c => written.push(c),
})
writer.write('data')
writer.flush()
writer.flush() // second flush should be no-op
expect(written).toEqual(['data'])
})
test("overflow triggers deferred flush when maxBufferSize reached", () => {
const written: string[] = [];
test('overflow triggers deferred flush when maxBufferSize reached', () => {
const written: string[] = []
const writer = createBufferedWriter({
writeFn: (c) => written.push(c),
writeFn: c => written.push(c),
maxBufferSize: 2,
});
writer.write("a");
writer.write("b");
})
writer.write('a')
writer.write('b')
// 2 writes = maxBufferSize, triggers flushDeferred via setImmediate
expect(written).toEqual([]);
});
expect(written).toEqual([])
})
test("overflow triggers deferred flush when maxBufferBytes reached", () => {
const written: string[] = [];
test('overflow triggers deferred flush when maxBufferBytes reached', () => {
const written: string[] = []
const writer = createBufferedWriter({
writeFn: (c) => written.push(c),
writeFn: c => written.push(c),
maxBufferBytes: 5,
});
writer.write("abc");
writer.write("def");
})
writer.write('abc')
writer.write('def')
// total 6 bytes > 5, triggers flushDeferred
expect(written).toEqual([]);
});
expect(written).toEqual([])
})
test("dispose flushes remaining buffer", () => {
const written: string[] = [];
test('dispose flushes remaining buffer', () => {
const written: string[] = []
const writer = createBufferedWriter({
writeFn: (c) => written.push(c),
});
writer.write("final");
writer.dispose();
expect(written).toEqual(["final"]);
});
writeFn: c => written.push(c),
})
writer.write('final')
writer.dispose()
expect(written).toEqual(['final'])
})
test("dispose flushes pending overflow", () => {
const written: string[] = [];
test('dispose flushes pending overflow', () => {
const written: string[] = []
const writer = createBufferedWriter({
writeFn: (c) => written.push(c),
writeFn: c => written.push(c),
maxBufferSize: 1,
});
writer.write("overflow-data");
})
writer.write('overflow-data')
// overflow triggered but deferred; dispose should flush it synchronously
writer.dispose();
expect(written).toEqual(["overflow-data"]);
});
writer.dispose()
expect(written).toEqual(['overflow-data'])
})
test("coalesced overflow — multiple overflows merge before write", () => {
const written: string[] = [];
test('coalesced overflow — multiple overflows merge before write', () => {
const written: string[] = []
const writer = createBufferedWriter({
writeFn: (c) => written.push(c),
writeFn: c => written.push(c),
maxBufferSize: 1,
});
writer.write("a"); // triggers first overflow (deferred)
writer.write("b"); // pendingOverflow exists, coalesces
writer.dispose(); // flushes coalesced overflow
expect(written).toEqual(["ab"]);
});
})
writer.write('a') // triggers first overflow (deferred)
writer.write('b') // pendingOverflow exists, coalesces
writer.dispose() // flushes coalesced overflow
expect(written).toEqual(['ab'])
})
test("multiple flushes produce concatenated writes", () => {
const written: string[] = [];
test('multiple flushes produce concatenated writes', () => {
const written: string[] = []
const writer = createBufferedWriter({
writeFn: (c) => written.push(c),
});
writer.write("batch1");
writer.flush();
writer.write("batch2");
writer.flush();
expect(written).toEqual(["batch1", "batch2"]);
});
});
writeFn: c => written.push(c),
})
writer.write('batch1')
writer.flush()
writer.write('batch2')
writer.flush()
expect(written).toEqual(['batch1', 'batch2'])
})
})

View File

@@ -67,7 +67,9 @@ describe('Bun.hash Node.js polyfill (FNV-1a)', () => {
// unsigned int. They use different widths so direct equality is not expected.
// This test just verifies the native API exists and returns a numeric type.
if (typeof globalThis.Bun?.hash === 'function') {
const result = (globalThis.Bun.hash as (s: string) => bigint | number)('hello')
const result = (globalThis.Bun.hash as (s: string) => bigint | number)(
'hello',
)
expect(['number', 'bigint']).toContain(typeof result)
}
})

View File

@@ -1,148 +1,148 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
import {
stripHtmlComments,
isMemoryFilePath,
getLargeMemoryFiles,
MAX_MEMORY_CHARACTER_COUNT,
type MemoryFileInfo,
} from "../claudemd";
} from '../claudemd'
function mockMemoryFile(overrides: Partial<MemoryFileInfo> = {}): MemoryFileInfo {
function mockMemoryFile(
overrides: Partial<MemoryFileInfo> = {},
): MemoryFileInfo {
return {
path: "/project/CLAUDE.md",
type: "Project",
content: "test content",
path: '/project/CLAUDE.md',
type: 'Project',
content: 'test content',
...overrides,
};
}
}
describe("stripHtmlComments", () => {
test("strips block-level HTML comments (own line)", () => {
describe('stripHtmlComments', () => {
test('strips block-level HTML comments (own line)', () => {
// CommonMark type-2 HTML blocks: comment must start at beginning of line
const result = stripHtmlComments("text\n<!-- block comment -->\nmore");
expect(result.content).not.toContain("block comment");
expect(result.stripped).toBe(true);
});
const result = stripHtmlComments('text\n<!-- block comment -->\nmore')
expect(result.content).not.toContain('block comment')
expect(result.stripped).toBe(true)
})
test("returns stripped: false when no comments", () => {
const result = stripHtmlComments("no comments here");
expect(result.stripped).toBe(false);
expect(result.content).toBe("no comments here");
});
test('returns stripped: false when no comments', () => {
const result = stripHtmlComments('no comments here')
expect(result.stripped).toBe(false)
expect(result.content).toBe('no comments here')
})
test("returns stripped: true when block comments exist", () => {
const result = stripHtmlComments("hello\n<!-- world -->\nend");
expect(result.stripped).toBe(true);
});
test('returns stripped: true when block comments exist', () => {
const result = stripHtmlComments('hello\n<!-- world -->\nend')
expect(result.stripped).toBe(true)
})
test("handles empty string", () => {
const result = stripHtmlComments("");
expect(result.content).toBe("");
expect(result.stripped).toBe(false);
});
test('handles empty string', () => {
const result = stripHtmlComments('')
expect(result.content).toBe('')
expect(result.stripped).toBe(false)
})
test("handles multiple block comments", () => {
const result = stripHtmlComments(
"a\n<!-- c1 -->\nb\n<!-- c2 -->\nc"
);
expect(result.content).not.toContain("c1");
expect(result.content).not.toContain("c2");
expect(result.stripped).toBe(true);
});
test('handles multiple block comments', () => {
const result = stripHtmlComments('a\n<!-- c1 -->\nb\n<!-- c2 -->\nc')
expect(result.content).not.toContain('c1')
expect(result.content).not.toContain('c2')
expect(result.stripped).toBe(true)
})
test("preserves code block content", () => {
const input = "text\n```html\n<!-- not stripped -->\n```\nmore";
const result = stripHtmlComments(input);
expect(result.content).toContain("<!-- not stripped -->");
});
test('preserves code block content', () => {
const input = 'text\n```html\n<!-- not stripped -->\n```\nmore'
const result = stripHtmlComments(input)
expect(result.content).toContain('<!-- not stripped -->')
})
test("preserves inline comments within paragraphs", () => {
test('preserves inline comments within paragraphs', () => {
// Inline comments are NOT stripped (CommonMark paragraph semantics)
const result = stripHtmlComments("text <!-- inline --> more");
expect(result.content).toContain("<!-- inline -->");
expect(result.stripped).toBe(false);
});
const result = stripHtmlComments('text <!-- inline --> more')
expect(result.content).toContain('<!-- inline -->')
expect(result.stripped).toBe(false)
})
test("leaves unclosed HTML comment unchanged", () => {
const result = stripHtmlComments("<!-- no close some text");
expect(result.content).toBe("<!-- no close some text");
expect(result.stripped).toBe(false);
});
test('leaves unclosed HTML comment unchanged', () => {
const result = stripHtmlComments('<!-- no close some text')
expect(result.content).toBe('<!-- no close some text')
expect(result.stripped).toBe(false)
})
test("strips comment and keeps same-line residual content", () => {
const result = stripHtmlComments("<!-- note -->some text");
expect(result.content).toContain("some text");
expect(result.content).not.toContain("<!--");
expect(result.stripped).toBe(true);
});
});
test('strips comment and keeps same-line residual content', () => {
const result = stripHtmlComments('<!-- note -->some text')
expect(result.content).toContain('some text')
expect(result.content).not.toContain('<!--')
expect(result.stripped).toBe(true)
})
})
describe("isMemoryFilePath", () => {
test("returns true for CLAUDE.md path", () => {
expect(isMemoryFilePath("/project/CLAUDE.md")).toBe(true);
});
describe('isMemoryFilePath', () => {
test('returns true for CLAUDE.md path', () => {
expect(isMemoryFilePath('/project/CLAUDE.md')).toBe(true)
})
test("returns true for CLAUDE.local.md path", () => {
expect(isMemoryFilePath("/project/CLAUDE.local.md")).toBe(true);
});
test('returns true for CLAUDE.local.md path', () => {
expect(isMemoryFilePath('/project/CLAUDE.local.md')).toBe(true)
})
test("returns true for .claude/rules/ path", () => {
expect(isMemoryFilePath("/project/.claude/rules/foo.md")).toBe(true);
});
test('returns true for .claude/rules/ path', () => {
expect(isMemoryFilePath('/project/.claude/rules/foo.md')).toBe(true)
})
test("returns false for regular file", () => {
expect(isMemoryFilePath("/project/src/main.ts")).toBe(false);
});
test('returns false for regular file', () => {
expect(isMemoryFilePath('/project/src/main.ts')).toBe(false)
})
test("returns false for unrelated .md file", () => {
expect(isMemoryFilePath("/project/README.md")).toBe(false);
});
test('returns false for unrelated .md file', () => {
expect(isMemoryFilePath('/project/README.md')).toBe(false)
})
test("returns false for .claude directory non-rules file", () => {
expect(isMemoryFilePath("/project/.claude/settings.json")).toBe(false);
});
test('returns false for .claude directory non-rules file', () => {
expect(isMemoryFilePath('/project/.claude/settings.json')).toBe(false)
})
test("returns false for lowercase claude.md (case-sensitive match)", () => {
expect(isMemoryFilePath("/project/claude.md")).toBe(false);
});
test('returns false for lowercase claude.md (case-sensitive match)', () => {
expect(isMemoryFilePath('/project/claude.md')).toBe(false)
})
test("returns false for non-.md file in .claude/rules/", () => {
expect(isMemoryFilePath(".claude/rules/foo.txt")).toBe(false);
});
});
test('returns false for non-.md file in .claude/rules/', () => {
expect(isMemoryFilePath('.claude/rules/foo.txt')).toBe(false)
})
})
describe("getLargeMemoryFiles", () => {
test("returns files exceeding threshold", () => {
const largeContent = "x".repeat(MAX_MEMORY_CHARACTER_COUNT + 1);
describe('getLargeMemoryFiles', () => {
test('returns files exceeding threshold', () => {
const largeContent = 'x'.repeat(MAX_MEMORY_CHARACTER_COUNT + 1)
const files = [
mockMemoryFile({ content: "small" }),
mockMemoryFile({ content: largeContent, path: "/big.md" }),
];
const result = getLargeMemoryFiles(files);
expect(result).toHaveLength(1);
expect(result[0].path).toBe("/big.md");
});
mockMemoryFile({ content: 'small' }),
mockMemoryFile({ content: largeContent, path: '/big.md' }),
]
const result = getLargeMemoryFiles(files)
expect(result).toHaveLength(1)
expect(result[0].path).toBe('/big.md')
})
test("returns empty array when all files are small", () => {
test('returns empty array when all files are small', () => {
const files = [
mockMemoryFile({ content: "small" }),
mockMemoryFile({ content: "also small" }),
];
expect(getLargeMemoryFiles(files)).toEqual([]);
});
mockMemoryFile({ content: 'small' }),
mockMemoryFile({ content: 'also small' }),
]
expect(getLargeMemoryFiles(files)).toEqual([])
})
test("correctly identifies threshold boundary", () => {
const atThreshold = "x".repeat(MAX_MEMORY_CHARACTER_COUNT);
const overThreshold = "x".repeat(MAX_MEMORY_CHARACTER_COUNT + 1);
test('correctly identifies threshold boundary', () => {
const atThreshold = 'x'.repeat(MAX_MEMORY_CHARACTER_COUNT)
const overThreshold = 'x'.repeat(MAX_MEMORY_CHARACTER_COUNT + 1)
const files = [
mockMemoryFile({ content: atThreshold }),
mockMemoryFile({ content: overThreshold }),
];
const result = getLargeMemoryFiles(files);
expect(result).toHaveLength(1);
});
]
const result = getLargeMemoryFiles(files)
expect(result).toHaveLength(1)
})
test("returns empty array for empty input", () => {
expect(getLargeMemoryFiles([])).toEqual([]);
});
});
test('returns empty array for empty input', () => {
expect(getLargeMemoryFiles([])).toEqual([])
})
})

View File

@@ -1,136 +1,138 @@
import { describe, expect, test } from "bun:test";
import { collapseHookSummaries } from "../collapseHookSummaries";
import { describe, expect, test } from 'bun:test'
import { collapseHookSummaries } from '../collapseHookSummaries'
function makeHookSummary(overrides: Partial<{
hookLabel: string;
hookCount: number;
hookInfos: any[];
hookErrors: any[];
preventedContinuation: boolean;
hasOutput: boolean;
totalDurationMs: number;
}> = {}): any {
function makeHookSummary(
overrides: Partial<{
hookLabel: string
hookCount: number
hookInfos: any[]
hookErrors: any[]
preventedContinuation: boolean
hasOutput: boolean
totalDurationMs: number
}> = {},
): any {
return {
type: "system",
subtype: "stop_hook_summary",
hookLabel: overrides.hookLabel ?? "PostToolUse",
type: 'system',
subtype: 'stop_hook_summary',
hookLabel: overrides.hookLabel ?? 'PostToolUse',
hookCount: overrides.hookCount ?? 1,
hookInfos: overrides.hookInfos ?? [],
hookErrors: overrides.hookErrors ?? [],
preventedContinuation: overrides.preventedContinuation ?? false,
hasOutput: overrides.hasOutput ?? false,
totalDurationMs: overrides.totalDurationMs ?? 10,
};
}
}
function makeNonHookMessage(): any {
return { type: "user", message: { content: "hello" } };
return { type: 'user', message: { content: 'hello' } }
}
describe("collapseHookSummaries", () => {
test("returns same messages when no hook summaries", () => {
const messages = [makeNonHookMessage(), makeNonHookMessage()];
expect(collapseHookSummaries(messages)).toEqual(messages);
});
describe('collapseHookSummaries', () => {
test('returns same messages when no hook summaries', () => {
const messages = [makeNonHookMessage(), makeNonHookMessage()]
expect(collapseHookSummaries(messages)).toEqual(messages)
})
test("collapses consecutive messages with same hookLabel", () => {
test('collapses consecutive messages with same hookLabel', () => {
const messages = [
makeHookSummary({ hookLabel: "PostToolUse", hookCount: 1 }),
makeHookSummary({ hookLabel: "PostToolUse", hookCount: 2 }),
];
const result = collapseHookSummaries(messages);
expect(result).toHaveLength(1);
expect(result[0].hookCount).toBe(3);
});
makeHookSummary({ hookLabel: 'PostToolUse', hookCount: 1 }),
makeHookSummary({ hookLabel: 'PostToolUse', hookCount: 2 }),
]
const result = collapseHookSummaries(messages)
expect(result).toHaveLength(1)
expect(result[0].hookCount).toBe(3)
})
test("does not collapse messages with different hookLabels", () => {
test('does not collapse messages with different hookLabels', () => {
const messages = [
makeHookSummary({ hookLabel: "PostToolUse" }),
makeHookSummary({ hookLabel: "PreToolUse" }),
];
const result = collapseHookSummaries(messages);
expect(result).toHaveLength(2);
});
makeHookSummary({ hookLabel: 'PostToolUse' }),
makeHookSummary({ hookLabel: 'PreToolUse' }),
]
const result = collapseHookSummaries(messages)
expect(result).toHaveLength(2)
})
test("aggregates hookCount across collapsed messages", () => {
test('aggregates hookCount across collapsed messages', () => {
const messages = [
makeHookSummary({ hookLabel: "A", hookCount: 3 }),
makeHookSummary({ hookLabel: "A", hookCount: 5 }),
];
const result = collapseHookSummaries(messages);
expect(result[0].hookCount).toBe(8);
});
makeHookSummary({ hookLabel: 'A', hookCount: 3 }),
makeHookSummary({ hookLabel: 'A', hookCount: 5 }),
]
const result = collapseHookSummaries(messages)
expect(result[0].hookCount).toBe(8)
})
test("merges hookInfos arrays", () => {
const info1 = { tool: "Read" };
const info2 = { tool: "Write" };
test('merges hookInfos arrays', () => {
const info1 = { tool: 'Read' }
const info2 = { tool: 'Write' }
const messages = [
makeHookSummary({ hookLabel: "A", hookInfos: [info1] }),
makeHookSummary({ hookLabel: "A", hookInfos: [info2] }),
];
const result = collapseHookSummaries(messages);
expect(result[0].hookInfos).toEqual([info1, info2]);
});
makeHookSummary({ hookLabel: 'A', hookInfos: [info1] }),
makeHookSummary({ hookLabel: 'A', hookInfos: [info2] }),
]
const result = collapseHookSummaries(messages)
expect(result[0].hookInfos).toEqual([info1, info2])
})
test("merges hookErrors arrays", () => {
const err1 = new Error("e1");
const err2 = new Error("e2");
test('merges hookErrors arrays', () => {
const err1 = new Error('e1')
const err2 = new Error('e2')
const messages = [
makeHookSummary({ hookLabel: "A", hookErrors: [err1] }),
makeHookSummary({ hookLabel: "A", hookErrors: [err2] }),
];
const result = collapseHookSummaries(messages);
expect(result[0].hookErrors).toHaveLength(2);
});
makeHookSummary({ hookLabel: 'A', hookErrors: [err1] }),
makeHookSummary({ hookLabel: 'A', hookErrors: [err2] }),
]
const result = collapseHookSummaries(messages)
expect(result[0].hookErrors).toHaveLength(2)
})
test("takes max totalDurationMs", () => {
test('takes max totalDurationMs', () => {
const messages = [
makeHookSummary({ hookLabel: "A", totalDurationMs: 50 }),
makeHookSummary({ hookLabel: "A", totalDurationMs: 100 }),
makeHookSummary({ hookLabel: "A", totalDurationMs: 75 }),
];
const result = collapseHookSummaries(messages);
expect(result[0].totalDurationMs).toBe(100);
});
makeHookSummary({ hookLabel: 'A', totalDurationMs: 50 }),
makeHookSummary({ hookLabel: 'A', totalDurationMs: 100 }),
makeHookSummary({ hookLabel: 'A', totalDurationMs: 75 }),
]
const result = collapseHookSummaries(messages)
expect(result[0].totalDurationMs).toBe(100)
})
test("takes any truthy preventContinuation", () => {
test('takes any truthy preventContinuation', () => {
const messages = [
makeHookSummary({ hookLabel: "A", preventedContinuation: false }),
makeHookSummary({ hookLabel: "A", preventedContinuation: true }),
];
const result = collapseHookSummaries(messages);
expect(result[0].preventedContinuation).toBe(true);
});
makeHookSummary({ hookLabel: 'A', preventedContinuation: false }),
makeHookSummary({ hookLabel: 'A', preventedContinuation: true }),
]
const result = collapseHookSummaries(messages)
expect(result[0].preventedContinuation).toBe(true)
})
test("leaves single hook summary unchanged", () => {
const msg = makeHookSummary({ hookLabel: "PostToolUse", hookCount: 5 });
const result = collapseHookSummaries([msg]);
expect(result).toHaveLength(1);
expect(result[0].hookCount).toBe(5);
});
test('leaves single hook summary unchanged', () => {
const msg = makeHookSummary({ hookLabel: 'PostToolUse', hookCount: 5 })
const result = collapseHookSummaries([msg])
expect(result).toHaveLength(1)
expect(result[0].hookCount).toBe(5)
})
test("handles three consecutive same-label summaries", () => {
test('handles three consecutive same-label summaries', () => {
const messages = [
makeHookSummary({ hookLabel: "X", hookCount: 1 }),
makeHookSummary({ hookLabel: "X", hookCount: 1 }),
makeHookSummary({ hookLabel: "X", hookCount: 1 }),
];
const result = collapseHookSummaries(messages);
expect(result).toHaveLength(1);
expect(result[0].hookCount).toBe(3);
});
makeHookSummary({ hookLabel: 'X', hookCount: 1 }),
makeHookSummary({ hookLabel: 'X', hookCount: 1 }),
makeHookSummary({ hookLabel: 'X', hookCount: 1 }),
]
const result = collapseHookSummaries(messages)
expect(result).toHaveLength(1)
expect(result[0].hookCount).toBe(3)
})
test("preserves non-hook messages in between", () => {
test('preserves non-hook messages in between', () => {
const messages = [
makeHookSummary({ hookLabel: "A" }),
makeHookSummary({ hookLabel: 'A' }),
makeNonHookMessage(),
makeHookSummary({ hookLabel: "A" }),
];
const result = collapseHookSummaries(messages);
expect(result).toHaveLength(3);
});
makeHookSummary({ hookLabel: 'A' }),
]
const result = collapseHookSummaries(messages)
expect(result).toHaveLength(3)
})
test("returns empty array for empty input", () => {
expect(collapseHookSummaries([])).toEqual([]);
});
});
test('returns empty array for empty input', () => {
expect(collapseHookSummaries([])).toEqual([])
})
})

View File

@@ -1,94 +1,111 @@
import { describe, expect, test } from "bun:test";
import { collapseTeammateShutdowns } from "../collapseTeammateShutdowns";
import { describe, expect, test } from 'bun:test'
import { collapseTeammateShutdowns } from '../collapseTeammateShutdowns'
function makeShutdownMsg(uuid = "1"): any {
function makeShutdownMsg(uuid = '1'): any {
return {
type: "attachment",
type: 'attachment',
uuid,
timestamp: Date.now(),
attachment: {
type: "task_status",
taskType: "in_process_teammate",
status: "completed",
type: 'task_status',
taskType: 'in_process_teammate',
status: 'completed',
},
};
}
}
function makeNonShutdownMsg(): any {
return { type: "user", message: { content: "hello" } };
return { type: 'user', message: { content: 'hello' } }
}
describe("collapseTeammateShutdowns", () => {
test("returns same messages when no teammate shutdowns", () => {
const msgs = [makeNonShutdownMsg(), makeNonShutdownMsg()];
expect(collapseTeammateShutdowns(msgs)).toEqual(msgs);
});
describe('collapseTeammateShutdowns', () => {
test('returns same messages when no teammate shutdowns', () => {
const msgs = [makeNonShutdownMsg(), makeNonShutdownMsg()]
expect(collapseTeammateShutdowns(msgs)).toEqual(msgs)
})
test("leaves single shutdown message unchanged", () => {
const msgs = [makeShutdownMsg()];
const result = collapseTeammateShutdowns(msgs);
expect(result).toHaveLength(1);
expect(result[0]).toEqual(msgs[0]);
});
test('leaves single shutdown message unchanged', () => {
const msgs = [makeShutdownMsg()]
const result = collapseTeammateShutdowns(msgs)
expect(result).toHaveLength(1)
expect(result[0]).toEqual(msgs[0])
})
test("collapses consecutive shutdown messages into batch", () => {
const msgs = [makeShutdownMsg("1"), makeShutdownMsg("2")];
const result = collapseTeammateShutdowns(msgs);
expect(result).toHaveLength(1);
expect((result[0] as any).attachment.type).toBe("teammate_shutdown_batch");
});
test('collapses consecutive shutdown messages into batch', () => {
const msgs = [makeShutdownMsg('1'), makeShutdownMsg('2')]
const result = collapseTeammateShutdowns(msgs)
expect(result).toHaveLength(1)
expect((result[0] as any).attachment.type).toBe('teammate_shutdown_batch')
})
test("batch attachment has correct count", () => {
const msgs = [makeShutdownMsg("1"), makeShutdownMsg("2"), makeShutdownMsg("3")];
const result = collapseTeammateShutdowns(msgs);
expect((result[0] as any).attachment.count).toBe(3);
});
test('batch attachment has correct count', () => {
const msgs = [
makeShutdownMsg('1'),
makeShutdownMsg('2'),
makeShutdownMsg('3'),
]
const result = collapseTeammateShutdowns(msgs)
expect((result[0] as any).attachment.count).toBe(3)
})
test("does not collapse non-consecutive shutdowns", () => {
const msgs = [makeShutdownMsg("1"), makeNonShutdownMsg(), makeShutdownMsg("2")];
const result = collapseTeammateShutdowns(msgs);
expect(result).toHaveLength(3);
expect((result[0] as any).attachment.type).toBe("task_status");
expect((result[2] as any).attachment.type).toBe("task_status");
});
test('does not collapse non-consecutive shutdowns', () => {
const msgs = [
makeShutdownMsg('1'),
makeNonShutdownMsg(),
makeShutdownMsg('2'),
]
const result = collapseTeammateShutdowns(msgs)
expect(result).toHaveLength(3)
expect((result[0] as any).attachment.type).toBe('task_status')
expect((result[2] as any).attachment.type).toBe('task_status')
})
test("preserves non-shutdown messages between shutdowns", () => {
const msgs = [makeShutdownMsg("1"), makeNonShutdownMsg(), makeShutdownMsg("2")];
const result = collapseTeammateShutdowns(msgs);
expect(result[1]).toEqual(makeNonShutdownMsg());
});
test('preserves non-shutdown messages between shutdowns', () => {
const msgs = [
makeShutdownMsg('1'),
makeNonShutdownMsg(),
makeShutdownMsg('2'),
]
const result = collapseTeammateShutdowns(msgs)
expect(result[1]).toEqual(makeNonShutdownMsg())
})
test("handles empty array", () => {
expect(collapseTeammateShutdowns([])).toEqual([]);
});
test('handles empty array', () => {
expect(collapseTeammateShutdowns([])).toEqual([])
})
test("handles mixed message types", () => {
const msgs = [makeNonShutdownMsg(), makeShutdownMsg("1"), makeShutdownMsg("2"), makeNonShutdownMsg()];
const result = collapseTeammateShutdowns(msgs);
expect(result).toHaveLength(3);
expect((result[1] as any).attachment.type).toBe("teammate_shutdown_batch");
});
test('handles mixed message types', () => {
const msgs = [
makeNonShutdownMsg(),
makeShutdownMsg('1'),
makeShutdownMsg('2'),
makeNonShutdownMsg(),
]
const result = collapseTeammateShutdowns(msgs)
expect(result).toHaveLength(3)
expect((result[1] as any).attachment.type).toBe('teammate_shutdown_batch')
})
test("collapses more than 2 consecutive shutdowns", () => {
const msgs = Array.from({ length: 5 }, (_, i) => makeShutdownMsg(String(i)));
const result = collapseTeammateShutdowns(msgs);
expect(result).toHaveLength(1);
expect((result[0] as any).attachment.count).toBe(5);
});
test('collapses more than 2 consecutive shutdowns', () => {
const msgs = Array.from({ length: 5 }, (_, i) => makeShutdownMsg(String(i)))
const result = collapseTeammateShutdowns(msgs)
expect(result).toHaveLength(1)
expect((result[0] as any).attachment.count).toBe(5)
})
test("non-teammate task_status messages are not collapsed", () => {
test('non-teammate task_status messages are not collapsed', () => {
const nonTeammate: any = {
type: "attachment",
uuid: "x",
type: 'attachment',
uuid: 'x',
timestamp: Date.now(),
attachment: {
type: "task_status",
taskType: "subagent",
status: "completed",
type: 'task_status',
taskType: 'subagent',
status: 'completed',
},
};
const msgs = [nonTeammate, { ...nonTeammate, uuid: "y" }];
const result = collapseTeammateShutdowns(msgs);
expect(result).toHaveLength(2);
});
});
}
const msgs = [nonTeammate, { ...nonTeammate, uuid: 'y' }]
const result = collapseTeammateShutdowns(msgs)
expect(result).toHaveLength(2)
})
})

View File

@@ -1,70 +1,70 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
import {
NOTIFICATION_CHANNELS,
EDITOR_MODES,
TEAMMATE_MODES,
} from "../configConstants";
} from '../configConstants'
describe("NOTIFICATION_CHANNELS", () => {
test("contains expected channels", () => {
expect(NOTIFICATION_CHANNELS).toContain("auto");
expect(NOTIFICATION_CHANNELS).toContain("iterm2");
expect(NOTIFICATION_CHANNELS).toContain("terminal_bell");
expect(NOTIFICATION_CHANNELS).toContain("kitty");
expect(NOTIFICATION_CHANNELS).toContain("ghostty");
});
describe('NOTIFICATION_CHANNELS', () => {
test('contains expected channels', () => {
expect(NOTIFICATION_CHANNELS).toContain('auto')
expect(NOTIFICATION_CHANNELS).toContain('iterm2')
expect(NOTIFICATION_CHANNELS).toContain('terminal_bell')
expect(NOTIFICATION_CHANNELS).toContain('kitty')
expect(NOTIFICATION_CHANNELS).toContain('ghostty')
})
test("is readonly array", () => {
expect(Array.isArray(NOTIFICATION_CHANNELS)).toBe(true);
test('is readonly array', () => {
expect(Array.isArray(NOTIFICATION_CHANNELS)).toBe(true)
// TypeScript enforces readonly at compile time; runtime is still a plain array
expect(NOTIFICATION_CHANNELS.length).toBeGreaterThan(0);
});
expect(NOTIFICATION_CHANNELS.length).toBeGreaterThan(0)
})
test("includes all documented channels", () => {
test('includes all documented channels', () => {
expect(NOTIFICATION_CHANNELS).toEqual([
"auto",
"iterm2",
"iterm2_with_bell",
"terminal_bell",
"kitty",
"ghostty",
"notifications_disabled",
]);
});
'auto',
'iterm2',
'iterm2_with_bell',
'terminal_bell',
'kitty',
'ghostty',
'notifications_disabled',
])
})
test("has no duplicate entries", () => {
const unique = new Set(NOTIFICATION_CHANNELS);
expect(unique.size).toBe(NOTIFICATION_CHANNELS.length);
});
});
test('has no duplicate entries', () => {
const unique = new Set(NOTIFICATION_CHANNELS)
expect(unique.size).toBe(NOTIFICATION_CHANNELS.length)
})
})
describe("EDITOR_MODES", () => {
describe('EDITOR_MODES', () => {
test("contains 'normal' and 'vim'", () => {
expect(EDITOR_MODES).toContain("normal");
expect(EDITOR_MODES).toContain("vim");
});
expect(EDITOR_MODES).toContain('normal')
expect(EDITOR_MODES).toContain('vim')
})
test("has exactly 2 entries", () => {
expect(EDITOR_MODES).toHaveLength(2);
});
test('has exactly 2 entries', () => {
expect(EDITOR_MODES).toHaveLength(2)
})
test("is ordered: normal, vim", () => {
expect(EDITOR_MODES).toEqual(["normal", "vim"]);
});
});
test('is ordered: normal, vim', () => {
expect(EDITOR_MODES).toEqual(['normal', 'vim'])
})
})
describe("TEAMMATE_MODES", () => {
describe('TEAMMATE_MODES', () => {
test("contains 'auto', 'tmux', 'in-process'", () => {
expect(TEAMMATE_MODES).toContain("auto");
expect(TEAMMATE_MODES).toContain("tmux");
expect(TEAMMATE_MODES).toContain("in-process");
});
expect(TEAMMATE_MODES).toContain('auto')
expect(TEAMMATE_MODES).toContain('tmux')
expect(TEAMMATE_MODES).toContain('in-process')
})
test("has exactly 3 entries", () => {
expect(TEAMMATE_MODES).toHaveLength(3);
});
test('has exactly 3 entries', () => {
expect(TEAMMATE_MODES).toHaveLength(3)
})
test("is ordered: auto, tmux, in-process", () => {
expect(TEAMMATE_MODES).toEqual(["auto", "tmux", "in-process"]);
});
});
test('is ordered: auto, tmux, in-process', () => {
expect(TEAMMATE_MODES).toEqual(['auto', 'tmux', 'in-process'])
})
})

View File

@@ -1,72 +1,72 @@
import { describe, expect, test } from "bun:test";
import { insertBlockAfterToolResults } from "../contentArray";
import { describe, expect, test } from 'bun:test'
import { insertBlockAfterToolResults } from '../contentArray'
describe("insertBlockAfterToolResults", () => {
test("inserts after last tool_result", () => {
describe('insertBlockAfterToolResults', () => {
test('inserts after last tool_result', () => {
const content: any[] = [
{ type: "tool_result", content: "r1" },
{ type: "text", text: "hello" },
];
insertBlockAfterToolResults(content, { type: "text", text: "inserted" });
expect(content[1]).toEqual({ type: "text", text: "inserted" });
expect(content).toHaveLength(3);
});
{ type: 'tool_result', content: 'r1' },
{ type: 'text', text: 'hello' },
]
insertBlockAfterToolResults(content, { type: 'text', text: 'inserted' })
expect(content[1]).toEqual({ type: 'text', text: 'inserted' })
expect(content).toHaveLength(3)
})
test("inserts after last of multiple tool_results", () => {
test('inserts after last of multiple tool_results', () => {
const content: any[] = [
{ type: "tool_result", content: "r1" },
{ type: "tool_result", content: "r2" },
{ type: "text", text: "end" },
];
insertBlockAfterToolResults(content, { type: "text", text: "new" });
expect(content[2]).toEqual({ type: "text", text: "new" });
});
{ type: 'tool_result', content: 'r1' },
{ type: 'tool_result', content: 'r2' },
{ type: 'text', text: 'end' },
]
insertBlockAfterToolResults(content, { type: 'text', text: 'new' })
expect(content[2]).toEqual({ type: 'text', text: 'new' })
})
test("appends continuation when inserted block would be last", () => {
const content: any[] = [{ type: "tool_result", content: "r1" }];
insertBlockAfterToolResults(content, { type: "text", text: "new" });
expect(content).toHaveLength(3); // original + inserted + continuation
expect(content[2]).toEqual({ type: "text", text: "." });
});
test('appends continuation when inserted block would be last', () => {
const content: any[] = [{ type: 'tool_result', content: 'r1' }]
insertBlockAfterToolResults(content, { type: 'text', text: 'new' })
expect(content).toHaveLength(3) // original + inserted + continuation
expect(content[2]).toEqual({ type: 'text', text: '.' })
})
test("inserts before last block when no tool_results", () => {
test('inserts before last block when no tool_results', () => {
const content: any[] = [
{ type: "text", text: "a" },
{ type: "text", text: "b" },
];
insertBlockAfterToolResults(content, { type: "text", text: "new" });
expect(content[1]).toEqual({ type: "text", text: "new" });
expect(content).toHaveLength(3);
});
{ type: 'text', text: 'a' },
{ type: 'text', text: 'b' },
]
insertBlockAfterToolResults(content, { type: 'text', text: 'new' })
expect(content[1]).toEqual({ type: 'text', text: 'new' })
expect(content).toHaveLength(3)
})
test("handles empty array", () => {
const content: any[] = [];
insertBlockAfterToolResults(content, { type: "text", text: "new" });
expect(content).toHaveLength(1);
expect(content[0]).toEqual({ type: "text", text: "new" });
});
test('handles empty array', () => {
const content: any[] = []
insertBlockAfterToolResults(content, { type: 'text', text: 'new' })
expect(content).toHaveLength(1)
expect(content[0]).toEqual({ type: 'text', text: 'new' })
})
test("handles single element array with no tool_result", () => {
const content: any[] = [{ type: "text", text: "only" }];
insertBlockAfterToolResults(content, { type: "text", text: "new" });
expect(content[0]).toEqual({ type: "text", text: "new" });
expect(content[1]).toEqual({ type: "text", text: "only" });
});
test('handles single element array with no tool_result', () => {
const content: any[] = [{ type: 'text', text: 'only' }]
insertBlockAfterToolResults(content, { type: 'text', text: 'new' })
expect(content[0]).toEqual({ type: 'text', text: 'new' })
expect(content[1]).toEqual({ type: 'text', text: 'only' })
})
test("inserts after last tool_result with mixed interleaving", () => {
test('inserts after last tool_result with mixed interleaving', () => {
const content: any[] = [
{ type: "tool_result", content: "r1" },
{ type: "text", text: "mid1" },
{ type: "tool_result", content: "r2" },
{ type: "text", text: "mid2" },
{ type: "tool_result", content: "r3" },
{ type: "text", text: "end" },
];
insertBlockAfterToolResults(content, { type: "text", text: "inserted" });
{ type: 'tool_result', content: 'r1' },
{ type: 'text', text: 'mid1' },
{ type: 'tool_result', content: 'r2' },
{ type: 'text', text: 'mid2' },
{ type: 'tool_result', content: 'r3' },
{ type: 'text', text: 'end' },
]
insertBlockAfterToolResults(content, { type: 'text', text: 'inserted' })
// Inserted after r3 (index 4), so at index 5
expect(content[5]).toEqual({ type: "text", text: "inserted" });
expect(content[5]).toEqual({ type: 'text', text: 'inserted' })
// Original end text should shift to index 6
expect(content[6]).toEqual({ type: "text", text: "end" });
expect(content).toHaveLength(7);
});
});
expect(content[6]).toEqual({ type: 'text', text: 'end' })
expect(content).toHaveLength(7)
})
})

View File

@@ -1,103 +1,103 @@
import { describe, expect, test } from "bun:test";
import { normalizeControlMessageKeys } from "../controlMessageCompat";
import { describe, expect, test } from 'bun:test'
import { normalizeControlMessageKeys } from '../controlMessageCompat'
describe("normalizeControlMessageKeys", () => {
describe('normalizeControlMessageKeys', () => {
// --- basic camelCase to snake_case ---
test("converts requestId to request_id", () => {
const obj = { requestId: "123" };
const result = normalizeControlMessageKeys(obj);
expect(result).toEqual({ request_id: "123" });
expect((result as any).requestId).toBeUndefined();
});
test('converts requestId to request_id', () => {
const obj = { requestId: '123' }
const result = normalizeControlMessageKeys(obj)
expect(result).toEqual({ request_id: '123' })
expect((result as any).requestId).toBeUndefined()
})
test("leaves request_id unchanged", () => {
const obj = { request_id: "123" };
normalizeControlMessageKeys(obj);
expect(obj).toEqual({ request_id: "123" });
});
test('leaves request_id unchanged', () => {
const obj = { request_id: '123' }
normalizeControlMessageKeys(obj)
expect(obj).toEqual({ request_id: '123' })
})
// --- both present: snake_case wins ---
test("keeps snake_case when both requestId and request_id exist", () => {
const obj = { requestId: "camel", request_id: "snake" };
const result = normalizeControlMessageKeys(obj) as any;
expect(result.request_id).toBe("snake");
test('keeps snake_case when both requestId and request_id exist', () => {
const obj = { requestId: 'camel', request_id: 'snake' }
const result = normalizeControlMessageKeys(obj) as any
expect(result.request_id).toBe('snake')
// requestId is NOT deleted when request_id already exists
// because the condition `!('request_id' in record)` prevents the branch
expect(result.requestId).toBe("camel");
});
expect(result.requestId).toBe('camel')
})
// --- nested response ---
test("normalizes nested response.requestId", () => {
const obj = { response: { requestId: "456" } };
normalizeControlMessageKeys(obj);
expect((obj as any).response.request_id).toBe("456");
expect((obj as any).response.requestId).toBeUndefined();
});
test('normalizes nested response.requestId', () => {
const obj = { response: { requestId: '456' } }
normalizeControlMessageKeys(obj)
expect((obj as any).response.request_id).toBe('456')
expect((obj as any).response.requestId).toBeUndefined()
})
test("leaves nested response.request_id unchanged", () => {
const obj = { response: { request_id: "789" } };
normalizeControlMessageKeys(obj);
expect((obj as any).response.request_id).toBe("789");
});
test('leaves nested response.request_id unchanged', () => {
const obj = { response: { request_id: '789' } }
normalizeControlMessageKeys(obj)
expect((obj as any).response.request_id).toBe('789')
})
test("nested response: snake_case wins when both present", () => {
test('nested response: snake_case wins when both present', () => {
const obj = {
response: { requestId: "camel", request_id: "snake" },
};
normalizeControlMessageKeys(obj);
expect((obj as any).response.request_id).toBe("snake");
expect((obj as any).response.requestId).toBe("camel");
});
response: { requestId: 'camel', request_id: 'snake' },
}
normalizeControlMessageKeys(obj)
expect((obj as any).response.request_id).toBe('snake')
expect((obj as any).response.requestId).toBe('camel')
})
// --- non-object inputs ---
test("returns null as-is", () => {
expect(normalizeControlMessageKeys(null)).toBeNull();
});
test('returns null as-is', () => {
expect(normalizeControlMessageKeys(null)).toBeNull()
})
test("returns undefined as-is", () => {
expect(normalizeControlMessageKeys(undefined)).toBeUndefined();
});
test('returns undefined as-is', () => {
expect(normalizeControlMessageKeys(undefined)).toBeUndefined()
})
test("returns string as-is", () => {
expect(normalizeControlMessageKeys("hello")).toBe("hello");
});
test('returns string as-is', () => {
expect(normalizeControlMessageKeys('hello')).toBe('hello')
})
test("returns number as-is", () => {
expect(normalizeControlMessageKeys(42)).toBe(42);
});
test('returns number as-is', () => {
expect(normalizeControlMessageKeys(42)).toBe(42)
})
// --- empty and edge cases ---
test("empty object is unchanged", () => {
const obj = {};
normalizeControlMessageKeys(obj);
expect(obj).toEqual({});
});
test('empty object is unchanged', () => {
const obj = {}
normalizeControlMessageKeys(obj)
expect(obj).toEqual({})
})
test("mutates the original object in place", () => {
const obj: Record<string, unknown> = { requestId: "abc", other: "data" };
const result = normalizeControlMessageKeys(obj);
expect(result).toBe(obj); // same reference
expect(obj).toEqual({ request_id: "abc", other: "data" });
});
test('mutates the original object in place', () => {
const obj: Record<string, unknown> = { requestId: 'abc', other: 'data' }
const result = normalizeControlMessageKeys(obj)
expect(result).toBe(obj) // same reference
expect(obj).toEqual({ request_id: 'abc', other: 'data' })
})
test("does not affect other keys on the object", () => {
const obj = { requestId: "123", type: "control_request", payload: {} };
normalizeControlMessageKeys(obj);
expect((obj as any).type).toBe("control_request");
expect((obj as any).payload).toEqual({});
expect((obj as any).request_id).toBe("123");
});
test('does not affect other keys on the object', () => {
const obj = { requestId: '123', type: 'control_request', payload: {} }
normalizeControlMessageKeys(obj)
expect((obj as any).type).toBe('control_request')
expect((obj as any).payload).toEqual({})
expect((obj as any).request_id).toBe('123')
})
test("handles response being null", () => {
const obj = { response: null, requestId: "x" };
normalizeControlMessageKeys(obj);
expect((obj as any).request_id).toBe("x");
expect((obj as any).response).toBeNull();
});
test('handles response being null', () => {
const obj = { response: null, requestId: 'x' }
normalizeControlMessageKeys(obj)
expect((obj as any).request_id).toBe('x')
expect((obj as any).response).toBeNull()
})
test("handles response being a non-object (string)", () => {
const obj = { response: "not-an-object" };
normalizeControlMessageKeys(obj);
expect((obj as any).response).toBe("not-an-object");
});
});
test('handles response being a non-object (string)', () => {
const obj = { response: 'not-an-object' }
normalizeControlMessageKeys(obj)
expect((obj as any).response).toBe('not-an-object')
})
})

View File

@@ -1,253 +1,255 @@
import { describe, expect, test } from "bun:test";
import { parseCronExpression, computeNextCronRun, cronToHuman } from "../cron";
import { describe, expect, test } from 'bun:test'
import { parseCronExpression, computeNextCronRun, cronToHuman } from '../cron'
describe("parseCronExpression", () => {
describe("valid expressions", () => {
test("parses wildcard fields", () => {
const result = parseCronExpression("* * * * *");
expect(result).not.toBeNull();
expect(result!.minute).toHaveLength(60);
expect(result!.hour).toHaveLength(24);
expect(result!.dayOfMonth).toHaveLength(31);
expect(result!.month).toHaveLength(12);
expect(result!.dayOfWeek).toHaveLength(7);
});
describe('parseCronExpression', () => {
describe('valid expressions', () => {
test('parses wildcard fields', () => {
const result = parseCronExpression('* * * * *')
expect(result).not.toBeNull()
expect(result!.minute).toHaveLength(60)
expect(result!.hour).toHaveLength(24)
expect(result!.dayOfMonth).toHaveLength(31)
expect(result!.month).toHaveLength(12)
expect(result!.dayOfWeek).toHaveLength(7)
})
test("parses specific values", () => {
const result = parseCronExpression("30 14 1 6 3");
expect(result).not.toBeNull();
expect(result!.minute).toEqual([30]);
expect(result!.hour).toEqual([14]);
expect(result!.dayOfMonth).toEqual([1]);
expect(result!.month).toEqual([6]);
expect(result!.dayOfWeek).toEqual([3]);
});
test('parses specific values', () => {
const result = parseCronExpression('30 14 1 6 3')
expect(result).not.toBeNull()
expect(result!.minute).toEqual([30])
expect(result!.hour).toEqual([14])
expect(result!.dayOfMonth).toEqual([1])
expect(result!.month).toEqual([6])
expect(result!.dayOfWeek).toEqual([3])
})
test("parses step syntax", () => {
const result = parseCronExpression("*/5 * * * *");
expect(result).not.toBeNull();
expect(result!.minute).toEqual([0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55]);
});
test('parses step syntax', () => {
const result = parseCronExpression('*/5 * * * *')
expect(result).not.toBeNull()
expect(result!.minute).toEqual([
0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55,
])
})
test("parses range syntax", () => {
const result = parseCronExpression("1-5 * * * *");
expect(result).not.toBeNull();
expect(result!.minute).toEqual([1, 2, 3, 4, 5]);
});
test('parses range syntax', () => {
const result = parseCronExpression('1-5 * * * *')
expect(result).not.toBeNull()
expect(result!.minute).toEqual([1, 2, 3, 4, 5])
})
test("parses range with step", () => {
const result = parseCronExpression("1-10/3 * * * *");
expect(result).not.toBeNull();
expect(result!.minute).toEqual([1, 4, 7, 10]);
});
test('parses range with step', () => {
const result = parseCronExpression('1-10/3 * * * *')
expect(result).not.toBeNull()
expect(result!.minute).toEqual([1, 4, 7, 10])
})
test("parses comma-separated list", () => {
const result = parseCronExpression("1,15,30 * * * *");
expect(result).not.toBeNull();
expect(result!.minute).toEqual([1, 15, 30]);
});
test('parses comma-separated list', () => {
const result = parseCronExpression('1,15,30 * * * *')
expect(result).not.toBeNull()
expect(result!.minute).toEqual([1, 15, 30])
})
test("parses day-of-week 7 as Sunday alias", () => {
const result = parseCronExpression("0 0 * * 7");
expect(result).not.toBeNull();
expect(result!.dayOfWeek).toEqual([0]);
});
test('parses day-of-week 7 as Sunday alias', () => {
const result = parseCronExpression('0 0 * * 7')
expect(result).not.toBeNull()
expect(result!.dayOfWeek).toEqual([0])
})
test("parses range with day-of-week 7", () => {
const result = parseCronExpression("0 0 * * 5-7");
expect(result).not.toBeNull();
expect(result!.dayOfWeek).toEqual([0, 5, 6]);
});
test('parses range with day-of-week 7', () => {
const result = parseCronExpression('0 0 * * 5-7')
expect(result).not.toBeNull()
expect(result!.dayOfWeek).toEqual([0, 5, 6])
})
test("parses complex combined expression", () => {
const result = parseCronExpression("0,30 9-17 * * 1-5");
expect(result).not.toBeNull();
expect(result!.minute).toEqual([0, 30]);
expect(result!.hour).toEqual([9, 10, 11, 12, 13, 14, 15, 16, 17]);
expect(result!.dayOfWeek).toEqual([1, 2, 3, 4, 5]);
});
});
test('parses complex combined expression', () => {
const result = parseCronExpression('0,30 9-17 * * 1-5')
expect(result).not.toBeNull()
expect(result!.minute).toEqual([0, 30])
expect(result!.hour).toEqual([9, 10, 11, 12, 13, 14, 15, 16, 17])
expect(result!.dayOfWeek).toEqual([1, 2, 3, 4, 5])
})
})
describe("invalid expressions", () => {
test("returns null for wrong field count", () => {
expect(parseCronExpression("* * *")).toBeNull();
});
describe('invalid expressions', () => {
test('returns null for wrong field count', () => {
expect(parseCronExpression('* * *')).toBeNull()
})
test("returns null for out-of-range values", () => {
expect(parseCronExpression("60 * * * *")).toBeNull();
});
test('returns null for out-of-range values', () => {
expect(parseCronExpression('60 * * * *')).toBeNull()
})
test("returns null for invalid step", () => {
expect(parseCronExpression("*/0 * * * *")).toBeNull();
});
test('returns null for invalid step', () => {
expect(parseCronExpression('*/0 * * * *')).toBeNull()
})
test("returns null for reversed range", () => {
expect(parseCronExpression("10-5 * * * *")).toBeNull();
});
test('returns null for reversed range', () => {
expect(parseCronExpression('10-5 * * * *')).toBeNull()
})
test("returns null for empty string", () => {
expect(parseCronExpression("")).toBeNull();
});
test('returns null for empty string', () => {
expect(parseCronExpression('')).toBeNull()
})
test("returns null for non-numeric tokens", () => {
expect(parseCronExpression("abc * * * *")).toBeNull();
});
});
test('returns null for non-numeric tokens', () => {
expect(parseCronExpression('abc * * * *')).toBeNull()
})
})
describe("field range validation", () => {
test("minute: 0-59", () => {
expect(parseCronExpression("0 * * * *")).not.toBeNull();
expect(parseCronExpression("59 * * * *")).not.toBeNull();
expect(parseCronExpression("60 * * * *")).toBeNull();
});
describe('field range validation', () => {
test('minute: 0-59', () => {
expect(parseCronExpression('0 * * * *')).not.toBeNull()
expect(parseCronExpression('59 * * * *')).not.toBeNull()
expect(parseCronExpression('60 * * * *')).toBeNull()
})
test("hour: 0-23", () => {
expect(parseCronExpression("* 0 * * *")).not.toBeNull();
expect(parseCronExpression("* 23 * * *")).not.toBeNull();
expect(parseCronExpression("* 24 * * *")).toBeNull();
});
test('hour: 0-23', () => {
expect(parseCronExpression('* 0 * * *')).not.toBeNull()
expect(parseCronExpression('* 23 * * *')).not.toBeNull()
expect(parseCronExpression('* 24 * * *')).toBeNull()
})
test("dayOfMonth: 1-31", () => {
expect(parseCronExpression("* * 1 * *")).not.toBeNull();
expect(parseCronExpression("* * 31 * *")).not.toBeNull();
expect(parseCronExpression("* * 0 * *")).toBeNull();
expect(parseCronExpression("* * 32 * *")).toBeNull();
});
test('dayOfMonth: 1-31', () => {
expect(parseCronExpression('* * 1 * *')).not.toBeNull()
expect(parseCronExpression('* * 31 * *')).not.toBeNull()
expect(parseCronExpression('* * 0 * *')).toBeNull()
expect(parseCronExpression('* * 32 * *')).toBeNull()
})
test("month: 1-12", () => {
expect(parseCronExpression("* * * 1 *")).not.toBeNull();
expect(parseCronExpression("* * * 12 *")).not.toBeNull();
expect(parseCronExpression("* * * 0 *")).toBeNull();
expect(parseCronExpression("* * * 13 *")).toBeNull();
});
test('month: 1-12', () => {
expect(parseCronExpression('* * * 1 *')).not.toBeNull()
expect(parseCronExpression('* * * 12 *')).not.toBeNull()
expect(parseCronExpression('* * * 0 *')).toBeNull()
expect(parseCronExpression('* * * 13 *')).toBeNull()
})
test("dayOfWeek: 0-6 (plus 7 alias)", () => {
expect(parseCronExpression("* * * * 0")).not.toBeNull();
expect(parseCronExpression("* * * * 6")).not.toBeNull();
expect(parseCronExpression("* * * * 7")).not.toBeNull(); // alias for 0
expect(parseCronExpression("* * * * 8")).toBeNull();
});
});
});
test('dayOfWeek: 0-6 (plus 7 alias)', () => {
expect(parseCronExpression('* * * * 0')).not.toBeNull()
expect(parseCronExpression('* * * * 6')).not.toBeNull()
expect(parseCronExpression('* * * * 7')).not.toBeNull() // alias for 0
expect(parseCronExpression('* * * * 8')).toBeNull()
})
})
})
describe("computeNextCronRun", () => {
test("finds next minute", () => {
const fields = parseCronExpression("31 14 * * *")!;
const from = new Date(2026, 0, 15, 14, 30, 45); // 14:30:45
const next = computeNextCronRun(fields, from);
expect(next).not.toBeNull();
expect(next!.getHours()).toBe(14);
expect(next!.getMinutes()).toBe(31);
});
describe('computeNextCronRun', () => {
test('finds next minute', () => {
const fields = parseCronExpression('31 14 * * *')!
const from = new Date(2026, 0, 15, 14, 30, 45) // 14:30:45
const next = computeNextCronRun(fields, from)
expect(next).not.toBeNull()
expect(next!.getHours()).toBe(14)
expect(next!.getMinutes()).toBe(31)
})
test("finds next hour", () => {
const fields = parseCronExpression("0 15 * * *")!;
const from = new Date(2026, 0, 15, 14, 30);
const next = computeNextCronRun(fields, from);
expect(next).not.toBeNull();
expect(next!.getHours()).toBe(15);
expect(next!.getMinutes()).toBe(0);
});
test('finds next hour', () => {
const fields = parseCronExpression('0 15 * * *')!
const from = new Date(2026, 0, 15, 14, 30)
const next = computeNextCronRun(fields, from)
expect(next).not.toBeNull()
expect(next!.getHours()).toBe(15)
expect(next!.getMinutes()).toBe(0)
})
test("rolls to next day", () => {
const fields = parseCronExpression("0 10 * * *")!;
const from = new Date(2026, 0, 15, 14, 30);
const next = computeNextCronRun(fields, from);
expect(next).not.toBeNull();
expect(next!.getDate()).toBe(16);
expect(next!.getHours()).toBe(10);
});
test('rolls to next day', () => {
const fields = parseCronExpression('0 10 * * *')!
const from = new Date(2026, 0, 15, 14, 30)
const next = computeNextCronRun(fields, from)
expect(next).not.toBeNull()
expect(next!.getDate()).toBe(16)
expect(next!.getHours()).toBe(10)
})
test("is strictly after from date", () => {
const fields = parseCronExpression("30 14 * * *")!;
const from = new Date(2026, 0, 15, 14, 30, 0); // exactly on cron time
const next = computeNextCronRun(fields, from);
expect(next).not.toBeNull();
expect(next!.getTime()).toBeGreaterThan(from.getTime());
});
test('is strictly after from date', () => {
const fields = parseCronExpression('30 14 * * *')!
const from = new Date(2026, 0, 15, 14, 30, 0) // exactly on cron time
const next = computeNextCronRun(fields, from)
expect(next).not.toBeNull()
expect(next!.getTime()).toBeGreaterThan(from.getTime())
})
test("every 5 minutes from arbitrary time", () => {
const fields = parseCronExpression("*/5 * * * *")!;
const from = new Date(2026, 0, 15, 14, 32);
const next = computeNextCronRun(fields, from);
expect(next).not.toBeNull();
expect(next!.getMinutes()).toBe(35);
});
test('every 5 minutes from arbitrary time', () => {
const fields = parseCronExpression('*/5 * * * *')!
const from = new Date(2026, 0, 15, 14, 32)
const next = computeNextCronRun(fields, from)
expect(next).not.toBeNull()
expect(next!.getMinutes()).toBe(35)
})
test("every minute", () => {
const fields = parseCronExpression("* * * * *")!;
const from = new Date(2026, 0, 15, 14, 32, 45);
const next = computeNextCronRun(fields, from);
expect(next).not.toBeNull();
expect(next!.getMinutes()).toBe(33);
});
test('every minute', () => {
const fields = parseCronExpression('* * * * *')!
const from = new Date(2026, 0, 15, 14, 32, 45)
const next = computeNextCronRun(fields, from)
expect(next).not.toBeNull()
expect(next!.getMinutes()).toBe(33)
})
test("handles step across midnight", () => {
const fields = parseCronExpression("0 0 * * *")!;
const from = new Date(2026, 0, 15, 23, 59);
const next = computeNextCronRun(fields, from);
expect(next).not.toBeNull();
expect(next!.getHours()).toBe(0);
expect(next!.getDate()).toBe(16);
});
test('handles step across midnight', () => {
const fields = parseCronExpression('0 0 * * *')!
const from = new Date(2026, 0, 15, 23, 59)
const next = computeNextCronRun(fields, from)
expect(next).not.toBeNull()
expect(next!.getHours()).toBe(0)
expect(next!.getDate()).toBe(16)
})
test("OR semantics when both dom and dow constrained", () => {
test('OR semantics when both dom and dow constrained', () => {
// dom=15, dow=3(Wed) - matches 15th OR Wednesday
const fields = parseCronExpression("0 0 15 * 3")!;
const from = new Date(2026, 0, 12, 0, 0); // Monday Jan 12
const next = computeNextCronRun(fields, from);
expect(next).not.toBeNull();
const fields = parseCronExpression('0 0 15 * 3')!
const from = new Date(2026, 0, 12, 0, 0) // Monday Jan 12
const next = computeNextCronRun(fields, from)
expect(next).not.toBeNull()
// Should match the first of either: next Wednesday(Jan 14) or 15th(Jan 15)
const dayOfWeek = next!.getDay();
const dayOfMonth = next!.getDate();
expect(dayOfWeek === 3 || dayOfMonth === 15).toBe(true);
});
});
const dayOfWeek = next!.getDay()
const dayOfMonth = next!.getDate()
expect(dayOfWeek === 3 || dayOfMonth === 15).toBe(true)
})
})
describe("cronToHuman", () => {
test("every N minutes", () => {
expect(cronToHuman("*/5 * * * *")).toBe("Every 5 minutes");
});
describe('cronToHuman', () => {
test('every N minutes', () => {
expect(cronToHuman('*/5 * * * *')).toBe('Every 5 minutes')
})
test("every minute", () => {
expect(cronToHuman("*/1 * * * *")).toBe("Every minute");
});
test('every minute', () => {
expect(cronToHuman('*/1 * * * *')).toBe('Every minute')
})
test("every hour at :00", () => {
expect(cronToHuman("0 * * * *")).toBe("Every hour");
});
test('every hour at :00', () => {
expect(cronToHuman('0 * * * *')).toBe('Every hour')
})
test("every hour at :30", () => {
expect(cronToHuman("30 * * * *")).toBe("Every hour at :30");
});
test('every hour at :30', () => {
expect(cronToHuman('30 * * * *')).toBe('Every hour at :30')
})
test("every N hours", () => {
expect(cronToHuman("0 */2 * * *")).toBe("Every 2 hours");
});
test('every N hours', () => {
expect(cronToHuman('0 */2 * * *')).toBe('Every 2 hours')
})
test("daily at specific time", () => {
const result = cronToHuman("30 9 * * *");
expect(result).toContain("Every day at");
expect(result).toContain("9:30");
});
test('daily at specific time', () => {
const result = cronToHuman('30 9 * * *')
expect(result).toContain('Every day at')
expect(result).toContain('9:30')
})
test("specific day of week", () => {
const result = cronToHuman("0 9 * * 3");
expect(result).toContain("Wednesday");
expect(result).toContain("9:00");
});
test('specific day of week', () => {
const result = cronToHuman('0 9 * * 3')
expect(result).toContain('Wednesday')
expect(result).toContain('9:00')
})
test("weekdays", () => {
const result = cronToHuman("0 9 * * 1-5");
expect(result).toContain("Weekdays");
expect(result).toContain("9:00");
});
test('weekdays', () => {
const result = cronToHuman('0 9 * * 1-5')
expect(result).toContain('Weekdays')
expect(result).toContain('9:00')
})
test("returns raw cron for complex patterns", () => {
expect(cronToHuman("0,30 9-17 * * 1-5")).toBe("0,30 9-17 * * 1-5");
});
test('returns raw cron for complex patterns', () => {
expect(cronToHuman('0,30 9-17 * * 1-5')).toBe('0,30 9-17 * * 1-5')
})
test("returns raw cron for wrong field count", () => {
expect(cronToHuman("* * *")).toBe("* * *");
});
});
test('returns raw cron for wrong field count', () => {
expect(cronToHuman('* * *')).toBe('* * *')
})
})

View File

@@ -8,7 +8,13 @@ describe('cronScheduler baseline helpers', () => {
test('isRecurringTaskAged returns false when maxAgeMs is zero', () => {
expect(
isRecurringTaskAged(
{ id: 'a', cron: '* * * * *', prompt: 'x', createdAt: 0, recurring: true },
{
id: 'a',
cron: '* * * * *',
prompt: 'x',
createdAt: 0,
recurring: true,
},
10_000,
0,
),
@@ -41,7 +47,13 @@ describe('cronScheduler baseline helpers', () => {
expect(
isRecurringTaskAged(
{ id: 'c', cron: '* * * * *', prompt: 'x', createdAt: 0, recurring: true },
{
id: 'c',
cron: '* * * * *',
prompt: 'x',
createdAt: 0,
recurring: true,
},
10_000,
100,
),

View File

@@ -41,7 +41,12 @@ afterEach(async () => {
describe('cronTasks baseline', () => {
test('session-only cron tasks remain in memory and do not create the cron file', async () => {
const id = await addCronTask('* * * * *', 'session-only prompt', true, false)
const id = await addCronTask(
'* * * * *',
'session-only prompt',
true,
false,
)
const tasks = await listAllCronTasks()
@@ -107,7 +112,12 @@ describe('cronTasks baseline', () => {
test('daemon-style listAllCronTasks(dir) excludes session-only tasks', async () => {
await addCronTask('* * * * *', 'session prompt', true, false)
const durableId = await addCronTask('* * * * *', 'durable prompt', true, true)
const durableId = await addCronTask(
'* * * * *',
'durable prompt',
true,
true,
)
const sessionView = await listAllCronTasks()
const daemonView = await listAllCronTasks(tempDir)
@@ -130,7 +140,12 @@ describe('cronTasks baseline', () => {
})
test('removeCronTasks with dir does not mutate session-only task storage', async () => {
const sessionId = await addCronTask('* * * * *', 'keep session task', true, false)
const sessionId = await addCronTask(
'* * * * *',
'keep session task',
true,
false,
)
await addCronTask('* * * * *', 'durable prompt', true, true)
await removeCronTasks([sessionId], tempDir)
@@ -194,7 +209,11 @@ describe('cronTasks baseline', () => {
test('jitteredNextCronRunMs returns the exact next fire time when no second match exists in range', () => {
const fromMs = new Date('2026-04-12T10:00:00').getTime()
const exact = nextCronRunMs('0 0 29 2 *', fromMs)
const jittered = oneShotJitteredNextCronRunMs('0 0 29 2 *', fromMs, '89abcdef')
const jittered = oneShotJitteredNextCronRunMs(
'0 0 29 2 *',
fromMs,
'89abcdef',
)
expect(exact).not.toBeNull()
expect(jittered).not.toBeNull()

View File

@@ -1,108 +1,122 @@
import { describe, expect, test } from "bun:test";
import { parseGitRemote, parseGitHubRepository } from "../detectRepository";
import { describe, expect, test } from 'bun:test'
import { parseGitRemote, parseGitHubRepository } from '../detectRepository'
describe("parseGitRemote", () => {
describe('parseGitRemote', () => {
// HTTPS
test("parses HTTPS URL: https://github.com/owner/repo.git", () => {
const result = parseGitRemote("https://github.com/owner/repo.git");
expect(result).toEqual({ host: "github.com", owner: "owner", name: "repo" });
});
test('parses HTTPS URL: https://github.com/owner/repo.git', () => {
const result = parseGitRemote('https://github.com/owner/repo.git')
expect(result).toEqual({ host: 'github.com', owner: 'owner', name: 'repo' })
})
test("parses HTTPS URL without .git suffix", () => {
const result = parseGitRemote("https://github.com/owner/repo");
expect(result).toEqual({ host: "github.com", owner: "owner", name: "repo" });
});
test('parses HTTPS URL without .git suffix', () => {
const result = parseGitRemote('https://github.com/owner/repo')
expect(result).toEqual({ host: 'github.com', owner: 'owner', name: 'repo' })
})
test("parses HTTPS URL with subdirectory path (only takes first 2 segments)", () => {
const result = parseGitRemote("https://github.com/owner/repo.git");
expect(result).not.toBeNull();
expect(result!.name).toBe("repo");
});
test('parses HTTPS URL with subdirectory path (only takes first 2 segments)', () => {
const result = parseGitRemote('https://github.com/owner/repo.git')
expect(result).not.toBeNull()
expect(result!.name).toBe('repo')
})
// SSH
test("parses SSH URL: git@github.com:owner/repo.git", () => {
const result = parseGitRemote("git@github.com:owner/repo.git");
expect(result).toEqual({ host: "github.com", owner: "owner", name: "repo" });
});
test('parses SSH URL: git@github.com:owner/repo.git', () => {
const result = parseGitRemote('git@github.com:owner/repo.git')
expect(result).toEqual({ host: 'github.com', owner: 'owner', name: 'repo' })
})
test("parses SSH URL without .git suffix", () => {
const result = parseGitRemote("git@github.com:owner/repo");
expect(result).toEqual({ host: "github.com", owner: "owner", name: "repo" });
});
test('parses SSH URL without .git suffix', () => {
const result = parseGitRemote('git@github.com:owner/repo')
expect(result).toEqual({ host: 'github.com', owner: 'owner', name: 'repo' })
})
// ssh://
test("parses ssh:// URL: ssh://git@github.com/owner/repo.git", () => {
const result = parseGitRemote("ssh://git@github.com/owner/repo.git");
expect(result).toEqual({ host: "github.com", owner: "owner", name: "repo" });
});
test('parses ssh:// URL: ssh://git@github.com/owner/repo.git', () => {
const result = parseGitRemote('ssh://git@github.com/owner/repo.git')
expect(result).toEqual({ host: 'github.com', owner: 'owner', name: 'repo' })
})
// git://
test("parses git:// URL", () => {
const result = parseGitRemote("git://github.com/owner/repo.git");
expect(result).toEqual({ host: "github.com", owner: "owner", name: "repo" });
});
test('parses git:// URL', () => {
const result = parseGitRemote('git://github.com/owner/repo.git')
expect(result).toEqual({ host: 'github.com', owner: 'owner', name: 'repo' })
})
// Boundary
test("returns null for invalid URL", () => {
expect(parseGitRemote("not-a-url")).toBeNull();
});
test('returns null for invalid URL', () => {
expect(parseGitRemote('not-a-url')).toBeNull()
})
test("returns null for empty string", () => {
expect(parseGitRemote("")).toBeNull();
});
test('returns null for empty string', () => {
expect(parseGitRemote('')).toBeNull()
})
test("handles GHE hostname", () => {
const result = parseGitRemote("https://ghe.corp.com/team/project.git");
expect(result).toEqual({ host: "ghe.corp.com", owner: "team", name: "project" });
});
test('handles GHE hostname', () => {
const result = parseGitRemote('https://ghe.corp.com/team/project.git')
expect(result).toEqual({
host: 'ghe.corp.com',
owner: 'team',
name: 'project',
})
})
test("handles port number in URL", () => {
const result = parseGitRemote("https://github.com:443/owner/repo.git");
expect(result).not.toBeNull();
expect(result!.owner).toBe("owner");
expect(result!.name).toBe("repo");
});
test('handles port number in URL', () => {
const result = parseGitRemote('https://github.com:443/owner/repo.git')
expect(result).not.toBeNull()
expect(result!.owner).toBe('owner')
expect(result!.name).toBe('repo')
})
test("rejects SSH config alias without real hostname", () => {
expect(parseGitRemote("git@github.com-work:owner/repo.git")).toBeNull();
});
test('rejects SSH config alias without real hostname', () => {
expect(parseGitRemote('git@github.com-work:owner/repo.git')).toBeNull()
})
test("handles repo names with dots", () => {
const result = parseGitRemote("https://github.com/owner/cc.kurs.web.git");
expect(result).toEqual({ host: "github.com", owner: "owner", name: "cc.kurs.web" });
});
});
test('handles repo names with dots', () => {
const result = parseGitRemote('https://github.com/owner/cc.kurs.web.git')
expect(result).toEqual({
host: 'github.com',
owner: 'owner',
name: 'cc.kurs.web',
})
})
})
describe("parseGitHubRepository", () => {
describe('parseGitHubRepository', () => {
test("extracts 'owner/repo' from valid remote URL", () => {
expect(parseGitHubRepository("https://github.com/owner/repo.git")).toBe("owner/repo");
});
expect(parseGitHubRepository('https://github.com/owner/repo.git')).toBe(
'owner/repo',
)
})
test("handles plain 'owner/repo' string input", () => {
expect(parseGitHubRepository("owner/repo")).toBe("owner/repo");
});
expect(parseGitHubRepository('owner/repo')).toBe('owner/repo')
})
test("returns null for non-GitHub host", () => {
expect(parseGitHubRepository("https://gitlab.com/owner/repo.git")).toBeNull();
});
test('returns null for non-GitHub host', () => {
expect(
parseGitHubRepository('https://gitlab.com/owner/repo.git'),
).toBeNull()
})
test("returns null for invalid input", () => {
expect(parseGitHubRepository("not-valid")).toBeNull();
});
test('returns null for invalid input', () => {
expect(parseGitHubRepository('not-valid')).toBeNull()
})
test("is case-sensitive for owner/repo", () => {
expect(parseGitHubRepository("Owner/Repo")).toBe("Owner/Repo");
});
test('is case-sensitive for owner/repo', () => {
expect(parseGitHubRepository('Owner/Repo')).toBe('Owner/Repo')
})
test("handles SSH format for github.com", () => {
expect(parseGitHubRepository("git@github.com:owner/repo.git")).toBe("owner/repo");
});
test('handles SSH format for github.com', () => {
expect(parseGitHubRepository('git@github.com:owner/repo.git')).toBe(
'owner/repo',
)
})
test("returns null for GHE SSH URL", () => {
expect(parseGitHubRepository("git@ghe.corp.com:owner/repo.git")).toBeNull();
});
test('returns null for GHE SSH URL', () => {
expect(parseGitHubRepository('git@ghe.corp.com:owner/repo.git')).toBeNull()
})
test("handles plain owner/repo with .git suffix", () => {
expect(parseGitHubRepository("owner/repo.git")).toBe("owner/repo");
});
});
test('handles plain owner/repo with .git suffix', () => {
expect(parseGitHubRepository('owner/repo.git')).toBe('owner/repo')
})
})

View File

@@ -1,117 +1,130 @@
import { describe, expect, test } from "bun:test";
import { adjustHunkLineNumbers, getPatchFromContents } from "../diff";
import { describe, expect, test } from 'bun:test'
import { adjustHunkLineNumbers, getPatchFromContents } from '../diff'
describe("adjustHunkLineNumbers", () => {
test("shifts hunk line numbers by offset", () => {
describe('adjustHunkLineNumbers', () => {
test('shifts hunk line numbers by offset', () => {
const hunks = [
{ oldStart: 1, oldLines: 3, newStart: 1, newLines: 4, lines: [" a", "-b", "+c", "+d", " e"] },
] as any[];
const result = adjustHunkLineNumbers(hunks, 10);
expect(result[0].oldStart).toBe(11);
expect(result[0].newStart).toBe(11);
});
{
oldStart: 1,
oldLines: 3,
newStart: 1,
newLines: 4,
lines: [' a', '-b', '+c', '+d', ' e'],
},
] as any[]
const result = adjustHunkLineNumbers(hunks, 10)
expect(result[0].oldStart).toBe(11)
expect(result[0].newStart).toBe(11)
})
test("returns original hunks for zero offset", () => {
test('returns original hunks for zero offset', () => {
const hunks = [
{ oldStart: 5, oldLines: 2, newStart: 5, newLines: 2, lines: [] },
] as any[];
const result = adjustHunkLineNumbers(hunks, 0);
expect(result).toBe(hunks); // same reference
});
] as any[]
const result = adjustHunkLineNumbers(hunks, 0)
expect(result).toBe(hunks) // same reference
})
test("handles negative offset", () => {
test('handles negative offset', () => {
const hunks = [
{ oldStart: 10, oldLines: 2, newStart: 10, newLines: 2, lines: [] },
] as any[];
const result = adjustHunkLineNumbers(hunks, -5);
expect(result[0].oldStart).toBe(5);
expect(result[0].newStart).toBe(5);
});
] as any[]
const result = adjustHunkLineNumbers(hunks, -5)
expect(result[0].oldStart).toBe(5)
expect(result[0].newStart).toBe(5)
})
test("handles empty hunks array", () => {
expect(adjustHunkLineNumbers([], 10)).toEqual([]);
});
});
test('handles empty hunks array', () => {
expect(adjustHunkLineNumbers([], 10)).toEqual([])
})
})
describe("getPatchFromContents", () => {
test("returns hunks for different content", () => {
describe('getPatchFromContents', () => {
test('returns hunks for different content', () => {
const hunks = getPatchFromContents({
filePath: "test.txt",
oldContent: "hello\nworld",
newContent: "hello\nplanet",
});
expect(hunks.length).toBe(1);
const allLines = hunks[0].lines;
expect(allLines).toContain("-world");
expect(allLines).toContain("+planet");
});
filePath: 'test.txt',
oldContent: 'hello\nworld',
newContent: 'hello\nplanet',
})
expect(hunks.length).toBe(1)
const allLines = hunks[0].lines
expect(allLines).toContain('-world')
expect(allLines).toContain('+planet')
})
test("returns empty hunks for identical content", () => {
test('returns empty hunks for identical content', () => {
const hunks = getPatchFromContents({
filePath: "test.txt",
oldContent: "same content",
newContent: "same content",
});
expect(hunks).toEqual([]);
});
filePath: 'test.txt',
oldContent: 'same content',
newContent: 'same content',
})
expect(hunks).toEqual([])
})
test("handles content with ampersands", () => {
test('handles content with ampersands', () => {
const hunks = getPatchFromContents({
filePath: "test.txt",
oldContent: "a & b",
newContent: "a & c",
});
expect(hunks.length).toBeGreaterThan(0);
filePath: 'test.txt',
oldContent: 'a & b',
newContent: 'a & c',
})
expect(hunks.length).toBeGreaterThan(0)
// Verify ampersands are unescaped in the output
const allLines = hunks.flatMap((h: any) => h.lines);
expect(allLines.some((l: string) => l.includes("&"))).toBe(true);
});
const allLines = hunks.flatMap((h: any) => h.lines)
expect(allLines.some((l: string) => l.includes('&'))).toBe(true)
})
test("handles empty old content (new file)", () => {
test('handles empty old content (new file)', () => {
const hunks = getPatchFromContents({
filePath: "test.txt",
oldContent: "",
newContent: "new content",
});
expect(hunks.length).toBeGreaterThan(0);
const allLines = hunks.flatMap((h: any) => h.lines);
expect(allLines.some((l: string) => l.startsWith("+"))).toBe(true);
});
filePath: 'test.txt',
oldContent: '',
newContent: 'new content',
})
expect(hunks.length).toBeGreaterThan(0)
const allLines = hunks.flatMap((h: any) => h.lines)
expect(allLines.some((l: string) => l.startsWith('+'))).toBe(true)
})
test("handles content with dollar signs", () => {
test('handles content with dollar signs', () => {
const hunks = getPatchFromContents({
filePath: "test.txt",
oldContent: "price: $5",
newContent: "price: $10",
});
expect(hunks.length).toBeGreaterThan(0);
const allLines = hunks.flatMap((h: any) => h.lines);
expect(allLines.some((l: string) => l.includes("$"))).toBe(true);
filePath: 'test.txt',
oldContent: 'price: $5',
newContent: 'price: $10',
})
expect(hunks.length).toBeGreaterThan(0)
const allLines = hunks.flatMap((h: any) => h.lines)
expect(allLines.some((l: string) => l.includes('$'))).toBe(true)
// Verify dollar signs are unescaped (not the token)
expect(allLines.some((l: string) => l.includes("<<:DOLLAR_TOKEN:>>"))).toBe(false);
});
expect(allLines.some((l: string) => l.includes('<<:DOLLAR_TOKEN:>>'))).toBe(
false,
)
})
test("handles deleting all content", () => {
test('handles deleting all content', () => {
const hunks = getPatchFromContents({
filePath: "test.txt",
oldContent: "line1\nline2\nline3",
newContent: "",
});
expect(hunks.length).toBeGreaterThan(0);
const allLines = hunks.flatMap((h: any) => h.lines);
expect(allLines.some((l: string) => l.startsWith("-"))).toBe(true);
expect(allLines.every((l: string) => l.startsWith("-") || l.startsWith(" ") || l.startsWith("\\"))).toBe(true);
});
filePath: 'test.txt',
oldContent: 'line1\nline2\nline3',
newContent: '',
})
expect(hunks.length).toBeGreaterThan(0)
const allLines = hunks.flatMap((h: any) => h.lines)
expect(allLines.some((l: string) => l.startsWith('-'))).toBe(true)
expect(
allLines.every(
(l: string) =>
l.startsWith('-') || l.startsWith(' ') || l.startsWith('\\'),
),
).toBe(true)
})
test("ignoreWhitespace treats indentation changes as identical", () => {
const old = "function foo() {\n return 42;\n}\n";
const nw = "function foo() {\n\treturn 42;\n}\n";
test('ignoreWhitespace treats indentation changes as identical', () => {
const old = 'function foo() {\n return 42;\n}\n'
const nw = 'function foo() {\n\treturn 42;\n}\n'
const hunks = getPatchFromContents({
filePath: "test.txt",
filePath: 'test.txt',
oldContent: old,
newContent: nw,
ignoreWhitespace: true,
});
expect(hunks).toEqual([]);
});
});
})
expect(hunks).toEqual([])
})
})

View File

@@ -1,110 +1,119 @@
import { describe, expect, test } from "bun:test";
import { parseDirectMemberMessage, sendDirectMemberMessage } from "../directMemberMessage";
import { describe, expect, test } from 'bun:test'
import {
parseDirectMemberMessage,
sendDirectMemberMessage,
} from '../directMemberMessage'
describe("parseDirectMemberMessage", () => {
describe('parseDirectMemberMessage', () => {
test("parses '@agent-name hello world'", () => {
const result = parseDirectMemberMessage("@agent-name hello world");
expect(result).toEqual({ recipientName: "agent-name", message: "hello world" });
});
const result = parseDirectMemberMessage('@agent-name hello world')
expect(result).toEqual({
recipientName: 'agent-name',
message: 'hello world',
})
})
test("parses '@agent-name single-word'", () => {
const result = parseDirectMemberMessage("@agent-name single-word");
expect(result).toEqual({ recipientName: "agent-name", message: "single-word" });
});
const result = parseDirectMemberMessage('@agent-name single-word')
expect(result).toEqual({
recipientName: 'agent-name',
message: 'single-word',
})
})
test("returns null for non-matching input", () => {
expect(parseDirectMemberMessage("hello world")).toBeNull();
});
test('returns null for non-matching input', () => {
expect(parseDirectMemberMessage('hello world')).toBeNull()
})
test("returns null for empty string", () => {
expect(parseDirectMemberMessage("")).toBeNull();
});
test('returns null for empty string', () => {
expect(parseDirectMemberMessage('')).toBeNull()
})
test("returns null for '@name' without message", () => {
expect(parseDirectMemberMessage("@name")).toBeNull();
});
expect(parseDirectMemberMessage('@name')).toBeNull()
})
test("handles hyphenated agent names like '@my-agent msg'", () => {
const result = parseDirectMemberMessage("@my-agent msg");
expect(result).toEqual({ recipientName: "my-agent", message: "msg" });
});
const result = parseDirectMemberMessage('@my-agent msg')
expect(result).toEqual({ recipientName: 'my-agent', message: 'msg' })
})
test("handles multiline message content", () => {
const result = parseDirectMemberMessage("@agent line1\nline2");
expect(result).toEqual({ recipientName: "agent", message: "line1\nline2" });
});
test('handles multiline message content', () => {
const result = parseDirectMemberMessage('@agent line1\nline2')
expect(result).toEqual({ recipientName: 'agent', message: 'line1\nline2' })
})
test("extracts correct recipientName and message", () => {
const result = parseDirectMemberMessage("@alice please fix the bug");
expect(result?.recipientName).toBe("alice");
expect(result?.message).toBe("please fix the bug");
});
test('extracts correct recipientName and message', () => {
const result = parseDirectMemberMessage('@alice please fix the bug')
expect(result?.recipientName).toBe('alice')
expect(result?.message).toBe('please fix the bug')
})
test("trims message whitespace", () => {
const result = parseDirectMemberMessage("@agent hello ");
expect(result?.message).toBe("hello");
});
});
test('trims message whitespace', () => {
const result = parseDirectMemberMessage('@agent hello ')
expect(result?.message).toBe('hello')
})
})
describe("sendDirectMemberMessage", () => {
test("returns error when no team context", async () => {
const result = await sendDirectMemberMessage("agent", "hello", null as any);
expect(result).toEqual({ success: false, error: "no_team_context" });
});
describe('sendDirectMemberMessage', () => {
test('returns error when no team context', async () => {
const result = await sendDirectMemberMessage('agent', 'hello', null as any)
expect(result).toEqual({ success: false, error: 'no_team_context' })
})
test("returns error for unknown recipient", async () => {
test('returns error for unknown recipient', async () => {
const teamContext = {
teamName: "team1",
teammates: { alice: { name: "alice" } },
};
teamName: 'team1',
teammates: { alice: { name: 'alice' } },
}
const result = await sendDirectMemberMessage(
"bob",
"hello",
'bob',
'hello',
teamContext as any,
async () => {},
);
)
expect(result).toEqual({
success: false,
error: "unknown_recipient",
recipientName: "bob",
});
});
error: 'unknown_recipient',
recipientName: 'bob',
})
})
test("calls writeToMailbox with correct args for valid recipient", async () => {
let mailboxArgs: any = null;
test('calls writeToMailbox with correct args for valid recipient', async () => {
let mailboxArgs: any = null
const teamContext = {
teamName: "team1",
teammates: { alice: { name: "alice" } },
};
teamName: 'team1',
teammates: { alice: { name: 'alice' } },
}
const result = await sendDirectMemberMessage(
"alice",
"hello",
'alice',
'hello',
teamContext as any,
async (recipient, msg, team) => {
mailboxArgs = { recipient, msg, team };
mailboxArgs = { recipient, msg, team }
},
);
expect(result).toEqual({ success: true, recipientName: "alice" });
expect(mailboxArgs.recipient).toBe("alice");
expect(mailboxArgs.msg.text).toBe("hello");
expect(mailboxArgs.msg.from).toBe("user");
expect(mailboxArgs.team).toBe("team1");
});
)
expect(result).toEqual({ success: true, recipientName: 'alice' })
expect(mailboxArgs.recipient).toBe('alice')
expect(mailboxArgs.msg.text).toBe('hello')
expect(mailboxArgs.msg.from).toBe('user')
expect(mailboxArgs.team).toBe('team1')
})
test("returns success for valid message", async () => {
test('returns success for valid message', async () => {
const teamContext = {
teamName: "team1",
teammates: { bob: { name: "bob" } },
};
teamName: 'team1',
teammates: { bob: { name: 'bob' } },
}
const result = await sendDirectMemberMessage(
"bob",
"test message",
'bob',
'test message',
teamContext as any,
async () => {},
);
expect(result.success).toBe(true);
)
expect(result.success).toBe(true)
if (result.success) {
expect(result.recipientName).toBe("bob");
expect(result.recipientName).toBe('bob')
}
});
});
})
})

View File

@@ -1,134 +1,128 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
import {
stripDisplayTags,
stripDisplayTagsAllowEmpty,
stripIdeContextTags,
} from "../displayTags";
} from '../displayTags'
describe("stripDisplayTags", () => {
test("strips a single system tag and returns remaining text", () => {
describe('stripDisplayTags', () => {
test('strips a single system tag and returns remaining text', () => {
expect(
stripDisplayTags("<system-reminder>secret stuff</system-reminder>text")
).toBe("text");
});
stripDisplayTags('<system-reminder>secret stuff</system-reminder>text'),
).toBe('text')
})
test("strips multiple tags and preserves text between them", () => {
test('strips multiple tags and preserves text between them', () => {
const input =
"<hook-output>data</hook-output>hello <task-info>info</task-info>world";
expect(stripDisplayTags(input)).toBe("hello world");
});
'<hook-output>data</hook-output>hello <task-info>info</task-info>world'
expect(stripDisplayTags(input)).toBe('hello world')
})
test("preserves uppercase JSX component names", () => {
expect(stripDisplayTags("fix the <Button> layout")).toBe(
"fix the <Button> layout"
);
});
test('preserves uppercase JSX component names', () => {
expect(stripDisplayTags('fix the <Button> layout')).toBe(
'fix the <Button> layout',
)
})
test("preserves angle brackets in prose (when x < y)", () => {
expect(stripDisplayTags("when x < y")).toBe("when x < y");
});
test('preserves angle brackets in prose (when x < y)', () => {
expect(stripDisplayTags('when x < y')).toBe('when x < y')
})
test("preserves DOCTYPE declarations", () => {
expect(stripDisplayTags("<!DOCTYPE html>")).toBe("<!DOCTYPE html>");
});
test('preserves DOCTYPE declarations', () => {
expect(stripDisplayTags('<!DOCTYPE html>')).toBe('<!DOCTYPE html>')
})
test("returns original text when stripping would result in empty", () => {
const input = "<system-reminder>all tags</system-reminder>";
expect(stripDisplayTags(input)).toBe(input);
});
test('returns original text when stripping would result in empty', () => {
const input = '<system-reminder>all tags</system-reminder>'
expect(stripDisplayTags(input)).toBe(input)
})
test("strips tags with attributes", () => {
expect(
stripDisplayTags('<context type="ide">data</context>hello')
).toBe("hello");
});
test('strips tags with attributes', () => {
expect(stripDisplayTags('<context type="ide">data</context>hello')).toBe(
'hello',
)
})
test("handles multi-line tag content", () => {
const input = "<info>\nline1\nline2\n</info>remaining";
expect(stripDisplayTags(input)).toBe("remaining");
});
test('handles multi-line tag content', () => {
const input = '<info>\nline1\nline2\n</info>remaining'
expect(stripDisplayTags(input)).toBe('remaining')
})
test("returns trimmed result", () => {
expect(
stripDisplayTags(" <tag>content</tag> hello ")
).toBe("hello");
});
test('returns trimmed result', () => {
expect(stripDisplayTags(' <tag>content</tag> hello ')).toBe('hello')
})
test("handles empty string input", () => {
test('handles empty string input', () => {
// Empty string is falsy, so stripDisplayTags returns original
expect(stripDisplayTags("")).toBe("");
});
expect(stripDisplayTags('')).toBe('')
})
test("handles whitespace-only input", () => {
test('handles whitespace-only input', () => {
// After trim, result is empty string which is falsy, returns original
expect(stripDisplayTags(" ")).toBe(" ");
});
});
expect(stripDisplayTags(' ')).toBe(' ')
})
})
describe("stripDisplayTagsAllowEmpty", () => {
test("returns empty string when all content is tags", () => {
describe('stripDisplayTagsAllowEmpty', () => {
test('returns empty string when all content is tags', () => {
expect(
stripDisplayTagsAllowEmpty("<system-reminder>stuff</system-reminder>")
).toBe("");
});
stripDisplayTagsAllowEmpty('<system-reminder>stuff</system-reminder>'),
).toBe('')
})
test("strips tags and returns remaining text", () => {
expect(
stripDisplayTagsAllowEmpty("<tag>content</tag>hello")
).toBe("hello");
});
test('strips tags and returns remaining text', () => {
expect(stripDisplayTagsAllowEmpty('<tag>content</tag>hello')).toBe('hello')
})
test("returns empty string for empty input", () => {
expect(stripDisplayTagsAllowEmpty("")).toBe("");
});
test('returns empty string for empty input', () => {
expect(stripDisplayTagsAllowEmpty('')).toBe('')
})
test("returns empty string for whitespace-only content after strip", () => {
expect(
stripDisplayTagsAllowEmpty("<tag>content</tag> ")
).toBe("");
});
});
test('returns empty string for whitespace-only content after strip', () => {
expect(stripDisplayTagsAllowEmpty('<tag>content</tag> ')).toBe('')
})
})
describe("stripIdeContextTags", () => {
test("strips ide_opened_file tags", () => {
describe('stripIdeContextTags', () => {
test('strips ide_opened_file tags', () => {
expect(
stripIdeContextTags(
"<ide_opened_file>path/to/file.ts</ide_opened_file>hello"
)
).toBe("hello");
});
'<ide_opened_file>path/to/file.ts</ide_opened_file>hello',
),
).toBe('hello')
})
test("strips ide_selection tags", () => {
test('strips ide_selection tags', () => {
expect(
stripIdeContextTags("<ide_selection>selected code</ide_selection>world")
).toBe("world");
});
stripIdeContextTags('<ide_selection>selected code</ide_selection>world'),
).toBe('world')
})
test("strips ide tags with attributes", () => {
test('strips ide tags with attributes', () => {
expect(
stripIdeContextTags(
'<ide_opened_file path="foo.ts">content</ide_opened_file>text'
)
).toBe("text");
});
'<ide_opened_file path="foo.ts">content</ide_opened_file>text',
),
).toBe('text')
})
test("preserves other lowercase tags", () => {
test('preserves other lowercase tags', () => {
expect(
stripIdeContextTags("<system-reminder>data</system-reminder>hello")
).toBe("<system-reminder>data</system-reminder>hello");
});
stripIdeContextTags('<system-reminder>data</system-reminder>hello'),
).toBe('<system-reminder>data</system-reminder>hello')
})
test("preserves user-typed HTML like <code>", () => {
expect(stripIdeContextTags("use <code>foo</code> here")).toBe(
"use <code>foo</code> here"
);
});
test('preserves user-typed HTML like <code>', () => {
expect(stripIdeContextTags('use <code>foo</code> here')).toBe(
'use <code>foo</code> here',
)
})
test("strips only IDE tags while preserving other tags and text", () => {
test('strips only IDE tags while preserving other tags and text', () => {
const input =
"<ide_opened_file>f.ts</ide_opened_file><system-reminder>x</system-reminder>text";
'<ide_opened_file>f.ts</ide_opened_file><system-reminder>x</system-reminder>text'
expect(stripIdeContextTags(input)).toBe(
"<system-reminder>x</system-reminder>text"
);
});
});
'<system-reminder>x</system-reminder>text',
)
})
})

View File

@@ -13,7 +13,8 @@ mock.module('src/utils/auth.js', () => ({
isTeamSubscriber: () => false,
}))
mock.module('src/services/analytics/growthbook.js', () => ({
getFeatureValue_CACHED_MAY_BE_STALE: (_key: string, defaultValue: unknown) => defaultValue ?? {},
getFeatureValue_CACHED_MAY_BE_STALE: (_key: string, defaultValue: unknown) =>
defaultValue ?? {},
}))
mock.module('src/utils/model/modelSupportOverrides.js', () => ({
get3PModelCapabilityOverride: () => undefined,

View File

@@ -1,4 +1,4 @@
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { describe, expect, test, beforeEach, afterEach } from 'bun:test'
import {
isEnvTruthy,
isEnvDefinedFalsy,
@@ -10,324 +10,333 @@ import {
isBareMode,
shouldMaintainProjectWorkingDir,
getClaudeConfigHomeDir,
} from "../envUtils";
} from '../envUtils'
// ─── isEnvTruthy ───────────────────────────────────────────────────────
describe("isEnvTruthy", () => {
describe('isEnvTruthy', () => {
test("returns true for '1'", () => {
expect(isEnvTruthy("1")).toBe(true);
});
expect(isEnvTruthy('1')).toBe(true)
})
test("returns true for 'true'", () => {
expect(isEnvTruthy("true")).toBe(true);
});
expect(isEnvTruthy('true')).toBe(true)
})
test("returns true for 'TRUE'", () => {
expect(isEnvTruthy("TRUE")).toBe(true);
});
expect(isEnvTruthy('TRUE')).toBe(true)
})
test("returns true for 'yes'", () => {
expect(isEnvTruthy("yes")).toBe(true);
});
expect(isEnvTruthy('yes')).toBe(true)
})
test("returns true for 'on'", () => {
expect(isEnvTruthy("on")).toBe(true);
});
expect(isEnvTruthy('on')).toBe(true)
})
test("returns true for boolean true", () => {
expect(isEnvTruthy(true)).toBe(true);
});
test('returns true for boolean true', () => {
expect(isEnvTruthy(true)).toBe(true)
})
test("returns false for '0'", () => {
expect(isEnvTruthy("0")).toBe(false);
});
expect(isEnvTruthy('0')).toBe(false)
})
test("returns false for 'false'", () => {
expect(isEnvTruthy("false")).toBe(false);
});
expect(isEnvTruthy('false')).toBe(false)
})
test("returns false for empty string", () => {
expect(isEnvTruthy("")).toBe(false);
});
test('returns false for empty string', () => {
expect(isEnvTruthy('')).toBe(false)
})
test("returns false for undefined", () => {
expect(isEnvTruthy(undefined)).toBe(false);
});
test('returns false for undefined', () => {
expect(isEnvTruthy(undefined)).toBe(false)
})
test("returns false for boolean false", () => {
expect(isEnvTruthy(false)).toBe(false);
});
test('returns false for boolean false', () => {
expect(isEnvTruthy(false)).toBe(false)
})
test("returns true for ' true ' (trimmed)", () => {
expect(isEnvTruthy(" true ")).toBe(true);
});
});
expect(isEnvTruthy(' true ')).toBe(true)
})
})
// ─── isEnvDefinedFalsy ─────────────────────────────────────────────────
describe("isEnvDefinedFalsy", () => {
describe('isEnvDefinedFalsy', () => {
test("returns true for '0'", () => {
expect(isEnvDefinedFalsy("0")).toBe(true);
});
expect(isEnvDefinedFalsy('0')).toBe(true)
})
test("returns true for 'false'", () => {
expect(isEnvDefinedFalsy("false")).toBe(true);
});
expect(isEnvDefinedFalsy('false')).toBe(true)
})
test("returns true for 'no'", () => {
expect(isEnvDefinedFalsy("no")).toBe(true);
});
expect(isEnvDefinedFalsy('no')).toBe(true)
})
test("returns true for 'off'", () => {
expect(isEnvDefinedFalsy("off")).toBe(true);
});
expect(isEnvDefinedFalsy('off')).toBe(true)
})
test("returns true for boolean false", () => {
expect(isEnvDefinedFalsy(false)).toBe(true);
});
test('returns true for boolean false', () => {
expect(isEnvDefinedFalsy(false)).toBe(true)
})
test("returns false for undefined", () => {
expect(isEnvDefinedFalsy(undefined)).toBe(false);
});
test('returns false for undefined', () => {
expect(isEnvDefinedFalsy(undefined)).toBe(false)
})
test("returns false for '1'", () => {
expect(isEnvDefinedFalsy("1")).toBe(false);
});
expect(isEnvDefinedFalsy('1')).toBe(false)
})
test("returns false for 'true'", () => {
expect(isEnvDefinedFalsy("true")).toBe(false);
});
expect(isEnvDefinedFalsy('true')).toBe(false)
})
test("returns false for empty string", () => {
expect(isEnvDefinedFalsy("")).toBe(false);
});
});
test('returns false for empty string', () => {
expect(isEnvDefinedFalsy('')).toBe(false)
})
})
// ─── parseEnvVars ──────────────────────────────────────────────────────
describe("parseEnvVars", () => {
test("parses KEY=VALUE pairs", () => {
const result = parseEnvVars(["FOO=bar", "BAZ=qux"]);
expect(result).toEqual({ FOO: "bar", BAZ: "qux" });
});
describe('parseEnvVars', () => {
test('parses KEY=VALUE pairs', () => {
const result = parseEnvVars(['FOO=bar', 'BAZ=qux'])
expect(result).toEqual({ FOO: 'bar', BAZ: 'qux' })
})
test("handles value with equals sign", () => {
const result = parseEnvVars(["URL=http://host?a=1&b=2"]);
expect(result).toEqual({ URL: "http://host?a=1&b=2" });
});
test('handles value with equals sign', () => {
const result = parseEnvVars(['URL=http://host?a=1&b=2'])
expect(result).toEqual({ URL: 'http://host?a=1&b=2' })
})
test("returns empty object for undefined", () => {
expect(parseEnvVars(undefined)).toEqual({});
});
test('returns empty object for undefined', () => {
expect(parseEnvVars(undefined)).toEqual({})
})
test("returns empty object for empty array", () => {
expect(parseEnvVars([])).toEqual({});
});
test('returns empty object for empty array', () => {
expect(parseEnvVars([])).toEqual({})
})
test("throws for missing value", () => {
expect(() => parseEnvVars(["NOVALUE"])).toThrow("Invalid environment variable format");
});
test('throws for missing value', () => {
expect(() => parseEnvVars(['NOVALUE'])).toThrow(
'Invalid environment variable format',
)
})
test("throws for empty key", () => {
expect(() => parseEnvVars(["=value"])).toThrow("Invalid environment variable format");
});
});
test('throws for empty key', () => {
expect(() => parseEnvVars(['=value'])).toThrow(
'Invalid environment variable format',
)
})
})
// ─── hasNodeOption ─────────────────────────────────────────────────────
describe("hasNodeOption", () => {
const saved = process.env.NODE_OPTIONS;
describe('hasNodeOption', () => {
const saved = process.env.NODE_OPTIONS
afterEach(() => {
if (saved === undefined) delete process.env.NODE_OPTIONS;
else process.env.NODE_OPTIONS = saved;
});
if (saved === undefined) delete process.env.NODE_OPTIONS
else process.env.NODE_OPTIONS = saved
})
test("returns true when flag present", () => {
process.env.NODE_OPTIONS = "--max-old-space-size=4096 --inspect";
expect(hasNodeOption("--inspect")).toBe(true);
});
test('returns true when flag present', () => {
process.env.NODE_OPTIONS = '--max-old-space-size=4096 --inspect'
expect(hasNodeOption('--inspect')).toBe(true)
})
test("returns false when flag absent", () => {
process.env.NODE_OPTIONS = "--max-old-space-size=4096";
expect(hasNodeOption("--inspect")).toBe(false);
});
test('returns false when flag absent', () => {
process.env.NODE_OPTIONS = '--max-old-space-size=4096'
expect(hasNodeOption('--inspect')).toBe(false)
})
test("returns false when NODE_OPTIONS not set", () => {
delete process.env.NODE_OPTIONS;
expect(hasNodeOption("--inspect")).toBe(false);
});
test('returns false when NODE_OPTIONS not set', () => {
delete process.env.NODE_OPTIONS
expect(hasNodeOption('--inspect')).toBe(false)
})
test("does not match partial flags", () => {
process.env.NODE_OPTIONS = "--inspect-brk";
expect(hasNodeOption("--inspect")).toBe(false);
});
});
test('does not match partial flags', () => {
process.env.NODE_OPTIONS = '--inspect-brk'
expect(hasNodeOption('--inspect')).toBe(false)
})
})
// ─── getAWSRegion ──────────────────────────────────────────────────────
describe("getAWSRegion", () => {
const savedRegion = process.env.AWS_REGION;
const savedDefault = process.env.AWS_DEFAULT_REGION;
describe('getAWSRegion', () => {
const savedRegion = process.env.AWS_REGION
const savedDefault = process.env.AWS_DEFAULT_REGION
afterEach(() => {
if (savedRegion === undefined) delete process.env.AWS_REGION;
else process.env.AWS_REGION = savedRegion;
if (savedDefault === undefined) delete process.env.AWS_DEFAULT_REGION;
else process.env.AWS_DEFAULT_REGION = savedDefault;
});
if (savedRegion === undefined) delete process.env.AWS_REGION
else process.env.AWS_REGION = savedRegion
if (savedDefault === undefined) delete process.env.AWS_DEFAULT_REGION
else process.env.AWS_DEFAULT_REGION = savedDefault
})
test("uses AWS_REGION when set", () => {
process.env.AWS_REGION = "eu-west-1";
expect(getAWSRegion()).toBe("eu-west-1");
});
test('uses AWS_REGION when set', () => {
process.env.AWS_REGION = 'eu-west-1'
expect(getAWSRegion()).toBe('eu-west-1')
})
test("falls back to AWS_DEFAULT_REGION", () => {
delete process.env.AWS_REGION;
process.env.AWS_DEFAULT_REGION = "ap-northeast-1";
expect(getAWSRegion()).toBe("ap-northeast-1");
});
test('falls back to AWS_DEFAULT_REGION', () => {
delete process.env.AWS_REGION
process.env.AWS_DEFAULT_REGION = 'ap-northeast-1'
expect(getAWSRegion()).toBe('ap-northeast-1')
})
test("defaults to us-east-1", () => {
delete process.env.AWS_REGION;
delete process.env.AWS_DEFAULT_REGION;
expect(getAWSRegion()).toBe("us-east-1");
});
});
test('defaults to us-east-1', () => {
delete process.env.AWS_REGION
delete process.env.AWS_DEFAULT_REGION
expect(getAWSRegion()).toBe('us-east-1')
})
})
// ─── getDefaultVertexRegion ────────────────────────────────────────────
describe("getDefaultVertexRegion", () => {
const saved = process.env.CLOUD_ML_REGION;
describe('getDefaultVertexRegion', () => {
const saved = process.env.CLOUD_ML_REGION
afterEach(() => {
if (saved === undefined) delete process.env.CLOUD_ML_REGION;
else process.env.CLOUD_ML_REGION = saved;
});
if (saved === undefined) delete process.env.CLOUD_ML_REGION
else process.env.CLOUD_ML_REGION = saved
})
test("uses CLOUD_ML_REGION when set", () => {
process.env.CLOUD_ML_REGION = "europe-west4";
expect(getDefaultVertexRegion()).toBe("europe-west4");
});
test('uses CLOUD_ML_REGION when set', () => {
process.env.CLOUD_ML_REGION = 'europe-west4'
expect(getDefaultVertexRegion()).toBe('europe-west4')
})
test("defaults to us-east5", () => {
delete process.env.CLOUD_ML_REGION;
expect(getDefaultVertexRegion()).toBe("us-east5");
});
});
test('defaults to us-east5', () => {
delete process.env.CLOUD_ML_REGION
expect(getDefaultVertexRegion()).toBe('us-east5')
})
})
// ─── getVertexRegionForModel ───────────────────────────────────────────
describe("getVertexRegionForModel", () => {
describe('getVertexRegionForModel', () => {
const envKeys = [
"VERTEX_REGION_CLAUDE_HAIKU_4_5",
"VERTEX_REGION_CLAUDE_4_0_SONNET",
"VERTEX_REGION_CLAUDE_4_6_SONNET",
"CLOUD_ML_REGION",
];
const saved: Record<string, string | undefined> = {};
'VERTEX_REGION_CLAUDE_HAIKU_4_5',
'VERTEX_REGION_CLAUDE_4_0_SONNET',
'VERTEX_REGION_CLAUDE_4_6_SONNET',
'CLOUD_ML_REGION',
]
const saved: Record<string, string | undefined> = {}
beforeEach(() => {
for (const k of envKeys) saved[k] = process.env[k];
});
for (const k of envKeys) saved[k] = process.env[k]
})
afterEach(() => {
for (const k of envKeys) {
if (saved[k] === undefined) delete process.env[k];
else process.env[k] = saved[k];
if (saved[k] === undefined) delete process.env[k]
else process.env[k] = saved[k]
}
});
})
test("returns model-specific override when set", () => {
process.env.VERTEX_REGION_CLAUDE_HAIKU_4_5 = "us-central1";
expect(getVertexRegionForModel("claude-haiku-4-5-20251001")).toBe("us-central1");
});
test('returns model-specific override when set', () => {
process.env.VERTEX_REGION_CLAUDE_HAIKU_4_5 = 'us-central1'
expect(getVertexRegionForModel('claude-haiku-4-5-20251001')).toBe(
'us-central1',
)
})
test("falls back to default vertex region when override not set", () => {
delete process.env.VERTEX_REGION_CLAUDE_4_0_SONNET;
delete process.env.CLOUD_ML_REGION;
expect(getVertexRegionForModel("claude-sonnet-4-some-variant")).toBe("us-east5");
});
test('falls back to default vertex region when override not set', () => {
delete process.env.VERTEX_REGION_CLAUDE_4_0_SONNET
delete process.env.CLOUD_ML_REGION
expect(getVertexRegionForModel('claude-sonnet-4-some-variant')).toBe(
'us-east5',
)
})
test("returns default region for unknown model prefix", () => {
delete process.env.CLOUD_ML_REGION;
expect(getVertexRegionForModel("unknown-model-123")).toBe("us-east5");
});
test('returns default region for unknown model prefix', () => {
delete process.env.CLOUD_ML_REGION
expect(getVertexRegionForModel('unknown-model-123')).toBe('us-east5')
})
test("returns default region for undefined model", () => {
delete process.env.CLOUD_ML_REGION;
expect(getVertexRegionForModel(undefined)).toBe("us-east5");
});
});
test('returns default region for undefined model', () => {
delete process.env.CLOUD_ML_REGION
expect(getVertexRegionForModel(undefined)).toBe('us-east5')
})
})
// ─── isBareMode ────────────────────────────────────────────────────────
describe("isBareMode", () => {
const saved = process.env.CLAUDE_CODE_SIMPLE;
const originalArgv = [...process.argv];
describe('isBareMode', () => {
const saved = process.env.CLAUDE_CODE_SIMPLE
const originalArgv = [...process.argv]
afterEach(() => {
if (saved === undefined) delete process.env.CLAUDE_CODE_SIMPLE;
else process.env.CLAUDE_CODE_SIMPLE = saved;
process.argv.length = 0;
process.argv.push(...originalArgv);
});
if (saved === undefined) delete process.env.CLAUDE_CODE_SIMPLE
else process.env.CLAUDE_CODE_SIMPLE = saved
process.argv.length = 0
process.argv.push(...originalArgv)
})
test("returns true when CLAUDE_CODE_SIMPLE=1", () => {
process.env.CLAUDE_CODE_SIMPLE = "1";
expect(isBareMode()).toBe(true);
});
test('returns true when CLAUDE_CODE_SIMPLE=1', () => {
process.env.CLAUDE_CODE_SIMPLE = '1'
expect(isBareMode()).toBe(true)
})
test("returns true when --bare in argv", () => {
process.argv.push("--bare");
expect(isBareMode()).toBe(true);
});
test('returns true when --bare in argv', () => {
process.argv.push('--bare')
expect(isBareMode()).toBe(true)
})
test("returns false when neither set", () => {
delete process.env.CLAUDE_CODE_SIMPLE;
test('returns false when neither set', () => {
delete process.env.CLAUDE_CODE_SIMPLE
// argv doesn't have --bare by default
expect(isBareMode()).toBe(false);
});
});
expect(isBareMode()).toBe(false)
})
})
// ─── shouldMaintainProjectWorkingDir ───────────────────────────────────
describe("shouldMaintainProjectWorkingDir", () => {
const saved = process.env.CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR;
describe('shouldMaintainProjectWorkingDir', () => {
const saved = process.env.CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR
afterEach(() => {
if (saved === undefined) delete process.env.CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR;
else process.env.CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR = saved;
});
if (saved === undefined)
delete process.env.CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR
else process.env.CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR = saved
})
test("returns true when set to truthy", () => {
process.env.CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR = "1";
expect(shouldMaintainProjectWorkingDir()).toBe(true);
});
test('returns true when set to truthy', () => {
process.env.CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR = '1'
expect(shouldMaintainProjectWorkingDir()).toBe(true)
})
test("returns false when not set", () => {
delete process.env.CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR;
expect(shouldMaintainProjectWorkingDir()).toBe(false);
});
});
test('returns false when not set', () => {
delete process.env.CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR
expect(shouldMaintainProjectWorkingDir()).toBe(false)
})
})
// ─── getClaudeConfigHomeDir ────────────────────────────────────────────
describe("getClaudeConfigHomeDir", () => {
const saved = process.env.CLAUDE_CONFIG_DIR;
describe('getClaudeConfigHomeDir', () => {
const saved = process.env.CLAUDE_CONFIG_DIR
afterEach(() => {
if (saved === undefined) delete process.env.CLAUDE_CONFIG_DIR;
else process.env.CLAUDE_CONFIG_DIR = saved;
});
if (saved === undefined) delete process.env.CLAUDE_CONFIG_DIR
else process.env.CLAUDE_CONFIG_DIR = saved
})
test("uses CLAUDE_CONFIG_DIR when set", () => {
process.env.CLAUDE_CONFIG_DIR = "/tmp/test-claude";
test('uses CLAUDE_CONFIG_DIR when set', () => {
process.env.CLAUDE_CONFIG_DIR = '/tmp/test-claude'
// Memoized by CLAUDE_CONFIG_DIR key, so changing env gives fresh value
expect(getClaudeConfigHomeDir()).toBe("/tmp/test-claude");
});
expect(getClaudeConfigHomeDir()).toBe('/tmp/test-claude')
})
test("returns a string ending with .claude by default", () => {
delete process.env.CLAUDE_CONFIG_DIR;
const result = getClaudeConfigHomeDir();
expect(result).toMatch(/\.claude$/);
});
});
test('returns a string ending with .claude by default', () => {
delete process.env.CLAUDE_CONFIG_DIR
const result = getClaudeConfigHomeDir()
expect(result).toMatch(/\.claude$/)
})
})

View File

@@ -1,94 +1,94 @@
import { mock, describe, expect, test } from "bun:test";
import { debugMock } from "../../../tests/mocks/debug";
import { mock, describe, expect, test } from 'bun:test'
import { debugMock } from '../../../tests/mocks/debug'
// Mock debug.ts to cut bootstrap/state dependency chain
mock.module("src/utils/debug.ts", debugMock);
mock.module('src/utils/debug.ts', debugMock)
const { validateBoundedIntEnvVar } = await import("../envValidation");
const { validateBoundedIntEnvVar } = await import('../envValidation')
describe("validateBoundedIntEnvVar", () => {
test("returns default when value is undefined", () => {
const result = validateBoundedIntEnvVar("TEST_VAR", undefined, 100, 1000);
expect(result).toEqual({ effective: 100, status: "valid" });
});
describe('validateBoundedIntEnvVar', () => {
test('returns default when value is undefined', () => {
const result = validateBoundedIntEnvVar('TEST_VAR', undefined, 100, 1000)
expect(result).toEqual({ effective: 100, status: 'valid' })
})
test("returns default when value is empty string", () => {
const result = validateBoundedIntEnvVar("TEST_VAR", "", 100, 1000);
expect(result).toEqual({ effective: 100, status: "valid" });
});
test('returns default when value is empty string', () => {
const result = validateBoundedIntEnvVar('TEST_VAR', '', 100, 1000)
expect(result).toEqual({ effective: 100, status: 'valid' })
})
test("returns parsed value when valid and within limit", () => {
const result = validateBoundedIntEnvVar("TEST_VAR", "500", 100, 1000);
expect(result).toEqual({ effective: 500, status: "valid" });
});
test('returns parsed value when valid and within limit', () => {
const result = validateBoundedIntEnvVar('TEST_VAR', '500', 100, 1000)
expect(result).toEqual({ effective: 500, status: 'valid' })
})
test("caps value at upper limit", () => {
const result = validateBoundedIntEnvVar("TEST_VAR", "2000", 100, 1000);
expect(result.effective).toBe(1000);
expect(result.status).toBe("capped");
expect(result.message).toBe("Capped from 2000 to 1000");
});
test('caps value at upper limit', () => {
const result = validateBoundedIntEnvVar('TEST_VAR', '2000', 100, 1000)
expect(result.effective).toBe(1000)
expect(result.status).toBe('capped')
expect(result.message).toBe('Capped from 2000 to 1000')
})
test("returns default for non-numeric value", () => {
const result = validateBoundedIntEnvVar("TEST_VAR", "abc", 100, 1000);
expect(result.effective).toBe(100);
expect(result.status).toBe("invalid");
expect(result.message).toBe('Invalid value "abc" (using default: 100)');
});
test('returns default for non-numeric value', () => {
const result = validateBoundedIntEnvVar('TEST_VAR', 'abc', 100, 1000)
expect(result.effective).toBe(100)
expect(result.status).toBe('invalid')
expect(result.message).toBe('Invalid value "abc" (using default: 100)')
})
test("returns default for zero", () => {
const result = validateBoundedIntEnvVar("TEST_VAR", "0", 100, 1000);
expect(result.effective).toBe(100);
expect(result.status).toBe("invalid");
});
test('returns default for zero', () => {
const result = validateBoundedIntEnvVar('TEST_VAR', '0', 100, 1000)
expect(result.effective).toBe(100)
expect(result.status).toBe('invalid')
})
test("returns default for negative value", () => {
const result = validateBoundedIntEnvVar("TEST_VAR", "-5", 100, 1000);
expect(result.effective).toBe(100);
expect(result.status).toBe("invalid");
});
test('returns default for negative value', () => {
const result = validateBoundedIntEnvVar('TEST_VAR', '-5', 100, 1000)
expect(result.effective).toBe(100)
expect(result.status).toBe('invalid')
})
test("handles value at exact upper limit", () => {
const result = validateBoundedIntEnvVar("TEST_VAR", "1000", 100, 1000);
expect(result.effective).toBe(1000);
expect(result.status).toBe("valid");
});
test('handles value at exact upper limit', () => {
const result = validateBoundedIntEnvVar('TEST_VAR', '1000', 100, 1000)
expect(result.effective).toBe(1000)
expect(result.status).toBe('valid')
})
test("handles value of 1 (no lower bound check, only parsed > 0)", () => {
const result = validateBoundedIntEnvVar("TEST_VAR", "1", 100, 1000);
expect(result.effective).toBe(1);
expect(result.status).toBe("valid");
});
test('handles value of 1 (no lower bound check, only parsed > 0)', () => {
const result = validateBoundedIntEnvVar('TEST_VAR', '1', 100, 1000)
expect(result.effective).toBe(1)
expect(result.status).toBe('valid')
})
test("truncates float input via parseInt", () => {
const result = validateBoundedIntEnvVar("TEST_VAR", "50.7", 100, 1000);
expect(result.effective).toBe(50);
expect(result.status).toBe("valid");
});
test('truncates float input via parseInt', () => {
const result = validateBoundedIntEnvVar('TEST_VAR', '50.7', 100, 1000)
expect(result.effective).toBe(50)
expect(result.status).toBe('valid')
})
test("handles whitespace in value", () => {
const result = validateBoundedIntEnvVar("TEST_VAR", " 500 ", 100, 1000);
expect(result.effective).toBe(500);
expect(result.status).toBe("valid");
});
test('handles whitespace in value', () => {
const result = validateBoundedIntEnvVar('TEST_VAR', ' 500 ', 100, 1000)
expect(result.effective).toBe(500)
expect(result.status).toBe('valid')
})
test("value=1 with high defaultValue returns 1 (no lower bound enforcement)", () => {
test('value=1 with high defaultValue returns 1 (no lower bound enforcement)', () => {
// Function only checks parsed > 0 and parsed <= upperLimit
// It does NOT enforce that parsed >= defaultValue
const result = validateBoundedIntEnvVar("TEST_VAR", "1", 100, 1000);
expect(result.effective).toBe(1);
expect(result.status).toBe("valid");
});
const result = validateBoundedIntEnvVar('TEST_VAR', '1', 100, 1000)
expect(result.effective).toBe(1)
expect(result.status).toBe('valid')
})
test("caps very large number at upper limit", () => {
const result = validateBoundedIntEnvVar("TEST_VAR", "999999999", 100, 1000);
expect(result.effective).toBe(1000);
expect(result.status).toBe("capped");
});
test('caps very large number at upper limit', () => {
const result = validateBoundedIntEnvVar('TEST_VAR', '999999999', 100, 1000)
expect(result.effective).toBe(1000)
expect(result.status).toBe('capped')
})
test("treats NaN-producing strings as invalid", () => {
const result = validateBoundedIntEnvVar("TEST_VAR", "NaN", 100, 1000);
expect(result.effective).toBe(100);
expect(result.status).toBe("invalid");
});
});
test('treats NaN-producing strings as invalid', () => {
const result = validateBoundedIntEnvVar('TEST_VAR', 'NaN', 100, 1000)
expect(result.effective).toBe(100)
expect(result.status).toBe('invalid')
})
})

View File

@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
import {
AbortError,
ClaudeError,
@@ -16,274 +16,292 @@ import {
shortErrorStack,
isFsInaccessible,
classifyAxiosError,
} from "../errors";
} from '../errors'
// ─── Error classes ──────────────────────────────────────────────────────
describe("ClaudeError", () => {
test("sets name to constructor name", () => {
const e = new ClaudeError("test");
expect(e.name).toBe("ClaudeError");
expect(e.message).toBe("test");
});
});
describe('ClaudeError', () => {
test('sets name to constructor name', () => {
const e = new ClaudeError('test')
expect(e.name).toBe('ClaudeError')
expect(e.message).toBe('test')
})
})
describe("AbortError", () => {
test("sets name to AbortError", () => {
const e = new AbortError("cancelled");
expect(e.name).toBe("AbortError");
});
});
describe('AbortError', () => {
test('sets name to AbortError', () => {
const e = new AbortError('cancelled')
expect(e.name).toBe('AbortError')
})
})
describe("ConfigParseError", () => {
test("stores filePath and defaultConfig", () => {
const e = new ConfigParseError("bad", "/tmp/cfg", { x: 1 });
expect(e.filePath).toBe("/tmp/cfg");
expect(e.defaultConfig).toEqual({ x: 1 });
});
});
describe('ConfigParseError', () => {
test('stores filePath and defaultConfig', () => {
const e = new ConfigParseError('bad', '/tmp/cfg', { x: 1 })
expect(e.filePath).toBe('/tmp/cfg')
expect(e.defaultConfig).toEqual({ x: 1 })
})
})
describe("ShellError", () => {
test("stores stdout, stderr, code, interrupted", () => {
const e = new ShellError("out", "err", 1, false);
expect(e.stdout).toBe("out");
expect(e.stderr).toBe("err");
expect(e.code).toBe(1);
expect(e.interrupted).toBe(false);
});
});
describe('ShellError', () => {
test('stores stdout, stderr, code, interrupted', () => {
const e = new ShellError('out', 'err', 1, false)
expect(e.stdout).toBe('out')
expect(e.stderr).toBe('err')
expect(e.code).toBe(1)
expect(e.interrupted).toBe(false)
})
})
describe("TelemetrySafeError", () => {
test("uses message as telemetryMessage by default", () => {
const e =
new TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS("msg");
expect(e.telemetryMessage).toBe("msg");
});
describe('TelemetrySafeError', () => {
test('uses message as telemetryMessage by default', () => {
const e = new TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS(
'msg',
)
expect(e.telemetryMessage).toBe('msg')
})
test("uses separate telemetryMessage when provided", () => {
const e =
new TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS(
"full msg",
"safe msg"
);
expect(e.message).toBe("full msg");
expect(e.telemetryMessage).toBe("safe msg");
});
});
test('uses separate telemetryMessage when provided', () => {
const e = new TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS(
'full msg',
'safe msg',
)
expect(e.message).toBe('full msg')
expect(e.telemetryMessage).toBe('safe msg')
})
})
// ─── isAbortError ───────────────────────────────────────────────────────
describe("isAbortError", () => {
test("returns true for AbortError instance", () => {
expect(isAbortError(new AbortError())).toBe(true);
});
describe('isAbortError', () => {
test('returns true for AbortError instance', () => {
expect(isAbortError(new AbortError())).toBe(true)
})
test("returns true for DOMException-style abort", () => {
const e = new Error("aborted");
e.name = "AbortError";
expect(isAbortError(e)).toBe(true);
});
test('returns true for DOMException-style abort', () => {
const e = new Error('aborted')
e.name = 'AbortError'
expect(isAbortError(e)).toBe(true)
})
test("returns false for regular error", () => {
expect(isAbortError(new Error("nope"))).toBe(false);
});
test('returns false for regular error', () => {
expect(isAbortError(new Error('nope'))).toBe(false)
})
test("returns false for non-error", () => {
expect(isAbortError("string")).toBe(false);
expect(isAbortError(null)).toBe(false);
});
});
test('returns false for non-error', () => {
expect(isAbortError('string')).toBe(false)
expect(isAbortError(null)).toBe(false)
})
})
// ─── hasExactErrorMessage ───────────────────────────────────────────────
describe("hasExactErrorMessage", () => {
test("returns true for matching message", () => {
expect(hasExactErrorMessage(new Error("test"), "test")).toBe(true);
});
describe('hasExactErrorMessage', () => {
test('returns true for matching message', () => {
expect(hasExactErrorMessage(new Error('test'), 'test')).toBe(true)
})
test("returns false for different message", () => {
expect(hasExactErrorMessage(new Error("a"), "b")).toBe(false);
});
test('returns false for different message', () => {
expect(hasExactErrorMessage(new Error('a'), 'b')).toBe(false)
})
test("returns false for non-Error", () => {
expect(hasExactErrorMessage("string", "string")).toBe(false);
});
});
test('returns false for non-Error', () => {
expect(hasExactErrorMessage('string', 'string')).toBe(false)
})
})
// ─── toError ────────────────────────────────────────────────────────────
describe("toError", () => {
test("returns Error as-is", () => {
const e = new Error("test");
expect(toError(e)).toBe(e);
});
describe('toError', () => {
test('returns Error as-is', () => {
const e = new Error('test')
expect(toError(e)).toBe(e)
})
test("wraps string in Error", () => {
const e = toError("oops");
expect(e).toBeInstanceOf(Error);
expect(e.message).toBe("oops");
});
test('wraps string in Error', () => {
const e = toError('oops')
expect(e).toBeInstanceOf(Error)
expect(e.message).toBe('oops')
})
test("wraps number in Error", () => {
expect(toError(42).message).toBe("42");
});
});
test('wraps number in Error', () => {
expect(toError(42).message).toBe('42')
})
})
// ─── errorMessage ───────────────────────────────────────────────────────
describe("errorMessage", () => {
test("extracts message from Error", () => {
expect(errorMessage(new Error("hello"))).toBe("hello");
});
describe('errorMessage', () => {
test('extracts message from Error', () => {
expect(errorMessage(new Error('hello'))).toBe('hello')
})
test("stringifies non-Error", () => {
expect(errorMessage(42)).toBe("42");
expect(errorMessage(null)).toBe("null");
});
});
test('stringifies non-Error', () => {
expect(errorMessage(42)).toBe('42')
expect(errorMessage(null)).toBe('null')
})
})
// ─── getErrnoCode / isENOENT / getErrnoPath ────────────────────────────
describe("getErrnoCode", () => {
test("extracts code from errno-like error", () => {
const e = Object.assign(new Error(), { code: "ENOENT" });
expect(getErrnoCode(e)).toBe("ENOENT");
});
describe('getErrnoCode', () => {
test('extracts code from errno-like error', () => {
const e = Object.assign(new Error(), { code: 'ENOENT' })
expect(getErrnoCode(e)).toBe('ENOENT')
})
test("returns undefined for no code", () => {
expect(getErrnoCode(new Error())).toBeUndefined();
});
test('returns undefined for no code', () => {
expect(getErrnoCode(new Error())).toBeUndefined()
})
test("returns undefined for non-string code", () => {
expect(getErrnoCode({ code: 123 })).toBeUndefined();
});
test('returns undefined for non-string code', () => {
expect(getErrnoCode({ code: 123 })).toBeUndefined()
})
test("returns undefined for non-object", () => {
expect(getErrnoCode(null)).toBeUndefined();
expect(getErrnoCode("string")).toBeUndefined();
});
});
test('returns undefined for non-object', () => {
expect(getErrnoCode(null)).toBeUndefined()
expect(getErrnoCode('string')).toBeUndefined()
})
})
describe("isENOENT", () => {
test("returns true for ENOENT", () => {
expect(isENOENT(Object.assign(new Error(), { code: "ENOENT" }))).toBe(true);
});
describe('isENOENT', () => {
test('returns true for ENOENT', () => {
expect(isENOENT(Object.assign(new Error(), { code: 'ENOENT' }))).toBe(true)
})
test("returns false for other codes", () => {
expect(isENOENT(Object.assign(new Error(), { code: "EACCES" }))).toBe(
false
);
});
});
test('returns false for other codes', () => {
expect(isENOENT(Object.assign(new Error(), { code: 'EACCES' }))).toBe(false)
})
})
describe("getErrnoPath", () => {
test("extracts path from errno error", () => {
const e = Object.assign(new Error(), { path: "/tmp/file" });
expect(getErrnoPath(e)).toBe("/tmp/file");
});
describe('getErrnoPath', () => {
test('extracts path from errno error', () => {
const e = Object.assign(new Error(), { path: '/tmp/file' })
expect(getErrnoPath(e)).toBe('/tmp/file')
})
test("returns undefined when no path", () => {
expect(getErrnoPath(new Error())).toBeUndefined();
});
});
test('returns undefined when no path', () => {
expect(getErrnoPath(new Error())).toBeUndefined()
})
})
// ─── shortErrorStack ────────────────────────────────────────────────────
describe("shortErrorStack", () => {
test("returns string for non-Error", () => {
expect(shortErrorStack("oops")).toBe("oops");
});
describe('shortErrorStack', () => {
test('returns string for non-Error', () => {
expect(shortErrorStack('oops')).toBe('oops')
})
test("returns message when no stack", () => {
const e = new Error("test");
e.stack = undefined;
expect(shortErrorStack(e)).toBe("test");
});
test('returns message when no stack', () => {
const e = new Error('test')
e.stack = undefined
expect(shortErrorStack(e)).toBe('test')
})
test("truncates long stacks", () => {
const e = new Error("test");
const frames = Array.from({ length: 20 }, (_, i) => ` at frame${i}`);
e.stack = `Error: test\n${frames.join("\n")}`;
const result = shortErrorStack(e, 3);
const lines = result.split("\n");
expect(lines).toHaveLength(4); // header + 3 frames
});
test('truncates long stacks', () => {
const e = new Error('test')
const frames = Array.from({ length: 20 }, (_, i) => ` at frame${i}`)
e.stack = `Error: test\n${frames.join('\n')}`
const result = shortErrorStack(e, 3)
const lines = result.split('\n')
expect(lines).toHaveLength(4) // header + 3 frames
})
test("preserves short stacks", () => {
const e = new Error("test");
e.stack = "Error: test\n at frame1\n at frame2";
expect(shortErrorStack(e, 5)).toBe(e.stack);
});
});
test('preserves short stacks', () => {
const e = new Error('test')
e.stack = 'Error: test\n at frame1\n at frame2'
expect(shortErrorStack(e, 5)).toBe(e.stack)
})
})
// ─── isFsInaccessible ──────────────────────────────────────────────────
describe("isFsInaccessible", () => {
test("returns true for ENOENT", () => {
expect(isFsInaccessible(Object.assign(new Error(), { code: "ENOENT" }))).toBe(true);
});
describe('isFsInaccessible', () => {
test('returns true for ENOENT', () => {
expect(
isFsInaccessible(Object.assign(new Error(), { code: 'ENOENT' })),
).toBe(true)
})
test("returns true for EACCES", () => {
expect(isFsInaccessible(Object.assign(new Error(), { code: "EACCES" }))).toBe(true);
});
test('returns true for EACCES', () => {
expect(
isFsInaccessible(Object.assign(new Error(), { code: 'EACCES' })),
).toBe(true)
})
test("returns true for EPERM", () => {
expect(isFsInaccessible(Object.assign(new Error(), { code: "EPERM" }))).toBe(true);
});
test('returns true for EPERM', () => {
expect(
isFsInaccessible(Object.assign(new Error(), { code: 'EPERM' })),
).toBe(true)
})
test("returns true for ENOTDIR", () => {
expect(isFsInaccessible(Object.assign(new Error(), { code: "ENOTDIR" }))).toBe(true);
});
test('returns true for ENOTDIR', () => {
expect(
isFsInaccessible(Object.assign(new Error(), { code: 'ENOTDIR' })),
).toBe(true)
})
test("returns true for ELOOP", () => {
expect(isFsInaccessible(Object.assign(new Error(), { code: "ELOOP" }))).toBe(true);
});
test('returns true for ELOOP', () => {
expect(
isFsInaccessible(Object.assign(new Error(), { code: 'ELOOP' })),
).toBe(true)
})
test("returns false for other codes", () => {
expect(isFsInaccessible(Object.assign(new Error(), { code: "EEXIST" }))).toBe(false);
});
});
test('returns false for other codes', () => {
expect(
isFsInaccessible(Object.assign(new Error(), { code: 'EEXIST' })),
).toBe(false)
})
})
// ─── classifyAxiosError ─────────────────────────────────────────────────
describe("classifyAxiosError", () => {
describe('classifyAxiosError', () => {
test("returns 'other' for non-axios error", () => {
expect(classifyAxiosError(new Error("test")).kind).toBe("other");
});
expect(classifyAxiosError(new Error('test')).kind).toBe('other')
})
test("returns 'auth' for 401", () => {
const e = { isAxiosError: true, response: { status: 401 }, message: "unauth" };
expect(classifyAxiosError(e).kind).toBe("auth");
});
const e = {
isAxiosError: true,
response: { status: 401 },
message: 'unauth',
}
expect(classifyAxiosError(e).kind).toBe('auth')
})
test("returns 'auth' for 403", () => {
const e = { isAxiosError: true, response: { status: 403 }, message: "forbidden" };
expect(classifyAxiosError(e).kind).toBe("auth");
});
const e = {
isAxiosError: true,
response: { status: 403 },
message: 'forbidden',
}
expect(classifyAxiosError(e).kind).toBe('auth')
})
test("returns 'timeout' for ECONNABORTED", () => {
const e = { isAxiosError: true, code: "ECONNABORTED", message: "timeout" };
expect(classifyAxiosError(e).kind).toBe("timeout");
});
const e = { isAxiosError: true, code: 'ECONNABORTED', message: 'timeout' }
expect(classifyAxiosError(e).kind).toBe('timeout')
})
test("returns 'network' for ECONNREFUSED", () => {
const e = { isAxiosError: true, code: "ECONNREFUSED", message: "refused" };
expect(classifyAxiosError(e).kind).toBe("network");
});
const e = { isAxiosError: true, code: 'ECONNREFUSED', message: 'refused' }
expect(classifyAxiosError(e).kind).toBe('network')
})
test("returns 'network' for ENOTFOUND", () => {
const e = { isAxiosError: true, code: "ENOTFOUND", message: "nope" };
expect(classifyAxiosError(e).kind).toBe("network");
});
const e = { isAxiosError: true, code: 'ENOTFOUND', message: 'nope' }
expect(classifyAxiosError(e).kind).toBe('network')
})
test("returns 'http' for other axios errors", () => {
const e = { isAxiosError: true, response: { status: 500 }, message: "err" };
const result = classifyAxiosError(e);
expect(result.kind).toBe("http");
expect(result.status).toBe(500);
});
const e = { isAxiosError: true, response: { status: 500 }, message: 'err' }
const result = classifyAxiosError(e)
expect(result.kind).toBe('http')
expect(result.status).toBe(500)
})
test("returns 'other' for null", () => {
expect(classifyAxiosError(null).kind).toBe("other");
});
});
expect(classifyAxiosError(null).kind).toBe('other')
})
})

View File

@@ -1,92 +1,92 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
import {
convertLeadingTabsToSpaces,
addLineNumbers,
stripLineNumberPrefix,
pathsEqual,
normalizePathForComparison,
} from "../file";
} from '../file'
describe("convertLeadingTabsToSpaces", () => {
test("converts leading tabs to 2 spaces each", () => {
expect(convertLeadingTabsToSpaces("\t\thello")).toBe(" hello");
});
describe('convertLeadingTabsToSpaces', () => {
test('converts leading tabs to 2 spaces each', () => {
expect(convertLeadingTabsToSpaces('\t\thello')).toBe(' hello')
})
test("only converts leading tabs", () => {
expect(convertLeadingTabsToSpaces("\thello\tworld")).toBe(" hello\tworld");
});
test('only converts leading tabs', () => {
expect(convertLeadingTabsToSpaces('\thello\tworld')).toBe(' hello\tworld')
})
test("returns unchanged if no tabs", () => {
expect(convertLeadingTabsToSpaces("no tabs")).toBe("no tabs");
});
test('returns unchanged if no tabs', () => {
expect(convertLeadingTabsToSpaces('no tabs')).toBe('no tabs')
})
test("handles empty string", () => {
expect(convertLeadingTabsToSpaces("")).toBe("");
});
test('handles empty string', () => {
expect(convertLeadingTabsToSpaces('')).toBe('')
})
test("handles multiline content", () => {
const input = "\tline1\n\t\tline2\nline3";
const expected = " line1\n line2\nline3";
expect(convertLeadingTabsToSpaces(input)).toBe(expected);
});
});
test('handles multiline content', () => {
const input = '\tline1\n\t\tline2\nline3'
const expected = ' line1\n line2\nline3'
expect(convertLeadingTabsToSpaces(input)).toBe(expected)
})
})
describe("addLineNumbers", () => {
test("adds line numbers starting from 1", () => {
const result = addLineNumbers({ content: "a\nb\nc", startLine: 1 });
expect(result).toMatch(/^\s*1[→\t]a\n\s*2[→\t]b\n\s*3[→\t]c$/);
});
describe('addLineNumbers', () => {
test('adds line numbers starting from 1', () => {
const result = addLineNumbers({ content: 'a\nb\nc', startLine: 1 })
expect(result).toMatch(/^\s*1[→\t]a\n\s*2[→\t]b\n\s*3[→\t]c$/)
})
test("returns empty string for empty content", () => {
expect(addLineNumbers({ content: "", startLine: 1 })).toBe("");
});
test('returns empty string for empty content', () => {
expect(addLineNumbers({ content: '', startLine: 1 })).toBe('')
})
test("respects startLine offset", () => {
const result = addLineNumbers({ content: "hello", startLine: 10 });
expect(result).toMatch(/^\s*10[→\t]hello$/);
});
});
test('respects startLine offset', () => {
const result = addLineNumbers({ content: 'hello', startLine: 10 })
expect(result).toMatch(/^\s*10[→\t]hello$/)
})
})
describe("stripLineNumberPrefix", () => {
test("strips arrow-separated prefix", () => {
expect(stripLineNumberPrefix(" 1→content")).toBe("content");
});
describe('stripLineNumberPrefix', () => {
test('strips arrow-separated prefix', () => {
expect(stripLineNumberPrefix(' 1→content')).toBe('content')
})
test("strips tab-separated prefix", () => {
expect(stripLineNumberPrefix("1\tcontent")).toBe("content");
});
test('strips tab-separated prefix', () => {
expect(stripLineNumberPrefix('1\tcontent')).toBe('content')
})
test("returns line unchanged if no prefix", () => {
expect(stripLineNumberPrefix("no prefix")).toBe("no prefix");
});
test('returns line unchanged if no prefix', () => {
expect(stripLineNumberPrefix('no prefix')).toBe('no prefix')
})
test("handles large line numbers", () => {
expect(stripLineNumberPrefix("123456→content")).toBe("content");
});
});
test('handles large line numbers', () => {
expect(stripLineNumberPrefix('123456→content')).toBe('content')
})
})
describe("normalizePathForComparison", () => {
test("normalizes redundant separators", () => {
const result = normalizePathForComparison("/a//b/c");
expect(result).toBe("/a/b/c");
});
describe('normalizePathForComparison', () => {
test('normalizes redundant separators', () => {
const result = normalizePathForComparison('/a//b/c')
expect(result).toBe('/a/b/c')
})
test("resolves dot segments", () => {
const result = normalizePathForComparison("/a/./b/../c");
expect(result).toBe("/a/c");
});
});
test('resolves dot segments', () => {
const result = normalizePathForComparison('/a/./b/../c')
expect(result).toBe('/a/c')
})
})
describe("pathsEqual", () => {
test("returns true for identical paths", () => {
expect(pathsEqual("/a/b/c", "/a/b/c")).toBe(true);
});
describe('pathsEqual', () => {
test('returns true for identical paths', () => {
expect(pathsEqual('/a/b/c', '/a/b/c')).toBe(true)
})
test("returns true for equivalent paths with dot segments", () => {
expect(pathsEqual("/a/./b", "/a/b")).toBe(true);
});
test('returns true for equivalent paths with dot segments', () => {
expect(pathsEqual('/a/./b', '/a/b')).toBe(true)
})
test("returns false for different paths", () => {
expect(pathsEqual("/a/b", "/a/c")).toBe(false);
});
});
test('returns false for different paths', () => {
expect(pathsEqual('/a/b', '/a/c')).toBe(false)
})
})

View File

@@ -72,7 +72,12 @@ describe('FileStateCache LRU eviction', () => {
test('sizeCalculation handles null/undefined content', () => {
const cache = new FileStateCache(100, 10000)
cache.set('a', { content: null as unknown as string, timestamp: 0, offset: undefined, limit: undefined })
cache.set('a', {
content: null as unknown as string,
timestamp: 0,
offset: undefined,
limit: undefined,
})
expect(cache.calculatedSize).toBe(1) // Math.max(1, 0) = 1
})

View File

@@ -1,122 +1,121 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
import {
FINGERPRINT_SALT,
extractFirstMessageText,
computeFingerprint,
} from "../fingerprint";
} from '../fingerprint'
describe("FINGERPRINT_SALT", () => {
describe('FINGERPRINT_SALT', () => {
test("has expected value '59cf53e54c78'", () => {
expect(FINGERPRINT_SALT).toBe("59cf53e54c78");
});
});
expect(FINGERPRINT_SALT).toBe('59cf53e54c78')
})
})
describe("extractFirstMessageText", () => {
test("extracts text from first user message", () => {
const messages = [
{ type: "user", message: { content: "hello world" } },
];
expect(extractFirstMessageText(messages as any)).toBe("hello world");
});
describe('extractFirstMessageText', () => {
test('extracts text from first user message', () => {
const messages = [{ type: 'user', message: { content: 'hello world' } }]
expect(extractFirstMessageText(messages as any)).toBe('hello world')
})
test("extracts text from single user message with array content", () => {
test('extracts text from single user message with array content', () => {
const messages = [
{
type: "user",
message: {
content: [{ type: "text", text: "hello" }, { type: "image", url: "x" }],
},
},
];
expect(extractFirstMessageText(messages as any)).toBe("hello");
});
test("returns empty string when no user messages", () => {
const messages = [
{ type: "assistant", message: { content: "hi" } },
];
expect(extractFirstMessageText(messages as any)).toBe("");
});
test("skips assistant messages", () => {
const messages = [
{ type: "assistant", message: { content: "hi" } },
{ type: "user", message: { content: "hello" } },
];
expect(extractFirstMessageText(messages as any)).toBe("hello");
});
test("handles mixed content blocks (text + image)", () => {
const messages = [
{
type: "user",
type: 'user',
message: {
content: [
{ type: "image", url: "http://example.com/img.png" },
{ type: "text", text: "after image" },
{ type: 'text', text: 'hello' },
{ type: 'image', url: 'x' },
],
},
},
];
expect(extractFirstMessageText(messages as any)).toBe("after image");
});
]
expect(extractFirstMessageText(messages as any)).toBe('hello')
})
test("returns empty string for empty array", () => {
expect(extractFirstMessageText([])).toBe("");
});
test('returns empty string when no user messages', () => {
const messages = [{ type: 'assistant', message: { content: 'hi' } }]
expect(extractFirstMessageText(messages as any)).toBe('')
})
test("returns empty string when content has no text block", () => {
test('skips assistant messages', () => {
const messages = [
{ type: 'assistant', message: { content: 'hi' } },
{ type: 'user', message: { content: 'hello' } },
]
expect(extractFirstMessageText(messages as any)).toBe('hello')
})
test('handles mixed content blocks (text + image)', () => {
const messages = [
{
type: "user",
type: 'user',
message: {
content: [{ type: "image", url: "x" }],
content: [
{ type: 'image', url: 'http://example.com/img.png' },
{ type: 'text', text: 'after image' },
],
},
},
];
expect(extractFirstMessageText(messages as any)).toBe("");
});
});
]
expect(extractFirstMessageText(messages as any)).toBe('after image')
})
describe("computeFingerprint", () => {
test("returns deterministic 3-char hex string", () => {
const result = computeFingerprint("test message", "1.0.0");
expect(result).toHaveLength(3);
expect(result).toMatch(/^[0-9a-f]{3}$/);
});
test('returns empty string for empty array', () => {
expect(extractFirstMessageText([])).toBe('')
})
test("same input produces same fingerprint", () => {
const a = computeFingerprint("same input", "1.0.0");
const b = computeFingerprint("same input", "1.0.0");
expect(a).toBe(b);
});
test('returns empty string when content has no text block', () => {
const messages = [
{
type: 'user',
message: {
content: [{ type: 'image', url: 'x' }],
},
},
]
expect(extractFirstMessageText(messages as any)).toBe('')
})
})
test("different message text produces different fingerprint", () => {
const a = computeFingerprint("hello world from test one", "1.0.0");
const b = computeFingerprint("goodbye world from test two", "1.0.0");
expect(a).not.toBe(b);
});
describe('computeFingerprint', () => {
test('returns deterministic 3-char hex string', () => {
const result = computeFingerprint('test message', '1.0.0')
expect(result).toHaveLength(3)
expect(result).toMatch(/^[0-9a-f]{3}$/)
})
test("different version produces different fingerprint", () => {
const a = computeFingerprint("same text", "1.0.0");
const b = computeFingerprint("same text", "2.0.0");
expect(a).not.toBe(b);
});
test('same input produces same fingerprint', () => {
const a = computeFingerprint('same input', '1.0.0')
const b = computeFingerprint('same input', '1.0.0')
expect(a).toBe(b)
})
test("handles short strings (length < 21)", () => {
const result = computeFingerprint("hi", "1.0.0");
expect(result).toHaveLength(3);
expect(result).toMatch(/^[0-9a-f]{3}$/);
});
test('different message text produces different fingerprint', () => {
const a = computeFingerprint('hello world from test one', '1.0.0')
const b = computeFingerprint('goodbye world from test two', '1.0.0')
expect(a).not.toBe(b)
})
test("handles empty string", () => {
const result = computeFingerprint("", "1.0.0");
expect(result).toHaveLength(3);
expect(result).toMatch(/^[0-9a-f]{3}$/);
});
test('different version produces different fingerprint', () => {
const a = computeFingerprint('same text', '1.0.0')
const b = computeFingerprint('same text', '2.0.0')
expect(a).not.toBe(b)
})
test("fingerprint is valid hex", () => {
const result = computeFingerprint("any message here for testing", "3.5.1");
expect(result).toMatch(/^[0-9a-f]{3}$/);
});
});
test('handles short strings (length < 21)', () => {
const result = computeFingerprint('hi', '1.0.0')
expect(result).toHaveLength(3)
expect(result).toMatch(/^[0-9a-f]{3}$/)
})
test('handles empty string', () => {
const result = computeFingerprint('', '1.0.0')
expect(result).toHaveLength(3)
expect(result).toMatch(/^[0-9a-f]{3}$/)
})
test('fingerprint is valid hex', () => {
const result = computeFingerprint('any message here for testing', '3.5.1')
expect(result).toMatch(/^[0-9a-f]{3}$/)
})
})

View File

@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
import {
formatFileSize,
formatSecondsShort,
@@ -8,291 +8,291 @@ import {
formatRelativeTime,
formatRelativeTimeAgo,
formatLogMetadata,
} from "../format";
} from '../format'
describe("formatFileSize", () => {
test("formats bytes", () => {
expect(formatFileSize(500)).toBe("500 bytes");
});
describe('formatFileSize', () => {
test('formats bytes', () => {
expect(formatFileSize(500)).toBe('500 bytes')
})
test("formats kilobytes", () => {
expect(formatFileSize(1536)).toBe("1.5KB");
});
test('formats kilobytes', () => {
expect(formatFileSize(1536)).toBe('1.5KB')
})
test("formats megabytes", () => {
expect(formatFileSize(1.5 * 1024 * 1024)).toBe("1.5MB");
});
test('formats megabytes', () => {
expect(formatFileSize(1.5 * 1024 * 1024)).toBe('1.5MB')
})
test("formats gigabytes", () => {
expect(formatFileSize(2 * 1024 * 1024 * 1024)).toBe("2GB");
});
test('formats gigabytes', () => {
expect(formatFileSize(2 * 1024 * 1024 * 1024)).toBe('2GB')
})
test("removes trailing .0", () => {
expect(formatFileSize(1024)).toBe("1KB");
});
});
test('removes trailing .0', () => {
expect(formatFileSize(1024)).toBe('1KB')
})
})
describe("formatSecondsShort", () => {
test("formats milliseconds to seconds", () => {
expect(formatSecondsShort(1234)).toBe("1.2s");
});
describe('formatSecondsShort', () => {
test('formats milliseconds to seconds', () => {
expect(formatSecondsShort(1234)).toBe('1.2s')
})
test("formats zero", () => {
expect(formatSecondsShort(0)).toBe("0.0s");
});
test('formats zero', () => {
expect(formatSecondsShort(0)).toBe('0.0s')
})
test("formats sub-second", () => {
expect(formatSecondsShort(500)).toBe("0.5s");
});
});
test('formats sub-second', () => {
expect(formatSecondsShort(500)).toBe('0.5s')
})
})
describe("formatDuration", () => {
test("formats 0 as 0s", () => {
expect(formatDuration(0)).toBe("0s");
});
describe('formatDuration', () => {
test('formats 0 as 0s', () => {
expect(formatDuration(0)).toBe('0s')
})
test("formats seconds", () => {
expect(formatDuration(5000)).toBe("5s");
});
test('formats seconds', () => {
expect(formatDuration(5000)).toBe('5s')
})
test("formats minutes and seconds", () => {
expect(formatDuration(125000)).toBe("2m 5s");
});
test('formats minutes and seconds', () => {
expect(formatDuration(125000)).toBe('2m 5s')
})
test("formats hours", () => {
expect(formatDuration(3661000)).toBe("1h 1m 1s");
});
test('formats hours', () => {
expect(formatDuration(3661000)).toBe('1h 1m 1s')
})
test("formats days", () => {
expect(formatDuration(90000000)).toBe("1d 1h 0m");
});
test('formats days', () => {
expect(formatDuration(90000000)).toBe('1d 1h 0m')
})
test("hideTrailingZeros removes zero components", () => {
expect(formatDuration(3600000, { hideTrailingZeros: true })).toBe("1h");
expect(formatDuration(60000, { hideTrailingZeros: true })).toBe("1m");
});
test('hideTrailingZeros removes zero components', () => {
expect(formatDuration(3600000, { hideTrailingZeros: true })).toBe('1h')
expect(formatDuration(60000, { hideTrailingZeros: true })).toBe('1m')
})
test("mostSignificantOnly returns largest unit", () => {
expect(formatDuration(90000000, { mostSignificantOnly: true })).toBe("1d");
expect(formatDuration(3661000, { mostSignificantOnly: true })).toBe("1h");
});
});
test('mostSignificantOnly returns largest unit', () => {
expect(formatDuration(90000000, { mostSignificantOnly: true })).toBe('1d')
expect(formatDuration(3661000, { mostSignificantOnly: true })).toBe('1h')
})
})
describe("formatNumber", () => {
test("formats small numbers as-is", () => {
expect(formatNumber(900)).toBe("900");
});
describe('formatNumber', () => {
test('formats small numbers as-is', () => {
expect(formatNumber(900)).toBe('900')
})
test("formats thousands with k suffix", () => {
expect(formatNumber(1321)).toBe("1.3k");
});
test('formats thousands with k suffix', () => {
expect(formatNumber(1321)).toBe('1.3k')
})
test("formats millions", () => {
expect(formatNumber(1500000)).toBe("1.5m");
});
test('formats millions', () => {
expect(formatNumber(1500000)).toBe('1.5m')
})
test("formats 0 as-is", () => {
expect(formatNumber(0)).toBe("0");
});
test('formats 0 as-is', () => {
expect(formatNumber(0)).toBe('0')
})
test("formats billions", () => {
expect(formatNumber(1500000000)).toBe("1.5b");
});
});
test('formats billions', () => {
expect(formatNumber(1500000000)).toBe('1.5b')
})
})
describe("formatTokens", () => {
test("removes .0 from formatted number", () => {
expect(formatTokens(1000)).toBe("1k");
});
describe('formatTokens', () => {
test('removes .0 from formatted number', () => {
expect(formatTokens(1000)).toBe('1k')
})
test("formats small numbers", () => {
expect(formatTokens(500)).toBe("500");
});
test('formats small numbers', () => {
expect(formatTokens(500)).toBe('500')
})
test("formats 1000 without .0", () => {
expect(formatTokens(1000)).toBe("1k");
});
test('formats 1000 without .0', () => {
expect(formatTokens(1000)).toBe('1k')
})
test("formats 1500 as 1.5k", () => {
expect(formatTokens(1500)).toBe("1.5k");
});
});
test('formats 1500 as 1.5k', () => {
expect(formatTokens(1500)).toBe('1.5k')
})
})
describe("formatRelativeTime", () => {
const now = new Date("2026-01-15T12:00:00Z");
describe('formatRelativeTime', () => {
const now = new Date('2026-01-15T12:00:00Z')
test("formats seconds ago", () => {
const date = new Date("2026-01-15T11:59:30Z");
expect(formatRelativeTime(date, { now })).toBe("30s ago");
});
test('formats seconds ago', () => {
const date = new Date('2026-01-15T11:59:30Z')
expect(formatRelativeTime(date, { now })).toBe('30s ago')
})
test("formats minutes ago", () => {
const date = new Date("2026-01-15T11:55:00Z");
expect(formatRelativeTime(date, { now })).toBe("5m ago");
});
test('formats minutes ago', () => {
const date = new Date('2026-01-15T11:55:00Z')
expect(formatRelativeTime(date, { now })).toBe('5m ago')
})
test("formats future time", () => {
const date = new Date("2026-01-15T13:00:00Z");
expect(formatRelativeTime(date, { now })).toBe("in 1h");
});
test('formats future time', () => {
const date = new Date('2026-01-15T13:00:00Z')
expect(formatRelativeTime(date, { now })).toBe('in 1h')
})
test("handles zero difference", () => {
expect(formatRelativeTime(now, { now })).toBe("0s ago");
});
test('handles zero difference', () => {
expect(formatRelativeTime(now, { now })).toBe('0s ago')
})
test("formats hours ago", () => {
const date = new Date("2026-01-15T09:00:00Z");
expect(formatRelativeTime(date, { now })).toBe("3h ago");
});
test('formats hours ago', () => {
const date = new Date('2026-01-15T09:00:00Z')
expect(formatRelativeTime(date, { now })).toBe('3h ago')
})
test("formats days ago", () => {
const date = new Date("2026-01-13T12:00:00Z");
expect(formatRelativeTime(date, { now })).toBe("2d ago");
});
test('formats days ago', () => {
const date = new Date('2026-01-13T12:00:00Z')
expect(formatRelativeTime(date, { now })).toBe('2d ago')
})
test("formats weeks ago", () => {
const date = new Date("2026-01-01T12:00:00Z");
expect(formatRelativeTime(date, { now })).toBe("2w ago");
});
});
test('formats weeks ago', () => {
const date = new Date('2026-01-01T12:00:00Z')
expect(formatRelativeTime(date, { now })).toBe('2w ago')
})
})
describe("formatRelativeTimeAgo", () => {
const now = new Date("2026-01-15T12:00:00Z");
describe('formatRelativeTimeAgo', () => {
const now = new Date('2026-01-15T12:00:00Z')
test("formats past date with 'ago' suffix", () => {
const date = new Date("2026-01-15T11:59:30Z");
const result = formatRelativeTimeAgo(date, { now });
expect(result).toBe("30s ago");
});
const date = new Date('2026-01-15T11:59:30Z')
const result = formatRelativeTimeAgo(date, { now })
expect(result).toBe('30s ago')
})
test("formats future date without 'ago' suffix", () => {
const date = new Date("2026-01-15T13:00:00Z");
const result = formatRelativeTimeAgo(date, { now });
expect(result).toBe("in 1h");
});
const date = new Date('2026-01-15T13:00:00Z')
const result = formatRelativeTimeAgo(date, { now })
expect(result).toBe('in 1h')
})
test("formats minutes ago", () => {
const date = new Date("2026-01-15T11:55:00Z");
const result = formatRelativeTimeAgo(date, { now });
expect(result).toBe("5m ago");
});
test('formats minutes ago', () => {
const date = new Date('2026-01-15T11:55:00Z')
const result = formatRelativeTimeAgo(date, { now })
expect(result).toBe('5m ago')
})
test("formats hours ago", () => {
const date = new Date("2026-01-15T09:00:00Z");
const result = formatRelativeTimeAgo(date, { now });
expect(result).toBe("3h ago");
});
test('formats hours ago', () => {
const date = new Date('2026-01-15T09:00:00Z')
const result = formatRelativeTimeAgo(date, { now })
expect(result).toBe('3h ago')
})
test("formats days ago", () => {
const date = new Date("2026-01-13T12:00:00Z");
const result = formatRelativeTimeAgo(date, { now });
expect(result).toBe("2d ago");
});
test('formats days ago', () => {
const date = new Date('2026-01-13T12:00:00Z')
const result = formatRelativeTimeAgo(date, { now })
expect(result).toBe('2d ago')
})
test("handles date equal to now as past", () => {
test('handles date equal to now as past', () => {
// date === now, treated as past (not future)
const result = formatRelativeTimeAgo(now, { now });
expect(result).toBe("0s ago");
});
const result = formatRelativeTimeAgo(now, { now })
expect(result).toBe('0s ago')
})
test("uses numeric always for past dates", () => {
test('uses numeric always for past dates', () => {
// Should always use numeric format for past dates
const date = new Date("2026-01-15T11:59:00Z");
const result = formatRelativeTimeAgo(date, { now });
expect(result).toContain("ago");
});
const date = new Date('2026-01-15T11:59:00Z')
const result = formatRelativeTimeAgo(date, { now })
expect(result).toContain('ago')
})
test("future date does not contain 'ago'", () => {
const date = new Date("2026-01-15T14:00:00Z");
const result = formatRelativeTimeAgo(date, { now });
expect(result).not.toContain("ago");
});
});
const date = new Date('2026-01-15T14:00:00Z')
const result = formatRelativeTimeAgo(date, { now })
expect(result).not.toContain('ago')
})
})
describe("formatLogMetadata", () => {
describe('formatLogMetadata', () => {
// Use a date very recently in the past so it always shows "Xs ago" or similar
const modified = new Date(Date.now() - 5 * 60 * 1000); // 5 minutes ago
const modified = new Date(Date.now() - 5 * 60 * 1000) // 5 minutes ago
test("includes relative time and message count", () => {
test('includes relative time and message count', () => {
const result = formatLogMetadata({
modified,
messageCount: 10,
});
expect(result).toContain("ago");
expect(result).toContain("10 messages");
});
})
expect(result).toContain('ago')
expect(result).toContain('10 messages')
})
test("uses fileSize instead of messageCount when provided", () => {
test('uses fileSize instead of messageCount when provided', () => {
const result = formatLogMetadata({
modified,
messageCount: 5,
fileSize: 1536,
});
expect(result).toContain("1.5KB");
expect(result).not.toContain("messages");
});
})
expect(result).toContain('1.5KB')
expect(result).not.toContain('messages')
})
test("includes gitBranch when provided", () => {
test('includes gitBranch when provided', () => {
const result = formatLogMetadata({
modified,
messageCount: 3,
gitBranch: "main",
});
expect(result).toContain("main");
});
gitBranch: 'main',
})
expect(result).toContain('main')
})
test("omits gitBranch when not provided", () => {
test('omits gitBranch when not provided', () => {
const result = formatLogMetadata({
modified,
messageCount: 3,
});
})
// Should not have a dangling separator from missing branch
expect(result).not.toMatch(/^ · | · $/);
});
expect(result).not.toMatch(/^ · | · $/)
})
test("includes tag when provided", () => {
test('includes tag when provided', () => {
const result = formatLogMetadata({
modified,
messageCount: 3,
tag: "my-tag",
});
expect(result).toContain("#my-tag");
});
tag: 'my-tag',
})
expect(result).toContain('#my-tag')
})
test("includes agentSetting when provided", () => {
test('includes agentSetting when provided', () => {
const result = formatLogMetadata({
modified,
messageCount: 3,
agentSetting: "custom-agent",
});
expect(result).toContain("@custom-agent");
});
agentSetting: 'custom-agent',
})
expect(result).toContain('@custom-agent')
})
test("includes prNumber when provided", () => {
test('includes prNumber when provided', () => {
const result = formatLogMetadata({
modified,
messageCount: 3,
prNumber: 42,
});
expect(result).toContain("#42");
});
})
expect(result).toContain('#42')
})
test("includes prRepository with prNumber when both provided", () => {
test('includes prRepository with prNumber when both provided', () => {
const result = formatLogMetadata({
modified,
messageCount: 3,
prNumber: 99,
prRepository: "owner/repo",
});
expect(result).toContain("owner/repo#99");
});
prRepository: 'owner/repo',
})
expect(result).toContain('owner/repo#99')
})
test("parts are joined with ' · ' separator", () => {
const result = formatLogMetadata({
modified,
messageCount: 5,
gitBranch: "feat/x",
});
expect(result).toContain(" · ");
});
});
gitBranch: 'feat/x',
})
expect(result).toContain(' · ')
})
})

View File

@@ -1,86 +1,86 @@
import { describe, expect, test, beforeAll, afterAll } from "bun:test";
import { formatBriefTimestamp } from "../formatBriefTimestamp";
import { describe, expect, test, beforeAll, afterAll } from 'bun:test'
import { formatBriefTimestamp } from '../formatBriefTimestamp'
let savedLcAll: string | undefined;
let savedLcAll: string | undefined
beforeAll(() => {
savedLcAll = process.env.LC_ALL;
process.env.LC_ALL = "en_US.UTF-8";
});
savedLcAll = process.env.LC_ALL
process.env.LC_ALL = 'en_US.UTF-8'
})
afterAll(() => {
if (savedLcAll === undefined) delete process.env.LC_ALL;
else process.env.LC_ALL = savedLcAll;
});
if (savedLcAll === undefined) delete process.env.LC_ALL
else process.env.LC_ALL = savedLcAll
})
describe("formatBriefTimestamp", () => {
describe('formatBriefTimestamp', () => {
// Fixed "now" for deterministic tests: 2026-04-02T14:00:00Z (Thursday)
const now = new Date("2026-04-02T14:00:00Z");
const now = new Date('2026-04-02T14:00:00Z')
test("same day timestamp returns time only (contains colon)", () => {
const result = formatBriefTimestamp("2026-04-02T10:30:00Z", now);
expect(result).toContain(":");
test('same day timestamp returns time only (contains colon)', () => {
const result = formatBriefTimestamp('2026-04-02T10:30:00Z', now)
expect(result).toContain(':')
// Should NOT contain a weekday name since it's the same day
expect(result).not.toMatch(
/Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday/
);
});
/Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday/,
)
})
test("yesterday returns weekday and time", () => {
test('yesterday returns weekday and time', () => {
// 2026-04-01 is Wednesday
const result = formatBriefTimestamp("2026-04-01T16:15:00Z", now);
expect(result).toContain("Wednesday");
expect(result).toContain(":");
});
const result = formatBriefTimestamp('2026-04-01T16:15:00Z', now)
expect(result).toContain('Wednesday')
expect(result).toContain(':')
})
test("3 days ago returns weekday and time", () => {
test('3 days ago returns weekday and time', () => {
// 2026-03-30 is Monday
const result = formatBriefTimestamp("2026-03-30T09:00:00Z", now);
expect(result).toContain("Monday");
expect(result).toContain(":");
});
const result = formatBriefTimestamp('2026-03-30T09:00:00Z', now)
expect(result).toContain('Monday')
expect(result).toContain(':')
})
test("6 days ago returns weekday and time (still within 6-day window)", () => {
test('6 days ago returns weekday and time (still within 6-day window)', () => {
// 2026-03-27 is Friday
const result = formatBriefTimestamp("2026-03-27T12:00:00Z", now);
expect(result).toContain("Friday");
expect(result).toContain(":");
});
const result = formatBriefTimestamp('2026-03-27T12:00:00Z', now)
expect(result).toContain('Friday')
expect(result).toContain(':')
})
test("7+ days ago returns weekday, month, day, and time", () => {
test('7+ days ago returns weekday, month, day, and time', () => {
// 2026-03-20 is Friday, 13 days ago
const result = formatBriefTimestamp("2026-03-20T14:30:00Z", now);
expect(result).toContain("Friday");
expect(result).toContain(":");
const result = formatBriefTimestamp('2026-03-20T14:30:00Z', now)
expect(result).toContain('Friday')
expect(result).toContain(':')
// Should contain month abbreviation (Mar)
expect(result).toMatch(/Mar/);
});
expect(result).toMatch(/Mar/)
})
test("much older date returns full format with month", () => {
const result = formatBriefTimestamp("2025-12-25T08:00:00Z", now);
expect(result).toContain(":");
expect(result).toMatch(/Dec/);
});
test('much older date returns full format with month', () => {
const result = formatBriefTimestamp('2025-12-25T08:00:00Z', now)
expect(result).toContain(':')
expect(result).toMatch(/Dec/)
})
test("invalid ISO string returns empty string", () => {
expect(formatBriefTimestamp("not-a-date", now)).toBe("");
});
test('invalid ISO string returns empty string', () => {
expect(formatBriefTimestamp('not-a-date', now)).toBe('')
})
test("empty string returns empty string", () => {
expect(formatBriefTimestamp("", now)).toBe("");
});
test('empty string returns empty string', () => {
expect(formatBriefTimestamp('', now)).toBe('')
})
test("same day early morning returns time format", () => {
const result = formatBriefTimestamp("2026-04-02T01:05:00Z", now);
expect(result).toContain(":");
test('same day early morning returns time format', () => {
const result = formatBriefTimestamp('2026-04-02T01:05:00Z', now)
expect(result).toContain(':')
// Should be time-only format
expect(result.length).toBeLessThan(20);
});
expect(result.length).toBeLessThan(20)
})
test("uses current time as default when now is not provided", () => {
test('uses current time as default when now is not provided', () => {
// Just verify it returns a non-empty string for a recent timestamp
const recent = new Date();
recent.setMinutes(recent.getMinutes() - 5);
const result = formatBriefTimestamp(recent.toISOString());
expect(result).not.toBe("");
expect(result).toContain(":");
});
});
const recent = new Date()
recent.setMinutes(recent.getMinutes() - 5)
const result = formatBriefTimestamp(recent.toISOString())
expect(result).not.toBe('')
expect(result).toContain(':')
})
})

View File

@@ -1,164 +1,167 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
import {
parseFrontmatter,
splitPathInFrontmatter,
parsePositiveIntFromFrontmatter,
parseBooleanFrontmatter,
parseShellFrontmatter,
} from "../frontmatterParser";
} from '../frontmatterParser'
describe("parseFrontmatter", () => {
test("parses valid frontmatter", () => {
describe('parseFrontmatter', () => {
test('parses valid frontmatter', () => {
const md = `---
description: A test
type: user
---
Content here`;
const result = parseFrontmatter(md);
expect(result.frontmatter.description).toBe("A test");
expect(result.frontmatter.type).toBe("user");
expect(result.content).toBe("Content here");
});
Content here`
const result = parseFrontmatter(md)
expect(result.frontmatter.description).toBe('A test')
expect(result.frontmatter.type).toBe('user')
expect(result.content).toBe('Content here')
})
test("returns empty frontmatter when none exists", () => {
const md = "Just content, no frontmatter";
const result = parseFrontmatter(md);
expect(result.frontmatter).toEqual({});
expect(result.content).toBe(md);
});
test('returns empty frontmatter when none exists', () => {
const md = 'Just content, no frontmatter'
const result = parseFrontmatter(md)
expect(result.frontmatter).toEqual({})
expect(result.content).toBe(md)
})
test("handles empty frontmatter block", () => {
test('handles empty frontmatter block', () => {
const md = `---
---
Content`;
const result = parseFrontmatter(md);
expect(result.frontmatter).toEqual({});
expect(result.content).toBe("Content");
});
Content`
const result = parseFrontmatter(md)
expect(result.frontmatter).toEqual({})
expect(result.content).toBe('Content')
})
test("handles frontmatter with list values", () => {
test('handles frontmatter with list values', () => {
const md = `---
allowed-tools:
- Bash
- Read
---
Content`;
const result = parseFrontmatter(md);
expect(result.frontmatter["allowed-tools"]).toEqual(["Bash", "Read"]);
});
});
Content`
const result = parseFrontmatter(md)
expect(result.frontmatter['allowed-tools']).toEqual(['Bash', 'Read'])
})
})
describe("splitPathInFrontmatter", () => {
test("splits comma-separated paths", () => {
expect(splitPathInFrontmatter("a, b, c")).toEqual(["a", "b", "c"]);
});
describe('splitPathInFrontmatter', () => {
test('splits comma-separated paths', () => {
expect(splitPathInFrontmatter('a, b, c')).toEqual(['a', 'b', 'c'])
})
test("expands brace patterns", () => {
expect(splitPathInFrontmatter("src/*.{ts,tsx}")).toEqual([
"src/*.ts",
"src/*.tsx",
]);
});
test('expands brace patterns', () => {
expect(splitPathInFrontmatter('src/*.{ts,tsx}')).toEqual([
'src/*.ts',
'src/*.tsx',
])
})
test("handles nested brace expansion", () => {
expect(splitPathInFrontmatter("{a,b}/{c,d}")).toEqual([
"a/c", "a/d", "b/c", "b/d",
]);
});
test('handles nested brace expansion', () => {
expect(splitPathInFrontmatter('{a,b}/{c,d}')).toEqual([
'a/c',
'a/d',
'b/c',
'b/d',
])
})
test("handles array input", () => {
expect(splitPathInFrontmatter(["a", "b"])).toEqual(["a", "b"]);
});
test('handles array input', () => {
expect(splitPathInFrontmatter(['a', 'b'])).toEqual(['a', 'b'])
})
test("returns empty array for non-string", () => {
expect(splitPathInFrontmatter(123 as any)).toEqual([]);
});
test('returns empty array for non-string', () => {
expect(splitPathInFrontmatter(123 as any)).toEqual([])
})
test("preserves braces in comma-separated list", () => {
expect(splitPathInFrontmatter("a, src/*.{ts,tsx}")).toEqual([
"a",
"src/*.ts",
"src/*.tsx",
]);
});
});
test('preserves braces in comma-separated list', () => {
expect(splitPathInFrontmatter('a, src/*.{ts,tsx}')).toEqual([
'a',
'src/*.ts',
'src/*.tsx',
])
})
})
describe("parsePositiveIntFromFrontmatter", () => {
test("returns number for positive integer", () => {
expect(parsePositiveIntFromFrontmatter(5)).toBe(5);
});
describe('parsePositiveIntFromFrontmatter', () => {
test('returns number for positive integer', () => {
expect(parsePositiveIntFromFrontmatter(5)).toBe(5)
})
test("parses string number", () => {
expect(parsePositiveIntFromFrontmatter("10")).toBe(10);
});
test('parses string number', () => {
expect(parsePositiveIntFromFrontmatter('10')).toBe(10)
})
test("returns undefined for zero", () => {
expect(parsePositiveIntFromFrontmatter(0)).toBeUndefined();
});
test('returns undefined for zero', () => {
expect(parsePositiveIntFromFrontmatter(0)).toBeUndefined()
})
test("returns undefined for negative number", () => {
expect(parsePositiveIntFromFrontmatter(-1)).toBeUndefined();
});
test('returns undefined for negative number', () => {
expect(parsePositiveIntFromFrontmatter(-1)).toBeUndefined()
})
test("returns undefined for float", () => {
expect(parsePositiveIntFromFrontmatter(1.5)).toBeUndefined();
});
test('returns undefined for float', () => {
expect(parsePositiveIntFromFrontmatter(1.5)).toBeUndefined()
})
test("returns undefined for null/undefined", () => {
expect(parsePositiveIntFromFrontmatter(null)).toBeUndefined();
expect(parsePositiveIntFromFrontmatter(undefined)).toBeUndefined();
});
test('returns undefined for null/undefined', () => {
expect(parsePositiveIntFromFrontmatter(null)).toBeUndefined()
expect(parsePositiveIntFromFrontmatter(undefined)).toBeUndefined()
})
test("returns undefined for non-numeric string", () => {
expect(parsePositiveIntFromFrontmatter("abc")).toBeUndefined();
});
});
test('returns undefined for non-numeric string', () => {
expect(parsePositiveIntFromFrontmatter('abc')).toBeUndefined()
})
})
describe("parseBooleanFrontmatter", () => {
test("returns true for boolean true", () => {
expect(parseBooleanFrontmatter(true)).toBe(true);
});
describe('parseBooleanFrontmatter', () => {
test('returns true for boolean true', () => {
expect(parseBooleanFrontmatter(true)).toBe(true)
})
test("returns true for string 'true'", () => {
expect(parseBooleanFrontmatter("true")).toBe(true);
});
expect(parseBooleanFrontmatter('true')).toBe(true)
})
test("returns false for boolean false", () => {
expect(parseBooleanFrontmatter(false)).toBe(false);
});
test('returns false for boolean false', () => {
expect(parseBooleanFrontmatter(false)).toBe(false)
})
test("returns false for string 'false'", () => {
expect(parseBooleanFrontmatter("false")).toBe(false);
});
expect(parseBooleanFrontmatter('false')).toBe(false)
})
test("returns false for null/undefined", () => {
expect(parseBooleanFrontmatter(null)).toBe(false);
expect(parseBooleanFrontmatter(undefined)).toBe(false);
});
});
test('returns false for null/undefined', () => {
expect(parseBooleanFrontmatter(null)).toBe(false)
expect(parseBooleanFrontmatter(undefined)).toBe(false)
})
})
describe("parseShellFrontmatter", () => {
describe('parseShellFrontmatter', () => {
test("returns bash for 'bash'", () => {
expect(parseShellFrontmatter("bash", "test")).toBe("bash");
});
expect(parseShellFrontmatter('bash', 'test')).toBe('bash')
})
test("returns powershell for 'powershell'", () => {
expect(parseShellFrontmatter("powershell", "test")).toBe("powershell");
});
expect(parseShellFrontmatter('powershell', 'test')).toBe('powershell')
})
test("returns undefined for null", () => {
expect(parseShellFrontmatter(null, "test")).toBeUndefined();
});
test('returns undefined for null', () => {
expect(parseShellFrontmatter(null, 'test')).toBeUndefined()
})
test("returns undefined for unrecognized value", () => {
expect(parseShellFrontmatter("zsh", "test")).toBeUndefined();
});
test('returns undefined for unrecognized value', () => {
expect(parseShellFrontmatter('zsh', 'test')).toBeUndefined()
})
test("is case insensitive", () => {
expect(parseShellFrontmatter("BASH", "test")).toBe("bash");
});
test('is case insensitive', () => {
expect(parseShellFrontmatter('BASH', 'test')).toBe('bash')
})
test("returns undefined for empty string", () => {
expect(parseShellFrontmatter("", "test")).toBeUndefined();
});
});
test('returns undefined for empty string', () => {
expect(parseShellFrontmatter('', 'test')).toBeUndefined()
})
})

View File

@@ -1,114 +1,114 @@
import { describe, expect, test } from "bun:test";
import { lastX, returnValue, all, toArray, fromArray } from "../generators";
import { describe, expect, test } from 'bun:test'
import { lastX, returnValue, all, toArray, fromArray } from '../generators'
async function* range(n: number): AsyncGenerator<number, void> {
for (let i = 0; i < n; i++) {
yield i;
yield i
}
}
describe("lastX", () => {
test("returns last yielded value", async () => {
const result = await lastX(range(5));
expect(result).toBe(4);
});
describe('lastX', () => {
test('returns last yielded value', async () => {
const result = await lastX(range(5))
expect(result).toBe(4)
})
test("returns only value from single-yield generator", async () => {
const result = await lastX(range(1));
expect(result).toBe(0);
});
test('returns only value from single-yield generator', async () => {
const result = await lastX(range(1))
expect(result).toBe(0)
})
test("throws on empty generator", async () => {
await expect(lastX(range(0))).rejects.toThrow("No items in generator");
});
});
test('throws on empty generator', async () => {
await expect(lastX(range(0))).rejects.toThrow('No items in generator')
})
})
describe("returnValue", () => {
test("returns generator return value", async () => {
describe('returnValue', () => {
test('returns generator return value', async () => {
async function* gen(): AsyncGenerator<number, string> {
yield 1;
return "done";
yield 1
return 'done'
}
const result = await returnValue(gen());
expect(result).toBe("done");
});
const result = await returnValue(gen())
expect(result).toBe('done')
})
test("returns undefined for void return", async () => {
test('returns undefined for void return', async () => {
async function* gen(): AsyncGenerator<number, void> {
yield 1;
yield 1
}
const result = await returnValue(gen());
expect(result).toBeUndefined();
});
});
const result = await returnValue(gen())
expect(result).toBeUndefined()
})
})
describe("toArray", () => {
test("collects all yielded values", async () => {
const result = await toArray(range(4));
expect(result).toEqual([0, 1, 2, 3]);
});
describe('toArray', () => {
test('collects all yielded values', async () => {
const result = await toArray(range(4))
expect(result).toEqual([0, 1, 2, 3])
})
test("returns empty array for empty generator", async () => {
const result = await toArray(fromArray([]));
expect(result).toEqual([]);
});
test('returns empty array for empty generator', async () => {
const result = await toArray(fromArray([]))
expect(result).toEqual([])
})
test("preserves order", async () => {
const result = await toArray(fromArray(["c", "b", "a"]));
expect(result).toEqual(["c", "b", "a"]);
});
});
test('preserves order', async () => {
const result = await toArray(fromArray(['c', 'b', 'a']))
expect(result).toEqual(['c', 'b', 'a'])
})
})
describe("fromArray", () => {
test("yields all array elements", async () => {
const result = await toArray(fromArray([10, 20, 30]));
expect(result).toEqual([10, 20, 30]);
});
describe('fromArray', () => {
test('yields all array elements', async () => {
const result = await toArray(fromArray([10, 20, 30]))
expect(result).toEqual([10, 20, 30])
})
test("yields nothing for empty array", async () => {
const result = await toArray(fromArray([]));
expect(result).toEqual([]);
});
});
test('yields nothing for empty array', async () => {
const result = await toArray(fromArray([]))
expect(result).toEqual([])
})
})
describe("all", () => {
test("merges multiple generators preserving yield order", async () => {
const gen1 = fromArray([1, 2]);
const gen2 = fromArray([3, 4]);
const result = await toArray(all([gen1, gen2]));
describe('all', () => {
test('merges multiple generators preserving yield order', async () => {
const gen1 = fromArray([1, 2])
const gen2 = fromArray([3, 4])
const result = await toArray(all([gen1, gen2]))
// All values from both generators should be present
expect(result.sort()).toEqual([1, 2, 3, 4]);
});
expect(result.sort()).toEqual([1, 2, 3, 4])
})
test("respects concurrency cap", async () => {
const gen1 = fromArray([1]);
const gen2 = fromArray([2]);
const gen3 = fromArray([3]);
const result = await toArray(all([gen1, gen2, gen3], 2));
expect(result.sort()).toEqual([1, 2, 3]);
});
test('respects concurrency cap', async () => {
const gen1 = fromArray([1])
const gen2 = fromArray([2])
const gen3 = fromArray([3])
const result = await toArray(all([gen1, gen2, gen3], 2))
expect(result.sort()).toEqual([1, 2, 3])
})
test("handles empty generator array", async () => {
const result = await toArray(all([]));
expect(result).toEqual([]);
});
test('handles empty generator array', async () => {
const result = await toArray(all([]))
expect(result).toEqual([])
})
test("handles single generator", async () => {
const result = await toArray(all([fromArray([42])]));
expect(result).toEqual([42]);
});
test('handles single generator', async () => {
const result = await toArray(all([fromArray([42])]))
expect(result).toEqual([42])
})
test("handles generators of different lengths", async () => {
const gen1 = fromArray([1, 2, 3]);
const gen2 = fromArray([10]);
const result = await toArray(all([gen1, gen2]));
test('handles generators of different lengths', async () => {
const gen1 = fromArray([1, 2, 3])
const gen2 = fromArray([10])
const result = await toArray(all([gen1, gen2]))
// all() merges concurrently, just verify all values are present
expect([...result].sort((a, b) => a - b)).toEqual([1, 2, 3, 10]);
});
expect([...result].sort((a, b) => a - b)).toEqual([1, 2, 3, 10])
})
test("yields all values from all generators", async () => {
const gens = [fromArray([1]), fromArray([2]), fromArray([3])];
const result = await toArray(all(gens));
expect(result).toHaveLength(3);
});
});
test('yields all values from all generators', async () => {
const gens = [fromArray([1]), fromArray([2]), fromArray([3])]
const result = await toArray(all(gens))
expect(result).toHaveLength(3)
})
})

View File

@@ -1,124 +1,122 @@
import { describe, expect, test } from "bun:test";
import { normalizeGitRemoteUrl } from "../git";
import { describe, expect, test } from 'bun:test'
import { normalizeGitRemoteUrl } from '../git'
describe("normalizeGitRemoteUrl", () => {
describe("SSH format (git@host:owner/repo)", () => {
test("normalizes basic SSH URL", () => {
expect(normalizeGitRemoteUrl("git@github.com:owner/repo.git")).toBe(
"github.com/owner/repo"
);
});
describe('normalizeGitRemoteUrl', () => {
describe('SSH format (git@host:owner/repo)', () => {
test('normalizes basic SSH URL', () => {
expect(normalizeGitRemoteUrl('git@github.com:owner/repo.git')).toBe(
'github.com/owner/repo',
)
})
test("handles SSH URL without .git suffix", () => {
expect(normalizeGitRemoteUrl("git@github.com:owner/repo")).toBe(
"github.com/owner/repo"
);
});
test('handles SSH URL without .git suffix', () => {
expect(normalizeGitRemoteUrl('git@github.com:owner/repo')).toBe(
'github.com/owner/repo',
)
})
test("handles nested paths", () => {
expect(normalizeGitRemoteUrl("git@gitlab.com:group/sub/repo.git")).toBe(
"gitlab.com/group/sub/repo"
);
});
});
test('handles nested paths', () => {
expect(normalizeGitRemoteUrl('git@gitlab.com:group/sub/repo.git')).toBe(
'gitlab.com/group/sub/repo',
)
})
})
describe("HTTPS format", () => {
test("normalizes basic HTTPS URL", () => {
describe('HTTPS format', () => {
test('normalizes basic HTTPS URL', () => {
expect(normalizeGitRemoteUrl('https://github.com/owner/repo.git')).toBe(
'github.com/owner/repo',
)
})
test('handles HTTPS without .git suffix', () => {
expect(normalizeGitRemoteUrl('https://github.com/owner/repo')).toBe(
'github.com/owner/repo',
)
})
test('handles HTTP URL', () => {
expect(normalizeGitRemoteUrl('http://github.com/owner/repo.git')).toBe(
'github.com/owner/repo',
)
})
test('handles HTTPS with auth', () => {
expect(
normalizeGitRemoteUrl("https://github.com/owner/repo.git")
).toBe("github.com/owner/repo");
});
normalizeGitRemoteUrl('https://user@github.com/owner/repo.git'),
).toBe('github.com/owner/repo')
})
})
test("handles HTTPS without .git suffix", () => {
expect(normalizeGitRemoteUrl("https://github.com/owner/repo")).toBe(
"github.com/owner/repo"
);
});
describe('ssh:// format', () => {
test('normalizes ssh:// URL', () => {
expect(normalizeGitRemoteUrl('ssh://git@github.com/owner/repo')).toBe(
'github.com/owner/repo',
)
})
test("handles HTTP URL", () => {
expect(normalizeGitRemoteUrl("http://github.com/owner/repo.git")).toBe(
"github.com/owner/repo"
);
});
test('handles ssh:// with .git suffix', () => {
expect(normalizeGitRemoteUrl('ssh://git@github.com/owner/repo.git')).toBe(
'github.com/owner/repo',
)
})
})
test("handles HTTPS with auth", () => {
expect(
normalizeGitRemoteUrl("https://user@github.com/owner/repo.git")
).toBe("github.com/owner/repo");
});
});
describe("ssh:// format", () => {
test("normalizes ssh:// URL", () => {
expect(
normalizeGitRemoteUrl("ssh://git@github.com/owner/repo")
).toBe("github.com/owner/repo");
});
test("handles ssh:// with .git suffix", () => {
expect(
normalizeGitRemoteUrl("ssh://git@github.com/owner/repo.git")
).toBe("github.com/owner/repo");
});
});
describe("CCR proxy URLs", () => {
test("handles legacy proxy format (assumes github.com)", () => {
describe('CCR proxy URLs', () => {
test('handles legacy proxy format (assumes github.com)', () => {
expect(
normalizeGitRemoteUrl(
"http://local_proxy@127.0.0.1:16583/git/owner/repo"
)
).toBe("github.com/owner/repo");
});
'http://local_proxy@127.0.0.1:16583/git/owner/repo',
),
).toBe('github.com/owner/repo')
})
test("handles GHE proxy format (host in path)", () => {
test('handles GHE proxy format (host in path)', () => {
expect(
normalizeGitRemoteUrl(
"http://local_proxy@127.0.0.1:16583/git/ghe.company.com/owner/repo"
)
).toBe("ghe.company.com/owner/repo");
});
'http://local_proxy@127.0.0.1:16583/git/ghe.company.com/owner/repo',
),
).toBe('ghe.company.com/owner/repo')
})
test("handles localhost proxy", () => {
test('handles localhost proxy', () => {
expect(
normalizeGitRemoteUrl(
"http://proxy@localhost:8080/git/owner/repo"
)
).toBe("github.com/owner/repo");
});
});
normalizeGitRemoteUrl('http://proxy@localhost:8080/git/owner/repo'),
).toBe('github.com/owner/repo')
})
})
describe("case normalization", () => {
test("converts to lowercase", () => {
expect(normalizeGitRemoteUrl("git@GitHub.COM:Owner/Repo.git")).toBe(
"github.com/owner/repo"
);
});
describe('case normalization', () => {
test('converts to lowercase', () => {
expect(normalizeGitRemoteUrl('git@GitHub.COM:Owner/Repo.git')).toBe(
'github.com/owner/repo',
)
})
test("converts HTTPS to lowercase", () => {
expect(
normalizeGitRemoteUrl("https://GitHub.COM/Owner/Repo.git")
).toBe("github.com/owner/repo");
});
});
test('converts HTTPS to lowercase', () => {
expect(normalizeGitRemoteUrl('https://GitHub.COM/Owner/Repo.git')).toBe(
'github.com/owner/repo',
)
})
})
describe("edge cases", () => {
test("returns null for empty string", () => {
expect(normalizeGitRemoteUrl("")).toBeNull();
});
describe('edge cases', () => {
test('returns null for empty string', () => {
expect(normalizeGitRemoteUrl('')).toBeNull()
})
test("returns null for whitespace only", () => {
expect(normalizeGitRemoteUrl(" ")).toBeNull();
});
test('returns null for whitespace only', () => {
expect(normalizeGitRemoteUrl(' ')).toBeNull()
})
test("returns null for unrecognized format", () => {
expect(normalizeGitRemoteUrl("not-a-url")).toBeNull();
});
test('returns null for unrecognized format', () => {
expect(normalizeGitRemoteUrl('not-a-url')).toBeNull()
})
test("trims whitespace before parsing", () => {
expect(
normalizeGitRemoteUrl(" git@github.com:owner/repo.git ")
).toBe("github.com/owner/repo");
});
});
});
test('trims whitespace before parsing', () => {
expect(normalizeGitRemoteUrl(' git@github.com:owner/repo.git ')).toBe(
'github.com/owner/repo',
)
})
})
})

View File

@@ -1,286 +1,294 @@
import { describe, expect, test } from "bun:test";
import { parseGitNumstat, parseGitDiff, parseShortstat } from "../gitDiff";
import { describe, expect, test } from 'bun:test'
import { parseGitNumstat, parseGitDiff, parseShortstat } from '../gitDiff'
describe("parseGitNumstat", () => {
test("parses single file", () => {
const result = parseGitNumstat("5\t3\tsrc/foo.ts");
describe('parseGitNumstat', () => {
test('parses single file', () => {
const result = parseGitNumstat('5\t3\tsrc/foo.ts')
expect(result.stats).toEqual({
filesCount: 1,
linesAdded: 5,
linesRemoved: 3,
});
expect(result.perFileStats.get("src/foo.ts")).toEqual({
})
expect(result.perFileStats.get('src/foo.ts')).toEqual({
added: 5,
removed: 3,
isBinary: false,
});
});
})
})
test("parses multiple files", () => {
const input = "10\t2\ta.ts\n3\t0\tb.ts\n0\t7\tc.ts";
const result = parseGitNumstat(input);
test('parses multiple files', () => {
const input = '10\t2\ta.ts\n3\t0\tb.ts\n0\t7\tc.ts'
const result = parseGitNumstat(input)
expect(result.stats).toEqual({
filesCount: 3,
linesAdded: 13,
linesRemoved: 9,
});
expect(result.perFileStats.size).toBe(3);
});
})
expect(result.perFileStats.size).toBe(3)
})
test("handles binary file with dash counts", () => {
const result = parseGitNumstat("-\t-\timage.png");
expect(result.perFileStats.get("image.png")).toEqual({
test('handles binary file with dash counts', () => {
const result = parseGitNumstat('-\t-\timage.png')
expect(result.perFileStats.get('image.png')).toEqual({
added: 0,
removed: 0,
isBinary: true,
});
});
})
})
test("handles rename format", () => {
const result = parseGitNumstat("1\t0\told.txt => new.txt");
const entry = result.perFileStats.get("old.txt => new.txt");
expect(entry).not.toBeUndefined();
expect(entry!.added).toBe(1);
expect(entry!.isBinary).toBe(false);
});
test('handles rename format', () => {
const result = parseGitNumstat('1\t0\told.txt => new.txt')
const entry = result.perFileStats.get('old.txt => new.txt')
expect(entry).not.toBeUndefined()
expect(entry!.added).toBe(1)
expect(entry!.isBinary).toBe(false)
})
test("handles filename with tabs", () => {
const result = parseGitNumstat('1\t0\t"tab\tfile.txt"');
test('handles filename with tabs', () => {
const result = parseGitNumstat('1\t0\t"tab\tfile.txt"')
// parts.slice(2).join('\t') preserves the rest
expect(result.stats.filesCount).toBe(1);
});
expect(result.stats.filesCount).toBe(1)
})
test("returns empty for empty string", () => {
const result = parseGitNumstat("");
test('returns empty for empty string', () => {
const result = parseGitNumstat('')
expect(result.stats).toEqual({
filesCount: 0,
linesAdded: 0,
linesRemoved: 0,
});
expect(result.perFileStats.size).toBe(0);
});
})
expect(result.perFileStats.size).toBe(0)
})
test("skips lines with fewer than 3 tab-separated parts", () => {
const result = parseGitNumstat("invalid-line\n5\t3\tsrc/foo.ts");
expect(result.stats.filesCount).toBe(1);
});
test('skips lines with fewer than 3 tab-separated parts', () => {
const result = parseGitNumstat('invalid-line\n5\t3\tsrc/foo.ts')
expect(result.stats.filesCount).toBe(1)
})
test("handles zero additions and zero deletions", () => {
const result = parseGitNumstat("0\t0\tempty-change.ts");
expect(result.perFileStats.get("empty-change.ts")).toEqual({
test('handles zero additions and zero deletions', () => {
const result = parseGitNumstat('0\t0\tempty-change.ts')
expect(result.perFileStats.get('empty-change.ts')).toEqual({
added: 0,
removed: 0,
isBinary: false,
});
});
});
})
})
})
describe("parseGitDiff", () => {
test("parses single file with one hunk", () => {
describe('parseGitDiff', () => {
test('parses single file with one hunk', () => {
const input = [
"diff --git a/foo.ts b/foo.ts",
"index abc..def 100644",
"--- a/foo.ts",
"+++ b/foo.ts",
"@@ -1,3 +1,4 @@",
" line1",
"+added",
" line2",
" line3",
].join("\n");
'diff --git a/foo.ts b/foo.ts',
'index abc..def 100644',
'--- a/foo.ts',
'+++ b/foo.ts',
'@@ -1,3 +1,4 @@',
' line1',
'+added',
' line2',
' line3',
].join('\n')
const result = parseGitDiff(input);
expect(result.size).toBe(1);
const hunks = result.get("foo.ts")!;
expect(hunks).toHaveLength(1);
expect(hunks[0].oldStart).toBe(1);
expect(hunks[0].oldLines).toBe(3);
expect(hunks[0].newStart).toBe(1);
expect(hunks[0].newLines).toBe(4);
expect(hunks[0].lines).toEqual([" line1", "+added", " line2", " line3"]);
});
const result = parseGitDiff(input)
expect(result.size).toBe(1)
const hunks = result.get('foo.ts')!
expect(hunks).toHaveLength(1)
expect(hunks[0].oldStart).toBe(1)
expect(hunks[0].oldLines).toBe(3)
expect(hunks[0].newStart).toBe(1)
expect(hunks[0].newLines).toBe(4)
expect(hunks[0].lines).toEqual([' line1', '+added', ' line2', ' line3'])
})
test("parses multiple hunks in one file", () => {
test('parses multiple hunks in one file', () => {
const input = [
"diff --git a/bar.ts b/bar.ts",
"index abc..def 100644",
"--- a/bar.ts",
"+++ b/bar.ts",
"@@ -1,2 +1,3 @@",
" a",
"+b",
" c",
"@@ -10,2 +11,2 @@",
" d",
"-e",
"+f",
].join("\n");
'diff --git a/bar.ts b/bar.ts',
'index abc..def 100644',
'--- a/bar.ts',
'+++ b/bar.ts',
'@@ -1,2 +1,3 @@',
' a',
'+b',
' c',
'@@ -10,2 +11,2 @@',
' d',
'-e',
'+f',
].join('\n')
const result = parseGitDiff(input);
const hunks = result.get("bar.ts")!;
expect(hunks).toHaveLength(2);
expect(hunks[0].oldStart).toBe(1);
expect(hunks[1].oldStart).toBe(10);
});
const result = parseGitDiff(input)
const hunks = result.get('bar.ts')!
expect(hunks).toHaveLength(2)
expect(hunks[0].oldStart).toBe(1)
expect(hunks[1].oldStart).toBe(10)
})
test("skips binary files marker", () => {
test('skips binary files marker', () => {
const input = [
"diff --git a/img.png b/img.png",
"Binary files a/img.png and b/img.png differ",
].join("\n");
'diff --git a/img.png b/img.png',
'Binary files a/img.png and b/img.png differ',
].join('\n')
const result = parseGitDiff(input);
const result = parseGitDiff(input)
// Binary file has no hunks, so it's not in the result
expect(result.size).toBe(0);
});
expect(result.size).toBe(0)
})
test("parses new file mode", () => {
test('parses new file mode', () => {
const input = [
"diff --git a/new.ts b/new.ts",
"new file mode 100644",
"--- /dev/null",
"+++ b/new.ts",
"@@ -0,0 +1,2 @@",
"+line1",
"+line2",
].join("\n");
'diff --git a/new.ts b/new.ts',
'new file mode 100644',
'--- /dev/null',
'+++ b/new.ts',
'@@ -0,0 +1,2 @@',
'+line1',
'+line2',
].join('\n')
const result = parseGitDiff(input);
const hunks = result.get("new.ts")!;
expect(hunks).toHaveLength(1);
expect(hunks[0].lines).toEqual(["+line1", "+line2"]);
});
const result = parseGitDiff(input)
const hunks = result.get('new.ts')!
expect(hunks).toHaveLength(1)
expect(hunks[0].lines).toEqual(['+line1', '+line2'])
})
test("parses deleted file", () => {
test('parses deleted file', () => {
const input = [
"diff --git a/old.ts b/old.ts",
"deleted file mode 100644",
"--- a/old.ts",
"+++ /dev/null",
"@@ -1,2 +0,0 @@",
"-line1",
"-line2",
].join("\n");
'diff --git a/old.ts b/old.ts',
'deleted file mode 100644',
'--- a/old.ts',
'+++ /dev/null',
'@@ -1,2 +0,0 @@',
'-line1',
'-line2',
].join('\n')
const result = parseGitDiff(input);
const hunks = result.get("old.ts")!;
expect(hunks).toHaveLength(1);
});
const result = parseGitDiff(input)
const hunks = result.get('old.ts')!
expect(hunks).toHaveLength(1)
})
test("returns empty map for empty input", () => {
const result = parseGitDiff("");
expect(result.size).toBe(0);
});
test('returns empty map for empty input', () => {
const result = parseGitDiff('')
expect(result.size).toBe(0)
})
test("handles multiple files", () => {
test('handles multiple files', () => {
const input = [
"diff --git a/a.ts b/a.ts",
"--- a/a.ts",
"+++ b/a.ts",
"@@ -1 +1 @@",
"-old",
"+new",
"diff --git a/b.ts b/b.ts",
"--- a/b.ts",
"+++ b/b.ts",
"@@ -1 +1 @@",
"-x",
"+y",
].join("\n");
'diff --git a/a.ts b/a.ts',
'--- a/a.ts',
'+++ b/a.ts',
'@@ -1 +1 @@',
'-old',
'+new',
'diff --git a/b.ts b/b.ts',
'--- a/b.ts',
'+++ b/b.ts',
'@@ -1 +1 @@',
'-x',
'+y',
].join('\n')
const result = parseGitDiff(input);
expect(result.size).toBe(2);
expect(result.has("a.ts")).toBe(true);
expect(result.has("b.ts")).toBe(true);
});
const result = parseGitDiff(input)
expect(result.size).toBe(2)
expect(result.has('a.ts')).toBe(true)
expect(result.has('b.ts')).toBe(true)
})
test("skips hunk without comma (single line)", () => {
test('skips hunk without comma (single line)', () => {
const input = [
"diff --git a/solo.ts b/solo.ts",
"--- a/solo.ts",
"+++ b/solo.ts",
"@@ -1 +1 @@",
"-old",
"+new",
].join("\n");
'diff --git a/solo.ts b/solo.ts',
'--- a/solo.ts',
'+++ b/solo.ts',
'@@ -1 +1 @@',
'-old',
'+new',
].join('\n')
const result = parseGitDiff(input);
const hunks = result.get("solo.ts")!;
expect(hunks[0].oldLines).toBe(1); // default when no comma
expect(hunks[0].newLines).toBe(1);
});
});
const result = parseGitDiff(input)
const hunks = result.get('solo.ts')!
expect(hunks[0].oldLines).toBe(1) // default when no comma
expect(hunks[0].newLines).toBe(1)
})
})
describe("parseShortstat", () => {
test("parses full shortstat with insertions and deletions", () => {
const result = parseShortstat(" 3 files changed, 10 insertions(+), 5 deletions(-)");
describe('parseShortstat', () => {
test('parses full shortstat with insertions and deletions', () => {
const result = parseShortstat(
' 3 files changed, 10 insertions(+), 5 deletions(-)',
)
expect(result).toEqual({
filesCount: 3,
linesAdded: 10,
linesRemoved: 5,
});
});
})
})
test("parses single file", () => {
const result = parseShortstat(" 1 file changed, 2 insertions(+), 1 deletion(-)");
test('parses single file', () => {
const result = parseShortstat(
' 1 file changed, 2 insertions(+), 1 deletion(-)',
)
expect(result).toEqual({
filesCount: 1,
linesAdded: 2,
linesRemoved: 1,
});
});
})
})
test("parses insertions only", () => {
const result = parseShortstat(" 2 files changed, 5 insertions(+)");
test('parses insertions only', () => {
const result = parseShortstat(' 2 files changed, 5 insertions(+)')
expect(result).toEqual({
filesCount: 2,
linesAdded: 5,
linesRemoved: 0,
});
});
})
})
test("parses deletions only", () => {
const result = parseShortstat(" 1 file changed, 3 deletions(-)");
test('parses deletions only', () => {
const result = parseShortstat(' 1 file changed, 3 deletions(-)')
expect(result).toEqual({
filesCount: 1,
linesAdded: 0,
linesRemoved: 3,
});
});
})
})
test("parses files changed only (no insertions or deletions)", () => {
const result = parseShortstat(" 2 files changed");
test('parses files changed only (no insertions or deletions)', () => {
const result = parseShortstat(' 2 files changed')
expect(result).toEqual({
filesCount: 2,
linesAdded: 0,
linesRemoved: 0,
});
});
})
})
test("returns null for empty string", () => {
expect(parseShortstat("")).toBeNull();
});
test('returns null for empty string', () => {
expect(parseShortstat('')).toBeNull()
})
test("returns null for non-matching string", () => {
expect(parseShortstat("nothing to see here")).toBeNull();
});
test('returns null for non-matching string', () => {
expect(parseShortstat('nothing to see here')).toBeNull()
})
test("handles large numbers", () => {
const result = parseShortstat(" 100 files changed, 50000 insertions(+), 30000 deletions(-)");
test('handles large numbers', () => {
const result = parseShortstat(
' 100 files changed, 50000 insertions(+), 30000 deletions(-)',
)
expect(result).toEqual({
filesCount: 100,
linesAdded: 50000,
linesRemoved: 30000,
});
});
})
})
test("handles zero insertions and deletions explicitly", () => {
test('handles zero insertions and deletions explicitly', () => {
// git can output "0 insertions(+), 0 deletions(-)"
const result = parseShortstat(" 1 file changed, 0 insertions(+), 0 deletions(-)");
const result = parseShortstat(
' 1 file changed, 0 insertions(+), 0 deletions(-)',
)
expect(result).toEqual({
filesCount: 1,
linesAdded: 0,
linesRemoved: 0,
});
});
});
})
})
})

View File

@@ -1,40 +1,40 @@
import { describe, expect, test } from "bun:test";
import { extractGlobBaseDirectory } from "../glob";
import { describe, expect, test } from 'bun:test'
import { extractGlobBaseDirectory } from '../glob'
describe("extractGlobBaseDirectory", () => {
test("extracts base dir from glob with *", () => {
const result = extractGlobBaseDirectory("src/utils/*.ts");
expect(result.baseDir).toBe("src/utils");
expect(result.relativePattern).toBe("*.ts");
});
describe('extractGlobBaseDirectory', () => {
test('extracts base dir from glob with *', () => {
const result = extractGlobBaseDirectory('src/utils/*.ts')
expect(result.baseDir).toBe('src/utils')
expect(result.relativePattern).toBe('*.ts')
})
test("extracts base dir from glob with **", () => {
const result = extractGlobBaseDirectory("src/**/*.ts");
expect(result.baseDir).toBe("src");
expect(result.relativePattern).toBe("**/*.ts");
});
test('extracts base dir from glob with **', () => {
const result = extractGlobBaseDirectory('src/**/*.ts')
expect(result.baseDir).toBe('src')
expect(result.relativePattern).toBe('**/*.ts')
})
test("returns dirname for literal path", () => {
const result = extractGlobBaseDirectory("src/utils/file.ts");
expect(result.baseDir).toBe("src/utils");
expect(result.relativePattern).toBe("file.ts");
});
test('returns dirname for literal path', () => {
const result = extractGlobBaseDirectory('src/utils/file.ts')
expect(result.baseDir).toBe('src/utils')
expect(result.relativePattern).toBe('file.ts')
})
test("handles glob starting with pattern", () => {
const result = extractGlobBaseDirectory("*.ts");
expect(result.baseDir).toBe("");
expect(result.relativePattern).toBe("*.ts");
});
test('handles glob starting with pattern', () => {
const result = extractGlobBaseDirectory('*.ts')
expect(result.baseDir).toBe('')
expect(result.relativePattern).toBe('*.ts')
})
test("handles braces pattern", () => {
const result = extractGlobBaseDirectory("src/{a,b}/*.ts");
expect(result.baseDir).toBe("src");
expect(result.relativePattern).toBe("{a,b}/*.ts");
});
test('handles braces pattern', () => {
const result = extractGlobBaseDirectory('src/{a,b}/*.ts')
expect(result.baseDir).toBe('src')
expect(result.relativePattern).toBe('{a,b}/*.ts')
})
test("handles question mark pattern", () => {
const result = extractGlobBaseDirectory("src/?.ts");
expect(result.baseDir).toBe("src");
expect(result.relativePattern).toBe("?.ts");
});
});
test('handles question mark pattern', () => {
const result = extractGlobBaseDirectory('src/?.ts')
expect(result.baseDir).toBe('src')
expect(result.relativePattern).toBe('?.ts')
})
})

View File

@@ -1,152 +1,152 @@
import { describe, expect, test } from "bun:test";
import { applyGrouping } from "../groupToolUses";
import { describe, expect, test } from 'bun:test'
import { applyGrouping } from '../groupToolUses'
// Helper: build minimal tool-use assistant message
function makeToolUseMsg(
uuid: string,
messageId: string,
toolUseId: string,
toolName: string
toolName: string,
): any {
return {
type: "assistant",
type: 'assistant',
uuid,
timestamp: Date.now(),
message: {
id: messageId,
content: [{ type: "tool_use", id: toolUseId, name: toolName, input: {} }],
content: [{ type: 'tool_use', id: toolUseId, name: toolName, input: {} }],
},
};
}
}
// Helper: build minimal tool-result user message
function makeToolResultMsg(uuid: string, toolUseId: string): any {
return {
type: "user",
type: 'user',
uuid,
timestamp: Date.now(),
message: {
content: [{ type: "tool_result", tool_use_id: toolUseId, content: "ok" }],
content: [{ type: 'tool_result', tool_use_id: toolUseId, content: 'ok' }],
},
};
}
}
// Helper: build minimal text assistant message
function makeTextMsg(uuid: string, text: string): any {
return {
type: "assistant",
type: 'assistant',
uuid,
timestamp: Date.now(),
message: { id: `msg-${uuid}`, content: [{ type: "text", text }] },
};
message: { id: `msg-${uuid}`, content: [{ type: 'text', text }] },
}
}
// Minimal tool definitions
const groupableTool: any = { name: "Grep", renderGroupedToolUse: true };
const nonGroupableTool: any = { name: "Bash", renderGroupedToolUse: undefined };
const groupableTool: any = { name: 'Grep', renderGroupedToolUse: true }
const nonGroupableTool: any = { name: 'Bash', renderGroupedToolUse: undefined }
// ─── applyGrouping ────────────────────────────────────────────────────
describe("applyGrouping", () => {
test("returns all messages in verbose mode", () => {
describe('applyGrouping', () => {
test('returns all messages in verbose mode', () => {
const msgs = [
makeToolUseMsg("u1", "m1", "tu1", "Grep"),
makeToolUseMsg("u2", "m1", "tu2", "Grep"),
];
const result = applyGrouping(msgs, [groupableTool], true);
expect(result.messages).toHaveLength(2);
expect(result.messages).toBe(msgs); // same reference
});
makeToolUseMsg('u1', 'm1', 'tu1', 'Grep'),
makeToolUseMsg('u2', 'm1', 'tu2', 'Grep'),
]
const result = applyGrouping(msgs, [groupableTool], true)
expect(result.messages).toHaveLength(2)
expect(result.messages).toBe(msgs) // same reference
})
test("does not group when tool lacks renderGroupedToolUse", () => {
test('does not group when tool lacks renderGroupedToolUse', () => {
const msgs = [
makeToolUseMsg("u1", "m1", "tu1", "Bash"),
makeToolUseMsg("u2", "m1", "tu2", "Bash"),
];
const result = applyGrouping(msgs, [nonGroupableTool]);
expect(result.messages).toHaveLength(2);
makeToolUseMsg('u1', 'm1', 'tu1', 'Bash'),
makeToolUseMsg('u2', 'm1', 'tu2', 'Bash'),
]
const result = applyGrouping(msgs, [nonGroupableTool])
expect(result.messages).toHaveLength(2)
// Both messages should pass through as-is
expect(result.messages[0]).toBe(msgs[0]);
});
expect(result.messages[0]).toBe(msgs[0])
})
test("does not group single tool use", () => {
const msgs = [makeToolUseMsg("u1", "m1", "tu1", "Grep")];
const result = applyGrouping(msgs, [groupableTool]);
expect(result.messages).toHaveLength(1);
expect((result.messages[0] as any).type).toBe("assistant");
});
test('does not group single tool use', () => {
const msgs = [makeToolUseMsg('u1', 'm1', 'tu1', 'Grep')]
const result = applyGrouping(msgs, [groupableTool])
expect(result.messages).toHaveLength(1)
expect((result.messages[0] as any).type).toBe('assistant')
})
test("groups 2+ tool uses of same type from same message", () => {
test('groups 2+ tool uses of same type from same message', () => {
const msgs = [
makeToolUseMsg("u1", "m1", "tu1", "Grep"),
makeToolUseMsg("u2", "m1", "tu2", "Grep"),
makeToolUseMsg("u3", "m1", "tu3", "Grep"),
];
const result = applyGrouping(msgs, [groupableTool]);
expect(result.messages).toHaveLength(1);
const grouped = result.messages[0] as any;
expect(grouped.type).toBe("grouped_tool_use");
expect(grouped.toolName).toBe("Grep");
expect(grouped.messages).toHaveLength(3);
});
makeToolUseMsg('u1', 'm1', 'tu1', 'Grep'),
makeToolUseMsg('u2', 'm1', 'tu2', 'Grep'),
makeToolUseMsg('u3', 'm1', 'tu3', 'Grep'),
]
const result = applyGrouping(msgs, [groupableTool])
expect(result.messages).toHaveLength(1)
const grouped = result.messages[0] as any
expect(grouped.type).toBe('grouped_tool_use')
expect(grouped.toolName).toBe('Grep')
expect(grouped.messages).toHaveLength(3)
})
test("does not group tool uses from different messages", () => {
test('does not group tool uses from different messages', () => {
const msgs = [
makeToolUseMsg("u1", "m1", "tu1", "Grep"),
makeToolUseMsg("u2", "m2", "tu2", "Grep"),
];
const result = applyGrouping(msgs, [groupableTool]);
makeToolUseMsg('u1', 'm1', 'tu1', 'Grep'),
makeToolUseMsg('u2', 'm2', 'tu2', 'Grep'),
]
const result = applyGrouping(msgs, [groupableTool])
// Each belongs to a different message.id, so no group (< 2 per group)
expect(result.messages).toHaveLength(2);
});
expect(result.messages).toHaveLength(2)
})
test("collects tool results for grouped uses", () => {
test('collects tool results for grouped uses', () => {
const msgs = [
makeToolUseMsg("u1", "m1", "tu1", "Grep"),
makeToolUseMsg("u2", "m1", "tu2", "Grep"),
makeToolResultMsg("u3", "tu1"),
makeToolResultMsg("u4", "tu2"),
];
const result = applyGrouping(msgs, [groupableTool]);
const grouped = result.messages[0] as any;
expect(grouped.type).toBe("grouped_tool_use");
expect(grouped.results).toHaveLength(2);
});
makeToolUseMsg('u1', 'm1', 'tu1', 'Grep'),
makeToolUseMsg('u2', 'm1', 'tu2', 'Grep'),
makeToolResultMsg('u3', 'tu1'),
makeToolResultMsg('u4', 'tu2'),
]
const result = applyGrouping(msgs, [groupableTool])
const grouped = result.messages[0] as any
expect(grouped.type).toBe('grouped_tool_use')
expect(grouped.results).toHaveLength(2)
})
test("skips user messages whose tool_results are all grouped", () => {
test('skips user messages whose tool_results are all grouped', () => {
const msgs = [
makeToolUseMsg("u1", "m1", "tu1", "Grep"),
makeToolUseMsg("u2", "m1", "tu2", "Grep"),
makeToolResultMsg("u3", "tu1"),
makeToolResultMsg("u4", "tu2"),
];
const result = applyGrouping(msgs, [groupableTool]);
makeToolUseMsg('u1', 'm1', 'tu1', 'Grep'),
makeToolUseMsg('u2', 'm1', 'tu2', 'Grep'),
makeToolResultMsg('u3', 'tu1'),
makeToolResultMsg('u4', 'tu2'),
]
const result = applyGrouping(msgs, [groupableTool])
// Only the grouped message should remain — result messages are consumed
expect(result.messages).toHaveLength(1);
});
expect(result.messages).toHaveLength(1)
})
test("preserves non-grouped messages alongside groups", () => {
test('preserves non-grouped messages alongside groups', () => {
const msgs = [
makeTextMsg("u0", "thinking..."),
makeToolUseMsg("u1", "m1", "tu1", "Grep"),
makeToolUseMsg("u2", "m1", "tu2", "Grep"),
makeTextMsg("u3", "done"),
];
const result = applyGrouping(msgs, [groupableTool]);
expect(result.messages).toHaveLength(3); // text + grouped + text
expect((result.messages[0] as any).type).toBe("assistant");
expect((result.messages[1] as any).type).toBe("grouped_tool_use");
expect((result.messages[2] as any).type).toBe("assistant");
});
makeTextMsg('u0', 'thinking...'),
makeToolUseMsg('u1', 'm1', 'tu1', 'Grep'),
makeToolUseMsg('u2', 'm1', 'tu2', 'Grep'),
makeTextMsg('u3', 'done'),
]
const result = applyGrouping(msgs, [groupableTool])
expect(result.messages).toHaveLength(3) // text + grouped + text
expect((result.messages[0] as any).type).toBe('assistant')
expect((result.messages[1] as any).type).toBe('grouped_tool_use')
expect((result.messages[2] as any).type).toBe('assistant')
})
test("handles empty messages array", () => {
const result = applyGrouping([], [groupableTool]);
expect(result.messages).toHaveLength(0);
});
test('handles empty messages array', () => {
const result = applyGrouping([], [groupableTool])
expect(result.messages).toHaveLength(0)
})
test("handles empty tools array", () => {
const msgs = [makeToolUseMsg("u1", "m1", "tu1", "Grep")];
const result = applyGrouping(msgs, []);
expect(result.messages).toHaveLength(1);
});
});
test('handles empty tools array', () => {
const msgs = [makeToolUseMsg('u1', 'm1', 'tu1', 'Grep')]
const result = applyGrouping(msgs, [])
expect(result.messages).toHaveLength(1)
})
})

View File

@@ -1,76 +1,76 @@
import { describe, expect, test } from "bun:test";
import { djb2Hash, hashContent, hashPair } from "../hash";
import { describe, expect, test } from 'bun:test'
import { djb2Hash, hashContent, hashPair } from '../hash'
describe("djb2Hash", () => {
test("returns a number", () => {
expect(typeof djb2Hash("hello")).toBe("number");
});
describe('djb2Hash', () => {
test('returns a number', () => {
expect(typeof djb2Hash('hello')).toBe('number')
})
test("returns 0 for empty string", () => {
expect(djb2Hash("")).toBe(0);
});
test('returns 0 for empty string', () => {
expect(djb2Hash('')).toBe(0)
})
test("is deterministic", () => {
expect(djb2Hash("test")).toBe(djb2Hash("test"));
});
test('is deterministic', () => {
expect(djb2Hash('test')).toBe(djb2Hash('test'))
})
test("different strings produce different hashes", () => {
expect(djb2Hash("abc")).not.toBe(djb2Hash("def"));
});
test('different strings produce different hashes', () => {
expect(djb2Hash('abc')).not.toBe(djb2Hash('def'))
})
test("returns 32-bit integer", () => {
const hash = djb2Hash("some long string to hash");
expect(Number.isSafeInteger(hash)).toBe(true);
});
test('returns 32-bit integer', () => {
const hash = djb2Hash('some long string to hash')
expect(Number.isSafeInteger(hash)).toBe(true)
})
test("has known answer for 'hello'", () => {
expect(djb2Hash("hello")).toBe(99162322);
});
});
expect(djb2Hash('hello')).toBe(99162322)
})
})
describe("hashContent", () => {
test("returns a string", () => {
expect(typeof hashContent("hello")).toBe("string");
});
describe('hashContent', () => {
test('returns a string', () => {
expect(typeof hashContent('hello')).toBe('string')
})
test("is deterministic", () => {
expect(hashContent("test")).toBe(hashContent("test"));
});
test('is deterministic', () => {
expect(hashContent('test')).toBe(hashContent('test'))
})
test("different strings produce different hashes", () => {
expect(hashContent("abc")).not.toBe(hashContent("def"));
});
test('different strings produce different hashes', () => {
expect(hashContent('abc')).not.toBe(hashContent('def'))
})
test("returns numeric string for empty string", () => {
expect(hashContent("")).toMatch(/^\d+$/);
});
test('returns numeric string for empty string', () => {
expect(hashContent('')).toMatch(/^\d+$/)
})
test("returns numeric string format", () => {
expect(hashContent("hello")).toMatch(/^\d+$/);
});
});
test('returns numeric string format', () => {
expect(hashContent('hello')).toMatch(/^\d+$/)
})
})
describe("hashPair", () => {
test("returns a string", () => {
expect(typeof hashPair("a", "b")).toBe("string");
});
describe('hashPair', () => {
test('returns a string', () => {
expect(typeof hashPair('a', 'b')).toBe('string')
})
test("is deterministic", () => {
expect(hashPair("a", "b")).toBe(hashPair("a", "b"));
});
test('is deterministic', () => {
expect(hashPair('a', 'b')).toBe(hashPair('a', 'b'))
})
test("order matters", () => {
expect(hashPair("a", "b")).not.toBe(hashPair("b", "a"));
});
test('order matters', () => {
expect(hashPair('a', 'b')).not.toBe(hashPair('b', 'a'))
})
test("disambiguates different splits", () => {
expect(hashPair("ts", "code")).not.toBe(hashPair("tsc", "ode"));
});
test('disambiguates different splits', () => {
expect(hashPair('ts', 'code')).not.toBe(hashPair('tsc', 'ode'))
})
test("handles empty strings", () => {
expect(hashPair("", "")).toMatch(/^\d+$/);
expect(hashPair("", "a")).toMatch(/^\d+$/);
expect(hashPair("a", "")).toMatch(/^\d+$/);
expect(hashPair("", "a")).not.toBe(hashPair("a", ""));
});
});
test('handles empty strings', () => {
expect(hashPair('', '')).toMatch(/^\d+$/)
expect(hashPair('', 'a')).toMatch(/^\d+$/)
expect(hashPair('a', '')).toMatch(/^\d+$/)
expect(hashPair('', 'a')).not.toBe(hashPair('a', ''))
})
})

View File

@@ -1,163 +1,163 @@
import { describe, expect, test } from "bun:test";
import { calculateHorizontalScrollWindow } from "../horizontalScroll";
import { describe, expect, test } from 'bun:test'
import { calculateHorizontalScrollWindow } from '../horizontalScroll'
describe("calculateHorizontalScrollWindow", () => {
describe('calculateHorizontalScrollWindow', () => {
// Basic scenarios
test("all items fit within available width", () => {
const result = calculateHorizontalScrollWindow([10, 10, 10], 50, 3, 1);
test('all items fit within available width', () => {
const result = calculateHorizontalScrollWindow([10, 10, 10], 50, 3, 1)
expect(result).toEqual({
startIndex: 0,
endIndex: 3,
showLeftArrow: false,
showRightArrow: false,
});
});
})
})
test("single item selected within view", () => {
const result = calculateHorizontalScrollWindow([20], 50, 3, 0);
test('single item selected within view', () => {
const result = calculateHorizontalScrollWindow([20], 50, 3, 0)
expect(result).toEqual({
startIndex: 0,
endIndex: 1,
showLeftArrow: false,
showRightArrow: false,
});
});
})
})
test("selected item at beginning", () => {
const widths = [10, 10, 10, 10, 10];
const result = calculateHorizontalScrollWindow(widths, 25, 3, 0);
expect(result.startIndex).toBe(0);
expect(result.showLeftArrow).toBe(false);
expect(result.showRightArrow).toBe(true);
expect(result.endIndex).toBeGreaterThan(0);
});
test('selected item at beginning', () => {
const widths = [10, 10, 10, 10, 10]
const result = calculateHorizontalScrollWindow(widths, 25, 3, 0)
expect(result.startIndex).toBe(0)
expect(result.showLeftArrow).toBe(false)
expect(result.showRightArrow).toBe(true)
expect(result.endIndex).toBeGreaterThan(0)
})
test("selected item at end", () => {
const widths = [10, 10, 10, 10, 10];
const result = calculateHorizontalScrollWindow(widths, 25, 3, 4);
expect(result.endIndex).toBe(5);
expect(result.showRightArrow).toBe(false);
expect(result.showLeftArrow).toBe(true);
});
test('selected item at end', () => {
const widths = [10, 10, 10, 10, 10]
const result = calculateHorizontalScrollWindow(widths, 25, 3, 4)
expect(result.endIndex).toBe(5)
expect(result.showRightArrow).toBe(false)
expect(result.showLeftArrow).toBe(true)
})
test("selected item beyond visible range scrolls right", () => {
const widths = [10, 10, 10, 10, 10];
const result = calculateHorizontalScrollWindow(widths, 20, 3, 4);
expect(result.startIndex).toBeLessThanOrEqual(4);
expect(result.endIndex).toBeGreaterThan(4);
});
test('selected item beyond visible range scrolls right', () => {
const widths = [10, 10, 10, 10, 10]
const result = calculateHorizontalScrollWindow(widths, 20, 3, 4)
expect(result.startIndex).toBeLessThanOrEqual(4)
expect(result.endIndex).toBeGreaterThan(4)
})
test("selected item before visible range scrolls left", () => {
const widths = [10, 10, 10, 10, 10];
test('selected item before visible range scrolls left', () => {
const widths = [10, 10, 10, 10, 10]
// Select last item first (simulates initial scroll to end)
const result = calculateHorizontalScrollWindow(widths, 20, 3, 0);
expect(result.startIndex).toBe(0);
});
const result = calculateHorizontalScrollWindow(widths, 20, 3, 0)
expect(result.startIndex).toBe(0)
})
// Arrow indicators
test("showLeftArrow when items hidden on left", () => {
const widths = [10, 10, 10, 10, 10];
const result = calculateHorizontalScrollWindow(widths, 15, 3, 4);
expect(result.showLeftArrow).toBe(true);
});
test('showLeftArrow when items hidden on left', () => {
const widths = [10, 10, 10, 10, 10]
const result = calculateHorizontalScrollWindow(widths, 15, 3, 4)
expect(result.showLeftArrow).toBe(true)
})
test("showRightArrow when items hidden on right", () => {
const widths = [10, 10, 10, 10, 10];
const result = calculateHorizontalScrollWindow(widths, 15, 3, 0);
expect(result.showRightArrow).toBe(true);
});
test('showRightArrow when items hidden on right', () => {
const widths = [10, 10, 10, 10, 10]
const result = calculateHorizontalScrollWindow(widths, 15, 3, 0)
expect(result.showRightArrow).toBe(true)
})
test("no arrows when all items visible", () => {
const result = calculateHorizontalScrollWindow([10, 10], 50, 3, 0);
expect(result.showLeftArrow).toBe(false);
expect(result.showRightArrow).toBe(false);
});
test('no arrows when all items visible', () => {
const result = calculateHorizontalScrollWindow([10, 10], 50, 3, 0)
expect(result.showLeftArrow).toBe(false)
expect(result.showRightArrow).toBe(false)
})
test("both arrows when items hidden on both sides", () => {
const widths = [10, 10, 10, 10, 10, 10, 10];
test('both arrows when items hidden on both sides', () => {
const widths = [10, 10, 10, 10, 10, 10, 10]
// Select middle item with limited width
const result = calculateHorizontalScrollWindow(widths, 20, 3, 3);
const result = calculateHorizontalScrollWindow(widths, 20, 3, 3)
// Both arrows may or may not show depending on exact fit
expect(result.startIndex).toBeLessThanOrEqual(3);
expect(result.endIndex).toBeGreaterThan(3);
});
expect(result.startIndex).toBeLessThanOrEqual(3)
expect(result.endIndex).toBeGreaterThan(3)
})
// Boundary conditions
test("empty itemWidths array", () => {
const result = calculateHorizontalScrollWindow([], 50, 3, 0);
test('empty itemWidths array', () => {
const result = calculateHorizontalScrollWindow([], 50, 3, 0)
expect(result).toEqual({
startIndex: 0,
endIndex: 0,
showLeftArrow: false,
showRightArrow: false,
});
});
})
})
test("single item", () => {
const result = calculateHorizontalScrollWindow([30], 50, 3, 0);
test('single item', () => {
const result = calculateHorizontalScrollWindow([30], 50, 3, 0)
expect(result).toEqual({
startIndex: 0,
endIndex: 1,
showLeftArrow: false,
showRightArrow: false,
});
});
})
})
test("available width is 0", () => {
const result = calculateHorizontalScrollWindow([10, 10], 0, 3, 0);
test('available width is 0', () => {
const result = calculateHorizontalScrollWindow([10, 10], 0, 3, 0)
// With 0 width, nothing fits except maybe the selected
expect(result.startIndex).toBe(0);
});
expect(result.startIndex).toBe(0)
})
test("item wider than available width", () => {
const result = calculateHorizontalScrollWindow([100], 50, 3, 0);
test('item wider than available width', () => {
const result = calculateHorizontalScrollWindow([100], 50, 3, 0)
// Total width > available, but only one item
expect(result.startIndex).toBe(0);
expect(result.endIndex).toBe(1);
});
expect(result.startIndex).toBe(0)
expect(result.endIndex).toBe(1)
})
test("all items same width", () => {
const widths = [10, 10, 10, 10];
const result = calculateHorizontalScrollWindow(widths, 25, 3, 2);
expect(result.startIndex).toBeLessThanOrEqual(2);
expect(result.endIndex).toBeGreaterThan(2);
});
test('all items same width', () => {
const widths = [10, 10, 10, 10]
const result = calculateHorizontalScrollWindow(widths, 25, 3, 2)
expect(result.startIndex).toBeLessThanOrEqual(2)
expect(result.endIndex).toBeGreaterThan(2)
})
test("varying item widths", () => {
const widths = [5, 20, 5, 20, 5];
const result = calculateHorizontalScrollWindow(widths, 20, 3, 2);
expect(result.startIndex).toBeLessThanOrEqual(2);
expect(result.endIndex).toBeGreaterThan(2);
});
test('varying item widths', () => {
const widths = [5, 20, 5, 20, 5]
const result = calculateHorizontalScrollWindow(widths, 20, 3, 2)
expect(result.startIndex).toBeLessThanOrEqual(2)
expect(result.endIndex).toBeGreaterThan(2)
})
test("firstItemHasSeparator adds separator width to first item", () => {
const widths = [10, 10, 10, 10, 10];
const withSep = calculateHorizontalScrollWindow(widths, 20, 3, 4, true);
const withoutSep = calculateHorizontalScrollWindow(widths, 20, 3, 4, false);
test('firstItemHasSeparator adds separator width to first item', () => {
const widths = [10, 10, 10, 10, 10]
const withSep = calculateHorizontalScrollWindow(widths, 20, 3, 4, true)
const withoutSep = calculateHorizontalScrollWindow(widths, 20, 3, 4, false)
// Both should include selected index 4
expect(withSep.endIndex).toBe(5);
expect(withoutSep.endIndex).toBe(5);
});
expect(withSep.endIndex).toBe(5)
expect(withoutSep.endIndex).toBe(5)
})
test("selectedIdx in middle of overflow", () => {
const widths = [10, 10, 10, 10, 10, 10, 10];
const result = calculateHorizontalScrollWindow(widths, 25, 3, 3);
expect(result.startIndex).toBeLessThanOrEqual(3);
expect(result.endIndex).toBeGreaterThan(3);
});
test('selectedIdx in middle of overflow', () => {
const widths = [10, 10, 10, 10, 10, 10, 10]
const result = calculateHorizontalScrollWindow(widths, 25, 3, 3)
expect(result.startIndex).toBeLessThanOrEqual(3)
expect(result.endIndex).toBeGreaterThan(3)
})
test("scroll snaps to show selected at left edge", () => {
const widths = [10, 10, 10, 10, 10];
test('scroll snaps to show selected at left edge', () => {
const widths = [10, 10, 10, 10, 10]
// Jump to last item which forces scroll
const result = calculateHorizontalScrollWindow(widths, 20, 3, 4);
expect(result.startIndex).toBeLessThanOrEqual(4);
expect(result.endIndex).toBe(5);
});
const result = calculateHorizontalScrollWindow(widths, 20, 3, 4)
expect(result.startIndex).toBeLessThanOrEqual(4)
expect(result.endIndex).toBe(5)
})
test("scroll snaps to show selected at right edge", () => {
const widths = [10, 10, 10, 10, 10];
const result = calculateHorizontalScrollWindow(widths, 20, 3, 4);
expect(result.endIndex).toBe(5);
expect(result.startIndex).toBeGreaterThan(0);
});
});
test('scroll snaps to show selected at right edge', () => {
const widths = [10, 10, 10, 10, 10]
const result = calculateHorizontalScrollWindow(widths, 20, 3, 4)
expect(result.endIndex).toBe(5)
expect(result.startIndex).toBeGreaterThan(0)
})
})

View File

@@ -1,99 +1,99 @@
import { describe, expect, test } from "bun:test";
import { createHyperlink, OSC8_START, OSC8_END } from "../hyperlink";
import { describe, expect, test } from 'bun:test'
import { createHyperlink, OSC8_START, OSC8_END } from '../hyperlink'
// ─── OSC8 constants ────────────────────────────────────────────────────
describe("OSC8 constants", () => {
test("OSC8_START is the correct escape sequence", () => {
expect(OSC8_START).toBe("\x1b]8;;");
});
describe('OSC8 constants', () => {
test('OSC8_START is the correct escape sequence', () => {
expect(OSC8_START).toBe('\x1b]8;;')
})
test("OSC8_END is the BEL character", () => {
expect(OSC8_END).toBe("\x07");
});
});
test('OSC8_END is the BEL character', () => {
expect(OSC8_END).toBe('\x07')
})
})
// ─── createHyperlink ───────────────────────────────────────────────────
describe("createHyperlink", () => {
test("supported + no content: wraps URL in OSC 8 with URL as display text", () => {
const url = "https://example.com";
const result = createHyperlink(url, undefined, { supportsHyperlinks: true });
describe('createHyperlink', () => {
test('supported + no content: wraps URL in OSC 8 with URL as display text', () => {
const url = 'https://example.com'
const result = createHyperlink(url, undefined, { supportsHyperlinks: true })
expect(result).toContain(OSC8_START);
expect(result).toContain(OSC8_END);
expect(result).toContain(OSC8_START)
expect(result).toContain(OSC8_END)
// Structure: OSC8_START + url + OSC8_END + coloredText + OSC8_START + OSC8_END
expect(result).toStartWith(`${OSC8_START}${url}${OSC8_END}`);
expect(result).toEndWith(`${OSC8_START}${OSC8_END}`);
});
expect(result).toStartWith(`${OSC8_START}${url}${OSC8_END}`)
expect(result).toEndWith(`${OSC8_START}${OSC8_END}`)
})
test("supported + content: shows content as link text", () => {
const url = "https://example.com";
const content = "click here";
const result = createHyperlink(url, content, { supportsHyperlinks: true });
test('supported + content: shows content as link text', () => {
const url = 'https://example.com'
const content = 'click here'
const result = createHyperlink(url, content, { supportsHyperlinks: true })
expect(result).toStartWith(`${OSC8_START}${url}${OSC8_END}`);
expect(result).toContain("click here");
expect(result).toEndWith(`${OSC8_START}${OSC8_END}`);
});
expect(result).toStartWith(`${OSC8_START}${url}${OSC8_END}`)
expect(result).toContain('click here')
expect(result).toEndWith(`${OSC8_START}${OSC8_END}`)
})
test("not supported: returns plain URL regardless of content", () => {
const url = "https://example.com";
const result = createHyperlink(url, "some content", {
test('not supported: returns plain URL regardless of content', () => {
const url = 'https://example.com'
const result = createHyperlink(url, 'some content', {
supportsHyperlinks: false,
});
})
expect(result).toBe(url);
});
expect(result).toBe(url)
})
test("not supported + no content: returns plain URL", () => {
const url = "https://example.com/path?q=1";
test('not supported + no content: returns plain URL', () => {
const url = 'https://example.com/path?q=1'
const result = createHyperlink(url, undefined, {
supportsHyperlinks: false,
});
})
expect(result).toBe(url);
});
expect(result).toBe(url)
})
test("URL with special characters works when supported", () => {
const url = "https://example.com/path?a=1&b=2#section";
const result = createHyperlink(url, undefined, { supportsHyperlinks: true });
test('URL with special characters works when supported', () => {
const url = 'https://example.com/path?a=1&b=2#section'
const result = createHyperlink(url, undefined, { supportsHyperlinks: true })
expect(result).toStartWith(`${OSC8_START}${url}${OSC8_END}`);
expect(result).toEndWith(`${OSC8_START}${OSC8_END}`);
});
expect(result).toStartWith(`${OSC8_START}${url}${OSC8_END}`)
expect(result).toEndWith(`${OSC8_START}${OSC8_END}`)
})
test("URL with special characters works when not supported", () => {
const url = "https://example.com/path?a=1&b=2#section";
test('URL with special characters works when not supported', () => {
const url = 'https://example.com/path?a=1&b=2#section'
const result = createHyperlink(url, undefined, {
supportsHyperlinks: false,
});
})
expect(result).toBe(url);
});
expect(result).toBe(url)
})
test("supported link text contains the display content", () => {
const result = createHyperlink("https://example.com", "text", {
test('supported link text contains the display content', () => {
const result = createHyperlink('https://example.com', 'text', {
supportsHyperlinks: true,
});
})
// The colored text portion is between the two OSC8 sequences
const inner = result.slice(
`${OSC8_START}https://example.com${OSC8_END}`.length,
result.length - `${OSC8_START}${OSC8_END}`.length
);
result.length - `${OSC8_START}${OSC8_END}`.length,
)
// chalk.blue may or may not emit ANSI depending on environment,
// but the display text must always be present
expect(inner).toContain("text");
});
expect(inner).toContain('text')
})
test("empty content string is treated as display text when supported", () => {
const url = "https://example.com";
const result = createHyperlink(url, "", { supportsHyperlinks: true });
test('empty content string is treated as display text when supported', () => {
const url = 'https://example.com'
const result = createHyperlink(url, '', { supportsHyperlinks: true })
// Empty string is falsy, so displayText falls back to url
// Actually: content ?? url — "" is not null/undefined, so "" is used
expect(result).toStartWith(`${OSC8_START}${url}${OSC8_END}`);
expect(result).toEndWith(`${OSC8_START}${OSC8_END}`);
});
});
expect(result).toStartWith(`${OSC8_START}${url}${OSC8_END}`)
expect(result).toEndWith(`${OSC8_START}${OSC8_END}`)
})
})

View File

@@ -5,7 +5,10 @@
* causing API errors. The fix detects the actual format from magic bytes.
*/
import { describe, expect, test } from 'bun:test'
import { detectImageFormatFromBase64, detectImageFormatFromBuffer } from '../imageResizer.js'
import {
detectImageFormatFromBase64,
detectImageFormatFromBuffer,
} from '../imageResizer.js'
// ── Magic byte helpers ────────────────────────────────────────────────────────
@@ -17,9 +20,18 @@ const JPEG_HEADER = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10])
const GIF_HEADER = Buffer.from([0x47, 0x49, 0x46, 0x38, 0x39, 0x61])
/** WebP: RIFF....WEBP */
const WEBP_HEADER = Buffer.from([
0x52, 0x49, 0x46, 0x46, // RIFF
0x00, 0x00, 0x00, 0x00, // file size (placeholder)
0x57, 0x45, 0x42, 0x50, // WEBP
0x52,
0x49,
0x46,
0x46, // RIFF
0x00,
0x00,
0x00,
0x00, // file size (placeholder)
0x57,
0x45,
0x42,
0x50, // WEBP
])
function toBase64(buf: Buffer): string {
@@ -64,7 +76,9 @@ describe('detectImageFormatFromBase64', () => {
})
test('detects JPEG from base64-encoded JPEG header', () => {
expect(detectImageFormatFromBase64(toBase64(JPEG_HEADER))).toBe('image/jpeg')
expect(detectImageFormatFromBase64(toBase64(JPEG_HEADER))).toBe(
'image/jpeg',
)
})
test('detects GIF from base64-encoded GIF header', () => {
@@ -72,7 +86,9 @@ describe('detectImageFormatFromBase64', () => {
})
test('detects WebP from base64-encoded WebP header', () => {
expect(detectImageFormatFromBase64(toBase64(WEBP_HEADER))).toBe('image/webp')
expect(detectImageFormatFromBase64(toBase64(WEBP_HEADER))).toBe(
'image/webp',
)
})
test('returns image/png as default for empty string', () => {

View File

@@ -1,149 +1,149 @@
import { mock, describe, expect, test } from "bun:test";
import { logMock } from "../../../tests/mocks/log";
import { mock, describe, expect, test } from 'bun:test'
import { logMock } from '../../../tests/mocks/log'
// Mock log.ts to cut the heavy dependency chain (log.ts → bootstrap/state.ts → analytics)
mock.module("src/utils/log.ts", logMock);
mock.module('src/utils/log.ts', logMock)
const { safeParseJSON, safeParseJSONC, parseJSONL, addItemToJSONCArray } =
await import("../json");
await import('../json')
// ─── safeParseJSON ──────────────────────────────────────────────────────
describe("safeParseJSON", () => {
test("parses valid object", () => {
expect(safeParseJSON('{"a":1}')).toEqual({ a: 1 });
});
describe('safeParseJSON', () => {
test('parses valid object', () => {
expect(safeParseJSON('{"a":1}')).toEqual({ a: 1 })
})
test("parses valid array", () => {
expect(safeParseJSON("[1,2,3]")).toEqual([1, 2, 3]);
});
test('parses valid array', () => {
expect(safeParseJSON('[1,2,3]')).toEqual([1, 2, 3])
})
test("parses string value", () => {
expect(safeParseJSON('"hello"')).toBe("hello");
});
test('parses string value', () => {
expect(safeParseJSON('"hello"')).toBe('hello')
})
test("parses number value", () => {
expect(safeParseJSON("42")).toBe(42);
});
test('parses number value', () => {
expect(safeParseJSON('42')).toBe(42)
})
test("parses boolean value", () => {
expect(safeParseJSON("true")).toBe(true);
});
test('parses boolean value', () => {
expect(safeParseJSON('true')).toBe(true)
})
test("parses null value", () => {
expect(safeParseJSON("null")).toBeNull();
});
test('parses null value', () => {
expect(safeParseJSON('null')).toBeNull()
})
test("returns null for invalid JSON", () => {
expect(safeParseJSON("{bad}")).toBeNull();
});
test('returns null for invalid JSON', () => {
expect(safeParseJSON('{bad}')).toBeNull()
})
test("returns null for empty string", () => {
expect(safeParseJSON("")).toBeNull();
});
test('returns null for empty string', () => {
expect(safeParseJSON('')).toBeNull()
})
test("returns null for undefined input", () => {
expect(safeParseJSON(undefined as any)).toBeNull();
});
test('returns null for undefined input', () => {
expect(safeParseJSON(undefined as any)).toBeNull()
})
test("returns null for null input", () => {
expect(safeParseJSON(null as any)).toBeNull();
});
test('returns null for null input', () => {
expect(safeParseJSON(null as any)).toBeNull()
})
test("handles JSON with BOM", () => {
expect(safeParseJSON('\uFEFF{"a":1}')).toEqual({ a: 1 });
});
test('handles JSON with BOM', () => {
expect(safeParseJSON('\uFEFF{"a":1}')).toEqual({ a: 1 })
})
test("parses nested objects", () => {
const input = '{"a":{"b":{"c":1}}}';
expect(safeParseJSON(input)).toEqual({ a: { b: { c: 1 } } });
});
});
test('parses nested objects', () => {
const input = '{"a":{"b":{"c":1}}}'
expect(safeParseJSON(input)).toEqual({ a: { b: { c: 1 } } })
})
})
// ─── safeParseJSONC ─────────────────────────────────────────────────────
describe("safeParseJSONC", () => {
test("parses standard JSON", () => {
expect(safeParseJSONC('{"a":1}')).toEqual({ a: 1 });
});
describe('safeParseJSONC', () => {
test('parses standard JSON', () => {
expect(safeParseJSONC('{"a":1}')).toEqual({ a: 1 })
})
test("parses JSON with single-line comments", () => {
expect(safeParseJSONC('{\n// comment\n"a":1\n}')).toEqual({ a: 1 });
});
test('parses JSON with single-line comments', () => {
expect(safeParseJSONC('{\n// comment\n"a":1\n}')).toEqual({ a: 1 })
})
test("parses JSON with block comments", () => {
expect(safeParseJSONC('{\n/* comment */\n"a":1\n}')).toEqual({ a: 1 });
});
test('parses JSON with block comments', () => {
expect(safeParseJSONC('{\n/* comment */\n"a":1\n}')).toEqual({ a: 1 })
})
test("parses JSON with trailing commas", () => {
expect(safeParseJSONC('{"a":1,}')).toEqual({ a: 1 });
});
test('parses JSON with trailing commas', () => {
expect(safeParseJSONC('{"a":1,}')).toEqual({ a: 1 })
})
test("returns null for null input", () => {
expect(safeParseJSONC(null as any)).toBeNull();
});
test('returns null for null input', () => {
expect(safeParseJSONC(null as any)).toBeNull()
})
test("returns null for empty string", () => {
expect(safeParseJSONC("")).toBeNull();
});
});
test('returns null for empty string', () => {
expect(safeParseJSONC('')).toBeNull()
})
})
// ─── parseJSONL ─────────────────────────────────────────────────────────
describe("parseJSONL", () => {
test("parses multiple lines", () => {
const result = parseJSONL('{"a":1}\n{"b":2}');
expect(result).toEqual([{ a: 1 }, { b: 2 }]);
});
describe('parseJSONL', () => {
test('parses multiple lines', () => {
const result = parseJSONL('{"a":1}\n{"b":2}')
expect(result).toEqual([{ a: 1 }, { b: 2 }])
})
test("returns empty array for empty string", () => {
expect(parseJSONL("")).toEqual([]);
});
test('returns empty array for empty string', () => {
expect(parseJSONL('')).toEqual([])
})
test("parses single line", () => {
expect(parseJSONL('{"a":1}')).toEqual([{ a: 1 }]);
});
test('parses single line', () => {
expect(parseJSONL('{"a":1}')).toEqual([{ a: 1 }])
})
test("accepts Buffer input", () => {
const buf = Buffer.from('{"x":1}\n{"y":2}');
const result = parseJSONL(buf as any);
expect(result).toEqual([{ x: 1 }, { y: 2 }]);
});
test('accepts Buffer input', () => {
const buf = Buffer.from('{"x":1}\n{"y":2}')
const result = parseJSONL(buf as any)
expect(result).toEqual([{ x: 1 }, { y: 2 }])
})
// NOTE: Skipping malformed-line test — Bun.JSONL.parseChunk hangs
// indefinitely in its error-recovery loop when encountering bad lines.
});
})
// ─── addItemToJSONCArray ────────────────────────────────────────────────
describe("addItemToJSONCArray", () => {
test("appends to existing array", () => {
const result = addItemToJSONCArray('["a","b"]', "c");
const parsed = JSON.parse(result);
expect(parsed).toEqual(["a", "b", "c"]);
});
describe('addItemToJSONCArray', () => {
test('appends to existing array', () => {
const result = addItemToJSONCArray('["a","b"]', 'c')
const parsed = JSON.parse(result)
expect(parsed).toEqual(['a', 'b', 'c'])
})
test("appends to empty array", () => {
const result = addItemToJSONCArray("[]", "item");
const parsed = JSON.parse(result);
expect(parsed).toEqual(["item"]);
});
test('appends to empty array', () => {
const result = addItemToJSONCArray('[]', 'item')
const parsed = JSON.parse(result)
expect(parsed).toEqual(['item'])
})
test("creates array from empty content", () => {
const result = addItemToJSONCArray("", "first");
const parsed = JSON.parse(result);
expect(parsed).toEqual(["first"]);
});
test('creates array from empty content', () => {
const result = addItemToJSONCArray('', 'first')
const parsed = JSON.parse(result)
expect(parsed).toEqual(['first'])
})
test("handles object item", () => {
const result = addItemToJSONCArray("[]", { key: "val" });
const parsed = JSON.parse(result);
expect(parsed).toEqual([{ key: "val" }]);
});
test('handles object item', () => {
const result = addItemToJSONCArray('[]', { key: 'val' })
const parsed = JSON.parse(result)
expect(parsed).toEqual([{ key: 'val' }])
})
test("wraps item in new array for non-array content", () => {
const result = addItemToJSONCArray('{"a":1}', "item");
const parsed = JSON.parse(result);
expect(parsed).toEqual(["item"]);
});
});
test('wraps item in new array for non-array content', () => {
const result = addItemToJSONCArray('{"a":1}', 'item')
const parsed = JSON.parse(result)
expect(parsed).toEqual(['item'])
})
})

View File

@@ -21,10 +21,14 @@ const { LanBeacon } = await import('../lanBeacon.js')
type MockCall = [string, ...unknown[]]
function getMessageHandler(): ((msg: Buffer, rinfo: { address: string; port: number }) => void) | undefined {
function getMessageHandler():
| ((msg: Buffer, rinfo: { address: string; port: number }) => void)
| undefined {
const calls = mockSocket.on.mock.calls as unknown as MockCall[]
const call = calls.find(c => c[0] === 'message')
return call?.[1] as ((msg: Buffer, rinfo: { address: string; port: number }) => void) | undefined
return call?.[1] as
| ((msg: Buffer, rinfo: { address: string; port: number }) => void)
| undefined
}
describe('LanBeacon', () => {
@@ -155,7 +159,10 @@ describe('LanBeacon', () => {
beacon.updateAnnounce({ role: 'sub' })
beacon.start()
// The send call should include the updated role
const sendCalls = mockSocket.send.mock.calls as unknown as [Buffer, ...unknown[]][]
const sendCalls = mockSocket.send.mock.calls as unknown as [
Buffer,
...unknown[],
][]
const sendCall = sendCalls[0]
if (sendCall) {
const payload = JSON.parse(sendCall[0].toString())

View File

@@ -1,54 +1,54 @@
import { describe, expect, test } from "bun:test";
import { lazySchema } from "../lazySchema";
import { describe, expect, test } from 'bun:test'
import { lazySchema } from '../lazySchema'
describe("lazySchema", () => {
test("returns a function", () => {
const factory = lazySchema(() => 42);
expect(typeof factory).toBe("function");
});
describe('lazySchema', () => {
test('returns a function', () => {
const factory = lazySchema(() => 42)
expect(typeof factory).toBe('function')
})
test("calls factory on first invocation", () => {
let callCount = 0;
test('calls factory on first invocation', () => {
let callCount = 0
const factory = lazySchema(() => {
callCount++;
return "result";
});
factory();
expect(callCount).toBe(1);
});
callCount++
return 'result'
})
factory()
expect(callCount).toBe(1)
})
test("returns cached result on subsequent invocations", () => {
const factory = lazySchema(() => ({ value: Math.random() }));
const first = factory();
const second = factory();
expect(first).toBe(second);
});
test('returns cached result on subsequent invocations', () => {
const factory = lazySchema(() => ({ value: Math.random() }))
const first = factory()
const second = factory()
expect(first).toBe(second)
})
test("factory is called only once", () => {
let callCount = 0;
test('factory is called only once', () => {
let callCount = 0
const factory = lazySchema(() => {
callCount++;
return "cached";
});
factory();
factory();
factory();
expect(callCount).toBe(1);
});
callCount++
return 'cached'
})
factory()
factory()
factory()
expect(callCount).toBe(1)
})
test("works with different return types", () => {
const numFactory = lazySchema(() => 123);
expect(numFactory()).toBe(123);
test('works with different return types', () => {
const numFactory = lazySchema(() => 123)
expect(numFactory()).toBe(123)
const arrFactory = lazySchema(() => [1, 2, 3]);
expect(arrFactory()).toEqual([1, 2, 3]);
});
const arrFactory = lazySchema(() => [1, 2, 3])
expect(arrFactory()).toEqual([1, 2, 3])
})
test("each call to lazySchema returns independent cache", () => {
const a = lazySchema(() => ({ id: "a" }));
const b = lazySchema(() => ({ id: "b" }));
expect(a()).not.toBe(b());
expect(a().id).toBe("a");
expect(b().id).toBe("b");
});
});
test('each call to lazySchema returns independent cache', () => {
const a = lazySchema(() => ({ id: 'a' }))
const b = lazySchema(() => ({ id: 'b' }))
expect(a()).not.toBe(b())
expect(a().id).toBe('a')
expect(b().id).toBe('b')
})
})

View File

@@ -1,58 +1,58 @@
import { describe, expect, test } from "bun:test";
import { padAligned } from "../markdown";
import { describe, expect, test } from 'bun:test'
import { padAligned } from '../markdown'
describe("padAligned", () => {
test("left-aligns: pads with spaces on right", () => {
const result = padAligned("hello", 5, 10, "left");
expect(result).toBe("hello ");
expect(result.length).toBe(10);
});
describe('padAligned', () => {
test('left-aligns: pads with spaces on right', () => {
const result = padAligned('hello', 5, 10, 'left')
expect(result).toBe('hello ')
expect(result.length).toBe(10)
})
test("right-aligns: pads with spaces on left", () => {
const result = padAligned("hello", 5, 10, "right");
expect(result).toBe(" hello");
expect(result.length).toBe(10);
});
test('right-aligns: pads with spaces on left', () => {
const result = padAligned('hello', 5, 10, 'right')
expect(result).toBe(' hello')
expect(result.length).toBe(10)
})
test("center-aligns: pads with spaces on both sides", () => {
const result = padAligned("hi", 2, 6, "center");
expect(result).toBe(" hi ");
expect(result.length).toBe(6);
});
test('center-aligns: pads with spaces on both sides', () => {
const result = padAligned('hi', 2, 6, 'center')
expect(result).toBe(' hi ')
expect(result.length).toBe(6)
})
test("no padding when displayWidth equals targetWidth", () => {
const result = padAligned("hello", 5, 5, "left");
expect(result).toBe("hello");
});
test('no padding when displayWidth equals targetWidth', () => {
const result = padAligned('hello', 5, 5, 'left')
expect(result).toBe('hello')
})
test("handles content wider than targetWidth", () => {
const result = padAligned("hello world", 11, 5, "left");
expect(result).toBe("hello world");
});
test('handles content wider than targetWidth', () => {
const result = padAligned('hello world', 11, 5, 'left')
expect(result).toBe('hello world')
})
test("null/undefined align defaults to left", () => {
expect(padAligned("hi", 2, 5, null)).toBe("hi ");
expect(padAligned("hi", 2, 5, undefined)).toBe("hi ");
});
test('null/undefined align defaults to left', () => {
expect(padAligned('hi', 2, 5, null)).toBe('hi ')
expect(padAligned('hi', 2, 5, undefined)).toBe('hi ')
})
test("handles empty string content", () => {
const result = padAligned("", 0, 5, "center");
expect(result).toBe(" ");
});
test('handles empty string content', () => {
const result = padAligned('', 0, 5, 'center')
expect(result).toBe(' ')
})
test("handles zero displayWidth", () => {
const result = padAligned("", 0, 3, "left");
expect(result).toBe(" ");
});
test('handles zero displayWidth', () => {
const result = padAligned('', 0, 3, 'left')
expect(result).toBe(' ')
})
test("handles zero targetWidth", () => {
const result = padAligned("hello", 5, 0, "left");
expect(result).toBe("hello");
});
test('handles zero targetWidth', () => {
const result = padAligned('hello', 5, 0, 'left')
expect(result).toBe('hello')
})
test("center alignment with odd padding distribution", () => {
const result = padAligned("hi", 2, 7, "center");
expect(result).toBe(" hi ");
expect(result.length).toBe(7);
});
});
test('center alignment with odd padding distribution', () => {
const result = padAligned('hi', 2, 7, 'center')
expect(result).toBe(' hi ')
expect(result.length).toBe(7)
})
})

View File

@@ -1,226 +1,226 @@
import { mock, describe, expect, test, beforeEach } from "bun:test";
import { logMock } from "../../../tests/mocks/log";
import { mock, describe, expect, test, beforeEach } from 'bun:test'
import { logMock } from '../../../tests/mocks/log'
// Mock log.ts to cut the bootstrap/state dependency chain
mock.module("src/utils/log.ts", logMock);
mock.module('src/utils/log.ts', logMock)
const { memoizeWithTTL, memoizeWithTTLAsync, memoizeWithLRU } = await import(
"../memoize"
);
'../memoize'
)
// ─── memoizeWithTTL ────────────────────────────────────────────────────
describe("memoizeWithTTL", () => {
test("returns cached value on second call", () => {
let calls = 0;
describe('memoizeWithTTL', () => {
test('returns cached value on second call', () => {
let calls = 0
const fn = memoizeWithTTL((x: number) => {
calls++;
return x * 2;
}, 60_000);
calls++
return x * 2
}, 60_000)
expect(fn(5)).toBe(10);
expect(fn(5)).toBe(10);
expect(calls).toBe(1);
});
expect(fn(5)).toBe(10)
expect(fn(5)).toBe(10)
expect(calls).toBe(1)
})
test("different args get separate cache entries", () => {
let calls = 0;
test('different args get separate cache entries', () => {
let calls = 0
const fn = memoizeWithTTL((x: number) => {
calls++;
return x + 1;
}, 60_000);
calls++
return x + 1
}, 60_000)
expect(fn(1)).toBe(2);
expect(fn(2)).toBe(3);
expect(calls).toBe(2);
});
expect(fn(1)).toBe(2)
expect(fn(2)).toBe(3)
expect(calls).toBe(2)
})
test("cache.clear empties the cache", () => {
let calls = 0;
test('cache.clear empties the cache', () => {
let calls = 0
const fn = memoizeWithTTL(() => {
calls++;
return "val";
}, 60_000);
calls++
return 'val'
}, 60_000)
fn();
fn.cache.clear();
fn();
expect(calls).toBe(2);
});
fn()
fn.cache.clear()
fn()
expect(calls).toBe(2)
})
test("returns stale value and triggers background refresh after TTL", async () => {
let calls = 0;
test('returns stale value and triggers background refresh after TTL', async () => {
let calls = 0
const fn = memoizeWithTTL((x: number) => {
calls++;
return x * calls;
}, 1); // 1ms TTL
calls++
return x * calls
}, 1) // 1ms TTL
const first = fn(10);
expect(first).toBe(10); // calls=1, 10*1
const first = fn(10)
expect(first).toBe(10) // calls=1, 10*1
// Wait for TTL to expire
await new Promise((r) => setTimeout(r, 10));
await new Promise(r => setTimeout(r, 10))
// Should return stale value (10) and trigger background refresh
const second = fn(10);
expect(second).toBe(10); // stale value returned immediately
const second = fn(10)
expect(second).toBe(10) // stale value returned immediately
// Wait for background refresh microtask
await new Promise((r) => setTimeout(r, 10));
await new Promise(r => setTimeout(r, 10))
// Now cache should have refreshed value (calls=2 during refresh, 10*2=20)
const third = fn(10);
expect(third).toBe(20);
});
});
const third = fn(10)
expect(third).toBe(20)
})
})
// ─── memoizeWithTTLAsync ───────────────────────────────────────────────
describe("memoizeWithTTLAsync", () => {
test("caches async result", async () => {
let calls = 0;
describe('memoizeWithTTLAsync', () => {
test('caches async result', async () => {
let calls = 0
const fn = memoizeWithTTLAsync(async (x: number) => {
calls++;
return x * 2;
}, 60_000);
calls++
return x * 2
}, 60_000)
expect(await fn(5)).toBe(10);
expect(await fn(5)).toBe(10);
expect(calls).toBe(1);
});
expect(await fn(5)).toBe(10)
expect(await fn(5)).toBe(10)
expect(calls).toBe(1)
})
test("deduplicates concurrent cold-miss calls", async () => {
let calls = 0;
test('deduplicates concurrent cold-miss calls', async () => {
let calls = 0
const fn = memoizeWithTTLAsync(async (x: number) => {
calls++;
await new Promise((r) => setTimeout(r, 20));
return x;
}, 60_000);
calls++
await new Promise(r => setTimeout(r, 20))
return x
}, 60_000)
const [a, b, c] = await Promise.all([fn(1), fn(1), fn(1)]);
expect(a).toBe(1);
expect(b).toBe(1);
expect(c).toBe(1);
expect(calls).toBe(1);
});
const [a, b, c] = await Promise.all([fn(1), fn(1), fn(1)])
expect(a).toBe(1)
expect(b).toBe(1)
expect(c).toBe(1)
expect(calls).toBe(1)
})
test("cache.clear forces re-computation", async () => {
let calls = 0;
test('cache.clear forces re-computation', async () => {
let calls = 0
const fn = memoizeWithTTLAsync(async () => {
calls++;
return "v";
}, 60_000);
calls++
return 'v'
}, 60_000)
await fn();
fn.cache.clear();
await fn();
expect(calls).toBe(2);
});
await fn()
fn.cache.clear()
await fn()
expect(calls).toBe(2)
})
test("returns stale value on TTL expiry", async () => {
let calls = 0;
test('returns stale value on TTL expiry', async () => {
let calls = 0
const fn = memoizeWithTTLAsync(async () => {
calls++;
return calls;
}, 1); // 1ms TTL
calls++
return calls
}, 1) // 1ms TTL
const first = await fn();
expect(first).toBe(1);
const first = await fn()
expect(first).toBe(1)
await new Promise((r) => setTimeout(r, 10));
await new Promise(r => setTimeout(r, 10))
// Should return stale value (1) immediately
const second = await fn();
expect(second).toBe(1);
});
});
const second = await fn()
expect(second).toBe(1)
})
})
// ─── memoizeWithLRU ────────────────────────────────────────────────────
describe("memoizeWithLRU", () => {
test("caches results by key", () => {
let calls = 0;
describe('memoizeWithLRU', () => {
test('caches results by key', () => {
let calls = 0
const fn = memoizeWithLRU(
(x: number) => {
calls++;
return x * 2;
calls++
return x * 2
},
(x) => String(x),
10
);
x => String(x),
10,
)
expect(fn(5)).toBe(10);
expect(fn(5)).toBe(10);
expect(calls).toBe(1);
});
expect(fn(5)).toBe(10)
expect(fn(5)).toBe(10)
expect(calls).toBe(1)
})
test("evicts least recently used when max reached", () => {
let calls = 0;
test('evicts least recently used when max reached', () => {
let calls = 0
const fn = memoizeWithLRU(
(x: number) => {
calls++;
return x;
calls++
return x
},
(x) => String(x),
3
);
x => String(x),
3,
)
fn(1);
fn(2);
fn(3);
expect(calls).toBe(3);
fn(1)
fn(2)
fn(3)
expect(calls).toBe(3)
fn(4); // evicts key "1"
expect(fn.cache.has("1")).toBe(false);
expect(fn.cache.has("4")).toBe(true);
});
fn(4) // evicts key "1"
expect(fn.cache.has('1')).toBe(false)
expect(fn.cache.has('4')).toBe(true)
})
test("cache.size returns current size", () => {
test('cache.size returns current size', () => {
const fn = memoizeWithLRU(
(x: number) => x,
(x) => String(x),
10
);
x => String(x),
10,
)
fn(1);
fn(2);
expect(fn.cache.size()).toBe(2);
});
fn(1)
fn(2)
expect(fn.cache.size()).toBe(2)
})
test("cache.delete removes entry", () => {
test('cache.delete removes entry', () => {
const fn = memoizeWithLRU(
(x: number) => x,
(x) => String(x),
10
);
x => String(x),
10,
)
fn(1);
expect(fn.cache.has("1")).toBe(true);
fn.cache.delete("1");
expect(fn.cache.has("1")).toBe(false);
});
fn(1)
expect(fn.cache.has('1')).toBe(true)
fn.cache.delete('1')
expect(fn.cache.has('1')).toBe(false)
})
test("cache.get returns value without updating recency", () => {
test('cache.get returns value without updating recency', () => {
const fn = memoizeWithLRU(
(x: number) => x * 10,
(x) => String(x),
10
);
x => String(x),
10,
)
fn(5);
expect(fn.cache.get("5")).toBe(50);
});
fn(5)
expect(fn.cache.get('5')).toBe(50)
})
test("cache.clear empties everything", () => {
test('cache.clear empties everything', () => {
const fn = memoizeWithLRU(
(x: number) => x,
(x) => String(x),
10
);
x => String(x),
10,
)
fn(1);
fn(2);
fn.cache.clear();
expect(fn.cache.size()).toBe(0);
});
});
fn(1)
fn(2)
fn.cache.clear()
expect(fn.cache.size()).toBe(0)
})
})

View File

@@ -1,197 +1,215 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import {
clearCommandQueue,
dequeue,
dequeueAllMatching,
enqueue,
enqueuePendingNotification,
hasCommandsInQueue,
isSlashCommand,
peek,
resetCommandQueue,
clearCommandQueue,
dequeue,
dequeueAllMatching,
enqueue,
enqueuePendingNotification,
hasCommandsInQueue,
isSlashCommand,
peek,
resetCommandQueue,
} from '../messageQueueManager.js'
// Reset module-level queue state between tests
beforeEach(() => {
resetCommandQueue()
resetCommandQueue()
})
afterEach(() => {
resetCommandQueue()
resetCommandQueue()
})
describe('messageQueueManager.isSlashCommand', () => {
test('treats normal slash commands as slash commands', () => {
expect(isSlashCommand({ value: '/help', mode: 'prompt' } as any)).toBe(true)
})
test('treats normal slash commands as slash commands', () => {
expect(isSlashCommand({ value: '/help', mode: 'prompt' } as any)).toBe(true)
})
test('keeps remote bridge slash commands slash-routed when bridgeOrigin is set', () => {
expect(
isSlashCommand({
value: '/proactive',
mode: 'prompt',
skipSlashCommands: true,
bridgeOrigin: true,
} as any),
).toBe(true)
})
test('keeps remote bridge slash commands slash-routed when bridgeOrigin is set', () => {
expect(
isSlashCommand({
value: '/proactive',
mode: 'prompt',
skipSlashCommands: true,
bridgeOrigin: true,
} as any),
).toBe(true)
})
test('keeps skipSlashCommands text-only when bridgeOrigin is absent', () => {
expect(
isSlashCommand({
value: '/proactive',
mode: 'prompt',
skipSlashCommands: true,
} as any),
).toBe(false)
})
test('keeps skipSlashCommands text-only when bridgeOrigin is absent', () => {
expect(
isSlashCommand({
value: '/proactive',
mode: 'prompt',
skipSlashCommands: true,
} as any),
).toBe(false)
})
})
describe('messageQueueManager.enqueue', () => {
test('adds command to queue with default next priority', () => {
enqueue({ value: 'hello', mode: 'prompt' } as any)
expect(hasCommandsInQueue()).toBe(true)
const cmd = dequeue()
expect(cmd).toBeDefined()
expect(cmd!.value).toBe('hello')
expect(cmd!.priority).toBe('next')
})
test('adds command to queue with default next priority', () => {
enqueue({ value: 'hello', mode: 'prompt' } as any)
expect(hasCommandsInQueue()).toBe(true)
const cmd = dequeue()
expect(cmd).toBeDefined()
expect(cmd!.value).toBe('hello')
expect(cmd!.priority).toBe('next')
})
test('preserves explicit priority', () => {
enqueue({ value: 'urgent', mode: 'prompt', priority: 'now' } as any)
const cmd = dequeue()
expect(cmd!.priority).toBe('now')
})
test('preserves explicit priority', () => {
enqueue({ value: 'urgent', mode: 'prompt', priority: 'now' } as any)
const cmd = dequeue()
expect(cmd!.priority).toBe('now')
})
})
describe('messageQueueManager.enqueuePendingNotification', () => {
test('adds command with later priority', () => {
enqueuePendingNotification({ value: '<task-notification/>', mode: 'task-notification' } as any)
const cmd = dequeue()
expect(cmd).toBeDefined()
expect(cmd!.priority).toBe('later')
expect(cmd!.mode).toBe('task-notification')
})
test('adds command with later priority', () => {
enqueuePendingNotification({
value: '<task-notification/>',
mode: 'task-notification',
} as any)
const cmd = dequeue()
expect(cmd).toBeDefined()
expect(cmd!.priority).toBe('later')
expect(cmd!.mode).toBe('task-notification')
})
})
describe('messageQueueManager.dequeue', () => {
test('returns undefined when queue empty', () => {
expect(dequeue()).toBeUndefined()
})
test('returns undefined when queue empty', () => {
expect(dequeue()).toBeUndefined()
})
test('returns highest priority command', () => {
enqueuePendingNotification({ value: 'later-cmd', mode: 'task-notification' } as any)
enqueue({ value: 'next-cmd', mode: 'prompt' } as any)
enqueue({ value: 'now-cmd', mode: 'prompt', priority: 'now' } as any)
test('returns highest priority command', () => {
enqueuePendingNotification({
value: 'later-cmd',
mode: 'task-notification',
} as any)
enqueue({ value: 'next-cmd', mode: 'prompt' } as any)
enqueue({ value: 'now-cmd', mode: 'prompt', priority: 'now' } as any)
const first = dequeue()
expect(first!.value).toBe('now-cmd')
const first = dequeue()
expect(first!.value).toBe('now-cmd')
const second = dequeue()
expect(second!.value).toBe('next-cmd')
const second = dequeue()
expect(second!.value).toBe('next-cmd')
const third = dequeue()
expect(third!.value).toBe('later-cmd')
})
const third = dequeue()
expect(third!.value).toBe('later-cmd')
})
test('FIFO within same priority', () => {
enqueue({ value: 'first', mode: 'prompt' } as any)
enqueue({ value: 'second', mode: 'prompt' } as any)
test('FIFO within same priority', () => {
enqueue({ value: 'first', mode: 'prompt' } as any)
enqueue({ value: 'second', mode: 'prompt' } as any)
expect(dequeue()!.value).toBe('first')
expect(dequeue()!.value).toBe('second')
})
expect(dequeue()!.value).toBe('first')
expect(dequeue()!.value).toBe('second')
})
test('respects filter parameter', () => {
enqueue({ value: 'prompt-cmd', mode: 'prompt' } as any)
enqueuePendingNotification({ value: 'task-cmd', mode: 'task-notification' } as any)
test('respects filter parameter', () => {
enqueue({ value: 'prompt-cmd', mode: 'prompt' } as any)
enqueuePendingNotification({
value: 'task-cmd',
mode: 'task-notification',
} as any)
// Filter to only task-notification commands
const cmd = dequeue(c => c.mode === 'task-notification')
expect(cmd).toBeDefined()
expect(cmd!.value).toBe('task-cmd')
// Filter to only task-notification commands
const cmd = dequeue(c => c.mode === 'task-notification')
expect(cmd).toBeDefined()
expect(cmd!.value).toBe('task-cmd')
// Prompt command should still be in queue
expect(hasCommandsInQueue()).toBe(true)
expect(dequeue()!.value).toBe('prompt-cmd')
})
// Prompt command should still be in queue
expect(hasCommandsInQueue()).toBe(true)
expect(dequeue()!.value).toBe('prompt-cmd')
})
})
describe('messageQueueManager.peek', () => {
test('returns undefined when queue empty', () => {
expect(peek()).toBeUndefined()
})
test('returns undefined when queue empty', () => {
expect(peek()).toBeUndefined()
})
test('returns highest priority without removing', () => {
enqueuePendingNotification({ value: 'later', mode: 'task-notification' } as any)
enqueue({ value: 'next', mode: 'prompt' } as any)
test('returns highest priority without removing', () => {
enqueuePendingNotification({
value: 'later',
mode: 'task-notification',
} as any)
enqueue({ value: 'next', mode: 'prompt' } as any)
expect(peek()!.value).toBe('next')
expect(hasCommandsInQueue()).toBe(true)
expect(dequeue()!.value).toBe('next')
})
expect(peek()!.value).toBe('next')
expect(hasCommandsInQueue()).toBe(true)
expect(dequeue()!.value).toBe('next')
})
})
describe('messageQueueManager.dequeueAllMatching', () => {
test('removes all matching commands', () => {
enqueue({ value: 'a', mode: 'prompt' } as any)
enqueue({ value: 'b', mode: 'task-notification' } as any)
enqueue({ value: 'c', mode: 'task-notification' } as any)
test('removes all matching commands', () => {
enqueue({ value: 'a', mode: 'prompt' } as any)
enqueue({ value: 'b', mode: 'task-notification' } as any)
enqueue({ value: 'c', mode: 'task-notification' } as any)
const matched = dequeueAllMatching(c => c.mode === 'task-notification')
expect(matched).toHaveLength(2)
expect(matched.map(c => c.value)).toEqual(['b', 'c'])
const matched = dequeueAllMatching(c => c.mode === 'task-notification')
expect(matched).toHaveLength(2)
expect(matched.map(c => c.value)).toEqual(['b', 'c'])
// Remaining command should still be in queue
expect(dequeue()!.value).toBe('a')
})
// Remaining command should still be in queue
expect(dequeue()!.value).toBe('a')
})
test('returns empty array when no matches', () => {
enqueue({ value: 'a', mode: 'prompt' } as any)
const matched = dequeueAllMatching(c => c.mode === 'bash')
expect(matched).toHaveLength(0)
expect(hasCommandsInQueue()).toBe(true)
})
test('returns empty array when no matches', () => {
enqueue({ value: 'a', mode: 'prompt' } as any)
const matched = dequeueAllMatching(c => c.mode === 'bash')
expect(matched).toHaveLength(0)
expect(hasCommandsInQueue()).toBe(true)
})
test('returns empty array when queue empty', () => {
const matched = dequeueAllMatching(() => true)
expect(matched).toHaveLength(0)
})
test('returns empty array when queue empty', () => {
const matched = dequeueAllMatching(() => true)
expect(matched).toHaveLength(0)
})
})
describe('messageQueueManager.clearCommandQueue', () => {
test('removes all commands', () => {
enqueue({ value: 'a', mode: 'prompt' } as any)
enqueue({ value: 'b', mode: 'prompt' } as any)
expect(hasCommandsInQueue()).toBe(true)
test('removes all commands', () => {
enqueue({ value: 'a', mode: 'prompt' } as any)
enqueue({ value: 'b', mode: 'prompt' } as any)
expect(hasCommandsInQueue()).toBe(true)
clearCommandQueue()
expect(hasCommandsInQueue()).toBe(false)
})
clearCommandQueue()
expect(hasCommandsInQueue()).toBe(false)
})
test('no-op on empty queue', () => {
clearCommandQueue()
expect(hasCommandsInQueue()).toBe(false)
})
test('no-op on empty queue', () => {
clearCommandQueue()
expect(hasCommandsInQueue()).toBe(false)
})
})
describe('messageQueueManager priority ordering', () => {
test('now dequeued before next and later', () => {
enqueuePendingNotification({ value: 'later', mode: 'task-notification' } as any)
enqueue({ value: 'next', mode: 'prompt' } as any)
enqueue({ value: 'now', mode: 'prompt', priority: 'now' } as any)
test('now dequeued before next and later', () => {
enqueuePendingNotification({
value: 'later',
mode: 'task-notification',
} as any)
enqueue({ value: 'next', mode: 'prompt' } as any)
enqueue({ value: 'now', mode: 'prompt', priority: 'now' } as any)
expect(dequeue()!.value).toBe('now')
expect(dequeue()!.value).toBe('next')
expect(dequeue()!.value).toBe('later')
})
expect(dequeue()!.value).toBe('now')
expect(dequeue()!.value).toBe('next')
expect(dequeue()!.value).toBe('later')
})
test('next dequeued before later', () => {
enqueuePendingNotification({ value: 'later', mode: 'task-notification' } as any)
enqueue({ value: 'next', mode: 'prompt' } as any)
test('next dequeued before later', () => {
enqueuePendingNotification({
value: 'later',
mode: 'task-notification',
} as any)
enqueue({ value: 'next', mode: 'prompt' } as any)
expect(dequeue()!.value).toBe('next')
expect(dequeue()!.value).toBe('later')
})
expect(dequeue()!.value).toBe('next')
expect(dequeue()!.value).toBe('later')
})
})

View File

@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
import {
deriveShortMessageId,
INTERRUPT_MESSAGE,
@@ -27,488 +27,492 @@ import {
AUTO_REJECT_MESSAGE,
DONT_ASK_REJECT_MESSAGE,
SYNTHETIC_MODEL,
} from "../messages";
import type { Message, AssistantMessage, UserMessage } from "../../types/message";
} from '../messages'
import type {
Message,
AssistantMessage,
UserMessage,
} from '../../types/message'
// ─── Helpers ─────────────────────────────────────────────────────────────
function makeAssistantMsg(
contentBlocks: Array<{ type: string; text?: string; [key: string]: any }>
contentBlocks: Array<{ type: string; text?: string; [key: string]: any }>,
): AssistantMessage {
return createAssistantMessage({
content: contentBlocks as any,
});
})
}
function makeUserMsg(text: string): UserMessage {
return createUserMessage({ content: text });
return createUserMessage({ content: text })
}
// ─── deriveShortMessageId ───────────────────────────────────────────────
describe("deriveShortMessageId", () => {
test("returns 6-char string", () => {
const id = deriveShortMessageId("550e8400-e29b-41d4-a716-446655440000");
expect(id).toHaveLength(6);
});
describe('deriveShortMessageId', () => {
test('returns 6-char string', () => {
const id = deriveShortMessageId('550e8400-e29b-41d4-a716-446655440000')
expect(id).toHaveLength(6)
})
test("is deterministic for same input", () => {
const uuid = "a0b1c2d3-e4f5-6789-abcd-ef0123456789";
expect(deriveShortMessageId(uuid)).toBe(deriveShortMessageId(uuid));
});
test('is deterministic for same input', () => {
const uuid = 'a0b1c2d3-e4f5-6789-abcd-ef0123456789'
expect(deriveShortMessageId(uuid)).toBe(deriveShortMessageId(uuid))
})
test("produces different IDs for different UUIDs", () => {
const id1 = deriveShortMessageId("00000000-0000-0000-0000-000000000001");
const id2 = deriveShortMessageId("ffffffff-ffff-ffff-ffff-ffffffffffff");
expect(id1).not.toBe(id2);
});
});
test('produces different IDs for different UUIDs', () => {
const id1 = deriveShortMessageId('00000000-0000-0000-0000-000000000001')
const id2 = deriveShortMessageId('ffffffff-ffff-ffff-ffff-ffffffffffff')
expect(id1).not.toBe(id2)
})
})
// ─── Constants ──────────────────────────────────────────────────────────
describe("message constants", () => {
test("SYNTHETIC_MESSAGES contains expected messages", () => {
expect(SYNTHETIC_MESSAGES.has(INTERRUPT_MESSAGE)).toBe(true);
expect(SYNTHETIC_MESSAGES.has(INTERRUPT_MESSAGE_FOR_TOOL_USE)).toBe(true);
expect(SYNTHETIC_MESSAGES.has(CANCEL_MESSAGE)).toBe(true);
expect(SYNTHETIC_MESSAGES.has(REJECT_MESSAGE)).toBe(true);
expect(SYNTHETIC_MESSAGES.has(NO_RESPONSE_REQUESTED)).toBe(true);
});
describe('message constants', () => {
test('SYNTHETIC_MESSAGES contains expected messages', () => {
expect(SYNTHETIC_MESSAGES.has(INTERRUPT_MESSAGE)).toBe(true)
expect(SYNTHETIC_MESSAGES.has(INTERRUPT_MESSAGE_FOR_TOOL_USE)).toBe(true)
expect(SYNTHETIC_MESSAGES.has(CANCEL_MESSAGE)).toBe(true)
expect(SYNTHETIC_MESSAGES.has(REJECT_MESSAGE)).toBe(true)
expect(SYNTHETIC_MESSAGES.has(NO_RESPONSE_REQUESTED)).toBe(true)
})
test("SYNTHETIC_MODEL is <synthetic>", () => {
expect(SYNTHETIC_MODEL).toBe("<synthetic>");
});
});
test('SYNTHETIC_MODEL is <synthetic>', () => {
expect(SYNTHETIC_MODEL).toBe('<synthetic>')
})
})
// ─── Message factories ──────────────────────────────────────────────────
describe("createAssistantMessage", () => {
test("creates assistant message with string content", () => {
const msg = createAssistantMessage({ content: "hello" });
expect(msg.type).toBe("assistant");
expect(msg.message!.role).toBe("assistant");
expect(msg.message!.content![0] as any).toBeTruthy();
expect((msg.message!.content![0] as any).text).toBe("hello");
});
describe('createAssistantMessage', () => {
test('creates assistant message with string content', () => {
const msg = createAssistantMessage({ content: 'hello' })
expect(msg.type).toBe('assistant')
expect(msg.message!.role).toBe('assistant')
expect(msg.message!.content![0] as any).toBeTruthy()
expect((msg.message!.content![0] as any).text).toBe('hello')
})
test("creates assistant message with content blocks", () => {
const blocks = [{ type: "text" as const, text: "hello" }];
const msg = createAssistantMessage({ content: blocks as any });
expect(msg.type).toBe("assistant");
expect(msg.message.content).toHaveLength(1);
});
test('creates assistant message with content blocks', () => {
const blocks = [{ type: 'text' as const, text: 'hello' }]
const msg = createAssistantMessage({ content: blocks as any })
expect(msg.type).toBe('assistant')
expect(msg.message.content).toHaveLength(1)
})
test("generates unique uuid per call", () => {
const msg1 = createAssistantMessage({ content: "a" });
const msg2 = createAssistantMessage({ content: "b" });
expect(msg1.uuid).not.toBe(msg2.uuid);
});
test('generates unique uuid per call', () => {
const msg1 = createAssistantMessage({ content: 'a' })
const msg2 = createAssistantMessage({ content: 'b' })
expect(msg1.uuid).not.toBe(msg2.uuid)
})
test("has isApiErrorMessage false", () => {
const msg = createAssistantMessage({ content: "test" });
expect(msg.isApiErrorMessage).toBe(false);
});
});
test('has isApiErrorMessage false', () => {
const msg = createAssistantMessage({ content: 'test' })
expect(msg.isApiErrorMessage).toBe(false)
})
})
describe("createAssistantAPIErrorMessage", () => {
test("sets isApiErrorMessage to true", () => {
const msg = createAssistantAPIErrorMessage({ content: "error" });
expect(msg.isApiErrorMessage).toBe(true);
});
describe('createAssistantAPIErrorMessage', () => {
test('sets isApiErrorMessage to true', () => {
const msg = createAssistantAPIErrorMessage({ content: 'error' })
expect(msg.isApiErrorMessage).toBe(true)
})
test("includes error details", () => {
test('includes error details', () => {
const msg = createAssistantAPIErrorMessage({
content: "fail",
errorDetails: "rate limited",
});
expect(msg.errorDetails).toBe("rate limited");
});
});
content: 'fail',
errorDetails: 'rate limited',
})
expect(msg.errorDetails).toBe('rate limited')
})
})
describe("createUserMessage", () => {
test("creates user message with string content", () => {
const msg = createUserMessage({ content: "hello" });
expect(msg.type).toBe("user");
expect(msg.message.role).toBe("user");
expect(msg.message.content).toBe("hello");
});
describe('createUserMessage', () => {
test('creates user message with string content', () => {
const msg = createUserMessage({ content: 'hello' })
expect(msg.type).toBe('user')
expect(msg.message.role).toBe('user')
expect(msg.message.content).toBe('hello')
})
test("generates unique uuid", () => {
const msg1 = createUserMessage({ content: "a" });
const msg2 = createUserMessage({ content: "b" });
expect(msg1.uuid).not.toBe(msg2.uuid);
});
test('generates unique uuid', () => {
const msg1 = createUserMessage({ content: 'a' })
const msg2 = createUserMessage({ content: 'b' })
expect(msg1.uuid).not.toBe(msg2.uuid)
})
test("uses provided uuid when given", () => {
test('uses provided uuid when given', () => {
const msg = createUserMessage({
content: "test",
uuid: "custom-uuid-1234-5678-abcd-ef0123456789",
});
expect(msg.uuid).toBe("custom-uuid-1234-5678-abcd-ef0123456789");
});
content: 'test',
uuid: 'custom-uuid-1234-5678-abcd-ef0123456789',
})
expect(msg.uuid).toBe('custom-uuid-1234-5678-abcd-ef0123456789')
})
test("sets isMeta flag", () => {
const msg = createUserMessage({ content: "test", isMeta: true });
expect(msg.isMeta).toBe(true);
});
});
test('sets isMeta flag', () => {
const msg = createUserMessage({ content: 'test', isMeta: true })
expect(msg.isMeta).toBe(true)
})
})
describe("createUserInterruptionMessage", () => {
test("creates interrupt message without tool use", () => {
const msg = createUserInterruptionMessage({});
expect(msg.type).toBe("user");
expect((msg.message.content as any)[0].text).toBe(INTERRUPT_MESSAGE);
});
describe('createUserInterruptionMessage', () => {
test('creates interrupt message without tool use', () => {
const msg = createUserInterruptionMessage({})
expect(msg.type).toBe('user')
expect((msg.message.content as any)[0].text).toBe(INTERRUPT_MESSAGE)
})
test("creates interrupt message with tool use", () => {
const msg = createUserInterruptionMessage({ toolUse: true });
test('creates interrupt message with tool use', () => {
const msg = createUserInterruptionMessage({ toolUse: true })
expect((msg.message.content as any)[0].text).toBe(
INTERRUPT_MESSAGE_FOR_TOOL_USE
);
});
});
INTERRUPT_MESSAGE_FOR_TOOL_USE,
)
})
})
describe("prepareUserContent", () => {
test("returns string when no preceding blocks", () => {
describe('prepareUserContent', () => {
test('returns string when no preceding blocks', () => {
const result = prepareUserContent({
inputString: "hello",
inputString: 'hello',
precedingInputBlocks: [],
});
expect(result).toBe("hello");
});
})
expect(result).toBe('hello')
})
test("returns array when preceding blocks exist", () => {
const blocks = [{ type: "image" as const, source: {} } as any];
test('returns array when preceding blocks exist', () => {
const blocks = [{ type: 'image' as const, source: {} } as any]
const result = prepareUserContent({
inputString: "describe this",
inputString: 'describe this',
precedingInputBlocks: blocks,
});
expect(Array.isArray(result)).toBe(true);
expect((result as any[]).length).toBe(2);
expect((result as any[])[1].text).toBe("describe this");
});
});
})
expect(Array.isArray(result)).toBe(true)
expect((result as any[]).length).toBe(2)
expect((result as any[])[1].text).toBe('describe this')
})
})
describe("createToolResultStopMessage", () => {
test("creates tool result with error flag", () => {
const result = createToolResultStopMessage("tool-123");
expect(result.type).toBe("tool_result");
expect(result.is_error).toBe(true);
expect(result.tool_use_id).toBe("tool-123");
expect(result.content).toBe(CANCEL_MESSAGE);
});
});
describe('createToolResultStopMessage', () => {
test('creates tool result with error flag', () => {
const result = createToolResultStopMessage('tool-123')
expect(result.type).toBe('tool_result')
expect(result.is_error).toBe(true)
expect(result.tool_use_id).toBe('tool-123')
expect(result.content).toBe(CANCEL_MESSAGE)
})
})
// ─── isSyntheticMessage ─────────────────────────────────────────────────
describe("isSyntheticMessage", () => {
test("identifies interrupt message as synthetic", () => {
describe('isSyntheticMessage', () => {
test('identifies interrupt message as synthetic', () => {
const msg: any = {
type: "user",
message: { content: [{ type: "text", text: INTERRUPT_MESSAGE }] },
};
expect(isSyntheticMessage(msg)).toBe(true);
});
type: 'user',
message: { content: [{ type: 'text', text: INTERRUPT_MESSAGE }] },
}
expect(isSyntheticMessage(msg)).toBe(true)
})
test("identifies cancel message as synthetic", () => {
test('identifies cancel message as synthetic', () => {
const msg: any = {
type: "user",
message: { content: [{ type: "text", text: CANCEL_MESSAGE }] },
};
expect(isSyntheticMessage(msg)).toBe(true);
});
type: 'user',
message: { content: [{ type: 'text', text: CANCEL_MESSAGE }] },
}
expect(isSyntheticMessage(msg)).toBe(true)
})
test("returns false for normal user message", () => {
test('returns false for normal user message', () => {
const msg: any = {
type: "user",
message: { content: [{ type: "text", text: "hello" }] },
};
expect(isSyntheticMessage(msg)).toBe(false);
});
type: 'user',
message: { content: [{ type: 'text', text: 'hello' }] },
}
expect(isSyntheticMessage(msg)).toBe(false)
})
test("returns false for progress message", () => {
test('returns false for progress message', () => {
const msg: any = {
type: "progress",
message: { content: [{ type: "text", text: INTERRUPT_MESSAGE }] },
};
expect(isSyntheticMessage(msg)).toBe(false);
});
type: 'progress',
message: { content: [{ type: 'text', text: INTERRUPT_MESSAGE }] },
}
expect(isSyntheticMessage(msg)).toBe(false)
})
test("returns false for string content", () => {
test('returns false for string content', () => {
const msg: any = {
type: "user",
type: 'user',
message: { content: INTERRUPT_MESSAGE },
};
expect(isSyntheticMessage(msg)).toBe(false);
});
});
}
expect(isSyntheticMessage(msg)).toBe(false)
})
})
// ─── getLastAssistantMessage ────────────────────────────────────────────
describe("getLastAssistantMessage", () => {
test("returns last assistant message", () => {
const a1 = makeAssistantMsg([{ type: "text", text: "first" }]);
const u = makeUserMsg("mid");
const a2 = makeAssistantMsg([{ type: "text", text: "last" }]);
const result = getLastAssistantMessage([a1, u, a2]);
expect(result).toBe(a2);
});
describe('getLastAssistantMessage', () => {
test('returns last assistant message', () => {
const a1 = makeAssistantMsg([{ type: 'text', text: 'first' }])
const u = makeUserMsg('mid')
const a2 = makeAssistantMsg([{ type: 'text', text: 'last' }])
const result = getLastAssistantMessage([a1, u, a2])
expect(result).toBe(a2)
})
test("returns undefined for empty array", () => {
expect(getLastAssistantMessage([])).toBeUndefined();
});
test('returns undefined for empty array', () => {
expect(getLastAssistantMessage([])).toBeUndefined()
})
test("returns undefined when no assistant messages", () => {
const u = makeUserMsg("hello");
expect(getLastAssistantMessage([u])).toBeUndefined();
});
});
test('returns undefined when no assistant messages', () => {
const u = makeUserMsg('hello')
expect(getLastAssistantMessage([u])).toBeUndefined()
})
})
// ─── hasToolCallsInLastAssistantTurn ────────────────────────────────────
describe("hasToolCallsInLastAssistantTurn", () => {
test("returns true when last assistant has tool_use", () => {
describe('hasToolCallsInLastAssistantTurn', () => {
test('returns true when last assistant has tool_use', () => {
const msg = makeAssistantMsg([
{ type: "text", text: "let me check" },
{ type: "tool_use", id: "t1", name: "Bash", input: {} },
]);
expect(hasToolCallsInLastAssistantTurn([msg])).toBe(true);
});
{ type: 'text', text: 'let me check' },
{ type: 'tool_use', id: 't1', name: 'Bash', input: {} },
])
expect(hasToolCallsInLastAssistantTurn([msg])).toBe(true)
})
test("returns false when last assistant has only text", () => {
const msg = makeAssistantMsg([{ type: "text", text: "done" }]);
expect(hasToolCallsInLastAssistantTurn([msg])).toBe(false);
});
test('returns false when last assistant has only text', () => {
const msg = makeAssistantMsg([{ type: 'text', text: 'done' }])
expect(hasToolCallsInLastAssistantTurn([msg])).toBe(false)
})
test("returns false for empty messages", () => {
expect(hasToolCallsInLastAssistantTurn([])).toBe(false);
});
});
test('returns false for empty messages', () => {
expect(hasToolCallsInLastAssistantTurn([])).toBe(false)
})
})
// ─── extractTag ─────────────────────────────────────────────────────────
describe("extractTag", () => {
test("extracts simple tag content", () => {
expect(extractTag("<foo>bar</foo>", "foo")).toBe("bar");
});
describe('extractTag', () => {
test('extracts simple tag content', () => {
expect(extractTag('<foo>bar</foo>', 'foo')).toBe('bar')
})
test("extracts tag with attributes", () => {
expect(extractTag('<foo class="a">bar</foo>', "foo")).toBe("bar");
});
test('extracts tag with attributes', () => {
expect(extractTag('<foo class="a">bar</foo>', 'foo')).toBe('bar')
})
test("handles multiline content", () => {
expect(extractTag("<foo>\nline1\nline2\n</foo>", "foo")).toBe(
"\nline1\nline2\n"
);
});
test('handles multiline content', () => {
expect(extractTag('<foo>\nline1\nline2\n</foo>', 'foo')).toBe(
'\nline1\nline2\n',
)
})
test("returns null for missing tag", () => {
expect(extractTag("<foo>bar</foo>", "baz")).toBeNull();
});
test('returns null for missing tag', () => {
expect(extractTag('<foo>bar</foo>', 'baz')).toBeNull()
})
test("returns null for empty html", () => {
expect(extractTag("", "foo")).toBeNull();
});
test('returns null for empty html', () => {
expect(extractTag('', 'foo')).toBeNull()
})
test("returns null for empty tagName", () => {
expect(extractTag("<foo>bar</foo>", "")).toBeNull();
});
test('returns null for empty tagName', () => {
expect(extractTag('<foo>bar</foo>', '')).toBeNull()
})
test("is case-insensitive", () => {
expect(extractTag("<FOO>bar</FOO>", "foo")).toBe("bar");
});
});
test('is case-insensitive', () => {
expect(extractTag('<FOO>bar</FOO>', 'foo')).toBe('bar')
})
})
// ─── isNotEmptyMessage ──────────────────────────────────────────────────
describe("isNotEmptyMessage", () => {
test("returns true for message with text content", () => {
describe('isNotEmptyMessage', () => {
test('returns true for message with text content', () => {
const msg: any = {
type: "user",
message: { content: "hello" },
};
expect(isNotEmptyMessage(msg)).toBe(true);
});
type: 'user',
message: { content: 'hello' },
}
expect(isNotEmptyMessage(msg)).toBe(true)
})
test("returns false for empty string content", () => {
test('returns false for empty string content', () => {
const msg: any = {
type: "user",
message: { content: " " },
};
expect(isNotEmptyMessage(msg)).toBe(false);
});
type: 'user',
message: { content: ' ' },
}
expect(isNotEmptyMessage(msg)).toBe(false)
})
test("returns false for empty content array", () => {
test('returns false for empty content array', () => {
const msg: any = {
type: "user",
type: 'user',
message: { content: [] },
};
expect(isNotEmptyMessage(msg)).toBe(false);
});
}
expect(isNotEmptyMessage(msg)).toBe(false)
})
test("returns true for progress message", () => {
test('returns true for progress message', () => {
const msg: any = {
type: "progress",
type: 'progress',
message: { content: [] },
};
expect(isNotEmptyMessage(msg)).toBe(true);
});
}
expect(isNotEmptyMessage(msg)).toBe(true)
})
test("returns true for multi-block content", () => {
test('returns true for multi-block content', () => {
const msg: any = {
type: "user",
type: 'user',
message: {
content: [
{ type: "text", text: "a" },
{ type: "text", text: "b" },
{ type: 'text', text: 'a' },
{ type: 'text', text: 'b' },
],
},
};
expect(isNotEmptyMessage(msg)).toBe(true);
});
}
expect(isNotEmptyMessage(msg)).toBe(true)
})
test("returns true for non-text block", () => {
test('returns true for non-text block', () => {
const msg: any = {
type: "user",
type: 'user',
message: {
content: [{ type: "tool_use", id: "t1", name: "Bash", input: {} }],
content: [{ type: 'tool_use', id: 't1', name: 'Bash', input: {} }],
},
};
expect(isNotEmptyMessage(msg)).toBe(true);
});
}
expect(isNotEmptyMessage(msg)).toBe(true)
})
test("returns false for whitespace-only text block in content array", () => {
test('returns false for whitespace-only text block in content array', () => {
const msg: any = {
type: "user",
type: 'user',
message: {
content: [{ type: "text", text: " " }],
content: [{ type: 'text', text: ' ' }],
},
};
expect(isNotEmptyMessage(msg)).toBe(false);
});
});
}
expect(isNotEmptyMessage(msg)).toBe(false)
})
})
// ─── deriveUUID ─────────────────────────────────────────────────────────
describe("deriveUUID", () => {
test("produces deterministic output", () => {
const parent = "550e8400-e29b-41d4-a716-446655440000" as any;
expect(deriveUUID(parent, 0)).toBe(deriveUUID(parent, 0));
});
describe('deriveUUID', () => {
test('produces deterministic output', () => {
const parent = '550e8400-e29b-41d4-a716-446655440000' as any
expect(deriveUUID(parent, 0)).toBe(deriveUUID(parent, 0))
})
test("produces different output for different indices", () => {
const parent = "550e8400-e29b-41d4-a716-446655440000" as any;
expect(deriveUUID(parent, 0)).not.toBe(deriveUUID(parent, 1));
});
test('produces different output for different indices', () => {
const parent = '550e8400-e29b-41d4-a716-446655440000' as any
expect(deriveUUID(parent, 0)).not.toBe(deriveUUID(parent, 1))
})
test("preserves UUID-like length", () => {
const parent = "550e8400-e29b-41d4-a716-446655440000" as any;
const derived = deriveUUID(parent, 5);
expect(derived.length).toBe(parent.length);
});
});
test('preserves UUID-like length', () => {
const parent = '550e8400-e29b-41d4-a716-446655440000' as any
const derived = deriveUUID(parent, 5)
expect(derived.length).toBe(parent.length)
})
})
// ─── isClassifierDenial ─────────────────────────────────────────────────
describe("isClassifierDenial", () => {
test("returns true for classifier denial prefix", () => {
describe('isClassifierDenial', () => {
test('returns true for classifier denial prefix', () => {
expect(
isClassifierDenial(
"Permission for this action has been denied. Reason: unsafe"
)
).toBe(true);
});
'Permission for this action has been denied. Reason: unsafe',
),
).toBe(true)
})
test("returns false for normal content", () => {
expect(isClassifierDenial("hello world")).toBe(false);
});
});
test('returns false for normal content', () => {
expect(isClassifierDenial('hello world')).toBe(false)
})
})
// ─── Message builder functions ──────────────────────────────────────────
describe("AUTO_REJECT_MESSAGE", () => {
test("includes tool name", () => {
const msg = AUTO_REJECT_MESSAGE("Bash");
expect(msg).toContain("Bash");
expect(msg).toContain("denied");
});
});
describe('AUTO_REJECT_MESSAGE', () => {
test('includes tool name', () => {
const msg = AUTO_REJECT_MESSAGE('Bash')
expect(msg).toContain('Bash')
expect(msg).toContain('denied')
})
})
describe("DONT_ASK_REJECT_MESSAGE", () => {
test("includes tool name and dont ask mode", () => {
const msg = DONT_ASK_REJECT_MESSAGE("Write");
expect(msg).toContain("Write");
expect(msg).toContain("don't ask mode");
});
});
describe('DONT_ASK_REJECT_MESSAGE', () => {
test('includes tool name and dont ask mode', () => {
const msg = DONT_ASK_REJECT_MESSAGE('Write')
expect(msg).toContain('Write')
expect(msg).toContain("don't ask mode")
})
})
describe("buildYoloRejectionMessage", () => {
test("includes reason", () => {
const msg = buildYoloRejectionMessage("potentially destructive");
expect(msg).toContain("potentially destructive");
expect(msg).toContain("denied");
});
});
describe('buildYoloRejectionMessage', () => {
test('includes reason', () => {
const msg = buildYoloRejectionMessage('potentially destructive')
expect(msg).toContain('potentially destructive')
expect(msg).toContain('denied')
})
})
describe("buildClassifierUnavailableMessage", () => {
test("includes tool name and model", () => {
const msg = buildClassifierUnavailableMessage("Bash", "classifier-v1");
expect(msg).toContain("Bash");
expect(msg).toContain("classifier-v1");
expect(msg).toContain("unavailable");
});
describe('buildClassifierUnavailableMessage', () => {
test('includes tool name and model', () => {
const msg = buildClassifierUnavailableMessage('Bash', 'classifier-v1')
expect(msg).toContain('Bash')
expect(msg).toContain('classifier-v1')
expect(msg).toContain('unavailable')
})
test("tells the model to wait and retry later", () => {
const msg = buildClassifierUnavailableMessage("Bash", "classifier-v1");
expect(msg).toContain("Wait briefly and then try this action again.");
expect(msg).toContain("come back to it later");
});
});
test('tells the model to wait and retry later', () => {
const msg = buildClassifierUnavailableMessage('Bash', 'classifier-v1')
expect(msg).toContain('Wait briefly and then try this action again.')
expect(msg).toContain('come back to it later')
})
})
describe("normalizeMessages", () => {
test("splits multi-block assistant message into individual messages", () => {
describe('normalizeMessages', () => {
test('splits multi-block assistant message into individual messages', () => {
const msg = makeAssistantMsg([
{ type: "text", text: "first" },
{ type: "text", text: "second" },
]);
const normalized = normalizeMessages([msg]);
expect(normalized.length).toBe(2);
{ type: 'text', text: 'first' },
{ type: 'text', text: 'second' },
])
const normalized = normalizeMessages([msg])
expect(normalized.length).toBe(2)
// Verify each split message contains only one content block
expect(normalized[0].message.content).toHaveLength(1);
expect((normalized[0].message.content as any[])[0].text).toBe("first");
expect(normalized[1].message.content).toHaveLength(1);
expect((normalized[1].message.content as any[])[0].text).toBe("second");
});
expect(normalized[0].message.content).toHaveLength(1)
expect((normalized[0].message.content as any[])[0].text).toBe('first')
expect(normalized[1].message.content).toHaveLength(1)
expect((normalized[1].message.content as any[])[0].text).toBe('second')
})
test("handles empty array", () => {
const result = normalizeMessages([] as AssistantMessage[]);
expect(result).toEqual([]);
});
test('handles empty array', () => {
const result = normalizeMessages([] as AssistantMessage[])
expect(result).toEqual([])
})
test("preserves single-block message", () => {
const msg = makeAssistantMsg([{ type: "text", text: "hello" }]);
const normalized = normalizeMessages([msg]);
expect(normalized.length).toBe(1);
});
});
test('preserves single-block message', () => {
const msg = makeAssistantMsg([{ type: 'text', text: 'hello' }])
const normalized = normalizeMessages([msg])
expect(normalized.length).toBe(1)
})
})
describe("normalizeMessagesForAPI", () => {
test("preserves Gemini thought signature metadata on tool_use blocks", () => {
describe('normalizeMessagesForAPI', () => {
test('preserves Gemini thought signature metadata on tool_use blocks', () => {
const assistant = makeAssistantMsg([
{
type: "tool_use",
id: "tool-1",
name: "Bash",
input: { command: "pwd" },
_geminiThoughtSignature: "sig-123",
type: 'tool_use',
id: 'tool-1',
name: 'Bash',
input: { command: 'pwd' },
_geminiThoughtSignature: 'sig-123',
},
]);
])
const normalized = normalizeMessagesForAPI([assistant]);
const block = (normalized[0] as AssistantMessage).message!.content![0] as any;
const normalized = normalizeMessagesForAPI([assistant])
const block = (normalized[0] as AssistantMessage).message!
.content![0] as any
expect(block.type).toBe("tool_use");
expect(block._geminiThoughtSignature).toBe("sig-123");
});
});
expect(block.type).toBe('tool_use')
expect(block._geminiThoughtSignature).toBe('sig-123')
})
})

View File

@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
// formatPrice and COST_TIER constants are pure data/functions from modelCost.ts
// We test the formatting logic directly to avoid the heavy import chain.
@@ -18,63 +18,63 @@ function formatModelPricing(costs: {
return `${formatPrice(costs.inputTokens)}/${formatPrice(costs.outputTokens)} per Mtok`
}
describe("COST_TIER constant values", () => {
describe('COST_TIER constant values', () => {
// These verify the documented pricing from https://platform.claude.com/docs/en/about-claude/pricing
test("COST_TIER_3_15: $3/$15 (Sonnet tier)", () => {
test('COST_TIER_3_15: $3/$15 (Sonnet tier)', () => {
expect(formatModelPricing({ inputTokens: 3, outputTokens: 15 })).toBe(
"$3/$15 per Mtok",
'$3/$15 per Mtok',
)
})
test("COST_TIER_15_75: $15/$75 (Opus 4/4.1 tier)", () => {
test('COST_TIER_15_75: $15/$75 (Opus 4/4.1 tier)', () => {
expect(formatModelPricing({ inputTokens: 15, outputTokens: 75 })).toBe(
"$15/$75 per Mtok",
'$15/$75 per Mtok',
)
})
test("COST_TIER_5_25: $5/$25 (Opus 4.5/4.6 tier)", () => {
test('COST_TIER_5_25: $5/$25 (Opus 4.5/4.6 tier)', () => {
expect(formatModelPricing({ inputTokens: 5, outputTokens: 25 })).toBe(
"$5/$25 per Mtok",
'$5/$25 per Mtok',
)
})
test("COST_TIER_30_150: $30/$150 (Fast Opus 4.6)", () => {
test('COST_TIER_30_150: $30/$150 (Fast Opus 4.6)', () => {
expect(formatModelPricing({ inputTokens: 30, outputTokens: 150 })).toBe(
"$30/$150 per Mtok",
'$30/$150 per Mtok',
)
})
test("COST_HAIKU_35: $0.80/$4 (Haiku 3.5)", () => {
test('COST_HAIKU_35: $0.80/$4 (Haiku 3.5)', () => {
expect(formatModelPricing({ inputTokens: 0.8, outputTokens: 4 })).toBe(
"$0.80/$4 per Mtok",
'$0.80/$4 per Mtok',
)
})
test("COST_HAIKU_45: $1/$5 (Haiku 4.5)", () => {
test('COST_HAIKU_45: $1/$5 (Haiku 4.5)', () => {
expect(formatModelPricing({ inputTokens: 1, outputTokens: 5 })).toBe(
"$1/$5 per Mtok",
'$1/$5 per Mtok',
)
})
})
describe("formatPrice", () => {
describe('formatPrice', () => {
test("formats integers without decimals: 3 → '$3'", () => {
expect(formatPrice(3)).toBe("$3")
expect(formatPrice(3)).toBe('$3')
})
test("formats floats with 2 decimals: 0.8 → '$0.80'", () => {
expect(formatPrice(0.8)).toBe("$0.80")
expect(formatPrice(0.8)).toBe('$0.80')
})
test("formats large integers: 150 → '$150'", () => {
expect(formatPrice(150)).toBe("$150")
expect(formatPrice(150)).toBe('$150')
})
test("formats 1 as integer: '$1'", () => {
expect(formatPrice(1)).toBe("$1")
expect(formatPrice(1)).toBe('$1')
})
test("formats mixed decimal: 22.5 → '$22.50'", () => {
expect(formatPrice(22.5)).toBe("$22.50")
expect(formatPrice(22.5)).toBe('$22.50')
})
})

View File

@@ -1,170 +1,170 @@
import { describe, expect, test } from "bun:test";
import { parseCellId, mapNotebookCellsToToolResult } from "../notebook";
import { describe, expect, test } from 'bun:test'
import { parseCellId, mapNotebookCellsToToolResult } from '../notebook'
// ─── parseCellId ───────────────────────────────────────────────────────
describe("parseCellId", () => {
test("parses cell-0 to 0", () => {
expect(parseCellId("cell-0")).toBe(0);
});
describe('parseCellId', () => {
test('parses cell-0 to 0', () => {
expect(parseCellId('cell-0')).toBe(0)
})
test("parses cell-5 to 5", () => {
expect(parseCellId("cell-5")).toBe(5);
});
test('parses cell-5 to 5', () => {
expect(parseCellId('cell-5')).toBe(5)
})
test("parses cell-100 to 100", () => {
expect(parseCellId("cell-100")).toBe(100);
});
test('parses cell-100 to 100', () => {
expect(parseCellId('cell-100')).toBe(100)
})
test("returns undefined for cell- (no number)", () => {
expect(parseCellId("cell-")).toBeUndefined();
});
test('returns undefined for cell- (no number)', () => {
expect(parseCellId('cell-')).toBeUndefined()
})
test("returns undefined for cell-abc (non-numeric)", () => {
expect(parseCellId("cell-abc")).toBeUndefined();
});
test('returns undefined for cell-abc (non-numeric)', () => {
expect(parseCellId('cell-abc')).toBeUndefined()
})
test("returns undefined for other-format", () => {
expect(parseCellId("other-format")).toBeUndefined();
});
test('returns undefined for other-format', () => {
expect(parseCellId('other-format')).toBeUndefined()
})
test("returns undefined for empty string", () => {
expect(parseCellId("")).toBeUndefined();
});
test('returns undefined for empty string', () => {
expect(parseCellId('')).toBeUndefined()
})
test("returns undefined for prefix-only match like cell-0-extra", () => {
test('returns undefined for prefix-only match like cell-0-extra', () => {
// regex is /^cell-(\d+)$/ so trailing text should fail
expect(parseCellId("cell-0-extra")).toBeUndefined();
});
expect(parseCellId('cell-0-extra')).toBeUndefined()
})
test("returns undefined for negative numbers", () => {
expect(parseCellId("cell--1")).toBeUndefined();
});
test('returns undefined for negative numbers', () => {
expect(parseCellId('cell--1')).toBeUndefined()
})
test("parses leading zeros correctly", () => {
expect(parseCellId("cell-007")).toBe(7);
});
});
test('parses leading zeros correctly', () => {
expect(parseCellId('cell-007')).toBe(7)
})
})
// ─── mapNotebookCellsToToolResult ──────────────────────────────────────
describe("mapNotebookCellsToToolResult", () => {
test("returns tool result with correct tool_use_id", () => {
describe('mapNotebookCellsToToolResult', () => {
test('returns tool result with correct tool_use_id', () => {
const data = [
{
cellType: "code",
cellType: 'code',
source: 'print("hello")',
cell_id: "cell-0",
language: "python",
cell_id: 'cell-0',
language: 'python',
},
];
]
const result = mapNotebookCellsToToolResult(data, "tool-123");
expect(result.tool_use_id).toBe("tool-123");
expect(result.type).toBe("tool_result");
});
const result = mapNotebookCellsToToolResult(data, 'tool-123')
expect(result.tool_use_id).toBe('tool-123')
expect(result.type).toBe('tool_result')
})
test("content array contains text blocks for cell content", () => {
test('content array contains text blocks for cell content', () => {
const data = [
{
cellType: "code",
cellType: 'code',
source: 'x = 1',
cell_id: "cell-0",
language: "python",
cell_id: 'cell-0',
language: 'python',
},
];
]
const result = mapNotebookCellsToToolResult(data, "tool-1");
expect(result.content).toBeInstanceOf(Array);
expect(result.content!.length).toBeGreaterThanOrEqual(1);
const result = mapNotebookCellsToToolResult(data, 'tool-1')
expect(result.content).toBeInstanceOf(Array)
expect(result.content!.length).toBeGreaterThanOrEqual(1)
const firstBlock = result.content![0] as { type: string; text: string };
expect(firstBlock.type).toBe("text");
expect(firstBlock.text).toContain('cell id="cell-0"');
expect(firstBlock.text).toContain("x = 1");
});
const firstBlock = result.content![0] as { type: string; text: string }
expect(firstBlock.type).toBe('text')
expect(firstBlock.text).toContain('cell id="cell-0"')
expect(firstBlock.text).toContain('x = 1')
})
test("merges adjacent text blocks from multiple cells", () => {
test('merges adjacent text blocks from multiple cells', () => {
const data = [
{
cellType: "code",
source: "a = 1",
cell_id: "cell-0",
language: "python",
cellType: 'code',
source: 'a = 1',
cell_id: 'cell-0',
language: 'python',
},
{
cellType: "code",
source: "b = 2",
cell_id: "cell-1",
language: "python",
cellType: 'code',
source: 'b = 2',
cell_id: 'cell-1',
language: 'python',
},
];
]
const result = mapNotebookCellsToToolResult(data, "tool-2");
const result = mapNotebookCellsToToolResult(data, 'tool-2')
// Two adjacent text blocks should be merged into one
const textBlocks = (result.content as any[]).filter(
(b: any) => b.type === "text"
);
expect(textBlocks).toHaveLength(1);
});
(b: any) => b.type === 'text',
)
expect(textBlocks).toHaveLength(1)
})
test("preserves image blocks without merging", () => {
test('preserves image blocks without merging', () => {
const data = [
{
cellType: "code",
source: "plot()",
cell_id: "cell-0",
language: "python",
cellType: 'code',
source: 'plot()',
cell_id: 'cell-0',
language: 'python',
outputs: [
{
output_type: "display_data",
text: "",
output_type: 'display_data',
text: '',
image: {
image_data: "iVBORw0KGgo=",
media_type: "image/png" as const,
image_data: 'iVBORw0KGgo=',
media_type: 'image/png' as const,
},
},
],
},
{
cellType: "code",
source: "print(1)",
cell_id: "cell-1",
language: "python",
cellType: 'code',
source: 'print(1)',
cell_id: 'cell-1',
language: 'python',
},
];
]
const result = mapNotebookCellsToToolResult(data, "tool-3");
const types = (result.content as any[]).map((b: any) => b.type);
expect(types).toContain("image");
});
const result = mapNotebookCellsToToolResult(data, 'tool-3')
const types = (result.content as any[]).map((b: any) => b.type)
expect(types).toContain('image')
})
test("markdown cell includes cell_type metadata", () => {
test('markdown cell includes cell_type metadata', () => {
const data = [
{
cellType: "markdown",
source: "# Title",
cell_id: "cell-0",
cellType: 'markdown',
source: '# Title',
cell_id: 'cell-0',
},
];
]
const result = mapNotebookCellsToToolResult(data, "tool-4");
const textBlock = result.content![0] as { type: string; text: string };
expect(textBlock.text).toContain("<cell_type>markdown</cell_type>");
});
const result = mapNotebookCellsToToolResult(data, 'tool-4')
const textBlock = result.content![0] as { type: string; text: string }
expect(textBlock.text).toContain('<cell_type>markdown</cell_type>')
})
test("non-python code cell includes language metadata", () => {
test('non-python code cell includes language metadata', () => {
const data = [
{
cellType: "code",
source: "val x = 1",
cell_id: "cell-0",
language: "scala",
cellType: 'code',
source: 'val x = 1',
cell_id: 'cell-0',
language: 'scala',
},
];
]
const result = mapNotebookCellsToToolResult(data, "tool-5");
const textBlock = result.content![0] as { type: string; text: string };
expect(textBlock.text).toContain("<language>scala</language>");
});
});
const result = mapNotebookCellsToToolResult(data, 'tool-5')
const textBlock = result.content![0] as { type: string; text: string }
expect(textBlock.text).toContain('<language>scala</language>')
})
})

View File

@@ -1,55 +1,55 @@
import { describe, expect, test } from "bun:test";
import { objectGroupBy } from "../objectGroupBy";
import { describe, expect, test } from 'bun:test'
import { objectGroupBy } from '../objectGroupBy'
describe("objectGroupBy", () => {
test("groups items by key", () => {
const result = objectGroupBy([1, 2, 3, 4], (n) =>
n % 2 === 0 ? "even" : "odd"
);
expect(result.even).toEqual([2, 4]);
expect(result.odd).toEqual([1, 3]);
});
describe('objectGroupBy', () => {
test('groups items by key', () => {
const result = objectGroupBy([1, 2, 3, 4], n =>
n % 2 === 0 ? 'even' : 'odd',
)
expect(result.even).toEqual([2, 4])
expect(result.odd).toEqual([1, 3])
})
test("returns empty object for empty input", () => {
const result = objectGroupBy([], () => "key");
expect(Object.keys(result)).toHaveLength(0);
});
test('returns empty object for empty input', () => {
const result = objectGroupBy([], () => 'key')
expect(Object.keys(result)).toHaveLength(0)
})
test("handles single group", () => {
const result = objectGroupBy(["a", "b", "c"], () => "all");
expect(result.all).toEqual(["a", "b", "c"]);
});
test('handles single group', () => {
const result = objectGroupBy(['a', 'b', 'c'], () => 'all')
expect(result.all).toEqual(['a', 'b', 'c'])
})
test("passes index to keySelector", () => {
const result = objectGroupBy(["a", "b", "c", "d"], (_, i) =>
i < 2 ? "first" : "second"
);
expect(result.first).toEqual(["a", "b"]);
expect(result.second).toEqual(["c", "d"]);
});
test('passes index to keySelector', () => {
const result = objectGroupBy(['a', 'b', 'c', 'd'], (_, i) =>
i < 2 ? 'first' : 'second',
)
expect(result.first).toEqual(['a', 'b'])
expect(result.second).toEqual(['c', 'd'])
})
test("works with objects", () => {
test('works with objects', () => {
const items = [
{ name: "Alice", role: "admin" },
{ name: "Bob", role: "user" },
{ name: "Charlie", role: "admin" },
];
const result = objectGroupBy(items, (item) => item.role);
expect(result.admin).toHaveLength(2);
expect(result.user).toHaveLength(1);
});
{ name: 'Alice', role: 'admin' },
{ name: 'Bob', role: 'user' },
{ name: 'Charlie', role: 'admin' },
]
const result = objectGroupBy(items, item => item.role)
expect(result.admin).toHaveLength(2)
expect(result.user).toHaveLength(1)
})
test("handles key function returning undefined", () => {
const result = objectGroupBy([1, 2, 3], () => undefined as any);
expect(result["undefined"]).toEqual([1, 2, 3]);
});
test('handles key function returning undefined', () => {
const result = objectGroupBy([1, 2, 3], () => undefined as any)
expect(result['undefined']).toEqual([1, 2, 3])
})
test("handles keys with special characters", () => {
test('handles keys with special characters', () => {
const result = objectGroupBy(
[{ key: "a/b" }, { key: "a.b" }, { key: "a/b" }],
(item) => item.key
);
expect(result["a/b"]).toHaveLength(2);
expect(result["a.b"]).toHaveLength(1);
});
});
[{ key: 'a/b' }, { key: 'a.b' }, { key: 'a/b' }],
item => item.key,
)
expect(result['a/b']).toHaveLength(2)
expect(result['a.b']).toHaveLength(1)
})
})

View File

@@ -1,249 +1,249 @@
import { describe, expect, test } from "bun:test";
import { tmpdir } from "os";
import { resolve } from "path";
import { describe, expect, test } from 'bun:test'
import { tmpdir } from 'os'
import { resolve } from 'path'
import {
getFsImplementation,
setFsImplementation,
setOriginalFsImplementation,
type FsOperations,
} from "../fsOperations";
} from '../fsOperations'
import {
containsPathTraversal,
expandPath,
normalizePathForConfigKey,
toRelativePath,
getDirectoryForPath,
} from "../path";
} from '../path'
// ─── containsPathTraversal ──────────────────────────────────────────────
describe("containsPathTraversal", () => {
test("detects ../ at start", () => {
expect(containsPathTraversal("../foo")).toBe(true);
});
describe('containsPathTraversal', () => {
test('detects ../ at start', () => {
expect(containsPathTraversal('../foo')).toBe(true)
})
test("detects ../ in middle", () => {
expect(containsPathTraversal("foo/../bar")).toBe(true);
});
test('detects ../ in middle', () => {
expect(containsPathTraversal('foo/../bar')).toBe(true)
})
test("detects .. at end", () => {
expect(containsPathTraversal("foo/..")).toBe(true);
});
test('detects .. at end', () => {
expect(containsPathTraversal('foo/..')).toBe(true)
})
test("detects standalone ..", () => {
expect(containsPathTraversal("..")).toBe(true);
});
test('detects standalone ..', () => {
expect(containsPathTraversal('..')).toBe(true)
})
test("detects backslash traversal", () => {
expect(containsPathTraversal("foo\\..\\bar")).toBe(true);
});
test('detects backslash traversal', () => {
expect(containsPathTraversal('foo\\..\\bar')).toBe(true)
})
test("returns false for normal path", () => {
expect(containsPathTraversal("foo/bar/baz")).toBe(false);
});
test('returns false for normal path', () => {
expect(containsPathTraversal('foo/bar/baz')).toBe(false)
})
test("returns false for single dot", () => {
expect(containsPathTraversal("./foo")).toBe(false);
});
test('returns false for single dot', () => {
expect(containsPathTraversal('./foo')).toBe(false)
})
test("returns false for ... in filename", () => {
expect(containsPathTraversal("foo/...bar")).toBe(false);
});
test('returns false for ... in filename', () => {
expect(containsPathTraversal('foo/...bar')).toBe(false)
})
test("returns false for empty string", () => {
expect(containsPathTraversal("")).toBe(false);
});
test('returns false for empty string', () => {
expect(containsPathTraversal('')).toBe(false)
})
test("returns false for dotdot in filename without separator", () => {
expect(containsPathTraversal("foo..bar")).toBe(false);
});
test('returns false for dotdot in filename without separator', () => {
expect(containsPathTraversal('foo..bar')).toBe(false)
})
test("detects backslash traversal foo\\..\\bar", () => {
expect(containsPathTraversal("foo\\..\\bar")).toBe(true);
});
test('detects backslash traversal foo\\..\\bar', () => {
expect(containsPathTraversal('foo\\..\\bar')).toBe(true)
})
test("detects .. at end of absolute path", () => {
expect(containsPathTraversal("/path/to/..")).toBe(true);
});
});
test('detects .. at end of absolute path', () => {
expect(containsPathTraversal('/path/to/..')).toBe(true)
})
})
// ─── expandPath ─────────────────────────────────────────────────────────
describe("expandPath", () => {
test("expands ~/ to home directory", () => {
const result = expandPath("~/Documents");
expect(result).not.toContain("~");
expect(result).toContain("Documents");
});
describe('expandPath', () => {
test('expands ~/ to home directory', () => {
const result = expandPath('~/Documents')
expect(result).not.toContain('~')
expect(result).toContain('Documents')
})
test("expands bare ~ to home directory", () => {
const result = expandPath("~");
expect(result).not.toContain("~");
test('expands bare ~ to home directory', () => {
const result = expandPath('~')
expect(result).not.toContain('~')
// Should equal home directory
const { homedir } = require("os");
expect(result).toBe(homedir());
});
const { homedir } = require('os')
expect(result).toBe(homedir())
})
test("passes absolute paths through normalized", () => {
expect(expandPath("/usr/local/bin")).toBe("/usr/local/bin");
});
test('passes absolute paths through normalized', () => {
expect(expandPath('/usr/local/bin')).toBe('/usr/local/bin')
})
test("resolves relative path against baseDir", () => {
expect(expandPath("src", "/project")).toBe("/project/src");
});
test('resolves relative path against baseDir', () => {
expect(expandPath('src', '/project')).toBe('/project/src')
})
test("returns baseDir for empty string", () => {
expect(expandPath("", "/project")).toBe("/project");
});
test('returns baseDir for empty string', () => {
expect(expandPath('', '/project')).toBe('/project')
})
test("returns cwd-based path for empty string without baseDir", () => {
const result = expandPath("");
test('returns cwd-based path for empty string without baseDir', () => {
const result = expandPath('')
// Should be a valid absolute path (cwd normalized)
const { isAbsolute } = require("path");
expect(isAbsolute(result)).toBe(true);
});
});
const { isAbsolute } = require('path')
expect(isAbsolute(result)).toBe(true)
})
})
// ─── normalizePathForConfigKey ──────────────────────────────────────────
describe("normalizePathForConfigKey", () => {
test("normalizes forward slashes (no change on POSIX)", () => {
expect(normalizePathForConfigKey("foo/bar/baz")).toBe("foo/bar/baz");
});
describe('normalizePathForConfigKey', () => {
test('normalizes forward slashes (no change on POSIX)', () => {
expect(normalizePathForConfigKey('foo/bar/baz')).toBe('foo/bar/baz')
})
test("resolves dot segments", () => {
expect(normalizePathForConfigKey("foo/./bar")).toBe("foo/bar");
});
test('resolves dot segments', () => {
expect(normalizePathForConfigKey('foo/./bar')).toBe('foo/bar')
})
test("resolves double-dot segments", () => {
expect(normalizePathForConfigKey("foo/bar/../baz")).toBe("foo/baz");
});
test('resolves double-dot segments', () => {
expect(normalizePathForConfigKey('foo/bar/../baz')).toBe('foo/baz')
})
test("handles absolute path", () => {
const result = normalizePathForConfigKey("/Users/test/project");
expect(result).toBe("/Users/test/project");
});
test('handles absolute path', () => {
const result = normalizePathForConfigKey('/Users/test/project')
expect(result).toBe('/Users/test/project')
})
test("converts backslashes to forward slashes", () => {
const result = normalizePathForConfigKey("foo\\bar\\baz");
expect(result).toBe("foo/bar/baz");
});
test('converts backslashes to forward slashes', () => {
const result = normalizePathForConfigKey('foo\\bar\\baz')
expect(result).toBe('foo/bar/baz')
})
test("normalizes mixed separators foo/bar\\baz", () => {
const result = normalizePathForConfigKey("foo/bar\\baz");
expect(result).toBe("foo/bar/baz");
});
test('normalizes mixed separators foo/bar\\baz', () => {
const result = normalizePathForConfigKey('foo/bar\\baz')
expect(result).toBe('foo/bar/baz')
})
test("normalizes redundant separators foo//bar", () => {
const result = normalizePathForConfigKey("foo//bar");
expect(result).toBe("foo/bar");
});
});
test('normalizes redundant separators foo//bar', () => {
const result = normalizePathForConfigKey('foo//bar')
expect(result).toBe('foo/bar')
})
})
// ─── toRelativePath ─────────────────────────────────────────────────────
describe("toRelativePath", () => {
test("returns relative path for a child of cwd", () => {
describe('toRelativePath', () => {
test('returns relative path for a child of cwd', () => {
// Build a path that is inside the current working directory.
// resolve() returns an absolute path, and toRelativePath should give
// back just the final segment (or relative form without ..).
const abs = resolve(process.cwd(), "package.json");
const result = toRelativePath(abs);
expect(result).toBe("package.json");
expect(result).not.toContain("..");
});
const abs = resolve(process.cwd(), 'package.json')
const result = toRelativePath(abs)
expect(result).toBe('package.json')
expect(result).not.toContain('..')
})
test("returns absolute path when target is outside cwd", () => {
test('returns absolute path when target is outside cwd', () => {
// A well-known absolute path that is always outside any typical cwd
// (any absolute path that doesn't start with process.cwd() will work)
const cwd = process.cwd();
const cwd = process.cwd()
// Build a path guaranteed to be outside cwd by going to the root's parent
// of cwd, then a sibling directory with an unlikely name
const outsidePath = resolve(cwd, "../../__unlikely_dir_xyz__");
const result = toRelativePath(outsidePath);
const outsidePath = resolve(cwd, '../../__unlikely_dir_xyz__')
const result = toRelativePath(outsidePath)
// relative(cwd, outsidePath) will start with '../..' so function returns absolute
expect(result).toBe(outsidePath);
});
expect(result).toBe(outsidePath)
})
test("returns empty string for cwd itself", () => {
const cwd = process.cwd();
const result = toRelativePath(cwd);
test('returns empty string for cwd itself', () => {
const cwd = process.cwd()
const result = toRelativePath(cwd)
// relative(cwd, cwd) === '' which does not start with '..'
expect(result).toBe("");
});
expect(result).toBe('')
})
test("returns a string for any absolute path", () => {
const abs = resolve(process.cwd(), "src");
const result = toRelativePath(abs);
expect(typeof result).toBe("string");
});
});
test('returns a string for any absolute path', () => {
const abs = resolve(process.cwd(), 'src')
const result = toRelativePath(abs)
expect(typeof result).toBe('string')
})
})
// ─── getDirectoryForPath ─────────────────────────────────────────────────
describe("getDirectoryForPath", () => {
test("returns the path itself when given an existing directory", () => {
setOriginalFsImplementation();
const dir = resolve(tmpdir(), "ccb-existing-dir");
const baseFs = getFsImplementation();
describe('getDirectoryForPath', () => {
test('returns the path itself when given an existing directory', () => {
setOriginalFsImplementation()
const dir = resolve(tmpdir(), 'ccb-existing-dir')
const baseFs = getFsImplementation()
setFsImplementation({
...baseFs,
statSync: ((path: string) => {
if (path === dir) {
return { isDirectory: () => true } as any;
return { isDirectory: () => true } as any
}
return baseFs.statSync(path);
}) as FsOperations["statSync"],
});
return baseFs.statSync(path)
}) as FsOperations['statSync'],
})
try {
const result = getDirectoryForPath(dir);
expect(result).toBe(dir);
const result = getDirectoryForPath(dir)
expect(result).toBe(dir)
} finally {
setOriginalFsImplementation();
setOriginalFsImplementation()
}
});
})
test("returns parent directory for a known file", () => {
setOriginalFsImplementation();
const expectedParent = resolve(tmpdir(), "ccb-file-parent");
const file = resolve(expectedParent, "sample.txt");
const baseFs = getFsImplementation();
test('returns parent directory for a known file', () => {
setOriginalFsImplementation()
const expectedParent = resolve(tmpdir(), 'ccb-file-parent')
const file = resolve(expectedParent, 'sample.txt')
const baseFs = getFsImplementation()
setFsImplementation({
...baseFs,
statSync: ((path: string) => {
if (path === file) {
return { isDirectory: () => false } as any;
return { isDirectory: () => false } as any
}
return baseFs.statSync(path);
}) as FsOperations["statSync"],
});
return baseFs.statSync(path)
}) as FsOperations['statSync'],
})
try {
const result = getDirectoryForPath(file);
expect(result).toBe(expectedParent);
const result = getDirectoryForPath(file)
expect(result).toBe(expectedParent)
} finally {
setOriginalFsImplementation();
setOriginalFsImplementation()
}
});
})
test("returns parent directory for a non-existent path", () => {
setOriginalFsImplementation();
const expectedParent = resolve(tmpdir(), "ccb-missing-parent");
const nonExistent = resolve(expectedParent, "does-not-exist-xyz123.ts");
const baseFs = getFsImplementation();
test('returns parent directory for a non-existent path', () => {
setOriginalFsImplementation()
const expectedParent = resolve(tmpdir(), 'ccb-missing-parent')
const nonExistent = resolve(expectedParent, 'does-not-exist-xyz123.ts')
const baseFs = getFsImplementation()
setFsImplementation({
...baseFs,
statSync: ((path: string) => {
if (path === nonExistent) {
throw new Error("ENOENT");
throw new Error('ENOENT')
}
return baseFs.statSync(path);
}) as FsOperations["statSync"],
});
return baseFs.statSync(path)
}) as FsOperations['statSync'],
})
try {
const result = getDirectoryForPath(nonExistent);
expect(result).toBe(expectedParent);
const result = getDirectoryForPath(nonExistent)
expect(result).toBe(expectedParent)
} finally {
setOriginalFsImplementation();
setOriginalFsImplementation()
}
});
});
})
})

View File

@@ -1,110 +1,115 @@
import { afterEach, describe, expect, test } from "bun:test";
import { afterEach, describe, expect, test } from 'bun:test'
import {
getPrivacyLevel,
isEssentialTrafficOnly,
isTelemetryDisabled,
getEssentialTrafficOnlyReason,
} from "../privacyLevel";
} from '../privacyLevel'
describe("getPrivacyLevel", () => {
const originalDisableNonessential = process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
const originalDisableTelemetry = process.env.DISABLE_TELEMETRY;
describe('getPrivacyLevel', () => {
const originalDisableNonessential =
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC
const originalDisableTelemetry = process.env.DISABLE_TELEMETRY
afterEach(() => {
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
delete process.env.DISABLE_TELEMETRY;
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC
delete process.env.DISABLE_TELEMETRY
if (originalDisableNonessential !== undefined) {
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = originalDisableNonessential;
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC =
originalDisableNonessential
}
if (originalDisableTelemetry !== undefined) {
process.env.DISABLE_TELEMETRY = originalDisableTelemetry;
process.env.DISABLE_TELEMETRY = originalDisableTelemetry
}
});
})
test("returns 'default' when no env vars set", () => {
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
delete process.env.DISABLE_TELEMETRY;
expect(getPrivacyLevel()).toBe("default");
});
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC
delete process.env.DISABLE_TELEMETRY
expect(getPrivacyLevel()).toBe('default')
})
test("returns 'essential-traffic' when CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC is set", () => {
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
delete process.env.DISABLE_TELEMETRY;
expect(getPrivacyLevel()).toBe("essential-traffic");
});
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = '1'
delete process.env.DISABLE_TELEMETRY
expect(getPrivacyLevel()).toBe('essential-traffic')
})
test("returns 'no-telemetry' when DISABLE_TELEMETRY is set", () => {
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
process.env.DISABLE_TELEMETRY = "1";
expect(getPrivacyLevel()).toBe("no-telemetry");
});
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC
process.env.DISABLE_TELEMETRY = '1'
expect(getPrivacyLevel()).toBe('no-telemetry')
})
test("'essential-traffic' takes priority over 'no-telemetry'", () => {
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
process.env.DISABLE_TELEMETRY = "1";
expect(getPrivacyLevel()).toBe("essential-traffic");
});
});
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = '1'
process.env.DISABLE_TELEMETRY = '1'
expect(getPrivacyLevel()).toBe('essential-traffic')
})
})
describe("isEssentialTrafficOnly", () => {
const original = process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
describe('isEssentialTrafficOnly', () => {
const original = process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC
afterEach(() => {
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
if (original !== undefined) process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = original;
});
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC
if (original !== undefined)
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = original
})
test("returns true for 'essential-traffic' level", () => {
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
expect(isEssentialTrafficOnly()).toBe(true);
});
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = '1'
expect(isEssentialTrafficOnly()).toBe(true)
})
test("returns false for 'default' level", () => {
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
delete process.env.DISABLE_TELEMETRY;
expect(isEssentialTrafficOnly()).toBe(false);
});
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC
delete process.env.DISABLE_TELEMETRY
expect(isEssentialTrafficOnly()).toBe(false)
})
test("returns false for 'no-telemetry' level", () => {
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
process.env.DISABLE_TELEMETRY = "1";
expect(isEssentialTrafficOnly()).toBe(false);
});
});
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC
process.env.DISABLE_TELEMETRY = '1'
expect(isEssentialTrafficOnly()).toBe(false)
})
})
describe("isTelemetryDisabled", () => {
describe('isTelemetryDisabled', () => {
afterEach(() => {
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
delete process.env.DISABLE_TELEMETRY;
});
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC
delete process.env.DISABLE_TELEMETRY
})
test("returns true for 'no-telemetry' level", () => {
process.env.DISABLE_TELEMETRY = "1";
expect(isTelemetryDisabled()).toBe(true);
});
process.env.DISABLE_TELEMETRY = '1'
expect(isTelemetryDisabled()).toBe(true)
})
test("returns true for 'essential-traffic' level", () => {
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
expect(isTelemetryDisabled()).toBe(true);
});
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = '1'
expect(isTelemetryDisabled()).toBe(true)
})
test("returns false for 'default' level", () => {
expect(isTelemetryDisabled()).toBe(false);
});
});
expect(isTelemetryDisabled()).toBe(false)
})
})
describe("getEssentialTrafficOnlyReason", () => {
describe('getEssentialTrafficOnlyReason', () => {
afterEach(() => {
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
});
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC
})
test("returns env var name when restricted", () => {
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
expect(getEssentialTrafficOnlyReason()).toBe("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC");
});
test('returns env var name when restricted', () => {
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = '1'
expect(getEssentialTrafficOnlyReason()).toBe(
'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
)
})
test("returns null when unrestricted", () => {
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
expect(getEssentialTrafficOnlyReason()).toBeNull();
});
});
test('returns null when unrestricted', () => {
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC
expect(getEssentialTrafficOnlyReason()).toBeNull()
})
})

View File

@@ -1,162 +1,174 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import {
resetCommandQueue,
enqueue,
enqueuePendingNotification,
resetCommandQueue,
enqueue,
enqueuePendingNotification,
} from '../messageQueueManager.js'
import { hasQueuedCommands, processQueueIfReady } from '../queueProcessor.js'
beforeEach(() => {
resetCommandQueue()
resetCommandQueue()
})
afterEach(() => {
resetCommandQueue()
resetCommandQueue()
})
describe('processQueueIfReady', () => {
test('returns processed:false when queue empty', () => {
const result = processQueueIfReady({
executeInput: async () => {},
})
expect(result.processed).toBe(false)
})
test('returns processed:false when queue empty', () => {
const result = processQueueIfReady({
executeInput: async () => {},
})
expect(result.processed).toBe(false)
})
test('processes single slash command individually', () => {
const executed: string[][] = []
enqueue({ value: '/help', mode: 'prompt' } as any)
test('processes single slash command individually', () => {
const executed: string[][] = []
enqueue({ value: '/help', mode: 'prompt' } as any)
const result = processQueueIfReady({
executeInput: async cmds => {
executed.push(cmds.map(c => c.value as string))
},
})
const result = processQueueIfReady({
executeInput: async cmds => {
executed.push(cmds.map(c => c.value as string))
},
})
expect(result.processed).toBe(true)
expect(executed).toHaveLength(1)
expect(executed[0]).toEqual(['/help'])
})
expect(result.processed).toBe(true)
expect(executed).toHaveLength(1)
expect(executed[0]).toEqual(['/help'])
})
test('processes bash mode command individually', () => {
const executed: string[][] = []
enqueue({ value: 'git status', mode: 'bash' } as any)
test('processes bash mode command individually', () => {
const executed: string[][] = []
enqueue({ value: 'git status', mode: 'bash' } as any)
const result = processQueueIfReady({
executeInput: async cmds => {
executed.push(cmds.map(c => c.value as string))
},
})
const result = processQueueIfReady({
executeInput: async cmds => {
executed.push(cmds.map(c => c.value as string))
},
})
expect(result.processed).toBe(true)
expect(executed).toHaveLength(1)
expect(executed[0]).toEqual(['git status'])
})
expect(result.processed).toBe(true)
expect(executed).toHaveLength(1)
expect(executed[0]).toEqual(['git status'])
})
test('batches commands with same mode', () => {
const executed: string[][] = []
enqueuePendingNotification({ value: '<task1/>', mode: 'task-notification' } as any)
enqueuePendingNotification({ value: '<task2/>', mode: 'task-notification' } as any)
test('batches commands with same mode', () => {
const executed: string[][] = []
enqueuePendingNotification({
value: '<task1/>',
mode: 'task-notification',
} as any)
enqueuePendingNotification({
value: '<task2/>',
mode: 'task-notification',
} as any)
const result = processQueueIfReady({
executeInput: async cmds => {
executed.push(cmds.map(c => c.value as string))
},
})
const result = processQueueIfReady({
executeInput: async cmds => {
executed.push(cmds.map(c => c.value as string))
},
})
expect(result.processed).toBe(true)
expect(executed).toHaveLength(1)
expect(executed[0]).toEqual(['<task1/>', '<task2/>'])
})
expect(result.processed).toBe(true)
expect(executed).toHaveLength(1)
expect(executed[0]).toEqual(['<task1/>', '<task2/>'])
})
test('does not mix different modes in same batch', () => {
const executed: string[][] = []
enqueue({ value: 'hello', mode: 'prompt' } as any)
enqueuePendingNotification({ value: '<task/>', mode: 'task-notification' } as any)
test('does not mix different modes in same batch', () => {
const executed: string[][] = []
enqueue({ value: 'hello', mode: 'prompt' } as any)
enqueuePendingNotification({
value: '<task/>',
mode: 'task-notification',
} as any)
const result = processQueueIfReady({
executeInput: async cmds => {
executed.push(cmds.map(c => c.value as string))
},
})
const result = processQueueIfReady({
executeInput: async cmds => {
executed.push(cmds.map(c => c.value as string))
},
})
expect(result.processed).toBe(true)
// Only the 'prompt' mode command should be processed (higher priority than task-notification)
expect(executed).toHaveLength(1)
expect(executed[0]).toEqual(['hello'])
expect(result.processed).toBe(true)
// Only the 'prompt' mode command should be processed (higher priority than task-notification)
expect(executed).toHaveLength(1)
expect(executed[0]).toEqual(['hello'])
// The task-notification is still in queue
expect(hasQueuedCommands()).toBe(true)
})
// The task-notification is still in queue
expect(hasQueuedCommands()).toBe(true)
})
test('skips commands with agentId set (subagent notifications)', () => {
// This simulates the v2.1.119 fix: subagent task-notification with agentId
// should not be processed by the main thread queue processor
enqueuePendingNotification({
value: '<task-notification>subagent result</task-notification>',
mode: 'task-notification',
agentId: 'agent-123',
} as any)
test('skips commands with agentId set (subagent notifications)', () => {
// This simulates the v2.1.119 fix: subagent task-notification with agentId
// should not be processed by the main thread queue processor
enqueuePendingNotification({
value: '<task-notification>subagent result</task-notification>',
mode: 'task-notification',
agentId: 'agent-123',
} as any)
const result = processQueueIfReady({
executeInput: async () => {},
})
const result = processQueueIfReady({
executeInput: async () => {},
})
// Should not process — it's a subagent notification
expect(result.processed).toBe(false)
})
// Should not process — it's a subagent notification
expect(result.processed).toBe(false)
})
test('returns processed:false when only subagent commands in queue', () => {
enqueuePendingNotification({
value: '<task-notification/>',
mode: 'task-notification',
agentId: 'agent-456',
} as any)
enqueuePendingNotification({
value: '<task-notification/>',
mode: 'task-notification',
agentId: 'agent-789',
} as any)
test('returns processed:false when only subagent commands in queue', () => {
enqueuePendingNotification({
value: '<task-notification/>',
mode: 'task-notification',
agentId: 'agent-456',
} as any)
enqueuePendingNotification({
value: '<task-notification/>',
mode: 'task-notification',
agentId: 'agent-789',
} as any)
const result = processQueueIfReady({
executeInput: async () => {},
})
const result = processQueueIfReady({
executeInput: async () => {},
})
expect(result.processed).toBe(false)
expect(hasQueuedCommands()).toBe(true)
})
expect(result.processed).toBe(false)
expect(hasQueuedCommands()).toBe(true)
})
test('processes main-thread command but skips subagent command', () => {
const executed: string[][] = []
enqueuePendingNotification({ value: '<main-task/>', mode: 'task-notification' } as any)
enqueuePendingNotification({
value: '<sub-task/>',
mode: 'task-notification',
agentId: 'agent-123',
} as any)
test('processes main-thread command but skips subagent command', () => {
const executed: string[][] = []
enqueuePendingNotification({
value: '<main-task/>',
mode: 'task-notification',
} as any)
enqueuePendingNotification({
value: '<sub-task/>',
mode: 'task-notification',
agentId: 'agent-123',
} as any)
const result = processQueueIfReady({
executeInput: async cmds => {
executed.push(cmds.map(c => c.value as string))
},
})
const result = processQueueIfReady({
executeInput: async cmds => {
executed.push(cmds.map(c => c.value as string))
},
})
expect(result.processed).toBe(true)
expect(executed).toHaveLength(1)
expect(executed[0]).toEqual(['<main-task/>'])
expect(result.processed).toBe(true)
expect(executed).toHaveLength(1)
expect(executed[0]).toEqual(['<main-task/>'])
// Subagent command still in queue
expect(hasQueuedCommands()).toBe(true)
})
// Subagent command still in queue
expect(hasQueuedCommands()).toBe(true)
})
})
describe('hasQueuedCommands', () => {
test('returns false when queue empty', () => {
expect(hasQueuedCommands()).toBe(false)
})
test('returns false when queue empty', () => {
expect(hasQueuedCommands()).toBe(false)
})
test('returns true when commands in queue', () => {
enqueue({ value: 'hello', mode: 'prompt' } as any)
expect(hasQueuedCommands()).toBe(true)
})
test('returns true when commands in queue', () => {
enqueue({ value: 'hello', mode: 'prompt' } as any)
expect(hasQueuedCommands()).toBe(true)
})
})

View File

@@ -1,75 +1,75 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
import {
partiallySanitizeUnicode,
recursivelySanitizeUnicode,
} from "../sanitization";
} from '../sanitization'
// ─── partiallySanitizeUnicode ───────────────────────────────────────────
describe("partiallySanitizeUnicode", () => {
test("preserves normal ASCII text", () => {
expect(partiallySanitizeUnicode("hello world")).toBe("hello world");
});
describe('partiallySanitizeUnicode', () => {
test('preserves normal ASCII text', () => {
expect(partiallySanitizeUnicode('hello world')).toBe('hello world')
})
test("preserves CJK characters", () => {
expect(partiallySanitizeUnicode("你好世界")).toBe("你好世界");
});
test('preserves CJK characters', () => {
expect(partiallySanitizeUnicode('你好世界')).toBe('你好世界')
})
test("removes zero-width spaces", () => {
expect(partiallySanitizeUnicode("hello\u200Bworld")).toBe("helloworld");
});
test('removes zero-width spaces', () => {
expect(partiallySanitizeUnicode('hello\u200Bworld')).toBe('helloworld')
})
test("removes BOM", () => {
expect(partiallySanitizeUnicode("\uFEFFhello")).toBe("hello");
});
test('removes BOM', () => {
expect(partiallySanitizeUnicode('\uFEFFhello')).toBe('hello')
})
test("removes directional formatting", () => {
expect(partiallySanitizeUnicode("hello\u202Aworld")).toBe("helloworld");
});
test('removes directional formatting', () => {
expect(partiallySanitizeUnicode('hello\u202Aworld')).toBe('helloworld')
})
test("removes private use area characters", () => {
expect(partiallySanitizeUnicode("hello\uE000world")).toBe("helloworld");
});
test('removes private use area characters', () => {
expect(partiallySanitizeUnicode('hello\uE000world')).toBe('helloworld')
})
test("handles empty string", () => {
expect(partiallySanitizeUnicode("")).toBe("");
});
test('handles empty string', () => {
expect(partiallySanitizeUnicode('')).toBe('')
})
test("handles string with only dangerous characters", () => {
const result = partiallySanitizeUnicode("\u200B\u200C\u200D\uFEFF");
expect(result.length).toBeLessThanOrEqual(1); // ZWJ may survive NFKC
});
});
test('handles string with only dangerous characters', () => {
const result = partiallySanitizeUnicode('\u200B\u200C\u200D\uFEFF')
expect(result.length).toBeLessThanOrEqual(1) // ZWJ may survive NFKC
})
})
// ─── recursivelySanitizeUnicode ─────────────────────────────────────────
describe("recursivelySanitizeUnicode", () => {
test("sanitizes string values", () => {
expect(recursivelySanitizeUnicode("hello\u200Bworld")).toBe("helloworld");
});
describe('recursivelySanitizeUnicode', () => {
test('sanitizes string values', () => {
expect(recursivelySanitizeUnicode('hello\u200Bworld')).toBe('helloworld')
})
test("sanitizes array elements", () => {
const result = recursivelySanitizeUnicode(["a\u200Bb", "c\uFEFFd"]);
expect(result).toEqual(["ab", "cd"]);
});
test('sanitizes array elements', () => {
const result = recursivelySanitizeUnicode(['a\u200Bb', 'c\uFEFFd'])
expect(result).toEqual(['ab', 'cd'])
})
test("sanitizes object values recursively", () => {
test('sanitizes object values recursively', () => {
const result = recursivelySanitizeUnicode({
key: "val\u200Bue",
nested: { inner: "te\uFEFFst" },
});
expect(result).toEqual({ key: "value", nested: { inner: "test" } });
});
key: 'val\u200Bue',
nested: { inner: 'te\uFEFFst' },
})
expect(result).toEqual({ key: 'value', nested: { inner: 'test' } })
})
test("preserves numbers", () => {
expect(recursivelySanitizeUnicode(42)).toBe(42);
});
test('preserves numbers', () => {
expect(recursivelySanitizeUnicode(42)).toBe(42)
})
test("preserves booleans", () => {
expect(recursivelySanitizeUnicode(true)).toBe(true);
});
test('preserves booleans', () => {
expect(recursivelySanitizeUnicode(true)).toBe(true)
})
test("preserves null", () => {
expect(recursivelySanitizeUnicode(null)).toBeNull();
});
});
test('preserves null', () => {
expect(recursivelySanitizeUnicode(null)).toBeNull()
})
})

View File

@@ -1,48 +1,48 @@
import { describe, expect, test } from "bun:test";
import { z } from "zod/v4";
import { semanticBoolean } from "../semanticBoolean";
import { describe, expect, test } from 'bun:test'
import { z } from 'zod/v4'
import { semanticBoolean } from '../semanticBoolean'
describe("semanticBoolean", () => {
test("parses boolean true to true", () => {
expect(semanticBoolean().parse(true)).toBe(true);
});
describe('semanticBoolean', () => {
test('parses boolean true to true', () => {
expect(semanticBoolean().parse(true)).toBe(true)
})
test("parses boolean false to false", () => {
expect(semanticBoolean().parse(false)).toBe(false);
});
test('parses boolean false to false', () => {
expect(semanticBoolean().parse(false)).toBe(false)
})
test("parses string 'true' to true", () => {
expect(semanticBoolean().parse("true")).toBe(true);
});
expect(semanticBoolean().parse('true')).toBe(true)
})
test("parses string 'false' to false", () => {
expect(semanticBoolean().parse("false")).toBe(false);
});
expect(semanticBoolean().parse('false')).toBe(false)
})
test("rejects string 'TRUE' (case-sensitive)", () => {
expect(() => semanticBoolean().parse("TRUE")).toThrow();
});
expect(() => semanticBoolean().parse('TRUE')).toThrow()
})
test("rejects string 'FALSE' (case-sensitive)", () => {
expect(() => semanticBoolean().parse("FALSE")).toThrow();
});
expect(() => semanticBoolean().parse('FALSE')).toThrow()
})
test("rejects number 1", () => {
expect(() => semanticBoolean().parse(1)).toThrow();
});
test('rejects number 1', () => {
expect(() => semanticBoolean().parse(1)).toThrow()
})
test("rejects null", () => {
expect(() => semanticBoolean().parse(null)).toThrow();
});
test('rejects null', () => {
expect(() => semanticBoolean().parse(null)).toThrow()
})
test("rejects undefined", () => {
expect(() => semanticBoolean().parse(undefined)).toThrow();
});
test('rejects undefined', () => {
expect(() => semanticBoolean().parse(undefined)).toThrow()
})
test("works with custom inner schema (z.boolean().optional())", () => {
const schema = semanticBoolean(z.boolean().optional());
expect(schema.parse(true)).toBe(true);
expect(schema.parse("false")).toBe(false);
expect(schema.parse(undefined)).toBeUndefined();
});
});
test('works with custom inner schema (z.boolean().optional())', () => {
const schema = semanticBoolean(z.boolean().optional())
expect(schema.parse(true)).toBe(true)
expect(schema.parse('false')).toBe(false)
expect(schema.parse(undefined)).toBeUndefined()
})
})

View File

@@ -1,52 +1,52 @@
import { describe, expect, test } from "bun:test";
import { z } from "zod/v4";
import { semanticNumber } from "../semanticNumber";
import { describe, expect, test } from 'bun:test'
import { z } from 'zod/v4'
import { semanticNumber } from '../semanticNumber'
describe("semanticNumber", () => {
test("parses number 42", () => {
expect(semanticNumber().parse(42)).toBe(42);
});
describe('semanticNumber', () => {
test('parses number 42', () => {
expect(semanticNumber().parse(42)).toBe(42)
})
test("parses number 0", () => {
expect(semanticNumber().parse(0)).toBe(0);
});
test('parses number 0', () => {
expect(semanticNumber().parse(0)).toBe(0)
})
test("parses negative number -5", () => {
expect(semanticNumber().parse(-5)).toBe(-5);
});
test('parses negative number -5', () => {
expect(semanticNumber().parse(-5)).toBe(-5)
})
test("parses float 3.14", () => {
expect(semanticNumber().parse(3.14)).toBeCloseTo(3.14);
});
test('parses float 3.14', () => {
expect(semanticNumber().parse(3.14)).toBeCloseTo(3.14)
})
test("parses string '42' to 42", () => {
expect(semanticNumber().parse("42")).toBe(42);
});
expect(semanticNumber().parse('42')).toBe(42)
})
test("parses string '-7.5' to -7.5", () => {
expect(semanticNumber().parse("-7.5")).toBe(-7.5);
});
expect(semanticNumber().parse('-7.5')).toBe(-7.5)
})
test("rejects string 'abc'", () => {
expect(() => semanticNumber().parse("abc")).toThrow();
});
expect(() => semanticNumber().parse('abc')).toThrow()
})
test("rejects empty string ''", () => {
expect(() => semanticNumber().parse("")).toThrow();
});
expect(() => semanticNumber().parse('')).toThrow()
})
test("rejects null", () => {
expect(() => semanticNumber().parse(null)).toThrow();
});
test('rejects null', () => {
expect(() => semanticNumber().parse(null)).toThrow()
})
test("rejects boolean true", () => {
expect(() => semanticNumber().parse(true)).toThrow();
});
test('rejects boolean true', () => {
expect(() => semanticNumber().parse(true)).toThrow()
})
test("works with custom inner schema (z.number().int().min(0))", () => {
const schema = semanticNumber(z.number().int().min(0));
expect(schema.parse(5)).toBe(5);
expect(schema.parse("10")).toBe(10);
expect(() => schema.parse(-1)).toThrow();
});
});
test('works with custom inner schema (z.number().int().min(0))', () => {
const schema = semanticNumber(z.number().int().min(0))
expect(schema.parse(5)).toBe(5)
expect(schema.parse('10')).toBe(10)
expect(() => schema.parse(-1)).toThrow()
})
})

View File

@@ -1,114 +1,114 @@
import { describe, expect, test } from "bun:test";
import { gt, gte, lt, lte, satisfies, order } from "../semver";
import { describe, expect, test } from 'bun:test'
import { gt, gte, lt, lte, satisfies, order } from '../semver'
describe("gt", () => {
test("returns true when a > b", () => {
expect(gt("2.0.0", "1.0.0")).toBe(true);
});
describe('gt', () => {
test('returns true when a > b', () => {
expect(gt('2.0.0', '1.0.0')).toBe(true)
})
test("returns false when a < b", () => {
expect(gt("1.0.0", "2.0.0")).toBe(false);
});
test('returns false when a < b', () => {
expect(gt('1.0.0', '2.0.0')).toBe(false)
})
test("returns false when equal", () => {
expect(gt("1.0.0", "1.0.0")).toBe(false);
});
test('returns false when equal', () => {
expect(gt('1.0.0', '1.0.0')).toBe(false)
})
test("returns false for 0.0.0 vs 0.0.0", () => {
expect(gt("0.0.0", "0.0.0")).toBe(false);
});
test('returns false for 0.0.0 vs 0.0.0', () => {
expect(gt('0.0.0', '0.0.0')).toBe(false)
})
test("release is greater than pre-release", () => {
expect(gt("1.0.0", "1.0.0-alpha")).toBe(true);
});
});
test('release is greater than pre-release', () => {
expect(gt('1.0.0', '1.0.0-alpha')).toBe(true)
})
})
describe("gte", () => {
test("returns true when a > b", () => {
expect(gte("2.0.0", "1.0.0")).toBe(true);
});
describe('gte', () => {
test('returns true when a > b', () => {
expect(gte('2.0.0', '1.0.0')).toBe(true)
})
test("returns true when equal", () => {
expect(gte("1.0.0", "1.0.0")).toBe(true);
});
test('returns true when equal', () => {
expect(gte('1.0.0', '1.0.0')).toBe(true)
})
test("returns false when a < b", () => {
expect(gte("1.0.0", "2.0.0")).toBe(false);
});
});
test('returns false when a < b', () => {
expect(gte('1.0.0', '2.0.0')).toBe(false)
})
})
describe("lt", () => {
test("returns true when a < b", () => {
expect(lt("1.0.0", "2.0.0")).toBe(true);
});
describe('lt', () => {
test('returns true when a < b', () => {
expect(lt('1.0.0', '2.0.0')).toBe(true)
})
test("returns false when a > b", () => {
expect(lt("2.0.0", "1.0.0")).toBe(false);
});
test('returns false when a > b', () => {
expect(lt('2.0.0', '1.0.0')).toBe(false)
})
test("returns false when equal", () => {
expect(lt("1.0.0", "1.0.0")).toBe(false);
});
});
test('returns false when equal', () => {
expect(lt('1.0.0', '1.0.0')).toBe(false)
})
})
describe("lte", () => {
test("returns true when a < b", () => {
expect(lte("1.0.0", "2.0.0")).toBe(true);
});
describe('lte', () => {
test('returns true when a < b', () => {
expect(lte('1.0.0', '2.0.0')).toBe(true)
})
test("returns true when equal", () => {
expect(lte("1.0.0", "1.0.0")).toBe(true);
});
test('returns true when equal', () => {
expect(lte('1.0.0', '1.0.0')).toBe(true)
})
test("returns false when a > b", () => {
expect(lte("2.0.0", "1.0.0")).toBe(false);
});
});
test('returns false when a > b', () => {
expect(lte('2.0.0', '1.0.0')).toBe(false)
})
})
describe("satisfies", () => {
test("matches exact version", () => {
expect(satisfies("1.2.3", "1.2.3")).toBe(true);
});
describe('satisfies', () => {
test('matches exact version', () => {
expect(satisfies('1.2.3', '1.2.3')).toBe(true)
})
test("matches range", () => {
expect(satisfies("1.2.3", ">=1.0.0")).toBe(true);
});
test('matches range', () => {
expect(satisfies('1.2.3', '>=1.0.0')).toBe(true)
})
test("does not match out-of-range version", () => {
expect(satisfies("0.9.0", ">=1.0.0")).toBe(false);
});
test('does not match out-of-range version', () => {
expect(satisfies('0.9.0', '>=1.0.0')).toBe(false)
})
test("matches caret range", () => {
expect(satisfies("1.2.3", "^1.0.0")).toBe(true);
});
test('matches caret range', () => {
expect(satisfies('1.2.3', '^1.0.0')).toBe(true)
})
test("does not match major bump in caret", () => {
expect(satisfies("2.0.0", "^1.0.0")).toBe(false);
});
test('does not match major bump in caret', () => {
expect(satisfies('2.0.0', '^1.0.0')).toBe(false)
})
test("matches tilde range", () => {
expect(satisfies("1.2.5", "~1.2.3")).toBe(true);
});
test('matches tilde range', () => {
expect(satisfies('1.2.5', '~1.2.3')).toBe(true)
})
test("matches wildcard range", () => {
expect(satisfies("2.0.0", "*")).toBe(true);
});
});
test('matches wildcard range', () => {
expect(satisfies('2.0.0', '*')).toBe(true)
})
})
describe("order", () => {
test("returns 1 when a > b", () => {
expect(order("2.0.0", "1.0.0")).toBe(1);
});
describe('order', () => {
test('returns 1 when a > b', () => {
expect(order('2.0.0', '1.0.0')).toBe(1)
})
test("returns -1 when a < b", () => {
expect(order("1.0.0", "2.0.0")).toBe(-1);
});
test('returns -1 when a < b', () => {
expect(order('1.0.0', '2.0.0')).toBe(-1)
})
test("returns 0 when equal", () => {
expect(order("1.0.0", "1.0.0")).toBe(0);
});
test('returns 0 when equal', () => {
expect(order('1.0.0', '1.0.0')).toBe(0)
})
test("compares patch versions", () => {
expect(order("1.0.1", "1.0.0")).toBe(1);
});
});
test('compares patch versions', () => {
expect(order('1.0.1', '1.0.0')).toBe(1)
})
})

View File

@@ -1,99 +1,99 @@
import { describe, expect, test } from "bun:test";
import { sequential } from "../sequential";
import { describe, expect, test } from 'bun:test'
import { sequential } from '../sequential'
describe("sequential", () => {
test("wraps async function, returns same result", async () => {
const fn = sequential(async (x: number) => x * 2);
expect(await fn(5)).toBe(10);
});
describe('sequential', () => {
test('wraps async function, returns same result', async () => {
const fn = sequential(async (x: number) => x * 2)
expect(await fn(5)).toBe(10)
})
test("single call resolves normally", async () => {
const fn = sequential(async () => "ok");
expect(await fn()).toBe("ok");
});
test('single call resolves normally', async () => {
const fn = sequential(async () => 'ok')
expect(await fn()).toBe('ok')
})
test("concurrent calls execute sequentially (FIFO order)", async () => {
const order: number[] = [];
test('concurrent calls execute sequentially (FIFO order)', async () => {
const order: number[] = []
const fn = sequential(async (n: number) => {
order.push(n);
await new Promise(r => setTimeout(r, 10));
return n;
});
order.push(n)
await new Promise(r => setTimeout(r, 10))
return n
})
const results = await Promise.all([fn(1), fn(2), fn(3)]);
expect(results).toEqual([1, 2, 3]);
expect(order).toEqual([1, 2, 3]);
});
const results = await Promise.all([fn(1), fn(2), fn(3)])
expect(results).toEqual([1, 2, 3])
expect(order).toEqual([1, 2, 3])
})
test("preserves arguments correctly", async () => {
const fn = sequential(async (a: number, b: string) => `${a}-${b}`);
expect(await fn(42, "test")).toBe("42-test");
});
test('preserves arguments correctly', async () => {
const fn = sequential(async (a: number, b: string) => `${a}-${b}`)
expect(await fn(42, 'test')).toBe('42-test')
})
test("error in first call does not block subsequent calls", async () => {
let callCount = 0;
test('error in first call does not block subsequent calls', async () => {
let callCount = 0
const fn = sequential(async () => {
callCount++;
if (callCount === 1) throw new Error("first fail");
return "ok";
});
callCount++
if (callCount === 1) throw new Error('first fail')
return 'ok'
})
await expect(fn()).rejects.toThrow("first fail");
expect(await fn()).toBe("ok");
});
await expect(fn()).rejects.toThrow('first fail')
expect(await fn()).toBe('ok')
})
test("preserves rejection reason", async () => {
test('preserves rejection reason', async () => {
const fn = sequential(async () => {
throw new Error("specific error");
});
await expect(fn()).rejects.toThrow("specific error");
});
throw new Error('specific error')
})
await expect(fn()).rejects.toThrow('specific error')
})
test("multiple args passed correctly", async () => {
const fn = sequential(async (a: number, b: number, c: number) => a + b + c);
expect(await fn(1, 2, 3)).toBe(6);
});
test('multiple args passed correctly', async () => {
const fn = sequential(async (a: number, b: number, c: number) => a + b + c)
expect(await fn(1, 2, 3)).toBe(6)
})
test("returns different wrapper for each call to sequential", () => {
const fn1 = sequential(async () => 1);
const fn2 = sequential(async () => 2);
expect(fn1).not.toBe(fn2);
});
test('returns different wrapper for each call to sequential', () => {
const fn1 = sequential(async () => 1)
const fn2 = sequential(async () => 2)
expect(fn1).not.toBe(fn2)
})
test("handles rapid concurrent calls", async () => {
const order: number[] = [];
test('handles rapid concurrent calls', async () => {
const order: number[] = []
const fn = sequential(async (n: number) => {
order.push(n);
return n;
});
order.push(n)
return n
})
const promises = Array.from({ length: 10 }, (_, i) => fn(i));
const results = await Promise.all(promises);
expect(results).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
expect(order).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
});
const promises = Array.from({ length: 10 }, (_, i) => fn(i))
const results = await Promise.all(promises)
expect(results).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
expect(order).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
})
test("execution order matches call order", async () => {
const log: string[] = [];
test('execution order matches call order', async () => {
const log: string[] = []
const fn = sequential(async (label: string) => {
log.push(`start:${label}`);
await new Promise(r => setTimeout(r, 5));
log.push(`end:${label}`);
return label;
});
log.push(`start:${label}`)
await new Promise(r => setTimeout(r, 5))
log.push(`end:${label}`)
return label
})
await Promise.all([fn("a"), fn("b")]);
expect(log[0]).toBe("start:a");
expect(log[1]).toBe("end:a");
expect(log[2]).toBe("start:b");
expect(log[3]).toBe("end:b");
});
await Promise.all([fn('a'), fn('b')])
expect(log[0]).toBe('start:a')
expect(log[1]).toBe('end:a')
expect(log[2]).toBe('start:b')
expect(log[3]).toBe('end:b')
})
test("works with functions returning different types", async () => {
test('works with functions returning different types', async () => {
const fn = sequential(async (x: number): Promise<string | number> => {
return x > 0 ? "positive" : x;
});
expect(await fn(5)).toBe("positive");
expect(await fn(-1)).toBe(-1);
});
});
return x > 0 ? 'positive' : x
})
expect(await fn(5)).toBe('positive')
expect(await fn(-1)).toBe(-1)
})
})

View File

@@ -1,63 +1,63 @@
import { describe, expect, test } from "bun:test";
import { difference, every, intersects, union } from "../set";
import { describe, expect, test } from 'bun:test'
import { difference, every, intersects, union } from '../set'
describe("difference", () => {
test("returns elements in a but not in b", () => {
const result = difference(new Set([1, 2, 3]), new Set([2, 3, 4]));
expect(result).toEqual(new Set([1]));
});
describe('difference', () => {
test('returns elements in a but not in b', () => {
const result = difference(new Set([1, 2, 3]), new Set([2, 3, 4]))
expect(result).toEqual(new Set([1]))
})
test("returns empty set when a is subset of b", () => {
expect(difference(new Set([1, 2]), new Set([1, 2, 3]))).toEqual(new Set());
});
test('returns empty set when a is subset of b', () => {
expect(difference(new Set([1, 2]), new Set([1, 2, 3]))).toEqual(new Set())
})
test("returns a when b is empty", () => {
expect(difference(new Set([1, 2]), new Set())).toEqual(new Set([1, 2]));
});
});
test('returns a when b is empty', () => {
expect(difference(new Set([1, 2]), new Set())).toEqual(new Set([1, 2]))
})
})
describe("intersects", () => {
test("returns true when sets share elements", () => {
expect(intersects(new Set([1, 2]), new Set([2, 3]))).toBe(true);
});
describe('intersects', () => {
test('returns true when sets share elements', () => {
expect(intersects(new Set([1, 2]), new Set([2, 3]))).toBe(true)
})
test("returns false when sets are disjoint", () => {
expect(intersects(new Set([1, 2]), new Set([3, 4]))).toBe(false);
});
test('returns false when sets are disjoint', () => {
expect(intersects(new Set([1, 2]), new Set([3, 4]))).toBe(false)
})
test("returns false for empty sets", () => {
expect(intersects(new Set(), new Set([1]))).toBe(false);
expect(intersects(new Set([1]), new Set())).toBe(false);
});
});
test('returns false for empty sets', () => {
expect(intersects(new Set(), new Set([1]))).toBe(false)
expect(intersects(new Set([1]), new Set())).toBe(false)
})
})
describe("every", () => {
test("returns true when a is subset of b", () => {
expect(every(new Set([1, 2]), new Set([1, 2, 3]))).toBe(true);
});
describe('every', () => {
test('returns true when a is subset of b', () => {
expect(every(new Set([1, 2]), new Set([1, 2, 3]))).toBe(true)
})
test("returns false when a has elements not in b", () => {
expect(every(new Set([1, 4]), new Set([1, 2, 3]))).toBe(false);
});
test('returns false when a has elements not in b', () => {
expect(every(new Set([1, 4]), new Set([1, 2, 3]))).toBe(false)
})
test("returns true for empty a", () => {
expect(every(new Set(), new Set([1, 2]))).toBe(true);
});
});
test('returns true for empty a', () => {
expect(every(new Set(), new Set([1, 2]))).toBe(true)
})
})
describe("union", () => {
test("combines both sets", () => {
const result = union(new Set([1, 2]), new Set([3, 4]));
expect(result).toEqual(new Set([1, 2, 3, 4]));
});
describe('union', () => {
test('combines both sets', () => {
const result = union(new Set([1, 2]), new Set([3, 4]))
expect(result).toEqual(new Set([1, 2, 3, 4]))
})
test("deduplicates shared elements", () => {
const result = union(new Set([1, 2]), new Set([2, 3]));
expect(result).toEqual(new Set([1, 2, 3]));
});
test('deduplicates shared elements', () => {
const result = union(new Set([1, 2]), new Set([2, 3]))
expect(result).toEqual(new Set([1, 2, 3]))
})
test("handles empty sets", () => {
expect(union(new Set(), new Set([1]))).toEqual(new Set([1]));
expect(union(new Set([1]), new Set())).toEqual(new Set([1]));
});
});
test('handles empty sets', () => {
expect(union(new Set(), new Set([1]))).toEqual(new Set([1]))
expect(union(new Set([1]), new Set())).toEqual(new Set([1]))
})
})

View File

@@ -1,58 +1,58 @@
import { describe, expect, test } from "bun:test";
import { parseSlashCommand } from "../slashCommandParsing";
import { describe, expect, test } from 'bun:test'
import { parseSlashCommand } from '../slashCommandParsing'
describe("parseSlashCommand", () => {
test("parses simple command", () => {
const result = parseSlashCommand("/search foo bar");
describe('parseSlashCommand', () => {
test('parses simple command', () => {
const result = parseSlashCommand('/search foo bar')
expect(result).toEqual({
commandName: "search",
args: "foo bar",
commandName: 'search',
args: 'foo bar',
isMcp: false,
});
});
})
})
test("parses command without args", () => {
const result = parseSlashCommand("/help");
test('parses command without args', () => {
const result = parseSlashCommand('/help')
expect(result).toEqual({
commandName: "help",
args: "",
commandName: 'help',
args: '',
isMcp: false,
});
});
})
})
test("parses MCP command", () => {
const result = parseSlashCommand("/tool (MCP) arg1 arg2");
test('parses MCP command', () => {
const result = parseSlashCommand('/tool (MCP) arg1 arg2')
expect(result).toEqual({
commandName: "tool (MCP)",
args: "arg1 arg2",
commandName: 'tool (MCP)',
args: 'arg1 arg2',
isMcp: true,
});
});
})
})
test("parses MCP command without args", () => {
const result = parseSlashCommand("/tool (MCP)");
test('parses MCP command without args', () => {
const result = parseSlashCommand('/tool (MCP)')
expect(result).toEqual({
commandName: "tool (MCP)",
args: "",
commandName: 'tool (MCP)',
args: '',
isMcp: true,
});
});
})
})
test("returns null for non-slash input", () => {
expect(parseSlashCommand("hello")).toBeNull();
});
test('returns null for non-slash input', () => {
expect(parseSlashCommand('hello')).toBeNull()
})
test("returns null for empty string", () => {
expect(parseSlashCommand("")).toBeNull();
});
test('returns null for empty string', () => {
expect(parseSlashCommand('')).toBeNull()
})
test("returns null for just slash", () => {
expect(parseSlashCommand("/")).toBeNull();
});
test('returns null for just slash', () => {
expect(parseSlashCommand('/')).toBeNull()
})
test("trims whitespace before parsing", () => {
const result = parseSlashCommand(" /search foo ");
expect(result!.commandName).toBe("search");
expect(result!.args).toBe("foo");
});
});
test('trims whitespace before parsing', () => {
const result = parseSlashCommand(' /search foo ')
expect(result!.commandName).toBe('search')
expect(result!.args).toBe('foo')
})
})

View File

@@ -1,130 +1,126 @@
import { describe, expect, test } from "bun:test";
import { sleep, withTimeout } from "../sleep";
import { sequential } from "../sequential";
import { describe, expect, test } from 'bun:test'
import { sleep, withTimeout } from '../sleep'
import { sequential } from '../sequential'
// ─── sleep ─────────────────────────────────────────────────────────────
describe("sleep", () => {
test("resolves after timeout", async () => {
const start = Date.now();
await sleep(50);
expect(Date.now() - start).toBeGreaterThanOrEqual(40);
});
describe('sleep', () => {
test('resolves after timeout', async () => {
const start = Date.now()
await sleep(50)
expect(Date.now() - start).toBeGreaterThanOrEqual(40)
})
test("resolves immediately when signal already aborted", async () => {
const ac = new AbortController();
ac.abort();
const start = Date.now();
await sleep(10_000, ac.signal);
expect(Date.now() - start).toBeLessThan(50);
});
test('resolves immediately when signal already aborted', async () => {
const ac = new AbortController()
ac.abort()
const start = Date.now()
await sleep(10_000, ac.signal)
expect(Date.now() - start).toBeLessThan(50)
})
test("resolves early on abort (default: no throw)", async () => {
const ac = new AbortController();
const start = Date.now();
const p = sleep(10_000, ac.signal);
setTimeout(() => ac.abort(), 30);
await p;
expect(Date.now() - start).toBeLessThan(200);
});
test('resolves early on abort (default: no throw)', async () => {
const ac = new AbortController()
const start = Date.now()
const p = sleep(10_000, ac.signal)
setTimeout(() => ac.abort(), 30)
await p
expect(Date.now() - start).toBeLessThan(200)
})
test("rejects on abort with throwOnAbort", async () => {
const ac = new AbortController();
ac.abort();
test('rejects on abort with throwOnAbort', async () => {
const ac = new AbortController()
ac.abort()
await expect(
sleep(10_000, ac.signal, { throwOnAbort: true })
).rejects.toThrow("aborted");
});
sleep(10_000, ac.signal, { throwOnAbort: true }),
).rejects.toThrow('aborted')
})
test("rejects with custom abortError", async () => {
const ac = new AbortController();
ac.abort();
const customErr = () => new Error("custom abort");
test('rejects with custom abortError', async () => {
const ac = new AbortController()
ac.abort()
const customErr = () => new Error('custom abort')
await expect(
sleep(10_000, ac.signal, { abortError: customErr })
).rejects.toThrow("custom abort");
});
sleep(10_000, ac.signal, { abortError: customErr }),
).rejects.toThrow('custom abort')
})
test("throwOnAbort rejects on mid-sleep abort", async () => {
const ac = new AbortController();
const p = sleep(10_000, ac.signal, { throwOnAbort: true });
setTimeout(() => ac.abort(), 20);
await expect(p).rejects.toThrow("aborted");
});
test('throwOnAbort rejects on mid-sleep abort', async () => {
const ac = new AbortController()
const p = sleep(10_000, ac.signal, { throwOnAbort: true })
setTimeout(() => ac.abort(), 20)
await expect(p).rejects.toThrow('aborted')
})
test("works without signal", async () => {
await sleep(10);
test('works without signal', async () => {
await sleep(10)
// just verify it resolves
});
});
})
})
// ─── withTimeout ───────────────────────────────────────────────────────
describe("withTimeout", () => {
test("resolves when promise completes before timeout", async () => {
const result = await withTimeout(
Promise.resolve(42),
1000,
"timed out"
);
expect(result).toBe(42);
});
describe('withTimeout', () => {
test('resolves when promise completes before timeout', async () => {
const result = await withTimeout(Promise.resolve(42), 1000, 'timed out')
expect(result).toBe(42)
})
test("rejects when promise takes too long", async () => {
const slow = new Promise((resolve) => setTimeout(resolve, 5000));
await expect(
withTimeout(slow, 50, "operation timed out")
).rejects.toThrow("operation timed out");
});
test('rejects when promise takes too long', async () => {
const slow = new Promise(resolve => setTimeout(resolve, 5000))
await expect(withTimeout(slow, 50, 'operation timed out')).rejects.toThrow(
'operation timed out',
)
})
test("rejects propagate through", async () => {
test('rejects propagate through', async () => {
await expect(
withTimeout(Promise.reject(new Error("inner")), 1000, "timeout")
).rejects.toThrow("inner");
});
});
withTimeout(Promise.reject(new Error('inner')), 1000, 'timeout'),
).rejects.toThrow('inner')
})
})
// ─── sequential ────────────────────────────────────────────────────────
describe("sequential", () => {
test("executes calls in order", async () => {
const order: number[] = [];
describe('sequential', () => {
test('executes calls in order', async () => {
const order: number[] = []
const fn = sequential(async (n: number) => {
await sleep(10);
order.push(n);
return n;
});
await sleep(10)
order.push(n)
return n
})
const results = await Promise.all([fn(1), fn(2), fn(3)]);
expect(order).toEqual([1, 2, 3]);
expect(results).toEqual([1, 2, 3]);
});
const results = await Promise.all([fn(1), fn(2), fn(3)])
expect(order).toEqual([1, 2, 3])
expect(results).toEqual([1, 2, 3])
})
test("returns correct result for each call", async () => {
const fn = sequential(async (x: number) => x * 2);
const r1 = await fn(5);
const r2 = await fn(10);
expect(r1).toBe(10);
expect(r2).toBe(20);
});
test('returns correct result for each call', async () => {
const fn = sequential(async (x: number) => x * 2)
const r1 = await fn(5)
const r2 = await fn(10)
expect(r1).toBe(10)
expect(r2).toBe(20)
})
test("propagates errors without blocking queue", async () => {
test('propagates errors without blocking queue', async () => {
const fn = sequential(async (x: number) => {
if (x === 2) throw new Error("fail");
return x;
});
if (x === 2) throw new Error('fail')
return x
})
const p1 = fn(1);
const p2 = fn(2);
const p3 = fn(3);
const p1 = fn(1)
const p2 = fn(2)
const p3 = fn(3)
expect(await p1).toBe(1);
await expect(p2).rejects.toThrow("fail");
expect(await p3).toBe(3);
});
expect(await p1).toBe(1)
await expect(p2).rejects.toThrow('fail')
expect(await p3).toBe(3)
})
test("handles single call", async () => {
const fn = sequential(async (s: string) => s.toUpperCase());
expect(await fn("hello")).toBe("HELLO");
});
});
test('handles single call', async () => {
const fn = sequential(async (s: string) => s.toUpperCase())
expect(await fn('hello')).toBe('HELLO')
})
})

View File

@@ -1,86 +1,86 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
const sliceAnsi = (await import("../sliceAnsi")).default;
const ESC = "\x1b";
const sliceAnsi = (await import('../sliceAnsi')).default
const ESC = '\x1b'
describe("sliceAnsi", () => {
test("plain text slice identical to String.slice", () => {
expect(sliceAnsi("hello world", 0, 5)).toBe("hello");
expect(sliceAnsi("hello world", 6)).toBe("world");
});
describe('sliceAnsi', () => {
test('plain text slice identical to String.slice', () => {
expect(sliceAnsi('hello world', 0, 5)).toBe('hello')
expect(sliceAnsi('hello world', 6)).toBe('world')
})
test("slice entire string", () => {
expect(sliceAnsi("abc", 0)).toBe("abc");
});
test('slice entire string', () => {
expect(sliceAnsi('abc', 0)).toBe('abc')
})
test("empty slice (start === end)", () => {
expect(sliceAnsi("abc", 2, 2)).toBe("");
});
test('empty slice (start === end)', () => {
expect(sliceAnsi('abc', 2, 2)).toBe('')
})
test("preserves ANSI color codes within slice", () => {
const input = "\x1b[31mred\x1b[0m normal";
const result = sliceAnsi(input, 0, 3);
expect(result).toContain("\x1b[31m");
expect(result).toContain("red");
});
test('preserves ANSI color codes within slice', () => {
const input = '\x1b[31mred\x1b[0m normal'
const result = sliceAnsi(input, 0, 3)
expect(result).toContain('\x1b[31m')
expect(result).toContain('red')
})
test("closes opened ANSI styles at slice end", () => {
const input = "\x1b[31mhello world\x1b[0m";
const result = sliceAnsi(input, 0, 5);
expect(result).toContain("\x1b[31m");
expect(result).toContain("hello");
test('closes opened ANSI styles at slice end', () => {
const input = '\x1b[31mhello world\x1b[0m'
const result = sliceAnsi(input, 0, 5)
expect(result).toContain('\x1b[31m')
expect(result).toContain('hello')
// undoAnsiCodes uses specific close codes (e.g. \x1b[39m for foreground)
expect(result).toMatch(new RegExp(`${ESC}\\[\\d+m`));
expect(result).toMatch(new RegExp(`${ESC}\\[\\d+m`))
// The result should start with open code and end with a close code
const withoutText = result.replace("hello", "");
const withoutText = result.replace('hello', '')
// Should have at least one open and one close code
expect(withoutText.length).toBeGreaterThan(0);
});
expect(withoutText.length).toBeGreaterThan(0)
})
test("slice starting mid-ANSI skips codes before start", () => {
const input = "\x1b[31mhello\x1b[0m \x1b[32mworld\x1b[0m";
const result = sliceAnsi(input, 6, 11);
expect(result).toContain("world");
expect(result).toContain("\x1b[32m");
expect(result).not.toContain("\x1b[31m");
});
test('slice starting mid-ANSI skips codes before start', () => {
const input = '\x1b[31mhello\x1b[0m \x1b[32mworld\x1b[0m'
const result = sliceAnsi(input, 6, 11)
expect(result).toContain('world')
expect(result).toContain('\x1b[32m')
expect(result).not.toContain('\x1b[31m')
})
test("slice of plain text from middle", () => {
expect(sliceAnsi("abcdefgh", 2, 5)).toBe("cde");
});
test('slice of plain text from middle', () => {
expect(sliceAnsi('abcdefgh', 2, 5)).toBe('cde')
})
test("slice past end of string returns everything", () => {
expect(sliceAnsi("abc", 0, 100)).toBe("abc");
});
test('slice past end of string returns everything', () => {
expect(sliceAnsi('abc', 0, 100)).toBe('abc')
})
test("slice starting at end returns empty", () => {
expect(sliceAnsi("abc", 3)).toBe("");
});
test('slice starting at end returns empty', () => {
expect(sliceAnsi('abc', 3)).toBe('')
})
test("handles empty string", () => {
expect(sliceAnsi("", 0, 5)).toBe("");
});
test('handles empty string', () => {
expect(sliceAnsi('', 0, 5)).toBe('')
})
test("multiple ANSI codes nested", () => {
const input = "\x1b[1m\x1b[31mbold red\x1b[0m\x1b[0m";
const result = sliceAnsi(input, 0, 4);
expect(result).toContain("bold");
test('multiple ANSI codes nested', () => {
const input = '\x1b[1m\x1b[31mbold red\x1b[0m\x1b[0m'
const result = sliceAnsi(input, 0, 4)
expect(result).toContain('bold')
// Both styles should be opened and then closed
expect(result).toContain("\x1b[1m");
expect(result).toContain("\x1b[31m");
});
expect(result).toContain('\x1b[1m')
expect(result).toContain('\x1b[31m')
})
test("slice with no end parameter returns to end of string", () => {
expect(sliceAnsi("hello world", 6)).toBe("world");
});
test('slice with no end parameter returns to end of string', () => {
expect(sliceAnsi('hello world', 6)).toBe('world')
})
test("ANSI codes at boundaries are handled correctly", () => {
const input = "a\x1b[31mb\x1b[0mc";
test('ANSI codes at boundaries are handled correctly', () => {
const input = 'a\x1b[31mb\x1b[0mc'
// "abc" visually, position: a=0, b=1, c=2
const result = sliceAnsi(input, 1, 2);
const result = sliceAnsi(input, 1, 2)
// undoAnsiCodes uses \x1b[39m for foreground reset, not \x1b[0m
expect(result).toContain("b");
expect(result).toContain("\x1b[31m");
expect(result).toMatch(new RegExp(`${ESC}\\[\\d+m.*${ESC}\\[\\d+m`)); // open + close codes
});
});
expect(result).toContain('b')
expect(result).toContain('\x1b[31m')
expect(result).toMatch(new RegExp(`${ESC}\\[\\d+m.*${ESC}\\[\\d+m`)) // open + close codes
})
})

View File

@@ -1,162 +1,164 @@
import { describe, expect, test } from "bun:test";
import { Stream } from "../stream";
import { describe, expect, test } from 'bun:test'
import { Stream } from '../stream'
describe("Stream", () => {
test("enqueue then read resolves with the value", async () => {
const stream = new Stream<number>();
stream[Symbol.asyncIterator]();
stream.enqueue(42);
const result = await stream.next();
expect(result).toEqual({ done: false, value: 42 });
});
describe('Stream', () => {
test('enqueue then read resolves with the value', async () => {
const stream = new Stream<number>()
stream[Symbol.asyncIterator]()
stream.enqueue(42)
const result = await stream.next()
expect(result).toEqual({ done: false, value: 42 })
})
test("enqueue multiple then drain in order", async () => {
const stream = new Stream<string>();
stream[Symbol.asyncIterator]();
stream.enqueue("a");
stream.enqueue("b");
stream.enqueue("c");
expect(await stream.next()).toEqual({ done: false, value: "a" });
expect(await stream.next()).toEqual({ done: false, value: "b" });
expect(await stream.next()).toEqual({ done: false, value: "c" });
});
test('enqueue multiple then drain in order', async () => {
const stream = new Stream<string>()
stream[Symbol.asyncIterator]()
stream.enqueue('a')
stream.enqueue('b')
stream.enqueue('c')
expect(await stream.next()).toEqual({ done: false, value: 'a' })
expect(await stream.next()).toEqual({ done: false, value: 'b' })
expect(await stream.next()).toEqual({ done: false, value: 'c' })
})
test("next() blocks until enqueue provides a value", async () => {
const stream = new Stream<number>();
stream[Symbol.asyncIterator]();
const promise = stream.next();
test('next() blocks until enqueue provides a value', async () => {
const stream = new Stream<number>()
stream[Symbol.asyncIterator]()
const promise = stream.next()
// Not resolved yet — enqueue after a microtask
stream.enqueue(99);
const result = await promise;
expect(result).toEqual({ done: false, value: 99 });
});
stream.enqueue(99)
const result = await promise
expect(result).toEqual({ done: false, value: 99 })
})
test("done() resolves pending reader with done:true", async () => {
const stream = new Stream<number>();
stream[Symbol.asyncIterator]();
const promise = stream.next();
stream.done();
expect(await promise).toEqual({ done: true, value: undefined });
});
test('done() resolves pending reader with done:true', async () => {
const stream = new Stream<number>()
stream[Symbol.asyncIterator]()
const promise = stream.next()
stream.done()
expect(await promise).toEqual({ done: true, value: undefined })
})
test("done() with no pending reader — subsequent next returns done:true", async () => {
const stream = new Stream<number>();
stream[Symbol.asyncIterator]();
stream.done();
expect(await stream.next()).toEqual({ done: true, value: undefined });
});
test('done() with no pending reader — subsequent next returns done:true', async () => {
const stream = new Stream<number>()
stream[Symbol.asyncIterator]()
stream.done()
expect(await stream.next()).toEqual({ done: true, value: undefined })
})
test("error() rejects pending reader", async () => {
const stream = new Stream<number>();
stream[Symbol.asyncIterator]();
const promise = stream.next();
stream.error(new Error("boom"));
expect(promise).rejects.toThrow("boom");
});
test('error() rejects pending reader', async () => {
const stream = new Stream<number>()
stream[Symbol.asyncIterator]()
const promise = stream.next()
stream.error(new Error('boom'))
expect(promise).rejects.toThrow('boom')
})
test("error() after done — hasError is set but next returns done:true (isDone checked first)", async () => {
const stream = new Stream<number>();
stream[Symbol.asyncIterator]();
stream.done();
stream.error(new Error("late error"));
test('error() after done — hasError is set but next returns done:true (isDone checked first)', async () => {
const stream = new Stream<number>()
stream[Symbol.asyncIterator]()
stream.done()
stream.error(new Error('late error'))
// next() checks isDone before hasError, so it returns done:true
expect(await stream.next()).toEqual({ done: true, value: undefined });
});
expect(await stream.next()).toEqual({ done: true, value: undefined })
})
test("enqueue after done — queue is checked before isDone, value is consumed", async () => {
const stream = new Stream<number>();
stream[Symbol.asyncIterator]();
stream.done();
stream.enqueue(1);
test('enqueue after done — queue is checked before isDone, value is consumed', async () => {
const stream = new Stream<number>()
stream[Symbol.asyncIterator]()
stream.done()
stream.enqueue(1)
// next() checks queue.length > 0 first, so enqueued value is returned
expect(await stream.next()).toEqual({ done: false, value: 1 });
expect(await stream.next()).toEqual({ done: false, value: 1 })
// After draining queue, done takes effect
expect(await stream.next()).toEqual({ done: true, value: undefined });
});
expect(await stream.next()).toEqual({ done: true, value: undefined })
})
test("return() marks stream as done and calls returned callback", async () => {
let called = false;
const stream = new Stream<number>(() => { called = true; });
stream[Symbol.asyncIterator]();
const result = await stream.return();
expect(result).toEqual({ done: true, value: undefined });
expect(called).toBe(true);
test('return() marks stream as done and calls returned callback', async () => {
let called = false
const stream = new Stream<number>(() => {
called = true
})
stream[Symbol.asyncIterator]()
const result = await stream.return()
expect(result).toEqual({ done: true, value: undefined })
expect(called).toBe(true)
// Subsequent next returns done
expect(await stream.next()).toEqual({ done: true, value: undefined });
});
expect(await stream.next()).toEqual({ done: true, value: undefined })
})
test("return() without callback still works", async () => {
const stream = new Stream<number>();
stream[Symbol.asyncIterator]();
const result = await stream.return();
expect(result).toEqual({ done: true, value: undefined });
});
test('return() without callback still works', async () => {
const stream = new Stream<number>()
stream[Symbol.asyncIterator]()
const result = await stream.return()
expect(result).toEqual({ done: true, value: undefined })
})
test("Symbol.asyncIterator throws on second call", () => {
const stream = new Stream<number>();
stream[Symbol.asyncIterator]();
test('Symbol.asyncIterator throws on second call', () => {
const stream = new Stream<number>()
stream[Symbol.asyncIterator]()
expect(() => stream[Symbol.asyncIterator]()).toThrow(
"Stream can only be iterated once"
);
});
'Stream can only be iterated once',
)
})
test("for-await-of iteration drains queued values then ends", async () => {
const stream = new Stream<string>();
stream.enqueue("x");
stream.enqueue("y");
stream.done();
const results: string[] = [];
test('for-await-of iteration drains queued values then ends', async () => {
const stream = new Stream<string>()
stream.enqueue('x')
stream.enqueue('y')
stream.done()
const results: string[] = []
for await (const value of stream) {
results.push(value);
results.push(value)
}
expect(results).toEqual(["x", "y"]);
});
expect(results).toEqual(['x', 'y'])
})
test("for-await-of blocks until done", async () => {
const stream = new Stream<number>();
const results: number[] = [];
test('for-await-of blocks until done', async () => {
const stream = new Stream<number>()
const results: number[] = []
const iterPromise = (async () => {
for await (const v of stream) {
results.push(v);
results.push(v)
}
})();
})()
// Enqueue after a tick
await Promise.resolve();
stream.enqueue(1);
stream.enqueue(2);
stream.done();
await Promise.resolve()
stream.enqueue(1)
stream.enqueue(2)
stream.done()
await iterPromise;
expect(results).toEqual([1, 2]);
});
await iterPromise
expect(results).toEqual([1, 2])
})
test("error during for-await-of rejects the loop", async () => {
const stream = new Stream<number>();
test('error during for-await-of rejects the loop', async () => {
const stream = new Stream<number>()
const iterPromise = (async () => {
for await (const _ of stream) {
// will error before any value
}
})();
stream.error(new Error("stream broken"));
expect(iterPromise).rejects.toThrow("stream broken");
});
})()
stream.error(new Error('stream broken'))
expect(iterPromise).rejects.toThrow('stream broken')
})
test("concurrent enqueue from multiple sources does not lose data", async () => {
const stream = new Stream<number>();
test('concurrent enqueue from multiple sources does not lose data', async () => {
const stream = new Stream<number>()
// Rapid sequential enqueue
for (let i = 0; i < 100; i++) {
stream.enqueue(i);
stream.enqueue(i)
}
stream.done();
stream.done()
const results: number[] = [];
const results: number[] = []
for await (const v of stream) {
results.push(v);
results.push(v)
}
expect(results.length).toBe(100);
expect(results[0]).toBe(0);
expect(results[99]).toBe(99);
});
});
expect(results.length).toBe(100)
expect(results[0]).toBe(0)
expect(results[99]).toBe(99)
})
})

View File

@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
import {
escapeRegExp,
capitalize,
@@ -10,187 +10,187 @@ import {
safeJoinLines,
EndTruncatingAccumulator,
truncateToLines,
} from "../stringUtils";
} from '../stringUtils'
describe("escapeRegExp", () => {
test("escapes special regex chars", () => {
expect(escapeRegExp("a.b*c?d")).toBe("a\\.b\\*c\\?d");
});
describe('escapeRegExp', () => {
test('escapes special regex chars', () => {
expect(escapeRegExp('a.b*c?d')).toBe('a\\.b\\*c\\?d')
})
test("escapes brackets and parens", () => {
expect(escapeRegExp("[foo](bar)")).toBe("\\[foo\\]\\(bar\\)");
});
test('escapes brackets and parens', () => {
expect(escapeRegExp('[foo](bar)')).toBe('\\[foo\\]\\(bar\\)')
})
test("escapes all special chars", () => {
const allSpecialChars = "^$" + "{}()|[]\\.*+?";
test('escapes all special chars', () => {
const allSpecialChars = '^$' + '{}()|[]\\.*+?'
expect(escapeRegExp(allSpecialChars)).toBe(
"\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\\\.\\*\\+\\?"
);
});
'\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\\\.\\*\\+\\?',
)
})
test("returns normal string unchanged", () => {
expect(escapeRegExp("hello")).toBe("hello");
});
});
test('returns normal string unchanged', () => {
expect(escapeRegExp('hello')).toBe('hello')
})
})
describe("capitalize", () => {
test("uppercases first char", () => {
expect(capitalize("hello")).toBe("Hello");
});
describe('capitalize', () => {
test('uppercases first char', () => {
expect(capitalize('hello')).toBe('Hello')
})
test("does NOT lowercase rest", () => {
expect(capitalize("fooBar")).toBe("FooBar");
});
test('does NOT lowercase rest', () => {
expect(capitalize('fooBar')).toBe('FooBar')
})
test("handles single char", () => {
expect(capitalize("a")).toBe("A");
});
test('handles single char', () => {
expect(capitalize('a')).toBe('A')
})
test("handles empty string", () => {
expect(capitalize("")).toBe("");
});
});
test('handles empty string', () => {
expect(capitalize('')).toBe('')
})
})
describe("plural", () => {
test("returns singular for 1", () => {
expect(plural(1, "file")).toBe("file");
});
describe('plural', () => {
test('returns singular for 1', () => {
expect(plural(1, 'file')).toBe('file')
})
test("returns plural for 0", () => {
expect(plural(0, "file")).toBe("files");
});
test('returns plural for 0', () => {
expect(plural(0, 'file')).toBe('files')
})
test("returns plural for many", () => {
expect(plural(3, "file")).toBe("files");
});
test('returns plural for many', () => {
expect(plural(3, 'file')).toBe('files')
})
test("uses custom plural form", () => {
expect(plural(2, "entry", "entries")).toBe("entries");
});
});
test('uses custom plural form', () => {
expect(plural(2, 'entry', 'entries')).toBe('entries')
})
})
describe("firstLineOf", () => {
test("returns first line of multiline string", () => {
expect(firstLineOf("line1\nline2\nline3")).toBe("line1");
});
describe('firstLineOf', () => {
test('returns first line of multiline string', () => {
expect(firstLineOf('line1\nline2\nline3')).toBe('line1')
})
test("returns whole string if no newline", () => {
expect(firstLineOf("single line")).toBe("single line");
});
test('returns whole string if no newline', () => {
expect(firstLineOf('single line')).toBe('single line')
})
test("returns empty string for leading newline", () => {
expect(firstLineOf("\nline2")).toBe("");
});
});
test('returns empty string for leading newline', () => {
expect(firstLineOf('\nline2')).toBe('')
})
})
describe("countCharInString", () => {
test("counts occurrences of a character", () => {
expect(countCharInString("hello world", "l")).toBe(3);
});
describe('countCharInString', () => {
test('counts occurrences of a character', () => {
expect(countCharInString('hello world', 'l')).toBe(3)
})
test("returns 0 for no match", () => {
expect(countCharInString("hello", "z")).toBe(0);
});
test('returns 0 for no match', () => {
expect(countCharInString('hello', 'z')).toBe(0)
})
test("counts from start offset", () => {
expect(countCharInString("aabaa", "a", 2)).toBe(2);
});
test('counts from start offset', () => {
expect(countCharInString('aabaa', 'a', 2)).toBe(2)
})
test("returns 0 for empty string", () => {
expect(countCharInString("", "a")).toBe(0);
});
});
test('returns 0 for empty string', () => {
expect(countCharInString('', 'a')).toBe(0)
})
})
describe("normalizeFullWidthDigits", () => {
test("converts full-width digits to half-width", () => {
expect(normalizeFullWidthDigits("")).toBe("0123456789");
});
describe('normalizeFullWidthDigits', () => {
test('converts full-width digits to half-width', () => {
expect(normalizeFullWidthDigits('')).toBe('0123456789')
})
test("leaves half-width digits unchanged", () => {
expect(normalizeFullWidthDigits("0123")).toBe("0123");
});
test('leaves half-width digits unchanged', () => {
expect(normalizeFullWidthDigits('0123')).toBe('0123')
})
test("handles mixed content", () => {
expect(normalizeFullWidthDigits("test")).toBe("test123");
});
});
test('handles mixed content', () => {
expect(normalizeFullWidthDigits('test')).toBe('test123')
})
})
describe("normalizeFullWidthSpace", () => {
test("converts full-width space to half-width", () => {
expect(normalizeFullWidthSpace("a\u3000b")).toBe("a b");
});
describe('normalizeFullWidthSpace', () => {
test('converts full-width space to half-width', () => {
expect(normalizeFullWidthSpace('a\u3000b')).toBe('a b')
})
test("leaves normal spaces unchanged", () => {
expect(normalizeFullWidthSpace("a b")).toBe("a b");
});
});
test('leaves normal spaces unchanged', () => {
expect(normalizeFullWidthSpace('a b')).toBe('a b')
})
})
describe("safeJoinLines", () => {
test("joins lines with delimiter", () => {
expect(safeJoinLines(["a", "b", "c"], ",")).toBe("a,b,c");
});
describe('safeJoinLines', () => {
test('joins lines with delimiter', () => {
expect(safeJoinLines(['a', 'b', 'c'], ',')).toBe('a,b,c')
})
test("truncates when exceeding maxSize", () => {
const result = safeJoinLines(["hello", "world", "foo"], ",", 12);
expect(result.length).toBeLessThanOrEqual(12 + "...[truncated]".length);
expect(result).toContain("...[truncated]");
});
test('truncates when exceeding maxSize', () => {
const result = safeJoinLines(['hello', 'world', 'foo'], ',', 12)
expect(result.length).toBeLessThanOrEqual(12 + '...[truncated]'.length)
expect(result).toContain('...[truncated]')
})
test("returns empty string for empty input", () => {
expect(safeJoinLines([])).toBe("");
});
});
test('returns empty string for empty input', () => {
expect(safeJoinLines([])).toBe('')
})
})
describe("EndTruncatingAccumulator", () => {
test("accumulates text", () => {
const acc = new EndTruncatingAccumulator(100);
acc.append("hello ");
acc.append("world");
expect(acc.toString()).toBe("hello world");
});
describe('EndTruncatingAccumulator', () => {
test('accumulates text', () => {
const acc = new EndTruncatingAccumulator(100)
acc.append('hello ')
acc.append('world')
expect(acc.toString()).toBe('hello world')
})
test("truncates when exceeding maxSize", () => {
const acc = new EndTruncatingAccumulator(10);
acc.append("12345678901234567890");
expect(acc.truncated).toBe(true);
expect(acc.length).toBe(10);
});
test('truncates when exceeding maxSize', () => {
const acc = new EndTruncatingAccumulator(10)
acc.append('12345678901234567890')
expect(acc.truncated).toBe(true)
expect(acc.length).toBe(10)
})
test("reports total bytes received", () => {
const acc = new EndTruncatingAccumulator(5);
acc.append("1234567890");
expect(acc.totalBytes).toBe(10);
});
test('reports total bytes received', () => {
const acc = new EndTruncatingAccumulator(5)
acc.append('1234567890')
expect(acc.totalBytes).toBe(10)
})
test("clear resets state", () => {
const acc = new EndTruncatingAccumulator(100);
acc.append("hello");
acc.clear();
expect(acc.toString()).toBe("");
expect(acc.length).toBe(0);
expect(acc.truncated).toBe(false);
});
test('clear resets state', () => {
const acc = new EndTruncatingAccumulator(100)
acc.append('hello')
acc.clear()
expect(acc.toString()).toBe('')
expect(acc.length).toBe(0)
expect(acc.truncated).toBe(false)
})
test("stops accepting data once truncated and full", () => {
const acc = new EndTruncatingAccumulator(5);
acc.append("12345");
acc.append("67890");
expect(acc.length).toBe(5);
acc.append("more");
expect(acc.length).toBe(5);
});
});
test('stops accepting data once truncated and full', () => {
const acc = new EndTruncatingAccumulator(5)
acc.append('12345')
acc.append('67890')
expect(acc.length).toBe(5)
acc.append('more')
expect(acc.length).toBe(5)
})
})
describe("truncateToLines", () => {
test("returns text unchanged if within limit", () => {
expect(truncateToLines("a\nb\nc", 5)).toBe("a\nb\nc");
});
describe('truncateToLines', () => {
test('returns text unchanged if within limit', () => {
expect(truncateToLines('a\nb\nc', 5)).toBe('a\nb\nc')
})
test("truncates text exceeding limit", () => {
expect(truncateToLines("a\nb\nc\nd\ne", 3)).toBe("a\nb\nc…");
});
test('truncates text exceeding limit', () => {
expect(truncateToLines('a\nb\nc\nd\ne', 3)).toBe('a\nb\nc…')
})
test("handles single line", () => {
expect(truncateToLines("hello", 1)).toBe("hello");
});
});
test('handles single line', () => {
expect(truncateToLines('hello', 1)).toBe('hello')
})
})

View File

@@ -1,7 +1,7 @@
import { describe, expect, test } from "bun:test";
import { buildEffectiveSystemPrompt } from "../systemPrompt";
import { describe, expect, test } from 'bun:test'
import { buildEffectiveSystemPrompt } from '../systemPrompt'
const defaultPrompt = ["You are a helpful assistant.", "Follow instructions."];
const defaultPrompt = ['You are a helpful assistant.', 'Follow instructions.']
function buildPrompt(overrides: Record<string, unknown> = {}) {
return buildEffectiveSystemPrompt({
@@ -11,78 +11,78 @@ function buildPrompt(overrides: Record<string, unknown> = {}) {
defaultSystemPrompt: defaultPrompt,
appendSystemPrompt: undefined,
...overrides,
});
})
}
describe("buildEffectiveSystemPrompt", () => {
test("returns default system prompt when no overrides", () => {
const result = buildPrompt();
expect(Array.from(result)).toEqual(defaultPrompt);
});
describe('buildEffectiveSystemPrompt', () => {
test('returns default system prompt when no overrides', () => {
const result = buildPrompt()
expect(Array.from(result)).toEqual(defaultPrompt)
})
test("overrideSystemPrompt replaces everything", () => {
const result = buildPrompt({ overrideSystemPrompt: "override" });
expect(Array.from(result)).toEqual(["override"]);
});
test('overrideSystemPrompt replaces everything', () => {
const result = buildPrompt({ overrideSystemPrompt: 'override' })
expect(Array.from(result)).toEqual(['override'])
})
test("customSystemPrompt replaces default", () => {
const result = buildPrompt({ customSystemPrompt: "custom" });
expect(Array.from(result)).toEqual(["custom"]);
});
test('customSystemPrompt replaces default', () => {
const result = buildPrompt({ customSystemPrompt: 'custom' })
expect(Array.from(result)).toEqual(['custom'])
})
test("appendSystemPrompt is appended after main prompt", () => {
const result = buildPrompt({ appendSystemPrompt: "appended" });
expect(Array.from(result)).toEqual([...defaultPrompt, "appended"]);
});
test('appendSystemPrompt is appended after main prompt', () => {
const result = buildPrompt({ appendSystemPrompt: 'appended' })
expect(Array.from(result)).toEqual([...defaultPrompt, 'appended'])
})
test("agent definition replaces default prompt", () => {
test('agent definition replaces default prompt', () => {
const agentDef = {
getSystemPrompt: () => "agent prompt",
agentType: "custom",
} as any;
const result = buildPrompt({ mainThreadAgentDefinition: agentDef });
expect(Array.from(result)).toEqual(["agent prompt"]);
});
getSystemPrompt: () => 'agent prompt',
agentType: 'custom',
} as any
const result = buildPrompt({ mainThreadAgentDefinition: agentDef })
expect(Array.from(result)).toEqual(['agent prompt'])
})
test("agent definition with append combines both", () => {
test('agent definition with append combines both', () => {
const agentDef = {
getSystemPrompt: () => "agent prompt",
agentType: "custom",
} as any;
getSystemPrompt: () => 'agent prompt',
agentType: 'custom',
} as any
const result = buildPrompt({
mainThreadAgentDefinition: agentDef,
appendSystemPrompt: "extra",
});
expect(Array.from(result)).toEqual(["agent prompt", "extra"]);
});
appendSystemPrompt: 'extra',
})
expect(Array.from(result)).toEqual(['agent prompt', 'extra'])
})
test("override takes precedence over agent and custom", () => {
test('override takes precedence over agent and custom', () => {
const agentDef = {
getSystemPrompt: () => "agent prompt",
agentType: "custom",
} as any;
getSystemPrompt: () => 'agent prompt',
agentType: 'custom',
} as any
const result = buildPrompt({
mainThreadAgentDefinition: agentDef,
customSystemPrompt: "custom",
appendSystemPrompt: "extra",
overrideSystemPrompt: "override",
});
expect(Array.from(result)).toEqual(["override"]);
});
customSystemPrompt: 'custom',
appendSystemPrompt: 'extra',
overrideSystemPrompt: 'override',
})
expect(Array.from(result)).toEqual(['override'])
})
test("returns array of strings", () => {
const result = buildPrompt();
expect(Array.isArray(result)).toBe(true);
test('returns array of strings', () => {
const result = buildPrompt()
expect(Array.isArray(result)).toBe(true)
for (const item of result) {
expect(typeof item).toBe("string");
expect(typeof item).toBe('string')
}
});
})
test("custom + append combines both", () => {
test('custom + append combines both', () => {
const result = buildPrompt({
customSystemPrompt: "custom",
appendSystemPrompt: "extra",
});
expect(Array.from(result)).toEqual(["custom", "extra"]);
});
});
customSystemPrompt: 'custom',
appendSystemPrompt: 'extra',
})
expect(Array.from(result)).toEqual(['custom', 'extra'])
})
})

View File

@@ -1,104 +1,91 @@
import { describe, expect, test } from "bun:test";
import { toTaggedId } from "../taggedId";
import { describe, expect, test } from 'bun:test'
import { toTaggedId } from '../taggedId'
const BASE_58_CHARS =
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
describe("toTaggedId", () => {
describe('toTaggedId', () => {
test("zero UUID produces all base58 '1's (first char)", () => {
const result = toTaggedId(
"user",
"00000000-0000-0000-0000-000000000000"
);
const result = toTaggedId('user', '00000000-0000-0000-0000-000000000000')
// base58 of 0 is all '1's (the first base58 character)
expect(result).toBe("user_01" + "1".repeat(22));
});
expect(result).toBe('user_01' + '1'.repeat(22))
})
test("format is tag_01 + 22 base58 chars", () => {
const result = toTaggedId(
"user",
"550e8400-e29b-41d4-a716-446655440000"
);
test('format is tag_01 + 22 base58 chars', () => {
const result = toTaggedId('user', '550e8400-e29b-41d4-a716-446655440000')
expect(result).toMatch(
new RegExp(`^user_01[${BASE_58_CHARS.replace(/[-]/g, "\\-")}]{22}$`)
);
});
new RegExp(`^user_01[${BASE_58_CHARS.replace(/[-]/g, '\\-')}]{22}$`),
)
})
test("output starts with the provided tag", () => {
const result = toTaggedId("org", "550e8400-e29b-41d4-a716-446655440000");
expect(result.startsWith("org_01")).toBe(true);
});
test('output starts with the provided tag', () => {
const result = toTaggedId('org', '550e8400-e29b-41d4-a716-446655440000')
expect(result.startsWith('org_01')).toBe(true)
})
test("UUID with hyphens equals UUID without hyphens", () => {
test('UUID with hyphens equals UUID without hyphens', () => {
const withHyphens = toTaggedId(
"user",
"550e8400-e29b-41d4-a716-446655440000"
);
'user',
'550e8400-e29b-41d4-a716-446655440000',
)
const withoutHyphens = toTaggedId(
"user",
"550e8400e29b41d4a716446655440000"
);
expect(withHyphens).toBe(withoutHyphens);
});
'user',
'550e8400e29b41d4a716446655440000',
)
expect(withHyphens).toBe(withoutHyphens)
})
test("different tags produce different prefixes", () => {
const uuid = "550e8400-e29b-41d4-a716-446655440000";
const userResult = toTaggedId("user", uuid);
const orgResult = toTaggedId("org", uuid);
const msgResult = toTaggedId("msg", uuid);
test('different tags produce different prefixes', () => {
const uuid = '550e8400-e29b-41d4-a716-446655440000'
const userResult = toTaggedId('user', uuid)
const orgResult = toTaggedId('org', uuid)
const msgResult = toTaggedId('msg', uuid)
// They share the same base58 suffix but different prefixes
expect(userResult.slice(userResult.indexOf("_01") + 3)).toBe(
orgResult.slice(orgResult.indexOf("_01") + 3)
);
expect(userResult).not.toBe(orgResult);
expect(orgResult).not.toBe(msgResult);
});
expect(userResult.slice(userResult.indexOf('_01') + 3)).toBe(
orgResult.slice(orgResult.indexOf('_01') + 3),
)
expect(userResult).not.toBe(orgResult)
expect(orgResult).not.toBe(msgResult)
})
test("different UUIDs produce different encoded parts", () => {
const result1 = toTaggedId(
"user",
"550e8400-e29b-41d4-a716-446655440000"
);
const result2 = toTaggedId(
"user",
"661f9500-f3ac-52e5-b827-557766550111"
);
expect(result1).not.toBe(result2);
});
test('different UUIDs produce different encoded parts', () => {
const result1 = toTaggedId('user', '550e8400-e29b-41d4-a716-446655440000')
const result2 = toTaggedId('user', '661f9500-f3ac-52e5-b827-557766550111')
expect(result1).not.toBe(result2)
})
test("encoded part is always exactly 22 characters", () => {
test('encoded part is always exactly 22 characters', () => {
const uuids = [
"00000000-0000-0000-0000-000000000000",
"ffffffff-ffff-ffff-ffff-ffffffffffff",
"550e8400-e29b-41d4-a716-446655440000",
"00000000-0000-0000-0000-000000000001",
];
'00000000-0000-0000-0000-000000000000',
'ffffffff-ffff-ffff-ffff-ffffffffffff',
'550e8400-e29b-41d4-a716-446655440000',
'00000000-0000-0000-0000-000000000001',
]
for (const uuid of uuids) {
const result = toTaggedId("test", uuid);
const encoded = result.slice("test_01".length);
expect(encoded).toHaveLength(22);
const result = toTaggedId('test', uuid)
const encoded = result.slice('test_01'.length)
expect(encoded).toHaveLength(22)
}
});
})
test("throws on invalid UUID (too short)", () => {
expect(() => toTaggedId("user", "abcdef")).toThrow("Invalid UUID hex length");
});
test('throws on invalid UUID (too short)', () => {
expect(() => toTaggedId('user', 'abcdef')).toThrow(
'Invalid UUID hex length',
)
})
test("throws on invalid UUID (too long)", () => {
test('throws on invalid UUID (too long)', () => {
expect(() =>
toTaggedId("user", "550e8400e29b41d4a716446655440000ff")
).toThrow("Invalid UUID hex length");
});
toTaggedId('user', '550e8400e29b41d4a716446655440000ff'),
).toThrow('Invalid UUID hex length')
})
test("max UUID (all f's) produces valid base58 output", () => {
const result = toTaggedId(
"user",
"ffffffff-ffff-ffff-ffff-ffffffffffff"
);
expect(result.startsWith("user_01")).toBe(true);
const encoded = result.slice("user_01".length);
const result = toTaggedId('user', 'ffffffff-ffff-ffff-ffff-ffffffffffff')
expect(result.startsWith('user_01')).toBe(true)
const encoded = result.slice('user_01'.length)
for (const ch of encoded) {
expect(BASE_58_CHARS).toContain(ch);
expect(BASE_58_CHARS).toContain(ch)
}
});
});
})
})

View File

@@ -1,84 +1,87 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
import {
buildTaskStateMessage,
getTaskStateSnapshotKey,
} from "../taskStateMessage";
} from '../taskStateMessage'
describe("buildTaskStateMessage", () => {
test("filters internal tasks and preserves public task fields", () => {
const message = buildTaskStateMessage("tasklist", [
describe('buildTaskStateMessage', () => {
test('filters internal tasks and preserves public task fields', () => {
const message = buildTaskStateMessage('tasklist', [
{
id: "1",
subject: "Visible task",
description: "Shown in web UI",
activeForm: "Doing visible task",
status: "in_progress",
owner: "agent-1",
blocks: ["2"],
id: '1',
subject: 'Visible task',
description: 'Shown in web UI',
activeForm: 'Doing visible task',
status: 'in_progress',
owner: 'agent-1',
blocks: ['2'],
blockedBy: [],
},
{
id: "2",
subject: "Internal task",
description: "Hidden from web UI",
status: "pending",
id: '2',
subject: 'Internal task',
description: 'Hidden from web UI',
status: 'pending',
blocks: [],
blockedBy: [],
metadata: { _internal: true },
},
]);
])
expect(message.type).toBe("task_state");
expect(message.task_list_id).toBe("tasklist");
expect(message.uuid).toEqual(expect.any(String));
expect(message.type).toBe('task_state')
expect(message.task_list_id).toBe('tasklist')
expect(message.uuid).toEqual(expect.any(String))
expect(message.tasks).toEqual([
{
id: "1",
subject: "Visible task",
description: "Shown in web UI",
activeForm: "Doing visible task",
status: "in_progress",
owner: "agent-1",
blocks: ["2"],
id: '1',
subject: 'Visible task',
description: 'Shown in web UI',
activeForm: 'Doing visible task',
status: 'in_progress',
owner: 'agent-1',
blocks: ['2'],
blockedBy: [],
},
]);
});
])
})
test("builds a stable snapshot key for equivalent public tasks", () => {
test('builds a stable snapshot key for equivalent public tasks', () => {
const tasks = [
{
id: "2",
subject: "Second",
description: "Second task",
status: "pending",
id: '2',
subject: 'Second',
description: 'Second task',
status: 'pending',
blocks: [],
blockedBy: [],
},
{
id: "1",
subject: "First",
description: "First task",
status: "in_progress",
blocks: ["2"],
id: '1',
subject: 'First',
description: 'First task',
status: 'in_progress',
blocks: ['2'],
blockedBy: [],
},
{
id: "internal",
subject: "Internal task",
description: "Hidden",
status: "pending",
id: 'internal',
subject: 'Internal task',
description: 'Hidden',
status: 'pending',
blocks: [],
blockedBy: [],
metadata: { _internal: true },
},
];
]
const firstKey = getTaskStateSnapshotKey("tasklist", tasks as any);
const secondKey = getTaskStateSnapshotKey("tasklist", [...tasks].reverse() as any);
const message = buildTaskStateMessage("tasklist", tasks as any);
const firstKey = getTaskStateSnapshotKey('tasklist', tasks as any)
const secondKey = getTaskStateSnapshotKey(
'tasklist',
[...tasks].reverse() as any,
)
const message = buildTaskStateMessage('tasklist', tasks as any)
expect(firstKey).toBe(secondKey);
expect(message.tasks.map(task => task.id)).toEqual(["1", "2"]);
});
});
expect(firstKey).toBe(secondKey)
expect(message.tasks.map(task => task.id)).toEqual(['1', '2'])
})
})

View File

@@ -366,9 +366,7 @@ describe('teammate mailbox retention', () => {
throw new Error('Expected filesystem errno code')
}
const expectedCodes =
process.platform === 'win32'
? ['EISDIR', 'EPERM', 'EACCES']
: ['EISDIR']
process.platform === 'win32' ? ['EISDIR', 'EPERM', 'EACCES'] : ['EISDIR']
expect(expectedCodes).toContain(code)
expect((await stat(inboxPath)).isDirectory()).toBe(true)
})
@@ -384,7 +382,11 @@ describe('teammate mailbox retention', () => {
test('readMailbox rejects non-array mailbox files', async () => {
const inboxPath = getInboxPath('worker', 'alpha')
await mkdir(dirname(inboxPath), { recursive: true })
await writeFile(inboxPath, JSON.stringify({ text: 'not an array' }), 'utf-8')
await writeFile(
inboxPath,
JSON.stringify({ text: 'not an array' }),
'utf-8',
)
await expect(readMailbox('worker', 'alpha')).rejects.toThrow(
'expected message array',
@@ -418,7 +420,11 @@ describe('teammate mailbox retention', () => {
test('readMailbox rejects oversized mailbox files before parsing', async () => {
const inboxPath = getInboxPath('worker', 'alpha')
await mkdir(dirname(inboxPath), { recursive: true })
await writeFile(inboxPath, `[${' '.repeat(MAX_MAILBOX_FILE_BYTES)}]`, 'utf-8')
await writeFile(
inboxPath,
`[${' '.repeat(MAX_MAILBOX_FILE_BYTES)}]`,
'utf-8',
)
await expect(readMailbox('worker', 'alpha')).rejects.toThrow(
'Mailbox file exceeds',

View File

@@ -1,131 +1,141 @@
import { describe, expect, test } from "bun:test";
import { segmentTextByHighlights, type TextHighlight } from "../textHighlighting";
import { describe, expect, test } from 'bun:test'
import {
segmentTextByHighlights,
type TextHighlight,
} from '../textHighlighting'
describe("segmentTextByHighlights", () => {
describe('segmentTextByHighlights', () => {
// Basic
test("returns single segment with no highlights", () => {
const segments = segmentTextByHighlights("hello world", []);
expect(segments).toHaveLength(1);
expect(segments[0].text).toBe("hello world");
expect(segments[0].highlight).toBeUndefined();
});
test('returns single segment with no highlights', () => {
const segments = segmentTextByHighlights('hello world', [])
expect(segments).toHaveLength(1)
expect(segments[0].text).toBe('hello world')
expect(segments[0].highlight).toBeUndefined()
})
test("returns highlighted segment for single highlight", () => {
test('returns highlighted segment for single highlight', () => {
const highlights: TextHighlight[] = [
{ start: 0, end: 5, color: undefined, priority: 0 },
];
const segments = segmentTextByHighlights("hello world", highlights);
expect(segments.length).toBeGreaterThanOrEqual(2);
expect(segments.some(s => s.highlight !== undefined)).toBe(true);
});
]
const segments = segmentTextByHighlights('hello world', highlights)
expect(segments.length).toBeGreaterThanOrEqual(2)
expect(segments.some(s => s.highlight !== undefined)).toBe(true)
})
test("returns three segments for highlight in the middle", () => {
test('returns three segments for highlight in the middle', () => {
const highlights: TextHighlight[] = [
{ start: 3, end: 7, color: undefined, priority: 0 },
];
const segments = segmentTextByHighlights("hello world", highlights);
expect(segments.length).toBeGreaterThanOrEqual(2);
});
]
const segments = segmentTextByHighlights('hello world', highlights)
expect(segments.length).toBeGreaterThanOrEqual(2)
})
test("highlight covering entire text", () => {
test('highlight covering entire text', () => {
const highlights: TextHighlight[] = [
{ start: 0, end: 5, color: undefined, priority: 0 },
];
const segments = segmentTextByHighlights("hello", highlights);
expect(segments).toHaveLength(1);
expect(segments[0].highlight).toBeDefined();
});
]
const segments = segmentTextByHighlights('hello', highlights)
expect(segments).toHaveLength(1)
expect(segments[0].highlight).toBeDefined()
})
// Multiple highlights
test("handles non-overlapping highlights", () => {
test('handles non-overlapping highlights', () => {
const highlights: TextHighlight[] = [
{ start: 0, end: 3, color: undefined, priority: 0 },
{ start: 6, end: 9, color: undefined, priority: 0 },
];
const segments = segmentTextByHighlights("abcXYZdef", highlights);
const highlighted = segments.filter(s => s.highlight);
expect(highlighted.length).toBe(2);
});
]
const segments = segmentTextByHighlights('abcXYZdef', highlights)
const highlighted = segments.filter(s => s.highlight)
expect(highlighted.length).toBe(2)
})
test("handles overlapping highlights (priority-based)", () => {
test('handles overlapping highlights (priority-based)', () => {
const highlights: TextHighlight[] = [
{ start: 0, end: 5, color: undefined, priority: 0 },
{ start: 3, end: 8, color: undefined, priority: 1 },
];
const segments = segmentTextByHighlights("hello world", highlights);
]
const segments = segmentTextByHighlights('hello world', highlights)
// Overlapping: higher priority wins or they don't overlap
expect(segments.length).toBeGreaterThan(0);
});
expect(segments.length).toBeGreaterThan(0)
})
test("handles adjacent highlights", () => {
test('handles adjacent highlights', () => {
const highlights: TextHighlight[] = [
{ start: 0, end: 3, color: undefined, priority: 0 },
{ start: 3, end: 6, color: undefined, priority: 0 },
];
const segments = segmentTextByHighlights("abcdef", highlights);
const highlighted = segments.filter(s => s.highlight);
expect(highlighted.length).toBe(2);
});
]
const segments = segmentTextByHighlights('abcdef', highlights)
const highlighted = segments.filter(s => s.highlight)
expect(highlighted.length).toBe(2)
})
// Boundary
test("highlight starting at 0", () => {
test('highlight starting at 0', () => {
const highlights: TextHighlight[] = [
{ start: 0, end: 3, color: undefined, priority: 0 },
];
const segments = segmentTextByHighlights("abcdef", highlights);
expect(segments[0].start).toBe(0);
});
]
const segments = segmentTextByHighlights('abcdef', highlights)
expect(segments[0].start).toBe(0)
})
test("highlight ending at text length", () => {
const text = "hello";
test('highlight ending at text length', () => {
const text = 'hello'
const highlights: TextHighlight[] = [
{ start: 3, end: 5, color: undefined, priority: 0 },
];
const segments = segmentTextByHighlights(text, highlights);
expect(segments.length).toBeGreaterThan(0);
});
]
const segments = segmentTextByHighlights(text, highlights)
expect(segments.length).toBeGreaterThan(0)
})
test("empty highlights array returns single segment", () => {
const segments = segmentTextByHighlights("text", []);
expect(segments).toHaveLength(1);
expect(segments[0].highlight).toBeUndefined();
});
test('empty highlights array returns single segment', () => {
const segments = segmentTextByHighlights('text', [])
expect(segments).toHaveLength(1)
expect(segments[0].highlight).toBeUndefined()
})
// Properties
test("preserves highlight color property", () => {
test('preserves highlight color property', () => {
const highlights: TextHighlight[] = [
{ start: 0, end: 3, color: "primary" as any, priority: 0 },
];
const segments = segmentTextByHighlights("abc", highlights);
const highlighted = segments.find(s => s.highlight);
expect(highlighted?.highlight?.color as string).toBe("primary");
});
{ start: 0, end: 3, color: 'primary' as any, priority: 0 },
]
const segments = segmentTextByHighlights('abc', highlights)
const highlighted = segments.find(s => s.highlight)
expect(highlighted?.highlight?.color as string).toBe('primary')
})
test("preserves highlight priority property", () => {
test('preserves highlight priority property', () => {
const highlights: TextHighlight[] = [
{ start: 0, end: 3, color: undefined, priority: 5 },
];
const segments = segmentTextByHighlights("abc", highlights);
const highlighted = segments.find(s => s.highlight);
expect(highlighted?.highlight?.priority).toBe(5);
});
]
const segments = segmentTextByHighlights('abc', highlights)
const highlighted = segments.find(s => s.highlight)
expect(highlighted?.highlight?.priority).toBe(5)
})
test("preserves dimColor and inverse flags", () => {
test('preserves dimColor and inverse flags', () => {
const highlights: TextHighlight[] = [
{ start: 0, end: 3, color: undefined, priority: 0, dimColor: true, inverse: true },
];
const segments = segmentTextByHighlights("abc", highlights);
const highlighted = segments.find(s => s.highlight);
expect(highlighted?.highlight?.dimColor).toBe(true);
expect(highlighted?.highlight?.inverse).toBe(true);
});
{
start: 0,
end: 3,
color: undefined,
priority: 0,
dimColor: true,
inverse: true,
},
]
const segments = segmentTextByHighlights('abc', highlights)
const highlighted = segments.find(s => s.highlight)
expect(highlighted?.highlight?.dimColor).toBe(true)
expect(highlighted?.highlight?.inverse).toBe(true)
})
test("highlights with start === end are skipped", () => {
test('highlights with start === end are skipped', () => {
const highlights: TextHighlight[] = [
{ start: 3, end: 3, color: undefined, priority: 0 },
];
const segments = segmentTextByHighlights("abcdef", highlights);
expect(segments).toHaveLength(1);
expect(segments[0].highlight).toBeUndefined();
});
});
]
const segments = segmentTextByHighlights('abcdef', highlights)
expect(segments).toHaveLength(1)
expect(segments[0].highlight).toBeUndefined()
})
})

View File

@@ -1,150 +1,150 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
import {
parseTokenBudget,
findTokenBudgetPositions,
getBudgetContinuationMessage,
} from "../tokenBudget";
} from '../tokenBudget'
describe("parseTokenBudget", () => {
describe('parseTokenBudget', () => {
// --- shorthand at start ---
test("parses +500k at start", () => {
expect(parseTokenBudget("+500k")).toBe(500_000);
});
test('parses +500k at start', () => {
expect(parseTokenBudget('+500k')).toBe(500_000)
})
test("parses +2.5M at start", () => {
expect(parseTokenBudget("+2.5M")).toBe(2_500_000);
});
test('parses +2.5M at start', () => {
expect(parseTokenBudget('+2.5M')).toBe(2_500_000)
})
test("parses +1b at start", () => {
expect(parseTokenBudget("+1b")).toBe(1_000_000_000);
});
test('parses +1b at start', () => {
expect(parseTokenBudget('+1b')).toBe(1_000_000_000)
})
test("parses shorthand with leading whitespace", () => {
expect(parseTokenBudget(" +500k")).toBe(500_000);
});
test('parses shorthand with leading whitespace', () => {
expect(parseTokenBudget(' +500k')).toBe(500_000)
})
// --- shorthand at end ---
test("parses +1.5m at end of sentence", () => {
expect(parseTokenBudget("do this +1.5m")).toBe(1_500_000);
});
test('parses +1.5m at end of sentence', () => {
expect(parseTokenBudget('do this +1.5m')).toBe(1_500_000)
})
test("parses shorthand at end with trailing period", () => {
expect(parseTokenBudget("please continue +100k.")).toBe(100_000);
});
test('parses shorthand at end with trailing period', () => {
expect(parseTokenBudget('please continue +100k.')).toBe(100_000)
})
test("parses shorthand at end with trailing whitespace", () => {
expect(parseTokenBudget("keep going +250k ")).toBe(250_000);
});
test('parses shorthand at end with trailing whitespace', () => {
expect(parseTokenBudget('keep going +250k ')).toBe(250_000)
})
// --- verbose ---
test("parses 'use 2M tokens'", () => {
expect(parseTokenBudget("use 2M tokens")).toBe(2_000_000);
});
expect(parseTokenBudget('use 2M tokens')).toBe(2_000_000)
})
test("parses 'spend 500k tokens'", () => {
expect(parseTokenBudget("spend 500k tokens")).toBe(500_000);
});
expect(parseTokenBudget('spend 500k tokens')).toBe(500_000)
})
test("parses verbose with singular 'token'", () => {
expect(parseTokenBudget("use 1k token")).toBe(1_000);
});
expect(parseTokenBudget('use 1k token')).toBe(1_000)
})
test("parses verbose embedded in sentence", () => {
expect(parseTokenBudget("please use 3.5m tokens for this task")).toBe(
3_500_000
);
});
test('parses verbose embedded in sentence', () => {
expect(parseTokenBudget('please use 3.5m tokens for this task')).toBe(
3_500_000,
)
})
// --- no match (returns null) ---
test("returns null for plain text", () => {
expect(parseTokenBudget("hello world")).toBeNull();
});
test('returns null for plain text', () => {
expect(parseTokenBudget('hello world')).toBeNull()
})
test("returns null for bare number without +", () => {
expect(parseTokenBudget("500k")).toBeNull();
});
test('returns null for bare number without +', () => {
expect(parseTokenBudget('500k')).toBeNull()
})
test("returns null for number without suffix", () => {
expect(parseTokenBudget("+500")).toBeNull();
});
test('returns null for number without suffix', () => {
expect(parseTokenBudget('+500')).toBeNull()
})
test("returns null for empty string", () => {
expect(parseTokenBudget("")).toBeNull();
});
test('returns null for empty string', () => {
expect(parseTokenBudget('')).toBeNull()
})
// --- case insensitivity ---
test("is case insensitive for suffix", () => {
expect(parseTokenBudget("+500K")).toBe(500_000);
expect(parseTokenBudget("+2m")).toBe(2_000_000);
expect(parseTokenBudget("+1B")).toBe(1_000_000_000);
});
test('is case insensitive for suffix', () => {
expect(parseTokenBudget('+500K')).toBe(500_000)
expect(parseTokenBudget('+2m')).toBe(2_000_000)
expect(parseTokenBudget('+1B')).toBe(1_000_000_000)
})
// --- priority: start shorthand wins over end/verbose ---
test("start shorthand takes priority over verbose in same text", () => {
expect(parseTokenBudget("+100k use 2M tokens")).toBe(100_000);
});
});
test('start shorthand takes priority over verbose in same text', () => {
expect(parseTokenBudget('+100k use 2M tokens')).toBe(100_000)
})
})
describe("findTokenBudgetPositions", () => {
test("returns single position for +500k at start", () => {
const positions = findTokenBudgetPositions("+500k");
expect(positions).toHaveLength(1);
expect(positions[0]!.start).toBe(0);
expect(positions[0]!.end).toBe(5);
});
describe('findTokenBudgetPositions', () => {
test('returns single position for +500k at start', () => {
const positions = findTokenBudgetPositions('+500k')
expect(positions).toHaveLength(1)
expect(positions[0]!.start).toBe(0)
expect(positions[0]!.end).toBe(5)
})
test("returns position for shorthand at end", () => {
const text = "do this +100k";
const positions = findTokenBudgetPositions(text);
expect(positions).toHaveLength(1);
expect(positions[0]!.start).toBe(8);
expect(text.slice(positions[0]!.start, positions[0]!.end)).toBe("+100k");
});
test('returns position for shorthand at end', () => {
const text = 'do this +100k'
const positions = findTokenBudgetPositions(text)
expect(positions).toHaveLength(1)
expect(positions[0]!.start).toBe(8)
expect(text.slice(positions[0]!.start, positions[0]!.end)).toBe('+100k')
})
test("returns position for verbose match", () => {
const text = "please use 2M tokens here";
const positions = findTokenBudgetPositions(text);
expect(positions).toHaveLength(1);
test('returns position for verbose match', () => {
const text = 'please use 2M tokens here'
const positions = findTokenBudgetPositions(text)
expect(positions).toHaveLength(1)
expect(text.slice(positions[0]!.start, positions[0]!.end)).toBe(
"use 2M tokens"
);
});
'use 2M tokens',
)
})
test("returns multiple positions for combined shorthand + verbose", () => {
const text = "use 2M tokens and then +500k";
const positions = findTokenBudgetPositions(text);
expect(positions.length).toBeGreaterThanOrEqual(2);
});
test('returns multiple positions for combined shorthand + verbose', () => {
const text = 'use 2M tokens and then +500k'
const positions = findTokenBudgetPositions(text)
expect(positions.length).toBeGreaterThanOrEqual(2)
})
test("returns empty array for no match", () => {
expect(findTokenBudgetPositions("hello world")).toEqual([]);
});
test('returns empty array for no match', () => {
expect(findTokenBudgetPositions('hello world')).toEqual([])
})
test("does not double-count when +500k matches both start and end", () => {
const positions = findTokenBudgetPositions("+500k");
expect(positions).toHaveLength(1);
});
});
test('does not double-count when +500k matches both start and end', () => {
const positions = findTokenBudgetPositions('+500k')
expect(positions).toHaveLength(1)
})
})
describe("getBudgetContinuationMessage", () => {
test("formats a continuation message with correct values", () => {
const msg = getBudgetContinuationMessage(50, 250_000, 500_000);
expect(msg).toContain("50%");
expect(msg).toContain("250,000");
expect(msg).toContain("500,000");
expect(msg).toContain("Keep working");
expect(msg).toContain("do not summarize");
});
describe('getBudgetContinuationMessage', () => {
test('formats a continuation message with correct values', () => {
const msg = getBudgetContinuationMessage(50, 250_000, 500_000)
expect(msg).toContain('50%')
expect(msg).toContain('250,000')
expect(msg).toContain('500,000')
expect(msg).toContain('Keep working')
expect(msg).toContain('do not summarize')
})
test("formats zero values", () => {
const msg = getBudgetContinuationMessage(0, 0, 100_000);
expect(msg).toContain("0%");
expect(msg).toContain("0 / 100,000");
});
test('formats zero values', () => {
const msg = getBudgetContinuationMessage(0, 0, 100_000)
expect(msg).toContain('0%')
expect(msg).toContain('0 / 100,000')
})
test("formats large numbers with commas", () => {
const msg = getBudgetContinuationMessage(75, 7_500_000, 10_000_000);
expect(msg).toContain("7,500,000");
expect(msg).toContain("10,000,000");
});
});
test('formats large numbers with commas', () => {
const msg = getBudgetContinuationMessage(75, 7_500_000, 10_000_000)
expect(msg).toContain('7,500,000')
expect(msg).toContain('10,000,000')
})
})

View File

@@ -1,11 +1,11 @@
import { mock, describe, expect, test } from "bun:test";
import { logMock } from "../../../tests/mocks/log";
import { mock, describe, expect, test } from 'bun:test'
import { logMock } from '../../../tests/mocks/log'
// Mock heavy dependency chain: tokenEstimation.ts → log.ts → bootstrap/state.ts
mock.module("src/utils/log.ts", logMock);
mock.module('src/utils/log.ts', logMock)
// Mock tokenEstimation to avoid pulling in API provider deps
mock.module("src/services/tokenEstimation.ts", () => ({
mock.module('src/services/tokenEstimation.ts', () => ({
roughTokenCountEstimation: (text: string) => Math.ceil(text.length / 4),
roughTokenCountEstimationForMessages: (msgs: any[]) => msgs.length * 100,
roughTokenCountEstimationForMessage: () => 100,
@@ -14,7 +14,7 @@ mock.module("src/services/tokenEstimation.ts", () => ({
countTokensWithAPI: async () => 0,
countMessagesTokensWithAPI: async () => 0,
countTokensViaHaikuFallback: async () => 0,
}));
}))
// Mock slowOperations to avoid bun:bundle import
mock.module('src/utils/slowOperations.ts', () => ({
@@ -36,7 +36,7 @@ const {
getCurrentUsage,
doesMostRecentAssistantMessageExceed200k,
getAssistantMessageContentLength,
} = await import("../tokens");
} = await import('../tokens')
// ─── Helpers ────────────────────────────────────────────────────────────
@@ -44,16 +44,16 @@ function makeAssistantMessage(
content: any[],
usage?: any,
model?: string,
id?: string
id?: string,
) {
return {
type: "assistant" as const,
type: 'assistant' as const,
uuid: `test-${Math.random()}`,
message: {
id: id ?? `msg_${Math.random()}`,
role: "assistant" as const,
role: 'assistant' as const,
content,
model: model ?? "claude-sonnet-4-20250514",
model: model ?? 'claude-sonnet-4-20250514',
usage: usage ?? {
input_tokens: 100,
output_tokens: 50,
@@ -62,221 +62,221 @@ function makeAssistantMessage(
},
},
isApiErrorMessage: false,
};
}
}
function makeUserMessage(text: string) {
return {
type: "user" as const,
type: 'user' as const,
uuid: `test-${Math.random()}`,
message: { role: "user" as const, content: text },
};
message: { role: 'user' as const, content: text },
}
}
// ─── getTokenCountFromUsage ─────────────────────────────────────────────
describe("getTokenCountFromUsage", () => {
test("sums all token fields", () => {
describe('getTokenCountFromUsage', () => {
test('sums all token fields', () => {
const usage = {
input_tokens: 100,
output_tokens: 50,
cache_creation_input_tokens: 20,
cache_read_input_tokens: 10,
};
expect(getTokenCountFromUsage(usage as any)).toBe(180);
});
}
expect(getTokenCountFromUsage(usage as any)).toBe(180)
})
test("handles missing cache fields", () => {
test('handles missing cache fields', () => {
const usage = {
input_tokens: 100,
output_tokens: 50,
};
expect(getTokenCountFromUsage(usage as any)).toBe(150);
});
}
expect(getTokenCountFromUsage(usage as any)).toBe(150)
})
test("handles zero values", () => {
test('handles zero values', () => {
const usage = {
input_tokens: 0,
output_tokens: 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
};
expect(getTokenCountFromUsage(usage as any)).toBe(0);
});
});
}
expect(getTokenCountFromUsage(usage as any)).toBe(0)
})
})
// ─── getTokenUsage ──────────────────────────────────────────────────────
describe("getTokenUsage", () => {
test("returns usage for valid assistant message", () => {
const msg = makeAssistantMessage([{ type: "text", text: "hello" }]);
const usage = getTokenUsage(msg as any);
expect(usage).toBeDefined();
expect(usage!.input_tokens).toBe(100);
});
describe('getTokenUsage', () => {
test('returns usage for valid assistant message', () => {
const msg = makeAssistantMessage([{ type: 'text', text: 'hello' }])
const usage = getTokenUsage(msg as any)
expect(usage).toBeDefined()
expect(usage!.input_tokens).toBe(100)
})
test("returns undefined for user message", () => {
const msg = makeUserMessage("hello");
expect(getTokenUsage(msg as any)).toBeUndefined();
});
test('returns undefined for user message', () => {
const msg = makeUserMessage('hello')
expect(getTokenUsage(msg as any)).toBeUndefined()
})
test("returns undefined for synthetic model", () => {
test('returns undefined for synthetic model', () => {
const msg = makeAssistantMessage(
[{ type: "text", text: "hello" }],
[{ type: 'text', text: 'hello' }],
{ input_tokens: 10, output_tokens: 5 },
"<synthetic>"
);
expect(getTokenUsage(msg as any)).toBeUndefined();
});
});
'<synthetic>',
)
expect(getTokenUsage(msg as any)).toBeUndefined()
})
})
// ─── tokenCountFromLastAPIResponse ──────────────────────────────────────
describe("tokenCountFromLastAPIResponse", () => {
test("returns token count from last assistant message", () => {
describe('tokenCountFromLastAPIResponse', () => {
test('returns token count from last assistant message', () => {
const msgs = [
makeAssistantMessage([{ type: "text", text: "hi" }], {
makeAssistantMessage([{ type: 'text', text: 'hi' }], {
input_tokens: 200,
output_tokens: 100,
cache_creation_input_tokens: 50,
cache_read_input_tokens: 25,
}),
];
expect(tokenCountFromLastAPIResponse(msgs as any)).toBe(375);
});
]
expect(tokenCountFromLastAPIResponse(msgs as any)).toBe(375)
})
test("returns 0 for empty messages", () => {
expect(tokenCountFromLastAPIResponse([])).toBe(0);
});
test('returns 0 for empty messages', () => {
expect(tokenCountFromLastAPIResponse([])).toBe(0)
})
test("skips user messages to find last assistant", () => {
test('skips user messages to find last assistant', () => {
const msgs = [
makeAssistantMessage([{ type: "text", text: "hi" }], {
makeAssistantMessage([{ type: 'text', text: 'hi' }], {
input_tokens: 100,
output_tokens: 50,
}),
makeUserMessage("reply"),
];
expect(tokenCountFromLastAPIResponse(msgs as any)).toBe(150);
});
});
makeUserMessage('reply'),
]
expect(tokenCountFromLastAPIResponse(msgs as any)).toBe(150)
})
})
// ─── messageTokenCountFromLastAPIResponse ───────────────────────────────
describe("messageTokenCountFromLastAPIResponse", () => {
test("returns output_tokens from last assistant", () => {
describe('messageTokenCountFromLastAPIResponse', () => {
test('returns output_tokens from last assistant', () => {
const msgs = [
makeAssistantMessage([{ type: "text", text: "hi" }], {
makeAssistantMessage([{ type: 'text', text: 'hi' }], {
input_tokens: 200,
output_tokens: 75,
}),
];
expect(messageTokenCountFromLastAPIResponse(msgs as any)).toBe(75);
});
]
expect(messageTokenCountFromLastAPIResponse(msgs as any)).toBe(75)
})
test("returns 0 for empty messages", () => {
expect(messageTokenCountFromLastAPIResponse([])).toBe(0);
});
});
test('returns 0 for empty messages', () => {
expect(messageTokenCountFromLastAPIResponse([])).toBe(0)
})
})
// ─── getCurrentUsage ────────────────────────────────────────────────────
describe("getCurrentUsage", () => {
test("returns usage object from last assistant", () => {
describe('getCurrentUsage', () => {
test('returns usage object from last assistant', () => {
const msgs = [
makeAssistantMessage([{ type: "text", text: "hi" }], {
makeAssistantMessage([{ type: 'text', text: 'hi' }], {
input_tokens: 100,
output_tokens: 50,
cache_creation_input_tokens: 10,
cache_read_input_tokens: 5,
}),
];
const usage = getCurrentUsage(msgs as any);
]
const usage = getCurrentUsage(msgs as any)
expect(usage).toEqual({
input_tokens: 100,
output_tokens: 50,
cache_creation_input_tokens: 10,
cache_read_input_tokens: 5,
});
});
})
})
test("returns null for empty messages", () => {
expect(getCurrentUsage([])).toBeNull();
});
test('returns null for empty messages', () => {
expect(getCurrentUsage([])).toBeNull()
})
test("defaults cache fields to 0", () => {
test('defaults cache fields to 0', () => {
const msgs = [
makeAssistantMessage([{ type: "text", text: "hi" }], {
makeAssistantMessage([{ type: 'text', text: 'hi' }], {
input_tokens: 100,
output_tokens: 50,
}),
];
const usage = getCurrentUsage(msgs as any);
expect(usage!.cache_creation_input_tokens).toBe(0);
expect(usage!.cache_read_input_tokens).toBe(0);
});
});
]
const usage = getCurrentUsage(msgs as any)
expect(usage!.cache_creation_input_tokens).toBe(0)
expect(usage!.cache_read_input_tokens).toBe(0)
})
})
// ─── doesMostRecentAssistantMessageExceed200k ───────────────────────────
describe("doesMostRecentAssistantMessageExceed200k", () => {
test("returns false when under 200k", () => {
describe('doesMostRecentAssistantMessageExceed200k', () => {
test('returns false when under 200k', () => {
const msgs = [
makeAssistantMessage([{ type: "text", text: "hi" }], {
makeAssistantMessage([{ type: 'text', text: 'hi' }], {
input_tokens: 1000,
output_tokens: 500,
}),
];
expect(doesMostRecentAssistantMessageExceed200k(msgs as any)).toBe(false);
});
]
expect(doesMostRecentAssistantMessageExceed200k(msgs as any)).toBe(false)
})
test("returns true when over 200k", () => {
test('returns true when over 200k', () => {
const msgs = [
makeAssistantMessage([{ type: "text", text: "hi" }], {
makeAssistantMessage([{ type: 'text', text: 'hi' }], {
input_tokens: 190000,
output_tokens: 15000,
}),
];
expect(doesMostRecentAssistantMessageExceed200k(msgs as any)).toBe(true);
});
]
expect(doesMostRecentAssistantMessageExceed200k(msgs as any)).toBe(true)
})
test("returns false for empty messages", () => {
expect(doesMostRecentAssistantMessageExceed200k([])).toBe(false);
});
});
test('returns false for empty messages', () => {
expect(doesMostRecentAssistantMessageExceed200k([])).toBe(false)
})
})
// ─── getAssistantMessageContentLength ───────────────────────────────────
describe("getAssistantMessageContentLength", () => {
test("counts text content length", () => {
const msg = makeAssistantMessage([{ type: "text", text: "hello" }]);
expect(getAssistantMessageContentLength(msg as any)).toBe(5);
});
describe('getAssistantMessageContentLength', () => {
test('counts text content length', () => {
const msg = makeAssistantMessage([{ type: 'text', text: 'hello' }])
expect(getAssistantMessageContentLength(msg as any)).toBe(5)
})
test("counts multiple blocks", () => {
test('counts multiple blocks', () => {
const msg = makeAssistantMessage([
{ type: "text", text: "hello" },
{ type: "text", text: "world" },
]);
expect(getAssistantMessageContentLength(msg as any)).toBe(10);
});
{ type: 'text', text: 'hello' },
{ type: 'text', text: 'world' },
])
expect(getAssistantMessageContentLength(msg as any)).toBe(10)
})
test("counts thinking content", () => {
test('counts thinking content', () => {
const msg = makeAssistantMessage([
{ type: "thinking", thinking: "let me think" },
]);
expect(getAssistantMessageContentLength(msg as any)).toBe(12);
});
{ type: 'thinking', thinking: 'let me think' },
])
expect(getAssistantMessageContentLength(msg as any)).toBe(12)
})
test("returns 0 for empty content", () => {
const msg = makeAssistantMessage([]);
expect(getAssistantMessageContentLength(msg as any)).toBe(0);
});
test('returns 0 for empty content', () => {
const msg = makeAssistantMessage([])
expect(getAssistantMessageContentLength(msg as any)).toBe(0)
})
test("counts tool_use input", () => {
test('counts tool_use input', () => {
const msg = makeAssistantMessage([
{ type: "tool_use", id: "t1", name: "Bash", input: { command: "ls" } },
]);
expect(getAssistantMessageContentLength(msg as any)).toBeGreaterThan(0);
});
});
{ type: 'tool_use', id: 't1', name: 'Bash', input: { command: 'ls' } },
])
expect(getAssistantMessageContentLength(msg as any)).toBeGreaterThan(0)
})
})

View File

@@ -1,97 +1,97 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
const { treeify } = await import("../treeify");
const { treeify } = await import('../treeify')
describe("treeify", () => {
test("renders flat tree with two keys", () => {
const result = treeify({ a: "value-a", b: "value-b" });
const lines = result.split("\n");
expect(lines.length).toBe(2);
expect(lines[0]).toContain("a");
expect(lines[0]).toContain("value-a");
expect(lines[1]).toContain("b");
expect(lines[1]).toContain("value-b");
});
describe('treeify', () => {
test('renders flat tree with two keys', () => {
const result = treeify({ a: 'value-a', b: 'value-b' })
const lines = result.split('\n')
expect(lines.length).toBe(2)
expect(lines[0]).toContain('a')
expect(lines[0]).toContain('value-a')
expect(lines[1]).toContain('b')
expect(lines[1]).toContain('value-b')
})
test("uses branch character for non-last items", () => {
const result = treeify({ a: "1", b: "2" });
test('uses branch character for non-last items', () => {
const result = treeify({ a: '1', b: '2' })
// First item uses ├ (branch), last uses └ (lastBranch)
expect(result).toContain("├");
expect(result).toContain("└");
});
expect(result).toContain('├')
expect(result).toContain('└')
})
test("uses lastBranch for single item", () => {
const result = treeify({ only: "val" });
expect(result).toContain("└");
expect(result).not.toContain("├");
});
test('uses lastBranch for single item', () => {
const result = treeify({ only: 'val' })
expect(result).toContain('└')
expect(result).not.toContain('├')
})
test("renders nested objects", () => {
const result = treeify({ parent: { child: "val" } });
expect(result).toContain("parent");
expect(result).toContain("child");
expect(result).toContain("val");
});
test('renders nested objects', () => {
const result = treeify({ parent: { child: 'val' } })
expect(result).toContain('parent')
expect(result).toContain('child')
expect(result).toContain('val')
})
test("renders arrays with length", () => {
const result = treeify({ items: ["1", "2", "3"] } as any);
expect(result).toContain("items");
expect(result).toContain("[Array(3)]");
});
test('renders arrays with length', () => {
const result = treeify({ items: ['1', '2', '3'] } as any)
expect(result).toContain('items')
expect(result).toContain('[Array(3)]')
})
test("detects circular references", () => {
const obj: Record<string, unknown> = { name: "root" };
obj.self = obj;
const result = treeify(obj as any);
expect(result).toContain("[Circular]");
});
test('detects circular references', () => {
const obj: Record<string, unknown> = { name: 'root' }
obj.self = obj
const result = treeify(obj as any)
expect(result).toContain('[Circular]')
})
test("returns (empty) for empty object", () => {
const result = treeify({});
expect(result).toBe("(empty)");
});
test('returns (empty) for empty object', () => {
const result = treeify({})
expect(result).toBe('(empty)')
})
test("hideFunctions filters out function values", () => {
const obj = { name: "test", fn: () => {} };
const result = treeify(obj as any, { hideFunctions: true });
expect(result).toContain("name");
expect(result).not.toContain("fn");
});
test('hideFunctions filters out function values', () => {
const obj = { name: 'test', fn: () => {} }
const result = treeify(obj as any, { hideFunctions: true })
expect(result).toContain('name')
expect(result).not.toContain('fn')
})
test("showValues false hides leaf values", () => {
const obj = { name: "test" };
const result = treeify(obj, { showValues: false });
expect(result).toContain("name");
expect(result).not.toContain("test");
});
test('showValues false hides leaf values', () => {
const obj = { name: 'test' }
const result = treeify(obj, { showValues: false })
expect(result).toContain('name')
expect(result).not.toContain('test')
})
test("showValues true shows function as [Function]", () => {
const obj = { fn: () => {} };
const result = treeify(obj as any, { showValues: true });
expect(result).toContain("[Function]");
});
test('showValues true shows function as [Function]', () => {
const obj = { fn: () => {} }
const result = treeify(obj as any, { showValues: true })
expect(result).toContain('[Function]')
})
test("deep nesting produces correct indentation", () => {
const obj = { a: { b: { c: "deep" } } };
const result = treeify(obj);
const lines = result.split("\n");
expect(lines.length).toBe(3);
test('deep nesting produces correct indentation', () => {
const obj = { a: { b: { c: 'deep' } } }
const result = treeify(obj)
const lines = result.split('\n')
expect(lines.length).toBe(3)
// Each level adds indentation
expect(lines[2].length).toBeGreaterThan(lines[1].length);
});
expect(lines[2].length).toBeGreaterThan(lines[1].length)
})
test("handles empty string key with string value", () => {
const obj = { " ": "whitespace-key" };
const result = treeify(obj);
expect(result).toContain("whitespace-key");
});
test('handles empty string key with string value', () => {
const obj = { ' ': 'whitespace-key' }
const result = treeify(obj)
expect(result).toContain('whitespace-key')
})
test("handles mixed object and primitive values", () => {
const obj = { name: "test", nested: { inner: "val" }, count: 5 };
const result = treeify(obj as any);
expect(result).toContain("name");
expect(result).toContain("nested");
expect(result).toContain("inner");
expect(result).toContain("count");
});
});
test('handles mixed object and primitive values', () => {
const obj = { name: 'test', nested: { inner: 'val' }, count: 5 }
const result = treeify(obj as any)
expect(result).toContain('name')
expect(result).toContain('nested')
expect(result).toContain('inner')
expect(result).toContain('count')
})
})

View File

@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
import {
truncatePathMiddle,
@@ -7,185 +7,185 @@ import {
truncateToWidthNoEllipsis,
truncate,
wrapText,
} from "../truncate";
} from '../truncate'
// ─── truncateToWidth ────────────────────────────────────────────────────
describe("truncateToWidth", () => {
test("returns original when within limit", () => {
expect(truncateToWidth("hello", 10)).toBe("hello");
});
describe('truncateToWidth', () => {
test('returns original when within limit', () => {
expect(truncateToWidth('hello', 10)).toBe('hello')
})
test("truncates long string with ellipsis", () => {
const result = truncateToWidth("hello world", 8);
expect(result).toBe("hello w…");
});
test('truncates long string with ellipsis', () => {
const result = truncateToWidth('hello world', 8)
expect(result).toBe('hello w…')
})
test("returns ellipsis for maxWidth 1", () => {
expect(truncateToWidth("hello", 1)).toBe("…");
});
test('returns ellipsis for maxWidth 1', () => {
expect(truncateToWidth('hello', 1)).toBe('…')
})
test("handles empty string", () => {
expect(truncateToWidth("", 10)).toBe("");
});
test('handles empty string', () => {
expect(truncateToWidth('', 10)).toBe('')
})
// ── CJK / wide-character tests ──
test("truncates CJK string at width boundary (2 per char)", () => {
expect(truncateToWidth("你好世界", 4)).toBe("你…");
});
test('truncates CJK string at width boundary (2 per char)', () => {
expect(truncateToWidth('你好世界', 4)).toBe('你…')
})
test("truncates CJK string preserving full characters", () => {
expect(truncateToWidth("你好世界", 6)).toBe("你好…");
});
test('truncates CJK string preserving full characters', () => {
expect(truncateToWidth('你好世界', 6)).toBe('你好…')
})
test("passes through CJK string when within limit", () => {
expect(truncateToWidth("你好", 4)).toBe("你好");
});
test('passes through CJK string when within limit', () => {
expect(truncateToWidth('你好', 4)).toBe('你好')
})
test("handles mixed ASCII + CJK", () => {
expect(truncateToWidth("hello你好", 8)).toBe("hello你…");
});
test('handles mixed ASCII + CJK', () => {
expect(truncateToWidth('hello你好', 8)).toBe('hello你…')
})
test("passes through mixed ASCII + CJK at exact limit", () => {
expect(truncateToWidth("hello你好", 9)).toBe("hello你好");
});
test('passes through mixed ASCII + CJK at exact limit', () => {
expect(truncateToWidth('hello你好', 9)).toBe('hello你好')
})
test("truncates string containing emoji", () => {
const result = truncateToWidth("hello 👋 world", 10);
expect(result).toBe("hello 👋 …");
});
test('truncates string containing emoji', () => {
const result = truncateToWidth('hello 👋 world', 10)
expect(result).toBe('hello 👋 …')
})
test("passes through single emoji at sufficient width", () => {
expect(truncateToWidth("👋", 2)).toBe("👋");
});
});
test('passes through single emoji at sufficient width', () => {
expect(truncateToWidth('👋', 2)).toBe('👋')
})
})
// ─── truncateStartToWidth ───────────────────────────────────────────────
describe("truncateStartToWidth", () => {
test("returns original when within limit", () => {
expect(truncateStartToWidth("hello", 10)).toBe("hello");
});
describe('truncateStartToWidth', () => {
test('returns original when within limit', () => {
expect(truncateStartToWidth('hello', 10)).toBe('hello')
})
test("truncates from start with ellipsis prefix", () => {
expect(truncateStartToWidth("hello world", 8)).toBe("…o world");
});
test('truncates from start with ellipsis prefix', () => {
expect(truncateStartToWidth('hello world', 8)).toBe('…o world')
})
test("returns ellipsis for maxWidth 1", () => {
expect(truncateStartToWidth("hello", 1)).toBe("…");
});
test('returns ellipsis for maxWidth 1', () => {
expect(truncateStartToWidth('hello', 1)).toBe('…')
})
test("truncates CJK from start", () => {
expect(truncateStartToWidth("你好世界", 4)).toBe("…界");
});
test('truncates CJK from start', () => {
expect(truncateStartToWidth('你好世界', 4)).toBe('…界')
})
test("truncates CJK from start preserving characters", () => {
expect(truncateStartToWidth("你好世界", 6)).toBe("…世界");
});
});
test('truncates CJK from start preserving characters', () => {
expect(truncateStartToWidth('你好世界', 6)).toBe('…世界')
})
})
// ─── truncateToWidthNoEllipsis ──────────────────────────────────────────
describe("truncateToWidthNoEllipsis", () => {
test("returns original when within limit", () => {
expect(truncateToWidthNoEllipsis("hello", 10)).toBe("hello");
});
describe('truncateToWidthNoEllipsis', () => {
test('returns original when within limit', () => {
expect(truncateToWidthNoEllipsis('hello', 10)).toBe('hello')
})
test("truncates without ellipsis", () => {
const result = truncateToWidthNoEllipsis("hello world", 5);
expect(result).toBe("hello");
expect(result.includes("…")).toBe(false);
});
test('truncates without ellipsis', () => {
const result = truncateToWidthNoEllipsis('hello world', 5)
expect(result).toBe('hello')
expect(result.includes('…')).toBe(false)
})
test("returns empty for maxWidth 0", () => {
expect(truncateToWidthNoEllipsis("hello", 0)).toBe("");
});
test('returns empty for maxWidth 0', () => {
expect(truncateToWidthNoEllipsis('hello', 0)).toBe('')
})
test("truncates CJK without ellipsis", () => {
expect(truncateToWidthNoEllipsis("你好世界", 4)).toBe("你好");
});
});
test('truncates CJK without ellipsis', () => {
expect(truncateToWidthNoEllipsis('你好世界', 4)).toBe('你好')
})
})
// ─── truncatePathMiddle ─────────────────────────────────────────────────
describe("truncatePathMiddle", () => {
test("returns original when path fits", () => {
expect(truncatePathMiddle("src/index.ts", 50)).toBe("src/index.ts");
});
describe('truncatePathMiddle', () => {
test('returns original when path fits', () => {
expect(truncatePathMiddle('src/index.ts', 50)).toBe('src/index.ts')
})
test("truncates middle of long path", () => {
const path = "src/components/deeply/nested/folder/MyComponent.tsx";
const result = truncatePathMiddle(path, 30);
expect(result).toContain("…");
expect(result.endsWith("MyComponent.tsx")).toBe(true);
});
test('truncates middle of long path', () => {
const path = 'src/components/deeply/nested/folder/MyComponent.tsx'
const result = truncatePathMiddle(path, 30)
expect(result).toContain('…')
expect(result.endsWith('MyComponent.tsx')).toBe(true)
})
test("returns ellipsis for maxLength 0", () => {
expect(truncatePathMiddle("src/index.ts", 0)).toBe("…");
});
test('returns ellipsis for maxLength 0', () => {
expect(truncatePathMiddle('src/index.ts', 0)).toBe('…')
})
test("handles path without slashes", () => {
const result = truncatePathMiddle("verylongfilename.ts", 10);
expect(result).toContain("…");
});
test('handles path without slashes', () => {
const result = truncatePathMiddle('verylongfilename.ts', 10)
expect(result).toContain('…')
})
test("handles short maxLength < 5", () => {
expect(truncatePathMiddle("src/components/foo.ts", 4)).toBe("src…");
});
test('handles short maxLength < 5', () => {
expect(truncatePathMiddle('src/components/foo.ts', 4)).toBe('src…')
})
test("handles very short maxLength 1", () => {
expect(truncatePathMiddle("/a/b", 1)).toBe("…");
});
});
test('handles very short maxLength 1', () => {
expect(truncatePathMiddle('/a/b', 1)).toBe('…')
})
})
// ─── truncate ───────────────────────────────────────────────────────────
describe("truncate", () => {
test("returns original when within limit", () => {
expect(truncate("hello", 10)).toBe("hello");
});
describe('truncate', () => {
test('returns original when within limit', () => {
expect(truncate('hello', 10)).toBe('hello')
})
test("truncates long string", () => {
const result = truncate("hello world foo bar", 10);
expect(result).toContain("…");
});
test('truncates long string', () => {
const result = truncate('hello world foo bar', 10)
expect(result).toContain('…')
})
test("truncates at newline in singleLine mode", () => {
const result = truncate("first line\nsecond line", 50, true);
expect(result).toBe("first line…");
});
test('truncates at newline in singleLine mode', () => {
const result = truncate('first line\nsecond line', 50, true)
expect(result).toBe('first line…')
})
test("does not truncate at newline when singleLine is false", () => {
const result = truncate("first\nsecond", 50, false);
expect(result).toBe("first\nsecond");
});
test('does not truncate at newline when singleLine is false', () => {
const result = truncate('first\nsecond', 50, false)
expect(result).toBe('first\nsecond')
})
test("truncates singleLine when first line exceeds maxWidth", () => {
const result = truncate("a very long first line\nsecond", 10, true);
expect(result).toContain("…");
expect(result).not.toContain("\n");
});
});
test('truncates singleLine when first line exceeds maxWidth', () => {
const result = truncate('a very long first line\nsecond', 10, true)
expect(result).toContain('…')
expect(result).not.toContain('\n')
})
})
// ─── wrapText ───────────────────────────────────────────────────────────
describe("wrapText", () => {
test("wraps text at specified width", () => {
const result = wrapText("hello world", 6);
expect(result.length).toBeGreaterThan(1);
});
describe('wrapText', () => {
test('wraps text at specified width', () => {
const result = wrapText('hello world', 6)
expect(result.length).toBeGreaterThan(1)
})
test("returns single line when text fits", () => {
expect(wrapText("hello", 10)).toEqual(["hello"]);
});
test('returns single line when text fits', () => {
expect(wrapText('hello', 10)).toEqual(['hello'])
})
test("handles empty string", () => {
expect(wrapText("", 10)).toEqual([]);
});
test('handles empty string', () => {
expect(wrapText('', 10)).toEqual([])
})
test("wraps each character on width 1", () => {
const result = wrapText("abc", 1);
expect(result).toEqual(["a", "b", "c"]);
});
});
test('wraps each character on width 1', () => {
const result = wrapText('abc', 1)
expect(result).toEqual(['a', 'b', 'c'])
})
})

View File

@@ -91,7 +91,9 @@ afterEach(async () => {
}
})
async function closeServer(server: ReturnType<typeof createServer>): Promise<void> {
async function closeServer(
server: ReturnType<typeof createServer>,
): Promise<void> {
await new Promise<void>(resolve => {
server.close(() => resolve())
})

View File

@@ -110,9 +110,7 @@ describe('attachUdsResponseReader', () => {
},
})
socket.emitData(
Buffer.from(`\n${JSON.stringify({ type: 'response' })}\n`),
)
socket.emitData(Buffer.from(`\n${JSON.stringify({ type: 'response' })}\n`))
expect(settled).toBe(true)
expect(settledError).toBeUndefined()

View File

@@ -1,76 +1,76 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
import {
matchesNegativeKeyword,
matchesKeepGoingKeyword,
} from "../userPromptKeywords";
} from '../userPromptKeywords'
describe("matchesNegativeKeyword", () => {
describe('matchesNegativeKeyword', () => {
test("matches 'wtf'", () => {
expect(matchesNegativeKeyword("wtf is going on")).toBe(true);
});
expect(matchesNegativeKeyword('wtf is going on')).toBe(true)
})
test("matches 'shit'", () => {
expect(matchesNegativeKeyword("this is shit")).toBe(true);
});
expect(matchesNegativeKeyword('this is shit')).toBe(true)
})
test("matches 'fucking broken'", () => {
expect(matchesNegativeKeyword("this is fucking broken")).toBe(true);
});
expect(matchesNegativeKeyword('this is fucking broken')).toBe(true)
})
test("does not match normal input like 'fix the bug'", () => {
expect(matchesNegativeKeyword("fix the bug")).toBe(false);
});
expect(matchesNegativeKeyword('fix the bug')).toBe(false)
})
test("is case-insensitive", () => {
expect(matchesNegativeKeyword("WTF is this")).toBe(true);
expect(matchesNegativeKeyword("This Sucks")).toBe(true);
});
test('is case-insensitive', () => {
expect(matchesNegativeKeyword('WTF is this')).toBe(true)
expect(matchesNegativeKeyword('This Sucks')).toBe(true)
})
test("matches partial word in sentence", () => {
expect(matchesNegativeKeyword("please help, damn it")).toBe(true);
});
});
test('matches partial word in sentence', () => {
expect(matchesNegativeKeyword('please help, damn it')).toBe(true)
})
})
describe("matchesKeepGoingKeyword", () => {
describe('matchesKeepGoingKeyword', () => {
test("matches exact 'continue'", () => {
expect(matchesKeepGoingKeyword("continue")).toBe(true);
});
expect(matchesKeepGoingKeyword('continue')).toBe(true)
})
test("matches 'keep going'", () => {
expect(matchesKeepGoingKeyword("keep going")).toBe(true);
});
expect(matchesKeepGoingKeyword('keep going')).toBe(true)
})
test("matches 'go on'", () => {
expect(matchesKeepGoingKeyword("go on")).toBe(true);
});
expect(matchesKeepGoingKeyword('go on')).toBe(true)
})
test("does not match 'cont'", () => {
expect(matchesKeepGoingKeyword("cont")).toBe(false);
});
expect(matchesKeepGoingKeyword('cont')).toBe(false)
})
test("does not match empty string", () => {
expect(matchesKeepGoingKeyword("")).toBe(false);
});
test('does not match empty string', () => {
expect(matchesKeepGoingKeyword('')).toBe(false)
})
test("matches within larger sentence 'please continue'", () => {
// 'continue' must be the entire prompt (lowercased), not a substring
expect(matchesKeepGoingKeyword("please continue")).toBe(false);
});
expect(matchesKeepGoingKeyword('please continue')).toBe(false)
})
test("matches 'keep going' in sentence", () => {
expect(matchesKeepGoingKeyword("please keep going")).toBe(true);
});
expect(matchesKeepGoingKeyword('please keep going')).toBe(true)
})
test("matches 'go on' in sentence", () => {
expect(matchesKeepGoingKeyword("yes, go on")).toBe(true);
});
expect(matchesKeepGoingKeyword('yes, go on')).toBe(true)
})
test("is case-insensitive for 'continue'", () => {
expect(matchesKeepGoingKeyword("Continue")).toBe(true);
expect(matchesKeepGoingKeyword("CONTINUE")).toBe(true);
});
expect(matchesKeepGoingKeyword('Continue')).toBe(true)
expect(matchesKeepGoingKeyword('CONTINUE')).toBe(true)
})
test("is case-insensitive for 'keep going'", () => {
expect(matchesKeepGoingKeyword("Keep Going")).toBe(true);
});
});
expect(matchesKeepGoingKeyword('Keep Going')).toBe(true)
})
})

View File

@@ -1,51 +1,51 @@
import { describe, expect, test } from "bun:test";
import { validateUuid, createAgentId } from "../uuid";
import { describe, expect, test } from 'bun:test'
import { validateUuid, createAgentId } from '../uuid'
describe("validateUuid", () => {
test("validates correct UUID", () => {
const result = validateUuid("550e8400-e29b-41d4-a716-446655440000");
expect(result).toBe("550e8400-e29b-41d4-a716-446655440000");
});
describe('validateUuid', () => {
test('validates correct UUID', () => {
const result = validateUuid('550e8400-e29b-41d4-a716-446655440000')
expect(result).toBe('550e8400-e29b-41d4-a716-446655440000')
})
test("validates uppercase UUID", () => {
const result = validateUuid("550E8400-E29B-41D4-A716-446655440000");
expect(result).toBe("550E8400-E29B-41D4-A716-446655440000");
});
test('validates uppercase UUID', () => {
const result = validateUuid('550E8400-E29B-41D4-A716-446655440000')
expect(result).toBe('550E8400-E29B-41D4-A716-446655440000')
})
test("returns null for non-string", () => {
expect(validateUuid(123)).toBeNull();
expect(validateUuid(null)).toBeNull();
expect(validateUuid(undefined)).toBeNull();
});
test('returns null for non-string', () => {
expect(validateUuid(123)).toBeNull()
expect(validateUuid(null)).toBeNull()
expect(validateUuid(undefined)).toBeNull()
})
test("returns null for invalid UUID format", () => {
expect(validateUuid("not-a-uuid")).toBeNull();
expect(validateUuid("550e8400-e29b-41d4-a716")).toBeNull();
expect(validateUuid("550e8400e29b41d4a716446655440000")).toBeNull();
});
test('returns null for invalid UUID format', () => {
expect(validateUuid('not-a-uuid')).toBeNull()
expect(validateUuid('550e8400-e29b-41d4-a716')).toBeNull()
expect(validateUuid('550e8400e29b41d4a716446655440000')).toBeNull()
})
test("returns null for empty string", () => {
expect(validateUuid("")).toBeNull();
});
test('returns null for empty string', () => {
expect(validateUuid('')).toBeNull()
})
test("returns null for UUID with invalid chars", () => {
expect(validateUuid("550e8400-e29b-41d4-a716-44665544000g")).toBeNull();
});
test('returns null for UUID with invalid chars', () => {
expect(validateUuid('550e8400-e29b-41d4-a716-44665544000g')).toBeNull()
})
test("returns null for UUID with leading/trailing whitespace", () => {
expect(validateUuid(" 550e8400-e29b-41d4-a716-446655440000")).toBeNull();
expect(validateUuid("550e8400-e29b-41d4-a716-446655440000 ")).toBeNull();
});
});
test('returns null for UUID with leading/trailing whitespace', () => {
expect(validateUuid(' 550e8400-e29b-41d4-a716-446655440000')).toBeNull()
expect(validateUuid('550e8400-e29b-41d4-a716-446655440000 ')).toBeNull()
})
})
describe("createAgentId", () => {
test("generates id without label in correct format", () => {
const id = createAgentId();
expect(id).toMatch(/^a[0-9a-f]{16}$/);
});
describe('createAgentId', () => {
test('generates id without label in correct format', () => {
const id = createAgentId()
expect(id).toMatch(/^a[0-9a-f]{16}$/)
})
test("generates id with label in correct format", () => {
const id = createAgentId("compact");
expect(id).toMatch(/^acompact-[0-9a-f]{16}$/);
});
});
test('generates id with label in correct format', () => {
const id = createAgentId('compact')
expect(id).toMatch(/^acompact-[0-9a-f]{16}$/)
})
})

View File

@@ -1,116 +1,111 @@
import { describe, expect, test } from "bun:test";
import {
windowsPathToPosixPath,
posixPathToWindowsPath,
} from "../windowsPaths";
import { describe, expect, test } from 'bun:test'
import { windowsPathToPosixPath, posixPathToWindowsPath } from '../windowsPaths'
// ─── windowsPathToPosixPath ────────────────────────────────────────────
describe("windowsPathToPosixPath", () => {
test("converts drive letter path to posix", () => {
expect(windowsPathToPosixPath("C:\\Users\\foo")).toBe("/c/Users/foo");
});
describe('windowsPathToPosixPath', () => {
test('converts drive letter path to posix', () => {
expect(windowsPathToPosixPath('C:\\Users\\foo')).toBe('/c/Users/foo')
})
test("lowercases the drive letter", () => {
expect(windowsPathToPosixPath("D:\\Work\\project")).toBe(
"/d/Work/project"
);
});
test('lowercases the drive letter', () => {
expect(windowsPathToPosixPath('D:\\Work\\project')).toBe('/d/Work/project')
})
test("handles lowercase drive letter input", () => {
expect(windowsPathToPosixPath("e:\\data")).toBe("/e/data");
});
test('handles lowercase drive letter input', () => {
expect(windowsPathToPosixPath('e:\\data')).toBe('/e/data')
})
test("converts UNC path", () => {
expect(windowsPathToPosixPath("\\\\server\\share\\dir")).toBe(
"//server/share/dir"
);
});
test('converts UNC path', () => {
expect(windowsPathToPosixPath('\\\\server\\share\\dir')).toBe(
'//server/share/dir',
)
})
test("converts root drive path", () => {
expect(windowsPathToPosixPath("D:\\")).toBe("/d/");
});
test('converts root drive path', () => {
expect(windowsPathToPosixPath('D:\\')).toBe('/d/')
})
test("converts relative path by flipping backslashes", () => {
expect(windowsPathToPosixPath("src\\main.ts")).toBe("src/main.ts");
});
test('converts relative path by flipping backslashes', () => {
expect(windowsPathToPosixPath('src\\main.ts')).toBe('src/main.ts')
})
test("handles forward slashes in windows drive path", () => {
test('handles forward slashes in windows drive path', () => {
// The regex matches both / and \\ after drive letter
expect(windowsPathToPosixPath("C:/Users/foo")).toBe("/c/Users/foo");
});
expect(windowsPathToPosixPath('C:/Users/foo')).toBe('/c/Users/foo')
})
test("already-posix relative path passes through", () => {
expect(windowsPathToPosixPath("src/main.ts")).toBe("src/main.ts");
});
test('already-posix relative path passes through', () => {
expect(windowsPathToPosixPath('src/main.ts')).toBe('src/main.ts')
})
test("handles deeply nested path", () => {
test('handles deeply nested path', () => {
expect(
windowsPathToPosixPath("C:\\Users\\me\\Documents\\project\\src\\index.ts")
).toBe("/c/Users/me/Documents/project/src/index.ts");
});
});
windowsPathToPosixPath(
'C:\\Users\\me\\Documents\\project\\src\\index.ts',
),
).toBe('/c/Users/me/Documents/project/src/index.ts')
})
})
// ─── posixPathToWindowsPath ────────────────────────────────────────────
describe("posixPathToWindowsPath", () => {
test("converts MSYS2/Git Bash drive path to windows", () => {
expect(posixPathToWindowsPath("/c/Users/foo")).toBe("C:\\Users\\foo");
});
describe('posixPathToWindowsPath', () => {
test('converts MSYS2/Git Bash drive path to windows', () => {
expect(posixPathToWindowsPath('/c/Users/foo')).toBe('C:\\Users\\foo')
})
test("uppercases the drive letter", () => {
expect(posixPathToWindowsPath("/d/Work/project")).toBe(
"D:\\Work\\project"
);
});
test('uppercases the drive letter', () => {
expect(posixPathToWindowsPath('/d/Work/project')).toBe('D:\\Work\\project')
})
test("converts cygdrive path", () => {
expect(posixPathToWindowsPath("/cygdrive/d/work")).toBe("D:\\work");
});
test('converts cygdrive path', () => {
expect(posixPathToWindowsPath('/cygdrive/d/work')).toBe('D:\\work')
})
test("converts cygdrive root path", () => {
expect(posixPathToWindowsPath("/cygdrive/c/")).toBe("C:\\");
});
test('converts cygdrive root path', () => {
expect(posixPathToWindowsPath('/cygdrive/c/')).toBe('C:\\')
})
test("converts UNC posix path to windows UNC", () => {
expect(posixPathToWindowsPath("//server/share/dir")).toBe(
"\\\\server\\share\\dir"
);
});
test('converts UNC posix path to windows UNC', () => {
expect(posixPathToWindowsPath('//server/share/dir')).toBe(
'\\\\server\\share\\dir',
)
})
test("converts root drive posix path", () => {
expect(posixPathToWindowsPath("/d/")).toBe("D:\\");
});
test('converts root drive posix path', () => {
expect(posixPathToWindowsPath('/d/')).toBe('D:\\')
})
test("converts bare drive mount (no trailing slash)", () => {
test('converts bare drive mount (no trailing slash)', () => {
// /d matches the regex ^\/([A-Za-z])(\/|$) where $2 is empty
expect(posixPathToWindowsPath("/d")).toBe("D:\\");
});
expect(posixPathToWindowsPath('/d')).toBe('D:\\')
})
test("converts relative path by flipping forward slashes", () => {
expect(posixPathToWindowsPath("src/main.ts")).toBe("src\\main.ts");
});
test('converts relative path by flipping forward slashes', () => {
expect(posixPathToWindowsPath('src/main.ts')).toBe('src\\main.ts')
})
test("handles already-windows relative path", () => {
test('handles already-windows relative path', () => {
// No leading / or //, just flips / to backslash
expect(posixPathToWindowsPath("foo\\bar")).toBe("foo\\bar");
});
});
expect(posixPathToWindowsPath('foo\\bar')).toBe('foo\\bar')
})
})
// ─── round-trip conversions ────────────────────────────────────────────
describe("round-trip conversions", () => {
test("drive path round-trips windows -> posix -> windows", () => {
const original = "C:\\Users\\foo\\bar";
const posix = windowsPathToPosixPath(original);
const back = posixPathToWindowsPath(posix);
expect(back).toBe(original);
});
describe('round-trip conversions', () => {
test('drive path round-trips windows -> posix -> windows', () => {
const original = 'C:\\Users\\foo\\bar'
const posix = windowsPathToPosixPath(original)
const back = posixPathToWindowsPath(posix)
expect(back).toBe(original)
})
test("drive path round-trips posix -> windows -> posix", () => {
const original = "/c/Users/foo/bar";
const win = posixPathToWindowsPath(original);
const back = windowsPathToPosixPath(win);
expect(back).toBe(original);
});
});
test('drive path round-trips posix -> windows -> posix', () => {
const original = '/c/Users/foo/bar'
const win = posixPathToWindowsPath(original)
const back = windowsPathToPosixPath(win)
expect(back).toBe(original)
})
})

View File

@@ -1,58 +1,58 @@
import { describe, expect, test } from "bun:test";
import { withResolvers } from "../withResolvers";
import { describe, expect, test } from 'bun:test'
import { withResolvers } from '../withResolvers'
describe("withResolvers", () => {
test("returns object with promise, resolve, reject", () => {
const result = withResolvers<string>();
expect(result).toHaveProperty("promise");
expect(result).toHaveProperty("resolve");
expect(result).toHaveProperty("reject");
expect(typeof result.resolve).toBe("function");
expect(typeof result.reject).toBe("function");
});
describe('withResolvers', () => {
test('returns object with promise, resolve, reject', () => {
const result = withResolvers<string>()
expect(result).toHaveProperty('promise')
expect(result).toHaveProperty('resolve')
expect(result).toHaveProperty('reject')
expect(typeof result.resolve).toBe('function')
expect(typeof result.reject).toBe('function')
})
test("promise resolves when resolve is called", async () => {
const { promise, resolve } = withResolvers<string>();
resolve("hello");
const result = await promise;
expect(result).toBe("hello");
});
test('promise resolves when resolve is called', async () => {
const { promise, resolve } = withResolvers<string>()
resolve('hello')
const result = await promise
expect(result).toBe('hello')
})
test("promise rejects when reject is called", async () => {
const { promise, reject } = withResolvers<string>();
reject(new Error("fail"));
await expect(promise).rejects.toThrow("fail");
});
test('promise rejects when reject is called', async () => {
const { promise, reject } = withResolvers<string>()
reject(new Error('fail'))
await expect(promise).rejects.toThrow('fail')
})
test("resolve passes value through", async () => {
const { promise, resolve } = withResolvers<number>();
resolve(42);
expect(await promise).toBe(42);
});
test('resolve passes value through', async () => {
const { promise, resolve } = withResolvers<number>()
resolve(42)
expect(await promise).toBe(42)
})
test("reject passes error through", async () => {
const { promise, reject } = withResolvers<void>();
const err = new Error("custom error");
reject(err);
await expect(promise).rejects.toBe(err);
});
test('reject passes error through', async () => {
const { promise, reject } = withResolvers<void>()
const err = new Error('custom error')
reject(err)
await expect(promise).rejects.toBe(err)
})
test("promise is instanceof Promise", () => {
const { promise } = withResolvers<void>();
expect(promise).toBeInstanceOf(Promise);
});
test('promise is instanceof Promise', () => {
const { promise } = withResolvers<void>()
expect(promise).toBeInstanceOf(Promise)
})
test("works with generic type parameter", async () => {
const { promise, resolve } = withResolvers<{ name: string }>();
resolve({ name: "test" });
const result = await promise;
expect(result.name).toBe("test");
});
test('works with generic type parameter', async () => {
const { promise, resolve } = withResolvers<{ name: string }>()
resolve({ name: 'test' })
const result = await promise
expect(result.name).toBe('test')
})
test("resolve/reject can be called asynchronously", async () => {
const { promise, resolve } = withResolvers<number>();
setTimeout(() => resolve(99), 10);
const result = await promise;
expect(result).toBe(99);
});
});
test('resolve/reject can be called asynchronously', async () => {
const { promise, resolve } = withResolvers<number>()
setTimeout(() => resolve(99), 10)
const result = await promise
expect(result).toBe(99)
})
})

View File

@@ -1,85 +1,85 @@
import { describe, expect, test } from "bun:test";
import { generateWordSlug, generateShortWordSlug } from "../words";
import { describe, expect, test } from 'bun:test'
import { generateWordSlug, generateShortWordSlug } from '../words'
describe("generateWordSlug", () => {
test("returns three-part hyphenated slug", () => {
const slug = generateWordSlug();
const parts = slug.split("-");
expect(parts.length).toBe(3);
});
describe('generateWordSlug', () => {
test('returns three-part hyphenated slug', () => {
const slug = generateWordSlug()
const parts = slug.split('-')
expect(parts.length).toBe(3)
})
test("all parts are non-empty", () => {
test('all parts are non-empty', () => {
for (let i = 0; i < 10; i++) {
const slug = generateWordSlug();
const parts = slug.split("-");
const slug = generateWordSlug()
const parts = slug.split('-')
for (const part of parts) {
expect(part.length).toBeGreaterThan(0);
expect(part.length).toBeGreaterThan(0)
}
}
});
})
test("all parts are lowercase", () => {
test('all parts are lowercase', () => {
for (let i = 0; i < 10; i++) {
const slug = generateWordSlug();
expect(slug).toBe(slug.toLowerCase());
const slug = generateWordSlug()
expect(slug).toBe(slug.toLowerCase())
}
});
})
test("no consecutive hyphens", () => {
test('no consecutive hyphens', () => {
for (let i = 0; i < 10; i++) {
const slug = generateWordSlug();
expect(slug).not.toContain("--");
const slug = generateWordSlug()
expect(slug).not.toContain('--')
}
});
})
test("multiple calls produce varied results", () => {
const slugs = new Set<string>();
test('multiple calls produce varied results', () => {
const slugs = new Set<string>()
for (let i = 0; i < 20; i++) {
slugs.add(generateWordSlug());
slugs.add(generateWordSlug())
}
// With 50+ adjectives × 50+ verbs × 50+ nouns, 20 calls should produce mostly unique slugs
expect(slugs.size).toBeGreaterThan(10);
});
expect(slugs.size).toBeGreaterThan(10)
})
test("slug matches adjective-verb-noun pattern", () => {
const slug = generateWordSlug();
expect(slug).toMatch(/^[a-z]+-[a-z]+-[a-z]+$/);
});
});
test('slug matches adjective-verb-noun pattern', () => {
const slug = generateWordSlug()
expect(slug).toMatch(/^[a-z]+-[a-z]+-[a-z]+$/)
})
})
describe("generateShortWordSlug", () => {
test("returns two-part hyphenated slug", () => {
const slug = generateShortWordSlug();
const parts = slug.split("-");
expect(parts.length).toBe(2);
});
describe('generateShortWordSlug', () => {
test('returns two-part hyphenated slug', () => {
const slug = generateShortWordSlug()
const parts = slug.split('-')
expect(parts.length).toBe(2)
})
test("all parts are non-empty", () => {
test('all parts are non-empty', () => {
for (let i = 0; i < 10; i++) {
const slug = generateShortWordSlug();
const parts = slug.split("-");
const slug = generateShortWordSlug()
const parts = slug.split('-')
for (const part of parts) {
expect(part.length).toBeGreaterThan(0);
expect(part.length).toBeGreaterThan(0)
}
}
});
})
test("all parts are lowercase", () => {
test('all parts are lowercase', () => {
for (let i = 0; i < 10; i++) {
const slug = generateShortWordSlug();
expect(slug).toBe(slug.toLowerCase());
const slug = generateShortWordSlug()
expect(slug).toBe(slug.toLowerCase())
}
});
})
test("slug matches adjective-noun pattern", () => {
const slug = generateShortWordSlug();
expect(slug).toMatch(/^[a-z]+-[a-z]+$/);
});
test('slug matches adjective-noun pattern', () => {
const slug = generateShortWordSlug()
expect(slug).toMatch(/^[a-z]+-[a-z]+$/)
})
test("no consecutive hyphens", () => {
test('no consecutive hyphens', () => {
for (let i = 0; i < 10; i++) {
const slug = generateShortWordSlug();
expect(slug).not.toContain("--");
const slug = generateShortWordSlug()
expect(slug).not.toContain('--')
}
});
});
})
})

View File

@@ -1,84 +1,84 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
import {
getXDGStateHome,
getXDGCacheHome,
getXDGDataHome,
getUserBinDir,
} from "../xdg";
} from '../xdg'
describe("getXDGStateHome", () => {
test("returns ~/.local/state by default", () => {
const result = getXDGStateHome({ homedir: "/home/user" });
expect(result).toBe("/home/user/.local/state");
});
describe('getXDGStateHome', () => {
test('returns ~/.local/state by default', () => {
const result = getXDGStateHome({ homedir: '/home/user' })
expect(result).toBe('/home/user/.local/state')
})
test("respects XDG_STATE_HOME env var", () => {
test('respects XDG_STATE_HOME env var', () => {
const result = getXDGStateHome({
homedir: "/home/user",
env: { XDG_STATE_HOME: "/custom/state" },
});
expect(result).toBe("/custom/state");
});
homedir: '/home/user',
env: { XDG_STATE_HOME: '/custom/state' },
})
expect(result).toBe('/custom/state')
})
test("uses custom homedir from options", () => {
const result = getXDGStateHome({ homedir: "/opt/home" });
expect(result).toBe("/opt/home/.local/state");
});
});
test('uses custom homedir from options', () => {
const result = getXDGStateHome({ homedir: '/opt/home' })
expect(result).toBe('/opt/home/.local/state')
})
})
describe("getXDGCacheHome", () => {
test("returns ~/.cache by default", () => {
const result = getXDGCacheHome({ homedir: "/home/user" });
expect(result).toBe("/home/user/.cache");
});
describe('getXDGCacheHome', () => {
test('returns ~/.cache by default', () => {
const result = getXDGCacheHome({ homedir: '/home/user' })
expect(result).toBe('/home/user/.cache')
})
test("respects XDG_CACHE_HOME env var", () => {
test('respects XDG_CACHE_HOME env var', () => {
const result = getXDGCacheHome({
homedir: "/home/user",
env: { XDG_CACHE_HOME: "/tmp/cache" },
});
expect(result).toBe("/tmp/cache");
});
});
homedir: '/home/user',
env: { XDG_CACHE_HOME: '/tmp/cache' },
})
expect(result).toBe('/tmp/cache')
})
})
describe("getXDGDataHome", () => {
test("returns ~/.local/share by default", () => {
const result = getXDGDataHome({ homedir: "/home/user" });
expect(result).toBe("/home/user/.local/share");
});
describe('getXDGDataHome', () => {
test('returns ~/.local/share by default', () => {
const result = getXDGDataHome({ homedir: '/home/user' })
expect(result).toBe('/home/user/.local/share')
})
test("respects XDG_DATA_HOME env var", () => {
test('respects XDG_DATA_HOME env var', () => {
const result = getXDGDataHome({
homedir: "/home/user",
env: { XDG_DATA_HOME: "/custom/data" },
});
expect(result).toBe("/custom/data");
});
});
homedir: '/home/user',
env: { XDG_DATA_HOME: '/custom/data' },
})
expect(result).toBe('/custom/data')
})
})
describe("getUserBinDir", () => {
test("returns ~/.local/bin", () => {
const result = getUserBinDir({ homedir: "/home/user" });
expect(result).toBe("/home/user/.local/bin");
});
describe('getUserBinDir', () => {
test('returns ~/.local/bin', () => {
const result = getUserBinDir({ homedir: '/home/user' })
expect(result).toBe('/home/user/.local/bin')
})
test("uses custom homedir from options", () => {
const result = getUserBinDir({ homedir: "/opt/me" });
expect(result).toBe("/opt/me/.local/bin");
});
});
test('uses custom homedir from options', () => {
const result = getUserBinDir({ homedir: '/opt/me' })
expect(result).toBe('/opt/me/.local/bin')
})
})
describe("path construction", () => {
test("all paths end with correct subdirectory", () => {
const home = "/home/test";
expect(getXDGStateHome({ homedir: home })).toMatch(/\.local\/state$/);
expect(getXDGCacheHome({ homedir: home })).toMatch(/\.cache$/);
expect(getXDGDataHome({ homedir: home })).toMatch(/\.local\/share$/);
expect(getUserBinDir({ homedir: home })).toMatch(/\.local\/bin$/);
});
describe('path construction', () => {
test('all paths end with correct subdirectory', () => {
const home = '/home/test'
expect(getXDGStateHome({ homedir: home })).toMatch(/\.local\/state$/)
expect(getXDGCacheHome({ homedir: home })).toMatch(/\.cache$/)
expect(getXDGDataHome({ homedir: home })).toMatch(/\.local\/share$/)
expect(getUserBinDir({ homedir: home })).toMatch(/\.local\/bin$/)
})
test("respects HOME via homedir override", () => {
const result = getXDGStateHome({ homedir: "/Users/me" });
expect(result).toBe("/Users/me/.local/state");
});
});
test('respects HOME via homedir override', () => {
const result = getXDGStateHome({ homedir: '/Users/me' })
expect(result).toBe('/Users/me/.local/state')
})
})

View File

@@ -1,42 +1,42 @@
import { describe, expect, test } from "bun:test";
import { escapeXml, escapeXmlAttr } from "../xml";
import { describe, expect, test } from 'bun:test'
import { escapeXml, escapeXmlAttr } from '../xml'
describe("escapeXml", () => {
test("escapes ampersand", () => {
expect(escapeXml("a & b")).toBe("a &amp; b");
});
describe('escapeXml', () => {
test('escapes ampersand', () => {
expect(escapeXml('a & b')).toBe('a &amp; b')
})
test("escapes less-than", () => {
expect(escapeXml("<div>")).toBe("&lt;div&gt;");
});
test('escapes less-than', () => {
expect(escapeXml('<div>')).toBe('&lt;div&gt;')
})
test("escapes greater-than", () => {
expect(escapeXml("a > b")).toBe("a &gt; b");
});
test('escapes greater-than', () => {
expect(escapeXml('a > b')).toBe('a &gt; b')
})
test("escapes multiple special chars", () => {
expect(escapeXml("<a & b>")).toBe("&lt;a &amp; b&gt;");
});
test('escapes multiple special chars', () => {
expect(escapeXml('<a & b>')).toBe('&lt;a &amp; b&gt;')
})
test("returns empty string unchanged", () => {
expect(escapeXml("")).toBe("");
});
test('returns empty string unchanged', () => {
expect(escapeXml('')).toBe('')
})
test("returns normal text unchanged", () => {
expect(escapeXml("hello world")).toBe("hello world");
});
});
test('returns normal text unchanged', () => {
expect(escapeXml('hello world')).toBe('hello world')
})
})
describe("escapeXmlAttr", () => {
test("escapes double quotes", () => {
expect(escapeXmlAttr('say "hello"')).toBe("say &quot;hello&quot;");
});
describe('escapeXmlAttr', () => {
test('escapes double quotes', () => {
expect(escapeXmlAttr('say "hello"')).toBe('say &quot;hello&quot;')
})
test("escapes single quotes", () => {
expect(escapeXmlAttr("it's")).toBe("it&apos;s");
});
test('escapes single quotes', () => {
expect(escapeXmlAttr("it's")).toBe('it&apos;s')
})
test("escapes all special chars", () => {
expect(escapeXmlAttr('<a & "b">')).toBe("&lt;a &amp; &quot;b&quot;&gt;");
});
});
test('escapes all special chars', () => {
expect(escapeXmlAttr('<a & "b">')).toBe('&lt;a &amp; &quot;b&quot;&gt;')
})
})

View File

@@ -1,73 +1,73 @@
import { describe, expect, test } from "bun:test";
import z from "zod/v4";
import { zodToJsonSchema } from "../zodToJsonSchema";
import { describe, expect, test } from 'bun:test'
import z from 'zod/v4'
import { zodToJsonSchema } from '../zodToJsonSchema'
describe("zodToJsonSchema", () => {
test("converts string schema", () => {
const schema = z.string();
const result = zodToJsonSchema(schema);
expect(result.type).toBe("string");
});
describe('zodToJsonSchema', () => {
test('converts string schema', () => {
const schema = z.string()
const result = zodToJsonSchema(schema)
expect(result.type).toBe('string')
})
test("converts number schema", () => {
const schema = z.number();
const result = zodToJsonSchema(schema);
expect(result.type).toBe("number");
});
test('converts number schema', () => {
const schema = z.number()
const result = zodToJsonSchema(schema)
expect(result.type).toBe('number')
})
test("converts object schema with properties", () => {
test('converts object schema with properties', () => {
const schema = z.object({
name: z.string(),
age: z.number(),
});
const result = zodToJsonSchema(schema);
expect(result.type).toBe("object");
expect(result.properties).toBeDefined();
expect((result.properties as any).name).toEqual({ type: "string" });
expect((result.properties as any).age).toEqual({ type: "number" });
});
})
const result = zodToJsonSchema(schema)
expect(result.type).toBe('object')
expect(result.properties).toBeDefined()
expect((result.properties as any).name).toEqual({ type: 'string' })
expect((result.properties as any).age).toEqual({ type: 'number' })
})
test("converts enum schema", () => {
const schema = z.enum(["a", "b", "c"]);
const result = zodToJsonSchema(schema);
expect(result.enum).toEqual(["a", "b", "c"]);
});
test('converts enum schema', () => {
const schema = z.enum(['a', 'b', 'c'])
const result = zodToJsonSchema(schema)
expect(result.enum).toEqual(['a', 'b', 'c'])
})
test("converts optional fields", () => {
test('converts optional fields', () => {
const schema = z.object({
required: z.string(),
optional: z.string().optional(),
});
const result = zodToJsonSchema(schema);
expect(result.required).toEqual(["required"]);
expect(result.required).not.toContain("optional");
});
})
const result = zodToJsonSchema(schema)
expect(result.required).toEqual(['required'])
expect(result.required).not.toContain('optional')
})
test("caches results for same schema reference", () => {
const schema = z.string();
const first = zodToJsonSchema(schema);
const second = zodToJsonSchema(schema);
expect(first).toBe(second); // same reference (cached)
});
test('caches results for same schema reference', () => {
const schema = z.string()
const first = zodToJsonSchema(schema)
const second = zodToJsonSchema(schema)
expect(first).toBe(second) // same reference (cached)
})
test("different schemas get different results", () => {
const s1 = z.string();
const s2 = z.number();
const r1 = zodToJsonSchema(s1);
const r2 = zodToJsonSchema(s2);
expect(r1).not.toBe(r2);
expect(r1.type).not.toBe(r2.type);
});
test('different schemas get different results', () => {
const s1 = z.string()
const s2 = z.number()
const r1 = zodToJsonSchema(s1)
const r2 = zodToJsonSchema(s2)
expect(r1).not.toBe(r2)
expect(r1.type).not.toBe(r2.type)
})
test("converts array schema", () => {
const schema = z.array(z.string());
const result = zodToJsonSchema(schema);
expect(result.type).toBe("array");
expect((result.items as any).type).toBe("string");
});
test('converts array schema', () => {
const schema = z.array(z.string())
const result = zodToJsonSchema(schema)
expect(result.type).toBe('array')
expect((result.items as any).type).toBe('string')
})
test("converts boolean schema", () => {
const result = zodToJsonSchema(z.boolean());
expect(result.type).toBe("boolean");
});
});
test('converts boolean schema', () => {
const result = zodToJsonSchema(z.boolean())
expect(result.type).toBe('boolean')
})
})