Migrate sium to CodeError + add ModuleSeed/errCode builders

Pilot of phase 4 plus a libs/errs API change introduced after the
user pointed out that `code('buss::disposed')` repeats the module
name redundantly across every declaration.

libs/errs additions:

- New `ModuleSeed` branded type — string of the form `'<module>::'`
  produced once per module.
- `moduleSeed(module)` factory: validates the module name and returns
  a `ModuleSeed`.
- `errCode(parent, segment)` builder: composes a child `ErrCode` from
  a `ModuleSeed` (uses `::` separator) or another `ErrCode` (uses `.`
  separator). The builder picks the right separator automatically.
- `isModuleSeed(value)` discriminator.
- `matches(err, family)` now accepts `ModuleSeed | ErrCode`. Passing
  a seed matches any error from that module; passing a code matches
  hierarchically within the same module. Replaces the
  `matchesModule(err, 'buss')` helper introduced earlier in this
  session — that helper was redundant once seeds entered the API.

Sium pilot:

- arts/sium/consts.ts: declares `SIUM_ERR = moduleSeed('sium')` plus
  the two child codes via `errCode(SIUM_ERR, 'validation')` and
  `errCode(SIUM_ERR, 'async_schema')`. Module name appears exactly
  once.
- arts/sium/core/types.ts: `SiumValidationError` and
  `SiumAsyncSchemaError` extend `CodeError`. Hardcoded
  `this.name = 'SiumValidationError'` literals removed. Subclass-
  specific fields (`issues`, `schemaKind`) preserved.
- Adds `isSiumValidationError` / `isSiumAsyncSchemaError` guards
  (closes audit-1-5 section 3.2 for sium).
- 12 tests in arts/sium/test/errors.test.ts cover subclass shape,
  identity, `matches` against seed and exact code, and the fluent
  decorator chain.

docs/conventions.md updated:

- Section 4 rewritten around the `moduleSeed`/`errCode` pattern.
- "Family root" wording removed (was misleading — `BUSS_ERR='buss::base'`
  was a sibling, not an ancestor of its module's codes).
- `matches` documented as accepting either a seed or an ErrCode.

Verification: svelte-check 1403 files / 0 errors. Server 1315 tests
(+10 from previous), client 19 tests — all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
master
dev 5 months ago
parent 38160565b2
commit a849d00bff

@ -129,25 +129,46 @@ When in doubt, propose the addition here before introducing it.
## 4. Error codes (`ErrCode`) — special case under rule 2 ## 4. Error codes (`ErrCode`) — special case under rule 2
Error codes from `$libs/errs` follow rule 2 with category `ERR`: Error codes from `$libs/errs` follow rule 2 with category `ERR`. Each
module declares one `<MOD>_ERR` seed and builds the individual codes
from it via `errCode(parent, segment)` so the module string never
appears as a literal in error declarations:
```ts ```ts
import { code, type ErrCode } from '$libs/errs'; import { errCode, moduleSeed, type ErrCode, type ModuleSeed } from '$libs/errs';
export const BUSS_ERR: ErrCode = code('buss::base'); // family root export const BUSS_ERR: ModuleSeed = moduleSeed('buss'); // 'buss::'
export const BUSS_ERR_DISPOSED: ErrCode = code('buss::disposed'); export const BUSS_ERR_DISPOSED: ErrCode = errCode(BUSS_ERR, 'disposed'); // 'buss::disposed'
export const BUSS_ERR_INVALID_PAYLOAD: ErrCode = code('buss::invalid_payload'); export const BUSS_ERR_INVALID_PAYLOAD: ErrCode = errCode(BUSS_ERR, 'invalid_payload');
export const BUSS_ERR_LISTENER: ErrCode = code('buss::listener'); // sub-family root export const BUSS_ERR_LISTENER: ErrCode = errCode(BUSS_ERR, 'listener'); // 'buss::listener'
export const BUSS_ERR_LISTENER_FAILED: ErrCode = code('buss::listener.failed'); export const BUSS_ERR_LISTENER_FAILED: ErrCode = errCode(BUSS_ERR_LISTENER, 'failed'); // 'buss::listener.failed'
``` ```
The string value's path segments mirror the constant's `<NAME>` part: The constant's `<NAME>` part mirrors the path segments inside the
`BUSS_ERR_LISTENER_FAILED` ↔ `'buss::listener.failed'`. runtime value: `BUSS_ERR_LISTENER_FAILED` ↔ `'buss::listener.failed'`.
The module name lives in exactly one place: `moduleSeed('buss')`.
The `::` separator inside the string distinguishes module from The `::` separator distinguishes module from hierarchy. `errCode`
hierarchy. The constant uses `_` as the syntactic separator (rule 2). picks the right separator automatically — `::` after a seed, `.`
A helper `codeToLangPath(c)` converts `'buss::listener.failed'` → between segments. A helper `codeToLangPath(c)` converts
`'buss.listener.failed'` for the i18n path. `'buss::listener.failed'` → `'buss.listener.failed'` for the i18n
path.
### Family matching
A single predicate `matches(err, family)` from `$libs/errs` covers
both common cases. `family` can be:
- A **module seed** (`BUSS_ERR`) — matches any error from that
module regardless of hierarchy.
- An **`ErrCode`** — matches the code itself or any hierarchical
descendant within the same module.
```ts
matches(err, BUSS_ERR_DISPOSED) // exact code
matches(err, BUSS_ERR_LISTENER) // any descendant of buss::listener
matches(err, BUSS_ERR) // any error declared by buss
```
--- ---

@ -1,3 +1,5 @@
import { errCode, moduleSeed, type ErrCode, type ModuleSeed } from '$libs/errs';
export const SIUM_MODULE = 'sium'; export const SIUM_MODULE = 'sium';
export const SIUM_STANDARD_VENDOR = 'sium'; export const SIUM_STANDARD_VENDOR = 'sium';
@ -5,3 +7,13 @@ export const SIUM_DIAGNOSTIC_EVENTS = {
VALIDATION_FAILED: 'sium.validation_failed', VALIDATION_FAILED: 'sium.validation_failed',
RESOLVE_FALLBACK: 'sium.resolve_fallback' RESOLVE_FALLBACK: 'sium.resolve_fallback'
} as const; } as const;
// ── Error codes ────────────────────────────────────────────────────────
//
// Single source of truth for sium error identity. Used both as the
// `name` and `code` of every `CodeError` thrown by sium, and as the
// translation key (via `codeToLangPath`).
export const SIUM_ERR: ModuleSeed = moduleSeed(SIUM_MODULE);
export const SIUM_ERR_VALIDATION: ErrCode = errCode(SIUM_ERR, 'validation');
export const SIUM_ERR_ASYNC_SCHEMA: ErrCode = errCode(SIUM_ERR, 'async_schema');

@ -8,7 +8,12 @@ export { serializeSchema, countLeafFields, walkSchema } from './introspect';
export type { SerializedSchema, SerializedShape } from './introspect'; export type { SerializedSchema, SerializedShape } from './introspect';
export { pipe, refine, transform, codec, meta } from './pipe'; export { pipe, refine, transform, codec, meta } from './pipe';
export { min, max, length, regex, email, url, integer } from './refines'; export { min, max, length, regex, email, url, integer } from './refines';
export { SiumValidationError, SiumAsyncSchemaError } from './types'; export {
SiumAsyncSchemaError,
SiumValidationError,
isSiumAsyncSchemaError,
isSiumValidationError
} from './types';
export type { StandardSchemaV1 } from './standard-schema'; export type { StandardSchemaV1 } from './standard-schema';
export type { export type {

@ -26,6 +26,8 @@
*/ */
import type { StandardSchemaV1 } from './standard-schema'; import type { StandardSchemaV1 } from './standard-schema';
import { CodeError } from '$libs/errs';
import { SIUM_ERR_ASYNC_SCHEMA, SIUM_ERR_VALIDATION } from '../consts.ts';
// ─── Kind / wrapper / effect vocabulary ───────────────────────────────────── // ─── Kind / wrapper / effect vocabulary ─────────────────────────────────────
@ -294,12 +296,13 @@ export interface Step<I, O> {
* `decode` only when you know the input is valid (parsed forms, trusted * `decode` only when you know the input is valid (parsed forms, trusted
* storage) and want a cleaner call site. * storage) and want a cleaner call site.
*/ */
export class SiumValidationError extends Error { export class SiumValidationError extends CodeError {
readonly issues: ReadonlyArray<Issue>; readonly issues: ReadonlyArray<Issue>;
constructor(issues: ReadonlyArray<Issue>, message?: string) { constructor(issues: ReadonlyArray<Issue>, message?: string) {
super(message ?? `Validation failed with ${issues.length} issue(s)`); super(SIUM_ERR_VALIDATION, {
this.name = 'SiumValidationError'; message: message ?? `Validation failed with ${issues.length} issue(s)`
});
this.issues = issues; this.issues = issues;
} }
} }
@ -311,16 +314,24 @@ export class SiumValidationError extends Error {
* Consumers (e.g. `createForm`) detect this structurally via `instanceof` to * Consumers (e.g. `createForm`) detect this structurally via `instanceof` to
* fall back to the async API instead of surfacing a confusing generic error. * fall back to the async API instead of surfacing a confusing generic error.
*/ */
export class SiumAsyncSchemaError extends Error { export class SiumAsyncSchemaError extends CodeError {
readonly schemaKind: SchemaKind | undefined; readonly schemaKind: SchemaKind | undefined;
constructor(schemaKind?: SchemaKind, message?: string) { constructor(schemaKind?: SchemaKind, message?: string) {
super( super(SIUM_ERR_ASYNC_SCHEMA, {
message ?? message:
message ??
`Schema contains async steps — use decode() / validate() instead of decodeSync / validateSync.` `Schema contains async steps — use decode() / validate() instead of decodeSync / validateSync.`
); });
this.name = 'SiumAsyncSchemaError';
this.schemaKind = schemaKind; this.schemaKind = schemaKind;
} }
} }
export function isSiumValidationError(value: unknown): value is SiumValidationError {
return value instanceof SiumValidationError;
}
export function isSiumAsyncSchemaError(value: unknown): value is SiumAsyncSchemaError {
return value instanceof SiumAsyncSchemaError;
}

@ -41,8 +41,10 @@ export {
email, email,
url, url,
integer, integer,
SiumAsyncSchemaError,
SiumValidationError, SiumValidationError,
SiumAsyncSchemaError isSiumAsyncSchemaError,
isSiumValidationError
} from './core'; } from './core';
export type { export type {
SerializedSchema, SerializedSchema,

@ -0,0 +1,87 @@
import { describe, expect, it } from 'vitest';
import {
isSiumAsyncSchemaError,
isSiumValidationError,
SiumAsyncSchemaError,
SiumValidationError,
type Issue
} from '../index.ts';
import {
SIUM_ERR,
SIUM_ERR_ASYNC_SCHEMA,
SIUM_ERR_VALIDATION
} from '../consts.ts';
import { CodeError, isCodeError, matches } from '$libs/errs';
const ISSUE: Issue = {
path: ['name'],
message: 'required',
code: 'invalid_type'
} as Issue;
describe('SiumValidationError', () => {
it('extends CodeError with the validation code', () => {
const err = new SiumValidationError([ISSUE]);
expect(err).toBeInstanceOf(CodeError);
expect(err.code).toBe(SIUM_ERR_VALIDATION);
expect(err.name).toBe(SIUM_ERR_VALIDATION);
});
it('carries issues alongside the code', () => {
const err = new SiumValidationError([ISSUE]);
expect(err.issues).toEqual([ISSUE]);
});
it('is detected by isSiumValidationError and isCodeError', () => {
const err = new SiumValidationError([ISSUE]);
expect(isSiumValidationError(err)).toBe(true);
expect(isCodeError(err)).toBe(true);
});
it('is matched by the SIUM_ERR module seed', () => {
const err = new SiumValidationError([ISSUE]);
expect(matches(err, SIUM_ERR)).toBe(true);
});
});
describe('SiumAsyncSchemaError', () => {
it('extends CodeError with the async-schema code', () => {
const err = new SiumAsyncSchemaError('object');
expect(err).toBeInstanceOf(CodeError);
expect(err.code).toBe(SIUM_ERR_ASYNC_SCHEMA);
expect(err.name).toBe(SIUM_ERR_ASYNC_SCHEMA);
});
it('carries the schema kind', () => {
const err = new SiumAsyncSchemaError('union');
expect(err.schemaKind).toBe('union');
});
it('is detected by isSiumAsyncSchemaError and isCodeError', () => {
const err = new SiumAsyncSchemaError();
expect(isSiumAsyncSchemaError(err)).toBe(true);
expect(isCodeError(err)).toBe(true);
});
it('does not match the validation code', () => {
const err = new SiumAsyncSchemaError();
expect(matches(err, SIUM_ERR_VALIDATION)).toBe(false);
expect(matches(err, SIUM_ERR_ASYNC_SCHEMA)).toBe(true);
});
it('is matched by the SIUM_ERR module seed', () => {
const err = new SiumAsyncSchemaError();
expect(matches(err, SIUM_ERR)).toBe(true);
});
});
describe('CodeError fluent decorators on sium errors', () => {
it('preserves the subclass-specific fields after withMessage', () => {
const err = new SiumValidationError([ISSUE]);
const decorated = err.withMessage('User-facing failure');
expect(decorated.userMessage).toBe('User-facing failure');
// Decoration returns plain CodeError; subclass fields stay on the
// original instance only.
expect(matches(decorated, SIUM_ERR_VALIDATION)).toBe(true);
});
});

@ -1,4 +1,10 @@
import { type ErrCode, isEqualOrDescendantOf, moduleOf } from './code.ts'; import {
type ErrCode,
isEqualOrDescendantOf,
isModuleSeed,
moduleOf,
type ModuleSeed
} from './code.ts';
/** /**
* Metadata accumulated on an error as it travels through layers. Each * Metadata accumulated on an error as it travels through layers. Each
@ -162,20 +168,28 @@ export function isCodeError(value: unknown): value is CodeError {
} }
/** /**
* `true` when `value` is a `CodeError` whose `code` is `family` or a * `true` when `value` is a `CodeError` whose `code` matches `family`.
* descendant of `family`. Lets a single check cover entire families: * `family` can be:
*
* - A `ModuleSeed` (e.g. `BUSS_ERR = 'buss::'`) — matches any code
* declared by that module.
* - An `ErrCode` — matches the code itself or any hierarchical
* descendant within the same module.
* *
* ```ts * ```ts
* matches(err, BUSS_ERR_DISPOSED) // exact identity * matches(err, BUSS_ERR_DISPOSED) // exact code identity
* matches(err, BUSS_ERR) // any error from the buss module * matches(err, BUSS_ERR_LISTENER) // any descendant of buss::listener
* matches(err, BUSS_ERR_LISTENER) // any error under the listener sub-tree * matches(err, BUSS_ERR) // any error declared by buss (the seed)
* ``` * ```
* *
* Boundary check (alphanumeric-only segments + `.` separator) prevents * Boundary check on `ErrCode` (alphanumeric-only segments + `.`
* false positives like `'buss::listener_extra'` matching the family * separator) prevents false positives like `'buss::listener_extra'`
* `'buss::listener'`. * matching the family `'buss::listener'`.
*/ */
export function matches(value: unknown, family: ErrCode): boolean { export function matches(value: unknown, family: ErrCode | ModuleSeed): boolean {
if (!isCodeError(value)) return false; if (!isCodeError(value)) return false;
if (isModuleSeed(family)) {
return value.code.startsWith(family);
}
return isEqualOrDescendantOf(value.code, family); return isEqualOrDescendantOf(value.code, family);
} }

@ -18,17 +18,29 @@ import {
import { CodeFormatError } from './errors.ts'; import { CodeFormatError } from './errors.ts';
declare const __errCode: unique symbol; declare const __errCode: unique symbol;
declare const __moduleSeed: unique symbol;
/** /**
* Branded string that has been validated as `module::path.with.dots`. * Branded string that has been validated as `module::path.with.dots`.
* *
* Construction goes through `code(value)` (throws on invalid) or * Construction goes through `code(value)` (throws on invalid),
* `sub(parent, segment)` (composes from an existing `ErrCode`). The * `errCode(parent, segment)` (composes from a seed or another code),
* brand prevents accidentally passing an unvalidated string where an * or `sub(parent, segment)`. The brand prevents accidentally passing
* `ErrCode` is expected. * an unvalidated string where an `ErrCode` is expected.
*/ */
export type ErrCode = string & { readonly [__errCode]: true }; export type ErrCode = string & { readonly [__errCode]: true };
/**
* Module prefix string of the form `'<module>::'`. Produced once per
* module via `moduleSeed('module')` and used as the parent argument
* to `errCode(seed, segment)` so individual error codes don't repeat
* the module name in every declaration.
*
* `matches(err, seed)` is true for every code that starts with the
* seed, i.e. for every error declared by that module.
*/
export type ModuleSeed = string & { readonly [__moduleSeed]: true };
// ── Character predicates ────────────────────────────────────────────── // ── Character predicates ──────────────────────────────────────────────
const CODE_LOWER_A = 0x61; // 'a' const CODE_LOWER_A = 0x61; // 'a'
@ -228,6 +240,55 @@ export function codeToLangPath(c: ErrCode): string {
return c.replace(ERRS_SEP_MODULE, ERRS_SEP_PATH); return c.replace(ERRS_SEP_MODULE, ERRS_SEP_PATH);
} }
/**
* Build a `ModuleSeed` from a module name. The module name must be a
* valid segment (lowercase alphanumeric + internal underscores).
*
* @throws CodeFormatError when the module name is invalid.
*/
export function moduleSeed(module: string): ModuleSeed {
if (!isValidSegment(module)) {
throw new CodeFormatError(module, ERRS_VALIDATION_REASON_EMPTY_SEGMENT);
}
return `${module}${ERRS_SEP_MODULE}` as ModuleSeed;
}
/**
* `true` when `value` is a `ModuleSeed` (a string ending in `'::'`
* with a valid module portion). Use to discriminate seeds from
* regular `ErrCode`s.
*/
export function isModuleSeed(value: string): value is ModuleSeed {
if (!value.endsWith(ERRS_SEP_MODULE)) return false;
const module = value.slice(0, -ERRS_SEP_MODULE.length);
if (module.length === 0) return false;
return isValidSegment(module);
}
/**
* Compose an `ErrCode` from a parent (seed or existing code) and a
* segment. Picks the right separator: `seed + segment` for a seed,
* `parent.segment` for an existing code.
*
* ```ts
* const BUSS_ERR = moduleSeed('buss');
* const BUSS_ERR_DISPOSED = errCode(BUSS_ERR, 'disposed'); // 'buss::disposed'
* const BUSS_ERR_LISTENER = errCode(BUSS_ERR, 'listener'); // 'buss::listener'
* const BUSS_ERR_LISTENER_FAILED = errCode(BUSS_ERR_LISTENER, 'failed'); // 'buss::listener.failed'
* ```
*
* @throws CodeFormatError when `segment` is not a valid segment.
*/
export function errCode(parent: ModuleSeed | ErrCode, segment: string): ErrCode {
if (!isValidSegment(segment)) {
throw new CodeFormatError(segment, ERRS_VALIDATION_REASON_EMPTY_SEGMENT);
}
if (isModuleSeed(parent)) {
return `${parent}${segment}` as ErrCode;
}
return `${parent}${ERRS_SEP_PATH}${segment}` as ErrCode;
}
// ── Internal helpers ────────────────────────────────────────────────── // ── Internal helpers ──────────────────────────────────────────────────
function isValidSegment(segment: string): boolean { function isValidSegment(segment: string): boolean {

@ -36,17 +36,20 @@ export { CodeFormatError, isCodeFormatError } from './errors.ts';
export { export {
code, code,
codeToLangPath, codeToLangPath,
errCode,
isDescendantOf, isDescendantOf,
isEqualOrDescendantOf, isEqualOrDescendantOf,
isModuleSeed,
isValidCode, isValidCode,
leaf, leaf,
moduleOf, moduleOf,
moduleSeed,
parent, parent,
parts, parts,
sub, sub,
validateCode validateCode
} from './code.ts'; } from './code.ts';
export type { ErrCode } from './code.ts'; export type { ErrCode, ModuleSeed } from './code.ts';
export { CodeError, isCodeError, matches } from './code-error.ts'; export { CodeError, isCodeError, matches } from './code-error.ts';
export type { CodeErrorMeta } from './code-error.ts'; export type { CodeErrorMeta } from './code-error.ts';

@ -1,16 +1,18 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { import {
code,
CodeError, CodeError,
errCode,
isCodeError, isCodeError,
matches matches,
moduleSeed
} from '../index.ts'; } from '../index.ts';
const MOD_BASE = code('buss::base'); const BUSS_ERR = moduleSeed('buss');
const MOD_DISPOSED = code('buss::disposed'); const SESS_ERR = moduleSeed('sess');
const MOD_LISTENER = code('buss::listener'); const MOD_DISPOSED = errCode(BUSS_ERR, 'disposed');
const MOD_LISTENER_FAILED = code('buss::listener.failed'); const MOD_LISTENER = errCode(BUSS_ERR, 'listener');
const MOD_OTHER = code('sess::expired'); const MOD_LISTENER_FAILED = errCode(MOD_LISTENER, 'failed');
const MOD_OTHER = errCode(SESS_ERR, 'expired');
describe('CodeError — construction', () => { describe('CodeError — construction', () => {
it('uses the code as the error name', () => { it('uses the code as the error name', () => {
@ -178,7 +180,6 @@ describe('matches()', () => {
it('returns true for descendants of a family root', () => { it('returns true for descendants of a family root', () => {
const err = new CodeError(MOD_LISTENER_FAILED); const err = new CodeError(MOD_LISTENER_FAILED);
expect(matches(err, MOD_LISTENER)).toBe(true); expect(matches(err, MOD_LISTENER)).toBe(true);
expect(matches(err, MOD_BASE)).toBe(false);
}); });
it('returns true when the code IS the family', () => { it('returns true when the code IS the family', () => {
@ -203,3 +204,23 @@ describe('matches()', () => {
expect(matches(err, MOD_LISTENER_FAILED)).toBe(true); expect(matches(err, MOD_LISTENER_FAILED)).toBe(true);
}); });
}); });
describe('matches() with a ModuleSeed', () => {
it('returns true for any CodeError from the same module', () => {
const err1 = new CodeError(MOD_DISPOSED);
const err2 = new CodeError(MOD_LISTENER_FAILED);
expect(matches(err1, BUSS_ERR)).toBe(true);
expect(matches(err2, BUSS_ERR)).toBe(true);
});
it('returns false for a CodeError from a different module', () => {
const err = new CodeError(MOD_OTHER);
expect(matches(err, BUSS_ERR)).toBe(false);
});
it('returns false for non-CodeError values', () => {
expect(matches(new Error('x'), BUSS_ERR)).toBe(false);
expect(matches(null, BUSS_ERR)).toBe(false);
expect(matches(undefined, BUSS_ERR)).toBe(false);
});
});

@ -2,11 +2,14 @@ import { describe, expect, it } from 'vitest';
import { import {
code, code,
codeToLangPath, codeToLangPath,
errCode,
isDescendantOf, isDescendantOf,
isEqualOrDescendantOf, isEqualOrDescendantOf,
isModuleSeed,
isValidCode, isValidCode,
leaf, leaf,
moduleOf, moduleOf,
moduleSeed,
parent, parent,
parts, parts,
sub, sub,
@ -230,3 +233,67 @@ describe('codeToLangPath()', () => {
expect(codeToLangPath(c)).toBe('a.b.c'); expect(codeToLangPath(c)).toBe('a.b.c');
}); });
}); });
describe('moduleSeed()', () => {
it('produces a seed of the form "<module>::"', () => {
expect(moduleSeed('buss')).toBe('buss::');
expect(moduleSeed('sess')).toBe('sess::');
});
it('throws on invalid module names', () => {
expect(() => moduleSeed('')).toThrow();
expect(() => moduleSeed('Buss')).toThrow();
expect(() => moduleSeed('a-b')).toThrow();
expect(() => moduleSeed('_buss')).toThrow();
});
it('accepts internal underscores', () => {
expect(moduleSeed('libs_aapp')).toBe('libs_aapp::');
});
});
describe('isModuleSeed()', () => {
it('returns true for valid seeds', () => {
expect(isModuleSeed('buss::')).toBe(true);
expect(isModuleSeed(moduleSeed('sess'))).toBe(true);
});
it('returns false for full ErrCodes', () => {
expect(isModuleSeed('buss::disposed')).toBe(false);
expect(isModuleSeed(code('buss::disposed'))).toBe(false);
});
it('returns false for malformed strings', () => {
expect(isModuleSeed('buss')).toBe(false);
expect(isModuleSeed('::')).toBe(false);
expect(isModuleSeed('Buss::')).toBe(false);
});
});
describe('errCode()', () => {
it('appends segment to a seed with no separator', () => {
const seed = moduleSeed('buss');
expect(errCode(seed, 'disposed')).toBe('buss::disposed');
});
it('appends segment to an existing code with a dot', () => {
const seed = moduleSeed('buss');
const listener = errCode(seed, 'listener');
expect(errCode(listener, 'failed')).toBe('buss::listener.failed');
});
it('chains naturally for deeper hierarchies', () => {
const seed = moduleSeed('auth');
const user = errCode(seed, 'user');
const login = errCode(user, 'login');
expect(errCode(login, 'failed')).toBe('auth::user.login.failed');
});
it('throws on invalid segment', () => {
const seed = moduleSeed('buss');
expect(() => errCode(seed, '')).toThrow();
expect(() => errCode(seed, 'A')).toThrow();
expect(() => errCode(seed, 'a-b')).toThrow();
expect(() => errCode(seed, '_a')).toThrow();
});
});

Loading…
Cancel
Save

Powered by TurnKey Linux.