test: add unit tests for core modules (#35)
CI / check (pull_request) Failing after 1m39s

Cover high-priority untested modules:
- util: base32, result, refs-field, storage-root, log-tag
- util-agent: storage (normalizeWorkflowConfig, resolveStorageRoot), run (parseArgv)
- agent-builtin: tools (read-file, write-file, run-command), session, detail

627 → 719 tests (+92), all passing.

Refs #35
This commit is contained in:
2026-06-04 04:35:33 +00:00
parent c3ec4ac6df
commit 06e959e7a5
14 changed files with 873 additions and 0 deletions
+130
View File
@@ -0,0 +1,130 @@
import { describe, expect, it } from "vitest";
import {
CROCKFORD_BASE32_ALPHABET,
encodeCrockfordBase32Bits,
decodeCrockfordBase32Bits,
encodeUint64AsCrockford,
decodeCrockfordToUint64,
} from "../src/base32.js";
describe("CROCKFORD_BASE32_ALPHABET", () => {
it("has exactly 32 characters", () => {
expect(CROCKFORD_BASE32_ALPHABET).toHaveLength(32);
});
it("excludes I, L, O, U", () => {
expect(CROCKFORD_BASE32_ALPHABET).not.toContain("I");
expect(CROCKFORD_BASE32_ALPHABET).not.toContain("L");
expect(CROCKFORD_BASE32_ALPHABET).not.toContain("O");
expect(CROCKFORD_BASE32_ALPHABET).not.toContain("U");
});
});
describe("encodeCrockfordBase32Bits / decodeCrockfordBase32Bits", () => {
it("roundtrips zero with bitLength=5", () => {
const encoded = encodeCrockfordBase32Bits(0n, 5);
expect(encoded).toBe("0");
const decoded = decodeCrockfordBase32Bits(encoded, 5);
expect(decoded).toEqual({ ok: true, value: 0n });
});
it("roundtrips value 31 with bitLength=5", () => {
const encoded = encodeCrockfordBase32Bits(31n, 5);
expect(encoded).toBe("Z");
const decoded = decodeCrockfordBase32Bits(encoded, 5);
expect(decoded).toEqual({ ok: true, value: 31n });
});
it("roundtrips with bitLength=10", () => {
const encoded = encodeCrockfordBase32Bits(1023n, 10);
expect(encoded).toBe("ZZ");
const decoded = decodeCrockfordBase32Bits(encoded, 10);
expect(decoded).toEqual({ ok: true, value: 1023n });
});
it("roundtrips with non-multiple-of-5 bitLength", () => {
const value = 255n; // 8 bits
const encoded = encodeCrockfordBase32Bits(value, 8);
expect(encoded).toHaveLength(2); // 8 bits -> 10 bits padded -> 2 chars
const decoded = decodeCrockfordBase32Bits(encoded, 8);
expect(decoded).toEqual({ ok: true, value });
});
it("roundtrips large value", () => {
const value = (1n << 64n) - 1n;
const encoded = encodeCrockfordBase32Bits(value, 64);
const decoded = decodeCrockfordBase32Bits(encoded, 64);
expect(decoded).toEqual({ ok: true, value });
});
it("throws on bitLength <= 0", () => {
expect(() => encodeCrockfordBase32Bits(0n, 0)).toThrow("bitLength must be positive");
expect(() => encodeCrockfordBase32Bits(0n, -1)).toThrow("bitLength must be positive");
});
it("returns error on decode with bitLength <= 0", () => {
const result = decodeCrockfordBase32Bits("0", 0);
expect(result.ok).toBe(false);
});
it("returns error on invalid character", () => {
const result = decodeCrockfordBase32Bits("U", 5);
expect(result.ok).toBe(false);
});
it("returns error on wrong encoded length", () => {
const result = decodeCrockfordBase32Bits("00", 5);
expect(result.ok).toBe(false);
});
it("handles lowercase input on decode", () => {
const encoded = encodeCrockfordBase32Bits(10n, 5);
const decoded = decodeCrockfordBase32Bits(encoded.toLowerCase(), 5);
expect(decoded).toEqual({ ok: true, value: 10n });
});
});
describe("encodeUint64AsCrockford / decodeCrockfordToUint64", () => {
it("encodes to 13 characters", () => {
expect(encodeUint64AsCrockford(0n)).toHaveLength(13);
expect(encodeUint64AsCrockford(1n)).toHaveLength(13);
});
it("roundtrips 0n", () => {
const encoded = encodeUint64AsCrockford(0n);
expect(encoded).toBe("0000000000000");
const decoded = decodeCrockfordToUint64(encoded);
expect(decoded).toEqual({ ok: true, value: 0n });
});
it("roundtrips max uint64", () => {
const max = (1n << 64n) - 1n;
const encoded = encodeUint64AsCrockford(max);
const decoded = decodeCrockfordToUint64(encoded);
expect(decoded).toEqual({ ok: true, value: max });
});
it("roundtrips arbitrary value", () => {
const value = 0xDEAD_BEEF_CAFE_BABEn;
const encoded = encodeUint64AsCrockford(value);
const decoded = decodeCrockfordToUint64(encoded);
expect(decoded).toEqual({ ok: true, value });
});
it("masks values beyond 64 bits", () => {
const over = (1n << 64n) + 42n;
const encoded = encodeUint64AsCrockford(over);
const decoded = decodeCrockfordToUint64(encoded);
expect(decoded).toEqual({ ok: true, value: 42n });
});
it("returns error for invalid input", () => {
const result = decodeCrockfordToUint64("!!!");
expect(result.ok).toBe(false);
});
it("returns error for wrong length", () => {
const result = decodeCrockfordToUint64("000");
expect(result.ok).toBe(false);
});
});
+38
View File
@@ -0,0 +1,38 @@
import { describe, it, expect } from 'vitest';
import { assertValidLogTag } from '../src/process-logger/log-tag.js';
describe('assertValidLogTag', () => {
it('accepts valid 8-char Crockford Base32 tags', () => {
expect(() => assertValidLogTag('0123ABCD')).not.toThrow();
expect(() => assertValidLogTag('VWXYZ789')).not.toThrow();
expect(() => assertValidLogTag('00000000')).not.toThrow();
expect(() => assertValidLogTag('ZZZZZZZZ')).not.toThrow();
});
it('accepts lowercase (converted via toUpperCase)', () => {
expect(() => assertValidLogTag('abcdefgh')).not.toThrow();
expect(() => assertValidLogTag('0a1b2c3d')).not.toThrow();
});
it('throws on too short', () => {
expect(() => assertValidLogTag('1234567')).toThrow();
expect(() => assertValidLogTag('')).toThrow();
});
it('throws on too long', () => {
expect(() => assertValidLogTag('123456789')).toThrow();
});
it('throws on invalid chars I, L, O, U', () => {
expect(() => assertValidLogTag('IIIIIIII')).toThrow();
expect(() => assertValidLogTag('LLLLLLLL')).toThrow();
expect(() => assertValidLogTag('OOOOOOOO')).toThrow();
expect(() => assertValidLogTag('UUUUUUUU')).toThrow();
});
it('throws on special characters', () => {
expect(() => assertValidLogTag('1234567!')).toThrow();
expect(() => assertValidLogTag('ABCD-EFG')).toThrow();
expect(() => assertValidLogTag('ABCD EFG')).toThrow();
});
});
@@ -0,0 +1,40 @@
import { describe, it, expect } from 'vitest';
import { mergeRefsWithContentHash, normalizeRefsField } from '../src/refs-field.js';
describe('mergeRefsWithContentHash', () => {
it('appends a new content hash', () => {
expect(mergeRefsWithContentHash(['a', 'b'], 'c')).toEqual(['a', 'b', 'c']);
});
it('skips duplicate content hash', () => {
expect(mergeRefsWithContentHash(['a', 'b'], 'b')).toEqual(['a', 'b']);
});
it('preserves order', () => {
expect(mergeRefsWithContentHash(['x', 'y'], 'z')).toEqual(['x', 'y', 'z']);
});
it('handles empty refs', () => {
expect(mergeRefsWithContentHash([], 'a')).toEqual(['a']);
});
});
describe('normalizeRefsField', () => {
it('returns empty array for non-array', () => {
expect(normalizeRefsField(null)).toEqual([]);
expect(normalizeRefsField(undefined)).toEqual([]);
expect(normalizeRefsField(42)).toEqual([]);
});
it('passes through string array', () => {
expect(normalizeRefsField(['a', 'b'])).toEqual(['a', 'b']);
});
it('filters non-strings from mixed array', () => {
expect(normalizeRefsField(['a', 1, 'b', null])).toEqual(['a', 'b']);
});
it('handles empty array', () => {
expect(normalizeRefsField([])).toEqual([]);
});
});
+51
View File
@@ -0,0 +1,51 @@
import { describe, it, expect } from 'vitest';
import { ok, err } from '../src/result.js';
describe('result', () => {
describe('ok', () => {
it('wraps a value', () => {
const r = ok(42);
expect(r).toEqual({ ok: true, value: 42 });
});
it('wraps a string value', () => {
const r = ok('hello');
expect(r.ok).toBe(true);
if (r.ok) expect(r.value).toBe('hello');
});
});
describe('err', () => {
it('wraps an error', () => {
const r = err('fail');
expect(r).toEqual({ ok: false, error: 'fail' });
});
it('wraps an Error object', () => {
const e = new Error('boom');
const r = err(e);
expect(r.ok).toBe(false);
if (!r.ok) expect(r.error).toBe(e);
});
});
describe('type narrowing', () => {
it('narrows ok result', () => {
const r = ok(10) as ReturnType<typeof ok<number>> | ReturnType<typeof err<string>>;
if (r.ok) {
expect(r.value).toBe(10);
} else {
expect.unreachable();
}
});
it('narrows err result', () => {
const r = err('bad') as ReturnType<typeof ok<number>> | ReturnType<typeof err<string>>;
if (!r.ok) {
expect(r.error).toBe('bad');
} else {
expect.unreachable();
}
});
});
});
@@ -0,0 +1,25 @@
import { describe, it, expect } from 'vitest';
import { homedir } from 'node:os';
import { getDefaultStorageRoot, getDefaultWorkflowStorageRoot, getGlobalCasDir } from '../src/storage-root.js';
describe('getDefaultStorageRoot', () => {
it('returns homedir + /.uwf', () => {
expect(getDefaultStorageRoot()).toBe(homedir() + '/.uwf');
});
});
describe('getDefaultWorkflowStorageRoot', () => {
it('returns same as getDefaultStorageRoot (deprecated alias)', () => {
expect(getDefaultWorkflowStorageRoot()).toBe(getDefaultStorageRoot());
});
});
describe('getGlobalCasDir', () => {
it('appends /cas to given storage root', () => {
expect(getGlobalCasDir('/tmp/test')).toBe('/tmp/test/cas');
});
it('falls back to default when undefined', () => {
expect(getGlobalCasDir(undefined)).toBe(homedir() + '/.uwf/cas');
});
});